Skip to content

What to Do When PACE Issues a HOLD

A HOLD is a hard block. The day does not advance, no PR is opened, and the orchestrator exits with a non-zero status. Three review agents can issue a HOLD: GATE, SENTINEL, and CONDUIT. Each HOLD includes a hold_reason — a human-readable description of what failed and, where possible, what needs to change.


Step 1 — Find the hold_reason

The hold_reason appears in three places.

Terminal output — printed before the orchestrator exits:

[PACE] GATE: HOLD
[PACE] hold_reason: 3 of 5 acceptance criteria failed. Criterion 3 ("POST /users returns 201") failed — test UserCreationTest::test_create_user not found in test output. FORGE must add the missing test before this story can ship.

Escalation issue — if platform.ci is set to github, gitlab, or another platform, PACE opens an issue automatically with the hold_reason as the issue body.

Artifact files — in .pace/day-N/:

Terminal window
cat .pace/day-1/gate-report.yaml
cat .pace/day-1/sentinel-report.yaml
cat .pace/day-1/conduit-report.yaml

Each report has a decision field set to either SHIP, HOLD, or PARTIAL. Read the report from the agent that issued the HOLD.


Step 2 — Identify which agent issued the HOLD

Terminal window
grep -l "HOLD" .pace/day-1/*.yaml
AgentCommon HOLD causes
GATETests failing, acceptance criterion not met, test not found in output, coverage below threshold
SENTINELExploitable vulnerability (hardcoded secret, SQL injection, missing auth), FAIL-level finding
CONDUITBroken CI workflow, action pinned to @latest, failing step that does not block merge

Step 3 — Diagnose the issue

If GATE issued the HOLD

Run your test command manually from the repo root and read the full output:

Terminal window
# Python
pytest -v --tb=short
# Go
go test ./... -v
# Node.js
npm test -- --forceExit
# Java
mvn -q test
# C#
dotnet test -v minimal

Match the failure message to one of these common GATE HOLD patterns:

GATE HOLD patternWhat it meansWhat to do
test not found in outputFORGE wrote the feature code but did not write the testAdd the missing test to your test suite
criterion N failed — no evidenceFeature was built but the test does not assert the criterionReview and strengthen the test assertion
exit code 1A test assertion failedRead the failure message; fix code or fix the assertion
coverage below thresholdNot enough test coverageAdd tests until coverage meets the configured threshold

If SENTINEL issued the HOLD

Read the sentinel report and find the FAIL-level finding:

Terminal window
cat .pace/day-1/sentinel-report.yaml

Look for verdict: FAIL entries. Sentinel HOLDs are for exploitable vulnerabilities only — not advisories. Common cases:

Finding typeExampleFix
Hardcoded secretAPI key committed to sourceMove to environment variable; rotate the key
SQL injectionString concatenation in queryUse parameterised queries
Missing authenticationSensitive endpoint has no auth checkAdd auth middleware or guard
Unsafe deserializationDeserializing untrusted input without validationUse a safe deserialization library or validate input first

If CONDUIT issued the HOLD

Read the conduit report:

Terminal window
cat .pace/day-1/conduit-report.yaml

Conduit HOLDs are rare and indicate a CI/CD configuration problem that would break your pipeline. Common cases:

FindingFix
Action pinned to @master or @latestPin to a specific commit SHA or version tag (e.g., actions/checkout@v4)
Test step that does not block mergeUpdate branch protection rules to require the step
Makefile target referenced in CI does not existAdd the missing target or fix the reference in the workflow file

Step 4 — Decide: fix code, adjust criteria, or mark out_of_scope

You have three options after reading the hold_reason. Choose the one that matches the situation.

Option A — Fix the code

The right choice when the hold_reason is clear and the fix is straightforward. Make the change, run the test command locally to confirm it passes, then re-run the day.

Option B — Adjust the acceptance criterion

The right choice when the criterion was written too strictly and the actual outcome is acceptable:

# Before (too strict — brittlely tests the exact response shape):
acceptance_criteria:
- "POST /users returns 201 with user JSON including all fields"
# After (accepts the core behaviour):
acceptance_criteria:
- "POST /users returns 201 with user JSON"

Edit plan.yaml, save it, and re-run the day. FORGE will re-evaluate against the updated criteria.

Option C — Add to out_of_scope

The right choice when the acceptance criterion describes work that is genuinely deferred to a later story:

stories:
- id: story-1
title: "User registration endpoint"
status: pending
acceptance_criteria:
- "POST /users returns 201 with user JSON"
- "pytest -q exits 0"
out_of_scope:
- "Email verification — deferred to story-3"

GATE marks criteria that map to an out_of_scope entry as PARTIAL rather than FAIL. A story with only SHIP and PARTIAL verdicts (no FAIL) can ship.


Step 5 — Re-run the day

After making your fix:

Terminal window
python pace/orchestrator.py --day 1

PACE runs the full pipeline again from PRIME. The previous attempt’s cost is recorded so you can see the total:

Terminal window
cat .pace/day-1/attempts.yaml

If the HOLD persists, read the new hold_reason carefully — it may differ from the first one. FORGE may have fixed the original issue but introduced a new failure.


Step 6 — Escalate if retries are exhausted

If the pipeline has HOLDed twice and the remaining issue appears to be in the acceptance criteria or story scope (not in the code):

  1. Review plan.yaml — adjust acceptance criteria or add out_of_scope entries as needed
  2. Re-run the day

If you believe the issue is a PACE bug:

  1. Collect the full artifact set: ls .pace/day-N/
  2. Review attempts.yaml for the full attempt history
  3. Open an issue on the PACE repository with the relevant hold_reason and the artifact content

Quick reference — HOLD decision tree

HOLD received
├── GATE HOLD?
│ ├── Tests failing → fix code or tests → re-run
│ ├── Test not found → add the missing test → re-run
│ └── Criterion not met → adjust criterion or add out_of_scope → re-run
├── SENTINEL HOLD?
│ └── Security vulnerability → fix the code → re-run
│ (cannot be bypassed with out_of_scope)
└── CONDUIT HOLD?
└── CI/CD issue → fix the workflow → re-run
(cannot be bypassed with out_of_scope)