dev-loops 0.2.7 → 0.3.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 (48) hide show
  1. package/.claude/.claude-plugin/plugin.json +1 -1
  2. package/.claude/agents/dev-loop.md +1 -1
  3. package/.claude/agents/developer.md +1 -0
  4. package/.claude/agents/fixer.md +1 -0
  5. package/.claude/agents/review.md +30 -0
  6. package/.claude/skills/copilot-pr-followup/SKILL.md +57 -3
  7. package/.claude/skills/dev-loop/SKILL.md +5 -5
  8. package/.claude/skills/docs/anti-patterns.md +2 -0
  9. package/.claude/skills/docs/copilot-loop-operations.md +2 -2
  10. package/.claude/skills/local-implementation/SKILL.md +17 -3
  11. package/AGENTS.md +1 -1
  12. package/CHANGELOG.md +43 -0
  13. package/agents/developer.agent.md +1 -0
  14. package/agents/fixer.agent.md +1 -0
  15. package/agents/review.agent.md +30 -0
  16. package/cli/index.mjs +39 -6
  17. package/package.json +2 -2
  18. package/scripts/README.md +6 -5
  19. package/scripts/_core-helpers.mjs +1 -0
  20. package/scripts/claude/headless-dev-loop.mjs +53 -13
  21. package/scripts/claude/headless-info-smoke.mjs +45 -11
  22. package/scripts/github/build-adjacent-bundle.mjs +448 -0
  23. package/scripts/github/{create-draft-pr.mjs → create-pr.mjs} +28 -12
  24. package/scripts/github/detect-checkpoint-evidence.mjs +95 -4
  25. package/scripts/github/post-gate-findings.mjs +392 -0
  26. package/scripts/github/reconcile-draft-gate.mjs +2 -2
  27. package/scripts/github/request-copilot-review.mjs +69 -8
  28. package/scripts/github/upsert-checkpoint-verdict.mjs +597 -15
  29. package/scripts/github/verify-fresh-review-context.mjs +12 -1
  30. package/scripts/github/write-gate-context.mjs +634 -0
  31. package/scripts/github/write-gate-findings-log.mjs +1 -1
  32. package/scripts/loop/detect-change-scope.mjs +36 -11
  33. package/scripts/loop/detect-pr-gate-coordination-state.mjs +30 -18
  34. package/scripts/loop/detect-tracker-first-loop-state.mjs +38 -11
  35. package/scripts/loop/run-queue.mjs +87 -15
  36. package/scripts/projects/add-queue-item.mjs +60 -43
  37. package/scripts/projects/archive-done-items.mjs +135 -39
  38. package/scripts/projects/ensure-queue-board.mjs +65 -64
  39. package/scripts/projects/list-queue-items.mjs +57 -56
  40. package/scripts/projects/move-queue-item.mjs +123 -124
  41. package/scripts/projects/reorder-queue-item.mjs +62 -44
  42. package/scripts/projects/sync-item-status.mjs +198 -0
  43. package/skills/copilot-pr-followup/SKILL.md +57 -3
  44. package/skills/dev-loop/scripts/log-bash-exit-1.mjs +2 -2
  45. package/skills/dev-loop/scripts/phase-files.mjs +2 -2
  46. package/skills/docs/anti-patterns.md +2 -0
  47. package/skills/docs/copilot-loop-operations.md +2 -2
  48. package/skills/local-implementation/SKILL.md +17 -3
@@ -6,7 +6,9 @@ import {
6
6
  isCopilotLogin,
7
7
  isDirectCliRun,
8
8
  parseReviewThreads,
9
+ resolveDraftGateRoundResetMs,
9
10
  summarizeCopilotReviews,
11
+ summarizeGateReviewComments,
10
12
  } from "../_core-helpers.mjs";
11
13
  import { parsePrNumber, requireTokenValue, runChild } from "../_cli-primitives.mjs";
12
14
  import { fetchGithubReviewThreadsPayload } from "./capture-review-threads.mjs";
@@ -121,7 +123,7 @@ function parseRequestedReviewersPayload(text) {
121
123
  requested: users.some((user) => isCopilotLogin(user?.login)),
122
124
  };
123
125
  }
