dev-loops 0.6.0 → 0.7.2
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/.claude/.claude-plugin/plugin.json +1 -1
- package/.claude/agents/dev-loop.md +8 -7
- package/.claude/agents/fixer.md +1 -0
- package/.claude/agents/refiner.md +2 -2
- package/.claude/agents/review.md +4 -3
- package/.claude/commands/{auto.md → loop-auto.md} +1 -1
- package/.claude/commands/loop-continue.md +15 -0
- package/.claude/commands/loop-enqueue.md +22 -0
- package/.claude/commands/loop-grill.md +17 -0
- package/.claude/commands/{info.md → loop-info.md} +2 -2
- package/.claude/commands/loop-queue-status.md +24 -0
- package/.claude/commands/{start-spike.md → loop-start-spike.md} +2 -2
- package/.claude/commands/{start.md → loop-start.md} +1 -1
- package/.claude/commands/{status.md → loop-status.md} +1 -1
- package/.claude/hooks/_bash-command-classify.mjs +333 -29
- package/.claude/hooks/_hook-decisions.mjs +138 -15
- package/.claude/hooks/pre-tool-use-bash-gate.mjs +36 -15
- package/.claude/skills/copilot-pr-followup/SKILL.md +60 -34
- package/.claude/skills/dev-loop/SKILL.md +16 -12
- package/.claude/skills/docs/acceptance-criteria-verification.md +8 -3
- package/.claude/skills/docs/anti-patterns.md +5 -3
- package/.claude/skills/docs/artifact-authority-contract.md +30 -11
- package/.claude/skills/docs/confirmation-rules.md +1 -0
- package/.claude/skills/docs/contract-style-guide.md +37 -0
- package/.claude/skills/docs/copilot-loop-operations.md +19 -15
- package/.claude/skills/docs/cross-harness-regression-contract.md +60 -0
- package/.claude/skills/docs/debt-remediation-contract.md +1 -1
- package/.claude/skills/docs/epic-tree-refinement-procedure.md +25 -22
- package/.claude/skills/docs/issue-intake-procedure.md +26 -19
- package/.claude/skills/docs/local-planning-flow.md +1 -1
- package/.claude/skills/docs/local-planning-worked-example.md +1 -1
- package/.claude/skills/docs/merge-preconditions.md +30 -12
- package/.claude/skills/docs/plan-file-contract.md +1 -1
- package/.claude/skills/docs/pr-lifecycle-contract.md +44 -35
- package/.claude/skills/docs/public-dev-loop-contract.md +60 -44
- package/.claude/skills/docs/retrospective-checkpoint-contract.md +106 -79
- package/.claude/skills/docs/spike-mode-contract.md +14 -7
- package/.claude/skills/docs/stop-conditions.md +30 -15
- package/.claude/skills/docs/tracker-first-loop-state.md +13 -8
- package/.claude/skills/docs/ui-e2e-scoping-step.md +32 -0
- package/.claude/skills/docs/validation-policy.md +4 -5
- package/.claude/skills/docs/workflow-handoff-contract.md +39 -0
- package/.claude/skills/local-implementation/SKILL.md +62 -145
- package/.claude/skills/loop-grill/SKILL.md +163 -0
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +101 -0
- package/README.md +9 -9
- package/agents/dev-loop.agent.md +11 -10
- package/agents/developer.agent.md +1 -1
- package/agents/docs.agent.md +1 -1
- package/agents/fixer.agent.md +2 -1
- package/agents/quality.agent.md +1 -1
- package/agents/refiner.agent.md +3 -3
- package/agents/review.agent.md +5 -4
- package/cli/index.mjs +35 -42
- package/extension/README.md +4 -4
- package/extension/checks.ts +1 -5
- package/extension/harness-types.ts +1 -0
- package/extension/index.ts +6 -0
- package/extension/pi-extension-adapter.ts +2 -0
- package/extension/presentation.ts +7 -19
- package/package.json +12 -12
- package/scripts/_core-helpers.mjs +2 -0
- package/scripts/claude/headless-dev-loop.mjs +1 -1
- package/scripts/docs/validate-rule-ownership.mjs +442 -0
- package/scripts/docs/validate-state-machine-conformance.mjs +1092 -0
- package/scripts/github/_review-thread-mutations.mjs +5 -2
- package/scripts/github/capture-review-threads.mjs +2 -9
- package/scripts/github/comment-issue.mjs +2 -9
- package/scripts/github/create-label.mjs +133 -0
- package/scripts/github/create-pr.mjs +119 -10
- package/scripts/github/detect-checkpoint-evidence.mjs +259 -18
- package/scripts/github/detect-linked-issue-pr.mjs +22 -6
- package/scripts/github/edit-pr.mjs +259 -0
- package/scripts/github/fetch-ci-logs.mjs +2 -9
- package/scripts/github/list-issues.mjs +2 -9
- package/scripts/github/list-review-threads.mjs +277 -0
- package/scripts/github/manage-sub-issues.mjs +46 -10
- package/scripts/github/offer-human-handoff.mjs +8 -5
- package/scripts/github/post-gate-findings.mjs +20 -5
- package/scripts/github/probe-ci-status.mjs +8 -2
- package/scripts/github/probe-copilot-review.mjs +21 -14
- package/scripts/github/ready-for-review.mjs +26 -8
- package/scripts/github/reconcile-draft-gate.mjs +7 -3
- package/scripts/github/reply-resolve-review-thread.mjs +16 -5
- package/scripts/github/reply-resolve-review-threads.mjs +69 -7
- package/scripts/github/request-copilot-review.mjs +98 -26
- package/scripts/github/resolve-handoff-candidates.mjs +7 -3
- package/scripts/github/resolve-tracker-local-spec.mjs +9 -3
- package/scripts/github/stage-reviewer-draft.mjs +10 -2
- package/scripts/github/tick-verified-checkboxes.mjs +202 -0
- package/scripts/github/upsert-checkpoint-verdict.mjs +105 -15
- package/scripts/github/verify-briefing-prefixes.mjs +191 -0
- package/scripts/github/verify-fresh-review-context.mjs +226 -32
- package/scripts/github/view-pr.mjs +150 -0
- package/scripts/github/wait-pr-checks.mjs +171 -0
- package/scripts/github/write-gate-context.mjs +458 -65
- package/scripts/github/write-gate-findings-log.mjs +128 -2
- package/scripts/lib/jq-output.mjs +18 -0
- package/scripts/loop/_post-convergence-change.mjs +211 -0
- package/scripts/loop/_pr-runner-coordination.mjs +70 -0
- package/scripts/loop/_repo-root-resolver.mjs +47 -0
- package/scripts/loop/build-handoff-envelope.mjs +14 -4
- package/scripts/loop/check-retro-tooling.mjs +14 -3
- package/scripts/loop/checkpoint-contract.mjs +7 -4
- package/scripts/loop/cleanup-worktree.mjs +12 -3
- package/scripts/loop/conductor-monitor.mjs +12 -18
- package/scripts/loop/copilot-pr-handoff.mjs +162 -14
- package/scripts/loop/debt-remediate.mjs +24 -12
- package/scripts/loop/detect-change-scope.mjs +38 -9
- package/scripts/loop/detect-copilot-loop-state.mjs +34 -8
- package/scripts/loop/detect-copilot-session-activity.mjs +7 -3
- package/scripts/loop/detect-initial-copilot-pr-state.mjs +7 -3
- package/scripts/loop/detect-internal-only-pr.mjs +8 -2
- package/scripts/loop/detect-issue-refinement-artifact.mjs +6 -3
- package/scripts/loop/detect-pr-gate-coordination-state.mjs +184 -69
- package/scripts/loop/detect-reviewer-loop-state.mjs +12 -2
- package/scripts/loop/detect-tracker-first-loop-state.mjs +9 -1
- package/scripts/loop/detect-tracker-pr-state.mjs +10 -3
- package/scripts/loop/ensure-worktree.mjs +8 -3
- package/scripts/loop/info.mjs +12 -4
- package/scripts/loop/inspect-run-viewer/constants.mjs +4 -18
- package/scripts/loop/inspect-run-viewer/vendor/mermaid.min.js +3405 -0
- package/scripts/loop/inspect-run-viewer-ci-changes.mjs +18 -6
- package/scripts/loop/inspect-run-viewer.mjs +67 -4
- package/scripts/loop/inspect-run.mjs +8 -2
- package/scripts/loop/outer-loop.mjs +8 -4
- package/scripts/loop/pr-runner-coordination.mjs +2 -9
- package/scripts/loop/pre-commit-branch-guard.mjs +16 -10
- package/scripts/loop/pre-flight-gate.mjs +9 -9
- package/scripts/loop/pre-pr-ready-gate.mjs +8 -4
- package/scripts/loop/pre-write-remote-freshness-guard.mjs +13 -4
- package/scripts/loop/provision-worktree.mjs +74 -3
- package/scripts/loop/resolve-dev-loop-startup.mjs +216 -10
- package/scripts/loop/resolve-gate-dispatch.mjs +134 -0
- package/scripts/loop/resolve-pr-conflicts.mjs +14 -7
- package/scripts/loop/run-conductor-cycle.mjs +8 -2
- package/scripts/loop/run-queue.mjs +18 -9
- package/scripts/loop/run-refinement-audit.mjs +8 -9
- package/scripts/loop/run-watch-cycle.mjs +2 -9
- package/scripts/loop/sanctioned-commands.mjs +106 -0
- package/scripts/loop/steer-loop.mjs +29 -16
- package/scripts/loop/validate-pr-body-spec.mjs +223 -0
- package/scripts/loop/watch-initial-copilot-pr.mjs +8 -3
- package/scripts/pages/build-site.mjs +59 -8
- package/scripts/pages/build-state-atlas.mjs +443 -0
- package/scripts/projects/_resolve-project.mjs +1 -0
- package/scripts/projects/add-queue-item.mjs +60 -54
- package/scripts/projects/archive-done-items.mjs +49 -84
- package/scripts/projects/ensure-queue-board.mjs +10 -36
- package/scripts/projects/list-queue-items.mjs +59 -382
- package/scripts/projects/move-queue-item.mjs +21 -449
- package/scripts/projects/reconcile-queue.mjs +253 -0
- package/scripts/projects/reorder-queue-item.mjs +43 -68
- package/scripts/projects/resolve-active-board-item.mjs +108 -46
- package/scripts/projects/sync-item-status.mjs +15 -10
- package/scripts/refine/_refine-helpers.mjs +21 -5
- package/scripts/refine/exit-spike.mjs +18 -8
- package/scripts/refine/promote-plan.mjs +22 -16
- package/scripts/refine/prose-linkage-detector.mjs +1 -1
- package/scripts/refine/refine-plan-file.mjs +13 -4
- package/scripts/refine/refinement-completeness-checker.mjs +1 -1
- package/scripts/refine/scaffold-spike-file.mjs +4 -8
- package/scripts/refine/scope-boundary-cross-checker.mjs +1 -1
- package/scripts/refine/tree-integrity-validator.mjs +1 -1
- package/scripts/refine/validate-plan-file.mjs +1 -1
- package/scripts/refine/validate-spike-file.mjs +1 -1
- package/scripts/refine/verify.mjs +11 -6
- package/scripts/release/assert-core-dependency-version.mjs +123 -0
- package/scripts/release/extract-changelog-section.mjs +16 -2
- package/skills/copilot-pr-followup/SKILL.md +60 -34
- package/skills/dev-loop/SKILL.md +11 -7
- package/skills/docs/acceptance-criteria-verification.md +8 -3
- package/skills/docs/anti-patterns.md +5 -3
- package/skills/docs/artifact-authority-contract.md +30 -11
- package/skills/docs/confirmation-rules.md +1 -0
- package/skills/docs/contract-style-guide.md +37 -0
- package/skills/docs/copilot-loop-operations.md +19 -15
- package/skills/docs/cross-harness-regression-contract.md +60 -0
- package/skills/docs/debt-remediation-contract.md +1 -1
- package/skills/docs/epic-tree-refinement-procedure.md +25 -22
- package/skills/docs/issue-intake-procedure.md +26 -19
- package/skills/docs/local-planning-flow.md +1 -1
- package/skills/docs/local-planning-worked-example.md +1 -1
- package/skills/docs/merge-preconditions.md +30 -12
- package/skills/docs/plan-file-contract.md +1 -1
- package/skills/docs/pr-lifecycle-contract.md +44 -35
- package/skills/docs/public-dev-loop-contract.md +60 -44
- package/skills/docs/required-rules.json +149 -0
- package/skills/docs/retrospective-checkpoint-contract.md +106 -79
- package/skills/docs/spike-mode-contract.md +14 -7
- package/skills/docs/stop-conditions.md +30 -15
- package/skills/docs/tracker-first-loop-state.md +13 -8
- package/skills/docs/ui-e2e-scoping-step.md +32 -0
- package/skills/docs/validation-policy.md +4 -5
- package/skills/docs/workflow-handoff-contract.md +39 -0
- package/skills/local-implementation/SKILL.md +62 -145
- package/skills/loop-grill/SKILL.md +165 -0
- package/.claude/commands/continue.md +0 -15
- package/scripts/docs/validate-no-duplicate-rules.mjs +0 -250
- package/scripts/loop/conductor.mjs +0 -233
- package/scripts/loop/detect-stale-runner.mjs +0 -265
- 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
|
-
}
|