dev-loops 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/.claude/.claude-plugin/plugin.json +1 -1
  2. package/.claude/agents/dev-loop.md +1 -1
  3. package/.claude/agents/refiner.md +1 -0
  4. package/.claude/commands/auto.md +7 -0
  5. package/.claude/commands/continue.md +15 -0
  6. package/.claude/commands/info.md +7 -0
  7. package/.claude/commands/start-spike.md +16 -0
  8. package/.claude/commands/start.md +7 -0
  9. package/.claude/commands/status.md +6 -0
  10. package/.claude/hooks/_run-context.mjs +11 -4
  11. package/.claude/skills/dev-loop/SKILL.md +21 -6
  12. package/.claude/skills/dev-loop/templates/slides-story-review.md +54 -0
  13. package/.claude/skills/docs/artifact-authority-contract.md +86 -31
  14. package/.claude/skills/docs/local-planning-flow.md +63 -0
  15. package/.claude/skills/docs/local-planning-worked-example.md +139 -0
  16. package/.claude/skills/docs/merge-preconditions.md +35 -0
  17. package/.claude/skills/docs/plan-file-contract.md +37 -0
  18. package/.claude/skills/docs/release-runbook.md +45 -0
  19. package/.claude/skills/docs/retrospective-checkpoint-contract.md +55 -7
  20. package/.claude/skills/docs/spike-mode-contract.md +237 -0
  21. package/.claude/skills/docs/ui-e2e-scoping-step.md +102 -0
  22. package/.claude/skills/local-implementation/SKILL.md +1 -1
  23. package/CHANGELOG.md +73 -0
  24. package/README.md +21 -1
  25. package/agents/dev-loop.agent.md +8 -1
  26. package/agents/refiner.agent.md +1 -0
  27. package/cli/index.mjs +2 -0
  28. package/extension/index.ts +10 -1
  29. package/extension/presentation.ts +15 -0
  30. package/lib/dev-loops-core.mjs +141 -0
  31. package/package.json +8 -3
  32. package/scripts/claude/generate-claude-assets.mjs +15 -1
  33. package/scripts/github/capture-review-threads.mjs +20 -2
  34. package/scripts/github/comment-issue.mjs +181 -0
  35. package/scripts/github/fetch-ci-logs.mjs +215 -0
  36. package/scripts/github/list-issues.mjs +191 -0
  37. package/scripts/github/probe-copilot-review.mjs +69 -3
  38. package/scripts/github/upsert-checkpoint-verdict.mjs +18 -3
  39. package/scripts/lib/jq-output.mjs +297 -0
  40. package/scripts/loop/_handoff-contract.mjs +1 -0
  41. package/scripts/loop/check-retro-tooling.mjs +246 -0
  42. package/scripts/loop/copilot-pr-handoff.mjs +21 -3
  43. package/scripts/loop/detect-pr-gate-coordination-state.mjs +65 -2
  44. package/scripts/loop/docs-grill-contract.mjs +70 -0
  45. package/scripts/loop/info.mjs +21 -2
  46. package/scripts/loop/pr-runner-coordination.mjs +20 -4
  47. package/scripts/loop/resolve-dev-loop-startup.mjs +176 -5
  48. package/scripts/loop/resolve-pr-conflicts.mjs +357 -0
  49. package/scripts/loop/run-conductor-cycle.mjs +5 -0
  50. package/scripts/loop/run-watch-cycle.mjs +77 -3
  51. package/scripts/loop/slides-story-review-contract.mjs +123 -0
  52. package/scripts/pages/build-site.mjs +136 -0
  53. package/scripts/projects/add-queue-item.mjs +12 -2
  54. package/scripts/projects/list-queue-items.mjs +12 -2
  55. package/scripts/projects/move-queue-item.mjs +12 -2
  56. package/scripts/projects/resolve-active-board-item.mjs +193 -0
  57. package/scripts/refine/_refine-helpers.mjs +20 -0
  58. package/scripts/refine/exit-spike.mjs +186 -0
  59. package/scripts/refine/promote-plan.mjs +387 -0
  60. package/scripts/refine/refine-plan-file.mjs +165 -0
  61. package/scripts/refine/scaffold-spike-file.mjs +183 -0
  62. package/scripts/refine/validate-plan-file.mjs +64 -0
  63. package/scripts/refine/validate-spike-file.mjs +87 -0
  64. package/scripts/release/extract-changelog-section.mjs +111 -0
  65. package/skills/dev-loop/SKILL.md +24 -2
  66. package/skills/dev-loop/templates/slides-story-review.md +54 -0
  67. package/skills/docs/artifact-authority-contract.md +86 -31
  68. package/skills/docs/local-planning-flow.md +63 -0
  69. package/skills/docs/local-planning-worked-example.md +139 -0
  70. package/skills/docs/merge-preconditions.md +35 -0
  71. package/skills/docs/plan-file-contract.md +37 -0
  72. package/skills/docs/release-runbook.md +45 -0
  73. package/skills/docs/retrospective-checkpoint-contract.md +55 -7
  74. package/skills/docs/spike-mode-contract.md +237 -0
  75. package/skills/docs/ui-e2e-scoping-step.md +102 -0
  76. package/skills/local-implementation/SKILL.md +1 -1
