Skip to content

feat(entry): cold-start scoped entry from a forge ticket reference#189

Merged
pentaxis93 merged 10 commits into
mainfrom
issue-188/cold-start-scoped-entry-from-ticket
Jun 12, 2026
Merged

feat(entry): cold-start scoped entry from a forge ticket reference#189
pentaxis93 merged 10 commits into
mainfrom
issue-188/cold-start-scoped-entry-from-ticket

Conversation

@pentaxis93

Copy link
Copy Markdown
Collaborator

Closes #188. This is the runtime half of cold-start ticket entry; the methodology half (the acquire skill) landed in tesserine/groundwork#394.

What this adds

A session can be opened from nothing but a forge ticket reference, the natural developer entry — "start on runa#14":

  • runa run --ticket <REF> — cold-start the cascade from a ticket.
  • runa go --ticket <REF> — cold-start one interactive tick.
  • runa-mcp --session --ticket <ref> — the same entry session surface.

Accepted reference forms: a bare number, #N, owner/repo#N, a GitHub issue URL, or sourcehut:<tracker_id>#N. The runtime resolves the reference to a tracker identity only and never reads ticket content — the methodology performs all forge reads through its own mechanics. The runtime delivers the reference (and RUNA_ENTRY_TICKET) into the session; once the methodology materializes the work-unit, the session binds and the cascade computes take next on it.

Design — the promised scope

A session's scope is either bound (a recorded work-unit) or promised (a ticket reference standing for a work-unit not yet materialized). A promised scope admits exactly one step — the methodology's acquisition surface, derived methodology-neutrally as the sole unscoped producer of the work-unit artifact — and resolves to a bound scope when that step materializes the work-unit. Downstream of acquisition, a session opened from a reference and one opened from the materialized id are indistinguishable. A reference whose work-unit already exists degrades to a normal scoped session at open.

Entry substitutes only the acquisition step's trigger. Its requires preconditions still gate — an acquisition with unmet dependencies is blocked (exit 3), not executed, in every path (session, live run, and dry-run projection). The runtime core stays forge-neutral: it contains no ticket-content logic and libagent has no HTTP-client dependency.

Acceptance criteria

  • A single invocation carrying a ticket reference starts a session in which the methodology's entry receives that reference.
  • The methodology's acquisition produces the validated work-unit, and the cascade computes take next on it — observable via the dry-run projection (1. decompose [current, entry] / 2. take (work_unit=work-unit-14) [projected]).
  • All forge reads resolve through methodology-owned mechanics; the runtime core performs no forge content fetch.
  • Works in interactive and autonomous modes with identical semantics — mode changes only who issues the verb at what cadence.
  • The interface and session-surface contracts document the affordance, and the documented behavior matches a live run.

Layers touched

  • libagent/src/entry.rs (new) — reference parsing, acquisition-surface discovery, promise resolution.
  • libagent/src/context.rs — entry reference delivered into the prompt.
  • libagent/src/session.rsSessionScope::{Bound,Promised}, open_entry, advance-time binding, precondition gate.
  • libagent/src/projection.rsproject_entry_cascade (seeded, precondition-gated).
  • runa-mcp--session --ticket.
  • runa-cli--ticket on go/run, shared commands/entry.rs.
  • Docs: interface-contract, session-surface-contract, cli-reference, README, ARCHITECTURE, CHANGELOG.

Verification

663 tests pass (new entry unit tests + projection/session unit tests + CLI/MCP integration tests covering acquire→cascade, dry-run projection, mode parity, and precondition-blocked exits). cargo clippy --workspace --all-targets clean; cargo fmt --all --check clean. Live run confirms both the satisfiable path (projects take, exit 0) and the unmet-precondition path (blocked, exit 3) match the docs.

A session can now be opened from a forge ticket reference alone —
`runa run --ticket <REF>` and `runa go --ticket <REF>` (and
`runa-mcp --session --ticket <ref>`) — when no work-unit artifact exists
yet. The runtime resolves the reference to a tracker identity and serves
the methodology's acquisition surface; it never reads ticket content. Once
the methodology materializes the work-unit, the session binds and the
cascade computes `take` next on it. A reference whose work-unit already
exists degrades to a normal scoped session, indistinguishable downstream
of acquisition.

Scope is a property of the session: bound (a recorded work-unit) or
promised (a ticket reference standing for one not yet materialized). The
reference substitutes only the acquisition step's trigger — its `requires`
preconditions still gate, so an acquisition with unmet dependencies is
blocked rather than executed, in every path (session, live run, dry-run
projection). The acquisition surface is derived methodology-neutrally as
the sole unscoped producer of the work-unit artifact.

This is the runtime half of #188; the methodology half (the
`acquire` skill) landed in tesserine/groundwork#394.
Three correctness fixes in the cold-start ticket-entry paths:

- Entry projection now seeds the acquired `work-unit` explicitly, so an
  acquisition that declares it via `may_produce` or a required output choice
  (both accepted by `discover_acquisition_surface`, and the groundwork shape)
  projects the scoped follow-ups; previously `runa run --ticket --dry-run`
  showed only the acquisition step.
- A live `run --ticket` whose acquisition satisfies its contract but does not
  materialize a work-unit for this ticket now clears the persisted acquisition
  execution record, so no metadata claims the entry step completed.
