dev-loops 0.6.0 → 0.7.1

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 (172) hide show
  1. package/.claude/.claude-plugin/plugin.json +1 -1
  2. package/.claude/agents/dev-loop.md +2 -1
  3. package/.claude/agents/review.md +2 -1
  4. package/.claude/commands/{auto.md → loop-auto.md} +1 -1
  5. package/.claude/commands/loop-continue.md +15 -0
  6. package/.claude/commands/loop-enqueue.md +22 -0
  7. package/.claude/commands/loop-grill.md +17 -0
  8. package/.claude/commands/{info.md → loop-info.md} +2 -2
  9. package/.claude/commands/loop-queue-status.md +24 -0
  10. package/.claude/commands/{start-spike.md → loop-start-spike.md} +1 -1
  11. package/.claude/commands/{start.md → loop-start.md} +1 -1
  12. package/.claude/commands/{status.md → loop-status.md} +1 -1
  13. package/.claude/hooks/_bash-command-classify.mjs +333 -29
  14. package/.claude/hooks/_hook-decisions.mjs +138 -15
  15. package/.claude/hooks/pre-tool-use-bash-gate.mjs +36 -15
  16. package/.claude/skills/copilot-pr-followup/SKILL.md +24 -12
  17. package/.claude/skills/dev-loop/SKILL.md +15 -11
  18. package/.claude/skills/docs/acceptance-criteria-verification.md +6 -2
  19. package/.claude/skills/docs/anti-patterns.md +2 -1
  20. package/.claude/skills/docs/artifact-authority-contract.md +22 -4
  21. package/.claude/skills/docs/copilot-loop-operations.md +1 -1
  22. package/.claude/skills/docs/cross-harness-regression-contract.md +60 -0
  23. package/.claude/skills/docs/issue-intake-procedure.md +1 -0
  24. package/.claude/skills/docs/local-planning-flow.md +1 -1
  25. package/.claude/skills/docs/local-planning-worked-example.md +1 -1
  26. package/.claude/skills/docs/merge-preconditions.md +17 -1
  27. package/.claude/skills/docs/plan-file-contract.md +1 -1
  28. package/.claude/skills/docs/public-dev-loop-contract.md +1 -1
  29. package/.claude/skills/docs/retrospective-checkpoint-contract.md +90 -76
  30. package/.claude/skills/docs/ui-e2e-scoping-step.md +32 -0
  31. package/.claude/skills/docs/workflow-handoff-contract.md +39 -0
  32. package/.claude/skills/local-implementation/SKILL.md +25 -14
  33. package/.claude/skills/loop-grill/SKILL.md +163 -0
  34. package/AGENTS.md +1 -0
  35. package/CHANGELOG.md +95 -0
  36. package/README.md +9 -9
  37. package/agents/dev-loop.agent.md +3 -2
  38. package/agents/developer.agent.md +1 -1
  39. package/agents/docs.agent.md +1 -1
  40. package/agents/fixer.agent.md +1 -1
  41. package/agents/quality.agent.md +1 -1
  42. package/agents/refiner.agent.md +1 -1
  43. package/agents/review.agent.md +3 -2
  44. package/cli/index.mjs +35 -42
  45. package/extension/README.md +4 -4
  46. package/extension/checks.ts +1 -5
  47. package/extension/harness-types.ts +1 -0
  48. package/extension/index.ts +6 -0
  49. package/extension/pi-extension-adapter.ts +2 -0
  50. package/extension/presentation.ts +7 -19
  51. package/package.json +9 -10
  52. package/scripts/github/capture-review-threads.mjs +2 -9
  53. package/scripts/github/comment-issue.mjs +2 -9
  54. package/scripts/github/create-label.mjs +133 -0
  55. package/scripts/github/detect-checkpoint-evidence.mjs +170 -14
  56. package/scripts/github/detect-linked-issue-pr.mjs +22 -6
  57. package/scripts/github/edit-pr.mjs +259 -0
  58. package/scripts/github/fetch-ci-logs.mjs +2 -9
  59. package/scripts/github/list-issues.mjs +2 -9
  60. package/scripts/github/manage-sub-issues.mjs +46 -10
  61. package/scripts/github/offer-human-handoff.mjs +8 -5
  62. package/scripts/github/post-gate-findings.mjs +14 -2
  63. package/scripts/github/probe-ci-status.mjs +8 -2
  64. package/scripts/github/probe-copilot-review.mjs +21 -14
  65. package/scripts/github/ready-for-review.mjs +26 -8
  66. package/scripts/github/reconcile-draft-gate.mjs +7 -3
  67. package/scripts/github/reply-resolve-review-thread.mjs +16 -5
  68. package/scripts/github/reply-resolve-review-threads.mjs +69 -7
  69. package/scripts/github/request-copilot-review.mjs +8 -2
  70. package/scripts/github/resolve-handoff-candidates.mjs +7 -3
  71. package/scripts/github/resolve-tracker-local-spec.mjs +9 -3
  72. package/scripts/github/stage-reviewer-draft.mjs +10 -2
  73. package/scripts/github/tick-verified-checkboxes.mjs +202 -0
  74. package/scripts/github/upsert-checkpoint-verdict.mjs +31 -10
  75. package/scripts/github/verify-fresh-review-context.mjs +150 -31
  76. package/scripts/github/view-pr.mjs +150 -0
  77. package/scripts/github/write-gate-context.mjs +248 -61
  78. package/scripts/github/write-gate-findings-log.mjs +75 -1
  79. package/scripts/lib/jq-output.mjs +18 -0
  80. package/scripts/loop/_post-convergence-change.mjs +211 -0
  81. package/scripts/loop/_pr-runner-coordination.mjs +70 -0
  82. package/scripts/loop/_repo-root-resolver.mjs +47 -0
  83. package/scripts/loop/build-handoff-envelope.mjs +14 -4
  84. package/scripts/loop/check-retro-tooling.mjs +14 -3
  85. package/scripts/loop/checkpoint-contract.mjs +7 -4
  86. package/scripts/loop/cleanup-worktree.mjs +12 -3
  87. package/scripts/loop/conductor-monitor.mjs +12 -18
  88. package/scripts/loop/copilot-pr-handoff.mjs +135 -14
  89. package/scripts/loop/debt-remediate.mjs +24 -12
  90. package/scripts/loop/detect-change-scope.mjs +36 -7
  91. package/scripts/loop/detect-copilot-loop-state.mjs +11 -5
  92. package/scripts/loop/detect-copilot-session-activity.mjs +7 -3
  93. package/scripts/loop/detect-initial-copilot-pr-state.mjs +7 -3
  94. package/scripts/loop/detect-internal-only-pr.mjs +8 -2
  95. package/scripts/loop/detect-issue-refinement-artifact.mjs +6 -3
  96. package/scripts/loop/detect-pr-gate-coordination-state.mjs +149 -66
  97. package/scripts/loop/detect-reviewer-loop-state.mjs +12 -2
  98. package/scripts/loop/detect-tracker-first-loop-state.mjs +9 -1
  99. package/scripts/loop/detect-tracker-pr-state.mjs +10 -3
  100. package/scripts/loop/ensure-worktree.mjs +8 -3
  101. package/scripts/loop/info.mjs +12 -4
  102. package/scripts/loop/inspect-run-viewer/constants.mjs +4 -18
  103. package/scripts/loop/inspect-run-viewer/vendor/mermaid.min.js +3405 -0
  104. package/scripts/loop/inspect-run-viewer-ci-changes.mjs +18 -6
  105. package/scripts/loop/inspect-run-viewer.mjs +67 -4
  106. package/scripts/loop/inspect-run.mjs +8 -2
  107. package/scripts/loop/outer-loop.mjs +8 -4
  108. package/scripts/loop/pr-runner-coordination.mjs +2 -9
  109. package/scripts/loop/pre-commit-branch-guard.mjs +16 -10
  110. package/scripts/loop/pre-flight-gate.mjs +9 -9
  111. package/scripts/loop/pre-pr-ready-gate.mjs +8 -4
  112. package/scripts/loop/pre-write-remote-freshness-guard.mjs +13 -4
  113. package/scripts/loop/provision-worktree.mjs +74 -3
  114. package/scripts/loop/resolve-dev-loop-startup.mjs +76 -7
  115. package/scripts/loop/resolve-gate-dispatch.mjs +134 -0
  116. package/scripts/loop/resolve-pr-conflicts.mjs +14 -7
  117. package/scripts/loop/run-conductor-cycle.mjs +8 -2
  118. package/scripts/loop/run-queue.mjs +18 -9
  119. package/scripts/loop/run-refinement-audit.mjs +8 -9
  120. package/scripts/loop/run-watch-cycle.mjs +2 -9
  121. package/scripts/loop/sanctioned-commands.mjs +104 -0
  122. package/scripts/loop/steer-loop.mjs +29 -16
  123. package/scripts/loop/validate-pr-body-spec.mjs +207 -0
  124. package/scripts/loop/watch-initial-copilot-pr.mjs +8 -3
  125. package/scripts/pages/build-site.mjs +59 -8
  126. package/scripts/pages/build-state-atlas.mjs +441 -0
  127. package/scripts/projects/_resolve-project.mjs +148 -0
  128. package/scripts/projects/add-queue-item.mjs +60 -54
  129. package/scripts/projects/archive-done-items.mjs +49 -84
  130. package/scripts/projects/ensure-queue-board.mjs +10 -36
  131. package/scripts/projects/list-queue-items.mjs +116 -65
  132. package/scripts/projects/move-queue-item.mjs +28 -49
  133. package/scripts/projects/reconcile-queue.mjs +253 -0
  134. package/scripts/projects/reorder-queue-item.mjs +41 -47
  135. package/scripts/projects/resolve-active-board-item.mjs +108 -46
  136. package/scripts/projects/sync-item-status.mjs +15 -10
  137. package/scripts/refine/_refine-helpers.mjs +21 -5
  138. package/scripts/refine/exit-spike.mjs +18 -8
  139. package/scripts/refine/promote-plan.mjs +22 -16
  140. package/scripts/refine/prose-linkage-detector.mjs +1 -1
  141. package/scripts/refine/refine-plan-file.mjs +13 -4
  142. package/scripts/refine/refinement-completeness-checker.mjs +1 -1
  143. package/scripts/refine/scaffold-spike-file.mjs +4 -8
  144. package/scripts/refine/scope-boundary-cross-checker.mjs +1 -1
  145. package/scripts/refine/tree-integrity-validator.mjs +1 -1
  146. package/scripts/refine/validate-plan-file.mjs +1 -1
  147. package/scripts/refine/validate-spike-file.mjs +1 -1
  148. package/scripts/refine/verify.mjs +11 -6
  149. package/scripts/release/assert-core-dependency-version.mjs +123 -0
  150. package/scripts/release/extract-changelog-section.mjs +16 -2
  151. package/skills/copilot-pr-followup/SKILL.md +24 -12
  152. package/skills/dev-loop/SKILL.md +10 -6
  153. package/skills/docs/acceptance-criteria-verification.md +6 -2
  154. package/skills/docs/anti-patterns.md +2 -1
  155. package/skills/docs/artifact-authority-contract.md +22 -4
  156. package/skills/docs/copilot-loop-operations.md +1 -1
  157. package/skills/docs/cross-harness-regression-contract.md +60 -0
  158. package/skills/docs/issue-intake-procedure.md +1 -0
  159. package/skills/docs/local-planning-flow.md +1 -1
  160. package/skills/docs/local-planning-worked-example.md +1 -1
  161. package/skills/docs/merge-preconditions.md +17 -1
  162. package/skills/docs/plan-file-contract.md +1 -1
  163. package/skills/docs/public-dev-loop-contract.md +1 -1
  164. package/skills/docs/retrospective-checkpoint-contract.md +90 -76
  165. package/skills/docs/ui-e2e-scoping-step.md +32 -0
  166. package/skills/docs/workflow-handoff-contract.md +39 -0
  167. package/skills/local-implementation/SKILL.md +25 -14
  168. package/skills/loop-grill/SKILL.md +165 -0
  169. package/.claude/commands/continue.md +0 -15
  170. package/scripts/loop/conductor.mjs +0 -233
  171. package/scripts/loop/detect-stale-runner.mjs +0 -265
  172. package/scripts/loop/pre-push-main-guard.mjs +0 -117
