Hallmanac

hall9k-cli-reference

The full h9k/h9kd command surface and the domain semantics behind it — the board's state/phase/attention model, project homes, task and idea lifecycles, epics, Jira and GitHub backlog integration, branch templates, and auto-pr-review. Load this when a task needs CLI syntax or platform semantics beyond the four dev commands and the rules in AGENTS.md's own "Build / test / run" section.

Hallmanac 0 Actualitzat fa 1 mes
GitHub

Instal·la

npx skillscat add hallmanac/hall9k/hall9k-cli-reference

Instal·la des del registre de SkillsCat.

SKILL.md

The hall9k CLI reference

AGENTS.md keeps only dotnet build / dotnet test / dotnet run --project src/Hall9k.AppHost /
docker compose up -d and the rules a session must obey on every turn. Everything else about
operating the platform through h9k lives here, load it on demand rather than assuming a
dispatched session was pre-briefed on it.

h9k install                  # publish release binaries to ~/.hall9k/bin (no service registered)
h9k update                   # refresh an already-installed machine from the latest GitHub release, no repo/SDK needed
h9k uninstall [--purge-data] # take the platform off the machine; the database survives unless --purge-data (Decisions Log #83)
h9k daemon start|stop|status # the CLI-owned daemon lifecycle (Decisions Log #31)
h9k config show|set          # the daemon's durable operating settings: node ceiling (--max-concurrent-task-runs), the per-run session cap default (--session-cap-per-run), model-by-role, interactive-claim-stale-after-days, review-cycle caps, the review stage composition (--review-stage-composition, --accept-reduced-review to degrade it), a periodic token-spend budget (--spend-budget, --spend-period; backlog 59, Decisions Log #103, #111, #112, #120, #129)
h9k doctor [--yes]           # diagnose the database situation and what to do about it; --yes remediates non-interactively, for scripts and dispatched agents (Decisions Log #73, #74, #118)
h9k project add --name <n> --repo-url <url>   # register a project and create its home directory; offers to reactivate or rename an archived project whose name it collides with (--reactivate-archived, --rename-archived-to <NAME>) (Decisions Log #182); also runs h9k project join once the repository is reachable on disk, reporting plainly if it could not (§16 #190)
h9k project join <name> [--owner <fingerprint>]   # establish or confirm this node's identity in a project's ledger: generates this node's ed25519 key once under ~/.hall9k/keys/<node-id>, then writes its node file signed with it; the first join naming no --owner establishes this owner's root and that fingerprint becomes the owner id everywhere in Hall9k (h9k owner show <fingerprint>, h9k task assign --owner <fingerprint>), recorded on the Owner stream, and a later plain join keeps whatever root is already claimed; --owner <fingerprint> claims an existing root instead, unverified until the team half's vouch (not yet built); re-runnable to change the claim, retiring a self-created root (required in the project being joined, best-effort in every other reachable, non-archived project that holds it) when this node turns out to belong to another one (idea 202383dc, A2a, §16 #190)
h9k project init <name>      # create, repair or refresh a project's home; idempotent
h9k project list [--include-archived]   # every project with its tasks counted by attention bucket; archived ones are hidden unless asked for (Decisions Log #182)
h9k project show <name>      # one project: home, registration, settings, rollup, newest tasks
h9k project remove <name> [--reason <TEXT>] [--yes]   # archive a project: hides it from h9k project list, stops the dispatcher and every daemon sweep acting on it — reversible, nothing deleted, home directory untouched (Decisions Log #182)
h9k project remove <name> --purge [--reason <TEXT>] [--yes]   # archives (if not already) and schedules a permanent hard delete of the project's database footprint 24 hours out — the one exception to the platform's nothing-is-deleted doctrine. Refuses under the same task-state predicate a plain archive does; the confirmation names the scope in numbers (tasks, runs, ideas, epics) and the deadline before anything is scheduled. Visible while pending in h9k project list --include-archived and h9k project show; reactivating is refused until the purge is cancelled. A daemon sweep carries it out, on the next start if the deadline passed while the daemon was down (Decisions Log #185)
h9k project cancel-purge <name>   # ends a pending purge before it fires, leaving the project archived, never reactivated — the inverse of h9k project remove --purge (Decisions Log #185)
h9k project reactivate <name>   # undo h9k project remove: the project and its tasks are visible and live again — refused while a purge is pending, cancel it first (Decisions Log #182, #185)
h9k project rename <name> <NEW-NAME>   # change a project's name only; the id, home directory, repository, and every task/run/idea are untouched (Decisions Log #182)
h9k project set <name> --branch-template "{key}-{slug}"   # the team's branch convention; 'none' restores task/{shortid}-{slug} (Decisions Log #121)
h9k project set <name> --review-stage-composition <VALUE|default>   # which pre-PR review stages a run gets: full-pipeline (default), adversarial-only, conformance-only, skip-final-pass, none — also settable at node (h9k config set) and task (h9k task add/revise), task > project > node > default, frozen at each run's own dispatch; a value that removes a guarantee needs --accept-reduced-review (Decisions Log #129)
h9k project set <name> --auto-pr-review off|normal|first|now   # a GitHub reviewer assignment to this install's own login auto-starts a pr-review task. Default NORMAL for every project, new and existing; 'off' is an explicit opt-out. No request older than the project's own cutoff ever starts on its own (no backfill), every request is recorded and shown either way, and the state is printed at daemon start, in h9k status and in h9k project show (Decisions Log #34's amendment, #133, #161)
h9k project set <name> --max-parallel-tasks <N|default>   # this project's own ceiling in TASK RUNS, enforced by the dispatcher: a ceiling never a reservation, 0 pauses the project (held even on an idle node, and nothing but a human raises it), 'default' clears it so the node ceiling alone decides; takes effect next dispatch cycle, no restart. --max-parallel is a quiet alias; the old session-denominated value it used to record is retired, not converted (Decisions Log #140)
h9k project set <name> --priority high|normal|low|default   # which tier this project's ready work competes in for a FREE dispatch slot. Default normal, and free slots rotate: the eligible project longest unserved wins the next one; which of its own tasks takes it is decided by rank then oldest assignment, a follow-up lap past its first pull request outranking a retry or hand-back outranking a plain first claim (nothing to set — Decisions Log #188). 'high' is focus — wins every free slot over lower tiers while it has ready work and RELEASES ITSELF when its queue drains, which is the opposite of the sticky --max-parallel-tasks 0 pause. 'default' is the clearing word, restoring normal. Nothing preempts; every claim logs why that project won (Decisions Log #141)
h9k project set <name> --claim-gate off|tracker-assignee   # a task linked to a Jira card or GitHub issue is claimed on this install only while the tracker shows that item assigned to this install's own identity; default off (Decisions Log #142)
h9k project set <name> --close-linked-issue on-closeout|never|when-all-tasks-close|default   # whether true closeout closes a task's linked GitHub issue, and when; default when-all-tasks-close waits for every task linked to the same issue to close out or be abandoned, decided fresh at the last one — a task overrides it at publish/revise, --never-close-labels forces never for a labeled issue, Jira untouched (Decisions Log #154)
h9k project set <name> --writing-conventions "<TEXT>"   # how prose an agent composes for people has to read here, pasted verbatim into every prompt that asks a session to write something posted under the owner's login (PR title and body, a review-feedback lap's summary comment and thread replies, the note a review lap drafts for h9k pr approve / request-changes, an attended session's commit messages). The two mechanically checkable rules are re-checked immediately before the platform posts: an em dash is rewritten by context, an attribution line is dropped, an attribution welded into a sentence is not posted at all. A run never fails over a convention miss. 'default' restores the platform text (no em dashes, full sentences, no AI attribution)
h9k owner show [<owner>]     # one owner: identity (root fingerprint once established, else this install's local Guid), the projects registered to them, and every standing preference their work runs by. Omit the argument when this platform has one owner
h9k owner set [<owner>] --rerequest-review on|off|default   # whether closeout asks a pull request's reviewers for another pass once a fix follow-up pushed (Decisions Log #62). A project setting outranks this; the node default (DaemonOptions.DefaultReviewRerequest, off) sits under both
h9k owner set [<owner>] --voice-skill <NAME> | --clear-voice-skill   # the skill this owner WRITES IN, by name. Every prompt seam where a session composes text a human reads as the owner's (a pull request description, a review-thread reply, a commit message, a posted review finding, a drafted reply to a GitHub mention) then tells that session to load the skill and its matching context first: contexts/code-review.md for prose the session posts, contexts/explainer.md for a draft the owner reads and decides on. The skill is the owner's own — referenced by name, never copied into a project, a prompt template, or the platform — so the name must already be a skill directory in the owner's user skills (~/.claude/skills/<NAME>) or in a project home's skills/; a name in neither is refused naming both paths. Structure authority does not move: the repository's own PR-description rule and the project's --writing-conventions still decide the shape, the voice skill decides only the prose. --clear-voice-skill forgets it, and every seam then renders as it does for an owner who never named one
h9k task list --project <name> --state <state>   # browse live and done tasks, newest first (--all, --limit, --include-archived, --epic)
h9k status                   # the attention pane: state, phase, and attention on every row
h9k idea add "<text>"        # capture an idea; discovery starts, a project is optional
h9k epic add --project <name> --title "<name>"    # name a first-class grouping of tasks (Decisions Log #100)
h9k connection list          # every external account this install can reach, and where its credential lives

