feat(entry): cold-start scoped entry from a forge ticket reference#189
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #188. This is the runtime half of cold-start ticket entry; the methodology half (the
acquireskill) 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, orsourcehut:<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 (andRUNA_ENTRY_TICKET) into the session; once the methodology materializes thework-unit, the session binds and the cascade computestakenext 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 thework-unitartifact — 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
requirespreconditions 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 andlibagenthas no HTTP-client dependency.Acceptance criteria
work-unit, and the cascade computestakenext on it — observable via the dry-run projection (1. decompose [current, entry]/2. take (work_unit=work-unit-14) [projected]).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.rs—SessionScope::{Bound,Promised},open_entry, advance-time binding, precondition gate.libagent/src/projection.rs—project_entry_cascade(seeded, precondition-gated).runa-mcp—--session --ticket.runa-cli—--ticketongo/run, sharedcommands/entry.rs.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-targetsclean;cargo fmt --all --checkclean. Live run confirms both the satisfiable path (projectstake, exit 0) and the unmet-precondition path (blocked, exit 3) match the docs.