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,20 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { formatCliError, isDirectCliRun
|
|
3
|
-
import {
|
|
2
|
+
import { formatCliError, isDirectCliRun } from "../_core-helpers.mjs";
|
|
3
|
+
import { applyDevloopsBoard } from "./_resolve-project.mjs";
|
|
4
4
|
import { parseArgs } from "node:util";
|
|
5
|
-
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult } from "../lib/jq-output.mjs";
|
|
5
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
6
|
+
import { main, classifyExitCode } from "@dev-loops/core/projects/move-queue-item";
|
|
6
7
|
|
|
7
|
-
const USAGE = `Usage: dev-loops queue move --repo <owner/name> --project <number|id> --item <number|node-id> --to-column <name>
|
|
8
|
+
const USAGE = `Usage: dev-loops queue move --repo <owner/name> --project <number|id|board-uri> --item <number|node-id> --to-column <name>
|
|
8
9
|
(dev-loops project move … is a back-compat alias)
|
|
9
10
|
|
|
10
11
|
Move a GitHub Projects V2 item between Status columns.
|
|
11
12
|
|
|
12
13
|
Options:
|
|
13
|
-
--repo <owner/name>
|
|
14
|
-
--project <number|id>
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
--repo <owner/name> Required. Repository to scope the project search.
|
|
15
|
+
--project <number|id|board-uri> Project number, node ID, or board URI
|
|
16
|
+
(e.g. https://github.com/users/me/projects/3).
|
|
17
|
+
When omitted, resolved from .devloops
|
|
18
|
+
queue.projectNumber / queue.boardTitle.
|
|
19
|
+
--item <number|node-id> Required. Item to move: issue/PR number, or project item node ID.
|
|
20
|
+
--to-column <name> Required. Target Status column (e.g. "Next Up", "In Progress").
|
|
21
|
+
--help, -h Show this help.
|
|
18
22
|
|
|
19
23
|
Output (stdout):
|
|
20
24
|
JSON: { ok: true, item: { itemId, issueNumber, prNumber, previousColumn, newColumn } }
|
|
@@ -80,454 +84,18 @@ function parseCliArgs(argv) {
|
|
|
80
84
|
case "to-column":
|
|
81
85
|
args.toColumn = requireValue(token, "--to-column requires a value");
|
|
82
86
|
break;
|
|
83
|
-
|
|
84
|
-
args
|
|
85
|
-
break;
|
|
86
|
-
case "silent":
|
|
87
|
-
args.silent = true;
|
|
88
|
-
break;
|
|
89
|
-
default:
|
|
87
|
+
default: {
|
|
88
|
+
if (matchJqOutputToken(token, args, (t) => requireValue(t, "--jq requires a filter"))) break;
|
|
90
89
|
throw parseError(`Unknown flag: ${token.rawName}`);
|
|
90
|
+
}
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
return args;
|
|
94
94
|
}
|
|
95
|
-
// ── Validation ───────────────────────────────────────────────────────────
|
|
96
|
-
|
|
97
|
-
const OWNER_RE = /^[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$/;
|
|
98
|
-
const REPO_NAME_RE = /^[a-zA-Z0-9](?:[a-zA-Z0-9_.-]*[a-zA-Z0-9])?$/;
|
|
99
|
-
const GLOBAL_NODE_ID_RE = /^[A-Za-z0-9_]+$/;
|
|
100
|
-
|
|
101
|
-
function validateRepo(repo) {
|
|
102
|
-
if (!repo || typeof repo !== "string") {
|
|
103
|
-
throw Object.assign(new Error("--repo is required"), { code: "INVALID_REPO" });
|
|
104
|
-
}
|
|
105
|
-
const trimmed = repo.trim();
|
|
106
|
-
if (trimmed !== repo) {
|
|
107
|
-
throw Object.assign(new Error(`--repo must not have leading/trailing whitespace, got "${repo}"`), { code: "INVALID_REPO" });
|
|
108
|
-
}
|
|
109
|
-
const slashIdx = repo.indexOf("/");
|
|
110
|
-
if (slashIdx === -1) {
|
|
111
|
-
throw Object.assign(new Error(`--repo must be exactly owner/name, got "${repo}"`), { code: "INVALID_REPO" });
|
|
112
|
-
}
|
|
113
|
-
const owner = repo.slice(0, slashIdx);
|
|
114
|
-
const name = repo.slice(slashIdx + 1);
|
|
115
|
-
if (!owner || !name || !OWNER_RE.test(owner) || !REPO_NAME_RE.test(name)) {
|
|
116
|
-
throw Object.assign(new Error(`--repo must be exactly owner/name, got "${repo}"`), { code: "INVALID_REPO" });
|
|
117
|
-
}
|
|
118
|
-
return repo;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function parseProjectRef(raw) {
|
|
122
|
-
if (!raw || typeof raw !== "string" || raw.trim().length === 0) {
|
|
123
|
-
throw Object.assign(new Error("--project is required"), { code: "INVALID_PROJECT" });
|
|
124
|
-
}
|
|
125
|
-
const trimmed = raw.trim();
|
|
126
|
-
const asNum = Number(trimmed);
|
|
127
|
-
if (Number.isInteger(asNum) && asNum > 0 && String(asNum) === trimmed) {
|
|
128
|
-
return { kind: "number", value: asNum };
|
|
129
|
-
}
|
|
130
|
-
if (trimmed === "0") {
|
|
131
|
-
throw Object.assign(new Error(`--project must be a positive integer or a node ID, got "${raw}"`), { code: "INVALID_PROJECT" });
|
|
132
|
-
}
|
|
133
|
-
if (GLOBAL_NODE_ID_RE.test(trimmed)) {
|
|
134
|
-
return { kind: "id", value: trimmed };
|
|
135
|
-
}
|
|
136
|
-
throw Object.assign(new Error(`--project must be a positive integer or a node ID, got "${raw}"`), { code: "INVALID_PROJECT" });
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function parseItemRef(raw) {
|
|
140
|
-
if (!raw || typeof raw !== "string" || raw.trim().length === 0) {
|
|
141
|
-
throw Object.assign(new Error("--item is required"), { code: "INVALID_ITEM" });
|
|
142
|
-
}
|
|
143
|
-
const trimmed = raw.trim();
|
|
144
|
-
const asNum = Number(trimmed);
|
|
145
|
-
if (Number.isInteger(asNum) && asNum > 0 && String(asNum) === trimmed) {
|
|
146
|
-
return { kind: "number", value: asNum };
|
|
147
|
-
}
|
|
148
|
-
if (trimmed === "0") {
|
|
149
|
-
throw Object.assign(new Error(`--item must be a positive integer or an item node ID, got "${raw}"`), { code: "INVALID_ITEM" });
|
|
150
|
-
}
|
|
151
|
-
if (GLOBAL_NODE_ID_RE.test(trimmed)) {
|
|
152
|
-
return { kind: "id", value: trimmed };
|
|
153
|
-
}
|
|
154
|
-
throw Object.assign(new Error(`--item must be a positive integer or an item node ID, got "${raw}"`), { code: "INVALID_ITEM" });
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// ── API helpers ──────────────────────────────────────────────────────────
|
|
158
|
-
|
|
159
|
-
async function ghGraphql(query, vars, env, runChild = _runChild) {
|
|
160
|
-
const fieldArgs = [];
|
|
161
|
-
for (const [key, value] of Object.entries(vars)) {
|
|
162
|
-
fieldArgs.push("--field", `${key}=${value}`);
|
|
163
|
-
}
|
|
164
|
-
const result = await runChild(
|
|
165
|
-
"gh",
|
|
166
|
-
["api", "graphql", "--field", `query=${query}`, ...fieldArgs],
|
|
167
|
-
env,
|
|
168
|
-
);
|
|
169
|
-
if (result.code !== 0) {
|
|
170
|
-
const detail = result.stderr.trim() || `exit code ${result.code}`;
|
|
171
|
-
throw Object.assign(new Error(`gh api graphql failed: ${detail}`), { code: "GH_API_ERROR" });
|
|
172
|
-
}
|
|
173
|
-
const payload = parseJsonText(result.stdout);
|
|
174
|
-
if (payload.errors && payload.errors.length > 0) {
|
|
175
|
-
throw Object.assign(
|
|
176
|
-
new Error(`GraphQL errors: ${payload.errors.map((e) => e.message).join("; ")}`),
|
|
177
|
-
{ code: "GRAPHQL_ERROR" },
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
|
-
return payload;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// ── GraphQL fragments ────────────────────────────────────────────────────
|
|
184
|
-
|
|
185
|
-
const GET_USER_ID = [
|
|
186
|
-
"query($login:String!) {",
|
|
187
|
-
" user(login:$login) { id }",
|
|
188
|
-
"}"
|
|
189
|
-
].join("\n");
|
|
190
|
-
|
|
191
|
-
const GET_ORG_ID = [
|
|
192
|
-
"query($login:String!) {",
|
|
193
|
-
" organization(login:$login) { id }",
|
|
194
|
-
"}"
|
|
195
|
-
].join("\n");
|
|
196
|
-
|
|
197
|
-
const LIST_USER_PROJECTS = [
|
|
198
|
-
"query($login:String!, $after:String) {",
|
|
199
|
-
" user(login:$login) {",
|
|
200
|
-
" projectsV2(first:50, after:$after) {",
|
|
201
|
-
" pageInfo { hasNextPage endCursor }",
|
|
202
|
-
" nodes { id number title url }",
|
|
203
|
-
" }",
|
|
204
|
-
" }",
|
|
205
|
-
"}"
|
|
206
|
-
].join("\n");
|
|
207
|
-
|
|
208
|
-
const LIST_ORG_PROJECTS = [
|
|
209
|
-
"query($login:String!, $after:String) {",
|
|
210
|
-
" organization(login:$login) {",
|
|
211
|
-
" projectsV2(first:50, after:$after) {",
|
|
212
|
-
" pageInfo { hasNextPage endCursor }",
|
|
213
|
-
" nodes { id number title url }",
|
|
214
|
-
" }",
|
|
215
|
-
" }",
|
|
216
|
-
"}"
|
|
217
|
-
].join("\n");
|
|
218
|
-
|
|
219
|
-
const GET_PROJECT_FIELDS = [
|
|
220
|
-
"query($projectId:ID!, $after:String) {",
|
|
221
|
-
" node(id:$projectId) {",
|
|
222
|
-
" ... on ProjectV2 {",
|
|
223
|
-
" fields(first:50, after:$after) {",
|
|
224
|
-
" pageInfo { hasNextPage endCursor }",
|
|
225
|
-
" nodes {",
|
|
226
|
-
" ... on ProjectV2SingleSelectField {",
|
|
227
|
-
" id name",
|
|
228
|
-
" options { id name }",
|
|
229
|
-
" }",
|
|
230
|
-
" }",
|
|
231
|
-
" }",
|
|
232
|
-
" }",
|
|
233
|
-
" }",
|
|
234
|
-
"}"
|
|
235
|
-
].join("\n");
|
|
236
|
-
|
|
237
|
-
const GET_PROJECT_ITEMS_BY_CONTENT = [
|
|
238
|
-
"query($projectId:ID!, $after:String) {",
|
|
239
|
-
" node(id:$projectId) {",
|
|
240
|
-
" ... on ProjectV2 {",
|
|
241
|
-
" items(first:100, after:$after, orderBy:{field:POSITION, direction:ASC}) {",
|
|
242
|
-
" pageInfo { hasNextPage endCursor }",
|
|
243
|
-
" nodes {",
|
|
244
|
-
" id",
|
|
245
|
-
" fieldValues(first:20) {",
|
|
246
|
-
" nodes {",
|
|
247
|
-
" ... on ProjectV2ItemFieldSingleSelectValue {",
|
|
248
|
-
" field { ... on ProjectV2SingleSelectField { id name } }",
|
|
249
|
-
" name",
|
|
250
|
-
" }",
|
|
251
|
-
" }",
|
|
252
|
-
" }",
|
|
253
|
-
" content {",
|
|
254
|
-
" ... on Issue { __typename number repository { nameWithOwner } }",
|
|
255
|
-
" ... on PullRequest { __typename number repository { nameWithOwner } }",
|
|
256
|
-
" }",
|
|
257
|
-
" }",
|
|
258
|
-
" }",
|
|
259
|
-
" }",
|
|
260
|
-
" }",
|
|
261
|
-
"}"
|
|
262
|
-
].join("\n");
|
|
263
|
-
|
|
264
|
-
const UPDATE_ITEM_FIELD = [
|
|
265
|
-
"mutation($projectId:ID!, $itemId:ID!, $fieldId:ID!, $optionId:String!) {",
|
|
266
|
-
" updateProjectV2ItemFieldValue(input:{projectId:$projectId, itemId:$itemId, fieldId:$fieldId, value:{singleSelectOptionId:$optionId}}) {",
|
|
267
|
-
" projectV2Item {",
|
|
268
|
-
" id",
|
|
269
|
-
" }",
|
|
270
|
-
" }",
|
|
271
|
-
"}"
|
|
272
|
-
].join("\n");
|
|
273
|
-
|
|
274
|
-
// ── Owner resolution ────────────────────────────────────────────────────
|
|
275
|
-
|
|
276
|
-
async function resolveOwner(login, env, runChild) {
|
|
277
|
-
const userPayload = await ghGraphql(GET_USER_ID, { login }, env, runChild);
|
|
278
|
-
if (userPayload?.data?.user?.id) {
|
|
279
|
-
return { id: userPayload.data.user.id, kind: "user" };
|
|
280
|
-
}
|
|
281
|
-
const orgPayload = await ghGraphql(GET_ORG_ID, { login }, env, runChild);
|
|
282
|
-
if (orgPayload?.data?.organization?.id) {
|
|
283
|
-
return { id: orgPayload.data.organization.id, kind: "org" };
|
|
284
|
-
}
|
|
285
|
-
throw Object.assign(
|
|
286
|
-
new Error(`Could not resolve owner ID for "${login}"`),
|
|
287
|
-
{ code: "NO_USER_ID" },
|
|
288
|
-
);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// ── Paginated project listing ────────────────────────────────────────────
|
|
292
|
-
|
|
293
|
-
async function listAllProjects(login, kind, env, runChild) {
|
|
294
|
-
const query = kind === "org" ? LIST_ORG_PROJECTS : LIST_USER_PROJECTS;
|
|
295
|
-
const projects = [];
|
|
296
|
-
let after = null;
|
|
297
|
-
while (true) {
|
|
298
|
-
const vars = { login };
|
|
299
|
-
if (after) vars.after = after;
|
|
300
|
-
const payload = await ghGraphql(query, vars, env, runChild);
|
|
301
|
-
const connection = kind === "org"
|
|
302
|
-
? payload?.data?.organization?.projectsV2
|
|
303
|
-
: payload?.data?.user?.projectsV2;
|
|
304
|
-
const nodes = connection?.nodes ?? [];
|
|
305
|
-
projects.push(...nodes);
|
|
306
|
-
const pageInfo = connection?.pageInfo ?? {};
|
|
307
|
-
if (!pageInfo.hasNextPage) break;
|
|
308
|
-
if (!pageInfo.endCursor) {
|
|
309
|
-
throw Object.assign(
|
|
310
|
-
new Error("Invalid projects list payload: hasNextPage is true but endCursor is missing"),
|
|
311
|
-
{ code: "GH_API_ERROR" },
|
|
312
|
-
);
|
|
313
|
-
}
|
|
314
|
-
after = pageInfo.endCursor;
|
|
315
|
-
}
|
|
316
|
-
return projects;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// ── Paginated field listing ──────────────────────────────────────────────
|
|
320
|
-
|
|
321
|
-
async function listAllFields(projectId, env, runChild) {
|
|
322
|
-
const fields = [];
|
|
323
|
-
let after = null;
|
|
324
|
-
while (true) {
|
|
325
|
-
const vars = { projectId };
|
|
326
|
-
if (after) vars.after = after;
|
|
327
|
-
const payload = await ghGraphql(GET_PROJECT_FIELDS, vars, env, runChild);
|
|
328
|
-
const connection = payload?.data?.node?.fields;
|
|
329
|
-
const nodes = connection?.nodes ?? [];
|
|
330
|
-
fields.push(...nodes);
|
|
331
|
-
const pageInfo = connection?.pageInfo ?? {};
|
|
332
|
-
if (!pageInfo.hasNextPage) break;
|
|
333
|
-
if (!pageInfo.endCursor) {
|
|
334
|
-
throw Object.assign(
|
|
335
|
-
new Error("Invalid fields payload: hasNextPage is true but endCursor is missing"),
|
|
336
|
-
{ code: "GH_API_ERROR" },
|
|
337
|
-
);
|
|
338
|
-
}
|
|
339
|
-
after = pageInfo.endCursor;
|
|
340
|
-
}
|
|
341
|
-
return fields;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// ── Paginated item listing (position order) ──────────────────────────────
|
|
345
|
-
|
|
346
|
-
async function fetchAllItems(projectId, env, runChild) {
|
|
347
|
-
const items = [];
|
|
348
|
-
let after = null;
|
|
349
|
-
while (true) {
|
|
350
|
-
const vars = { projectId };
|
|
351
|
-
if (after) vars.after = after;
|
|
352
|
-
const payload = await ghGraphql(GET_PROJECT_ITEMS_BY_CONTENT, vars, env, runChild);
|
|
353
|
-
const connection = payload?.data?.node?.items;
|
|
354
|
-
const nodes = connection?.nodes ?? [];
|
|
355
|
-
items.push(...nodes);
|
|
356
|
-
const pageInfo = connection?.pageInfo ?? {};
|
|
357
|
-
if (!pageInfo.hasNextPage) break;
|
|
358
|
-
if (!pageInfo.endCursor) {
|
|
359
|
-
throw Object.assign(
|
|
360
|
-
new Error("Invalid items payload: hasNextPage is true but endCursor is missing"),
|
|
361
|
-
{ code: "GH_API_ERROR" },
|
|
362
|
-
);
|
|
363
|
-
}
|
|
364
|
-
after = pageInfo.endCursor;
|
|
365
|
-
}
|
|
366
|
-
return items;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
function statusOf(node) {
|
|
370
|
-
const fvs = node?.fieldValues?.nodes ?? [];
|
|
371
|
-
for (const fv of fvs) {
|
|
372
|
-
if (fv && fv.field && fv.field.name === "Status") return fv.name;
|
|
373
|
-
}
|
|
374
|
-
return null;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
// ── Exit code classification ────────────────────────────────────────────
|
|
378
|
-
|
|
379
|
-
function classifyExitCode(err) {
|
|
380
|
-
if (err.code === "INVALID_REPO" || err.code === "INVALID_PROJECT" || err.code === "INVALID_ITEM" ||
|
|
381
|
-
err.code === "INVALID_COLUMN" || err.code === "INVALID_ARGS") return 1;
|
|
382
|
-
if (err.code === "PROJECT_NOT_FOUND" || err.code === "FIELD_NOT_FOUND" || err.code === "COLUMN_NOT_FOUND" ||
|
|
383
|
-
err.code === "ITEM_NOT_FOUND") return 3;
|
|
384
|
-
return 2;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
// ── Main logic ──────────────────────────────────────────────────────────
|
|
388
|
-
|
|
389
|
-
async function main(args, { env = process.env, runChild } = {}) {
|
|
390
|
-
const child = runChild ?? _runChild;
|
|
391
|
-
const repo = validateRepo(args.repo);
|
|
392
|
-
const [owner, repoName] = repo.split("/");
|
|
393
|
-
const projectRef = parseProjectRef(args.project);
|
|
394
|
-
const itemRef = parseItemRef(args.item);
|
|
395
|
-
const toColumn = (args.toColumn ?? "").trim();
|
|
396
|
-
if (!toColumn) {
|
|
397
|
-
throw Object.assign(new Error("--to-column is required"), { code: "INVALID_COLUMN" });
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
// 1. Resolve owner
|
|
401
|
-
const { id: ownerId, kind: ownerKind } = await resolveOwner(owner, env, child);
|
|
402
|
-
|
|
403
|
-
// 2. Resolve project
|
|
404
|
-
const projects = await listAllProjects(owner, ownerKind, env, child);
|
|
405
|
-
let project;
|
|
406
|
-
if (projectRef.kind === "id") {
|
|
407
|
-
project = projects.find((p) => p.id === projectRef.value);
|
|
408
|
-
} else {
|
|
409
|
-
project = projects.find((p) => p.number === projectRef.value);
|
|
410
|
-
}
|
|
411
|
-
if (!project) {
|
|
412
|
-
throw Object.assign(
|
|
413
|
-
new Error(`Project ${projectRef.kind === "id" ? `"${projectRef.value}"` : `number ${projectRef.value}`} not found under owner "${owner}"`),
|
|
414
|
-
{ code: "PROJECT_NOT_FOUND" },
|
|
415
|
-
);
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// 3. Resolve Status field and target column
|
|
419
|
-
const fieldNodes = await listAllFields(project.id, env, child);
|
|
420
|
-
const statusField = fieldNodes.find((f) => f.name === "Status" && f.options);
|
|
421
|
-
if (!statusField) {
|
|
422
|
-
throw Object.assign(
|
|
423
|
-
new Error(`Status field not found in project "${project.title}" (number ${project.number})`),
|
|
424
|
-
{ code: "FIELD_NOT_FOUND" },
|
|
425
|
-
);
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
const targetOption = statusField.options.find((o) => o.name === toColumn);
|
|
429
|
-
if (!targetOption) {
|
|
430
|
-
const available = statusField.options.map((o) => o.name).join(", ");
|
|
431
|
-
throw Object.assign(
|
|
432
|
-
new Error(`Column "${toColumn}" not found in Status field. Available: ${available}`),
|
|
433
|
-
{ code: "COLUMN_NOT_FOUND" },
|
|
434
|
-
);
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
// 4. Find the item.
|
|
438
|
-
//
|
|
439
|
-
// Fetch the full board item list ONCE (paginated, position order) and resolve
|
|
440
|
-
// BOTH ref kinds against it. This reuses the proven pattern from
|
|
441
|
-
// reorder-queue-item / list-queue-items: a node-id ref matches by item.id, a
|
|
442
|
-
// number ref matches by content.number. Both are scoped to the requested repo
|
|
443
|
-
// so a cross-project ref fails closed with ITEM_NOT_FOUND. (The previous code
|
|
444
|
-
// used `ProjectV2.item` — a field that does not exist — for the node-id path,
|
|
445
|
-
// and a single non-paginated `items(first:10)` page for the number path, so it
|
|
446
|
-
// could not find items beyond the first page.)
|
|
447
|
-
const allItems = await fetchAllItems(project.id, env, child);
|
|
448
|
-
|
|
449
|
-
let match;
|
|
450
|
-
if (itemRef.kind === "id") {
|
|
451
|
-
match = allItems.find(
|
|
452
|
-
(it) => it.id === itemRef.value && it.content?.repository?.nameWithOwner === repo,
|
|
453
|
-
);
|
|
454
|
-
if (!match) {
|
|
455
|
-
throw Object.assign(
|
|
456
|
-
new Error(`Item "${itemRef.value}" not found in project "${project.title}" for repo "${repo}"`),
|
|
457
|
-
{ code: "ITEM_NOT_FOUND" },
|
|
458
|
-
);
|
|
459
|
-
}
|
|
460
|
-
} else {
|
|
461
|
-
match = allItems.find(
|
|
462
|
-
(it) =>
|
|
463
|
-
it.content &&
|
|
464
|
-
it.content.repository?.nameWithOwner === repo &&
|
|
465
|
-
it.content.number === itemRef.value,
|
|
466
|
-
);
|
|
467
|
-
if (!match) {
|
|
468
|
-
throw Object.assign(
|
|
469
|
-
new Error(`Item #${itemRef.value} not found in project "${project.title}" for repo "${repo}"`),
|
|
470
|
-
{ code: "ITEM_NOT_FOUND" },
|
|
471
|
-
);
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
const itemId = match.id;
|
|
476
|
-
const previousColumn = statusOf(match);
|
|
477
|
-
let issueNumber = null;
|
|
478
|
-
let prNumber = null;
|
|
479
|
-
if (match.content) {
|
|
480
|
-
if (match.content.__typename === "PullRequest") {
|
|
481
|
-
prNumber = match.content.number;
|
|
482
|
-
} else {
|
|
483
|
-
issueNumber = match.content.number;
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
// 5. No-op if already at target column
|
|
488
|
-
if (previousColumn === toColumn) {
|
|
489
|
-
return {
|
|
490
|
-
ok: true,
|
|
491
|
-
item: {
|
|
492
|
-
itemId,
|
|
493
|
-
issueNumber,
|
|
494
|
-
prNumber,
|
|
495
|
-
previousColumn,
|
|
496
|
-
newColumn: toColumn,
|
|
497
|
-
unchanged: true,
|
|
498
|
-
},
|
|
499
|
-
};
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
// 6. Update Status via mutation
|
|
503
|
-
const updatePayload = await ghGraphql(UPDATE_ITEM_FIELD, {
|
|
504
|
-
projectId: project.id,
|
|
505
|
-
itemId,
|
|
506
|
-
fieldId: statusField.id,
|
|
507
|
-
optionId: targetOption.id,
|
|
508
|
-
}, env, child);
|
|
509
|
-
|
|
510
|
-
const updated = updatePayload?.data?.updateProjectV2ItemFieldValue?.projectV2Item;
|
|
511
|
-
if (!updated) {
|
|
512
|
-
throw Object.assign(new Error("Failed to update item field value"), { code: "MUTATION_FAILED" });
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
return {
|
|
516
|
-
ok: true,
|
|
517
|
-
item: {
|
|
518
|
-
itemId,
|
|
519
|
-
issueNumber,
|
|
520
|
-
prNumber,
|
|
521
|
-
previousColumn,
|
|
522
|
-
newColumn: toColumn,
|
|
523
|
-
unchanged: false,
|
|
524
|
-
},
|
|
525
|
-
};
|
|
526
|
-
}
|
|
527
95
|
|
|
528
96
|
// ── CLI entrypoint ──────────────────────────────────────────────────────
|
|
529
97
|
|
|
530
|
-
async function runCli(argv, { stdout = process.stdout, stderr = process.stderr, env = process.env } = {}) {
|
|
98
|
+
async function runCli(argv, { stdout = process.stdout, stderr = process.stderr, env = process.env, cwd = process.cwd() } = {}) {
|
|
531
99
|
let args;
|
|
532
100
|
try {
|
|
533
101
|
args = parseCliArgs(argv);
|
|
@@ -540,6 +108,10 @@ async function runCli(argv, { stdout = process.stdout, stderr = process.stderr,
|
|
|
540
108
|
stdout.write(USAGE);
|
|
541
109
|
return;
|
|
542
110
|
}
|
|
111
|
+
|
|
112
|
+
// Resolve the board from .devloops when --project is absent.
|
|
113
|
+
applyDevloopsBoard(args, cwd);
|
|
114
|
+
|
|
543
115
|
try {
|
|
544
116
|
const result = await main(args, { env });
|
|
545
117
|
process.exitCode = emitResult(result, { jq: args.jq, silent: args.silent, stdout, stderr });
|