Scoped Workflow Closure¶
Home > Concepts > Scoped Workflow Closure
Scoped workflow closure prevents default-workflow monitors from treating an
unrelated pull request or stale process as current work.
The Problem¶
Workflow closure used to infer ownership from broad signals: recent pull requests, PR author, grep hits in titles or bodies, and PID-only process records. Those signals are convenient, but they are not authoritative. They can point at the wrong work when:
- the same user has multiple open PRs
- a newer unrelated PR appears while an older workflow is closing
- a process ID is reused by the operating system
- a persisted workstream record outlives the process it described
- a branch, repository, or work item is copied into text but does not identify the current workflow
Scoped workflow closure makes identity explicit. A workflow may notify, publish, check readiness, or report terminal PR state only after it validates the current work against first-class identifiers.
The Core Rule¶
Current-work identity is never inferred from recency, authorship, broad text matching, or a same-user PR list.
The workflow's own pull request is identified by a primary key: the exact
repository + head branch + base branch tuple, restricted to same-repository
(non-fork, non-cross-repository) PRs. GitHub allows only one OPEN pull request
per head → base within a repository, so this tuple is already unique for the
workflow's PR:
Tracking discriminators — PR/issue/work-item token, recipe/workstream id, title prefix, start time, and head SHA — are tie-breakers. They resolve the rare case of multiple candidates (for example, an OPEN and a previously CLOSED PR on the same head and base). A stale or drifted discriminator never rejects the unique primary-key PR.
For processes and workstreams the persisted scope still requires the full identity tuple:
Missing primary-key scope is not a soft match. It is display-only history.
PR Scope¶
PR scope identifies the pull request owned by the current workflow. The fields divide into the primary key and the tie-breakers:
| Scope field | Role | Purpose |
|---|---|---|
repository |
Primary key | Exact GitHub repository owner/name (same-repo only). |
head_ref |
Primary key | Exact PR branch expected for the workflow. |
base_ref |
Primary key | Exact target branch. |
pr_number or pr_url |
Authoritative override | Concrete PR identity when already known; bypasses primary-key and tie-breaker resolution. |
issue_number or work_item_id |
Tie-breaker | Tracking item the PR is expected to close or reference. |
expected_pr_title_prefix |
Tie-breaker | Deterministic title prefix. |
created_after |
Tie-breaker | Run start time; used to discard older candidates. |
head_sha |
Tie-breaker | Exact head SHA for exact-head operations. |
The shell helper workflow_pr_scope.sh is the single authority for current-work
PR ownership. workflow_publish_pr.sh, workflow_pr_ready.sh, and
workflow_final_status.sh call it before mutating or reporting PR state.
Persisted PR identity has priority: a known pr_number or pr_url is
authoritative and validated directly. Otherwise the helper resolves the primary
key. A single primary-key candidate is adopted as-is — no issue token, head SHA,
or title prefix is additionally required. Tie-breakers apply only when more than
one candidate shares the same head and base; if they eliminate everything but one
OPEN candidate remains, that candidate is adopted. OPEN is always preferred over
closed. The helper never falls back to recent PRs, PR author, or broad
title/body search.
This is why the workflow reliably adopts its own PR even when the branch bakes in
a local tracking issue (feat/issue-1014-...) while the PR body references a
different upstream issue (#4610), or when the remote head advances past the
locally captured SHA. The unique (head, base, same-repo) PR is matched;
the stale discriminators are ignored rather than treated as hard rejections.
Cross-repository and fork PRs are rejected by default. A workflow that starts in
rysweet/amplihack-rs may close only a PR whose base repository and head
repository both match rysweet/amplihack-rs, unless a future recipe explicitly
opts into a narrower fork policy.
When publishing, workflow_publish_pr.sh is collision-tolerant: if
gh pr create fails because a PR already exists for the branch, the helper
re-runs the scoped lookup and adopts the existing OPEN PR as an existing-open-pr
success instead of hard-failing the recipe. Only a genuinely absent PR combined
with a failed create is reported as FAILED_PR_CREATE.
Process Scope¶
Process scope identifies a running agent or workstream process owned by the current workflow. The authoritative fields are:
| Scope field | Purpose |
|---|---|
pid |
Process ID captured at launch. |
process_started_at or platform start marker |
Runtime start metadata used to detect PID reuse. |
repo_path |
Canonical repository root. |
workdir |
Canonical worktree or workstream working directory. |
branch |
Git branch captured at launch. |
base_ref |
Base branch captured at launch. |
recipe_run_id |
Current recipe run identity. |
tree_id |
Recipe tree identity for nested runs. |
workstream_id |
Logical workstream identity. |
issue_number or work_item_id |
Tracking item owned by the workstream. |
started_at |
Workstream start time. |
The Rust validator rejects dead, reused, too-old, missing-scope, and metadata-mismatched process records before monitors emit notifications or close workstreams.
Fail-Closed Behavior¶
Every scoped validator returns either an authoritative match or a named invalid reason. Invalid scope blocks current-work notifications and closure decisions.
Common invalid reasons:
| Reason | Meaning |
|---|---|
missing_scope |
The persisted state predates scoped closure fields. |
no_scoped_pr |
No PR matches the primary key (repository, head, base). This is an empty result, not a stale-discriminator rejection. |
multiple_scoped_prs |
Two or more OPEN candidates share the same head and base and survive tie-breaking. |
repo_mismatch |
Repository differs from the current workflow. |
workdir_mismatch |
Work directory differs from persisted process scope. |
branch_mismatch |
Branch or head ref differs from scope. |
workstream_mismatch |
Recipe, tree, or workstream id differs from scope. |
pid_reused |
Runtime process start metadata differs from the launch record. |
too_old |
Persisted process started outside the accepted age window. |
missing_scope |
Required PR or process scope is incomplete. |
Invalid records can still be shown in diagnostic output, but they are not authoritative. A monitor may say that a stale record exists; it must not notify as though that record is current work.
Why This Is Simpler¶
The design centralizes identity decisions:
- one shell helper for PR ownership
- one Rust validator for process ownership
- one persisted scope model shared by multitask launcher and orchestrator
Recipes and monitors no longer duplicate partial matching rules. They ask the scope validator whether a candidate is current work and respect the result.