dev-loops 0.2.6 → 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 (92) hide show
  1. package/.claude/.claude-plugin/plugin.json +1 -1
  2. package/.claude/agents/dev-loop.md +2 -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 +60 -0
  13. package/agents/dev-loop.agent.md +5 -1
  14. package/agents/developer.agent.md +1 -0
  15. package/agents/fixer.agent.md +1 -0
  16. package/agents/review.agent.md +30 -0
  17. package/cli/index.mjs +42 -7
  18. package/package.json +2 -2
  19. package/scripts/README.md +6 -5
  20. package/scripts/_cli-primitives.mjs +2 -0
  21. package/scripts/_core-helpers.mjs +1 -0
  22. package/scripts/claude/generate-claude-assets.mjs +12 -2
  23. package/scripts/claude/headless-dev-loop.mjs +53 -13
  24. package/scripts/claude/headless-info-smoke.mjs +45 -11
  25. package/scripts/docs/validate-links.mjs +20 -11
  26. package/scripts/github/build-adjacent-bundle.mjs +448 -0
  27. package/scripts/github/capture-review-threads.mjs +32 -14
  28. package/scripts/github/{create-draft-pr.mjs → create-pr.mjs} +28 -12
  29. package/scripts/github/detect-checkpoint-evidence.mjs +123 -15
  30. package/scripts/github/detect-linked-issue-pr.mjs +22 -10
  31. package/scripts/github/manage-sub-issues.mjs +37 -16
  32. package/scripts/github/post-gate-findings.mjs +392 -0
  33. package/scripts/github/probe-copilot-review.mjs +24 -12
  34. package/scripts/github/ready-for-review.mjs +17 -8
  35. package/scripts/github/reconcile-draft-gate.mjs +24 -12
  36. package/scripts/github/reply-resolve-review-threads.mjs +34 -15
  37. package/scripts/github/request-copilot-review.mjs +97 -19
  38. package/scripts/github/resolve-tracker-local-spec.mjs +29 -12
  39. package/scripts/github/stage-reviewer-draft.mjs +32 -14
  40. package/scripts/github/upsert-checkpoint-verdict.mjs +646 -41
  41. package/scripts/github/verify-fresh-review-context.mjs +12 -1
  42. package/scripts/github/write-gate-context.mjs +634 -0
  43. package/scripts/github/write-gate-findings-log.mjs +42 -21
  44. package/scripts/loop/build-handoff-envelope.mjs +32 -14
  45. package/scripts/loop/conductor-monitor.mjs +25 -9
  46. package/scripts/loop/conductor.mjs +31 -12
  47. package/scripts/loop/copilot-pr-handoff.mjs +31 -14
  48. package/scripts/loop/debt-remediate.mjs +28 -11
  49. package/scripts/loop/detect-change-scope.mjs +36 -11
  50. package/scripts/loop/detect-copilot-loop-state.mjs +29 -12
  51. package/scripts/loop/detect-copilot-session-activity.mjs +29 -12
  52. package/scripts/loop/detect-initial-copilot-pr-state.mjs +26 -10
  53. package/scripts/loop/detect-internal-only-pr.mjs +31 -13
  54. package/scripts/loop/detect-issue-refinement-artifact.mjs +29 -12
  55. package/scripts/loop/detect-pr-gate-coordination-state.mjs +56 -28
  56. package/scripts/loop/detect-reviewer-loop-state.mjs +35 -16
  57. package/scripts/loop/detect-stale-runner.mjs +32 -14
  58. package/scripts/loop/detect-tracker-first-loop-state.mjs +38 -11
  59. package/scripts/loop/detect-tracker-pr-state.mjs +23 -8
  60. package/scripts/loop/info.mjs +28 -10
  61. package/scripts/loop/inspect-run-viewer/cli.mjs +44 -21
  62. package/scripts/loop/inspect-run.mjs +35 -16
  63. package/scripts/loop/outer-loop.mjs +35 -16
  64. package/scripts/loop/pr-runner-coordination.mjs +31 -12
  65. package/scripts/loop/pre-commit-branch-guard.mjs +26 -9
  66. package/scripts/loop/pre-flight-gate.mjs +25 -9
  67. package/scripts/loop/pre-pr-ready-gate.mjs +24 -8
  68. package/scripts/loop/pre-push-main-guard.mjs +19 -5
  69. package/scripts/loop/pre-write-remote-freshness-guard.mjs +23 -7
  70. package/scripts/loop/resolve-dev-loop-startup.mjs +29 -12
  71. package/scripts/loop/run-conductor-cycle.mjs +23 -8
  72. package/scripts/loop/run-queue.mjs +87 -15
  73. package/scripts/loop/run-refinement-audit.mjs +44 -22
  74. package/scripts/loop/run-watch-cycle.mjs +28 -12
  75. package/scripts/loop/steer-loop.mjs +122 -62
  76. package/scripts/loop/watch-initial-copilot-pr.mjs +28 -12
  77. package/scripts/projects/add-queue-item.mjs +60 -43
  78. package/scripts/projects/archive-done-items.mjs +506 -0
  79. package/scripts/projects/ensure-queue-board.mjs +65 -64
  80. package/scripts/projects/list-queue-items.mjs +57 -56
  81. package/scripts/projects/move-queue-item.mjs +123 -124
  82. package/scripts/projects/reorder-queue-item.mjs +384 -108
  83. package/scripts/projects/sync-item-status.mjs +198 -0
  84. package/scripts/refine/_refine-helpers.mjs +21 -9
  85. package/scripts/refine/verify.mjs +31 -13
  86. package/skills/copilot-pr-followup/SKILL.md +57 -3
  87. package/skills/dev-loop/SKILL.md +9 -5
  88. package/skills/dev-loop/scripts/log-bash-exit-1.mjs +2 -2
  89. package/skills/dev-loop/scripts/phase-files.mjs +2 -2
  90. package/skills/docs/anti-patterns.md +2 -0
  91. package/skills/docs/copilot-loop-operations.md +2 -2
  92. package/skills/local-implementation/SKILL.md +17 -3
