@dev-loops/core 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 (54) hide show
  1. package/bin/capture-deep-persona-signals.mjs +143 -0
  2. package/bin/ensure-phase-files.mjs +7 -0
  3. package/bin/log-bash-exit-1.mjs +7 -0
  4. package/bin/parse-review-threads.mjs +7 -0
  5. package/package.json +78 -0
  6. package/src/analysis/change-classifier.mjs +146 -0
  7. package/src/analysis/diff-analyzer.mjs +285 -0
  8. package/src/bash-exit-one.mjs +130 -0
  9. package/src/cli/helpers.mjs +22 -0
  10. package/src/cli/primitives.mjs +70 -0
  11. package/src/cli/retry-wrapper.mjs +169 -0
  12. package/src/cli/subcommand-runner.mjs +246 -0
  13. package/src/config/config.mjs +965 -0
  14. package/src/debt/cluster.mjs +240 -0
  15. package/src/debt/debt-finding.mjs +68 -0
  16. package/src/debt/debt-signal.mjs +46 -0
  17. package/src/debt/deep-persona-signals.mjs +266 -0
  18. package/src/debt/remediation-to-issue.mjs +121 -0
  19. package/src/debt/score.mjs +127 -0
  20. package/src/debt/shape.mjs +214 -0
  21. package/src/github/copilot-helpers.mjs +343 -0
  22. package/src/github/repo-slug.mjs +105 -0
  23. package/src/github/review-threads.mjs +343 -0
  24. package/src/harness/adapter.mjs +57 -0
  25. package/src/harness/index.mjs +3 -0
  26. package/src/harness/noop-adapter.mjs +22 -0
  27. package/src/harness/pi-adapter.mjs +47 -0
  28. package/src/loop/async-start-contract.mjs +170 -0
  29. package/src/loop/conductor-routing.mjs +817 -0
  30. package/src/loop/copilot-ci-status.mjs +255 -0
  31. package/src/loop/copilot-loop-iterations.mjs +161 -0
  32. package/src/loop/copilot-loop-state.mjs +510 -0
  33. package/src/loop/handoff-envelope.mjs +800 -0
  34. package/src/loop/issue-refinement-artifact.mjs +268 -0
  35. package/src/loop/lifecycle-state.mjs +342 -0
  36. package/src/loop/phase-files.mjs +187 -0
  37. package/src/loop/policy-constants.mjs +17 -0
  38. package/src/loop/pr-gate-coordination.mjs +1278 -0
  39. package/src/loop/public-dev-loop-routing-contract.mjs +277 -0
  40. package/src/loop/public-dev-loop-routing.mjs +1746 -0
  41. package/src/loop/queue-board-ordering.mjs +38 -0
  42. package/src/loop/queue-board-sync.mjs +223 -0
  43. package/src/loop/queue-driver.mjs +164 -0
  44. package/src/loop/queue-parallel.mjs +190 -0
  45. package/src/loop/queue-state.mjs +230 -0
  46. package/src/loop/retrospective-checkpoint.mjs +178 -0
  47. package/src/loop/reviewer-loop-state.mjs +456 -0
  48. package/src/loop/run-inspection.mjs +604 -0
  49. package/src/loop/steering.mjs +793 -0
  50. package/src/loop/timeout-policy.mjs +73 -0
  51. package/src/loop/tracker-first-loop-state.mjs +87 -0
  52. package/src/loop/tracker-pr-state.mjs +301 -0
  53. package/src/loop/worktree-guard.mjs +141 -0
  54. package/src/refinement/ac-dod-matrix.mjs +95 -0
