dev-loops 0.2.7 → 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.
- package/.claude/.claude-plugin/plugin.json +1 -1
- package/.claude/agents/dev-loop.md +1 -1
- package/.claude/agents/developer.md +1 -0
- package/.claude/agents/fixer.md +1 -0
- package/.claude/agents/review.md +30 -0
- package/.claude/hooks/_run-context.mjs +9 -16
- package/.claude/skills/copilot-pr-followup/SKILL.md +62 -6
- package/.claude/skills/dev-loop/SKILL.md +6 -6
- package/.claude/skills/docs/anti-patterns.md +2 -0
- package/.claude/skills/docs/copilot-loop-operations.md +3 -3
- package/.claude/skills/docs/issue-intake-procedure.md +4 -0
- package/.claude/skills/docs/merge-preconditions.md +65 -1
- 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 +25 -5
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +69 -0
- package/README.md +8 -2
- package/agents/developer.agent.md +1 -0
- package/agents/fixer.agent.md +1 -0
- package/agents/review.agent.md +30 -0
- package/cli/index.mjs +60 -8
- package/extension/README.md +1 -1
- package/package.json +3 -3
- package/scripts/README.md +8 -7
- package/scripts/_core-helpers.mjs +1 -0
- package/scripts/claude/headless-dev-loop.mjs +53 -13
- package/scripts/claude/headless-info-smoke.mjs +45 -11
- package/scripts/github/build-adjacent-bundle.mjs +448 -0
- package/scripts/github/{create-draft-pr.mjs → create-pr.mjs} +28 -12
- package/scripts/github/detect-checkpoint-evidence.mjs +95 -4
- package/scripts/github/offer-human-handoff.mjs +147 -0
- package/scripts/github/post-gate-findings.mjs +392 -0
- package/scripts/github/probe-ci-status.mjs +468 -0
- package/scripts/github/reconcile-draft-gate.mjs +2 -2
- package/scripts/github/request-copilot-review.mjs +72 -11
- package/scripts/github/resolve-handoff-candidates.mjs +412 -0
- package/scripts/github/upsert-checkpoint-verdict.mjs +599 -17
- package/scripts/github/verify-fresh-review-context.mjs +12 -1
- package/scripts/github/write-gate-context.mjs +634 -0
- package/scripts/github/write-gate-findings-log.mjs +1 -1
- package/scripts/loop/_stale-runner-detection.mjs +1 -1
- package/scripts/loop/_worktree-path.mjs +27 -0
- package/scripts/loop/cleanup-worktree.mjs +175 -0
- package/scripts/loop/copilot-pr-handoff.mjs +1 -1
- package/scripts/loop/detect-change-scope.mjs +36 -11
- package/scripts/loop/detect-pr-gate-coordination-state.mjs +30 -18
- package/scripts/loop/detect-stale-runner.mjs +3 -4
- package/scripts/loop/detect-tracker-first-loop-state.mjs +38 -11
- package/scripts/loop/ensure-worktree.mjs +219 -0
- package/scripts/loop/outer-loop.mjs +1 -1
- package/scripts/loop/pr-runner-coordination.mjs +1 -1
- 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 +5 -5
- package/scripts/loop/run-queue.mjs +112 -16
- package/scripts/loop/run-watch-cycle.mjs +75 -22
- package/scripts/projects/add-queue-item.mjs +80 -48
- package/scripts/projects/archive-done-items.mjs +136 -39
- package/scripts/projects/ensure-queue-board.mjs +67 -65
- package/scripts/projects/list-queue-items.mjs +59 -57
- package/scripts/projects/move-queue-item.mjs +125 -125
- package/scripts/projects/reorder-queue-item.mjs +67 -48
- package/scripts/projects/sync-item-status.mjs +199 -0
- package/skills/copilot-pr-followup/SKILL.md +62 -6
- package/skills/dev-loop/SKILL.md +2 -2
- package/skills/dev-loop/scripts/log-bash-exit-1.mjs +2 -2
- package/skills/dev-loop/scripts/phase-files.mjs +2 -2
- package/skills/docs/anti-patterns.md +2 -0
- package/skills/docs/copilot-loop-operations.md +3 -3
- package/skills/docs/issue-intake-procedure.md +4 -0
- package/skills/docs/merge-preconditions.md +65 -1
- package/skills/docs/stop-conditions.md +1 -0
- package/skills/docs/tracker-first-loop-state.md +6 -6
- package/skills/local-implementation/SKILL.md +25 -5
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.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
|
+
|
|
31
|
+
## 0.3.0
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
35
|
+
- **Gate fan-out/fan-in review sub-loop** (epic #867, #895). The draft and pre-approval gates now run as a real fan-out on a **build-once neutral context bundle**: a deterministic context-builder script resolves review angles and builds ONE neutral bundle (full diff + adjacent code), then each independent, read-only `review` agent is seeded with that identical bundle verbatim and scoped to one angle, and a fan-in step consolidates the per-angle verdicts into a disposition ledger. The cost win is work-dedup (build once vs. N× re-derivation; a shared-prefix prompt-cache is an opportunistic bonus) — there is no fork primitive and no Workflow-tool dependency. Verdicts record their execution mode (`--execution-mode fanout_fanin | inline_single_agent`, with `--inline-reason`; #875) so the audit trail shows how the gate was actually run. See [docs/gate-review-sub-loop-contract.md](docs/gate-review-sub-loop-contract.md).
|
|
36
|
+
- **Context-builder handoff + dynamic angles** (#880, #895). A `write-gate-context` step emits the per-gate scope/diff artifact plus a deterministic, neutral `adjacentCode` bundle (each changed file's 1-hop import callers/callees/imports, with size guards + a stripped/truncated/missing manifest) that every reviewer is seeded with verbatim, and angles are resolved dynamically (configurable `mandatory` set plus `gates.dynamicAngles`), bounded by `gates.maxFanoutReviewers` (default 8).
|
|
37
|
+
- **Independent scoped reviewers + fan-in consolidation** (#881, #895). Per-angle `review` agents are independent fresh-context Agents seeded with the neutral bundle (never inheriting the main agent's state); they emit structured findings, and `consolidateFanin` merges them and computes the `fanout_fanin` verdict against `blockCleanOnFindingSeverities` (`must-fix`, `worth-fixing-now`).
|
|
38
|
+
- **Full-diff + adversarial scoped review with scope widening** (#886, #885, #895). The context-builder builds the full PR diff and a generous adjacent-code bundle once, and reviewers use it as their base and widen only per-angle when needed. Reviewers run adversarially against the complete change — this surfaced real defects (arg coercion, head-SHA casing, markdown injection, dead seams) that the prior single-pass review missed.
|
|
39
|
+
- **Fan-out findings posted to the PR** (#888, #887). The gate posts a single marker-tagged, idempotent PR comment listing its findings so Copilot and humans see them, and the loop fixes/resolves its own findings as it does Copilot comments. Opt out with `gates.postFindingsComments: false` (default on).
|
|
40
|
+
- **Configured board drives queue membership** (#884, #864). A configured GitHub Projects board's `Next Up` column is now the authoritative source of queue membership and ordering (not just status); emptiness reports a precise verdict (`queue_empty` / `board_empty` / `board_unavailable`) instead of a misleading generic message.
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
|
|
44
|
+
- **Gate fan-out evidence enforcement is now ON by default** (#882, #879, epic #867 final phase). A clean gate verdict requires the gate to have run via `--execution-mode fanout_fanin` with a findings-log ledger for the head SHA; the pre-merge evidence check fails closed otherwise. Repos can opt out with `gates.requireFanoutEvidence: false`.
|
|
45
|
+
- **Board status auto-syncs on dev-loop transitions** (#883, #874). A linked issue's board Status column is synced on loop transitions (e.g. PR opened → `In Progress`, merged → `Done`) via local `gh` auth — best-effort and non-fatal. Repairs the `move-queue-item` lookup that passed numeric (not string) project/item refs.
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
|
|
49
|
+
- **Skill shims import `@dev-loops/core` via its package specifier** (#890). `skills/dev-loop/scripts/log-bash-exit-1.mjs` and `phase-files.mjs` previously reached into core through cross-package relative paths (`../../../packages/core/src/...`), which are broken on disk for npm consumers because the published `dev-loops` package ships `skills/` but not `packages/core/`. They now import via the `@dev-loops/core` `exports` map. A contract test guards against reintroducing relative cross-package imports under `skills/`.
|
|
50
|
+
- **Draft-gate deadlock on ready PRs resolved** (#891). Posting a `draft_gate` verdict on a PR that is already ready-for-review (e.g. opened directly as ready) no longer dead-ends. `upsert-checkpoint-verdict` now (a) treats an already-satisfied draft gate as an idempotent no-op instead of a hard error, and (b) when a ready PR still needs clean draft-gate evidence, performs the draft→post→ready transition automatically — preserving the caller's execution mode (`fanout_fanin`), findings, and ledger. This is the fanout-aware analogue of `reconcile-draft-gate` (which only posts inline and so cannot satisfy `requireFanoutEvidence` on the draft gate).
|
|
51
|
+
- **PR self-assignment is now mechanically enforced** (#894). The draft-PR wrapper is renamed `scripts/github/create-draft-pr.mjs` → `scripts/github/create-pr.mjs` (`dev-loops pr create-draft` → `dev-loops pr create`, with the old subcommand kept as a deprecated alias). It now defaults `--assignee @me` when no `--assignee` is given (while still honoring an explicit `--assignee <login>`), so every PR opened through the canonical path is ALWAYS a draft and is always assigned — self-assigned by default — closing the silent gap where unassigned PRs (e.g. #889, #892, #893) missed the owner's assignee inbox. A new contract guard (`test/contracts/canonical-pr-creation-contract.test.mjs`) fails if any skill/agent procedure doc instructs opening a PR with raw `gh pr create`.
|
|
52
|
+
- **Post-round-cap convergence deadlock resolved** (#896). At the Copilot round cap with clean threads + green CI, a post-cap head that Copilot will not re-review now routes to a clean fallback that permits the `pre_approval_gate` to review the current head, instead of dead-ending at `ready_to_rerequest_review` (the deadlock #848 intended to prevent). Root cause: the coordination-context loader did not pass the resolved config into the loop interpreter, so `maxCopilotRounds` was unseen and `ROUND_CAP_CLEAN_FALLBACK` never resolved; the draft-gate round-reset is now a shared helper so `request-copilot-review` and `detect-pr-gate-coordination-state` agree on the round count. Genuinely-blocked states (failing/unconfirmed CI, unresolved feedback, conflicts, missing draft-gate evidence) still hold.
|
|
53
|
+
- **Gate verdict renders consolidated per-angle findings structurally** (#898). A `fanout_fanin` verdict comment renders the per-angle fan-in findings as a readable list (per-angle verdict + findings) via a new `--findings-json`, instead of collapsing the summary to a single run-on line. The gate-evidence parse contract is preserved (a single-line digest still anchors the `Findings summary:` field, and `gateEvidenceNote` is carried), input shape is validated (per-angle or flat-grouped; unrecognized input is rejected rather than silently dropped), and `--findings-summary` remains the inline fallback.
|
|
54
|
+
|
|
55
|
+
## 0.2.8
|
|
56
|
+
|
|
57
|
+
### Added
|
|
58
|
+
|
|
59
|
+
- **Local post-merge board archive** (#869). The dev-loop post-merge step archives `Done`-column board items older than a configurable threshold (`.devloops` `queue.archiveOlderThanDays`, default 7d) using local `gh` auth — best-effort, non-fatal, no CI/cron/PAT. On-demand `dev-loops project archive-done` is unchanged.
|
|
60
|
+
- **Gate execution-mode disclosure scaffolding** (#867, partial). Gate verdicts can record `--execution-mode` / `--inline-reason`; opt-in `gates.requireFanoutEvidence` (default off) is available. (Live fan-out/fan-in execution remains follow-up.)
|
|
61
|
+
|
|
62
|
+
### Fixed
|
|
63
|
+
|
|
64
|
+
- **`dev-loops project move` repaired** (#865). Item lookup now resolves both issue/PR number and node-id refs against a single paginated board-item list; fixes the `ITEM_NOT_FOUND` (unpaginated `first:10`) and the invalid `ProjectV2.item` GraphQL query.
|
|
65
|
+
|
|
66
|
+
### Changed
|
|
67
|
+
|
|
68
|
+
- **Index-based arg parsers migrated to `node:util.parseArgs`** (#857, #870). The remaining `argv[++i]` parsers across `scripts/projects`, `scripts/loop`, `scripts/claude`, and `archive-done-items.mjs` now use `parseArgs`; CLI contracts preserved and boolean flags reject an explicit inline `=value`.
|
|
69
|
+
|
|
70
|
+
### Documentation
|
|
71
|
+
|
|
72
|
+
- **Tooling-internals anti-pattern promoted** (#861, #863). The "use the CLI/`--help`/`skills/docs/` instead of reading tooling source" rule is now a canonical entry in `skills/docs/anti-patterns.md`, with a local failure-triage fast path and pointers from the `developer`/`fixer` agents.
|
|
73
|
+
|
|
5
74
|
## 0.2.7
|
|
6
75
|
|
|
7
76
|
### Fixed
|
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`
|
|
@@ -19,6 +19,7 @@ You are a focused implementation agent. You take a single clearly-scoped coding
|
|
|
19
19
|
- Do not re-plan the broader milestone unless a blocker forces it.
|
|
20
20
|
- Stay within the requested scope and files unless a small adjacent fix is required to complete the task safely.
|
|
21
21
|
- Preserve existing project conventions and package/runtime behavior.
|
|
22
|
+
- Tooling internals: use a tool's CLI, `--help`, and `skills/docs/` rather than reading its source. See [Anti-patterns](../skills/docs/anti-patterns.md#core-anti-patterns).
|
|
22
23
|
|
|
23
24
|
## Engineering Principles
|
|
24
25
|
- Prefer KISS: choose the simplest implementation that fully satisfies the task.
|
package/agents/fixer.agent.md
CHANGED
|
@@ -24,6 +24,7 @@ You are a focused review-fix agent. You take an existing pull request with revie
|
|
|
24
24
|
- If no code change is needed, reply with the reasoning and only then resolve if the concern is truly addressed.
|
|
25
25
|
- When unsure about correctness, architecture, security, or product tradeoffs, pause and ask for expert judgment rather than guessing. Use the available project workflow for expert review when possible, or clearly report the decision needed.
|
|
26
26
|
- Keep fixes tightly scoped to the review feedback unless a small adjacent change is required for correctness.
|
|
27
|
+
- Tooling internals: use a tool's CLI, `--help`, and `skills/docs/` rather than reading its source. See [Anti-patterns](../skills/docs/anti-patterns.md#core-anti-patterns).
|
|
27
28
|
|
|
28
29
|
## Review Workflow
|
|
29
30
|
1. Read unresolved review threads and any general review comments.
|
package/agents/review.agent.md
CHANGED
|
@@ -27,6 +27,36 @@ You are a focused pull request review agent. You review an implementation for co
|
|
|
27
27
|
- Only perform a full re-review when the caller explicitly requests one (e.g., "full review", "review from scratch", "re-review everything"), or when no prior review by that reviewer exists.
|
|
28
28
|
- Explicitly state the delta scope at the top of the output (e.g., "Delta review covering commits since `abc1234` on 2026-05-07").
|
|
29
29
|
|
|
30
|
+
## Scoped angle-review mode
|
|
31
|
+
|
|
32
|
+
This agent has two modes. The default mode is the full-PR review described in the rest of this file. The **scoped angle-review mode** is the per-angle reviewer of the gate-review fan-out ([Gate Review Sub-Loop Contract](../docs/gate-review-sub-loop-contract.md)): the gate skill builds ONE deterministic, neutral context bundle (diff + adjacent code) and seeds each independent `review` agent with that identical bundle, each scoped to exactly one review concern. You receive only the neutral artifact + your angle — never the main (orchestrating) agent's conversation, opinions, or state.
|
|
33
|
+
|
|
34
|
+
You are in scoped angle-review mode when the invocation supplies a single review `<angle>` plus a gate-context artifact path (`tmp/gate-context/<repo-slug>/pr-<N>/<gate>-<headSha>.json`, written by `scripts/github/write-gate-context.mjs`). Review like an external code reviewer hunting real bugs, not a process auditor: your job is to find concrete defects in the changed code and its call paths, not to remark that "there is no test" or that a doc section is thin. In that mode:
|
|
35
|
+
|
|
36
|
+
- **Fresh-context guard (mandatory):** run `node scripts/github/verify-fresh-review-context.mjs --scope <angle>` at startup. If it reports `fresh: false` (exit 1) or any error, refuse to proceed and report contamination — do not review on inherited context. "Fresh" here means your context is the **neutral builder artifact + your angle**, and explicitly NOT the main agent's conversation/state or a prior reviewer session's state. The injected neutral bundle is the INTENDED seed (allowed); main-agent or cross-session state bleed still fails closed.
|
|
37
|
+
- **Use the PROVIDED neutral bundle as your base; do NOT re-derive it from scratch:** the gate-context artifact carries the diff (`scope.diffPath`) and a deterministic, neutral adjacent-code bundle in `adjacentCode` (each changed file plus its 1-hop import in/out-edges, with a `stripped`/`truncated`/`missing` manifest). Start from this bundle — it is the build-once, work-deduped seed shared verbatim across all reviewers. Only widen (load more files) per-angle when the bundle genuinely lacks something your angle needs; do not re-build the whole diff/adjacent-code graph yourself.
|
|
38
|
+
- **Single-angle scope:** review ONLY the supplied angle's concern. Read the gate-context artifact for the resolved angle set, change scope (branch, head SHA, touched files), acceptance-criteria pointer, and validation posture. Do not review other angles; the fan-in pass consolidates across angles.
|
|
39
|
+
- **Read the FULL diff, not just hunks:** the gate-context artifact records `scope.diffPath` (a repo-relative path to the complete captured diff) and `scope.changedFiles` (full repo-relative paths). Read the entire diff from `scope.diffPath`; if it is `null` or missing, reconstruct it yourself with `git diff` against the change's base (e.g. `git diff origin/main...HEAD` or the branch/head SHA in `scope`). Do not review off a partial or summarized hunk list — you must see every changed line for your angle.
|
|
40
|
+
- **Reason about ADJACENT related code from the bundle first:** the `adjacentCode` bundle already includes the changed files' callers, callees, and imports (1-hop). Read those entries before opening anything else. For each changed function, trace the real call paths a defect would flow through, and confirm that caller and callee contracts still match (argument shapes, return shapes, error/null conventions, units). If the bundle truncated or stripped a file you need (see its `truncated`/`stripped` manifest), open that file directly to widen.
|
|
41
|
+
- **Review ADVERSARIALLY — hunt concrete defects:** actively try to break the changed code. Hunt for edge cases, missing or wrong input validation, numeric coercion bugs (NaN, Infinity, floats where integers are assumed, negative values, string-to-number coercion), null/undefined handling, off-by-one and boundary conditions, mismatched caller/callee contracts, and dedup/identity bugs (wrong key, reference vs. value equality, unstable ordering). For every finding, give the concrete `file:line` plus the specific failing scenario or input that triggers the defect — not high-altitude "is there a test?" commentary.
|
|
42
|
+
- **Strictly read-only, but WIDEN SCOPE when needed:** never edit files, stage, commit, push, request reviews, resolve threads, or post PR comments. Findings are returned via the output artifact only; fixes are applied later by the fix cycle, not by this agent. Because you are read-only, you MAY open any additional repository files required to judge your angle (callers, callees, contracts, sibling modules, configs, tests) — widening to read adjacent code is expected and safe. Record every file/module you consulted beyond the briefing's `changedFiles`/`diffPath` in the optional `contextWidened` field on your output artifact.
|
|
43
|
+
- **Structured findings artifact:** return a single JSON object the fan-in consolidator (`@dev-loops/core/loop/gate-fanin`) can parse, written to the deterministic per-angle path `tmp/gate-reviews/<repo-slug>/pr-<N>/<gate>-<headSha>/<angle>.json`:
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"angle": "<angle>",
|
|
48
|
+
"verdict": "clean" | "findings_present",
|
|
49
|
+
"findings": [
|
|
50
|
+
{ "severity": "must-fix" | "worth-fixing-now" | "defer", "file": "<path>", "line": 0, "summary": "<concise>", "recommendation": "<concise fix>" }
|
|
51
|
+
],
|
|
52
|
+
"contextWidened": ["<adjacent-path-consulted>", "..."]
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`verdict` is `clean` iff `findings` is empty; otherwise `findings_present`. `severity` uses the gate vocabulary (`must-fix` | `worth-fixing-now` | `defer`). `file`/`line`/`recommendation` are optional per finding. `contextWidened` is optional: list the adjacent files/modules you opened beyond the briefing to judge this angle (omit or leave empty if you reviewed only `changedFiles`).
|
|
57
|
+
|
|
58
|
+
When NOT given an angle scope, behave exactly as the full-PR review agent described below.
|
|
59
|
+
|
|
30
60
|
## Review Focus
|
|
31
61
|
- Scope correctness: does the implementation match the PR description's change summary, the stated acceptance criteria, and the relevant plan?
|
|
32
62
|
- Acceptance criteria coverage: are the stated acceptance criteria complete, testable, and actually satisfied?
|
package/cli/index.mjs
CHANGED
|
@@ -22,16 +22,19 @@ const SUBCOMMAND_ROUTES = {
|
|
|
22
22
|
"upsert-verdict": "scripts/github/upsert-checkpoint-verdict.mjs",
|
|
23
23
|
"detect-evidence": "scripts/github/detect-checkpoint-evidence.mjs",
|
|
24
24
|
"write-findings-log": "scripts/github/write-gate-findings-log.mjs",
|
|
25
|
+
"post-findings": "scripts/github/post-gate-findings.mjs",
|
|
25
26
|
"request-copilot": "scripts/github/request-copilot-review.mjs",
|
|
26
27
|
"probe-copilot": "scripts/github/probe-copilot-review.mjs",
|
|
27
28
|
"capture-threads": "scripts/github/capture-review-threads.mjs",
|
|
28
29
|
"reply-resolve": "scripts/github/reply-resolve-review-threads.mjs",
|
|
30
|
+
"offer-human-handoff": "scripts/github/offer-human-handoff.mjs",
|
|
29
31
|
},
|
|
30
32
|
loop: {
|
|
31
33
|
startup: "scripts/loop/resolve-dev-loop-startup.mjs",
|
|
32
34
|
"build-envelope": "scripts/loop/build-handoff-envelope.mjs",
|
|
33
35
|
outer: "scripts/loop/outer-loop.mjs",
|
|
34
36
|
"watch-cycle": "scripts/loop/run-watch-cycle.mjs",
|
|
37
|
+
"watch-ci": "scripts/github/probe-ci-status.mjs",
|
|
35
38
|
handoff: "scripts/loop/copilot-pr-handoff.mjs",
|
|
36
39
|
"watch-initial": "scripts/loop/watch-initial-copilot-pr.mjs",
|
|
37
40
|
"loop-state": "scripts/loop/detect-copilot-loop-state.mjs",
|
|
@@ -43,7 +46,7 @@ const SUBCOMMAND_ROUTES = {
|
|
|
43
46
|
"debt-remediate": "scripts/loop/debt-remediate.mjs",
|
|
44
47
|
},
|
|
45
48
|
pr: {
|
|
46
|
-
|
|
49
|
+
create: "scripts/github/create-pr.mjs",
|
|
47
50
|
"ready-for-review": "scripts/github/ready-for-review.mjs",
|
|
48
51
|
"reconcile-draft": "scripts/github/reconcile-draft-gate.mjs",
|
|
49
52
|
},
|
|
@@ -53,10 +56,18 @@ const SUBCOMMAND_ROUTES = {
|
|
|
53
56
|
move: "scripts/projects/move-queue-item.mjs",
|
|
54
57
|
reorder: "scripts/projects/reorder-queue-item.mjs",
|
|
55
58
|
"archive-done": "scripts/projects/archive-done-items.mjs",
|
|
59
|
+
"sync-status": "scripts/projects/sync-item-status.mjs",
|
|
56
60
|
ensure: "scripts/projects/ensure-queue-board.mjs",
|
|
57
61
|
},
|
|
58
62
|
queue: {
|
|
59
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",
|
|
60
71
|
},
|
|
61
72
|
inspect: {
|
|
62
73
|
run: "scripts/loop/inspect-run.mjs",
|
|
@@ -67,34 +78,54 @@ const SUBCOMMAND_ROUTES = {
|
|
|
67
78
|
},
|
|
68
79
|
};
|
|
69
80
|
|
|
81
|
+
// Back-compat subcommand aliases: { category: { oldName: { canonical, notice } } }.
|
|
82
|
+
// Aliases keep existing callers working while emitting a one-line deprecation
|
|
83
|
+
// notice to stderr so they migrate to the canonical subcommand.
|
|
84
|
+
const SUBCOMMAND_ALIASES = {
|
|
85
|
+
pr: {
|
|
86
|
+
"create-draft": {
|
|
87
|
+
canonical: "create",
|
|
88
|
+
notice: "[dev-loops] `pr create-draft` is deprecated; use `pr create` (always draft, self-assigned by default).",
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
function resolveSubcommandAlias(category, subcommand) {
|
|
94
|
+
return SUBCOMMAND_ALIASES[category]?.[subcommand] ?? null;
|
|
95
|
+
}
|
|
96
|
+
|
|
70
97
|
const TOP_LEVEL_COMMANDS = new Set(["help", "status", "doctor", "gates", "hide"]);
|
|
71
98
|
|
|
72
99
|
const HELP_CATEGORY_LABELS = {
|
|
73
100
|
gate: "Gate verdicts, evidence, and review operations",
|
|
74
101
|
loop: "Loop lifecycle",
|
|
75
102
|
pr: "PR helpers",
|
|
76
|
-
|
|
103
|
+
queue: "Queue board: run + management (add/list/reorder/move/sync-status/archive)",
|
|
104
|
+
project: "Alias for queue (GitHub Projects queue helpers)",
|
|
77
105
|
inspect: "Inspection (Pi extension only)",
|
|
78
106
|
refine: "Epic tree refinement verification",
|
|
79
107
|
};
|
|
80
108
|
|
|
81
|
-
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"];
|
|
82
110
|
|
|
83
111
|
const SUBCOMMAND_DESCRIPTIONS = {
|
|
84
112
|
gate: {
|
|
85
113
|
"upsert-verdict": "Post/update gate review comment",
|
|
86
114
|
"detect-evidence": "Check merge preconditions",
|
|
87
115
|
"write-findings-log": "Write disposition ledger",
|
|
116
|
+
"post-findings": "Post gate fan-out findings comment",
|
|
88
117
|
"request-copilot": "Request Copilot review",
|
|
89
118
|
"probe-copilot": "Poll for Copilot review activity",
|
|
90
119
|
"capture-threads": "Capture review threads",
|
|
91
120
|
"reply-resolve": "Reply and resolve review threads",
|
|
121
|
+
"offer-human-handoff": "Offer to assign PR to a human reviewer/assignee",
|
|
92
122
|
},
|
|
93
123
|
loop: {
|
|
94
124
|
startup: "Resolve dev-loop startup bundle",
|
|
95
125
|
"build-envelope": "Build handoff envelope from startup output",
|
|
96
126
|
outer: "Run outer-loop detection",
|
|
97
127
|
"watch-cycle": "Run Copilot wait cycle",
|
|
128
|
+
"watch-ci": "Block-wait on provider-agnostic CI (CircleCI/Actions/external)",
|
|
98
129
|
handoff: "Copilot PR handoff",
|
|
99
130
|
"watch-initial": "Watch initial Copilot PR",
|
|
100
131
|
"loop-state": "Detect Copilot loop state",
|
|
@@ -106,7 +137,7 @@ const SUBCOMMAND_DESCRIPTIONS = {
|
|
|
106
137
|
"debt-remediate": "File debt remediation issues",
|
|
107
138
|
},
|
|
108
139
|
pr: {
|
|
109
|
-
|
|
140
|
+
create: "Create PR (always draft, self-assigned by default)",
|
|
110
141
|
"ready-for-review": "Mark PR ready for review",
|
|
111
142
|
"reconcile-draft": "Reconcile non-draft PR",
|
|
112
143
|
},
|
|
@@ -116,10 +147,18 @@ const SUBCOMMAND_DESCRIPTIONS = {
|
|
|
116
147
|
move: "Move queue item between Status columns",
|
|
117
148
|
reorder: "Reorder items (move-to-top/move-after/order, --dry-run)",
|
|
118
149
|
"archive-done": "Archive closed Done items older than a duration",
|
|
150
|
+
"sync-status": "Sync a queued issue/PR's board Status column (best-effort)",
|
|
119
151
|
ensure: "Create/repair queue board bootstrap surface",
|
|
120
152
|
},
|
|
121
153
|
queue: {
|
|
122
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",
|
|
123
162
|
},
|
|
124
163
|
inspect: {
|
|
125
164
|
run: "Inspect run state",
|
|
@@ -275,16 +314,19 @@ function resolveSubcommandRoute(args) {
|
|
|
275
314
|
return { error: `Missing subcommand for '${category}'. Available: ${subs}` };
|
|
276
315
|
}
|
|
277
316
|
|
|
278
|
-
const
|
|
317
|
+
const requestedSubcommand = args[1];
|
|
318
|
+
const alias = resolveSubcommandAlias(category, requestedSubcommand);
|
|
319
|
+
const subcommand = alias ? alias.canonical : requestedSubcommand;
|
|
279
320
|
const scriptPath = routes[subcommand];
|
|
280
321
|
if (!scriptPath) {
|
|
281
322
|
const subs = Object.keys(routes).join(", ");
|
|
282
|
-
return { error: `Unknown subcommand '${
|
|
323
|
+
return { error: `Unknown subcommand '${requestedSubcommand}' for '${category}'. Available: ${subs}` };
|
|
283
324
|
}
|
|
284
325
|
|
|
285
326
|
return {
|
|
286
327
|
scriptPath: path.resolve(REPO_ROOT, scriptPath),
|
|
287
328
|
forwardedArgs: args.slice(2),
|
|
329
|
+
...(alias ? { deprecationNotice: alias.notice } : {}),
|
|
288
330
|
};
|
|
289
331
|
}
|
|
290
332
|
|
|
@@ -313,9 +355,17 @@ function parseTopLevelCommand(argv) {
|
|
|
313
355
|
}
|
|
314
356
|
// Check if any remaining arg is --help — delegate to script
|
|
315
357
|
if (args.slice(1).some((a) => a === "--help" || a === "-h")) {
|
|
316
|
-
const
|
|
358
|
+
const alias = resolveSubcommandAlias(cmd, sub);
|
|
359
|
+
const resolvedSub = alias ? alias.canonical : sub;
|
|
360
|
+
const scriptPath = routes[resolvedSub];
|
|
317
361
|
if (!scriptPath) return { kind: "category_help", category: cmd };
|
|
318
|
-
return {
|
|
362
|
+
return {
|
|
363
|
+
kind: "subcommand_help",
|
|
364
|
+
scriptPath: path.resolve(REPO_ROOT, scriptPath),
|
|
365
|
+
// Surface the deprecation notice on the --help fast-path too, so a
|
|
366
|
+
// deprecated alias signals migration in help mode (not only on dispatch).
|
|
367
|
+
...(alias ? { deprecationNotice: alias.notice } : {}),
|
|
368
|
+
};
|
|
319
369
|
}
|
|
320
370
|
const route = resolveSubcommandRoute(args);
|
|
321
371
|
if (route) return { kind: "subcommand", ...route };
|
|
@@ -345,6 +395,7 @@ export async function runCli({
|
|
|
345
395
|
return 0;
|
|
346
396
|
}
|
|
347
397
|
case "subcommand_help": {
|
|
398
|
+
if (fromTop.deprecationNotice) { writeLines(stderr, [fromTop.deprecationNotice]); }
|
|
348
399
|
const result = spawnSync("node", [fromTop.scriptPath, "--help"], {
|
|
349
400
|
cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"],
|
|
350
401
|
});
|
|
@@ -383,6 +434,7 @@ export async function runCli({
|
|
|
383
434
|
}
|
|
384
435
|
case "subcommand": {
|
|
385
436
|
if (fromTop.error) { writeLines(stderr, [fromTop.error]); return 1; }
|
|
437
|
+
if (fromTop.deprecationNotice) { writeLines(stderr, [fromTop.deprecationNotice]); }
|
|
386
438
|
const scriptArgs = fromTop.forwardedArgs || [];
|
|
387
439
|
const result = spawnSync("node", [fromTop.scriptPath, ...scriptArgs], {
|
|
388
440
|
cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"],
|
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/*"
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
},
|
|
69
69
|
"dependencies": {
|
|
70
70
|
"mermaid": "11.15.0",
|
|
71
|
-
"@dev-loops/core": "^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.
|
|
81
|
+
"version": "0.4.0",
|
|
82
82
|
"files": [
|
|
83
83
|
"cli/",
|
|
84
84
|
"lib/",
|
package/scripts/README.md
CHANGED
|
@@ -68,16 +68,17 @@ Failure behavior:
|
|
|
68
68
|
- malformed arguments, invalid JSON, and `gh` failures emit `{ "ok": false, "error": "..." }` on stderr and exit non-zero
|
|
69
69
|
- live capture is only allowed when both `--repo` and `--pr` are present
|
|
70
70
|
|
|
71
|
-
### `scripts/github/create-
|
|
71
|
+
### `scripts/github/create-pr.mjs`
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
Canonical wrapper around `gh pr create`. Every PR opened through this tool is ALWAYS a draft and is always assigned — self-assigned by default (`--assignee @me` when none is given), while still honoring an explicit `--assignee <login>`. Never call raw `gh pr create` to open a PR. (`dev-loops pr create` dispatches here; the legacy `dev-loops pr create-draft` subcommand still works as a deprecated alias.)
|
|
74
74
|
|
|
75
75
|
Usage:
|
|
76
|
-
- `node scripts/github/create-
|
|
77
|
-
- `node <resolved-skill-scripts>/github/create-
|
|
76
|
+
- `node scripts/github/create-pr.mjs [gh pr create args...]`
|
|
77
|
+
- `node <resolved-skill-scripts>/github/create-pr.mjs [gh pr create args...]`
|
|
78
78
|
|
|
79
79
|
Contract:
|
|
80
|
-
- injects exactly one `--draft` when the caller did not already supply it
|
|
80
|
+
- injects exactly one `--draft` when the caller did not already supply it (draft is the only mode)
|
|
81
|
+
- self-assigns by default (defaults `--assignee @me` when no `--assignee` is provided); honors an explicit `--assignee <login>` when one is given
|
|
81
82
|
- rejects `--ready` before invoking `gh`; use `gh pr ready` later after draft-gate approval
|
|
82
83
|
- forwards every other argument to `gh pr create` unchanged and in order
|
|
83
84
|
- preserves the underlying `gh pr create` stdout, stderr, and exit code without wrapping success output
|
|
@@ -498,7 +499,7 @@ Contract:
|
|
|
498
499
|
- enters watch only when request state is confirmed (`requested` or `already-requested`) and emits exact `watchArgs` + `watchTimeoutPolicy`
|
|
499
500
|
- watch refresh (`--watch-status`) is observational-only; rely on refreshed `loopDisposition` + `terminal` to decide whether to continue or stop
|
|
500
501
|
- explicit stop/blocked routing is machine-readable via `action: "stop"` plus `requestWatchContract.stopState`
|
|
501
|
-
- 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
|
|
502
503
|
|
|
503
504
|
Success output shape:
|
|
504
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"?: { ... } }`
|
|
@@ -721,7 +722,7 @@ Fetches the live PR head SHA plus visible PR issue comments, then summarizes the
|
|
|
721
722
|
latest valid `draft_gate` and `pre_approval_gate` checkpoint verdict comments.
|
|
722
723
|
Use this when a fresh session needs authoritative visible gate evidence for the
|
|
723
724
|
current head before running `gh pr ready` or declaring final-approval readiness.
|
|
724
|
-
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.
|
|
725
726
|
|
|
726
727
|
Required:
|
|
727
728
|
- `--repo <owner/name>`
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import { spawnSync } from "node:child_process";
|
|
21
21
|
import path from "node:path";
|
|
22
22
|
import { fileURLToPath } from "node:url";
|
|
23
|
+
import { parseArgs } from "node:util";
|
|
23
24
|
|
|
24
25
|
import { ensureRunId } from "@dev-loops/core/loop/run-context";
|
|
25
26
|
import {
|
|
@@ -28,22 +29,61 @@ import {
|
|
|
28
29
|
DEFAULT_CLAUDE_BIN,
|
|
29
30
|
} from "@dev-loops/core/claude/headless-entry";
|
|
30
31
|
|
|
31
|
-
function
|
|
32
|
+
function parseCliArgs(argv) {
|
|
32
33
|
const opts = { dryRun: false, claudeBin: DEFAULT_CLAUDE_BIN };
|
|
33
|
-
const requireValue = (name,
|
|
34
|
-
const v =
|
|
35
|
-
if (v ===
|
|
34
|
+
const requireValue = (name, token) => {
|
|
35
|
+
const v = token.value;
|
|
36
|
+
if (typeof v !== "string" || v.length === 0 || v.startsWith("--")) {
|
|
37
|
+
throw new Error(`${name} requires a value`);
|
|
38
|
+
}
|
|
36
39
|
return v;
|
|
37
40
|
};
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
|
|
42
|
+
const { tokens } = parseArgs({
|
|
43
|
+
args: [...argv],
|
|
44
|
+
options: {
|
|
45
|
+
"dry-run": { type: "boolean" },
|
|
46
|
+
issue: { type: "string" },
|
|
47
|
+
pr: { type: "string" },
|
|
48
|
+
"claude-bin": { type: "string" },
|
|
49
|
+
prompt: { type: "string" },
|
|
50
|
+
},
|
|
51
|
+
allowPositionals: true,
|
|
52
|
+
strict: false,
|
|
53
|
+
tokens: true,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
for (const token of tokens) {
|
|
57
|
+
if (token.kind === "positional") {
|
|
58
|
+
throw new Error(`unknown argument: ${token.value}`);
|
|
59
|
+
}
|
|
60
|
+
if (token.kind !== "option") {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
switch (token.name) {
|
|
64
|
+
case "dry-run":
|
|
65
|
+
if (token.value !== undefined) {
|
|
66
|
+
throw new Error(`unknown argument: ${token.rawName}=${token.value}`);
|
|
67
|
+
}
|
|
68
|
+
opts.dryRun = true;
|
|
69
|
+
break;
|
|
70
|
+
case "issue":
|
|
71
|
+
opts.issue = requireValue("--issue", token);
|
|
72
|
+
break;
|
|
73
|
+
case "pr":
|
|
74
|
+
opts.pr = requireValue("--pr", token);
|
|
75
|
+
break;
|
|
76
|
+
case "claude-bin":
|
|
77
|
+
opts.claudeBin = requireValue("--claude-bin", token);
|
|
78
|
+
break;
|
|
79
|
+
case "prompt":
|
|
80
|
+
opts.prompt = requireValue("--prompt", token);
|
|
81
|
+
break;
|
|
82
|
+
default:
|
|
83
|
+
throw new Error(`unknown argument: ${token.rawName}`);
|
|
84
|
+
}
|
|
46
85
|
}
|
|
86
|
+
|
|
47
87
|
if (opts.issue != null && opts.pr != null) {
|
|
48
88
|
throw new Error("--issue and --pr are mutually exclusive");
|
|
49
89
|
}
|
|
@@ -53,7 +93,7 @@ function parseArgs(argv) {
|
|
|
53
93
|
function main(argv) {
|
|
54
94
|
let opts;
|
|
55
95
|
try {
|
|
56
|
-
opts =
|
|
96
|
+
opts = parseCliArgs(argv);
|
|
57
97
|
} catch (error) {
|
|
58
98
|
process.stderr.write(JSON.stringify({ ok: false, error: error.message }) + "\n");
|
|
59
99
|
return 1;
|