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,513 @@
1
+ #!/usr/bin/env node
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { buildParseError, formatCliError, isDirectCliRun } from "../_core-helpers.mjs";
5
+ import { parsePositiveInteger, requireOptionValue, runCommand } from "../_cli-primitives.mjs";
6
+ const DEFAULT_MAX_LINES = 1000;
7
+ const DEFAULT_DUPLICATE_WINDOW_LINES = 4;
8
+ const DEFAULT_BRANCH_THRESHOLD = 25;
9
+ const DEFAULT_THIN_WRAPPER_MAX_LINES = 40;
10
+ const USAGE = `Usage:
11
+ run-refinement-audit.mjs --paths <comma-separated paths> [--root <path>] [--max-lines <n>] [--duplicate-window-lines <n>] [--branch-threshold <n>] [--thin-wrapper-max-lines <n>] [--output <path>]
12
+ run-refinement-audit.mjs --paths-file <file> [--root <path>] [--max-lines <n>] [--duplicate-window-lines <n>] [--branch-threshold <n>] [--thin-wrapper-max-lines <n>] [--output <path>]
13
+ Run a bounded refinement audit for explicit repo paths only. The helper never falls back to a whole-repo scan.
14
+ Required:
15
+ exactly one of:
16
+ --paths <comma-separated paths>
17
+ --paths-file <file>
18
+ Optional:
19
+ --root <path> Repo root (defaults to git rev-parse --show-toplevel)
20
+ --max-lines <n> Oversized-file threshold (default: ${DEFAULT_MAX_LINES})
21
+ --duplicate-window-lines <n> Duplicate-block window size (default: ${DEFAULT_DUPLICATE_WINDOW_LINES})
22
+ --branch-threshold <n> Branching-hotspot threshold (default: ${DEFAULT_BRANCH_THRESHOLD})
23
+ --thin-wrapper-max-lines <n> Thin-wrapper line threshold (default: ${DEFAULT_THIN_WRAPPER_MAX_LINES})
24
+ --output <path> Write the same success JSON emitted on stdout
25
+ Success output (stdout, JSON):
26
+ {
27
+ "ok": true,
28
+ "repoRoot": "/repo",
29
+ "paths": ["AGENTS.md", "agents/refiner.agent.md"],
30
+ "auditedFiles": [...],
31
+ "findings": [...],
32
+ "highestValueFollowUpCandidates": [...],
33
+ "scopeBoundary": { "mode": "bounded_paths_only", "fullRepoScan": false }
34
+ }
35
+ Failure behavior (stderr, JSON, exit 1):
36
+ - malformed arguments, blank paths, invalid thresholds, and zero auditable files fail closed
37
+ - findings are not a process failure; findings still return exit 0`.trim();
38
+ const parseError = buildParseError(USAGE);
39
+ const BRANCH_TOKEN_PATTERN = /\b(?:if|else|switch|case|for|while|catch|finally|break|continue)\b|&&|\|\||\?(?![?.])/gu;
40
+ const PRIORITY_ORDER = new Map([
41
+ ["high", 0],
42
+ ["medium", 1],
43
+ ["low", 2],
44
+ ]);
45
+ export function parseRefinementAuditCliArgs(argv) {
46
+ const args = [...argv];
47
+ const options = {
48
+ help: false,
49
+ paths: undefined,
50
+ pathsFile: undefined,
51
+ root: undefined,
52
+ maxLines: DEFAULT_MAX_LINES,
53
+ duplicateWindowLines: DEFAULT_DUPLICATE_WINDOW_LINES,
54
+ branchThreshold: DEFAULT_BRANCH_THRESHOLD,
55
+ thinWrapperMaxLines: DEFAULT_THIN_WRAPPER_MAX_LINES,
56
+ output: undefined,
57
+ };
58
+ while (args.length > 0) {
59
+ const token = args.shift();
60
+ if (token === "--help" || token === "-h") {
61
+ options.help = true;
62
+ return options;
63
+ }
64
+ if (token === "--paths") {
65
+ options.paths = requireOptionValue(args, "--paths", parseError, { flagPattern: /^-/u });
66
+ continue;
67
+ }
68
+ if (token === "--paths-file") {
69
+ options.pathsFile = requireOptionValue(args, "--paths-file", parseError, { flagPattern: /^-/u });
70
+ continue;
71
+ }
72
+ if (token === "--root") {
73
+ options.root = requireOptionValue(args, "--root", parseError, { flagPattern: /^-/u });
74
+ continue;
75
+ }
76
+ if (token === "--max-lines") {
77
+ options.maxLines = parsePositiveInteger(requireOptionValue(args, "--max-lines", parseError), "--max-lines", parseError);
78
+ continue;
79
+ }
80
+ if (token === "--duplicate-window-lines") {
81
+ options.duplicateWindowLines = parsePositiveInteger(
82
+ requireOptionValue(args, "--duplicate-window-lines", parseError),
83
+ "--duplicate-window-lines",
84
+ parseError,
85
+ );
86
+ continue;
87
+ }
88
+ if (token === "--branch-threshold") {
89
+ options.branchThreshold = parsePositiveInteger(
90
+ requireOptionValue(args, "--branch-threshold", parseError),
91
+ "--branch-threshold",
92
+ parseError,
93
+ );
94
+ continue;
95
+ }
96
+ if (token === "--thin-wrapper-max-lines") {
97
+ options.thinWrapperMaxLines = parsePositiveInteger(
98
+ requireOptionValue(args, "--thin-wrapper-max-lines", parseError),
99
+ "--thin-wrapper-max-lines",
100
+ parseError,
101
+ );
102
+ continue;
103
+ }
104
+ if (token === "--output") {
105
+ options.output = requireOptionValue(args, "--output", parseError, { flagPattern: /^-/u });
106
+ continue;
107
+ }
108
+ throw parseError(`Unknown argument: ${token}`);
109
+ }
110
+ if (options.paths !== undefined && options.pathsFile !== undefined) {
111
+ throw parseError("Specify exactly one of --paths or --paths-file");
112
+ }
113
+ if (options.paths === undefined && options.pathsFile === undefined) {
114
+ throw parseError("run-refinement-audit requires exactly one of --paths or --paths-file");
115
+ }
116
+ return options;
117
+ }
118
+ function toPosixPath(value) {
119
+ return value.split(path.sep).join("/");
120
+ }
121
+ function countLines(text) {
122
+ if (text.length === 0) {
123
+ return 0;
124
+ }
125
+ const lines = text.split(/\r?\n/u);
126
+ if (lines.at(-1) === "") {
127
+ lines.pop();
128
+ }
129
+ return lines.length;
130
+ }
131
+ function countBranchTokens(text) {
132
+ const matches = text.match(BRANCH_TOKEN_PATTERN);
133
+ return Array.isArray(matches) ? matches.length : 0;
134
+ }
135
+ function splitConfiguredPaths(raw) {
136
+ return raw.split(",").map((entry) => entry.trim());
137
+ }
138
+ function assertNoBlankPaths(entries) {
139
+ if (entries.length === 0 || entries.some((entry) => entry.length === 0)) {
140
+ throw parseError("Audit paths must be non-empty; blank paths are not allowed");
141
+ }
142
+ return entries;
143
+ }
144
+ async function loadConfiguredPaths(options, cwd) {
145
+ if (options.paths !== undefined) {
146
+ return assertNoBlankPaths(splitConfiguredPaths(options.paths));
147
+ }
148
+ const filePath = path.resolve(cwd, options.pathsFile);
149
+ let raw;
150
+ try {
151
+ raw = await readFile(filePath, "utf8");
152
+ } catch (error) {
153
+ const detail = error instanceof Error && typeof error.message === "string"
154
+ ? error.message
155
+ : String(error);
156
+ throw parseError(`Unreadable --paths-file input: ${detail}`);
157
+ }
158
+ const entries = raw.split(/\r?\n/u);
159
+ if (entries.at(-1) === "") {
160
+ entries.pop();
161
+ }
162
+ return assertNoBlankPaths(entries.map((entry) => entry.trim()));
163
+ }
164
+ async function resolveRepoRoot(options, { cwd, env, gitCommand }) {
165
+ if (typeof options.root === "string") {
166
+ return path.resolve(cwd, options.root);
167
+ }
168
+ const { stdout } = await runCommand(gitCommand, ["rev-parse", "--show-toplevel"], { cwd, env });
169
+ const repoRoot = stdout.trim();
170
+ if (repoRoot.length === 0) {
171
+ throw new Error("Unable to resolve repo root");
172
+ }
173
+ return repoRoot;
174
+ }
175
+ function normalizeRequestedPath(requestedPath, repoRoot) {
176
+ const resolvedPath = path.isAbsolute(requestedPath)
177
+ ? path.normalize(requestedPath)
178
+ : path.resolve(repoRoot, requestedPath);
179
+ const relativePath = path.relative(repoRoot, resolvedPath);
180
+ if (relativePath.length === 0) {
181
+ throw parseError("Repo root is not a valid bounded audit path; name a specific file or subdirectory");
182
+ }
183
+ if (relativePath.startsWith(`..${path.sep}`) || relativePath === ".." || path.isAbsolute(relativePath)) {
184
+ throw parseError(`Path is outside the repo root: ${requestedPath}`);
185
+ }
186
+ return toPosixPath(relativePath);
187
+ }
188
+ async function expandTrackedFiles(requestedPaths, { repoRoot, env, gitCommand }) {
189
+ const normalizedRequestedPaths = [];
190
+ const seenRequestedPaths = new Set();
191
+ const expandedTrackedFiles = [];
192
+ const seenTrackedFiles = new Set();
193
+ for (const requestedPath of requestedPaths) {
194
+ const normalizedPath = normalizeRequestedPath(requestedPath, repoRoot);
195
+ if (!seenRequestedPaths.has(normalizedPath)) {
196
+ seenRequestedPaths.add(normalizedPath);
197
+ normalizedRequestedPaths.push(normalizedPath);
198
+ }
199
+ const { stdout } = await runCommand(gitCommand, ["ls-files", "--", normalizedPath], { cwd: repoRoot, env });
200
+ const trackedFiles = stdout
201
+ .split(/\r?\n/u)
202
+ .map((entry) => entry.trim())
203
+ .filter((entry) => entry.length > 0);
204
+ for (const trackedFile of trackedFiles) {
205
+ const normalizedTrackedFile = toPosixPath(trackedFile);
206
+ if (!seenTrackedFiles.has(normalizedTrackedFile)) {
207
+ seenTrackedFiles.add(normalizedTrackedFile);
208
+ expandedTrackedFiles.push(normalizedTrackedFile);
209
+ }
210
+ }
211
+ }
212
+ return {
213
+ normalizedRequestedPaths,
214
+ expandedTrackedFiles,
215
+ };
216
+ }
217
+ function isBinaryBuffer(buffer) {
218
+ return buffer.includes(0);
219
+ }
220
+ function normalizeDuplicateLine(line) {
221
+ return line.replace(/\s+/gu, " ").trim();
222
+ }
223
+ function shouldConsiderDuplicateWindow(lines) {
224
+ const normalizedLines = lines.map(normalizeDuplicateLine);
225
+ const joined = normalizedLines.join("\n").trim();
226
+ const linesWithWordChars = normalizedLines.filter((line) => /[A-Za-z0-9]/u.test(line));
227
+ return joined.length >= 20 && linesWithWordChars.length >= 2;
228
+ }
229
+ function collectDuplicateBlockCounts(auditableRecords, duplicateWindowLines) {
230
+ const occurrences = new Map();
231
+ for (const record of auditableRecords) {
232
+ const lines = record.text.split(/\r?\n/u);
233
+ if (lines.length < duplicateWindowLines) {
234
+ continue;
235
+ }
236
+ for (let startIndex = 0; startIndex <= lines.length - duplicateWindowLines; startIndex += 1) {
237
+ const window = lines.slice(startIndex, startIndex + duplicateWindowLines);
238
+ if (!shouldConsiderDuplicateWindow(window)) {
239
+ continue;
240
+ }
241
+ const normalizedBlock = window.map(normalizeDuplicateLine).join("\n");
242
+ const values = occurrences.get(normalizedBlock) ?? [];
243
+ values.push({ path: record.path, startLine: startIndex + 1 });
244
+ occurrences.set(normalizedBlock, values);
245
+ }
246
+ }
247
+ const duplicateCountsByPath = new Map();
248
+ for (const values of occurrences.values()) {
249
+ if (values.length < 2) {
250
+ continue;
251
+ }
252
+ for (const value of values) {
253
+ duplicateCountsByPath.set(value.path, (duplicateCountsByPath.get(value.path) ?? 0) + 1);
254
+ }
255
+ }
256
+ return duplicateCountsByPath;
257
+ }
258
+ function isCommentLine(line) {
259
+ return line.startsWith("#")
260
+ || line.startsWith("//")
261
+ || line.startsWith("/*")
262
+ || line === "*"
263
+ || line.startsWith("* ")
264
+ || line.startsWith("* ")
265
+ || line.startsWith("*/");
266
+ }
267
+ function isWrapperLikeLine(line) {
268
+ return [
269
+ /^import\s.+\sfrom\s+["'][^"']+["'];?$/u,
270
+ /^export\s+\*\s+from\s+["'][^"']+["'];?$/u,
271
+ /^export\s+\*\s+as\s+[A-Za-z_$][\w$]*\s+from\s+["'][^"']+["'];?$/u,
272
+ /^export\s+(?:type\s+)?\{[^}]+\}\s+from\s+["'][^"']+["'];?$/u,
273
+ /^export\s+(?:type\s+)?\{[^}]+\};?$/u,
274
+ /^export\s+default\s+[A-Za-z_$][\w$]*;?$/u,
275
+ ].some((pattern) => pattern.test(line));
276
+ }
277
+ function detectThinWrapperCandidate(text, { lineCount, branchTokenCount, thinWrapperMaxLines }) {
278
+ if (lineCount === 0 || lineCount > thinWrapperMaxLines || branchTokenCount > 0) {
279
+ return false;
280
+ }
281
+ const meaningfulLines = text
282
+ .split(/\r?\n/u)
283
+ .map((line) => line.trim())
284
+ .filter((line) => line.length > 0 && !isCommentLine(line));
285
+ if (meaningfulLines.length === 0) {
286
+ return false;
287
+ }
288
+ return meaningfulLines.every(isWrapperLikeLine);
289
+ }
290
+ function compareFindings(left, right) {
291
+ const priorityDelta = (PRIORITY_ORDER.get(left.priority) ?? 99) - (PRIORITY_ORDER.get(right.priority) ?? 99);
292
+ if (priorityDelta !== 0) {
293
+ return priorityDelta;
294
+ }
295
+ const pathDelta = left.path.localeCompare(right.path);
296
+ if (pathDelta !== 0) {
297
+ return pathDelta;
298
+ }
299
+ return left.id.localeCompare(right.id);
300
+ }
301
+ function summarizeFollowUpReason(finding) {
302
+ switch (finding.id) {
303
+ case "oversized_file":
304
+ return "Trim or split only if the current refinement scope explicitly chooses to; keep the audit as planning input, not rewrite authorization.";
305
+ case "duplicate_block_candidate":
306
+ return "Check whether duplication should become current-scope cleanup, a risk/watchpoint, or an explicit defer; do not broaden silently.";
307
+ case "branching_hotspot":
308
+ return "Review whether branching complexity should affect AC/DoD or become a watchpoint for the current bounded slice.";
309
+ case "thin_wrapper_candidate":
310
+ return "Consider delete/merge/trim framing before preserving glue-only wrapper layers; defer if out of scope.";
311
+ default:
312
+ return finding.summary;
313
+ }
314
+ }
315
+ function buildFindings(auditableRecords, duplicateCountsByPath, thresholds) {
316
+ const findings = [];
317
+ for (const record of auditableRecords) {
318
+ if (record.lineCount > thresholds.maxLines) {
319
+ findings.push({
320
+ id: "oversized_file",
321
+ priority: "high",
322
+ path: record.path,
323
+ summary: "File exceeds the bounded audit line threshold.",
324
+ evidence: { lineCount: record.lineCount, threshold: thresholds.maxLines },
325
+ });
326
+ }
327
+ const duplicateBlockMatches = duplicateCountsByPath.get(record.path) ?? 0;
328
+ if (duplicateBlockMatches > 0) {
329
+ findings.push({
330
+ id: "duplicate_block_candidate",
331
+ priority: "medium",
332
+ path: record.path,
333
+ summary: "Normalized repeated text blocks appear more than once within the bounded audit scope.",
334
+ evidence: {
335
+ duplicateBlockMatches,
336
+ duplicateWindowLines: thresholds.duplicateWindowLines,
337
+ },
338
+ });
339
+ }
340
+ if (record.branchTokenCount > thresholds.branchThreshold) {
341
+ findings.push({
342
+ id: "branching_hotspot",
343
+ priority: "medium",
344
+ path: record.path,
345
+ summary: "Control-flow token count exceeds the bounded branching threshold.",
346
+ evidence: { branchTokenCount: record.branchTokenCount, threshold: thresholds.branchThreshold },
347
+ });
348
+ }
349
+ if (record.thinWrapperCandidate) {
350
+ findings.push({
351
+ id: "thin_wrapper_candidate",
352
+ priority: "low",
353
+ path: record.path,
354
+ summary: "Small file looks dominated by re-exports or passthrough-only wrapper glue.",
355
+ evidence: { lineCount: record.lineCount, threshold: thresholds.thinWrapperMaxLines },
356
+ });
357
+ }
358
+ }
359
+ return findings.sort(compareFindings);
360
+ }
361
+ function buildHighestValueFollowUpCandidates(findings) {
362
+ const candidates = [];
363
+ const seenPaths = new Set();
364
+ for (const finding of findings) {
365
+ if (seenPaths.has(finding.path)) {
366
+ continue;
367
+ }
368
+ seenPaths.add(finding.path);
369
+ candidates.push({
370
+ path: finding.path,
371
+ reason: summarizeFollowUpReason(finding),
372
+ });
373
+ }
374
+ return candidates;
375
+ }
376
+ async function auditTrackedFiles(trackedFiles, options, { repoRoot }) {
377
+ const auditableRecords = [];
378
+ const skippedRecords = [];
379
+ for (const trackedFile of trackedFiles) {
380
+ const absolutePath = path.join(repoRoot, trackedFile);
381
+ try {
382
+ const buffer = await readFile(absolutePath);
383
+ if (isBinaryBuffer(buffer)) {
384
+ skippedRecords.push({
385
+ path: trackedFile,
386
+ lineCount: null,
387
+ branchTokenCount: null,
388
+ duplicateBlockMatches: 0,
389
+ thinWrapperCandidate: false,
390
+ skipped: true,
391
+ skipReason: "binary_file",
392
+ });
393
+ continue;
394
+ }
395
+ const text = buffer.toString("utf8");
396
+ const lineCount = countLines(text);
397
+ const branchTokenCount = countBranchTokens(text);
398
+ const thinWrapperCandidate = detectThinWrapperCandidate(text, {
399
+ lineCount,
400
+ branchTokenCount,
401
+ thinWrapperMaxLines: options.thinWrapperMaxLines,
402
+ });
403
+ auditableRecords.push({
404
+ path: trackedFile,
405
+ text,
406
+ lineCount,
407
+ branchTokenCount,
408
+ thinWrapperCandidate,
409
+ });
410
+ } catch (error) {
411
+ skippedRecords.push({
412
+ path: trackedFile,
413
+ lineCount: null,
414
+ branchTokenCount: null,
415
+ duplicateBlockMatches: 0,
416
+ thinWrapperCandidate: false,
417
+ skipped: true,
418
+ skipReason: "unreadable_file",
419
+ skipDetail: error instanceof Error ? error.code ?? error.message : String(error),
420
+ });
421
+ }
422
+ }
423
+ if (auditableRecords.length === 0) {
424
+ throw parseError("Zero auditable files remain after expansion; provide tracked text files in the bounded scope");
425
+ }
426
+ const duplicateCountsByPath = collectDuplicateBlockCounts(auditableRecords, options.duplicateWindowLines);
427
+ const findings = buildFindings(auditableRecords, duplicateCountsByPath, {
428
+ maxLines: options.maxLines,
429
+ duplicateWindowLines: options.duplicateWindowLines,
430
+ branchThreshold: options.branchThreshold,
431
+ thinWrapperMaxLines: options.thinWrapperMaxLines,
432
+ });
433
+ const followUpCandidates = buildHighestValueFollowUpCandidates(findings);
434
+ const auditedFileByPath = new Map();
435
+ for (const record of auditableRecords) {
436
+ auditedFileByPath.set(record.path, {
437
+ path: record.path,
438
+ lineCount: record.lineCount,
439
+ branchTokenCount: record.branchTokenCount,
440
+ duplicateBlockMatches: duplicateCountsByPath.get(record.path) ?? 0,
441
+ thinWrapperCandidate: record.thinWrapperCandidate,
442
+ skipped: false,
443
+ });
444
+ }
445
+ for (const record of skippedRecords) {
446
+ auditedFileByPath.set(record.path, record);
447
+ }
448
+ const auditedFiles = trackedFiles.map((trackedFile) => auditedFileByPath.get(trackedFile));
449
+ return {
450
+ auditedFiles,
451
+ findings,
452
+ highestValueFollowUpCandidates: followUpCandidates,
453
+ };
454
+ }
455
+ export async function runCli(
456
+ argv = process.argv.slice(2),
457
+ {
458
+ stdout = process.stdout,
459
+ stderr = process.stderr,
460
+ cwd = process.cwd(),
461
+ env = process.env,
462
+ gitCommand = "git",
463
+ } = {},
464
+ ) {
465
+ const options = parseRefinementAuditCliArgs(argv);
466
+ if (options.help) {
467
+ stdout.write(`${USAGE}\n`);
468
+ return { ok: true, help: true };
469
+ }
470
+ const repoRoot = await resolveRepoRoot(options, { cwd, env, gitCommand });
471
+ const configuredPaths = await loadConfiguredPaths(options, cwd);
472
+ const { normalizedRequestedPaths, expandedTrackedFiles } = await expandTrackedFiles(configuredPaths, {
473
+ repoRoot,
474
+ env,
475
+ gitCommand,
476
+ });
477
+ if (expandedTrackedFiles.length === 0) {
478
+ throw parseError("Zero auditable files remain after expansion; provide tracked files in the bounded scope");
479
+ }
480
+ const auditResult = await auditTrackedFiles(expandedTrackedFiles, options, { repoRoot });
481
+ const payload = {
482
+ ok: true,
483
+ repoRoot,
484
+ paths: normalizedRequestedPaths,
485
+ auditedFiles: auditResult.auditedFiles,
486
+ findings: auditResult.findings,
487
+ highestValueFollowUpCandidates: auditResult.highestValueFollowUpCandidates,
488
+ scopeBoundary: {
489
+ mode: "bounded_paths_only",
490
+ fullRepoScan: false,
491
+ },
492
+ };
493
+ const serializedPayload = `${JSON.stringify(payload)}\n`;
494
+ if (typeof options.output === "string") {
495
+ const outputPath = path.resolve(cwd, options.output);
496
+ await mkdir(path.dirname(outputPath), { recursive: true });
497
+ await writeFile(outputPath, serializedPayload, "utf8");
498
+ }
499
+ stdout.write(serializedPayload);
500
+ return payload;
501
+ }
502
+ if (isDirectCliRun(import.meta.url)) {
503
+ runCli()
504
+ .then((result) => {
505
+ if (result?.ok === false) {
506
+ process.exitCode = 1;
507
+ }
508
+ })
509
+ .catch((error) => {
510
+ process.stderr.write(`${formatCliError(error)}\n`);
511
+ process.exitCode = 1;
512
+ });
513
+ }