@@ -0,0 +1,255 @@
1
+ const VALID_HEAD_SCOPED_CI_STATUSES = new Set(["success", "failure", "pending", "none"]);
2
+ const FAILURE_CONCLUSIONS = new Set(["FAILURE", "ACTION_REQUIRED", "TIMED_OUT", "STARTUP_FAILURE"]);
3
+ const SUCCESS_CONCLUSIONS = new Set(["SUCCESS", "NEUTRAL", "SKIPPED"]);
4
+ const STATUS_CONTEXT_FAILURE_STATES = new Set(["FAILURE", "ERROR"]);
5
+ const STATUS_CONTEXT_PENDING_STATES = new Set(["PENDING", "EXPECTED"]);
6
+ const STATUS_CONTEXT_SUCCESS_STATES = new Set(["SUCCESS"]);
7
+
8
+ function normalizeHeadScopedCiStatus(status) {
9
+ return VALID_HEAD_SCOPED_CI_STATUSES.has(status) ? status : "none";
10
+ }
11
+
12
+ function buildCiContract(overallStatus) {
13
+ const isWaiting = overallStatus === "pending" || overallStatus === "none";
14
+
15
+ return {
16
+ overallStatus,
17
+ rollup: {
18
+ success: overallStatus === "success",
19
+ pending: overallStatus === "pending",
20
+ failure: overallStatus === "failure",
21
+ none: overallStatus === "none",
22
+ },
23
+ semantics: {
24
+ wait: isWaiting,
25
+ blocked: overallStatus === "failure",
26
+ timeoutDisposition: isWaiting ? "remain_waiting" : "not_applicable",
27
+ },
28
+ };
29
+ }
30
+
31
+ /**
32
+ * Normalize the PR `statusCheckRollup` payload into a stable status.
33
+ * Supports both CheckRun-style entries (`status` + `conclusion`) and legacy
34
+ * StatusContext-style entries (`state`).
35
+ *
36
+ * @param {Array<object>} rollup
37
+ * @returns {"success"|"failure"|"pending"|"none"}
38
+ */
39
+ export function normalizeStatusCheckRollupStatus(rollup) {
40
+ if (!Array.isArray(rollup) || rollup.length === 0) {
41
+ return "none";
42
+ }
43
+
44
+ let hasPending = false;
45
+ let hasFailure = false;
46
+ let hasSuccess = false;
47
+ let hasUnsupportedCompleted = false;
48
+
49
+ for (const check of rollup) {
50
+ const state = typeof check?.state === "string" ? check.state.toUpperCase() : "";
51
+ if (STATUS_CONTEXT_FAILURE_STATES.has(state)) {
52
+ hasFailure = true;
53
+ continue;
54
+ }
55
+ if (STATUS_CONTEXT_PENDING_STATES.has(state)) {
56
+ hasPending = true;
57
+ continue;
58
+ }
59
+ if (STATUS_CONTEXT_SUCCESS_STATES.has(state)) {
60
+ hasSuccess = true;
61
+ continue;
62
+ }
63
+
64
+ const status = typeof check?.status === "string" ? check.status.toUpperCase() : "";
65
+ const conclusion = typeof check?.conclusion === "string" ? check.conclusion.toUpperCase() : "";
66
+
67
+ if (status === "COMPLETED" && FAILURE_CONCLUSIONS.has(conclusion)) {
68
+ hasFailure = true;
69
+ continue;
70
+ }
71
+
72
+ if (status !== "COMPLETED") {
73
+ hasPending = true;
74
+ continue;
75
+ }
76
+
77
+ if (SUCCESS_CONCLUSIONS.has(conclusion)) {
78
+ hasSuccess = true;
79
+ continue;
80
+ }
81
+
82
+ hasUnsupportedCompleted = true;
83
+ }
84
+
85
+ if (hasFailure) return "failure";
86
+ if (hasPending) return "pending";
87
+ if (hasUnsupportedCompleted) return "none";
88
+ if (hasSuccess) return "success";
89
+ return "none";
90
+ }
91
+
92
+ /**
93
+ * Summarize the GitHub check-runs API payload for one head SHA.
94
+ *
95
+ * @param {object} payload
96
+ * @returns {{ status: "success"|"failure"|"pending"|"none", unsupportedCompleted: boolean, failureDetails?: Array<string> }}
97
+ */
98
+ export function summarizeHeadScopedCheckRunsSignal(payload) {
99
+ const runs = Array.isArray(payload?.check_runs) ? payload.check_runs : [];
100
+ if (runs.length === 0) {
101
+ return { status: "none", unsupportedCompleted: false };
102
+ }
103
+
104
+ let hasPending = false;
105
+ let hasFailure = false;
106
+ let hasSuccess = false;
107
+ let hasUnsupportedCompleted = false;
108
+ const failureDetails = [];
109
+
110
+ for (const run of runs) {
111
+ const status = typeof run?.status === "string" ? run.status.toUpperCase() : "";
112
+ const conclusion = typeof run?.conclusion === "string" ? run.conclusion.toUpperCase() : "";
113
+
114
+ if (status !== "COMPLETED") {
115
+ hasPending = true;
116
+ continue;
117
+ }
118
+
119
+ if (FAILURE_CONCLUSIONS.has(conclusion)) {
120
+ hasFailure = true;
121
+ const name = typeof run?.name === "string" ? run.name : "";
122
+ if (name) failureDetails.push(name);
123
+ continue;
124
+ }
125
+
126
+ if (SUCCESS_CONCLUSIONS.has(conclusion)) {
127
+ hasSuccess = true;
128
+ continue;
129
+ }
130
+
131
+ hasUnsupportedCompleted = true;
132
+ }
133
+
134
+ if (hasFailure) return { status: "failure", unsupportedCompleted: hasUnsupportedCompleted, failureDetails };
135
+ if (hasPending) return { status: "pending", unsupportedCompleted: hasUnsupportedCompleted, failureDetails: failureDetails.length > 0 ? failureDetails : undefined };
136
+ if (hasUnsupportedCompleted) return { status: "none", unsupportedCompleted: true, failureDetails: failureDetails.length > 0 ? failureDetails : undefined };
137
+ if (hasSuccess) return { status: "success", unsupportedCompleted: false, failureDetails: failureDetails.length > 0 ? failureDetails : undefined };
138
+ return { status: "none", unsupportedCompleted: false, failureDetails: failureDetails.length > 0 ? failureDetails : undefined };
139
+ }
140
+
141
+ /**
142
+ * Normalize the GitHub check-runs API payload for one head SHA into a stable status.
143
+ *
144
+ * @param {object} payload
145
+ * @returns {"success"|"failure"|"pending"|"none"}
146
+ */
147
+ export function normalizeHeadScopedCheckRunsStatus(payload) {
148
+ return summarizeHeadScopedCheckRunsSignal(payload).status;
149
+ }
150
+
151
+ /**
152
+ * Normalize the GitHub commit-status API payload for one head SHA into a stable status.
153
+ *
154
+ * @param {object} payload
155
+ * @returns {"success"|"failure"|"pending"|"none"}
156
+ */
157
+ export function normalizeHeadScopedCommitStatus(payload) {
158
+ const statuses = Array.isArray(payload?.statuses) ? payload.statuses : [];
159
+ if (statuses.length === 0) {
160
+ return "none";
161
+ }
162
+
163
+ let hasPending = false;
164
+ let hasFailure = false;
165
+ let hasSuccess = false;
166
+
167
+ for (const statusItem of statuses) {
168
+ const state = typeof statusItem?.state === "string" ? statusItem.state.toLowerCase() : "";
169
+ if (state === "pending") {
170
+ hasPending = true;
171
+ continue;
172
+ }
173
+ if (state === "failure" || state === "error") {
174
+ hasFailure = true;
175
+ continue;
176
+ }
177
+ if (state === "success") {
178
+ hasSuccess = true;
179
+ continue;
180
+ }
181
+ }
182
+
183
+ if (hasFailure) return "failure";
184
+ if (hasPending) return "pending";
185
+ if (hasSuccess) return "success";
186
+ return "none";
187
+ }
188
+
189
+ /**
190
+ * Merge head-scoped check-runs + commit-status signals into one deterministic status.
191
+ *
192
+ * @param {"success"|"failure"|"pending"|"none"} checkRunsStatus
193
+ * @param {"success"|"failure"|"pending"|"none"} commitStatus
194
+ * @returns {"success"|"failure"|"pending"|"none"}
195
+ */
196
+ export function mergeHeadScopedCiStatuses(checkRunsStatus, commitStatus) {
197
+ const normalizedCheckRunsStatus = normalizeHeadScopedCiStatus(checkRunsStatus);
198
+ const normalizedCommitStatus = normalizeHeadScopedCiStatus(commitStatus);
199
+
200
+ if (normalizedCheckRunsStatus === "failure" || normalizedCommitStatus === "failure") {
201
+ return "failure";
202
+ }
203
+ if (normalizedCheckRunsStatus === "pending" || normalizedCommitStatus === "pending") {
204
+ return "pending";
205
+ }
206
+ if (normalizedCheckRunsStatus === "success" || normalizedCommitStatus === "success") {
207
+ return "success";
208
+ }
209
+ return "none";
210
+ }
211
+
212
+ /**
213
+ * Normalize the PR `statusCheckRollup` payload into the shared machine-readable contract output.
214
+ *
215
+ * @param {Array<object>} rollup
216
+ * @returns {{
217
+ * overallStatus: "success"|"failure"|"pending"|"none",
218
+ * rollup: { success: boolean, pending: boolean, failure: boolean, none: boolean },
219
+ * semantics: { wait: boolean, blocked: boolean, timeoutDisposition: "remain_waiting"|"not_applicable" }
220
+ * }}
221
+ */
222
+ export function normalizeStatusCheckRollupContract(rollup) {
223
+ return buildCiContract(normalizeStatusCheckRollupStatus(rollup));
224
+ }
225
+
226
+ /**
227
+ * Normalize current-head CI inputs into one machine-readable contract output.
228
+ *
229
+ * @param {{
230
+ * checkRunsStatus?: "success"|"failure"|"pending"|"none"|null,
231
+ * commitStatus?: "success"|"failure"|"pending"|"none"|null,
232
+ * checkRunsUnsupportedCompleted?: boolean|null
233
+ * }} input
234
+ * @returns {{
235
+ * overallStatus: "success"|"failure"|"pending"|"none",
236
+ * rollup: { success: boolean, pending: boolean, failure: boolean, none: boolean },
237
+ * semantics: { wait: boolean, blocked: boolean, timeoutDisposition: "remain_waiting"|"not_applicable" }
238
+ * }}
239
+ */
240
+ export function normalizeHeadScopedCiContract({
241
+ checkRunsStatus = "none",
242
+ commitStatus = "none",
243
+ checkRunsUnsupportedCompleted = false,
244
+ } = {}) {
245
+ const overallStatus = mergeHeadScopedCiStatuses(
246
+ normalizeHeadScopedCiStatus(checkRunsStatus ?? "none"),
247
+ normalizeHeadScopedCiStatus(commitStatus ?? "none"),
248
+ );
249
+
250
+ if (checkRunsUnsupportedCompleted === true && overallStatus === "success") {
251
+ return buildCiContract("none");
252
+ }
253
+
254
+ return buildCiContract(overallStatus);
255
+ }
@@ -0,0 +1,161 @@
1
+ import { isCopilotLogin, normalizeTimestamp } from "../github/copilot-helpers.mjs";
2
+
3
+ const ACTIVE_COPILOT_REVIEW_REQUEST_STATUSES = new Set(["requested", "already-requested"]);
4
+ const SUBMITTED_REVIEW_STATES = new Set(["APPROVED", "CHANGES_REQUESTED", "COMMENTED", "DISMISSED"]);
5
+
6
+ function normalizeReviewRequestEvents(events) {
7
+ if (!Array.isArray(events)) {
8
+ return [];
9
+ }
10
+
11
+ return events
12
+ .map((event) => ({
13
+ createdAtMs: normalizeTimestamp(event?.createdAt),
14
+ requestedReviewerLogin: typeof event?.requestedReviewerLogin === "string"
15
+ ? event.requestedReviewerLogin.trim()
16
+ : "",
17
+ }))
18
+ .filter((event) => event.createdAtMs !== null && isCopilotLogin(event.requestedReviewerLogin))
19
+ .sort((left, right) => left.createdAtMs - right.createdAtMs);
20
+ }
21
+
22
+ function normalizeReviews(reviews) {
23
+ if (!Array.isArray(reviews)) {
24
+ return [];
25
+ }
26
+
27
+ return reviews
28
+ .map((review, index) => {
29
+ const state = typeof review?.state === "string" ? review.state.toUpperCase() : "";
30
+ const submittedAtMs = normalizeTimestamp(review?.submittedAt ?? review?.createdAt);
31
+ const authorLogin = typeof review?.authorLogin === "string"
32
+ ? review.authorLogin.trim()
33
+ : "";
34
+ const commitSha = typeof review?.commitSha === "string" && review.commitSha.trim().length > 0
35
+ ? review.commitSha.trim()
36
+ : null;
37
+
38
+ return {
39
+ sortKey: index,
40
+ state,
41
+ submittedAtMs,
42
+ authorLogin,
43
+ commitSha,
44
+ };
45
+ })
46
+ .filter((review) => review.submittedAtMs !== null && isCopilotLogin(review.authorLogin) && SUBMITTED_REVIEW_STATES.has(review.state))
47
+ .sort((left, right) => left.submittedAtMs - right.submittedAtMs || left.sortKey - right.sortKey);
48
+ }
49
+
50
+ function normalizeReviewComments(comments) {
51
+ if (!Array.isArray(comments)) {
52
+ return [];
53
+ }
54
+
55
+ return comments
56
+ .map((comment) => ({
57
+ createdAtMs: normalizeTimestamp(comment?.createdAt),
58
+ authorLogin: typeof comment?.authorLogin === "string" ? comment.authorLogin.trim() : "",
59
+ }))
60
+ .filter((comment) => comment.createdAtMs !== null && isCopilotLogin(comment.authorLogin))
61
+ .sort((left, right) => left.createdAtMs - right.createdAtMs);
62
+ }
63
+
64
+ function normalizeCommits(commits) {
65
+ if (!Array.isArray(commits)) {
66
+ return [];
67
+ }
68
+
69
+ return commits
70
+ .map((commit, index) => ({
71
+ sortKey: index,
72
+ committedAtMs: normalizeTimestamp(commit?.committedAt),
73
+ authorLogin: typeof commit?.authorLogin === "string" ? commit.authorLogin.trim() : "",
74
+ sha: typeof commit?.sha === "string" && commit.sha.trim().length > 0 ? commit.sha.trim() : null,
75
+ }))
76
+ .filter((commit) => commit.committedAtMs !== null)
77
+ .sort((left, right) => left.committedAtMs - right.committedAtMs || left.sortKey - right.sortKey);
78
+ }
79
+
80
+ function normalizeReviewThreadSummary(summary) {
81
+ const totalThreads = typeof summary?.totalThreads === "number" && summary.totalThreads >= 0
82
+ ? Math.floor(summary.totalThreads)
83
+ : 0;
84
+ const unresolvedThreads = typeof summary?.unresolvedThreads === "number" && summary.unresolvedThreads >= 0
85
+ ? Math.floor(summary.unresolvedThreads)
86
+ : 0;
87
+
88
+ return {
89
+ totalThreads,
90
+ unresolvedThreads,
91
+ };
92
+ }
93
+
94
+ export function summarizeCopilotLoopIterations({
95
+ reviewRequestEvents,
96
+ reviews,
97
+ reviewComments,
98
+ commits,
99
+ reviewThreadSummary,
100
+ currentHeadSha = null,
101
+ currentReviewRequestStatus = "none",
102
+ degraded = false,
103
+ degradedReasons = [],
104
+ } = {}) {
105
+ const normalizedReviewRequests = normalizeReviewRequestEvents(reviewRequestEvents);
106
+ const normalizedReviews = normalizeReviews(reviews);
107
+ const normalizedReviewComments = normalizeReviewComments(reviewComments);
108
+ const normalizedCommits = normalizeCommits(commits);
109
+ const normalizedThreadSummary = normalizeReviewThreadSummary(reviewThreadSummary);
110
+
111
+ const hasActivePendingRequest = ACTIVE_COPILOT_REVIEW_REQUEST_STATUSES.has(currentReviewRequestStatus);
112
+ const latestCompletedReview = normalizedReviews.at(-1) ?? null;
113
+ const latestCompletedReviewAtMs = latestCompletedReview?.submittedAtMs ?? null;
114
+
115
+ const pendingCopilotReviewRounds = hasActivePendingRequest
116
+ && (
117
+ latestCompletedReviewAtMs === null
118
+ || normalizedReviewRequests.some((event) => event.createdAtMs > latestCompletedReviewAtMs)
119
+ || (
120
+ typeof currentHeadSha === "string"
121
+ && currentHeadSha.length > 0
122
+ && latestCompletedReview?.commitSha !== null
123
+ && latestCompletedReview.commitSha !== currentHeadSha
124
+ )
125
+ )
126
+ ? 1
127
+ : 0;
128
+
129
+ const completedCopilotReviewRounds = normalizedReviews.length;
130
+ const hasCopilotLoopHistory =
131
+ completedCopilotReviewRounds > 0
132
+ || pendingCopilotReviewRounds > 0
133
+ || normalizedReviewRequests.length > 0
134
+ || normalizedReviewComments.length > 0;
135
+
136
+ if (!hasCopilotLoopHistory) {
137
+ return {
138
+ available: false,
139
+ source: "github_pr_timeline",
140
+ reason: "no_copilot_review_history",
141
+ };
142
+ }
143
+
144
+ const firstCopilotFeedbackAtMs = normalizedReviewComments[0]?.createdAtMs ?? null;
145
+ const fixCommitsAfterFeedback = firstCopilotFeedbackAtMs === null
146
+ ? 0
147
+ : normalizedCommits.filter((commit) => commit.committedAtMs > firstCopilotFeedbackAtMs && commit.authorLogin.length > 0 && !isCopilotLogin(commit.authorLogin)).length;
148
+
149
+ return {
150
+ available: true,
151
+ source: "github_pr_timeline",
152
+ ...(degraded ? { degraded: true, degradedReasons: Array.isArray(degradedReasons) ? degradedReasons : [] } : {}),
153
+ completedCopilotReviewRounds,
154
+ pendingCopilotReviewRounds,
155
+ copilotReviewRequests: normalizedReviewRequests.length,
156
+ copilotReviewComments: normalizedReviewComments.length,
157
+ resolvedReviewThreads: Math.max(0, normalizedThreadSummary.totalThreads - normalizedThreadSummary.unresolvedThreads),
158
+ unresolvedReviewThreads: normalizedThreadSummary.unresolvedThreads,
159
+ fixCommitsAfterFeedback,
160
+ };
161
+ }