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,10 +1,12 @@
|
|
|
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/list-queue-items";
|
|
6
7
|
|
|
7
|
-
const USAGE = `Usage: dev-loops queue list --repo <owner/name> --project <number|id> [--column <name>] [--limit <n>]
|
|
8
|
+
const USAGE = `Usage: dev-loops queue list --repo <owner/name> [--project <number|id>] [--column <name>] [--limit <n>]
|
|
9
|
+
dev-loops queue list --repo <owner/name> [--project <number|id>] --summary [--done-limit <n>]
|
|
8
10
|
(dev-loops project list … is a back-compat alias)
|
|
9
11
|
|
|
10
12
|
List GitHub Projects V2 items filtered by Status column, ordered by position
|
|
@@ -12,13 +14,29 @@ ascending. Returns machine-readable JSON.
|
|
|
12
14
|
|
|
13
15
|
Options:
|
|
14
16
|
--repo <owner/name> Required. Repository to scope the project search.
|
|
15
|
-
--project <number|id>
|
|
17
|
+
--project <number|id> Project number (integer) or node ID. When omitted,
|
|
18
|
+
resolved from .devloops queue.projectNumber /
|
|
19
|
+
queue.boardTitle.
|
|
16
20
|
--column <name> Filter items by Status column value (e.g. "Next Up").
|
|
17
|
-
--limit <n> Return at most <n> items.
|
|
21
|
+
--limit <n> Return at most <n> items (flat mode only).
|
|
22
|
+
--summary Whole-board digest grouped by Status column, in board
|
|
23
|
+
column order. Emits { ok, groups: { <status>: { count, items } } }.
|
|
24
|
+
--group-by status Alias for --summary. Only "status" is supported.
|
|
25
|
+
--done-limit <n> With --summary: cap the "Done" group's items array to
|
|
26
|
+
<n> (or the last/terminal board column if no column is
|
|
27
|
+
named "Done"). Count stays the true total; use 0 for
|
|
28
|
+
counts only.
|
|
18
29
|
--help, -h Show this help.
|
|
19
30
|
|
|
31
|
+
Grouping / aggregation is done via --summary (this mode). Do NOT pipe flat
|
|
32
|
+
output through inline parsers (e.g. \`| python3\`) or reduce/group_by jq filters
|
|
33
|
+
to build a per-status digest — the summary mode is the sanctioned one-call path.
|
|
34
|
+
|
|
35
|
+
--summary is mutually exclusive with --column and --limit (both exit 1).
|
|
36
|
+
|
|
20
37
|
Output (stdout):
|
|
21
|
-
|
|
38
|
+
flat: { ok: true, items: [{ issueNumber, prNumber, title, url, itemId, contentId, status }, ...] }
|
|
39
|
+
summary: { ok: true, groups: { "<Status>": { count, items: [ <item>, ... ] }, ... } }
|
|
22
40
|
|
|
23
41
|
${JQ_OUTPUT_USAGE}
|
|
24
42
|
|
|
@@ -47,6 +65,9 @@ function parseCliArgs(argv) {
|
|
|
47
65
|
project: { type: "string" },
|
|
48
66
|
column: { type: "string" },
|
|
49
67
|
limit: { type: "string" },
|
|
68
|
+
summary: { type: "boolean" },
|
|
69
|
+
"group-by": { type: "string" },
|
|
70
|
+
"done-limit": { type: "string" },
|
|
50
71
|
help: { type: "boolean", short: "h" },
|
|
51
72
|
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
52
73
|
},
|
|
@@ -87,389 +108,41 @@ function parseCliArgs(argv) {
|
|
|
87
108
|
args.limit = val;
|
|
88
109
|
break;
|
|
89
110
|
}
|
|
90
|
-
case "
|
|
91
|
-
|
|
111
|
+
case "summary":
|
|
112
|
+
if (token.value !== undefined) {
|
|
113
|
+
throw parseError(`Unknown flag: ${token.rawName}=${token.value}`);
|
|
114
|
+
}
|
|
115
|
+
args.summary = true;
|
|
92
116
|
break;
|
|
93
|
-
case "
|
|
94
|
-
|
|
117
|
+
case "group-by": {
|
|
118
|
+
const val = requireValue(token, "--group-by requires a value (only \"status\" is supported)");
|
|
119
|
+
if (val !== "status") {
|
|
120
|
+
throw parseError(`--group-by only supports "status", got "${val}"`);
|
|
121
|
+
}
|
|
122
|
+
args.summary = true;
|
|
95
123
|
break;
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const OWNER_RE = /^[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$/;
|
|
105
|
-
const REPO_NAME_RE = /^[a-zA-Z0-9](?:[a-zA-Z0-9_.-]*[a-zA-Z0-9])?$/;
|
|
106
|
-
const GLOBAL_NODE_ID_RE = /^[A-Za-z0-9_]+$/;
|
|
107
|
-
|
|
108
|
-
function validateRepo(repo) {
|
|
109
|
-
if (!repo || typeof repo !== "string") {
|
|
110
|
-
throw Object.assign(new Error("--repo is required"), { code: "INVALID_REPO" });
|
|
111
|
-
}
|
|
112
|
-
const trimmed = repo.trim();
|
|
113
|
-
if (trimmed !== repo) {
|
|
114
|
-
throw Object.assign(
|
|
115
|
-
new Error(`--repo must not have leading/trailing whitespace, got "${repo}"`),
|
|
116
|
-
{ code: "INVALID_REPO" },
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
const slashIdx = repo.indexOf("/");
|
|
120
|
-
if (slashIdx === -1) {
|
|
121
|
-
throw Object.assign(new Error(`--repo must be exactly owner/name, got "${repo}"`), { code: "INVALID_REPO" });
|
|
122
|
-
}
|
|
123
|
-
const owner = repo.slice(0, slashIdx);
|
|
124
|
-
const name = repo.slice(slashIdx + 1);
|
|
125
|
-
if (!owner || !name || !OWNER_RE.test(owner) || !REPO_NAME_RE.test(name)) {
|
|
126
|
-
throw Object.assign(new Error(`--repo must be exactly owner/name, got "${repo}"`), { code: "INVALID_REPO" });
|
|
127
|
-
}
|
|
128
|
-
return repo;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function parseProjectRef(raw) {
|
|
132
|
-
if (!raw || typeof raw !== "string" || raw.trim().length === 0) {
|
|
133
|
-
throw Object.assign(new Error("--project is required"), { code: "INVALID_PROJECT" });
|
|
134
|
-
}
|
|
135
|
-
const trimmed = raw.trim();
|
|
136
|
-
const asNum = Number(trimmed);
|
|
137
|
-
if (Number.isInteger(asNum) && asNum > 0 && String(asNum) === trimmed) {
|
|
138
|
-
return { kind: "number", value: asNum };
|
|
139
|
-
}
|
|
140
|
-
// Reject bare "0" — valid node ID character but not a meaningful project reference
|
|
141
|
-
if (trimmed === "0") {
|
|
142
|
-
throw Object.assign(
|
|
143
|
-
new Error(`--project must be a positive integer or a node ID, got "${raw}"`),
|
|
144
|
-
{ code: "INVALID_PROJECT" },
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
if (GLOBAL_NODE_ID_RE.test(trimmed)) {
|
|
148
|
-
return { kind: "id", value: trimmed };
|
|
149
|
-
}
|
|
150
|
-
throw Object.assign(
|
|
151
|
-
new Error(`--project must be a positive integer or a node ID, got "${raw}"`),
|
|
152
|
-
{ code: "INVALID_PROJECT" },
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// ── API helpers ──────────────────────────────────────────────────────────
|
|
157
|
-
|
|
158
|
-
async function ghGraphql(query, vars, env, runChild = _runChild) {
|
|
159
|
-
const fieldArgs = [];
|
|
160
|
-
for (const [key, value] of Object.entries(vars)) {
|
|
161
|
-
fieldArgs.push("--field", `${key}=${value}`);
|
|
162
|
-
}
|
|
163
|
-
const result = await runChild(
|
|
164
|
-
"gh",
|
|
165
|
-
["api", "graphql", "--field", `query=${query}`, ...fieldArgs],
|
|
166
|
-
env,
|
|
167
|
-
);
|
|
168
|
-
if (result.code !== 0) {
|
|
169
|
-
const detail = result.stderr.trim() || `exit code ${result.code}`;
|
|
170
|
-
throw Object.assign(new Error(`gh api graphql failed: ${detail}`), { code: "GH_API_ERROR" });
|
|
171
|
-
}
|
|
172
|
-
const payload = parseJsonText(result.stdout);
|
|
173
|
-
if (payload.errors && payload.errors.length > 0) {
|
|
174
|
-
throw Object.assign(
|
|
175
|
-
new Error(`GraphQL errors: ${payload.errors.map((e) => e.message).join("; ")}`),
|
|
176
|
-
{ code: "GRAPHQL_ERROR" },
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
return payload;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// ── GraphQL fragments ────────────────────────────────────────────────────
|
|
183
|
-
|
|
184
|
-
const GET_USER_ID = [
|
|
185
|
-
"query($login:String!) {",
|
|
186
|
-
" user(login:$login) { id }",
|
|
187
|
-
"}"
|
|
188
|
-
].join("\n");
|
|
189
|
-
|
|
190
|
-
const GET_ORG_ID = [
|
|
191
|
-
"query($login:String!) {",
|
|
192
|
-
" organization(login:$login) { id }",
|
|
193
|
-
"}"
|
|
194
|
-
].join("\n");
|
|
195
|
-
|
|
196
|
-
const LIST_USER_PROJECTS = [
|
|
197
|
-
"query($login:String!, $after:String) {",
|
|
198
|
-
" user(login:$login) {",
|
|
199
|
-
" projectsV2(first:50, after:$after) {",
|
|
200
|
-
" pageInfo { hasNextPage endCursor }",
|
|
201
|
-
" nodes { id number title url }",
|
|
202
|
-
" }",
|
|
203
|
-
" }",
|
|
204
|
-
"}"
|
|
205
|
-
].join("\n");
|
|
206
|
-
|
|
207
|
-
const LIST_ORG_PROJECTS = [
|
|
208
|
-
"query($login:String!, $after:String) {",
|
|
209
|
-
" organization(login:$login) {",
|
|
210
|
-
" projectsV2(first:50, after:$after) {",
|
|
211
|
-
" pageInfo { hasNextPage endCursor }",
|
|
212
|
-
" nodes { id number title url }",
|
|
213
|
-
" }",
|
|
214
|
-
" }",
|
|
215
|
-
"}"
|
|
216
|
-
].join("\n");
|
|
217
|
-
|
|
218
|
-
const GET_PROJECT_FIELDS = [
|
|
219
|
-
"query($projectId:ID!, $after:String) {",
|
|
220
|
-
" node(id:$projectId) {",
|
|
221
|
-
" ... on ProjectV2 {",
|
|
222
|
-
" fields(first:50, after:$after) {",
|
|
223
|
-
" pageInfo { hasNextPage endCursor }",
|
|
224
|
-
" nodes {",
|
|
225
|
-
" ... on ProjectV2SingleSelectField {",
|
|
226
|
-
" id name",
|
|
227
|
-
" options { id name }",
|
|
228
|
-
" }",
|
|
229
|
-
" }",
|
|
230
|
-
" }",
|
|
231
|
-
" }",
|
|
232
|
-
" }",
|
|
233
|
-
"}"
|
|
234
|
-
].join("\n");
|
|
235
|
-
|
|
236
|
-
const GET_PROJECT_ITEMS = [
|
|
237
|
-
"query($projectId:ID!, $after:String) {",
|
|
238
|
-
" node(id:$projectId) {",
|
|
239
|
-
" ... on ProjectV2 {",
|
|
240
|
-
" items(first:100, after:$after) {",
|
|
241
|
-
" pageInfo { hasNextPage endCursor }",
|
|
242
|
-
" nodes {",
|
|
243
|
-
" id",
|
|
244
|
-
" fieldValues(first:20) {",
|
|
245
|
-
" nodes {",
|
|
246
|
-
" ... on ProjectV2ItemFieldSingleSelectValue {",
|
|
247
|
-
" field { ... on ProjectV2SingleSelectField { id name } }",
|
|
248
|
-
" name",
|
|
249
|
-
" }",
|
|
250
|
-
" }",
|
|
251
|
-
" }",
|
|
252
|
-
" content {",
|
|
253
|
-
" ... on Issue { number title url id }",
|
|
254
|
-
" ... on PullRequest { number title url id }",
|
|
255
|
-
" }",
|
|
256
|
-
" }",
|
|
257
|
-
" }",
|
|
258
|
-
" }",
|
|
259
|
-
" }",
|
|
260
|
-
"}"
|
|
261
|
-
].join("\n");
|
|
262
|
-
|
|
263
|
-
// ── Owner resolution ────────────────────────────────────────────────────
|
|
264
|
-
|
|
265
|
-
async function resolveOwner(login, env, runChild) {
|
|
266
|
-
const userPayload = await ghGraphql(GET_USER_ID, { login }, env, runChild);
|
|
267
|
-
if (userPayload?.data?.user?.id) {
|
|
268
|
-
return { id: userPayload.data.user.id, kind: "user" };
|
|
269
|
-
}
|
|
270
|
-
const orgPayload = await ghGraphql(GET_ORG_ID, { login }, env, runChild);
|
|
271
|
-
if (orgPayload?.data?.organization?.id) {
|
|
272
|
-
return { id: orgPayload.data.organization.id, kind: "org" };
|
|
273
|
-
}
|
|
274
|
-
throw Object.assign(
|
|
275
|
-
new Error(`Could not resolve owner ID for "${login}"`),
|
|
276
|
-
{ code: "NO_USER_ID" },
|
|
277
|
-
);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// ── Paginated project listing ────────────────────────────────────────────
|
|
281
|
-
|
|
282
|
-
async function listAllProjects(login, kind, env, runChild) {
|
|
283
|
-
const query = kind === "org" ? LIST_ORG_PROJECTS : LIST_USER_PROJECTS;
|
|
284
|
-
const projects = [];
|
|
285
|
-
let after = null;
|
|
286
|
-
while (true) {
|
|
287
|
-
const vars = { login };
|
|
288
|
-
if (after) vars.after = after;
|
|
289
|
-
const payload = await ghGraphql(query, vars, env, runChild);
|
|
290
|
-
const connection = kind === "org"
|
|
291
|
-
? payload?.data?.organization?.projectsV2
|
|
292
|
-
: payload?.data?.user?.projectsV2;
|
|
293
|
-
const nodes = connection?.nodes ?? [];
|
|
294
|
-
projects.push(...nodes);
|
|
295
|
-
const pageInfo = connection?.pageInfo ?? {};
|
|
296
|
-
if (!pageInfo.hasNextPage) break;
|
|
297
|
-
if (!pageInfo.endCursor) {
|
|
298
|
-
throw Object.assign(
|
|
299
|
-
new Error("Invalid projects list payload: hasNextPage is true but endCursor is missing"),
|
|
300
|
-
{ code: "GH_API_ERROR" },
|
|
301
|
-
);
|
|
302
|
-
}
|
|
303
|
-
after = pageInfo.endCursor;
|
|
304
|
-
}
|
|
305
|
-
return projects;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// ── Paginated field listing ──────────────────────────────────────────────
|
|
309
|
-
|
|
310
|
-
async function listAllFields(projectId, env, runChild) {
|
|
311
|
-
const fields = [];
|
|
312
|
-
let after = null;
|
|
313
|
-
while (true) {
|
|
314
|
-
const vars = { projectId };
|
|
315
|
-
if (after) vars.after = after;
|
|
316
|
-
const payload = await ghGraphql(GET_PROJECT_FIELDS, vars, env, runChild);
|
|
317
|
-
const connection = payload?.data?.node?.fields;
|
|
318
|
-
const nodes = connection?.nodes ?? [];
|
|
319
|
-
fields.push(...nodes);
|
|
320
|
-
const pageInfo = connection?.pageInfo ?? {};
|
|
321
|
-
if (!pageInfo.hasNextPage) break;
|
|
322
|
-
if (!pageInfo.endCursor) {
|
|
323
|
-
throw Object.assign(
|
|
324
|
-
new Error("Invalid fields payload: hasNextPage is true but endCursor is missing"),
|
|
325
|
-
{ code: "GH_API_ERROR" },
|
|
326
|
-
);
|
|
327
|
-
}
|
|
328
|
-
after = pageInfo.endCursor;
|
|
329
|
-
}
|
|
330
|
-
return fields;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// ── Paginated item listing ───────────────────────────────────────────────
|
|
334
|
-
|
|
335
|
-
async function listAllItems(projectId, env, runChild) {
|
|
336
|
-
const items = [];
|
|
337
|
-
let after = null;
|
|
338
|
-
while (true) {
|
|
339
|
-
const vars = { projectId };
|
|
340
|
-
if (after) vars.after = after;
|
|
341
|
-
const payload = await ghGraphql(GET_PROJECT_ITEMS, vars, env, runChild);
|
|
342
|
-
const connection = payload?.data?.node?.items;
|
|
343
|
-
const nodes = connection?.nodes ?? [];
|
|
344
|
-
items.push(...nodes);
|
|
345
|
-
const pageInfo = connection?.pageInfo ?? {};
|
|
346
|
-
if (!pageInfo.hasNextPage) break;
|
|
347
|
-
if (!pageInfo.endCursor) {
|
|
348
|
-
throw Object.assign(
|
|
349
|
-
new Error("Invalid items payload: hasNextPage is true but endCursor is missing"),
|
|
350
|
-
{ code: "GH_API_ERROR" },
|
|
351
|
-
);
|
|
352
|
-
}
|
|
353
|
-
after = pageInfo.endCursor;
|
|
354
|
-
}
|
|
355
|
-
return items;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// ── Exit code classification ────────────────────────────────────────────
|
|
359
|
-
|
|
360
|
-
function classifyExitCode(err) {
|
|
361
|
-
if (err.code === "INVALID_REPO" || err.code === "INVALID_PROJECT" || err.code === "INVALID_ARGS") return 1;
|
|
362
|
-
if (err.code === "PROJECT_NOT_FOUND" || err.code === "FIELD_NOT_FOUND" || err.code === "COLUMN_NOT_FOUND") return 3;
|
|
363
|
-
return 2;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// ── Main logic ──────────────────────────────────────────────────────────
|
|
367
|
-
|
|
368
|
-
async function main(args, { env = process.env, runChild } = {}) {
|
|
369
|
-
const child = runChild ?? _runChild;
|
|
370
|
-
const repo = validateRepo(args.repo);
|
|
371
|
-
const [owner] = repo.split("/");
|
|
372
|
-
const projectRef = parseProjectRef(args.project);
|
|
373
|
-
|
|
374
|
-
// 1. Resolve owner (user or org)
|
|
375
|
-
const { id: ownerId, kind: ownerKind } = await resolveOwner(owner, env, child);
|
|
376
|
-
|
|
377
|
-
// 2. Resolve project
|
|
378
|
-
let project;
|
|
379
|
-
if (projectRef.kind === "id") {
|
|
380
|
-
// Direct ID: use it directly (verify it belongs to owner via projects list)
|
|
381
|
-
const projects = await listAllProjects(owner, ownerKind, env, child);
|
|
382
|
-
project = projects.find((p) => p.id === projectRef.value);
|
|
383
|
-
if (!project) {
|
|
384
|
-
throw Object.assign(
|
|
385
|
-
new Error(`Project with ID "${projectRef.value}" not found under owner "${owner}"`),
|
|
386
|
-
{ code: "PROJECT_NOT_FOUND" },
|
|
387
|
-
);
|
|
388
|
-
}
|
|
389
|
-
} else {
|
|
390
|
-
// By number
|
|
391
|
-
const projects = await listAllProjects(owner, ownerKind, env, child);
|
|
392
|
-
project = projects.find((p) => p.number === projectRef.value);
|
|
393
|
-
if (!project) {
|
|
394
|
-
throw Object.assign(
|
|
395
|
-
new Error(`Project number ${projectRef.value} not found under owner "${owner}"`),
|
|
396
|
-
{ code: "PROJECT_NOT_FOUND" },
|
|
397
|
-
);
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// 3. Resolve Status field and target column
|
|
402
|
-
const fieldNodes = await listAllFields(project.id, env, child);
|
|
403
|
-
const statusField = fieldNodes.find((f) => f.name === "Status" && f.options);
|
|
404
|
-
if (!statusField) {
|
|
405
|
-
throw Object.assign(
|
|
406
|
-
new Error(`Status field not found in project "${project.title}" (number ${project.number})`),
|
|
407
|
-
{ code: "FIELD_NOT_FOUND" },
|
|
408
|
-
);
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
let targetOption = null;
|
|
412
|
-
if (args.column) {
|
|
413
|
-
targetOption = statusField.options.find(
|
|
414
|
-
(o) => o.name === args.column,
|
|
415
|
-
);
|
|
416
|
-
if (!targetOption) {
|
|
417
|
-
const available = statusField.options.map((o) => o.name).join(", ");
|
|
418
|
-
throw Object.assign(
|
|
419
|
-
new Error(
|
|
420
|
-
`Column "${args.column}" not found in Status field. Available: ${available}`,
|
|
421
|
-
),
|
|
422
|
-
{ code: "COLUMN_NOT_FOUND" },
|
|
423
|
-
);
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
// 4. List and filter items (ordered by position ascending, GraphQL default)
|
|
428
|
-
const rawItems = await listAllItems(project.id, env, child);
|
|
429
|
-
|
|
430
|
-
const results = [];
|
|
431
|
-
for (const item of rawItems) {
|
|
432
|
-
const content = item.content;
|
|
433
|
-
if (!content) continue;
|
|
434
|
-
|
|
435
|
-
// Determine status from field values
|
|
436
|
-
let status = null;
|
|
437
|
-
const fieldValues = item.fieldValues?.nodes ?? [];
|
|
438
|
-
for (const fv of fieldValues) {
|
|
439
|
-
if (fv && fv.field && fv.field.name === "Status") {
|
|
440
|
-
status = fv.name;
|
|
124
|
+
}
|
|
125
|
+
case "done-limit": {
|
|
126
|
+
const raw = requireValue(token, "--done-limit requires a non-negative integer");
|
|
127
|
+
const val = Number(raw);
|
|
128
|
+
if (!Number.isInteger(val) || val < 0) {
|
|
129
|
+
throw parseError(`--done-limit must be a non-negative integer, got "${raw}"`);
|
|
130
|
+
}
|
|
131
|
+
args.doneLimit = val;
|
|
441
132
|
break;
|
|
442
133
|
}
|
|
134
|
+
default: {
|
|
135
|
+
if (matchJqOutputToken(token, args, (t) => requireValue(t, "--jq requires a filter"))) break;
|
|
136
|
+
throw parseError(`Unknown flag: ${token.rawName}`);
|
|
137
|
+
}
|
|
443
138
|
}
|
|
444
|
-
|
|
445
|
-
// Filter by column
|
|
446
|
-
if (args.column && status !== args.column) continue;
|
|
447
|
-
|
|
448
|
-
const isPr = content.__typename === "PullRequest";
|
|
449
|
-
|
|
450
|
-
results.push({
|
|
451
|
-
issueNumber: isPr ? null : content.number,
|
|
452
|
-
prNumber: isPr ? content.number : null,
|
|
453
|
-
title: content.title ?? null,
|
|
454
|
-
url: content.url ?? null,
|
|
455
|
-
itemId: item.id,
|
|
456
|
-
contentId: content.id ?? null,
|
|
457
|
-
status: status ?? null,
|
|
458
|
-
});
|
|
459
139
|
}
|
|
460
|
-
|
|
461
|
-
// 5. Items are returned in position order from GraphQL. Apply limit.
|
|
462
|
-
const limited = args.limit ? results.slice(0, args.limit) : results;
|
|
463
|
-
|
|
464
|
-
return {
|
|
465
|
-
ok: true,
|
|
466
|
-
items: limited,
|
|
467
|
-
};
|
|
140
|
+
return args;
|
|
468
141
|
}
|
|
469
142
|
|
|
470
143
|
// ── CLI entrypoint ──────────────────────────────────────────────────────
|
|
471
144
|
|
|
472
|
-
async function runCli(argv, { stdout = process.stdout, stderr = process.stderr, env = process.env } = {}) {
|
|
145
|
+
async function runCli(argv, { stdout = process.stdout, stderr = process.stderr, env = process.env, cwd = process.cwd(), runChild } = {}) {
|
|
473
146
|
let args;
|
|
474
147
|
try {
|
|
475
148
|
args = parseCliArgs(argv);
|
|
@@ -482,8 +155,12 @@ async function runCli(argv, { stdout = process.stdout, stderr = process.stderr,
|
|
|
482
155
|
stdout.write(USAGE);
|
|
483
156
|
return;
|
|
484
157
|
}
|
|
158
|
+
|
|
159
|
+
// Resolve the board from .devloops when --project is absent.
|
|
160
|
+
applyDevloopsBoard(args, cwd);
|
|
161
|
+
|
|
485
162
|
try {
|
|
486
|
-
const result = await main(args, { env });
|
|
163
|
+
const result = await main(args, { env, runChild });
|
|
487
164
|
process.exitCode = emitResult(result, { jq: args.jq, silent: args.silent, stdout, stderr });
|
|
488
165
|
} catch (err) {
|
|
489
166
|
stderr.write(JSON.stringify({ ok: false, error: err.message, code: err.code ?? "UNKNOWN" }) + "\n");
|
|
@@ -498,4 +175,4 @@ if (isDirectCliRun(import.meta.url)) {
|
|
|
498
175
|
});
|
|
499
176
|
}
|
|
500
177
|
|
|
501
|
-
export { main };
|
|
178
|
+
export { main, parseCliArgs, runCli };
|