dev-loops 0.3.0 → 0.5.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.
- package/.claude/.claude-plugin/plugin.json +1 -1
- package/.claude/agents/dev-loop.md +1 -1
- package/.claude/agents/refiner.md +1 -0
- package/.claude/hooks/_run-context.mjs +9 -16
- package/.claude/skills/copilot-pr-followup/SKILL.md +9 -7
- package/.claude/skills/dev-loop/SKILL.md +17 -7
- package/.claude/skills/dev-loop/templates/slides-story-review.md +54 -0
- package/.claude/skills/docs/anti-patterns.md +1 -1
- package/.claude/skills/docs/artifact-authority-contract.md +86 -31
- package/.claude/skills/docs/copilot-loop-operations.md +1 -1
- package/.claude/skills/docs/issue-intake-procedure.md +4 -0
- package/.claude/skills/docs/local-planning-flow.md +63 -0
- package/.claude/skills/docs/local-planning-worked-example.md +139 -0
- package/.claude/skills/docs/merge-preconditions.md +100 -1
- package/.claude/skills/docs/plan-file-contract.md +37 -0
- package/.claude/skills/docs/retrospective-checkpoint-contract.md +55 -7
- package/.claude/skills/docs/spike-mode-contract.md +237 -0
- package/.claude/skills/docs/stop-conditions.md +1 -0
- package/.claude/skills/docs/tracker-first-loop-state.md +6 -6
- package/.claude/skills/local-implementation/SKILL.md +8 -2
- package/CHANGELOG.md +69 -0
- package/README.md +9 -2
- package/agents/refiner.agent.md +1 -0
- package/cli/index.mjs +21 -2
- package/extension/README.md +1 -1
- package/package.json +6 -4
- package/scripts/README.md +2 -2
- package/scripts/github/capture-review-threads.mjs +20 -2
- package/scripts/github/offer-human-handoff.mjs +147 -0
- package/scripts/github/probe-ci-status.mjs +468 -0
- package/scripts/github/probe-copilot-review.mjs +69 -3
- package/scripts/github/request-copilot-review.mjs +3 -3
- package/scripts/github/resolve-handoff-candidates.mjs +412 -0
- package/scripts/github/upsert-checkpoint-verdict.mjs +20 -5
- package/scripts/lib/jq-output.mjs +297 -0
- package/scripts/loop/_stale-runner-detection.mjs +1 -1
- package/scripts/loop/_worktree-path.mjs +27 -0
- package/scripts/loop/check-retro-tooling.mjs +246 -0
- package/scripts/loop/cleanup-worktree.mjs +175 -0
- package/scripts/loop/copilot-pr-handoff.mjs +22 -4
- package/scripts/loop/detect-pr-gate-coordination-state.mjs +35 -2
- package/scripts/loop/detect-stale-runner.mjs +3 -4
- package/scripts/loop/docs-grill-contract.mjs +70 -0
- package/scripts/loop/ensure-worktree.mjs +219 -0
- package/scripts/loop/info.mjs +21 -2
- package/scripts/loop/outer-loop.mjs +1 -1
- package/scripts/loop/pr-runner-coordination.mjs +21 -5
- package/scripts/loop/pre-flight-gate.mjs +10 -7
- package/scripts/loop/pre-push-main-guard.mjs +4 -4
- package/scripts/loop/provision-worktree.mjs +243 -0
- package/scripts/loop/resolve-dev-loop-startup.mjs +181 -10
- package/scripts/loop/resolve-pr-conflicts.mjs +357 -0
- package/scripts/loop/run-queue.mjs +25 -1
- package/scripts/loop/run-watch-cycle.mjs +152 -25
- package/scripts/loop/slides-story-review-contract.mjs +123 -0
- package/scripts/pages/build-site.mjs +136 -0
- package/scripts/projects/add-queue-item.mjs +32 -7
- package/scripts/projects/archive-done-items.mjs +2 -1
- package/scripts/projects/ensure-queue-board.mjs +2 -1
- package/scripts/projects/list-queue-items.mjs +14 -3
- package/scripts/projects/move-queue-item.mjs +14 -3
- package/scripts/projects/reorder-queue-item.mjs +5 -4
- package/scripts/projects/sync-item-status.mjs +2 -1
- package/scripts/refine/_refine-helpers.mjs +20 -0
- package/scripts/refine/exit-spike.mjs +186 -0
- package/scripts/refine/promote-plan.mjs +387 -0
- package/scripts/refine/refine-plan-file.mjs +165 -0
- package/scripts/refine/validate-plan-file.mjs +64 -0
- package/scripts/refine/validate-spike-file.mjs +87 -0
- package/skills/copilot-pr-followup/SKILL.md +9 -7
- package/skills/dev-loop/SKILL.md +13 -3
- package/skills/dev-loop/templates/slides-story-review.md +54 -0
- package/skills/docs/anti-patterns.md +1 -1
- package/skills/docs/artifact-authority-contract.md +86 -31
- package/skills/docs/copilot-loop-operations.md +1 -1
- package/skills/docs/issue-intake-procedure.md +4 -0
- package/skills/docs/local-planning-flow.md +63 -0
- package/skills/docs/local-planning-worked-example.md +139 -0
- package/skills/docs/merge-preconditions.md +100 -1
- package/skills/docs/plan-file-contract.md +37 -0
- package/skills/docs/retrospective-checkpoint-contract.md +55 -7
- package/skills/docs/spike-mode-contract.md +237 -0
- package/skills/docs/stop-conditions.md +1 -0
- package/skills/docs/tracker-first-loop-state.md +6 -6
- package/skills/local-implementation/SKILL.md +8 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Tracker-First Story-to-PR Contract
|
|
2
2
|
|
|
3
3
|
This document defines the adapter-agnostic MVP contract for the tracker-first
|
|
4
|
-
story-to-PR workflow in `
|
|
4
|
+
story-to-PR workflow in `dev-loops`.
|
|
5
5
|
|
|
6
6
|
**MVP invariant: one tracker work item → one GitHub PR.**
|
|
7
7
|
|
|
@@ -43,9 +43,9 @@ are out of scope for this slice.
|
|
|
43
43
|
| PR lifecycle facts | GitHub | Draft / ready-for-review, open / merged / closed, branch, head SHA |
|
|
44
44
|
| PR review and CI facts | GitHub | Reviewer assignments, review states, check-run results, merge status |
|
|
45
45
|
| Decision content | ADR / RFC artifacts (when linked) | Architecture decisions and RFCs referenced from PR body |
|
|
46
|
-
| PR projection and reverse-sync logic | `
|
|
46
|
+
| PR projection and reverse-sync logic | `dev-loops` | Title/body/label generation rules, state mapping, sync triggers |
|
|
47
47
|
|
|
48
|
-
`
|
|
48
|
+
`dev-loops` **does not** become the canonical owner of any business fields.
|
|
49
49
|
It provides projection and sync logic only.
|
|
50
50
|
|
|
51
51
|
## 3. PR Projection Contract
|
|
@@ -234,9 +234,9 @@ This contract is intentionally narrower than the parent epics:
|
|
|
234
234
|
|
|
235
235
|
## 7. Related
|
|
236
236
|
|
|
237
|
-
- Parent workflow-family epic: [mfittko/
|
|
238
|
-
- Umbrella artifact model epic: [mfittko/
|
|
239
|
-
- This contract (first implementable slice): [mfittko/
|
|
237
|
+
- Parent workflow-family epic: [mfittko/dev-loops#17](https://github.com/mfittko/dev-loops/issues/17)
|
|
238
|
+
- Umbrella artifact model epic: [mfittko/dev-loops#19](https://github.com/mfittko/dev-loops/issues/19)
|
|
239
|
+
- This contract (first implementable slice): [mfittko/dev-loops#21](https://github.com/mfittko/dev-loops/issues/21)
|
|
240
240
|
- Copilot loop state graph: [Copilot Loop State Graph](../../docs/copilot-loop-state-graph.md)
|
|
241
241
|
- Reviewer loop state graph: [Reviewer Loop State Graph](../../docs/reviewer-loop-state-graph.md)
|
|
242
242
|
|
|
@@ -68,7 +68,13 @@ For the `local_implementation` strategy, before any planning or implementation m
|
|
|
68
68
|
node scripts/loop/pre-flight-gate.mjs --expected-branch <working-branch> --check-subagents
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
-
Before creating or reusing a worktree for local implementation,
|
|
71
|
+
Before creating or reusing a worktree for local implementation, use the canonical lifecycle entrypoint, which fetches the base remote, then creates-or-reuses the worktree at its namespaced path and provisions it in one step:
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
node scripts/loop/ensure-worktree.mjs --repo-root <main> --issue <n>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
This creates (or reuses) the worktree at `tmp/worktrees/dev-loops/<kind>-<n>` from a freshly fetched `origin/main` (the authoritative base for all worktree creation) and provisions its gitignored runtime files. Raw `git worktree add -b <branch> tmp/worktrees/dev-loops/<kind>-<n> origin/main` is the underlying mechanism `ensure-worktree.mjs` runs for you. See [worktree guidance](../../docs/worktree-guidance.md).
|
|
72
78
|
|
|
73
79
|
This validates:
|
|
74
80
|
- Worktree isolation (current directory is under `tmp/worktrees/`)
|
|
@@ -81,7 +87,7 @@ Note: `--check-subagents` is advisory — it reports availability but does not b
|
|
|
81
87
|
|
|
82
88
|
This gate does **not** apply to other routed strategies (`copilot_pr_followup`, `external_pr_followup`, `reviewer_fixer`, `wait_watch`, `final_approval`, `issue_intake`). Those strategies have their own execution rules and may edit code from any checkout as needed.
|
|
83
89
|
|
|
84
|
-
Development-only bypass (`
|
|
90
|
+
Development-only bypass (`DEVLOOPS_PREFLIGHT_BYPASS=1`) exists for testing the gate itself but must not be used in production workflow runs. The bypass variable is a testing convenience, not an operational escape hatch.
|
|
85
91
|
|
|
86
92
|
## Narrow failure-triage fast path
|
|
87
93
|
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,75 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## Unreleased
|
|
6
|
+
|
|
7
|
+
## 0.5.0 - 2026-06-28
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **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).
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- **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.
|
|
16
|
+
- **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.
|
|
17
|
+
- **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).
|
|
18
|
+
- **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.
|
|
19
|
+
- **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.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- **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.
|
|
23
|
+
- **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.
|
|
24
|
+
- **"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.
|
|
25
|
+
- **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.
|
|
26
|
+
|
|
27
|
+
- **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.
|
|
28
|
+
- **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.
|
|
29
|
+
- **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.
|
|
30
|
+
- **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.
|
|
31
|
+
- **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.
|
|
32
|
+
- **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.
|
|
33
|
+
- **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).
|
|
34
|
+
- **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`.
|
|
35
|
+
- **`--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.
|
|
36
|
+
- **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.
|
|
37
|
+
- **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`.
|
|
38
|
+
- **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.
|
|
39
|
+
- **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`.
|
|
40
|
+
- **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`.
|
|
41
|
+
- **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`.
|
|
42
|
+
- **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.
|
|
43
|
+
- **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.
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- **`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.
|
|
48
|
+
- **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.
|
|
49
|
+
|
|
50
|
+
## 0.4.0
|
|
51
|
+
|
|
52
|
+
### Added
|
|
53
|
+
|
|
54
|
+
- **Opt-in human reviewer/assignee handoff at the pre-approval gate** (#920, pairs with #910). A new `approval.humanHandoff` config (`{ enabled (default false), candidatesFrom: ["codeowners"|"recent-committers"], assignees: [...] }`) plus `scripts/github/resolve-handoff-candidates.mjs` resolve a deduped, priority-ordered candidate list (configured `assignees` → CODEOWNERS last-match-wins for the PR's changed paths → recent committers via `git log`, excluding the PR author/bots; team handles flagged). `dev-loops gate offer-human-handoff --repo <o/n> --pr <n> [--assign <login>] [--request-review <login>]` prints the offer and, only on an explicit `--assign`/`--request-review` flag, runs `gh pr edit --add-assignee/--add-reviewer` — OFFER-only, never auto-assigns. Surfaced at the human-merge handoff so `autonomy.humanMergeOnly` routes the PR to a named human instead of parking silently. Disabled by default; fail-soft per source.
|
|
55
|
+
- **Provider-agnostic CI watcher `dev-loops loop watch-ci`** (#917). A block-waiting watcher (`scripts/github/probe-ci-status.mjs`) that polls a PR's combined check-run + commit-status state for the current head SHA until terminal or timeout — covering GitHub Actions, CircleCI, and any external commit-status/check-run, unlike Actions-only `gh run watch`. It short-circuits to `changed` when the head SHA advances mid-wait so the loop re-baselines. The no-checks path is race-safe: a fresh push where a provider (CircleCI/Actions) hasn't posted its first status yet is NOT settled green on the first poll — the watcher awaits a 2-consecutive-zero-check-poll grace before settling `none`→`success`, treats PR `statusCheckRollup`-expected-but-unreported checks as pending, and never fabricates green from a transient `gh api`/parse failure (an errored fetch forces pending; a persistent error settles `timeout`, never success).
|
|
56
|
+
- **`autonomy.humanMergeOnly` — fixed, non-overridable human-merge rule** (#910). Repos with a hard "a human must perform the merge" rule can now set `autonomy.humanMergeOnly: true` in `.devloops`, making merge an enforced repo invariant rather than a per-run default an explicit instruction can unlock. When set: `resolveAutonomyStopAt` always includes `merge` (even if `stopAt` is `[]`); the new authoritative gate `resolveEffectiveMergeAuthorized(mergeAuthorized, config)` fails closed — it returns `false` regardless of the `mergeAuthorized` envelope flag / explicit "merge" instruction — and the lifecycle resolver (`resolveLifecycleState`) therefore never advances to the terminal merge state, parking instead at the `pre_approval_gate` human-merge handoff. The agent still runs the full mechanical pre-merge evidence check and reports merge-ready, but never runs `gh pr merge` itself. The `queue run` path routes its `--merge-authorized` flag through the same gate. New resolvers `resolveHumanMergeOnly` / `resolveEffectiveMergeAuthorized` in `@dev-loops/core/config`; see [skills/docs/merge-preconditions.md](skills/docs/merge-preconditions.md).
|
|
57
|
+
- **Managed worktree lifecycle** (#909). dev-loops now owns the full worktree lifecycle through one shared canonical-path resolver. (1) **Namespaced naming:** loop-owned worktrees live at `tmp/worktrees/dev-loops/<kind>-<number>` (e.g. `issue-909`, `pr-908`) with no branch suffix, so the path is recomputable from the issue/PR number alone — `resolveWorktreePath({ repoRoot, kind, number })` (in `@dev-loops/core/loop/handoff-envelope`) is the single source of truth for create/provision/cleanup. (1a) **Lifecycle entrypoint:** `scripts/loop/ensure-worktree.mjs --repo-root <p> (--issue <n> | --pr <n>)` is the canonical create+provision command — it fetches the base remote, creates the worktree at the canonical path (or reuses one that already exists there, reporting a conflict instead of clobbering a different branch), then invokes the provisioning core in the same step, printing `{ ok, path, created|reused, provision }`. (2) **Auto-provisioning:** a new `.devloops` `worktree` section (`copyOnInit` / `linkOnInit`, both opt-in arrays of repo-relative literal paths or glob patterns) drives `scripts/loop/provision-worktree.mjs`, which copies (`fs.cp`) or absolute-symlinks the configured gitignored files/dirs from the main checkout into a fresh worktree — directories recurse, sources outside the main checkout are rejected, missing sources / empty globs fail soft, and reuse is idempotent. It does not run `npm install` and is not a `node_modules` mirror. (3) **Namespace-scoped cleanup:** `scripts/loop/cleanup-worktree.mjs` resolves the canonical path and runs `git worktree remove --force` + `git worktree prune` from the main checkout, refusing any path not under `tmp/worktrees/dev-loops/` and failing soft on git errors. See [docs/worktree-guidance.md](docs/worktree-guidance.md).
|
|
58
|
+
- **Consumer migration guide** (#769): [`docs/migrating-to-dev-loops.md`](docs/migrating-to-dev-loops.md) walks existing `pi-dev-loops` consumers through every breaking change — package name (`pi-dev-loops`→`dev-loops`, `@pi-dev-loops/core`→`@dev-loops/core`), repo slug, the full `PI_*`→`DEVLOOPS_*` env-var mapping (a deliberate clean break with no aliases), and the `.devloops` config location. Linked from the README. The env vars are not shimmed by design (`0.x`, YAGNI); the legacy `.pi/dev-loop/settings.yaml` config path still loads with a deprecation warning.
|
|
59
|
+
|
|
60
|
+
### Changed
|
|
61
|
+
|
|
62
|
+
- **Post-merge board archive is now a standard step of the post-merge hook** (#918). `archive-done-items.mjs` (applying `queue.archiveOlderThanDays`, default 7d) is wired into the canonical `merge-preconditions.md` "Post-merge" surface alongside worktree cleanup, and the copilot-pr-followup post-merge step is no longer framed as merely optional. Operator-induced (runs after merge); best-effort — the hook ignores a non-zero exit so a failed archive never blocks merge completion. NOT a cron/scheduled job.
|
|
63
|
+
- **Queue management surfaced under `dev-loops queue`** (#912). The queue board management commands (`add`, `list`, `reorder`, `move`, `sync-status`, `archive-done`, `ensure`) are now discoverable and runnable under `dev-loops queue <sub>` alongside the existing `queue run` — `dev-loops queue --help` lists them all with one-line descriptions. They delegate to the same `scripts/projects/*.mjs` implementations; `dev-loops project <sub>` is retained as a back-compat alias group (lowest-churn: the routing table is data-driven, so `queue` reuses the existing script mappings). Flag consistency: `queue add` now accepts `--column <name>` for the Status column (matching `queue list`), with `--status <name>` kept as a back-compat alias. `move`/`sync-status` keep their distinct `--to-column`. Removes the only reason to hand-write `gh api graphql` for queue work.
|
|
64
|
+
- **BREAKING: Node floor raised `>=20` → `>=24`** (#911). `engines.node` is now `>=24` in both `dev-loops` and `@dev-loops/core` (the latter previously declared no floor). CI already runs Node 24; this makes the supported floor explicit and unlocks Node 24 stdlib (e.g. native `fsp.glob`/`path.matchesGlob`). Consumers on Node < 24 must upgrade.
|
|
65
|
+
- **BREAKING: all `PI_*` environment variables renamed to `DEVLOOPS_*` — no aliases, no fallback** (#905). Completing the env-var neutralization left half-done by the rebrand (#763, surfaced in #769), every dev-loops-owned operational env var is now `DEVLOOPS_*`-only; the previous neutral-first alias pattern (which honored `PI_SUBAGENT_RUN_ID` / `PI_SUBAGENT_AVAILABLE` as fallbacks) is removed. This is a deliberate `0.x` breaking change — consumers must rename their env vars (migration covered by #769). Mapping: `PI_SUBAGENT_RUN_ID`→`DEVLOOPS_RUN_ID`, `PI_SUBAGENT_AVAILABLE`→`DEVLOOPS_SUBAGENT_AVAILABLE`, `PI_PREFLIGHT_BYPASS`→`DEVLOOPS_PREFLIGHT_BYPASS`, `PI_PREPUSH_BYPASS`→`DEVLOOPS_PREPUSH_BYPASS`, `PI_WORKTREE_BYPASS`→`DEVLOOPS_WORKTREE_BYPASS`, `PI_DEV_LOOPS_DEBUG`→`DEVLOOPS_DEBUG`, `PI_DEV_LOOP_STALE_RUNNER_MAX_AGE_MS`→`DEVLOOPS_STALE_RUNNER_MAX_AGE_MS`, `PI_DEV_LOOP_DETACHED`→`DEVLOOPS_DETACHED`. The Pi-runtime-injected vars dev-loops reads only to *integrate* with the Pi harness (`PI_SESSION`, `PI_INTERACTIVE`, `PI_AGENT_SESSIONS_DIR`, `PI_SUBAGENT_SESSIONS_DIR`, `PI_SUBAGENT_ASYNC_RUNS_DIR`, `PI_SUBAGENT_ASYNC_RESULTS_DIR`) are external Pi-platform contract vars and intentionally unchanged. A `cli-harness-agnostic` guard now asserts the code is harness-agnostic: no dev-loops-owned `PI_*` env var survives, and the Pi-runtime-injected vars may only be read at the harness-adapter boundary (`pi-adapter.mjs`, `conductor-monitor.mjs`) — a `PI_*` read anywhere else in code fails.
|
|
66
|
+
|
|
67
|
+
- **Remaining `pi-dev-loops` → `dev-loops` identity references aligned** (#906, closes #768). Residual stale-slug strings in a contract doc (and its generated `.claude` mirror) were corrected, guarded by the `docs-identity-contract` test so user-facing identity surfaces stay consistent.
|
|
68
|
+
|
|
69
|
+
### Fixed
|
|
70
|
+
|
|
71
|
+
- **`queue run` no longer fabricates `done` for undispatched items** (#913, data-integrity). The queue driver is a deterministic adapter over the board, not the orchestration harness — but its missing-orchestrator path fell back to a per-entry `{ ok: true, pr: null }`, which silently marked every `Next Up` item `done` and moved it to **Done** with `pr: null`/`runId: null` in ~1s without any work happening (a single resolve pass would "complete" an entire backlog untouched). The driver now requires a verifiable terminal signal (an orchestrator-supplied result, e.g. a merged PR) before reflecting an item to Done; with no orchestrator wired (`runEntry`) in the current harness, `dev-loops queue run` is a no-op that leaves every board column unchanged and reports `reason: "no-orchestrator"`. The legit reflect path (real merged PR → Done) is preserved.
|
|
72
|
+
- **Queue board `Next Up` membership now resolves from a title-only `.devloops` config** (#904, closes #901). `resolveNextUpOrder` passed the project number as a raw number, which `list-queue-items`' string-only `--project` guard rejected — so a board configured by `queue.boardTitle` alone reported "Board configured but unavailable; nothing to run" and never read `Next Up`. The project ref is now passed as a string.
|
|
73
|
+
|
|
5
74
|
## 0.3.0
|
|
6
75
|
|
|
7
76
|
### Added
|
package/README.md
CHANGED
|
@@ -89,7 +89,7 @@ pi install git:github.com/mfittko/dev-loops # global
|
|
|
89
89
|
pi install -l git:github.com/mfittko/dev-loops # project-local
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
-
The CLI requires Node `>=
|
|
92
|
+
The CLI requires Node `>=24` and a GitHub-authenticated `gh` CLI for repository workflows. See [Requirements](#requirements).
|
|
93
93
|
|
|
94
94
|
## Docker
|
|
95
95
|
|
|
@@ -199,6 +199,12 @@ Key surfaces:
|
|
|
199
199
|
|
|
200
200
|
Full details: [Extension Documentation](./extension/README.md) and `.pi/dev-loop/defaults.yaml`.
|
|
201
201
|
|
|
202
|
+
### Migrating from an earlier release
|
|
203
|
+
|
|
204
|
+
Upgrading an install from before the rename to `dev-loops`? The package name, repo slug,
|
|
205
|
+
and all `PI_*` environment variables changed (the env vars are a clean break — no aliases).
|
|
206
|
+
See the [migration guide](./docs/migrating-to-dev-loops.md) for the full change list.
|
|
207
|
+
|
|
202
208
|
## Package surface
|
|
203
209
|
|
|
204
210
|
The `dev-loops` package ships both a standalone CLI and a Pi extension. Consumer repos should prefer pinned Pi package installs; global npm installs are optional, not part of the Pi runtime contract.
|
|
@@ -247,7 +253,7 @@ See [Extension Documentation](./extension/README.md) for the full command and pa
|
|
|
247
253
|
|
|
248
254
|
## Requirements
|
|
249
255
|
|
|
250
|
-
- Node `>=
|
|
256
|
+
- Node `>=24`
|
|
251
257
|
- `gh` installed and authenticated for GitHub/Copilot workflows
|
|
252
258
|
- `pi-subagents` for async workflow assumptions
|
|
253
259
|
- A Pi host that satisfies peer dependencies on `@earendil-works/pi-coding-agent` and `@earendil-works/pi-tui`
|
|
@@ -268,3 +274,4 @@ CI splits into a small changed-files gate plus parallel `verify` and conditional
|
|
|
268
274
|
- [UI Smoke Harness](./docs/ui-smoke-harness.md) — reusable local Playwright/WebKit smoke baseline
|
|
269
275
|
- [UI Artifact Contract](./docs/ui-artifact-contract.md) — screenshot/state artifact contract and CI-promotion rules
|
|
270
276
|
- [UI Designer Review Loop](./docs/ui-designer-review-loop.md) — designer + vision (`uiReviewMode: vision`) review loop contract
|
|
277
|
+
- [Slides Story Review Loop](./docs/slides-story-review-loop.md) — bounded slides content & storytelling reviewer (narrative, not pixels)
|
package/agents/refiner.agent.md
CHANGED
|
@@ -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
|
@@ -27,12 +27,14 @@ const SUBCOMMAND_ROUTES = {
|
|
|
27
27
|
"probe-copilot": "scripts/github/probe-copilot-review.mjs",
|
|
28
28
|
"capture-threads": "scripts/github/capture-review-threads.mjs",
|
|
29
29
|
"reply-resolve": "scripts/github/reply-resolve-review-threads.mjs",
|
|
30
|
+
"offer-human-handoff": "scripts/github/offer-human-handoff.mjs",
|
|
30
31
|
},
|
|
31
32
|
loop: {
|
|
32
33
|
startup: "scripts/loop/resolve-dev-loop-startup.mjs",
|
|
33
34
|
"build-envelope": "scripts/loop/build-handoff-envelope.mjs",
|
|
34
35
|
outer: "scripts/loop/outer-loop.mjs",
|
|
35
36
|
"watch-cycle": "scripts/loop/run-watch-cycle.mjs",
|
|
37
|
+
"watch-ci": "scripts/github/probe-ci-status.mjs",
|
|
36
38
|
handoff: "scripts/loop/copilot-pr-handoff.mjs",
|
|
37
39
|
"watch-initial": "scripts/loop/watch-initial-copilot-pr.mjs",
|
|
38
40
|
"loop-state": "scripts/loop/detect-copilot-loop-state.mjs",
|
|
@@ -59,6 +61,13 @@ const SUBCOMMAND_ROUTES = {
|
|
|
59
61
|
},
|
|
60
62
|
queue: {
|
|
61
63
|
run: "scripts/loop/run-queue.mjs",
|
|
64
|
+
list: "scripts/projects/list-queue-items.mjs",
|
|
65
|
+
add: "scripts/projects/add-queue-item.mjs",
|
|
66
|
+
move: "scripts/projects/move-queue-item.mjs",
|
|
67
|
+
reorder: "scripts/projects/reorder-queue-item.mjs",
|
|
68
|
+
"archive-done": "scripts/projects/archive-done-items.mjs",
|
|
69
|
+
"sync-status": "scripts/projects/sync-item-status.mjs",
|
|
70
|
+
ensure: "scripts/projects/ensure-queue-board.mjs",
|
|
62
71
|
},
|
|
63
72
|
inspect: {
|
|
64
73
|
run: "scripts/loop/inspect-run.mjs",
|
|
@@ -91,12 +100,13 @@ const HELP_CATEGORY_LABELS = {
|
|
|
91
100
|
gate: "Gate verdicts, evidence, and review operations",
|
|
92
101
|
loop: "Loop lifecycle",
|
|
93
102
|
pr: "PR helpers",
|
|
94
|
-
|
|
103
|
+
queue: "Queue board: run + management (add/list/reorder/move/sync-status/archive)",
|
|
104
|
+
project: "Alias for queue (GitHub Projects queue helpers)",
|
|
95
105
|
inspect: "Inspection (Pi extension only)",
|
|
96
106
|
refine: "Epic tree refinement verification",
|
|
97
107
|
};
|
|
98
108
|
|
|
99
|
-
const TOP_LEVEL_HELP_CATEGORY_ORDER = ["gate", "loop", "pr", "project", "inspect", "refine"];
|
|
109
|
+
const TOP_LEVEL_HELP_CATEGORY_ORDER = ["gate", "loop", "pr", "queue", "project", "inspect", "refine"];
|
|
100
110
|
|
|
101
111
|
const SUBCOMMAND_DESCRIPTIONS = {
|
|
102
112
|
gate: {
|
|
@@ -108,12 +118,14 @@ const SUBCOMMAND_DESCRIPTIONS = {
|
|
|
108
118
|
"probe-copilot": "Poll for Copilot review activity",
|
|
109
119
|
"capture-threads": "Capture review threads",
|
|
110
120
|
"reply-resolve": "Reply and resolve review threads",
|
|
121
|
+
"offer-human-handoff": "Offer to assign PR to a human reviewer/assignee",
|
|
111
122
|
},
|
|
112
123
|
loop: {
|
|
113
124
|
startup: "Resolve dev-loop startup bundle",
|
|
114
125
|
"build-envelope": "Build handoff envelope from startup output",
|
|
115
126
|
outer: "Run outer-loop detection",
|
|
116
127
|
"watch-cycle": "Run Copilot wait cycle",
|
|
128
|
+
"watch-ci": "Block-wait on provider-agnostic CI (CircleCI/Actions/external)",
|
|
117
129
|
handoff: "Copilot PR handoff",
|
|
118
130
|
"watch-initial": "Watch initial Copilot PR",
|
|
119
131
|
"loop-state": "Detect Copilot loop state",
|
|
@@ -140,6 +152,13 @@ const SUBCOMMAND_DESCRIPTIONS = {
|
|
|
140
152
|
},
|
|
141
153
|
queue: {
|
|
142
154
|
run: "Run queue driver",
|
|
155
|
+
list: "List queue board items",
|
|
156
|
+
add: "Add issue/PR to queue board",
|
|
157
|
+
move: "Move queue item between Status columns",
|
|
158
|
+
reorder: "Reorder items (move-to-top/move-after/order, --dry-run)",
|
|
159
|
+
"archive-done": "Archive closed Done items older than a duration",
|
|
160
|
+
"sync-status": "Sync a queued issue/PR's board Status column (best-effort)",
|
|
161
|
+
ensure: "Create/repair queue board bootstrap surface",
|
|
143
162
|
},
|
|
144
163
|
inspect: {
|
|
145
164
|
run: "Inspect run state",
|
package/extension/README.md
CHANGED
|
@@ -210,7 +210,7 @@ Config is validated at runtime by Zod schemas (`packages/core/src/config/config.
|
|
|
210
210
|
## Runtime / build / test contract
|
|
211
211
|
|
|
212
212
|
Current Phase 3+ contract:
|
|
213
|
-
- Node runtime floor: `>=
|
|
213
|
+
- Node runtime floor: `>=24` (from `package.json`)
|
|
214
214
|
- Pi host expectations are documented from current peer dependencies rather than a tested pinned Pi version range
|
|
215
215
|
- the extension is source-loaded from `./extension/index.ts` through `package.json` `pi.extensions`
|
|
216
216
|
- the package exposes `skills` through `package.json` `pi.skills` for install-based global skill loading
|
package/package.json
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"pi-package"
|
|
16
16
|
],
|
|
17
17
|
"engines": {
|
|
18
|
-
"node": ">=
|
|
18
|
+
"node": ">=24"
|
|
19
19
|
},
|
|
20
20
|
"workspaces": [
|
|
21
21
|
"packages/*"
|
|
@@ -25,10 +25,12 @@
|
|
|
25
25
|
"test": "npm run test:assets && npm run test:extension && npm run test:scripts && npm run test:core && npm run test:docs",
|
|
26
26
|
"test:assets": "node --test --test-reporter ./test/failure-summary-reporter.mjs test/dev-loop-init-phase-smoke.test.mjs test/contracts/*.test.mjs test/workflow-handoff-contract.test.mjs",
|
|
27
27
|
"test:extension": "node --import tsx --test --test-reporter ./test/failure-summary-reporter.mjs test/extension-checks.test.mjs test/extension-post-merge-update.test.mjs test/extension-command-contract.test.mjs test/extension-package-contract.test.mjs test/extension-pi-adapter.test.mjs test/extension-claude-adapter.test.mjs test/dev-loops-core.test.mjs test/dev-loops-cli.test.mjs",
|
|
28
|
-
"test:scripts": "node --test --test-reporter ./test/failure-summary-reporter.mjs test/github/*.test.mjs test/loop/*.test.mjs test/docs/*.test.mjs test/projects/*.test.mjs",
|
|
28
|
+
"test:scripts": "node --test --test-reporter ./test/failure-summary-reporter.mjs test/github/*.test.mjs test/loop/*.test.mjs test/docs/*.test.mjs test/projects/*.test.mjs test/pages/*.test.mjs",
|
|
29
29
|
"test:core": "node --test --test-reporter ./test/failure-summary-reporter.mjs packages/core/test/*.test.mjs",
|
|
30
30
|
"test:dev-loop": "node --test --test-reporter ./test/failure-summary-reporter.mjs skills/dev-loop/scripts/dev-mode-context.test.mjs skills/dev-loop/scripts/render-template.test.mjs skills/dev-loop/scripts/post-gate-verdict-fallback.test.mjs",
|
|
31
31
|
"test:playwright:viewer": "playwright test -c playwright.inspect-run-viewer.config.mjs",
|
|
32
|
+
"test:playwright:deep-dive": "playwright test -c playwright.dev-loops-deep-dive.config.mjs",
|
|
33
|
+
"test:playwright:intro-deck": "playwright test -c playwright.intro-deck.config.mjs",
|
|
32
34
|
"smoke:headless": "node scripts/claude/headless-info-smoke.mjs",
|
|
33
35
|
"test:docs": "node scripts/docs/validate-links.mjs && node scripts/docs/validate-no-duplicate-rules.mjs",
|
|
34
36
|
"repo-wiki": "node scripts/repo-wiki.mjs",
|
|
@@ -68,7 +70,7 @@
|
|
|
68
70
|
},
|
|
69
71
|
"dependencies": {
|
|
70
72
|
"mermaid": "11.15.0",
|
|
71
|
-
"@dev-loops/core": "^0.
|
|
73
|
+
"@dev-loops/core": "^0.5.0"
|
|
72
74
|
},
|
|
73
75
|
"repository": {
|
|
74
76
|
"type": "git",
|
|
@@ -78,7 +80,7 @@
|
|
|
78
80
|
"url": "https://github.com/mfittko/dev-loops/issues"
|
|
79
81
|
},
|
|
80
82
|
"homepage": "https://github.com/mfittko/dev-loops#readme",
|
|
81
|
-
"version": "0.
|
|
83
|
+
"version": "0.5.0",
|
|
82
84
|
"files": [
|
|
83
85
|
"cli/",
|
|
84
86
|
"lib/",
|
package/scripts/README.md
CHANGED
|
@@ -499,7 +499,7 @@ Contract:
|
|
|
499
499
|
- enters watch only when request state is confirmed (`requested` or `already-requested`) and emits exact `watchArgs` + `watchTimeoutPolicy`
|
|
500
500
|
- watch refresh (`--watch-status`) is observational-only; rely on refreshed `loopDisposition` + `terminal` to decide whether to continue or stop
|
|
501
501
|
- explicit stop/blocked routing is machine-readable via `action: "stop"` plus `requestWatchContract.stopState`
|
|
502
|
-
- when `
|
|
502
|
+
- when `DEVLOOPS_RUN_ID` is set to a non-empty value, the helper enforces one-runner-per-PR ownership and returns `runnerOwnership` details when a conflicting or displaced run must stop
|
|
503
503
|
|
|
504
504
|
Success output shape:
|
|
505
505
|
- `{ "ok": true, "action": "watch"|"fix"|"stop", "state": "...", "allowedTransitions": [...], "nextAction": "...", "snapshot": {...}, "reviewRequestStatus"?: "...", "watchStatus"?: "...", "autoRerequestEligible": true|false, "sameHeadCleanConverged": true|false, "loopDisposition": "...", "terminal": true|false, "requestWatchContract": { "action": "...", "nextAction": "...", "requestStatus": "requested"|"already-requested"|"unavailable"|"failed"|"none", "routingState": "copilot_request_confirmed_waiting"|"ready_state_needs_copilot_request"|"draft_reset_requires_ready_state_reentry"|"non_ready_state", "watchEntryConfirmed": true|false, "watchArgs": { ... }|null, "stopState"?: "unavailable"|"blocked"|"draft_requires_ready_state_reentry"|"no_automatic_next_step" }, "watchTimeoutPolicy"?: { "classification": "external_healthy_wait", "minimumTimeoutMs": 1800000, "defaultTimeoutMs": 1800000 }, "watchArgs"?: { ... } }`
|
|
@@ -722,7 +722,7 @@ Fetches the live PR head SHA plus visible PR issue comments, then summarizes the
|
|
|
722
722
|
latest valid `draft_gate` and `pre_approval_gate` checkpoint verdict comments.
|
|
723
723
|
Use this when a fresh session needs authoritative visible gate evidence for the
|
|
724
724
|
current head before running `gh pr ready` or declaring final-approval readiness.
|
|
725
|
-
When `
|
|
725
|
+
When `DEVLOOPS_RUN_ID` is set to a non-empty value, it also enforces async runner ownership before reading GitHub facts, so stale/displaced runs fail closed before merge.
|
|
726
726
|
|
|
727
727
|
Required:
|
|
728
728
|
- `--repo <owner/name>`
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import { parseArgs } from "node:util";
|
|
12
12
|
import { parsePrNumber, requireTokenValue, runChild } from "../_cli-primitives.mjs";
|
|
13
13
|
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
14
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult } from "../lib/jq-output.mjs";
|
|
14
15
|
export const REVIEW_THREADS_QUERY = [
|
|
15
16
|
"query($owner: String!, $name: String!, $pr: Int!) {",
|
|
16
17
|
" repository(owner: $owner, name: $name) {",
|
|
@@ -45,9 +46,11 @@ Modes:
|
|
|
45
46
|
Options:
|
|
46
47
|
--output <path> Write JSON output to file in addition to stdout
|
|
47
48
|
--help, -h Show this help
|
|
49
|
+
${JQ_OUTPUT_USAGE}
|
|
48
50
|
Exit codes:
|
|
49
51
|
0 Success
|
|
50
52
|
1 Error
|
|
53
|
+
2 Invalid --jq filter
|
|
51
54
|
`;
|
|
52
55
|
export function parseCaptureCliArgs(argv) {
|
|
53
56
|
const { tokens } = parseArgs({
|
|
@@ -58,6 +61,7 @@ export function parseCaptureCliArgs(argv) {
|
|
|
58
61
|
output: { type: "string" },
|
|
59
62
|
repo: { type: "string" },
|
|
60
63
|
pr: { type: "string" },
|
|
64
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
61
65
|
},
|
|
62
66
|
allowPositionals: true,
|
|
63
67
|
strict: false,
|
|
@@ -69,6 +73,8 @@ export function parseCaptureCliArgs(argv) {
|
|
|
69
73
|
repo: undefined,
|
|
70
74
|
pr: undefined,
|
|
71
75
|
help: false,
|
|
76
|
+
jq: undefined,
|
|
77
|
+
silent: false,
|
|
72
78
|
};
|
|
73
79
|
for (const token of tokens) {
|
|
74
80
|
if (token.kind === "positional") {
|
|
@@ -97,6 +103,14 @@ export function parseCaptureCliArgs(argv) {
|
|
|
97
103
|
options.pr = parsePrNumber(requireTokenValue(token));
|
|
98
104
|
continue;
|
|
99
105
|
}
|
|
106
|
+
if (token.name === "jq") {
|
|
107
|
+
options.jq = requireTokenValue(token);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (token.name === "silent") {
|
|
111
|
+
options.silent = true;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
100
114
|
throw new Error(`Unknown argument: ${token.rawName}`);
|
|
101
115
|
}
|
|
102
116
|
const hasLiveArgs = options.repo !== undefined || options.pr !== undefined;
|
|
@@ -188,10 +202,14 @@ export async function runCli(
|
|
|
188
202
|
if (options.outputPath) {
|
|
189
203
|
await writeOutputFile(options.outputPath, payload);
|
|
190
204
|
}
|
|
191
|
-
|
|
205
|
+
return emitResult(payload, { jq: options.jq, silent: options.silent, stdout });
|
|
192
206
|
}
|
|
193
207
|
if (isDirectCliRun(import.meta.url)) {
|
|
194
|
-
runCli().
|
|
208
|
+
runCli().then((code) => {
|
|
209
|
+
if (typeof code === "number") {
|
|
210
|
+
process.exitCode = code;
|
|
211
|
+
}
|
|
212
|
+
}).catch((error) => {
|
|
195
213
|
process.stderr.write(`${formatCliError(error)}\n`);
|
|
196
214
|
process.exitCode = 1;
|
|
197
215
|
});
|