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,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { isCopilotLogin, parseReviewThreads } from "../_core-helpers.mjs";
|
|
2
|
+
import { isCopilotLogin, parseReviewThreads, sanitizeCopilotSummonTokens } from "../_core-helpers.mjs";
|
|
3
3
|
import { fetchGithubReviewThreadsPayload } from "./capture-review-threads.mjs";
|
|
4
4
|
export const MIN_DISMISSAL_REASON_LENGTH = 30;
|
|
5
5
|
export function hasCommitShaReference(text) {
|
|
@@ -175,12 +175,15 @@ export async function replyAndMaybeResolve(
|
|
|
175
175
|
} else {
|
|
176
176
|
await validateReplyTarget({ repo, pr, commentId, threadId }, { env, ghCommand });
|
|
177
177
|
}
|
|
178
|
+
// Neutralize any bare @copilot/`/copilot`* tokens the reply text quotes (e.g.
|
|
179
|
+
// a dismissal reason citing the anti-summon rule) so posting this reply can
|
|
180
|
+
// never arm request-copilot-review.mjs's anti-summon guard.
|
|
178
181
|
const reply = parseReplyPayload(await postReply(
|
|
179
182
|
{
|
|
180
183
|
repo,
|
|
181
184
|
pr,
|
|
182
185
|
commentId,
|
|
183
|
-
body,
|
|
186
|
+
body: sanitizeCopilotSummonTokens(body),
|
|
184
187
|
},
|
|
185
188
|
{ env, ghCommand },
|
|
186
189
|
));
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
import { parseArgs } from "node:util";
|
|
12
12
|
import { parsePrNumber, requireTokenValue, runChild } from "../_cli-primitives.mjs";
|
|
13
13
|
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
14
|
-
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult } from "../lib/jq-output.mjs";
|
|
14
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
15
15
|
export const REVIEW_THREADS_QUERY = [
|
|
16
16
|
"query($owner: String!, $name: String!, $pr: Int!) {",
|
|
17
17
|
" repository(owner: $owner, name: $name) {",
|
|
@@ -103,14 +103,7 @@ export function parseCaptureCliArgs(argv) {
|
|
|
103
103
|
options.pr = parsePrNumber(requireTokenValue(token));
|
|
104
104
|
continue;
|
|
105
105
|
}
|
|
106
|
-
if (token
|
|
107
|
-
options.jq = requireTokenValue(token);
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
if (token.name === "silent") {
|
|
111
|
-
options.silent = true;
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
106
|
+
if (matchJqOutputToken(token, options, (t) => requireTokenValue(t))) continue;
|
|
114
107
|
throw new Error(`Unknown argument: ${token.rawName}`);
|
|
115
108
|
}
|
|
116
109
|
const hasLiveArgs = options.repo !== undefined || options.pr !== undefined;
|
|
@@ -4,7 +4,7 @@ import { buildParseError, formatCliError, isDirectCliRun } from "../_core-helper
|
|
|
4
4
|
import { parseIssueNumber, requireTokenValue, runChild } from "../_cli-primitives.mjs";
|
|
5
5
|
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
6
6
|
import { parseArgs } from "node:util";
|
|
7
|
-
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult } from "../lib/jq-output.mjs";
|
|
7
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
8
8
|
|
|
9
9
|
const USAGE = `Usage: comment-issue.mjs --repo <owner/name> --issue <number> (--body <text> | --body-file <path>)
|
|
10
10
|
Post a comment on a GitHub issue. Thin wrapper over \`gh issue comment\` — use this
|
|
@@ -82,14 +82,7 @@ export function parseCommentIssueCliArgs(argv) {
|
|
|
82
82
|
options.bodyFile = rawPath;
|
|
83
83
|
continue;
|
|
84
84
|
}
|
|
85
|
-
if (token
|
|
86
|
-
options.jq = requireTokenValue(token, parseError);
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
if (token.name === "silent") {
|
|
90
|
-
options.silent = true;
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
85
|
+
if (matchJqOutputToken(token, options, (t) => requireTokenValue(t, parseError))) continue;
|
|
93
86
|
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
94
87
|
}
|
|
95
88
|
if (options.repo === undefined || options.issue === undefined) {
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { parseArgs } from "node:util";
|
|
4
|
+
import { buildParseError, formatCliError, isDirectCliRun } from "../_core-helpers.mjs";
|
|
5
|
+
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
6
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult } from "../lib/jq-output.mjs";
|
|
7
|
+
|
|
8
|
+
const USAGE = `Usage: create-label.mjs --repo <owner/name> --name <label> [--color <hex>] [--description <text>] [--force]
|
|
9
|
+
Create (or idempotently reuse) a GitHub label. Thin wrapper over \`gh label create\`
|
|
10
|
+
— use this instead of an agent-level raw \`gh label create\` so the loop's
|
|
11
|
+
internal-tooling record stays clean (siblings: comment-issue.mjs, edit-pr.mjs).
|
|
12
|
+
Required:
|
|
13
|
+
--repo <owner/name> Repository slug (e.g. owner/repo)
|
|
14
|
+
--name <label> Label name to create
|
|
15
|
+
Optional:
|
|
16
|
+
--color <hex> Hex color without '#' (default: d73a4a)
|
|
17
|
+
--description <text> Label description
|
|
18
|
+
--force Update the label if it already exists
|
|
19
|
+
Output (stdout, JSON):
|
|
20
|
+
{ "ok": true, "created": true, "name": "gate:full", "color": "d73a4a", "repo": "owner/repo" }
|
|
21
|
+
{ "ok": true, "created": false, "alreadyExists": true, "name": "gate:full" }
|
|
22
|
+
Error output (stderr, JSON):
|
|
23
|
+
{ "ok": false, "error": "...", "usage"?: "..." }
|
|
24
|
+
${JQ_OUTPUT_USAGE}
|
|
25
|
+
Exit codes:
|
|
26
|
+
0 Success (created or already exists)
|
|
27
|
+
1 Argument error or gh failure
|
|
28
|
+
2 Invalid --jq filter`.trim();
|
|
29
|
+
const parseError = buildParseError(USAGE);
|
|
30
|
+
|
|
31
|
+
export function parseCreateLabelCliArgs(argv) {
|
|
32
|
+
const { values } = parseArgs({
|
|
33
|
+
args: [...argv],
|
|
34
|
+
options: {
|
|
35
|
+
help: { type: "boolean", short: "h" },
|
|
36
|
+
repo: { type: "string" },
|
|
37
|
+
name: { type: "string" },
|
|
38
|
+
color: { type: "string" },
|
|
39
|
+
description: { type: "string" },
|
|
40
|
+
force: { type: "boolean" },
|
|
41
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
42
|
+
},
|
|
43
|
+
allowPositionals: false,
|
|
44
|
+
strict: false,
|
|
45
|
+
});
|
|
46
|
+
if (values.help) {
|
|
47
|
+
return { help: true };
|
|
48
|
+
}
|
|
49
|
+
const repo = typeof values.repo === "string" ? values.repo.trim() : undefined;
|
|
50
|
+
const name = typeof values.name === "string" ? values.name.trim() : undefined;
|
|
51
|
+
if (!repo || !name) {
|
|
52
|
+
throw parseError("Creating a label requires both --repo <owner/name> and --name <label>");
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
parseRepoSlug(repo);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
throw parseError(error instanceof Error ? error.message : String(error));
|
|
58
|
+
}
|
|
59
|
+
const color = typeof values.color === "string" && values.color.trim().length > 0 ? values.color.trim() : "d73a4a";
|
|
60
|
+
const description =
|
|
61
|
+
typeof values.description === "string" && values.description.length > 0 ? values.description : undefined;
|
|
62
|
+
return {
|
|
63
|
+
help: false,
|
|
64
|
+
repo,
|
|
65
|
+
name,
|
|
66
|
+
color,
|
|
67
|
+
description,
|
|
68
|
+
force: values.force === true,
|
|
69
|
+
jq: values.jq,
|
|
70
|
+
silent: values.silent === true,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Pure: assemble the `gh label create` argv. Exported so the arg shape can be
|
|
75
|
+
// tested without spawning gh.
|
|
76
|
+
export function buildLabelArgs({ repo, name, color, description, force }) {
|
|
77
|
+
const args = ["label", "create", name, "--repo", repo, "--color", color];
|
|
78
|
+
if (description !== undefined) {
|
|
79
|
+
args.push("--description", description);
|
|
80
|
+
}
|
|
81
|
+
if (force) {
|
|
82
|
+
args.push("--force");
|
|
83
|
+
}
|
|
84
|
+
return args;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function createLabel(options, { ghCommand = "gh", exec = execFileSync } = {}) {
|
|
88
|
+
const args = buildLabelArgs(options);
|
|
89
|
+
try {
|
|
90
|
+
exec(ghCommand, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
91
|
+
} catch (error) {
|
|
92
|
+
const stderr = typeof error?.stderr === "string" ? error.stderr : String(error?.stderr ?? "");
|
|
93
|
+
// Without --force, gh errors if the label exists. Treat that as idempotent
|
|
94
|
+
// success so re-runs are safe; any other failure propagates.
|
|
95
|
+
if (!options.force && /already exists/i.test(stderr)) {
|
|
96
|
+
return { ok: true, created: false, alreadyExists: true, name: options.name };
|
|
97
|
+
}
|
|
98
|
+
const detail = stderr.trim() || (error instanceof Error ? error.message : String(error));
|
|
99
|
+
throw new Error(`gh label create failed: ${detail}`);
|
|
100
|
+
}
|
|
101
|
+
return { ok: true, created: true, name: options.name, color: options.color, repo: options.repo };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function run(
|
|
105
|
+
argv = process.argv.slice(2),
|
|
106
|
+
{ stdout = process.stdout, stderr = process.stderr, ghCommand = "gh", exec = execFileSync } = {},
|
|
107
|
+
) {
|
|
108
|
+
let options;
|
|
109
|
+
try {
|
|
110
|
+
options = parseCreateLabelCliArgs(argv);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
stderr.write(`${formatCliError(error)}\n`);
|
|
113
|
+
return 1;
|
|
114
|
+
}
|
|
115
|
+
if (options.help) {
|
|
116
|
+
stdout.write(`${USAGE}\n`);
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
let result;
|
|
120
|
+
try {
|
|
121
|
+
result = createLabel(options, { ghCommand, exec });
|
|
122
|
+
} catch (error) {
|
|
123
|
+
stderr.write(`${JSON.stringify({ ok: false, error: error instanceof Error ? error.message : String(error) })}\n`);
|
|
124
|
+
return 1;
|
|
125
|
+
}
|
|
126
|
+
return emitResult(result, { jq: options.jq, silent: options.silent, stdout, stderr });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export const main = run;
|
|
130
|
+
|
|
131
|
+
if (isDirectCliRun(import.meta.url)) {
|
|
132
|
+
process.exitCode = run();
|
|
133
|
+
}
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
4
|
import { buildParseError, formatCliError, isDirectCliRun } from "../_core-helpers.mjs";
|
|
5
|
+
import { runChild as _runChild } from "../_cli-primitives.mjs";
|
|
6
|
+
import { resolveSettings, applyDevloopsBoard } from "../projects/_resolve-project.mjs";
|
|
7
|
+
import { main as addQueueItemMain } from "../projects/add-queue-item.mjs";
|
|
8
|
+
import { loadStateColumnMap, LOGICAL_COLUMN } from "@dev-loops/core/loop/queue-board-sync";
|
|
5
9
|
const USAGE = `Usage: create-pr.mjs [gh pr create args...]
|
|
6
10
|
Canonical PR-creation wrapper around \`gh pr create\`. Every PR opened through this
|
|
7
11
|
tool is ALWAYS a draft and is self-assigned by default. Never call raw \`gh pr create\`.
|
|
@@ -11,6 +15,16 @@ Behavior:
|
|
|
11
15
|
- honors an explicit \`--assignee <login>\` / \`-a <login>\` when supplied (no default injected)
|
|
12
16
|
- rejects \`--ready\` before invoking \`gh\`
|
|
13
17
|
- detects missing \`Closes #N\` / \`Fixes #N\` in \`--body\` or \`--body-file\` content (non-fatal stderr warning)
|
|
18
|
+
- \`--lightweight\` (consumed here in every form — bare or \`=true/1/false/0\`, last
|
|
19
|
+
occurrence wins — never forwarded to \`gh\`): when an explicit \`--body\`/
|
|
20
|
+
\`--body-file\` also carries no \`Closes #N\`/\`Fixes #N\`, the new PR is issue-less
|
|
21
|
+
lightweight and is auto-enqueued as a board PR item in the configured In Progress column
|
|
22
|
+
(reuses \`queue.projectNumber\` / \`queue.boardTitle\` from \`.devloops\`, same as the queue
|
|
23
|
+
scripts). Requires an explicit \`--repo owner/name\` (space or = form). A trailing stdout
|
|
24
|
+
line reports the outcome: \`{"board":{"enqueued":bool,...}}\`. No board configured, no
|
|
25
|
+
\`--repo\`, no explicit body source, or an enqueue error is a non-fatal no-op (noted in
|
|
26
|
+
that line; exit code unaffected). Omitting \`--lightweight\`, or a body that already
|
|
27
|
+
carries a closing keyword (tracker-backed), never calls the board.
|
|
14
28
|
- forwards every other argument to \`gh pr create\` unchanged
|
|
15
29
|
- preserves the underlying \`gh pr create\` stdout, stderr, and exit code
|
|
16
30
|
Examples:
|
|
@@ -26,8 +40,17 @@ Exit codes:
|
|
|
26
40
|
N same non-zero exit code returned by \`gh pr create\``.trim();
|
|
27
41
|
const parseError = buildParseError(USAGE);
|
|
28
42
|
const READY_FLAG_PATTERN = /^--ready(?:$|=)/u;
|
|
43
|
+
// Bare and inline-boolean forms, mirroring DRAFT_FLAG_PATTERN below: every
|
|
44
|
+
// matching token is consumed (never forwarded to gh, which rejects unknown
|
|
45
|
+
// flags), and the LAST occurrence decides — bare or =true/=1 enables,
|
|
46
|
+
// anything else (=false, =0, ...) disables.
|
|
47
|
+
const LIGHTWEIGHT_FLAG_PATTERN = /^--lightweight(?:=(.*))?$/iu;
|
|
48
|
+
// Both `--repo owner/name` and `--repo=owner/name` — gh accepts either form.
|
|
49
|
+
const REPO_FLAG_PATTERN = /^--repo(?:$|=)/u;
|
|
50
|
+
const PR_URL_NUMBER_PATTERN = /\/pull\/(\d+)(?:\D|$)/u;
|
|
29
51
|
const DRAFT_FLAG_PATTERN = /^--draft(?:=(.*))?$/iu;
|
|
30
|
-
|
|
52
|
+
// Shared inline-boolean truthiness for --draft= and --lightweight= values.
|
|
53
|
+
const TRUE_FLAG_VALUE_PATTERN = /^(?:true|1)$/iu;
|
|
31
54
|
// Detect both the long `--assignee`/`--assignee=<login>` forms and the `-a`
|
|
32
55
|
// short flag that `gh pr create` documents, so an explicit assignee in either
|
|
33
56
|
// form suppresses the `--assignee @me` default (otherwise a caller passing
|
|
@@ -56,8 +79,7 @@ async function resolveBody(args) {
|
|
|
56
79
|
}
|
|
57
80
|
return null; // unreadable → warn
|
|
58
81
|
}
|
|
59
|
-
|
|
60
|
-
const body = await resolveBody(args);
|
|
82
|
+
function warnMissingClosingKeyword(body) {
|
|
61
83
|
if (body === null) return; // no --body or --body-file, skip
|
|
62
84
|
if (!detectClosingKeyword(body)) {
|
|
63
85
|
process.stderr.write(
|
|
@@ -66,6 +88,48 @@ async function warnMissingClosingKeyword(args) {
|
|
|
66
88
|
);
|
|
67
89
|
}
|
|
68
90
|
}
|
|
91
|
+
// A plain string value for a single-value flag, in both the space form
|
|
92
|
+
// (`--repo owner/name`) and the inline form (`--repo=owner/name`); unlike
|
|
93
|
+
// resolveBody, never reads a file. Returns null when the flag is absent.
|
|
94
|
+
function getFlagValue(args, flagPattern) {
|
|
95
|
+
const idx = args.findIndex((token) => flagPattern.test(token));
|
|
96
|
+
if (idx === -1) return null;
|
|
97
|
+
const eq = args[idx].indexOf("=");
|
|
98
|
+
if (eq !== -1) return args[idx].slice(eq + 1);
|
|
99
|
+
return idx + 1 < args.length ? args[idx + 1] : null;
|
|
100
|
+
}
|
|
101
|
+
function parsePrNumberFromOutput(stdout) {
|
|
102
|
+
const match = PR_URL_NUMBER_PATTERN.exec(stdout ?? "");
|
|
103
|
+
return match ? Number(match[1]) : null;
|
|
104
|
+
}
|
|
105
|
+
// Auto-enqueue an issue-less lightweight PR as a board PR item in the configured
|
|
106
|
+
// In Progress column. Reuses the same .devloops queue.projectNumber /
|
|
107
|
+
// queue.boardTitle resolution and add-queue-item's idempotent add — never
|
|
108
|
+
// reimplements the board API calls. Never throws: an unconfigured board, a
|
|
109
|
+
// missing --repo, an unparsed PR number, or an enqueue failure are all
|
|
110
|
+
// non-fatal no-ops reported in the returned shape.
|
|
111
|
+
export async function enqueueIssuelessLightweightPr({ repo, prNumber, cwd, env, runChild }) {
|
|
112
|
+
if (!repo) return { enqueued: false, reason: "repo-not-specified" };
|
|
113
|
+
if (!Number.isInteger(prNumber) || prNumber < 1) return { enqueued: false, reason: "pr-number-not-parsed" };
|
|
114
|
+
const settings = resolveSettings(cwd);
|
|
115
|
+
if (!settings?.project && !settings?.title) {
|
|
116
|
+
return { enqueued: false, reason: "no-board-configured" };
|
|
117
|
+
}
|
|
118
|
+
const { columnNames, error: columnError } = loadStateColumnMap(cwd);
|
|
119
|
+
if (columnError) {
|
|
120
|
+
return { enqueued: false, reason: `config-error: ${columnError}` };
|
|
121
|
+
}
|
|
122
|
+
const args = { repo, item: prNumber };
|
|
123
|
+
applyDevloopsBoard(args, cwd);
|
|
124
|
+
args.column = columnNames[LOGICAL_COLUMN.IN_PROGRESS];
|
|
125
|
+
try {
|
|
126
|
+
const result = await addQueueItemMain(args, { env, runChild, cwd });
|
|
127
|
+
const { itemId, prNumber: itemPrNumber, status, alreadyPresent } = result.item;
|
|
128
|
+
return { enqueued: true, itemId, prNumber: itemPrNumber, status, alreadyPresent };
|
|
129
|
+
} catch (err) {
|
|
130
|
+
return { enqueued: false, reason: `enqueue-error: ${err instanceof Error ? err.message : String(err)}` };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
69
133
|
export function buildCreatePrArgs(argv) {
|
|
70
134
|
const args = [...argv];
|
|
71
135
|
if (args.includes("--help") || args.includes("-h")) {
|
|
@@ -79,7 +143,7 @@ export function buildCreatePrArgs(argv) {
|
|
|
79
143
|
}
|
|
80
144
|
const draftTokens = args.filter((token) => DRAFT_FLAG_PATTERN.test(token));
|
|
81
145
|
const lastDraftToken = draftTokens.length > 0 ? draftTokens.at(-1) : null;
|
|
82
|
-
const lastDraftSuppliesDraft = lastDraftToken === "--draft" || (typeof lastDraftToken === "string" &&
|
|
146
|
+
const lastDraftSuppliesDraft = lastDraftToken === "--draft" || (typeof lastDraftToken === "string" && TRUE_FLAG_VALUE_PATTERN.test(lastDraftToken.slice("--draft=".length)));
|
|
83
147
|
const hasAssignee = args.some((token) => ASSIGNEE_FLAG_PATTERN.test(token));
|
|
84
148
|
return {
|
|
85
149
|
help: false,
|
|
@@ -92,26 +156,71 @@ export function buildCreatePrArgs(argv) {
|
|
|
92
156
|
],
|
|
93
157
|
};
|
|
94
158
|
}
|
|
95
|
-
|
|
159
|
+
// captureStdout tees gh's stdout to a buffer (still writing it straight through to
|
|
160
|
+
// process.stdout, unbuffered) so the PR URL can be parsed once gh exits, without
|
|
161
|
+
// changing what a caller/terminal sees. Only enabled for the issue-less lightweight
|
|
162
|
+
// path; every other caller keeps the plain "inherit" byte-identical behavior.
|
|
163
|
+
export function spawnCreatePr(ghArgs, { ghCommand = "gh", env = process.env } = {}, { captureStdout = false } = {}) {
|
|
96
164
|
return new Promise((resolve, reject) => {
|
|
165
|
+
// `gh pr create` never reads stdin (body comes from --body/--body-file), so
|
|
166
|
+
// the captured path ignores it rather than inheriting: inheriting a
|
|
167
|
+
// never-closing stdin (e.g. an in-process test runner) would hang any child
|
|
168
|
+
// that waits for stdin to end.
|
|
97
169
|
const child = spawn(ghCommand, ghArgs, {
|
|
98
170
|
env,
|
|
99
|
-
stdio: "inherit",
|
|
171
|
+
stdio: captureStdout ? ["ignore", "pipe", "inherit"] : "inherit",
|
|
100
172
|
});
|
|
173
|
+
let stdout = "";
|
|
174
|
+
if (captureStdout) {
|
|
175
|
+
child.stdout.on("data", (chunk) => {
|
|
176
|
+
stdout += chunk;
|
|
177
|
+
process.stdout.write(chunk);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
101
180
|
child.on("error", reject);
|
|
102
181
|
child.on("close", (code) => {
|
|
103
|
-
resolve(typeof code === "number" ? code : 1);
|
|
182
|
+
resolve({ code: typeof code === "number" ? code : 1, stdout });
|
|
104
183
|
});
|
|
105
184
|
});
|
|
106
185
|
}
|
|
107
186
|
export async function main(argv = process.argv.slice(2), runtime = {}) {
|
|
108
|
-
|
|
187
|
+
// Last occurrence wins, same as the --draft handling in buildCreatePrArgs.
|
|
188
|
+
const lastLightweightToken = argv.filter((token) => LIGHTWEIGHT_FLAG_PATTERN.test(token)).at(-1) ?? null;
|
|
189
|
+
const lightweight = lastLightweightToken === "--lightweight" ||
|
|
190
|
+
(typeof lastLightweightToken === "string" && TRUE_FLAG_VALUE_PATTERN.test(lastLightweightToken.slice("--lightweight=".length)));
|
|
191
|
+
const forwardedArgv = argv.filter((token) => !LIGHTWEIGHT_FLAG_PATTERN.test(token));
|
|
192
|
+
const { help, ghArgs } = buildCreatePrArgs(forwardedArgv);
|
|
109
193
|
if (help) {
|
|
110
194
|
process.stdout.write(`${USAGE}\n`);
|
|
111
195
|
return 0;
|
|
112
196
|
}
|
|
113
|
-
await
|
|
114
|
-
|
|
197
|
+
const body = await resolveBody(forwardedArgv);
|
|
198
|
+
warnMissingClosingKeyword(body);
|
|
199
|
+
// Issue-less lightweight: caller signals lightweight AND an explicit body
|
|
200
|
+
// source (--body/--body-file) carries no closing keyword. A tracker-backed
|
|
201
|
+
// lightweight PR (closing keyword present) never reaches the board — its
|
|
202
|
+
// issue already owns the board entry. A null body (no explicit source) is
|
|
203
|
+
// NOT classified issue-less: the body may come from elsewhere (editor,
|
|
204
|
+
// template) and could carry a closing keyword this wrapper never saw, so it
|
|
205
|
+
// fails toward not enqueuing and reports body-not-provided instead.
|
|
206
|
+
const issueLess = lightweight && body !== null && !detectClosingKeyword(body);
|
|
207
|
+
const { code, stdout } = await spawnCreatePr(ghArgs, runtime, { captureStdout: issueLess });
|
|
208
|
+
if (lightweight && code === 0 && (issueLess || body === null)) {
|
|
209
|
+
const board = issueLess
|
|
210
|
+
? await enqueueIssuelessLightweightPr({
|
|
211
|
+
repo: getFlagValue(forwardedArgv, REPO_FLAG_PATTERN),
|
|
212
|
+
prNumber: parsePrNumberFromOutput(stdout),
|
|
213
|
+
cwd: runtime.cwd ?? process.cwd(),
|
|
214
|
+
env: runtime.env ?? process.env,
|
|
215
|
+
runChild: runtime.runChild ?? _runChild,
|
|
216
|
+
})
|
|
217
|
+
: { enqueued: false, reason: "body-not-provided" };
|
|
218
|
+
if (!board.enqueued) {
|
|
219
|
+
process.stderr.write(`[create-pr] Board note: PR not enqueued (${board.reason}).\n`);
|
|
220
|
+
}
|
|
221
|
+
process.stdout.write(`${JSON.stringify({ board })}\n`);
|
|
222
|
+
}
|
|
223
|
+
return code;
|
|
115
224
|
}
|
|
116
225
|
if (isDirectCliRun(import.meta.url)) {
|
|
117
226
|
try {
|