@@ -1,13 +1,16 @@
1
1
  #!/usr/bin/env node
2
+ import { parseArgs } from "node:util";
2
3
  import {
3
4
  buildParseError,
4
5
  formatCliError,
5
6
  isCopilotLogin,
6
7
  isDirectCliRun,
7
8
  parseReviewThreads,
9
+ resolveDraftGateRoundResetMs,
8
10
  summarizeCopilotReviews,
11
+ summarizeGateReviewComments,
9
12
  } from "../_core-helpers.mjs";
10
- import { parsePrNumber, requireOptionValue, runChild } from "../_cli-primitives.mjs";
13
+ import { parsePrNumber, requireTokenValue, runChild } from "../_cli-primitives.mjs";
11
14
  import { fetchGithubReviewThreadsPayload } from "./capture-review-threads.mjs";
12
15
  import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
13
16
  import { buildSnapshotFromPrFacts, interpretLoopState } from "@dev-loops/core/loop/copilot-loop-state";
@@ -52,32 +55,48 @@ Exit codes:
52
55
  1 Argument error or gh failure`.trim();
53
56
  const parseError = buildParseError(USAGE);
54
57
  export function parseRequestCliArgs(argv) {
55
- const args = [...argv];
58
+ const { tokens } = parseArgs({
59
+ args: [...argv],
60
+ options: {
61
+ help: { type: "boolean", short: "h" },
62
+ "force-rerequest-review": { type: "boolean" },
63
+ repo: { type: "string" },
64
+ pr: { type: "string" },
65
+ },
66
+ allowPositionals: true,
67
+ strict: false,
68
+ tokens: true,
69
+ });
56
70
  const options = {
57
71
  help: false,
58
72
  repo: undefined,
59
73
  pr: undefined,
60
74
  forceRerequestReview: false,
61
75
  };
62
- while (args.length > 0) {
63
- const token = args.shift();
64
- if (token === "--help" || token === "-h") {
76
+ for (const token of tokens) {
77
+ if (token.kind === "positional") {
78
+ throw parseError(`Unknown argument: ${token.value}`);
79
+ }
80
+ if (token.kind !== "option") {
81
+ continue;
82
+ }
83
+ if (token.name === "help") {
65
84
  options.help = true;
66
85
  return options;
67
86
  }
68
- if (token === "--force-rerequest-review") {
87
+ if (token.name === "force-rerequest-review") {
69
88
  options.forceRerequestReview = true;
70
89
  continue;
71
90
  }
72
- if (token === "--repo") {
73
- options.repo = requireOptionValue(args, "--repo", parseError).trim();
91
+ if (token.name === "repo") {
92
+ options.repo = requireTokenValue(token, parseError).trim();
74
93
  continue;
75
94
  }
76
- if (token === "--pr") {
77
- options.pr = parsePrNumber(requireOptionValue(args, "--pr", parseError), parseError);
95
+ if (token.name === "pr") {
96
+ options.pr = parsePrNumber(requireTokenValue(token, parseError), parseError);
78
97
  continue;
79
98
  }
80
- throw parseError(`Unknown argument: ${token}`);
99
+ throw parseError(`Unknown argument: ${token.rawName}`);
81
100
  }
82
101
  if (options.repo === undefined || options.pr === undefined) {
83
102
  throw parseError("Requesting Copilot review requires both --repo <owner/name> and --pr <number>");
@@ -104,7 +123,7 @@ function parseRequestedReviewersPayload(text) {
104
123
  requested: users.some((user) => isCopilotLogin(user?.login)),
105
124
  };
106
125
  }
107
- function parseReviewsPayload(text) {
126
+ function parseReviewsPayload(text, { draftGateResetAtMs = null } = {}) {
108
127
  let payload;
109
128
  try {
110
129
  payload = JSON.parse(text);
@@ -114,7 +133,10 @@ function parseReviewsPayload(text) {
114
133
  const headSha = typeof payload?.headRefOid === "string" && payload.headRefOid.trim().length > 0
115
134
  ? payload.headRefOid.trim()
116
135
  : null;
117
- 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 });
118
140
  return {
119
141
  prData: payload,
120
142
  headSha,
@@ -149,6 +171,51 @@ async function fetchCopilotReviewIds({ repo, pr }, { env = process.env, ghComman
149
171
  }
150
172
  return parseReviewsPayload(result.stdout);
151
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
+
152
219
  async function fetchCopilotReviewState(options, runtime) {
153
220
  const requestedReviewers = await fetchRequestedReviewers(options, runtime);
154
221
  const reviews = await fetchCopilotReviewIds(options, runtime);
@@ -417,7 +484,18 @@ export async function performCopilotReviewRequest(options, { env = process.env,
417
484
  }
418
485
  } catch {
419
486
  }
420
- 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
421
499
  && !before.requested
422
500
  && !before.hasPendingReviewOnCurrentHead) {
423
501
  if (!options.forceRerequestReview) {
@@ -434,9 +512,9 @@ export async function performCopilotReviewRequest(options, { env = process.env,
434
512
  repo: options.repo,
435
513
  pr: options.pr,
436
514
  reviewer: "Copilot",
437
- completedRounds: before.completedCopilotReviewRounds,
515
+ completedRounds,
438
516
  maxRounds,
439
- 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.`,
440
518
  };
