# Retrospective checkpoint contract

Canonical owner for the enforcement seam of the post-run behavioral retrospective checkpoint after qualifying async `dev-loop` completions in this repository.

## Fresh-context neutral retrospective (issue #1870)

<!-- rule: RETRO-FRESH-CONTEXT-MANDATORY -->
A qualifying retrospective MUST be produced by a **fresh-context, independent dispatch** — analogous to a gate reviewer — seeded with the cycle's **full agent/subagent tool-call/action/result record** (the existing session transcript/journal artifacts; no new transcript store). The retro evaluates neutrally against the contracts and the issue's acceptance criteria / definition of done / non-goals and produces its finding independently of the implementing agent's self-view.

An **inline, self-authored retrospective** — written by the same working/session context that did the work, from its own self-narrative — is **disallowed and fails the checkpoint**. A self-review reflects the working agent's own blind spots back at it: it validates consistency, not conformance, and cannot see a systematic error it itself committed (e.g. a uniformly-wrong PR template raises no anomaly signal to a self-referential reflection).

The requirement is pinned by the **write mechanism**, not convention: the CLI rejects an inline retro outright and the read side fails closed on every record the CLI could not have produced. The dispatch half (point 1) is agent discipline (`LOCAL-RETRO-FRESH-CONTEXT-DISPATCH`, `enforcement: "agent"`) — provenance is self-attested at write time, so a determined working session could self-author a retro and type `--retro-context fresh` regardless; the durable guarantee is that no inline/legacy record passes the checkpoint, not that the attestation itself is verifiable.

1. the retro pass is dispatched as a fresh-context subagent (no inherited working/session context or self-narrative) with the record path as its primary input;
2. the checkpoint CLI (`checkpoint-contract.mjs --state complete`) requires `--retro-context fresh` (an `inline` value is rejected outright) and `--record-source <path>` naming the record the retro was seeded with; `--record-source` MUST resolve (from the invocation cwd; absolute paths allowed) to an existing, non-empty file, so a retro attested against a record that does not exist is rejected at write time;
3. the durable artifact carries that provenance, and the pure resolver (`resolveCheckpointStateFromArtifact`) treats a `complete` record whose `provenance` does not pin a fresh-context pass over the record as `MISSING` — fail-closed, including every legacy inline retro.

Provenance shape:

```json
{ "context": "fresh", "seededFrom": "agent_tool_call_record", "recordSource": "<path to transcript/journal artifact>" }
```

`skipped` records are not provenance-gated: no retro ran, so there is nothing to have provenance.

<!-- rule: RETRO-ENFORCEMENT-CONFIG-GATED -->
Whether a missing checkpoint blocks the next qualifying start/resume MUST be controlled by `.devloops` at repo root `workflow.requireRetrospective`; shipped defaults remain permissive and this repo opts in.

## Relationship to formal dev mode

Formal local dev mode and the required post-run behavioral retrospective are related but distinct:

| Requirement | Scope |
|---|---|
| **Formal local dev mode** | Local implementation/self-improvement work; explicitly scoped in [Dev Loop Skill](../dev-loop/SKILL.md) |
| **Required post-run behavioral retrospective** | Every qualifying async GitHub-first `dev-loop` completion in this repo |

Routed GitHub-first async `dev-loop` runs do **not** need to be in full formal local dev mode. When `workflow.requireRetrospective` is enabled, they **do** require the retrospective checkpoint before the next qualifying start/resume.

## Qualifying completions

A qualifying async `dev-loop` completion is one that:
- routes through a GitHub-first Copilot-owned strategy gate, and
- has `routeKind === "route"` (inspect/status-only results do not qualify).

Qualifying gates:

| Gate | Strategy | Description |
|---|---|---|
| `copilot_pr_followup` | Copilot PR follow-up | Primary routed GitHub-first async path |
| `issue_intake` | Issue intake | Copilot-first issue assignment path |

