dev-loops 0.3.0 → 0.4.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 (53) hide show
  1. package/.claude/.claude-plugin/plugin.json +1 -1
  2. package/.claude/agents/dev-loop.md +1 -1
  3. package/.claude/hooks/_run-context.mjs +9 -16
  4. package/.claude/skills/copilot-pr-followup/SKILL.md +9 -7
  5. package/.claude/skills/dev-loop/SKILL.md +6 -6
  6. package/.claude/skills/docs/anti-patterns.md +1 -1
  7. package/.claude/skills/docs/copilot-loop-operations.md +1 -1
  8. package/.claude/skills/docs/issue-intake-procedure.md +4 -0
  9. package/.claude/skills/docs/merge-preconditions.md +65 -1
  10. package/.claude/skills/docs/stop-conditions.md +1 -0
  11. package/.claude/skills/docs/tracker-first-loop-state.md +6 -6
  12. package/.claude/skills/local-implementation/SKILL.md +8 -2
  13. package/CHANGELOG.md +26 -0
  14. package/README.md +8 -2
  15. package/cli/index.mjs +21 -2
  16. package/extension/README.md +1 -1
  17. package/package.json +3 -3
  18. package/scripts/README.md +2 -2
  19. package/scripts/github/offer-human-handoff.mjs +147 -0
  20. package/scripts/github/probe-ci-status.mjs +468 -0
  21. package/scripts/github/request-copilot-review.mjs +3 -3
  22. package/scripts/github/resolve-handoff-candidates.mjs +412 -0
  23. package/scripts/github/upsert-checkpoint-verdict.mjs +2 -2
  24. package/scripts/loop/_stale-runner-detection.mjs +1 -1
  25. package/scripts/loop/_worktree-path.mjs +27 -0
  26. package/scripts/loop/cleanup-worktree.mjs +175 -0
  27. package/scripts/loop/copilot-pr-handoff.mjs +1 -1
  28. package/scripts/loop/detect-stale-runner.mjs +3 -4
  29. package/scripts/loop/ensure-worktree.mjs +219 -0
  30. package/scripts/loop/outer-loop.mjs +1 -1
  31. package/scripts/loop/pr-runner-coordination.mjs +1 -1
  32. package/scripts/loop/pre-flight-gate.mjs +10 -7
  33. package/scripts/loop/pre-push-main-guard.mjs +4 -4
  34. package/scripts/loop/provision-worktree.mjs +243 -0
  35. package/scripts/loop/resolve-dev-loop-startup.mjs +5 -5
  36. package/scripts/loop/run-queue.mjs +25 -1
  37. package/scripts/loop/run-watch-cycle.mjs +75 -22
  38. package/scripts/projects/add-queue-item.mjs +20 -5
  39. package/scripts/projects/archive-done-items.mjs +2 -1
  40. package/scripts/projects/ensure-queue-board.mjs +2 -1
  41. package/scripts/projects/list-queue-items.mjs +2 -1
  42. package/scripts/projects/move-queue-item.mjs +2 -1
  43. package/scripts/projects/reorder-queue-item.mjs +5 -4
  44. package/scripts/projects/sync-item-status.mjs +2 -1
  45. package/skills/copilot-pr-followup/SKILL.md +9 -7
  46. package/skills/dev-loop/SKILL.md +2 -2
  47. package/skills/docs/anti-patterns.md +1 -1
  48. package/skills/docs/copilot-loop-operations.md +1 -1
  49. package/skills/docs/issue-intake-procedure.md +4 -0
  50. package/skills/docs/merge-preconditions.md +65 -1
  51. package/skills/docs/stop-conditions.md +1 -0
  52. package/skills/docs/tracker-first-loop-state.md +6 -6
  53. package/skills/local-implementation/SKILL.md +8 -2
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dev-loops",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Agent-harness-agnostic dev-loop: agents, skills, and hooks for GitHub/Copilot-driven development loops.",
5
5
  "author": {
6
6
  "name": "Manuel Fittko",
@@ -22,7 +22,7 @@ The envelope is the primary handoff artifact — it is derived from resolver out
22
22
 
23
23
  **Construction sequence:**
24
24
 
25
- 1. Run the deterministic startup resolver to produce the authoritative state bundle: `npx dev-loops@0.3.0 loop startup --issue <n>` for issues, or `npx dev-loops@0.3.0 loop startup --pr <n>` for PRs.
25
+ 1. Run the deterministic startup resolver to produce the authoritative state bundle: `npx dev-loops@0.4.0 loop startup --issue <n>` for issues, or `npx dev-loops@0.4.0 loop startup --pr <n>` for PRs.
26
26
  2. Pass the resolver output, resolved settings (merged from `.devloops` and `.pi/dev-loop/defaults.yaml`), and current gate state to `buildDevLoopHandoffEnvelope()`.
27
27
  3. **Validate the envelope** with `validateHandoffEnvelope()` before consuming any field. If validation returns `ok: false`, reject the handoff with the structured error — do not load requiredReads, do not execute nextAction, do not delegate.
28
28
  4. Read the envelope as the first artifact.
@@ -2,15 +2,11 @@
2
2
  /**
3
3
  * Neutral run-id / async-context contract.
4
4
  *
5
- * The dev-loop async path historically keyed off Pi's `PI_SUBAGENT_RUN_ID` env var to
5
+ * The dev-loop async path keys off the harness-neutral `DEVLOOPS_RUN_ID` env var to
6
6
  * identify an inspectable per-subagent run (runner ownership, async-start enforcement,
7
- * human-comment gating). This module generalizes that into a harness-neutral
8
- * `DEVLOOPS_RUN_ID`, keeping `PI_SUBAGENT_RUN_ID` as a backward-compatible alias, and
9
- * provides a mint-and-propagate path for harnesses (e.g. Claude Code) that inject no
10
- * native per-subagent run id.
11
- *
12
- * Marker precedence is neutral-first: a present `DEVLOOPS_RUN_ID` wins; otherwise the Pi
13
- * alias is honored. Existing Pi runs that set only `PI_SUBAGENT_RUN_ID` behave identically.
7
+ * human-comment gating), and provides a mint-and-propagate path for harnesses (e.g. Claude
8
+ * Code) that inject no native per-subagent run id. The harness sets `DEVLOOPS_RUN_ID` when
9
+ * dispatching an async subagent.
14
10
  *
15
11
  * This module is pure except for the explicit file/IO helpers (writeRunContext/readRunContext),
16
12
  * which take an injectable `fs` and `root` for testability.
@@ -22,16 +18,13 @@ import path from "node:path";
22
18
 
23
19
  /**
24
20
  * Env var names that carry the async-context run id, in resolution precedence order.
25
- * Neutral `DEVLOOPS_RUN_ID` first; Pi `PI_SUBAGENT_RUN_ID` retained as a compatibility alias.
21
+ * The neutral `DEVLOOPS_RUN_ID` is the sole marker.
26
22
  */
27
- export const RUN_ID_MARKERS = Object.freeze(["DEVLOOPS_RUN_ID", "PI_SUBAGENT_RUN_ID"]);
23
+ export const RUN_ID_MARKERS = Object.freeze(["DEVLOOPS_RUN_ID"]);
28
24
 
29
25
  /** Neutral env var name used when minting/propagating a run id. */
30
26
  export const NEUTRAL_RUN_ID_VAR = "DEVLOOPS_RUN_ID";
31
27
 
32
- /** Pi-compatibility alias env var name. */
33
- export const PI_RUN_ID_ALIAS_VAR = "PI_SUBAGENT_RUN_ID";
34
-
35
28
  /** State-file name (under `.pi/`, consistent with existing dev-loop checkpoint files). */
36
29
  export const RUN_CONTEXT_FILENAME = "dev-loop-run-context.json";
37
30
 
@@ -57,7 +50,7 @@ export function isClaudeHarness(env = process.env) {
57
50
  }
58
51
 
59
52
  /**
60
- * Resolve the active run id from the environment, neutral marker first.
53
+ * Resolve the active run id from the environment.
61
54
  *
62
55
  * @param {Record<string, string|undefined>} [env]
63
56
  * @returns {string|null} The trimmed run id, or null when none is set.
@@ -155,8 +148,8 @@ export function readRunContext({ root, fs = fsDefault }) {
155
148
  * Resolve the active run id, or mint one and persist a run-context state file.
156
149
  *
157
150
  * This is the "mint at startup and propagate" primitive a Claude dev-loop agent (or a
158
- * headless entry) calls before dispatching child work. When the env already carries a run
159
- * id (Pi alias or neutral), it is reused and no new id is minted.
151
+ * headless entry) calls before dispatching child work. When the env already carries a
152
+ * `DEVLOOPS_RUN_ID`, it is reused and no new id is minted.
160
153
  *
161
154
  * @param {object} [params]
162
155
  * @param {Record<string, string|undefined>} [params.env]
@@ -156,9 +156,9 @@ The outer-loop checkpoint is the canonical re-attachment artifact.
156
156
 
157
157
  Start every wait seam with a detector refresh: `detect-copilot-loop-state.mjs --repo <owner/name> --pr <number>`.
158
158
 
159
- Allowed wait tools: `detect-copilot-loop-state.mjs` (one-shot), `dev-loops loop watch-cycle` (persistent), `copilot-pr-handoff.mjs --watch-status` (refresh after timeout/idle), `gh run watch <run-id> --repo <owner/name>` (CI with known run id). Otherwise exit and resume later from fresh detector call.
159
+ Allowed wait tools: `detect-copilot-loop-state.mjs` (one-shot), `dev-loops loop watch-cycle` (persistent), `copilot-pr-handoff.mjs --watch-status` (refresh after timeout/idle), `dev-loops loop watch-ci --repo <owner/name> --pr <number>` (provider-agnostic CI: CircleCI / Actions / external commit-status), `gh run watch <run-id> --repo <owner/name>` (Actions-only fallback when the run id is known). Otherwise exit and resume later from fresh detector call.
160
160
 
161
- Practical rules: do not poll manually. `waiting_for_copilot_review` → `run-watch-cycle.mjs` or report-and-resume. `waiting_for_ci` with pending/none CI → `gh run watch <run-id> --repo <owner/name>` (known run id) or report-and-resume. Bounded CI exception: zero current-head suites + previous-head green + local `npm run verify` passed → rerun detector with `--local-validation-head-sha` for `crediblyGreen` promotion. `ciStatus=failure` → stop/fix, never wait.
161
+ Practical rules: do not poll manually. `waiting_for_copilot_review` → `run-watch-cycle.mjs` or report-and-resume. `waiting_for_ci` with pending/none CI → `dev-loops loop watch-ci --repo <owner/name> --pr <number>` (provider-agnostic; covers CircleCI / Actions / external commit-status) or report-and-resume; `gh run watch <run-id>` is an Actions-only fallback. `dev-loops loop watch-cycle` also auto-routes a `waiting_for_ci` boundary to this CI watcher. Bounded CI exception: zero current-head suites + previous-head green + local `npm run verify` passed → rerun detector with `--local-validation-head-sha` for `crediblyGreen` promotion. `ciStatus=failure` → stop/fix, never wait.
162
162
 
163
163
  Preferred approach:
164
164
  - route decisions through `copilot-pr-handoff.mjs` output; enter watcher only on `action: "watch"` with `watchEntryConfirmed=true`; prefer `dev-loops loop watch-cycle` for deterministic handoff → watch
@@ -238,7 +238,7 @@ When unresolved feedback exists, use a narrow follow-up loop:
238
238
  - if that local validation is still known red, continue remediation instead of re-requesting Copilot
239
239
  - after a fix push advances the PR head SHA, re-run `detect-copilot-loop-state.mjs` for the new head and apply the [Copilot CI Status Contract](../docs/copilot-ci-status-contract.md). Previous-head CI is stale; only current-head results unblock CI-dependent steps. if GitHub CI/checks for the updated head are known red for a fixable issue, continue remediation instead of re-requesting Copilot. only once the updated head is green or credibly green, explicitly re-request Copilot review for the new head. Always use `request-copilot-review.mjs` — never `gh api POST repos/.../requested_reviewers` directly.
240
240
  - only enter a wait/watch loop if the request result is confirmed as `requested` or `already-requested`
241
- - for `requested` / `already-requested`, immediately re-baseline with `detect-copilot-loop-state.mjs`; if the returned state is `waiting_for_copilot_review`, use `dev-loops loop watch-cycle` or stop/resume later, and if the returned state is `waiting_for_ci`, use `gh run watch` for a known run id or stop/resume later after that single detector refresh
241
+ - for `requested` / `already-requested`, immediately re-baseline with `detect-copilot-loop-state.mjs`; if the returned state is `waiting_for_copilot_review`, use `dev-loops loop watch-cycle` or stop/resume later, and if the returned state is `waiting_for_ci`, use `dev-loops loop watch-ci` (provider-agnostic CI wait; `gh run watch` is an Actions-only fallback) or stop/resume later after that single detector refresh
242
242
  - if the request result is `unavailable`, report that limitation and stop unless the user explicitly wants passive waiting anyway
243
243
  - if the request command fails unexpectedly, stop and report the error rather than sleeping and hoping for a new review
244
244
  13. after a confirmed re-requested Copilot pass, refresh PR thread state again before reporting completion; if fresh Copilot threads exist, return to this follow-up loop rather than stopping at `review requested`
@@ -309,7 +309,7 @@ The canonical checkpoint verdict comment contract is [Gate Review Comment Contra
309
309
  - **CI prerequisite:** resolve the draft gate config first (`resolveGateConfig(config, "draft")`). When `requireCi=true` (default), wait for green current-head CI before entering `draft_gate`. When `requireCi=false`, the draft gate may proceed without green CI. This draft-only override does **not** relax `pre_approval_gate`; final approval and merge readiness still require green current-head CI.
310
310
  - **Pass criteria:** all configured draft gate angles pass; all findings at severities in `blockCleanOnFindingSeverities` are addressed; validation passes; no unrelated files are included.
311
311
  - **Next step after passing:** mark the PR ready for review.
312
- - **Board status sync (best-effort, after ready-for-review):** once the PR is created/marked ready for review, sync the linked issue's board item to "In Progress" so the queue board reflects active work without a manual `add-queue-item --status`:
312
+ - **Board status sync (best-effort, after ready-for-review):** once the PR is created/marked ready for review, sync the linked issue's board item to "In Progress" so the queue board reflects active work without a manual `dev-loops queue sync-status --to-column <col>`:
313
313
  ```sh
314
314
  node <resolved-skill-scripts>/projects/sync-item-status.mjs --repo <owner/name> --item <linked-issue> --to-column "In Progress"
315
315
  ```
@@ -357,6 +357,8 @@ See [Merge Preconditions](../docs/merge-preconditions.md). Verify: zero unresolv
357
357
  After merge-ready preconditions pass, verify [Merge Preconditions](../docs/merge-preconditions.md) authoritatively before reporting merge-ready. Stop at the human approval checkpoint by default. Cross-check via `dev-loops gate capture-threads` (not prose assertion).
358
358
  Follow [Merge Preconditions](../docs/merge-preconditions.md): stop at `waiting_for_merge_authorization` after approval unless merge explicitly authorized. Run pre-merge gate evidence check before any `gh pr merge`.
359
359
 
360
+ When `approval.humanHandoff.enabled` is set, don't just park silently at this stop: run `dev-loops gate offer-human-handoff --repo <owner/name> --pr <number>` to surface candidate reviewers/assignees, then **offer** them to the operator. Only on operator confirmation, route the PR with `--assign <login>` / `--request-review <login>`. This is OFFER-only — never auto-assign. See the `approval.humanHandoff` section in [Merge Preconditions](../docs/merge-preconditions.md); it pairs with `autonomy.humanMergeOnly`.
361
+
360
362
  ### Mechanical pre-merge gate evidence check
361
363
 
362
364
  Immediately before any `gh pr merge`, run:
@@ -387,15 +389,15 @@ Do not report completion or advance to the next PR queue item until `.pi/dev-loo
387
389
 
388
390
  ### Post-merge board archive (best-effort)
389
391
 
390
- After the retrospective checkpoint write, optionally tidy the queue board locally:
392
+ After the retrospective checkpoint write, run the post-merge board archive as a standard step of the post-merge hook (see [Merge Preconditions](../docs/merge-preconditions.md) "Post-merge"):
391
393
 
392
394
  ```sh
393
- node <resolved-skill-scripts>/projects/archive-done-items.mjs --repo <owner/name>
395
+ node <resolved-skill-scripts>/projects/archive-done-items.mjs --repo <owner/name> || true
394
396
  ```
395
397
 
396
398
  Board and threshold resolve from `.devloops` (`queue.projectNumber`/`queue.boardTitle`, `queue.archiveOlderThanDays`, default 7d), using local `gh` auth — no CI, cron, or PAT. This step is best-effort and NON-FATAL: ignore any failure and never let it block the merge or the retrospective.
397
399
 
398
- Alongside the archive step, sync the linked issue's board item to "Done" so the queue board reflects the merge without a manual `add-queue-item --status`:
400
+ Alongside the archive step, sync the linked issue's board item to "Done" so the queue board reflects the merge without a manual `dev-loops queue sync-status --to-column <col>`:
399
401
 
400
402
  ```sh
401
403
  node <resolved-skill-scripts>/projects/sync-item-status.mjs --repo <owner/name> --item <linked-issue> --to-column "Done"
@@ -25,7 +25,7 @@ Required installed runtime contract docs are shared bundled copies under `../doc
25
25
 
26
26
  > Under the Claude Code harness the dev-loop runs as a single agent: run these steps directly — no read-only boundary and no separate async-subagent dispatch. See [Main Agent Contract](../docs/main-agent-contract.md).
27
27
 
28
- Resolve authoritative state via the startup resolver (`npx dev-loops@0.3.0 loop startup --issue <n>` for issues, `npx dev-loops@0.3.0 loop startup --pr <n>` for PRs), then immediately build the handoff envelope via `npx dev-loops@0.3.0 loop build-envelope --input <resolver-output.json>`. The envelope determines `requiredReads`, `nextAction`, `stopRules`, and `acceptance` — load only those files, execute only that bounded task. It is the first handoff artifact consumed before loading any route pack. See [Workflow Handoff Contract](../docs/workflow-handoff-contract.md) for the derivation contract.
28
+ Resolve authoritative state via the startup resolver (`npx dev-loops@0.4.0 loop startup --issue <n>` for issues, `npx dev-loops@0.4.0 loop startup --pr <n>` for PRs), then immediately build the handoff envelope via `npx dev-loops@0.4.0 loop build-envelope --input <resolver-output.json>`. The envelope determines `requiredReads`, `nextAction`, `stopRules`, and `acceptance` — load only those files, execute only that bounded task. It is the first handoff artifact consumed before loading any route pack. See [Workflow Handoff Contract](../docs/workflow-handoff-contract.md) for the derivation contract.
29
29
 
30
30
  **Retrospective checkpoint gate:** the resolver reads `.pi/dev-loop-retrospective-checkpoint.json` and injects the state. When the checkpoint is `missing` and the repo config `workflow.requireRetrospective` (set via `.devloops` at repo root) is `true`, the resolver returns `needs_reconcile`. Complete or explicitly skip the retrospective before starting.
31
31
 
@@ -81,7 +81,7 @@ Do not preload route packs before the resolver selects the strategy.
81
81
 
82
82
  ## Async dispatch
83
83
 
84
- **Async dispatch rule (enforced):** the resolver fails closed for GitHub-first strategies when `canonicalStateSummary.requiresAsyncDispatch` is `true` (default `required` mode) — inline invocation without an async run-id marker (`DEVLOOPS_RUN_ID`, or the `PI_SUBAGENT_RUN_ID` alias) is rejected for those routes. Under the Claude Code harness this requirement is relaxed automatically. See [Startup procedure](#startup-procedure).
84
+ **Async dispatch rule (enforced):** the resolver fails closed for GitHub-first strategies when `canonicalStateSummary.requiresAsyncDispatch` is `true` (default `required` mode) — inline invocation without an async run-id marker (`DEVLOOPS_RUN_ID`) is rejected for those routes. Under the Claude Code harness this requirement is relaxed automatically. See [Startup procedure](#startup-procedure).
85
85
 
86
86
  ## Fallback gate-comment poster
87
87
 
@@ -99,10 +99,10 @@ When `@dev-loops/core` is available again, switch back to the full helper. The f
99
99
 
100
100
  ## Read-only info shortcut
101
101
 
102
- Info/handoff requests can be served directly via `npx dev-loops@0.3.0 loop info` (read-only; no full dev-loop run required):
103
- - `npx dev-loops@0.3.0 loop info --issue <n>` — human-readable issue state summary (strategy, route, linked PR, next action)
104
- - `npx dev-loops@0.3.0 loop info --pr <n>` — human-readable PR state summary (branch, CI, threads, rounds, action)
105
- - `npx dev-loops@0.3.0 loop info --issue <n> --json` — machine-readable JSON output
102
+ Info/handoff requests can be served directly via `npx dev-loops@0.4.0 loop info` (read-only; no full dev-loop run required):
103
+ - `npx dev-loops@0.4.0 loop info --issue <n>` — human-readable issue state summary (strategy, route, linked PR, next action)
104
+ - `npx dev-loops@0.4.0 loop info --pr <n>` — human-readable PR state summary (branch, CI, threads, rounds, action)
105
+ - `npx dev-loops@0.4.0 loop info --issue <n> --json` — machine-readable JSON output
106
106
 
107
107
  ## Guard rules
108
108
 
@@ -11,7 +11,7 @@ Canonical owner for anti-pattern guidance across all workflow families.
11
11
  5. **Duplicate worktree paths**: Check `git worktree list` before creating new worktrees; reuse existing matching worktrees.
12
12
  6. **Main-checkout mutation**: Reserve main checkout for inspection/control; use `tmp/worktrees/` paths for mutation work.
13
13
  7. **Spelunking tooling internals instead of using the public surface**: Do not read installed package internals, scan tooling source, or run ad-hoc scripts to understand a tool's behavior. Use the CLI, its `--help` subcommands, and `skills/docs/`. Read tool source only when the task is to inspect or change that tool, or when a concrete failure path is inside it and no public CLI/docs path exists. Once a failure is concrete, search changed files for the exact pattern — don't run duplicate broad searches.
14
- 8. **Hand-editing the queue file when a board is configured**: When a GitHub Projects board is configured (`queue.projectNumber`/`queue.boardTitle` in `.devloops`), the board is the authoritative queue MEMBERSHIP and ordering source — not just status. Add work via the board (`dev-loops project add ... --status "Next Up"`), not by hand-editing `.pi/dev-loop-queue.json`; the queue runner reconciles board `Next Up` items into queue entries before each run. See the operator guides under `docs/` (queue board usage/setup) for the workflow.
14
+ 8. **Hand-editing the queue file when a board is configured**: When a GitHub Projects board is configured (`queue.projectNumber`/`queue.boardTitle` in `.devloops`), the board is the authoritative queue MEMBERSHIP and ordering source — not just status. Add work via the board (`dev-loops queue add ... --column "Next Up"`), not by hand-editing `.pi/dev-loop-queue.json`; the queue runner reconciles board `Next Up` items into queue entries before each run. See the operator guides under `docs/` (queue board usage/setup) for the workflow.
15
15
 
16
16
  ## Light mode exception
17
17
 
@@ -51,7 +51,7 @@ For tracker-first MVP `story -> PR -> tracker sync` work, the source-repo refere
51
51
 
52
52
  4. Branch on the detector output instead of inventing a polling loop:
53
53
  - `state=waiting_for_copilot_review` with `snapshot.copilotReviewOnCurrentHead=false`: do **not** poll manually; either run `node <resolved-skill-scripts>/loop/run-watch-cycle.mjs --repo <owner/name> --pr <number>` for persistent async waiting or report the wait state and resume later after the single detector call
54
- - `state=waiting_for_ci` with `snapshot.ciStatus` in `{ "pending", "none" }`: do **not** poll manually by default; use `gh run watch <run-id> --repo <owner/name>` when the current-head run id is already known, otherwise report pending CI and resume later after the single detector refresh. Bounded exception: if GitHub created zero current-head check suites/statuses, the previous head rollup was green, and local `npm run verify` already passed for the same current head, rerun `detect-copilot-loop-state.mjs` with `--local-validation-head-sha <current-head-sha>` so the detector can promote that exact zero-suite case to `snapshot.ciStatus="crediblyGreen"` instead of waiting forever on raw `none`.
54
+ - `state=waiting_for_ci` with `snapshot.ciStatus` in `{ "pending", "none" }`: do **not** poll manually by default; use `dev-loops loop watch-ci --repo <owner/name> --pr <number>` to block-wait on the combined check/status state (provider-agnostic — CircleCI, GitHub Actions, and any external commit-status / check-run). `gh run watch <run-id>` is an Actions-only fallback when the current-head run id is already known and you know the gating checks are GitHub Actions. Either way, on `timeout`/`changed` report pending CI and resume later after the single detector refresh. Bounded exception: if GitHub created zero current-head check suites/statuses, the previous head rollup was green, and local `npm run verify` already passed for the same current head, rerun `detect-copilot-loop-state.mjs` with `--local-validation-head-sha <current-head-sha>` so the detector can promote that exact zero-suite case to `snapshot.ciStatus="crediblyGreen"` instead of waiting forever on raw `none`.
55
55
  - `snapshot.ciStatus="failure"` remains a stop/fix state, never a wait loop
56
56
 
57
57
  5. For reviewer-side draft-review work, run `node <resolved-skill-scripts>/loop/detect-reviewer-loop-state.mjs --repo <owner/name> --pr <number> [--reviewer-login <login>] [--local-state <path>]`.
@@ -224,6 +224,10 @@ gh pr edit <pr-number> --repo <resolved-repo> --title "..." --body-file <body-fi
224
224
  gh pr ready <pr-number> --repo <resolved-repo>
225
225
  gh pr review <pr-number> --repo <resolved-repo> --approve --body "..."
226
226
  node <resolved-skill-scripts>/github/detect-checkpoint-evidence.mjs --repo <resolved-repo> --pr <pr-number>
227
+ ```
228
+
229
+ The merge itself is gated, not implied by the lines above. Before any `gh pr merge`, follow [Merge Preconditions](./merge-preconditions.md): all preconditions (green CI, clean `draft_gate` + current-head `pre_approval_gate`, zero unresolved threads, explicit merge authorization) must hold. Critically, when `autonomy.humanMergeOnly: true` is set, merge is a fixed human-only action — `resolveEffectiveMergeAuthorized` fails closed, the agent **never** runs `gh pr merge`, and instead reports merge-ready + gate evidence and hands off to a human. Only when **not** `humanMergeOnly` and merge is explicitly authorized may the agent run:
230
+ ```sh
227
231
  gh pr merge <pr-number> --repo <resolved-repo> --squash --delete-branch
228
232
  ```
229
233
 
@@ -32,9 +32,73 @@ A marker is allowed only while the PR is still in draft; it must be removed befo
32
32
  - `"Merge authorized if gates green"` is valid explicit authorization
33
33
  - Implied approval from prior turns is not sufficient
34
34
 
35
+ ### `autonomy.humanMergeOnly` — fixed human-only merge (non-overridable)
36
+
37
+ When a repo sets `autonomy.humanMergeOnly: true` in `.devloops`, merge is a fixed
38
+ human action and this authorization step is **non-overridable**:
39
+
40
+ - `resolveAutonomyStopAt` always includes `merge`, even if `stopAt` is set to `[]`.
41
+ - The effective merge authorization fails closed: `resolveEffectiveMergeAuthorized`
42
+ returns `false` regardless of the `mergeAuthorized` envelope flag or an explicit
43
+ "merge" instruction. The lifecycle resolver therefore never advances to the merge
44
+ state and parks at the `pre_approval_gate` human-merge handoff.
45
+ - The agent still runs the full mechanical pre-merge evidence check and reports
46
+ merge-ready + gate evidence, then hands off to a human to perform `gh pr merge`.
47
+ The agent **never** runs `gh pr merge` itself.
48
+
49
+ This makes human-gated merge an enforced repo invariant, not a per-run default an
50
+ explicit instruction can unlock.
51
+
52
+ ### `approval.humanHandoff` — offer to assign a human at the handoff (opt-in)
53
+
54
+ When a repo sets `approval.humanHandoff.enabled: true` in `.devloops`, the loop
55
+ does not just park silently at the human-merge stop — at the
56
+ `pre_approval_gate` / `waiting_for_merge_authorization` boundary it **offers**
57
+ to route the PR to a named human (pairs with `autonomy.humanMergeOnly`: when
58
+ human-merge is enforced, the handoff names who should take it). Disabled by
59
+ default; no candidate sourcing when disabled.
60
+
61
+ ```yaml
62
+ approval:
63
+ humanHandoff:
64
+ enabled: true
65
+ candidatesFrom: [codeowners, recent-committers]
66
+ assignees: [alice, bob]
67
+ ```
68
+
69
+ At the handoff boundary, resolve and surface candidates:
70
+
71
+ ```sh
72
+ dev-loops gate offer-human-handoff --repo <owner/name> --pr <number>
73
+ ```
74
+
75
+ This prints the deduped, ordered candidate list (priority:
76
+ `assignees` > `codeowners` for the touched paths (last-match-wins) >
77
+ `recent-committers` to those paths, PR author/bots excluded). It assigns no one.
78
+
79
+ **OFFER-only — the operator confirms the assignee** (auto-assigning without
80
+ confirmation is a non-goal). On confirmation, perform the action:
81
+
82
+ ```sh
83
+ dev-loops gate offer-human-handoff --repo <owner/name> --pr <number> \
84
+ --assign <login> --request-review <login>
85
+ ```
86
+
87
+ which runs `gh pr edit --add-assignee` / `--add-reviewer` for the confirmed
88
+ human(s).
89
+
35
90
  ## Post-merge
36
91
 
37
- - Remove merged worktree: `git worktree remove --force <path> && git worktree prune`
92
+ - Remove merged worktree (canonical): `node scripts/loop/cleanup-worktree.mjs --repo-root <main> (--issue <n> | --pr <n>)`.
93
+ It resolves the namespaced path, runs `git worktree remove --force <path> && git worktree prune` (the underlying
94
+ mechanism) from the main checkout, and refuses any path not under `tmp/worktrees/dev-loops/`. See
95
+ [worktree guidance](../../docs/worktree-guidance.md).
96
+ - Archive long-done queue items (operator-induced, NOT a cron): `node scripts/projects/archive-done-items.mjs --repo <owner/name> || true`.
97
+ Runs as part of this post-merge hook. It applies the configured `queue.archiveOlderThanDays` (default `7d`) and archives
98
+ board items whose issue/PR has been closed at least that long. Best-effort: run it as a standard post-merge step but ignore
99
+ any non-zero exit (a successful run that finds nothing to archive exits 0; a board/config-resolution/API error exits
100
+ non-zero, which the `|| true` masks) — a failure here must never block merge completion. See
101
+ [projects queue usage](../../docs/projects-queue-usage.md).
38
102
  - Clean up stale branches
39
103
 
40
104
  ## Cross-references
@@ -10,6 +10,7 @@ Canonical owner for agent stop / wait / block conditions across all workflow fam
10
10
  | `done` lifecycle state | all | Terminal stop |
11
11
  | `approval_ready` without explicit merge auth | `final_approval` | Stop at approval gate |
12
12
  | `merge_ready` without explicit merge auth | all | Stop at `waiting_for_merge_authorization` |
13
+ | `autonomy.humanMergeOnly: true` | all | Always stop at merge for human action, even with explicit merge auth — agent never runs `gh pr merge` (see [Merge preconditions](merge-preconditions.md)) |
13
14
  | Ambiguous / contradictory state | all | Fail closed to `needs_reconcile` |
14
15
  | Missing authoritative startup inputs | `dev-loop` | Fail closed |
15
16
 
@@ -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 `pi-dev-loops`.
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 | `pi-dev-loops` | Title/body/label generation rules, state mapping, sync triggers |
46
+ | PR projection and reverse-sync logic | `dev-loops` | Title/body/label generation rules, state mapping, sync triggers |
47
47
 
48
- `pi-dev-loops` **does not** become the canonical owner of any business fields.
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/pi-dev-loops#17](https://github.com/mfittko/pi-dev-loops/issues/17)
238
- - Umbrella artifact model epic: [mfittko/pi-dev-loops#19](https://github.com/mfittko/pi-dev-loops/issues/19)
239
- - This contract (first implementable slice): [mfittko/pi-dev-loops#21](https://github.com/mfittko/pi-dev-loops/issues/21)
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, always run `git fetch origin` first. Always create worktrees from a freshly fetched `origin/main`. The fetched `origin/main` is the authoritative base for all worktree creation.
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 (`PI_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.
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,32 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## Unreleased
6
+
7
+ ## 0.4.0
8
+
9
+ ### Added
10
+
11
+ - **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.
12
+ - **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).
13
+ - **`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).
14
+ - **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).
15
+ - **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.
16
+
17
+ ### Changed
18
+
19
+ - **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.
20
+ - **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.
21
+ - **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.
22
+ - **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.
23
+
24
+ - **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.
25
+
26
+ ### Fixed
27
+
28
+ - **`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.
29
+ - **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.
30
+
5
31
  ## 0.3.0
6
32
 
7
33
  ### 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 `>=20` and a GitHub-authenticated `gh` CLI for repository workflows. See [Requirements](#requirements).
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 `>=20`
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`
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
- project: "GitHub Projects queue helpers",
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",
@@ -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: `>=20` (from `package.json`)
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": ">=20"
18
+ "node": ">=24"
19
19
  },
20
20
  "workspaces": [
21
21
  "packages/*"
@@ -68,7 +68,7 @@
68
68
  },
69
69
  "dependencies": {
70
70
  "mermaid": "11.15.0",
71
- "@dev-loops/core": "^0.3.0"
71
+ "@dev-loops/core": "^0.4.0"
72
72
  },
73
73
  "repository": {
74
74
  "type": "git",
@@ -78,7 +78,7 @@
78
78
  "url": "https://github.com/mfittko/dev-loops/issues"
79
79
  },
80
80
  "homepage": "https://github.com/mfittko/dev-loops#readme",
81
- "version": "0.3.0",
81
+ "version": "0.4.0",
82
82
  "files": [
83
83
  "cli/",
84
84
  "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 `PI_SUBAGENT_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
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 `PI_SUBAGENT_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.
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>`