dev-loops 0.4.0 → 0.6.0
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 +1 -1
- package/.claude/agents/refiner.md +1 -0
- package/.claude/commands/auto.md +7 -0
- package/.claude/commands/continue.md +15 -0
- package/.claude/commands/info.md +7 -0
- package/.claude/commands/start-spike.md +16 -0
- package/.claude/commands/start.md +7 -0
- package/.claude/commands/status.md +6 -0
- package/.claude/hooks/_run-context.mjs +11 -4
- package/.claude/skills/dev-loop/SKILL.md +21 -6
- package/.claude/skills/dev-loop/templates/slides-story-review.md +54 -0
- package/.claude/skills/docs/artifact-authority-contract.md +86 -31
- package/.claude/skills/docs/local-planning-flow.md +63 -0
- package/.claude/skills/docs/local-planning-worked-example.md +139 -0
- package/.claude/skills/docs/merge-preconditions.md +35 -0
- package/.claude/skills/docs/plan-file-contract.md +37 -0
- package/.claude/skills/docs/release-runbook.md +45 -0
- package/.claude/skills/docs/retrospective-checkpoint-contract.md +55 -7
- package/.claude/skills/docs/spike-mode-contract.md +237 -0
- package/.claude/skills/docs/ui-e2e-scoping-step.md +102 -0
- package/.claude/skills/local-implementation/SKILL.md +1 -1
- package/CHANGELOG.md +73 -0
- package/README.md +21 -1
- package/agents/dev-loop.agent.md +8 -1
- package/agents/refiner.agent.md +1 -0
- package/cli/index.mjs +2 -0
- package/extension/index.ts +10 -1
- package/extension/presentation.ts +15 -0
- package/lib/dev-loops-core.mjs +141 -0
- package/package.json +8 -3
- package/scripts/claude/generate-claude-assets.mjs +15 -1
- package/scripts/github/capture-review-threads.mjs +20 -2
- package/scripts/github/comment-issue.mjs +181 -0
- package/scripts/github/fetch-ci-logs.mjs +215 -0
- package/scripts/github/list-issues.mjs +191 -0
- package/scripts/github/probe-copilot-review.mjs +69 -3
- package/scripts/github/upsert-checkpoint-verdict.mjs +18 -3
- package/scripts/lib/jq-output.mjs +297 -0
- package/scripts/loop/_handoff-contract.mjs +1 -0
- package/scripts/loop/check-retro-tooling.mjs +246 -0
- package/scripts/loop/copilot-pr-handoff.mjs +21 -3
- package/scripts/loop/detect-pr-gate-coordination-state.mjs +65 -2
- package/scripts/loop/docs-grill-contract.mjs +70 -0
- package/scripts/loop/info.mjs +21 -2
- package/scripts/loop/pr-runner-coordination.mjs +20 -4
- package/scripts/loop/resolve-dev-loop-startup.mjs +176 -5
- package/scripts/loop/resolve-pr-conflicts.mjs +357 -0
- package/scripts/loop/run-conductor-cycle.mjs +5 -0
- package/scripts/loop/run-watch-cycle.mjs +77 -3
- package/scripts/loop/slides-story-review-contract.mjs +123 -0
- package/scripts/pages/build-site.mjs +136 -0
- package/scripts/projects/add-queue-item.mjs +12 -2
- package/scripts/projects/list-queue-items.mjs +12 -2
- package/scripts/projects/move-queue-item.mjs +12 -2
- package/scripts/projects/resolve-active-board-item.mjs +193 -0
- package/scripts/refine/_refine-helpers.mjs +20 -0
- package/scripts/refine/exit-spike.mjs +186 -0
- package/scripts/refine/promote-plan.mjs +387 -0
- package/scripts/refine/refine-plan-file.mjs +165 -0
- package/scripts/refine/scaffold-spike-file.mjs +183 -0
- package/scripts/refine/validate-plan-file.mjs +64 -0
- package/scripts/refine/validate-spike-file.mjs +87 -0
- package/scripts/release/extract-changelog-section.mjs +111 -0
- package/skills/dev-loop/SKILL.md +24 -2
- package/skills/dev-loop/templates/slides-story-review.md +54 -0
- package/skills/docs/artifact-authority-contract.md +86 -31
- package/skills/docs/local-planning-flow.md +63 -0
- package/skills/docs/local-planning-worked-example.md +139 -0
- package/skills/docs/merge-preconditions.md +35 -0
- package/skills/docs/plan-file-contract.md +37 -0
- package/skills/docs/release-runbook.md +45 -0
- package/skills/docs/retrospective-checkpoint-contract.md +55 -7
- package/skills/docs/spike-mode-contract.md +237 -0
- package/skills/docs/ui-e2e-scoping-step.md +102 -0
- package/skills/local-implementation/SKILL.md +1 -1
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { buildParseError, formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
|
|
3
|
+
import { parsePrNumber, requireTokenValue, runChild } from "../_cli-primitives.mjs";
|
|
4
|
+
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
5
|
+
import { parseArgs } from "node:util";
|
|
6
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult } from "../lib/jq-output.mjs";
|
|
7
|
+
|
|
8
|
+
const DEFAULT_TAIL_LINES = 200;
|
|
9
|
+
|
|
10
|
+
const USAGE = `Usage: fetch-ci-logs.mjs --repo <owner/name> --pr <number> [--failed-only] [--tail <n>]
|
|
11
|
+
Fetch the GitHub Actions CI run log tail for a PR's current head SHA. Thin wrapper
|
|
12
|
+
over \`gh run list\` + \`gh run view --log\` — use this instead of an agent-level raw
|
|
13
|
+
\`gh run view\` so the loop's internal-tooling record stays clean (#993). Complements
|
|
14
|
+
probe-ci-status.mjs, which reports failed-check NAMES; this returns the LOG tail.
|
|
15
|
+
(Siblings: list-issues.mjs, comment-issue.mjs.)
|
|
16
|
+
Required:
|
|
17
|
+
--repo <owner/name> Repository slug (e.g. owner/repo)
|
|
18
|
+
--pr <number> Pull request number
|
|
19
|
+
Optional:
|
|
20
|
+
--failed-only Only include runs whose conclusion is failure
|
|
21
|
+
(default: all Actions runs for the head SHA)
|
|
22
|
+
--tail <n> Lines of log tail to return per run (default 200)
|
|
23
|
+
Output (stdout, JSON):
|
|
24
|
+
{ "ok": true, "repo": "owner/repo", "pr": 17, "headSha": "abc123",
|
|
25
|
+
"runs": [{ "runId": 42, "name": "ci", "conclusion": "failure", "logTail": "..." }] }
|
|
26
|
+
Notes:
|
|
27
|
+
Actions-only (gh run is GitHub Actions). CircleCI / external commit-status logs
|
|
28
|
+
are NOT covered — use the provider's UI for those (probe-ci-status names the check).
|
|
29
|
+
Error output (stderr, JSON):
|
|
30
|
+
{ "ok": false, "error": "...", "usage"?: "..." }
|
|
31
|
+
${JQ_OUTPUT_USAGE}
|
|
32
|
+
Exit codes:
|
|
33
|
+
0 Success
|
|
34
|
+
1 Argument error or gh failure
|
|
35
|
+
2 Invalid --jq filter`.trim();
|
|
36
|
+
const parseError = buildParseError(USAGE);
|
|
37
|
+
|
|
38
|
+
export function parseFetchCiLogsCliArgs(argv) {
|
|
39
|
+
const { tokens } = parseArgs({
|
|
40
|
+
args: [...argv],
|
|
41
|
+
options: {
|
|
42
|
+
help: { type: "boolean", short: "h" },
|
|
43
|
+
repo: { type: "string" },
|
|
44
|
+
pr: { type: "string" },
|
|
45
|
+
"failed-only": { type: "boolean" },
|
|
46
|
+
tail: { type: "string" },
|
|
47
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
48
|
+
},
|
|
49
|
+
allowPositionals: true,
|
|
50
|
+
strict: false,
|
|
51
|
+
tokens: true,
|
|
52
|
+
});
|
|
53
|
+
const options = {
|
|
54
|
+
help: false,
|
|
55
|
+
repo: undefined,
|
|
56
|
+
pr: undefined,
|
|
57
|
+
failedOnly: false,
|
|
58
|
+
tail: DEFAULT_TAIL_LINES,
|
|
59
|
+
jq: undefined,
|
|
60
|
+
silent: false,
|
|
61
|
+
};
|
|
62
|
+
for (const token of tokens) {
|
|
63
|
+
if (token.kind === "positional") {
|
|
64
|
+
throw parseError(`Unknown argument: ${token.value}`);
|
|
65
|
+
}
|
|
66
|
+
if (token.kind !== "option") {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (token.name === "help") {
|
|
70
|
+
options.help = true;
|
|
71
|
+
return options;
|
|
72
|
+
}
|
|
73
|
+
if (token.name === "repo") {
|
|
74
|
+
options.repo = requireTokenValue(token, parseError).trim();
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (token.name === "pr") {
|
|
78
|
+
options.pr = parsePrNumber(requireTokenValue(token, parseError), parseError);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (token.name === "failed-only") {
|
|
82
|
+
options.failedOnly = true;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (token.name === "tail") {
|
|
86
|
+
const raw = requireTokenValue(token, parseError);
|
|
87
|
+
const value = Number(raw);
|
|
88
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
89
|
+
throw parseError(`--tail must be a positive integer, got "${raw}"`);
|
|
90
|
+
}
|
|
91
|
+
options.tail = value;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (token.name === "jq") {
|
|
95
|
+
options.jq = requireTokenValue(token, parseError);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (token.name === "silent") {
|
|
99
|
+
options.silent = true;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
103
|
+
}
|
|
104
|
+
if (options.repo === undefined || options.pr === undefined) {
|
|
105
|
+
throw parseError("Fetching CI logs requires both --repo <owner/name> and --pr <number>");
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
parseRepoSlug(options.repo);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
throw parseError(error instanceof Error ? error.message : String(error));
|
|
111
|
+
}
|
|
112
|
+
return options;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function ghJson(run, ghCommand, args, env, label) {
|
|
116
|
+
const result = await run(ghCommand, args, env);
|
|
117
|
+
if (result.code !== 0) {
|
|
118
|
+
const detail = result.stderr.trim() || `exit code ${result.code}`;
|
|
119
|
+
throw new Error(`${label} failed: ${detail}`);
|
|
120
|
+
}
|
|
121
|
+
return parseJsonText(result.stdout, { label });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function tailLines(text, n) {
|
|
125
|
+
const lines = String(text).replace(/\r?\n$/u, "").split(/\r?\n/u);
|
|
126
|
+
return lines.slice(Math.max(0, lines.length - n)).join("\n");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function fetchCiLogs(options, { env = process.env, ghCommand = "gh", run = runChild } = {}) {
|
|
130
|
+
// 1. Resolve the PR's current head SHA — logs must be scoped to the head being
|
|
131
|
+
// evaluated, not a stale push.
|
|
132
|
+
const pr = await ghJson(
|
|
133
|
+
run,
|
|
134
|
+
ghCommand,
|
|
135
|
+
["pr", "view", String(options.pr), "--repo", options.repo, "--json", "headRefOid"],
|
|
136
|
+
env,
|
|
137
|
+
"gh pr view",
|
|
138
|
+
);
|
|
139
|
+
const headSha = typeof pr.headRefOid === "string" ? pr.headRefOid.trim() : "";
|
|
140
|
+
if (headSha.length === 0) {
|
|
141
|
+
throw new Error("gh pr view did not return headRefOid");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 2. List Actions runs for that exact commit.
|
|
145
|
+
const runs = await ghJson(
|
|
146
|
+
run,
|
|
147
|
+
ghCommand,
|
|
148
|
+
["run", "list", "--repo", options.repo, "--commit", headSha, "--json", "databaseId,name,conclusion,status"],
|
|
149
|
+
env,
|
|
150
|
+
"gh run list",
|
|
151
|
+
);
|
|
152
|
+
if (!Array.isArray(runs)) {
|
|
153
|
+
throw new Error("gh run list did not return a JSON array");
|
|
154
|
+
}
|
|
155
|
+
const selected = options.failedOnly
|
|
156
|
+
? runs.filter((r) => String(r?.conclusion).toLowerCase() === "failure")
|
|
157
|
+
: runs;
|
|
158
|
+
|
|
159
|
+
// 3. Fetch each run's log tail. --log-failed restricts to failed steps when the
|
|
160
|
+
// run failed; for non-failed runs it returns nothing, so fall back to --log.
|
|
161
|
+
const out = [];
|
|
162
|
+
for (const r of selected) {
|
|
163
|
+
const runId = Number.isInteger(r?.databaseId) ? r.databaseId : null;
|
|
164
|
+
if (runId === null) continue;
|
|
165
|
+
const conclusion = typeof r?.conclusion === "string" ? r.conclusion.toLowerCase() : null;
|
|
166
|
+
const logFlag = conclusion === "failure" ? "--log-failed" : "--log";
|
|
167
|
+
const logResult = await run(
|
|
168
|
+
ghCommand,
|
|
169
|
+
["run", "view", String(runId), "--repo", options.repo, logFlag],
|
|
170
|
+
env,
|
|
171
|
+
);
|
|
172
|
+
// A log fetch failure for one run shouldn't abort the others (logs expire);
|
|
173
|
+
// record an empty tail with a note rather than throwing.
|
|
174
|
+
const logTail =
|
|
175
|
+
logResult.code === 0
|
|
176
|
+
? tailLines(logResult.stdout, options.tail)
|
|
177
|
+
: `<log unavailable: ${logResult.stderr.trim() || `exit ${logResult.code}`}>`;
|
|
178
|
+
out.push({
|
|
179
|
+
runId,
|
|
180
|
+
name: typeof r?.name === "string" ? r.name : null,
|
|
181
|
+
conclusion,
|
|
182
|
+
logTail,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
return { ok: true, repo: options.repo, pr: options.pr, headSha, runs: out };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export async function runCli(
|
|
189
|
+
argv = process.argv.slice(2),
|
|
190
|
+
{ stdout = process.stdout, stderr = process.stderr, env = process.env, ghCommand = "gh", run = runChild } = {},
|
|
191
|
+
) {
|
|
192
|
+
let options;
|
|
193
|
+
try {
|
|
194
|
+
options = parseFetchCiLogsCliArgs(argv);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
stderr.write(`${formatCliError(error)}\n`);
|
|
197
|
+
return 1;
|
|
198
|
+
}
|
|
199
|
+
if (options.help) {
|
|
200
|
+
stdout.write(`${USAGE}\n`);
|
|
201
|
+
return 0;
|
|
202
|
+
}
|
|
203
|
+
let result;
|
|
204
|
+
try {
|
|
205
|
+
result = await fetchCiLogs(options, { env, ghCommand, run });
|
|
206
|
+
} catch (error) {
|
|
207
|
+
stderr.write(`${JSON.stringify({ ok: false, error: error instanceof Error ? error.message : String(error) })}\n`);
|
|
208
|
+
return 1;
|
|
209
|
+
}
|
|
210
|
+
return emitResult(result, { jq: options.jq, silent: options.silent, stdout, stderr });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (isDirectCliRun(import.meta.url)) {
|
|
214
|
+
runCli().then((code) => { process.exitCode = code; });
|
|
215
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { buildParseError, formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
|
|
3
|
+
import { requireTokenValue, runChild } from "../_cli-primitives.mjs";
|
|
4
|
+
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
5
|
+
import { parseArgs } from "node:util";
|
|
6
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult } from "../lib/jq-output.mjs";
|
|
7
|
+
|
|
8
|
+
const STATES = new Set(["open", "closed", "all"]);
|
|
9
|
+
|
|
10
|
+
const USAGE = `Usage: list-issues.mjs --repo <owner/name> [--state <open|closed|all>] [--label <l>] [--limit <n>]
|
|
11
|
+
List/filter repository issues. Thin wrapper over \`gh issue list\` — use this instead
|
|
12
|
+
of an agent-level raw \`gh issue list\` so the loop's internal-tooling record stays
|
|
13
|
+
clean (#993). The queue tool lists the project board, not arbitrary issue queries;
|
|
14
|
+
this fills that gap (siblings: comment-issue.mjs, fetch-ci-logs.mjs).
|
|
15
|
+
Required:
|
|
16
|
+
--repo <owner/name> Repository slug (e.g. owner/repo)
|
|
17
|
+
Optional:
|
|
18
|
+
--state <open|closed|all> Issue state filter (default open)
|
|
19
|
+
--label <l> Filter by label (repeatable; AND-combined by gh)
|
|
20
|
+
--limit <n> Return at most <n> issues (default 30)
|
|
21
|
+
Output (stdout, JSON):
|
|
22
|
+
{ "ok": true, "issues": [{ "number": 17, "title": "...", "state": "open", "labels": ["bug"] }, ...] }
|
|
23
|
+
Error output (stderr, JSON):
|
|
24
|
+
{ "ok": false, "error": "...", "usage"?: "..." }
|
|
25
|
+
${JQ_OUTPUT_USAGE}
|
|
26
|
+
Exit codes:
|
|
27
|
+
0 Success
|
|
28
|
+
1 Argument error or gh failure
|
|
29
|
+
2 Invalid --jq filter`.trim();
|
|
30
|
+
const parseError = buildParseError(USAGE);
|
|
31
|
+
|
|
32
|
+
export function parseListIssuesCliArgs(argv) {
|
|
33
|
+
const { tokens } = parseArgs({
|
|
34
|
+
args: [...argv],
|
|
35
|
+
options: {
|
|
36
|
+
help: { type: "boolean", short: "h" },
|
|
37
|
+
repo: { type: "string" },
|
|
38
|
+
state: { type: "string" },
|
|
39
|
+
label: { type: "string", multiple: true },
|
|
40
|
+
limit: { type: "string" },
|
|
41
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
42
|
+
},
|
|
43
|
+
allowPositionals: true,
|
|
44
|
+
strict: false,
|
|
45
|
+
tokens: true,
|
|
46
|
+
});
|
|
47
|
+
const options = {
|
|
48
|
+
help: false,
|
|
49
|
+
repo: undefined,
|
|
50
|
+
state: "open",
|
|
51
|
+
labels: [],
|
|
52
|
+
limit: 30,
|
|
53
|
+
jq: undefined,
|
|
54
|
+
silent: false,
|
|
55
|
+
};
|
|
56
|
+
for (const token of tokens) {
|
|
57
|
+
if (token.kind === "positional") {
|
|
58
|
+
throw parseError(`Unknown argument: ${token.value}`);
|
|
59
|
+
}
|
|
60
|
+
if (token.kind !== "option") {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (token.name === "help") {
|
|
64
|
+
options.help = true;
|
|
65
|
+
return options;
|
|
66
|
+
}
|
|
67
|
+
if (token.name === "repo") {
|
|
68
|
+
options.repo = requireTokenValue(token, parseError).trim();
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (token.name === "state") {
|
|
72
|
+
const state = requireTokenValue(token, parseError).trim().toLowerCase();
|
|
73
|
+
if (!STATES.has(state)) {
|
|
74
|
+
throw parseError("--state must be one of: open, closed, all");
|
|
75
|
+
}
|
|
76
|
+
options.state = state;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (token.name === "label") {
|
|
80
|
+
const label = requireTokenValue(token, parseError).trim();
|
|
81
|
+
if (label.length === 0) {
|
|
82
|
+
throw parseError("--label must be a non-empty string");
|
|
83
|
+
}
|
|
84
|
+
options.labels.push(label);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (token.name === "limit") {
|
|
88
|
+
const raw = requireTokenValue(token, parseError);
|
|
89
|
+
const value = Number(raw);
|
|
90
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
91
|
+
throw parseError(`--limit must be a positive integer, got "${raw}"`);
|
|
92
|
+
}
|
|
93
|
+
options.limit = value;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (token.name === "jq") {
|
|
97
|
+
options.jq = requireTokenValue(token, parseError);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (token.name === "silent") {
|
|
101
|
+
options.silent = true;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
105
|
+
}
|
|
106
|
+
if (options.repo === undefined) {
|
|
107
|
+
throw parseError("Listing issues requires --repo <owner/name>");
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
parseRepoSlug(options.repo);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
throw parseError(error instanceof Error ? error.message : String(error));
|
|
113
|
+
}
|
|
114
|
+
return options;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Returns a well-typed issue, or null if the gh entry is missing/invalid in any
|
|
118
|
+
// required field. Callers filter the nulls so the emitted shape stays
|
|
119
|
+
// well-typed (documented output) and --jq filters never hit null number/title/state.
|
|
120
|
+
function normalizeIssue(raw) {
|
|
121
|
+
if (!Number.isInteger(raw?.number) || typeof raw?.title !== "string" || typeof raw?.state !== "string") {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
number: raw.number,
|
|
126
|
+
title: raw.title,
|
|
127
|
+
// gh reports issue state UPPERCASE (OPEN/CLOSED); normalize to lowercase to
|
|
128
|
+
// match the --state flag vocabulary.
|
|
129
|
+
state: raw.state.toLowerCase(),
|
|
130
|
+
labels: Array.isArray(raw?.labels)
|
|
131
|
+
? raw.labels.map((l) => (typeof l?.name === "string" ? l.name : null)).filter((n) => n !== null)
|
|
132
|
+
: [],
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function listIssues(options, { env = process.env, ghCommand = "gh", run = runChild } = {}) {
|
|
137
|
+
const args = [
|
|
138
|
+
"issue",
|
|
139
|
+
"list",
|
|
140
|
+
"--repo",
|
|
141
|
+
options.repo,
|
|
142
|
+
"--state",
|
|
143
|
+
options.state,
|
|
144
|
+
"--limit",
|
|
145
|
+
String(options.limit),
|
|
146
|
+
"--json",
|
|
147
|
+
"number,title,state,labels",
|
|
148
|
+
];
|
|
149
|
+
for (const label of options.labels) {
|
|
150
|
+
args.push("--label", label);
|
|
151
|
+
}
|
|
152
|
+
const result = await run(ghCommand, args, env);
|
|
153
|
+
if (result.code !== 0) {
|
|
154
|
+
const detail = result.stderr.trim() || `exit code ${result.code}`;
|
|
155
|
+
throw new Error(`gh issue list failed: ${detail}`);
|
|
156
|
+
}
|
|
157
|
+
const payload = parseJsonText(result.stdout, { label: "gh issue list" });
|
|
158
|
+
if (!Array.isArray(payload)) {
|
|
159
|
+
throw new Error("gh issue list did not return a JSON array");
|
|
160
|
+
}
|
|
161
|
+
return { ok: true, issues: payload.map(normalizeIssue).filter((issue) => issue !== null) };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export async function runCli(
|
|
165
|
+
argv = process.argv.slice(2),
|
|
166
|
+
{ stdout = process.stdout, stderr = process.stderr, env = process.env, ghCommand = "gh", run = runChild } = {},
|
|
167
|
+
) {
|
|
168
|
+
let options;
|
|
169
|
+
try {
|
|
170
|
+
options = parseListIssuesCliArgs(argv);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
stderr.write(`${formatCliError(error)}\n`);
|
|
173
|
+
return 1;
|
|
174
|
+
}
|
|
175
|
+
if (options.help) {
|
|
176
|
+
stdout.write(`${USAGE}\n`);
|
|
177
|
+
return 0;
|
|
178
|
+
}
|
|
179
|
+
let result;
|
|
180
|
+
try {
|
|
181
|
+
result = await listIssues(options, { env, ghCommand, run });
|
|
182
|
+
} catch (error) {
|
|
183
|
+
stderr.write(`${JSON.stringify({ ok: false, error: error instanceof Error ? error.message : String(error) })}\n`);
|
|
184
|
+
return 1;
|
|
185
|
+
}
|
|
186
|
+
return emitResult(result, { jq: options.jq, silent: options.silent, stdout, stderr });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (isDirectCliRun(import.meta.url)) {
|
|
190
|
+
runCli().then((code) => { process.exitCode = code; });
|
|
191
|
+
}
|
|
@@ -3,6 +3,7 @@ import { setTimeout as delay } from "node:timers/promises";
|
|
|
3
3
|
import { buildParseError, formatCliError, isCopilotLogin, isDirectCliRun, parseJsonText, parseReviewThreads } from "../_core-helpers.mjs";
|
|
4
4
|
import { parseArgs } from "node:util";
|
|
5
5
|
import { parsePositiveInteger, requireTokenValue, runChild } from "../_cli-primitives.mjs";
|
|
6
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult } from "../lib/jq-output.mjs";
|
|
6
7
|
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
7
8
|
import {
|
|
8
9
|
DEFAULT_POLL_INTERVAL_MS,
|
|
@@ -24,6 +25,10 @@ Required:
|
|
|
24
25
|
Output (stdout, JSON):
|
|
25
26
|
{ "ok": true, "status": "changed"|"timeout"|"idle", "repo": "...", "pr": N, "attempts": N,
|
|
26
27
|
"newComments": [...], "newReviews": [...], "newIssueComments": [...] }
|
|
28
|
+
Concise mode:
|
|
29
|
+
--concise, --summary Human-readable summary: status, attempts, new-activity
|
|
30
|
+
counts, AND the current round's new Copilot comment bodies.
|
|
31
|
+
${JQ_OUTPUT_USAGE}
|
|
27
32
|
Activity statuses:
|
|
28
33
|
changed Fresh Copilot review activity found (check newComments/newReviews/newIssueComments)
|
|
29
34
|
timeout Watch period elapsed with no fresh Copilot activity
|
|
@@ -91,7 +96,14 @@ function rejectRemovedFlag(token) {
|
|
|
91
96
|
export function parseWatchCliArgs(argv) {
|
|
92
97
|
const { tokens } = parseArgs({
|
|
93
98
|
args: [...argv],
|
|
94
|
-
options: {
|
|
99
|
+
options: {
|
|
100
|
+
help: { type: "boolean", short: "h" },
|
|
101
|
+
repo: { type: "string" },
|
|
102
|
+
pr: { type: "string" },
|
|
103
|
+
concise: { type: "boolean" },
|
|
104
|
+
summary: { type: "boolean" },
|
|
105
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
106
|
+
},
|
|
95
107
|
allowPositionals: true,
|
|
96
108
|
strict: false,
|
|
97
109
|
tokens: true,
|
|
@@ -102,6 +114,9 @@ export function parseWatchCliArgs(argv) {
|
|
|
102
114
|
pr: undefined,
|
|
103
115
|
pollIntervalMs: DEFAULT_POLL_INTERVAL_MS,
|
|
104
116
|
timeoutMs: COPILOT_REVIEW_WAIT_TIMEOUT_MS,
|
|
117
|
+
concise: false,
|
|
118
|
+
jq: undefined,
|
|
119
|
+
silent: false,
|
|
105
120
|
};
|
|
106
121
|
for (const token of tokens) {
|
|
107
122
|
if (token.kind === "positional") {
|
|
@@ -125,6 +140,18 @@ export function parseWatchCliArgs(argv) {
|
|
|
125
140
|
options.pr = parsePositiveInteger(requireTokenValue(token, parseError), "--pr", parseError);
|
|
126
141
|
continue;
|
|
127
142
|
}
|
|
143
|
+
if (token.name === "concise" || token.name === "summary") {
|
|
144
|
+
options.concise = true;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (token.name === "jq") {
|
|
148
|
+
options.jq = requireTokenValue(token, parseError);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (token.name === "silent") {
|
|
152
|
+
options.silent = true;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
128
155
|
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
129
156
|
}
|
|
130
157
|
if (options.repo === undefined || options.pr === undefined) {
|
|
@@ -311,6 +338,37 @@ export function buildPollDelayMs(watchStartedAtMs, timeoutMs, pollIntervalMs, at
|
|
|
311
338
|
const scheduledAtMs = watchStartedAtMs + Math.min(timeoutMs, attempt * pollIntervalMs);
|
|
312
339
|
return Math.max(0, scheduledAtMs - nowMs);
|
|
313
340
|
}
|
|
341
|
+
// Human-readable concise summary of a probe result, including the current
|
|
342
|
+
// round's new Copilot comment bodies (the field `loop info --pr` omits).
|
|
343
|
+
export function formatProbeConcise(result) {
|
|
344
|
+
const newComments = result.newComments ?? [];
|
|
345
|
+
const newReviews = result.newReviews ?? [];
|
|
346
|
+
const newIssueComments = result.newIssueComments ?? [];
|
|
347
|
+
const lines = [
|
|
348
|
+
`Copilot probe: PR #${result.pr} (${result.repo})`,
|
|
349
|
+
` status: ${result.status}`,
|
|
350
|
+
` attempts: ${result.attempts}`,
|
|
351
|
+
` new threadComments: ${newComments.length}`,
|
|
352
|
+
` new reviews: ${newReviews.length}`,
|
|
353
|
+
` new issueComments: ${newIssueComments.length}`,
|
|
354
|
+
];
|
|
355
|
+
const bodies = [
|
|
356
|
+
...newReviews.map((r) => ({ kind: "review", body: r.body })),
|
|
357
|
+
...newComments.map((c) => ({ kind: "threadComment", body: c.body })),
|
|
358
|
+
...newIssueComments.map((c) => ({ kind: "issueComment", body: c.body })),
|
|
359
|
+
].filter((entry) => typeof entry.body === "string" && entry.body.trim().length > 0);
|
|
360
|
+
if (bodies.length > 0) {
|
|
361
|
+
lines.push(" new Copilot comment bodies this round:");
|
|
362
|
+
for (const entry of bodies) {
|
|
363
|
+
const indented = entry.body.trim().split("\n").map((l) => ` ${l}`).join("\n");
|
|
364
|
+
lines.push(` [${entry.kind}]`);
|
|
365
|
+
lines.push(indented);
|
|
366
|
+
}
|
|
367
|
+
} else {
|
|
368
|
+
lines.push(" new Copilot comment bodies this round: (none)");
|
|
369
|
+
}
|
|
370
|
+
return lines.join("\n");
|
|
371
|
+
}
|
|
314
372
|
export async function runCli(
|
|
315
373
|
argv = process.argv.slice(2),
|
|
316
374
|
{
|
|
@@ -325,10 +383,18 @@ export async function runCli(
|
|
|
325
383
|
return;
|
|
326
384
|
}
|
|
327
385
|
const result = await watchCopilotReview(options, { env, ghCommand });
|
|
328
|
-
|
|
386
|
+
if (options.concise && options.jq === undefined && !options.silent) {
|
|
387
|
+
stdout.write(`${formatProbeConcise(result)}\n`);
|
|
388
|
+
return result.ok === false ? 1 : 0;
|
|
389
|
+
}
|
|
390
|
+
return emitResult(result, { jq: options.jq, silent: options.silent, stdout });
|
|
329
391
|
}
|
|
330
392
|
if (isDirectCliRun(import.meta.url)) {
|
|
331
|
-
runCli().
|
|
393
|
+
runCli().then((code) => {
|
|
394
|
+
if (typeof code === "number") {
|
|
395
|
+
process.exitCode = code;
|
|
396
|
+
}
|
|
397
|
+
}).catch((error) => {
|
|
332
398
|
process.stderr.write(`${formatCliError(error)}\n`);
|
|
333
399
|
process.exitCode = 1;
|
|
334
400
|
});
|
|
@@ -3,6 +3,7 @@ import { readFile } from "node:fs/promises";
|
|
|
3
3
|
import { buildParseError, formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
|
|
4
4
|
import { loadDevLoopConfig, resolveGateConfig, resolveRefinementConfig } from "@dev-loops/core/config";
|
|
5
5
|
import { parseArgs } from "node:util";
|
|
6
|
+
import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult } from "../lib/jq-output.mjs";
|
|
6
7
|
import { parsePrNumber, requireTokenValue, runChild } from "../_cli-primitives.mjs";
|
|
7
8
|
import { truncateText } from "@dev-loops/core/bash-exit-one";
|
|
8
9
|
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
@@ -105,9 +106,11 @@ exists on a different head SHA (the old comment is stale for the current head).
|
|
|
105
106
|
Error output (stderr, JSON):
|
|
106
107
|
{ "ok": false, "error": "...", "usage": "..." }
|
|
107
108
|
{ "ok": false, "error": "..." }
|
|
109
|
+
${JQ_OUTPUT_USAGE}
|
|
108
110
|
Exit codes:
|
|
109
111
|
0 Success
|
|
110
|
-
1 Argument error, gh failure, or contradictory gate evidence
|
|
112
|
+
1 Argument error, gh failure, or contradictory gate evidence
|
|
113
|
+
2 Invalid --jq filter`.trim();
|
|
111
114
|
const parseError = buildParseError(USAGE);
|
|
112
115
|
function rejectRemovedFlag(token) {
|
|
113
116
|
throw parseError(
|
|
@@ -258,6 +261,7 @@ export function parseUpsertCheckpointVerdictCliArgs(argv) {
|
|
|
258
261
|
"findings-severity-counts": { type: "string" },
|
|
259
262
|
"execution-mode": { type: "string" },
|
|
260
263
|
"inline-reason": { type: "string" },
|
|
264
|
+
...JQ_OUTPUT_PARSE_OPTIONS,
|
|
261
265
|
},
|
|
262
266
|
allowPositionals: true,
|
|
263
267
|
strict: false,
|
|
@@ -277,6 +281,8 @@ export function parseUpsertCheckpointVerdictCliArgs(argv) {
|
|
|
277
281
|
findingsSeverityCounts: undefined,
|
|
278
282
|
executionMode: undefined,
|
|
279
283
|
inlineReason: undefined,
|
|
284
|
+
jq: undefined,
|
|
285
|
+
silent: false,
|
|
280
286
|
};
|
|
281
287
|
for (const token of tokens) {
|
|
282
288
|
if (token.kind === "positional") {
|
|
@@ -385,6 +391,14 @@ export function parseUpsertCheckpointVerdictCliArgs(argv) {
|
|
|
385
391
|
options.inlineReason = smartTruncate(reason, MAX_GATE_COMMENT_EXCERPT_LENGTH);
|
|
386
392
|
continue;
|
|
387
393
|
}
|
|
394
|
+
if (token.name === "jq") {
|
|
395
|
+
options.jq = requireTokenValue(token, parseError);
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
if (token.name === "silent") {
|
|
399
|
+
options.silent = true;
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
388
402
|
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
389
403
|
}
|
|
390
404
|
// Default execution mode to inline_single_agent when omitted. inlineReason is
|
|
@@ -1286,10 +1300,11 @@ async function main() {
|
|
|
1286
1300
|
const result = await upsertCheckpointVerdict(options);
|
|
1287
1301
|
// Emit the inline-execution warning only on success so the JSON error
|
|
1288
1302
|
// envelope on stderr stays clean and machine-parseable on failures.
|
|
1289
|
-
|
|
1303
|
+
// Suppress it under --silent, which contracts to zero output (exit code only).
|
|
1304
|
+
if (inlineWarning && !options.silent) {
|
|
1290
1305
|
process.stderr.write(`${inlineWarning}\n`);
|
|
1291
1306
|
}
|
|
1292
|
-
process.
|
|
1307
|
+
process.exitCode = emitResult(result, { jq: options.jq, silent: options.silent });
|
|
1293
1308
|
} catch (error) {
|
|
1294
1309
|
process.stderr.write(`${JSON.stringify({ ok: false, error: error instanceof Error ? error.message : String(error) })}\n`);
|
|
1295
1310
|
process.exitCode = 1;
|