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,29 @@
|
|
|
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 } from "./_resolve-project.mjs";
|
|
4
5
|
import { parseArgs } from "node:util";
|
|
5
|
-
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult } from "../lib/jq-output.mjs";
|
|
6
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
7
|
+
import { loadStateColumnMap, LOGICAL_COLUMN } from "@dev-loops/core/loop/queue-board-sync";
|
|
6
8
|
|
|
7
|
-
const USAGE = `Usage: dev-loops queue add --repo <owner/name> --project <number|id> --item <number>
|
|
9
|
+
const USAGE = `Usage: dev-loops queue add --repo <owner/name> [--project <number|id>] --item <number>
|
|
8
10
|
dev-loops project add … (back-compat alias for "queue add")
|
|
9
11
|
|
|
10
12
|
Add an existing issue or PR to a GitHub Projects V2 board.
|
|
11
13
|
|
|
12
14
|
Options:
|
|
13
15
|
--repo <owner/name> Required. Repository containing the issue/PR.
|
|
14
|
-
--project <number|id>
|
|
16
|
+
--project <number|id> Project number (integer) or node ID. When omitted,
|
|
17
|
+
resolved from .devloops queue.projectNumber /
|
|
18
|
+
queue.boardTitle.
|
|
15
19
|
--item <number> Required. Issue or PR number to add.
|
|
16
20
|
--column <name> Initial Status column (default: "Backlog").
|
|
17
21
|
--status <name> Back-compat alias for --column.
|
|
22
|
+
--next-up Land the item directly in the configured next_up
|
|
23
|
+
column (the normative pickup queue; "Next Up" by
|
|
24
|
+
default, honors queue.statusColumns.next_up). Sugar
|
|
25
|
+
for --column <that column>; cannot be combined with
|
|
26
|
+
a conflicting --column/--status.
|
|
18
27
|
--help, -h Show this help.
|
|
19
28
|
|
|
20
29
|
Output (stdout):
|
|
@@ -48,6 +57,7 @@ function parseCliArgs(argv) {
|
|
|
48
57
|
item: { type: "string" },
|
|
49
58
|
column: { type: "string" },
|
|
50
59
|
status: { type: "string" },
|
|
60
|
+
"next-up": { type: "boolean" },
|
|
51
61
|
help: { type: "boolean", short: "h" },
|
|
52
62
|
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
53
63
|
},
|
|
@@ -97,14 +107,16 @@ function parseCliArgs(argv) {
|
|
|
97
107
|
// resolved by argv order.
|
|
98
108
|
args.status = requireValue(token, "--status requires a value");
|
|
99
109
|
break;
|
|
100
|
-
case "
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
args.
|
|
110
|
+
case "next-up":
|
|
111
|
+
if (token.value !== undefined) {
|
|
112
|
+
throw Object.assign(new Error(`Unknown flag: ${token.rawName}=${token.value}`), { code: "INVALID_ARGS", usage: USAGE });
|
|
113
|
+
}
|
|
114
|
+
args.nextUp = true;
|
|
105
115
|
break;
|
|
106
|
-
default:
|
|
116
|
+
default: {
|
|
117
|
+
if (matchJqOutputToken(token, args, (t) => requireValue(t, "--jq requires a filter"))) break;
|
|
107
118
|
throw Object.assign(new Error(`Unknown flag: ${token.rawName}`), { code: "INVALID_ARGS", usage: USAGE });
|
|
119
|
+
}
|
|
108
120
|
}
|
|
109
121
|
}
|
|
110
122
|
return args;
|
|
@@ -114,7 +126,6 @@ function parseCliArgs(argv) {
|
|
|
114
126
|
|
|
115
127
|
const OWNER_RE = /^[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$/;
|
|
116
128
|
const REPO_NAME_RE = /^[a-zA-Z0-9](?:[a-zA-Z0-9_.-]*[a-zA-Z0-9])?$/;
|
|
117
|
-
const GLOBAL_NODE_ID_RE = /^[A-Za-z0-9_]+$/;
|
|
118
129
|
|
|
119
130
|
function validateRepo(repo) {
|
|
120
131
|
if (!repo || typeof repo !== "string") {
|
|
@@ -139,30 +150,6 @@ function validateRepo(repo) {
|
|
|
139
150
|
return repo;
|
|
140
151
|
}
|
|
141
152
|
|
|
142
|
-
function parseProjectRef(raw) {
|
|
143
|
-
if (!raw || typeof raw !== "string" || raw.trim().length === 0) {
|
|
144
|
-
throw Object.assign(new Error("--project is required"), { code: "INVALID_PROJECT" });
|
|
145
|
-
}
|
|
146
|
-
const trimmed = raw.trim();
|
|
147
|
-
const asNum = Number(trimmed);
|
|
148
|
-
if (Number.isInteger(asNum) && asNum > 0 && String(asNum) === trimmed) {
|
|
149
|
-
return { kind: "number", value: asNum };
|
|
150
|
-
}
|
|
151
|
-
if (trimmed === "0") {
|
|
152
|
-
throw Object.assign(
|
|
153
|
-
new Error(`--project must be a positive integer or a node ID, got "${raw}"`),
|
|
154
|
-
{ code: "INVALID_PROJECT" },
|
|
155
|
-
);
|
|
156
|
-
}
|
|
157
|
-
if (GLOBAL_NODE_ID_RE.test(trimmed)) {
|
|
158
|
-
return { kind: "id", value: trimmed };
|
|
159
|
-
}
|
|
160
|
-
throw Object.assign(
|
|
161
|
-
new Error(`--project must be a positive integer or a node ID, got "${raw}"`),
|
|
162
|
-
{ code: "INVALID_PROJECT" },
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
153
|
// ── API helpers ──────────────────────────────────────────────────────────
|
|
167
154
|
|
|
168
155
|
async function ghGraphql(query, vars, env, runChild = _runChild, { allowErrors = false } = {}) {
|
|
@@ -385,11 +372,11 @@ function classifyExitCode(err) {
|
|
|
385
372
|
|
|
386
373
|
// ── Main logic ──────────────────────────────────────────────────────────
|
|
387
374
|
|
|
388
|
-
async function main(args, { env = process.env, runChild } = {}) {
|
|
375
|
+
async function main(args, { env = process.env, runChild, cwd = process.cwd() } = {}) {
|
|
389
376
|
const child = runChild ?? _runChild;
|
|
390
377
|
const repo = validateRepo(args.repo);
|
|
391
378
|
const [owner, repoName] = repo.split("/");
|
|
392
|
-
const
|
|
379
|
+
const selector = resolveProjectSelector(args);
|
|
393
380
|
const itemNumber = args.item;
|
|
394
381
|
if (!Number.isInteger(itemNumber) || itemNumber < 1) {
|
|
395
382
|
throw Object.assign(new Error("--item is required and must be a positive integer"), { code: "INVALID_ITEM" });
|
|
@@ -400,28 +387,43 @@ async function main(args, { env = process.env, runChild } = {}) {
|
|
|
400
387
|
{ code: "INVALID_ARGS", usage: USAGE },
|
|
401
388
|
);
|
|
402
389
|
}
|
|
403
|
-
|
|
390
|
+
// --next-up is sugar for --column <resolved next_up display name> (the
|
|
391
|
+
// normative pickup queue, #1091), resolved through the SAME statusColumns
|
|
392
|
+
// mapping board-sync uses (#1098) so a renamed Next Up column agrees with
|
|
393
|
+
// an explicit --column of the same configured name.
|
|
394
|
+
const { columnNames, error: configError } = loadStateColumnMap(cwd);
|
|
395
|
+
// Fail CLOSED on a malformed `.devloops` when --next-up drives the target:
|
|
396
|
+
// silently using the literal "Next Up" could land in the wrong column (#1098).
|
|
397
|
+
// A plain `--column X` add never consults statusColumns, so it is unaffected.
|
|
398
|
+
if (args.nextUp && configError) {
|
|
399
|
+
throw Object.assign(
|
|
400
|
+
new Error(`could not resolve next_up column (config read/parse error: ${configError})`),
|
|
401
|
+
{ code: "CONFIG_ERROR" },
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
const nextUpColumn = columnNames[LOGICAL_COLUMN.NEXT_UP];
|
|
405
|
+
const explicitColumn = args.column ?? args.status ?? null;
|
|
406
|
+
if (args.nextUp && explicitColumn != null && explicitColumn.trim() !== nextUpColumn) {
|
|
407
|
+
throw Object.assign(
|
|
408
|
+
new Error(`Conflicting --next-up and --column/--status ("${explicitColumn}") — pass only one.`),
|
|
409
|
+
{ code: "INVALID_ARGS", usage: USAGE },
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
const targetStatus = (args.nextUp ? nextUpColumn : (explicitColumn ?? "Backlog")).trim();
|
|
404
413
|
if (!targetStatus) {
|
|
405
414
|
throw Object.assign(new Error("--column must not be empty"), { code: "INVALID_STATUS" });
|
|
406
415
|
}
|
|
407
416
|
|
|
408
417
|
// 1. Resolve owner
|
|
409
|
-
|
|
418
|
+
// URI refs encode owner+kind directly; skip the API round-trip for owner resolution.
|
|
419
|
+
const projectOwner = selector.projectRef?.kind === "uri" ? selector.projectRef.owner : owner;
|
|
420
|
+
const ownerKind = selector.projectRef?.kind === "uri"
|
|
421
|
+
? selector.projectRef.ownerKind
|
|
422
|
+
: (await resolveOwner(owner, env, child)).kind;
|
|
410
423
|
|
|
411
424
|
// 2. Resolve project
|
|
412
|
-
const projects = await listAllProjects(
|
|
413
|
-
|
|
414
|
-
if (projectRef.kind === "id") {
|
|
415
|
-
project = projects.find((p) => p.id === projectRef.value);
|
|
416
|
-
} else {
|
|
417
|
-
project = projects.find((p) => p.number === projectRef.value);
|
|
418
|
-
}
|
|
419
|
-
if (!project) {
|
|
420
|
-
throw Object.assign(
|
|
421
|
-
new Error(`Project ${projectRef.kind === "id" ? `"${projectRef.value}"` : `number ${projectRef.value}`} not found under owner "${owner}"`),
|
|
422
|
-
{ code: "PROJECT_NOT_FOUND" },
|
|
423
|
-
);
|
|
424
|
-
}
|
|
425
|
+
const projects = await listAllProjects(projectOwner, ownerKind, env, child);
|
|
426
|
+
const project = findProject(projects, selector, projectOwner);
|
|
425
427
|
|
|
426
428
|
// 3. Resolve Status field and target column
|
|
427
429
|
const fieldNodes = await listAllFields(project.id, env, child);
|
|
@@ -538,7 +540,7 @@ async function main(args, { env = process.env, runChild } = {}) {
|
|
|
538
540
|
|
|
539
541
|
// ── CLI entrypoint ──────────────────────────────────────────────────────
|
|
540
542
|
|
|
541
|
-
async function runCli(argv, { stdout = process.stdout, stderr = process.stderr, env = process.env } = {}) {
|
|
543
|
+
async function runCli(argv, { stdout = process.stdout, stderr = process.stderr, env = process.env, cwd = process.cwd(), runChild } = {}) {
|
|
542
544
|
let args;
|
|
543
545
|
try {
|
|
544
546
|
args = parseCliArgs(argv);
|
|
@@ -551,8 +553,12 @@ async function runCli(argv, { stdout = process.stdout, stderr = process.stderr,
|
|
|
551
553
|
stdout.write(USAGE);
|
|
552
554
|
return;
|
|
553
555
|
}
|
|
556
|
+
|
|
557
|
+
// Resolve the board from .devloops when --project is absent.
|
|
558
|
+
applyDevloopsBoard(args, cwd);
|
|
559
|
+
|
|
554
560
|
try {
|
|
555
|
-
const result = await main(args, { env });
|
|
561
|
+
const result = await main(args, { env, runChild, cwd });
|
|
556
562
|
process.exitCode = emitResult(result, { jq: args.jq, silent: args.silent, stdout, stderr });
|
|
557
563
|
} catch (err) {
|
|
558
564
|
stderr.write(JSON.stringify({ ok: false, error: err.message, code: err.code ?? "UNKNOWN" }) + "\n");
|
|
@@ -567,4 +573,4 @@ if (isDirectCliRun(import.meta.url)) {
|
|
|
567
573
|
});
|
|
568
574
|
}
|
|
569
575
|
|
|
570
|
-
export { main, parseCliArgs };
|
|
576
|
+
export { main, parseCliArgs, runCli };
|
|
@@ -1,37 +1,40 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { parse as parseYaml } from "yaml";
|
|
5
2
|
import { formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
|
|
6
3
|
import { runChild as _runChild } from "../_cli-primitives.mjs";
|
|
4
|
+
import { resolveSettings, parseProjectRef, findProject } from "./_resolve-project.mjs";
|
|
7
5
|
import { parseArgs } from "node:util";
|
|
6
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
7
|
+
import { loadStateColumnMap, LOGICAL_COLUMN } from "@dev-loops/core/loop/queue-board-sync";
|
|
8
8
|
|
|
9
|
-
const USAGE = `Usage: dev-loops queue archive-done --repo <owner/name> [--project <number|id>] [--older-than <duration>] [--dry-run]
|
|
9
|
+
const USAGE = `Usage: dev-loops queue archive-done --repo <owner/name> [--project <number|id|board-uri>] [--older-than <duration>] [--dry-run]
|
|
10
10
|
(dev-loops project archive-done … is a back-compat alias)
|
|
11
11
|
|
|
12
12
|
Archive GitHub Projects V2 items whose issue/PR has been closed for at least the
|
|
13
13
|
given duration. Operator-triggered (no webhooks). Uses archiveProjectV2Item.
|
|
14
14
|
|
|
15
15
|
Options:
|
|
16
|
-
--repo <owner/name>
|
|
17
|
-
--project <number|id>
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
--
|
|
16
|
+
--repo <owner/name> Required. Repository to scope the project search.
|
|
17
|
+
--project <number|id|board-uri> Project number, node ID, or board URI
|
|
18
|
+
(e.g. https://github.com/users/me/projects/3).
|
|
19
|
+
When omitted, resolved from .devloops
|
|
20
|
+
queue.projectNumber / queue.boardTitle.
|
|
21
|
+
--older-than <duration> Closed-for threshold. Format: <n><unit> where unit is
|
|
22
|
+
h (hours), d (days), or w (weeks). Default resolves
|
|
23
|
+
from .devloops queue.archiveOlderThanDays, else 7d.
|
|
24
|
+
--dry-run Print the intended archive mutation(s) without executing.
|
|
25
|
+
--help, -h Show this help.
|
|
25
26
|
|
|
26
27
|
Output (stdout):
|
|
27
28
|
JSON: { ok: true, olderThan, scanned, archivable, archived: [{ itemId, issueNumber, prNumber, closedAt }] }
|
|
28
29
|
dry-run: { ok: true, dryRun: true, olderThan, scanned, archivable, mutations: [{ query, variables }] }
|
|
29
30
|
(scanned = all repo board items; archivable = items selected for archival)
|
|
30
31
|
|
|
32
|
+
${JQ_OUTPUT_USAGE}
|
|
33
|
+
|
|
31
34
|
Exit codes:
|
|
32
35
|
0 — success
|
|
33
36
|
1 — usage or argument error
|
|
34
|
-
2 — GitHub API error
|
|
37
|
+
2 — GitHub API error / invalid --jq filter
|
|
35
38
|
3 — project not found
|
|
36
39
|
`.trim();
|
|
37
40
|
|
|
@@ -53,6 +56,7 @@ function parseCliArgs(argv) {
|
|
|
53
56
|
"older-than": { type: "string" },
|
|
54
57
|
"dry-run": { type: "boolean" },
|
|
55
58
|
help: { type: "boolean", short: "h" },
|
|
59
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
56
60
|
},
|
|
57
61
|
allowPositionals: true,
|
|
58
62
|
strict: false,
|
|
@@ -88,8 +92,10 @@ function parseCliArgs(argv) {
|
|
|
88
92
|
}
|
|
89
93
|
args.dryRun = true;
|
|
90
94
|
break;
|
|
91
|
-
default:
|
|
95
|
+
default: {
|
|
96
|
+
if (matchJqOutputToken(token, args, (t) => requireValue(t, "--jq requires a filter", "INVALID_ARGS"))) break;
|
|
92
97
|
throw Object.assign(new Error(`Unknown flag: ${token.rawName}`), { code: "INVALID_ARGS", usage: USAGE });
|
|
98
|
+
}
|
|
93
99
|
}
|
|
94
100
|
}
|
|
95
101
|
return args;
|
|
@@ -99,7 +105,6 @@ function parseCliArgs(argv) {
|
|
|
99
105
|
|
|
100
106
|
const OWNER_RE = /^[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$/;
|
|
101
107
|
const REPO_NAME_RE = /^[a-zA-Z0-9](?:[a-zA-Z0-9_.-]*[a-zA-Z0-9])?$/;
|
|
102
|
-
const GLOBAL_NODE_ID_RE = /^[A-Za-z0-9_]+$/;
|
|
103
108
|
|
|
104
109
|
function validateRepo(repo) {
|
|
105
110
|
if (!repo || typeof repo !== "string") {
|
|
@@ -117,21 +122,6 @@ function validateRepo(repo) {
|
|
|
117
122
|
return repo;
|
|
118
123
|
}
|
|
119
124
|
|
|
120
|
-
function parseProjectRef(raw) {
|
|
121
|
-
if (!raw || typeof raw !== "string" || raw.trim().length === 0) {
|
|
122
|
-
throw Object.assign(new Error("--project is required"), { code: "INVALID_PROJECT" });
|
|
123
|
-
}
|
|
124
|
-
const trimmed = raw.trim();
|
|
125
|
-
const asNum = Number(trimmed);
|
|
126
|
-
if (Number.isInteger(asNum) && asNum > 0 && String(asNum) === trimmed) {
|
|
127
|
-
return { kind: "number", value: asNum };
|
|
128
|
-
}
|
|
129
|
-
if (GLOBAL_NODE_ID_RE.test(trimmed) && trimmed !== "0") {
|
|
130
|
-
return { kind: "id", value: trimmed };
|
|
131
|
-
}
|
|
132
|
-
throw Object.assign(new Error(`--project must be a positive integer or a node ID, got "${raw}"`), { code: "INVALID_PROJECT" });
|
|
133
|
-
}
|
|
134
|
-
|
|
135
125
|
const DURATION_RE = /^(\d+)(h|d|w)$/;
|
|
136
126
|
const UNIT_MS = {
|
|
137
127
|
h: 60 * 60 * 1000,
|
|
@@ -154,37 +144,6 @@ function parseDuration(raw) {
|
|
|
154
144
|
return n * UNIT_MS[m[2]];
|
|
155
145
|
}
|
|
156
146
|
|
|
157
|
-
// ── Settings fallback ──────────────────────────────────────────────────────
|
|
158
|
-
|
|
159
|
-
// Read .devloops (and extension variants) queue settings, mirroring the
|
|
160
|
-
// resolution used by ensure-queue-board.mjs. Returns { project }, { title },
|
|
161
|
-
// and/or { olderThanDays } when configured; never throws on a missing/bad file.
|
|
162
|
-
function resolveSettings(cwd) {
|
|
163
|
-
const basePath = path.join(cwd, ".devloops");
|
|
164
|
-
const extensions = ["", ".yaml", ".yml", ".json"];
|
|
165
|
-
for (const ext of extensions) {
|
|
166
|
-
try {
|
|
167
|
-
const raw = readFileSync(basePath + ext, "utf-8");
|
|
168
|
-
const settings = ext === ".json" ? JSON.parse(raw) : parseYaml(raw);
|
|
169
|
-
const queue = settings?.queue;
|
|
170
|
-
if (!queue) return null;
|
|
171
|
-
const out = {};
|
|
172
|
-
if (typeof queue.projectNumber === "number" && Number.isInteger(queue.projectNumber) && queue.projectNumber > 0) {
|
|
173
|
-
out.project = queue.projectNumber;
|
|
174
|
-
} else if (typeof queue.boardTitle === "string" && queue.boardTitle.trim().length > 0) {
|
|
175
|
-
out.title = queue.boardTitle.trim();
|
|
176
|
-
}
|
|
177
|
-
if (typeof queue.archiveOlderThanDays === "number" && Number.isInteger(queue.archiveOlderThanDays) && queue.archiveOlderThanDays > 0) {
|
|
178
|
-
out.olderThanDays = queue.archiveOlderThanDays;
|
|
179
|
-
}
|
|
180
|
-
return out;
|
|
181
|
-
} catch {
|
|
182
|
-
// extension not present or unparseable — try next
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
return null;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
147
|
// ── API helpers ──────────────────────────────────────────────────────────
|
|
189
148
|
|
|
190
149
|
async function ghGraphql(query, vars, env, runChild = _runChild) {
|
|
@@ -350,12 +309,14 @@ function normalizeItem(node) {
|
|
|
350
309
|
// ── Selection logic (pure) ────────────────────────────────────────────────
|
|
351
310
|
|
|
352
311
|
// Select items whose issue/PR is closed and has been closed for >= olderThanMs.
|
|
353
|
-
|
|
312
|
+
// `doneColumn` defaults to the literal "Done" for direct/pure-unit callers;
|
|
313
|
+
// main() always passes the configured column name (#1098, #1143).
|
|
314
|
+
function selectArchivable(items, { now, olderThanMs, doneColumn = "Done" }) {
|
|
354
315
|
return items.filter((it) => {
|
|
355
316
|
if (it.isArchived) return false;
|
|
356
317
|
// Only archive items in the Done column — a closed issue/PR parked in
|
|
357
318
|
// another column (Backlog/Next Up/In Progress) must be left untouched.
|
|
358
|
-
if (it.status !==
|
|
319
|
+
if (it.status !== doneColumn) return false;
|
|
359
320
|
const c = it.content;
|
|
360
321
|
if (!c || !c.closed || !c.closedAt) return false;
|
|
361
322
|
const closedAtMs = Date.parse(c.closedAt);
|
|
@@ -375,9 +336,22 @@ function classifyExitCode(err) {
|
|
|
375
336
|
|
|
376
337
|
// ── Main logic ──────────────────────────────────────────────────────────
|
|
377
338
|
|
|
378
|
-
async function main(args, { env = process.env, runChild } = {}) {
|
|
339
|
+
async function main(args, { env = process.env, runChild, cwd = process.cwd() } = {}) {
|
|
379
340
|
const child = runChild ?? _runChild;
|
|
380
341
|
const repo = validateRepo(args.repo);
|
|
342
|
+
// Resolve the done column name through the SAME statusColumns mapping
|
|
343
|
+
// board-sync uses (#1098, #1143): a repo that renamed Done gets its
|
|
344
|
+
// configured column matched here, not the literal default. Fail CLOSED on a
|
|
345
|
+
// malformed `.devloops` — never silently archive against the literal "Done"
|
|
346
|
+
// and risk archiving nothing on a renamed/stale column.
|
|
347
|
+
const { columnNames, error: configError } = loadStateColumnMap(cwd);
|
|
348
|
+
if (configError) {
|
|
349
|
+
throw Object.assign(
|
|
350
|
+
new Error(`could not resolve done column (config read/parse error: ${configError})`),
|
|
351
|
+
{ code: "CONFIG_ERROR" },
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
const doneColumn = columnNames[LOGICAL_COLUMN.DONE];
|
|
381
355
|
const [owner] = repo.split("/");
|
|
382
356
|
// Board: explicit --project ref wins; otherwise resolve by board title from
|
|
383
357
|
// .devloops (passed in as args.projectTitle by runCli). Fail closed if neither.
|
|
@@ -396,22 +370,13 @@ async function main(args, { env = process.env, runChild } = {}) {
|
|
|
396
370
|
const olderThanMs = parseDuration(olderThanRaw);
|
|
397
371
|
const now = args.now ?? Date.now();
|
|
398
372
|
|
|
399
|
-
|
|
400
|
-
const
|
|
401
|
-
const
|
|
402
|
-
?
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
if (!project) {
|
|
407
|
-
const desc = projectRef
|
|
408
|
-
? (projectRef.kind === "id" ? `"${projectRef.value}"` : `number ${projectRef.value}`)
|
|
409
|
-
: `title "${projectTitle}"`;
|
|
410
|
-
throw Object.assign(
|
|
411
|
-
new Error(`Project ${desc} not found under owner "${owner}"`),
|
|
412
|
-
{ code: "PROJECT_NOT_FOUND" },
|
|
413
|
-
);
|
|
414
|
-
}
|
|
373
|
+
// URI refs encode owner+kind directly; skip the API round-trip for owner resolution.
|
|
374
|
+
const projectOwner = projectRef?.kind === "uri" ? projectRef.owner : owner;
|
|
375
|
+
const ownerKind = projectRef?.kind === "uri"
|
|
376
|
+
? projectRef.ownerKind
|
|
377
|
+
: (await resolveOwner(owner, env, child)).kind;
|
|
378
|
+
const projects = await listAllProjects(projectOwner, ownerKind, env, child);
|
|
379
|
+
const project = findProject(projects, { projectRef, projectTitle }, projectOwner);
|
|
415
380
|
|
|
416
381
|
const rawItems = await fetchAllItems(project.id, env, child);
|
|
417
382
|
// Only consider items whose content belongs to the target repo (single-repo scope).
|
|
@@ -419,7 +384,7 @@ async function main(args, { env = process.env, runChild } = {}) {
|
|
|
419
384
|
.filter((n) => n.content && n.content.repository?.nameWithOwner === repo)
|
|
420
385
|
.map(normalizeItem);
|
|
421
386
|
|
|
422
|
-
const archivable = selectArchivable(repoItems, { now, olderThanMs });
|
|
387
|
+
const archivable = selectArchivable(repoItems, { now, olderThanMs, doneColumn });
|
|
423
388
|
|
|
424
389
|
const mutations = archivable.map((it) => ({
|
|
425
390
|
query: ARCHIVE_ITEM,
|
|
@@ -489,8 +454,8 @@ async function runCli(argv, { stdout = process.stdout, stderr = process.stderr,
|
|
|
489
454
|
}
|
|
490
455
|
|
|
491
456
|
try {
|
|
492
|
-
const result = await main(args, { env });
|
|
493
|
-
|
|
457
|
+
const result = await main(args, { env, cwd });
|
|
458
|
+
process.exitCode = emitResult(result, { jq: args.jq, silent: args.silent, stdout, stderr });
|
|
494
459
|
} catch (err) {
|
|
495
460
|
stderr.write(JSON.stringify({ ok: false, error: err.message, code: err.code ?? "UNKNOWN" }) + "\n");
|
|
496
461
|
process.exitCode = classifyExitCode(err);
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
2
|
import { parseArgs } from "node:util";
|
|
5
|
-
import { parse as parseYaml } from "yaml";
|
|
6
3
|
import { formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
|
|
7
4
|
import { runChild as _runChild } from "../_cli-primitives.mjs";
|
|
5
|
+
import { resolveSettings } from "./_resolve-project.mjs";
|
|
6
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
|
|
8
7
|
|
|
9
8
|
const USAGE = `Usage: dev-loops queue ensure --repo <owner/name> [--project <number>] [--title <title>] [--link-repo <owner/name>] [--repair-rename]
|
|
10
9
|
(dev-loops project ensure … is a back-compat alias)
|
|
@@ -27,10 +26,12 @@ queue.projectNumber or queue.boardTitle.
|
|
|
27
26
|
Output (stdout):
|
|
28
27
|
JSON: { ok: true, project: { id, number, title, url, statusFieldId, linkedRepo } }
|
|
29
28
|
|
|
29
|
+
${JQ_OUTPUT_USAGE}
|
|
30
|
+
|
|
30
31
|
Exit codes:
|
|
31
32
|
0 — board exists or was created successfully (idempotent)
|
|
32
33
|
1 — usage or argument error
|
|
33
|
-
2 — GitHub API error
|
|
34
|
+
2 — GitHub API error / invalid --jq filter
|
|
34
35
|
3 — board schema/config mismatch (manual reconciliation needed)
|
|
35
36
|
`;
|
|
36
37
|
|
|
@@ -54,6 +55,7 @@ function parseCliArgs(argv) {
|
|
|
54
55
|
"link-repo": { type: "string" },
|
|
55
56
|
"repair-rename": { type: "boolean" },
|
|
56
57
|
help: { type: "boolean", short: "h" },
|
|
58
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
57
59
|
},
|
|
58
60
|
allowPositionals: true,
|
|
59
61
|
strict: false,
|
|
@@ -98,8 +100,10 @@ function parseCliArgs(argv) {
|
|
|
98
100
|
}
|
|
99
101
|
args.repairRename = true;
|
|
100
102
|
break;
|
|
101
|
-
default:
|
|
103
|
+
default: {
|
|
104
|
+
if (matchJqOutputToken(token, args, (t) => requireValue(t, "--jq requires a filter"))) break;
|
|
102
105
|
throw parseError(`Unknown flag: ${token.rawName}`);
|
|
106
|
+
}
|
|
103
107
|
}
|
|
104
108
|
}
|
|
105
109
|
return args;
|
|
@@ -146,36 +150,6 @@ function validateRepo(repo) {
|
|
|
146
150
|
return repo;
|
|
147
151
|
}
|
|
148
152
|
|
|
149
|
-
// ── Settings fallback ────────────────────────────────────────────────────
|
|
150
|
-
|
|
151
|
-
function resolveSettings(cwd) {
|
|
152
|
-
// Try bare .devloops and extension variants (.yaml, .yml, .json)
|
|
153
|
-
// to match the config loader's consumer override detection.
|
|
154
|
-
// .json uses strict JSON.parse; all others use the YAML parser.
|
|
155
|
-
const basePath = path.join(cwd, ".devloops");
|
|
156
|
-
const extensions = ["", ".yaml", ".yml", ".json"];
|
|
157
|
-
for (const ext of extensions) {
|
|
158
|
-
try {
|
|
159
|
-
const settingsPath = basePath + ext;
|
|
160
|
-
const raw = readFileSync(settingsPath, "utf-8");
|
|
161
|
-
const settings = ext === ".json" ? JSON.parse(raw) : parseYaml(raw);
|
|
162
|
-
const queue = settings?.queue;
|
|
163
|
-
if (queue) {
|
|
164
|
-
if (typeof queue.projectNumber === "number" && Number.isInteger(queue.projectNumber) && queue.projectNumber > 0) {
|
|
165
|
-
return { project: queue.projectNumber };
|
|
166
|
-
}
|
|
167
|
-
if (typeof queue.boardTitle === "string" && queue.boardTitle.trim().length > 0) {
|
|
168
|
-
return { title: queue.boardTitle.trim() };
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
return null;
|
|
172
|
-
} catch {
|
|
173
|
-
// extension not present or unparseable — try next
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
return null;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
153
|
// ── API helpers ──────────────────────────────────────────────────────────
|
|
180
154
|
|
|
181
155
|
async function ghGraphql(query, vars, env, runChild = _runChild) {
|
|
@@ -822,7 +796,7 @@ async function runCli(argv, { stdout = process.stdout, stderr = process.stderr,
|
|
|
822
796
|
|
|
823
797
|
try {
|
|
824
798
|
const result = await main(args, { env });
|
|
825
|
-
|
|
799
|
+
process.exitCode = emitResult(result, { jq: args.jq, silent: args.silent, stdout, stderr });
|
|
826
800
|
} catch (err) {
|
|
827
801
|
stderr.write(`${formatCliError(err)}\n`);
|
|
828
802
|
process.exitCode = classifyExitCode(err);
|