The board answers four questions with three surfaces (Decisions Log #66). State is the
lifecycle in eight words: Draft, Published, Working, Delivered, Waiting, Done, Failed, Archived — Waiting being a pr-review task whose posted review is waiting on its author (Decisions Log #160).
Delivered is pushed-with-the-merge-not-yet-observed, and Done renders only at true
closeout, which is the same bar the dependency rule uses. Phase is the line under a live row,
composed from the run's records plus an observation of the recorded process - it never claims a
session is doing something without seeing the process, and says "liveness not observed here" when
it cannot. Attention is needs-you or not, with the one-line cause and the command that clears
it. The run vocabulary (Running, UnderReview, AwaitingReview, ChecksFailing, …) is the phase
line's material and never appears in the Status column; --state still selects on it (as
run-failed for the one word the lifecycle vocabulary already owns).

Every project owns a home directory (Decisions Log #76), ~/.hall9k/projects/<name> unless
the project says otherwise, in the same shape on every machine:

<home>/
├── AGENTS.md   generated from the project's facts; a render, never hand-maintained
├── repo/       <name>.git (bare clone) · dev/ (a worktree on the primary branch) · wt-*/
├── ideas/
├── tasks/      _archive/ holds terminal tasks (closed out or abandoned); moved back if reopened
├── skills/     plain markdown skill docs, seeded from the install's canonical set
└── .claude/    generated Claude Code plumbing: skills/ symlinked into skills/, never copied

Creating it is a recipe, not an agent: h9k project add and h9k project init do the
directories, the bare clone with its fetch refspec corrected, the dev/ worktree, the skill
seeding and the render, all in platform code and all idempotent. Re-running it is also how a
home catches up: repo/dev is fetched and fast-forwarded, and the step says whether it moved,
was already current, or was left alone because local changes blocked it. h9k install publishes
the canonical skill set to ~/.hall9k/skills, and a project's skills/ links into it, so updating
the platform updates every project's platform skills in one move. The location is a setting;
the shape is the contract
, which is what lets the dispatcher hand a session paths instead of
sending it hunting.

Every task owns a directory under tasks/ named <shortid>-<slug> from its objective, holding
task.md (the readiness contract — frontmatter plus the agent context as prose, the same format
h9k task add/revise --file reads) and a workspace/ for whatever refinement accumulates beside
it; an idea with a project renders the same shape under ideas/ (backlog 48). Drafts render
exactly like published tasks — the browsing surface is where the thinking lives. The render is
one-way and daemon-driven
, a sweep over the store's current state rather than a per-event
handler (the same shape DispatchLoop and CardPublicationLoop already use): the daemon rewrites
a file only when its rendered content actually changed, and the first sweep after start is the
same reconciliation pass that backfills a home created after tasks existed. A human edits the file
and applies it back through the existing gate — h9k task revise <id> --file <path>, or
h9k idea revise <id> "<text>" (ideas have no --file form) — the store decides what happened,
never the file. An idea with no project yet has nowhere to render into, so it stays in its global
discovery workspace until assigned. Where that workspace lives is decided once, at capture, and
never changes after (backlog 49): an idea captured with a project whose home already exists gets
its workspace under that home from the start; an idea captured with no project, or with a project
that has no home yet, keeps its workspace at the global location permanently, even once it is
later assigned to a project — assignment never retroactively relocates an already-materialised
workspace. The hall9k project's own move into its default home landed as that cutover chore
(backlog 52): the project home at ~/.hall9k/projects/hall9k is canonical, and this repository is
worked from its repo/dev worktree.

The same bare repo also carries a hidden ledger (Decisions Log PLACEHOLDER-5b1f57cd, idea
202383dc), a set of
git refs under refs/hall9k/ that GitHub never shows and branch protection never sees (both match
refs/heads/*/refs/tags/* only): small files, one writer per path, history nobody merges into
anything. ILedger/GitLedger (Hall9k.Connectors.Ledger) read and write a file at a path in a
named ref by git plumbing alone — no worktree, no hand edits — fetching and pushing with an
explicit, single-ref refspec built from the caller's own ref name, gated by LedgerRefRegistry,
the one place every ref this platform ever touches under that namespace is named; an ordinary
git fetch origin never brings one down, and nothing uses a refs/hall9k/ ref this registry does
not already know about. A write is conditional on the
blob the caller last read there (Conflict comes back rather than clobbering someone else's), and
every commit is signed per invocation once a caller has a key. refs/hall9k/ledger/records and the
refs/hall9k/messages/ prefix are the first two registered refs; nothing reads or writes their
content yet — that is later work in the same idea (task records, the holder lock, node-to-node
messages) building on this same component rather than a second one.

Ideas come before tasks (Decisions Log #35, redesigned by backlog 31). An idea undergoes
discovery (what is this?); a draft task undergoes refinement (how does this become
executable?). A task is an idea with intent, and there is no single graduation ceremony: an idea
fans out into any number of draft tasks through the ordinary add door, and an explicit human act
concludes it.

h9k idea add "<text>"                             # capture: one command, one argument
h9k idea add "<text>" --project <name>            # --project is optional, at capture and after
h9k idea list                                     # what is still in discovery, newest first
h9k idea show <id>                                # note, project, workspace path, history, fan-out, outcome
h9k idea revise <id> "<text>"                     # rewrite the note; every version stays on the stream
h9k idea assign <id> --project <name>             # set or change where it belongs
h9k task add --from-idea <id> --objective "<…>"   # cut a draft task from it; repeatable, needs a project
h9k idea promote <id> [--project <name>]          # sugar: cuts one task (note's first sentence) and concludes
h9k idea conclude <id> --reason "<what came of it>"  # terminal: discovery produced something
h9k idea archive <id> --reason "<why>"            # terminal: discovery produced nothing; never deleted

Every idea owns a discovery workspace, where research notes, gathered files, and prototypes
accumulate: ~/.hall9k/ideas/<idea-id>/workspace for an idea whose capture-time project had no
home yet (or had none at all), or <home>/ideas/<shortid>-<slug>/workspace when it did (backlog
49). The stream records milestones only, never file contents, and every cut carries the current
workspace path forward as the draft's agent context. Cutting a task never concludes or archives
the idea — discovery may keep producing — so ending it is always the separate, explicit act above.

Task development and task dispatch are separate lifecycles (Decisions Log #34): h9k task add
creates a draft, and nothing dispatches until a human publishes and assigns it.

h9k task add --project <name> --objective "…"     # creates a Draft (identity, not readiness)
h9k task add --from-idea <id> --objective "…"     # cut a draft from an idea (backlog 31); repeatable, falls back to the idea's own project
h9k task add --project <name> --from-issue 42     # adopt a GitHub issue (number, owner/repo#42, or URL)
h9k task add --project <name> --from-jira PROJ-1  # adopt a Jira card (key or URL)
h9k task add --project <name> --from-pr 42        # adopt a pull request to review (always pr-review)
h9k task revise <id> --criteria "…" --blocked-by <id>   # Draft-only; each option replaces that part
h9k task add --project <name> --objective "…" --stacked-on <id>   # a STACKED edge (Decisions Log #144), not a plain blocked-by — see below
h9k task revise <id> --stacked-on <id>            # declare the stacked edge on a Draft; --clear-stacked-on drops it (leaving the blocked-by alone)
h9k task add --project <name> --objective "…" --stacked-on-pull-request 264   # the same edge on a pull request ANOTHER install owns (Decisions Log #153) — no blocked-by, no local task needed
h9k task revise <id> --stacked-on-pull-request 264   # declare the remote form on a Draft; mutually exclusive with --stacked-on, and --clear-stacked-on drops whichever form the task holds
h9k task revise <id> --queue-first                # the one revision Draft-only doesn't gate: marks the task-level queue-first fact (Decisions Log #127), settable in any live state; --clear-queue-first removes it
h9k task revise <id> --clear-interactive-mode     # the other revision Draft-only doesn't gate: clears the interactive-mode flag (below) directly, settable in any live state, for when neither h9k task handback nor a default h9k task release has an active interactive claim left to act on
h9k task revise <id> --review-stage-composition <VALUE|default>   # Draft-only, unlike the review caps below — a live change reaches only the task's next run (Decisions Log #129)
h9k task set-review-caps <id> --max-compliance-review-cycles <N>   # a task-level review-cycle-cap override, settable at any time — even while the run is live (Decisions Log #112)
h9k task publish <id> [--assign]                  # the readiness gate; --assign starts it too
h9k task publish <id> --no-existing-item          # required if a tracking backlog policy finds no linked item yet and has no publication already pending
h9k task publish <id> --untracked                 # the same gate's other exit: deliberately skip tracking for this task, attested on the stream
h9k task publish <id> --pre-approved              # the owner stops being a synchronous gate at the pull request: the daemon rebase-merges once every real gate (CI, review decision, requested reviewers, threads) reads satisfied (Decisions Log #135)
h9k task publish <id> --pre-approved after-human-review   # the same automatic merge, held until a human reviewer has actually been requested on the pull request AND every requested reviewer has approved the current head (Decisions Log #149)
h9k task assign <id> [<owner>] [--take]           # the dispatch trigger — Queued, or Blocked on dependencies; --take also takes the linked card/issue for this install when nobody holds it, in a project whose claim gate is on (Decisions Log #143)
h9k task set-session-cap <id> <cap>               # override how many agent sessions this task's run may hold at once; settable any time, even mid-run (Decisions Log #111)
h9k task set-pre-approved <id> on|off|after-human-review   # set standing pre-approval after publish, without the unassign/draft/revise/publish ceremony — settable on any live task whose pull request has not yet merged, Draft excepted (pre-approval is part of the readiness contract set at publish). after-human-review waits for a requested human reviewer to approve the head; flipping it to on is the emergency path and merges on the next sweep. No reviewer is ever named here — reviewers are added in GitHub (Decisions Log #135, #149)
h9k task unassign <id>                            # back to Published (refused while leased)
h9k task draft <id>                               # Published back to Draft, so it can be revised

The edit-after-the-fact path is unassign → draft → revise → publish → assign, each step an
explicit act. A dependency counts as met only at true closeout (the pull request merged and the
closeout monitor observed it); TASK-MODEL.md §2.3 has the whole picture.

An epic is a first-class named grouping of tasks (Decisions Log #100): its own id, title, and
Open/Closed state, event-sourced like everything else. Membership is optional and no-ceremony —
it rides the task's own stream, so the flat task model is undisturbed for everything ungrouped —
and a task joins or leaves at h9k task add --epic or h9k task revise --epic/--clear-epic,
must belong to the same project as the epic, and only an Open epic accepts new members. An epic
closes only by explicit human act with a reason, never automatically, not even when its last
member task closes out; there is no h9k epic reopen yet.

h9k epic add --project <name> --title "<name>"    # name a new epic: a project and a title
h9k epic list [--project <name>] [--state <state>]   # every epic with a member-task rollup; open|closed|all
h9k epic show <id>                                # one epic: title, state, Jira link, every member task
h9k epic link-jira <id> <key-or-url>              # record the Jira item this one corresponds to, an epic or a card it tracks on a team's behalf; identity only
h9k epic close <id> --reason "<why>"              # the only way an epic ends

A stacked pull-request edge is one explicit opt-in dependency and nothing is inferred
(Decisions Log #144, Brian's cohesion ruling 2026-08-28). --stacked-on <parent> implies the
--blocked-by edge and adds six behaviours a plain blocked-by never gets: the child dispatches at
the parent's Delivered rather than its merge; its branch is cut from the parent's branch head;
its pull request opens against the parent's branch, forming a GitHub stack — unless that branch
is already gone from origin because the parent merged mid-build, in which case it opens against the
project's base branch, where the retarget below would have put it anyway, with the replay still
owed; its diff, review
packet, self-review hunt and end-of-work recompose are all computed against the parent's branch, so
its reviewers read the child's own delta; it is not at the merge bar while its pull request is
still aimed at the parent (the board never says "the merge is yours" and a pre-approved child is
not auto-merged); and when the parent's branch moves out from under it the daemon dispatches a
mechanical replay (one git rebase --onto between two exact commits — the boundary everything
at or before which is the parent's, and the freshly observed commit it lands on — then the gates,
and no review cycle at all, since nothing new entered the branch; the boundary is observed or
recorded, never derived from git merge-base, which a force-pushed parent makes wrong). A merged
parent additionally retargets the child's pull request onto the project's base branch, since the
parent's branch is going away; a force-pushed parent does not — the base stays the parent's
branch and only the replay is owed.
The replay is bounded by the child's own rebase budget (DaemonOptions.MaxStackReplayRuns,
default 12, separate from the lifetime closeout ceiling so a busy parent cannot spend the child's
review budget); past the cap the child parks with the reason. Reserve the edge for slices of one
feature that are genuinely cohesive — the interactive-mode pair is the canonical example — and
never reach for it just because one task happens to wait on another. A plain --blocked-by task
behaves exactly as it always has. Slice two, not built: the child still starts at the parent's
Delivered rather than at its build-complete-before-review.

The parent can be a pull request another install owns (Decisions Log #153):
--stacked-on-pull-request <number> on h9k task add or h9k task revise, taking the number as
GitHub shows it (264 or #264) on this project's own repository. It is the form a reviewer on her
own node needs — her machine never held the teammate's run, so that parent never reaches Delivered
here and no local edge could ever release her task. Everything above holds, with three differences.
The pull request being open is the parent's Delivered, and that is the whole bar. Its state,
head branch, merge and force-pushes are read from GitHub by one sweep on the closeout watcher's own
cadence, and everything downstream reads what that sweep recorded rather than calling out again —
so the board can sit a few minutes behind the browser, and every line h9k task show prints about
the parent is labelled as an observation, with when that reading was taken (an unchanged look
records nothing, so that is when the parent last moved, not when it was last looked at). That
sweep reads only children that are assigned: a child still Published is watched by nothing and
would not dispatch anyway, so h9k task assign is the step that starts the watch — claiming one
straight from Published is refused, and the refusal says so. And no --blocked-by travels with
it
: there
is no local task to name, so the hold is a second one beside the unmet-dependency set. The two
forms are alternatives — declaring either replaces whatever the task stood on, and
--clear-stacked-on drops whichever it holds. A pull request the repository does not have yet is
ordinary waiting (the teammate has not opened it); one that closed unmerged is the dead parent
and reads as needing a human; one already merged when the child first dispatches leaves nothing to
stack on, so the child is an ordinary task on the base branch. Nothing needs to exist locally for
the parent — no task, no mirror, no issue — though when the pull request itself names an issue it
closes, h9k task show prints it and names a local task carrying the same reference if there
happens to be one.

One Jira card that needs many pull requests does not distribute across sibling tasks the way
it looks like it should.
A task can adopt an external item at all only if no other task already
carries it, unless that holder has since been abandoned: Done still holds the reference, so a
finished-but-not-abandoned holder still blocks a second adoption. The one further exception is a
Done pr-review task: its completed review does not hold its pull request hostage the way adopted
work holds its issue, so h9k task add --from-pr re-adopts it for a second pass, exactly as
TaskDecider.Reopen sends the owner here to do. --from-issue/--from-jira refuse a second
adoption of a card another live task holds, and h9k task link-jira and h9k task link-issue
enforce the identical rule on the same field's other doors
(TaskAddCommand.RefuseSecondAdoptionAsync), one card, one owning task, so a card never ends up
with two sets of runs and two closeout comments. A card that genuinely needs several tasks working
toward it (ARX-5510: one Jira card, eight pull requests) runs straight into that refusal if every
task tries to adopt it, and the refusal is the guard working as designed, not a bug to route
around; the pattern below is what the arx team found by trial and error against it. The epic
carries the card (h9k epic link-jira <epic> <key-or-url>, whose own --help still calls the
argument "the Jira epic's key": it stores whatever key or URL it is given verbatim and unverified,
epic or card alike, so this pattern's use of it for a card is not a misuse), exactly one member
task formally adopts it (--from-jira at creation, or h9k task link-jira after), and every
sibling task carries the same card only in its agent context: free text naming the epic and the
card, never a second ExternalReference, so eight tasks can share one card's work without eight
of them fighting over who owns it. On a project whose backlog policy is jira or github-issues,
that means every sibling task must publish with h9k task publish <id> --untracked: a sibling
carries no ExternalReference of its own, so without --untracked the pre-publish gate (below)
either refuses it outright or, taken the other way out with --no-existing-item, lets the
post-publish auto-tracking step mint a brand-new card or issue for it: exactly the second-card,
second-closeout problem this pattern exists to avoid. A sibling published --untracked this way
never carries an ExternalReference at all, permanently, so on a project whose branch template
(below) uses {key} every sibling branch renders it no-key: for ARX-5510's eight tasks that is
one branch named for the owning task's card and seven named no-key-<slug>, distinguished only by
slug and by ResolveBranchNameAsync's own collision retry when two slugs coincide. Nothing about
that is broken, but it is worth knowing before dispatch rather than discovering it there.

An operator can work a Published, Queued, or already-Blocked task interactively instead of dispatching it headless
(Decisions Log #122). On a Published task assigned to nobody, h9k task work assigns it to the
operator's own owner and claims it interactively in one atomic event append: the task is never
observably Queued in between, so the dispatcher, woken within moments by the doorbell notification
a plain h9k task assign would have sent, can never win the race to it. An unmet dependency —
whether just discovered here on a Published task, or already sitting Blocked from an ordinary
h9k task assign or a claim handed back or retried — warns rather than refuses outright (Decisions
Log #128): the platform names every open blocker, and --acknowledge-unmet-dependencies is the
human's recorded override to claim it anyway, the same bar h9k task assign itself holds an
assignment to. Not needed twice: an acknowledgment this task already carries from an earlier claim
on the same still-open blockers is honored without asking again, and h9k task show names whether
a claim's own acknowledgment was given fresh or carried forward from an earlier one. h9k task assign and h9k task publish --assign are both unchanged and remain the headless dispatch
triggers; there is no --interactive flag on assign — edges still gate automatic dispatch
exactly as before, and only this deliberate human claim gets the warn-and-proceed path. Whichever
state it entered from, the claim itself is held by the human, not a process, so there is no lease
and no heartbeat reclaim; closing the terminal is a normal way to leave, and running
h9k task work again re-enters the same worktree (Decisions Log #103). This claim always turns on
the task's own interactive-mode flag (task: interactive mode becomes a recorded property of the
task, design ruling R2): from here on, this task's run — and any later follow-up, retry, or reopen
of it — parks at each of the review engine's own four routine phase boundaries (build done to
review, review verdict to fix, fix to re-review, gates to pull request) for a recorded
h9k review proceed or h9k review resolve, whether or not a human is still building it (see
ORCHESTRATOR-WINDOW.md). The review-verdict-to-fix boundary takes a fourth verb too, on either
side of the pull request (Decisions Log #148): h9k review fixed <task> [--no-change "<why>"]
records that the human did the fix by hand in the run's own worktree, so the review agents check
that fix the way they would check a fix session's and no headless fix agent runs. It pushes the
branch when a pull request is already open, then re-enters the loop at the same fix-to-re-review
boundary a completed fix session lands on; it refuses over an uncommitted worktree, naming the
files, and refuses an unmoved branch tip unless --no-change states why — and refuses
--no-change over a tip that did move, since the commits are that cycle's answer and it cannot
also be a deliberate no-change dismissal. It spends no automatic
fix budget and no cap a fix session consumes, and the review cycle it opens counts exactly as one
a fix session opens does. h9k task handback and a default h9k task release are the two
ordinary exit doors that turn it back off; h9k task revise <id> --clear-interactive-mode is the
one that still reaches it once neither of those has an active interactive claim left to act on (a
headless follow-up dispatched under a real node claim while the flag is still on, or the task has
already reached Done with its pull request open). The same flag turns outbound reporting on too
(design ruling R8, idea fcaded0b): a dispatched build, review, or fix session reports judiciously,
never a running commentary, to the human's own registered session (h9k task register-session)
through the cross-session mesh's SendMessage tool — a small, fixed per-role vocabulary of
milestones, ending with each role's own end-of-phase report (a build's handoff, a review's verdict
and findings, a fix session's summary) as its last act before it ends normally. Every send, landed
or not, is logged through h9k task log-interaction so what the human was told lives on the run
stream rather than only in a transcript; with no registered session it skips sending and logs that
once for the phase, and with a registered session SendMessage cannot reach it still attempts and
logs each send individually — neither degradation holds up the boundary park above, which parks
exactly the same either way. No registered session is the ordinary case for a fresh headless
dispatch under interactive mode (h9k task start, an ordinary dispatch carrying the flag forward
from an earlier h9k task release --keep-interactive, or a retry, reopen, or follow-up redispatch
— each starts a new run, and no registration carries forward from an earlier one yet), so today the
build role's own milestones always take this skip-and-log path; only the review and fix roles,
dispatched later on the same run once a human's own h9k task work claim has registered against
it, can actually reach a live address. A handback is never one of these no-registration cases
either way: it clears the interactive-mode flag unconditionally, so a handback-dispatched run never
reaches these rules at all.

Delegating the build while staying at the wheel is its own command, distinct from handback
(task 15f889e3, design ruling R6): h9k task delegate <id> --note "<text>" dispatches a headless
build contractor onto the operator's own live interactive claim — the same run, worktree, and
branch, reused exactly as they stand, whether or not any work has been committed there yet — while
the task stays the operator's, still in interactive mode. It shares h9k task start's own
ceiling-exempt mechanism (the claim's sentinel node id already exempts it) and its slice-1
<task-shortid>-build mesh name — the contractor's own prompt, stream, settings and stderr files
are instead keyed on a per-delegation discriminator, so a claim delegated more than once never has
one contractor's transcript truncated by the next — and sets ResumesPreviousWork whenever the
branch already carries a commit ahead of base, the worktree holds modified or untracked files, or
its git status could not be read at all, exactly as a resumed retry does for the commit case and
conservatively folds the other two into "assume this worktree already holds work." --note is
required and carries the
operator's own handoff into the contractor's starting prompt in the blocker-handoff mold (what was
attempted, what is deliberate versus abandoned, what latitude is granted); the prompt's own default
posture toward inherited work is conservative, and discard latitude exists only when the note
grants it explicitly, and the contractor's own end-of-work recompose (below) never rewrites a
commit that predates this delegation — only the checkpoints the contractor itself adds. Refused on
everything but that live claim — a headless claim, a different owner's claim, a claim whose
interactive-mode flag is off (h9k task handback --now turns it off deliberately; there is no
boundary park left for a contractor's report to arrive at), a pr-review sentinel claim, a run
already handed to the standard pipeline, or a worktree another session is still attached to all
refuse by name. The reverse move: once the contractor reports back, h9k task work <id> re-enters
the same worktree interactively so the operator can finish the build themselves — the wheel changes
hands in both directions, always at a boundary.

By default h9k task work claims and cuts as above, then prints the worktree path, the branch,
and a starting prompt (assembled through WorkPromptBuilder, the same code every path already
uses) for the operator to paste into a Claude Code session started anywhere — it no longer
launches or waits on the session itself (Decisions Log #126). The pasted session's own first act
is the new agent-facing observation gate h9k task register-session <id>, which records its
process identity (read from CLAUDE_PID, Claude Code's own environment variable) the way a direct
launch's own launch-time recording always did; the double-booking and liveness guards below key
off that record, and a session that never registers degrades honestly to a no-op rather than a
false block, exactly as a claim nobody ever recorded a session against always has. --direct-launch
keeps the prior behavior for one release — h9k task work itself launches a plain interactive
Claude Code process and waits on it, resuming the most recently recorded session's own
conversation rather than starting a fresh one (falling back to a fresh session, announced, only
when the recorded one cannot be resumed — Decisions Log #124) — and is the only path the Windows
script-shim refusal still gates, since a pasted prompt travels through no argv:

h9k task work <id>                   # claim a Published, Queued, or already-Blocked task, cut the same branch/worktree headless dispatch would, print the worktree/branch/starting prompt
h9k task work <id> --direct-launch   # the prior behavior for one release: launch and wait on the session here instead of printing a prompt
h9k task work <id> --acknowledge-unmet-dependencies   # claim a task anyway, despite named open blockers
h9k task register-session <id>       # the pasted session's own first act: register its process identity against the claim
h9k task verify <id>                 # run the project's gates on demand against the claim's worktree
h9k task deliver <id>                # push and hand the claim into the standard delivery pipeline
h9k task delegate <id> --note "<text>"   # dispatch a headless build contractor onto this same claim for one phase, staying in interactive mode
h9k task handback <id>               # release the claim to a headless agent partway through, resuming the branch
h9k task handback <id> --first   # same release, plus the queue-first marker: the next free slot takes it regardless of age (Decisions Log #127)
h9k task handback <id> --now     # same release, dispatched immediately, ceiling-exempt, through h9k task start's own mechanism — refused together with --first
h9k task release <id>                # give an untouched claim back to the dispatch queue; clears interactive mode by default (an exit door alongside handback)
h9k task release <id> --keep-interactive   # same release, but the task's interactive-mode flag survives it, so a later headless run still parks at each boundary

A deliberate human kick-off dispatches a Published, Queued, or already-Blocked task on the spot, headless, instead
of dispatching it interactively or waiting on the queue (Decisions Log #125, "Take the Wheel"
epic 9272e514's start-it-mine mode): h9k task start <id> reuses h9k task work's own claim
shape exactly (the same ceiling-exempt sentinel, so every existing lever above — deliver, verify,
handback, release, the stale-claim nudge, and re-entering via h9k task work itself — accepts a
start-it-mine claim on the identical terms an interactive one already gets) and the same atomic
Published entry, but launches the
agent headless (claude -p, Claude Code's own completion mode) and detached rather than attached
to this terminal, under the slice-1 <task-shortid>-build name, addressable on the session mesh
the moment it starts. Shares h9k task work's own warn-then-acknowledge shape for an unmet
dependency, on a Published task and on an already-Blocked one alike (Decisions Log #128, closing
the gap #125 deliberately left open — that gap was about re-entering a live claim, which
h9k task start still never does, not about withholding a carried-forward acknowledgment from a
fresh claim on a Blocked task): the platform names every open blocker and advises, and
--acknowledge-unmet-dependencies is the human's recorded override to start anyway (the epic's own
ruling: "the platform advises, the human overrides, and the acknowledgment is recorded") —
recorded on the resulting TaskClaimed and surfaced on h9k task show beside the blockers it
overrode. Not needed twice: an acknowledgment this task already carries from an earlier claim on
the same still-open blockers is honored without asking again, whichever of h9k task start or
h9k task work gave it. h9k task start refuses Draft (publish it first), a pr-review task, a
reopened task's follow-up branch, and any task that already carries a live claim — there is no
re-entry branch the way h9k task work has one; a fresh claim is all this command ever makes, and
a fresh claim on an already-Blocked task is exactly what its own Blocked entry is, not a re-entry.
Giving such a claim back before it finishes — h9k task handback, h9k task release,
h9k task retry, or h9k pr resolve's own reopen — lands the task on Blocked rather than Queued
whenever the acknowledged dependency is still open, since claiming never clears it, only assigning
does; each of those commands names the still-open blocker(s) rather than claiming a run that will
not in fact dispatch, and the acknowledgment itself stays on record for whichever command reclaims
the task next. h9k task deliver recovers a start-it-mine session's own handoff and token
usage from its stream.jsonl at delivery time — the only point anything on this node reads that
file back, since a run dispatched under the sentinel node id above is never adopted to do it any
other way. A direct h9k task start invocation — unlike h9k task handback --now's own call into
this same mechanism, which passes false — always turns on the task's own interactive-mode flag too
(task: interactive mode becomes a recorded property of the task, design ruling R2), the identical
fact h9k task work's claim above always sets: the build here is headless either way, but from
this claim on, the run still parks at each of the review engine's own four routine phase boundaries
for a recorded h9k review proceed or h9k review resolve, so a task started this way is no
longer the fire-and-forget dispatch it was before this flag existed. The same two ordinary exit
doors, and the same h9k task revise --clear-interactive-mode fallback, turn it back off.

h9k task start <id>                              # dispatch a Published, Queued, or already-Blocked task headless, on the spot, ceiling-exempt
h9k task start <id> --acknowledge-unmet-dependencies   # start a task anyway, despite named open blockers

--from-issue and --from-jira adopt existing external work (PLAN.md §3.1a, Decisions Log #60,
#65): the item's title seeds the objective, its description becomes agent context, and the item is
recorded as the task's ExternalReference. Acceptance criteria are never read out of a description;
supply them with --criteria or at the prompt. Import is a one-time snapshot, so the state read at
import is recorded as an observation of that moment and never re-checked. Every source goes through
IWorkItemProvider in Hall9k.Connectors, so a new one is a resolver rather than a new command.

Jira is connected as a read credential plus a compose/execute write path (Decisions Log #65,
#102), because reading Jira is configuration-agnostic and writing it is not, but writing it also
has to be deterministic and auditable rather than left entirely to an agent's own judgment:

h9k connection add jira --site https://your-org.atlassian.net --email you@example.com
h9k connection list                               # provider, account, site, credential reference
h9k project set <project> --jira PROJ             # bind the board; 'none' clears it
h9k task push-to-jira <task>                      # dispatch an agent run that composes the card
h9k task write-jira <task> --op create --file <payload.json>  # hall9k executes a composed write
h9k task link-jira <task> PROJ-123                # record a pre-existing card, verified against Jira first

The platform never authors a card's content: issue types, required fields and routing rules are
the organisation's configuration, so push-to-jira dispatches a session into the project's own
repository, where its card-authoring skills live, to work out what the card should look like — but
that session makes no Jira call itself. It composes a payload and submits it through
write-jira, which is the sole executor of every Jira write (Decisions Log #102, #114): hall9k
validates the payload (a transition or a close is refused regardless of who composed it — that is
a team's workflow, done in Jira directly, never a write hall9k performs), records the intent with
the full payload before anything is sent, executes it against the Jira Cloud v2 REST API — the
same authenticated client the read side already uses — verifies the outcome by reading the item
back rather than trusting Jira's own success response alone, and records the outcome including the
returned key. A retried or replayed create narrows the window for a duplicate rather than closing
it outright: write-jira searches for a marker an earlier attempt's card would carry before
creating anything new, but Jira's search index updates asynchronously, so a retry inside that
index-lag window can still find nothing even though the card genuinely exists. Agent-facing
commands are observation gates
: write-jira and link-jira both read the key back through Jira
before recording anything, so an agent's or an operator's claim is an argument that gets checked
rather than a fact that gets accepted. Registered credentials are recorded as references (env:,
keychain:, file:) and never as secrets — the one registered connection's API token now covers
both directions, reading and writing alike, and it expires only the way any API token does (an
organisation revoking or rotating it), which is rarer and less routine than the machine-wide
browser login (twg login) the write path used before #114: a rejected credential is still a
handled state, not a crash. A write that hits one is recorded pending on the task, surfaces as a
needs-you row telling the operator to refresh the connection (h9k connection add jira), and the
daemon retries the identical write automatically once that succeeds — covering both an operator's
own write-jira and a daemon-dispatched write such as closeout's own merge comment. When a task
carrying a Jira reference merges, closeout comments the pull request on the card through this same
write surface; it never transitions the card, because which status a merge means is a team's
workflow rather than a fact about software. h9k doctor probes the registered connection's
credential whenever a project's backlog policy is jira, distinguishing no connection registered
from a rejected credential and teaching h9k connection add jira as the fix for the latter — the
Atlassian CLI (twg) this write path used before #114 is no longer required for any h9k or
h9kd operation.

Every published task is tracked automatically, per a project setting (backlog: track every
published task), and GitHub gets a write path of its own — unlike Jira, an issue's shape (title,
body, labels) is uniform enough for the platform to author deterministically, with no agent
needed:

h9k project set <project> --backlog none|github-issues|jira   # default none — today's behavior
h9k project set <project> --backlog-routing "<TEXT>"          # free text: verbatim to the jira agent; a label list for github-issues
h9k task link-issue <task> 123                                # record a GitHub issue, verified against gh first

h9k task publish checks the policy twice. First, before publishing: a tracking policy (jira or
github-issues) on a task that carries no linked item yet and has no publication already pending
refuses the publish outright, naming the three ways forward — link an existing item
(h9k task link-jira / h9k task link-issue), attest none exists and proceed with
h9k task publish <id> --no-existing-item, or attest that this task should skip tracking
altogether with h9k task publish <id> --untracked, for internal chores and platform tasks that
should not pollute a team's tracker — so a human or orchestrator confirms no existing item covers
the objective, or deliberately opts out of tracking, before a duplicate can be minted.
--no-existing-item and --untracked say opposite things and are refused together as
contradictory; --untracked on a project whose backlog policy is none, or any policy this build
doesn't recognize, is refused as meaningless — there is no tracking there to skip; and
--untracked on a task that already has a publication request outstanding
(h9k task push-to-jira, run by hand while still a Draft) is refused too, since that session
mints its card regardless of the flag — link it or wait for it instead. Both attestations land on the same
TaskPublished event, recording who chose which and when; h9k task show renders an
untracked-by-choice task honestly, distinct from one that predates the policy or was published
under policy none (both leave the attestation unset rather than defaulting to a look-alike
state). Then, after publishing: jira appends the
same request push-to-jira does (so a project with no Jira connection registered yet is told once,
at publish, rather than refused — push-to-jira remains the manual retry once one exists);
github-issues runs gh issue create itself, reads the created issue straight back the same way
--from-issue does, and records it through link-issue — the platform's own creation claim gets
the identical observation gate an agent's does. A task adopted with --from-issue or --from-jira
already carries its reference, so the pre-publish gate never fires and publishing it creates
nothing a second time. Closeout comments a merged pull request onto a linked GitHub issue exactly
as it does a linked Jira card — the Jira card is never transitioned, and a GitHub issue is closed
only under the configurable rule below; without one, closing is exactly as it always was: never.

A task's linked GitHub issue can close at true closeout, under a configurable rule (task: a
task's linked GitHub issue is closed at true closeout under a configurable rule, Decisions Log
#154). h9k project set <project> --close-linked-issue on-closeout|never|when-all-tasks-close
(default when-all-tasks-close) decides whether the merge note closeout posts also closes the
issue: on-closeout closes it in the same step as the note, every time; never posts the note
only; when-all-tasks-close posts the note every time and closes the issue only once every task
linked to it has itself reached true closeout or been abandoned, decided fresh at the last one —
the default needs no configuration for the common one-task-one-issue case, since it closes
immediately when there is nothing to wait for. h9k task publish --close-linked-issue / h9k task revise --close-linked-issue overrides it per task (default clears the override, deferring
to the project's setting live); h9k project set <project> --never-close-labels epic,prd,adr
forces never for an issue carrying any of those labels regardless of the project default. When
several tasks link one issue, the decision scans every linked task's own recorded rule: an
explicit never anywhere keeps it open, otherwise an explicit close-flavoured override anywhere
closes it, otherwise the label list and then the project default apply — one override on one
sibling is enough to decide the whole issue, and recency plays no part. when-all-tasks-close
runs that scan only once every other linked task has itself reached true closeout or been
abandoned; on-closeout has no sibling to wait for and runs it immediately, so a sibling's
explicit never still keeps the issue open. Abandoning
a task never closes its issue and posts no note. h9k task show renders the effective rule and
whether it is inherited or set explicitly.

A project can make the tracker's own assignment the one act that hands out work (Decisions
Log #142, idea 64c75e43). h9k project set <project> --claim-gate off|tracker-assignee — default
off, today's behaviour byte-for-byte — claims a task linked to a Jira card or a GitHub issue on
this install only while the tracker shows that item assigned to this install's own tracker
identity, so on a team where every teammate runs their own install against their own database,
two installs cannot both run the same card. The identity is read from the tracker, never typed:
the Jira accountId /rest/api/2/myself answers (captured at h9k connection add jira, read
live and recorded on first use for a connection registered earlier), or the login gh is
authenticated as, read live on every check and never stored. Each check reads the assignee field
alone, so a task's one-time content snapshot is untouched, and records TrackerAssignmentObserved
when it passes. Every claim door re-checks: the dispatcher leaves a refused task Queued and logs
once per episode, h9k task work and h9k task start refuse with the same wording and exit 70,
and h9k task assign warns on stderr naming the holder and the link but assigns anyway — the
tracker is the go signal and the queue is where the task waits. A queued task's line on
h9k status, h9k task show and h9k project show names the item and its holder. Untouched: a
task with no linked item, an untracked one, and a pr-review task, whose pull request's own
assignment is already auto-pr-review's signal. The gate itself never writes to the tracker and has
no override flag; a tracker that cannot be read holds the claim rather than releasing it, quotes
the tracker's error, tells a credential refusal apart from an outage, and is re-read no more often
than every three minutes. The one write this feature makes lives outside the gate, in the command
below.

One command can move the tracker and the board together (Decisions Log #143).
h9k task assign <id> [owner] --take is the one write this feature makes, and it lives outside the
gate rather than in it (TrackerAssignmentTake, which calls TrackerClaimGate for its read — the
gate every door and every sweep calls stays read-only, because the dispatcher calls it on a cadence
with nobody watching). In a gated project it reads the linked item fresh and, when the tracker
shows nobody holds it, writes this install's own tracker identity into the assignee field (Jira
as an assignee-only update through JiraWriteExecutor, GitHub as
gh issue edit --add-assignee with the login read live), reads the
item back, records TrackerAssignmentWritten from that read-back, and assigns — so the gate then
passes on its own instead of the task sitting in the queue waiting for a second act. It only ever
moves an item from unassigned to this install: one somebody else holds is refused with exit 70
naming the holder, nothing is written, the task is left exactly as it was, and there is no flag
that takes an item from another person
. Already yours records the observation and proceeds; an
unreadable tracker refuses rather than writing blind. A read-back that comes back naming somebody
else beside this install is refused too, and it is the one outcome only GitHub can produce
(--add-assignee adds where Jira's PUT replaces): two installs took the same issue in the same
moment, so neither may claim it, and the refusal names who else is on it and says to settle it with
them rather than to run the command again. The write is a field update, never a
transition
— the item's status, labels and milestone are untouched — but a team's own board
automation may react to an assignment, which is why the flag is explicit. Without it, an
interactive assign offers the same take on an unassigned item (defaulting to no) and a
non-interactive one warns and proceeds without writing anything. --take where there is no gate
to satisfy (gate off, or no linked card or issue) is refused rather than quietly honoured.
Releasing a task leaves the tracker assignment where it is.

A team's branch convention is a project setting, not a fork of the platform (Decisions Log
#121). h9k project set <project> --branch-template "<TEXT>" names a task's branch out of three
tokens — {shortid} (the task's short id), {slug} (its objective, hyphenated, capped at 30
characters) and {key} (the linked Jira key or GitHub issue number) — with everything else
literal, so --branch-template "{key}-{slug}" cuts ARX-14-add-rate-limiting. The default is
task/{shortid}-{slug}, exactly what the platform cut before the setting existed, so a project
that sets nothing sees no change at all; none restores it. Two rules make it safe rather than
merely convenient. The template is rendered and checked as a legal git ref at project set time,
so a name git would refuse is refused where a human can still fix it. And every token is fixed at
or before dispatch, because the rendered name is recorded on the run and pushed verbatim when the
pull request opens much later: the id cannot change, the objective cannot be revised once the task
leaves Draft, and an external item cannot be relinked to a different one. A branch name that could
drift between those two moments is the same failure a hand-renamed branch caused on the Windows
node on 2026-08-31, where the push hit a refspec that no longer existed and the task parked Failed.
A task carrying no linked item renders {key} as no-key — nothing was observed, said out loud,
rather than an empty segment or an invented card number.

A project's house writing style is a setting the platform actually delivers, not a rule that
lives only where the composing session cannot read it.
h9k project set <project> --writing-conventions "<TEXT>" records it and h9k project show prints it; the text is pasted
verbatim into every prompt that asks an agent to compose something posted to GitHub under the
owner's login, and the two rules a machine can also check (an em dash, an AI attribution string)
are re-checked immediately before the platform posts anything it composed. A hit with a mechanical
fix is rewritten (an em dash becomes a comma, a semicolon or a colon by context; an attribution
alone on its line is dropped); one without is not posted at all, and the run carries on rather than
failing. default restores the platform text. Origin incident (2026-09-09): a review-feedback
follow-up on AgelessRx/arx-platform#2042 posted its summary comment under the owner's login with
em dashes in most of its paragraphs, because the rule lived only in the orchestrator recipes and in
the operator's own CLAUDE.md, which --setting-sources project drops from a dispatched session.

A pull request GitHub assigns to this install's own login is a go signal in its own right, on
every project unless it has opted out
(idea e5e98a33, Decisions Log #34's own amendment, #133,
amended by #161): h9k project set <project> --auto-pr-review off|normal|first|now chooses the
speed, and normal is the default for every project, new and existing — the daemon polls GitHub,
on the closeout monitor's own interval-with-backoff shape, for open
pull requests in each project's repo requesting this install's own login — read back from gh
fresh every sweep, never a cached name — and mints, publishes, and starts a pr-review task exactly
as h9k task add --from-pr would, recording the GitHub assignment as provenance rather than a
human typing the command.

off is an explicit opt-out a human types, honoured for as long as it stands, and three
properties bound the default (all of #161, whose origin incident was this feature sitting
installed and silent on both nodes for three days):

  • Its state is always printed, at the default as much as at an explicit setting: one Info line
    per project in h9kd's own start-up log, one line per project in h9k status, and the
    Auto pr-review row in h9k project show — each naming the effective value and whether it is the
    project's own recorded choice or the platform default. Reading ProjectDetails.AutoPrReview
    directly does not answer that question (an inline projection stores a field's initialised
    default under its own key); AutoPrReviewSetting resolves it from the project's own stream.
  • No backfill. A review request whose GitHub requested-at time predates the project's
    registration — or, for a project older than this behaviour, this install's own first run of it,
    recorded once and never recomputed — never mints or starts anything on its own, whatever the
    setting says. Nor does one whose requested-at could not be read at all.
  • Every request is recorded once per pull request with its outcome, whatever the setting, and
    produces one Info line per pull request naming the pull request, the project, the setting and
    the outcome. h9k status renders it as a needs-you row wherever nothing started, naming the
    lever that ends that particular wait: both commands (h9k task add --from-pr <n>, or turning
    the setting on) where an explicit off held it, and h9k task add --from-pr <n> alone where
    the no-backfill cutoff did. Where a task is running it renders an informational row naming the
    task and following it — never needs-you there, since the daemon is already on it. The row
    clears when the request is withdrawn, the pull
    request closes, or a task adopts it; turning the setting on clears an off row, and
    deliberately does not clear one the no-backfill cutoff (or an unreadable requested-at) is
    holding — the cutoff outranks the setting, so h9k task add --from-pr is the only lever that
    ends those. Every row names the login GitHub made the request of rather than assuming it is the
    reader's, since a row is kept per reviewer login and two installs can share one database. A row
    is also kept per observing project and node, because the setting, the registration and the
    install's own adoption moment are what graded it: a repository two projects both point at gets
    one row each, and identical rows print once, so two of them appear only where the two projects
    genuinely disagree about the same request. A row
    whose covering task has closed says what actually holds a fresh mint back: nothing on an off
    project, this same standing request where auto pr-review minted that task itself, and nothing at
    all where a human adopted it by hand — the re-mint guard only ever matches a task the feature
    created, so the next sweep is free to mint one of its own there.

The three non-off speeds are the general dispatch levers, not new scheduling
code: normal joins the ordinary queue, first also sets the queue-first marker (#127), and now
claims it immediately, ceiling-exempt, through the same sentinel-node-id mechanism h9k task start
uses (#103, #125) — so a human re-speeds any auto-created task afterward with the identical general
levers. now is capped at one immediate, ceiling-exempt launch per sweep across every
project: the consent a human gives at --auto-pr-review now promises one extra concurrent agent
session, not an unbounded burst of them if several pull requests are newly assigned in the same
poll interval, so a candidate beyond that one launch is not dropped — it is minted, published and
assigned exactly as a first-speed task is, so it still takes the next free ordinary dispatch slot
rather than waiting a full poll interval for nothing to happen. One live task per pull request, the
same one-item-one-live-task rule adoption already
enforces (PLAN.md §3.1a): a re-request after an earlier auto-created review closed Done mints a
fresh task, noted as a re-review. An assignment withdrawn before the run dispatches concludes the
task honestly (abandoned, the go signal recalled by the same authority that gave it); withdrawn
after the run is Claimed or parked, it is recorded as an observation only — findings already
produced are never discarded for a reviewer reshuffle. The pr-review run itself (#99) is entirely
untouched: this changes only when a review starts, never what it does once it has.

A reviewer runs their own review lap on top of that same pr-review task (Decisions Log #149,
idea 21ddf2b3, walked 2026-09-06). h9k pr review is the reviewer's counterpart to
h9k task work: it prepares the lap and hands over a briefing, and it deliberately builds on the
pr-review task rather than beside it.

h9k pr review 42                       # attach to the pr-review task this node holds for #42, or adopt the PR when none exists
h9k pr review https://github.com/o/r/pull/42 --project <name>   # a URL, and --project when several are registered
h9k pr review 42 --no-worktree         # skip the read-only checkout: reviewing against a deployed environment
h9k pr approve <task> --note "…"       # END the lap: posts APPROVE on the PR's current head under YOUR login
h9k pr request-changes <task> --note "…" --finding "src/Foo.cs:42: <what is wrong>"   # END the lap with line comments; repeat --finding

What it attaches to, and when it adopts: the live pr-review task for that pull request, found
through the identical project-blind one-live-task-per-item dedup h9k task add --from-pr and
auto-pr-review already share (a Done pr-review does not hold its pull request hostage, so a lap
after one closed gets a fresh task). Attaching to a task whose automated run is already
ReviewParked rides on that run and reuses its worktree, so the reviewer's verdict resolves the
same park the machines produced; a task that never dispatched is claimed interactively and gets a
lap-owned run of its own. A run still mid-dispatch or mid-conformance-lens is refused by name — a
live agent is reading that worktree.

The connector is the prompt handoff (#126), never a launched process: h9k pr review prints a
briefing to paste into a Claude Code session started anywhere, and there is no --direct-launch
at all (a briefing quotes a whole findings report, so cmd.exe's embedded-newline problem is worse
here than for a work prompt, not better). The briefing is factual and unprescriptive by ruling
— the stated objective and acceptance criteria when this node can read the authoring task (and it
says it is showing the pull request's own description instead when it cannot), the surfaces
touched grouped two path segments deep with the diff's arithmetic, what CI ran with an unfinished
check reported as having concluded nothing and an unobserved rollup told apart from an empty one,
the merged findings report verbatim when one exists, and the author's own run's settlement,
unclaimed residuals and rulings when this node can read them. It volunteers no test scenarios,
areas of concern or review order; the session gives those the moment the reviewer asks, and helps
with local setup, running the suites, or writing an end-to-end test. Nothing is ever written to the
author's store.

The lap's push guard is Claude Code's own permissions.deny, written both to the run's settings
file and to <worktree>/.claude/settings.local.json so a session started in the checkout with no
--settings flag is covered: git push, the whole write half of gh pr (create, review,
comment, edit, merge, close, reopen, ready, lock, unlock, revert,
update-branch), the whole write half of gh issue (create, comment, edit, close,
reopen, lock, unlock, delete, transfer, pin, unpin, develop), gh api,
h9k pr approve and h9k pr request-changes are denied. The gh pr
side is denied verb-complete rather than by the few a session reaching for a review would use:
gh pr update-branch merges the base into somebody else's branch server-side and gh pr edit --body rewrites their description, both under the reviewer's login. The gh issue side is there
because it is the same resource under the other name: issues and pull requests share one number
sequence and one REST resource, so gh issue comment <pr-number> posts on the pull request's
conversation and starts a thread there — the first-class verb wrapping the very
gh api .../issues/<n>/comments endpoint the next sentence denies. gh api is in the list
because it is the surface the others reach through — this platform's own poster uses
gh api .../pulls/<n>/reviews, since gh pr review takes no line comments — and the two verdict
commands are in it because they ARE that poster: they reach the same endpoint under the reviewer's
own login and finalize the task besides, so a session that ran one would post a verdict the
reviewer never gave. A lap loses nothing by any of it, because every read it makes
goes through gh pr view / gh pr diff / gh pr checks, and gh issue view / list / status
stay open for the issues a pull request cites. It is a session-level permission deny matched on the
command as spelled, not a sandbox: it refuses the ordinary route to each of these, and a command
spelled around the prefix does not match it. git commit is deliberately not — the checkout is detached with no
local branch, so a commit there moves nothing, and the reviewer's own tests have to be committable;
the prompt sends those to a branch of their own, offered as a --stacked-on child of the authoring
task. A git pre-push hook is not available for this: git resolves hooks from the clone's single
shared hooks directory, so one written for this worktree would fire for the daemon's own legitimate
pushes from every other.

The lap never ends on its own. h9k pr approve / h9k pr request-changes post the GitHub
review on the pull request's current head (read live moments before, so the review names one
commit instead of floating, and a pull request closed or merged mid-lap is refused before anything
is posted — the read does not detect that the head moved, so an author who pushes during a long
lap gets the verdict pinned to a commit the reviewer may never have read; ratified as current-head
posting in #149) under the reviewer's own login, then record the
verdict on the task and PrReviewDelivered on the run — byte-for-byte where
h9k review resolve --merge-ready already leaves a pr-review run, so PrReviewEngine finalizes
from there with the worktree released and no merge ever observed. --note is required on both: a
review body this platform wrote would be Hall9k speaking under a human's login on somebody else's
pull request. The post comes before the record, deliberately — a failed post records nothing
and the reviewer re-runs; the mirror-image window (posted, then the record failed) is named in the
refusal text, which says the review IS on the pull request and points at
h9k review resolve --merge-ready to close the task by hand. GitHub rejects the entire review —
body and every other comment with it — when one --finding names a line the diff does not contain,
so nothing is posted until every line is one it accepts.

h9k review resolve <id> --merge-ready still closes a pr-review task out when the report was
walked and nothing needs posting (the walk-pr-review-findings path); h9k pr approve /
h9k pr request-changes are what replace that ceremony for somebody who actually reviewed.