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
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Reconcile board Status columns from live GitHub state (#1069). For each queue
|
|
3
|
+
// item it derives the target column from live facts (merged PR / closed issue →
|
|
4
|
+
// Done; open ready non-draft PR → In Progress) and moves ONLY the items whose
|
|
5
|
+
// derived column differs from their current Status. Backlog/Next Up ordering is
|
|
6
|
+
// left untouched (items that derive null are skipped). Idempotent: a second run
|
|
7
|
+
// over a converged board performs no moves.
|
|
8
|
+
import { formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
|
|
9
|
+
import { parseArgs } from "node:util";
|
|
10
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
11
|
+
import { applyDevloopsBoard } from "./_resolve-project.mjs";
|
|
12
|
+
import { main as listQueueItems } from "./list-queue-items.mjs";
|
|
13
|
+
import { main as moveQueueItem } from "./move-queue-item.mjs";
|
|
14
|
+
import { detectLinkedIssuePr } from "../github/detect-linked-issue-pr.mjs";
|
|
15
|
+
import { planReconcile, loadStateColumnMap, LOGICAL_COLUMN } from "@dev-loops/core/loop/queue-board-sync";
|
|
16
|
+
import { runChild as _runChild } from "../_cli-primitives.mjs";
|
|
17
|
+
|
|
18
|
+
const USAGE = `Usage: dev-loops queue reconcile --repo <owner/name> [--project <number|id|board-uri>]
|
|
19
|
+
(dev-loops project reconcile … is a back-compat alias)
|
|
20
|
+
|
|
21
|
+
Reconcile board Status columns from live GitHub state. Derives each item's
|
|
22
|
+
target column from live GitHub facts (merged PR or closed issue → Done; an open,
|
|
23
|
+
ready non-draft PR → In Progress) and moves ONLY the items whose derived column
|
|
24
|
+
differs from their current Status. Backlog/Next Up ordering is left untouched.
|
|
25
|
+
Idempotent: a second run over a converged board performs no moves. Best-effort:
|
|
26
|
+
individual per-item failures are recorded but do not fail the run.
|
|
27
|
+
|
|
28
|
+
Options:
|
|
29
|
+
--repo <owner/name> Required. Repository to scope the project search.
|
|
30
|
+
--project <number|id|board-uri>
|
|
31
|
+
Project number (integer), node ID, or board URI. When
|
|
32
|
+
omitted, resolved from .devloops queue.projectNumber /
|
|
33
|
+
queue.boardTitle.
|
|
34
|
+
--help, -h Show this help.
|
|
35
|
+
|
|
36
|
+
Output (stdout):
|
|
37
|
+
JSON: { ok: true, moved, unchanged, reconciled: [{ number, from, to, ok }, ...] }
|
|
38
|
+
|
|
39
|
+
${JQ_OUTPUT_USAGE}
|
|
40
|
+
|
|
41
|
+
Exit codes:
|
|
42
|
+
0 — success (reconcile is best-effort; per-item failures do not fail the run)
|
|
43
|
+
1 — usage or argument error
|
|
44
|
+
2 — GitHub API error / invalid --jq filter
|
|
45
|
+
3 — project, field, or column not found
|
|
46
|
+
`.trim();
|
|
47
|
+
|
|
48
|
+
function parseCliArgs(argv) {
|
|
49
|
+
const parseError = (message) => Object.assign(new Error(message), { usage: USAGE, code: "INVALID_ARGS" });
|
|
50
|
+
const requireValue = (token, message) => {
|
|
51
|
+
const v = token.value;
|
|
52
|
+
if (typeof v !== "string" || v.length === 0 || v.startsWith("-")) {
|
|
53
|
+
throw parseError(message);
|
|
54
|
+
}
|
|
55
|
+
return v;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const args = {};
|
|
59
|
+
const { tokens } = parseArgs({
|
|
60
|
+
args: [...argv],
|
|
61
|
+
options: {
|
|
62
|
+
repo: { type: "string" },
|
|
63
|
+
project: { type: "string" },
|
|
64
|
+
help: { type: "boolean", short: "h" },
|
|
65
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
66
|
+
},
|
|
67
|
+
allowPositionals: true,
|
|
68
|
+
strict: false,
|
|
69
|
+
tokens: true,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
for (const token of tokens) {
|
|
73
|
+
if (token.kind === "positional") {
|
|
74
|
+
throw parseError(`Unexpected argument: ${token.value}`);
|
|
75
|
+
}
|
|
76
|
+
if (token.kind !== "option") {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
switch (token.name) {
|
|
80
|
+
case "help":
|
|
81
|
+
if (token.value !== undefined) {
|
|
82
|
+
throw parseError(`Unknown flag: ${token.rawName}=${token.value}`);
|
|
83
|
+
}
|
|
84
|
+
args.help = true;
|
|
85
|
+
break;
|
|
86
|
+
case "repo":
|
|
87
|
+
args.repo = requireValue(token, "--repo requires a value (owner/name)");
|
|
88
|
+
break;
|
|
89
|
+
case "project":
|
|
90
|
+
args.project = requireValue(token, "--project requires a value (number or node ID)");
|
|
91
|
+
break;
|
|
92
|
+
default: {
|
|
93
|
+
if (matchJqOutputToken(token, args, (t) => requireValue(t, "--jq requires a filter"))) break;
|
|
94
|
+
throw parseError(`Unknown flag: ${token.rawName}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return args;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function ghJson(argv, { env, runChild }) {
|
|
102
|
+
const child = runChild ?? _runChild;
|
|
103
|
+
const result = await child("gh", argv, env);
|
|
104
|
+
if (result.code !== 0) {
|
|
105
|
+
const detail = result.stderr?.trim() || `exit code ${result.code}`;
|
|
106
|
+
throw Object.assign(new Error(`gh command failed: ${detail}`), { code: "GH_API_ERROR" });
|
|
107
|
+
}
|
|
108
|
+
return parseJsonText(result.stdout);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Real live-facts gatherer. For each item, resolve GitHub state into the fact
|
|
112
|
+
// shape consumed by deriveReconcileColumn. Keyed by the stable item node id
|
|
113
|
+
// (item.itemId), not the bare number, so a multi-repo board where two items
|
|
114
|
+
// share a number (repo-A PR #5 vs repo-B issue #5) cannot collide. Per-item gh
|
|
115
|
+
// failures are swallowed (best-effort): the item records all-null PR fields →
|
|
116
|
+
// derives null → untouched.
|
|
117
|
+
export async function gatherLiveFacts(items, repo, { env, runChild, doneColumn } = {}) {
|
|
118
|
+
const byItemId = new Map();
|
|
119
|
+
for (const item of items) {
|
|
120
|
+
const number = item.prNumber != null ? item.prNumber : item.issueNumber;
|
|
121
|
+
if (number == null || item.itemId == null) continue;
|
|
122
|
+
// Done is terminal for reconcile; planReconcile treats a missing fact as
|
|
123
|
+
// unchanged. At loop startup (doneColumn set) we skip the gh calls for
|
|
124
|
+
// Done items entirely — terminal + perf. An explicit `dev-loops queue
|
|
125
|
+
// reconcile` run passes no doneColumn, so Done items ARE gathered and a
|
|
126
|
+
// reopened/re-linked artifact can be moved back out of Done (recovery).
|
|
127
|
+
if (doneColumn != null && item.status === doneColumn) continue;
|
|
128
|
+
try {
|
|
129
|
+
if (item.prNumber != null) {
|
|
130
|
+
const pr = await ghJson(["pr", "view", String(item.prNumber), "--repo", repo, "--json", "state,isDraft,mergedAt"], { env, runChild });
|
|
131
|
+
byItemId.set(item.itemId, {
|
|
132
|
+
itemKind: "pr",
|
|
133
|
+
issueState: null,
|
|
134
|
+
prState: pr?.mergedAt ? "MERGED" : String(pr?.state ?? "").toUpperCase(),
|
|
135
|
+
prIsDraft: pr?.isDraft === true,
|
|
136
|
+
});
|
|
137
|
+
} else {
|
|
138
|
+
const issue = await ghJson(["issue", "view", String(item.issueNumber), "--repo", repo, "--json", "state"], { env, runChild });
|
|
139
|
+
const issueState = String(issue?.state ?? "").toUpperCase();
|
|
140
|
+
if (issueState === "CLOSED") {
|
|
141
|
+
byItemId.set(item.itemId, { itemKind: "issue", issueState: "CLOSED", prState: null, prIsDraft: null });
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
let prState = null;
|
|
145
|
+
let prIsDraft = null;
|
|
146
|
+
const linkage = await detectLinkedIssuePr({ repo, issue: item.issueNumber }, { env, runChild });
|
|
147
|
+
if (linkage?.hasOpenLinkedPr) {
|
|
148
|
+
const pr = await ghJson(["pr", "view", String(linkage.prNumber), "--repo", repo, "--json", "state,isDraft,mergedAt"], { env, runChild });
|
|
149
|
+
prState = pr?.mergedAt ? "MERGED" : String(pr?.state ?? "").toUpperCase();
|
|
150
|
+
prIsDraft = pr?.isDraft === true;
|
|
151
|
+
}
|
|
152
|
+
byItemId.set(item.itemId, { itemKind: "issue", issueState: "OPEN", prState, prIsDraft });
|
|
153
|
+
}
|
|
154
|
+
} catch {
|
|
155
|
+
// Best-effort: record inert facts so the item derives null (untouched).
|
|
156
|
+
byItemId.set(item.itemId, {
|
|
157
|
+
itemKind: item.prNumber != null ? "pr" : "issue",
|
|
158
|
+
issueState: item.prNumber != null ? null : "OPEN",
|
|
159
|
+
prState: null,
|
|
160
|
+
prIsDraft: null,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return byItemId;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function main(args, { env = process.env, runChild, cwd = process.cwd(), listItems, gatherFacts, moveItem, skipTerminalColumn = false } = {}) {
|
|
168
|
+
// Resolve the board from .devloops when --project is absent. Idempotent (only
|
|
169
|
+
// mutates when args.project === undefined), so callers that reach main()
|
|
170
|
+
// directly (e.g. the loop-startup self-heal) still resolve the board.
|
|
171
|
+
applyDevloopsBoard(args, cwd);
|
|
172
|
+
|
|
173
|
+
const list = await (listItems ?? ((a, o) => listQueueItems(a, o)))(
|
|
174
|
+
{ repo: args.repo, project: args.project, projectTitle: args.projectTitle },
|
|
175
|
+
{ env, runChild },
|
|
176
|
+
);
|
|
177
|
+
const items = list.items ?? [];
|
|
178
|
+
|
|
179
|
+
const { columnNames } = loadStateColumnMap(cwd);
|
|
180
|
+
|
|
181
|
+
// Loop startup (skipTerminalColumn: true) skips Done items for speed. An
|
|
182
|
+
// explicit CLI run (default false) leaves doneColumn null so Done items are
|
|
183
|
+
// gathered and a reopened/re-linked artifact can be recovered out of Done.
|
|
184
|
+
const doneColumn = skipTerminalColumn ? columnNames[LOGICAL_COLUMN.DONE] : null;
|
|
185
|
+
|
|
186
|
+
const factsByItemId = await (gatherFacts ?? ((its, repo, o) => gatherLiveFacts(its, repo, o)))(
|
|
187
|
+
items,
|
|
188
|
+
args.repo,
|
|
189
|
+
{ env, runChild, doneColumn },
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const { moves, unchanged } = planReconcile(items, factsByItemId, columnNames);
|
|
193
|
+
|
|
194
|
+
const move = moveItem ?? ((a, o) => moveQueueItem(a, o));
|
|
195
|
+
const reconciled = [];
|
|
196
|
+
let moved = 0;
|
|
197
|
+
for (const m of moves) {
|
|
198
|
+
try {
|
|
199
|
+
// Move by the stable item node id (move-queue-item accepts a node id per
|
|
200
|
+
// its `--item <number|node-id>` contract) so a multi-repo number collision
|
|
201
|
+
// can never target the wrong item.
|
|
202
|
+
await move(
|
|
203
|
+
{ repo: args.repo, project: args.project, projectTitle: args.projectTitle, item: m.itemId, toColumn: m.to },
|
|
204
|
+
{ env, runChild },
|
|
205
|
+
);
|
|
206
|
+
reconciled.push({ number: m.number, from: m.from, to: m.to, ok: true });
|
|
207
|
+
moved += 1;
|
|
208
|
+
} catch (err) {
|
|
209
|
+
// Best-effort: a single failed move does not abort the reconcile loop.
|
|
210
|
+
reconciled.push({ number: m.number, from: m.from, to: m.to, ok: false, error: err?.message ?? "move failed" });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return { ok: true, moved, unchanged, reconciled };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function classifyExitCode(err) {
|
|
218
|
+
if (err.code === "INVALID_ARGS" || err.code === "INVALID_REPO" || err.code === "INVALID_PROJECT") return 1;
|
|
219
|
+
if (err.code === "PROJECT_NOT_FOUND" || err.code === "FIELD_NOT_FOUND" || err.code === "COLUMN_NOT_FOUND") return 3;
|
|
220
|
+
return 2;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function runCli(argv, { stdout = process.stdout, stderr = process.stderr, env = process.env, cwd = process.cwd(), runChild } = {}) {
|
|
224
|
+
let args;
|
|
225
|
+
try {
|
|
226
|
+
args = parseCliArgs(argv);
|
|
227
|
+
} catch (err) {
|
|
228
|
+
stderr.write(`${formatCliError(err)}\n`);
|
|
229
|
+
process.exitCode = 1;
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (args.help) {
|
|
233
|
+
stdout.write(USAGE);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
const result = await main(args, { env, runChild, cwd });
|
|
239
|
+
process.exitCode = emitResult(result, { jq: args.jq, silent: args.silent, stdout, stderr });
|
|
240
|
+
} catch (err) {
|
|
241
|
+
stderr.write(JSON.stringify({ ok: false, error: err.message, code: err.code ?? "UNKNOWN" }) + "\n");
|
|
242
|
+
process.exitCode = classifyExitCode(err);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (isDirectCliRun(import.meta.url)) {
|
|
247
|
+
runCli(process.argv.slice(2)).catch((error) => {
|
|
248
|
+
process.stderr.write(JSON.stringify({ ok: false, error: error.message, code: error.code ?? "UNKNOWN" }) + "\n");
|
|
249
|
+
process.exitCode = 2;
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export { main, parseCliArgs, runCli };
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
|
|
3
3
|
import { runChild as _runChild } from "../_cli-primitives.mjs";
|
|
4
|
+
import { resolveProjectSelector, findProject, applyDevloopsBoard, parseItemRef } from "./_resolve-project.mjs";
|
|
4
5
|
import { parseArgs } from "node:util";
|
|
6
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
5
7
|
|
|
6
8
|
const USAGE = `Usage:
|
|
7
|
-
dev-loops queue reorder --repo <owner/name> --project <number|id> --item <number|node-id> [--after <number|node-id>]
|
|
8
|
-
dev-loops queue reorder move-to-top <ref> --repo <owner/name> --project <number|id>
|
|
9
|
-
dev-loops queue reorder move-after <ref> <after-ref> --repo <owner/name> --project <number|id>
|
|
10
|
-
dev-loops queue reorder order <ref1> <ref2> ... --repo <owner/name> --project <number|id>
|
|
9
|
+
dev-loops queue reorder --repo <owner/name> --project <number|id|board-uri> --item <number|node-id> [--after <number|node-id>]
|
|
10
|
+
dev-loops queue reorder move-to-top <ref> --repo <owner/name> --project <number|id|board-uri>
|
|
11
|
+
dev-loops queue reorder move-after <ref> <after-ref> --repo <owner/name> --project <number|id|board-uri>
|
|
12
|
+
dev-loops queue reorder order <ref1> <ref2> ... --repo <owner/name> --project <number|id|board-uri>
|
|
11
13
|
(dev-loops project reorder … is a back-compat alias)
|
|
12
14
|
|
|
13
15
|
Reorder GitHub Projects V2 items by board position via updateProjectV2ItemPosition.
|
|
@@ -22,22 +24,27 @@ Forms:
|
|
|
22
24
|
A <ref> is an issue/PR number OR a project item node ID. Works for both issues and PRs.
|
|
23
25
|
|
|
24
26
|
Options:
|
|
25
|
-
--repo <owner/name>
|
|
26
|
-
--project <number|id>
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
--
|
|
27
|
+
--repo <owner/name> Required. Repository to scope the project search.
|
|
28
|
+
--project <number|id|board-uri> Project number, node ID, or board URI
|
|
29
|
+
(e.g. https://github.com/users/me/projects/3).
|
|
30
|
+
When omitted, resolved from .devloops
|
|
31
|
+
queue.projectNumber / queue.boardTitle.
|
|
32
|
+
--item <number|node-id> Flag form: item to reorder.
|
|
33
|
+
--after <number|node-id> Flag form: position after this item. When omitted, move to top.
|
|
34
|
+
--dry-run Print the intended GraphQL mutation(s) without executing.
|
|
35
|
+
--help, -h Show this help.
|
|
31
36
|
|
|
32
37
|
Output (stdout):
|
|
33
38
|
JSON. Move/move-to-top: { ok, item, after_ref|null, before, after }.
|
|
34
39
|
order: { ok, moves: [...], before, after }.
|
|
35
40
|
dry-run: { ok, dryRun: true, mutations: [{ query, variables }], before }.
|
|
36
41
|
|
|
42
|
+
${JQ_OUTPUT_USAGE}
|
|
43
|
+
|
|
37
44
|
Exit codes:
|
|
38
45
|
0 — success
|
|
39
46
|
1 — usage or argument error
|
|
40
|
-
2 — GitHub API error
|
|
47
|
+
2 — GitHub API error / invalid --jq filter
|
|
41
48
|
3 — project, item, or after-item not found
|
|
42
49
|
`.trim();
|
|
43
50
|
|
|
@@ -70,6 +77,7 @@ function parseCliArgs(argv) {
|
|
|
70
77
|
after: { type: "string" },
|
|
71
78
|
"dry-run": { type: "boolean" },
|
|
72
79
|
help: { type: "boolean", short: "h" },
|
|
80
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
73
81
|
},
|
|
74
82
|
allowPositionals: true,
|
|
75
83
|
strict: false,
|
|
@@ -109,8 +117,10 @@ function parseCliArgs(argv) {
|
|
|
109
117
|
}
|
|
110
118
|
args.dryRun = true;
|
|
111
119
|
break;
|
|
112
|
-
default:
|
|
120
|
+
default: {
|
|
121
|
+
if (matchJqOutputToken(token, args, (t) => requireValue(t, "--jq requires a filter"))) break;
|
|
113
122
|
throw parseError(`Unknown flag: ${token.rawName}`);
|
|
123
|
+
}
|
|
114
124
|
}
|
|
115
125
|
}
|
|
116
126
|
return args;
|
|
@@ -119,7 +129,6 @@ function parseCliArgs(argv) {
|
|
|
119
129
|
|
|
120
130
|
const OWNER_RE = /^[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$/;
|
|
121
131
|
const REPO_NAME_RE = /^[a-zA-Z0-9](?:[a-zA-Z0-9_.-]*[a-zA-Z0-9])?$/;
|
|
122
|
-
const GLOBAL_NODE_ID_RE = /^[A-Za-z0-9_]+$/;
|
|
123
132
|
|
|
124
133
|
function validateRepo(repo) {
|
|
125
134
|
if (!repo || typeof repo !== "string") {
|
|
@@ -144,42 +153,6 @@ function validateRepo(repo) {
|
|
|
144
153
|
return repo;
|
|
145
154
|
}
|
|
146
155
|
|
|
147
|
-
function parseProjectRef(raw) {
|
|
148
|
-
if (!raw || typeof raw !== "string" || raw.trim().length === 0) {
|
|
149
|
-
throw Object.assign(new Error("--project is required"), { code: "INVALID_PROJECT" });
|
|
150
|
-
}
|
|
151
|
-
const trimmed = raw.trim();
|
|
152
|
-
const asNum = Number(trimmed);
|
|
153
|
-
if (Number.isInteger(asNum) && asNum > 0 && String(asNum) === trimmed) {
|
|
154
|
-
return { kind: "number", value: asNum };
|
|
155
|
-
}
|
|
156
|
-
if (trimmed === "0") {
|
|
157
|
-
throw Object.assign(new Error(`--project must be a positive integer or a node ID, got "${raw}"`), { code: "INVALID_PROJECT" });
|
|
158
|
-
}
|
|
159
|
-
if (GLOBAL_NODE_ID_RE.test(trimmed)) {
|
|
160
|
-
return { kind: "id", value: trimmed };
|
|
161
|
-
}
|
|
162
|
-
throw Object.assign(new Error(`--project must be a positive integer or a node ID, got "${raw}"`), { code: "INVALID_PROJECT" });
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function parseItemRef(raw) {
|
|
166
|
-
if (!raw || typeof raw !== "string" || raw.trim().length === 0) {
|
|
167
|
-
throw Object.assign(new Error("--item is required"), { code: "INVALID_ITEM" });
|
|
168
|
-
}
|
|
169
|
-
const trimmed = raw.trim();
|
|
170
|
-
const asNum = Number(trimmed);
|
|
171
|
-
if (Number.isInteger(asNum) && asNum > 0 && String(asNum) === trimmed) {
|
|
172
|
-
return { kind: "number", value: asNum };
|
|
173
|
-
}
|
|
174
|
-
if (trimmed === "0") {
|
|
175
|
-
throw Object.assign(new Error(`--item must be a positive integer or an item node ID, got "${raw}"`), { code: "INVALID_ITEM" });
|
|
176
|
-
}
|
|
177
|
-
if (GLOBAL_NODE_ID_RE.test(trimmed)) {
|
|
178
|
-
return { kind: "id", value: trimmed };
|
|
179
|
-
}
|
|
180
|
-
throw Object.assign(new Error(`--item must be a positive integer or an item node ID, got "${raw}"`), { code: "INVALID_ITEM" });
|
|
181
|
-
}
|
|
182
|
-
|
|
183
156
|
// ── API helpers ──────────────────────────────────────────────────────────
|
|
184
157
|
|
|
185
158
|
async function ghGraphql(query, vars, env, runChild = _runChild) {
|
|
@@ -522,19 +495,17 @@ function classifyExitCode(err) {
|
|
|
522
495
|
|
|
523
496
|
// ── Resolve owner + project (shared) ──────────────────────────────────────
|
|
524
497
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
}
|
|
537
|
-
return project;
|
|
498
|
+
// Resolve the project based on a selector (from resolveProjectSelector).
|
|
499
|
+
// When the selector contains a URI ref, the URI-encoded owner overrides the
|
|
500
|
+
// repo-derived owner so cross-scope boards (user vs org) resolve unambiguously.
|
|
501
|
+
async function resolveProject(repoOwner, selector, env, child) {
|
|
502
|
+
const projectRef = selector.projectRef;
|
|
503
|
+
const effectiveOwner = projectRef?.kind === "uri" ? projectRef.owner : repoOwner;
|
|
504
|
+
const ownerKind = projectRef?.kind === "uri"
|
|
505
|
+
? projectRef.ownerKind
|
|
506
|
+
: (await resolveOwner(repoOwner, env, child)).kind;
|
|
507
|
+
const projects = await listAllProjects(effectiveOwner, ownerKind, env, child);
|
|
508
|
+
return findProject(projects, selector, effectiveOwner);
|
|
538
509
|
}
|
|
539
510
|
|
|
540
511
|
function executePosition(projectId, itemId, afterId) {
|
|
@@ -551,7 +522,7 @@ function executePosition(projectId, itemId, afterId) {
|
|
|
551
522
|
async function mainFlagForm(args, { env, child, repo, owner, repoName, project }) {
|
|
552
523
|
const itemRef = parseItemRef(args.item);
|
|
553
524
|
let afterRef = null;
|
|
554
|
-
if (args.after !== undefined) afterRef = parseItemRef(args.after);
|
|
525
|
+
if (args.after !== undefined) afterRef = parseItemRef(args.after, "--after");
|
|
555
526
|
|
|
556
527
|
const item = await resolveProjectItem(project.id, itemRef, owner, repoName, repo, env, child);
|
|
557
528
|
|
|
@@ -625,7 +596,7 @@ async function mainSubcommand(args, { env, child, repo, project }) {
|
|
|
625
596
|
const items = await fetchAllItems(project.id, env, child);
|
|
626
597
|
|
|
627
598
|
// Resolve all referenced items up-front (fail closed before any mutation).
|
|
628
|
-
const refs = positional.map((p) => parseItemRef(p));
|
|
599
|
+
const refs = positional.map((p) => parseItemRef(p, "<ref>"));
|
|
629
600
|
const resolved = refs.map((ref) => resolveFromItems(items, ref, repo));
|
|
630
601
|
|
|
631
602
|
// Reordering positions within a single Status column. The before/after snapshot is scoped
|
|
@@ -742,7 +713,7 @@ async function main(args, { env = process.env, runChild } = {}) {
|
|
|
742
713
|
const child = runChild ?? _runChild;
|
|
743
714
|
const repo = validateRepo(args.repo);
|
|
744
715
|
const [owner, repoName] = repo.split("/");
|
|
745
|
-
const
|
|
716
|
+
const selector = resolveProjectSelector(args);
|
|
746
717
|
|
|
747
718
|
// Fail closed: the legacy flag form takes no positional arguments. A stray
|
|
748
719
|
// token (e.g. `reorder 630 --item ...`) must not be silently ignored.
|
|
@@ -753,7 +724,7 @@ async function main(args, { env = process.env, runChild } = {}) {
|
|
|
753
724
|
);
|
|
754
725
|
}
|
|
755
726
|
|
|
756
|
-
const project = await resolveProject(owner,
|
|
727
|
+
const project = await resolveProject(owner, selector, env, child);
|
|
757
728
|
|
|
758
729
|
if (args._subcommand) {
|
|
759
730
|
return mainSubcommand(args, { env, child, repo, project });
|
|
@@ -763,7 +734,7 @@ async function main(args, { env = process.env, runChild } = {}) {
|
|
|
763
734
|
|
|
764
735
|
// ── CLI entrypoint ──────────────────────────────────────────────────────
|
|
765
736
|
|
|
766
|
-
async function runCli(argv, { stdout = process.stdout, stderr = process.stderr, env = process.env } = {}) {
|
|
737
|
+
async function runCli(argv, { stdout = process.stdout, stderr = process.stderr, env = process.env, cwd = process.cwd() } = {}) {
|
|
767
738
|
let args;
|
|
768
739
|
try {
|
|
769
740
|
args = parseCliArgs(argv);
|
|
@@ -776,9 +747,13 @@ async function runCli(argv, { stdout = process.stdout, stderr = process.stderr,
|
|
|
776
747
|
stdout.write(USAGE);
|
|
777
748
|
return;
|
|
778
749
|
}
|
|
750
|
+
|
|
751
|
+
// Resolve the board from .devloops when --project is absent.
|
|
752
|
+
applyDevloopsBoard(args, cwd);
|
|
753
|
+
|
|
779
754
|
try {
|
|
780
755
|
const result = await main(args, { env });
|
|
781
|
-
|
|
756
|
+
process.exitCode = emitResult(result, { jq: args.jq, silent: args.silent, stdout, stderr });
|
|
782
757
|
} catch (err) {
|
|
783
758
|
stderr.write(JSON.stringify({ ok: false, error: err.message, code: err.code ?? "UNKNOWN" }) + "\n");
|
|
784
759
|
process.exitCode = classifyExitCode(err);
|