124
- function parseReviewsPayload(text) {
126
+ function parseReviewsPayload(text, { draftGateResetAtMs = null } = {}) {
125
127
  let payload;
126
128
  try {
127
129
  payload = JSON.parse(text);
@@ -131,7 +133,10 @@ function parseReviewsPayload(text) {
131
133
  const headSha = typeof payload?.headRefOid === "string" && payload.headRefOid.trim().length > 0
132
134
  ? payload.headRefOid.trim()
133
135
  : null;
134
- const reviewSummary = summarizeCopilotReviews(payload?.reviews, { headSha });
136
+ // Apply the draft-gate round reset so the completed round count matches what
137
+ // detect-pr-gate-coordination-state computes (#896): when the draft gate has
138
+ // re-passed clean on an earlier head, only reviews after that re-pass count.
139
+ const reviewSummary = summarizeCopilotReviews(payload?.reviews, { headSha, draftGateResetAtMs });
135
140
  return {
136
141
  prData: payload,
137
142
  headSha,
@@ -166,6 +171,51 @@ async function fetchCopilotReviewIds({ repo, pr }, { env = process.env, ghComman
166
171
  }
167
172
  return parseReviewsPayload(result.stdout);
168
173
  }
174
+
175
+ // Re-derive the completed Copilot round count with the draft-gate round reset
176
+ // applied, mirroring detect-pr-gate-coordination-state so both scripts agree on the
177
+ // completed count and therefore on round-cap-reached (#896). A clean draft_gate
178
+ // re-pass on an earlier head resets the count, so post-reset reviews must not be
179
+ // counted toward the cap.
180
+ //
181
+ // Queried lazily — only when the raw (un-reset) count has already hit the cap — so
182
+ // the common (under-cap) request path keeps its existing gh-call contract and adds
183
+ // no API round-trip. Uses a single issue-comments fetch (the same source the gate
184
+ // detector uses for the latest clean draft_gate marker), not the full checkpoint-
185
+ // evidence pipeline, to keep the added surface minimal. Best-effort: a fetch failure
186
+ // falls back to the raw count, so the cap is never silently disabled.
187
+ async function resolveDraftGateAdjustedRounds(options, { env = process.env, ghCommand = "gh" } = {}, before) {
188
+ try {
189
+ const currentHeadSha = typeof before?.prData?.headRefOid === "string" && before.prData.headRefOid.trim().length > 0
190
+ ? before.prData.headRefOid.trim()
191
+ : null;
192
+ const result = await runChild(
193
+ ghCommand,
194
+ ["api", "--paginate", "--slurp", `repos/${options.repo}/issues/${options.pr}/comments?per_page=100`],
195
+ env,
196
+ );
197
+ if (result.code !== 0) {
198
+ return before.completedCopilotReviewRounds ?? 0;
199
+ }
200
+ let comments;
201
+ try {
202
+ const payload = JSON.parse(result.stdout);
203
+ comments = Array.isArray(payload) ? payload.flat() : [];
204
+ } catch {
205
+ return before.completedCopilotReviewRounds ?? 0;
206
+ }
207
+ const gateSummary = summarizeGateReviewComments(comments);
208
+ const draftGateResetAtMs = resolveDraftGateRoundResetMs({ draftGate: gateSummary?.draft_gate, currentHeadSha });
209
+ if (draftGateResetAtMs == null) {
210
+ return before.completedCopilotReviewRounds ?? 0;
211
+ }
212
+ const adjusted = parseReviewsPayload(JSON.stringify(before.prData ?? {}), { draftGateResetAtMs });
213
+ return adjusted.completedCopilotReviewRounds ?? 0;
214
+ } catch {
215
+ return before.completedCopilotReviewRounds ?? 0;
216
+ }
217
+ }
218
+
169
219
  async function fetchCopilotReviewState(options, runtime) {
170
220
  const requestedReviewers = await fetchRequestedReviewers(options, runtime);
171
221
  const reviews = await fetchCopilotReviewIds(options, runtime);
@@ -434,7 +484,18 @@ export async function performCopilotReviewRequest(options, { env = process.env,
434
484
  }
435
485
  } catch {
436
486
  }
437
- if ((before.completedCopilotReviewRounds ?? 0) >= maxRounds
487
+ // Reconcile the completed-round count with detect-pr-gate-coordination-state (#896):
488
+ // when the raw count has reached the cap, re-derive it with the draft-gate round
489
+ // reset applied. A clean draft_gate re-pass on an earlier head resets the count, so
490
+ // a post-reset PR that detect reports as under-cap must NOT be refused here as
491
+ // cap-reached. Only query checkpoint evidence on this (at/over-cap) path.
492
+ let completedRounds = before.completedCopilotReviewRounds ?? 0;
493
+ if (completedRounds >= maxRounds
494
+ && !before.requested
495
+ && !before.hasPendingReviewOnCurrentHead) {
496
+ completedRounds = await resolveDraftGateAdjustedRounds(options, { env, ghCommand }, before);
497
+ }
498
+ if (completedRounds >= maxRounds
438
499
  && !before.requested
439
500
  && !before.hasPendingReviewOnCurrentHead) {
440
501
  if (!options.forceRerequestReview) {
@@ -451,9 +512,9 @@ export async function performCopilotReviewRequest(options, { env = process.env,
451
512
  repo: options.repo,
452
513
  pr: options.pr,
453
514
  reviewer: "Copilot",
454
- completedRounds: before.completedCopilotReviewRounds,
515
+ completedRounds,
455
516
  maxRounds,
456
- detail: `Round cap of ${maxRounds} reached with ${before.completedCopilotReviewRounds} completed rounds. No further re-requests will be made.`,
517
+ detail: `Round cap of ${maxRounds} reached with ${completedRounds} completed rounds. No further re-requests will be made.`,
457
518
  };
458
519
  }
459
520
  }
@@ -471,8 +532,8 @@ export async function performCopilotReviewRequest(options, { env = process.env,
471
532
  repo: options.repo,
472
533
  pr: options.pr,
473
534
  reviewer: "Copilot",
474
- detail: `Round cap of ${maxRounds} reached with ${before.completedCopilotReviewRounds} completed rounds. --force-rerequest-review was supplied but commit SHA data is unavailable, so change-since-last-review could not be evaluated.`,
475
- completedRounds: before.completedCopilotReviewRounds,
535
+ detail: `Round cap of ${maxRounds} reached with ${completedRounds} completed rounds. --force-rerequest-review was supplied but commit SHA data is unavailable, so change-since-last-review could not be evaluated.`,
536
+ completedRounds,
476
537
  maxRounds,
477
538
  };
478
539
  }
@@ -484,7 +545,7 @@ export async function performCopilotReviewRequest(options, { env = process.env,
484
545
  pr: options.pr,
485
546
  reviewer: "Copilot",
486
547
  detail: "No changes since last Copilot review. --force-rerequest-review requires new commits on the PR head.",
487
- completedRounds: before.completedCopilotReviewRounds,
548
+ completedRounds,
488
549
  maxRounds,
489
550
  };
490
551
  }