dev-loops 0.1.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 (156) hide show
  1. package/.pi/dev-loop/defaults.yaml +477 -0
  2. package/AGENTS.md +25 -0
  3. package/CHANGELOG.md +18 -0
  4. package/LICENSE +21 -0
  5. package/README.md +178 -0
  6. package/agents/dev-loop.agent.md +82 -0
  7. package/agents/developer.agent.md +37 -0
  8. package/agents/docs.agent.md +33 -0
  9. package/agents/fixer.agent.md +53 -0
  10. package/agents/quality.agent.md +28 -0
  11. package/agents/refiner.agent.md +87 -0
  12. package/agents/review.agent.md +64 -0
  13. package/cli/index.mjs +424 -0
  14. package/extension/README.md +233 -0
  15. package/extension/checks.ts +94 -0
  16. package/extension/index.ts +131 -0
  17. package/extension/post-merge-update.ts +512 -0
  18. package/extension/presentation.ts +107 -0
  19. package/lib/dev-loops-core.mjs +284 -0
  20. package/package.json +103 -0
  21. package/scripts/README.md +1007 -0
  22. package/scripts/_cli-primitives.mjs +10 -0
  23. package/scripts/_core-helpers.mjs +30 -0
  24. package/scripts/docs/validate-links.mjs +567 -0
  25. package/scripts/docs/validate-no-duplicate-rules.mjs +250 -0
  26. package/scripts/github/_review-thread-mutations.mjs +214 -0
  27. package/scripts/github/capture-review-threads.mjs +180 -0
  28. package/scripts/github/create-draft-pr.mjs +108 -0
  29. package/scripts/github/detect-checkpoint-evidence.mjs +393 -0
  30. package/scripts/github/detect-linked-issue-pr.mjs +331 -0
  31. package/scripts/github/manage-sub-issues.mjs +394 -0
  32. package/scripts/github/probe-copilot-review.mjs +323 -0
  33. package/scripts/github/ready-for-review.mjs +93 -0
  34. package/scripts/github/reconcile-draft-gate.mjs +328 -0
  35. package/scripts/github/reply-resolve-review-thread.mjs +42 -0
  36. package/scripts/github/reply-resolve-review-threads.mjs +329 -0
  37. package/scripts/github/request-copilot-review.mjs +551 -0
  38. package/scripts/github/resolve-tracker-local-spec.mjs +205 -0
  39. package/scripts/github/stage-reviewer-draft.mjs +191 -0
  40. package/scripts/github/upsert-checkpoint-verdict.mjs +694 -0
  41. package/scripts/github/verify-fresh-review-context.mjs +125 -0
  42. package/scripts/github/write-gate-findings-log.mjs +212 -0
  43. package/scripts/loop/_checkpoint-io.mjs +55 -0
  44. package/scripts/loop/_checkpoint-paths.mjs +28 -0
  45. package/scripts/loop/_handoff-contract.mjs +230 -0
  46. package/scripts/loop/_inspect-run-viewer-adapter.mjs +345 -0
  47. package/scripts/loop/_loop-evidence.mjs +32 -0
  48. package/scripts/loop/_pr-runner-coordination.mjs +611 -0
  49. package/scripts/loop/_stale-runner-detection.mjs +145 -0
  50. package/scripts/loop/_steering-state-file.mjs +134 -0
  51. package/scripts/loop/build-handoff-envelope.mjs +181 -0
  52. package/scripts/loop/checkpoint-contract.mjs +49 -0
  53. package/scripts/loop/conductor-monitor.mjs +1850 -0
  54. package/scripts/loop/conductor.mjs +214 -0
  55. package/scripts/loop/copilot-pr-handoff.mjs +493 -0
  56. package/scripts/loop/debt-remediate.mjs +304 -0
  57. package/scripts/loop/detect-change-scope.mjs +102 -0
  58. package/scripts/loop/detect-copilot-loop-state.mjs +454 -0
  59. package/scripts/loop/detect-copilot-session-activity.mjs +186 -0
  60. package/scripts/loop/detect-initial-copilot-pr-state.mjs +318 -0
  61. package/scripts/loop/detect-internal-only-pr.mjs +270 -0
  62. package/scripts/loop/detect-issue-refinement-artifact.mjs +163 -0
  63. package/scripts/loop/detect-pr-gate-coordination-state.mjs +509 -0
  64. package/scripts/loop/detect-reviewer-loop-state.mjs +231 -0
  65. package/scripts/loop/detect-stale-runner.mjs +250 -0
  66. package/scripts/loop/detect-tracker-first-loop-state.mjs +76 -0
  67. package/scripts/loop/detect-tracker-pr-state.mjs +102 -0
  68. package/scripts/loop/info.mjs +267 -0
  69. package/scripts/loop/inspect-run-viewer/cli.mjs +117 -0
  70. package/scripts/loop/inspect-run-viewer/constants.mjs +80 -0
  71. package/scripts/loop/inspect-run-viewer/graph.mjs +757 -0
  72. package/scripts/loop/inspect-run-viewer/handoff-envelope-renderer.mjs +398 -0
  73. package/scripts/loop/inspect-run-viewer/inbox.mjs +308 -0
  74. package/scripts/loop/inspect-run-viewer/managed-instance.mjs +750 -0
  75. package/scripts/loop/inspect-run-viewer/rendering.mjs +411 -0
  76. package/scripts/loop/inspect-run-viewer/server.mjs +638 -0
  77. package/scripts/loop/inspect-run-viewer/shared.mjs +103 -0
  78. package/scripts/loop/inspect-run-viewer/status.mjs +715 -0
  79. package/scripts/loop/inspect-run-viewer-ci-changes.mjs +77 -0
  80. package/scripts/loop/inspect-run-viewer.mjs +82 -0
  81. package/scripts/loop/inspect-run.mjs +382 -0
  82. package/scripts/loop/outer-loop.mjs +419 -0
  83. package/scripts/loop/pr-runner-coordination.mjs +143 -0
  84. package/scripts/loop/pre-commit-branch-guard.mjs +68 -0
  85. package/scripts/loop/pre-flight-gate.mjs +236 -0
  86. package/scripts/loop/pre-pr-ready-gate.mjs +183 -0
  87. package/scripts/loop/pre-push-main-guard.mjs +103 -0
  88. package/scripts/loop/pre-write-remote-freshness-guard.mjs +32 -0
  89. package/scripts/loop/print-gates.mjs +42 -0
  90. package/scripts/loop/resolve-dev-loop-startup.mjs +533 -0
  91. package/scripts/loop/run-conductor-cycle.mjs +322 -0
  92. package/scripts/loop/run-queue.mjs +124 -0
  93. package/scripts/loop/run-refinement-audit.mjs +513 -0
  94. package/scripts/loop/run-watch-cycle.mjs +358 -0
  95. package/scripts/loop/steer-loop.mjs +841 -0
  96. package/scripts/loop/ui-designer-review-contract.mjs +76 -0
  97. package/scripts/loop/watch-initial-copilot-pr.mjs +253 -0
  98. package/scripts/projects/add-queue-item.mjs +528 -0
  99. package/scripts/projects/ensure-queue-board.mjs +837 -0
  100. package/scripts/projects/list-queue-items.mjs +489 -0
  101. package/scripts/projects/move-queue-item.mjs +549 -0
  102. package/scripts/projects/reorder-queue-item.mjs +518 -0
  103. package/scripts/refine/_refine-helpers.mjs +258 -0
  104. package/scripts/refine/prose-linkage-detector.mjs +92 -0
  105. package/scripts/refine/refinement-completeness-checker.mjs +88 -0
  106. package/scripts/refine/scope-boundary-cross-checker.mjs +163 -0
  107. package/scripts/refine/tree-integrity-validator.mjs +211 -0
  108. package/scripts/refine/verify.mjs +178 -0
  109. package/scripts/repo-wiki-local.mjs +156 -0
  110. package/scripts/repo-wiki.mjs +119 -0
  111. package/skills/copilot-pr-followup/SKILL.md +380 -0
  112. package/skills/dev-loop/SKILL.md +141 -0
  113. package/skills/dev-loop/scripts/dev-mode-context.mjs +152 -0
  114. package/skills/dev-loop/scripts/dev-mode-context.test.mjs +80 -0
  115. package/skills/dev-loop/scripts/init-phase.mjs +71 -0
  116. package/skills/dev-loop/scripts/log-bash-exit-1.mjs +25 -0
  117. package/skills/dev-loop/scripts/phase-files.mjs +29 -0
  118. package/skills/dev-loop/scripts/post-gate-verdict-fallback.mjs +480 -0
  119. package/skills/dev-loop/scripts/post-gate-verdict-fallback.test.mjs +732 -0
  120. package/skills/dev-loop/scripts/render-template.mjs +82 -0
  121. package/skills/dev-loop/scripts/render-template.test.mjs +63 -0
  122. package/skills/dev-loop/templates/bootstrap-agents.md +26 -0
  123. package/skills/dev-loop/templates/bootstrap-implementation-state.md +31 -0
  124. package/skills/dev-loop/templates/bootstrap-implementation-workflow.md +17 -0
  125. package/skills/dev-loop/templates/dev-mode-retrospective.md +15 -0
  126. package/skills/dev-loop/templates/dev-mode-review.md +17 -0
  127. package/skills/dev-loop/templates/dev-mode-skill-changes.md +11 -0
  128. package/skills/dev-loop/templates/merged-phase-plan.md +19 -0
  129. package/skills/dev-loop/templates/phase-doc.md +27 -0
  130. package/skills/dev-loop/templates/phase-summary.md +13 -0
  131. package/skills/dev-loop/templates/phase-variant.md +15 -0
  132. package/skills/dev-loop/templates/retrospective.md +11 -0
  133. package/skills/dev-loop/templates/review.md +32 -0
  134. package/skills/dev-loop/templates/ui-vision-review.md +55 -0
  135. package/skills/docs/acceptance-criteria-verification.md +21 -0
  136. package/skills/docs/anti-patterns.md +21 -0
  137. package/skills/docs/artifact-authority-contract.md +119 -0
  138. package/skills/docs/confirmation-rules.md +28 -0
  139. package/skills/docs/copilot-ci-status-contract.md +52 -0
  140. package/skills/docs/copilot-loop-operations.md +233 -0
  141. package/skills/docs/debt-remediation-contract.md +107 -0
  142. package/skills/docs/entrypoint-strategies.md +115 -0
  143. package/skills/docs/epic-tree-refinement-procedure.md +234 -0
  144. package/skills/docs/issue-intake-procedure.md +235 -0
  145. package/skills/docs/main-agent-contract.md +72 -0
  146. package/skills/docs/merge-preconditions.md +29 -0
  147. package/skills/docs/pr-lifecycle-contract.md +209 -0
  148. package/skills/docs/public-dev-loop-contract.md +497 -0
  149. package/skills/docs/retrospective-checkpoint-contract.md +159 -0
  150. package/skills/docs/stop-conditions.md +29 -0
  151. package/skills/docs/structural-quality.md +42 -0
  152. package/skills/docs/tracker-first-loop-state.md +281 -0
  153. package/skills/docs/validation-policy.md +27 -0
  154. package/skills/docs/workflow-handoff-contract.md +135 -0
  155. package/skills/final-approval/SKILL.md +19 -0
  156. package/skills/local-implementation/SKILL.md +640 -0