`RETROSPECTIVE_QUALIFYING_GATES` in `packages/core/src/loop/retrospective-checkpoint.mjs` enumerates these as descriptive classification only — no runtime consumer consults it. The practical arming trigger is the extension's message-shape match (see `.pi/extensions/dev-loop-behavioral-review.ts` below), which fires without consulting this enumeration, and the recency mechanism below never re-derives "was this a qualifying gate" for a past cycle.

## Checkpoint states

A fresh session determines the status of the required retrospective by reading `.pi/dev-loop-retrospective-checkpoint.json` and, for a `complete`/`skipped` record, checking whether a newer PR has merged into the configured base branch since the recorded discharge point (see "Cycle scoping" below):

| File state | Mapped checkpoint state | Meaning |
|---|---|---|
| File absent (ENOENT) | `RETROSPECTIVE_CHECKPOINT_STATE.NONE` | No requirement has ever been observed on this working copy |
| `{ "state": "none" }` | `RETROSPECTIVE_CHECKPOINT_STATE.NONE` | Explicitly recorded as no requirement |
| `{ "state": "required" }` or `{ "state": "missing" }` | `RETROSPECTIVE_CHECKPOINT_STATE.MISSING` | Retrospective pending; blocks the next qualifying start/resume |
| `{ "state": "complete", "identity": {...}, "provenance": {...} }`, no newer PR has merged into the configured base | `RETROSPECTIVE_CHECKPOINT_STATE.COMPLETE` | Retrospective recorded for the current cycle as a fresh-context pass over the tool-call record; requirement satisfied |
| `{ "state": "complete", "identity": {...} }` with no/invalid/inline `provenance`, no newer PR merged into the configured base | `RETROSPECTIVE_CHECKPOINT_STATE.MISSING` | Inline or unverifiable retro — fails closed (see [RETRO-FRESH-CONTEXT-MANDATORY](#fresh-context-neutral-retrospective-issue-1870)); re-discharge with `--retro-context fresh --record-source <path>` |
| `{ "state": "complete", "identity": {...} }`, a newer PR merged into the configured base (or ancestry/association cannot be verified) | `RETROSPECTIVE_CHECKPOINT_STATE.MISSING` | Stale completion; a newer cycle has not been discharged |
| `{ "state": "complete" }` or `{ "state": "skipped" }` with no `identity` | `RETROSPECTIVE_CHECKPOINT_STATE.MISSING` | Legacy identity-less record — the shape every pre-cycle-scoping checkpoint has. It cannot be verified against any cycle, so it fails closed without running the ancestry check; discharge it by re-recording with the identity flags |
| `{ "state": "skipped", "identity": {...} }`, no newer PR has merged into the configured base | `RETROSPECTIVE_CHECKPOINT_STATE.SKIPPED` | Explicitly skipped with reason for the current cycle; requirement satisfied |
| `{ "state": "skipped", "identity": {...} }`, a newer PR merged into the configured base (or ancestry/association cannot be verified) | `RETROSPECTIVE_CHECKPOINT_STATE.MISSING` | Stale skip; a newer cycle has not been discharged |
| The JSON literal `null`, any other non-object value (e.g. a scalar or array), or an unrecognized `state` string | `RETROSPECTIVE_CHECKPOINT_STATE.MISSING` | Present-but-broken artifact fails closed — never treated as "nothing observed" |

<!-- rule: RETRO-ABSENT-NEVER-BLOCKS -->
An **absent** checkpoint file is the only case that resolves to `NONE`, not `MISSING` — deliberately. The file is gitignored and lives per-working-copy: a fresh clone or a brand-new worktree has never seen a checkpoint at all, and failing closed on absence would block every one of them on first run. This mechanism only re-derives the recency of an EXISTING `complete`/`skipped` record; a derived recency check can never, by itself, ARM the gate (produce a MISSING requirement) where none existed before — only an explicit `required`/`missing` write (see "Durable artifact format" below) or a stale existing record does that.

## Enforcement gate

The enforcement seam is the pure function `evaluateRetrospectiveGate` in `packages/core/src/loop/retrospective-checkpoint.mjs`. The checkpoint artifact may still exist even when enforcement is disabled; callers must first consult `workflow.requireRetrospective` to decide whether the checkpoint should block the next qualifying routed start/resume or remain advisory-only.

For convenience, the public routing helpers in `packages/core/src/loop/public-dev-loop-routing.mjs` also accept an optional `retrospectiveCheckpointState` input and apply the same gate internally before returning routed start/resume/status results. Callers should only pass that input when `workflow.requireRetrospective` is enabled for the active repo/workflow posture.

### Inputs

```js
evaluateRetrospectiveGate({
  checkpointState,  // one of RETROSPECTIVE_CHECKPOINT_STATE
  proposedRouting,  // result from evaluatePublicDevLoopRouting()
})
```

### Outputs

- **Pass-through** (proposed routing returned unchanged) when:
  - `checkpointState` is `none`, `complete`, or `skipped`
  - `proposedRouting` is already `stop`, `needs_reconcile`, or `inspect`
- **Fail-closed** (`needs_reconcile` result) when:
  - `checkpointState` is `missing`
  - `checkpointState` is unrecognized

### Caller contract

Callers have two supported integration options:

#### Option A — direct public-routing helper integration (preferred)

1. Read `.pi/dev-loop-retrospective-checkpoint.json` (if it exists).
2. Map the file contents to a `RETROSPECTIVE_CHECKPOINT_STATE` value.
3. Pass that value as `retrospectiveCheckpointState` to one of:
   - `evaluatePublicDevLoopRouting(...)`
   - `resolveAuthoritativeStartupResumeBundle(...)`
   - `resolveAuthoritativeDevLoopStatus(...)`
4. Use the returned result directly. When enforcement is enabled and the checkpoint is missing, these helpers fail closed to `needs_reconcile`.

#### Option B — explicit manual gate composition

1. Read `.pi/dev-loop-retrospective-checkpoint.json` (if it exists).
2. Map the file contents to a `RETROSPECTIVE_CHECKPOINT_STATE` value.
3. Call `evaluatePublicDevLoopRouting(...)` to get the proposed routing.
4. Call `evaluateRetrospectiveGate({ checkpointState, proposedRouting })`.
5. Use the gate result (not the raw routing result) as the effective routing decision when enforcement is enabled; otherwise keep the raw routing result and treat the checkpoint artifact as advisory context only.

<!-- rule: RETRO-GATE-FAIL-CLOSED -->
If the gate result is `needs_reconcile`, the caller MUST NOT proceed with the proposed routing. The `nextAction` field instructs the operator to complete or explicitly skip the retrospective.

## Advisory findings — never a merge gate (issue #1077, Reading B)

<!-- rule: RETRO-ADVISORY-NEVER-GATE -->
The retrospective is **advisory**: it runs, records flagged raw-call / discipline
observations honestly, and passes them back to the conductor (main agent) to
**decide** what to do with them — but it MUST NOT block a merge or any PR-lifecycle
transition of the current run. The pre-merge retrospective gate (`evaluateRetrospectiveMergeApproval`
and the `requireRetrospectiveGate` / `requireRetrospectiveInternalTooling` config
keys) has been **removed**. There is no `retrospective_gate_pending` / `blocked`
disposition on account of the internal-tooling raw-call record.

### How findings travel (Reading B)

<!-- rule: RETRO-FINDINGS-ENVELOPE-CARRY -->
1. **Deterministic return contract.** The loop subagent's handoff envelope MUST carry
   the retrospective findings as a structured `retrospectiveFindings` field — the
   `check-retro-tooling.mjs` JSON output (`{ internalToolingOnly, rawCallViolations,
   allowedWriteOps }`), not prose. The conductor reads that field. This is a hard
   contract; see [Workflow Handoff Contract](./workflow-handoff-contract.md).
2. **Durability — advisory PR comment.** The conductor posts a single advisory PR
   comment carrying the same findings from step 1. Durable and on-GitHub, but **not a
   gate**. No disk artifact is written for retrospective *findings* — they stay
   distinct from the persisted checkpoint state record (see [After retrospective is
   done](#after-retrospective-is-done-written-by-operator-or-skill)).
3. **No config.** There is nothing to configure: the retrospective always runs and
   always returns findings. `requireRetrospectiveGate` and
   `requireRetrospectiveInternalTooling` no longer exist.

A PR that is otherwise green becomes merge-ready with the violations **recorded**,
not blocked. The conductor may note them, open a follow-up, coach — or ignore.

### Internal-tooling-only rule (issue #982) — now advisory

This rule records the dev-loops maintainers' own dogfooding discipline: the loop's
own execution should use internal dev-loops tooling, not agent-level raw
`gh`/`python`/`node -e` escape hatches. **It no longer blocks.** The flagged calls
are reported as advisory findings via the envelope + PR comment.

**Flagged as raw-call violations:** `gh ...` (including `gh api`, `gh ... --jq`),
`python` / `python3`, `node -e` / `node --eval` (inline eval). **Allowed (NOT
violations):** dev-loops subcommands and `node scripts/*.mjs` invocations — those
scripts legitimately call `gh`/GraphQL internally; that is the tooling. The rule
targets the agent's own top-level shell calls, not a script's internals.

**Write-op allowlist (verifier only):** only `gh pr ready` has
no internal wrapper today; the verifier records it as an `allowedWriteOp` rather
than a violation so the gap is surfaced distinctly, not as a breach. Ops that DO
have a sanctioned wrapper — `gh pr merge` (`scripts/github/merge-pr.mjs`, issue
#1939), `gh issue create` (`scripts/github/create-issue.mjs`),
`gh issue edit` (`scripts/github/edit-issue.mjs`), `gh label create`
(`scripts/github/create-label.mjs`) — are NOT allowlisted, so a raw agent-level
call is flagged as a violation. The verifier only ever classifies the agent's own
top-level shell commands, never a wrapper's internal subprocess, so removing a
wrapped op from the allowlist produces no false positives. Close a remaining gap
with a wrapper to remove its allowlist entry. None of these block anything.

**Inline-interpreter check item:** the raw-call scan below mechanically catches
`node -e`/`python3 -c`/heredoc calls as a `rawCallViolations` entry — the same
class barred by `OPS-NO-INLINE-INTERPRETER` in
[Copilot loop operations](copilot-loop-operations.md). This is an addition to
what the retrospective records, not a new gate: `RETRO-ADVISORY-NEVER-GATE`
semantics are unchanged.

### Deterministic verifier (findings-producer)

`node scripts/loop/check-retro-tooling.mjs [--transcript <path>] [--json]` reads a
newline-delimited transcript of the shell commands the agent ran (one top-level
command per line, via `--transcript` or stdin) and reports agent-level raw
`gh`/`python`/`python3`/`node -e`/`node --eval` calls. It is a **findings-producer**:
its JSON output (`{ ok, internalToolingOnly, rawCallViolations, allowedWriteOps }`)
is returned to the conductor via the envelope's `retrospectiveFindings` field (the
envelope carries the normalized shape `{ internalToolingOnly, rawCallViolations,
allowedWriteOps }` — the redundant `ok` flag is dropped by normalization) — it
is **not** written to a checkpoint and **not** a gate. Exit code `1` when violations
are found, `0` when clean. The pure `analyzeTranscript(transcript)` export returns
`{ violations, allowedWriteOps, internalToolingOnly }`.

Matching rules: a tool name at the start of a command segment (start of line, or
after `&&`/`||`/`|`/`;`); `node` is a violation only with `-e`/`--eval`. Before
classifying, the verifier normalizes the segment head — it strips leading
`NAME=value` env-assignment prefixes (`GH_TOKEN=x gh api`), strips a leading
wrapper binary from `{sudo, env, xargs, time, nice, command}` (`sudo gh api`,
`xargs gh api`), and reduces a path-prefixed binary to its basename
(`./node_modules/.bin/gh`, `/usr/bin/python3`) — so the common prefixed/wrapped
raw-call forms are caught. Known limitation: it does NOT fully parse shell
quoting/substitution. A separator inside a quoted argument can over-report; deeply
obfuscated calls (command substitution `$(...)`, aliases, `eval`) may evade it —
prefer single-line, single-purpose commands in transcripts.

### Lifecycle reconciliation

The retrospective is described consistently as a **post-merge / advisory
reflection**, never a pre-merge blocker. The former contradiction —
`lifecycle-state.mjs` documenting the retro as a post-merge write while
`pr-gate-coordination.mjs` enforced it pre-merge — is resolved by removing the
pre-merge gate: the merge lifecycle step proceeds, and the retrospective is an
advisory reflection whose findings reach the conductor via the envelope.

## Cycle scoping — a checkpoint discharges exactly one qualifying completion

<!-- rule: RETRO-CHECKPOINT-CYCLE-SCOPED -->
`requireRetrospective` is not a one-time gate: a `complete` (or `skipped`) checkpoint MUST be scoped to the exact qualifying completion it discharges, not treated as satisfying every later one forever. The durable artifact carries an `identity` — at minimum `{ repo, prNumber, mergeCommit }` — alongside its `state`.

- **The question is PR-merge recency.** Has a newer PR merged into the configured base branch since the checkpoint's recorded discharge point? Direct commits, release commits, tags, open or closed-unmerged PRs, and PRs merged into another base do not open a retrospective cycle.
- **Derivation, at read time, on every evaluation.** There is no write-time "arming" step. Before inspecting ancestry, the checkpoint identity's `repo` MUST exactly match the current repository auto-detected from the checkout whose base history will be inspected; a foreign or unresolvable repository identity fails closed. `resolveHasNewerMergeSinceCheckpoint` (`scripts/loop/resolve-dev-loop-startup.mjs`) runs a best-effort `git fetch origin <baseBranch>`, uses `git rev-list <mergeCommit>..origin/<baseBranch>` to bound candidate commits, and queries the GraphQL `Commit.associatedPullRequests` connection as merge authority. A candidate qualifies only when GraphQL reports a `MERGED` PR whose merge commit is that candidate and whose base ref equals the configured base. This covers non-default configured bases and one-parent squash merges without the incorrect `git log --merges` filter. Nothing has to remember to write `state: "required"` for the gate to fire correctly.
- **Unverifiable ancestry or association fails closed.** When the checkpoint commit cannot be resolved against `origin/<baseBranch>`, or a required GitHub association lookup fails or returns malformed facts, the check resolves to `MISSING`. The GraphQL authority query MUST validate `pageInfo`, follow cursors, and stop at its fixed page bound; malformed responses, missing cursors, and exhausted pagination all fail closed. An unverifiable discharge claim must not be trusted; the outcome is identical to a confirmed newer PR merge.
- **Completion / skip.** Recording `complete` or `skipped` (via `checkpoint-contract.mjs --state <state> --repo <owner/name> --pr <n> --merge-commit <sha>`, alongside `--notes`/`--reason`) MUST carry the cycle `identity` — the CLI rejects `complete`/`skipped` with no identity at all (previously optional, which could write a record that then failed closed forever with no way to clear it by re-running the same command). A `complete` record additionally MUST carry the fresh-context provenance via `--retro-context fresh --record-source <path>` (the CLI rejects `inline` outright and rejects `complete` with no provenance flags at all). `--merge-commit` MUST be the full merge commit oid (`gh pr view --json mergeCommit --jq .mergeCommit.oid`), not an abbreviated/short sha — the CLI rejects anything that is not exactly 40 hex characters, since a short sha can never match a real commit oid on a later ancestry check and would leave the checkpoint permanently unresolvable (and so permanently stale). `--repo` MUST be `owner/name` shape. `skipped` is scoped exactly like `complete` — an explicit, reasoned escape hatch for one cycle, not a standing exemption.
- **Fail-closed backstop.** The pure resolver (`resolveCheckpointStateFromArtifact` in `packages/core/src/loop/retrospective-checkpoint.mjs`) takes the caller-derived ancestry result as a boolean (`hasNewerMergeSinceCheckpoint`) and treats a `complete`/`skipped` checkpoint as `MISSING`, not `COMPLETE`/`SKIPPED`, whenever it is set. It also treats a present-but-malformed artifact (not a JSON object — including the JSON literal `null`, which is present-but-broken rather than absent — or an unrecognized `state`) as `MISSING`. Only a genuinely absent file (no `.pi/dev-loop-retrospective-checkpoint.json` at all) resolves to `NONE` — see "RETRO-ABSENT-NEVER-BLOCKS" above.
- **Unaffected repos.** A repo with `workflow.requireRetrospective` unset or `false` never performs the ancestry check and never reads or applies the checkpoint file at all — the resolver's entire checkpoint read/injection block is gated on `requireRetrospective` being true, so any checkpoint file that happens to exist is inert. No ancestry fetch or log runs either. (The repo-root path resolution itself still runs one local `git worktree list` on every resolve, config-independent.)

### Checkpoint path resolves from the repo root, not cwd

<!-- rule: RETRO-CHECKPOINT-REPO-ROOT -->
`.pi/dev-loop-retrospective-checkpoint.json` is gitignored and lives **once per repo**, not once per worktree. Both the read path (`resolve-dev-loop-startup.mjs`) and the write path (`checkpoint-contract.mjs`) resolve the checkpoint's directory through `resolveCheckpointRepoRoot(cwd)` — the first line of `git worktree list` (always the main worktree, regardless of which worktree of the same repo `cwd` is inside), reusing the existing `parseMainWorktreePath` parser — rather than a cwd-relative path. This is deliberate: resolving cwd-relative would let a worktree's write be silently discarded the moment that worktree is later removed (e.g. by post-merge cleanup), and would let the main checkout and a worktree of the same repo disagree about the checkpoint state depending on which one last wrote it. `resolveCheckpointRepoRoot` falls back to `cwd` itself, never throwing, only when `git worktree list` cannot be resolved at all (`cwd` is not inside a git repo — the case exercised by tests). `.pi/extensions/dev-loop-behavioral-review.ts`'s best-effort `required`-marker write resolves through a vendored copy of the same logic (the extension bundle runs in a separate runtime, so the logic is duplicated rather than imported).

## Durable artifact format

`resolve-dev-loop-startup.mjs` never writes this file — it only reads it and derives the ancestry comparison live (see "Cycle scoping" above). The file is written by:

- **`.pi/extensions/dev-loop-behavioral-review.ts`** (best-effort, Pi-harness-specific): fires when it observes the standard async `dev-loop` completion message and writes a `required` marker. Its message-based detection does not carry a cycle identity, which is fine — `required` maps to `MISSING` regardless of identity.
- **`scripts/loop/checkpoint-contract.mjs`** (operator/skill-driven): records `complete`/`skipped`/`required`/`missing`/`none`, carrying the cycle identity via `--repo`/`--pr`/`--merge-commit` — MUST for `complete`/`skipped` (see "Cycle scoping" above), optional for `required`/`missing`, rejected for `none`.

### The `required` marker (written by the extension, best-effort)

```json
{
  "state": "required",
  "triggeredAt": "2026-05-29T16:00:00.000Z"
}
```

### After retrospective is done (written by operator or skill)

A minimal completion clears the startup/resume completion gate. The checkpoint
file carries **completion state**, the cycle `identity`, and the **fresh-context
provenance** (issue #1870) — retrospective
*findings* (`behavioralReview`, `rawCallViolations`, `internalToolingOnly`) do not
live on disk; they travel in the handoff envelope's `retrospectiveFindings` field
and an advisory PR comment (issue #1077, Reading B). A `complete` record without
provenance that pins a fresh-context pass over the tool-call record — including
every legacy inline self-authored retro — fails closed to `MISSING`:

```json
{
  "state": "complete",
  "completedAt": "2026-05-29T16:30:00.000Z",
  "notes": "Loop followed working agreement; minor drift on thread resolution.",
  "identity": { "repo": "owner/name", "prNumber": 1613, "mergeCommit": "3f8a1c9d2b7e4a6f0c5d8e1b3a7f2c9d5e8b1a4c" },
  "provenance": { "context": "fresh", "seededFrom": "agent_tool_call_record", "recordSource": "tmp/retrospectives/pr-1613/record.jsonl" }
}
```

### Explicit skip with reason

```json
{
  "state": "skipped",
  "skippedAt": "2026-05-29T16:30:00.000Z",
  "reason": "Trivial documentation-only change; no post-run audit needed.",
  "identity": { "repo": "owner/name", "prNumber": 1613, "mergeCommit": "3f8a1c9d2b7e4a6f0c5d8e1b3a7f2c9d5e8b1a4c" }
}
```

### Explicitly recording "no requirement"

```json
{ "state": "none" }
```

Maps to `RETROSPECTIVE_CHECKPOINT_STATE.NONE` — the same resolution as an absent file, recorded explicitly.

`{ "state": "missing" }` is accepted identically to `{ "state": "required" }` — both map to `RETROSPECTIVE_CHECKPOINT_STATE.MISSING`.

## Authoritative source locations

| Artifact | Location |
|---|---|
| Checkpoint state machine (identity normalization, ancestry-scoped state resolution) | `packages/core/src/loop/retrospective-checkpoint.mjs` (internal core module; not part of the public package exports surface — `normalizeCheckpointCycleIdentity`/`resolveCheckpointStateFromArtifact` are re-exported through `public-dev-loop-routing.mjs` for script-layer callers) |
| Read-time derivation (local git ancestry check, repo-root path resolution) | `scripts/loop/resolve-dev-loop-startup.mjs` (`buildResolveDevLoopStartupResult`, `resolveHasNewerMergeSinceCheckpoint`) |
| Manual write CLI (identity-required for complete/skipped, repo-root path resolution) | `scripts/loop/checkpoint-contract.mjs` (`resolveCheckpointRepoRoot`) |
| Internal-tooling verifier (findings-producer) | `scripts/loop/check-retro-tooling.mjs` (+ `test/loop/check-retro-tooling.test.mjs`) |
| Advisory findings envelope field | `packages/core/src/loop/handoff-envelope.mjs` — `retrospectiveFindings` |
| Tests | `packages/core/test/retrospective-checkpoint.test.mjs`, `test/loop/resolve-dev-loop-startup.test.mjs`, `test/loop/checkpoint-contract.test.mjs`, `packages/core/test/pr-gate-coordination.test.mjs`, `packages/core/test/handoff-envelope.test.mjs` |
| Extension (best-effort secondary trigger, writes required marker, fires review prompt) | `.pi/extensions/dev-loop-behavioral-review.ts` |
| Checkpoint file | `.pi/dev-loop-retrospective-checkpoint.json` |
| AGENTS.md repo contract | [Agent Instructions](../../AGENTS.md) — concise repo contract and working rules |
