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.
- package/.pi/dev-loop/defaults.yaml +477 -0
- package/AGENTS.md +25 -0
- package/CHANGELOG.md +18 -0
- package/LICENSE +21 -0
- package/README.md +178 -0
- package/agents/dev-loop.agent.md +82 -0
- package/agents/developer.agent.md +37 -0
- package/agents/docs.agent.md +33 -0
- package/agents/fixer.agent.md +53 -0
- package/agents/quality.agent.md +28 -0
- package/agents/refiner.agent.md +87 -0
- package/agents/review.agent.md +64 -0
- package/cli/index.mjs +424 -0
- package/extension/README.md +233 -0
- package/extension/checks.ts +94 -0
- package/extension/index.ts +131 -0
- package/extension/post-merge-update.ts +512 -0
- package/extension/presentation.ts +107 -0
- package/lib/dev-loops-core.mjs +284 -0
- package/package.json +103 -0
- package/scripts/README.md +1007 -0
- package/scripts/_cli-primitives.mjs +10 -0
- package/scripts/_core-helpers.mjs +30 -0
- package/scripts/docs/validate-links.mjs +567 -0
- package/scripts/docs/validate-no-duplicate-rules.mjs +250 -0
- package/scripts/github/_review-thread-mutations.mjs +214 -0
- package/scripts/github/capture-review-threads.mjs +180 -0
- package/scripts/github/create-draft-pr.mjs +108 -0
- package/scripts/github/detect-checkpoint-evidence.mjs +393 -0
- package/scripts/github/detect-linked-issue-pr.mjs +331 -0
- package/scripts/github/manage-sub-issues.mjs +394 -0
- package/scripts/github/probe-copilot-review.mjs +323 -0
- package/scripts/github/ready-for-review.mjs +93 -0
- package/scripts/github/reconcile-draft-gate.mjs +328 -0
- package/scripts/github/reply-resolve-review-thread.mjs +42 -0
- package/scripts/github/reply-resolve-review-threads.mjs +329 -0
- package/scripts/github/request-copilot-review.mjs +551 -0
- package/scripts/github/resolve-tracker-local-spec.mjs +205 -0
- package/scripts/github/stage-reviewer-draft.mjs +191 -0
- package/scripts/github/upsert-checkpoint-verdict.mjs +694 -0
- package/scripts/github/verify-fresh-review-context.mjs +125 -0
- package/scripts/github/write-gate-findings-log.mjs +212 -0
- package/scripts/loop/_checkpoint-io.mjs +55 -0
- package/scripts/loop/_checkpoint-paths.mjs +28 -0
- package/scripts/loop/_handoff-contract.mjs +230 -0
- package/scripts/loop/_inspect-run-viewer-adapter.mjs +345 -0
- package/scripts/loop/_loop-evidence.mjs +32 -0
- package/scripts/loop/_pr-runner-coordination.mjs +611 -0
- package/scripts/loop/_stale-runner-detection.mjs +145 -0
- package/scripts/loop/_steering-state-file.mjs +134 -0
- package/scripts/loop/build-handoff-envelope.mjs +181 -0
- package/scripts/loop/checkpoint-contract.mjs +49 -0
- package/scripts/loop/conductor-monitor.mjs +1850 -0
- package/scripts/loop/conductor.mjs +214 -0
- package/scripts/loop/copilot-pr-handoff.mjs +493 -0
- package/scripts/loop/debt-remediate.mjs +304 -0
- package/scripts/loop/detect-change-scope.mjs +102 -0
- package/scripts/loop/detect-copilot-loop-state.mjs +454 -0
- package/scripts/loop/detect-copilot-session-activity.mjs +186 -0
- package/scripts/loop/detect-initial-copilot-pr-state.mjs +318 -0
- package/scripts/loop/detect-internal-only-pr.mjs +270 -0
- package/scripts/loop/detect-issue-refinement-artifact.mjs +163 -0
- package/scripts/loop/detect-pr-gate-coordination-state.mjs +509 -0
- package/scripts/loop/detect-reviewer-loop-state.mjs +231 -0
- package/scripts/loop/detect-stale-runner.mjs +250 -0
- package/scripts/loop/detect-tracker-first-loop-state.mjs +76 -0
- package/scripts/loop/detect-tracker-pr-state.mjs +102 -0
- package/scripts/loop/info.mjs +267 -0
- package/scripts/loop/inspect-run-viewer/cli.mjs +117 -0
- package/scripts/loop/inspect-run-viewer/constants.mjs +80 -0
- package/scripts/loop/inspect-run-viewer/graph.mjs +757 -0
- package/scripts/loop/inspect-run-viewer/handoff-envelope-renderer.mjs +398 -0
- package/scripts/loop/inspect-run-viewer/inbox.mjs +308 -0
- package/scripts/loop/inspect-run-viewer/managed-instance.mjs +750 -0
- package/scripts/loop/inspect-run-viewer/rendering.mjs +411 -0
- package/scripts/loop/inspect-run-viewer/server.mjs +638 -0
- package/scripts/loop/inspect-run-viewer/shared.mjs +103 -0
- package/scripts/loop/inspect-run-viewer/status.mjs +715 -0
- package/scripts/loop/inspect-run-viewer-ci-changes.mjs +77 -0
- package/scripts/loop/inspect-run-viewer.mjs +82 -0
- package/scripts/loop/inspect-run.mjs +382 -0
- package/scripts/loop/outer-loop.mjs +419 -0
- package/scripts/loop/pr-runner-coordination.mjs +143 -0
- package/scripts/loop/pre-commit-branch-guard.mjs +68 -0
- package/scripts/loop/pre-flight-gate.mjs +236 -0
- package/scripts/loop/pre-pr-ready-gate.mjs +183 -0
- package/scripts/loop/pre-push-main-guard.mjs +103 -0
- package/scripts/loop/pre-write-remote-freshness-guard.mjs +32 -0
- package/scripts/loop/print-gates.mjs +42 -0
- package/scripts/loop/resolve-dev-loop-startup.mjs +533 -0
- package/scripts/loop/run-conductor-cycle.mjs +322 -0
- package/scripts/loop/run-queue.mjs +124 -0
- package/scripts/loop/run-refinement-audit.mjs +513 -0
- package/scripts/loop/run-watch-cycle.mjs +358 -0
- package/scripts/loop/steer-loop.mjs +841 -0
- package/scripts/loop/ui-designer-review-contract.mjs +76 -0
- package/scripts/loop/watch-initial-copilot-pr.mjs +253 -0
- package/scripts/projects/add-queue-item.mjs +528 -0
- package/scripts/projects/ensure-queue-board.mjs +837 -0
- package/scripts/projects/list-queue-items.mjs +489 -0
- package/scripts/projects/move-queue-item.mjs +549 -0
- package/scripts/projects/reorder-queue-item.mjs +518 -0
- package/scripts/refine/_refine-helpers.mjs +258 -0
- package/scripts/refine/prose-linkage-detector.mjs +92 -0
- package/scripts/refine/refinement-completeness-checker.mjs +88 -0
- package/scripts/refine/scope-boundary-cross-checker.mjs +163 -0
- package/scripts/refine/tree-integrity-validator.mjs +211 -0
- package/scripts/refine/verify.mjs +178 -0
- package/scripts/repo-wiki-local.mjs +156 -0
- package/scripts/repo-wiki.mjs +119 -0
- package/skills/copilot-pr-followup/SKILL.md +380 -0
- package/skills/dev-loop/SKILL.md +141 -0
- package/skills/dev-loop/scripts/dev-mode-context.mjs +152 -0
- package/skills/dev-loop/scripts/dev-mode-context.test.mjs +80 -0
- package/skills/dev-loop/scripts/init-phase.mjs +71 -0
- package/skills/dev-loop/scripts/log-bash-exit-1.mjs +25 -0
- package/skills/dev-loop/scripts/phase-files.mjs +29 -0
- package/skills/dev-loop/scripts/post-gate-verdict-fallback.mjs +480 -0
- package/skills/dev-loop/scripts/post-gate-verdict-fallback.test.mjs +732 -0
- package/skills/dev-loop/scripts/render-template.mjs +82 -0
- package/skills/dev-loop/scripts/render-template.test.mjs +63 -0
- package/skills/dev-loop/templates/bootstrap-agents.md +26 -0
- package/skills/dev-loop/templates/bootstrap-implementation-state.md +31 -0
- package/skills/dev-loop/templates/bootstrap-implementation-workflow.md +17 -0
- package/skills/dev-loop/templates/dev-mode-retrospective.md +15 -0
- package/skills/dev-loop/templates/dev-mode-review.md +17 -0
- package/skills/dev-loop/templates/dev-mode-skill-changes.md +11 -0
- package/skills/dev-loop/templates/merged-phase-plan.md +19 -0
- package/skills/dev-loop/templates/phase-doc.md +27 -0
- package/skills/dev-loop/templates/phase-summary.md +13 -0
- package/skills/dev-loop/templates/phase-variant.md +15 -0
- package/skills/dev-loop/templates/retrospective.md +11 -0
- package/skills/dev-loop/templates/review.md +32 -0
- package/skills/dev-loop/templates/ui-vision-review.md +55 -0
- package/skills/docs/acceptance-criteria-verification.md +21 -0
- package/skills/docs/anti-patterns.md +21 -0
- package/skills/docs/artifact-authority-contract.md +119 -0
- package/skills/docs/confirmation-rules.md +28 -0
- package/skills/docs/copilot-ci-status-contract.md +52 -0
- package/skills/docs/copilot-loop-operations.md +233 -0
- package/skills/docs/debt-remediation-contract.md +107 -0
- package/skills/docs/entrypoint-strategies.md +115 -0
- package/skills/docs/epic-tree-refinement-procedure.md +234 -0
- package/skills/docs/issue-intake-procedure.md +235 -0
- package/skills/docs/main-agent-contract.md +72 -0
- package/skills/docs/merge-preconditions.md +29 -0
- package/skills/docs/pr-lifecycle-contract.md +209 -0
- package/skills/docs/public-dev-loop-contract.md +497 -0
- package/skills/docs/retrospective-checkpoint-contract.md +159 -0
- package/skills/docs/stop-conditions.md +29 -0
- package/skills/docs/structural-quality.md +42 -0
- package/skills/docs/tracker-first-loop-state.md +281 -0
- package/skills/docs/validation-policy.md +27 -0
- package/skills/docs/workflow-handoff-contract.md +135 -0
- package/skills/final-approval/SKILL.md +19 -0
- 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 };
|