- A failed advance commit in a promised session now restores the prior scope
  on every post-bind rollback path, so a retried tick still represents the
  promised entry instead of a half-bound session.
`go --ticket` resolved the configured agent command before parsing the
ticket reference or checking the acquisition surface, so a bad reference or
an unsupported manifest in a project without `[agent].command` reported a
missing-agent config failure (exit 6) instead of its own usage (2) or
capability (6) error. Agent resolution now happens only after the reference
and acquisition surface are validated, just before the agent is launched.
A local `.runa/` from a `runa init` run was accidentally committed; its
`methodology_path` points at a non-portable `/tmp` path, so default runa
commands in any other checkout load it and fail (e.g. `runa list` reports a
missing manifest) before reaching real project state. Remove the tracked
config/state and gitignore `/.runa/` — this repo is a Rust project, not a
runa-managed one, so it should carry no runa project state.
Two correctness fixes in the cold-start ticket-entry paths:

- `open_entry` now scans and resolves the promise before discovering the
  acquisition surface, so a ticket whose work-unit already exists degrades to
  an ordinary bound session even when the methodology declares no (or an
  ambiguous) unscoped work-unit producer. Acquisition discovery now happens
  only on a genuine cold start.
- Entry substitutes only the acquisition's trigger; its scan-trust gate now
  applies alongside preconditions. A partial scan of an acquisition input (an
  unreadable file under a required type) blocks entry — in the session/MCP
  path, the live `run`/`go` pre-checks, and the dry-run projection — exactly as
  normal readiness classifies such a protocol BLOCKED. The promised-scope no
  longer carries a now-unused acquisition name; the pinned step holds it.
resolve_promise returned Ok(None) — authorizing cold-start acquisition —
even when the work-unit type was only partially scanned, so an unreadable
work-unit file could hide a matching or duplicate tracker root and let
run/go --ticket (and runa-mcp --session --ticket) duplicate existing ticket
work and bypass duplicate-root checks. A no-match result is now trusted only
when the work-unit scan is complete; otherwise resolution fails with
ScopedWorkUnitError::WorkUnitScanIncomplete. A found match still binds
(re-entry is safe under an unrelated gap).
The cold-start entry path re-derived "is this acquisition servable?" at four
sites (session open, session reconcile, dry-run projection, CLI pre-check),
each manually enumerating the readiness gates entry must honor. That
duplication is why successive reviews surfaced a missing gate one at a time:
preconditions, then partial-scan trust on inputs.

Introduce `check_acquisition_admissible` as the single source of truth that
enumerates those gates once — preconditions and scan trust, with the trigger
substituted by the operator's reference and currentness intentionally omitted.
All four surfaces now route through it, so the gate set is consistent by
construction rather than by hand-alignment, and cannot drift again.

Behavior-preserving: same gates, same outcomes, full suite green.
Two correctness fixes in the cold-start ticket-entry flow:

- The acquisition's execution record was derived from the protocol's original
  trigger, which the ticket reference substitutes and which is usually
  unsatisfied at entry — so the persisted record registered no trigger
  freshness inputs, and a later normal activation of the same acquisition could
  be falsely treated as current when its real trigger artifact changed. A new
  protocol_entry_execution_record registers the full trigger freshness baseline
  as if satisfied; the session (open + provenance refresh) and CLI entry paths
  use it.

- After a cold acquisition succeeds, the downstream run_with_scope started with
  executed_any = false, so a quiescent post-acquisition scope (e.g. a
  methodology that only needs acquisition) reported nothing-ready / exit 4
  despite having executed. run_with_scope now takes a prior_execution flag, so
  the cold-ticket path reports success when the post-acquisition scope is
  quiescent.
The acquisition admission gate used protocol_scan_incomplete_types, which
flags both `requires` and trigger-referenced types. But the ticket reference
substitutes the trigger, so the trigger artifact is never consulted — an
unreadable sibling under a trigger-only type wrongly blocked a valid
cold-start acquisition. check_acquisition_admissible now uses
precondition_scan_incomplete_types (requires only); scan gaps on a trigger-only
type are ignored. Requires-type scan gaps still block, and the work-unit
resolution scan-trust gate is unaffected.
…are forges

Two correctness fixes in the cold-start ticket-entry path:

- The CLI acquisition context was built with raw build_context, skipping the
  accepted-input trust filter that build_execution_plan and session next_context
  apply, so runa run --ticket (live and dry-run) could hand the agent optional
  artifacts from changed/partially-scanned types that normal execution
  withholds. The build-plus-filter is consolidated into one
  context::build_execution_context used by step, session, and ticket entry —
  the filter can no longer be applied inconsistently.

- A bare `#N` reference fell through to GitHub for any non-sourcehut forge
  type, so a typo or future custom RUNA_FORGE_TYPE silently bound a GitHub
  tracker identity the active deployment could never satisfy. The bare path now
  matches `github`/`sourcehut` explicitly and reports EntryError::UnsupportedForge
  (exit 2) otherwise.
@pentaxis93 pentaxis93 merged commit 62add72 into main Jun 12, 2026
1 check passed
@pentaxis93 pentaxis93 deleted the issue-188/cold-start-scoped-entry-from-ticket branch June 12, 2026 08:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

task(entry): cold-start scoped entry from a forge ticket reference

1 participant