441
519
  }
442
520
  }
@@ -454,8 +532,8 @@ export async function performCopilotReviewRequest(options, { env = process.env,
454
532
  repo: options.repo,
455
533
  pr: options.pr,
456
534
  reviewer: "Copilot",
457
- 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.`,
458
- 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,
459
537
  maxRounds,
460
538
  };
461
539
  }
@@ -467,7 +545,7 @@ export async function performCopilotReviewRequest(options, { env = process.env,
467
545
  pr: options.pr,
468
546
  reviewer: "Copilot",
469
547
  detail: "No changes since last Copilot review. --force-rerequest-review requires new commits on the PR head.",
470
- completedRounds: before.completedCopilotReviewRounds,
548
+ completedRounds,
471
549
  maxRounds,
472
550
  };
473
551
  }
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ import { parseArgs } from "node:util";
2
3
  import { buildParseError, formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
3
- import { parseIssueNumber, requireOptionValue, runChild } from "../_cli-primitives.mjs";
4
+ import { parseIssueNumber, requireTokenValue, runChild } from "../_cli-primitives.mjs";
4
5
  import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
5
6
  const ISSUE_JSON_FIELDS = "number,title,body,url,state";
6
7
  const USAGE = `Usage: resolve-tracker-local-spec.mjs (--repo <owner/name> --issue <number> | --issue-url <github-issue-url>)
@@ -60,32 +61,48 @@ export function parseGitHubIssueUrl(value) {
60
61
  };
61
62
  }
62
63
  export function parseResolveTrackerLocalSpecCliArgs(argv) {
63
- const args = [...argv];
64
+ const { tokens } = parseArgs({
65
+ args: [...argv],
66
+ options: {
67
+ help: { type: "boolean", short: "h" },
68
+ repo: { type: "string" },
69
+ issue: { type: "string" },
70
+ "issue-url": { type: "string" },
71
+ },
72
+ allowPositionals: true,
73
+ strict: false,
74
+ tokens: true,
75
+ });
64
76
  const options = {
65
77
  help: false,
66
78
  repo: undefined,
67
79
  issue: undefined,
68
80
  issueUrl: undefined,
69
81
  };
70
- while (args.length > 0) {
71
- const token = args.shift();
72
- if (token === "--help" || token === "-h") {
82
+ for (const token of tokens) {
83
+ if (token.kind === "positional") {
84
+ throw parseError(`Unknown argument: ${token.value}`);
85
+ }
86
+ if (token.kind !== "option") {
87
+ continue;
88
+ }
89
+ if (token.name === "help") {
73
90
  options.help = true;
74
91
  return options;
75
92
  }
76
- if (token === "--repo") {
77
- options.repo = requireOptionValue(args, "--repo", parseError).trim();
93
+ if (token.name === "repo") {
94
+ options.repo = requireTokenValue(token, parseError).trim();
78
95
  continue;
79
96
  }
80
- if (token === "--issue") {
81
- options.issue = parseIssueNumber(requireOptionValue(args, "--issue", parseError), parseError);
97
+ if (token.name === "issue") {
98
+ options.issue = parseIssueNumber(requireTokenValue(token, parseError), parseError);
82
99
  continue;
83
100
  }
84
- if (token === "--issue-url") {
85
- options.issueUrl = requireOptionValue(args, "--issue-url", parseError).trim();
101
+ if (token.name === "issue-url") {
102
+ options.issueUrl = requireTokenValue(token, parseError).trim();
86
103
  continue;
87
104
  }
88
- throw parseError(`Unknown argument: ${token}`);
105
+ throw parseError(`Unknown argument: ${token.rawName}`);
89
106
  }
90
107
  const usingIssueUrl = typeof options.issueUrl === "string";
91
108
  const usingRepoIssue = options.repo !== undefined || options.issue !== undefined;
@@ -3,7 +3,8 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { spawn } from "node:child_process";
5
5
  import { formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
6
- import { parsePositiveInteger, requireOptionValue } from "../_cli-primitives.mjs";
6
+ import { parseArgs } from "node:util";
7
+ import { parsePositiveInteger, requireTokenValue } from "../_cli-primitives.mjs";
7
8
  import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
8
9
  import { buildDraftReviewPayload } from "@dev-loops/core/loop/reviewer-loop-state";
9
10
  const HELP = `Usage: stage-reviewer-draft.mjs --repo <owner/name> --pr <number> --review-file <path> [--local-state-output <path>]
@@ -19,7 +20,19 @@ Exit codes:
19
20
  1 Error
20
21
  `;
21
22
  export function parseStageDraftCliArgs(argv) {
22
- const args = [...argv];
23
+ const { tokens } = parseArgs({
24
+ args: [...argv],
25
+ options: {
26
+ help: { type: "boolean", short: "h" },
27
+ repo: { type: "string" },
28
+ pr: { type: "string" },
29
+ "review-file": { type: "string" },
30
+ "local-state-output": { type: "string" },
31
+ },
32
+ allowPositionals: true,
33
+ strict: false,
34
+ tokens: true,
35
+ });
23
36
  const options = {
24
37
  repo: undefined,
25
38
  pr: undefined,
@@ -27,29 +40,34 @@ export function parseStageDraftCliArgs(argv) {
27
40
  localStateOutput: undefined,
28
41
  help: false,
29
42
  };
30
- while (args.length > 0) {
31
- const token = args.shift();
32
- if (token === "--help" || token === "-h") {
43
+ for (const token of tokens) {
44
+ if (token.kind === "positional") {
45
+ throw new Error(`Unknown argument: ${token.value}`);
46
+ }
47
+ if (token.kind !== "option") {
48
+ continue;
49
+ }
50
+ if (token.name === "help") {
33
51
  options.help = true;
34
52
  return options;
35
53
  }
36
- if (token === "--repo") {
37
- options.repo = requireOptionValue(args, "--repo").trim();
54
+ if (token.name === "repo") {
55
+ options.repo = requireTokenValue(token).trim();
38
56
  continue;
39
57
  }
40
- if (token === "--pr") {
41
- options.pr = parsePositiveInteger(requireOptionValue(args, "--pr"), "--pr");
58
+ if (token.name === "pr") {
59
+ options.pr = parsePositiveInteger(requireTokenValue(token), "--pr");
42
60
  continue;
43
61
  }
44
- if (token === "--review-file") {
45
- options.reviewFile = requireOptionValue(args, "--review-file");
62
+ if (token.name === "review-file") {
63
+ options.reviewFile = requireTokenValue(token);
46
64
  continue;
47
65
  }
48
- if (token === "--local-state-output") {
49
- options.localStateOutput = requireOptionValue(args, "--local-state-output");
66
+ if (token.name === "local-state-output") {
67
+ options.localStateOutput = requireTokenValue(token);
50
68
  continue;
51
69
  }
52
- throw new Error(`Unknown argument: ${token}`);
70
+ throw new Error(`Unknown argument: ${token.rawName}`);
53
71
  }
54
72
  if (!options.repo || !options.pr || !options.reviewFile) {
55
73
  throw new Error(