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,304 @@
1
+ #!/usr/bin/env node
2
+ // ============================================================================
3
+ // Debt remediation loop command
4
+ //
5
+ // Single CLI entrypoint: --input → cluster → score → shape → issue → report
6
+ //
7
+ // Usage:
8
+ // debt-remediate.mjs --input <path> [--repo <owner/name>] [--dry-run]
9
+ //
10
+ // Takes a JSON array of debt_signal objects, runs the full remediation pipeline,
11
+ // and creates GitHub issues for each remediation_item outcome.
12
+ // ============================================================================
13
+
14
+ import { readFile } from "node:fs/promises";
15
+ import path from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
+ import { execFileSync } from "node:child_process";
18
+
19
+ import { buildParseError, formatCliError, isDirectCliRun } from "../_core-helpers.mjs";
20
+ import { requireOptionValue } from "../_cli-primitives.mjs";
21
+ import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
22
+ import { DebtSignalSchema } from "@dev-loops/core/debt/signal";
23
+ import { clusterSignalsEnriched } from "@dev-loops/core/debt/cluster";
24
+ import { shapeFindings } from "@dev-loops/core/debt/shape";
25
+ import { createRemediationIssue } from "@dev-loops/core/debt/remediation-to-issue";
26
+
27
+ const REPO_ROOT = fileURLToPath(new URL("../..", import.meta.url));
28
+
29
+ const USAGE = `Usage:
30
+ debt-remediate.mjs --input <path>
31
+ debt-remediate.mjs --input <path> --repo <owner/name>
32
+ debt-remediate.mjs --input <path> --dry-run
33
+
34
+ Run the debt pipeline: cluster signals → score → shape → create
35
+ GitHub issues for remediation_items.
36
+
37
+ Required:
38
+ --input <path> Path to a JSON file with array of debt_signal objects
39
+
40
+ Optional:
41
+ --repo <owner/name> Target repository (default: detected from git remote)
42
+ --dry-run Validate and report without creating issues
43
+
44
+ Output (stdout, JSON):
45
+ { "ok": true, "signals": N, "findings": N, "remediationItems": N, "issues": [...], "summary": "..." }
46
+
47
+ Exit codes:
48
+ 0 Success (all remediation issue creations succeeded)
49
+ 1 Argument error, input validation failure, or issue creation failure`.trim();
50
+
51
+ const parseError = buildParseError(USAGE);
52
+
53
+ // ============================================================================
54
+ // Signal validation
55
+ // ============================================================================
56
+
57
+ function validateSignals(signals) {
58
+ if (!Array.isArray(signals)) {
59
+ return { ok: false, error: "Input must be a JSON array of debt_signal objects" };
60
+ }
61
+ if (signals.length === 0) {
62
+ return { ok: false, error: "Input array must contain at least one debt_signal" };
63
+ }
64
+
65
+ const errors = [];
66
+ for (let i = 0; i < signals.length; i++) {
67
+ const result = DebtSignalSchema.safeParse(signals[i]);
68
+ if (!result.success) {
69
+ errors.push({
70
+ index: i,
71
+ id: signals[i]?.id || `index-${i}`,
72
+ issues: result.error.issues,
73
+ });
74
+ }
75
+ }
76
+
77
+ if (errors.length > 0) {
78
+ return { ok: false, error: "Signal validation failed", validationErrors: errors };
79
+ }
80
+
81
+ return { ok: true, signals };
82
+ }
83
+
84
+ // ============================================================================
85
+ // Detect repo from git remote
86
+ // ============================================================================
87
+
88
+ function detectRepo() {
89
+ try {
90
+ const remote = execFileSync("git", ["remote", "get-url", "origin"], {
91
+ cwd: REPO_ROOT,
92
+ encoding: "utf-8",
93
+ }).trim();
94
+ // parseRepoSlug expects owner/name; extract from common remote URL formats
95
+ const match = remote.match(/(?:github\.com[:/])([^/]+\/[^/]+?)(?:\.git)?$/);
96
+ if (match) {
97
+ return parseRepoSlug(match[1]);
98
+ }
99
+ return null;
100
+ } catch {
101
+ return null;
102
+ }
103
+ }
104
+
105
+ // ============================================================================
106
+ // Build summary report
107
+ // ============================================================================
108
+
109
+ function buildReport(signalsCount, findingsCount, results) {
110
+ const remediationItems = results.filter(r => r.outcome === "remediation_item");
111
+ const epics = results.filter(r => r.outcome === "debt_epic");
112
+ const defers = results.filter(r => r.outcome === "defer");
113
+ const watches = results.filter(r => r.outcome === "watch");
114
+ const dismisses = results.filter(r => r.outcome === "dismiss");
115
+
116
+ const issuesCreated = remediationItems.filter(r => r.issueCreated);
117
+ const issuesFailed = remediationItems.filter(r => !r.issueCreated && r.issueError);
118
+
119
+ const summary = [
120
+ `${signalsCount} signals → ${findingsCount} findings`,
121
+ `${remediationItems.length} remediation items (${issuesCreated.length} issues created, ${issuesFailed.length} failed)`,
122
+ `${epics.length} debt epics`,
123
+ `${defers.length} deferred`,
124
+ `${watches.length} watching`,
125
+ `${dismisses.length} dismissed`,
126
+ ].join("; ");
127
+
128
+ return {
129
+ signals: signalsCount,
130
+ findings: findingsCount,
131
+ remediationItems: remediationItems.length,
132
+ debtEpics: epics.length,
133
+ deferred: defers.length,
134
+ watching: watches.length,
135
+ dismissed: dismisses.length,
136
+ issues: remediationItems.map(r => ({
137
+ findingId: r.findingId,
138
+ title: r.artifact?.title,
139
+ created: r.issueCreated || false,
140
+ issueNumber: r.issueNumber || null,
141
+ issueUrl: r.issueUrl || null,
142
+ error: r.issueError || null,
143
+ })),
144
+ summary,
145
+ };
146
+ }
147
+
148
+ // ============================================================================
149
+ // Run CLI (test-compatible: returns exitCode without calling process.exit)
150
+ // ============================================================================
151
+
152
+ export async function runCli(argv) {
153
+ const args = [...argv];
154
+ const options = { input: undefined, repo: undefined, dryRun: false, help: false };
155
+
156
+ while (args.length > 0) {
157
+ const token = args.shift();
158
+ if (token === "--help" || token === "-h") {
159
+ options.help = true;
160
+ break;
161
+ }
162
+ if (token === "--input") {
163
+ options.input = requireOptionValue(args, "--input", parseError);
164
+ continue;
165
+ }
166
+ if (token === "--repo") {
167
+ options.repo = requireOptionValue(args, "--repo", parseError);
168
+ continue;
169
+ }
170
+ if (token === "--dry-run") {
171
+ options.dryRun = true;
172
+ continue;
173
+ }
174
+ throw parseError(`Unknown flag: ${token}`);
175
+ }
176
+
177
+ if (options.help) {
178
+ process.stdout.write(USAGE + "\n");
179
+ return { exitCode: 0 };
180
+ }
181
+
182
+ if (!options.input) {
183
+ throw parseError("Missing required flag: --input <path>");
184
+ }
185
+
186
+ // Resolve input path
187
+ const inputPath = path.resolve(options.input);
188
+
189
+ // Read and parse input
190
+ let rawInput;
191
+ try {
192
+ rawInput = await readFile(inputPath, "utf-8");
193
+ } catch (err) {
194
+ return { exitCode: 1, output: { ok: false, error: `Cannot read input file: ${inputPath}`, detail: err.message } };
195
+ }
196
+
197
+ let signals;
198
+ try {
199
+ signals = JSON.parse(rawInput);
200
+ } catch (err) {
201
+ return { exitCode: 1, output: { ok: false, error: "Input file is not valid JSON", detail: err.message } };
202
+ }
203
+
204
+ // Validate signals
205
+ const validation = validateSignals(signals);
206
+ if (!validation.ok) {
207
+ return { exitCode: 1, output: validation };
208
+ }
209
+
210
+ // Resolve repo
211
+ let repo;
212
+ if (options.repo) {
213
+ try {
214
+ repo = parseRepoSlug(options.repo);
215
+ } catch {
216
+ return { exitCode: 1, output: { ok: false, error: `Invalid repo slug: ${options.repo}` } };
217
+ }
218
+ } else {
219
+ repo = detectRepo();
220
+ }
221
+
222
+ if (!repo) {
223
+ return { exitCode: 1, output: { ok: false, error: "Cannot detect repository. Pass --repo <owner/name>." } };
224
+ }
225
+
226
+ // Run pipeline: cluster → score → shape
227
+ const findings = clusterSignalsEnriched(signals);
228
+ const shaped = shapeFindings(findings);
229
+
230
+ // Create issues for remediation_items (skip in dry-run mode)
231
+ const results = [];
232
+ let anyIssueFailed = false;
233
+ for (const { outcome, artifact, findingId } of shaped) {
234
+ if (outcome === "remediation_item" && artifact && !options.dryRun) {
235
+ try {
236
+ const issueResult = createRemediationIssue(artifact, repo);
237
+ if (!issueResult.ok) {
238
+ anyIssueFailed = true;
239
+ }
240
+ results.push({
241
+ outcome,
242
+ findingId,
243
+ artifact,
244
+ issueCreated: issueResult.ok,
245
+ issueNumber: issueResult.issueNumber || null,
246
+ issueUrl: issueResult.issueUrl || null,
247
+ issueError: issueResult.ok ? null : issueResult.error,
248
+ });
249
+ } catch (err) {
250
+ anyIssueFailed = true;
251
+ results.push({
252
+ outcome,
253
+ findingId,
254
+ artifact,
255
+ issueCreated: false,
256
+ issueNumber: null,
257
+ issueUrl: null,
258
+ issueError: err.message || "Unknown error creating issue",
259
+ });
260
+ }
261
+ } else if (outcome === "remediation_item" && artifact && options.dryRun) {
262
+ results.push({
263
+ outcome,
264
+ findingId,
265
+ artifact,
266
+ issueCreated: false,
267
+ issueNumber: null,
268
+ issueUrl: null,
269
+ issueError: null,
270
+ dryRun: true,
271
+ });
272
+ } else {
273
+ results.push({ outcome, findingId, artifact });
274
+ }
275
+ }
276
+
277
+ // Build report
278
+ const report = buildReport(signals.length, findings.length, results);
279
+ report.ok = !anyIssueFailed;
280
+ report.dryRun = options.dryRun;
281
+ report.repo = `${repo.owner}/${repo.name}`;
282
+
283
+ const outputTarget = report.ok ? process.stdout : process.stderr;
284
+ outputTarget.write(JSON.stringify(report) + "\n");
285
+ return { exitCode: anyIssueFailed ? 1 : 0 };
286
+ }
287
+
288
+ // ============================================================================
289
+ // Direct CLI entrypoint
290
+ // ============================================================================
291
+
292
+ if (isDirectCliRun(import.meta.url)) {
293
+ runCli(process.argv.slice(2)).then(({ exitCode, output }) => {
294
+ if (output) {
295
+ process.stderr.write(JSON.stringify(output) + "\n");
296
+ }
297
+ process.exitCode = exitCode;
298
+ }).catch((err) => {
299
+ process.stderr.write(`${formatCliError(err)}\n`);
300
+ process.exitCode = 1;
301
+ });
302
+ }
303
+
304
+ export { validateSignals, buildReport, detectRepo };
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env node
2
+ import { execFileSync } from "node:child_process";
3
+ import process from "node:process";
4
+ function parseArgs() {
5
+ const args = process.argv.slice(2);
6
+ const opts = { base: null, head: null };
7
+ for (let i = 0; i < args.length; i++) {
8
+ if (args[i] === "--help" || args[i] === "-h") {
9
+ process.stdout.write(`Usage: detect-change-scope.mjs [--base <ref>] [--head <ref>]
10
+ Detect change scope from git diff for light-mode eligibility.
11
+ Options:
12
+ --base <ref> Override base ref (default: HEAD~1)
13
+ --head <ref> Override head ref; ignored unless --base is also set
14
+ --help, -h Show this help
15
+ Exit codes:
16
+ 0 Success
17
+ 1 Error
18
+ `);
19
+ process.exit(0);
20
+ }
21
+ if (args[i] === "--base" && i + 1 < args.length) opts.base = args[++i];
22
+ else if (args[i] === "--head" && i + 1 < args.length) opts.head = args[++i];
23
+ }
24
+ return opts;
25
+ }
26
+ export function parseGitDiffStat(output) {
27
+ const trimmed = output.trim();
28
+ if (trimmed.length === 0) {
29
+ return { filesChanged: 0, linesChanged: 0 };
30
+ }
31
+ const lines = trimmed.split("\n");
32
+ const lastLine = lines[lines.length - 1];
33
+ const isSummary = /\d+\s+files?\s+changed/.test(lastLine) || /\d+\s+insertion/.test(lastLine) || /\d+\s+deletion/.test(lastLine);
34
+ const fileCount = isSummary ? lines.length - 1 : lines.length;
35
+ let insertions = 0;
36
+ let deletions = 0;
37
+ if (isSummary) {
38
+ const insMatch = lastLine.match(/(\d+)\s+insertion/);
39
+ const delMatch = lastLine.match(/(\d+)\s+deletion/);
40
+ if (insMatch) insertions = parseInt(insMatch[1], 10);
41
+ if (delMatch) deletions = parseInt(delMatch[1], 10);
42
+ }
43
+ return { filesChanged: fileCount, linesChanged: insertions + deletions };
44
+ }
45
+ function detectScope({ base, head } = {}) {
46
+ let diffArgs = ["diff", "--stat"];
47
+ if (base && head) {
48
+ diffArgs.push(`${base}..${head}`);
49
+ } else if (base) {
50
+ diffArgs.push(base);
51
+ } else {
52
+ diffArgs.push("HEAD~1..HEAD");
53
+ }
54
+ let output;
55
+ try {
56
+ output = execFileSync("git", diffArgs, { encoding: "utf8", maxBuffer: 1_000_000 });
57
+ } catch (err) {
58
+ return { ok: false, filesChanged: 0, linesChanged: 0, error: err instanceof Error ? err.message : String(err) };
59
+ }
60
+ const parsed = parseGitDiffStat(output);
61
+ return { ok: true, ...parsed };
62
+ }
63
+ function isEligibleForLightMode(scope, threshold) {
64
+ return scope.filesChanged <= threshold.maxFiles && scope.linesChanged <= threshold.maxLines;
65
+ }
66
+ async function main() {
67
+ const opts = parseArgs();
68
+ const scope = detectScope(opts);
69
+ let threshold = { maxFiles: 3, maxLines: 200 };
70
+ let eligible = false;
71
+ try {
72
+ const { loadDevLoopConfig, resolveLightMode } = await import(
73
+ "@dev-loops/core/config"
74
+ );
75
+ const { config, errors } = await loadDevLoopConfig({ repoRoot: process.cwd() });
76
+ if (Array.isArray(errors) && errors.length > 0) {
77
+ } else {
78
+ const lightMode = resolveLightMode(config);
79
+ if (lightMode && scope.ok !== false) {
80
+ threshold = { maxFiles: lightMode.maxFiles, maxLines: lightMode.maxLines };
81
+ eligible = isEligibleForLightMode(scope, threshold);
82
+ }
83
+ }
84
+ } catch {
85
+ }
86
+ process.stdout.write(
87
+ JSON.stringify({
88
+ ...scope,
89
+ eligibleForLightMode: eligible,
90
+ threshold,
91
+ }) + "\n"
92
+ );
93
+ }
94
+ const isDirectRun =
95
+ process.argv[1] && process.argv[1].includes("detect-change-scope.mjs");
96
+ if (isDirectRun) {
97
+ main().catch((err) => {
98
+ process.stderr.write(`${err.message}\n`);
99
+ process.exitCode = 1;
100
+ });
101
+ }
102
+ export { detectScope, isEligibleForLightMode };