Metadata-Version: 2.5
Name: kestrel-feature-talon
Version: 0.2.13
Summary: Portable Kestrel feature integration for the Talon execution engine
Project-URL: Homepage, https://kestrelsovereign.com
Project-URL: Source, https://github.com/KestrelSovereignAI/kestrel-feature-talon
Project-URL: Issues, https://github.com/KestrelSovereignAI/kestrel-feature-talon/issues
Author: UncleSaurus
Maintainer: UncleSaurus
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: agents,ai,coding-agent,kestrel,talon
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: MacOS
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Requires-Python: <3.15,>=3.11
Requires-Dist: fastapi<1,>=0.115
Requires-Dist: kestrel-feature-workflows<0.8,>=0.7
Requires-Dist: kestrel-sovereign-sdk<0.40,>=0.37.1
Requires-Dist: kestrel-sovereign<0.54.0,>=0.53.20
Provides-Extra: test
Requires-Dist: pytest-asyncio>=1.1.0; extra == 'test'
Requires-Dist: pytest>=8.0.0; extra == 'test'
Description-Content-Type: text/markdown

# kestrel-feature-talon

Portable Kestrel feature integration and runtime contracts for the external
Talon coding-agent engine.

## Package boundary

This distribution and the Talon engine have separate ownership:

| Component | Distribution / command | Owns |
| --- | --- | --- |
| Talon feature | `kestrel-feature-talon` | Kestrel SDK feature discovery, agent tools, permissions, Talon-specific completion/pipeline/fleet-coding sources, the `talon:<job_id>` wait provider, health checks, and setup integration |
| Workflow runtime | `kestrel-feature-workflows` | Public workflow models and execution contracts used by the fleet workflow registered and built by this feature |
| Talon engine | `kestrel-talon` / `kestrel-talon` | Issue processing, coding-agent execution, and its command-line runtime |

Runtime health is exposed through the `talon_health` tool. The feature's
synchronous `health()` method is a direct convenience probe; it is not a
registered SDK health hook.

The 0.2 operator API supports launch, observation, pause, resume, and cancel.
It deliberately does not support stage retry: Workflows 0.5 cannot pass fresh
catalog/launch authority through its generic retry seam, so a generic retry of
a failed Talon stage is terminalized without another engine dispatch. Launch a
new governed run instead. Result fields for tests, review, demo, and Eye remain
`unknown`/`not_reported` unless Talon supplies a bounded verified producer
contract. Only Talon's bounded final claim-summary contract can populate PR
identity today; arbitrary or partial log text is never promoted to evidence.

Installing this feature also installs its compatible workflow runtime
dependency. It does not install or bundle the Talon engine. The host must install
`kestrel-talon` separately and either put its executable on `PATH` or configure
its absolute path as described below.

The compatibility ladder is intentional: Talon feature 0.2.0 is the migration
bridge for Sovereign 0.52, where the bundled predecessor still exists. Talon
feature 0.2.1 is the sole-owner release for Sovereign 0.53 and requires
`kestrel-sovereign>=0.53.0,<0.54.0`; 0.53 supplies the public turn-bound session
adapter needed to preserve durable completion wakes without reaching into core
lifecycle internals.

## Supported platforms: POSIX only

Linux and macOS, as `pyproject.toml`'s classifiers say. This is a requirement
of CLI dispatch rather than a preference: a dispatched job is launched as an
`sh` wrapper script and identified for its whole lifetime by an inherited
`flock` descriptor, and neither has a stock Windows form. Off POSIX no job can
be dispatched, so no job exists to observe, pause, resume or cancel.

Nothing downstream of dispatch therefore carries a `sys.platform` branch. The
cancel, pause/resume and process-teardown paths used to, and those branches
were removed rather than brought into line: they could only ever have run
against a job that could not have been launched, while reading — to the next
person auditing the stop path — as a support claim this package does not make.
Adding Windows support would start at dispatch (a wrapper that is not `sh`) and
at identity (a portable lock in place of `fcntl`), not at termination.

## Install and enable

