Scoped Workflow Closure Reference¶
Home > Reference > Scoped Workflow Closure
Field-level contract for scoped PR ownership, scoped process ownership, multitask state persistence, and fail-closed monitor decisions.
Contents¶
- Workflow Context Inputs
- Environment Inputs
- Operation Requirements
- PR Scope Persistence
- PR Scope Helper
- PR Scope Result Schema
- Multitask State Fields
- Process Scope API
- Process Validation Outcomes
- Recipe Integration Contract
- Failure Semantics
- Deterministic Checks
Workflow Context Inputs¶
default-workflow and workflow closure recipes pass these identity fields to
publish, readiness, final-status, and multitask monitor steps.
| Field | Type | Required | Meaning |
|---|---|---|---|
repo_path |
path string | Yes | Repository root or worktree root for the current workflow. |
repository |
owner/name string |
Required for GitHub PR scope | Exact GitHub repository. Resolved from repo_path when omitted and a GitHub remote exists. |
head_ref or branch |
string | Yes | Exact branch owned by the workflow. |
base_ref |
string | Required for PR scope | Exact PR target branch. |
pr_number |
integer or numeric string | Required when resuming a known PR | Existing pull request number. |
pr_url |
URL string | Optional with pr_number |
Existing pull request URL. Must agree with repository and pr_number. |
issue_number |
integer or numeric string | Required when the workflow is issue-backed | GitHub issue expected in PR metadata or closing references. |
work_item_id |
string | Required when the workflow is work-item-backed | Non-GitHub work item expected in PR metadata. |
expected_pr_title_prefix |
string | Required for title-based candidate lookup | Prefix that newly created workflow PRs must use. |
created_after |
RFC 3339 timestamp | Recorded for tie-breaking | Workflow start time. Discards older candidates only when disambiguating multiple primary-key matches; never rejects the sole primary-key PR. |
head_sha |
full Git SHA | Required for readiness, final status, and exact-head no-op paths | PR head SHA that must match before scoped success is reported. |
recipe_run_id |
string | Required for multitask monitor authority | Unique recipe run id. |
tree_id |
string | Required for nested recipe authority | Recipe execution tree id. |
workstream_id |
string | Required for workstream authority | Logical workstream id. |
Known PR identity is authoritative when present. If pr_number or pr_url is
known, validators check that concrete PR first and fail closed on mismatch.
Scoped lookup without a known PR resolves against a primary key — the exact
tuple repository + head_ref + base_ref restricted to same-repository,
non-cross-repository PRs. GitHub permits only one OPEN pull request per
head → base within a repository, so this tuple already uniquely identifies the
workflow's own PR. When the primary key yields exactly one candidate, it is
adopted as current work without requiring any additional discriminator.
issue_number, work_item_id, expected_pr_title_prefix, created_after, and
head_sha are tie-breakers. They are applied only when the primary key
returns more than one candidate (for example, when open and closed PRs share the
same head and base). A tie-breaker narrows an ambiguous candidate set; it never
hard-rejects the sole primary-key candidate. If tie-breakers eliminate every
candidate but exactly one OPEN candidate exists, that OPEN candidate is adopted
rather than returning no_scoped_pr. OPEN candidates are always preferred over
closed candidates when state varies at any resolution step.
Environment Inputs¶
| Variable | Required | Meaning |
|---|---|---|
AMPLIHACK_HOME |
No | Overrides the framework bundle root used by recipe tools. |
AMPLIHACK_AGENT_BINARY |
No | Propagates the active agent binary to nested workflow agents. |
No environment variable disables scoped validation. If scope is incomplete, the validator returns a named invalid result.
Operational runtime settings such as NODE_OPTIONS may be useful for large
nested runs, but they are not identity inputs and do not affect scoped
validation.
Operation Requirements¶
| Operation | Required identity |
|---|---|
| Publish a new PR | repository, head_ref, base_ref, and created_after. issue_number, work_item_id, or expected_pr_title_prefix are recorded for tie-breaking and PR body linkage. |
| Update or resume a known PR | pr_number or pr_url, plus repository, head_ref, base_ref, and created_after. |
| Lookup a PR before identity is persisted | repository, head_ref, and base_ref (the primary key). Tracking discriminators are optional and used only to break ties. |
| Check readiness | Known PR identity plus repository, head_ref, base_ref, created_after, and head_sha. |
| Report final status | Known PR identity plus repository, head_ref, base_ref, created_after, and head_sha. |
| Validate a workstream process | repo_path, workdir, branch, base_ref, recipe_run_id, tree_id, workstream_id, started_at, pid, and process start metadata. |
The primary-key lookup (repository + head_ref + base_ref, same-repository) is
sufficient to identify the workflow's own PR because GitHub allows only one OPEN
PR per head → base. Tracking discriminators (issue_number, work_item_id,
expected_pr_title_prefix) and head_sha are tie-breakers for the rare case of
multiple candidates on the same head and base; they are not required to adopt a
unique candidate, and a stale or mismatched discriminator never rejects the sole
primary-key candidate.
PR Scope Persistence¶
PR identity is passed through recipe context and publish outputs. Closure
recipes validate a persisted pr_number or pr_url first when either exists.
{
"pr_identity": {
"repository": "rysweet/amplihack-rs",
"number": 812,
"url": "https://github.com/rysweet/amplihack-rs/pull/812",
"head_ref": "feat/issue-754-scoped-closure",
"base_ref": "main",
"issue_number": 754,
"work_item_id": null,
"expected_pr_title_prefix": "Fix issue #754:",
"created_after": "2026-06-12T03:52:44Z",
"head_sha": "6c8e3b2a4f2e55d0fb65d51d94d8f4c16f37a111",
"captured_at": "2026-06-12T04:03:24Z"
}
}
Precedence:
- Validate persisted
numberandurlwhen either exists (authoritative override — this short-circuits above the primary key). - If no concrete PR identity exists, resolve the primary key
repository + head_ref + base_ref(same-repository, non-cross-repository). A single primary-key candidate is adopted directly. - If more than one primary-key candidate exists, apply tracking discriminators
and
head_shaas tie-breakers, preferring OPEN over closed. If tie-breakers eliminate all candidates but one OPEN candidate remains, adopt it. - Reject only genuine mismatches (different head branch, cross-repository, or an
authoritative
pr_number/pr_urlthat disagrees). Do not reject the sole primary-key candidate because a staleissue_numberorhead_shano longer matches. - Persist the concrete PR identity immediately after publish succeeds.
PR Scope Helper¶
amplifier-bundle/tools/workflow_pr_scope.sh is the only helper that determines
current-work PR identity for workflow closure.
amplifier-bundle/tools/workflow_pr_scope.sh \
--repo rysweet/amplihack-rs \
--head feat/issue-754-scoped-closure \
--base main \
--created-after 2026-06-12T03:52:44Z \
--issue 754 \
--expected-pr-title-prefix "Fix issue #754:" \
--head-sha "$(git rev-parse HEAD)"
When a PR is already known, pass the concrete identity:
amplifier-bundle/tools/workflow_pr_scope.sh \
--repo rysweet/amplihack-rs \
--pr-number 812 \
--pr-url https://github.com/rysweet/amplihack-rs/pull/812 \
--head feat/issue-754-scoped-closure \
--base main \
--created-after 2026-06-12T03:52:44Z \
--issue 754
Rules:
--repo,--head, and--baseform the primary key. Combined with the same-repository / non-cross-repository guard, they are exact-match fields.--pr-numberand--pr-urlare an authoritative override: when supplied they identify the PR directly and short-circuit the primary-key and tie-breaker passes. They must identify the same repository and PR.- When the primary key returns exactly one candidate, the helper returns it without consulting any tie-breaker.
--issue,--work-item,--expected-pr-title-prefix,--created-after, and--head-shaare tie-breakers. They are consulted only when the primary key returns more than one candidate.--expected-pr-title-prefixis a prefix check;--issueand--work-itemmatch structured closing/reference fields or exact workflow metadata (never a broad text grep);--head-shacompares the PR head SHA;--created-afterrejects candidates created before the run.- A stale or mismatched tie-breaker never rejects the sole primary-key candidate. When tie-breakers eliminate every candidate but exactly one OPEN candidate exists, that OPEN candidate is returned. OPEN candidates are preferred over closed candidates at every step.
- The PR base repository and head repository must match
--repoby default; fork and cross-repository PRs are rejected during the primary-key pass. - A PR on a different head branch is never matched. Zero primary-key candidates
exit non-zero with
no_scoped_pr; two or more OPEN candidates that survive tie-breaking exit non-zero withmultiple_scoped_prs. - The helper never falls back to
gh pr list --author @me, recent PR ordering, or broad title/body grep.
PR Scope Result Schema¶
Successful validation prints JSON on stdout and exits 0:
{
"ok": true,
"scoped": true,
"number": 812,
"url": "https://github.com/rysweet/amplihack-rs/pull/812",
"headRefName": "feat/issue-754-scoped-closure",
"baseRefName": "main",
"headRefOid": "6c8e3b2a4f2e55d0fb65d51d94d8f4c16f37a111",
"createdAt": "2026-06-12T04:03:19Z"
}
Invalid validation prints JSON and exits non-zero:
The reason value is stable and intended for recipes, shell tests, and monitor
checks. Human-readable message text is diagnostic only.
PR Scope Resolution Passes¶
workflow_pr_scope.sh resolves the current-work PR through ordered passes. The
first pass that produces a single result wins.
| Pass | Condition | Behavior |
|---|---|---|
| Authoritative override | --pr-number or --pr-url supplied |
Inspect that exact PR. Return it on match; fail closed on mismatch. Skips the remaining passes. |
| Primary key | No authoritative override | Filter to headRefName == --head AND baseRefName == --base AND isCrossRepository == false AND owner/name == --repo. |
| Single primary-key candidate | Primary key returns exactly one | Return it as ok: true. No tie-breaker is consulted. |
| Tie-breakers | Primary key returns two or more | Apply --issue, --work-item, --expected-pr-title-prefix, --created-after, --head-sha, preferring OPEN over closed. |
| Single-OPEN fallback | Tie-breakers eliminate all, one OPEN remains | Return that OPEN candidate rather than no_scoped_pr. |
| Loud ambiguity | Two or more OPEN candidates survive | Exit non-zero with multiple_scoped_prs. |
| Empty | Primary key returns zero | Exit non-zero with no_scoped_pr. |
Safety invariants that hold across all passes:
- A PR on a different
headRefNameis never matched. isCrossRepository == trueand fork PRs are rejected in the primary-key pass.- The head/base equality check is part of the primary key, never a tie-breaker.
- All GitHub stderr is routed through URL-credential redaction and length-capped before it appears in any diagnostic output.
WorkstreamState persists logical workflow scope and concrete process scope.
Missing fields deserialize successfully so old state files remain readable.
{
"workstream_id": "issue-754-closure",
"status": "running",
"scope": {
"repository": "rysweet/amplihack-rs",
"repo_path": "/home/user/src/amplihack-rs",
"workdir": "/home/user/src/amplihack-rs/worktrees/feat/issue-754-scoped-closure",
"branch": "feat/issue-754-scoped-closure",
"base_ref": "main",
"issue_number": 754,
"work_item_id": null,
"recipe_run_id": "run-01JZ7G7R4Z8KX8F2M9S5YH1VKT",
"tree_id": "tree-01JZ7G7R5A7W1JYQXP3CBB2C2E",
"workstream_id": "issue-754-closure",
"started_at": "2026-06-12T03:52:44Z"
},
"process_scope": {
"pid": 41872,
"process_started_at": "2026-06-12T03:52:47Z",
"process_start_marker": "41872:74293011",
"captured_at": "2026-06-12T03:52:48Z"
},
"process_scope": {
"pid": 41872,
"repository": "rysweet/amplihack-rs",
"repo_root": "/home/user/src/amplihack-rs/worktrees/feat/issue-754-scoped-closure",
"workdir": "/home/user/src/amplihack-rs/worktrees/feat/issue-754-scoped-closure",
"branch": "feat/issue-754-scoped-closure",
"issue_id": "754",
"work_item_id": "754",
"recipe_run_id": "tree-01JZ7G7R5A7W1JYQXP3CBB2C2E",
"tree_id": "tree-01JZ7G7R5A7W1JYQXP3CBB2C2E",
"workstream_id": "ws-754",
"process_started_at": "74293011",
"recorded_at": "2026-06-12T04:03:24Z"
}
}
Legacy state with missing scope or process_scope is valid for display. It is
not valid for notifications, closure, readiness, or terminal status decisions.
Process Scope API¶
The multitask monitor validates process ownership through
process_scope.rs.
pub struct CurrentWorkflowScope {
pub repository: String,
pub repo_root: String,
pub workdir: String,
pub branch: String,
pub issue_id: String,
pub work_item_id: String,
pub recipe_run_id: String,
pub tree_id: String,
pub workstream_id: String,
}
pub struct ProcessScope {
pub pid: Option<u32>,
pub repository: String,
pub repo_root: String,
pub workdir: String,
pub branch: String,
pub issue_id: String,
pub work_item_id: String,
pub recipe_run_id: String,
pub tree_id: String,
pub workstream_id: String,
pub process_started_at: String,
pub recorded_at: String,
}
pub struct ProcessSnapshot {
pub pid: u32,
pub alive: bool,
pub workdir: String,
pub process_started_at: String,
}
pub struct ProcessScopeConfig {
pub max_age_seconds: i64,
}
pub enum ProcessScopeValidation {
Valid,
MissingScope,
Dead,
PidReused,
TooOld,
RepoMismatch,
WorkdirMismatch,
BranchMismatch,
WorkstreamMismatch,
}
pub fn validate_process_scope(
current: &CurrentWorkflowScope,
persisted_workstream: &WorkstreamScope,
persisted_process: &ProcessScope,
snapshot: &ProcessSnapshot,
config: &ProcessScopeConfig,
) -> ProcessScopeValidation;
All path comparisons use canonical paths when the paths exist. If canonical resolution fails for a required path, validation returns the corresponding mismatch instead of guessing.
Process Validation Outcomes¶
Rust enum names are internal. CLI and JSON output serializes validation reasons as snake_case.
| Rust outcome | Serialized reason | Authoritative | Meaning |
|---|---|---|---|
Valid |
valid |
Yes | Persisted state, current workflow scope, and runtime snapshot match. |
MissingScope |
missing_scope |
No | Old state lacks required scope fields. |
Dead |
dead |
No | PID is not running. |
PidReused |
pid_reused |
No | PID exists, but runtime start metadata differs from the launch record. |
TooOld |
too_old |
No | Process age exceeds ProcessScopeConfig::max_age_seconds. |
RepoMismatch |
repo_mismatch |
No | Persisted repository or repo path differs from current workflow. |
WorkdirMismatch |
workdir_mismatch |
No | Persisted workdir differs from current workflow workdir. |
BranchMismatch |
branch_mismatch |
No | Persisted branch differs from current branch. |
WorkstreamMismatch |
workstream_mismatch |
No | Recipe run, tree id, or workstream id differs. |
Only valid permits monitor notifications or closure behavior.
Recipe Integration Contract¶
These bundled tools and recipes use scoped validation:
| Component | Contract |
|---|---|
workflow_publish_pr.sh |
Persists PR identity after creation and validates known PR identity before updating an existing PR. On gh pr create collision, re-runs the scoped lookup and adopts an existing OPEN PR as existing-open-pr success instead of failing the recipe. |
workflow_pr_ready.sh |
Refuses readiness checks until workflow_pr_scope.sh returns ok: true. |
workflow_final_status.sh |
Requires validated PR metadata before reporting terminal PR status. |
workflow-terminal-state.yaml |
Uses persisted PR URL or number, then scoped lookup; no recent-PR fallback. |
workflow-tdd.yaml |
Carries repository, head branch, base branch, tracking item, recipe run, and start-time scope through test and closure steps. |
quality-loop.yaml |
May survey broad PRs for quality reporting, but those surveys are not current-work identity. |
| multitask launcher | Captures workstream and process scope at process launch. |
| multitask orchestrator | Emits notifications and closure decisions only for serialized valid process scope. |
Failure Semantics¶
Scoped closure fails closed, but a stale tracking discriminator is not a failure:
- missing primary-key scope (repository, head, base) blocks publish/readiness/final-status current-work claims
- unrelated PRs are ignored even when newer or authored by the same account
- a PR on a different head branch is never adopted
- cross-repository and fork PRs are rejected by default
- two or more OPEN candidates on the same head and base block with
multiple_scoped_prsuntil scope is made more specific - a stale
issue_numberorhead_shadoes not reject the unique primary-key PR; those fields only break ties among multiple candidates no_scoped_pris reserved for a genuinely empty primary-key result- stale process records are display-only
- PID reuse is rejected when runtime start metadata differs
- missing runtime start metadata is rejected as incomplete process scope
- exact-head readiness blocks when
head_shadiffers from PR head
Failure output names the invalid reason and the checked identifiers. It does not persist secrets, full environment dumps, auth headers, agent prompts, or full process command lines.
Deterministic Checks¶
The regression suite covers these contracts:
cargo test -p amplihack-cli --test issue_754_scoped_monitor_contracts
cargo test -p amplihack-cli --test pr_scope_primary_key_test
amplifier-bundle/recipes/tests/test-default-workflow-reliability.sh \
scoped-pr-identity
Representative checks:
| Check | Expected result |
|---|---|
unique branch/base PR with a stale --issue and --head-sha |
helper adopts it with ok: true |
| PR body references a different upstream issue than the branch tracking issue | helper still adopts it (ok: true) |
| newer unrelated PR exists for same author | scoped helper rejects it |
| PR on a different head branch | helper never matches it |
| zero primary-key candidates | helper exits non-zero with no_scoped_pr |
| two or more OPEN candidates on the same head and base | helper exits non-zero with multiple_scoped_prs |
| fork or cross-repository PR candidate | helper rejects it during the primary-key pass |
gh pr create collides with an existing OPEN PR |
publish returns existing-open-pr success (exit 0), not FAILED_PR_CREATE |
| persisted legacy workstream lacks scope | monitor marks display-only and does not notify |
| persisted PID is dead | validation returns dead |
| PID exists with different start marker | validation returns pid_reused |
| process start metadata is missing | validation returns missing_scope |
| repo, workdir, branch, recipe, tracking item, or workstream differs | validation returns the named mismatch |