dev-loops 0.2.6 → 0.3.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/.claude/.claude-plugin/plugin.json +1 -1
- package/.claude/agents/dev-loop.md +2 -1
- package/.claude/agents/developer.md +1 -0
- package/.claude/agents/fixer.md +1 -0
- package/.claude/agents/review.md +30 -0
- package/.claude/skills/copilot-pr-followup/SKILL.md +57 -3
- package/.claude/skills/dev-loop/SKILL.md +5 -5
- package/.claude/skills/docs/anti-patterns.md +2 -0
- package/.claude/skills/docs/copilot-loop-operations.md +2 -2
- package/.claude/skills/local-implementation/SKILL.md +17 -3
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +60 -0
- package/agents/dev-loop.agent.md +5 -1
- package/agents/developer.agent.md +1 -0
- package/agents/fixer.agent.md +1 -0
- package/agents/review.agent.md +30 -0
- package/cli/index.mjs +42 -7
- package/package.json +2 -2
- package/scripts/README.md +6 -5
- package/scripts/_cli-primitives.mjs +2 -0
- package/scripts/_core-helpers.mjs +1 -0
- package/scripts/claude/generate-claude-assets.mjs +12 -2
- package/scripts/claude/headless-dev-loop.mjs +53 -13
- package/scripts/claude/headless-info-smoke.mjs +45 -11
- package/scripts/docs/validate-links.mjs +20 -11
- package/scripts/github/build-adjacent-bundle.mjs +448 -0
- package/scripts/github/capture-review-threads.mjs +32 -14
- package/scripts/github/{create-draft-pr.mjs → create-pr.mjs} +28 -12
- package/scripts/github/detect-checkpoint-evidence.mjs +123 -15
- package/scripts/github/detect-linked-issue-pr.mjs +22 -10
- package/scripts/github/manage-sub-issues.mjs +37 -16
- package/scripts/github/post-gate-findings.mjs +392 -0
- package/scripts/github/probe-copilot-review.mjs +24 -12
- package/scripts/github/ready-for-review.mjs +17 -8
- package/scripts/github/reconcile-draft-gate.mjs +24 -12
- package/scripts/github/reply-resolve-review-threads.mjs +34 -15
- package/scripts/github/request-copilot-review.mjs +97 -19
- package/scripts/github/resolve-tracker-local-spec.mjs +29 -12
- package/scripts/github/stage-reviewer-draft.mjs +32 -14
- package/scripts/github/upsert-checkpoint-verdict.mjs +646 -41
- package/scripts/github/verify-fresh-review-context.mjs +12 -1
- package/scripts/github/write-gate-context.mjs +634 -0
- package/scripts/github/write-gate-findings-log.mjs +42 -21
- package/scripts/loop/build-handoff-envelope.mjs +32 -14
- package/scripts/loop/conductor-monitor.mjs +25 -9
- package/scripts/loop/conductor.mjs +31 -12
- package/scripts/loop/copilot-pr-handoff.mjs +31 -14
- package/scripts/loop/debt-remediate.mjs +28 -11
- package/scripts/loop/detect-change-scope.mjs +36 -11
- package/scripts/loop/detect-copilot-loop-state.mjs +29 -12
- package/scripts/loop/detect-copilot-session-activity.mjs +29 -12
- package/scripts/loop/detect-initial-copilot-pr-state.mjs +26 -10
- package/scripts/loop/detect-internal-only-pr.mjs +31 -13
- package/scripts/loop/detect-issue-refinement-artifact.mjs +29 -12
- package/scripts/loop/detect-pr-gate-coordination-state.mjs +56 -28
- package/scripts/loop/detect-reviewer-loop-state.mjs +35 -16
- package/scripts/loop/detect-stale-runner.mjs +32 -14
- package/scripts/loop/detect-tracker-first-loop-state.mjs +38 -11
- package/scripts/loop/detect-tracker-pr-state.mjs +23 -8
- package/scripts/loop/info.mjs +28 -10
- package/scripts/loop/inspect-run-viewer/cli.mjs +44 -21
- package/scripts/loop/inspect-run.mjs +35 -16
- package/scripts/loop/outer-loop.mjs +35 -16
- package/scripts/loop/pr-runner-coordination.mjs +31 -12
- package/scripts/loop/pre-commit-branch-guard.mjs +26 -9
- package/scripts/loop/pre-flight-gate.mjs +25 -9
- package/scripts/loop/pre-pr-ready-gate.mjs +24 -8
- package/scripts/loop/pre-push-main-guard.mjs +19 -5
- package/scripts/loop/pre-write-remote-freshness-guard.mjs +23 -7
- package/scripts/loop/resolve-dev-loop-startup.mjs +29 -12
- package/scripts/loop/run-conductor-cycle.mjs +23 -8
- package/scripts/loop/run-queue.mjs +87 -15
- package/scripts/loop/run-refinement-audit.mjs +44 -22
- package/scripts/loop/run-watch-cycle.mjs +28 -12
- package/scripts/loop/steer-loop.mjs +122 -62
- package/scripts/loop/watch-initial-copilot-pr.mjs +28 -12
- package/scripts/projects/add-queue-item.mjs +60 -43
- package/scripts/projects/archive-done-items.mjs +506 -0
- package/scripts/projects/ensure-queue-board.mjs +65 -64
- package/scripts/projects/list-queue-items.mjs +57 -56
- package/scripts/projects/move-queue-item.mjs +123 -124
- package/scripts/projects/reorder-queue-item.mjs +384 -108
- package/scripts/projects/sync-item-status.mjs +198 -0
- package/scripts/refine/_refine-helpers.mjs +21 -9
- package/scripts/refine/verify.mjs +31 -13
- package/skills/copilot-pr-followup/SKILL.md +57 -3
- package/skills/dev-loop/SKILL.md +9 -5
- package/skills/dev-loop/scripts/log-bash-exit-1.mjs +2 -2
- package/skills/dev-loop/scripts/phase-files.mjs +2 -2
- package/skills/docs/anti-patterns.md +2 -0
- package/skills/docs/copilot-loop-operations.md +2 -2
- package/skills/local-implementation/SKILL.md +17 -3
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from "node:util";
|
|
2
3
|
import {
|
|
3
4
|
buildParseError,
|
|
4
5
|
formatCliError,
|
|
@@ -8,9 +9,13 @@ import {
|
|
|
8
9
|
summarizeGateReviewCommentMarkers,
|
|
9
10
|
summarizeGateReviewComments,
|
|
10
11
|
} from "../_core-helpers.mjs";
|
|
11
|
-
import {
|
|
12
|
+
import { access } from "node:fs/promises";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import { parsePrNumber, requireTokenValue, runChild } from "../_cli-primitives.mjs";
|
|
12
15
|
import { fetchGithubReviewThreadsPayload } from "./capture-review-threads.mjs";
|
|
13
16
|
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
17
|
+
import { loadDevLoopConfig, resolveGateConfig, resolveRequireFanoutEvidence } from "@dev-loops/core/config";
|
|
18
|
+
import { buildLogPath } from "./write-gate-findings-log.mjs";
|
|
14
19
|
import { ensureAsyncRunnerOwnership } from "../loop/_pr-runner-coordination.mjs";
|
|
15
20
|
import { detectStaleRunner } from "../loop/_stale-runner-detection.mjs";
|
|
16
21
|
const USAGE = `Usage: detect-checkpoint-evidence.mjs --repo <owner/name> --pr <number>
|
|
@@ -84,30 +89,46 @@ Exit codes:
|
|
|
84
89
|
1 Argument error, gh failure, malformed gh JSON, or missing required pre-merge gate evidence.`.trim();
|
|
85
90
|
const parseError = buildParseError(USAGE);
|
|
86
91
|
export function parseDetectCheckpointEvidenceCliArgs(argv) {
|
|
87
|
-
const
|
|
92
|
+
const { tokens } = parseArgs({
|
|
93
|
+
args: [...argv],
|
|
94
|
+
options: {
|
|
95
|
+
help: { type: "boolean", short: "h" },
|
|
96
|
+
repo: { type: "string" },
|
|
97
|
+
pr: { type: "string" },
|
|
98
|
+
"require-before-merge": { type: "boolean" },
|
|
99
|
+
},
|
|
100
|
+
allowPositionals: true,
|
|
101
|
+
strict: false,
|
|
102
|
+
tokens: true,
|
|
103
|
+
});
|
|
88
104
|
const options = {
|
|
89
105
|
help: false,
|
|
90
106
|
repo: undefined,
|
|
91
107
|
pr: undefined,
|
|
92
108
|
};
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
109
|
+
for (const token of tokens) {
|
|
110
|
+
if (token.kind === "positional") {
|
|
111
|
+
throw parseError(`Unknown argument: ${token.value}`);
|
|
112
|
+
}
|
|
113
|
+
if (token.kind !== "option") {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (token.name === "help") {
|
|
96
117
|
options.help = true;
|
|
97
118
|
return options;
|
|
98
119
|
}
|
|
99
|
-
if (token === "
|
|
100
|
-
options.repo =
|
|
120
|
+
if (token.name === "repo") {
|
|
121
|
+
options.repo = requireTokenValue(token, parseError).trim();
|
|
101
122
|
continue;
|
|
102
123
|
}
|
|
103
|
-
if (token === "
|
|
104
|
-
options.pr = parsePrNumber(
|
|
124
|
+
if (token.name === "pr") {
|
|
125
|
+
options.pr = parsePrNumber(requireTokenValue(token, parseError), parseError);
|
|
105
126
|
continue;
|
|
106
127
|
}
|
|
107
|
-
if (token === "
|
|
128
|
+
if (token.name === "require-before-merge") {
|
|
108
129
|
throw parseError(`--require-before-merge has been removed: gate evidence enforcement is now always-on by default. Omit the flag.`);
|
|
109
130
|
}
|
|
110
|
-
throw parseError(`Unknown argument: ${token}`);
|
|
131
|
+
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
111
132
|
}
|
|
112
133
|
if (options.repo === undefined || options.pr === undefined) {
|
|
113
134
|
throw parseError("detect-checkpoint-evidence requires both --repo <owner/name> and --pr <number>");
|
|
@@ -183,6 +204,8 @@ function emptyGateMarkerSummary() {
|
|
|
183
204
|
verdict: null,
|
|
184
205
|
findingsSummary: null,
|
|
185
206
|
nextAction: null,
|
|
207
|
+
executionMode: null,
|
|
208
|
+
inlineReason: null,
|
|
186
209
|
contractComplete: false,
|
|
187
210
|
commentId: null,
|
|
188
211
|
commentUrl: null,
|
|
@@ -199,13 +222,15 @@ function normalizeGateMarkerSummary(summary) {
|
|
|
199
222
|
verdict: summary.verdict,
|
|
200
223
|
findingsSummary: summary.findingsSummary,
|
|
201
224
|
nextAction: summary.nextAction,
|
|
225
|
+
executionMode: summary.executionMode ?? null,
|
|
226
|
+
inlineReason: summary.inlineReason ?? null,
|
|
202
227
|
contractComplete: summary.contractComplete === true,
|
|
203
228
|
commentId: summary.commentId,
|
|
204
229
|
commentUrl: summary.commentUrl,
|
|
205
230
|
updatedAt: summary.updatedAt,
|
|
206
231
|
};
|
|
207
232
|
}
|
|
208
|
-
export function buildPreMergeGateCheck(evidence, unresolvedThreadCount = null, staleRunnerCheck = null) {
|
|
233
|
+
export function buildPreMergeGateCheck(evidence, unresolvedThreadCount = null, staleRunnerCheck = null, fanoutEnforcement = null) {
|
|
209
234
|
const failures = [];
|
|
210
235
|
if (!(evidence.draftGate.visible && evidence.draftGate.verdict === "clean")) {
|
|
211
236
|
failures.push("missing visible clean draft_gate comment");
|
|
@@ -219,6 +244,24 @@ export function buildPreMergeGateCheck(evidence, unresolvedThreadCount = null, s
|
|
|
219
244
|
)) {
|
|
220
245
|
failures.push("missing visible clean current-head pre_approval_gate comment");
|
|
221
246
|
}
|
|
247
|
+
// Fail-closed fan-out evidence enforcement (gates.requireFanoutEvidence, ON by
|
|
248
|
+
// default / opt-out). When disabled or config-unavailable, fanoutEnforcement is
|
|
249
|
+
// { required: false, gates: [] } so the `.required` guard skips this block.
|
|
250
|
+
if (fanoutEnforcement && fanoutEnforcement.required) {
|
|
251
|
+
for (const gate of fanoutEnforcement.gates) {
|
|
252
|
+
if (gate.executionMode !== "fanout_fanin") {
|
|
253
|
+
failures.push(
|
|
254
|
+
`${gate.name}: requireFanoutEvidence is enabled but executionMode is "${gate.executionMode ?? "unset"}" (expected "fanout_fanin"); inline gate verdicts are not accepted`,
|
|
255
|
+
);
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
if (!gate.ledgerExists) {
|
|
259
|
+
failures.push(
|
|
260
|
+
`${gate.name}: requireFanoutEvidence is enabled but no findings-log ledger exists for the reviewed head (${gate.ledgerPath})`,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
222
265
|
if (typeof unresolvedThreadCount === "number" && unresolvedThreadCount !== 0) {
|
|
223
266
|
if (unresolvedThreadCount === -1) {
|
|
224
267
|
failures.push("could not fetch review thread state from GitHub API; re-run gate evidence check when API connectivity is restored");
|
|
@@ -236,6 +279,52 @@ export function buildPreMergeGateCheck(evidence, unresolvedThreadCount = null, s
|
|
|
236
279
|
failures,
|
|
237
280
|
};
|
|
238
281
|
}
|
|
282
|
+
async function ledgerExists(fullPath) {
|
|
283
|
+
try {
|
|
284
|
+
await access(fullPath);
|
|
285
|
+
return true;
|
|
286
|
+
} catch {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Build the fan-out evidence enforcement descriptor.
|
|
292
|
+
*
|
|
293
|
+
* Enforcement is ON by default (opt-out via gates.requireFanoutEvidence: false).
|
|
294
|
+
* Returns { required: false } when enforcement is disabled OR when config is
|
|
295
|
+
* unavailable (config == null — null or undefined — after a failed load) — config-unavailable must
|
|
296
|
+
* fail open and never enable enforcement. When enabled, records per-required-gate
|
|
297
|
+
* executionMode and whether the deterministic findings-log ledger exists for the
|
|
298
|
+
* reviewed head SHA so the pre-merge check can fail closed on inline verdicts or
|
|
299
|
+
* missing ledgers.
|
|
300
|
+
*/
|
|
301
|
+
async function buildFanoutEnforcement({ repo, pr, currentHeadSha, draftGateMarker, preApprovalGateMarker, config, cwd }) {
|
|
302
|
+
// Fail open when config could not be loaded/validated. `== null` covers both
|
|
303
|
+
// null and undefined; the loader only ever yields null on failure, but the
|
|
304
|
+
// loose check defensively treats an absent config as unavailable.
|
|
305
|
+
if (config == null || !resolveRequireFanoutEvidence(config)) {
|
|
306
|
+
return { required: false, gates: [] };
|
|
307
|
+
}
|
|
308
|
+
const draftRequired = resolveGateConfig(config, "draft").required;
|
|
309
|
+
const preApprovalRequired = resolveGateConfig(config, "preApproval").required;
|
|
310
|
+
const gateSpecs = [
|
|
311
|
+
{ name: "draft_gate", marker: draftGateMarker, required: draftRequired },
|
|
312
|
+
{ name: "pre_approval_gate", marker: preApprovalGateMarker, required: preApprovalRequired },
|
|
313
|
+
].filter((spec) => spec.required && spec.marker.visible);
|
|
314
|
+
const gates = [];
|
|
315
|
+
for (const spec of gateSpecs) {
|
|
316
|
+
const headSha = spec.marker.headSha ?? currentHeadSha;
|
|
317
|
+
const ledgerPath = buildLogPath({ repo, pr, gate: spec.name, headSha, tmpRoot: "tmp" });
|
|
318
|
+
const fullPath = path.resolve(cwd, ledgerPath);
|
|
319
|
+
gates.push({
|
|
320
|
+
name: spec.name,
|
|
321
|
+
executionMode: spec.marker.executionMode ?? null,
|
|
322
|
+
ledgerPath,
|
|
323
|
+
ledgerExists: await ledgerExists(fullPath),
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
return { required: true, gates };
|
|
327
|
+
}
|
|
239
328
|
export async function detectCheckpointEvidence(options, { env = process.env, ghCommand = "gh", cwd = process.cwd() } = {}) {
|
|
240
329
|
const runnerOwnership = await ensureAsyncRunnerOwnership({
|
|
241
330
|
repo: options.repo,
|
|
@@ -282,6 +371,24 @@ export async function detectCheckpointEvidence(options, { env = process.env, ghC
|
|
|
282
371
|
const allComments = [...commentsPayload, ...prReviews];
|
|
283
372
|
const commentSummary = summarizeGateReviewComments(allComments);
|
|
284
373
|
const markerSummary = summarizeGateReviewCommentMarkers(allComments, { headSha: currentHeadSha });
|
|
374
|
+
const draftGateMarker = normalizeGateMarkerSummary(markerSummary.draft_gate);
|
|
375
|
+
const preApprovalGateMarker = normalizeGateMarkerSummary(markerSummary.pre_approval_gate);
|
|
376
|
+
// loadDevLoopConfig never throws: it returns { config, warnings, errors }.
|
|
377
|
+
// A non-empty errors array means the config could not be loaded/validated, so
|
|
378
|
+
// treat it as config-unavailable and leave fan-out enforcement disabled
|
|
379
|
+
// (preserves default behavior). Other gate checks remain unaffected.
|
|
380
|
+
let config = null;
|
|
381
|
+
const { config: loadedConfig, errors: configErrors } = await loadDevLoopConfig({ repoRoot: cwd });
|
|
382
|
+
config = Array.isArray(configErrors) && configErrors.length > 0 ? null : loadedConfig;
|
|
383
|
+
const fanoutEnforcement = await buildFanoutEnforcement({
|
|
384
|
+
repo: options.repo,
|
|
385
|
+
pr: options.pr,
|
|
386
|
+
currentHeadSha,
|
|
387
|
+
draftGateMarker,
|
|
388
|
+
preApprovalGateMarker,
|
|
389
|
+
config,
|
|
390
|
+
cwd,
|
|
391
|
+
});
|
|
285
392
|
return {
|
|
286
393
|
ok: true,
|
|
287
394
|
repo: options.repo,
|
|
@@ -289,9 +396,10 @@ export async function detectCheckpointEvidence(options, { env = process.env, ghC
|
|
|
289
396
|
currentHeadSha,
|
|
290
397
|
draftGate: normalizeGateSummary(commentSummary.draft_gate),
|
|
291
398
|
preApprovalGate: normalizeGateSummary(commentSummary.pre_approval_gate),
|
|
292
|
-
draftGateMarker
|
|
293
|
-
preApprovalGateMarker
|
|
399
|
+
draftGateMarker,
|
|
400
|
+
preApprovalGateMarker,
|
|
294
401
|
draftGateSatisfied: commentSummary.draft_gate?.verdict === "clean" && typeof commentSummary.draft_gate?.headSha === "string",
|
|
402
|
+
fanoutEnforcement,
|
|
295
403
|
...(runnerOwnership.status !== "skipped_no_async_run_id" ? { runnerOwnership } : {}),
|
|
296
404
|
staleRunner: {
|
|
297
405
|
status: staleRunnerDetection.status,
|
|
@@ -334,7 +442,7 @@ async function main() {
|
|
|
334
442
|
? [`exit signal recorded for run ${result.staleRunner.activeRun?.runId}: refuse to merge`]
|
|
335
443
|
: [],
|
|
336
444
|
};
|
|
337
|
-
const preMergeGateCheck = buildPreMergeGateCheck(result, unresolvedThreadCount, staleRunnerCheck);
|
|
445
|
+
const preMergeGateCheck = buildPreMergeGateCheck(result, unresolvedThreadCount, staleRunnerCheck, result.fanoutEnforcement);
|
|
338
446
|
const output = { ...result, preMergeGateCheck, staleRunnerCheck };
|
|
339
447
|
if (!preMergeGateCheck.ok) {
|
|
340
448
|
process.stderr.write(`${JSON.stringify({
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from "node:util";
|
|
2
3
|
import { buildParseError, formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
|
|
3
|
-
import { parseIssueNumber,
|
|
4
|
+
import { parseIssueNumber, requireTokenValue, runChild } from "../_cli-primitives.mjs";
|
|
4
5
|
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
5
6
|
export const LINKED_ISSUE_PR_QUERY = [
|
|
6
7
|
"query($owner:String!, $name:String!, $issue:Int!, $after:String) {",
|
|
@@ -75,27 +76,38 @@ Error output (stderr, JSON):
|
|
|
75
76
|
{ "ok": false, "error": "..." }`.trim();
|
|
76
77
|
const parseError = buildParseError(USAGE);
|
|
77
78
|
export function parseDetectLinkedIssuePrCliArgs(argv) {
|
|
78
|
-
const
|
|
79
|
+
const { tokens } = parseArgs({
|
|
80
|
+
args: [...argv],
|
|
81
|
+
options: { help: { type: "boolean", short: "h" }, repo: { type: "string" }, issue: { type: "string" } },
|
|
82
|
+
allowPositionals: true,
|
|
83
|
+
strict: false,
|
|
84
|
+
tokens: true,
|
|
85
|
+
});
|
|
79
86
|
const options = {
|
|
80
87
|
help: false,
|
|
81
88
|
repo: undefined,
|
|
82
89
|
issue: undefined,
|
|
83
90
|
};
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
91
|
+
for (const token of tokens) {
|
|
92
|
+
if (token.kind === "positional") {
|
|
93
|
+
throw parseError(`Unknown argument: ${token.value}`);
|
|
94
|
+
}
|
|
95
|
+
if (token.kind !== "option") {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (token.name === "help") {
|
|
87
99
|
options.help = true;
|
|
88
100
|
return options;
|
|
89
101
|
}
|
|
90
|
-
if (token === "
|
|
91
|
-
options.repo =
|
|
102
|
+
if (token.name === "repo") {
|
|
103
|
+
options.repo = requireTokenValue(token, parseError).trim();
|
|
92
104
|
continue;
|
|
93
105
|
}
|
|
94
|
-
if (token === "
|
|
95
|
-
options.issue = parseIssueNumber(
|
|
106
|
+
if (token.name === "issue") {
|
|
107
|
+
options.issue = parseIssueNumber(requireTokenValue(token, parseError), parseError);
|
|
96
108
|
continue;
|
|
97
109
|
}
|
|
98
|
-
throw parseError(`Unknown argument: ${token}`);
|
|
110
|
+
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
99
111
|
}
|
|
100
112
|
if (options.repo === undefined || options.issue === undefined) {
|
|
101
113
|
throw parseError("Linked PR detection requires both --repo <owner/name> and --issue <number>");
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from "node:util";
|
|
2
3
|
import { buildParseError, formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
|
|
3
|
-
import { parsePositiveInteger,
|
|
4
|
+
import { parsePositiveInteger, requireTokenValue, runChild } from "../_cli-primitives.mjs";
|
|
4
5
|
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
5
6
|
const USAGE = `Usage: manage-sub-issues.mjs <command> --repo <owner/name> --issue <number> [options]
|
|
6
7
|
Deterministic helper for reading, linking, ordering, and verifying GitHub sub-issue trees.
|
|
@@ -76,37 +77,57 @@ export function parseManageSubIssuesCliArgs(argv) {
|
|
|
76
77
|
expected: undefined,
|
|
77
78
|
ordered: false,
|
|
78
79
|
};
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
const { tokens } = parseArgs({
|
|
81
|
+
args,
|
|
82
|
+
options: {
|
|
83
|
+
help: { type: "boolean", short: "h" },
|
|
84
|
+
repo: { type: "string" },
|
|
85
|
+
issue: { type: "string" },
|
|
86
|
+
child: { type: "string" },
|
|
87
|
+
order: { type: "string" },
|
|
88
|
+
expected: { type: "string" },
|
|
89
|
+
ordered: { type: "boolean" },
|
|
90
|
+
},
|
|
91
|
+
allowPositionals: true,
|
|
92
|
+
strict: false,
|
|
93
|
+
tokens: true,
|
|
94
|
+
});
|
|
95
|
+
for (const token of tokens) {
|
|
96
|
+
if (token.kind === "positional") {
|
|
97
|
+
throw parseError(`Unknown argument: ${token.value}`);
|
|
98
|
+
}
|
|
99
|
+
if (token.kind !== "option") {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (token.name === "help") {
|
|
82
103
|
options.help = true;
|
|
83
104
|
return options;
|
|
84
105
|
}
|
|
85
|
-
if (token === "
|
|
86
|
-
options.repo =
|
|
106
|
+
if (token.name === "repo") {
|
|
107
|
+
options.repo = requireTokenValue(token, parseError).trim();
|
|
87
108
|
continue;
|
|
88
109
|
}
|
|
89
|
-
if (token === "
|
|
90
|
-
options.issue = parsePositiveInteger(
|
|
110
|
+
if (token.name === "issue") {
|
|
111
|
+
options.issue = parsePositiveInteger(requireTokenValue(token, parseError), "Issue number", parseError);
|
|
91
112
|
continue;
|
|
92
113
|
}
|
|
93
|
-
if (token === "
|
|
94
|
-
options.child = parsePositiveInteger(
|
|
114
|
+
if (token.name === "child") {
|
|
115
|
+
options.child = parsePositiveInteger(requireTokenValue(token, parseError), "Issue number", parseError);
|
|
95
116
|
continue;
|
|
96
117
|
}
|
|
97
|
-
if (token === "
|
|
98
|
-
options.order = parseIssueList(
|
|
118
|
+
if (token.name === "order") {
|
|
119
|
+
options.order = parseIssueList(requireTokenValue(token, parseError));
|
|
99
120
|
continue;
|
|
100
121
|
}
|
|
101
|
-
if (token === "
|
|
102
|
-
options.expected = parseIssueList(
|
|
122
|
+
if (token.name === "expected") {
|
|
123
|
+
options.expected = parseIssueList(requireTokenValue(token, parseError));
|
|
103
124
|
continue;
|
|
104
125
|
}
|
|
105
|
-
if (token === "
|
|
126
|
+
if (token.name === "ordered") {
|
|
106
127
|
options.ordered = true;
|
|
107
128
|
continue;
|
|
108
129
|
}
|
|
109
|
-
throw parseError(`Unknown argument: ${token}`);
|
|
130
|
+
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
110
131
|
}
|
|
111
132
|
if (options.repo === undefined || options.issue === undefined) {
|
|
112
133
|
throw parseError("Both --repo <owner/name> and --issue <number> are required");
|