@@ -0,0 +1,345 @@
1
+ import { parseRepoSlugParts } from "@dev-loops/core/github/repo-slug";
2
+ import { inspectRun } from "./inspect-run.mjs";
3
+ import { runChild } from "../_cli-primitives.mjs";
4
+ const ASSIGNED_PR_LIST_CACHE_TTL_MS = 15_000;
5
+ const DEFAULT_UPDATED_WITHIN_DAYS = 7;
6
+ const DEFAULT_RESULT_LIMIT = 25;
7
+ const MAX_RESULT_LIMIT = 100;
8
+ const DEFAULT_PR_STATE = "open";
9
+ const DEFAULT_INBOX_MODE = "assignee";
10
+ const DEFAULT_INBOX_SIGNAL = "waiting";
11
+ function malformedTargetError(message) {
12
+ const error = new Error(message);
13
+ error.code = "MALFORMED_TARGET";
14
+ return error;
15
+ }
16
+ export function parseGhJsonOutput(stdout) {
17
+ try {
18
+ return JSON.parse(stdout);
19
+ } catch {
20
+ throw new Error(`Invalid JSON from gh: ${stdout.trim() || "<empty>"}`);
21
+ }
22
+ }
23
+ function parsePositivePr(value) {
24
+ if (typeof value === "number" && Number.isInteger(value) && value > 0) {
25
+ return value;
26
+ }
27
+ if (typeof value === "string" && /^\d+$/.test(value) && Number(value) > 0) {
28
+ return Number(value);
29
+ }
30
+ throw malformedTargetError("target.pr must be a positive integer");
31
+ }
32
+ function parseUpdatedWithinDays(value) {
33
+ if (value === undefined || value === "") {
34
+ return DEFAULT_UPDATED_WITHIN_DAYS;
35
+ }
36
+ if (value === null || value === "all") {
37
+ return null;
38
+ }
39
+ if (typeof value === "number" && Number.isInteger(value) && value > 0) {
40
+ return value;
41
+ }
42
+ if (typeof value === "string" && /^\d+$/.test(value) && Number(value) > 0) {
43
+ return Number(value);
44
+ }
45
+ throw malformedTargetError("updatedWithinDays must be a positive integer or 'all'");
46
+ }
47
+ function parseResultLimit(value) {
48
+ if (value === undefined || value === null || value === "") {
49
+ return DEFAULT_RESULT_LIMIT;
50
+ }
51
+ if (typeof value === "number" && Number.isInteger(value) && value > 0) {
52
+ return Math.min(value, MAX_RESULT_LIMIT);
53
+ }
54
+ if (typeof value === "string" && /^\d+$/.test(value) && Number(value) > 0) {
55
+ return Math.min(Number(value), MAX_RESULT_LIMIT);
56
+ }
57
+ throw malformedTargetError(`limit must be a positive integer <= ${MAX_RESULT_LIMIT}`);
58
+ }
59
+ function parsePrState(value) {
60
+ if (value === undefined || value === null || value === "") {
61
+ return DEFAULT_PR_STATE;
62
+ }
63
+ const normalized = String(value).trim().toLowerCase();
64
+ if (normalized === "open" || normalized === "closed" || normalized === "all") {
65
+ return normalized;
66
+ }
67
+ throw malformedTargetError("state must be one of: open, closed, all");
68
+ }
69
+ function parseInboxMode(value) {
70
+ if (value === undefined || value === null || value === "") {
71
+ return DEFAULT_INBOX_MODE;
72
+ }
73
+ const normalized = String(value).trim().toLowerCase();
74
+ if (normalized === "assignee" || normalized === "reviewer" || normalized === "involved") {
75
+ return normalized;
76
+ }
77
+ throw malformedTargetError("mode must be one of: assignee, reviewer, involved");
78
+ }
79
+ function formatUtcDateDaysAgo(daysAgo, nowMs) {
80
+ const date = new Date(nowMs - (daysAgo * 24 * 60 * 60 * 1000));
81
+ const year = date.getUTCFullYear();
82
+ const month = String(date.getUTCMonth() + 1).padStart(2, "0");
83
+ const day = String(date.getUTCDate()).padStart(2, "0");
84
+ return `${year}-${month}-${day}`;
85
+ }
86
+ function buildPrSearchArgs({
87
+ repoSlug,
88
+ mode,
89
+ state,
90
+ updatedWithinDays,
91
+ limit,
92
+ jsonFields,
93
+ review,
94
+ checks,
95
+ nowMs = Date.now(),
96
+ } = {}) {
97
+ const ghArgs = [
98
+ "search",
99
+ "prs",
100
+ ];
101
+ if (mode === "assignee") {
102
+ ghArgs.push("--assignee", "@me");
103
+ } else if (mode === "reviewer") {
104
+ ghArgs.push("--review-requested", "@me");
105
+ } else {
106
+ ghArgs.push("--involves", "@me");
107
+ }
108
+ if (typeof repoSlug === "string" && repoSlug.length > 0) {
109
+ ghArgs.push("--repo", repoSlug);
110
+ }
111
+ if (state !== "all") {
112
+ ghArgs.push("--state", state);
113
+ }
114
+ if (typeof review === "string" && review.length > 0) {
115
+ ghArgs.push("--review", review);
116
+ }
117
+ if (typeof checks === "string" && checks.length > 0) {
118
+ ghArgs.push("--checks", checks);
119
+ }
120
+ ghArgs.push(
121
+ "--sort",
122
+ "updated",
123
+ "--order",
124
+ "desc",
125
+ );
126
+ if (updatedWithinDays !== null) {
127
+ ghArgs.push("--updated", `>=${formatUtcDateDaysAgo(updatedWithinDays, nowMs)}`);
128
+ }
129
+ ghArgs.push(
130
+ "--limit",
131
+ String(limit),
132
+ "--json",
133
+ jsonFields.join(","),
134
+ );
135
+ return ghArgs;
136
+ }
137
+ function renderSearchEntryKey(repo, pr) {
138
+ return `${String(repo).toLowerCase()}#${String(pr)}`;
139
+ }
140
+ function createEntryKeySet(payload, toRepoSlugImpl) {
141
+ if (!Array.isArray(payload)) {
142
+ return new Set();
143
+ }
144
+ const keys = new Set();
145
+ for (const item of payload) {
146
+ const repo = toRepoSlugImpl(item?.repository);
147
+ if (repo === null) {
148
+ continue;
149
+ }
150
+ try {
151
+ const normalizedTarget = normalizeInspectionTarget({ repo, pr: item?.number });
152
+ keys.add(renderSearchEntryKey(normalizedTarget.repo, normalizedTarget.pr));
153
+ } catch {
154
+ continue;
155
+ }
156
+ }
157
+ return keys;
158
+ }
159
+ function normalizeSearchState(value) {
160
+ const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
161
+ if (normalized === "open" || normalized === "closed" || normalized === "merged") {
162
+ return normalized;
163
+ }
164
+ return DEFAULT_PR_STATE;
165
+ }
166
+ function deriveInboxSignal({ state, isDraft, attentionKeys, pendingKeys, readyKeys, entryKey }) {
167
+ if (state === "closed" || state === "merged") {
168
+ return "closed";
169
+ }
170
+ if (attentionKeys.has(entryKey)) {
171
+ return "attention";
172
+ }
173
+ if (pendingKeys.has(entryKey) || isDraft) {
174
+ return "pending";
175
+ }
176
+ if (readyKeys.has(entryKey)) {
177
+ return "ready";
178
+ }
179
+ return DEFAULT_INBOX_SIGNAL;
180
+ }
181
+ export function normalizeInspectionTarget(target) {
182
+ if (target === null || typeof target !== "object") {
183
+ throw malformedTargetError("target must be an object with repo and pr");
184
+ }
185
+ const rawRepo = typeof target.repo === "string" ? target.repo.trim() : "";
186
+ if (rawRepo.length === 0) {
187
+ throw malformedTargetError("target.repo is required");
188
+ }
189
+ try {
190
+ parseRepoSlugParts(rawRepo, { errorMessage: "target.repo must match <owner/name>" });
191
+ } catch (error) {
192
+ throw malformedTargetError(error instanceof Error ? error.message : String(error));
193
+ }
194
+ return {
195
+ repo: rawRepo,
196
+ pr: parsePositivePr(target.pr),
197
+ };
198
+ }
199
+ export function createInspectionViewerAdapter({ inspectRunImpl = inspectRun, runGhJsonImpl = null, nowImpl = () => Date.now() } = {}) {
200
+ const runGhJson = async (args, { env = process.env, ghCommand = "gh" } = {}) => {
201
+ if (typeof runGhJsonImpl === "function") {
202
+ return runGhJsonImpl(args, { env, ghCommand });
203
+ }
204
+ const result = await runChild(ghCommand, args, env);
205
+ if (result.code !== 0) {
206
+ throw new Error(`Command failed: ${ghCommand} ${args.join(" ")}\n${result.stderr.trim() || "(no stderr output)"}`);
207
+ }
208
+ return parseGhJsonOutput(result.stdout);
209
+ };
210
+ const toRepoSlug = (repository) => {
211
+ if (repository === null || typeof repository !== "object") {
212
+ return null;
213
+ }
214
+ if (typeof repository.nameWithOwner === "string" && repository.nameWithOwner.trim().length > 0) {
215
+ return repository.nameWithOwner.trim();
216
+ }
217
+ const ownerLogin = typeof repository.owner?.login === "string" ? repository.owner.login.trim() : "";
218
+ const repoName = typeof repository.name === "string" ? repository.name.trim() : "";
219
+ if (ownerLogin.length === 0 || repoName.length === 0) {
220
+ return null;
221
+ }
222
+ return `${ownerLogin}/${repoName}`;
223
+ };
224
+ const assignedPrListCache = new Map();
225
+ return {
226
+ async loadSnapshot(target, options = {}) {
227
+ const normalizedTarget = normalizeInspectionTarget(target);
228
+ return inspectRunImpl({ ...options, ...normalizedTarget });
229
+ },
230
+ async listAssignedPullRequests(options = {}) {
231
+ const {
232
+ repo,
233
+ limit = DEFAULT_RESULT_LIMIT,
234
+ updatedWithinDays = DEFAULT_UPDATED_WITHIN_DAYS,
235
+ state = DEFAULT_PR_STATE,
236
+ mode = DEFAULT_INBOX_MODE,
237
+ env = process.env,
238
+ ghCommand = "gh",
239
+ } = options;
240
+ const repoSlug = typeof repo === "string" ? repo.trim() : "";
241
+ if (repoSlug.length > 0) {
242
+ try {
243
+ parseRepoSlugParts(repoSlug, { errorMessage: "repo must match <owner/name>" });
244
+ } catch (error) {
245
+ throw malformedTargetError(error instanceof Error ? error.message : String(error));
246
+ }
247
+ }
248
+ const normalizedLimit = parseResultLimit(limit);
249
+ const normalizedUpdatedWithinDays = parseUpdatedWithinDays(updatedWithinDays);
250
+ const normalizedState = parsePrState(state);
251
+ const normalizedMode = parseInboxMode(mode);
252
+ const nowMs = nowImpl();
253
+ for (const [key, entry] of assignedPrListCache.entries()) {
254
+ if ((nowMs - entry.cachedAt) > ASSIGNED_PR_LIST_CACHE_TTL_MS) {
255
+ assignedPrListCache.delete(key);
256
+ }
257
+ }
258
+ const cacheKey = `${ghCommand}::${repoSlug.length > 0 ? repoSlug.toLowerCase() : "all-repos"}::${normalizedMode}::${normalizedState}::${normalizedLimit}::${normalizedUpdatedWithinDays ?? "all"}`;
259
+ const cached = assignedPrListCache.get(cacheKey);
260
+ if (cached && (nowMs - cached.cachedAt) <= ASSIGNED_PR_LIST_CACHE_TTL_MS) {
261
+ return cached.payload.map((entry) => ({
262
+ target: { ...entry.target },
263
+ title: entry.title,
264
+ updatedAt: entry.updatedAt,
265
+ signal: entry.signal ?? DEFAULT_INBOX_SIGNAL,
266
+ }));
267
+ }
268
+ const baseQueryArgs = buildPrSearchArgs({
269
+ repoSlug,
270
+ mode: normalizedMode,
271
+ state: normalizedState,
272
+ updatedWithinDays: normalizedUpdatedWithinDays === null ? null : normalizedUpdatedWithinDays,
273
+ limit: normalizedLimit,
274
+ jsonFields: ["number", "title", "repository", "updatedAt", "state", "isDraft"],
275
+ nowMs,
276
+ });
277
+ const queryArgsFor = (overrides = {}) => buildPrSearchArgs({
278
+ repoSlug,
279
+ mode: normalizedMode,
280
+ state: normalizedState,
281
+ updatedWithinDays: normalizedUpdatedWithinDays === null ? null : normalizedUpdatedWithinDays,
282
+ limit: normalizedLimit,
283
+ jsonFields: ["number", "repository"],
284
+ nowMs,
285
+ ...overrides,
286
+ });
287
+ const [payload, changesRequestedPayload, failingChecksPayload, pendingChecksPayload, approvedPayload] = await Promise.all([
288
+ runGhJson(baseQueryArgs, { env, ghCommand }),
289
+ runGhJson(queryArgsFor({ review: "changes_requested" }), { env, ghCommand }),
290
+ runGhJson(queryArgsFor({ checks: "failure" }), { env, ghCommand }),
291
+ runGhJson(queryArgsFor({ checks: "pending" }), { env, ghCommand }),
292
+ runGhJson(queryArgsFor({ review: "approved" }), { env, ghCommand }),
293
+ ]);
294
+ if (!Array.isArray(payload)) {
295
+ return [];
296
+ }
297
+ const attentionKeys = new Set([
298
+ ...createEntryKeySet(changesRequestedPayload, toRepoSlug),
299
+ ...createEntryKeySet(failingChecksPayload, toRepoSlug),
300
+ ]);
301
+ const pendingKeys = createEntryKeySet(pendingChecksPayload, toRepoSlug);
302
+ const readyKeys = createEntryKeySet(approvedPayload, toRepoSlug);
303
+ const normalized = [];
304
+ for (const item of payload) {
305
+ const itemRepo = toRepoSlug(item?.repository);
306
+ if (itemRepo === null) {
307
+ continue;
308
+ }
309
+ try {
310
+ const target = normalizeInspectionTarget({ repo: itemRepo, pr: item?.number });
311
+ const entryKey = renderSearchEntryKey(target.repo, target.pr);
312
+ normalized.push({
313
+ target,
314
+ title: typeof item?.title === "string" && item.title.trim().length > 0
315
+ ? item.title.trim()
316
+ : null,
317
+ updatedAt: typeof item?.updatedAt === "string" && item.updatedAt.trim().length > 0
318
+ ? item.updatedAt.trim()
319
+ : null,
320
+ signal: deriveInboxSignal({
321
+ state: normalizeSearchState(item?.state),
322
+ isDraft: item?.isDraft === true,
323
+ attentionKeys,
324
+ pendingKeys,
325
+ readyKeys,
326
+ entryKey,
327
+ }),
328
+ });
329
+ } catch {
330
+ continue;
331
+ }
332
+ }
333
+ assignedPrListCache.set(cacheKey, {
334
+ cachedAt: nowMs,
335
+ payload: normalized.map((entry) => ({
336
+ target: { ...entry.target },
337
+ title: entry.title,
338
+ updatedAt: entry.updatedAt,
339
+ signal: entry.signal ?? DEFAULT_INBOX_SIGNAL,
340
+ })),
341
+ });
342
+ return normalized;
343
+ },
344
+ };
345
+ }
@@ -0,0 +1,32 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { parseJsonText } from "../_core-helpers.mjs";
3
+ import { autoDetectSnapshot as autoDetectCopilotSnapshot } from "./detect-copilot-loop-state.mjs";
4
+ import { autoDetectReviewerSnapshot } from "./detect-reviewer-loop-state.mjs";
5
+ import {
6
+ interpretLoopState,
7
+ normalizeSnapshot as normalizeCopilotSnapshot,
8
+ } from "@dev-loops/core/loop/copilot-loop-state";
9
+ import {
10
+ interpretReviewerLoopState,
11
+ normalizeReviewerSnapshot,
12
+ } from "@dev-loops/core/loop/reviewer-loop-state";
13
+ export async function loadCopilotEvidence({ repo, pr, copilotInputPath }, { env = process.env, ghCommand = "gh" } = {}) {
14
+ let snapshot;
15
+ if (copilotInputPath !== undefined) {
16
+ const text = await readFile(copilotInputPath, "utf8");
17
+ snapshot = normalizeCopilotSnapshot(parseJsonText(text));
18
+ } else {
19
+ snapshot = await autoDetectCopilotSnapshot({ repo, pr }, { env, ghCommand });
20
+ }
21
+ return { snapshot, interpretation: interpretLoopState(snapshot) };
22
+ }
23
+ export async function loadReviewerEvidence({ repo, pr, reviewerLogin, reviewerInputPath }, { env = process.env, ghCommand = "gh" } = {}) {
24
+ let snapshot;
25
+ if (reviewerInputPath !== undefined) {
26
+ const text = await readFile(reviewerInputPath, "utf8");
27
+ snapshot = normalizeReviewerSnapshot(parseJsonText(text));
28
+ } else {
29
+ snapshot = await autoDetectReviewerSnapshot({ repo, pr, reviewerLogin }, { env, ghCommand });
30
+ }
31
+ return { snapshot, interpretation: interpretReviewerLoopState(snapshot) };
32
+ }