# Changelog

All notable changes to this project will be documented in this file.

## Unreleased

## 0.9.0 - 2026-07-09

The v0.9 headline is the **`/loop-review-ui` epic (#1114)** — a UI-review dev-loop route that proves a change in the running app — plus a batch of loop-hardening and dev-loop self-convention fixes.

### Added

- **`/loop-review-ui` — running-app UI-review route (#1114, Stages 0–6).** A new dev-loop strategy that boots the app and reviews a PR against it: route + command scaffold with stop rules and acceptance self-validation (#1279); provision-and-boot module (#1280); auth + Playwright drive harness that captures page errors, error responses, and a server-log tail (#1281); diagnose that anchors reproduced exceptions to the exact diff lines (#1282); report that posts a head-pinned pending review with inline comments and a self-contained screenshot artifact, severity→event policy, harness-aware hosting (Claude Artifacts out of the box, fail-closed elsewhere) (#1286); teardown with an always-emit side-effect ledger (#1288); and command help + README + a per-project run/auth recipe contract (#1289). The full GitHub-native artifact-hosting fallback is tracked as follow-up #1285.
- **Headless auto-refine of parked un-refined items (#1258).** A deterministic `queue parked-unrefined` discovery helper surfaces issues the #1251 enqueue fail-safe parked without a refinement artifact; a headless/`--auto` dev-loop session auto-refines them at the orchestrator layer (`loop-grill --auto`) and promotes each into the pickup column via `queue move` — bounded (ascending order, at most once per session), with unrefinable items left parked with a recorded reason. No LLM in any coordinator script.

### Changed

- **PR-body convention + `pr-description` gate angle (#1283).** The `pr-description` draft-gate angle no longer requires (or flags the absence of) a "File-by-file changes" section — GitHub's Files-changed tab already covers it and the mandate churned gate findings. Linked-issue AC-checkbox syncing is now a first-class capability of `tick-verified-checkboxes.mjs` (`--issue`, or `--pr --issue` in one call), so a tracker-backed PR's approval gate keeps the issue's checkboxes honest instead of relying on a skippable manual step.
- **`gh issue edit` call-sites routed through the `edit-issue.mjs` wrapper (#1255)** so the loop's internal-tooling record stays complete (no agent-level raw `gh issue edit`).

### Fixed

- **CI verify-suite drift guard (#1273).** A contract test fails the build if `package.json`'s `test:*` scripts drift from the `ci.yml` verify-suite matrix in either direction.

### Performance

- **Faster `test:scripts` real-git files (#1277).** The heaviest real-git-repo test files were converted to a module-seam gh mock, in-process CLI, cheap shell git-stubs, and a copy-once git fixture (e.g. conductor-monitor 11.5s→2.5s), with genuine git-boundary/CLI-error smokes deliberately kept as real spawns. Suite wall time is throughput-bound and largely unchanged; throughput-aware wall reduction is tracked as follow-up #1294.

## 0.8.0 - 2026-07-08

<!-- Release date is set at tag time; see the release runbook. This section is prepared ahead of the v0.8.0 tag. -->

Everything merged since v0.7.1 — the v0.7.x contract-audit epic (#1104) plus follow-on release-gate fixes. **Versioning note:** most of these commits are ancestors of the `v0.7.2` tag and therefore already ship in the published `0.7.2` npm artifact, but `v0.7.2`'s release notes documented only the #1241 packaging hotfix (see the 0.7.2 section below). This section backfills that omitted changelog and adds the nine changes that genuinely postdate the `v0.7.2` tag (#1240, #1242, #1244, #1249, #1250, #1253, #1254, #1256, and #1236/#1252); it becomes the notes for the eventual `v0.8.0` tag.

### Added

- **Issue-less lightweight PR-first flow (#1210/#1215).** A PR with no backing issue can drive the lightweight path with a configurable composed Copilot round cap; such PRs auto-enqueue as board items so they are still tracked (#1218/#1226).
- **State-machine conformance and invariant harness (#1148/#1189).** Built on a rule-ownership foundation (#1183). The PR lifecycle state machine is exported from `@dev-loops/core` as the single source for docs, the state atlas, and the conformance harness (#1193/#1216), and `public-dev-loop-routing` is wired into the L2/L3 conformance harness so all five machines are covered (#1233/#1239).
- **Inline reviewer briefing prefix (#1220/#1229).** The reviewed content is carried inline, size-capped and hash-enforced, on top of invariant-prefix-first reviewer briefings with prefix-hash enforcement (#1207/#1214).
- **First-class loop primitives (#1198/#1223).** `list-review-threads` and `wait-pr-checks`, plus a sanctioned `edit-issue.mjs` wrapper for AC-reflection body edits (#1253) and a refinement-artifact-at-enqueue requirement so no un-refined item enters Next Up (#1251/#1254).
- **State-atlas fullscreen lightbox for diagrams (#1217).** Diagrams open in a fullscreen lightbox for readable inspection.

### Fixed

- **Package-escaping `@dev-loops/core` imports broke consumer installs (#1241).** Shipped as the v0.7.2 hotfix; full detail is in the 0.7.2 section below.
- **Converge-then-gate enforced at `pre_approval` entry (#1190/#1219).** Retires the known gap where a gate could run before Copilot convergence.
- **Fanout provenance fails closed on missing mandatory or foreign angles (#1196/#1225).** Briefing-prefix verification is also scoped by (gate, headSha) (#1249).
- **Reviewer-loop submission-failure edges join the transition table (#1200/#1221).** Copilot summon literals are sanitized on write with a code-span-aware guard and an honest blocked status (#1213/#1222).
- **Issue-less lightweight PR-first three-origin contracts reconciled (#1242).** Adds a gate-coordination `specSource` branch; a contradiction sweep covers gate-skip scoping, coverage-modality ownership, and advisory wording (#1244).
- **run-claim coordination file anchored at the git common dir, not CWD (#1250).** The per-PR runner lease now resolves its coordination file from the shared git common dir, so a runner started from a different worktree/CWD attaches to the same lease instead of forking a second one.
- **`queue --item` accepts the full node-ID alphabet across the projects scripts (#1227/#1230).** The `--item` node-ID parser now accepts the complete GitHub node-ID character set, so board items whose IDs use previously-rejected characters resolve consistently across the projects scripts.
- **release.yml dispatches npm-publish.yml explicitly after creating the release (#1187/#1188).** Pages deploy is bumped to deploy-pages v5 (#1211/#1212) and the gate-hub flowchart renders left-to-right (#1208/#1209).
- **docs-validator manifest gap and corpus→manifest completeness (#1238).** Epic AC1/AC2 zero-states restored — canonical-owner openers plus remaining phrase pins (#1240).

### Changed

- **Contract corpus condensation + single-owner rule IDs.** Rule ownership migrated to single-owner IDs across the gate, queue, loop, worktree, intake, and public-loop clusters, with residual normative phrase-pins retired to a zero-pin end state (#1149–#1159, #1191–#1206). Inline interpreters were banned from coordinator flows in favor of sanctioned paths by reference (#1224/#1228), a final corpus condensation sweep rewired rule IDs with zero semantic change (#1236/#1252), and RFC-2119 modality was harmonized on newly-tagged merge/confirmation clauses (#1256).

## 0.7.2 - 2026-07-06

### Fixed

- **Package-escaping imports in `@dev-loops/core` broke every consumer install (#1241).** `queue-board-sync.mjs` and `queue-board-ordering.mjs` imported `move-queue-item`/`list-queue-items` via `../../../../scripts/projects/*`, paths outside the package root that the published tarball (src/**+bin/** only) cannot resolve — so `resolve-active-board-item` (loop startup) and `ready-for-review` threw `ERR_MODULE_NOT_FOUND` on install for every release from 0.3.0 to 0.7.1. The `main` implementations now live in `@dev-loops/core` (`src/projects/`, exported via the package map); the two `scripts/projects/*` files are thin CLI wrappers importing core by package name. A packaged-install smoke test (`test:pack`) npm-packs both packages, installs them in a temp dir, imports every `@dev-loops/core` export-map entry, and runs the affected CLIs with `--help` — closing the artifact-layout gap that let five releases ship broken.

## 0.7.1 - 2026-07-05

### Changed

- **`captureDiffFromBase` pins `diff.renames`/`diff.algorithm`/`diff.context` and `core.abbrev`/`core.autocrlf` for cross-environment reproducibility (#1168).** The isolation `-c` overrides for the persisted `--base` neutral diff seed already made the diff byte-identical within a single run, but a machine with a contrary local gitconfig could still produce different bytes for the same base/HEAD pair — and, via `diff.renames`, a different `scope.changedFiles`/`adjacentCode` membership (rename vs delete+add). The five settings are now pinned alongside the existing overrides.

### Removed

- **Dead `isFenceLine` field dropped from `stepFence` (#1166).** `stepFence` (`packages/core/src/loop/issue-refinement-artifact.mjs`) computed and returned `isFenceLine` on every branch, but none of its three call sites (`parseMarkdownSections`, `extractChecklistItems`, `sectionHasBody`) read it. Removed from the return shape and its JSDoc; `fence`/`insideFence` and all fence-detection logic are unchanged.

### Fixed

- **Worktree provisioning creates the `@dev-loops/core` workspace link so `scripts/**` CLIs validate the branch's core (#1144).** Worktrees provisioned by `ensure-worktree.mjs`/`provision-worktree.mjs` have no `node_modules`, so any `scripts/**` CLI importing `@dev-loops/core` resolved the bare specifier by walking up to the MAIN checkout's `node_modules/@dev-loops/core` instead of the worktree's own `packages/core` — `npm run verify` in a worktree silently tested main's core, not the branch's. Provisioning now creates the relative symlink `node_modules/@dev-loops/core → ../../packages/core` unconditionally: idempotent on a correct existing link, replaces a stale/broken link, never clobbers a real file/dir already at the destination, and is reported in the provisioning summary.
- **Sibling logical columns resolved via `loadStateColumnMap` in pickup and archive (#1143).** `resolve-active-board-item.mjs` hardcoded the literal `"In Progress"` column for the live `/loop-continue` pickup query instead of resolving it through `loadStateColumnMap`/`LOGICAL_COLUMN.IN_PROGRESS` the way #1142 already did for Next Up, so a repo overriding `queue.statusColumns.in_progress` would have pickup silently miss the active item. An audit found the same gap in `archive-done-items.mjs` (`selectArchivable()` hardcoded `"Done"`). Both now resolve through the shared mapping, fail closed on a malformed `.devloops`, and honor configured overrides; `--help`/display literals in other scripts were left as-is (illustrative examples, not routing logic).
- **The post-convergence significance detector is content-aware, so comment/JSDoc-only changes don't reopen Copilot past the round cap (#1137).** Significance in `_post-convergence-change.mjs` (shared by `copilot-pr-handoff` and `detect-pr-gate-coordination-state`) was classified by path/size only, so a trivial comment fix could re-trip a re-request past the cap. A new `isCommentOnlyFileChange` classifier inspects the compare diff's patch hunks with a per-hunk block-comment state machine (reset at every `@@` header, conservative toward "significant") and filters comment-only JS/TS files out before the existing `>=20 lines`/`>=2 files` thresholds. A file with no `patch` (binary/oversized) is never treated as comment-only; the stale `+++`/`---` header-skip (never present in `gh compare` patches) was removed.
- **`copilot-pr-handoff` fails closed on a pending Copilot review at the round cap (#1165).** At the cap with a `--force-rerequest-review` in flight, handoff routed to `ROUND_CAP_CLEAN_FALLBACK` ("continue to pre_approval_gate") whenever the fail-silent secondary reopen-facts fetch (`gh pr view` + `gh compare`) failed — silently reading significance as false — while `detect-pr-gate-coordination-state`, reusing its already-validated PR data, correctly gated; the loop follows handoff as the pre-flight authority, so a significant change could skip Copilot's review (hit live on PR #1164). While a requested review is pending on the current head at the cap, handoff now surfaces `waiting_for_copilot_review` (action `watch`) before the fragile escape-hatch fetch can downgrade to "proceed"; a `--watch-status` refresh skips the guard so a request that never resolves re-settles to the clean fallback and the loop cannot dead-end.
- **Merge-block hook message explains that evidence writes and `gh pr merge` must be separate tool calls (#1172).** The "vanishing" gate-evidence ledgers on PRs #1167/#1169/#1171 were never swept: the PreToolUse bash gate evaluates `gh pr merge` before the tool call executes, so a compound command that both writes gate evidence (`write-gate-findings-log`/`upsert-checkpoint-verdict`) and merges in the same call is blocked at hook time — the write never runs. `decideBashGate` now appends a hint to the deny message in exactly that compound case (a bare `gh pr merge` keeps the standard message; no allow/deny boundary changes), and the write → verify → merge-alone contract is documented in `skills/docs/merge-preconditions.md`.
- **Light-mode-aware pre-merge gate accepts scoped inline verdicts (#1174).** `buildPreMergeGateCheck` rejected any `inline_single_agent` verdict whenever `gates.requireFanoutEvidence` was on (the repo default), so a genuinely under-threshold micro-PR's #1043 light-path verdict was unmergeable without a full fan-out (hit live on PR #1173, 1 file / 11 lines). An inline verdict is now accepted only when lightMode is enabled, the merge-base scope re-derived at merge time is under threshold (underivable scope → rejected, fail-closed), no `gate:full` label is present, and a non-empty inline reason is recorded; the findings-log ledger is still required, and the fan-out path (`fanout_fanin` verdicts, ledger, provenance enforcement) is unchanged.
- **`validate-pr-body-spec` enforces a `Closes #N` closing-keyword reference (#1181).** Five lightweight PRs merged without auto-closing their issues because the PR-body-as-spec validator never required the linkage. It now fails closed with `missing_closing_issue_reference` when the body carries no GitHub closing-keyword issue reference, supports `--expected-issue <n>` (`closes_wrong_issue` on a mismatch), and reports the extracted `closesIssues` — all keyword variants and the cross-repo `owner/repo#N` form accepted, while references inside fenced code blocks or inline code spans don't count.

## 0.7.0 - 2026-07-05

### Added

- **Lightweight local-first path — PR body as spec-of-record, no phase doc (#1025).** Local-first now supports a lightweight path for chores, bugfixes, and small mechanical changes where the PR description itself is the spec-of-record — no phase/plan doc is minted or committed. When active, `loop startup` no longer requires `--plan-file` and the pre-approval review agent reads the PR body (objective, in-scope/non-goals, testable acceptance criteria, definition of done, risks) instead of resolving a plan-file path; a new `validate-pr-body-spec` CLI enforces the required sections fail-closed. The gate sequence is unchanged (draft → pre-approval fanout → detect-evidence → human merge); only the backing artifact differs. Closes the phase-history pollution from minting a numbered phase doc for a six-file rename chore.
- **`lightMode` wired to gate dispatch — micro-PRs skip full fanout (#1043).** A PR under the `localImplementation.lightMode` threshold (`maxFiles`/`maxLines`, and no `gate:full` label) collapses the `draft_gate` + `pre_approval_gate` fanout to a single inline correctness + no-op check and skips Copilot polling; the draft→ready boundary is still recorded (`requireDraftFirst`). Two escalation paths: the inline check auto-escalates to full fanout on any finding at or above `worth-fixing-now`, and the maintainer-controlled `gate:full` label forces full fanout regardless of size. Scope detection fails closed — a diff whose size can't be determined runs the full fanout, not the light path.
- **Context-builder additive angle authority (#1048).** Gate angle resolution was subtractive-only (it could drop configured angles but never add one). With `gates.<gate>.additiveAngles` enabled (default off), the resolver may now pull in a review lens the diff's change categories imply even when it isn't in the gate's configured pool — drawn from `gates.anglePool` (or the union of known personas). Bounded and auditable: `excludeAngles` stays a hard ceiling, `mandatoryAngles` the floor, and each addition is recorded with an `"added"` rationale naming the triggering category alongside the existing kept/dropped entries.
- **Grouped board-summary mode for `queue list` (#1061).** `dev-loops queue list --summary` (alias `--group-by status`) returns a whole-board digest grouped by Status column in board order (`{ ok, groups: { <status>: { count, items } } }`), with `--done-limit` to cap the Done group, composing with `--jq`/`--silent`. One sanctioned call for the by-status overview that previously tempted an inline `| python3` grouping (retro rule 5); `--summary` is mutually exclusive with `--column`/`--limit`.
- **Cross-harness regression contract (#1086).** dev-loops is harness-agnostic (Pi + Claude Code); the rule "any harness-specific change MUST NOT regress other harnesses" is now a repo-wide working rule in `AGENTS.md`, with the detailed procedure in `skills/docs/cross-harness-regression-contract.md` (what counts as a harness-specific change, the required Claude-Code-path suites, and the additive-on-Pi / no-op-or-validated-on-Claude bar). Promotes the constraint from an ad-hoc section of #1084 to a universal, pre-0.7 enforced contract.

### Changed

- **`LOGIC_CHANGE` maps to a core review subset instead of fallback-to-all (#1049).** `dynamicAngles` was effectively decorative for code PRs: any non-trivial code change classified as `ambiguous`, which fell back to the full 15-angle draft pool. `LOGIC_CHANGE` now maps to a core-for-code angle subset in `CATEGORY_ANGLE_MAP` and no longer forces `ambiguous`/fallback-to-all (reserved for genuinely unclassifiable diffs); other categories still union in their mapped angles. A typical code PR now resolves to a handful of angles, with peripheral lenses (link-check, config-drift, etc.) appearing only when the diff implicates their category. Mandatory floor and exclude ceiling unchanged; each dropped angle keeps its recorded rationale.
- **`--jq`/`--silent` are now a base-CLI guarantee for every JSON-emitting command (#1071).** The output contract was per-script opt-in, so JSON-emitting commands like `sync-item-status.mjs` rejected `--jq` and forced grep/inline-parse workarounds. Every JSON-emitting command now routes through the shared emit path (`scripts/lib/jq-output.mjs`), so `--jq` filtering, invalid-filter fail-closed (exit 2), and `--silent` exit-code-only come for free; a contract test enforces it so new commands inherit the guarantee and can't regress.
- **Sanctioned operation→wrapper command map baked into the default handoff (#1081).** Which dev-loops wrapper to use for which operation (reads: `view-pr`/`probe-ci-status`/`fetch-ci-logs`/`list-issues`/`probe-copilot-review`; edits: `edit-pr`/`comment-issue`; lifecycle: `ready-for-review`/`create-pr`/`request-copilot-review`/`reply-resolve`/gate-verdict) is now a structural part of every dev-loop handoff, sourced once (`scripts/loop/sanctioned-commands.mjs`, carried via the handoff envelope + `agents/dev-loop.agent.md`) instead of an ad-hoc per-handoff decision — so a spawned subagent no longer re-derives the map or rediscovers a wrapper mid-run.
- **Playwright configs consolidated into one projects-based, registry-driven config (#1056).** The five byte-identical `playwright.*.config.mjs` files (one per UI slice) collapse to a single `playwright.config.mjs` with one Playwright project per registered slice; `test:playwright:<slice>` scripts run `--project=<slice>`. Adding a UI artifact now needs only a registry entry (already required by the ui-e2e-scoping gate) and no new config file; per-slice output dirs / report paths are preserved.
- **Deduplicated the three Playwright smoke jobs via a composite action (#1058).** `viewer-smoke` / `deck-smoke` / `article-smoke` shared copy-pasted setup (checkout, Node, `npm ci`, WebKit cache-restore, `playwright install`). The shared setup moves into `.github/actions/playwright-webkit/action.yml`, keeping the three separate jobs and their zero-cost `changes`-gated skips (a matrix would have paid setup on skipped legs). `UI_E2E_CHECK_NAMES` and the scoping contract are unaffected.
- **Faster `test:scripts` real-git tests (#1129).** Real-git fixtures paid `git` fsync on every object write; `test:scripts` now runs with `core.fsync=none` / `core.fsyncObjectFiles=false` (via `GIT_CONFIG_*`), cutting the process-spawn-heavy suite's wall time with no behavior change. The originally-proposed concurrent single-pool `verify` runner was reverted (oversubscription risk); the fsync disable is the kept win.

### Removed

- **`mermaid` is no longer a runtime dependency (#1089).** The inspect-run viewer serves the same pinned `mermaid.min.js` (11.15.0) from a vendored copy at `scripts/loop/inspect-run-viewer/vendor/mermaid.min.js` instead of resolving it out of `node_modules`; the `/assets/mermaid.min.js` route, `loadMermaidBrowserScript` seam, and offline behavior are unchanged. The vendored bundle is sha256-pinned by test (`test/loop/inspect-run-viewer-client.test.mjs`) and `test/extension-package-contract.test.mjs` guards that mermaid stays out of `dependencies`. Net install footprint shrinks (the mermaid transitive tree no longer installs); the npm tarball grows ~3.3MB unpacked from the vendored asset.
- **Breaking: dead-code deletion — ponytail audit batch 1 (#1088).** Removed unused public surface with no in-repo callers: the `@dev-loops/core` export subpaths `./debt/signals`, `./debt/score`, `./debt/finding`, and `./refinement/ac-dod-matrix` along with the now-dead `refinementContract` handoff-envelope plumbing (builder pass-through and validation in `handoff-envelope.mjs`) that only that module fed (the underlying `debt/score.mjs` module remains and is still tested internally); the `dev-loops-capture-deep-persona-signals` bin and the whole deep-persona-signals feature (`packages/core/src/debt/deep-persona-signals.mjs`); `createClaudeExtensionAdapter` (removed from the `@dev-loops/core/harness` exports along with its `claude-extension-adapter.mjs` module); the dead OUTER-graph tables `OUTER_STATE_TO_ROUTING_OUTCOME`, `OUTER_STATE_TO_OUTER_ACTION`, and `OUTER_NONTERMINAL_STATES` from `@dev-loops/core/loop/conductor-routing`; the config surface `localPlanning.plansDir` with its resolver `resolvePlansDir`, plus the unrelated dead config resolvers `resolveInternalPathPatterns` and `resolveMaxFanoutReviewers`; and the script CLIs `scripts/loop/conductor.mjs`, `scripts/loop/detect-stale-runner.mjs`, and `scripts/loop/pre-push-main-guard.mjs` (with their tests). Deprecation shim: a `localPlanning` block in a consumer `.devloops` file is still **accepted and ignored** (tolerated deprecated key in both zod schemas and `schemas/dev-loop-config.schema.json`) so existing configs keep parsing; remove the block at your convenience.

### Fixed

- **Guard `draft_gate` self-heal against unbounded recursion on a lagged draft-state read (#1020).** `upsert-checkpoint-verdict`'s `postDraftGateViaDraftTransition` converts a `ready` PR to draft, re-posts the verdict, then restores ready — but when GitHub's draft-state read lagged the conversion mutation, the re-entry saw the PR still non-draft and recursed, hanging with a swallowed Node exit 13. A `_draftTransitionInProgress` reentrancy guard now breaks the loop and fails closed with the complete manual recovery commands instead of hanging.
- **Resolve repo root and ledgers worktree-relative, not from `process.cwd()` (#1052; folds #1050 and #1019).** Under the normal per-PR-worktree flow the session cwd and the PR worktree are different checkouts, so cwd-bound reads/writes silently diverged: the pre-merge fan-out ledger check bound to the session cwd and false-blocked clean gates (#1050, hit PR #1044 twice), and `detect-copilot-loop-state` read `maxCopilotRounds` from cwd instead of the worktree `.devloops` (#1019). A shared worktree resolver now derives the repo root from the PR worktree, so the ledger writer and the pre-merge check bind to the same checkout regardless of session cwd. Pure resolution-location correctness — no gate semantics or ledger-format change.
- **Block subagent-initiated external writes (#1051).** Gate/dev-loop subagents had twice minted unauthorized, user-attributed GitHub issues. The bash gate now blocks raw `gh issue create` / `gh issue comment` / `gh pr comment` on the target repo when initiated by a subagent and directs to the sanctioned path; contract-driven writes (gate-verdict comments, reply-resolve review threads, board sync) and the main-agent/operator path are unaffected. Unit + e2e coverage mirrors the `gh pr create` guard.
- **Wrapped `gh pr view` / `gh pr edit` so the retro internal-tooling gate is satisfiable (#1057).** The remaining PR-facing reads/edits a real gate run needs had no dev-loops wrapper, so an honest run could never record a clean retrospective (`internalToolingOnly: true`, `rawCallViolations: []`) and the gate blocked code-clean PRs by waiver. New `scripts/github/view-pr.mjs` and `edit-pr.mjs` (jq-output-backed, `--jq`/`--silent`) complete the coverage; `workflow.requireRetrospectiveInternalTooling` is re-armed to `true`.
- **Tick verified PR-body acceptance-criteria checkboxes after a clean `pre_approval_gate` (#1066).** The gate verified acceptance criteria but only updated the linked issue, so merged PRs displayed all `- [ ]` unchecked, reading as never-confirmed. A single `gh pr edit --body-file` now flips `- [ ]` → `- [x]` for the items the gate positively confirmed (CRLF preserved, duplicate flipped labels de-duped, only verified items ticked, unconfirmable items left unchecked — fail closed).
- **Key fresh-context review sentinels per head SHA / round (#1095).** On a `draft_gate` retry after a fix commit, the round-1 per-(cwd, scope) sentinel from `verify-fresh-review-context.mjs` was still present, so every retry reviewer failed closed and burned a launch to refuse. The sentinel key now includes the head-SHA round component, so a new head never collides with a prior round while same-round re-entry still fails closed; the orchestrator-owned sentinel lifecycle is documented in the gate-review sub-loop contract.
- **Honor `queue.statusColumns.next_up` across ordering, pickup, and the projects layer (#1098).** Board-sync resolved logical columns through the `statusColumns` display-name overrides, but the ordering (`resolveNextUpOrder`) and `add-queue-item --next-up` hardcoded the literal `"Next Up"`, so a repo that renamed its Next Up column got a driver querying a non-existent column (fail-closed board-query-error or empty). The logical→display resolution now flows through the same shared mapping everywhere; board-sync stays the single source of the mapping.
- **Auto-release the runner-coordination lock on run completion + document stale-lock takeover (#1109, Seam 1 of #1084).** A completed/stopped run left its `.pi/runner-coordination/<repo>/pr-<n>.json` claim in place, so a merge-authorized re-dispatch (fresh run id) was refused `ownership_lost` — hit live on a 9.5h-old stale lock. A run now releases its coordination claims on completion/stop, and the stale-lock takeover path is documented; a genuinely active (non-stale) claim still fails closed.
- **Record fan-out provenance + opt-in enforcement, route-to-conductor (#1110, Seam 2 dev-loops of #1084).** `requireFanoutEvidence` was artifact-only, so a single agent could self-produce every per-angle review plus a ledger and label the verdict `fanout_fanin`. The findings-log ledger now records fan-out provenance (distinct reviewer count and dispatch provenance), and an opt-in `requireFanoutProvenance` check in `buildPreMergeGateCheck` re-validates it (internally consistent across checkouts, `distinctReviewers >=` the floor) to distinguish real parallel review from single-agent artifacts; a child that cannot fan out fails closed with a route-to-conductor message.
- **Fix `tools:` frontmatter so pi provisions the subagent tool at child depth (#1111, Seam 2 core of #1084).** pi-subagents parses the `tools:` frontmatter line with a naive comma split (no real YAML), so the flow-sequence form `tools: [read, …, subagent]` read `subagent]` (bracket attached) and never matched — child fan-out was impossible on pi. The `tools:` lines in `agents/*.agent.md` and `.pi/agents/*.agent.md` drop the brackets for the comma-scalar form; the generated `.claude/` mirror regenerates byte-identical (`normalizeToolList` already splits on commas), so the fix is additive on pi and a no-op on Claude Code (governed by the #1086 cross-harness contract).
- **Make `copilot-pr-handoff` and gate-coordination agree past the Copilot round cap (#1126).** Round-cap enforcement lived only in `copilot-pr-handoff.mjs`, so after `maxCopilotRounds` was reached `detect-pr-gate-coordination-state.mjs` still surfaced `rerequest_copilot_review` — advertising a re-request the handoff would refuse. Both now defer to a shared round-cap + significant-change detector, so they agree at the cap boundary.
- **Treat body-mention cross-references as non-owning board links (#1130, #1124).** `detect-linked-issue-pr` accepted a bare `CrossReferencedEvent` as an issue's linked open ready PR, but that event fires on any body mention (`Seam 1 of #1084`, `part of #X`), so a sub-issue-tree PR dragged every mentioned sibling into In Progress and jammed `resolve-active-board-item` with "multiple in-progress". An owning link now requires `willCloseTarget: true` (or a `ConnectedEvent`); a direct `gatherLiveFacts` open-issue→linked-ready-PR unit test (#1124) covers the reconcile bridge.
- **Run gate fan-out reviewers in the PR's actual worktree/head (#1135).** Reviewers dispatched with worktree isolation got a fresh checkout of stale `main` and no access to the gitignored `tmp/gate-context` bundle, so a reviewer could silently review the wrong tree without the seeded context. The read-only per-angle review dispatch now runs in the PR worktree/head with the seeded gate-context bundle (a worktree-locality guard rejects an out-of-tree `--context-path`), honoring the build-once/seed-many contract.
- **`write-gate-context --base` builds the diff + adjacentCode bundle on the CLI path (#1140).** The library built the "build once, seed many" bundle only when handed a diff; the CLI exposed no diff flag, so every CLI-driven gate wrote a thin briefing (`scope.diffPath: null`, empty `changedFiles`, no `adjacentCode`) and each of N reviewers re-ran the same `git diff`. A new `--base <ref>` flag captures the full diff plus the per-file 1-hop adjacent-code bundle once (`scope.diffSource="base"`), so every reviewer is seeded with the identical neutral bundle; without it the CLI still emits an explicit thin briefing.
- **Fix `manage-sub-issues reorder` always failing (#1160).** Reorder seeded its cursor with `after_id=0`, but GitHub's priority endpoint rejects `0` with an empty-body HTTP 500, which `gh` surfaced as `unexpected end of JSON input` — the loop aborted on the first call so no reorder ever succeeded. The first item now uses `before_id=<current head>` (or skips when it is already first) and chains `after_id` after; `ghApi` also surfaces the HTTP status on empty-body non-2xx responses.
- **Eliminated the version-skew class: pinned consumer-facing CLI invocations + release-time core-version assertion (#1036, root cause of #1033).** Every consumer-facing CLI example now pins `npx dev-loops@<version>` (single source: the package version) instead of a bare `npx dev-loops` that resolves a possibly-stale global/cached copy: `README.md`, `docs/migrating-to-dev-loops.md`, and the `cli/index.mjs` help/setup strings. Global `npm install -g dev-loops` is reframed as an optional, drift-prone shell convenience — explicitly not the supported invocation path. A new fail-closed contract test (`test/contracts/cli-invocation-contract.test.mjs`) bans bare `npx dev-loops` across the README/docs/CLI surface. The Pi extension already resolves the CLI and `@dev-loops/core` from the installed pinned package (module imports, updated via `pi update git:...`), never a global install; `extension/README.md` now states that lockstep contract. Root-cause fix for the mis-published-bundle failure mode: `scripts/release/assert-core-dependency-version.mjs` (node-builtins-only) fails the release when root `dev-loops`'s `@dev-loops/core` dependency major.minor differs from the version being released, wired as a step in `.github/workflows/release.yml` before the GitHub Release is created; a unit test (`test/docs/assert-core-dependency-version.test.mjs`) rejects the #1033-shaped bad manifest (`0.6.2` / `^0.2.6`) and guards the real manifest. Non-goals (YAGNI): no per-startup runtime skew watcher, no auto `npm i -g` on update.

- **Bash gate now blocks raw `gh pr create`, closing the draft-first hole.** The PreToolUse bash gate already blocked `gh pr ready`/`gh pr merge` without evidence, but nothing guarded PR *creation* — so raw `gh pr create` (which defaults to ready-for-review) silently bypassed the `workflow.requireDraftFirst` contract. It now denies raw `gh pr create` on the target repo and directs callers to the canonical wrapper `scripts/github/create-pr.mjs` (`dev-loops pr create`), which always drafts and self-assigns. The wrapper runs `gh pr create` inside a node child process, so its command is unaffected; an explicit non-target `--repo` and non-target-repo cwd pass through. The create gate now denies an explicit `--repo` targeting the repo regardless of cwd (#1047) — raw `gh pr create --repo <target>` run from outside the repo previously slipped through. New `commandContainsGhPrCreate`/`extractRepoFlagFromGhPrCreateAnywhere` in `packages/core/src/loop/bash-command-classify.mjs`, a create branch in `decideBashGate` (`packages/core/src/claude/hook-decisions.mjs`), and detection wiring in `.claude/hooks/pre-tool-use-bash-gate.mjs`; unit + e2e coverage added. The `gh pr <verb>` matcher now also treats newlines/`\r` as segment separators and normalizes a leading env-assignment (`GH_TOKEN=x`), `command`/`env`/`exec` wrapper, or absolute/relative gh path before the `^gh` test — closing the same bypass gap identically for `gh pr ready`/`gh pr merge` (subshell/group forms remain out of scope). The create gate no longer short-circuits ready/merge gating in compound commands: an out-of-scope create (e.g. `gh pr create --repo other/repo && gh pr merge 5`) now falls through so the gated ready/merge segment is still evaluated instead of being allowed early. The create gate now evaluates each create segment's scope so a leading out-of-scope create can't shield a later in-scope raw create.

- **`strategy.default: local-first` now respected end to end as the built-in code default (#1033).** `BUILT_IN_DEFAULT_TARGET_PREFERENCE` and `BUILT_IN_DEFAULTS.strategy.default` changed from `github-first` to `local-first` in `packages/core/src/loop/public-dev-loop-routing.mjs` and `packages/core/src/config/config.mjs`. The `resolveTargetPreference` fallback in `scripts/loop/resolve-dev-loop-startup.mjs` (line 267) and the config-load-error fallback (line 675) were also changed from `prefer_github_first` to `prefer_local`, so configless repos now resolve to local-first through the CLI path too. The 32 tests that previously relied on the implicit github-first default were updated to pin `PREFER_GITHUB_FIRST` explicitly; one config test assertion and one routing-config test updated to match the new built-in default. Docs updated: `targetPreference` table in `skills/docs/public-dev-loop-contract.md` now shows `prefer_local (default)`; `BUILT_IN_DEFAULTS` description in `skills/docs/artifact-authority-contract.md` updated to `local-first`.

## 0.6.2 - 2026-06-30

### Changed

- **Terminology pass: 'named human/person' → 'contributor' across all doc surfaces (#1026).** Articles (`docs/articles/dev-loops-deep-dive.{md,html}`, `introducing-dev-loops.html`), decks (`docs/presentations/dev-loops-deep-dive.html`, `introducing-dev-loops.html`, `applied-dev-loops-presentation.md`), `skills/docs/merge-preconditions.md`, `schemas/dev-loop-config.schema.json`, and `packages/core/src/config/config.mjs` docstring. Deep-dive article subtitle tightened; attention-bottleneck section rewritten for clarity. `.md`/`.html` pair synced (`fast enough to be routine`).

### Fixed

- **`deriveUiE2ePassed` blocked the gate on SKIPPED UI e2e checks (#1026).** `viewer-smoke` is `SKIPPED` (not `FAILURE`) on PRs that don't touch viewer files — it means "not applicable to this run." The function now treats `SKIPPED` the same as `SUCCESS` so a skipped check no longer incorrectly blocks `run_pre_approval_gate`. Test coverage added.

- **`plugin.json` was missing from the v0.6.1 version bump (#1026).** `.claude/.claude-plugin/plugin.json` was still at `0.6.0`; bumped to `0.6.1` (now `0.6.2` in this release). Stale generated `.claude/` assets regenerated.

## 0.6.1 - 2026-06-30

### Changed

- **Docs: refresh intro article + deck to the 0.6.0 command surface + current velocity snapshot (#1015, #1018).** `/dev-loops:continue` now shows both forms — `/dev-loops:continue 112` (issue or PR) and bare `/dev-loops:continue` (resumes the single in-progress board item, fail-closed on 0/multiple). Adds `/dev-loops:start-spike "…"` to the command list in both the intro article (`docs/articles/introducing-dev-loops.{md,html}`) and the intro deck (`docs/presentations/introducing-dev-loops.html`). The Pi command set in the catch-all router note now lists `start-spike`. The velocity snapshot paragraph is updated from the previous "two-week, ~100 PR" estimate to a verified four-day snapshot (87 PRs, ~22/day, v0.4.0 + v0.5.0).

- **Deep-dive deck: desktop responsiveness, mandatory snap, keyboard nav (#1021, #1022).** Desktop layout adjusts for wider viewports. Section scroll-snap is enforced. Keyboard navigation is enabled.

- **Namespace Claude Code slash commands with `loop-` prefix to avoid collisions (#1023).** The 6 dev-loop commands (`.claude/commands/`) are now `/loop-start`, `/loop-auto`, `/loop-continue`, `/loop-start-spike`, `/loop-info`, `/loop-status` — renamed from bare `/start` etc. which collided with Claude Code built-ins (notably `/status`). Source files renamed from `commands/*.command.md` to `commands/loop-*.command.md`; generated assets updated; no-drift test updated.

### Fixed

- **`release.yml` auto-Release never ran — the CHANGELOG extractor pulled in `@dev-loops/core` (#1016, regression in #996's first run).** `scripts/release/extract-changelog-section.mjs` imported `isDirectCliRun` from `scripts/_core-helpers.mjs`, which re-exports from the `@dev-loops/core` workspace package. `release.yml` runs the extractor with **no `npm ci`** (a text parser needs no install), so on a `v*` tag push the import `ERR_MODULE_NOT_FOUND`ed before any notes were extracted and the GitHub Release was never created (the same hands-off-publish gap #996 set out to close). The extractor now imports **only `node:` builtins** — `isDirectCliRun` is inlined (`node:fs` `realpathSync` + `node:url` `fileURLToPath`), all behavior unchanged (`--version`/`--changelog`, leading-`v` strip, heading lookahead so `0.5.1` ≠ `0.5.10`, stop-at-next-`## `, no Unreleased bleed, exit 1 absent/empty, exit 2 arg/file error, the `extractChangelogSection()` export). A new guard test parses the script's `import` statements and asserts every specifier is `node:`-prefixed, so a future workspace import can't silently reintroduce the deps-free break. `release.yml` is unchanged — the standalone script is the fix.

- **Close `gh pr merge` bypass hole in the PreToolUse gate (#1024).** A hand-run `gh pr merge` could skip the pre-approval gate entirely because the PreToolUse Bash hook only blocked `gh pr ready`. The hook now also blocks `gh pr merge` unless `detect-checkpoint-evidence.mjs` confirms clean current-head `draft_gate` + `pre_approval_gate` evidence (fanout_fanin, no inline verdicts). Two compound-command bypass paths are also closed: (1) the all-segments scanner (`commandContainsGhPrReady`/`commandContainsGhPrMerge`) catches gated verbs in any shell segment, not just the first; (2) `gh pr ready N && gh pr merge N` no longer short-circuits to ready-only — both verbs are detected independently and the stricter merge gate is applied. Pi extension public API (`isGhPrReadyCommand`, extractors) retains first-segment-only semantics (correct: `false && gh pr ready 42` short-circuits so ready never ran).

## 0.6.0 - 2026-06-29

### Added

- **`/start-spike` — first-class command to start a spike from a question (#988, P2; closes #988).** `/dev-loops:start-spike <question>` (and the Pi `/dev-loops start-spike` subcommand) starts a time-boxed dev-loop spike, a thin wrapper over the already-shipped `--spike` intake (#964/#965/#966). Inline free-text scaffolds a startable findings artifact and `--file <path>` uses a pre-authored one; both then run `resolve-dev-loop-startup.mjs --spike <path>` and hand the bundle to the `dev-loop` skill (gates.spike, exit via `exit-spike.mjs` — discard/graduate per `skills/docs/spike-mode-contract.md`). Because the argument is free text/a path rather than a number, `start-spike` is parsed on a **separate path** from the numeric verbs (start/auto/continue/info), keeping their numeric-validation invariant intact. The one new piece is `scripts/refine/scaffold-spike-file.mjs --question <text> --out <path>`, a pure section builder + file writer that fills `## Question` from the arg and stubs `## Approach`/`## Findings` (leaving `## Recommendation` for the spike) so the result passes `validateSpikeExplorationSections` and is immediately startable (JSON `{ ok, path, question }` + `--jq`/`--silent` via the shared `scripts/lib/jq-output.mjs`). No new spike behavior, gate, or format. `node:test` coverage for the scaffold (startable/in-progress, empty-question fail-closed, CLI write + end-to-end startability), the separate free-text/`--file` parse path (numeric verbs unaffected), and the generated-command presence + no-strategy-leak guard. Also widened the Pi command-palette `description` string's `continue <pr>` to `continue [issue|pr]` for consistency with the P1 surface widening.

- **Path-triggered UI e2e auto-scoping gate criterion (#976, P2; replaces opt-in-by-annotation).** A PR that adds or modifies a *rendered* HTML artifact now MUST run the shared UI e2e assertions (mobile + desktop) AND register that artifact in the suite — inclusion is triggered by the changed-file set, never by a human annotating the PR/phase doc. The deterministic core is `packages/core/src/loop/ui-e2e-scoping.mjs` (`evaluateUiE2eScoping`): explicit, conservative path globs (`docs/articles/*.html`, `docs/presentations/*.html`, single-segment, plus the viewer source `scripts/loop/inspect-run-viewer.mjs`) classify changed paths into rendered-artifact descriptors and check each against the registry-membership list keyed by full repo-relative path (`REGISTERED_ARTIFACT_PATHS` mirrors `DECK_REGISTRY` at `docs/presentations/<deck>` + `ARTICLE_REGISTRY` at `docs/articles/<file>`; `VIEWER_ARTIFACT_ID` mirrors `VIEWER_REGISTRY`, kept in sync by hand — the deck/article sync test asserts only that `REGISTERED_ARTIFACT_PATHS` matches `DECK_REGISTRY` + `ARTICLE_REGISTRY`, since the viewer harness pulls `@playwright/test` into core). Full-path keying (not basename) means a deck and an article that share a basename are distinct artifacts. Each artifact family has a stable, path-conditioned CI job (`viewer-smoke`/`deck-smoke`/`article-smoke`) named to match `UI_E2E_CHECK_NAMES`, so a deck- or article-only PR has a satisfiable signal. It is wired as a gate precondition in `evaluatePrGateCoordination` (`packages/core/src/loop/pr-gate-coordination.mjs`), at a seam distinct from the mergeability (#980) and retrospective (#982) preconditions, and **fails closed**: a rendered-artifact change that is unregistered, or registered but whose UI e2e suite has not passed for this head, blocks with `nextAction: run_ui_e2e_suite` and a reason naming the artifact (and that it needs registration / the suite must pass). Non-UI changes pass through (`required: false`). The detect layer reads the changed-file set from `gh pr view --json files` (`extractChangedFiles`) and derives `uiE2ePassed` from the `statusCheckRollup` UI e2e check(s) (`deriveUiE2ePassed` / `UI_E2E_CHECK_NAMES`); a UI e2e check that is absent reads as unknown and fails closed. Standard-step doc: [ui-e2e-scoping-step](skills/docs/ui-e2e-scoping-step.md). `node:test` coverage: helper trigger/negative/fail-closed (unregistered + not-passed) cases + registry sync, gate-level integration (required-pass, non-UI, both fail-closed paths), and detect-helper unit tests.

- **`/continue` dual-routing — bare resumes the current in-progress board item (#988, P1).** `/dev-loops:continue` (and the Pi `/dev-loops continue` subcommand) is now dual-routed: with an argument it continues that artifact (the `continue` verb widened from PR-only to `either`, so `123`/`#123`/a GitHub issue-or-PR URL all normalize and the resolver picks the canonical artifact); bare (no argument) it picks up the single in-progress board item. The bare path adds one new thin helper, `scripts/projects/resolve-active-board-item.mjs --repo <owner/name> --project <number|id>`, a pure list→single-target collapse over `list-queue-items.mjs --column "In Progress"` (JSON `{ ok, target: { kind, number } }` + `--jq`/`--silent` via the shared `scripts/lib/jq-output.mjs`): exactly one in-progress item → that target (prefers the linked PR over the issue); **zero or more than one → fails closed** (exit 3) with a reason naming the items and instructing `/continue #N` — it never guesses. No new routing logic: both `/continue` branches hand the resolved target/intent to the existing `dev-loop` skill (`loop startup` → build-envelope → route), still stopping at the human-approval checkpoint. `/dev-loops:dev-loop` remains the catch-all router. `node:test` coverage for the resolver (1/0/multiple), the widened verb + bare/`#`/URL arg normalize, and the no-strategy-leak guard.

- Direct dev-loop slash commands as thin wrappers over the public contract (#972): `/dev-loops:start <issue>`, `/dev-loops:auto <issue>`, `/dev-loops:continue [issue|pr]`, `/dev-loops:info <issue|pr>`, and `/dev-loops:status` for the Claude Code plugin (generated under `.claude/commands/` from `commands/*.command.md`), with equivalent `/dev-loops start|auto|continue|info|status` subcommands in the Pi extension (`status` already existed; the start/auto/continue/info entrypoint subcommands are the new parity). `/dev-loops:dev-loop` remains the catch-all router; no new routing logic was added.

- **Wrapped the last three agent-level `gh` reads + re-armed the #982 internal-tooling discipline (#993).** Three reads the loop still shelled out for now have thin dev-loops wrappers, following the #981 convention (JSON result + `--jq`/`--silent` via the shared `scripts/lib/jq-output.mjs` helper — `JQ_OUTPUT_PARSE_OPTIONS`/`JQ_OUTPUT_USAGE`/`emitResult`, so the jq-subset filter, fail-closed-on-invalid-filter exit `2`, and exit-code-only `--silent` behavior are reused, not reimplemented): `scripts/github/fetch-ci-logs.mjs --repo --pr [--failed-only] [--tail <n>]` resolves the PR's current head SHA, lists the Actions runs for that commit, and returns each run's log tail (`--log-failed` for a failed run, `--log` otherwise; a per-run log fetch failure records an `<log unavailable: …>` note instead of aborting) — the LOG complement to `probe-ci-status.mjs`, which names the failed checks; `scripts/github/list-issues.mjs --repo [--state open|closed|all] [--label <l>…] [--limit <n>]` returns `{ ok, issues: [{ number, title, state, labels }] }` (state lowercased, labels flattened) for arbitrary issue queries the queue/board tool doesn't cover; `scripts/github/comment-issue.mjs --repo --issue (--body <text> | --body-file <path>)` posts a comment via `gh issue comment` and returns `{ ok, commentUrl }` (sibling to the PR comment/verdict posters). Each is a thin wrapper over `gh` (the script calling gh internally IS the allowed tooling). With these in place, every agent-level raw `gh` read now has a wrapper, so a fully clean internal-tooling record is achievable — the repo-root `.devloops` flips `workflow.requireRetrospectiveInternalTooling` back to `true` (it was set false in #995 pending this), re-arming the hard retro check (#982) on our own retros; the developer-mode rationale comment is restored to the enforce-on posture. The token-economical reading convention in `skills/dev-loop/SKILL.md` (+ generated `.claude/` mirror) names the three wrappers as THE path — never raw `gh run view`/`gh issue list`/`gh issue comment`. `node:test` coverage per wrapper (gh stubbed via the injected `run`): arg parsing/validation, the success path (failing-job log tail for a red PR, filtered issues, comment URL returned), and the shared output flags exercised through `runCli` — `--jq` extraction, invalid-filter fail-closed exit `2`, `--silent` exit-code-only, and gh-failure exit `1`.

- **Automated GitHub Release on tag push (#996).** Pushing a `v*` tag now creates the GitHub Release automatically, which fires `npm-publish.yml` — tag → Release → publish is hands-off (closing the gap where v0.5.0 was tagged but never published because no Release was created). New `.github/workflows/release.yml` triggers `on: push: tags: ['v*']`: it mirrors npm-publish's on-main guard (tag commit must be an ancestor of `origin/main`), extracts the release notes from the `## <version>` CHANGELOG.md section, is idempotent (no-op if a Release for the tag already exists), and fails closed if the version has no CHANGELOG section (no empty/auto-generated release for an undocumented version). Notes extraction is factored into `scripts/release/extract-changelog-section.mjs` (`--version <v> [--changelog <path>]`) with `node:test` coverage: present section, fail-closed absent version, the `## Unreleased`-above-latest layout, stop-at-next-`## ` boundary, and version-prefix disambiguation. `npm-publish.yml` is unchanged (still `on: release: published`). Tagging stays the only manual step — see [release-runbook](skills/docs/release-runbook.md).

### Changed

- **Reconciled the UI-validation docs to the auto-scoped model and added a worked example (#977, docs-only; UI-e2e epic UE3, follows #975/#976).** The four `docs/ui-*.md` docs no longer describe UI validation as an opt-in-by-annotation convention. `docs/ui-validation-contract.md` now leads with the path-triggered, registry-backed, fail-closed criterion, lists the shared harness assertions (section visibility, CSP-meta lock, 390px mobile fit, wide-element negative control), and carries a worked end-to-end example for the intro deck (`docs/presentations/introducing-dev-loops.html`): `DECK_REGISTRY["intro-deck"]` + thin `defineDeckSuite` spec → `REGISTERED_ARTIFACT_PATHS` membership → `ui_e2e_scoping` gate requirement (fails closed if unregistered) → the satisfiable `deck-smoke` CI signal (`UI_E2E_CHECK_NAMES`). `docs/ui-smoke-harness.md` reframes `webkit-smoke-harness.mjs` as the lower WebKit/config/capture seam the shared deck/article/viewer suites build on and drops the stale "not mandatory CI" claim; `docs/ui-artifact-contract.md` renames CI promotion to auto-scoped CI enforcement; `docs/ui-designer-review-loop.md` notes it is the optional design-review pass, distinct from the required gate. All four now cross-link the canonical owner `skills/docs/ui-e2e-scoping-step.md` and the shared harness. No behavior/harness/gate change. Closes #939 (its slide responsive-fit goal is delivered by the #975 mobile-fit harness).

- **Taught the direct subcommand structure across the how-to surfaces (#1001, follows #972).** The intro article (`docs/articles/introducing-dev-loops.{md,html}`) and the intro deck setup slide (`docs/presentations/introducing-dev-loops.html`) now show the direct named commands from #972 (`/dev-loops:start`, `:auto`, `:continue`, `:info`, `:status`), framing `/dev-loops:dev-loop` as the catch-all router for plain-language intent (with the Pi `/dev-loops start|auto|continue|info|status …` parity noted on both surfaces — the article spells out the full set, the deck setup slide flags that Pi drops the colon). The deck slide stays within the 390px mobile-fit harness (#975); section ids unchanged. Conceptual/routing-contract prose (deep-dive article + deck, public-dev-loop-contract, main-agent-contract) was left untouched.

- **Per-artifact Playwright deck assertions extracted into a shared harness + registry (#975, generalizes #939).** The two presentation-deck specs (`test/playwright/intro-deck.spec.mjs`, `test/playwright/deep-dive-deck.spec.mjs`) were ~190 lines of near-identical assertions each (same server, same three tests, only the deck path / section ids / mobile-capture id differed). The reusable assertions now live once in `test/playwright/harness/deck-fit-harness.mjs`: the single-file deck server, the `networkidle`+`innerWidth===390`+`document.fonts.ready` layout-settle barrier, the per-element `getBoundingClientRect().right <= innerWidth + 1` mobile-fit check (no overflow-x scroller exemption) with the defensive `scrollingElement.scrollWidth` guard and the per-section vertical-clip check (`clientHeight < scrollHeight` while `overflow-y` is hidden/clip), section-id presence + no-horizontal-scroll, a CSP-`<meta>` guard (`default-src 'none'`), and the guard-the-guard test (a deliberately-wide element MUST fail the fit check). A `DECK_REGISTRY` holds each deck as data (`{ sliceId, deck, sectionIds, mobileCapture }`) and `defineDeckSuite(entry)` generates the full per-deck suite; the two deck specs shrink to thin registrations that resolve the deck path and call `defineDeckSuite`. The inspect-run viewer (an interactive dashboard, not a fit-checked deck) registers via `test/playwright/harness/inspect-run-viewer-harness.mjs` (`startViewer`/`openTab`/`waitForMermaidGraph`/`captureViewerState`/`VIEWER_REGISTRY`), removing the duplicated server-setup + repeated capture blocks from its spec, and now runs the shared `assertSectionIdsAndNoHorizontalScroll` over its registered panel ids. The per-artifact `playwright.*.config.mjs` files and `test:playwright:*` scripts are unchanged (each config's `testMatch` still scopes which registration runs). All existing assertions still pass — `intro-deck` 3/3, `deep-dive-deck` 3/3, `inspect-run-viewer` 7/7 green.

### Fixed

- **Robust, bounded `<dev-loops-package-root>` resolution for user-level installs (#1009).** The `dev-loop` agent prompt (`agents/dev-loop.agent.md`) and the `dev-loop` skill (`skills/dev-loop/SKILL.md`) previously told the Pi runtime to resolve the package-local CLI by assuming a single fixed `../..` (agent) / `../../..` (skill) package-relative layout. Under a Pi user-level install the agent file is synced to `~/.agents/` and the package itself lives at `~/.pi/agent/npm/node_modules/dev-loops/` (skills are exposed via `package.json` `pi.skills`, loaded from that package location, not copied into `~/.agents/`), so the fixed relative guess missed the package root, the CLI could not be found, and the agent improvised an unbounded `find / -name dev-loop.agent.md` full-filesystem walk that stalled 60s+ and tripped the async needs-attention timeout. The resolution prose now lists an ordered set of **bounded** candidate roots — a best-effort `require.resolve('dev-loops/cli/index.mjs')` probe (cwd-dependent; reliable only when `dev-loops` is on Node's module search path; wrapped in try/catch so a miss exits non-zero silently instead of printing a stack trace that reads as a hard failure), the Pi user-agent npm root (`~/.pi/agent/npm/node_modules/dev-loops`, the reliable path for user-level installs), the legacy package-relative path, and the global npm root — taking the first whose `cli/index.mjs` exists. The prose now explicitly **forbids** `find /` / any unbounded filesystem walk and instructs the agent to stop and ask the orchestrator/operator for the path when every bounded candidate fails. Pi-only prose, stripped from the generated `.claude/` mirrors (which already pin `npx dev-loops@<version>` and rely on Node resolution); `.claude/` regenerated and in sync; cli-invocation contract test green.

- **Restored `PI_SUBAGENT_RUN_ID` as a recognized async-context run-id alias — the Pi async-start gate no longer fails closed (#1008, regression from #905).** The Pi runtime injects only `PI_SUBAGENT_RUN_ID` (never the neutral `DEVLOOPS_RUN_ID`) into each async-subagent child env, but #905 dropped that alias from `RUN_ID_MARKERS` as a "deliberate breaking change", leaving the sole marker absent under Pi. With `workflow.asyncStartMode: "required"` this made `validateAsyncStartContext` return `rejected` at startup step 1, so no dev-loop work could start under Pi. `RUN_ID_MARKERS` in `packages/core/src/loop/run-context.mjs` is now `["DEVLOOPS_RUN_ID", "PI_SUBAGENT_RUN_ID"]` — the neutral var stays the primary (and the only var dev-loops mints/propagates), with the Pi-injected name honored as a precedence-after alias. The alias threads through everywhere the primary does (`resolveRunId`/`ensureRunId` loop the markers; `ASYNC_CONTEXT_MARKERS` re-exports them, so the async-start gate recognizes it), and the rejection message names both again ("Set DEVLOOPS_RUN_ID (or the PI_SUBAGENT_RUN_ID alias) to proceed", matching #830). `PI_SUBAGENT_RUN_ID` is an externally-injected Pi-runtime contract var (not a dev-loops-owned `PI_*` name — #905's rename of owned vars to `DEVLOOPS_*` stands), so the harness-agnostic env guard (`test/contracts/cli-harness-agnostic.test.mjs`) allowlists it as a runtime-injected marker confined to the run-context/async-start modules + their tests (and the generated `.claude` mirror). `node:test`: Pi-only-marker recognition under `required` (the regression reproduction), neutral-primary precedence, and the both-markers rejection message.

## 0.5.0 - 2026-06-28

### Added

- **Conflict-free (mergeable) is a required gate precondition + deterministic auto-resolve (#980).** A PR that conflicts with its base gets no `pull_request` CI run (GitHub can't compute the merge ref), so the gate silently stalls green-less. Mergeability is now a required precondition checked at every gate (draft + pre-approval) and again before merge: the detect layer fetches `gh pr view --json mergeable,mergeStateStatus` and the evaluator (`evaluatePrGateCoordination`) blocks a `CONFLICTING`/`DIRTY`/`BEHIND` PR (`gateBoundary: conflict_resolution`, `nextAction: resolve_merge_conflicts`). Because GitHub computes `mergeable` asynchronously, a freshly-pushed head briefly reads `UNKNOWN`; `fetchPrFactsWithSettledMergeable` re-polls a bounded number of times and, if it never settles, the gate fails closed to a recheck (`nextAction: wait_for_ci`) — an unsettled merge state is never treated as clean. A new conservative helper `scripts/loop/resolve-pr-conflicts.mjs` merges `origin/<base>` into the PR branch and resolves ONLY the safe additive case — a `CHANGELOG.md` conflict where both sides only ADD list/section entries (keep BOTH sides, in order) — then runs `npm run test:docs` and (with `--push`) pushes; ANY other conflicted path, or a non-additive CHANGELOG edit, FAILS CLOSED naming the conflicted paths (no general conflict-resolution engine). `loop info` surfaces a `Mergeable:` line (mergeStateStatus included) so a conflict is diagnosed immediately rather than mistaken for missing CI. `node:test` coverage: CONFLICTING/UNKNOWN/MERGEABLE evaluator cases, the bounded UNKNOWN re-poll, and real-git fixtures for the additive-CHANGELOG resolve, the non-CHANGELOG fail-closed (path named), the non-additive-CHANGELOG fail-closed, and a clean merge. Docs: [merge-preconditions](skills/docs/merge-preconditions.md) documents the conflict-check-and-resolve step (before CI/Copilot and before merge).

### Changed

- **Pages decks auto-deploy on push to `main`** (#941, closes #940; supersedes the operator-gated deploy from #930). Pages is enabled (Source: GitHub Actions), so the deploy job no longer needs the `workflow_dispatch` gate `#930` shipped while Pages was off. `.github/workflows/pages.yml` now scopes the deploy job to the main ref (`if: github.ref == 'refs/heads/main'`) instead of `if: github.event_name == 'workflow_dispatch'`, so a merge to `main` publishes the assembled `site/` automatically. A manual `workflow_dispatch` still deploys, but only when run against `main`; dispatching any other branch/tag builds the artifact and skips deploy. The workflow header comment, the deploy-job inline comment, and `docs/presentations/README.md` were updated to describe auto-deploy on merge in place of the one-time operator "Run workflow" step.
- **Public materials reframed around the pull principle** (#991). The handoff-framing passages across the four public pieces now lead with the Kanban-style insight that the next step is always known: the authoritative resolver/state graph computes the next action for any change at any time, `loop info` surfaces it, and the board mirrors it, so whoever is free — agent or human — pulls the next bounded step from the visible state. The handoff becomes optional, at most additive: when one happens it adds a note and stays a recorded decision, and it is no longer a load-bearing blocking step, which is what dissolves the waiting-for-handoff. Edited in sync (`.md` + `.html`): the intro article's lede, "Model-agnostic by construction", and "Where to go deeper" pointer (`docs/articles/introducing-dev-loops.md`/`.html`); the deep-dive article's Part 1 — the two-part intro, "The one idea" (retitled "the next step is always known"), "What a known next step buys you", "Why a state graph beats a prompt" (recast as the thing that makes the next step always-known and pullable), and the close (`docs/articles/dev-loops-deep-dive.md`/`.html`); the intro deck's hero, the idea slide (retitled "The Next Step Is Always Known"), the model-agnostic slide, and the companion-pieces note (`docs/presentations/introducing-dev-loops.html`); and the deep-dive deck's hero, core-idea slide (retitled "The Next Step Is Always Known"), why-graphs slide, and close (`docs/presentations/dev-loops-deep-dive.html`). The "still a recorded decision when it happens" point is kept, now subordinate to the pull framing. Diagrams and the Diagram 1–8 caption numbering are unchanged; the deck section ids and 390px mobile-fit are intact (deep-dive deck Playwright spec green). The A/B-contrast deslop step (`docs/ab-contrast-deslop-step.md`) was applied to all rewritten prose — the pull idea is stated as plain declaratives with no "pull, not push" / "rather than" / "instead of" antithesis.
- **Public materials consolidated to ONE deep dive per format** (#978). The two deep-dive articles (`eliminating-coordination-delay` + `make-the-waiting-visible`) merged into one `docs/articles/dev-loops-deep-dive.md`/`.html`, and the two deep-dive decks (`applied-dev-loops.html` + `process-observability.html`) merged into one `docs/presentations/dev-loops-deep-dive.html`. Each merged piece runs in two parts — Part 1 eliminating coordination delay (explicit handoffs, fan-out/fan-in review, mid-flight steering, the state graph), then Part 2 make the waiting visible (interrupt cost, handoff discovery, the git blind spot, the four fields, the measurement loop, grounding in real mechanisms) — under one title/hero, one intro, and one close. The deep-dive article reuses the article design system (glass cards, inline-flow nodes, inline SVG diagrams) and the deck reuses the deck design system with stable per-slide section ids and mobile-fit layout. The old four published files are removed (and the orphaned `*-notes.md` review records for the two removed articles). `scripts/pages/build-site.mjs` `ARTICLES`/`DECKS`/`NAV_LINKS` collapse to the single deep-dive in each format (nav labels "Deep dive" / "Deep dive (deck)"); the article and deck share the source basename `dev-loops-deep-dive.html` under different `docs/` dirs, so the deck publishes as `dev-loops-deep-dive-deck.html` to avoid clobbering the article in `site/`. The Pages nav is now Intro article (landing) + Deep-dive article + Intro deck + Deep-dive deck. The two old deck Playwright specs/configs (`applied-deck`, `observability-deck`) and the `test:playwright:deck`/`:obs-deck` scripts are replaced by one `deep-dive-deck.spec.mjs` + `playwright.dev-loops-deep-dive.config.mjs` + `test:playwright:deep-dive` (section-ids, 390px mobile fit/no-clip, CSP, guard-the-guard). The intro article's "Where to go deeper", `docs/index.md`, and `docs/presentations/README.md` point at the consolidated pages. The A/B-contrast deslop step was applied to the merged prose (new part-divider headlines and bridge sentences kept clean; the source pieces were already deslopped).
- **Local-first low-noise posture made the coherent default (#953, builds on #949/#950/#951/#952).** The shipped extension-defaults layer (`packages/core/src/config/extension-defaults.yaml`) — the local-first opinion that sits above the built-in github-first code defaults and below repo config — now sets three existing knobs to their low-noise values, each with an inline intent comment: `autonomy.humanMergeOnly: true` (local-first never auto-merges; a human always merges), `queue.maxAutoFiledIssues: 1` (local-first is PR-first per #952, so auto-filing issues is near-zero; a low cap keeps tracker noise minimal), and an explicit `gates.postFindingsComments: true` (gate findings live ON the PR as review evidence, not tracker noise — keep them on). No new resolver and no new `strategy → knob` coupling: the values come purely from the existing config-merge layering (built-in < extension < repo `.pi/dev-loop/defaults.*` < repo `.devloops`), which already permits all three knobs at the file/extension layer (the runtime Zod schema needed no change). The published JSON-schema artifact `schemas/dev-loop-config.schema.json` was reconciled to match that runtime contract — its `gates` block (which used `additionalProperties: false`) now lists `postFindingsComments` plus the previously-missing `requireFanoutEvidence`/`maxFanoutReviewers`, so the schema no longer rejects the shipped config. The github-first/built-in posture is unchanged — `BUILT_IN_DEFAULTS` still yields `humanMergeOnly: false`, `maxAutoFiledIssues: 10`, and a resolved `postFindingsComments: true`. The post-promotion draft→pre-approval→human-merge flow is untouched. `node:test` coverage locks it: a merged-config assertion (`humanMergeOnly === true`, `maxAutoFiledIssues === 1`, `postFindingsComments === true` under the shipped defaults), built-in-default assertions for the unchanged github-first posture, and an explicit local-first phase-doc intake test (strategy from the shipped extension default, inputSource phase-docs from repo settings) asserting via a logging `gh` stub that NO `gh issue create` / `gh pr create` and NO Copilot request fire before promotion.
- **Both presentation decks deslopped — shorter headlines + A/B-contrast prose removed** (#936, applies the #944 standard step). Every slide headline on `docs/presentations/applied-dev-loops.html` and `docs/presentations/process-observability.html` was shortened to a short 3–7 word claim and stripped of the binary-contrast antipattern: "The Work Is One Loop Inside Another — and a Handoff Is Never Guessed" → "Loops Inside Loops"; "Prompt-Only Workflows Drift; a State Graph Can't" → "State Graphs Pin Behavior"; "One Interrupt Costs Five Transitions, Not Five Minutes" → "One Interrupt, Five Transitions"; "Those Four Fields Aren't a Wish — They're Where the Work Already Lives" → "The Fields Already Live in the Work"; "The Cheapest Speed-Up Is Making the Waiting Visible" → "Make the Waiting Visible" (and the rest). Slide body text in the publishable HTML renders lost the same "X, not Y" / negation-by-contrast construction in both orderings ("verified, never assumed" → "verified against the real result"; "don't add up. They multiply" → "multiply"; "Stop optimizing… Start measuring…" → "Measure how long the work waits"), with load-bearing distinctions kept plain. The HTML renders are the published source of truth; the Slidev `*-presentation.md` sources mirror the shortened headlines only (their body text already predates the HTML restructure — see `docs/presentations/README.md`). The dark visual identity, section ids, CSP guard, mobile-fit responsiveness, and all six Playwright deck tests are unchanged. Deploys to GitHub Pages on merge.

### Added
- **First-class `--jq` field-selection, `--silent`/`-s` exit-code checks, and concise output across the read/action scripts** (#981, subsumes #963). One shared helper `scripts/lib/jq-output.mjs` (`evaluateJqFilter`, `emitResult`, `JQ_OUTPUT_PARSE_OPTIONS`, `JQ_OUTPUT_USAGE`, `JqFilterError`) gives the loop a token-economical way to read tool JSON so it never falls back to `gh api | python3` or inline `node -e`. `--jq <filter>` applies a gh-style jq-subset filter (field access, `.a.b` chains, `.[]`/`.field[]` iteration, `.[N]` index, `|` pipes, `select(...)`, `==`/`!=`/`<`/`<=`/`>`/`>=`, `length`, `keys`) to the script's result and prints only the filtered value; an unsupported filter fails closed (stderr + exit `2`). `--silent`/`-s` suppresses stdout and maps the result to an exit code only (`0` pass/truthy, `1` fail/falsy) for zero-output yes/no checks — composing with `--jq` as a predicate (`… --jq '.ciStatus=="success"' -s; echo $?`), where an invalid filter still fails closed at exit `2`, distinct from a clean predicate-false silent exit `1`. Without these flags every script's JSON shape is unchanged. The full subset (not full jq — overkill for the loop's field/predicate needs) is applied uniformly to `scripts/github/capture-review-threads.mjs`, `scripts/loop/copilot-pr-handoff.mjs`, `scripts/loop/pr-runner-coordination.mjs`, `scripts/github/upsert-checkpoint-verdict.mjs`, and the queue scripts `scripts/projects/add-queue-item.mjs`/`move-queue-item.mjs`/`list-queue-items.mjs` (the #963 operator-action-scripts-JSON-only goal folded in here). In addition, the two scripts that emitted a large blob with no concise mode — `scripts/loop/run-watch-cycle.mjs` and `scripts/github/probe-copilot-review.mjs` — gain a `--concise`/`--summary` human-readable mode covering loop state, Copilot round count, unresolved/actionable thread counts, round-cap-clean eligibility, CI status, next action, and (the field `loop info --pr` omits) the current round's NEW Copilot comment bodies. The token-economical reading convention (subcommand/concise → `--silent` → `--jq` → `gh --jq` → never `| python3`/`node -e`) is documented in `skills/dev-loop/SKILL.md` and referenced from the round-cap gate-cadence rule. `node:test` coverage: a helper unit spec (subset extraction success, fail-closed on unsupported syntax, `emitResult` jq/silent/invalid-filter exit-code semantics), an end-to-end CLI spec (`--jq` extraction, invalid filter exit `2` + stderr, `--silent` pass exit `0` silent, predicate-false exit `1` silent, silent+invalid-filter exit `2`, unchanged JSON shape), and concise-mode specs for run-watch-cycle/probe-copilot-review.
- **Developer-mode internal-tooling-only retrospective check (#982).** A new opt-in workflow flag `workflow.requireRetrospectiveInternalTooling` (default **OFF**, defined in `packages/core/src/config/config.mjs`, shipped explicitly false in `extension-defaults.yaml`, declared in `schemas/dev-loop-config.schema.json`) gates an internal-tooling-only check in the retrospective merge gate. **It is scoped to developer mode — the dev-loops maintainers dogfooding the tooling on themselves — and never blocks consumers** of the extension, who may legitimately use raw `gh`/`python`/`node -e` in their own workflow. When the flag is ON, the merge-approval evaluator (`evaluateRetrospectiveMergeApproval(checkpoint, { developerMode })` in `packages/core/src/loop/pr-gate-coordination.mjs`, routed through at every merge-ready boundary in `evaluatePrGateCoordination`) requires `behavioralReview.internalToolingOnly: true` and `behavioralReview.rawCallViolations: []`, blocking (`retrospective_gate_pending`) when `internalToolingOnly` is not `true` **OR** `rawCallViolations` is missing/non-empty. When the flag is OFF (the consumer default) the check is inert: a complete, merge-approved checkpoint passes exactly as before — even if it omits the fields or records a violation — so a consumer's state changes are never blocked (this also resolves the draft-review back-compat concern: old checkpoints only fail closed in developer mode). The dev-loops repo opts in via its own repo-root `.devloops` (`workflow.requireRetrospectiveInternalTooling: true`), so the discipline still applies to our own retros. The rule treats agent-level top-level raw `gh` (incl. `gh api`/`--jq`), `python`/`python3`, and `node -e`/`node --eval` as the same breach; `node scripts/*.mjs` and dev-loops subcommands are allowed (the scripts call gh/GraphQL internally — that IS the tooling). The dependency-free, deterministic verifier `scripts/loop/check-retro-tooling.mjs` (pure `analyzeTranscript(transcript)` export) is unchanged: it reads a newline-delimited transcript of the agent's shell commands (`--transcript <path>` or stdin) and returns the `rawCallViolations` list, distinguishing script-internal gh from agent-level raw calls and keeping a small explicit write-op allowlist (`gh pr merge`/`gh pr ready`/`gh issue create`/`gh issue edit`, recorded as `allowedWriteOps`). `node:test` coverage: developer-mode-ON gate tests for a recorded violation / `internalToolingOnly:false` / missing-field-old-checkpoint (all block) and a clean record (allows `FINAL_APPROVAL_READY`), the key developer-mode-OFF test asserting a consumer is NOT blocked even with missing fields OR a recorded violation, plus the unchanged verifier spec (`test/loop/check-retro-tooling.test.mjs`). The retrospective-checkpoint contract doc (`skills/docs/retrospective-checkpoint-contract.md` + generated `.claude/` mirror) documents the developer-mode scoping, the opt-in flag, and that consumers are never blocked. Known verifier limitation: segment splitting does not parse shell quoting, so a separator inside a quoted argument can over-report (never under-report) — the fail-closed direction.
- **"Introducing dev-loops" presentation deck** (#973, companion to the #971 overview article). A self-contained, CSP-safe `docs/presentations/introducing-dev-loops.html` (9 slides: the coordination-delay problem, how manual handoffs compound, every handoff a recorded decision, one bounded decision per change, model-agnostic by construction (open-source models drive the work), the aggregate proof data, install/run, and the companion pieces) reusing the existing decks’ dark glass-card design system, CSP meta, and mobile-fit responsiveness. Added to `scripts/pages/build-site.mjs` `DECKS` (navLabel "Intro (deck)") so it publishes and joins the shared Pages nav; a mirror Playwright spec + config + `test:playwright:intro-deck` script match the existing per-deck tests. Aggregate data only (no internal PR/issue numbers or script paths in slide prose); the A/B-contrast deslop step was applied.
- **Model-agnostic section in the intro article + open-source model proof** (#979). A new "Model-agnostic by construction" section in `docs/articles/introducing-dev-loops.md`/`.html` (the Pages landing page) explains that the guardrails — every handoff a recorded decision, the same draft → review → green-CI gate, fail-closed on ambiguity, a person merging — bound how far any single step can stray, so a strong open-source model can drive most or all of the work (cheaper, self-hostable, no single-vendor dependence; the gate holds the quality bar). Names the concrete proof: the loop has driven real work end to end on DeepSeek V4, Kimi K2.6, MiniMax M3, Qwen 3.6, and GLM 5.2. Aggregate/conceptual only; A/B-contrast deslop applied.

- **Spike-mode contract doc + worked example** (#966, docs-only; phase 3 of 3 of the spike-mode track #955, closing it; builds on #964/#965). A new canonical operator-sequence doc [`skills/docs/spike-mode-contract.md`](skills/docs/spike-mode-contract.md) walks spike mode end to end against the merged surfaces: author the exploration scaffold and validate it (`scripts/refine/validate-spike-file.mjs`, base sections Question/Approach/Findings/Recommendation), start it (`scripts/loop/resolve-dev-loop-startup.mjs --spike`, `buildSpikeInput` + `evaluateSpikeIntakeState`, intake states `spike_in_progress`/`spike_ready_for_exit`/`ambiguous_fail_closed`), run it under the relaxed `gates.spike` profile, then exit via `scripts/refine/exit-spike.mjs` with disposition `discard` (zero tracker artifacts; the findings doc is the whole record) or `graduate` (`buildGraduatedPlanBody` emits a #947-consumable plan file that enters the existing plan→PR promotion path #952). The doc documents the relaxed gate profile and its rationale — `angles: [scope, docs]`, `required: false`, `requireCi: false` (`packages/core/src/config/extension-defaults.yaml`), resolved through the same `resolveGateConfig(config, "spike")` path and absent for non-spike work — and carries one worked example (`spike-cache.md`) from a question through findings to BOTH exits, showing the actual commands/states and, for graduate, the resulting plan-file body. Cross-referenced from the [Artifact Authority Contract](skills/docs/artifact-authority-contract.md) relationship table and `docs/index.md`; the generated `.claude/skills/docs/` mirror was regenerated. No behavior, schema, defaults, or scripts changed.
- **Spike-mode relaxed gate profile + discard/graduate exits** (#965, phase 2 of the spike-mode track #955; builds on #964 and reuses #951/#952/#953). A spike is now runnable to a conclusion: a lighter gate posture plus an explicit exit that either discards (zero tracker artifacts) or graduates (emits a #947-consumable plan file). **Relaxed gate profile (config):** a new `gates.spike` profile (same `GateConfig` shape as `gates.draft`/`gates.preApproval`) resolved through the SAME `resolveGateConfig(config, "spike")` path and the existing config-merge layering — no new `strategy → knob` resolver. The shipped extension default (`packages/core/src/config/extension-defaults.yaml`) is intentionally lighter than the production draft → pre-approval → Copilot set: a small docs-first angle set (`scope`, `docs`), `required: false`, and `requireCi: false`, because a spike's deliverable is a findings doc, not production code. Runtime zod (`GatesConfig`/`FileGatesConfig` in `packages/core/src/config/config.mjs`) and the published JSON-schema artifact (`schemas/dev-loop-config.schema.json`, `gates.spike` → `$defs/gateConfig`) were kept in sync (#953's lesson). The github-first/production posture is UNCHANGED: `draft`/`preApproval` stay `required: true`/`requireCi: true`, and `gates.spike` is absent for non-spike work. **Discard/graduate exit contract (pure):** a new `packages/core/src/loop/spike-exit-contract.mjs` (no `scripts/` import, no fs/network/gh; mirrors spike-intake-contract) exports frozen `SPIKE_EXIT_DISPOSITION`/`SPIKE_EXIT_ACTION` plus `evaluateSpikeExit({ spikeIntakeState, disposition }) -> { ok, action?, reason?, spikeIntakeState? }`, eligible ONLY from `spike_ready_for_exit` and failing closed with `not_ready_for_exit` (non-ready/ambiguous state) or `unknown_disposition` (anything but `discard`/`graduate`). `buildGraduatedPlanBody({ question, approach, findings, recommendation })` maps the spike's four sections onto a #947-consumable plan-file body with the four base authoring sections (Status/Objective/In scope/Explicit non-goals) so it passes `validatePlanFile` and enters the existing plan→PR promotion path (#952) unchanged; it is idempotent and fails closed (throws) on an empty required section. The export is added to `packages/core/package.json`. **Thin CLI:** `scripts/refine/exit-spike.mjs` (`--spike-file`/`--disposition`/`--plan-file`/`--json`, mirroring promote-plan.mjs) owns all I/O — it reads the spike, computes intake-state facts via the P1 surfaces (`validateSpikeExplorationSections` for the scaffold + the Recommendation exit-marker), classifies via the pure contract, and on graduate writes the local-first plan file; discard creates ZERO tracker artifacts and graduation opens nothing on the tracker (it only writes a local plan file, then promotes via the existing path). Structured success/error JSON matches the sibling refine/promote scripts. `node:test` coverage: the pure exit contract (discard/graduate eligible from ready; fail-closed on unknown disposition + non-ready/ambiguous/missing state) + the plan-body builder (base-valid output passes `validatePlanFile`, carries the spike content, idempotent, throws on empty sections); the gate-profile resolution (spike profile resolves; shipped default is relaxed and lighter than draft, with draft/preApproval unchanged); and the CLI (discard → empty gh-stub log + exit 0, graduate → base-valid plan file written + empty gh log + idempotent re-run, fail-closed not-ready/unknown-disposition → empty gh log + exit 1, arg validation). Narrative skill docs are #966 (NOT in this phase); no production-flow change for non-spike work.
- **Top-of-funnel article "Introducing dev-loops" + CSP-safe HTML render** (#971, sibling of the #942/#943 article PRs). `docs/articles/introducing-dev-loops.md` is the single what/why/how-do-I-start entry point the two concept articles lacked: a plain-language introduction to the idea (coordination delay is where AI-assisted lead time goes; the loop makes every handoff an explicit, recorded decision; a person merges by default; work starts from a durable plan or issue), a "what it does to the work" section that uses the repository’s own ~two-week history (~100 merged PRs, ~7/day, about seven in eight tied to a tracked item, every one drafted, reviewed, green-CI gated, and human-merged) to show the straightforward, repeatable process the loop produces, and concrete setup instructions (the Claude Code plugin slash commands, the Pi extension install, the plain-language `start/auto/continue dev loop` entrypoint, and a minimal `.devloops` covering start-local vs issue intake, Copilot rounds on/off, and human-merge). A self-contained, CSP-safe `docs/articles/introducing-dev-loops.html` ports the dark glass-card identity from the sibling renders (no font/CDN/remote assets). Cross-linked from a new Articles section in `docs/index.md`, and the two existing articles gain a one-line "start here" pointer so they read as deep dives beneath the intro. Aggregate figures are gh-verified; the A/B-contrast deslop step (`docs/ab-contrast-deslop-step.md`) was applied (zero flagged constructions). The intro article is also made the **GitHub Pages landing page**: `scripts/pages/build-site.mjs` now publishes `docs/articles/introducing-dev-loops.html` as `site/index.html` (superseding the prior generated "Presentation Decks" card index from #930), publishes the two deep-dive articles and two decks alongside it, and injects a shared navigation bar into the article pages linking the other resources (`injectNav`, which fails closed if a page lacks its `<style>`/`<body>` anchors); the decks are copied as-is. `test/pages/build-site.test.mjs` updated. On merge, `pages.yml` rebuilds and deploys the new landing page.
- **Spike-mode entry + findings-artifact format** (#964, phase 1 of the spike-mode track #955; reuses #949/#950). A spike is a time-boxed exploratory loop startable from a local question with no GitHub issue. A new spike artifact format mirrors the plan-file format with base sections **Question / Approach / Findings / Recommendation**; a focused sibling validator `scripts/refine/validate-spike-file.mjs` exports `validateSpikeFile(markdownText) -> { checker: "validate-spike-file", ok, errors }` with a distinct `missing_*` code per absent/empty section (`missing_question`/`missing_approach`/`missing_findings`/`missing_recommendation`) plus a thin `--input`/`--json`/`--help` CLI mirroring `validate-plan-file.mjs`. The base-section-checking loop is shared with `validatePlanFile` via a small parameterized helper `checkBaseSections(markdownText, checker, sectionCodes)` in `scripts/refine/_refine-helpers.mjs` (DRY without forcing a shared abstraction across the distinct heading sets). `scripts/loop/resolve-dev-loop-startup.mjs` gains `--spike <path>`, mutually exclusive with `--issue`/`--pr`/`--input`/`--plan-file`: it reuses the `local_implementation` strategy and the existing `local_phase` target as a work-origin addition, makes ZERO `gh` calls / no tracker artifacts at entry (no production-gate ceremony), is exempt from the worktree-isolation `needs_reconcile` guard (a spike has no issue to key a worktree on), and fails closed (exit 1, no bundle) on a missing/unreadable spike file or one missing the exploration scaffold (Question/Approach/Findings). A new pure, deterministic contract `packages/core/src/loop/spike-intake-contract.mjs` (no `scripts/` import, no fs/network/gh) exports a frozen `SPIKE_INTAKE_STATE` and `evaluateSpikeIntakeState({ baseSectionsValid, hasRecommendation }) -> { state }` classifying the spike as `spike_in_progress` (exploration ongoing, no Recommendation yet — phase 2's discard exit) or `spike_ready_for_exit` (Recommendation reached — phase 2's discard/graduate exit seam), failing closed (`ambiguous_fail_closed`) on a malformed artifact. These states are DISTINCT from `PLAN_FILE_INTAKE_STATE` (a spike is not a plan-needing-refinement). The resolver threads the resulting `spikeIntakeState` onto its JSON output and the new core export is added to `packages/core/package.json`. `node:test` coverage: the pure contract spec, the validator spec (all sections ok / each missing→its code / empty-body / CLI), and resolver specs (valid spike → `spike_ready_for_exit` with zero `gh` calls via the empty-log stub, in-progress spike → `spike_in_progress`, missing/malformed → fail closed, mutual exclusivity with `--issue`/`--plan-file`). The relaxed gate profile + discard/graduate exits (#965) and narrative docs (#966) are NOT in this phase; no production-flow change.
- **Local-first plan-file flow documented end to end + worked example** (#954, docs-only; builds on #949/#950/#951/#952/#953). The canonical [Artifact Authority Contract](skills/docs/artifact-authority-contract.md) (at `skills/docs/`; the `docs/` path in the original stub was stale) now documents the full local-first flow across P1–P5: the plan-file artifact + `localPlanning.plansDir` default (`docs/phases/`) and validator from P1; the `--plan-file` startup entry + `PLAN_FILE_INTAKE_STATE` states (`new_plan_needs_refinement` / `plan_refined_ready_for_promotion` / `ambiguous_fail_closed`) from P2; the in-place refine + `local_human_review` checkpoint (AC/DoD/`Coverage matrix`/`Docs-grill findings` sections) from P3; PR-first promotion + the bidirectional plan↔PR link (`prNumber` front-matter, `already_promoted` idempotency) from P4; and the low-noise extension defaults from P5. The contract was reconciled to the shipped posture: the effective default is `strategy.default: local-first` from `packages/core/src/config/extension-defaults.yaml` (decision #7 of #947), which sits above the `BUILT_IN_DEFAULTS` github-first fallback in `packages/core/src/config/config.mjs`; the stale `.pi/dev-loop/defaults.yaml` shipped-default claim and the "dev-loops is tracker-first" statement were corrected (the package ships defaults via the extension layer and the repo's own `.devloops` sets `local-first`). A new operator-sequence skill doc [`skills/docs/local-planning-flow.md`](skills/docs/local-planning-flow.md) walks validate-plan-file → resolve-dev-loop-startup `--plan-file` → refine-plan-file → promote-plan, and a worked example [`skills/docs/local-planning-worked-example.md`](skills/docs/local-planning-worked-example.md) shows one plan file (`docs/phases/phase-42.md`) evolving through every stage (base sections → refinement sections + coverage matrix + docs-grill findings → `prNumber` front-matter). All three docs are reachable from `docs/index.md` and cross-link the contract; the generated `.claude/skills/docs/` mirror was regenerated. No behavior, schema, defaults, or scripts changed.
- **Local-first PR-FIRST plan promotion — no issue ever minted** (#952, builds on #949/#950/#951). A pure contract `packages/core/src/loop/plan-file-promote-contract.mjs` exports `evaluatePromoteEligibility({ baseSectionsValid, hasAcceptanceCriteria, hasDefinitionOfDone, existingPrNumber }) -> { ok, action, reason, planFileIntakeState, existingPrNumber }` (`PLAN_FILE_PROMOTE_ACTION` `promote`/`already_promoted`), `buildPromotionPrBody({ planDocPath, acceptanceCriteria, definitionOfDone })`, and a minimal additive plan front-matter parser/serializer (`parsePlanFrontMatter`, `readLinkedPrNumber`, `writeLinkedPrNumber`, `PLAN_FILE_PR_FRONT_MATTER_KEY`). Eligibility is fail-closed: promotion acts ONLY on P3's `plan_refined_ready_for_promotion` state (composed via P2's `evaluatePlanFileIntakeState`); any other state returns `not_ready_for_promotion` with no action so the caller makes zero GitHub mutation. The PR body is the self-contained spec-of-record — it references the committed plan-doc path and carries the FULL `Acceptance criteria` + `Definition of done` extracted from the refined plan, and never an issue-close keyword (no issue is minted). The module is pure (no GitHub, no network, no filesystem I/O). A thin CLI `scripts/refine/promote-plan.mjs` (`--plan-file <path> [--base <branch>] [--branch <name>] [--json]`) owns all I/O: it reads the plan, computes the section-presence facts with the P1 `validatePlanFile`/`extractSection` surfaces, runs the eligibility gate, and on `promote` commits the plan doc to a branch and opens EXACTLY ONE draft PR via the canonical `scripts/github/create-pr.mjs` wrapper (always `--draft`, self-assigned; never raw `gh pr create`), then writes the returned PR number back into the plan's `prNumber:` front-matter and commits the link — the bidirectional plan↔PR link (doc path in PR body, PR number in plan front-matter). The opened draft PR is a normal-shaped PR that enters the existing loop unchanged via `loop startup --pr <n>`; no new lifecycle state and no separate authority artifact. Idempotent: a plan already carrying `prNumber` returns `already_promoted`, opens nothing, and reports the existing PR. The front-matter is a minimal additive extension to P1's plan-file format (documented in `skills/docs/plan-file-contract.md`): plans without a leading `---` block are unchanged and fully valid. A `node:test` contract spec plus a CLI spec cover the ready-gate (refuse non-ready/ambiguous/partially-refined with zero gh calls), the single draft-PR open (one `gh pr create --draft`, asserted via a logging `gh` stub), the plan-doc commit + bidirectional link, idempotency, the fail-closed reasons, zero-pre-promotion activity (empty gh-stub log on refusal and on the idempotent no-op), and the PR body carrying the full AC + DoD.
- **Local-first refine + human-review checkpoint (off-tracker)** (#951, builds on #949/#950, consumes #948). A pure contract `packages/core/src/loop/plan-file-refine-contract.mjs` exports `refinePlanFileInPlace({ markdownText, baseSectionsValid, hasAcceptanceCriteria, hasDefinitionOfDone, payload }) -> { ok, planFileIntakeState, refinedMarkdown, grillDispositions, stop }` plus `PLAN_FILE_REFINE_STOP`/`DOCS_GRILL_FINDINGS_HEADING`/`COVERAGE_MATRIX_HEADING`. It generalizes the proposal-first intake pattern (emit a local artifact, human-gate it, make zero tracker mutation, stop and ask) to plan files: starting from P2's `new_plan_needs_refinement`, it writes the refiner-produced `Acceptance criteria`, `Definition of done`, `Coverage matrix`, and recorded `Docs-grill findings` sections in place into the single canonical plan file, advances P2's intake state to `plan_refined_ready_for_promotion` via `evaluatePlanFileIntakeState`, and returns a `local_human_review` stop so the loop pauses for human review before any promotion. The docs-grill runs as a step of refinement: each finding is classified with #948's `classifyDocsGrillFinding` and recorded into the plan file, and a malformed finding fails the grill closed. The rewrite is idempotent — a re-run strips any prior copy of each refinement section before appending, so the section count stays at one each. It is pure (no GitHub, no network, no filesystem I/O): the caller supplies the section-presence facts and the refiner payload and writes the returned markdown back, keeping the zero-tracker-mutation guarantee structural. Fail-closed paths cover a non-`new_plan_needs_refinement` starting state, an ambiguous/base-invalid plan, missing AC/DoD/coverage-matrix payload pieces, and a failed grill — each surfaces a reason and advances/writes nothing. A thin CLI `scripts/refine/refine-plan-file.mjs` (`--plan-file <path> --payload <path> [--json]`) reads the plan and the refiner output, computes the facts with the P1 `validatePlanFile`/`extractSection` surfaces, calls the contract, writes the refined plan back in place on success (exit 0, `local_human_review` stop), and fails closed without writing on a fail-closed reason (exit 1). A `node:test` contract spec plus a CLI spec cover refine-from-needs-refinement, the docs-grill-as-a-step recording, in-place idempotency, state advance + local stop, the fail-closed cases, and a zero-tracker-mutation assertion (the logging `gh` stub's call log is empty across the whole path).
- **Autonomous in-loop docs-grill formalized as a standard step** (#948, sibling of #944 and #929). `docs/docs-grill-step.md` documents the repeatable step that interrogates a change against the repo's own contracts/docs while the loop runs — without a manual main-agent pass: what it checks (claims vs contracts, code-vs-doc drift, stale references, contract-surface accuracy), where it fires (the [refiner](agents/refiner.agent.md) cross-checks the active phase against the contracts it references during refinement, and the existing `docs` pre-approval angle resolves to the [docs persona](agents/docs.agent.md) review mode as one fan-out angle of the [gate review sub-loop](docs/gate-review-sub-loop-contract.md)), and the keep/fix rule. The rule is codified as a pure classifier `classifyDocsGrillFinding` in `scripts/loop/docs-grill-contract.mjs` (`DOCS_GRILL_FINDING_KINDS` `drift`/`stale_reference`/`cosmetic`; `DOCS_GRILL_DISPOSITIONS` `record_finding`/`fix_in_place`/`route_followup`/`ignore_cosmetic`): real drift between code/behavior and a contract claim is recorded as a finding, doc-only drift is fixed in place or routed as a follow-up, and a cosmetic nit never blocks a gate; an unknown kind fails closed (`invalid_finding`). The refiner agent gains an explicit docs cross-check line pointing at the step. The two firing surfaces already ran in-loop, so this captures the contract without adding a new entrypoint. The local-first epic (#947) tree refinement is recorded as the first run (each node was grilled against the contracts it reuses during the refiner+grill fan-out). Cross-linked from `docs/index.md`.
- **`--plan-file` startup entry + local-planning intake state machine** (#950, builds on #949). `scripts/loop/resolve-dev-loop-startup.mjs` gains `--plan-file <path>`, mutually exclusive with `--issue`/`--pr`/`--input`. It reuses the `local_implementation` strategy and the existing `local_phase` target as a work-origin addition: a valid plan resolves to a `local_phase` bundle with the plan path carried as the target `phase` and no issue/PR number. The path is read-only (zero tracker mutation, no `gh` calls) and exempt from the worktree-isolation `needs_reconcile` guard, because a pre-promotion plan has no issue to key a worktree on. A missing/unreadable plan, or one failing the `validatePlanFile` base-section contract, fails closed (exit 1, no readiness bundle). A new pure, deterministic intake evaluator `evaluatePlanFileIntakeState({ baseSectionsValid, hasAcceptanceCriteria, hasDefinitionOfDone }) -> { state }` (frozen `PLAN_FILE_INTAKE_STATE` enum, no I/O) in `packages/core/src/loop/plan-file-intake-contract.mjs` classifies the plan as `new_plan_needs_refinement` (base sections only), `plan_refined_ready_for_promotion` (base sections plus `Acceptance criteria` + `Definition of done`), or `ambiguous_fail_closed` (invalid base, or exactly one of the two refinement sections). The resolver detects refined-vs-needs-refinement via `extractSection` for each `PLAN_FILE_REFINEMENT_SECTIONS` heading and threads the resulting `planFileIntakeState` onto its JSON output.
- **Plan-file contract + validator for local-planning mode** (#949). The persisted markdown plan file reuses the existing phase-doc format under `docs/phases/`; a new `localPlanning` config family adds `plansDir` (default `docs/phases/`) across all four config locations (`DevLoopConfigSchema`, `FileConfigSchema`, `BUILT_IN_DEFAULTS`, and the packaged `extension-defaults.yaml`), with `resolvePlansDir(config)` returning the configured directory or the default. A pure validator `scripts/refine/validate-plan-file.mjs` exports `validatePlanFile(markdownText) -> { checker: "validate-plan-file", ok, errors }` that checks the base authoring sections a plan carries before refinement (`Status`, `Objective`, `In scope`, `Explicit non-goals`), reporting each absent or empty-body section under a distinct `missing_*` code; its thin CLI takes `--input`/`--json`/`--help`, reports the verdict in the JSON payload, and exits non-zero on argument or path errors. The format and required sections are documented in `skills/docs/plan-file-contract.md` (mirrored to `.claude/skills/docs/`) and linked from the Artifact Authority Contract.
- **A/B (binary-contrast) prose removal formalized as a standard deslop step + applied to the two articles** (#944). `docs/ab-contrast-deslop-step.md` documents a repeatable editorial step that removes the binary-contrast / negation-by-contrast antipattern in both orderings ("X, not Y" and "Y, not X") — the strongest single AI tell in generated prose: the detection spec, the parallel-analysis → final-check → human-likeness flow, and the keep-load-bearing-distinctions/cut-the-scaffolding rewrite rule (with a keep-vs-cut table). It is the first documented sub-step of the broader deslop step tracked in #936. Applied as the first run to both Medium articles (`eliminating-coordination-delay`, `make-the-waiting-visible`) and their HTML renders: an audit found ~120 constructions across the articles and decks; the article prose now states each point directly while keeping real technical distinctions plain (the merge signal stays read-from-CI, automation stays state-gated), followed by a human-likeness pass to keep the cadence natural. Each article's `-notes.md` records the pass; a residual check finds zero flagged constructions in the article `.md`/`.html`.
- **Presentation decks publishable to GitHub Pages** (#930). `.github/workflows/pages.yml` runs the standard Pages pipeline (`actions/configure-pages` + `actions/upload-pages-artifact` + `actions/deploy-pages`, with `permissions: { pages: write, id-token: write, contents: read }` and `concurrency: { group: pages }`). A committed, reusable build script (`scripts/pages/build-site.mjs`, exported `buildSite`/`DECKS`) deterministically assembles `site/` — copying the self-contained deck renders (`docs/presentations/applied-dev-loops.html`, `process-observability.html`) and generating a CSP-safe dark-aesthetic `site/index.html` linking both. The deck HTML stays the single source of truth; `site/` is assembled (gitignored), never hand-maintained. The **build** job (assemble + upload, which validates the assembly) runs on push to `main` + `workflow_dispatch`. At the time of this PR the **deploy** job was gated to `workflow_dispatch` (`if: github.event_name == 'workflow_dispatch'`) until an operator enabled Pages, with the build job validating the assembly on every push; **#941 (below) superseded that gate** once Pages was enabled, so the deploy job now auto-publishes on every push to `main`. `docs/presentations/README.md` documents the decks, local viewing, the deploy flow, and the private-repo plan caveat (a public Pages site from a private repo needs Pro/Team/Enterprise). Also adds the missing `@media (prefers-reduced-motion: reduce)` guard to `applied-dev-loops.html` for parity with the observability deck (disables `scroll-behavior: smooth` on `html`, `scroll-snap-type` on `body`, `scroll-snap-align` on `.slide`). A `node:test` spec (`test/pages/build-site.test.mjs`) asserts the build produces `index.html` + both decks and that the index links both.
- **Medium-ready long-form article: "Eliminating Coordination Delay in AI-Assisted Dev Workflows"** (#934). `docs/articles/eliminating-coordination-delay.md` is a public-audience, ~1500-word prose piece derived from the Applied dev-loops narrative — zoomed-out and durable (no version-pegging, no raw enum/identifier dumps): hook (cheap code, leaky guessed handoffs) → the one idea (never guess a handoff; make every handoff an explicit, observable decision) → what it buys (safe pauses, mid-flight steering, parallel review → one consolidated verdict, "done" that means merged and read from the real merge signal) → why a state graph beats prompt-only → a one-line close. It carries four captioned `mermaid` diagrams (nested-loops state diagram, PR gate lifecycle flowchart, fan-out/fan-in evidence→verdict flow, mid-flight steering flow) plus a "Rendering the diagrams on Medium" note. A self-contained, CSP-safe preview render `docs/articles/eliminating-coordination-delay.html` ports the dark glass-card identity (navy gradient, glass cards, violet accent, blue kicker, system fonts) in an article layout (~65ch measure) with the four diagrams as inline CSS-flow / inline SVG (no mermaid runtime, no remote resources, wide diagrams scroll in their own container). One prose-adapted storytelling/editorial review pass is recorded and applied in `docs/articles/eliminating-coordination-delay-notes.md`.
- **Long-form article "Make the Waiting Visible" + a self-contained HTML preview render** (#935). `docs/articles/make-the-waiting-visible.md` adapts the [Process Observability deck](docs/presentations/process-observability-presentation.md) into a Medium-ready public-audience essay (~1.6k words, optional title/dek/tags front-matter): AI writes code in seconds, then the work *sits* — the slow part is the invisible waiting between actions. The arc runs hook → interrupt cost (one interrupt = five transitions) → handoffs restart discovery → git history hides the waiting → make state observable (owner / blocker / latest decision / safe next step) → measure → change → verify → ground it in real mechanisms (board lifecycle, gate evidence trail, deterministic next-action resolver, provider-agnostic CI waits, post-merge reclaim/archive) → close (*"The next agent will write your code in seconds. The lever you control is how long it waits afterward, so measure that."*). Four captioned `mermaid` diagrams (interrupt-cost chain, handoff round trip, measurement loop, observable-state grounding) plus a "rendering on Medium" note. A standalone, CSP-safe `docs/articles/make-the-waiting-visible.html` renders the full article in the dark glass-card identity (navy gradient, glass cards, violet accent, blue kicker, system fonts) as an article layout (~65ch measure), drawing the four diagrams as inline CSS flow / inline SVG — no mermaid runtime, no font/CDN/remote assets. One editorial/storytelling review pass (lens from [docs/slides-story-review-loop.md](docs/slides-story-review-loop.md), adapted for prose) is recorded and applied in `docs/articles/make-the-waiting-visible-notes.md`.
- **Slides content & storytelling review loop formalized as a bounded reviewer mode** (#929). A sibling of the [UI Designer + Vision Review Loop](docs/ui-designer-review-loop.md) behind `dev-loop` that judges a deck's *narrative*, not its pixels — "does it land?" rather than "does it look right?". `docs/slides-story-review-loop.md` defines the contract: the public entrypoint/dependency boundary (no new public name), a fail-closed REQUIRED INPUT BUNDLE (deck source path + slice-level acceptance criteria + a short storytelling brief + optional captured slide screenshots from the UI smoke harness), the public-audience REVIEW LENS (arc/hook/close, one message per slide + claim titles, sequencing/no-forward-refs, jargon translation, cut/merge/reorder), and a REQUIRED OUTPUT BUNDLE (findings + corrective actions + a single structured outcome `story_review_satisfied` | `needs_iteration`). The pure module `scripts/loop/slides-story-review-contract.mjs` exports the outcome constants plus a fail-closed input-bundle validator and a result-shape validator (no I/O); the prompt template lives at `skills/dev-loop/templates/slides-story-review.md`. The two inline applications already run over the decks are recorded as the first two runs (`docs/presentations/applied-dev-loops-review-notes.md` #926, `docs/presentations/process-observability-review-notes.md` #927). Cross-linked from the UI loop doc, the README, and `docs/index.md`.
- **Process Observability deck refreshed + a self-contained shareable HTML render** (#927). `docs/presentations/process-observability-presentation.md` (Slidev) is restructured into one 9-slide public-audience story arc in the existing dark glass-card style: claim-style titles, one message per slide, jargon trimmed (`task state` / `pipeline latency` pills cut), the two overlapping handoff-cost slides merged, and a memorable close (*"Stop optimizing how fast you write code. Start measuring how long it waits."*) replacing the bare metric grid. A new grounding slide (`instrumented`) ties the abstract "observable state cuts delay" claim to what actually exists — the queue board lifecycle (owner / safe next step), the gate evidence trail (latest decision + findings), the deterministic next-action resolver, and "automate only where state supports safe continuation" via provider-agnostic CI waits and operator-induced post-merge worktree reclaim / long-done archival — in plain language, no version labels or raw identifiers. A standalone, CSP-safe `docs/presentations/process-observability.html` ships the full deck with all CSS inline (ported from `style.css`: navy gradient, glass cards, violet accent, blue kicker, mono pills) and the flowcharts rendered as inline CSS/HTML flow (no mermaid/CDN, no remote resources) — each slide is a stable-id `<section>` (`hero`, `interrupt-cost`, `handoff`, `blind-spot`, `observable-state`, `measurement-loop`, `instrumented`, `metrics`, `close`). A thin WebKit smoke spec (`test/playwright/observability-deck.spec.mjs` + `playwright.observability-deck.config.mjs`, `npm run test:playwright:obs-deck`) asserts every named section is present with no body horizontal overflow and captures the `hero`, `interrupt-cost`, `observable-state`, `measurement-loop`, `instrumented`, `metrics`, and `close` states under `test-results/ui-smoke/observability-deck/`; one designer/vision review pass (notes in `docs/presentations/process-observability-review-notes.md`, storytelling + visual sections) confirmed equal-height cards, legible flow diagrams, and a landing close with no corrective CSS required beyond the ported baseline.
- **Applied dev-loops deck refreshed for v0.4.0 + a self-contained shareable HTML render** (#926). `docs/presentations/applied-dev-loops-presentation.md` (Slidev) gains two slides in the existing dark glass-card style: the **gate fan-out/fan-in sub-loop** (build-once neutral bundle = full diff + 1-hop import adjacency, size-guarded; independent per-angle reviewers seeded with the identical bundle; `consolidateFanin` merges per-angle verdicts; no fork primitive / no Workflow dependency; fail-closed `fanout_fanin` verdict with enforced severity counts) and **the coordination runtime owning the full lifecycle** (enforced human merge via `autonomy.humanMergeOnly`, managed `tmp/worktrees/dev-loops/` worktrees, provider-agnostic `watch-ci`, queue board as a deterministic adapter). A standalone, CSP-safe `docs/presentations/applied-dev-loops.html` ships the full deck with all CSS inline and the mermaid diagrams rendered as inline CSS flow (no font/mermaid CDN, no remote resources) — each slide is a stable-id `<section>` for UI smoke targeting. A thin WebKit smoke spec (`test/playwright/applied-deck.spec.mjs` + `playwright.applied-deck.config.mjs`, `npm run test:playwright:deck`) captures the `hero`, `core-idea`, `parallel-review`, `trust`, and `impact` named states under `test-results/ui-smoke/applied-deck/`; one designer/vision review pass (notes in `docs/presentations/applied-dev-loops-review-notes.md`) fixed ragged card heights via equal-height grid rows. A follow-up **public-audience storytelling pass** restructured the deck to one ~8-slide narrative arc (claim-style titles, jargon translated to plain language, raw enum/pill walls cut to at most one identifier per slide as evidence, mechanism→outcome close) while keeping the dark glass-card visual identity unchanged.

### Fixed

- **`promote-plan` pushes the head branch before opening the PR, and the plan-commit step is re-runnable (#969).** Promotion committed the plan doc and then called `create-pr.mjs` (`gh pr create --head <branch>`) without first pushing the fresh local branch; when `gh pr create` did not auto-push, promotion failed `pr_create_failed` AFTER the plan commit had landed — an unrecoverable partial state (plan committed, no PR, no `prNumber`) where a re-run re-entered PROMOTE, found nothing to stage, and dead-ended on `git_commit_failed`. Fix in `scripts/refine/promote-plan.mjs`: (1) the head branch is now pushed with `git push -u origin <branch>` BEFORE invoking `create-pr.mjs`, so a fresh branch exists on the remote and `gh pr create --head` succeeds; a push failure fails closed with the new `git_push_failed` reason + detail and makes ZERO `gh` mutation. (2) The plan-commit step is idempotent: when the plan doc is already committed at HEAD (`git diff --cached --quiet` reports nothing staged), the commit is skipped instead of failing `git_commit_failed`, so a prior partial run recovers on a plain re-run — it pushes, opens the draft PR, and writes the plan↔PR link. The existing fail-closed ready-gate (zero `gh`), `already_promoted` idempotency, and the post-PR-open `failAfterPrOpen` link-commit recovery are unchanged. `test/loop/promote-plan.test.mjs` gains a real bare `origin` remote in its setup and three asserts: the fresh-branch run pushes the branch (asserted via `git ls-remote`) then opens exactly one draft PR, a partial state (plan committed at HEAD on the head branch, no `prNumber`) recovers on re-run with one `gh` call, and a missing-remote push failure surfaces `git_push_failed` with a detail and zero `gh` calls.
- **Decks fit the phone screen — no horizontal scroll, no clipped content (~390px)** (#937). On a phone the inline flow diagram (`.flow { min-width: max-content }`, `.node { white-space: nowrap }`) forced its grid track wider than the viewport, and `.slide { overflow: hidden }` then **clipped** anything taller than one screen (bullets cut mid-word, flow nodes sheared, slide bottoms cut off). The requirement is that content **fits** both dimensions, not that it scrolls inside a card. Fix (both `docs/presentations/applied-dev-loops.html` and `process-observability.html`): (1) **diagrams fit by stacking** — at `≤600px` `.flow` switches to `flex-direction: column` with arrows rotated to point down and `.node { white-space: normal }`, so the diagram lays out vertically and fits the width (measured 319px flow in a 319px card, zero internal scroll); `.flow { flex-wrap: wrap }` keeps desktop rows fitting too, and `.flow-scroll { overflow-x: auto }` is now a never-triggered last-resort safety, not the fix; (2) **no vertical clip** — `.slide { overflow: hidden }` → `overflow: visible` so a tall slide grows and the page scrolls between slides instead of cutting content off; (3) **mobile sizing** — a `≤600px` breakpoint reduces heading/card/padding scale and top-aligns slides so content fits comfortably. `min-width: 0` on grid children and `overflow-wrap: anywhere` on `p`/`li`/`code` are added. The CSS lives only in the HTML renders (the Slidev `*-presentation.md` sources carry none of it). The dark visual identity, section ids, CSP guard, and reduced-motion guard are unchanged. Both Playwright deck specs (`test/playwright/applied-deck.spec.mjs`, `observability-deck.spec.mjs`) are hardened to enforce **fit** at mobile (390×844): a settle barrier (`waitForLoadState("networkidle")` + `waitForFunction(innerWidth === 390)` + `document.fonts.ready`) removes the cold-start false-fail that measured desktop geometry; the horizontal check now fails if **any** element's `getBoundingClientRect().right > innerWidth + 1` (the `overflow-x:auto` scroller exemption is dropped — diagrams must fit) and asserts `document.scrollingElement.scrollWidth <= innerWidth + 1` (no horizontal page scroll); a vertical-clip check fails any section whose `clientHeight < scrollHeight` while `overflow-y` is `hidden`/`clip`; a guard-the-guard test confirms the fit check fails on a deliberately-wide element. Each spec also captures one mobile named state.

## 0.4.0

### Added

- **Opt-in human reviewer/assignee handoff at the pre-approval gate** (#920, pairs with #910). A new `approval.humanHandoff` config (`{ enabled (default false), candidatesFrom: ["codeowners"|"recent-committers"], assignees: [...] }`) plus `scripts/github/resolve-handoff-candidates.mjs` resolve a deduped, priority-ordered candidate list (configured `assignees` → CODEOWNERS last-match-wins for the PR's changed paths → recent committers via `git log`, excluding the PR author/bots; team handles flagged). `dev-loops gate offer-human-handoff --repo <o/n> --pr <n> [--assign <login>] [--request-review <login>]` prints the offer and, only on an explicit `--assign`/`--request-review` flag, runs `gh pr edit --add-assignee/--add-reviewer` — OFFER-only, never auto-assigns. Surfaced at the human-merge handoff so `autonomy.humanMergeOnly` routes the PR to a named human instead of parking silently. Disabled by default; fail-soft per source.
- **Provider-agnostic CI watcher `dev-loops loop watch-ci`** (#917). A block-waiting watcher (`scripts/github/probe-ci-status.mjs`) that polls a PR's combined check-run + commit-status state for the current head SHA until terminal or timeout — covering GitHub Actions, CircleCI, and any external commit-status/check-run, unlike Actions-only `gh run watch`. It short-circuits to `changed` when the head SHA advances mid-wait so the loop re-baselines. The no-checks path is race-safe: a fresh push where a provider (CircleCI/Actions) hasn't posted its first status yet is NOT settled green on the first poll — the watcher awaits a 2-consecutive-zero-check-poll grace before settling `none`→`success`, treats PR `statusCheckRollup`-expected-but-unreported checks as pending, and never fabricates green from a transient `gh api`/parse failure (an errored fetch forces pending; a persistent error settles `timeout`, never success).
- **`autonomy.humanMergeOnly` — fixed, non-overridable human-merge rule** (#910). Repos with a hard "a human must perform the merge" rule can now set `autonomy.humanMergeOnly: true` in `.devloops`, making merge an enforced repo invariant rather than a per-run default an explicit instruction can unlock. When set: `resolveAutonomyStopAt` always includes `merge` (even if `stopAt` is `[]`); the new authoritative gate `resolveEffectiveMergeAuthorized(mergeAuthorized, config)` fails closed — it returns `false` regardless of the `mergeAuthorized` envelope flag / explicit "merge" instruction — and the lifecycle resolver (`resolveLifecycleState`) therefore never advances to the terminal merge state, parking instead at the `pre_approval_gate` human-merge handoff. The agent still runs the full mechanical pre-merge evidence check and reports merge-ready, but never runs `gh pr merge` itself. The `queue run` path routes its `--merge-authorized` flag through the same gate. New resolvers `resolveHumanMergeOnly` / `resolveEffectiveMergeAuthorized` in `@dev-loops/core/config`; see [skills/docs/merge-preconditions.md](skills/docs/merge-preconditions.md).
- **Managed worktree lifecycle** (#909). dev-loops now owns the full worktree lifecycle through one shared canonical-path resolver. (1) **Namespaced naming:** loop-owned worktrees live at `tmp/worktrees/dev-loops/<kind>-<number>` (e.g. `issue-909`, `pr-908`) with no branch suffix, so the path is recomputable from the issue/PR number alone — `resolveWorktreePath({ repoRoot, kind, number })` (in `@dev-loops/core/loop/handoff-envelope`) is the single source of truth for create/provision/cleanup. (1a) **Lifecycle entrypoint:** `scripts/loop/ensure-worktree.mjs --repo-root <p> (--issue <n> | --pr <n>)` is the canonical create+provision command — it fetches the base remote, creates the worktree at the canonical path (or reuses one that already exists there, reporting a conflict instead of clobbering a different branch), then invokes the provisioning core in the same step, printing `{ ok, path, created|reused, provision }`. (2) **Auto-provisioning:** a new `.devloops` `worktree` section (`copyOnInit` / `linkOnInit`, both opt-in arrays of repo-relative literal paths or glob patterns) drives `scripts/loop/provision-worktree.mjs`, which copies (`fs.cp`) or absolute-symlinks the configured gitignored files/dirs from the main checkout into a fresh worktree — directories recurse, sources outside the main checkout are rejected, missing sources / empty globs fail soft, and reuse is idempotent. It does not run `npm install` and is not a `node_modules` mirror. (3) **Namespace-scoped cleanup:** `scripts/loop/cleanup-worktree.mjs` resolves the canonical path and runs `git worktree remove --force` + `git worktree prune` from the main checkout, refusing any path not under `tmp/worktrees/dev-loops/` and failing soft on git errors. See [docs/worktree-guidance.md](docs/worktree-guidance.md).
- **Consumer migration guide** (#769): [`docs/migrating-to-dev-loops.md`](docs/migrating-to-dev-loops.md) walks existing `pi-dev-loops` consumers through every breaking change — package name (`pi-dev-loops`→`dev-loops`, `@pi-dev-loops/core`→`@dev-loops/core`), repo slug, the full `PI_*`→`DEVLOOPS_*` env-var mapping (a deliberate clean break with no aliases), and the `.devloops` config location. Linked from the README. The env vars are not shimmed by design (`0.x`, YAGNI); the legacy `.pi/dev-loop/settings.yaml` config path still loads with a deprecation warning.

### Changed

- **Post-merge board archive is now a standard step of the post-merge hook** (#918). `archive-done-items.mjs` (applying `queue.archiveOlderThanDays`, default 7d) is wired into the canonical `merge-preconditions.md` "Post-merge" surface alongside worktree cleanup, and the copilot-pr-followup post-merge step is no longer framed as merely optional. Operator-induced (runs after merge); best-effort — the hook ignores a non-zero exit so a failed archive never blocks merge completion. NOT a cron/scheduled job.
- **Queue management surfaced under `dev-loops queue`** (#912). The queue board management commands (`add`, `list`, `reorder`, `move`, `sync-status`, `archive-done`, `ensure`) are now discoverable and runnable under `dev-loops queue <sub>` alongside the existing `queue run` — `dev-loops queue --help` lists them all with one-line descriptions. They delegate to the same `scripts/projects/*.mjs` implementations; `dev-loops project <sub>` is retained as a back-compat alias group (lowest-churn: the routing table is data-driven, so `queue` reuses the existing script mappings). Flag consistency: `queue add` now accepts `--column <name>` for the Status column (matching `queue list`), with `--status <name>` kept as a back-compat alias. `move`/`sync-status` keep their distinct `--to-column`. Removes the only reason to hand-write `gh api graphql` for queue work.
- **BREAKING: Node floor raised `>=20` → `>=24`** (#911). `engines.node` is now `>=24` in both `dev-loops` and `@dev-loops/core` (the latter previously declared no floor). CI already runs Node 24; this makes the supported floor explicit and unlocks Node 24 stdlib (e.g. native `fsp.glob`/`path.matchesGlob`). Consumers on Node < 24 must upgrade.
- **BREAKING: all `PI_*` environment variables renamed to `DEVLOOPS_*` — no aliases, no fallback** (#905). Completing the env-var neutralization left half-done by the rebrand (#763, surfaced in #769), every dev-loops-owned operational env var is now `DEVLOOPS_*`-only; the previous neutral-first alias pattern (which honored `PI_SUBAGENT_RUN_ID` / `PI_SUBAGENT_AVAILABLE` as fallbacks) is removed. This is a deliberate `0.x` breaking change — consumers must rename their env vars (migration covered by #769). Mapping: `PI_SUBAGENT_RUN_ID`→`DEVLOOPS_RUN_ID`, `PI_SUBAGENT_AVAILABLE`→`DEVLOOPS_SUBAGENT_AVAILABLE`, `PI_PREFLIGHT_BYPASS`→`DEVLOOPS_PREFLIGHT_BYPASS`, `PI_PREPUSH_BYPASS`→`DEVLOOPS_PREPUSH_BYPASS`, `PI_WORKTREE_BYPASS`→`DEVLOOPS_WORKTREE_BYPASS`, `PI_DEV_LOOPS_DEBUG`→`DEVLOOPS_DEBUG`, `PI_DEV_LOOP_STALE_RUNNER_MAX_AGE_MS`→`DEVLOOPS_STALE_RUNNER_MAX_AGE_MS`, `PI_DEV_LOOP_DETACHED`→`DEVLOOPS_DETACHED`. The Pi-runtime-injected vars dev-loops reads only to *integrate* with the Pi harness (`PI_SESSION`, `PI_INTERACTIVE`, `PI_AGENT_SESSIONS_DIR`, `PI_SUBAGENT_SESSIONS_DIR`, `PI_SUBAGENT_ASYNC_RUNS_DIR`, `PI_SUBAGENT_ASYNC_RESULTS_DIR`) are external Pi-platform contract vars and intentionally unchanged. A `cli-harness-agnostic` guard now asserts the code is harness-agnostic: no dev-loops-owned `PI_*` env var survives, and the Pi-runtime-injected vars may only be read at the harness-adapter boundary (`pi-adapter.mjs`, `conductor-monitor.mjs`) — a `PI_*` read anywhere else in code fails.

- **Remaining `pi-dev-loops` → `dev-loops` identity references aligned** (#906, closes #768). Residual stale-slug strings in a contract doc (and its generated `.claude` mirror) were corrected, guarded by the `docs-identity-contract` test so user-facing identity surfaces stay consistent.

### Fixed

- **`queue run` no longer fabricates `done` for undispatched items** (#913, data-integrity). The queue driver is a deterministic adapter over the board, not the orchestration harness — but its missing-orchestrator path fell back to a per-entry `{ ok: true, pr: null }`, which silently marked every `Next Up` item `done` and moved it to **Done** with `pr: null`/`runId: null` in ~1s without any work happening (a single resolve pass would "complete" an entire backlog untouched). The driver now requires a verifiable terminal signal (an orchestrator-supplied result, e.g. a merged PR) before reflecting an item to Done; with no orchestrator wired (`runEntry`) in the current harness, `dev-loops queue run` is a no-op that leaves every board column unchanged and reports `reason: "no-orchestrator"`. The legit reflect path (real merged PR → Done) is preserved.
- **Queue board `Next Up` membership now resolves from a title-only `.devloops` config** (#904, closes #901). `resolveNextUpOrder` passed the project number as a raw number, which `list-queue-items`' string-only `--project` guard rejected — so a board configured by `queue.boardTitle` alone reported "Board configured but unavailable; nothing to run" and never read `Next Up`. The project ref is now passed as a string.

## 0.3.0

### Added

- **Gate fan-out/fan-in review sub-loop** (epic #867, #895). The draft and pre-approval gates now run as a real fan-out on a **build-once neutral context bundle**: a deterministic context-builder script resolves review angles and builds ONE neutral bundle (full diff + adjacent code), then each independent, read-only `review` agent is seeded with that identical bundle verbatim and scoped to one angle, and a fan-in step consolidates the per-angle verdicts into a disposition ledger. The cost win is work-dedup (build once vs. N× re-derivation; a shared-prefix prompt-cache is an opportunistic bonus) — there is no fork primitive and no Workflow-tool dependency. Verdicts record their execution mode (`--execution-mode fanout_fanin | inline_single_agent`, with `--inline-reason`; #875) so the audit trail shows how the gate was actually run. See [docs/gate-review-sub-loop-contract.md](docs/gate-review-sub-loop-contract.md).
  - **Context-builder handoff + dynamic angles** (#880, #895). A `write-gate-context` step emits the per-gate scope/diff artifact plus a deterministic, neutral `adjacentCode` bundle (each changed file's 1-hop import callers/callees/imports, with size guards + a stripped/truncated/missing manifest) that every reviewer is seeded with verbatim, and angles are resolved dynamically (configurable `mandatory` set plus `gates.dynamicAngles`), bounded by `gates.maxFanoutReviewers` (default 8).
  - **Independent scoped reviewers + fan-in consolidation** (#881, #895). Per-angle `review` agents are independent fresh-context Agents seeded with the neutral bundle (never inheriting the main agent's state); they emit structured findings, and `consolidateFanin` merges them and computes the `fanout_fanin` verdict against `blockCleanOnFindingSeverities` (`must-fix`, `worth-fixing-now`).
  - **Full-diff + adversarial scoped review with scope widening** (#886, #885, #895). The context-builder builds the full PR diff and a generous adjacent-code bundle once, and reviewers use it as their base and widen only per-angle when needed. Reviewers run adversarially against the complete change — this surfaced real defects (arg coercion, head-SHA casing, markdown injection, dead seams) that the prior single-pass review missed.
- **Fan-out findings posted to the PR** (#888, #887). The gate posts a single marker-tagged, idempotent PR comment listing its findings so Copilot and humans see them, and the loop fixes/resolves its own findings as it does Copilot comments. Opt out with `gates.postFindingsComments: false` (default on).
- **Configured board drives queue membership** (#884, #864). A configured GitHub Projects board's `Next Up` column is now the authoritative source of queue membership and ordering (not just status); emptiness reports a precise verdict (`queue_empty` / `board_empty` / `board_unavailable`) instead of a misleading generic message.

### Changed

- **Gate fan-out evidence enforcement is now ON by default** (#882, #879, epic #867 final phase). A clean gate verdict requires the gate to have run via `--execution-mode fanout_fanin` with a findings-log ledger for the head SHA; the pre-merge evidence check fails closed otherwise. Repos can opt out with `gates.requireFanoutEvidence: false`.
- **Board status auto-syncs on dev-loop transitions** (#883, #874). A linked issue's board Status column is synced on loop transitions (e.g. PR opened → `In Progress`, merged → `Done`) via local `gh` auth — best-effort and non-fatal. Repairs the `move-queue-item` lookup that passed numeric (not string) project/item refs.

### Fixed

- **Skill shims import `@dev-loops/core` via its package specifier** (#890). `skills/dev-loop/scripts/log-bash-exit-1.mjs` and `phase-files.mjs` previously reached into core through cross-package relative paths (`../../../packages/core/src/...`), which are broken on disk for npm consumers because the published `dev-loops` package ships `skills/` but not `packages/core/`. They now import via the `@dev-loops/core` `exports` map. A contract test guards against reintroducing relative cross-package imports under `skills/`.
- **Draft-gate deadlock on ready PRs resolved** (#891). Posting a `draft_gate` verdict on a PR that is already ready-for-review (e.g. opened directly as ready) no longer dead-ends. `upsert-checkpoint-verdict` now (a) treats an already-satisfied draft gate as an idempotent no-op instead of a hard error, and (b) when a ready PR still needs clean draft-gate evidence, performs the draft→post→ready transition automatically — preserving the caller's execution mode (`fanout_fanin`), findings, and ledger. This is the fanout-aware analogue of `reconcile-draft-gate` (which only posts inline and so cannot satisfy `requireFanoutEvidence` on the draft gate).
- **PR self-assignment is now mechanically enforced** (#894). The draft-PR wrapper is renamed `scripts/github/create-draft-pr.mjs` → `scripts/github/create-pr.mjs` (`dev-loops pr create-draft` → `dev-loops pr create`, with the old subcommand kept as a deprecated alias). It now defaults `--assignee @me` when no `--assignee` is given (while still honoring an explicit `--assignee <login>`), so every PR opened through the canonical path is ALWAYS a draft and is always assigned — self-assigned by default — closing the silent gap where unassigned PRs (e.g. #889, #892, #893) missed the owner's assignee inbox. A new contract guard (`test/contracts/canonical-pr-creation-contract.test.mjs`) fails if any skill/agent procedure doc instructs opening a PR with raw `gh pr create`.
- **Post-round-cap convergence deadlock resolved** (#896). At the Copilot round cap with clean threads + green CI, a post-cap head that Copilot will not re-review now routes to a clean fallback that permits the `pre_approval_gate` to review the current head, instead of dead-ending at `ready_to_rerequest_review` (the deadlock #848 intended to prevent). Root cause: the coordination-context loader did not pass the resolved config into the loop interpreter, so `maxCopilotRounds` was unseen and `ROUND_CAP_CLEAN_FALLBACK` never resolved; the draft-gate round-reset is now a shared helper so `request-copilot-review` and `detect-pr-gate-coordination-state` agree on the round count. Genuinely-blocked states (failing/unconfirmed CI, unresolved feedback, conflicts, missing draft-gate evidence) still hold.
- **Gate verdict renders consolidated per-angle findings structurally** (#898). A `fanout_fanin` verdict comment renders the per-angle fan-in findings as a readable list (per-angle verdict + findings) via a new `--findings-json`, instead of collapsing the summary to a single run-on line. The gate-evidence parse contract is preserved (a single-line digest still anchors the `Findings summary:` field, and `gateEvidenceNote` is carried), input shape is validated (per-angle or flat-grouped; unrecognized input is rejected rather than silently dropped), and `--findings-summary` remains the inline fallback.

## 0.2.8

### Added

- **Local post-merge board archive** (#869). The dev-loop post-merge step archives `Done`-column board items older than a configurable threshold (`.devloops` `queue.archiveOlderThanDays`, default 7d) using local `gh` auth — best-effort, non-fatal, no CI/cron/PAT. On-demand `dev-loops project archive-done` is unchanged.
- **Gate execution-mode disclosure scaffolding** (#867, partial). Gate verdicts can record `--execution-mode` / `--inline-reason`; opt-in `gates.requireFanoutEvidence` (default off) is available. (Live fan-out/fan-in execution remains follow-up.)

### Fixed

- **`dev-loops project move` repaired** (#865). Item lookup now resolves both issue/PR number and node-id refs against a single paginated board-item list; fixes the `ITEM_NOT_FOUND` (unpaginated `first:10`) and the invalid `ProjectV2.item` GraphQL query.

### Changed

- **Index-based arg parsers migrated to `node:util.parseArgs`** (#857, #870). The remaining `argv[++i]` parsers across `scripts/projects`, `scripts/loop`, `scripts/claude`, and `archive-done-items.mjs` now use `parseArgs`; CLI contracts preserved and boolean flags reject an explicit inline `=value`.

### Documentation

- **Tooling-internals anti-pattern promoted** (#861, #863). The "use the CLI/`--help`/`skills/docs/` instead of reading tooling source" rule is now a canonical entry in `skills/docs/anti-patterns.md`, with a local failure-triage fast path and pointers from the `developer`/`fixer` agents.

## 0.2.7

### Fixed

- **Deterministic, harness-aware dev-loops CLI invocation** (#801, #833). Pi runtime skills/agents now invoke the package-local `node <dev-loops-package-root>/cli/index.mjs`; the generated Claude tree pins `npx dev-loops@<version>` (version injected at generation time) so the plugin and CLI no longer drift.
- **Round-cap Copilot-gate deadlock resolved** (#848). At the round cap with clean threads + green CI, the loop routes to a clean fallback instead of dead-ending at `waiting_for_copilot_review` when a lingering reviewer assignment / post-cap push leaves the head unreviewed. The pre-approval gate still reviews any post-cap head.
- **Draft-gate ordering after external un-draft** (#836). Verified + regression-guarded: a non-draft PR without clean `draft_gate` evidence is routed to `reconcile_draft_gate` and cannot merge; the relayed-authorization deadlock is moot under the single-agent Claude harness.

### Added

- **Projects board reorder + Done-cleanup CLI** (#789). `project reorder move-to-top|move-after|order` (with `--dry-run`, diff-friendly output, cross-column fail-closed) and `project archive-done [--older-than]`.
- **Loop-state-driven board status sync** (#793). Board Status column is derived from the loop state via a pure, config-driven mapping (`queue.statusColumns` / `queue.stateColumnMap`), opt-in, fail-open, reverse-safe.

### Changed

- **Arg parsing migrated to `node:util.parseArgs`** (#808). All hand-rolled `while/shift` parsers (49 scripts/modules + 3 core files) now use `parseArgs` via shared adapters, with CLI contracts preserved. (Index-based parsers tracked in #857.)

## 0.2.6

### Fixed

- **Claude plugin hooks are self-contained** (#843). The bundled PreToolUse/PostToolUse hooks
  imported a bare `@dev-loops/core`, which is unresolvable from the marketplace plugin cache (no
  `node_modules` there), so every hook crashed on load — the two PreToolUse gates were silently
  failing open. The asset generator now emits a vendored, relative-import hook bundle
  (`.claude/hooks/_*.mjs`) from the canonical core modules, drift-guarded by the no-drift check.
- **Retrospective gate is opt-in for consumers** (#841). `extension-defaults.yaml` shipped
  `requireRetrospective`/`requireRetrospectiveGate: true`, forcing the retrospective merge gate on
  every consumer's product PRs against the code default and the contract. Both now default `false`;
  the dev-loops repo opts in via its own `.devloops`.
- **Dev mode is opt-in for consumers** (#846). `extension-defaults.yaml` shipped
  `devModeDefault: true`, pushing every consumer's product phases into the loop's self-improvement
  mode (which edits the loop's own skill/agent prompts). Now defaults `false`; the dev-loops repo
  opts in via `.devloops`.

### Added

- **Merge-blocking PR-title gate** (#842). The gate pipeline now flags `WIP`/`[WIP]`/`DRAFT`/
  `DO NOT MERGE`/`🚧` (case-insensitive) in the PR **title**, blocking the draft→ready transition
  and — for non-draft PRs — entry to the pre-approval gate and final approval. Documented in the
  merge-preconditions and PR-lifecycle contracts.
- **Effective async-start mode is surfaced** (#834). The handoff envelope now reports
  `asyncStartEffective` and `asyncStartRelaxedBy` alongside the unchanged configured
  `asyncStartMode`, so the Claude harness relaxation (`required`→`allowed`) is visible instead of
  reading as a contradiction.

### Changed

- **Deduplicated PR aggregation** (#809). The duplicated `listOpenPrs` helper is extracted into a
  shared `scripts/loop/_loop-pr-aggregation.mjs` and reused by `conductor-monitor.mjs` and
  `run-conductor-cycle.mjs`. No behavior change.

## 0.2.5

### Changed

- **Claude Code: the Copilot PR follow-up loop runs inline** (#838, completing the umbrella
  collapse from #837). The copilot-pr-followup skill's Pi "persistence model" — *subagents do
  bounded work and exit on the wait boundary; the main session re-dispatches* — is now scoped to
  Pi via `<!-- pi-only -->`. Under the Claude harness the single dev-loop agent runs the
  `watch → fix/reply/resolve → re-request → watch` loop **inline**: the helper-owned wait tools
  (`dev-loops loop watch-cycle`, `gh run watch`, `dev-loops gate probe-copilot`) block inline and return, so
  the agent keeps looping until terminal or the watch budget expires — no exit-and-redispatch. The
  outer-loop checkpoint, watch budget, the forbidden-shell-watcher rules, and the gate requirements
  are unchanged and harness-agnostic. Pi behavior is unchanged.

## 0.2.4

### Changed

- **Claude Code: the dev-loop runs as a single agent** (#837). The Pi "umbrella" execution model —
  a strictly read-only main agent that must dispatch an async `dev-loop` subagent, with all
  mutations and state-changing CLI (`gate`/`pr`/`loop`) confined to that subagent — is now scoped
  to Pi only. Under the Claude harness the dev-loop agent performs the steps directly: it reads and
  writes repo files, runs git/PR operations, runs the `dev-loops` CLI, and **posts gate verdicts
  under the operating session's identity** (fixing clean gates that previously stalled, unable to
  record their verdict without separate "coordinator authority"). The `gh pr ready` draft-gate
  guard still applies, and the read-only boundary remains available opt-in via
  `DEVLOOPS_MAIN_AGENT_READONLY=1`. Implemented by scoping the Pi read-only/dispatch contract in
  `main-agent-contract.md` and the dev-loop skill's startup procedure behind `<!-- pi-only -->`
  markers; the asset generator now applies that stripping to bundled contract docs too, so the
  Claude plugin ships the single-agent model while Pi keeps the full contract. Pi behavior is
  unchanged. (Follow-up #838 tracks the copilot-pr-followup/conductor async-execution model.)

## 0.2.3

### Added

- **Opt out of the Copilot review gate via `refinement.maxCopilotRounds: 0`** (#832). For repos
  without a Copilot reviewer configured (or that prefer local-harness-only review), setting
  `maxCopilotRounds: 0` disables the external Copilot review cycle entirely — the loop runs
  `draft_gate → pre_approval_gate` with no Copilot request or wait. The config schema now accepts
  `0` (`nonnegative`; negative still rejected); `evaluatePrGateCoordination` routes `0` through the
  existing `internal_only` path, `shouldGuardCopilotReviewRequest` never forces a request at `0`,
  and the watch-cycle handoff (`copilot-pr-handoff`) skips the request too. Default (`5`) unchanged.
  Documented in the README, extension config docs, and the `copilot-pr-followup` skill.

## 0.2.2

### Fixed

- **Claude Code: dev-loop no longer dead-ends on the async-start contract** (#830). Running
  `/dev-loop` from the installed plugin failed immediately because `dev-loops loop startup`
  enforces an async-start contract — it requires a run-id env marker (`DEVLOOPS_RUN_ID` /
  `PI_SUBAGENT_RUN_ID`) that Pi injects when dispatching an async subagent but Claude Code's
  Agent tool does not. That contract guards against detached, uninspectable background
  processes, a risk that does not exist under Claude's Agent model (each subagent run is
  visible and inspectable). The async requirement remains configurable via
  `workflow.asyncStartMode` (`required` | `allowed`); under the Claude harness it is now
  **relaxed to `allowed` at runtime** via `resolveEffectiveAsyncStartMode`, which consults the
  new `isClaudeHarness` helper (`CLAUDECODE=1`) in `@dev-loops/core/loop/run-context`. An
  explicit `DEVLOOPS_RUN_ID` still resolves as `valid`, and Pi behavior is unchanged (outside
  Claude the configured mode is honored verbatim).
- The async-start CLI contract test is now hermetic — it clears `CLAUDECODE` (and the run-id
  markers) so the rejection path is exercised regardless of the harness the suite runs under.
- The generated `dev-loop` skill prose no longer claims `PI_SUBAGENT_RUN_ID` is *required* — it
  now describes the async run-id marker (`DEVLOOPS_RUN_ID` / `PI_SUBAGENT_RUN_ID` alias) and notes
  the Claude-harness relaxation, so the plugin's docs match the runtime behavior. Subagent
  spawning via the `dev-loop` agent is confirmed correctly wired: it grants the `Agent` tool
  (the current subagent-spawning tool, renamed from `Task` in Claude Code v2.1.63) and the
  strategy skills delegate to the worker agents (`developer`/`quality`/`refiner`/`fixer`/`review`/`docs`).

## 0.2.1

### Added

- **Claude Code marketplace catalog** (#828): ship `.claude-plugin/marketplace.json` at the repo
  root so the repo can be added as a plugin marketplace (`/plugin marketplace add mfittko/dev-loops`,
  or the *Manage Plugins → Marketplaces → Add* UI) and the plugin installed with
  `/plugin install dev-loops@dev-loops`. The catalog's single plugin entry sources the existing
  in-repo plugin at `./.claude`; the plugin version stays authoritative in `plugin.json`. A
  contract test locks the catalog shape, and `.claude-plugin/` is added to the npm `files`
  allowlist. Verified end-to-end with `claude plugin validate` + `marketplace add`/`install`
  (4 skills, 7 agents, 2 hooks).

### Changed

- `plugin.json` now declares an `author` (clears the marketplace-validation warning).
- README "Claude Code plugin" section drops the `(preview)` framing and documents marketplace
  install; the two CLI help lines that said plugin packaging was "in progress" are updated.

## 0.2.0

### Added — Claude Code harness (agent-harness-agnostic dev-loop)

dev-loops is now dual-harness: it runs under both Pi and Claude Code. Pi behavior is unchanged.

- **Harness adapter seam** (#770): a neutral `ExtensionHarnessAdapter` (exec + lifecycle +
  command registration + ui) with Pi and Claude adapters; `@dev-loops/core/harness`.
- **Neutral run-id contract** (#771): `DEVLOOPS_RUN_ID` (with `PI_SUBAGENT_RUN_ID` as a
  backward-compatible alias) via `@dev-loops/core/loop/run-context`; all runner-coordination /
  async-start readers route through it.
- **Generated `.claude` assets** (#772, #816, #817): a deterministic generator emits
  `.claude/agents` + `.claude/skills` from the canonical Pi sources (`@dev-loops/core/claude/
  asset-generation`), with the Pi→Claude tool-name mapping, bundled shared contract docs +
  templates, and Pi-runtime-only prose stripped via `<!-- pi-only -->` markers.
- **Claude hooks + read-only enforcement** (#773): PreToolUse Bash draft-gate guard + Write/Edit
  main-agent read-only guard (`@dev-loops/core/claude/hook-decisions`), opt-in via
  `DEVLOOPS_MAIN_AGENT_READONLY`.
- **CLI Pi-neutrality** (#774): `npx dev-loops --help`/`status` run with no `@earendil-works/pi-*`
  present; Pi-only install strings no longer shown unconditionally.
- **Headless entry** (#775): a `claude -p` headless dev-loop entry (`@dev-loops/core/claude/
  headless-entry`) that mints + propagates the run id, plus an offline read-only CI/Docker smoke
  (`npm run smoke:headless`); the Pi Docker smoke is preserved (dual-harness).
- **Claude Code plugin** (#818, #824): `.claude/.claude-plugin/plugin.json` (plugin root
  `.claude/`) bundling the dev-loop agents, skills, and hooks —
  `claude --plugin-dir .claude` loads 4 skills, 7 agents, 2 hooks.

### Changed

- `@dev-loops/core` bumped to `^0.2.0` (new `claude/*`, `loop/run-context`, and
  `loop/bash-command-classify` exports).

## 0.1.3

### Fixed

- Removed a stale `defaults.yaml` from the `files` allowlist and regenerated the lockfile (#806).

## 0.1.2

### Changed

- Ship the extension-packaged dev-loop defaults only; removed the duplicated
  `.pi/dev-loop/defaults.yaml` (#805).

## 0.1.1

### Changed

- Renamed the Pi peer dependencies to the `@earendil-works/pi-*` scope (#799).

## 0.1.0

### Added

- Initial publishable `dev-loops` v0.1.0 package metadata.
- Primary npm package name is the unscoped `dev-loops` (`@mfittko/dev-loops` kept only as a documented fallback).
- Public npm provenance and access configuration.
- `@dev-loops/core` `^0.1.0` dependency for the extracted scoped runtime package.
- CLI entrypoint `dev-loops` via `./cli/index.mjs`.
- Repository, bugs, and homepage URLs pointing to `mfittko/dev-loops`.

### Removed

- Broken `postinstall` lifecycle script that failed on consumer installs.
