# Extension scaffold

`dev-loops` ships a lightweight package extension for readiness UX plus one bounded local UI lifecycle seam.

Installing the package exposes two thin wrappers over one shared deterministic core:
- the Pi extension command family rooted at `/dev-loops`
- the shell CLI entrypoint `dev-loops`
- a bounded post-merge helper that queues one `pi update git:github.com/mfittko/dev-loops` after a successful in-session `gh pr merge ...` or `git merge ...` inside this repo and flushes it on `agent_end`

Installing the package with `pi install git:github.com/mfittko/dev-loops` exposes the packaged skills through `package.json` `pi.skills`, and the extension syncs packaged agent files (`agents/*.agent.md`) into `~/.agents/` on `session_start`.

**Version pinning (no global-install skew).** The extension resolves both the CLI and `@dev-loops/core` from the installed, pinned package (module imports, not a global `dev-loops` binary), and updates itself via the pinned `pi update git:github.com/mfittko/dev-loops` seam above. It never depends on a separately-updated global install, so it stays in lockstep with the installed version by construction (#1036).

## Command surface

- `/dev-loops`
  - defaults to help output for the available subcommands
- `/dev-loops status`
  - concise readiness summary plus lightweight next steps
- `/dev-loops doctor`
  - full diagnostic report with explicit pass/fail detail
- `/dev-loops hide`
  - removes the readiness widget cleanly
- `/dev-loops inspect open [--repo <owner/name>]`
  - start or reuse the managed local inspect-run viewer and best-effort open it in the browser
- `/dev-loops inspect resume [--repo <owner/name>]`
  - reattach only to a confirmed live managed inspect-run viewer; fails closed when nothing live is managed
- `/dev-loops inspect status [--repo <owner/name>]`
  - report one bounded local lifecycle state plus the current URL when known
- `/dev-loops inspect stop [--repo <owner/name>]`
  - stop only the recorded managed inspect-run viewer process
- `/dev-loops inspect restart [--repo <owner/name>]`
  - explicitly restart the recorded managed inspect-run viewer; never kill an unknown listener
- `dev-loops`
  - defaults to help output for the available subcommands
- `dev-loops help`
  - prints shell help for the shared command family
- `dev-loops status`
  - prints the concise readiness summary in shell-friendly output
- `dev-loops doctor`
  - prints the full diagnostic report in shell-friendly output
- `dev-loops gates`
  - prints active review angles with their prompts from config
- `/dev-loops gates`
  - same as above, but inside the Pi extension
- `dev-loops inspect open [--repo <owner/name>]`
  - same as the slash form: start or reuse the managed viewer and best-effort open it in the browser
- `dev-loops inspect resume [--repo <owner/name>]`
  - same as the slash form: reattach only to a confirmed live managed viewer
- `dev-loops inspect status [--repo <owner/name>]`
  - same as the slash form: report one bounded local lifecycle state plus the current URL when known
- `dev-loops inspect stop [--repo <owner/name>]`
  - same as the slash form: stop only the recorded managed viewer process
- `dev-loops inspect restart [--repo <owner/name>]`
  - same as the slash form: explicitly restart the recorded managed viewer; never kill an unknown listener
- `dev-loops hide`
  - is intentionally unsupported and exits non-zero with a shell-friendly stderr message because `hide` is session-local Pi UI behavior

## Inspect local UI lifecycle ownership

This slice is intentionally narrow.

Extension-owned behavior:
- the Pi-side operator surface for the lifecycle, `/dev-loops inspect ...`; the shell surface `dev-loops inspect ...` drives the same lifecycle through the shared executor against the same managed-instance record, so neither surface owns it exclusively
- repo-local managed-instance record at `.pi/ui-servers/inspect-run-viewer.json`
- safe URL discovery, liveness checks, resume/reattach, stop, and explicit restart handling
- best-effort browser open
- fail-closed handling for stale ownership and unknown listeners

Viewer-script-owned behavior:
- HTTP server implementation
- viewer HTML/JS rendering
- inbox and query-state behavior
- snapshot loading through the existing adapter
- read-only route behavior and localhost safety rules

Lifecycle states reported by the extension-managed seam are intentionally bounded to:
- `running`
- `stopped`
- `stale_record`
- `conflict_unmanaged_listener`

Guard rails for this seam:
- loopback-first local-only posture
- no remote/public hosting
- no generic local app platform
- no background watcher/supervisor behavior
- no inspect-run viewer redesign

## Current readiness checks

The extension currently reports on:
- `gh` installed
- `gh` authenticated
- `subagent` command available
- inside a git repository

Readiness and help messaging should lead with `dev-loop` as the single public workflow entrypoint. Internal compatibility seams may still exist for runtime/routing purposes, but the readiness surface should not present them as separate user-facing checks or workflow choices.

The messaging distinguishes between local loop readiness and remote GitHub/Copilot readiness. Missing `gh` or `gh auth` blocks remote-loop readiness, but does not imply that local phase-based work is completely unavailable.

## Package install contract for this phase

- `pi install git:github.com/mfittko/dev-loops` is the distribution mechanism for the extension, skills, scripts, packaged agents, and required installed runtime contract docs
- `pi install -l git:github.com/mfittko/dev-loops` is the project-local replacement for the old `install repo` flow
- `pi update git:github.com/mfittko/dev-loops` refreshes an installed package
- source-tree canonical contract docs live under `skills/docs/`; installer/package output must ship this shared docs bundle with the installed skills subtree: [Public Dev Loop Contract](../skills/docs/public-dev-loop-contract.md) and [Retrospective Checkpoint Contract](../skills/docs/retrospective-checkpoint-contract.md)
- installed skill/runtime guidance must read those bundled shared docs (from installed `skills/<skill>/`, resolve via `../docs/`) instead of assuming a source checkout is present; a missing bundled contract doc is a packaging/installer bug
- packaged agents are refreshed into `~/.agents/` on each `session_start`
- `/dev-loops install ...` and `/dev-loops update ...` are removed; use `pi install` / `pi update` directly instead

## Configuration

The dev-loop workflow is driven by the shipped defaults in `packages/core/src/config/extension-defaults.yaml`, an optional repo-local `.pi/dev-loop/defaults.*` layer that merges on top of them, and an optional consumer settings file at `.devloops` at repo root (the loader also accepts `.devloops.yaml`, `.devloops.yml`, and `.devloops.json`; legacy `.pi/dev-loop/settings.*` and `.pi/dev-loop/overrides.*` load only as a fallback when no `.devloops` is present, and are ignored when `.devloops` exists — a deprecation warning still fires).

### How consumers customize config

Create `.devloops` at your project root. It merges on top of the shipped defaults. Accepted formats: `.devloops` (bare, YAML-format), `.devloops.yaml`, `.devloops.yml`, or `.devloops.json`. Legacy `.pi/dev-loop/settings.*` and `overrides.*` still load as fallbacks with a deprecation warning. You can override any section, including workflow policy defaults:

```yaml
# Example: add a custom review angle with a dedicated persona agent
gates:
  preApproval:
    angles:
      - dry
      - kiss
      - yagni
      - security    # your custom angle

personas:
  security:
    persona: security-reviewer
    prompt: >-
      Audit for auth bypasses, secret leaks, insecure defaults,
      unsafe command execution, and data exposure risks.
    defaultModel: null

  # Override an existing angle's prompt
  dry:
    persona: review
    prompt: >-
      Flag duplication. In this repo, also check for duplicated
      contract language across docs/ and skills/.
    defaultModel: null

# Override gate requirements
refinement:
  fanOut: 5            # run 5 parallel review variants instead of 3
  maxCopilotRounds: 0  # 0 disables the Copilot review gate entirely (local-harness-only
                       # review: draft_gate → pre_approval_gate, no Copilot). Default: 5.

autonomy:
  stopAt:
    - draft-pr
    - merge        # stop for confirmation at both gates

workflow:
  requireRetrospective: true
  requireDraftFirst: true
  devModeDefault: true
  baseBranch: spike/shakapacker-to-vite  # operate against a non-main integration branch
```

### Model tiers (`models.tiers` / `models.roleTiers`)

Subagents run on a harness-neutral **tier** so a single policy expresses "high on both harnesses" even though the concrete model ids differ. A tier alias maps to a per-harness concrete model (or `null` = inherit / no-op on that harness); `models.roleTiers` maps each role/angle to an alias (or `inherit`).

**Default policy (zero config):** routine subagents (`developer`, `docs`, `fixer`, `quality`) → `low`; planning (`refiner`) and critical review (`review`, including gate fan-out angles via the review tier forced by `kind: "angle"`) → `high`; the conductor (`dev-loop`) → `inherit`. Built-in tiers are `low: { claude: opus, pi: null }`, `high: { claude: opus, pi: null }`. Both Claude tiers use opus because Opus 5.5 is currently the most cost-efficient Claude model per task; set `models.tiers.low.claude` to move routine roles to a cheaper model.

**Per-harness resolution:** `resolveRoleModel(config, { role, harness })` from `@dev-loops/core/config` returns a concrete model id or `null`. Precedence: `models.roles[role]` (concrete, highest) > tier from `models.roleTiers[role]` (or the built-in role tier) mapped through `models.tiers[alias][harness]` > `null`. The committed `.claude/agents/*.md` tree bakes the built-in (zero-config) tier policy into each agent's `model:` frontmatter — asset generation does not read repo `.devloops` config, so per-repo config never regenerates that frontmatter. Per-repo/per-dispatch tuning happens at dispatch time: on Pi via `resolveRoleModel` (passed only when non-null), on Claude via the Task `model` param.

**Zero-config no-op on Pi:** because built-in tiers ship `pi: null`, every role resolves to `null` on Pi — no model override is passed, a genuine no-op — until an operator sets concrete Pi ids. The same config drives both harnesses (cross-harness contract #1086).

```yaml
models:
  tiers:
    low:  { claude: sonnet, pi: claude-3-5-haiku }   # set pi ids to opt Pi in
    high: { claude: opus,   pi: claude-3-5-sonnet }
  roleTiers:
    quality: high            # promote a routine role
    developer: inherit       # opt a role out entirely
  roles:
    correctness: gpt-5       # concrete per-angle override (beats any tier)
```

Angles are dispatched with `resolveRoleModel(config, { role: angle, harness, kind: "angle" })`. Their precedence is: `models.roles[angle]` (concrete) > explicit `models.roleTiers[angle]` > the **`review` tier** (high). A gate review runs at review quality regardless of the angle's persona, so an angle whose **name** collides with a built-in routine role no longer takes that role's tier — e.g. the `docs` review angle resolves via the `review` tier (`high`), not the `docs`→low writer role (only its persona/agent still comes from the registry). Genuinely distinct angles (`correctness`, `renderer-security`, `acceptance-criteria`) resolve `high` too. Only a **bare routine role** dispatch (no `kind`, e.g. the `docs`/`developer` subagents) still resolves `low` via `roleTiers`. Retarget a specific angle by setting `models.roleTiers[angle]` explicitly. An unknown tier alias in `models.roleTiers` is rejected by the schema with a clear error.

### MCP tools for Claude subagents (`extraTools`)

Claude Code agents carry an explicit `tools:` allowlist, so an MCP tool (for example a code-graph server) is invisible to every dev-loops subagent. The top-level `extraTools` key appends MCP entries to the tools list of `developer`, `fixer` and `refiner` for a session. The shipped and committed agent files do not change. Pi ignores the key.

```yaml
extraTools:
  developer:
    - mcp__<server>      # whole server; also mcp__<server>__* or mcp__<server>__<tool>
extraToolsGuidance:
  mcp__<server>: "Apply this only when mcp__<server>__* tools are in your tool list. Use them first for symbol and caller questions."
```

- Each role takes 1 to 16 unique entries that match `mcp__<server>`, `mcp__<server>__*` or `mcp__<server>__<tool>`. Built-in tool names are rejected.
- `judge`, `review`, `gate-coordinator` and `dev-loop` fail with `role "<role>" is hook-guarded and cannot take extra tools`. Any other role fails with `role "<role>" does not support extraTools`.
- Start Claude Code with `dev-loops loop claude-launch [-- <claude args>]`. The launcher renders the configured roles through `claude --agents`, passes the entries through `--allowedTools` and sets `DEVLOOPS_AGENT_OVERRIDES`. The headless entry does the same. With no `extraTools` entry the launcher runs plain `claude`.
- The `--agents` render replaces the bare agent name only. The agent guard denies a `dev-loops:<role>` dispatch of a rendered role and names the bare agent type to dispatch.
- A session started without the launcher needs the same entries in `.claude/settings.local.json` under `permissions.allow`. In auto mode the classifier may still deny an allowed MCP call.
- `extraToolsGuidance` maps an `extraTools` entry to usage guidance text (1 to 2000 characters). A key that no role lists in `extraTools`, an empty, whitespace-only, multi-line or too-long value fails config load and names the key. Pi ignores the key.
- The launcher and the headless entry append the guidance to the prompt of each role that lists the key, only when the server is detected for the session. Detection reads `mcpServers` in `<config>/.claude.json` (user scope), `projects[<repo root>].mcpServers` in the same file (local scope) and `mcpServers` in `<repo root>/.mcp.json` (project scope). `<config>` is `$CLAUDE_CONFIG_DIR` when set and the home directory otherwise. Servers in `disabledMcpServers` are removed. A missing or malformed file contributes no server. Detection never runs `claude mcp list`.
- Detection limit: plugin-provided servers, managed MCP config, claude.ai connectors and a `--mcp-config` argument are not seen, so those servers get no guidance. Detected names map characters outside `[A-Za-z0-9_-]` to `_` before matching, as Claude Code does for tool names. A server name that contains `__` never matches its tool prefix and gets no guidance. Detection gates the guidance text only. The tools list and `--allowedTools` stay as configured.
- The rendered block is a fixed frame, `## Session MCP tool guidance`, followed by one line per qualifying key. The frame tells the worker to apply a line only when tools named `mcp__<server>__*` for that server are in its tool list, because the launcher cannot see whether a server connects.
- Worktree freshness limit: a graph index describes the checkout it indexed. Guidance can tell the worker to index its own worktree once and to fall back to the main-checkout index. On fallback the worker confirms each answer with Read or Grep in the worktree before an edit. Provisioning does not index.
- Requires dev-loops 1.0.6 or later. An older CLI fails config load on the `extraToolsGuidance` key.

### File classification (`classify`)

The diff classifier assigns every changed file one kind: `code`, `docs`, `config`, `test`, `ci`, `asset` or `unknown`. The size budget, angle selection, tier matching and carry-forward all read this kind. Outside `docs/` and test directories, `.html`, `.htm`, `.css`, `.scss`, `.sass` and `.less` are `code`. Web source in a test directory or with a test-token basename stays `test`, like `.js`. The `asset` kind covers `.svg`, `.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`, `.ico`, `.woff`, `.woff2`, `.ttf`, `.otf` and `.eot`. Under `docs/` these files stay `docs`. An `asset` file counts as zero logic lines, stays out of the unclassified ratio and is never carried forward across a head bump. A size tier pattern that matches an `asset` file with changed text lines, such as `.svg`, blocks. A binary asset such as `.png` has no numstat lines and never blocks. `.md` is `docs` outside test directories and `.github/`, including under `site/`.

The top-level `classify` key extends these tables. Both sub-keys are optional.

```yaml
classify:
  extensions:
    code: [".vue", ".svelte"]
    asset: [".avif"]
  paths:
    - pattern: "site/assets/**"
      kind: asset
```

- Allowed kinds are `code`, `docs`, `config`, `test`, `ci` and `asset`. `unknown` and any other name fail config validation.
- An extension starts with `.` and has no whitespace, `/`, `\` or further `.`. Matching is case-insensitive on the file's last extension. The same extension under two kinds fails validation.
- A path pattern uses the `**/`, `**` and `*` glob subset (`*` stays within one segment, everything else is literal). It is anchored to the whole repo-relative path, normalizes `\` to `/` and is case-sensitive. A pattern is repo-relative: no surrounding whitespace, no leading `./` or `/`, no trailing `/`, and no empty, `.` or `..` segments. Other shapes fail config validation.
- Precedence: the first matching `paths` entry in listed order, then `extensions`, then the built-in tables. Paths under `.github/` stay `ci`. Dev-loop config sources (`.devloops`, `.devloops.yaml`, `.devloops.yml`, `.devloops.json`, `.pi/dev-loop/settings.*`, `.pi/dev-loop/defaults.*` and the shipped defaults file) stay `config`. An `extensions` entry overrides the built-in test-file conventions for that extension; use a `paths` entry for finer control.
- A later config layer replaces `classify.extensions` and `classify.paths` as whole values.
- `gates.<gate>.angles[].kinds` and `gates.<gate>.tiers[].match.kinds` accept `asset`.
- Requires dev-loops 1.0.6 or later. An older CLI rejects the `classify` key as unknown.

### Validation (`validation`)

The top-level `validation` key controls gate validation ([ADR 0137](../docs/decisions/0137-validation-modes-and-consumer-configured-commands.md)). All sub-keys are optional.

```yaml
validation:
  mode: ci-only            # default; "local" runs the full suite in the gate
  fullCommand: cargo test  # local mode only; non-Node full-validation command
  paths:                   # targeted worker checks, first match wins
    - match: "spec/models/**/*_spec.rb"
      surface: models
      commands: ["bundle exec rspec {path}"]
```

- `ci-only` (the default for consumers): current-head CI is the only full-validation authority. `dev-loops gate resolve-validation` runs no local suite and records an artifact with `status: "complete"` and `authority: "ci-authoritative"` when current-head CI is green. Red CI records `failed`. Pending, missing or wrong-head CI records `incomplete`, as does a rollup with no SUCCESS check (only SKIPPED or NEUTRAL entries, or only loop-derived checks; reason "current-head CI ran no checks"). This applies to every profile, targeted and full-repository. This works for repos without `package.json`, such as a Rails or Rust app.
- `local`: the gate runs the full suite. With `fullCommand` it runs that command through `sh -c` at the repo root (a Rust repo sets `cargo test`). Without it, `local` mode runs the `package.json` `verify` script through Bun. In `local` mode, a repo without `package.json` serves a targeted request with the declared `validation.fullCommand`, and the artifact records one suite named `full`.
- `paths` rules map changed paths to targeted worker commands for `resolveTargetedValidation(changedPaths)` (first match wins). When every path maps to non-empty commands, the result is the sorted, deduplicated union of `commands` and `gateSuites` across surfaces. `{path}` expands to each matching path, which may use only `A-Za-z0-9._/-` and no segment starting with `-`. An unmapped, invalid or unsafe templated path needs full validation. A rule with `commands: []` pins its paths to full validation. `gateSuites` names explicit gate `--suite` scripts.
- The gate reads the `validation` block only from the default branch (`origin/HEAD`). A PR that adds or changes the block records `incomplete` and runs nothing until that change merges. A malformed command (empty, multi-line) or map fails config validation. No command is read from a PR title, PR body or CLI argument.
- Only the `.devloops` family file is read for `validation`. A `validation` key in `.pi/dev-loop/defaults.*` passes config validation and has no effect.
- Requires a dev-loops CLI that knows the `validation` key. An older CLI rejects it as unknown.

### Available review angles

The shipped defaults activate the angles below. Every angle is listed once; `holistic` and `contradiction-lens` run in both the draft and preApproval pools. Angles you define yourself are opt-in — add them to your `gates.draft.angles` or `gates.preApproval.angles` and they use the prompts defined in the personas registry. Prompts can be overridden in consumer repos through `personas.<angle>.prompt` without depending on this repository's audit examples.

| Pool | Angles |
|---|---|
| Draft gate (`gates.draft.angles`) | `scope` — scope compliance; `coverage` — test coverage; `correctness` — acceptance criteria; `ci-guard` — CI/workflow reproducibility; `contract-surface` — schema/runtime/docs plus CLI help/stdout/stderr drift; `link-check` — Markdown links, anchors, doc paths; `config-drift` — config/schema/docs/runtime disagreement; `gate-evidence` — missing/stale gate-review PR evidence; `no-op` — ineffective tool or command usage; `input-validation` — CLI/API args, repo slugs, IDs, sentinels; `threat-model` — adversarial end-to-end threat model; `packaging-runtime` — installers, bundles, copied assets, runtime imports; `state-concurrency` — state files, locks, polling/process cleanup; `renderer-security` — HTML/attribute/URL escaping and user content; `determinism` — ordering, tie-breakers, strict stubs, env/time independence; `pr-comments` — PR comment and review-thread state; `code-conformance` — repo code conventions and patterns; `semantic-drift` — code versus documented behavior; `pr-description` — PR description accuracy; `deslop` — structural deslop audit |
| Pre-approval gate (`gates.preApproval.angles`) | `dry` — duplication; `kiss` — over-engineering; `yagni` — speculative features; `srp` — Single Responsibility; `soc` — Separation of Concerns; `deep` — structural quality / deslop audit; `ocp` — Open/Closed (extension over modification); `lsp` — Liskov Substitution (subtype contracts); `isp` — Interface Segregation (fat interfaces); `dip` — Dependency Inversion (abstractions); `docs` — documentation links, command references, stale docs; `pr-checklist` — PR checklist; `correctness-final` — final correctness pass on the current head; `ui-validation` — UI validation contract |
| Both pools | `holistic` — whole-change review; `contradiction-lens` — new rules against the rule registry |

### Workflow defaults

The optional `workflow` family carries repo-level workflow posture without hardcoding it into prose-only guidance. Shipped defaults stay permissive:

```yaml
workflow:
  requireRetrospective: false
  requireDraftFirst: false
  devModeDefault: false
  # baseBranch: unset by default — the loop auto-detects the repo's default
  # branch (origin/HEAD, else main/master).
```

- `requireRetrospective` — when enabled by repo settings, startup routes normally and reports `pendingRetrospectives`; `merge-pr` refuses to merge until the checkpoint is discharged (ADR 0125). Retrospective findings stay advisory.
- `requireRetrospectiveGate` — **removed (issue #1077)**. The retrospective is now advisory: it always runs and returns findings to the conductor via the envelope's `retrospectiveFindings` field and an advisory PR comment; findings are advisory; under `requireRetrospective` the only retrospective input that blocks `merge-pr` is an undischarged earlier checkpoint. There is no longer a merge-gate config key for the retrospective.
- `requireDraftFirst` — marks draft-first PR creation as required workflow policy for repos that opt in
- `devModeDefault` — declares that local implementation should default to formal dev mode; this is config-only for now and establishes source-of-truth config plus docs for future runtime consumers
- `baseBranch` — repo-level base/integration branch override (bare name, e.g. `main` or `spike/shakapacker-to-vite`). When set, a full dev-loop (worktree creation, PR targeting, issue-less scope measurement) operates against it instead of the auto-detected default branch — the knob a phased migration (or any work that must stack on one long-lived integration branch and never touch `main`) needs. Unset (the default) keeps auto-detecting `origin/HEAD`, else `main`/`master`. Resolved by `resolveBaseBranch(config, { cwd })` from `@dev-loops/core/config`; `scripts/loop/ensure-worktree.mjs` and `scripts/github/create-pr.mjs` stay config-agnostic — the resolved base is injected as an explicit `--base` by the resolver's `nextAction` hint and the `local-implementation` SKILL's PR-create step.

### Config precedence

1. Built-in defaults (`packages/core/src/config/config.mjs` `BUILT_IN_DEFAULTS`)
2. Shipped defaults (`packages/core/src/config/extension-defaults.yaml`)
3. Repo-local override layer (`.pi/dev-loop/defaults.*` — merges on top of the shipped defaults when present)
4. Consumer settings (`.devloops` at repo root — preferred and authoritative when present; `.devloops.yaml`/`.devloops.yml`/`.devloops.json` also load; legacy `.pi/dev-loop/settings.*` and `.pi/dev-loop/overrides.*` load only as a fallback when no `.devloops` is present; when `.devloops` exists they are ignored, with a deprecation warning)

### Adding custom review angles

1. Add the angle name to `gates.draft.angles` or `gates.preApproval.angles`
2. Add a `personas.<angle>` entry with a `persona` agent name and a `prompt` instruction
3. Create the corresponding `Agent file` (`agents/<persona>.agent.md`) if using a new persona
4. Optionally set a per-angle model override via `models.roles.<angle>`

### Config format

YAML is preferred (`.yaml` or `.yml`). JSON (`.json`) is supported as a fallback for backward compatibility. When both exist, YAML takes priority.

Config is validated at runtime by Zod schemas (`packages/core/src/config/config.mjs`).

## Runtime / build / test contract

Current Phase 3+ contract:
- Node runtime floor: `>=24` (from `package.json`)
- Pi host expectations are documented from current peer dependencies rather than a tested pinned Pi version range
- the extension is source-loaded from `./extension/index.ts` through `package.json` `pi.extensions`
- the package exposes `skills` through `package.json` `pi.skills` for install-based global skill loading
- the shell CLI is exposed through `package.json` `bin.dev-loops`
- the extension syncs packaged agent files (`agents/*.agent.md`) into `~/.agents/` on `session_start` so user-level agents are available outside this repo
- on `session_start` the extension also registers itself as a required child extension for that session (via `pi-subagents/required-child-extensions`), so a foreground (`async: false`) child loads the same `tool_call` read-only role gate; the registration is disposed on `session_shutdown` and is a guarded optional import when pi-subagents is absent
- package install/update happens through `pi install` / `pi update`
- this phase does not yet claim a specific supported `gh` version; it only checks `gh` presence and authentication state
- this phase does not require a separate compiled build or `dist/` pipeline

Validation and test suites are intentionally explicit:
- Bun 1.4.1 is pinned exactly in `package.json` and is the contributor/CI installer, script runner, and unit-test runner; begin with `bun install --frozen-lockfile`
- Choose targeted checks with `resolveTargetedValidation(changedPaths)`; launch a local full-repository run only through `dev-loops gate resolve-validation --profile full-repository --repo <owner/name> --pr <number> --gate <gate> --head-sha <full SHA>` from the PR worktree on a clean commit
- The full suite runs `bun run verify` underneath; it attempts every suite with attributable output, and any suite failing fails verify. `bun run test` remains its alias
- `bun run test:extension`
- `bun run test:scripts`
- `bun run test:assets`
- `bun run test:dev-loop`
- `bun run test:playwright:viewer` remains an explicit Node-run viewer/browser smoke, not part of the default root verify path
- published extension/CLI consumers require only Node `>=24`; npm remains intentional for package install, pack, registry, dist-tag, provenance, and publish workflows

## Design rule

Both wrappers should stay thin. Shared workflow mechanics should live in deterministic `packages/core/` modules and `scripts/`, not in extension-only or CLI-only command logic. Runtime command support that bridges both surfaces belongs in `lib/dev-loops-core.mjs`. See [Library vs Packages Core Boundary](../docs/lib-vs-packages-core-boundary.md) for the full ownership rule.