Install the feature into the same Python environment as Kestrel Sovereign:

```bash
python -m pip install kestrel-feature-talon
```

Kestrel discovers `TalonCoordinatorFeature` from the standard Sovereign
feature entry-point group. The wheel also publishes a pre-boot setup provider
and the `fleet_coding_pipeline` workflow-to-feature ownership claim required by
Sovereign 0.53. Capture the installed package
in the host feature manifest so a later `uv sync` can be repaired, enable the
feature if it was disabled, and restart the affected agent after enablement:

```bash
kestrel feature sync --capture
kestrel feature enable talon
```

On a host whose `.kestrel-host-features.toml` already declares the package,
`kestrel feature sync` installs or restores it; `--capture` is only needed when
first recording the currently installed extensions.

Run the optional Kestrel setup step to collect the engine's GitHub credentials:

```bash
kestrel setup talon
```

The setup step writes `GITHUB_TOKEN` and optional `GITHUB_HUMAN_REVIEWER`
values to the host project's `.env`, preserving unrelated values and backing
up an existing file before a change. It does not install the engine or choose
runtime paths.

## Runtime configuration

Paths are explicit and installation-independent. Configure them under
`[talon.runtime]` in a host-selected TOML file, or provide the equivalent
values through the host context:

```toml
[talon.runtime]
engine_executable = "/opt/kestrel/bin/kestrel-talon"
project_parent = "/srv/kestrel/projects"
running_agent_source_root = "/srv/kestrel/kestrel-sovereign"
```

The host supplies the TOML filename through the feature's `config_path`; the
package does not select a global configuration file. A host may instead place
the runtime mapping directly in the feature configuration or public
`talon_host_context`.

Feature configuration applied through the SDK/HTTP configuration surface is
stored atomically as `talon_feature_config.toml` with mode `0600` beside the
stock agent's `storage_path`. On initialization, an explicit `talon_config` on
the agent or its `talon_host_context` takes precedence; otherwise the feature
loads that package-owned file. The file stores only the feature's `config_path`
and direct `runtime` mapping—the host-selected TOML remains independently
owned. A corrupt persisted file is reported as a configuration error and no
runtime paths are inferred in its place.

### Making `fleet_coding_pipeline` runnable: `pipeline_mode`

Contributing the pipeline is not enough to run it. `workflow_run` loads
definitions from the agent's workflow *store*, and nothing consumes a
contributed `WorkflowRegistration` into that store. An agent that dispatches
the pipeline — Strategic Memory's `signal_dispatch` does, by default — opts in
per agent:

```toml
# agent_data/<Agent>/kestrel.toml
[features.talon.config]
config_path = "/srv/kestrel/kestrel.toml"
pipeline_mode = "autonomous"   # or "consent"
```

At ready time the feature defines the chosen variant into **that agent's own**
store through the governed `workflow_define` tool, signed with the agent's
identity, and only if no live definition exists. `consent` keeps the
`approve_dispatch` stage, so every run waits for a human in the Approvals tab
before Talon writes code; `autonomous` omits it, and the safety moves
downstream to the merge gate. With `pipeline_mode` absent nothing is defined —
there is deliberately no default, because either default would be a signed
write to every agent's store that never asked for one.

It runs off the boot path and calls as a feature lifecycle hook (session id
`feature-lifecycle`). On an agent whose workflow tools require approval, a
Sovereign that knows that id refuses the define at once rather than queueing
an approval nobody is there to answer — logged at ERROR; grant the workflows
tools `ALLOW` or define the pipeline once by hand. An older Sovereign queues
the approval instead, which is answerable in the Approvals tab because boot
has not waited on it.

It only ever adds a definition. To switch variant, revoke the live one
(`workflow_revoke_definition`) and restart. A live definition of the *other*
variant is reported by what it means: configured `consent` while the live
pipeline is `autonomous` is an ERROR — runs are not waiting for anyone. A failed attempt is logged at
ERROR naming the consequence, because the silent version of this failure left
one agent's daily dispatch failing for 23 days (sovereign #3280).

