The supported host launch resolves existing saved CA trust before Node starts gate work, propagates it to children and verifies TLS before expensive checks. Transport prerequisites return unrun exit 88; authentication, authorization and quiescence retain separate diagnostics. See saved host trust for precedence and recovery. No trust-store or credential change occurs at launch.
Recovering an interrupted gate publication
The pre-admission documentation exemption classifies the requested candidate worktree, even when the gate runner is launched from another checkout. Git merge-base/diff reads and derived-artifact checks use that candidate directory. An unavailable Git identity continues through ordinary admission; it is not a documentation exemption. The shared classifier and artifact registry determine the same exemption as the pre-push hook.
The controller retains <full-log>.<lease-id>.execution.json before descendant
cleanup. It binds the observed command result to its original owner, lease, gate,
candidate, merged tree, evidence plan, toolchain and artifact. This checkpoint is
execution material; it is not a canonical pass. Termination decisions remain in
the adjacent .termination.jsonl journal, including the Windows owned-job close
policy recorded before launch.
Before sending a completed result, the controller atomically saves the exact
request in the existing slot’s pending-evidence file. If acknowledgement is lost,
run pnpm run pregate -- --finalize-evidence from the original governed worktree
with its original provider/session attribution. Recovery validates the checkpoint,
current evidence policy and original completion time, then uses the normal
authenticated recorder. Publication and lease binding share a transaction; replay
returns the existing receipt only for matching content and actor. A recovered
failure stays failed. Recovery never extends the evidence lifetime.
Missing checkpoints, changed identities/content, expired evidence and leases outside the recorder’s active/released contract are refused. Preserve those files as inconclusive material and obtain a newly admitted run with the repaired controller. Do not edit a lease, invent a pass file or turn a timeline note into local-CI evidence. The September 12 profile execution predates this checkpoint contract and is outside the evidence freshness window.
Windows commands run in a kernel Job Object before starting descendants; closing the owned handle fences that membership without traversing recycled parent PIDs. The wrapper watches its owned input pipe, so loss of its controller also closes the job. This uses the documented Windows Job Object contract. Normal Windows gates use job membership alone. The shared individual-cleanup helper also supports a bound Windows handle and creation-time comparison. Linux observations reuse the shared fence reader’s kernel start ticks. Other POSIX hosts use process start time; a fresh identity check precedes signalling, but separate observation and signal calls retain a race and second-resolution start times on those hosts cannot prove same-second PID reuse impossible. Unreadable or inconsistent ownership never authorizes individual termination. These protections do not establish the cause of historical executor losses.
Completion requires a validated packet from the current command, not merely a wrapper exit code. A missing executable, wrapper startup failure, missing or invalid packet, or unreadable cleanup identity produces an infrastructure inconclusive result. An unknown process is never killed to obtain a clean result.
The server admits gate client revision 2 or newer. After the canonical release deploys this floor, refresh the governed source checkout before starting a new gate; a revision refusal names that refresh, not a permission override. Preserve older completed material and pending requests. Refreshing a client does not extend an expired lease or convert old unqualified material into a pass.
Isolated verification users
For permission-removal and concurrent-user tests, issue_ux_verification_sign_in
accepts an optional fixture with the owning capsuleId, active leaseId,
role profile, and slot (0 or 1). Reuse the returned runId for that run.
This creates ordinary users on development or test installations only, through
the existing signed-link exchange. It requires current user permissions,
assistant grants, and admission to the exact workroom. A fixture never grants
access to another room.
Renewing a link preserves role removals, disabled state, and absolute expiry. Five profiles and two slots bound a run to ten accounts; one lease owns one run. Sessions end no later than the lease and the original two-hour limit. Fixture expiry is checked at current-user authorization even if cleanup has not run.
After testing, cancel and settle the run’s tasks and close its rooms through
their normal actions, then call the same tool with retireRunId. Pending work
is returned explicitly. Completed retirement revokes the fixture connections,
disables only those accounts, and retains provenance and audit. The existing
retention sweep provides bounded expiry cleanup. Retire fixtures before rolling
back code that enforces their expiry. The legacy shared persona remains the
default when fixture is omitted.
Fixture retirement records its own completion in the run’s result; it never earns a passing UX verdict. A running run without a completed verdict closes as skipped. Existing verification outcomes remain intact, and a passed run still revokes its fixture users and credentials before cleanup is complete.
Semantic review publication and infrastructure recovery
The publication adapter reads semantic-review-policy.json. Its mode is the source of truth for receipt enforcement; a skill must not present a shadow observation as an enforced refusal. This procedure applies to external Claude, Codex and Grok delivery. Build Studio keeps its own transition contract.
New requests use the role-bound v2 request contract and produce v3 receipts.
Historical v1 requests and v2 receipts remain readable, but cannot satisfy a new
role-bound review. Publication expects code-change by default. Document callers
must supply their expected role with --artifact-type spec or --artifact-type plan
when recording and validating a receipt; the expected role comes from the caller,
not from the receipt. A receipt for one role cannot authorize another role.
- Freeze a clean DCO-signed commit and produce its required gate evidence.
Call
review_semantic_changewith the governing Workroom, immutable base/head trees, committed diff digest and failure analysis bound to that evidence. Source-local test prose does not replace executed gate evidence. Invalid analysis or unresolved, expired or mismatched evidence returnsinvalid_failure_analysiswith structured reasons before a review task is created. Reconcile the Workroom head and cite current passing evidence for that room and exact change before resubmitting. Existing completed gate records are retained; a refusal does not authorize changing their identity. Execution revalidates evidence again because it can change after admission. - Repair blocking code findings. Re-review a changed tree; stop after two failed repair rounds. A changed diff, policy, reviewer or specialist set invalidates a receipt. An infrastructure refusal or uncertain execution is not a finding.
- Record a returned native receipt with
pnpm review:semantic-gate -- record --receipt-file <tool-result.json> --evidence-id <id>. Then runpnpm review:semantic-gate -- validatefor the current commit. In enforced mode an unavailable or stale receipt blocks publication. Follow the returned remedy; never change mode or forge a receipt to pass the check. - If infrastructure cannot run the native review, use
node scripts/gate-context.mjs --situation review-infrastructure-unavailable. Do not repeatedly submit an unchanged request to unavailable capacity or poll a task whose execution has stopped. Use the existing review recovery contract after a verified remediation. - In shadow mode the adapter may allow publication with a missing receipt. For a bounded DEV repair under explicit operator authorization, preserve an independent assessment of the exact change, record the native review as unrun or inconclusive, and proceed through passing source checks and the protected PR/merge queue. Reuse authorization already granted for that scope; a phase change does not require a new exception question. Shadow alone grants no authority and does not excuse ignoring a completed review’s blocking findings.
Independent client reviews are valid assessments with their own author, source identity, findings and outcome. Cite them separately in the Workroom and PR; they are not native receipts. Publication permission, passing review, merge and live verification remain separate facts. DCO, grant intersection, ownership and production integrity remain mandatory throughout recovery.
CI scopes a pull request from its source head and that head’s merge-base with origin/main. GitHub’s checkout may be a synthetic merge; using its base with the source head includes unrelated main changes as reverse diffs and can turn documentation into exhaustive work. Both comparison endpoints must describe the same source history. Merge-group and other non-PR events retain exhaustive verification.
Public documentation publication. CI’s Documentation Publication Build builds docs/ with the same GitHub Pages Jekyll action used by legacy Pages publication. It runs for changed documentation or CI configuration on PRs, merge candidates and main; missing comparison history runs the build. Unchanged publication inputs produce a terminal no-build result. The job also checks that malformed Liquid is rejected, and feeds the required Merge Readiness aggregate. It only builds: the existing Pages channel still owns deployment. A green build is not evidence that the public site has deployed. Jekyll processes Liquid inside code fences; protect literal examples using its raw tag guidance.
Status: task-specific gate contract. AGENTS.md §4 carries the invariant; this reference carries the detailed rules and procedure. For platform or gate failure, read recovery routing first.
Documentation impact is part of done. Every Build Studio build and every external Claude / Codex / Grok implementation thread must decide whether the change affects users, AI coworkers, public-site positioning, setup/install, operations, architecture/contributor workflows, route maps, prompts, or external-agent behavior. If it does, update the correct human-readable docs in the same branch: docs/user-guide/ for operator workflows and in-app help, docs/index.html / docs/README.md for public pre-install positioning, docs/architecture/ for architecture/contributor explanation, AGENTS.md for durable development doctrine, and docs/superpowers/ for implementation history. “Fix first, document later” is only an emergency or explicitly approved sequencing choice: the same branch or Workroom must still record the spec/doc/backlog/WWMD follow-up before the work is claimed done. If no docs are needed, record the concrete no-docs-needed reason in the plan, PR body, or evidence. Do not claim done while docs exposed to users, AI coworkers, or opendigitalproductfactory.com are knowingly stale.
TypeScript errors only surface in next build, not in vitest or IDE checks. Run the build per epic, not per release. Pre-existing failures: note them and fix if feasible. Do not defer.
Signing the browser in for UX verification (BI-9369DEB5). A fully automated installation is verified by agents with no person present, and most pages are session-gated. An agent never types a password and never asks the operator to sign in; it asks the platform to sign its own browser in: the MCP tool issue_ux_verification_sign_in (pack ux-verification, grant sandbox_execute, capability manage_platform) returns a one-time link that opens a two-hour session as the seeded persona automation@dpf.local and lands on the requested path. The link is permitted on development- and test-class installations by default; a production installation refuses unless an operator records PlatformConfig automation.signIn.enabled. Design: docs/superpowers/specs/2026-09-03-automation-persona-sign-in-design.md.
Where each gate runs. The four gates above describe what must pass, not where the binaries execute. Cheap source-local checks (targeted vitest, pnpm --filter <pkg> typecheck) can run in the topic worktree. Runtime-bound gates (next build against the production bundle, UX verification against the served portal, migration apply against a live Postgres) run against the canonical local install after the governed self-upgrade path has deployed the target image/source, OR against the shared local-CI convergence sandbox that every worktree leases sequentially via claim_nonprod_environment_lease(environmentKey="local-integration-ci"). A gate that did not execute because the worktree could not host its runtime is an unrun gate, not a red gate: re-run it via the sandbox lease, capture that evidence in the PR (§6). Per-worktree runnable runtimes are explicitly NOT the answer — at DPF’s expected scale (1k–10k concurrent worktrees) the convergence sandbox is the only tenable substrate for runtime-bound verification. See kernel principle for the full scale rationale + lease workflow.
Sandbox freshness is gate step-zero (BI-ECDF9520). A shared sandbox that checked out new source but still serves an old dependency graph produces false red builds that are not product evidence — this happened when apps/web/node_modules/next stayed symlinked to a next@16.2.7 store path while the merged pnpm-lock.yaml required 16.2.9, “failing” /_global-error and jamming an unrelated branch. Rules: (1) before any sandbox build/test gate, run node scripts/sandbox-freshness-preflight.mjs --converge (the local-integration plan does this automatically) — it asserts branch/SHA, lockfile-vs-installed-graph agreement, and that the workspace links for next/react/react-dom/typescript/prisma resolve to the locked versions, and refuses to start a second install in a sandbox that already has one running — only installs whose working directory is the sandbox (or inside it) count, an install whose working directory cannot be read counts conservatively, and a same-sandbox install is waited for (bounded by DPF_LOCAL_CI_FRESHNESS_INSTALL_WAIT_MS, default 10 minutes; poll DPF_LOCAL_CI_FRESHNESS_INSTALL_POLL_MS) before the sandbox is re-evaluated, so only a timeout reports not-ready (BI-8DC6F267); (2) a red or unproven sandbox classifies the gate as blocked_sandbox_drift — a sandbox defect, never a product build failure; the gate runner records it that way and exits 3 (drift) / 4 (not ready), reserved codes distinct from a real build failure’s exit 1; (3) repair goes through one mutex-guarded convergence sequence — relink, forced relink, scratch node_modules reset, and finally a scratch-local pnpm store when the shared store keeps serving stale package content — never a second racing install, never a manual per-worktree install; (4) build evidence records must carry branch/SHA, lease id, freshness verdict, and the actually-resolved next/react/react-dom versions — the gate runner attaches these automatically. Do not install dependencies in topic worktrees to work around a stale sandbox — that recreates the drift one layer down (worktrees are source-control isolation, §5 above).
Live portal refresh rule. Normal feature verification and release validation MUST NOT rebuild the main dpf portal directly with docker compose build, docker compose up, scripts/redeploy-portal.*, or equivalent host-side image swaps. The main portal update path is /ops/self-upgrade (or the same governed self-upgrade runner/promoter path), which owns quiescence, recovery point creation, image rebuild/swap, health evidence, and rollback. Direct compose rebuilds of the main dpf portal/portal-init services are reserved for install/bootstrap/recovery work and must be named as such in the report; they do not count as the normal PR/release verification path. Direct redeploy helpers must refuse to run while any SelfUpgradeRun is queued, pending, running, or completing; bypass that guard only with DPF_ALLOW_REDEPLOY_DURING_SELF_UPGRADE=1 for an intentional recovery override. For unmerged feature work, use source-local gates plus the shared local-CI convergence sandbox or contributor preview instead of mutating the live portal.
Agents request the advance; they do not press the button (BI-2128872C). An agent advances the live install with the request_self_upgrade MCP tool. Outside the maintenance window, or during an operator blackout, that request is deferred: nothing drains now, the answer is deferred_to_window with the runAt time, and the scheduled upgrade picks it up in the next window. Plan live validation for after that time. Never press Upgrade now on /ops/self-upgrade in a browser, whether signed in as the operator or as the automation persona. That button is the operator’s immediate override; from the automation persona it is deferred like the tool, and from the operator’s own session the portal cannot tell an agent from the operator. An urgent or security release is the operator’s call.
Step zero for live-install functional verification. Before driving any feature’s happy path on the live install (gate #3 above for a runtime-bound change), run the preflight — pnpm verify:preflight [-- --feature-sha <sha>] [-- --portal-url <url>] — and follow its verdict via the dpf-verify-on-live-install skill. The preflight returns one deterministic verdict (CAN-TEST / MUST-ADVANCE / BLOCKED) and one next action, replacing the hand-rolled git rev-parse + curl /api/platform/version skew check. The BLOCKED stop-rule is binding: when the governed advance is blocked by an unrelated defect (e.g. main does not build in Docker), file a BI for that blocker and STOP the verification task — do not let it silently become a multi-hour build-infra fix. Single source of truth: docs/superpowers/specs/2026-06-06-procedural-functional-verification-design.md.
Waiting for a queued gate: resume its durable identity. A queue admission is pending evidence. Exit 75 hands the same lease, candidate SHA, worktree and owner session to the canonical detached resumer when available. Observe the recorded task and pregate:status; do not loop over fresh pregate claims. If the resumer could not start, retain the recorded resume identity and report recovery unavailable instead of abandoning the request or making a new claim. Terminal claims stop. A new attempt follows reconciliation and has its own explicit identity. The current wrapper exits 7 for abandoned admission or an uncorroborated zero runner exit; only a SHA-bound PASS is a pass. A killed test child or missing failure receipt is inconclusive: read the slot log and stage receipt before calling it a product failure.
Local pre-commit gates. Pre-commit hook at .githooks/pre-commit runs a staged Gitleaks secret scan before bytes enter Git history, then runs pnpm --filter <affected> typecheck on .ts/.tsx/.mts/.cts commits and rejects on failure. For apps/web that script checks the production program (tsconfig.json, which excludes *.test.ts(x), e2e/, tests/, test-support/ and the vitest setup files); the test files are their own program, tsconfig.test.json, checked by pnpm --filter web typecheck:tests and by the required Typecheck (web tests) CI job. Staging a web test file runs typecheck:all (both programs), and the local-CI gate always runs typecheck:all — a gate that checked only the production program let three test-only type errors through to CI on PR #5285. Splitting them keeps the gate every push pays at the size of the shipped code, not the shipped code plus 3,300 test files (plan 2026-09-08 M11). Set once: git config core.hooksPath .githooks (auto for new clones via postinstall). Manual scans: pnpm security:secrets:staged for staged content and pnpm security:secrets for the current tree. Emergency bypasses exist for verified false positives only: DPF_SKIP_SECRET_SCAN=1 and DPF_SKIP_TYPECHECK=1; CI still gates the PR.
Local-CI pre-push gate (default-on). pnpm run pregate authenticates to MCP with a client_credentials client or, until retirement, the DPF_MCP_BEARER_TOKEN PAT (credential resolution: MCP tool authorization runbook), leases the shared local-CI sandbox, runs the CI-equivalent runner, and records a SHA-bound evidence record with an expiry; the pre-push hook then REFUSES a push of runtime code without an unexpired passing record for the current branch+SHA. Docs-only diffs (base DPF_PREPUSH_BASE_REF, default origin/main), delete/tag pushes, and main pass without a record. A supported bypass is recorded, never silent: after the operator explicitly authorizes the exact run, use DPF_SKIP_PREPUSH_GATE=1 DPF_SKIP_PREPUSH_GATE_REASON="operator-emergency: <gate, reason, and scope>" git push. The receipt remains skipped: true / gatePassed: false; it never converts an unrun or broken gate into a pass. DCO and the cheap deterministic preflight still refuse unsafe changes, and a PR lacking a passing record for its head SHA needs the matching Local-CI-Override: attestation in the PR body (pnpm pr:health surfaces it). If the failing gate exposes no checked-in audited override, AGENTS.md’s refusal rule still requires a stop. Pre-PR verification is the sandbox lease — “test on :3000” is only valid AFTER PR/merge/self-upgrade (§13). Full mechanics, the Unit Tests job, and test-runner pins (mobile jest ^29.x, web/db vitest): docs/testing/pre-pr-gate.md.
A running gate keeps its lease, and its verdict, through a transient refusal (2026-09-24). The lease supervisor used to treat every refusal of a renewal as final, so a portal drain (portal_quiescing) or a tool that threw fenced a healthy run mid-build and its verdict was lost. Now only a refusal that proves the lease is gone is final; a quiescing portal, a thrown tool, a retryable refusal or an unreadable reply is retried like an unreachable service, inside the same expiry budget. Renewal is allowed during quiescence (release already was). A release that fails at the end is logged as release-failed and no longer takes the verdict with it. The process-tree tracker records each descendant’s command line and will not kill a PID that Windows has since handed to another process (descendant-pid-reused). The server capacity broker reads each host probe on its own: one failed probe keeps the pool closed but names itself (host-probe-unreadable:<probe>) instead of blanking every number as host-cpu-unmeasurable, and a failed sample is read once more, 750 ms later, before a claim is refused on it.
A resumed gate runs the commit it queued, and a cancelled wait stays cancelled (BI-D35B85BF). The detached resumer pins the branch, commit, worktree, owner and lease it resumes, and runs every attempt from that worktree. If the worktree has moved on (new commit, moved branch, uncommitted change), the resumed gate stops with exit 82 and claims nothing; it never falls back to the legacy claim key. If the wait it resumes was cancelled, it stops with exit 81 and makes no replacement claim. Neither is a verdict: both are recorded as unrun, and gate:wait does not retry them. When main moves and the same candidate re-claims under a new integration identity, the server retires the same owner’s obsolete queued wait (phase: superseded) before queuing the new one. It never cancels a running lease or another owner’s wait; those claims are refused with nonprod_resume_lease_active or nonprod_resume_not_owner.
pnpm gate:wait encodes the invocation contract below — prefer it to a hand-written loop (BI-22E11EA2). The rules in this section are real and hard-won, and they were being re-derived in bash once per session and got wrong: one measured session wrote four retry loops, two of which misclassified infrastructure as a failing diff. gate:wait reads pregate:status FIRST (so a recorded PASS is never re-gated into a superseding run), invokes pregate foreground and unpiped only when no verdict exists, watches the status after a queued exit instead of racing the detached resumer, re-claims only when the watch window elapses with no verdict, and exits 7 — not 0, not 1 — when a deadline passes without any verdict. Classification lives in scripts/lib/gate-exit-classification.mjs, which is the single place that says what each pregate exit code means.
How to invoke pregate, and how to read a red one (BI-E6EF0B2C session, 2026-08-27/28). The gate’s policy is above; this is its invocation contract, which existed only in the tool’s own stderr and cost one session six wasted runs on a fourteen-line change.
- Run it foreground, unpiped, as the SOLE command. A captured or wrapped
pnpm run pregate— stdout is not a terminal, andDPF_PREGATE_ALLOW_CAPTUREis unset — stops immediately with exit 11 and says it is not the sole foreground command. It does not occupy the pool (BI-4BCD269B).pnpm gate:waitsetsDPF_PREGATE_ALLOW_CAPTURE=1and is the sanctioned wrapper;CI=trueandGITHUB_ACTIONS=trueare too.--helpand--dry-runstill run when their output is captured. On Windows,gate:waitlaunchespnpm.cmdrather thanpnpm, because the bare name does not spawn and used to be reported asgate failed (exit null)(BI-2FFDBE62). Chaining (git rebase && pnpm run pregate) still lets a harness kill the run at timeout instead of backgrounding it. - Never interrupt it. A killed run writes a canonical FAIL evidence record with no recorded reason, and every subsequent run replays that cached fail — the same tree can never pass again.
pregate:statussays so explicitly (“gate record status failed with NO recorded reason for THIS run … this is not evidence that the diff is bad”). When the gate log did name a failing test or check, status quotes that line fromfailureSummaryinstead of saying no reason was recorded (BI-C1121FFB). The only escape from a genuine cached FAIL is a fresh SHA, and a plain rebase is not always enough: the gate key is content-derived, so a rebase that preserves the tree replays the same evidence. A real content change is required.prisma migrate deployfailing with P1001 is not that cache: it is recorded as control-plane starvation, without product evidence (BI-B62A6686). pregate:statusis the verdict, not the console text. The command’s own tail says this; believe it.- Exit 5 /
blocked_control_plane_starvationis infrastructure, not your diff. The gate labels it that way itself. It fires when the portal, MCP, Docker or Postgres degrades mid-build — most often because the install self-upgraded underneath the run, which drains the control plane. Re-run once the stack is settled; do not “fix” the diff. Exit 10 / statusconflictis your branch, not infrastructure (BI-FCC1DCB2, BI-45553400). The runner’s first step merges the candidate ontoorigin/main. When that merge stops on conflicts, the runner names the unmerged paths, aborts the merge and exits 10. The same exit is written before admission whengit merge-treereports a conflict, instead of labelling the resumegate-identity-unavailable(BI-1F11D17A). The gate recordsconflict, andpregate:statusreports FAIL withnext merge origin/main into your branch and resolve: <paths>. Re-running cannot change it. Merge main, resolve, commit, and gate the new SHA. Before this, the stopped run was rewritten toblocked_control_plane_starvationor source drift, and authors retried it for hours. - A preflight where every guard fails at once is the clone, not the change. On a shallow clone whose
origin/mainhas advanced past the shallow boundary,git diff origin/main...HEADreturnsfatal: no merge baseand each guard fails on the same missing diff. Fix withgit fetch --deepen=300. - A red suite of filesystem-walking or DB-touching tests, all timing out, is host load. Timeouts and mass-failures are load; assertion failures are code. One session saw 27 failed files / 1 failed test under memory pressure, and 165 files / 2086 tests green on a quiet re-run.
Two mandatory pre-push checks the sandbox record does not cover (stuck-PR sweep, PRs #4172/#4173/#4189). The sandbox gate above verifies a runtime record, but two author-fixable failure classes were shipping red because they were only caught in CI, after the push, once the agent had moved on. Both are now enforced host-native in .githooks/pre-push-gate, need no sandbox, and cost seconds:
- DCO sign-off is checked before every topic-branch push.
scripts/pre-push-dco-check.mjs(pnpm dco:check) scansorigin/main..HEAD— MERGE commits included — with the same predicatepnpm pr:readyuses (scripts/lib/dco-signoff.mjsisSignedOff), and refuses the push if any commit lacks aSigned-off-by:trailer. This runs before the docs-only exemption and the recorded-override path, because an unsigned commit (classically a corrupted or auto-generated merge — PR #4189) is unsigned regardless of what it changed. Whenorigin/maincannot be resolved on the host it WARNS (exit 2) rather than blocking — an offline fork is not hard-failed and CI’s DCO app still enforces sign-off. - A recorded sandbox-gate skip still owes the cheap guard-parity preflight. When a push takes the
DPF_SKIP_PREPUSH_GATE=1override path (the standard source-only agent case viaexternal-contribution-no-install), the gate now runsscripts/pregate-preflight.mjsbefore allowing the push. The preflight runs the same deterministic CI policy guards CI runs — includingux-fit-gate— so a stale UX-Fit manifest (PR #4173), a prose ratchet, or a module-size breach is caught locally instead of turning the PR red. It self-degrades: a guard the host cannot run WARNS, only a genuine deterministic violation blocks. Rare escape hatch:DPF_SKIP_PREGATE_PREFLIGHT_REASON="why"alongside the override.
A cloud agent session records its push instead of being refused. A claude.ai/code container (CLAUDE_CODE_REMOTE=true) has no local DPF install and cannot reach the local-CI lease, so pregate can never run there. When such a push has no passing record and is not docs-only, the hook records it through the same override path under external-contribution-no-install (reason CLOUD_AGENT_SESSION_OVERRIDE_REASON in packages/dpf-skill-pack/hooks/lib/local-ci-override.mjs), and the PreToolUse pregate-evidence-guard allows the publish on the same basis. It is never a pass: DCO and the guard-parity preflight above still apply, pnpm pr:health reports it as an override, and cloud CI plus the merge queue remain the gate. A genuine PASS and the docs-only exemption still take precedence.
Stage 0 — worktree compile-readiness. Before the guards run, pregate-preflight.mjs enforces readiness: a source-only worktree with a code change is auto-healed via the managed bootstrap (scripts/lib/bootstrap-worktree-deps.mjs) or, if the heal fails, blocked with the exact missing artifacts — so a thread never runs its guards on an unprovisioned tree and misreads Cannot find package 'react' / a missing Prisma client as its own regression. Docs-only diffs stay source-only (no install, so the ~1k–10k worktree fleet pays no blanket cost). Opt-out: DPF_SKIP_COMPILE_READY_GATE="why". Stage 0 of the Resilient Concurrent Development Process.
The auto-heal used to no-op on a partially-provisioned tree. bootstrapWorktreeDeps gated its install on the bare existence of node_modules, so a worktree seeded with a partial one — the observed shape was 71 entries, no .pnpm, no .bin, against 1133 in a healthy sibling — skipped the install permanently. Re-running the documented remedy was a no-op and there was no force flag, so the tree stayed source-only, pregate refused it a lease (“guard parity preflight failed … no lease was claimed”), and it could never run the mandatory gate at all. Which trees were affected was invisible until someone tried to push. The install is now gated on measured readiness (probeWorktreeReadiness), and node scripts/lib/bootstrap-worktree-deps.mjs --force . reinstalls even when the probe already reports compile-ready. The fail-safe contract is unchanged: any failure still returns source-only and the CLI still exits 0, so a bootstrap failure can never break a seed script.
The auto-heal also used to abort on a stale node_modules from another install. A reused worktree whose node_modules came from an ad-hoc install against a different store held only a handful of packages. pnpm treats that directory as incompatible and wants to purge it, and the purge asks for confirmation. Without a TTY, the install aborts with ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY, so the bootstrap reported managed_install_failed and the tree stayed source-only. The managed install now passes --config.confirm-modules-purge=false when node_modules is the worktree’s own directory or is absent (BI-7DD16424). When node_modules is a symlink or junction, the confirmation stays on: a purge through the link would empty the tree it points at. In that case, investigate the link; never rm -rf it.
A build decision counts wherever it is recorded. Readiness also fails closed on an unclassified build script, and DPF records those decisions in the top-level allowBuilds: block of pnpm-workspace.yaml — the canonical policy home, where every entry must carry an explicit boolean (enforced by scripts/check-build-script-policy.mjs). pnpm reports a package denied there under “Automatically ignored builds during installation”, because it is simply absent from the allowlist; only a package in package.json’s pnpm.ignoredBuiltDependencies reaches the “Explicitly ignored” section. To a stdout-only parser those two states are indistinguishable, so puppeteer, protobufjs and unrs-resolver read as undecided despite each carrying a deliberate false, and every worktree created off main sat source-only and unpushable with no way to converge (BI-6945BEEF). classifyIgnoredBuilds now takes the policy-denied set from policyDeniedBuilds(), so a recorded decision counts. It stays strict where it matters: an unreadable or malformed workspace file yields an empty set rather than passing everything, and a package with no recorded decision is still flagged even when its siblings are decided. Do not “fix” this class by adding pnpm.onlyBuiltDependencies to package.json — that flips pnpm into allowlist mode and silently denies esbuild, msw, prisma and @prisma/engines, which breaks Prisma.
The hook asks for the verdict; it does not look for the file. pregate writes slot-scoped gate records (dpf-local-ci-gate-slot-N.json). The pre-push hook used to stat an unslotted dpf-local-ci-gate.json — which is written by exactly one path, the override branch, as {gatePassed:false, skipped:true}. So the file it consulted for a PASS could never contain one: a genuine, evidence-backed pregate PASS was invisible to the hook, pnpm run pregate:status and git push disagreed about the same fact, and the only way past a mandatory gate was the override — which is how a gate decays into a formality (BI-A167CF8A). The hook now delegates to scripts/pregate-status.mjs, the canonical slot-aware reader, which already applies the branch/sha, evidence-pending and expiry checks and exits 0 only on a PASS for the current HEAD. The unslotted record remains the override receipt, never a pass channel. scripts/pre-push-gate-slot-contract.test.mjs holds writer and reader together so they cannot drift apart again. The two other readers of that record — pnpm pr:health and the skill-pack PreToolUse hook pregate-evidence-guard.mjs, which fires on git push / gh pr create — consult every slot too (BI-5529B5AC): pr:health selects across slots the way the reader does, and the hook runs scripts/pregate-status.mjs --json when it is present and falls back to every dpf-local-ci-gate*.json beside the slot-0 path when it is not (the plugin ships to installs with no scripts/ tree). A PASS also retires the losers: once a slot passes a branch+SHA, any sibling slot record for the same branch+SHA that is not a pass is rewritten as status: superseded naming the winner, so no reader that opens one file can mistake the earlier queued, blocked or overridden attempt for a verdict. pregate:status re-reads the winner a superseded record names. If that record no longer holds a pass for the same branch and SHA (a late lease loss can rewrite it after it superseded its sibling), the reason names the winner’s actual status and says no slot holds a pass, so a superseded record can never manufacture one (BI-A9031FF3).
A push is not “done”. Every gate path that lets a push through now prints an explicit reminder that a PR is complete only when its required checks are GREEN — verify with pnpm pr:health <PR#> after CI starts and do not leave a red PR unattended (the abandonment behind PR #4172). pnpm pr:health remains the authoritative green-checks completion verdict.
Docs-PR field notes (salvaged from agent sessions — cross-surface). A docs-only change still trips real guards; these recur:
- The override reason is allowlisted, not free-form.
DPF_SKIP_PREPUSH_GATE_REASONMUST start with one ofdocs-adjacent/delete-or-tag-only/operator-emergency/external-contribution-no-install/install-bootstrap-recovery/gate-infrastructure-unavailable; any other prefix is refused. The last one is evidence-gated, not attestation-gated (BI-02E5F2A1, DI-FC5699629694): the hook accepts it only when it can itself capture machine evidence that the gate’s infrastructure failed. It re-attempts the gate’s ownclaim_nonprod_environment_leasecall (scripts/pre-push-gate-infrastructure-probe.mjs) and embeds the failure response — HTTP 401unauthorized: invalid or expired token, connection refused, a 5xx — asinfrastructureEvidencein the gate record; prose in the reason is never evidence. If the claim succeeds (admitted, queued or reused) the probe releases what it claimed and refuses the code, because the gate can run. The record isstatus: "gate-unrun"/gatePassed: false;pnpm pr:healthand the PreToolUse guard report the push as gate-unrun, never passed and reject the code when the evidence is missing, and aLocal-CI-Override:trailer carrying it is always a blocker (a trailer is prose). Per push; nothing persists beyond the record. Cloud CI stays the safety net. - Regenerate derived doc artifacts after editing docs, or the
Docs Link IntegrityandDerived Artifact Registryguards fail deterministically in preflight:node scripts/gen-doc-index.mjs(index/anchors) andnode scripts/render-doc-diagrams.mjs(mermaid SVGs + manifest, rendered through the pinnedminlag/mermaid-clitool image via Docker or a localMMDC=) when a doc’s diagram source changed. The pre-commit hook regenerates on commit, but preflight fails before the commit. - Editing a doc makes the
Doc Anchor Existenceguard validate every backlog anchor in that file — so a pre-existing deadBI-*/EP-*citation you didn’t add surfaces as a new failure. Fix or retarget the anchor (or drop the file from the change), don’t absorb it silently. docs/superpowers/is frozen history — the Doc Reference Integrity gate excludes it as a source. Do not rewrite dated specs/plans to reflect a later rename; harmonize live/canonical surfaces only.- The managed bootstrap can perturb
packages/db/package.json+pnpm-lock.yaml(observed: an@prisma/adapter-pgpatch downgrade). Strip those from a docs-only PR before committing — they are not your change. - Preflight and the trailer gates read the COMMITTED diff + commit/PR-body trailers, not the working tree — a clean pre-commit
pregate-preflight.mjsrun does not prove the committed state passes; the design-grounding / docs-impact / seed-fit gates readPR_BODY. The fast loop ispnpm gate:local(BI-85270E96): it runs the deterministic diff-scoped gates (ux-fit, docs-impact, design-grounding, spec-plan-doc, data-impact, convergence, seed-fit, clock-bombs, module-size, one-action-result, prose-lint) withDPF_GATE_INCLUDE_WORKING_TREE=1, so staged, unstaged and untracked files count BEFORE the first commit, and--message-file <path>feeds the planned commit message asPR_BODYso the trailer gates judge the text the commit will carry.--committedrestores CI’s view;--only a,bnarrows the list. Exit 1 is a failing gate; exit 7 means a gate could not run here and CI still owns it. Run it once with the message drafted, commit once, push once. The pre-commit typecheck gate is content-hash cached under.git/dpf-typecheck-okfor the same reason: an amend that changes nothing TypeScript sees no longer paystypecheck:allagain. - A fake cuid or other high-entropy token in a test trips gitleaks
generic-api-key. Use an obviously-fake hyphenated placeholder (e.g.user-cuid-placeholder), not a realistic-looking id. - A rerun replays the original webhook payload. Editing PR body/labels does not update a running workflow’s event data. Diagnose the named failure with
pnpm pr:health <n>and reproduce its affected guard with the corrected metadata. If the workflow requires a new synchronize event, push a signed correction carrying the required trailer; use--force-with-leaseonly when intentionally amending published history. A metadata-only correction does not automatically require another full local integration gate. Recheck auto-merge after any force-push; re-arm it when necessary. A queued PR is waiting, not a failed build.
Published corrections and host guard reuse
For an unchanged published PR, inspect health/status and preserve valid executed evidence; a queue delay or failed GitHub job is not a command to rerun the whole local gate. For changed source or a main refresh, run affected source tests and reproduce the named failure, obtain independent review of changed code, and leave final main+PR integration to required GitHub checks and the protected merge queue. Never relabel an earlier full local PASS as covering a changed tree.
The pre-push hook and readiness command automatically verify eligible published
corrections under BI-25CB699C. They require an initial qualified publication
anchor, a current open PR, and actual required checks and merge-queue protection.
Changed bytes execute the existing affected-test plan; unchanged bytes execute
no source suite. Fresh metadata guards still run. Failed, skipped, stale or
unbound proof refuses publication without restarting full local integration.
The emitted published-correction attestation records full local integration
as unrun/delegated, never as a canonical PASS. Initial publication retains
its existing gate. See PR health
for commands, proof identity and freshness requirements. Scoped operator
recovery remains a separate audited route when its conditions apply.
pregate:preflight and pr:ready reuse wholly successful local guard results
for at most one hour when the clean worktree, HEAD/tree, main, command plan,
toolchain and environment still match. Readiness also binds PR body and labels.
Identity is re-read before returning a cache hit and after execution. Dirty
files, skips, failures, expired receipts and missing identity never yield a
cached PASS. Commit-message changes invalidate host guard results because some
guards read those messages. Reuse prints the original execution time and does
not create canonical integration evidence. These per-worktree receipts extend
the existing stage writer and owner-fenced lock; stale owners use its existing
reconciliation, while live/unmeasurable owners are not displaced.
- “Sandbox drift” from
pnpm install already runningmeans an install in the gate’s own sandbox outlived the wait, not a broken environment. Since BI-8DC6F267 an install in another worktree no longer counts;sandbox-freshness-preflightwaits up to 10 minutes for an install running in the sandbox itself (or one whose working directory it cannot read), and only then reportsSANDBOX_NOT_READY: pnpm install already running (pid N, …), whichgate-worktreereports asBLOCKED (sandbox drift) … This is a sandbox defect, NOT product build evidence. That wording invites a hunt for an environment fault; check the named pid first (ps -p N -o command=,lsof -a -p N -d cwd) — a hung install in the sandbox is the usual cause. The aborted attempt leaves a released lease bound to the gate key, so the immediate retry fails differently, asgate_evidence_blocked/mismatched-evidence. Recover withnode scripts/local-ci-slot-cleanup.mjs --slot-key <slot>— the lease payload names the exactcleanupCommand— then re-run. Serialize installs against the gate; never gate two branches at once (the gate holds one slot anyway). - Run
pnpm run pregatein the FOREGROUND, UNPIPED, as the sole command. Backgrounding it with output redirected produced the record statusblocked_child_signal_death with NO recorded reason, whose own text says it is not evidence the diff is bad. The exit code lied; the record did not.pnpm run pregate:statusis the verdict — read it before treating any gate failure as a finding about the change.
Build Studio mirrors this gate. Per-task and pre-ship verification in the sandbox runs typecheck + production build. A Build-Studio-produced PR cannot fail CI typecheck — if it would, it never leaves the sandbox. Implementation status: landed ⟦runtime: dated snapshot, not doctrine — re-audited 2026-06-19; re-confirm against the cited files⟧ — the sandbox shells npx tsc --noEmit and npx vitest run (apps/web/lib/build/coding-agent.ts), the specialist/review prompts require NODE_ENV=production pnpm --filter web build before ship (apps/web/lib/build/build-agent-prompts.ts), and the orchestrator scopes the verdict to the build’s own diff and gates the build→review transition on it (apps/web/lib/build/build-orchestrator.ts, apps/web/lib/queue/functions/build-review-verification.ts). Agents run scoped tests only (BI-F5D4FA50). Verification runs the test files for the build’s changed files; the whole suite runs in cloud CI and the merge queue. An agent’s own unscoped run (vitest run, pnpm test or jest with no test files) is refused, by run_sandbox_command and, for CLI agents that use their own shell, by the runner configs when the sandbox env sets DPF_SANDBOX_TEST_SCOPE=enforce. Each decision is recorded. The sandbox service is CPU-bounded and caps vitest and jest workers, so one build cannot close the local-CI pool for host-cpu-high (apps/web/lib/build/sandbox/test-scope-guard.ts; design: docs/superpowers/specs/2026-10-08-sandbox-scoped-agent-tests-design.md).
Review never repeats a verdict it already has (BI-FBA2FDBE). The review job’s finalize stage records each guard-gauntlet run under its gate identity: repository, tree, guard plan and toolchain. When the same identity already has a recorded failure, finalize reuses it instead of re-running the gauntlet. A guard that printed “the guard did not run” makes the run inconclusive (gauntlet-not-run); it is never a failing verdict and is never reused. The semantic change review runs only after a finished finalize, because it needs the failure analysis that finalize writes. Any other finalize outcome stops the job, and that outcome is on the build’s trail (apps/web/lib/build/finalize-stage-runner.ts).
A build’s own guard failures go back to its coding agent (BI-B2EEA6DE). When finalize ends gauntlet-failed, the review job hands the build back from review to build. It records verificationOut.gauntletRepair (tree, evidence record, failing guards, attempt) and queues build/gauntlet.repair. That job dispatches one repair task through the orchestrator’s specialist dispatch: the brief names each failing guard, carries the guard output clipped to its tail, and forbids weakening, skipping or editing any guard or its baseline. The build then returns to review, and finalize re-evaluates the new tree. A build gets at most two hand-backs; after that it escalates to the operator once (escalateBuildToHuman), and nothing more is dispatched (apps/web/lib/build/gauntlet-repair.ts). A semantic change review whose next action is “repair” takes the same path (BI-50E8802C): its blocking findings (critical and important, with location and suggestion) become the brief, and guard and review hand-backs share the two-attempt bound. An inconclusive review is retried, not repaired.
Two failures only CI used to see now have local guards (BI-25EF1456). A "use client" component that reaches @dpf/db, server-only or a Node built-in through a lib import passes vitest and tsc and fails only next build (“Module not found: fs”); scripts/check-no-server-imports-in-client.mjs (in the guard loop, pnpm check:guards) walks value imports from every affected client module and prints the exact chain in under a second — the fix is the *-vocabulary.ts split. And a route over its field budget is measured only by the CI sweep; pnpm ux:sweep-touched -- --run --base-url <served portal> maps the changed files to the page routes that render them (route manifest plus the same import walk, ancestor layouts included) and runs the existing ux:sweep ratchet on exactly those routes. The default feedback run covers static routes with a 40-route cap. Shared-layout acceptance uses the complete touched inventory: --exhaustive --fixture-map <admitted-preview-map> includes dynamic routes and requires concrete fixtures, source/lease binding and complete accounting. Desktop/mobile and normal/Simple contexts are selected explicitly; missing fixtures never lower the denominator.
Auto-merge armed is not auto-merge happening (BI-53BE1C77)
Arming auto-merge (gh pr merge <n> --squash --auto) is a promise that fires only when every check is green. When a check stays red, GitHub sends no signal that the promise will never be kept, and the merge-queue churn watch only sees PRs that reached the queue — a PR blocked before the queue never does. PR #5333 sat six days like this, BLOCKED with auto-merge armed, and nobody was told.
.github/workflows/stuck-auto-merge-alarm.yml runs hourly and posts one comment per stuck head SHA on any open, non-draft PR that has had auto-merge armed while BLOCKED / DIRTY / BEHIND (or with a failing check) for at least four hours, naming the failing checks. The clock restarts on a push, on re-arming, or on a check completing, so a fresh push is never flagged early; a PR with a check still running is a transient and is never flagged. Pure logic: scripts/check-stuck-auto-merge.mjs (findStuckPullRequests, shouldComment). Run it locally with node scripts/check-stuck-auto-merge.mjs --dry-run.
The alarm is GitHub-side only. The in-portal Needs-you inbox does not yet carry it: the contributor PR snapshot (apps/web/lib/contributor-change-lanes/) is read from the /pulls list endpoint, which returns neither mergeable_state nor the checks, and carries no auto-merge field — so an attention source over it would have nothing to read. That extension is a follow-up, not a half-built source.
The room follows its PR through CI (BI-88341B5D)
Getting a PR merged is the Workroom’s duty, not the client’s: the same thing happens whether Claude Code, Codex, Grok, Antigravity or Build Studio opened it. Design: docs/superpowers/specs/2026-10-01-pr-follow-through-workroom-duty-design.md. The five-minute reconciler (apps/web/lib/queue/functions/build-pr-delivery-reconcile.ts) now covers every non-terminal room with a bound PR, not only Build Studio rooms, and keeps one record per room at workspaceState.prDelivery (prDelivery.followThrough holds the CI classification, the hold and the reused post-push-ci-failure budget). On each pass it:
- arms auto-merge through the merge queue when the PR is green, and updates a branch that is
BEHIND. It never merges by hand, force-pushes, dismisses a check or touches required-check configuration; - classifies a red check once per head SHA (
apps/web/lib/build/pr-follow-through.ts).CANCELLED/TIMED_OUT/STARTUP_FAILURE/STALE/ERRORare infrastructure: it re-runs that workflow run’s failed jobs once and then raises the platform operator.FAILUREis a defect: at apreauthorizedboundary the room may dispatch a bounded repair (2 per PR); below that, or while no repair worker is wired, it stages the repair packet for a person. Anything else (ACTION_REQUIRED) goes to a person unclassified; - records each step on the room as a
workroom-pr-follow-throughactivity, and sends a hold that needs a person to the escalation inbox once per head; - makes liveness read CI: a room whose PR is red and waiting on a person reads
stalled(held, never reaped), notlive.
A quiet room, or one whose boundary is advise, is recorded only. The reconciler still runs in shadow by default (DPF_BUILD_PR_DELIVERY_RECONCILER_MODE), so it proposes rather than acts until the default flips (design §7 slice 6). Webhook intake of check_suite / workflow_run / merge_group, the repair worker itself (design §8 open question) and the client accelerator readback are later slices. Until they land, keep arming auto-merge yourself with gh pr merge <n> --squash --auto; the room arming it again is a no-op.
A merged pull request is delivered, not discovered (BI-A6E4D205)
The thread that pushes should be able to end at the push. It could not, because nothing told the platform when a pull request merged — it went looking, on a timer.
POST /api/platform/git/updates has always been a signed GitHub webhook receiver: HMAC-SHA256 against DPF_GIT_WEBHOOK_SECRET, idempotent on x-github-delivery, emitting Inngest events. It handled push and filed everything else as an inert candidate. So pull_request with action: closed, merged: true — the one event that answers “did this ship” — arrived, was verified, and was never acted on.
What filled the gap was build/pr-delivery-reconcile, a REST poll every five minutes, scoped to Workrooms carrying a linked feature build. Agent-authored pull requests were not covered at all. Two things depended on a fact that arrived late or never:
- a worktree becomes Tier-A reapable at the instant its PR merges, and the SessionEnd reaper has already run and correctly declined by then;
- completion needs
mergedThroughGates, so a merged item could sit with recorded evidence and no receipt.
Now the receiver emits build/pr-merged.received carrying repository, number, head branch, head sha, merge commit and merge time. readPullRequestMergedSignal (apps/web/lib/build/pull-request-merged-signal.ts) is the pure decision and is deliberately strict about one thing: a closed-unmerged pull request carries the same action, and treating it as delivery would record evidence for abandoned work and reap the worktree holding the only copy of the branch. A rebase merge legitimately has no merge commit, so that field stays optional rather than making the payload look malformed.
The first subscriber is build/pr-merged-binding. It matches the room on repository and head branch, and records the pull request number, its canonical URL, and the head SHA when the room had none. It never repoints a room bound to a different pull request: two answers is a conflict a person should see, not something a webhook resolves. The inventory sync (ops/contributor-inventory-sync-cron, every ten minutes) applies the same rules from GitHub’s API and remains the backstop for a missed delivery.
A delivery is never lost to a failed send (BI-C26D5DC5). The intake records the candidate as emit-pending, sends its events under deterministic ids (<event>:<delivery key>), then settles the row. If the send fails, the route answers 503, so GitHub marks the delivery failed. A redelivery of that delivery id sends what was owed; one whose events were already sent is a no-op, and Inngest drops a duplicate send by its id.
The five-minute cron stays as a backstop for missed deliveries. That is the same primary/backstop split the worktree janitor documents, applied to delivery.
Operational prerequisite: the webhook must be registered on the repository for the pull_request (and push) events, with content type application/json and the install’s DPF_GIT_WEBHOOK_SECRET as its Secret. Until BI-C26D5DC5 no install path supplied that secret: compose never passed it to the portal, so every production portal refused every delivery with 503. Now the installers generate it into the install .env, and a release self-upgrade adds it when missing (scripts/promote.sh exports the value the recreated portal starts with; scripts/installer/install-release-assets.mjs persists that same value). A value already set is never rotated. Read it from the install .env to configure GitHub. verifyGitHubSignature returns true when no secret is set (local dev); the route itself refuses unsigned traffic in production.
A loopback-bound install (the default, DPF_HOST_BIND_ADDRESS=127.0.0.1) cannot be reached by GitHub. For a development install, forward deliveries with the GitHub CLI’s webhook extension (gh extension install cli/gh-webhook): gh webhook forward --repo <owner/repo> --events pull_request,push --url http://localhost:3000/api/platform/git/updates --secret <DPF_GIT_WEBHOOK_SECRET>. It creates a repository webhook that exists only while the command runs and needs repository admin. Deliveries missed while it is down are recovered by the inventory sync.
Reaping the worktree when the PR merges (BI-848360EF)
build/pr-merged.received now has a second subscriber: build/pr-merged-reap.
The SessionEnd hook remains the primary reaper and it is correct — but it can only reap a Tier-A tree, and Tier A requires merged. The normal sequence is gate, push, open PR, end the thread, and the PR merges in the queue later. At SessionEnd the branch is unmerged, so the hook correctly declines and nothing revisits that worktree. Measured: 2026-09-09 saw 101 worktrees with 48 merged and unreaped; a manual sweep took it to 55 and it was back to 157 by 2026-09-22.
The subscriber does not implement its own rules. It runs scripts/worktree-janitor.mjs --branch <headRefName> --json --tier-a-only, so every existing protection still decides: a live session heartbeat, an active Workroom claim, .worktree-pinned, an active lease, an open PR, a dirty tree. Removal goes through the junction-safe helper, which matters because each worktree carries roughly 28 junctions into the root clone’s node_modules and a recursive delete that follows one is what wiped packages/* on 2026-08-15.
That delegation is deliberate rather than tidy. classifyWorktree places its liveness gate above the merged/Tier-A check, and says why: “the moment a live session’s PR merges, its clean tree first becomes Tier-A eligible — exactly when it must NOT be reaped.” A merge-triggered reaper is precisely the caller that walks into that window, so it must go through the classifier, never around it.
--branch is new on the janitor and narrows the scan without changing a single verdict. An unscoped live run is refused by assertTierAOnly, as is a live run that could reach Tier B — a merge says nothing about stale-but-unmerged work.
Flags, shared with the fleet backstop so there is one switch to reason about:
DPF_WORKTREE_JANITOR_ENABLED— run at all. Default off.DPF_WORKTREE_JANITOR_AUTO_REAP— remove rather than report. Default off, so the event can be soaked before it deletes.
A scan that cannot reach its subject logs UNHEALTHY and returns ran: false. It never reports success, because the fleet backstop already proved how expensive that mistake is: its script was never copied into the image, every run died with MODULE_NOT_FOUND, and the caught error read exactly like a clean sweep (BI-B3370CB2).
Task-specific operating rules
The mandatory build gate requires affected unit tests, production build, applicable UX verification and added migration application. Tier execution to the change; unrun is not passed.
- Cheap source-local checks run in the worktree; runtime-bound gates do not (§1) — never rebuild the live portal by hand;
/ops/self-upgradeowns quiescence, recovery points and rollback. - Tier the gate to the change; the heavy build runs once, in the cloud. Fast local checks (typecheck, lint, affected tests — no Docker) gate the push; the full build is the cloud merge-queue safety net; docs → lint only. → process spec
- Documentation impact is part of done. Decide whether a change affects users, AI coworkers, positioning, install, operations, architecture, routes, prompts or external-agent behavior; update the right docs surface in the same branch, or record a concrete no-docs-needed reason. Do not claim done while docs exposed to users or coworkers are knowingly stale.
- Pre-existing failures: note them and fix if feasible. Do not defer silently.
- Never weaken auth to make a test pass. Use a seeded persona at its real privilege level; if a check blocks you, that is the finding.
- A gate that could not run is not a verdict. Infrastructure failure — a fenced lease, a killed child, a starved host — is recorded as inconclusive and re-runs on the same SHA. Never a FAIL against the diff. Fail closed on safety; fail open on infrastructure. → kernel principle
A refusal carries its own remedy
pregate:preflight used to end a failure with a pointer — “see every constraint
that applies to this diff: pnpm gate:context”. A pointer costs the reader a
decision it usually declines: over one session of nine PRs that line appeared on
every run and gate:context was invoked zero times, so every required
attestation and every stale derived artifact was found by colliding with its
refusal instead.
The refusal now EMITS the obligations: which attestations this diff requires,
which of them belong in the PR body rather than a commit trailer (the Seed
Contribution Fit gate reads the push-event body, so the same text in a trailer
does not satisfy it), and each stale derived artifact with the command that
regenerates it. pnpm land consumes the same data.
The rule behind it: an obligation an agent has to remember does not survive context compaction. Put it in the gate’s output, or in code, and it survives.