@@ -1,265 +0,0 @@
1
- #!/usr/bin/env node
2
- import process from "node:process";
3
- import { buildParseError, formatCliError, isDirectCliRun } from "../_core-helpers.mjs";
4
- import { parsePrNumber, requireTokenValue } from "../_cli-primitives.mjs";
5
- import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
6
- import { resolveRunId as resolveEnvRunId } from "@dev-loops/core/loop/run-context";
7
- import { parseArgs } from "node:util";
8
- import {
9
- detectStaleRunner,
10
- STALE_RUNNER_ERROR,
11
- } from "./_stale-runner-detection.mjs";
12
- const USAGE = `Usage: detect-stale-runner.mjs --repo <owner/name> --pr <number>
13
- Detect whether the active runner for a PR is stale or has received an exit
14
- signal. Fails closed with status "stale_runner" or "exit_signal_recorded" so
15
- the pre-merge guard can refuse to proceed.
16
- Required:
17
- --repo <owner/name> Repository slug (e.g. owner/repo)
18
- --pr <number> Pull request number
19
- Optional:
20
- --stale-runner-max-age-ms <ms>
21
- Override the staleness threshold (default 30 minutes,
22
- or $DEVLOOPS_STALE_RUNNER_MAX_AGE_MS).
23
- --run-id <id> Override the active run id (default: read from
24
- DEVLOOPS_RUN_ID). When supplied, the detector additionally
25
- verifies the current run id is still the active owner.
26
- Output (stdout, JSON; always includes staleRunnerCheck):
27
- {
28
- "ok": true,
29
- "repo": "owner/repo",
30
- "pr": 17,
31
- "status": "fresh_runner",
32
- "activeRun": { "runId": "...", "claimedAt": "...", "updatedAt": "..." },
33
- "staleRunnerCheck": {
34
- "ok": true,
35
- "failures": []
36
- }
37
- }
38
- or on failure:
39
- {
40
- "ok": false,
41
- "error": "stale_runner" | "exit_signal_recorded",
42
- "message": "...",
43
- "staleRunnerCheck": {
44
- "ok": false,
45
- "failures": ["stale runner: run X claimed N ms ago, last updated M ms ago (max age K ms)"]
46
- }
47
- }
48
- Exit codes:
49
- 0 Success / fresh runner / no owner record
50
- 1 Argument error or stale/exit-signal condition detected`.trim();
51
- const parseError = buildParseError(USAGE);
52
- function parseCliArgs(argv) {
53
- const options = {
54
- help: false,
55
- repo: undefined,
56
- pr: undefined,
57
- staleRunnerMaxAgeMs: undefined,
58
- runId: undefined,
59
- };
60
- const { tokens } = parseArgs({
61
- args: [...argv],
62
- options: {
63
- help: { type: "boolean", short: "h" },
64
- repo: { type: "string" },
65
- pr: { type: "string" },
66
- "stale-runner-max-age-ms": { type: "string" },
67
- "run-id": { type: "string" },
68
- },
69
- allowPositionals: true,
70
- strict: false,
71
- tokens: true,
72
- });
73
- for (const token of tokens) {
74
- if (token.kind === "positional") {
75
- throw parseError(`Unknown argument: ${token.value}`);
76
- }
77
- if (token.kind !== "option") {
78
- continue;
79
- }
80
- if (token.name === "help") {
81
- options.help = true;
82
- return options;
83
- }
84
- if (token.name === "repo") {
85
- options.repo = requireTokenValue(token, parseError).trim();
86
- continue;
87
- }
88
- if (token.name === "pr") {
89
- options.pr = parsePrNumber(requireTokenValue(token, parseError), parseError);
90
- continue;
91
- }
92
- if (token.name === "stale-runner-max-age-ms") {
93
- const raw = requireTokenValue(token, parseError).trim();
94
- const parsed = Number(raw);
95
- if (!Number.isFinite(parsed) || parsed <= 0) {
96
- throw parseError(`--stale-runner-max-age-ms must be a positive integer (ms), got: ${raw}`);
97
- }
98
- options.staleRunnerMaxAgeMs = Math.floor(parsed);
99
- continue;
100
- }
101
- if (token.name === "run-id") {
102
- options.runId = requireTokenValue(token, parseError).trim();
103
- continue;
104
- }
105
- throw parseError(`Unknown argument: ${token.rawName}`);
106
- }
107
- if (options.repo === undefined || options.pr === undefined) {
108
- throw parseError("detect-stale-runner requires both --repo <owner/name> and --pr <number>");
109
- }
110
- try {
111
- parseRepoSlug(options.repo);
112
- } catch (error) {
113
- throw parseError(error instanceof Error ? error.message : String(error));
114
- }
115
- return options;
116
- }
117
- function resolveRunId(explicitRunId, env) {
118
- if (typeof explicitRunId === "string" && explicitRunId.trim().length > 0) {
119
- return explicitRunId.trim();
120
- }
121
- return resolveEnvRunId(env);
122
- }
123
- function buildStaleRunnerCheck(detection) {
124
- if (detection.status === "no_owner_record") {
125
- return {
126
- ok: true,
127
- failures: [],
128
- };
129
- }
130
- if (detection.status === "exit_signal_recorded") {
131
- return {
132
- ok: false,
133
- failures: [`exit signal recorded for run ${detection.activeRun?.runId ?? "unknown"}: refuse to merge`],
134
- };
135
- }
136
- if (detection.status === "stale_runner") {
137
- return {
138
- ok: false,
139
- failures: [
140
- `stale runner: run ${detection.staleRunner.runId} claimed ${detection.staleRunner.claimedAgeMs}ms ago, last updated ${detection.staleRunner.updatedAgeMs}ms ago (max age ${detection.staleRunner.maxAgeMs}ms)`,
141
- ],
142
- };
143
- }
144
- return { ok: true, failures: [] };
145
- }
146
- export async function runDetectStaleRunner(options, { env = process.env, cwd = process.cwd() } = {}) {
147
- const detection = await detectStaleRunner({
148
- repo: options.repo,
149
- pr: options.pr,
150
- maxAgeMs: options.staleRunnerMaxAgeMs,
151
- cwd,
152
- });
153
- const explicitRunId = resolveRunId(options.runId, env);
154
- const ownershipLost = explicitRunId !== null
155
- && detection.activeRun !== null
156
- && detection.activeRun.runId !== explicitRunId;
157
- const ownershipMissing = explicitRunId !== null && detection.activeRun === null;
158
- const staleRunnerCheck = buildStaleRunnerCheck(detection);
159
- if (ownershipMissing) {
160
- return {
161
- ok: false,
162
- error: "ownership_lost",
163
- repo: options.repo.trim().toLowerCase(),
164
- pr: options.pr,
165
- status: "ownership_lost",
166
- activeRun: null,
167
- runId: explicitRunId,
168
- exitSignals: [],
169
- filePath: detection.filePath,
170
- maxAgeMs: detection.maxAgeMs,
171
- message: `Stale-runner check: run ${explicitRunId} is no longer the active owner of ${options.repo}#${options.pr}; no active owner record exists.`,
172
- staleRunnerCheck: {
173
- ok: false,
174
- failures: [`ownership_lost: no active owner record exists; run ${explicitRunId} is not the owner`],
175
- },
176
- };
177
- }
178
- if (ownershipLost) {
179
- return {
180
- ok: false,
181
- error: "ownership_lost",
182
- repo: options.repo.trim().toLowerCase(),
183
- pr: options.pr,
184
- status: "ownership_lost",
185
- activeRun: detection.activeRun,
186
- runId: explicitRunId,
187
- exitSignals: detection.exitSignal?.signals ?? [],
188
- filePath: detection.filePath,
189
- maxAgeMs: detection.maxAgeMs,
190
- message: `Stale-runner check: run ${explicitRunId} is no longer the active owner of ${options.repo}#${options.pr}; current owner is ${detection.activeRun?.runId}.`,
191
- staleRunnerCheck: {
192
- ok: false,
193
- failures: [`ownership_lost: active owner is ${detection.activeRun?.runId ?? "unknown"}, not ${explicitRunId}`],
194
- },
195
- };
196
- }
197
- if (detection.status === "exit_signal_recorded") {
198
- return {
199
- ok: false,
200
- error: STALE_RUNNER_ERROR.EXIT_SIGNAL_RECORDED,
201
- repo: options.repo.trim().toLowerCase(),
202
- pr: options.pr,
203
- status: "exit_signal_recorded",
204
- activeRun: detection.activeRun,
205
- runId: explicitRunId,
206
- exitSignals: detection.exitSignal?.signals ?? [],
207
- filePath: detection.filePath,
208
- maxAgeMs: detection.maxAgeMs,
209
- message: detection.message,
210
- staleRunnerCheck,
211
- };
212
- }
213
- if (detection.status === "stale_runner") {
214
- return {
215
- ok: false,
216
- error: STALE_RUNNER_ERROR.STALE_RUNNER,
217
- repo: options.repo.trim().toLowerCase(),
218
- pr: options.pr,
219
- status: "stale_runner",
220
- activeRun: detection.activeRun,
221
- runId: explicitRunId,
222
- exitSignals: [],
223
- staleRunner: detection.staleRunner,
224
- filePath: detection.filePath,
225
- maxAgeMs: detection.maxAgeMs,
226
- message: detection.message,
227
- staleRunnerCheck,
228
- };
229
- }
230
- return {
231
- ok: true,
232
- repo: options.repo.trim().toLowerCase(),
233
- pr: options.pr,
234
- status: detection.status,
235
- activeRun: detection.activeRun,
236
- runId: explicitRunId,
237
- exitSignals: [],
238
- filePath: detection.filePath,
239
- maxAgeMs: detection.maxAgeMs,
240
- staleRunnerCheck,
241
- };
242
- }
243
- async function main() {
244
- try {
245
- const options = parseCliArgs(process.argv.slice(2));
246
- if (options.help) {
247
- console.log(USAGE);
248
- return;
249
- }
250
- const result = await runDetectStaleRunner(options, { env: process.env });
251
- if (!result.ok) {
252
- console.error(JSON.stringify(result));
253
- process.exitCode = 1;
254
- return;
255
- }
256
- console.log(JSON.stringify(result));
257
- } catch (error) {
258
- const payload = formatCliError(error, { usage: USAGE });
259
- console.error(JSON.stringify(payload));
260
- process.exitCode = 1;
261
- }
262
- }
263
- if (isDirectCliRun(import.meta.url)) {
264
- await main();
265
- }
@@ -1,117 +0,0 @@
1
- #!/usr/bin/env node
2
- import { createInterface } from "node:readline";
3
- import { buildParseError, formatCliError, isDirectCliRun } from "../_core-helpers.mjs";
4
- import { parseArgs } from "node:util";
5
-
6
- const DEVLOOPS_PREPUSH_BYPASS_VAR = "DEVLOOPS_PREPUSH_BYPASS";
7
- const BLOCKED_REFS = ["refs/heads/main"];
8
-
9
- const USAGE = `Usage:
10
- pre-push-main-guard.mjs
11
-
12
- Reads pre-push hook input from stdin (Git pre-push hook protocol).
13
- Blocks direct pushes to protected refs (by default: refs/heads/main).
14
-
15
- Exit codes:
16
- 0 Push allowed (non-main ref, or bypassed)
17
- 1 Push blocked (target is a protected ref)
18
-
19
- Bypass:
20
- DEVLOOPS_PREPUSH_BYPASS=1 Skip all checks (for emergencies only).
21
- Preferred: push a feature branch and open a PR.`.trim();
22
-
23
- const parseError = buildParseError(USAGE);
24
-
25
- /**
26
- * Parse pre-push hook input lines.
27
- *
28
- * Git pre-push hook protocol: each line is four whitespace-delimited fields:
29
- * <local ref> <local sha> <remote ref> <remote sha>
30
- * The fourth field (remote sha) is all-zeros for new branches.
31
- *
32
- * This parser accepts lines with at least three fields (treating the
33
- * optional fourth field as null when absent). Malformed non-empty
34
- * lines with fewer than three fields are ignored silently — the guard
35
- * prefers to fail-open on unparseable input rather than silently
36
- * blocking unknown refs.
37
- */
38
- async function readPushRefs(input) {
39
- const refs = [];
40
- const rl = createInterface({ input, crlfDelay: Infinity });
41
- for await (const line of rl) {
42
- const trimmed = line.trim();
43
- if (!trimmed) continue;
44
- const parts = trimmed.split(/\s+/);
45
- if (parts.length >= 3) {
46
- refs.push({ localRef: parts[0], localSha: parts[1], remoteRef: parts[2], remoteSha: parts[3] || null });
47
- }
48
- }
49
- return refs;
50
- }
51
-
52
- /**
53
- * Check whether any push target is a blocked ref.
54
- */
55
- function findBlockedRef(refs) {
56
- for (const ref of refs) {
57
- if (BLOCKED_REFS.includes(ref.remoteRef)) {
58
- return ref.remoteRef;
59
- }
60
- }
61
- return null;
62
- }
63
-
64
- export function parsePrePushGuardCliArgs(argv) {
65
- const { tokens } = parseArgs({
66
- args: [...argv],
67
- options: {
68
- help: { type: "boolean", short: "h" },
69
- },
70
- allowPositionals: true,
71
- strict: false,
72
- tokens: true,
73
- });
74
- for (const token of tokens) {
75
- if (token.kind === "positional") {
76
- throw parseError(`Unknown argument: ${token.value}`);
77
- }
78
- if (token.kind !== "option") {
79
- continue;
80
- }
81
- if (token.name === "help") return { help: true };
82
- throw parseError(`Unknown argument: ${token.rawName}`);
83
- }
84
- return { help: false };
85
- }
86
-
87
- export async function runCli(argv = process.argv.slice(2), { stdout = process.stdout, stderr = process.stderr, stdin = process.stdin, env = process.env } = {}) {
88
- const options = parsePrePushGuardCliArgs(argv);
89
- if (options.help) { stdout.write(`${USAGE}\n`); return { ok: true, help: true }; }
90
-
91
- if ((env[DEVLOOPS_PREPUSH_BYPASS_VAR] ?? "").trim() === "1") {
92
- stdout.write(JSON.stringify({ ok: true, bypassed: true, reason: `${DEVLOOPS_PREPUSH_BYPASS_VAR}=1` }) + "\n");
93
- return { ok: true, bypassed: true };
94
- }
95
-
96
- const refs = await readPushRefs(stdin);
97
- const blockedRef = findBlockedRef(refs);
98
-
99
- if (blockedRef) {
100
- const payload = {
101
- ok: false,
102
- error: "direct_push_to_main_blocked",
103
- blockedRef,
104
- message: "Direct pushes to main branch are blocked. Push a feature branch and open a pull request instead.",
105
- };
106
- stderr.write(`${JSON.stringify(payload)}\n`);
107
- return payload;
108
- }
109
-
110
- const payload = { ok: true, blocked: false, refsChecked: refs.length };
111
- stdout.write(`${JSON.stringify(payload)}\n`);
112
- return payload;
113
- }
114
-
115
- if (isDirectCliRun(import.meta.url)) {
116
- runCli().then((result) => { if (result?.ok === false) { process.exitCode = 1; } }).catch((error) => { process.stderr.write(`${formatCliError(error)}\n`); process.exitCode = 1; });
117
- }