A stored definition is signed and cannot change, so a release that changes the
pipeline's spec ships it as a new version. "Present" means a live definition at
that version or later. An older live one is superseded: the new version is
defined beside it in the configured mode, and `workflow_run` starts the newest
live version. The older row is left in place. Version 2 is the first such
change. Version 1's `talon_run` stage fixed `self_review: true`, overriding the
host preference on every run (#42), and recorded no job id (#43). If the new
version cannot be defined, the ERROR names the older version runs keep
starting, rather than claiming dispatch will fail.

The `talon_run` stage sets no run flag itself. `self_review` and
`skip_clarification` come from `[talon.preference]` exactly as for
`talon_claim`, unless the run's params state them. `talon_claim`, the
pipeline and the operator API all resolve these values through one function,
`runtime.resolve_execution`, so the same inputs launch the same argv on every
path. `skip_clarification` stated on an iterate-mode run is refused, because
`kestrel-talon iterate` has no clarification phase. The stage projects the
dispatched `job_id`, `log_path` and `wait_ref` into the run params, so
`workflow_status` and `workflow_history` name the job it started. This needs
`kestrel-feature-workflows>=0.7`, the first version with stage output
projection.

Version 3 (#46) holds no turn across the Talon run. Version 2's `verify_ci`
waited on the job inside its own turn for at most 3600 s, so every run whose
job took longer failed with `job_still_running` while the job went on to open
its PR. Now `talon_run` dispatches and returns, and `await_talon_job` waits
durably for a `talon.job_settled` event: an `await_signal` gate whose deadline
is a Talon run's budget, 10 h (`max_iterations` × `max_turns` × 60 s at the
preference defaults, four times over), not a held turn. The poll that sees the
job go terminal publishes the event, addressed to the run, and the gate's
predicate matches only the `job_id` that `talon_run` projected. So another
job finishing never advances the run. `verify_ci` runs only for a settled
job. A job that did not end `complete` with a `completed` disposition fails
the run with that outcome as the reason (`job_not_complete:blocked`, plus the
question). A completed job gets the existing CI probe against its PR head. If
the deadline wins, `talon_job_timeout` fails the run with a
`talon_job_timeout` reason that names the job and what it was doing.

The wait is a stage of its own, and read-only, because Workflows compensates
a timed-out stage before taking its false branch. On the irreversible
`talon_run` that compensation leaves residue, and the run would fail on a
bare `await_signal_timeout`. A separate stage fixes its source boundary after
`talon_run` returns, and the runner refuses any event committed before that
boundary. So the stage's own effect arms the job first, and nothing is
published for a job until it is armed.

Workflows keeps a run on the version it started on, so a v2 run in flight
when v3 is deployed still goes from `talon_run` straight to `verify_ci`, and
nothing armed its job. `verify_ci` no longer holds a wait for it. While that
job is still running, the run fails with
`superseded_definition:v2 (job not terminal)`. The job carries on, and its
`talon.job_complete` wake is delivered as usual. If the job has already
finished, the v2 run's CI is verified as normal. The stored v1 and v2
definitions stay in place, and new runs start v3.

`verify_ci` judges CI on the run's own repository and the head of the PR its
job opened. It looks for no check name of its own. With no branch protection
to say otherwise, every check run and commit status at that head counts, so a
repository with a single `test` check is judged on that one check. The run's
reason says what kind of result it was (#51):

- `checks_failed`: red, because a check concluded badly.
- `checks_timeout`: checks were still running, or a required check never
  appeared, when the 30-minute CI window ran out.
- `checks_unavailable`: GitHub returned no check run and no commit status for
  the head at all. A head with no checks is polled for the whole window,
  because checks register after the push, and this is the reason only if
  there are still none at the end.
- `checks_unreadable`: no whole snapshot could be read. There was no token,
  GitHub refused or kept failing the request, or the answer could not be
  interpreted. This is final at once.

Each of these fails the run closed. A failed stage records only its error, so
the error is where `verify_ci` writes what it judged: the repository, the PR
head, each check with its conclusion, any required checks and, for
`checks_unreadable`, the refusal, such as `HTTP 403 from
/repos/<owner>/<name>/commits/<sha>/check-runs`.

GitHub serves a public repository's check runs to any token, but a private
repository's only to a token allowed to read them. A fine-grained token needs
**Checks: read** and **Commit statuses: read** on every repository the
pipeline runs against. Without them, every private repository's runs fail as
`checks_unreadable` while the public core repository's pass.

`await_signal` needs two host hooks that kestrel-sovereign 0.53.9 does not
ship: `workflow_await_signal_event_trust_verifier` and
`workflow_await_signal_privacy_transition_lock` (kestrel-sovereign#3484). On a
host without them, Workflows refuses a v3 run at start, before Talon is
dispatched. Deploy this release with a Sovereign that carries #3484.

Agents without a concrete string or `pathlib.Path` `storage_path` still accept
configuration for the current process, but that configuration is deliberately
ephemeral and will not survive restart. Generic `os.PathLike` objects and test
doubles are not treated as state locations.

`engine_executable` takes precedence. When omitted, the feature performs only
a normal `PATH` lookup for `kestrel-talon`; it never searches relative to its
installed package or assumes sibling checkouts. `project_parent` and
`running_agent_source_root` never have implicit filesystem defaults.

Talon workspace clones are kept under
`<project_parent>/.talon-workspaces/<owner>__<repo>`. Every dispatch and
verification operation rejects a workspace that contains, equals, or sits
inside `running_agent_source_root`.

The package's `TalonConfigAdapter` reads and atomically updates the selected
TOML file while retaining unrelated configuration values. Hosts may attach a
`talon_host_context` mapping or object to the owning agent. In addition to the
runtime paths above, the feature recognizes public context values for
`agent_name`, `did`, `dispatcher`, `storage_path`, `scheduler`,
`observability_store`, `wait_registry`, `current_signal` (or
`get_current_signal()`), and `origin_session_id` (or
`get_origin_session_id()`). This adapter keeps workflow/session correlation and
durable job storage independent of private Sovereign agent fields. When that
explicit context does not define an origin, Sovereign 0.53's public
`agent.get_turn_bound_session_id()` adapter supplies the live turn's session;
no private lifecycle compatibility alias is consulted. Likewise a context that
does not define `dispatcher` or `did` leaves the agent's own public
`dispatcher` and `did` in force, so a partial adapter cannot keep a job's
`talon.job_settled` event from reaching the run waiting on it. A value the
context does define, even `None`, overrides the agent's.

## Tool and contribution contract

Version 0.2.1 preserves every tool of the public contract extracted from
Kestrel Sovereign 0.52.0, and adds `talon_cancel` and `talon_progress`:

| Tool | Command prefix |
| --- | --- |
| `scan_stale_work` | `!talon scan-stale-work` |
| `talon_claim` | `!talon claim` |
| `talon_file_and_claim` | `!talon file-and-claim` |
| `talon_github_write` | `!talon github-write` |
| `talon_get_config` | `!talon config` |
| `talon_set_config` | `!talon set-config` |
| `talon_verify` | `!talon verify` |
| `talon_schedule_work_rescue` | `!talon schedule-rescue` |
| `talon_workspace_status` | `!talon workspace-status` |
| `talon_setup_workspace` | `!talon setup-workspace` |
| `talon_batch` | `!talon batch` |
| `talon_status` | `!talon status` |
| `talon_job_log` | `!talon job-log` |
| `talon_pause` | `!talon pause` |
| `talon_resume` | `!talon resume` |
| `talon_health` | `!talon health` |
| `talon_cancel` | `!talon cancel` |
| `talon_progress` | `!talon progress` |

The 0.52.0 tool names, command prefixes, and parameter schemas remain
compatible; the snapshot of that surface is a migration fidelity check, not a
ceiling. `talon_cancel` stops one dispatched job's whole process group and
records the outcome. Every `talon_claim` is dispatched as such a group:
earlier releases preferred an A2A rail for an unattended claim with nothing
customised, and a job dispatched there had no supported stop, because the A2A
cancel endpoint marks the remote task `canceled` without reaching the process
doing the work. That rail was removed rather than left half-cancellable;
delegating a claim over A2A would return only together with a real stop
contract. `talon_cancel` still refuses, fail-closed, any job that is not a
local CLI process group — including a job `talon_status` listed from
observability rather than from this coordinator's registry — and says so in its
own error rather than reporting the job as unknown, because "no such job" reads
as "already gone". When the group is provably stopped but the outcome
could not be durably recorded, it reports `cancel_unrecorded` as a partial
result instead of claiming the job was cancelled, and a retry re-attempts only
the recording. Every signal it sends, escalation included, goes through the one
function in the package able to signal a process group at all, which authorizes
each signal against the job's group-owned identity re-read at that instant. So a
group that exits during the SIGTERM grace period is never escalated onto through
its reissued PGID, and a job dispatched without an identity is refused rather
than signalled on the strength of a recyclable number. A job whose engine
exited cleanly while something it started still holds the job's identity keeps
reporting as running, from a restarted agent exactly as from the one that
launched it, because a recorded exit describes the worker and not the job. That
outcome is not discarded — it is applied once no holder of the identity is
left. A job whose liveness cannot be established is reported as running with
`liveness` `"indeterminate"` rather than as finished.

The bound on all of this is deliberate and worth knowing: liveness answers for
the dispatch wrapper and the worker, which are what hold the job's identity. A
descendant the engine starts with `close_fds=True` joins the process group
without inheriting that identity, so once both holders have exited such a
descendant is **not** observed — the job reports finished from the outcome the
wrapper recorded while that descendant may still be running. Observing it would
mean falling back on the bare, recyclable PGID, which is the thing the identity
exists to replace; `job_identity`'s module docstring records why the bound is
accepted rather than widened. The
`talon_schedule_work_rescue` description and behavior are an intentional
Sovereign 0.53 disclosure correction: the command now refuses a schedule that
cannot perform live Talon discovery instead of claiming a recurring rescue was
installed. `talon_claim` gains two additive parameters, `resume` and
`resume_branch`; every predecessor parameter is unchanged and both new ones are
optional.

A finished job's `talon.job_complete` wake also carries its `disposition` —
`completed`, `clarifying`, `blocked`, `failed`, or `unknown` — read from the
processing summary Talon prints rather than from the exit code, because Talon
exits 0 for a run that stopped to ask a question. A `clarifying` or `blocked`
run is reported with that word and its question, not as completed, and the
wake tells the orchestrator to answer on the issue, clear Talon's gate
(`agent-blocked`, or 👍 / "ready" / `agent-ready` for a clarifying run), and
re-dispatch from its own loop. A summary that cannot be read is `unknown`,
never `completed`. A `talon_pipeline_dispatch` workflow stage that waits on
the run applies the same rule: it reports `state: "complete"` (with its
`disposition`) only for a `completed` run, and fails the stage closed —
naming the disposition and the question — for any other.

### Working or wedged: progress signals

Process liveness cannot tell a run that is editing files from a hung one, and
Talon writes its job log only at iteration boundaries. So every *running*
registry job in `talon_status` also carries three signals, read from what the
run has produced:

- `last_activity_at`: the newest mtime anywhere under the job's worktree,
  `.git` excluded, from one bounded `lstat` walk.
- `files_changed`: the number of paths `git diff <base_sha>` reports, plus
  untracked files. `base_sha` is the commit the run started from, so the count
  is this run's own work and not drift against a moving `main`.
- `exit_file_present`: whether the job's `.exit` sidecar exists.

`talon_status` also returns `observed_at`, the instant those timestamps are
read against. `talon_progress(job_id)` is the detailed view and is kept off the
common path. It adds the worktree's path, branch and base commit, a per-file
diffstat, the untracked file list, one `ps` sample of the engine process
(state, `etime`, CPU time and share, which separate waiting on the model from
spinning), and when the log's last line was written, labelled as the least
reliable signal. Its fields follow the epic's order: worktree, `.exit`
sidecar, process, log.

The worktree is found through kestrel-talon's run locator (kestrel-talon#243).
The first `Run locator: <path>` line of the run's own output names a
`run-locator.json` recording the worktree, base commit, pid and process group.
The coordinator never derives the engine's `<repo>-issue-<n>` layout. A
locator is used only if its process group is the job's recorded `pgid` and it
names the job's repository and issue (or pull request), because the file is
rewritten by every run handed that worktree.

Everything the run can write is read as evidence and never obeyed. Git
configuration can name programs that git runs while it only reads, such as
`core.fsmonitor`, a clean filter, an external diff or `textconv`. The run's
repository, worktree and user configuration are all writable by the coding
agent. So the diff and the untracked list are read through a private git
directory that the coordinator writes for each observation. It holds a copy
of the run's index (with the shared index a `core.splitIndex` index names)
and exclude patterns, a configuration naming only the object format, and
read-only access to the run's objects. Nothing reads the run's configuration
there, apart from a few settings that change what git reports and name no
program: ignore rules, case, file modes and line endings.
Submodules are compared by their recorded commit. Because the index is a copy,
polling a live run never takes the `index.lock` that the agent's commits need.
Every string either tool returns, including the worktree path, branch, file
names and log line read from those files, passes through one redaction step
before it reaches tool output.

**Absence is not zero.** Some signals cannot be observed. Examples are a batch
job (no single worktree), a run dispatched without `--worktree`, a run that has
not printed its locator yet or reported it could not write one, a locator
written by another run, a worktree that is gone, a walk that exceeds its bound,
and a git command that fails. Each such field reads `"unknown"`, and
`progress_unknown` maps the field to the reason. It is never `0`, `[]` or
`null`, so a quiet run and an unobservable run never read the same.

The same dispositions are what kestrel-sovereign's `signal_dispatch` reads,
through the wait provider's read-only `finished_runs()`, to avoid
re-dispatching an issue whose last run asked a question nobody has answered.
That read reports whether it covered every job in the registry. A registry
that exists but cannot be read whole — corrupt or oversized `jobs.json`, a
record it cannot interpret, or a finished job whose log is gone — is reported
incomplete with the reason, and dispatch stops with `RUN_HISTORY_UNCONFIRMED`
rather than treating it as no history. An agent that has never dispatched a
Talon job has no registry yet, and that is an empty history.

`signal_dispatch` picks across every repository in `scan_repos`. A
`fleet_coding_pipeline` run for a repository nobody has provisioned fails at
`talon_run` within milliseconds, unattended (#50). The ruling on #50 is that
the selector skips such a candidate with the typed reason
`no_talon_workspace` and names it in its output, so the run is never started.
The skip belongs to kestrel-sovereign's selector (kestrel-sovereign#3548).
What it reads comes from this package, from the same wait provider:
`workspace_readiness(repo)`. The answer is `{"repo", "provisioned": true,
"workspace"}` when the claim would run. Otherwise `provisioned` is false and
`reason` says why. It is either
`no_talon_workspace`, which also gives the `workspace` path and the
`next_step` (`talon_setup_workspace(repo='<owner>/<name>')`) that provisions
it, or `talon_workspace_unusable`, when provisioning would not help: Talon's
runtime paths or policy are unconfigured or unreadable, or the path overlaps
the running agent's own source tree. `detail` is the refusal the dispatch itself
would give. The read provisions nothing. `talon_setup_workspace` stays
approval-gated, and a repository becomes dispatchable only when an operator
runs it. The answer comes from the same rule that `talon_claim`, the
pipeline's iterate branch and `talon_batch` apply before they launch, so the
read and the dispatch cannot disagree.

This package supplies only that read. Until a kestrel-sovereign that carries
#3548 is deployed, `signal_dispatch` does not ask. A run for an unprovisioned
repository is still started and then fails at `talon_run`, as before.

The feature also contributes the agent-scoped `talon.coordinator` service,
the `talon:<job_id>` wait provider, Talon completion/pipeline/fleet-coding
workflow sources, conservative permission defaults, and the optional setup
step. Generic `stalled_work_rescue` sources remain host infrastructure; Talon
does not register or override them. Sovereign 0.53's generic sweep is
echo-only, so `talon_schedule_work_rescue` refuses to create a recurring no-op
and points operators to `scan_stale_work` for live Talon findings.
`scan_stale_work` is allowed for unattended discovery; every other Talon tool
defaults to operator approval.

`talon_pause` writes a private durable dispatch marker and CLI dispatch checks
it before any external effect. The marker survives agent restarts;
`talon_resume` clears it explicitly. These tools do not depend on a retired
core scheduler task. `talon_file_and_claim` treats its audited GitHub issue
creation and Talon launch as two explicit boundaries: a pre-existing pause
prevents the GitHub write, while a pause requested during approval or issue
creation takes effect immediately and blocks the later dispatch. In that race,
the tool reports the already-filed issue as a partial outcome for manual review.

## Preserved work from a failed run

When a run fails, kestrel-talon commits what the agent produced and keeps the
checkout. `talon_claim(resume=True, resume_branch=...)` continues that checkout
rather than preserving it aside and branching fresh — the two coordinates of
the recovery command a failed run prints. They arrive together on purpose:
`resume` without a branch continues whichever `issue-<n>-*` branch the checkout
happens to be on, which is the hole `resume_branch` closes. The tool carries
the engine's own refusals instead of relying on a background subprocess exiting
2 where nobody reads it, so `resume` without a worktree, `resume` without a
concrete issue, and `resume_branch` without `resume` (including an empty one)
are refused with nothing dispatched. This is unrelated to `talon_resume`, which
un-pauses dispatch.

Two behaviors around that checkout are the feature's own:

* A fresh claim is **refused** when a previous attempt at the same issue holds
  work that exists nowhere else — unpushed commits, or an uncommitted tree.
  Talon preserves such a checkout aside rather than deleting it, so nothing is
  destroyed either way; what the refusal prevents is the work moving one
  `talon-preserved-*` directory further from anyone looking for it with no
  trace of that in the dispatch result. The refusal names the checkout, its
  branch, and the exact `resume` call that continues it. Work that is already
  pushed never blocks a claim.
* When a job ends without succeeding, its preserved branch is **pushed**, so
  the commits stop being the only copy. Never forced, never committed on the
  run's behalf, and never against a branch that does not belong to the job's
  issue; a job still holding its group-owned identity is left alone until it is
  really over. The outcome — including a rejected push — is recorded on the job
  record rather than left to be inferred.

Both act on the branch the checkout is actually on, re-read from git, never on
the directory name that suggested looking there.

The host-scoped `TalonHostFeature` owns `/api/talon`, a capability-gated Talon
panel, lifecycle workers, and the `talon.operator` service. Manual claim and
approved-batch launches accept only opaque values resolved through
`claws.catalog>=1.1.0`, while durable run state and artifacts are projected
through `workflows.runs>=1.0.0`. Configure the same runtime paths under
`[talon.runtime]`; `operator_state_path` may additionally select the private
checksum-bound intent/evidence root. If omitted, it is placed beneath the
explicit `project_parent`.

Hosts may inject a Castle or Workflows `operator_context_resolver`; that
resolver always takes precedence. Without one, the operator API resolves the
authenticated single-tenant policy through `claws.catalog>=1.3.0`. Missing or
incompatible policy services fail closed.

Referenced operator evidence is never evicted behind an active or completed
run API. The private store instead enforces a fail-closed retention ceiling of
10,000 run directories and 128 MiB of evidence; once either bound is reached,
new artifact finalization is refused until an operator archives the state root
or provisions a larger policy in a future release. Expired pre-dispatch intents
are pruned off the event loop, while dispatched and linked intents remain
recoverable until their external job is finalized.

## Development

```bash
uv lock --check
uv run --extra test pytest -q
uv build
git diff --check
```

The wheel and source distribution are built from this repository alone. No
sibling checkout or editable/path dependency is part of the package metadata.
