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,480 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// post-gate-verdict-fallback.mjs
|
|
3
|
+
//
|
|
4
|
+
// Minimal gate-verdict-comment poster for the fallback path used when the
|
|
5
|
+
// `@dev-loops/core` package is not installed in the consumer repo and the
|
|
6
|
+
// full `scripts/github/upsert-checkpoint-verdict.mjs` helper is therefore
|
|
7
|
+
// unavailable. Posts the same visible comment format as the full helper, but
|
|
8
|
+
// without the full helper's idempotent same-head update, stale-head detection,
|
|
9
|
+
// gate-coordination validation, or internal-only PR short-circuit.
|
|
10
|
+
//
|
|
11
|
+
// Contract reference: docs/gate-review-comment-contract.md (rendered body must
|
|
12
|
+
// remain parser-stable for gate name and head SHA). The skill-bundled copy at
|
|
13
|
+
// skills/docs/gate-review-comment-contract.md inherits from the source-repo doc
|
|
14
|
+
// and is not a separate contract surface.
|
|
15
|
+
//
|
|
16
|
+
// Degraded semantics (vs. the full helper):
|
|
17
|
+
// - one-shot create only; no idempotent same-head update
|
|
18
|
+
// - no stale-head detection against existing comments
|
|
19
|
+
// - no gate-coordination state validation
|
|
20
|
+
// - no blocking-severity count enforcement (caller is responsible)
|
|
21
|
+
// - no internal-only PR short-circuit
|
|
22
|
+
//
|
|
23
|
+
// The script always emits a stderr warning explaining that fallback mode is
|
|
24
|
+
// active and the audit trail is degraded. On posting failure it fails closed
|
|
25
|
+
// with a non-zero exit so the calling agent does not silently proceed past
|
|
26
|
+
// the gate-comment requirement.
|
|
27
|
+
|
|
28
|
+
import { readFile } from "node:fs/promises";
|
|
29
|
+
import { spawn as defaultSpawn } from "node:child_process";
|
|
30
|
+
import { pathToFileURL } from "node:url";
|
|
31
|
+
|
|
32
|
+
const GATE_NAMES = new Set(["draft_gate", "pre_approval_gate"]);
|
|
33
|
+
const VERDICTS = new Set(["clean", "findings_present", "blocked"]);
|
|
34
|
+
function isSafeRepoSegment(segment) {
|
|
35
|
+
return typeof segment === "string"
|
|
36
|
+
&& segment.length > 0
|
|
37
|
+
&& segment !== "."
|
|
38
|
+
&& segment !== ".."
|
|
39
|
+
&& !/[\\/]/.test(segment)
|
|
40
|
+
&& !/\s/.test(segment);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isValidRepoSlug(repo) {
|
|
44
|
+
if (typeof repo !== "string") return false;
|
|
45
|
+
const trimmed = repo.trim();
|
|
46
|
+
const parts = trimmed.split("/");
|
|
47
|
+
if (parts.length !== 2) return false;
|
|
48
|
+
return isSafeRepoSegment(parts[0]) && isSafeRepoSegment(parts[1]);
|
|
49
|
+
}
|
|
50
|
+
const SHA_PATTERN = /^[0-9a-f]{7,64}$/i;
|
|
51
|
+
const USAGE = `Usage: post-gate-verdict-fallback.mjs --repo <owner/name> --pr <number> --head-sha <sha> --verdict <clean|findings_present|blocked> (--findings-summary <text> | --findings-file <path>) --next-action <text> [--gate <draft_gate|pre_approval_gate>] [--gh-command <path>]
|
|
52
|
+
Minimal fallback poster for draft_gate / pre_approval_gate checkpoint verdict comments.
|
|
53
|
+
Use only when @dev-loops/core is not installed; otherwise prefer scripts/github/upsert-checkpoint-verdict.mjs.
|
|
54
|
+
Required:
|
|
55
|
+
--repo <owner/name>
|
|
56
|
+
--pr <number>
|
|
57
|
+
--head-sha <sha> Full or 7+ char hex prefix
|
|
58
|
+
--verdict <clean|findings_present|blocked>
|
|
59
|
+
--findings-summary <text> Single-line summary
|
|
60
|
+
--findings-file <path> Read summary from file (preserves
|
|
61
|
+
newlines; takes precedence when
|
|
62
|
+
both are provided)
|
|
63
|
+
--next-action <text>
|
|
64
|
+
Optional:
|
|
65
|
+
--gate <draft_gate|pre_approval_gate> Defaults to draft_gate
|
|
66
|
+
--gh-command <path> Defaults to "gh"
|
|
67
|
+
Output (stdout, JSON):
|
|
68
|
+
{
|
|
69
|
+
"ok": true,
|
|
70
|
+
"action": "created",
|
|
71
|
+
"repo": "owner/repo",
|
|
72
|
+
"pr": 17,
|
|
73
|
+
"gate": "draft_gate",
|
|
74
|
+
"headSha": "abc1234",
|
|
75
|
+
"commentId": 101,
|
|
76
|
+
"commentUrl": "https://github.com/owner/repo/pull/17#issuecomment-101",
|
|
77
|
+
"fallback": true,
|
|
78
|
+
"warning": "..."
|
|
79
|
+
}
|
|
80
|
+
Exit codes:
|
|
81
|
+
0 Success
|
|
82
|
+
1 Argument error or gh failure`.trim();
|
|
83
|
+
|
|
84
|
+
export function buildParseError(usage) {
|
|
85
|
+
return (message) => {
|
|
86
|
+
const error = new Error(message);
|
|
87
|
+
error.usage = usage;
|
|
88
|
+
return error;
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function requireOptionValue(args, flag, parseError) {
|
|
93
|
+
// Peek at the next token without consuming it so we can detect a flag-like next value
|
|
94
|
+
// and fail with a clearer error rather than treating it as the current flag's value.
|
|
95
|
+
if (args.length === 0) {
|
|
96
|
+
throw parseError(`${flag} requires a non-empty value`);
|
|
97
|
+
}
|
|
98
|
+
const next = args[0];
|
|
99
|
+
if (typeof next !== "string" || next.length === 0 || /^-/u.test(next)) {
|
|
100
|
+
throw parseError(`${flag} requires a non-empty value (got ${JSON.stringify(next)})`);
|
|
101
|
+
}
|
|
102
|
+
return args.shift();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function normalizeRepoSlug(value) {
|
|
106
|
+
if (typeof value !== "string") return null;
|
|
107
|
+
const trimmed = value.trim();
|
|
108
|
+
return isValidRepoSlug(trimmed) ? trimmed : null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function normalizePrNumber(value) {
|
|
112
|
+
if (typeof value !== "string") return null;
|
|
113
|
+
const trimmed = value.trim();
|
|
114
|
+
if (!/^\d+$/.test(trimmed)) return null;
|
|
115
|
+
const num = Number.parseInt(trimmed, 10);
|
|
116
|
+
return Number.isInteger(num) && num > 0 ? num : null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function normalizeHeadSha(value) {
|
|
120
|
+
if (typeof value !== "string") return null;
|
|
121
|
+
const trimmed = value.trim().toLowerCase();
|
|
122
|
+
return SHA_PATTERN.test(trimmed) ? trimmed : null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function normalizeGate(value) {
|
|
126
|
+
if (typeof value !== "string") return null;
|
|
127
|
+
const normalized = value.trim().toLowerCase();
|
|
128
|
+
return GATE_NAMES.has(normalized) ? normalized : null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function normalizeVerdict(value) {
|
|
132
|
+
if (typeof value !== "string") return null;
|
|
133
|
+
const normalized = value.trim().toLowerCase();
|
|
134
|
+
return VERDICTS.has(normalized) ? normalized : null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function normalizeRequiredText(value, flag, parseError) {
|
|
138
|
+
if (typeof value !== "string") {
|
|
139
|
+
throw parseError(`${flag} must be a non-empty string`);
|
|
140
|
+
}
|
|
141
|
+
const trimmed = value.trim();
|
|
142
|
+
if (trimmed.length === 0) {
|
|
143
|
+
throw parseError(`${flag} must be a non-empty string`);
|
|
144
|
+
}
|
|
145
|
+
return trimmed;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function collapseWhitespace(value) {
|
|
149
|
+
return String(value).replace(/\s+/gu, " ").trim();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function smartTruncate(value, limit) {
|
|
153
|
+
const text = String(value);
|
|
154
|
+
if (text.length <= limit) {
|
|
155
|
+
return text;
|
|
156
|
+
}
|
|
157
|
+
const truncated = text.slice(0, limit);
|
|
158
|
+
const lastSpace = truncated.lastIndexOf(" ");
|
|
159
|
+
const breakPoint = lastSpace > Math.floor(limit * 0.7) ? lastSpace : limit;
|
|
160
|
+
const retained = truncated.slice(0, breakPoint);
|
|
161
|
+
const omitted = text.length - retained.length;
|
|
162
|
+
return `${retained}…[truncated ${omitted} chars]`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function parsePostGateVerdictFallbackCliArgs(argv, { parseError } = {}) {
|
|
166
|
+
const parseErr = parseError ?? buildParseError(USAGE);
|
|
167
|
+
const args = [...argv];
|
|
168
|
+
const options = {
|
|
169
|
+
repo: undefined,
|
|
170
|
+
pr: undefined,
|
|
171
|
+
gate: undefined,
|
|
172
|
+
headSha: undefined,
|
|
173
|
+
verdict: undefined,
|
|
174
|
+
findingsSummary: undefined,
|
|
175
|
+
findingsFile: undefined,
|
|
176
|
+
nextAction: undefined,
|
|
177
|
+
ghCommand: undefined,
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
while (args.length > 0) {
|
|
181
|
+
const token = args.shift();
|
|
182
|
+
if (token === "--repo") {
|
|
183
|
+
const repo = normalizeRepoSlug(requireOptionValue(args, "--repo", parseErr));
|
|
184
|
+
if (!repo) {
|
|
185
|
+
throw parseErr("--repo must be of the form owner/name: each segment must be non-empty, must not be \".\" or \"..\", and must not contain whitespace, slashes, or backslashes");
|
|
186
|
+
}
|
|
187
|
+
options.repo = repo;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (token === "--pr") {
|
|
191
|
+
const pr = normalizePrNumber(requireOptionValue(args, "--pr", parseErr));
|
|
192
|
+
if (!pr) {
|
|
193
|
+
throw parseErr("--pr must be a positive integer");
|
|
194
|
+
}
|
|
195
|
+
options.pr = pr;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (token === "--head-sha") {
|
|
199
|
+
const headSha = normalizeHeadSha(requireOptionValue(args, "--head-sha", parseErr));
|
|
200
|
+
if (!headSha) {
|
|
201
|
+
throw parseErr("--head-sha must be a 7-64 character hexadecimal SHA");
|
|
202
|
+
}
|
|
203
|
+
options.headSha = headSha;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (token === "--gate") {
|
|
207
|
+
const gate = normalizeGate(requireOptionValue(args, "--gate", parseErr));
|
|
208
|
+
if (!gate) {
|
|
209
|
+
throw parseErr("--gate must be one of: draft_gate, pre_approval_gate");
|
|
210
|
+
}
|
|
211
|
+
options.gate = gate;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (token === "--verdict") {
|
|
215
|
+
const verdict = normalizeVerdict(requireOptionValue(args, "--verdict", parseErr));
|
|
216
|
+
if (!verdict) {
|
|
217
|
+
throw parseErr("--verdict must be one of: clean, findings_present, blocked");
|
|
218
|
+
}
|
|
219
|
+
options.verdict = verdict;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (token === "--findings-summary") {
|
|
223
|
+
options.findingsSummary = normalizeRequiredText(
|
|
224
|
+
requireOptionValue(args, "--findings-summary", parseErr),
|
|
225
|
+
"--findings-summary",
|
|
226
|
+
parseErr,
|
|
227
|
+
);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (token === "--findings-file") {
|
|
231
|
+
const rawPath = requireOptionValue(args, "--findings-file", parseErr).trim();
|
|
232
|
+
if (rawPath.length === 0) {
|
|
233
|
+
throw parseErr("--findings-file must be a non-empty path");
|
|
234
|
+
}
|
|
235
|
+
options.findingsFile = rawPath;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (token === "--next-action") {
|
|
239
|
+
options.nextAction = normalizeRequiredText(
|
|
240
|
+
requireOptionValue(args, "--next-action", parseErr),
|
|
241
|
+
"--next-action",
|
|
242
|
+
parseErr,
|
|
243
|
+
);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (token === "--gh-command") {
|
|
247
|
+
const cmd = requireOptionValue(args, "--gh-command", parseErr).trim();
|
|
248
|
+
if (cmd.length === 0) {
|
|
249
|
+
throw parseErr("--gh-command must be a non-empty path or executable name");
|
|
250
|
+
}
|
|
251
|
+
options.ghCommand = cmd;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
throw parseErr(`Unknown argument: ${token}`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const required = ["repo", "pr", "headSha", "verdict", "nextAction"];
|
|
258
|
+
const missing = required.filter((key) => options[key] === undefined);
|
|
259
|
+
if (options.findingsSummary === undefined && options.findingsFile === undefined) {
|
|
260
|
+
missing.push("findingsSummary|findingsFile");
|
|
261
|
+
}
|
|
262
|
+
if (missing.length > 0) {
|
|
263
|
+
throw parseErr(
|
|
264
|
+
`post-gate-verdict-fallback requires --repo, --pr, --head-sha, --verdict, --next-action, and either --findings-summary or --findings-file (missing: ${missing.join(", ")})`,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (options.gate === undefined) {
|
|
269
|
+
options.gate = "draft_gate";
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return options;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Per-field cap for findingsSummary when read from --findings-file or supplied inline.
|
|
276
|
+
// Mirrors the full helper's MAX_GATE_COMMENT_TEXT_LENGTH behavior so a large file cannot
|
|
277
|
+
// blow out the rendered comment; the full template structure (gate name, head SHA, verdict,
|
|
278
|
+
// next action) is always preserved so parseGateReviewCommentBody() stays deterministic.
|
|
279
|
+
const MAX_FINDINGS_SUMMARY_LENGTH = 2000;
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Render the visible gate-review comment body in the same parser-stable
|
|
283
|
+
* format used by `scripts/github/upsert-checkpoint-verdict.mjs`'s
|
|
284
|
+
* `renderGateReviewCommentBody`. Mirrors that helper's shape so the existing
|
|
285
|
+
* detectors can still parse gate name and head SHA out of fallback comments.
|
|
286
|
+
*/
|
|
287
|
+
export function renderFallbackGateReviewCommentBody({
|
|
288
|
+
gate,
|
|
289
|
+
headSha,
|
|
290
|
+
verdict,
|
|
291
|
+
findingsSummary,
|
|
292
|
+
nextAction,
|
|
293
|
+
blockCleanOnFindingSeverities,
|
|
294
|
+
}) {
|
|
295
|
+
const summary = smartTruncate(String(findingsSummary ?? ""), MAX_FINDINGS_SUMMARY_LENGTH);
|
|
296
|
+
const lines = [
|
|
297
|
+
`### Gate review: \`${gate}\``,
|
|
298
|
+
"",
|
|
299
|
+
`**Reviewed head SHA:** \`${headSha}\``,
|
|
300
|
+
`**Verdict:** ${verdict}`,
|
|
301
|
+
];
|
|
302
|
+
if (
|
|
303
|
+
(verdict === "findings_present" || verdict === "blocked")
|
|
304
|
+
&& Array.isArray(blockCleanOnFindingSeverities)
|
|
305
|
+
&& blockCleanOnFindingSeverities.length > 0
|
|
306
|
+
) {
|
|
307
|
+
const sevs = blockCleanOnFindingSeverities.join(", ");
|
|
308
|
+
lines.push(`**Blocking severities:** ${sevs} (clean requires no findings matching these severities)`);
|
|
309
|
+
}
|
|
310
|
+
lines.push(
|
|
311
|
+
"",
|
|
312
|
+
`**Findings summary:** ${summary}`,
|
|
313
|
+
"",
|
|
314
|
+
`**Next action:** ${nextAction}`,
|
|
315
|
+
);
|
|
316
|
+
return lines.join("\n");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function resolveFindingsSummary(options, { parseError }) {
|
|
320
|
+
if (typeof options.findingsFile === "string" && options.findingsFile.length > 0) {
|
|
321
|
+
let content;
|
|
322
|
+
try {
|
|
323
|
+
content = await readFile(options.findingsFile, "utf8");
|
|
324
|
+
} catch (err) {
|
|
325
|
+
throw parseError(`Cannot read --findings-file "${options.findingsFile}": ${err instanceof Error ? err.message : String(err)}`);
|
|
326
|
+
}
|
|
327
|
+
// Match the full helper's --findings-file semantics: trim only trailing newlines so the
|
|
328
|
+
// summary preserves its internal newlines and any intentional leading content. Reject
|
|
329
|
+
// whitespace-only files via a separate .trim()-based emptiness check.
|
|
330
|
+
const trimmedTrailing = content.replace(/\n+$/, "");
|
|
331
|
+
if (trimmedTrailing.trim().length === 0) {
|
|
332
|
+
throw parseError(`--findings-file "${options.findingsFile}" is empty or contains only whitespace`);
|
|
333
|
+
}
|
|
334
|
+
return trimmedTrailing;
|
|
335
|
+
}
|
|
336
|
+
if (typeof options.findingsSummary === "string" && options.findingsSummary.length > 0) {
|
|
337
|
+
// Single-line summaries only; multi-line must use --findings-file.
|
|
338
|
+
return collapseWhitespace(options.findingsSummary);
|
|
339
|
+
}
|
|
340
|
+
throw parseError("post-gate-verdict-fallback requires either --findings-summary or --findings-file");
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export async function postGateVerdictViaGh({
|
|
344
|
+
repo,
|
|
345
|
+
pr,
|
|
346
|
+
body,
|
|
347
|
+
env = process.env,
|
|
348
|
+
ghCommand = "gh",
|
|
349
|
+
spawnImpl = defaultSpawn,
|
|
350
|
+
}) {
|
|
351
|
+
return new Promise((resolve, reject) => {
|
|
352
|
+
const payload = JSON.stringify({ body });
|
|
353
|
+
const child = spawnImpl(
|
|
354
|
+
ghCommand,
|
|
355
|
+
["api", "--method", "POST", "-H", "Content-Type: application/json", "--input", "-", `repos/${repo}/issues/${pr}/comments`],
|
|
356
|
+
{ env, stdio: ["pipe", "pipe", "pipe"] },
|
|
357
|
+
);
|
|
358
|
+
let stdout = "";
|
|
359
|
+
let stderr = "";
|
|
360
|
+
child.stdout.on("data", (chunk) => {
|
|
361
|
+
stdout += String(chunk);
|
|
362
|
+
});
|
|
363
|
+
child.stderr.on("data", (chunk) => {
|
|
364
|
+
stderr += String(chunk);
|
|
365
|
+
});
|
|
366
|
+
child.on("error", (err) => {
|
|
367
|
+
reject(new Error(`gh api failed to spawn: ${err instanceof Error ? err.message : String(err)}`));
|
|
368
|
+
});
|
|
369
|
+
child.on("close", (code) => {
|
|
370
|
+
if (code !== 0) {
|
|
371
|
+
const detail = stderr.trim() || `exit code ${code}`;
|
|
372
|
+
reject(new Error(`gh api failed to post gate verdict comment for ${repo}#${pr}: ${detail}`));
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
let responsePayload;
|
|
376
|
+
try {
|
|
377
|
+
responsePayload = JSON.parse(stdout);
|
|
378
|
+
} catch (err) {
|
|
379
|
+
reject(new Error(`gh api returned non-JSON response for ${repo}#${pr}: ${err instanceof Error ? err.message : String(err)}`));
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const commentId = Number.isInteger(responsePayload?.id) ? responsePayload.id : null;
|
|
383
|
+
const commentUrl = typeof responsePayload?.html_url === "string" && responsePayload.html_url.length > 0
|
|
384
|
+
? responsePayload.html_url
|
|
385
|
+
: null;
|
|
386
|
+
if (commentId === null || commentUrl === null) {
|
|
387
|
+
reject(new Error(`gh api response missing comment id/html_url for ${repo}#${pr}: ${stdout.trim().slice(0, 200)}`));
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
resolve({ commentId, commentUrl });
|
|
391
|
+
});
|
|
392
|
+
child.stdin.end(payload);
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export function buildFallbackWarning() {
|
|
397
|
+
return [
|
|
398
|
+
"[post-gate-verdict-fallback] WARNING: fallback mode active.",
|
|
399
|
+
"The full @dev-loops/core helper (scripts/github/upsert-checkpoint-verdict.mjs) was not available,",
|
|
400
|
+
"so this comment was posted via the degraded gh-only fallback poster.",
|
|
401
|
+
"Audit trail is degraded: no idempotent same-head update, no stale-head detection,",
|
|
402
|
+
"no gate-coordination validation, no internal-only PR short-circuit, no blocking-severity count enforcement.",
|
|
403
|
+
"Install @dev-loops/core to restore full gate-comment semantics.",
|
|
404
|
+
].join(" ");
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Programmatic entry point. Resolves CLI args, renders the visible body,
|
|
409
|
+
* posts via gh, and emits a stderr warning explaining the degraded audit
|
|
410
|
+
* trail. Throws on argument errors or gh failures (fail-closed).
|
|
411
|
+
*/
|
|
412
|
+
export async function runCli(
|
|
413
|
+
argv = process.argv.slice(2),
|
|
414
|
+
{
|
|
415
|
+
env = process.env,
|
|
416
|
+
spawn = defaultSpawn,
|
|
417
|
+
ghCommand,
|
|
418
|
+
stdoutSink,
|
|
419
|
+
stderrSink,
|
|
420
|
+
parseErrorFactory,
|
|
421
|
+
} = {},
|
|
422
|
+
) {
|
|
423
|
+
const parseError = parseErrorFactory ?? buildParseError(USAGE);
|
|
424
|
+
const options = parsePostGateVerdictFallbackCliArgs(argv, { parseError });
|
|
425
|
+
const findingsSummary = await resolveFindingsSummary(options, { parseError });
|
|
426
|
+
const body = renderFallbackGateReviewCommentBody({
|
|
427
|
+
gate: options.gate,
|
|
428
|
+
headSha: options.headSha,
|
|
429
|
+
verdict: options.verdict,
|
|
430
|
+
findingsSummary,
|
|
431
|
+
nextAction: options.nextAction,
|
|
432
|
+
});
|
|
433
|
+
const warning = buildFallbackWarning();
|
|
434
|
+
if (stderrSink && Array.isArray(stderrSink)) {
|
|
435
|
+
stderrSink.push(`${warning}\n`);
|
|
436
|
+
} else {
|
|
437
|
+
process.stderr.write(`${warning}\n`);
|
|
438
|
+
}
|
|
439
|
+
const { commentId, commentUrl } = await postGateVerdictViaGh({
|
|
440
|
+
repo: options.repo,
|
|
441
|
+
pr: options.pr,
|
|
442
|
+
body,
|
|
443
|
+
env,
|
|
444
|
+
ghCommand: ghCommand ?? options.ghCommand ?? "gh",
|
|
445
|
+
spawnImpl: spawn,
|
|
446
|
+
});
|
|
447
|
+
const result = {
|
|
448
|
+
ok: true,
|
|
449
|
+
action: "created",
|
|
450
|
+
repo: options.repo,
|
|
451
|
+
pr: options.pr,
|
|
452
|
+
gate: options.gate,
|
|
453
|
+
headSha: options.headSha,
|
|
454
|
+
commentId,
|
|
455
|
+
commentUrl,
|
|
456
|
+
fallback: true,
|
|
457
|
+
warning,
|
|
458
|
+
};
|
|
459
|
+
if (stdoutSink && Array.isArray(stdoutSink)) {
|
|
460
|
+
stdoutSink.push(`${JSON.stringify(result)}\n`);
|
|
461
|
+
} else {
|
|
462
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
463
|
+
}
|
|
464
|
+
return 0;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const invokedAsScript = process.argv[1]
|
|
468
|
+
? import.meta.url === pathToFileURL(process.argv[1]).href
|
|
469
|
+
: false;
|
|
470
|
+
|
|
471
|
+
if (invokedAsScript) {
|
|
472
|
+
runCli().catch((error) => {
|
|
473
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
474
|
+
process.stderr.write(`${message}\n`);
|
|
475
|
+
if (error?.usage) {
|
|
476
|
+
process.stderr.write(`${error.usage}\n`);
|
|
477
|
+
}
|
|
478
|
+
process.exitCode = 1;
|
|
479
|
+
});
|
|
480
|
+
}
|