@@ -0,0 +1,102 @@
1
+ # UI e2e scoping step
2
+
3
+ Canonical owner for **when the shared UI/mobile e2e loop is required**. Inclusion
4
+ is **path-triggered and deterministic** — it does not depend on a human annotating
5
+ the PR or a phase doc.
6
+
7
+ ## Trigger: a rendered-artifact change requires UI e2e coverage
8
+
9
+ A PR that **adds or modifies a rendered HTML artifact** MUST run the shared UI e2e
10
+ assertions (mobile + desktop) AND register that artifact in the e2e suite. A
11
+ "rendered artifact" is anything that renders to a served page or component:
12
+
13
+ - a presentation deck — `docs/presentations/*.html` (registered in `DECK_REGISTRY`)
14
+ - an article page — `docs/articles/*.html` (registered in `ARTICLE_REGISTRY`; the
15
+ intro article is the published landing page)
16
+ - the inspect-run viewer's served page/component — `scripts/loop/inspect-run-viewer.mjs`
17
+
18
+ The trigger is the PR's **changed-file set** matched against these explicit globs.
19
+ It is conservative on purpose (issue #976 scope): only artifacts that render to a
20
+ page/component, matched by exact directory + single-segment `*.html` globs (no
21
+ recursion) and the viewer source path.
22
+
23
+ Registration is keyed on the **full repo-relative path**, not the basename, so
24
+ `docs/articles/X.html` and `docs/presentations/X.html` (which share basenames,
25
+ e.g. `introducing-dev-loops.html`) are **distinct** artifacts and can never alias
26
+ onto each other — editing the article never counts as deck coverage and vice versa.
27
+
28
+ ### Examples — required
29
+
30
+ - `docs/presentations/introducing-dev-loops.html` edited → UI e2e **required**
31
+ (deck-smoke CI job runs the deck fit specs).
32
+ - `docs/articles/introducing-dev-loops.html` (the landing page) edited → UI e2e
33
+ **required** (article-smoke CI job runs the article fit specs); it is its own
34
+ registration, NOT covered by the same-named deck.
35
+ - A new `docs/articles/new-page.html` added → UI e2e **required**, and because the
36
+ new page is not yet in `ARTICLE_REGISTRY` the gate **fails closed** until it is
37
+ registered (add an entry + a thin spec calling `defineArticleSuite`).
38
+ - `scripts/loop/inspect-run-viewer.mjs` edited → UI e2e **required** (the viewer is
39
+ registered in `VIEWER_REGISTRY`).
40
+
41
+ ### Examples — not required
42
+
43
+ - `packages/core/src/loop/copilot-loop-state.mjs`, `README.md`, `*.test.mjs`,
44
+ `docs/articles/foo.md` (not `.html`), or a nested `docs/articles/sub/x.html`
45
+ (the glob is single-segment) → UI e2e **not required**; the gate passes through.
46
+
47
+ ## Register the artifact in the suite
48
+
49
+ "Registered" means the artifact appears in the shared registries:
50
+
51
+ - decks → `DECK_REGISTRY` in `test/playwright/harness/deck-fit-harness.mjs`
52
+ (served from `docs/presentations/<deck>`) with a thin spec calling `defineDeckSuite`.
53
+ - articles → `ARTICLE_REGISTRY` in `test/playwright/harness/deck-fit-harness.mjs`
54
+ (served from `docs/articles/<file>`) with a thin spec calling `defineArticleSuite`
55
+ (shared fit/CSP/no-horizontal-scroll assertions, minus the deck's per-section
56
+ named captures).
57
+ - the viewer → `VIEWER_REGISTRY` in
58
+ `test/playwright/harness/inspect-run-viewer-harness.mjs`.
59
+
60
+ The deterministic membership list the gate checks against lives in
61
+ `packages/core/src/loop/ui-e2e-scoping.mjs` (`REGISTERED_ARTIFACT_PATHS`, keyed by
62
+ full repo-relative path; `VIEWER_ARTIFACT_ID`). A sync test keeps
63
+ `REGISTERED_ARTIFACT_PATHS` from drifting from `DECK_REGISTRY` + `ARTICLE_REGISTRY`;
64
+ the single-valued `VIEWER_ARTIFACT_ID` is not import-checked against `VIEWER_REGISTRY`
65
+ (that harness pulls `@playwright/test` into core), so keep the two in sync by hand.
66
+
67
+ ## Fail-closed semantics
68
+
69
+ The check is wired as a gate precondition in `evaluatePrGateCoordination`
70
+ (`packages/core/src/loop/pr-gate-coordination.mjs`), at a seam distinct from the
71
+ mergeability (#980) and retrospective (#982) preconditions. It fails closed:
72
+
73
+ - **Unregistered rendered artifact touched** → gate blocks with
74
+ `nextAction: run_ui_e2e_suite`, reason naming the artifact and that it needs
75
+ registration in the suite.
76
+ - **Registered, but UI e2e suite did not pass for this head** (`uiE2ePassed` is
77
+ `false`/unknown) → gate blocks with the same action, reason that the suite must
78
+ run and pass first.
79
+ - **Non-UI change** → `required: false`, gate passes through untouched.
80
+
81
+ The detect layer reads the changed-file set from `gh pr view --json files` and
82
+ derives `uiE2ePassed` from the `statusCheckRollup` UI e2e check(s)
83
+ (`UI_E2E_CHECK_NAMES`): present UI e2e checks must all be `SUCCESS`; if none is
84
+ present the value is unknown and the gate fails closed.
85
+
86
+ Each rendered-artifact family has a stable, satisfiable CI job in
87
+ `.github/workflows/ci.yml`, path/diff-conditioned in the `changes` job and named
88
+ to match `UI_E2E_CHECK_NAMES` so the gate can read a real signal:
89
+
90
+ - `viewer-smoke` → inspect-run viewer spec (triggered by the viewer source set).
91
+ - `deck-smoke` → both presentation deck fit specs (triggered by
92
+ `docs/presentations/**` and the deck specs/harness/configs).
93
+ - `article-smoke` → both article fit specs (triggered by `docs/articles/**` and the
94
+ article specs/harness/configs).
95
+
96
+ So a deck- or article-only PR has a CI check that can actually satisfy the gate;
97
+ without these jobs such a PR would fail closed with no satisfiable signal.
98
+
99
+ ## Non-goals
100
+
101
+ Not always-on screenshot testing; not mandatory multi-browser. The criterion is
102
+ only the conservative path-glob + registry-membership + passing-coverage check.
@@ -508,7 +508,7 @@ After the phase plan passes review:
508
508
  3. Run local validation:
509
509
  - `npm run verify`
510
510
  - when a narrower local check is more honest for the touched slice, say so explicitly and run the narrowest justified subset instead of pretending the full verify path was unnecessary
511
- - for user-facing HTML/UI/component slices when the user opts in, add a bounded deterministic browser smoke harness (prefer fixture-backed Playwright WebKit plus screenshot capture); use `npm run test:playwright:viewer` when that viewer/browser surface is part of the slice, and wire it into CI once it becomes required validation for that slice
511
+ - for a *rendered* HTML artifact (a deck `docs/presentations/*.html`, an article `docs/articles/*.html`, or the inspect-run viewer source) UI e2e coverage is **required and auto-scoped** — not opt-in. Touching one obliges you to register it in the shared harness and have a passing UI e2e suite, or the gate fails closed (see [UI e2e scoping step](../docs/ui-e2e-scoping-step.md)); run the matching shared suite locally, e.g. `npm run test:playwright:intro-deck` / `:intro-article` / `:viewer`. For a bespoke local UI surface that is not a registered rendered artifact, the WebKit smoke seam (fixture-backed Playwright WebKit plus screenshot capture) remains available without being mandatory
512
512
  - **Commit immediately after validation passes.** Once `npm run verify` (or the narrowest justified validation subset) is green, stage and commit the current slice of work with an atomic commit message. For tracker-backed sessions, also push the commit. In non-interactive subagent sessions, commit authorization is implicit — the subagent was dispatched to implement and must commit before exiting. In interactive sessions, wait for coordination agent authorization. Do not defer commits — uncommitted changes when the session terminates are a workflow defect.
513
513
  4. Review the implementation against the merged phase plan.
514
514
  5. Run the default pre-approval gate as a full review / fix loop on the branch before calling it review-complete, approval-ready, merge-ready, or ready for final handoff:
package/CHANGELOG.md CHANGED
@@ -4,6 +4,79 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.6.0 - 2026-06-29
8
+
9
+ ### Added
10
+
11
+ - **`/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.
12
+
13
+ - **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.
14
+
15
+ - **`/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.
16
+
17
+ - 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.
18
+
19
+ - **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`.
20
+
21
+ - **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).
22
+
23
+ ### Changed
24
+
25
+ - **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).
26
+
27
+ - **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.
28
+
29
+ - **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.
30
+
31
+ ### Fixed
32
+
33
+ - **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.
34
+
35
+ - **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.
36
+
37
+ ## 0.5.0 - 2026-06-28
38
+
39
+ ### Added
40
+
41
+ - **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).
42
+
43
+ ### Changed
44
+
45
+ - **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.
46
+ - **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.
47
+ - **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).
48
+ - **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.
49
+ - **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.
50
+
51
+ ### Added
52
+ - **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.
53
+ - **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.
54
+ - **"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.
55
+ - **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.
56
+
57
+ - **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.
58
+ - **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.
59
+ - **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.
60
+ - **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.
61
+ - **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.
62
+ - **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.
63
+ - **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).
64
+ - **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`.
65
+ - **`--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.
66
+ - **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.
67
+ - **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`.
68
+ - **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.
69
+ - **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`.
70
+ - **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`.
71
+ - **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`.
72
+ - **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.
73
+ - **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.
74
+
75
+ ### Fixed
76
+
77
+ - **`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.
78
+ - **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.
79
+
7
80
  ## 0.4.0
8
81
 
9
82
  ### Added
package/README.md CHANGED
@@ -30,6 +30,21 @@ Use **`dev-loop`** as the single public workflow entrypoint:
30
30
 
31
31
  The `dev-loop` entrypoint resolves authoritative state, picks the correct internal strategy, and routes work deterministically. Users never need to choose internal strategy names. See the canonical shorthand example mapping in the [Public Dev Loop Contract](./skills/docs/public-dev-loop-contract.md).
32
32
 
33
+ ### Direct commands
34
+
35
+ The same entrypoints are also available as direct named commands — thin wrappers over the public contract, no separate routing:
36
+
37
+ | Command | Equivalent intent |
38
+ | --- | --- |
39
+ | `/dev-loops:start <issue>` | start dev loop on issue `<issue>` |
40
+ | `/dev-loops:auto <issue>` | auto dev loop on issue `<issue>` (autonomous until human approval) |
41
+ | `/dev-loops:continue [issue\|pr]` | continue dev loop on `<issue\|pr>`; bare resumes the single in-progress board item |
42
+ | `/dev-loops:start-spike <question>` | start a time-boxed spike from a question (or `--file <path>` for a pre-authored findings file) |
43
+ | `/dev-loops:info <issue|pr>` | read-only state summary (`loop info`) |
44
+ | `/dev-loops:status` | dev-loop readiness (gh auth, git repo, subagent) |
45
+
46
+ `/dev-loops:dev-loop` remains the catch-all router. Inside Pi the same set is reachable as `/dev-loops start|auto|continue|start-spike|info|status …`.
47
+
33
48
  ## Install from npm
34
49
 
35
50
  ### CLI only
@@ -231,9 +246,13 @@ subagent({
231
246
 
232
247
  Do not call internal routed skills such as `local-implementation`, `copilot-pr-followup`, or `final-approval` directly; `dev-loop` selects them from the current GitHub/repo state.
233
248
 
234
- The `/dev-loops` command surface is for readiness and configuration UX:
249
+ The `/dev-loops` command surface covers the direct dev-loop entrypoints plus readiness and configuration UX:
235
250
 
236
251
  ```bash
252
+ /dev-loops start <issue> # dispatch: start dev loop on issue <issue>
253
+ /dev-loops auto <issue> # dispatch: auto dev loop on issue <issue>
254
+ /dev-loops continue [issue|pr] # dispatch: continue dev loop on <issue|pr>; bare = current in-progress board item
255
+ /dev-loops info <issue|pr> # read-only state summary
237
256
  /dev-loops status
238
257
  /dev-loops doctor
239
258
  /dev-loops gates
@@ -274,3 +293,4 @@ CI splits into a small changed-files gate plus parallel `verify` and conditional
274
293
  - [UI Smoke Harness](./docs/ui-smoke-harness.md) — reusable local Playwright/WebKit smoke baseline
275
294
  - [UI Artifact Contract](./docs/ui-artifact-contract.md) — screenshot/state artifact contract and CI-promotion rules
276
295
  - [UI Designer Review Loop](./docs/ui-designer-review-loop.md) — designer + vision (`uiReviewMode: vision`) review loop contract
296
+ - [Slides Story Review Loop](./docs/slides-story-review-loop.md) — bounded slides content & storytelling reviewer (narrative, not pixels)
@@ -26,7 +26,14 @@ The envelope is the primary handoff artifact — it is derived from resolver out
26
26
 
27
27
  **Construction sequence:**
28
28
  <!-- pi-only -->
29
- **CLI invocation (`<dev-loops-package-root>`):** dev-loop CLI commands are invoked as `node <dev-loops-package-root>/cli/index.mjs <verb...>` using the package-local CLI rather than `npx`, so they resolve unambiguously from the installed package without a global install. Resolve `<dev-loops-package-root>` from this agent's own installed path: this agent is installed at `<package-root>/.pi/agents/dev-loop.agent.md`, so the package root is `../..` from this agent's directory (`agents` `.pi` package root). (The `dev-loop` skill resolves it analogously from its own installed path.)
29
+ **CLI invocation (`<dev-loops-package-root>`):** dev-loop CLI commands are invoked as `node <dev-loops-package-root>/cli/index.mjs <verb...>` using the package-local CLI rather than `npx`, so they resolve unambiguously from the installed package without a global install. Resolve `<dev-loops-package-root>` via the first of these **bounded** candidates whose `cli/index.mjs` exists never assume a single fixed layout (this agent may be installed user-level at `~/.agents/`, where the old `../..` package-relative guess resolves to `~`, not the package):
30
+
31
+ 1. **Node module resolution** (best-effort first try): `node -e "try{const p=require('node:path');console.log(p.resolve(p.dirname(require.resolve('dev-loops/cli/index.mjs')),'..'))}catch{process.exit(1)}"` — resolves the package root when `dev-loops` is reachable from Node's module search path (notably under `~/.pi/agent/npm`); this is cwd-dependent and commonly misses from a target-repo cwd, so the probe is wrapped in try/catch (no stack trace, exits non-zero on miss) — treat a non-zero exit as "probe missed, try the next candidate", not a hard failure.
32
+ 2. **Pi user-agent npm root** (reliable for user-level installs): `~/.pi/agent/npm/node_modules/dev-loops`.
33
+ 3. **Package-relative (legacy):** `../..` from this agent's own directory (the original package-local install layout).
34
+ 4. **Global npm root:** `$(npm root -g)/dev-loops`.
35
+
36
+ NEVER fall back to `find /` or any unbounded filesystem walk to locate the CLI — it stalls and trips the needs-attention timeout. If every bounded candidate fails, stop and ask the orchestrator/operator for the dev-loops package root rather than searching. (The `dev-loop` skill resolves it analogously.)
30
37
  <!-- /pi-only -->
31
38
 
32
39
  1. Run the deterministic startup resolver to produce the authoritative state bundle: `node <dev-loops-package-root>/cli/index.mjs loop startup --issue <n>` for issues, or `node <dev-loops-package-root>/cli/index.mjs loop startup --pr <n>` for PRs.
@@ -46,6 +46,7 @@ For the active phase, require and produce:
46
46
  - Do not invent audit findings when no audit artifact was provided
47
47
  - when the phase includes watcher or predicate-driven behavior: explicit timeout semantics and negative-case expectations for non-target identities/events
48
48
  - when the phase relies on package-first shared helpers inside a source-loaded workspace: explicit integration expectations about whether local callers use published package imports or a thin source/workspace adapter during development
49
+ - cross-check the phase's claims against the contracts and docs they reference (the autonomous docs-grill step, see ../docs/docs-grill-step.md): surface code-vs-doc drift, stale references, and contract-surface inaccuracies as refinement findings while the claims are still being verified
49
50
 
50
51
  ## Working style
51
52
  - Prefer parallel fresh-context fan-out/fan-in when it improves refinement quality or surfaces materially different variants.
package/cli/index.mjs CHANGED
@@ -58,6 +58,7 @@ const SUBCOMMAND_ROUTES = {
58
58
  "archive-done": "scripts/projects/archive-done-items.mjs",
59
59
  "sync-status": "scripts/projects/sync-item-status.mjs",
60
60
  ensure: "scripts/projects/ensure-queue-board.mjs",
61
+ "resolve-active": "scripts/projects/resolve-active-board-item.mjs",
61
62
  },
62
63
  queue: {
63
64
  run: "scripts/loop/run-queue.mjs",
@@ -68,6 +69,7 @@ const SUBCOMMAND_ROUTES = {
68
69
  "archive-done": "scripts/projects/archive-done-items.mjs",
69
70
  "sync-status": "scripts/projects/sync-item-status.mjs",
70
71
  ensure: "scripts/projects/ensure-queue-board.mjs",
72
+ "resolve-active": "scripts/projects/resolve-active-board-item.mjs",
71
73
  },
72
74
  inspect: {
73
75
  run: "scripts/loop/inspect-run.mjs",
@@ -8,6 +8,7 @@ import { createExtensionCoreRuntime } from './checks.ts';
8
8
  import { createPostMergeUpdateHook } from './post-merge-update.ts';
9
9
  import { createPiExtensionAdapter, type ExtensionAPI } from './pi-extension-adapter.ts';
10
10
  import {
11
+ buildEntrypointLines,
11
12
  buildHelpLines,
12
13
  buildInspectLines,
13
14
  buildInspectNotification,
@@ -72,7 +73,7 @@ export default function (pi: ExtensionAPI, runtimeOverrides: ExtensionRuntimeOve
72
73
  });
73
74
 
74
75
  adapter.registerCommand('dev-loops', {
75
- description: 'Manage dev-loops readiness and inspect-run local UI lifecycle: /dev-loops [help|status|doctor|gates|hide|inspect ...]',
76
+ description: 'Run a dev-loop entrypoint or manage readiness: /dev-loops [start <issue>|auto <issue>|continue [issue|pr]|start-spike <question>|info <issue|pr>|status|doctor|gates|hide|inspect ...]',
76
77
  handler: async (args, ctx) => {
77
78
  const result = await executeDevLoopsCommand({
78
79
  input: args,
@@ -89,6 +90,14 @@ export default function (pi: ExtensionAPI, runtimeOverrides: ExtensionRuntimeOve
89
90
  ctx.ui.setWidget(WIDGET_KEY, buildHelpLines(), { placement: 'belowEditor' });
90
91
  ctx.ui.notify('dev-loops help', 'info');
91
92
  return;
93
+ case 'entrypoint':
94
+ ctx.ui.setWidget(WIDGET_KEY, buildEntrypointLines(result.action, result.intent), { placement: 'belowEditor' });
95
+ ctx.ui.notify(`dev-loops ${result.action}: ${result.intent}`, 'info');
96
+ return;
97
+ case 'start_spike':
98
+ ctx.ui.setWidget(WIDGET_KEY, buildEntrypointLines('start-spike', result.intent), { placement: 'belowEditor' });
99
+ ctx.ui.notify(`dev-loops start-spike: ${result.intent}`, 'info');
100
+ return;
92
101
  case 'checks':
93
102
  ctx.ui.setWidget(WIDGET_KEY, buildWidgetLines(result.action as Extract<DevLoopsAction, 'doctor' | 'status'>, result.checks), {
94
103
  placement: 'belowEditor',
@@ -35,11 +35,26 @@ export function orderedSetupSteps(checks: DevLoopCheck[]): string[] {
35
35
  ];
36
36
  }
37
37
 
38
+ export function buildEntrypointLines(action: string, intent: string): string[] {
39
+ return [
40
+ `dev-loops ${action}`,
41
+ `Dispatch this public intent through the dev-loop skill:`,
42
+ `/skill:dev-loop ${intent}`,
43
+ 'The dev-loop skill resolves authoritative state and routes deterministically — no strategy choice needed.',
44
+ ];
45
+ }
46
+
38
47
  export function buildHelpLines(): string[] {
39
48
  return [
40
49
  'dev-loops help',
41
50
  'Workflow entry:',
42
51
  '- /skill:dev-loop — single public entrypoint; routing handles the rest',
52
+ 'Direct entrypoints:',
53
+ '- /dev-loops start <issue>',
54
+ '- /dev-loops auto <issue>',
55
+ '- /dev-loops continue [issue|pr]',
56
+ '- /dev-loops start-spike <question>',
57
+ '- /dev-loops info <issue|pr>',
43
58
  'Commands:',
44
59
  '- /dev-loops status',
45
60
  '- /dev-loops doctor',