@h-rig/standard-plugin 0.0.6-alpha.141 → 0.0.6-alpha.143
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/dist/src/bundle.d.ts +6 -5
- package/dist/src/bundle.js +1693 -1300
- package/dist/src/cli-surface.d.ts +4 -0
- package/dist/src/cli-surface.js +21 -0
- package/dist/src/drift/metadata.d.ts +13 -0
- package/dist/src/drift/metadata.js +33 -0
- package/dist/src/drift/plugin.d.ts +4 -14
- package/dist/src/drift/plugin.js +4 -5
- package/dist/src/github-issues-source.js +52 -20
- package/dist/src/index.d.ts +0 -2
- package/dist/src/index.js +1657 -1414
- package/dist/src/lifecycle-closeout.d.ts +2 -0
- package/dist/src/lifecycle-closeout.js +6 -0
- package/dist/src/plugin.d.ts +8 -11
- package/dist/src/plugin.js +1657 -1397
- package/dist/src/product-entrypoint.d.ts +11 -0
- package/dist/src/product-entrypoint.js +101 -0
- package/dist/src/run-worker-panels.d.ts +15 -0
- package/dist/src/run-worker-panels.js +53 -0
- package/package.json +24 -8
package/dist/src/plugin.js
CHANGED
|
@@ -1,1489 +1,1702 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __returnValue = (v) => v;
|
|
4
|
+
function __exportSetter(name, newValue) {
|
|
5
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
6
|
+
}
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, {
|
|
10
|
+
get: all[name],
|
|
11
|
+
enumerable: true,
|
|
12
|
+
configurable: true,
|
|
13
|
+
set: __exportSetter.bind(all, name)
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
5
17
|
|
|
6
|
-
// packages/standard-plugin/src/
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return {
|
|
16
|
-
async resolveGitHubToken(input) {
|
|
17
|
-
if (input.purpose === "selected-repo") {
|
|
18
|
-
return { token: cleanToken(process.env.RIG_GITHUB_SELECTED_TOKEN ?? process.env.RIG_GITHUB_TOKEN ?? null) ?? "", source: "signed-in-user" };
|
|
19
|
-
}
|
|
20
|
-
const token = cleanToken(process.env.RIG_GITHUB_TOKEN ?? process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? null);
|
|
21
|
-
if (!token) {
|
|
22
|
-
throw new Error("No host GitHub token is configured for admin fallback.");
|
|
23
|
-
}
|
|
24
|
-
return { token, source: "host-admin-fallback" };
|
|
25
|
-
}
|
|
18
|
+
// packages/standard-plugin/src/drift/metadata.ts
|
|
19
|
+
import { Schema } from "effect";
|
|
20
|
+
import { StageMutation as StageMutationSchema } from "@rig/contracts";
|
|
21
|
+
var DOCS_DRIFT_VALIDATOR_ID = "std:docs-drift", DOCS_DRIFT_CLI_ID = "std:drift", DOCS_DRIFT_STAGE_ID = "docs-drift", DOCS_DRIFT_CAPABILITY_ID = "std:docs-drift-capability", DOCS_DRIFT_VALIDATOR, DOCS_DRIFT_STAGE_MUTATION, DOCS_DRIFT_CLI_COMMAND = "rig drift [--docs <csv>] [--ignore <csv>] [--fail-on-drift] [--json]";
|
|
22
|
+
var init_metadata = __esm(() => {
|
|
23
|
+
DOCS_DRIFT_VALIDATOR = {
|
|
24
|
+
id: DOCS_DRIFT_VALIDATOR_ID,
|
|
25
|
+
category: "regression",
|
|
26
|
+
description: "Detect documentation references that drifted from the source tree."
|
|
26
27
|
};
|
|
28
|
+
DOCS_DRIFT_STAGE_MUTATION = Schema.decodeUnknownSync(StageMutationSchema)({
|
|
29
|
+
op: "insert",
|
|
30
|
+
stage: {
|
|
31
|
+
id: DOCS_DRIFT_STAGE_ID,
|
|
32
|
+
kind: "gate",
|
|
33
|
+
before: ["merge-gate"],
|
|
34
|
+
after: ["open-pr"]
|
|
35
|
+
},
|
|
36
|
+
contributedBy: DOCS_DRIFT_STAGE_ID
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// packages/standard-plugin/src/product-entrypoint.ts
|
|
41
|
+
var exports_product_entrypoint = {};
|
|
42
|
+
__export(exports_product_entrypoint, {
|
|
43
|
+
runRigOmpProductCommand: () => runRigOmpProductCommand
|
|
44
|
+
});
|
|
45
|
+
import { resolve as resolve3 } from "path";
|
|
46
|
+
import { runCli } from "@oh-my-pi/pi-coding-agent/cli";
|
|
47
|
+
import { parseArgs } from "@oh-my-pi/pi-coding-agent/cli/args";
|
|
48
|
+
import { resolveCliArgv } from "@oh-my-pi/pi-coding-agent/cli-commands";
|
|
49
|
+
import { prepareAcpTerminalAuthArgs } from "@oh-my-pi/pi-coding-agent/modes/acp/terminal-auth";
|
|
50
|
+
import { runRootCommand } from "@oh-my-pi/pi-coding-agent/main";
|
|
51
|
+
import { createAgentSession } from "@oh-my-pi/pi-coding-agent/sdk";
|
|
52
|
+
import { applyIdentityEnv, identityFilterFromEnv, reconcileRuns } from "@rig/client";
|
|
53
|
+
import rigExtension from "@rig/rig-extension";
|
|
54
|
+
import { resolveRigOmpConfigOverlayPath } from "@rig/runtime/control-plane/remote-config";
|
|
55
|
+
function withRigDefaultConfig(projectRoot, argv) {
|
|
56
|
+
return ["--config", resolveRigOmpConfigOverlayPath(projectRoot), ...argv];
|
|
57
|
+
}
|
|
58
|
+
function createRigAgentSession(options = {}) {
|
|
59
|
+
return createAgentSession({
|
|
60
|
+
...options,
|
|
61
|
+
extensions: [(api) => rigExtension(api), ...options.extensions ?? []]
|
|
62
|
+
});
|
|
27
63
|
}
|
|
28
|
-
function
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
64
|
+
function productArgv(input) {
|
|
65
|
+
if (input.command === "launch" && input.args.length === 0)
|
|
66
|
+
return [];
|
|
67
|
+
if (input.command === "launch" && input.args[0]?.startsWith("-"))
|
|
68
|
+
return [...input.args];
|
|
69
|
+
return [input.command, ...input.args];
|
|
70
|
+
}
|
|
71
|
+
async function runRigOmpProductCommand(input) {
|
|
72
|
+
const projectRoot = resolve3(input.projectRoot);
|
|
73
|
+
const previousProjectRoot = process.env.RIG_PROJECT_ROOT;
|
|
74
|
+
const previousCwd = process.cwd();
|
|
75
|
+
const restorePublicIdentityEnv = applyIdentityEnv(projectRoot);
|
|
76
|
+
process.env.RIG_PROJECT_ROOT = projectRoot;
|
|
77
|
+
process.chdir(projectRoot);
|
|
78
|
+
try {
|
|
79
|
+
const argv = productArgv(input);
|
|
80
|
+
if (argv[0]?.startsWith("__omp_worker_") || argv[0] === "--smoke-test") {
|
|
81
|
+
await runCli(argv);
|
|
82
|
+
return { ok: true, group: "product", command: input.command };
|
|
37
83
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
try {
|
|
45
|
-
const parsed = JSON.parse(readFileSync(stateFile, "utf8"));
|
|
46
|
-
const token = typeof parsed.token === "string" ? cleanToken(parsed.token) : null;
|
|
47
|
-
if (token)
|
|
48
|
-
return token;
|
|
49
|
-
} catch {}
|
|
84
|
+
if (process.stdin.isTTY) {
|
|
85
|
+
const reconcile = await reconcileRuns({
|
|
86
|
+
workspaceRoot: projectRoot,
|
|
87
|
+
identityFilter: identityFilterFromEnv()
|
|
88
|
+
}).catch(() => ({ flipped: [], resumable: [] }));
|
|
89
|
+
globalThis.__RIG_RESUMABLE__ = reconcile.resumable;
|
|
50
90
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
91
|
+
const resolved = resolveCliArgv(argv);
|
|
92
|
+
if ("error" in resolved) {
|
|
93
|
+
process.stderr.write(`error: ${resolved.error}
|
|
94
|
+
`);
|
|
95
|
+
process.exitCode = 1;
|
|
96
|
+
return { ok: true, group: "product", command: input.command };
|
|
97
|
+
}
|
|
98
|
+
const [ompCommand, ...ompCommandArgs] = resolved.argv;
|
|
99
|
+
if (ompCommand === "launch") {
|
|
100
|
+
const args = withRigDefaultConfig(projectRoot, ompCommandArgs);
|
|
101
|
+
await runRootCommand(parseArgs(args), args, { createAgentSession: createRigAgentSession });
|
|
102
|
+
} else if (ompCommand === "acp") {
|
|
103
|
+
const { args: preparedArgs, terminalAuth } = prepareAcpTerminalAuthArgs(ompCommandArgs);
|
|
104
|
+
const args = withRigDefaultConfig(projectRoot, preparedArgs);
|
|
105
|
+
const parsed = parseArgs(args);
|
|
106
|
+
if (!terminalAuth)
|
|
107
|
+
parsed.mode = "acp";
|
|
108
|
+
await runRootCommand(parsed, args, { createAgentSession: createRigAgentSession });
|
|
109
|
+
} else if (ompCommand === "join") {
|
|
110
|
+
const link = ompCommandArgs[0]?.trim();
|
|
111
|
+
if (!link) {
|
|
112
|
+
process.stderr.write(`Usage: rig join <link>
|
|
113
|
+
`);
|
|
114
|
+
process.exitCode = 1;
|
|
115
|
+
return { ok: true, group: "product", command: input.command };
|
|
61
116
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
117
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
118
|
+
process.stderr.write(`rig join requires an interactive terminal
|
|
119
|
+
`);
|
|
120
|
+
process.exitCode = 1;
|
|
121
|
+
return { ok: true, group: "product", command: input.command };
|
|
65
122
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
var RIG_METADATA_START = "<!-- rig:metadata:start -->";
|
|
73
|
-
var RIG_METADATA_END = "<!-- rig:metadata:end -->";
|
|
74
|
-
var RIG_STATUS_LABELS = new Set(["rig:running", "rig:pr-open", "rig:ci-fixing", "rig:merging", "rig:done", "rig:needs-attention"]);
|
|
75
|
-
var DEFAULT_GH_TIMEOUT_MS = 15000;
|
|
76
|
-
var DEFAULT_GITHUB_ISSUE_LIST_LIMIT = 1000;
|
|
77
|
-
function statusFor(issue) {
|
|
78
|
-
const state = (issue.state ?? "").toUpperCase();
|
|
79
|
-
if (state === "CLOSED")
|
|
80
|
-
return "closed";
|
|
81
|
-
const labelNames = labelNamesFor(issue);
|
|
82
|
-
if (labelNames.includes("in-progress"))
|
|
83
|
-
return "in_progress";
|
|
84
|
-
if (labelNames.includes("blocked"))
|
|
85
|
-
return "blocked";
|
|
86
|
-
if (labelNames.includes("ready"))
|
|
87
|
-
return "ready";
|
|
88
|
-
if (labelNames.includes("under-review"))
|
|
89
|
-
return "under_review";
|
|
90
|
-
if (labelNames.includes("failed"))
|
|
91
|
-
return "failed";
|
|
92
|
-
if (labelNames.includes("cancelled"))
|
|
93
|
-
return "cancelled";
|
|
94
|
-
return "open";
|
|
95
|
-
}
|
|
96
|
-
function parseIssueRefs(raw) {
|
|
97
|
-
return raw.split(",").map((s) => s.trim()).map((s) => s.replace(/^#/, "").match(/^(\d+)/)?.[1] ?? "").filter((s) => s.length > 0);
|
|
98
|
-
}
|
|
99
|
-
function parseMetadataList(body, key) {
|
|
100
|
-
const block = body.match(/<!-- rig:metadata:start -->\s*([\s\S]*?)\s*<!-- rig:metadata:end -->/);
|
|
101
|
-
if (!block)
|
|
102
|
-
return [];
|
|
103
|
-
const lines = block[1].split(/\r?\n/);
|
|
104
|
-
const values = [];
|
|
105
|
-
for (let index = 0;index < lines.length; index += 1) {
|
|
106
|
-
const line = lines[index];
|
|
107
|
-
const sameLine = line.match(new RegExp(`^${key}:\\s*(.+)$`, "i"));
|
|
108
|
-
if (sameLine) {
|
|
109
|
-
values.push(...parseIssueRefs(sameLine[1]));
|
|
110
|
-
continue;
|
|
123
|
+
const args = withRigDefaultConfig(projectRoot, []);
|
|
124
|
+
const parsed = parseArgs(args);
|
|
125
|
+
parsed.join = link;
|
|
126
|
+
await runRootCommand(parsed, args, { createAgentSession: createRigAgentSession });
|
|
127
|
+
} else {
|
|
128
|
+
await runCli(resolved.argv);
|
|
111
129
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
130
|
+
return { ok: true, group: "product", command: input.command };
|
|
131
|
+
} finally {
|
|
132
|
+
restorePublicIdentityEnv();
|
|
133
|
+
process.chdir(previousCwd);
|
|
134
|
+
if (previousProjectRoot === undefined) {
|
|
135
|
+
delete process.env.RIG_PROJECT_ROOT;
|
|
136
|
+
} else {
|
|
137
|
+
process.env.RIG_PROJECT_ROOT = previousProjectRoot;
|
|
119
138
|
}
|
|
120
139
|
}
|
|
121
|
-
return [...new Set(values)];
|
|
122
|
-
}
|
|
123
|
-
function parseDeps(body) {
|
|
124
|
-
const match = body.match(/^depends-on:\s*([^\n]+)/im);
|
|
125
|
-
const bodyRefs = match ? parseIssueRefs(match[1]) : [];
|
|
126
|
-
return [...new Set([...bodyRefs, ...parseMetadataList(body, "depends-on")])];
|
|
127
140
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
141
|
+
var init_product_entrypoint = () => {};
|
|
142
|
+
|
|
143
|
+
// packages/standard-plugin/src/drift/extract-refs.ts
|
|
144
|
+
function stripFenceLines(markdown) {
|
|
145
|
+
const lines = markdown.split(/\r?\n/);
|
|
146
|
+
let fenced = false;
|
|
147
|
+
return lines.map((line) => {
|
|
148
|
+
if (/^\s*(```|~~~)/.test(line)) {
|
|
149
|
+
fenced = !fenced;
|
|
150
|
+
return "";
|
|
151
|
+
}
|
|
152
|
+
return fenced ? "" : line;
|
|
153
|
+
});
|
|
132
154
|
}
|
|
133
|
-
function
|
|
134
|
-
|
|
135
|
-
const typed = labels.find((l) => l.startsWith("type:"));
|
|
136
|
-
if (typed)
|
|
137
|
-
return typed.slice("type:".length);
|
|
138
|
-
if (labels.includes("epic"))
|
|
139
|
-
return "epic";
|
|
140
|
-
return "task";
|
|
155
|
+
function normalizeToken(raw) {
|
|
156
|
+
return raw.trim().replace(/^['"]|['"]$/g, "").replace(/[),.;:]+$/g, "").replace(/#L\d+(?:-L\d+)?$/i, "");
|
|
141
157
|
}
|
|
142
|
-
function
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const parsedDeps = parseDeps(body);
|
|
151
|
-
const deps = nativeDependencies?.deps ? [...new Set([...parsedDeps, ...nativeDependencies.deps])] : parsedDeps;
|
|
152
|
-
return {
|
|
153
|
-
id: String(issue.number),
|
|
154
|
-
...typeof issueNodeId === "string" && issueNodeId.trim() ? { issueNodeId: issueNodeId.trim() } : {},
|
|
155
|
-
deps,
|
|
156
|
-
status: statusFor(issue),
|
|
157
|
-
title: issue.title,
|
|
158
|
-
body,
|
|
159
|
-
scope,
|
|
160
|
-
role,
|
|
161
|
-
validators,
|
|
162
|
-
url: issue.url ?? issue.html_url,
|
|
163
|
-
issueType: issueTypeFor(issue),
|
|
164
|
-
sourceIssueId: `${repo}#${issue.number}`,
|
|
165
|
-
parentChildDeps: parseParents(body),
|
|
166
|
-
labels: labelNames,
|
|
167
|
-
...nativeDependencies?.degraded ? { nativeDependenciesDegraded: true, nativeDependenciesError: nativeDependencies.degraded } : {},
|
|
168
|
-
raw: issue
|
|
169
|
-
};
|
|
158
|
+
function classifyReference(raw) {
|
|
159
|
+
if (raw.startsWith("@"))
|
|
160
|
+
return null;
|
|
161
|
+
if (PATH_REF.test(raw))
|
|
162
|
+
return "path";
|
|
163
|
+
if (SYMBOL_REF.test(raw))
|
|
164
|
+
return "symbol";
|
|
165
|
+
return null;
|
|
170
166
|
}
|
|
171
|
-
function
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
167
|
+
function pushReference(refs, seen, raw, line) {
|
|
168
|
+
const value = normalizeToken(raw);
|
|
169
|
+
if (!value)
|
|
170
|
+
return;
|
|
171
|
+
const kind = classifyReference(value);
|
|
172
|
+
if (!kind)
|
|
173
|
+
return;
|
|
174
|
+
const key = `${kind}:${value}:${line}`;
|
|
175
|
+
if (seen.has(key))
|
|
176
|
+
return;
|
|
177
|
+
seen.add(key);
|
|
178
|
+
refs.push({ kind, value, line });
|
|
177
179
|
}
|
|
178
|
-
function
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
180
|
+
function extractDriftReferences(markdown) {
|
|
181
|
+
const refs = [];
|
|
182
|
+
const seen = new Set;
|
|
183
|
+
const lines = stripFenceLines(markdown);
|
|
184
|
+
for (const [index, line] of lines.entries()) {
|
|
185
|
+
const lineNumber = index + 1;
|
|
186
|
+
for (const match of line.matchAll(INLINE_CODE)) {
|
|
187
|
+
pushReference(refs, seen, match[1] ?? "", lineNumber);
|
|
188
|
+
}
|
|
189
|
+
for (const match of line.matchAll(MARKDOWN_LINK)) {
|
|
190
|
+
pushReference(refs, seen, match[1] ?? "", lineNumber);
|
|
191
|
+
}
|
|
183
192
|
}
|
|
184
|
-
|
|
185
|
-
return JSON.stringify(value);
|
|
186
|
-
return String(value);
|
|
193
|
+
return refs;
|
|
187
194
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
]
|
|
194
|
-
|
|
195
|
-
const pattern = new RegExp(`${RIG_METADATA_START.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*[\\s\\S]*?\\s*${RIG_METADATA_END.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`);
|
|
196
|
-
if (pattern.test(body))
|
|
197
|
-
return body.replace(pattern, rendered);
|
|
198
|
-
return body.trim().length > 0 ? `${body.trimEnd()}
|
|
195
|
+
var INLINE_CODE, MARKDOWN_LINK, SYMBOL_REF, PATH_REF;
|
|
196
|
+
var init_extract_refs = __esm(() => {
|
|
197
|
+
INLINE_CODE = /`([^`\n]+)`/g;
|
|
198
|
+
MARKDOWN_LINK = /\[[^\]]+\]\(([^)\s]+)\)/g;
|
|
199
|
+
SYMBOL_REF = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)?$/;
|
|
200
|
+
PATH_REF = /^(?:\.\.?\/)?(?:[A-Za-z0-9_.-]+\/)+[A-Za-z0-9_.-]+$|^[A-Za-z0-9_.-]+\.(?:ts|tsx|js|jsx|mjs|cjs|json|md|mdx|css|scss|html|yml|yaml|toml|rs|go|py|rb|java|kt|swift|c|cc|cpp|h|hpp)$/;
|
|
201
|
+
});
|
|
199
202
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const lines = [
|
|
206
|
-
RIG_STATUS_COMMENT_MARKER,
|
|
207
|
-
`### Rig status: ${input.status}`,
|
|
208
|
-
"",
|
|
209
|
-
input.summary
|
|
210
|
-
];
|
|
211
|
-
if (input.runId)
|
|
212
|
-
lines.push("", `- Run: ${input.runId}`);
|
|
213
|
-
if (input.prUrl)
|
|
214
|
-
lines.push(`- PR: ${input.prUrl}`);
|
|
215
|
-
for (const detail of input.details ?? [])
|
|
216
|
-
lines.push(`- ${detail}`);
|
|
217
|
-
return lines.join(`
|
|
218
|
-
`);
|
|
203
|
+
// packages/standard-plugin/src/drift/git-adapter.ts
|
|
204
|
+
import { execFile } from "child_process";
|
|
205
|
+
import { promisify } from "util";
|
|
206
|
+
function processError(value) {
|
|
207
|
+
return value && typeof value === "object" ? value : null;
|
|
219
208
|
}
|
|
220
|
-
function
|
|
221
|
-
|
|
209
|
+
function lineCount(output) {
|
|
210
|
+
const trimmed = output.trim();
|
|
211
|
+
return trimmed ? trimmed.split(/\r?\n/).length : 0;
|
|
222
212
|
}
|
|
223
|
-
function
|
|
213
|
+
function makeDriftGit(projectRoot) {
|
|
214
|
+
async function git(args) {
|
|
215
|
+
const result = await execFileAsync("git", [...args], {
|
|
216
|
+
cwd: projectRoot,
|
|
217
|
+
encoding: "utf8",
|
|
218
|
+
maxBuffer: 10 * 1024 * 1024
|
|
219
|
+
});
|
|
220
|
+
return String(result.stdout);
|
|
221
|
+
}
|
|
222
|
+
async function grepCountAt(symbolOrPath, commit) {
|
|
223
|
+
try {
|
|
224
|
+
return lineCount(await git(["grep", "-F", "-n", "-e", symbolOrPath, commit, "--"]));
|
|
225
|
+
} catch (error) {
|
|
226
|
+
const detail = processError(error);
|
|
227
|
+
if (detail?.code === 1)
|
|
228
|
+
return 0;
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
224
232
|
return {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
+
async lastCommitTouching(path) {
|
|
234
|
+
const commit = (await git(["log", "-n", "1", "--format=%H", "--", path])).trim();
|
|
235
|
+
return commit || "HEAD";
|
|
236
|
+
},
|
|
237
|
+
async grepCount(symbolOrPath) {
|
|
238
|
+
return grepCountAt(symbolOrPath, "HEAD");
|
|
239
|
+
},
|
|
240
|
+
async grepCountAtCommit(symbolOrPath, commit) {
|
|
241
|
+
return grepCountAt(symbolOrPath, commit);
|
|
242
|
+
},
|
|
243
|
+
async wasRenamed(symbolOrPath, sinceCommit) {
|
|
244
|
+
if (!symbolOrPath.includes("/") && !symbolOrPath.includes("."))
|
|
245
|
+
return false;
|
|
246
|
+
try {
|
|
247
|
+
const output = await git(["log", "--name-status", "--format=", `${sinceCommit}..HEAD`]);
|
|
248
|
+
return output.split(/\r?\n/).some((line) => {
|
|
249
|
+
const match = line.match(/^R\d*\s+(.+?)\s+(.+)$/);
|
|
250
|
+
return Boolean(match && (match[1] === symbolOrPath || match[2] === symbolOrPath));
|
|
251
|
+
});
|
|
252
|
+
} catch (error) {
|
|
253
|
+
const detail = processError(error);
|
|
254
|
+
if (detail?.code === 128)
|
|
255
|
+
return false;
|
|
256
|
+
throw error;
|
|
257
|
+
}
|
|
233
258
|
}
|
|
234
259
|
};
|
|
235
260
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
261
|
+
var execFileAsync;
|
|
262
|
+
var init_git_adapter = __esm(() => {
|
|
263
|
+
execFileAsync = promisify(execFile);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// packages/standard-plugin/src/drift/detect.ts
|
|
267
|
+
var exports_detect = {};
|
|
268
|
+
__export(exports_detect, {
|
|
269
|
+
detectStaleAnchors: () => detectStaleAnchors,
|
|
270
|
+
detectDrift: () => detectDrift,
|
|
271
|
+
detectDeletedReferences: () => detectDeletedReferences
|
|
272
|
+
});
|
|
273
|
+
import { existsSync as existsSync3 } from "fs";
|
|
274
|
+
import { readdir, readFile, stat } from "fs/promises";
|
|
275
|
+
import { basename as basename2, extname, relative, resolve as resolve4 } from "path";
|
|
276
|
+
function globLikeMatch(path, pattern) {
|
|
277
|
+
if (pattern === path)
|
|
278
|
+
return true;
|
|
279
|
+
if (pattern.startsWith("**/*"))
|
|
280
|
+
return path.endsWith(pattern.slice(4));
|
|
281
|
+
if (pattern.endsWith("/**"))
|
|
282
|
+
return path.startsWith(pattern.slice(0, -3));
|
|
283
|
+
if (pattern.includes("*")) {
|
|
284
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
285
|
+
return new RegExp(`^${escaped}$`).test(path);
|
|
286
|
+
}
|
|
287
|
+
return path.startsWith(pattern);
|
|
256
288
|
}
|
|
257
|
-
function
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
assertGhSuccess(args, res, options.env);
|
|
261
|
-
if (!res.stdout || res.stdout.trim() === "")
|
|
262
|
-
return [];
|
|
263
|
-
return JSON.parse(res.stdout);
|
|
289
|
+
function isDefaultDoc(path) {
|
|
290
|
+
const lower = basename2(path).toLowerCase();
|
|
291
|
+
return (path.endsWith(".md") || path.endsWith(".mdx")) && !lower.startsWith("changelog") && !lower.includes("generated");
|
|
264
292
|
}
|
|
265
|
-
function
|
|
266
|
-
|
|
267
|
-
const res = spawn(bin, [...args], options);
|
|
268
|
-
assertGhSuccess(args, res, options.env);
|
|
293
|
+
function isIgnored(path, patterns) {
|
|
294
|
+
return (patterns ?? []).some((pattern) => globLikeMatch(path, pattern));
|
|
269
295
|
}
|
|
270
|
-
function
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
296
|
+
async function collectFiles(root, options) {
|
|
297
|
+
const files = [];
|
|
298
|
+
async function visit(dir) {
|
|
299
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
300
|
+
if (entry.isDirectory() && DEFAULT_IGNORED_DIRS[entry.name])
|
|
301
|
+
continue;
|
|
302
|
+
const absolute = resolve4(dir, entry.name);
|
|
303
|
+
const rel = relative(root, absolute).replace(/\\/g, "/");
|
|
304
|
+
if (isIgnored(rel, options.ignore))
|
|
305
|
+
continue;
|
|
306
|
+
if (entry.isDirectory()) {
|
|
307
|
+
await visit(absolute);
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (!entry.isFile())
|
|
311
|
+
continue;
|
|
312
|
+
if (options.docs) {
|
|
313
|
+
const matchesConfigured = options.patterns && options.patterns.length > 0 ? options.patterns.some((pattern) => globLikeMatch(rel, pattern)) : isDefaultDoc(rel);
|
|
314
|
+
if (matchesConfigured)
|
|
315
|
+
files.push(rel);
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (SOURCE_EXTENSIONS[extname(entry.name)])
|
|
319
|
+
files.push(rel);
|
|
320
|
+
}
|
|
274
321
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
322
|
+
await visit(root);
|
|
323
|
+
return files.sort();
|
|
324
|
+
}
|
|
325
|
+
async function sourceReferenceCount(projectRoot, reference, docPath) {
|
|
326
|
+
if (reference.kind === "path")
|
|
327
|
+
return existsSync3(resolve4(projectRoot, reference.value)) ? 1 : 0;
|
|
328
|
+
let count = 0;
|
|
329
|
+
const sourceFiles = await collectFiles(projectRoot, { docs: false });
|
|
330
|
+
for (const sourceFile of sourceFiles) {
|
|
331
|
+
if (sourceFile === docPath)
|
|
332
|
+
continue;
|
|
333
|
+
const text = await readFile(resolve4(projectRoot, sourceFile), "utf8").catch(() => "");
|
|
334
|
+
if (text.includes(reference.value))
|
|
335
|
+
count += 1;
|
|
278
336
|
}
|
|
337
|
+
return count;
|
|
279
338
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
};
|
|
289
|
-
function asProjectRecord(value) {
|
|
290
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
339
|
+
function deletedReferenceFinding(docPath, reference) {
|
|
340
|
+
return {
|
|
341
|
+
kind: "deleted-reference",
|
|
342
|
+
docPath,
|
|
343
|
+
line: reference.line,
|
|
344
|
+
reference: reference.value,
|
|
345
|
+
detail: `Documented reference "${reference.value}" no longer exists in the source tree.`,
|
|
346
|
+
confidence: "high"
|
|
347
|
+
};
|
|
291
348
|
}
|
|
292
|
-
function
|
|
293
|
-
return
|
|
349
|
+
function staleAnchorFinding(docPath, reference) {
|
|
350
|
+
return {
|
|
351
|
+
kind: "stale-anchor",
|
|
352
|
+
docPath,
|
|
353
|
+
line: reference.line,
|
|
354
|
+
reference: reference.value,
|
|
355
|
+
detail: `Documented path "${reference.value}" changed after this doc was last updated.`,
|
|
356
|
+
confidence: "medium"
|
|
357
|
+
};
|
|
294
358
|
}
|
|
295
|
-
function
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
return "running";
|
|
306
|
-
case "under_review":
|
|
307
|
-
case "review":
|
|
308
|
-
case "pr_open":
|
|
309
|
-
return "prOpen";
|
|
310
|
-
case "ci_fixing":
|
|
311
|
-
case "fixing":
|
|
312
|
-
return "ciFixing";
|
|
313
|
-
case "merging":
|
|
314
|
-
case "merge":
|
|
315
|
-
return "merging";
|
|
316
|
-
case "closed":
|
|
317
|
-
case "completed":
|
|
318
|
-
case "done":
|
|
319
|
-
return "done";
|
|
320
|
-
case "blocked":
|
|
321
|
-
case "cancelled":
|
|
322
|
-
case "failed":
|
|
323
|
-
case "needs_attention":
|
|
324
|
-
return "needsAttention";
|
|
325
|
-
default:
|
|
326
|
-
return null;
|
|
359
|
+
async function detectDeletedReferences(projectRoot, docPath, git = makeDriftGit(projectRoot)) {
|
|
360
|
+
const markdown = await readFile(resolve4(projectRoot, docPath), "utf8");
|
|
361
|
+
const docCommit = await git.lastCommitTouching(docPath);
|
|
362
|
+
const findings = [];
|
|
363
|
+
for (const reference of extractDriftReferences(markdown)) {
|
|
364
|
+
if (await sourceReferenceCount(projectRoot, reference, docPath) > 0)
|
|
365
|
+
continue;
|
|
366
|
+
if (await git.wasRenamed(reference.value, docCommit))
|
|
367
|
+
continue;
|
|
368
|
+
findings.push(deletedReferenceFinding(docPath, reference));
|
|
327
369
|
}
|
|
370
|
+
return findings;
|
|
328
371
|
}
|
|
329
|
-
function
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
372
|
+
async function detectStaleAnchors(projectRoot, docPath, git = makeDriftGit(projectRoot)) {
|
|
373
|
+
const markdown = await readFile(resolve4(projectRoot, docPath), "utf8");
|
|
374
|
+
const docCommit = await git.lastCommitTouching(docPath);
|
|
375
|
+
const findings = [];
|
|
376
|
+
for (const reference of extractDriftReferences(markdown).filter((ref) => ref.kind === "path")) {
|
|
377
|
+
if (!existsSync3(resolve4(projectRoot, reference.value)))
|
|
378
|
+
continue;
|
|
379
|
+
const sourceStat = await stat(resolve4(projectRoot, reference.value)).catch(() => null);
|
|
380
|
+
if (!sourceStat?.isFile())
|
|
381
|
+
continue;
|
|
382
|
+
const sourceCommit = await git.lastCommitTouching(reference.value);
|
|
383
|
+
if (sourceCommit !== docCommit && !await git.wasRenamed(reference.value, docCommit)) {
|
|
384
|
+
findings.push(staleAnchorFinding(docPath, reference));
|
|
336
385
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
function issueNodeIdFor(issue) {
|
|
342
|
-
const id = issue.id ?? issue.nodeId ?? issue.node_id;
|
|
343
|
-
return typeof id === "string" && id.trim().length > 0 ? id.trim() : null;
|
|
386
|
+
}
|
|
387
|
+
return findings;
|
|
344
388
|
}
|
|
345
|
-
function
|
|
346
|
-
const
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
const nodes = Array.isArray(blockedBy?.nodes) ? blockedBy.nodes : [];
|
|
361
|
-
return [...new Set(nodes.flatMap((node) => {
|
|
362
|
-
const ref = nativeIssueDependencyRef(node, currentRepo);
|
|
363
|
-
return ref ? [ref] : [];
|
|
364
|
-
}))];
|
|
365
|
-
}
|
|
366
|
-
async function readNativeDependenciesForIssue(input) {
|
|
367
|
-
const issueId = issueNodeIdFor(input.issue);
|
|
368
|
-
if (!issueId)
|
|
369
|
-
return { deps: [], degraded: "GitHub issue node id is unavailable." };
|
|
370
|
-
const query = `
|
|
371
|
-
query RigIssueNativeDependencies($issueId: ID!) {
|
|
372
|
-
node(id: $issueId) {
|
|
373
|
-
... on Issue {
|
|
374
|
-
blockedBy(first: 100) {
|
|
375
|
-
nodes {
|
|
376
|
-
number
|
|
377
|
-
repository { name owner { login } }
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
}
|
|
389
|
+
async function detectDrift(options) {
|
|
390
|
+
const git = options.git ?? makeDriftGit(options.projectRoot);
|
|
391
|
+
const docs = await collectFiles(options.projectRoot, {
|
|
392
|
+
docs: true,
|
|
393
|
+
...options.docsGlobs !== undefined ? { patterns: options.docsGlobs } : {},
|
|
394
|
+
...options.ignoreGlobs !== undefined ? { ignore: options.ignoreGlobs } : {}
|
|
395
|
+
});
|
|
396
|
+
const findings = [];
|
|
397
|
+
let degraded = false;
|
|
398
|
+
for (const docPath of docs) {
|
|
399
|
+
try {
|
|
400
|
+
findings.push(...await detectDeletedReferences(options.projectRoot, docPath, git));
|
|
401
|
+
findings.push(...await detectStaleAnchors(options.projectRoot, docPath, git));
|
|
402
|
+
} catch {
|
|
403
|
+
degraded = true;
|
|
382
404
|
}
|
|
383
|
-
`;
|
|
384
|
-
try {
|
|
385
|
-
return {
|
|
386
|
-
deps: nativeDependencyRefsFrom(await input.fetchGraphQL(query, { issueId }, "gh-cli"), input.repo)
|
|
387
|
-
};
|
|
388
|
-
} catch (error) {
|
|
389
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
390
|
-
return { deps: [], degraded: detail };
|
|
391
405
|
}
|
|
406
|
+
return {
|
|
407
|
+
generatedAt: new Date().toISOString(),
|
|
408
|
+
scanned: docs.length,
|
|
409
|
+
degraded,
|
|
410
|
+
findings
|
|
411
|
+
};
|
|
392
412
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
413
|
+
var DEFAULT_IGNORED_DIRS, SOURCE_EXTENSIONS;
|
|
414
|
+
var init_detect = __esm(() => {
|
|
415
|
+
init_extract_refs();
|
|
416
|
+
init_git_adapter();
|
|
417
|
+
DEFAULT_IGNORED_DIRS = {
|
|
418
|
+
".git": true,
|
|
419
|
+
node_modules: true,
|
|
420
|
+
dist: true,
|
|
421
|
+
build: true,
|
|
422
|
+
coverage: true,
|
|
423
|
+
".next": true,
|
|
424
|
+
vendor: true
|
|
425
|
+
};
|
|
426
|
+
SOURCE_EXTENSIONS = {
|
|
427
|
+
".ts": true,
|
|
428
|
+
".tsx": true,
|
|
429
|
+
".js": true,
|
|
430
|
+
".jsx": true,
|
|
431
|
+
".mjs": true,
|
|
432
|
+
".cjs": true,
|
|
433
|
+
".rs": true,
|
|
434
|
+
".go": true,
|
|
435
|
+
".py": true,
|
|
436
|
+
".rb": true,
|
|
437
|
+
".java": true,
|
|
438
|
+
".kt": true,
|
|
439
|
+
".swift": true,
|
|
440
|
+
".c": true,
|
|
441
|
+
".cc": true,
|
|
442
|
+
".cpp": true,
|
|
443
|
+
".h": true,
|
|
444
|
+
".hpp": true,
|
|
445
|
+
".json": true,
|
|
446
|
+
".toml": true,
|
|
447
|
+
".yml": true,
|
|
448
|
+
".yaml": true
|
|
449
|
+
};
|
|
450
|
+
});
|
|
408
451
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
452
|
+
// packages/standard-plugin/src/drift/plugin.ts
|
|
453
|
+
var exports_plugin = {};
|
|
454
|
+
__export(exports_plugin, {
|
|
455
|
+
runDriftCli: () => runDriftCli,
|
|
456
|
+
runDocsDriftValidation: () => runDocsDriftValidation,
|
|
457
|
+
highConfidenceDriftFindings: () => highConfidenceDriftFindings,
|
|
458
|
+
executeDrift: () => executeDrift,
|
|
459
|
+
driftGateResult: () => driftGateResult,
|
|
460
|
+
createDocsDriftValidator: () => createDocsDriftValidator,
|
|
461
|
+
createDocsDriftRuntimeCliCommand: () => createDocsDriftRuntimeCliCommand,
|
|
462
|
+
createDocsDriftGateStage: () => createDocsDriftGateStage,
|
|
463
|
+
DOCS_DRIFT_VALIDATOR_ID: () => DOCS_DRIFT_VALIDATOR_ID,
|
|
464
|
+
DOCS_DRIFT_VALIDATOR: () => DOCS_DRIFT_VALIDATOR,
|
|
465
|
+
DOCS_DRIFT_STAGE_MUTATION: () => DOCS_DRIFT_STAGE_MUTATION,
|
|
466
|
+
DOCS_DRIFT_STAGE_ID: () => DOCS_DRIFT_STAGE_ID,
|
|
467
|
+
DOCS_DRIFT_RUNTIME_CLI_COMMAND: () => DOCS_DRIFT_RUNTIME_CLI_COMMAND,
|
|
468
|
+
DOCS_DRIFT_CLI_ID: () => DOCS_DRIFT_CLI_ID,
|
|
469
|
+
DOCS_DRIFT_CLI_COMMAND: () => DOCS_DRIFT_CLI_COMMAND,
|
|
470
|
+
DOCS_DRIFT_CAPABILITY_ID: () => DOCS_DRIFT_CAPABILITY_ID
|
|
471
|
+
});
|
|
472
|
+
function highConfidenceDriftFindings(report) {
|
|
473
|
+
return report.findings.filter((finding) => finding.confidence === "high");
|
|
420
474
|
}
|
|
421
|
-
function
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
if (projectString(record?.name)?.toLowerCase() !== "status")
|
|
426
|
-
continue;
|
|
427
|
-
const id = projectString(record?.id);
|
|
428
|
-
if (!id)
|
|
429
|
-
continue;
|
|
430
|
-
const options = Array.isArray(record?.options) ? record.options.flatMap((option) => {
|
|
431
|
-
const optionRecord = asProjectRecord(option);
|
|
432
|
-
const optionId = projectString(optionRecord?.id);
|
|
433
|
-
const name = projectString(optionRecord?.name);
|
|
434
|
-
return optionId && name ? [{ id: optionId, name }] : [];
|
|
435
|
-
}) : [];
|
|
436
|
-
return { id, name: "Status", options };
|
|
475
|
+
function driftGateResult(report, mode = "enforce") {
|
|
476
|
+
const high = highConfidenceDriftFindings(report);
|
|
477
|
+
if (mode === "enforce" && high.length > 0) {
|
|
478
|
+
return { kind: "block", reason: `${high.length} high-confidence documentation drift finding(s).` };
|
|
437
479
|
}
|
|
438
|
-
|
|
480
|
+
return { kind: "allow" };
|
|
439
481
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
`;
|
|
455
|
-
return projectStatusFieldFrom(await input.fetchGraphQL(query, { projectId: input.projectId }, input.token), input.projectId);
|
|
482
|
+
function createDocsDriftGateStage(options = {}) {
|
|
483
|
+
return async (ctx) => {
|
|
484
|
+
const projectRoot = typeof ctx.metadata?.projectRoot === "string" ? ctx.metadata.projectRoot : process.cwd();
|
|
485
|
+
const report = await detectDrift({
|
|
486
|
+
projectRoot,
|
|
487
|
+
...options.docsGlobs !== undefined ? { docsGlobs: options.docsGlobs } : {},
|
|
488
|
+
...options.ignoreGlobs !== undefined ? { ignoreGlobs: options.ignoreGlobs } : {}
|
|
489
|
+
});
|
|
490
|
+
return driftGateResult(report, options.failOnDrift ? "enforce" : "observe");
|
|
491
|
+
};
|
|
456
492
|
}
|
|
457
|
-
async function
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
const nodes = asProjectRecord(asProjectRecord(asProjectRecord(data)?.node)?.items)?.nodes;
|
|
469
|
-
for (const node of Array.isArray(nodes) ? nodes : []) {
|
|
470
|
-
const record = asProjectRecord(node);
|
|
471
|
-
const content = asProjectRecord(record?.content);
|
|
472
|
-
if (projectString(content?.id) === input.issueNodeId) {
|
|
473
|
-
const id2 = projectString(record?.id);
|
|
474
|
-
if (id2)
|
|
475
|
-
return { id: id2, created: false };
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
const mutation = `
|
|
479
|
-
mutation RigAddIssueToProject($projectId: ID!, $contentId: ID!) {
|
|
480
|
-
addProjectV2ItemById(input: { projectId: $projectId, contentId: $contentId }) { item { id } }
|
|
481
|
-
}
|
|
482
|
-
`;
|
|
483
|
-
const created = await input.fetchGraphQL(mutation, { projectId: input.projectId, contentId: input.issueNodeId }, input.token);
|
|
484
|
-
const addResult = asProjectRecord(asProjectRecord(created)?.addProjectV2ItemById);
|
|
485
|
-
const id = projectString(asProjectRecord(addResult?.item)?.id);
|
|
486
|
-
if (!id)
|
|
487
|
-
throw new Error("GitHub Project item creation did not return an item id.");
|
|
488
|
-
return { id, created: true };
|
|
493
|
+
async function runDocsDriftValidation(options) {
|
|
494
|
+
const report = await detectDrift(options);
|
|
495
|
+
const high = highConfidenceDriftFindings(report);
|
|
496
|
+
const passed = options.failOnDrift ? high.length === 0 : true;
|
|
497
|
+
const findingWord = report.findings.length === 1 ? "finding" : "findings";
|
|
498
|
+
return {
|
|
499
|
+
id: DOCS_DRIFT_VALIDATOR_ID,
|
|
500
|
+
passed,
|
|
501
|
+
summary: `docs drift scanned ${report.scanned} doc(s), ${report.findings.length} ${findingWord}`,
|
|
502
|
+
details: JSON.stringify(report)
|
|
503
|
+
};
|
|
489
504
|
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
505
|
+
function createDocsDriftValidator(options = {}) {
|
|
506
|
+
return {
|
|
507
|
+
...DOCS_DRIFT_VALIDATOR,
|
|
508
|
+
async run(ctx) {
|
|
509
|
+
return runDocsDriftValidation({
|
|
510
|
+
projectRoot: ctx.workspaceRoot,
|
|
511
|
+
...options.docsGlobs !== undefined ? { docsGlobs: options.docsGlobs } : {},
|
|
512
|
+
...options.ignoreGlobs !== undefined ? { ignoreGlobs: options.ignoreGlobs } : {},
|
|
513
|
+
...options.failOnDrift !== undefined ? { failOnDrift: options.failOnDrift } : {}
|
|
514
|
+
});
|
|
499
515
|
}
|
|
500
|
-
|
|
501
|
-
await input.fetchGraphQL(mutation, {
|
|
502
|
-
projectId: input.projectId,
|
|
503
|
-
itemId: input.itemId,
|
|
504
|
-
fieldId: input.fieldId,
|
|
505
|
-
optionId: input.optionId
|
|
506
|
-
}, input.token);
|
|
516
|
+
};
|
|
507
517
|
}
|
|
508
|
-
function
|
|
509
|
-
const
|
|
510
|
-
|
|
518
|
+
function takeOptionValue(args, index, flag) {
|
|
519
|
+
const value = args[index + 1];
|
|
520
|
+
if (!value)
|
|
521
|
+
throw new Error(`${flag} requires a value`);
|
|
522
|
+
return value;
|
|
511
523
|
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
if (!lifecycleStatus)
|
|
520
|
-
return;
|
|
521
|
-
const issueNodeId = fetchIssueNodeId(bin, repo, spawnFn, id, extraEnv, timeoutMs);
|
|
522
|
-
if (!issueNodeId)
|
|
523
|
-
throw new Error(`GitHub issue ${repo}#${id} did not expose a node id for Projects status sync.`);
|
|
524
|
-
const projectStatus = projectString(projects.statuses?.[lifecycleStatus]) ?? DEFAULT_PROJECT_STATUSES[lifecycleStatus];
|
|
525
|
-
const fetchGraphQL = ghGraphQLFetch(bin, spawnFn, extraEnv, timeoutMs);
|
|
526
|
-
const field = await resolveProjectStatusField({ projectId, token: "gh-cli", fetchGraphQL });
|
|
527
|
-
const option = field.options.find((candidate) => candidate.name.toLowerCase() === projectStatus.toLowerCase() || candidate.id === projectStatus);
|
|
528
|
-
if (!option)
|
|
529
|
-
throw new Error(`GitHub Project ${projectId} Status field does not contain option "${projectStatus}".`);
|
|
530
|
-
const item = await ensureIssueProjectItem({ projectId, issueNodeId, token: "gh-cli", fetchGraphQL });
|
|
531
|
-
await updateIssueProjectStatus({
|
|
532
|
-
projectId,
|
|
533
|
-
itemId: item.id,
|
|
534
|
-
fieldId: projectString(projects.statusFieldId) ?? field.id,
|
|
535
|
-
optionId: option.id,
|
|
536
|
-
token: "gh-cli",
|
|
537
|
-
fetchGraphQL
|
|
538
|
-
});
|
|
524
|
+
function takeFlag(args, flag) {
|
|
525
|
+
const rest = [...args];
|
|
526
|
+
const index = rest.indexOf(flag);
|
|
527
|
+
if (index < 0)
|
|
528
|
+
return { value: false, rest };
|
|
529
|
+
rest.splice(index, 1);
|
|
530
|
+
return { value: true, rest };
|
|
539
531
|
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
532
|
+
function takeOption(args, flag) {
|
|
533
|
+
const rest = [...args];
|
|
534
|
+
const index = rest.indexOf(flag);
|
|
535
|
+
if (index < 0)
|
|
536
|
+
return { rest };
|
|
537
|
+
const value = rest[index + 1];
|
|
538
|
+
if (!value || value.startsWith("-"))
|
|
539
|
+
throw new Error(`${flag} requires a value.`);
|
|
540
|
+
rest.splice(index, 2);
|
|
541
|
+
return { value, rest };
|
|
549
542
|
}
|
|
550
|
-
function
|
|
551
|
-
if (
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
return true;
|
|
555
|
-
return isTerminalTaskStatus(status);
|
|
543
|
+
function requireNoExtraArgs(args, usage) {
|
|
544
|
+
if (args.length > 0)
|
|
545
|
+
throw new Error(`Unexpected argument: ${args[0]}
|
|
546
|
+
Usage: ${usage}`);
|
|
556
547
|
}
|
|
557
|
-
function
|
|
558
|
-
return
|
|
548
|
+
function parseCsv(value) {
|
|
549
|
+
return value?.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0) ?? [];
|
|
559
550
|
}
|
|
560
|
-
function
|
|
561
|
-
|
|
551
|
+
function driftSummary(report) {
|
|
552
|
+
const highConfidence = highConfidenceDriftFindings(report).length;
|
|
553
|
+
return { total: report.findings.length, highConfidence, degraded: report.degraded };
|
|
562
554
|
}
|
|
563
|
-
function
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
return null;
|
|
588
|
-
default:
|
|
589
|
-
throw new Error(`unsupported status: ${status}`);
|
|
555
|
+
async function executeDrift(context, args, options = {}) {
|
|
556
|
+
const json = takeFlag(args, "--json");
|
|
557
|
+
const docs = takeOption(json.rest, "--docs");
|
|
558
|
+
const ignore = takeOption(docs.rest, "--ignore");
|
|
559
|
+
const failOnDrift = takeFlag(ignore.rest, "--fail-on-drift");
|
|
560
|
+
requireNoExtraArgs(failOnDrift.rest, "rig drift [--docs <csv>] [--ignore <csv>] [--fail-on-drift] [--json]");
|
|
561
|
+
const docsGlobs = parseCsv(docs.value);
|
|
562
|
+
const ignoreGlobs = parseCsv(ignore.value);
|
|
563
|
+
const effectiveDocsGlobs = docsGlobs.length > 0 ? docsGlobs : options.docsGlobs;
|
|
564
|
+
const effectiveIgnoreGlobs = ignoreGlobs.length > 0 ? ignoreGlobs : options.ignoreGlobs;
|
|
565
|
+
const effectiveFailOnDrift = failOnDrift.value || options.failOnDrift === true;
|
|
566
|
+
const report = await detectDrift({
|
|
567
|
+
projectRoot: context.projectRoot,
|
|
568
|
+
...effectiveDocsGlobs !== undefined ? { docsGlobs: effectiveDocsGlobs } : {},
|
|
569
|
+
...effectiveIgnoreGlobs !== undefined ? { ignoreGlobs: effectiveIgnoreGlobs } : {}
|
|
570
|
+
});
|
|
571
|
+
const failed = effectiveFailOnDrift && highConfidenceDriftFindings(report).length > 0;
|
|
572
|
+
const details = { report, summary: driftSummary(report), failOnDrift: effectiveFailOnDrift, failed };
|
|
573
|
+
if (context.outputMode === "text") {
|
|
574
|
+
if (json.value)
|
|
575
|
+
console.log(JSON.stringify(details, null, 2));
|
|
576
|
+
else
|
|
577
|
+
console.log(report.findings.length === 0 ? `No drift findings across ${report.scanned} documents.` : report.findings.map((finding) => `${finding.docPath}:${finding.line ?? "?"} ${finding.kind} ${finding.confidence} ${finding.detail}`).join(`
|
|
578
|
+
`));
|
|
590
579
|
}
|
|
580
|
+
return { ok: !failed, group: "drift", command: "scan", details };
|
|
591
581
|
}
|
|
592
|
-
function
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
case "ci_fixing":
|
|
603
|
-
return "rig:ci-fixing";
|
|
604
|
-
case "merging":
|
|
605
|
-
return "rig:merging";
|
|
606
|
-
case "needs_attention":
|
|
607
|
-
case "failed":
|
|
608
|
-
case "blocked":
|
|
609
|
-
return "rig:needs-attention";
|
|
610
|
-
case "ready":
|
|
611
|
-
case "cancelled":
|
|
612
|
-
case "open":
|
|
613
|
-
return null;
|
|
614
|
-
default:
|
|
615
|
-
return null;
|
|
616
|
-
}
|
|
582
|
+
function createDocsDriftRuntimeCliCommand(options = {}) {
|
|
583
|
+
return {
|
|
584
|
+
id: DOCS_DRIFT_CLI_ID,
|
|
585
|
+
family: "drift",
|
|
586
|
+
command: DOCS_DRIFT_CLI_COMMAND,
|
|
587
|
+
description: "Scan documentation for stale code references.",
|
|
588
|
+
usage: DOCS_DRIFT_CLI_COMMAND,
|
|
589
|
+
projectRequired: true,
|
|
590
|
+
run: (context, args) => executeDrift(context, args, options)
|
|
591
|
+
};
|
|
617
592
|
}
|
|
618
|
-
async function
|
|
619
|
-
const
|
|
620
|
-
const
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
593
|
+
async function runDriftCli(args, options = {}) {
|
|
594
|
+
const docsGlobs = [];
|
|
595
|
+
const ignoreGlobs = [];
|
|
596
|
+
let json = false;
|
|
597
|
+
let failOnDrift = false;
|
|
598
|
+
for (let index = 0;index < args.length; index += 1) {
|
|
599
|
+
const arg = args[index];
|
|
600
|
+
if (arg === "--json") {
|
|
601
|
+
json = true;
|
|
624
602
|
continue;
|
|
625
|
-
|
|
603
|
+
}
|
|
604
|
+
if (arg === "--fail-on-drift") {
|
|
605
|
+
failOnDrift = true;
|
|
626
606
|
continue;
|
|
627
|
-
try {
|
|
628
|
-
runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, "--remove-label", l], spawnFn, extraEnv, timeoutMs);
|
|
629
|
-
} catch {}
|
|
630
|
-
}
|
|
631
|
-
for (const label of [targetLabel, targetRigLabel].filter((value) => Boolean(value))) {
|
|
632
|
-
try {
|
|
633
|
-
runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, "--add-label", label], spawnFn, extraEnv, timeoutMs);
|
|
634
|
-
} catch (error) {
|
|
635
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
636
|
-
if (!/not found/i.test(message)) {
|
|
637
|
-
throw error;
|
|
638
|
-
}
|
|
639
|
-
ensureStatusLabel(bin, repo, spawnFn, label, extraEnv, timeoutMs);
|
|
640
|
-
runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, "--add-label", label], spawnFn, extraEnv, timeoutMs);
|
|
641
607
|
}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
upsertRigStickyComment(bin, repo, spawnFn, String(id), buildRigStickyStatusComment({
|
|
647
|
-
status: "running",
|
|
648
|
-
summary: "Rig run started."
|
|
649
|
-
}), extraEnv, timeoutMs);
|
|
608
|
+
if (arg === "--docs") {
|
|
609
|
+
docsGlobs.push(takeOptionValue(args, index, arg));
|
|
610
|
+
index += 1;
|
|
611
|
+
continue;
|
|
650
612
|
}
|
|
613
|
+
if (arg === "--ignore") {
|
|
614
|
+
ignoreGlobs.push(takeOptionValue(args, index, arg));
|
|
615
|
+
index += 1;
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
throw new Error(`Unknown rig drift argument: ${arg}`);
|
|
651
619
|
}
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
"
|
|
664
|
-
label,
|
|
665
|
-
"--repo",
|
|
666
|
-
repo,
|
|
667
|
-
"--color",
|
|
668
|
-
"6f42c1",
|
|
669
|
-
"--description",
|
|
670
|
-
"Task status managed by Rig"
|
|
671
|
-
], spawnFn, extraEnv, timeoutMs);
|
|
672
|
-
} catch (error) {
|
|
673
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
674
|
-
if (!/already exists/i.test(message)) {
|
|
675
|
-
throw error;
|
|
620
|
+
const report = await detectDrift({
|
|
621
|
+
projectRoot: options.projectRoot ?? process.cwd(),
|
|
622
|
+
...docsGlobs.length > 0 ? { docsGlobs } : {},
|
|
623
|
+
...ignoreGlobs.length > 0 ? { ignoreGlobs } : {}
|
|
624
|
+
});
|
|
625
|
+
const write = options.write ?? ((message) => console.log(message));
|
|
626
|
+
if (json) {
|
|
627
|
+
write(JSON.stringify(report));
|
|
628
|
+
} else {
|
|
629
|
+
write(`Scanned ${report.scanned} doc(s); ${report.findings.length} drift finding(s).`);
|
|
630
|
+
for (const finding of report.findings) {
|
|
631
|
+
write(`${finding.confidence.toUpperCase()} ${finding.kind} ${finding.docPath}${finding.line ? `:${finding.line}` : ""} ${finding.reference ?? ""} \u2014 ${finding.detail}`);
|
|
676
632
|
}
|
|
677
633
|
}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
if (existing) {
|
|
683
|
-
runGhVoid(bin, ["api", "-X", "PATCH", `repos/${repo}/issues/comments/${existing.id}`, "-f", `body=${body}`], spawnFn, extraEnv, timeoutMs);
|
|
684
|
-
return;
|
|
634
|
+
const high = highConfidenceDriftFindings(report);
|
|
635
|
+
if (failOnDrift && high.length > 0) {
|
|
636
|
+
options.writeError?.(`${high.length} high-confidence drift finding(s).`);
|
|
637
|
+
return 2;
|
|
685
638
|
}
|
|
686
|
-
|
|
687
|
-
}
|
|
688
|
-
function notifyTaskChanged(onTaskChanged, repo, id, status) {
|
|
689
|
-
onTaskChanged?.({ repo, id, ...status ? { status } : {}, reason: "github-issue-updated" });
|
|
639
|
+
return 0;
|
|
690
640
|
}
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
641
|
+
var DOCS_DRIFT_RUNTIME_CLI_COMMAND;
|
|
642
|
+
var init_plugin = __esm(() => {
|
|
643
|
+
init_detect();
|
|
644
|
+
init_metadata();
|
|
645
|
+
init_metadata();
|
|
646
|
+
DOCS_DRIFT_RUNTIME_CLI_COMMAND = createDocsDriftRuntimeCliCommand();
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
// packages/standard-plugin/src/plugin.ts
|
|
650
|
+
import { resolve as resolve5 } from "path";
|
|
651
|
+
import { definePlugin } from "@rig/core/config";
|
|
652
|
+
|
|
653
|
+
// packages/standard-plugin/src/github-issues-source.ts
|
|
654
|
+
import { spawnSync } from "child_process";
|
|
655
|
+
import { existsSync, readFileSync } from "fs";
|
|
656
|
+
import { resolve } from "path";
|
|
657
|
+
function cleanToken(value) {
|
|
658
|
+
const trimmed = value?.trim() ?? "";
|
|
659
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
702
660
|
}
|
|
703
|
-
function
|
|
704
|
-
for (const rawLabel of labels) {
|
|
705
|
-
const label = rawLabel.trim();
|
|
706
|
-
if (!label)
|
|
707
|
-
continue;
|
|
708
|
-
if (action === "--add-label") {
|
|
709
|
-
try {
|
|
710
|
-
runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, action, label], spawnFn, extraEnv, timeoutMs);
|
|
711
|
-
} catch (error) {
|
|
712
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
713
|
-
if (!/not found/i.test(message))
|
|
714
|
-
throw error;
|
|
715
|
-
ensureStatusLabel(bin, repo, spawnFn, label, extraEnv, timeoutMs);
|
|
716
|
-
runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, action, label], spawnFn, extraEnv, timeoutMs);
|
|
717
|
-
}
|
|
718
|
-
continue;
|
|
719
|
-
}
|
|
720
|
-
try {
|
|
721
|
-
runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, action, label], spawnFn, extraEnv, timeoutMs);
|
|
722
|
-
} catch {}
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
async function applyIssueUpdate(bin, repo, spawnFn, id, update, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs) {
|
|
726
|
-
if (update.status) {
|
|
727
|
-
await applyIssueStatus(bin, repo, spawnFn, id, update.status, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs);
|
|
728
|
-
}
|
|
729
|
-
if (update.comment?.trim() && shouldWriteIssueUpdate(issueUpdates, update.status)) {
|
|
730
|
-
if (isRigStickyStatusComment(update.comment)) {
|
|
731
|
-
upsertRigStickyComment(bin, repo, spawnFn, String(id), update.comment, extraEnv, timeoutMs);
|
|
732
|
-
} else {
|
|
733
|
-
runGhVoid(bin, ["issue", "comment", String(id), "--repo", repo, "--body", update.comment], spawnFn, extraEnv, timeoutMs);
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
const editArgs = ["issue", "edit", String(id), "--repo", repo];
|
|
737
|
-
if (update.title?.trim()) {
|
|
738
|
-
editArgs.push("--title", update.title.trim());
|
|
739
|
-
}
|
|
740
|
-
const nextBody = update.metadata ? updateRigOwnedMetadataBlock(update.body ?? fetchIssueBody(bin, repo, spawnFn, id, extraEnv, timeoutMs) ?? "", update.metadata) : update.body;
|
|
741
|
-
if (nextBody !== undefined) {
|
|
742
|
-
editArgs.push("--body", nextBody);
|
|
743
|
-
}
|
|
744
|
-
if (editArgs.length > 5) {
|
|
745
|
-
runGhVoid(bin, editArgs, spawnFn, extraEnv, timeoutMs);
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
function createGitHubIssuesTaskSource(opts) {
|
|
749
|
-
const bin = opts.ghBinary ?? "gh";
|
|
750
|
-
const state = opts.state ?? "open";
|
|
751
|
-
const repo = `${opts.owner}/${opts.repo}`;
|
|
752
|
-
const spawnFn = opts.spawn ?? spawnSync;
|
|
753
|
-
const timeoutMs = Math.max(1000, Math.trunc(opts.timeoutMs ?? DEFAULT_GH_TIMEOUT_MS));
|
|
754
|
-
const listLimit = Math.max(1, Math.trunc(opts.listLimit ?? DEFAULT_GITHUB_ISSUE_LIST_LIMIT));
|
|
755
|
-
const issueUpdates = issueUpdatesMode(opts.issueUpdates);
|
|
756
|
-
async function issueToTaskWithOptionalNativeDependencies(issue, env) {
|
|
757
|
-
if (!opts.useNativeDependencies)
|
|
758
|
-
return issueToTask(issue, repo);
|
|
759
|
-
const nativeDependencies = await readNativeDependenciesForIssue({
|
|
760
|
-
issue,
|
|
761
|
-
repo,
|
|
762
|
-
fetchGraphQL: ghGraphQLFetch(bin, spawnFn, env, timeoutMs)
|
|
763
|
-
});
|
|
764
|
-
return issueToTask(issue, repo, nativeDependencies);
|
|
765
|
-
}
|
|
661
|
+
function createEnvGitHubCredentialProvider() {
|
|
766
662
|
return {
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
const labelArg = opts.labels && opts.labels.length > 0 ? ["--label", opts.labels.join(",")] : [];
|
|
771
|
-
const assigneeArg = opts.assignee?.trim() ? ["--assignee", opts.assignee.trim()] : [];
|
|
772
|
-
const args = [
|
|
773
|
-
"issue",
|
|
774
|
-
"list",
|
|
775
|
-
"--repo",
|
|
776
|
-
repo,
|
|
777
|
-
...labelArg,
|
|
778
|
-
...assigneeArg,
|
|
779
|
-
"--state",
|
|
780
|
-
state,
|
|
781
|
-
"--limit",
|
|
782
|
-
String(listLimit),
|
|
783
|
-
"--json",
|
|
784
|
-
"number,title,body,labels,state,url,assignees,id"
|
|
785
|
-
];
|
|
786
|
-
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
787
|
-
const rawIssues = runGh(bin, args, spawnFn, env, timeoutMs);
|
|
788
|
-
if (rawIssues.length >= listLimit) {
|
|
789
|
-
throw new Error(`GitHub issue list for ${repo} reached the configured limit (${listLimit}); refusing to silently truncate matching issues. Increase taskSource.options.listLimit or narrow labels/state/assignee.`);
|
|
663
|
+
async resolveGitHubToken(input) {
|
|
664
|
+
if (input.purpose === "selected-repo") {
|
|
665
|
+
return { token: cleanToken(process.env.RIG_GITHUB_SELECTED_TOKEN ?? process.env.RIG_GITHUB_TOKEN ?? null) ?? "", source: "signed-in-user" };
|
|
790
666
|
}
|
|
791
|
-
const
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
async get(id) {
|
|
795
|
-
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
796
|
-
let issue;
|
|
797
|
-
try {
|
|
798
|
-
issue = runGh(bin, [
|
|
799
|
-
"issue",
|
|
800
|
-
"view",
|
|
801
|
-
String(id),
|
|
802
|
-
"--repo",
|
|
803
|
-
repo,
|
|
804
|
-
"--json",
|
|
805
|
-
"number,title,body,labels,state,url,assignees,id"
|
|
806
|
-
], spawnFn, env, timeoutMs);
|
|
807
|
-
} catch (error) {
|
|
808
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
809
|
-
if (/could not resolve to (an? )?(issue|pullrequest)|no issues? (found|matched)|404 not found|gh: not found|gh issue view\b[\s\S]*failed \(exit \d+\): not found\b/i.test(detail)) {
|
|
810
|
-
return;
|
|
811
|
-
}
|
|
812
|
-
throw new Error(`Failed to read task ${id} from GitHub repo ${repo}: ${detail}`);
|
|
667
|
+
const token = cleanToken(process.env.RIG_GITHUB_TOKEN ?? process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? null);
|
|
668
|
+
if (!token) {
|
|
669
|
+
throw new Error("No host GitHub token is configured for admin fallback.");
|
|
813
670
|
}
|
|
814
|
-
return
|
|
815
|
-
},
|
|
816
|
-
async updateStatus(id, status) {
|
|
817
|
-
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
818
|
-
await applyIssueStatus(bin, repo, spawnFn, id, status, opts.projects, opts.assignee, issueUpdates, env, timeoutMs);
|
|
819
|
-
notifyTaskChanged(opts.onTaskChanged, repo, id, status);
|
|
820
|
-
},
|
|
821
|
-
async updateTask(id, update) {
|
|
822
|
-
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
823
|
-
await applyIssueUpdate(bin, repo, spawnFn, id, update, opts.projects, opts.assignee, issueUpdates, env, timeoutMs);
|
|
824
|
-
notifyTaskChanged(opts.onTaskChanged, repo, id, update.status);
|
|
825
|
-
},
|
|
826
|
-
async addLabels(id, labels) {
|
|
827
|
-
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
828
|
-
applyLabels(bin, repo, spawnFn, id, labels, "--add-label", env, timeoutMs);
|
|
829
|
-
notifyTaskChanged(opts.onTaskChanged, repo, id);
|
|
830
|
-
},
|
|
831
|
-
async removeLabels(id, labels) {
|
|
832
|
-
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
833
|
-
applyLabels(bin, repo, spawnFn, id, labels, "--remove-label", env, timeoutMs);
|
|
834
|
-
notifyTaskChanged(opts.onTaskChanged, repo, id);
|
|
835
|
-
},
|
|
836
|
-
async createIssue(input) {
|
|
837
|
-
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
838
|
-
const body = input.body ?? "";
|
|
839
|
-
const args = [
|
|
840
|
-
"api",
|
|
841
|
-
"-X",
|
|
842
|
-
"POST",
|
|
843
|
-
`repos/${repo}/issues`,
|
|
844
|
-
"-f",
|
|
845
|
-
`title=${input.title}`,
|
|
846
|
-
"-f",
|
|
847
|
-
`body=${body}`,
|
|
848
|
-
...(input.labels ?? []).flatMap((label) => ["-f", `labels[]=${label}`])
|
|
849
|
-
];
|
|
850
|
-
const issue = runGh(bin, args, spawnFn, env, timeoutMs);
|
|
851
|
-
notifyTaskChanged(opts.onTaskChanged, repo, String(issue.number));
|
|
852
|
-
return issueToTask({ ...issue, body: issue.body ?? body }, repo);
|
|
853
|
-
},
|
|
854
|
-
async create(input) {
|
|
855
|
-
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
856
|
-
const body = bodyForCreatedTask(input);
|
|
857
|
-
const args = [
|
|
858
|
-
"api",
|
|
859
|
-
"-X",
|
|
860
|
-
"POST",
|
|
861
|
-
`repos/${repo}/issues`,
|
|
862
|
-
"-f",
|
|
863
|
-
`title=${input.title}`,
|
|
864
|
-
"-f",
|
|
865
|
-
`body=${body}`,
|
|
866
|
-
"-f",
|
|
867
|
-
"labels[]=rig:generated"
|
|
868
|
-
];
|
|
869
|
-
const issue = runGh(bin, args, spawnFn, env, timeoutMs);
|
|
870
|
-
notifyTaskChanged(opts.onTaskChanged, repo, String(issue.number));
|
|
871
|
-
return issueToTask({ ...issue, body: issue.body ?? body }, repo);
|
|
872
|
-
},
|
|
873
|
-
async getIssueBody(id) {
|
|
874
|
-
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
875
|
-
return fetchIssueBody(bin, repo, spawnFn, id, env, timeoutMs);
|
|
671
|
+
return { token, source: "host-admin-fallback" };
|
|
876
672
|
}
|
|
877
673
|
};
|
|
878
674
|
}
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
const labels = Array.isArray(raw.labels) ? raw.labels.filter((label) => typeof label === "string") : [];
|
|
888
|
-
const scope = labels.filter((label) => label.startsWith("scope:")).map((label) => label.slice("scope:".length));
|
|
889
|
-
const validators = labels.filter((label) => label.startsWith("validator:")).map((label) => label.slice("validator:".length));
|
|
890
|
-
const roleLabel = labels.find((label) => label.startsWith("role:"));
|
|
891
|
-
return {
|
|
892
|
-
id: raw["id"] ?? inferredId,
|
|
893
|
-
deps: raw["deps"] ?? raw["depends_on"] ?? [],
|
|
894
|
-
status: raw["status"] ?? "ready",
|
|
895
|
-
...scope.length > 0 ? { scope } : {},
|
|
896
|
-
...roleLabel ? { role: roleLabel.slice("role:".length) } : {},
|
|
897
|
-
...validators.length > 0 ? { validators, validation: validators } : {},
|
|
898
|
-
...raw
|
|
675
|
+
function createStateGitHubCredentialProvider(options = {}) {
|
|
676
|
+
const addCandidate = (candidates, path) => {
|
|
677
|
+
const trimmed = path?.trim();
|
|
678
|
+
if (!trimmed)
|
|
679
|
+
return;
|
|
680
|
+
const resolved = resolve(trimmed);
|
|
681
|
+
if (!candidates.includes(resolved))
|
|
682
|
+
candidates.push(resolved);
|
|
899
683
|
};
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
const configured = opts.path ?? opts.dir;
|
|
904
|
-
if (!configured) {
|
|
905
|
-
throw new Error("createFilesTaskSource: either `path` or `dir` must be provided");
|
|
906
|
-
}
|
|
907
|
-
const directory = isAbsolute(configured) ? configured : resolve2(opts.projectRoot ?? process.cwd(), configured);
|
|
908
|
-
const findTaskFile = (id) => {
|
|
909
|
-
if (!existsSync2(directory))
|
|
684
|
+
const addStateDir = (candidates, dir) => {
|
|
685
|
+
const trimmed = dir?.trim();
|
|
686
|
+
if (!trimmed)
|
|
910
687
|
return;
|
|
911
|
-
|
|
912
|
-
|
|
688
|
+
addCandidate(candidates, resolve(trimmed, "github-auth.json"));
|
|
689
|
+
};
|
|
690
|
+
const addProjectStateDir = (candidates, root) => {
|
|
691
|
+
const trimmed = root?.trim();
|
|
692
|
+
if (!trimmed)
|
|
693
|
+
return;
|
|
694
|
+
addStateDir(candidates, resolve(trimmed, ".rig", "state"));
|
|
695
|
+
};
|
|
696
|
+
const stateFileCandidates = () => {
|
|
697
|
+
const candidates = [];
|
|
698
|
+
addCandidate(candidates, options.stateFile ?? process.env.RIG_GITHUB_AUTH_STATE_FILE);
|
|
699
|
+
addStateDir(candidates, options.stateDir);
|
|
700
|
+
addProjectStateDir(candidates, process.env.PROJECT_RIG_ROOT);
|
|
701
|
+
addProjectStateDir(candidates, process.env.RIG_PROJECT_ROOT);
|
|
702
|
+
addProjectStateDir(candidates, process.env.RIG_HOST_PROJECT_ROOT);
|
|
703
|
+
addProjectStateDir(candidates, process.cwd());
|
|
704
|
+
addStateDir(candidates, process.env.RIG_STATE_DIR);
|
|
705
|
+
return candidates;
|
|
706
|
+
};
|
|
707
|
+
const readToken = () => {
|
|
708
|
+
for (const stateFile of stateFileCandidates()) {
|
|
709
|
+
if (!existsSync(stateFile))
|
|
913
710
|
continue;
|
|
914
|
-
const p = join(directory, name);
|
|
915
711
|
try {
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
if (
|
|
919
|
-
return
|
|
712
|
+
const parsed = JSON.parse(readFileSync(stateFile, "utf8"));
|
|
713
|
+
const token = typeof parsed.token === "string" ? cleanToken(parsed.token) : null;
|
|
714
|
+
if (token)
|
|
715
|
+
return token;
|
|
920
716
|
} catch {}
|
|
921
717
|
}
|
|
922
|
-
return;
|
|
923
|
-
};
|
|
924
|
-
const applyUpdate = (id, update) => {
|
|
925
|
-
const file = findTaskFile(id);
|
|
926
|
-
if (!file) {
|
|
927
|
-
throw new Error(`files task not found: ${id}`);
|
|
928
|
-
}
|
|
929
|
-
const raw = JSON.parse(readFileSync2(file, "utf-8"));
|
|
930
|
-
if (update.status)
|
|
931
|
-
raw.status = update.status;
|
|
932
|
-
if (update.title !== undefined)
|
|
933
|
-
raw.title = update.title;
|
|
934
|
-
if (update.body !== undefined)
|
|
935
|
-
raw.body = update.body;
|
|
936
|
-
if (update.comment?.trim()) {
|
|
937
|
-
const existing = Array.isArray(raw.comments) ? raw.comments : [];
|
|
938
|
-
raw.comments = [
|
|
939
|
-
...existing,
|
|
940
|
-
{
|
|
941
|
-
body: update.comment,
|
|
942
|
-
createdAt: new Date().toISOString(),
|
|
943
|
-
source: "rig"
|
|
944
|
-
}
|
|
945
|
-
];
|
|
946
|
-
}
|
|
947
|
-
writeFileSync(file, `${JSON.stringify(raw, null, 2)}
|
|
948
|
-
`, "utf-8");
|
|
718
|
+
return null;
|
|
949
719
|
};
|
|
950
720
|
return {
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
return [];
|
|
956
|
-
const out = [];
|
|
957
|
-
for (const name of readdirSync(directory)) {
|
|
958
|
-
if (!pattern.test(name))
|
|
959
|
-
continue;
|
|
960
|
-
const p = join(directory, name);
|
|
961
|
-
try {
|
|
962
|
-
if (!statSync(p).isFile())
|
|
963
|
-
continue;
|
|
964
|
-
out.push(readTaskFile(p, pattern));
|
|
965
|
-
} catch (err) {
|
|
966
|
-
console.warn(`[files-source] skipped ${name}: ${err.message}`);
|
|
967
|
-
}
|
|
721
|
+
async resolveGitHubToken(input) {
|
|
722
|
+
const token = readToken();
|
|
723
|
+
if (input.purpose === "selected-repo") {
|
|
724
|
+
return { token: token ?? cleanToken(process.env.RIG_GITHUB_SELECTED_TOKEN ?? process.env.RIG_GITHUB_TOKEN ?? null) ?? "", source: "signed-in-user" };
|
|
968
725
|
}
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
const
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
},
|
|
978
|
-
async updateTask(id, update) {
|
|
979
|
-
applyUpdate(id, update);
|
|
726
|
+
if (token) {
|
|
727
|
+
return { token, source: "signed-in-user" };
|
|
728
|
+
}
|
|
729
|
+
const fallback = cleanToken(process.env.RIG_GITHUB_TOKEN ?? process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? null);
|
|
730
|
+
if (!fallback) {
|
|
731
|
+
throw new Error("No signed-in GitHub token is stored for Rig and no host admin fallback token is configured.");
|
|
732
|
+
}
|
|
733
|
+
return { token: fallback, source: "host-admin-fallback" };
|
|
980
734
|
}
|
|
981
735
|
};
|
|
982
736
|
}
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
return
|
|
1007
|
-
|
|
1008
|
-
}
|
|
1009
|
-
function normalizeToken(raw) {
|
|
1010
|
-
return raw.trim().replace(/^['"]|['"]$/g, "").replace(/[),.;:]+$/g, "").replace(/#L\d+(?:-L\d+)?$/i, "");
|
|
737
|
+
var STATUS_LABELS = new Set(["ready", "blocked", "in-progress", "under-review", "failed", "cancelled"]);
|
|
738
|
+
var RIG_STATUS_COMMENT_MARKER = "<!-- rig:status-comment -->";
|
|
739
|
+
var RIG_METADATA_START = "<!-- rig:metadata:start -->";
|
|
740
|
+
var RIG_METADATA_END = "<!-- rig:metadata:end -->";
|
|
741
|
+
var RIG_STATUS_LABELS = new Set(["rig:running", "rig:pr-open", "rig:ci-fixing", "rig:merging", "rig:done", "rig:needs-attention"]);
|
|
742
|
+
var DEFAULT_GH_TIMEOUT_MS = 15000;
|
|
743
|
+
var DEFAULT_GITHUB_ISSUE_LIST_LIMIT = 1000;
|
|
744
|
+
function statusFor(issue) {
|
|
745
|
+
const state = (issue.state ?? "").toUpperCase();
|
|
746
|
+
if (state === "CLOSED")
|
|
747
|
+
return "closed";
|
|
748
|
+
const labelNames = labelNamesFor(issue);
|
|
749
|
+
if (labelNames.includes("in-progress"))
|
|
750
|
+
return "in_progress";
|
|
751
|
+
if (labelNames.includes("blocked"))
|
|
752
|
+
return "blocked";
|
|
753
|
+
if (labelNames.includes("ready"))
|
|
754
|
+
return "ready";
|
|
755
|
+
if (labelNames.includes("under-review"))
|
|
756
|
+
return "under_review";
|
|
757
|
+
if (labelNames.includes("failed"))
|
|
758
|
+
return "failed";
|
|
759
|
+
if (labelNames.includes("cancelled"))
|
|
760
|
+
return "cancelled";
|
|
761
|
+
return "open";
|
|
1011
762
|
}
|
|
1012
|
-
function
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
if (PATH_REF.test(raw))
|
|
1016
|
-
return "path";
|
|
1017
|
-
if (SYMBOL_REF.test(raw))
|
|
1018
|
-
return "symbol";
|
|
1019
|
-
return null;
|
|
763
|
+
function parseIssueRefs(raw) {
|
|
764
|
+
const refs = [...raw.matchAll(/(?:^|[^\w/.-])(?:[\w.-]+\/[\w.-]+#|#)?(\d+)\b/g)].map((match) => match[1]).filter((value) => Boolean(value));
|
|
765
|
+
return [...new Set(refs)];
|
|
1020
766
|
}
|
|
1021
|
-
function
|
|
1022
|
-
|
|
1023
|
-
if (!value)
|
|
1024
|
-
return;
|
|
1025
|
-
const kind = classifyReference(value);
|
|
1026
|
-
if (!kind)
|
|
1027
|
-
return;
|
|
1028
|
-
const key = `${kind}:${value}:${line}`;
|
|
1029
|
-
if (seen.has(key))
|
|
1030
|
-
return;
|
|
1031
|
-
seen.add(key);
|
|
1032
|
-
refs.push({ kind, value, line });
|
|
767
|
+
function metadataKeyPattern(keys) {
|
|
768
|
+
return new RegExp(`^(?:${keys.map((key) => key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")}):\\s*(.*)$`, "i");
|
|
1033
769
|
}
|
|
1034
|
-
function
|
|
1035
|
-
const
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
770
|
+
function parseMetadataList(body, keys) {
|
|
771
|
+
const block = body.match(/<!-- rig:metadata:start -->\s*([\s\S]*?)\s*<!-- rig:metadata:end -->/);
|
|
772
|
+
if (!block)
|
|
773
|
+
return [];
|
|
774
|
+
const lines = block[1].split(/\r?\n/);
|
|
775
|
+
const values = [];
|
|
776
|
+
const keyPattern = metadataKeyPattern(keys);
|
|
777
|
+
for (let index = 0;index < lines.length; index += 1) {
|
|
778
|
+
const line = lines[index];
|
|
779
|
+
const sameLine = line.match(keyPattern);
|
|
780
|
+
if (!sameLine)
|
|
781
|
+
continue;
|
|
782
|
+
const inlineValue = sameLine[1]?.trim() ?? "";
|
|
783
|
+
if (inlineValue) {
|
|
784
|
+
values.push(...parseIssueRefs(inlineValue));
|
|
785
|
+
continue;
|
|
1042
786
|
}
|
|
1043
|
-
for (
|
|
1044
|
-
|
|
787
|
+
for (let cursor = index + 1;cursor < lines.length; cursor += 1) {
|
|
788
|
+
const item = lines[cursor].match(/^\s*-\s*(.+)$/);
|
|
789
|
+
if (!item)
|
|
790
|
+
break;
|
|
791
|
+
values.push(...parseIssueRefs(item[1]));
|
|
1045
792
|
}
|
|
1046
793
|
}
|
|
1047
|
-
return
|
|
794
|
+
return [...new Set(values)];
|
|
1048
795
|
}
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
return
|
|
796
|
+
function parseBodyKeyRefs(body, keys) {
|
|
797
|
+
const keyPattern = metadataKeyPattern(keys);
|
|
798
|
+
const values = body.split(/\r?\n/).flatMap((line) => {
|
|
799
|
+
const match = line.match(keyPattern);
|
|
800
|
+
return match?.[1] ? parseIssueRefs(match[1]) : [];
|
|
801
|
+
});
|
|
802
|
+
return [...new Set(values)];
|
|
1056
803
|
}
|
|
1057
|
-
function
|
|
1058
|
-
const
|
|
1059
|
-
return
|
|
804
|
+
function parseDeps(body) {
|
|
805
|
+
const keys = ["depends-on", "deps", "blocked-by", "blocked_by"];
|
|
806
|
+
return [...new Set([...parseBodyKeyRefs(body, keys), ...parseMetadataList(body, keys)])];
|
|
1060
807
|
}
|
|
1061
|
-
function
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
808
|
+
function parseParents(body) {
|
|
809
|
+
const keys = ["parents", "parent"];
|
|
810
|
+
return [...new Set([...parseBodyKeyRefs(body, keys), ...parseMetadataList(body, keys)])];
|
|
811
|
+
}
|
|
812
|
+
function issueTypeFor(issue) {
|
|
813
|
+
const labels = labelNamesFor(issue);
|
|
814
|
+
const typed = labels.find((l) => l.startsWith("type:"));
|
|
815
|
+
if (typed)
|
|
816
|
+
return typed.slice("type:".length);
|
|
817
|
+
if (labels.includes("epic"))
|
|
818
|
+
return "epic";
|
|
819
|
+
return "task";
|
|
820
|
+
}
|
|
821
|
+
function issueToTask(issue, repo, nativeDependencies) {
|
|
822
|
+
const labelNames = labelNamesFor(issue);
|
|
823
|
+
const scope = labelNames.filter((l) => l.startsWith("scope:")).map((l) => l.slice("scope:".length));
|
|
824
|
+
const roleLabel = labelNames.find((l) => l.startsWith("role:"));
|
|
825
|
+
const role = roleLabel ? roleLabel.slice("role:".length) : undefined;
|
|
826
|
+
const validators = labelNames.filter((l) => l.startsWith("validator:")).map((l) => l.slice("validator:".length));
|
|
827
|
+
const body = issue.body ?? "";
|
|
828
|
+
const issueNodeId = issue.id ?? issue.nodeId ?? issue.node_id;
|
|
829
|
+
const parsedDeps = parseDeps(body);
|
|
830
|
+
const deps = nativeDependencies?.deps ? [...new Set([...parsedDeps, ...nativeDependencies.deps])] : parsedDeps;
|
|
1080
831
|
return {
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
const match = line.match(/^R\d*\s+(.+?)\s+(.+)$/);
|
|
1098
|
-
return Boolean(match && (match[1] === symbolOrPath || match[2] === symbolOrPath));
|
|
1099
|
-
});
|
|
1100
|
-
} catch (error) {
|
|
1101
|
-
const detail = processError(error);
|
|
1102
|
-
if (detail?.code === 128)
|
|
1103
|
-
return false;
|
|
1104
|
-
throw error;
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
832
|
+
id: String(issue.number),
|
|
833
|
+
...typeof issueNodeId === "string" && issueNodeId.trim() ? { issueNodeId: issueNodeId.trim() } : {},
|
|
834
|
+
deps,
|
|
835
|
+
status: statusFor(issue),
|
|
836
|
+
title: issue.title,
|
|
837
|
+
body,
|
|
838
|
+
scope,
|
|
839
|
+
role,
|
|
840
|
+
validators,
|
|
841
|
+
url: issue.url ?? issue.html_url,
|
|
842
|
+
issueType: issueTypeFor(issue),
|
|
843
|
+
sourceIssueId: `${repo}#${issue.number}`,
|
|
844
|
+
parentChildDeps: parseParents(body),
|
|
845
|
+
labels: labelNames,
|
|
846
|
+
...nativeDependencies?.degraded ? { nativeDependenciesDegraded: true, nativeDependenciesError: nativeDependencies.degraded } : {},
|
|
847
|
+
raw: issue
|
|
1107
848
|
};
|
|
1108
849
|
}
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
".ts": true,
|
|
1122
|
-
".tsx": true,
|
|
1123
|
-
".js": true,
|
|
1124
|
-
".jsx": true,
|
|
1125
|
-
".mjs": true,
|
|
1126
|
-
".cjs": true,
|
|
1127
|
-
".rs": true,
|
|
1128
|
-
".go": true,
|
|
1129
|
-
".py": true,
|
|
1130
|
-
".rb": true,
|
|
1131
|
-
".java": true,
|
|
1132
|
-
".kt": true,
|
|
1133
|
-
".swift": true,
|
|
1134
|
-
".c": true,
|
|
1135
|
-
".cc": true,
|
|
1136
|
-
".cpp": true,
|
|
1137
|
-
".h": true,
|
|
1138
|
-
".hpp": true,
|
|
1139
|
-
".json": true,
|
|
1140
|
-
".toml": true,
|
|
1141
|
-
".yml": true,
|
|
1142
|
-
".yaml": true
|
|
1143
|
-
};
|
|
1144
|
-
function globLikeMatch(path, pattern) {
|
|
1145
|
-
if (pattern === path)
|
|
1146
|
-
return true;
|
|
1147
|
-
if (pattern.startsWith("**/*"))
|
|
1148
|
-
return path.endsWith(pattern.slice(4));
|
|
1149
|
-
if (pattern.endsWith("/**"))
|
|
1150
|
-
return path.startsWith(pattern.slice(0, -3));
|
|
1151
|
-
if (pattern.includes("*")) {
|
|
1152
|
-
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
1153
|
-
return new RegExp(`^${escaped}$`).test(path);
|
|
850
|
+
function labelNamesFor(issue) {
|
|
851
|
+
return (issue.labels ?? []).flatMap((label) => {
|
|
852
|
+
if (typeof label === "string")
|
|
853
|
+
return [label];
|
|
854
|
+
return typeof label.name === "string" ? [label.name] : [];
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
function yamlScalar(value) {
|
|
858
|
+
if (Array.isArray(value)) {
|
|
859
|
+
return value.length === 0 ? "[]" : `
|
|
860
|
+
${value.map((entry) => ` - ${String(entry)}`).join(`
|
|
861
|
+
`)}`;
|
|
1154
862
|
}
|
|
1155
|
-
|
|
863
|
+
if (value && typeof value === "object")
|
|
864
|
+
return JSON.stringify(value);
|
|
865
|
+
return String(value);
|
|
1156
866
|
}
|
|
1157
|
-
function
|
|
1158
|
-
const
|
|
1159
|
-
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
|
|
867
|
+
function updateRigOwnedMetadataBlock(body, metadata) {
|
|
868
|
+
const rendered = [
|
|
869
|
+
RIG_METADATA_START,
|
|
870
|
+
...Object.entries(metadata).map(([key, value]) => Array.isArray(value) ? `${key}:${yamlScalar(value)}` : `${key}: ${yamlScalar(value)}`),
|
|
871
|
+
RIG_METADATA_END
|
|
872
|
+
].join(`
|
|
873
|
+
`);
|
|
874
|
+
const pattern = new RegExp(`${RIG_METADATA_START.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*[\\s\\S]*?\\s*${RIG_METADATA_END.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`);
|
|
875
|
+
if (pattern.test(body))
|
|
876
|
+
return body.replace(pattern, rendered);
|
|
877
|
+
return body.trim().length > 0 ? `${body.trimEnd()}
|
|
878
|
+
|
|
879
|
+
${rendered}
|
|
880
|
+
` : `${rendered}
|
|
881
|
+
`;
|
|
1163
882
|
}
|
|
1164
|
-
|
|
1165
|
-
const
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
continue;
|
|
1180
|
-
if (options.docs) {
|
|
1181
|
-
const matchesConfigured = options.patterns && options.patterns.length > 0 ? options.patterns.some((pattern) => globLikeMatch(rel, pattern)) : isDefaultDoc(rel);
|
|
1182
|
-
if (matchesConfigured)
|
|
1183
|
-
files.push(rel);
|
|
1184
|
-
continue;
|
|
1185
|
-
}
|
|
1186
|
-
if (SOURCE_EXTENSIONS[extname(entry.name)])
|
|
1187
|
-
files.push(rel);
|
|
1188
|
-
}
|
|
1189
|
-
}
|
|
1190
|
-
await visit(root);
|
|
1191
|
-
return files.sort();
|
|
883
|
+
function buildRigStickyStatusComment(input) {
|
|
884
|
+
const lines = [
|
|
885
|
+
RIG_STATUS_COMMENT_MARKER,
|
|
886
|
+
`### Rig status: ${input.status}`,
|
|
887
|
+
"",
|
|
888
|
+
input.summary
|
|
889
|
+
];
|
|
890
|
+
if (input.runId)
|
|
891
|
+
lines.push("", `- Run: ${input.runId}`);
|
|
892
|
+
if (input.prUrl)
|
|
893
|
+
lines.push(`- PR: ${input.prUrl}`);
|
|
894
|
+
for (const detail of input.details ?? [])
|
|
895
|
+
lines.push(`- ${detail}`);
|
|
896
|
+
return lines.join(`
|
|
897
|
+
`);
|
|
1192
898
|
}
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
return existsSync3(resolve3(projectRoot, reference.value)) ? 1 : 0;
|
|
1196
|
-
let count = 0;
|
|
1197
|
-
const sourceFiles = await collectFiles(projectRoot, { docs: false });
|
|
1198
|
-
for (const sourceFile of sourceFiles) {
|
|
1199
|
-
if (sourceFile === docPath)
|
|
1200
|
-
continue;
|
|
1201
|
-
const text = await readFile(resolve3(projectRoot, sourceFile), "utf8").catch(() => "");
|
|
1202
|
-
if (text.includes(reference.value))
|
|
1203
|
-
count += 1;
|
|
1204
|
-
}
|
|
1205
|
-
return count;
|
|
899
|
+
function isRigStickyStatusComment(body) {
|
|
900
|
+
return body.includes(RIG_STATUS_COMMENT_MARKER);
|
|
1206
901
|
}
|
|
1207
|
-
function
|
|
902
|
+
function ghSpawnOptions(extraEnv, timeoutMs) {
|
|
1208
903
|
return {
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
904
|
+
encoding: "utf-8",
|
|
905
|
+
timeout: timeoutMs,
|
|
906
|
+
env: {
|
|
907
|
+
...process.env,
|
|
908
|
+
...process.env.GH_TOKEN !== undefined ? { GH_TOKEN: process.env.GH_TOKEN } : {},
|
|
909
|
+
...process.env.GITHUB_TOKEN !== undefined ? { GITHUB_TOKEN: process.env.GITHUB_TOKEN } : {},
|
|
910
|
+
...process.env.RIG_GITHUB_TOKEN !== undefined ? { RIG_GITHUB_TOKEN: process.env.RIG_GITHUB_TOKEN } : {},
|
|
911
|
+
...extraEnv ?? {}
|
|
912
|
+
}
|
|
1215
913
|
};
|
|
1216
914
|
}
|
|
1217
|
-
function
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
915
|
+
function credentialEnv(token) {
|
|
916
|
+
const clean = token?.trim() ?? "";
|
|
917
|
+
return { GH_TOKEN: clean, GITHUB_TOKEN: clean, RIG_GITHUB_TOKEN: clean };
|
|
918
|
+
}
|
|
919
|
+
async function resolveCredentialEnv(opts, purpose) {
|
|
920
|
+
if (!opts.credentialProvider)
|
|
921
|
+
return;
|
|
922
|
+
const input = {
|
|
923
|
+
owner: opts.owner,
|
|
924
|
+
repo: opts.repo,
|
|
925
|
+
workspaceId: opts.workspaceId ?? `${opts.owner}/${opts.repo}`,
|
|
926
|
+
...opts.userId ? { userId: opts.userId } : {},
|
|
927
|
+
purpose
|
|
1225
928
|
};
|
|
929
|
+
const resolved = await opts.credentialProvider.resolveGitHubToken(input);
|
|
930
|
+
return credentialEnv(resolved.token);
|
|
1226
931
|
}
|
|
1227
|
-
|
|
1228
|
-
const
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
932
|
+
function tokenDiagnostic(value) {
|
|
933
|
+
const clean = value?.trim() ?? "";
|
|
934
|
+
return clean ? `present(len=${clean.length})` : "missing";
|
|
935
|
+
}
|
|
936
|
+
function runGh(bin, args, spawn, extraEnv, timeoutMs) {
|
|
937
|
+
const options = ghSpawnOptions(extraEnv, timeoutMs);
|
|
938
|
+
const res = spawn(bin, [...args], options);
|
|
939
|
+
assertGhSuccess(args, res, options.env);
|
|
940
|
+
if (!res.stdout || res.stdout.trim() === "")
|
|
941
|
+
return [];
|
|
942
|
+
return JSON.parse(res.stdout);
|
|
943
|
+
}
|
|
944
|
+
function runGhVoid(bin, args, spawn, extraEnv, timeoutMs) {
|
|
945
|
+
const options = ghSpawnOptions(extraEnv, timeoutMs);
|
|
946
|
+
const res = spawn(bin, [...args], options);
|
|
947
|
+
assertGhSuccess(args, res, options.env);
|
|
948
|
+
}
|
|
949
|
+
function assertGhSuccess(args, res, env) {
|
|
950
|
+
if (res.error) {
|
|
951
|
+
const msg = res.error.message ?? String(res.error);
|
|
952
|
+
throw new Error(`gh CLI not available \u2014 install gh (brew install gh / apt install gh): ${msg}`);
|
|
953
|
+
}
|
|
954
|
+
if (res.status !== 0) {
|
|
955
|
+
throw new Error(`gh ${args.join(" ")} failed (exit ${res.status}): ${res.stderr}
|
|
956
|
+
[rig gh env:standard-plugin] GH_TOKEN=${tokenDiagnostic(env.GH_TOKEN)} GITHUB_TOKEN=${tokenDiagnostic(env.GITHUB_TOKEN)} RIG_GITHUB_TOKEN=${tokenDiagnostic(env.RIG_GITHUB_TOKEN)}`);
|
|
1237
957
|
}
|
|
1238
|
-
return findings;
|
|
1239
958
|
}
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
959
|
+
var DEFAULT_PROJECT_STATUSES = {
|
|
960
|
+
todo: "Todo",
|
|
961
|
+
running: "In Progress",
|
|
962
|
+
prOpen: "In Review",
|
|
963
|
+
ciFixing: "In Review",
|
|
964
|
+
merging: "In Review",
|
|
965
|
+
done: "Done",
|
|
966
|
+
needsAttention: "Needs Attention"
|
|
967
|
+
};
|
|
968
|
+
function asProjectRecord(value) {
|
|
969
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
970
|
+
}
|
|
971
|
+
function projectString(value) {
|
|
972
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
973
|
+
}
|
|
974
|
+
function projectLifecycleStatusForTaskStatus(status) {
|
|
975
|
+
const normalized = status?.trim().toLowerCase().replace(/[-\s]+/g, "_");
|
|
976
|
+
switch (normalized) {
|
|
977
|
+
case "draft":
|
|
978
|
+
case "open":
|
|
979
|
+
case "queued":
|
|
980
|
+
case "ready":
|
|
981
|
+
return "todo";
|
|
982
|
+
case "running":
|
|
983
|
+
case "in_progress":
|
|
984
|
+
return "running";
|
|
985
|
+
case "under_review":
|
|
986
|
+
case "review":
|
|
987
|
+
case "pr_open":
|
|
988
|
+
return "prOpen";
|
|
989
|
+
case "ci_fixing":
|
|
990
|
+
case "fixing":
|
|
991
|
+
return "ciFixing";
|
|
992
|
+
case "merging":
|
|
993
|
+
case "merge":
|
|
994
|
+
return "merging";
|
|
995
|
+
case "closed":
|
|
996
|
+
case "completed":
|
|
997
|
+
case "done":
|
|
998
|
+
return "done";
|
|
999
|
+
case "blocked":
|
|
1000
|
+
case "cancelled":
|
|
1001
|
+
case "failed":
|
|
1002
|
+
case "needs_attention":
|
|
1003
|
+
return "needsAttention";
|
|
1004
|
+
default:
|
|
1005
|
+
return null;
|
|
1254
1006
|
}
|
|
1255
|
-
return findings;
|
|
1256
1007
|
}
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1008
|
+
function ghGraphQLFetch(bin, spawnFn, extraEnv, timeoutMs) {
|
|
1009
|
+
return async (query, variables) => {
|
|
1010
|
+
const args = ["api", "graphql", "-f", `query=${query}`];
|
|
1011
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
1012
|
+
if (value === undefined || value === null)
|
|
1013
|
+
continue;
|
|
1014
|
+
args.push("-f", `${key}=${String(value)}`);
|
|
1015
|
+
}
|
|
1016
|
+
const response = runGh(bin, args, spawnFn, extraEnv, timeoutMs);
|
|
1017
|
+
return asProjectRecord(response)?.data ?? response;
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
function issueNodeIdFor(issue) {
|
|
1021
|
+
const id = issue.id ?? issue.nodeId ?? issue.node_id;
|
|
1022
|
+
return typeof id === "string" && id.trim().length > 0 ? id.trim() : null;
|
|
1023
|
+
}
|
|
1024
|
+
function nativeIssueDependencyRef(value, currentRepo) {
|
|
1025
|
+
const record = asProjectRecord(value);
|
|
1026
|
+
const number = typeof record?.number === "number" ? String(record.number) : projectString(record?.number);
|
|
1027
|
+
if (!number)
|
|
1028
|
+
return null;
|
|
1029
|
+
const repository = asProjectRecord(record?.repository);
|
|
1030
|
+
const owner = projectString(asProjectRecord(repository?.owner)?.login);
|
|
1031
|
+
const name = projectString(repository?.name);
|
|
1032
|
+
if (!owner || !name || `${owner}/${name}` === currentRepo)
|
|
1033
|
+
return number;
|
|
1034
|
+
return `${owner}/${name}#${number}`;
|
|
1035
|
+
}
|
|
1036
|
+
function nativeDependencyRefsFrom(data, currentRepo) {
|
|
1037
|
+
const issue = asProjectRecord(asProjectRecord(data)?.node);
|
|
1038
|
+
const blockedBy = asProjectRecord(issue?.blockedBy);
|
|
1039
|
+
const nodes = Array.isArray(blockedBy?.nodes) ? blockedBy.nodes : [];
|
|
1040
|
+
return [...new Set(nodes.flatMap((node) => {
|
|
1041
|
+
const ref = nativeIssueDependencyRef(node, currentRepo);
|
|
1042
|
+
return ref ? [ref] : [];
|
|
1043
|
+
}))];
|
|
1044
|
+
}
|
|
1045
|
+
async function readNativeDependenciesForIssue(input) {
|
|
1046
|
+
const issueId = issueNodeIdFor(input.issue);
|
|
1047
|
+
if (!issueId)
|
|
1048
|
+
return { deps: [], degraded: "GitHub issue node id is unavailable." };
|
|
1049
|
+
const query = `
|
|
1050
|
+
query RigIssueNativeDependencies($issueId: ID!) {
|
|
1051
|
+
node(id: $issueId) {
|
|
1052
|
+
... on Issue {
|
|
1053
|
+
blockedBy(first: 100) {
|
|
1054
|
+
nodes {
|
|
1055
|
+
number
|
|
1056
|
+
repository { name owner { login } }
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
`;
|
|
1063
|
+
try {
|
|
1064
|
+
return {
|
|
1065
|
+
deps: nativeDependencyRefsFrom(await input.fetchGraphQL(query, { issueId }, "gh-cli"), input.repo)
|
|
1066
|
+
};
|
|
1067
|
+
} catch (error) {
|
|
1068
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1069
|
+
return { deps: [], degraded: detail };
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
function formatIssueReference(ref) {
|
|
1073
|
+
const clean = ref.trim().replace(/^#/, "");
|
|
1074
|
+
return /^\d+$/.test(clean) ? `#${clean}` : clean;
|
|
1075
|
+
}
|
|
1076
|
+
function appendReferenceLines(body, deps, parents) {
|
|
1077
|
+
const lines = [];
|
|
1078
|
+
const cleanDeps = (deps ?? []).map(formatIssueReference).filter((ref) => ref.length > 0);
|
|
1079
|
+
const cleanParents = (parents ?? []).map(formatIssueReference).filter((ref) => ref.length > 0);
|
|
1080
|
+
if (cleanDeps.length > 0)
|
|
1081
|
+
lines.push(`depends-on: ${cleanDeps.join(", ")}`);
|
|
1082
|
+
if (cleanParents.length > 0)
|
|
1083
|
+
lines.push(`parents: ${cleanParents.join(", ")}`);
|
|
1084
|
+
if (lines.length === 0)
|
|
1085
|
+
return body;
|
|
1086
|
+
return body.trim().length > 0 ? `${body.trimEnd()}
|
|
1087
|
+
|
|
1088
|
+
${lines.join(`
|
|
1089
|
+
`)}` : lines.join(`
|
|
1090
|
+
`);
|
|
1091
|
+
}
|
|
1092
|
+
function bodyForCreatedTask(input) {
|
|
1093
|
+
const metadata = { ...input.metadata ?? {} };
|
|
1094
|
+
if (input.deps && input.deps.length > 0)
|
|
1095
|
+
metadata["depends-on"] = input.deps.map(formatIssueReference);
|
|
1096
|
+
if (input.parents && input.parents.length > 0)
|
|
1097
|
+
metadata.parents = input.parents.map(formatIssueReference);
|
|
1098
|
+
return updateRigOwnedMetadataBlock(appendReferenceLines(input.body, input.deps, input.parents), metadata);
|
|
1099
|
+
}
|
|
1100
|
+
function projectStatusFieldFrom(data, projectId) {
|
|
1101
|
+
const fields = asProjectRecord(asProjectRecord(asProjectRecord(data)?.node)?.fields)?.nodes;
|
|
1102
|
+
for (const node of Array.isArray(fields) ? fields : []) {
|
|
1103
|
+
const record = asProjectRecord(node);
|
|
1104
|
+
if (projectString(record?.name)?.toLowerCase() !== "status")
|
|
1105
|
+
continue;
|
|
1106
|
+
const id = projectString(record?.id);
|
|
1107
|
+
if (!id)
|
|
1108
|
+
continue;
|
|
1109
|
+
const options = Array.isArray(record?.options) ? record.options.flatMap((option) => {
|
|
1110
|
+
const optionRecord = asProjectRecord(option);
|
|
1111
|
+
const optionId = projectString(optionRecord?.id);
|
|
1112
|
+
const name = projectString(optionRecord?.name);
|
|
1113
|
+
return optionId && name ? [{ id: optionId, name }] : [];
|
|
1114
|
+
}) : [];
|
|
1115
|
+
return { id, name: "Status", options };
|
|
1116
|
+
}
|
|
1117
|
+
throw new Error(`GitHub Project ${projectId} does not expose a Status single-select field.`);
|
|
1118
|
+
}
|
|
1119
|
+
async function resolveProjectStatusField(input) {
|
|
1120
|
+
const query = `
|
|
1121
|
+
query RigProjectStatusField($projectId: ID!) {
|
|
1122
|
+
node(id: $projectId) {
|
|
1123
|
+
... on ProjectV2 {
|
|
1124
|
+
fields(first: 50) {
|
|
1125
|
+
nodes {
|
|
1126
|
+
... on ProjectV2FieldCommon { id name }
|
|
1127
|
+
... on ProjectV2SingleSelectField { id name options { id name } }
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
`;
|
|
1134
|
+
return projectStatusFieldFrom(await input.fetchGraphQL(query, { projectId: input.projectId }, input.token), input.projectId);
|
|
1135
|
+
}
|
|
1136
|
+
async function ensureIssueProjectItem(input) {
|
|
1137
|
+
const query = `
|
|
1138
|
+
query RigFindProjectIssueItem($projectId: ID!) {
|
|
1139
|
+
node(id: $projectId) {
|
|
1140
|
+
... on ProjectV2 {
|
|
1141
|
+
items(first: 100) { nodes { id content { ... on Issue { id } } } }
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
`;
|
|
1146
|
+
const data = await input.fetchGraphQL(query, { projectId: input.projectId }, input.token);
|
|
1147
|
+
const nodes = asProjectRecord(asProjectRecord(asProjectRecord(data)?.node)?.items)?.nodes;
|
|
1148
|
+
for (const node of Array.isArray(nodes) ? nodes : []) {
|
|
1149
|
+
const record = asProjectRecord(node);
|
|
1150
|
+
const content = asProjectRecord(record?.content);
|
|
1151
|
+
if (projectString(content?.id) === input.issueNodeId) {
|
|
1152
|
+
const id2 = projectString(record?.id);
|
|
1153
|
+
if (id2)
|
|
1154
|
+
return { id: id2, created: false };
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
const mutation = `
|
|
1158
|
+
mutation RigAddIssueToProject($projectId: ID!, $contentId: ID!) {
|
|
1159
|
+
addProjectV2ItemById(input: { projectId: $projectId, contentId: $contentId }) { item { id } }
|
|
1160
|
+
}
|
|
1161
|
+
`;
|
|
1162
|
+
const created = await input.fetchGraphQL(mutation, { projectId: input.projectId, contentId: input.issueNodeId }, input.token);
|
|
1163
|
+
const addResult = asProjectRecord(asProjectRecord(created)?.addProjectV2ItemById);
|
|
1164
|
+
const id = projectString(asProjectRecord(addResult?.item)?.id);
|
|
1165
|
+
if (!id)
|
|
1166
|
+
throw new Error("GitHub Project item creation did not return an item id.");
|
|
1167
|
+
return { id, created: true };
|
|
1168
|
+
}
|
|
1169
|
+
async function updateIssueProjectStatus(input) {
|
|
1170
|
+
const mutation = `
|
|
1171
|
+
mutation RigUpdateProjectStatus($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
|
|
1172
|
+
updateProjectV2ItemFieldValue(input: {
|
|
1173
|
+
projectId: $projectId,
|
|
1174
|
+
itemId: $itemId,
|
|
1175
|
+
fieldId: $fieldId,
|
|
1176
|
+
value: { singleSelectOptionId: $optionId }
|
|
1177
|
+
}) { projectV2Item { id } }
|
|
1178
|
+
}
|
|
1179
|
+
`;
|
|
1180
|
+
await input.fetchGraphQL(mutation, {
|
|
1181
|
+
projectId: input.projectId,
|
|
1182
|
+
itemId: input.itemId,
|
|
1183
|
+
fieldId: input.fieldId,
|
|
1184
|
+
optionId: input.optionId
|
|
1185
|
+
}, input.token);
|
|
1186
|
+
}
|
|
1187
|
+
function fetchIssueNodeId(bin, repo, spawnFn, id, extraEnv, timeoutMs) {
|
|
1188
|
+
const issue = runGh(bin, ["issue", "view", String(id), "--repo", repo, "--json", "id"], spawnFn, extraEnv, timeoutMs);
|
|
1189
|
+
return projectString(issue.id) ?? projectString(issue.nodeId) ?? projectString(issue.node_id);
|
|
1190
|
+
}
|
|
1191
|
+
async function syncGitHubProjectStatus(bin, repo, spawnFn, id, status, projects, extraEnv, timeoutMs) {
|
|
1192
|
+
if (!projects?.enabled)
|
|
1193
|
+
return;
|
|
1194
|
+
const projectId = projectString(projects.projectId);
|
|
1195
|
+
if (!projectId)
|
|
1196
|
+
throw new Error("GitHub Projects status sync is enabled but projectId is missing.");
|
|
1197
|
+
const lifecycleStatus = projectLifecycleStatusForTaskStatus(status);
|
|
1198
|
+
if (!lifecycleStatus)
|
|
1199
|
+
return;
|
|
1200
|
+
const issueNodeId = fetchIssueNodeId(bin, repo, spawnFn, id, extraEnv, timeoutMs);
|
|
1201
|
+
if (!issueNodeId)
|
|
1202
|
+
throw new Error(`GitHub issue ${repo}#${id} did not expose a node id for Projects status sync.`);
|
|
1203
|
+
const projectStatus = projectString(projects.statuses?.[lifecycleStatus]) ?? DEFAULT_PROJECT_STATUSES[lifecycleStatus];
|
|
1204
|
+
const fetchGraphQL = ghGraphQLFetch(bin, spawnFn, extraEnv, timeoutMs);
|
|
1205
|
+
const field = await resolveProjectStatusField({ projectId, token: "gh-cli", fetchGraphQL });
|
|
1206
|
+
const option = field.options.find((candidate) => candidate.name.toLowerCase() === projectStatus.toLowerCase() || candidate.id === projectStatus);
|
|
1207
|
+
if (!option)
|
|
1208
|
+
throw new Error(`GitHub Project ${projectId} Status field does not contain option "${projectStatus}".`);
|
|
1209
|
+
const item = await ensureIssueProjectItem({ projectId, issueNodeId, token: "gh-cli", fetchGraphQL });
|
|
1210
|
+
await updateIssueProjectStatus({
|
|
1211
|
+
projectId,
|
|
1212
|
+
itemId: item.id,
|
|
1213
|
+
fieldId: projectString(projects.statusFieldId) ?? field.id,
|
|
1214
|
+
optionId: option.id,
|
|
1215
|
+
token: "gh-cli",
|
|
1216
|
+
fetchGraphQL
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
var TERMINAL_TASK_STATUSES = new Set(["closed", "completed", "merged", "cancelled", "resolved", "done"]);
|
|
1220
|
+
function normalizeTaskStatusToken(status) {
|
|
1221
|
+
return status?.trim().toLowerCase().replace(/[-\s]+/g, "_") ?? "";
|
|
1222
|
+
}
|
|
1223
|
+
function issueUpdatesMode(value) {
|
|
1224
|
+
return value === "off" || value === "minimal" || value === "lifecycle" ? value : "lifecycle";
|
|
1225
|
+
}
|
|
1226
|
+
function isTerminalTaskStatus(status) {
|
|
1227
|
+
return TERMINAL_TASK_STATUSES.has(normalizeTaskStatusToken(status));
|
|
1228
|
+
}
|
|
1229
|
+
function shouldWriteIssueUpdate(mode, status) {
|
|
1230
|
+
if (mode === "off")
|
|
1231
|
+
return false;
|
|
1232
|
+
if (mode === "lifecycle")
|
|
1233
|
+
return true;
|
|
1234
|
+
return isTerminalTaskStatus(status);
|
|
1235
|
+
}
|
|
1236
|
+
function isRunningStatus(status) {
|
|
1237
|
+
return normalizeTaskStatusToken(status) === "running";
|
|
1238
|
+
}
|
|
1239
|
+
function assignRunningIssue(bin, repo, spawnFn, id, assignee, extraEnv, timeoutMs) {
|
|
1240
|
+
runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, "--add-assignee", assignee?.trim() || "@me"], spawnFn, extraEnv, timeoutMs);
|
|
1241
|
+
}
|
|
1242
|
+
function statusLabelFor(status) {
|
|
1243
|
+
switch (status) {
|
|
1244
|
+
case "running":
|
|
1245
|
+
case "in_progress":
|
|
1246
|
+
return "in-progress";
|
|
1247
|
+
case "blocked":
|
|
1248
|
+
return "blocked";
|
|
1249
|
+
case "ready":
|
|
1250
|
+
return "ready";
|
|
1251
|
+
case "under_review":
|
|
1252
|
+
return "under-review";
|
|
1253
|
+
case "failed":
|
|
1254
|
+
return "failed";
|
|
1255
|
+
case "cancelled":
|
|
1256
|
+
return "cancelled";
|
|
1257
|
+
case "ci_fixing":
|
|
1258
|
+
return "under-review";
|
|
1259
|
+
case "merging":
|
|
1260
|
+
return "under-review";
|
|
1261
|
+
case "needs_attention":
|
|
1262
|
+
return "blocked";
|
|
1263
|
+
case "closed":
|
|
1264
|
+
case "completed":
|
|
1265
|
+
case "open":
|
|
1266
|
+
return null;
|
|
1267
|
+
default:
|
|
1268
|
+
throw new Error(`unsupported status: ${status}`);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
function rigStatusLabelFor(status) {
|
|
1272
|
+
switch (status) {
|
|
1273
|
+
case "running":
|
|
1274
|
+
case "in_progress":
|
|
1275
|
+
return "rig:running";
|
|
1276
|
+
case "under_review":
|
|
1277
|
+
return "rig:pr-open";
|
|
1278
|
+
case "closed":
|
|
1279
|
+
case "completed":
|
|
1280
|
+
return "rig:done";
|
|
1281
|
+
case "ci_fixing":
|
|
1282
|
+
return "rig:ci-fixing";
|
|
1283
|
+
case "merging":
|
|
1284
|
+
return "rig:merging";
|
|
1285
|
+
case "needs_attention":
|
|
1286
|
+
case "failed":
|
|
1287
|
+
case "blocked":
|
|
1288
|
+
return "rig:needs-attention";
|
|
1289
|
+
case "ready":
|
|
1290
|
+
case "cancelled":
|
|
1291
|
+
case "open":
|
|
1292
|
+
return null;
|
|
1293
|
+
default:
|
|
1294
|
+
return null;
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
async function applyIssueStatus(bin, repo, spawnFn, id, status, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs) {
|
|
1298
|
+
const targetLabel = status === "closed" || status === "completed" ? null : statusLabelFor(status);
|
|
1299
|
+
const targetRigLabel = rigStatusLabelFor(status);
|
|
1300
|
+
const shouldSyncLifecycle = shouldWriteIssueUpdate(issueUpdates, status);
|
|
1301
|
+
for (const l of [...STATUS_LABELS, ...RIG_STATUS_LABELS]) {
|
|
1302
|
+
if (targetLabel !== null && l === targetLabel)
|
|
1303
|
+
continue;
|
|
1304
|
+
if (targetRigLabel !== null && l === targetRigLabel)
|
|
1305
|
+
continue;
|
|
1306
|
+
try {
|
|
1307
|
+
runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, "--remove-label", l], spawnFn, extraEnv, timeoutMs);
|
|
1308
|
+
} catch {}
|
|
1309
|
+
}
|
|
1310
|
+
for (const label of [targetLabel, targetRigLabel].filter((value) => Boolean(value))) {
|
|
1311
|
+
try {
|
|
1312
|
+
runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, "--add-label", label], spawnFn, extraEnv, timeoutMs);
|
|
1313
|
+
} catch (error) {
|
|
1314
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1315
|
+
if (!/not found/i.test(message)) {
|
|
1316
|
+
throw error;
|
|
1317
|
+
}
|
|
1318
|
+
ensureStatusLabel(bin, repo, spawnFn, label, extraEnv, timeoutMs);
|
|
1319
|
+
runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, "--add-label", label], spawnFn, extraEnv, timeoutMs);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
if (isRunningStatus(status)) {
|
|
1323
|
+
assignRunningIssue(bin, repo, spawnFn, id, runningAssignee, extraEnv, timeoutMs);
|
|
1324
|
+
if (shouldSyncLifecycle) {
|
|
1325
|
+
upsertRigStickyComment(bin, repo, spawnFn, String(id), buildRigStickyStatusComment({
|
|
1326
|
+
status: "running",
|
|
1327
|
+
summary: "Rig run started."
|
|
1328
|
+
}), extraEnv, timeoutMs);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
if (shouldSyncLifecycle) {
|
|
1332
|
+
await syncGitHubProjectStatus(bin, repo, spawnFn, id, status, projects, extraEnv, timeoutMs);
|
|
1333
|
+
}
|
|
1334
|
+
if (status === "closed" || status === "completed") {
|
|
1335
|
+
runGhVoid(bin, ["issue", "close", String(id), "--repo", repo], spawnFn, extraEnv, timeoutMs);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
function ensureStatusLabel(bin, repo, spawnFn, label, extraEnv, timeoutMs) {
|
|
1339
|
+
try {
|
|
1340
|
+
runGhVoid(bin, [
|
|
1341
|
+
"label",
|
|
1342
|
+
"create",
|
|
1343
|
+
label,
|
|
1344
|
+
"--repo",
|
|
1345
|
+
repo,
|
|
1346
|
+
"--color",
|
|
1347
|
+
"6f42c1",
|
|
1348
|
+
"--description",
|
|
1349
|
+
"Task status managed by Rig"
|
|
1350
|
+
], spawnFn, extraEnv, timeoutMs);
|
|
1351
|
+
} catch (error) {
|
|
1352
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1353
|
+
if (!/already exists/i.test(message)) {
|
|
1354
|
+
throw error;
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
function upsertRigStickyComment(bin, repo, spawnFn, id, body, extraEnv, timeoutMs) {
|
|
1359
|
+
const comments = runGh(bin, ["api", `repos/${repo}/issues/${id}/comments`, "--paginate"], spawnFn, extraEnv, timeoutMs);
|
|
1360
|
+
const existing = comments.find((comment) => typeof comment.body === "string" && comment.body.includes(RIG_STATUS_COMMENT_MARKER));
|
|
1361
|
+
if (existing) {
|
|
1362
|
+
runGhVoid(bin, ["api", "-X", "PATCH", `repos/${repo}/issues/comments/${existing.id}`, "-f", `body=${body}`], spawnFn, extraEnv, timeoutMs);
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
runGhVoid(bin, ["api", "-X", "POST", `repos/${repo}/issues/${id}/comments`, "-f", `body=${body}`], spawnFn, extraEnv, timeoutMs);
|
|
1366
|
+
}
|
|
1367
|
+
function notifyTaskChanged(onTaskChanged, repo, id, status) {
|
|
1368
|
+
onTaskChanged?.({ repo, id, ...status ? { status } : {}, reason: "github-issue-updated" });
|
|
1369
|
+
}
|
|
1370
|
+
function fetchIssueBody(bin, repo, spawnFn, id, extraEnv, timeoutMs) {
|
|
1371
|
+
const issue = runGh(bin, [
|
|
1372
|
+
"issue",
|
|
1373
|
+
"view",
|
|
1374
|
+
String(id),
|
|
1375
|
+
"--repo",
|
|
1376
|
+
repo,
|
|
1377
|
+
"--json",
|
|
1378
|
+
"body"
|
|
1379
|
+
], spawnFn, extraEnv, timeoutMs);
|
|
1380
|
+
return typeof issue.body === "string" ? issue.body : undefined;
|
|
1381
|
+
}
|
|
1382
|
+
function applyLabels(bin, repo, spawnFn, id, labels, action, extraEnv, timeoutMs) {
|
|
1383
|
+
for (const rawLabel of labels) {
|
|
1384
|
+
const label = rawLabel.trim();
|
|
1385
|
+
if (!label)
|
|
1386
|
+
continue;
|
|
1387
|
+
if (action === "--add-label") {
|
|
1388
|
+
try {
|
|
1389
|
+
runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, action, label], spawnFn, extraEnv, timeoutMs);
|
|
1390
|
+
} catch (error) {
|
|
1391
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1392
|
+
if (!/not found/i.test(message))
|
|
1393
|
+
throw error;
|
|
1394
|
+
ensureStatusLabel(bin, repo, spawnFn, label, extraEnv, timeoutMs);
|
|
1395
|
+
runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, action, label], spawnFn, extraEnv, timeoutMs);
|
|
1396
|
+
}
|
|
1397
|
+
continue;
|
|
1398
|
+
}
|
|
1399
|
+
try {
|
|
1400
|
+
runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, action, label], spawnFn, extraEnv, timeoutMs);
|
|
1401
|
+
} catch {}
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
async function applyIssueUpdate(bin, repo, spawnFn, id, update, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs) {
|
|
1405
|
+
if (update.status) {
|
|
1406
|
+
await applyIssueStatus(bin, repo, spawnFn, id, update.status, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs);
|
|
1407
|
+
}
|
|
1408
|
+
if (update.comment?.trim() && shouldWriteIssueUpdate(issueUpdates, update.status)) {
|
|
1409
|
+
if (isRigStickyStatusComment(update.comment)) {
|
|
1410
|
+
upsertRigStickyComment(bin, repo, spawnFn, String(id), update.comment, extraEnv, timeoutMs);
|
|
1411
|
+
} else {
|
|
1412
|
+
runGhVoid(bin, ["issue", "comment", String(id), "--repo", repo, "--body", update.comment], spawnFn, extraEnv, timeoutMs);
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
const editArgs = ["issue", "edit", String(id), "--repo", repo];
|
|
1416
|
+
if (update.title?.trim()) {
|
|
1417
|
+
editArgs.push("--title", update.title.trim());
|
|
1418
|
+
}
|
|
1419
|
+
const nextBody = update.metadata ? updateRigOwnedMetadataBlock(update.body ?? fetchIssueBody(bin, repo, spawnFn, id, extraEnv, timeoutMs) ?? "", update.metadata) : update.body;
|
|
1420
|
+
if (nextBody !== undefined) {
|
|
1421
|
+
editArgs.push("--body", nextBody);
|
|
1422
|
+
}
|
|
1423
|
+
if (editArgs.length > 5) {
|
|
1424
|
+
runGhVoid(bin, editArgs, spawnFn, extraEnv, timeoutMs);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
function createGitHubIssuesTaskSource(opts) {
|
|
1428
|
+
const bin = opts.ghBinary ?? "gh";
|
|
1429
|
+
const state = opts.state ?? "open";
|
|
1430
|
+
const repo = `${opts.owner}/${opts.repo}`;
|
|
1431
|
+
const spawnFn = opts.spawn ?? spawnSync;
|
|
1432
|
+
const timeoutMs = Math.max(1000, Math.trunc(opts.timeoutMs ?? DEFAULT_GH_TIMEOUT_MS));
|
|
1433
|
+
const listLimit = Math.max(1, Math.trunc(opts.listLimit ?? DEFAULT_GITHUB_ISSUE_LIST_LIMIT));
|
|
1434
|
+
const issueUpdates = issueUpdatesMode(opts.issueUpdates);
|
|
1435
|
+
async function issueToTaskWithOptionalNativeDependencies(issue, env) {
|
|
1436
|
+
if (!opts.useNativeDependencies)
|
|
1437
|
+
return issueToTask(issue, repo);
|
|
1438
|
+
const nativeDependencies = await readNativeDependenciesForIssue({
|
|
1439
|
+
issue,
|
|
1440
|
+
repo,
|
|
1441
|
+
fetchGraphQL: ghGraphQLFetch(bin, spawnFn, env, timeoutMs)
|
|
1442
|
+
});
|
|
1443
|
+
return issueToTask(issue, repo, nativeDependencies);
|
|
1444
|
+
}
|
|
1445
|
+
return {
|
|
1446
|
+
id: "std:github-issues",
|
|
1447
|
+
kind: "github-issues",
|
|
1448
|
+
async list() {
|
|
1449
|
+
const labelArg = opts.labels && opts.labels.length > 0 ? ["--label", opts.labels.join(",")] : [];
|
|
1450
|
+
const assigneeArg = opts.assignee?.trim() ? ["--assignee", opts.assignee.trim()] : [];
|
|
1451
|
+
const args = [
|
|
1452
|
+
"issue",
|
|
1453
|
+
"list",
|
|
1454
|
+
"--repo",
|
|
1455
|
+
repo,
|
|
1456
|
+
...labelArg,
|
|
1457
|
+
...assigneeArg,
|
|
1458
|
+
"--state",
|
|
1459
|
+
state,
|
|
1460
|
+
"--limit",
|
|
1461
|
+
String(listLimit),
|
|
1462
|
+
"--json",
|
|
1463
|
+
"number,title,body,labels,state,url,assignees,id"
|
|
1464
|
+
];
|
|
1465
|
+
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
1466
|
+
const rawIssues = runGh(bin, args, spawnFn, env, timeoutMs);
|
|
1467
|
+
if (rawIssues.length >= listLimit) {
|
|
1468
|
+
throw new Error(`GitHub issue list for ${repo} reached the configured limit (${listLimit}); refusing to silently truncate matching issues. Increase taskSource.options.listLimit or narrow labels/state/assignee.`);
|
|
1469
|
+
}
|
|
1470
|
+
const issues = rawIssues.filter((issue) => !issue.pull_request);
|
|
1471
|
+
return Promise.all(issues.map((issue) => issueToTaskWithOptionalNativeDependencies(issue, env)));
|
|
1472
|
+
},
|
|
1473
|
+
async get(id) {
|
|
1474
|
+
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
1475
|
+
let issue;
|
|
1476
|
+
try {
|
|
1477
|
+
issue = runGh(bin, [
|
|
1478
|
+
"issue",
|
|
1479
|
+
"view",
|
|
1480
|
+
String(id),
|
|
1481
|
+
"--repo",
|
|
1482
|
+
repo,
|
|
1483
|
+
"--json",
|
|
1484
|
+
"number,title,body,labels,state,url,assignees,id"
|
|
1485
|
+
], spawnFn, env, timeoutMs);
|
|
1486
|
+
} catch (error) {
|
|
1487
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1488
|
+
if (/could not resolve to (an? )?(issue|pullrequest)|no issues? (found|matched)|404 not found|gh: not found|gh issue view\b[\s\S]*failed \(exit \d+\): not found\b/i.test(detail)) {
|
|
1489
|
+
return;
|
|
1490
|
+
}
|
|
1491
|
+
throw new Error(`Failed to read task ${id} from GitHub repo ${repo}: ${detail}`);
|
|
1492
|
+
}
|
|
1493
|
+
return issueToTaskWithOptionalNativeDependencies(issue, env);
|
|
1494
|
+
},
|
|
1495
|
+
async updateStatus(id, status) {
|
|
1496
|
+
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
1497
|
+
await applyIssueStatus(bin, repo, spawnFn, id, status, opts.projects, opts.assignee, issueUpdates, env, timeoutMs);
|
|
1498
|
+
notifyTaskChanged(opts.onTaskChanged, repo, id, status);
|
|
1499
|
+
},
|
|
1500
|
+
async updateTask(id, update) {
|
|
1501
|
+
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
1502
|
+
await applyIssueUpdate(bin, repo, spawnFn, id, update, opts.projects, opts.assignee, issueUpdates, env, timeoutMs);
|
|
1503
|
+
notifyTaskChanged(opts.onTaskChanged, repo, id, update.status);
|
|
1504
|
+
},
|
|
1505
|
+
async addLabels(id, labels) {
|
|
1506
|
+
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
1507
|
+
applyLabels(bin, repo, spawnFn, id, labels, "--add-label", env, timeoutMs);
|
|
1508
|
+
notifyTaskChanged(opts.onTaskChanged, repo, id);
|
|
1509
|
+
},
|
|
1510
|
+
async removeLabels(id, labels) {
|
|
1511
|
+
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
1512
|
+
applyLabels(bin, repo, spawnFn, id, labels, "--remove-label", env, timeoutMs);
|
|
1513
|
+
notifyTaskChanged(opts.onTaskChanged, repo, id);
|
|
1514
|
+
},
|
|
1515
|
+
async createIssue(input) {
|
|
1516
|
+
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
1517
|
+
const body = input.body ?? "";
|
|
1518
|
+
const args = [
|
|
1519
|
+
"api",
|
|
1520
|
+
"-X",
|
|
1521
|
+
"POST",
|
|
1522
|
+
`repos/${repo}/issues`,
|
|
1523
|
+
"-f",
|
|
1524
|
+
`title=${input.title}`,
|
|
1525
|
+
"-f",
|
|
1526
|
+
`body=${body}`,
|
|
1527
|
+
...(input.labels ?? []).flatMap((label) => ["-f", `labels[]=${label}`])
|
|
1528
|
+
];
|
|
1529
|
+
const issue = runGh(bin, args, spawnFn, env, timeoutMs);
|
|
1530
|
+
notifyTaskChanged(opts.onTaskChanged, repo, String(issue.number));
|
|
1531
|
+
return issueToTask({ ...issue, body: issue.body ?? body }, repo);
|
|
1532
|
+
},
|
|
1533
|
+
async create(input) {
|
|
1534
|
+
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
1535
|
+
const body = bodyForCreatedTask(input);
|
|
1536
|
+
const args = [
|
|
1537
|
+
"api",
|
|
1538
|
+
"-X",
|
|
1539
|
+
"POST",
|
|
1540
|
+
`repos/${repo}/issues`,
|
|
1541
|
+
"-f",
|
|
1542
|
+
`title=${input.title}`,
|
|
1543
|
+
"-f",
|
|
1544
|
+
`body=${body}`,
|
|
1545
|
+
"-f",
|
|
1546
|
+
"labels[]=rig:generated"
|
|
1547
|
+
];
|
|
1548
|
+
const issue = runGh(bin, args, spawnFn, env, timeoutMs);
|
|
1549
|
+
notifyTaskChanged(opts.onTaskChanged, repo, String(issue.number));
|
|
1550
|
+
return issueToTask({ ...issue, body: issue.body ?? body }, repo);
|
|
1551
|
+
},
|
|
1552
|
+
async getIssueBody(id) {
|
|
1553
|
+
const env = await resolveCredentialEnv(opts, "selected-repo");
|
|
1554
|
+
return fetchIssueBody(bin, repo, spawnFn, id, env, timeoutMs);
|
|
1555
|
+
}
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
// packages/standard-plugin/src/files-source.ts
|
|
1560
|
+
import { readFileSync as readFileSync2, readdirSync, existsSync as existsSync2, statSync, writeFileSync } from "fs";
|
|
1561
|
+
import { join, basename, isAbsolute, resolve as resolve2 } from "path";
|
|
1562
|
+
var DEFAULT_PATTERN = /\.(task\.)?json$/;
|
|
1563
|
+
function readTaskFile(file, pattern) {
|
|
1564
|
+
const raw = JSON.parse(readFileSync2(file, "utf-8"));
|
|
1565
|
+
const inferredId = basename(file).replace(pattern, "");
|
|
1566
|
+
const labels = Array.isArray(raw.labels) ? raw.labels.filter((label) => typeof label === "string") : [];
|
|
1567
|
+
const scope = labels.filter((label) => label.startsWith("scope:")).map((label) => label.slice("scope:".length));
|
|
1568
|
+
const validators = labels.filter((label) => label.startsWith("validator:")).map((label) => label.slice("validator:".length));
|
|
1569
|
+
const roleLabel = labels.find((label) => label.startsWith("role:"));
|
|
1570
|
+
return {
|
|
1571
|
+
id: raw["id"] ?? inferredId,
|
|
1572
|
+
deps: raw["deps"] ?? raw["depends_on"] ?? [],
|
|
1573
|
+
status: raw["status"] ?? "ready",
|
|
1574
|
+
...scope.length > 0 ? { scope } : {},
|
|
1575
|
+
...roleLabel ? { role: roleLabel.slice("role:".length) } : {},
|
|
1576
|
+
...validators.length > 0 ? { validators, validation: validators } : {},
|
|
1577
|
+
...raw
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
function createFilesTaskSource(opts) {
|
|
1581
|
+
const pattern = opts.pattern ?? DEFAULT_PATTERN;
|
|
1582
|
+
const configured = opts.path ?? opts.dir;
|
|
1583
|
+
if (!configured) {
|
|
1584
|
+
throw new Error("createFilesTaskSource: either `path` or `dir` must be provided");
|
|
1585
|
+
}
|
|
1586
|
+
const directory = isAbsolute(configured) ? configured : resolve2(opts.projectRoot ?? process.cwd(), configured);
|
|
1587
|
+
const findTaskFile = (id) => {
|
|
1588
|
+
if (!existsSync2(directory))
|
|
1589
|
+
return;
|
|
1590
|
+
for (const name of readdirSync(directory)) {
|
|
1591
|
+
if (!pattern.test(name))
|
|
1592
|
+
continue;
|
|
1593
|
+
const p = join(directory, name);
|
|
1594
|
+
try {
|
|
1595
|
+
if (!statSync(p).isFile())
|
|
1596
|
+
continue;
|
|
1597
|
+
if (readTaskFile(p, pattern).id === id)
|
|
1598
|
+
return p;
|
|
1599
|
+
} catch {}
|
|
1600
|
+
}
|
|
1601
|
+
return;
|
|
1602
|
+
};
|
|
1603
|
+
const applyUpdate = (id, update) => {
|
|
1604
|
+
const file = findTaskFile(id);
|
|
1605
|
+
if (!file) {
|
|
1606
|
+
throw new Error(`files task not found: ${id}`);
|
|
1607
|
+
}
|
|
1608
|
+
const raw = JSON.parse(readFileSync2(file, "utf-8"));
|
|
1609
|
+
if (update.status)
|
|
1610
|
+
raw.status = update.status;
|
|
1611
|
+
if (update.title !== undefined)
|
|
1612
|
+
raw.title = update.title;
|
|
1613
|
+
if (update.body !== undefined)
|
|
1614
|
+
raw.body = update.body;
|
|
1615
|
+
if (update.comment?.trim()) {
|
|
1616
|
+
const existing = Array.isArray(raw.comments) ? raw.comments : [];
|
|
1617
|
+
raw.comments = [
|
|
1618
|
+
...existing,
|
|
1619
|
+
{
|
|
1620
|
+
body: update.comment,
|
|
1621
|
+
createdAt: new Date().toISOString(),
|
|
1622
|
+
source: "rig"
|
|
1623
|
+
}
|
|
1624
|
+
];
|
|
1625
|
+
}
|
|
1626
|
+
writeFileSync(file, `${JSON.stringify(raw, null, 2)}
|
|
1627
|
+
`, "utf-8");
|
|
1628
|
+
};
|
|
1629
|
+
return {
|
|
1630
|
+
id: "std:files",
|
|
1631
|
+
kind: "files",
|
|
1632
|
+
async list() {
|
|
1633
|
+
if (!existsSync2(directory))
|
|
1634
|
+
return [];
|
|
1635
|
+
const out = [];
|
|
1636
|
+
for (const name of readdirSync(directory)) {
|
|
1637
|
+
if (!pattern.test(name))
|
|
1638
|
+
continue;
|
|
1639
|
+
const p = join(directory, name);
|
|
1640
|
+
try {
|
|
1641
|
+
if (!statSync(p).isFile())
|
|
1642
|
+
continue;
|
|
1643
|
+
out.push(readTaskFile(p, pattern));
|
|
1644
|
+
} catch (err) {
|
|
1645
|
+
console.warn(`[files-source] skipped ${name}: ${err.message}`);
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
return out;
|
|
1649
|
+
},
|
|
1650
|
+
async get(id) {
|
|
1651
|
+
const all = await this.list();
|
|
1652
|
+
return all.find((t) => t.id === id);
|
|
1653
|
+
},
|
|
1654
|
+
async updateStatus(id, status) {
|
|
1655
|
+
applyUpdate(id, { status });
|
|
1656
|
+
},
|
|
1657
|
+
async updateTask(id, update) {
|
|
1658
|
+
applyUpdate(id, update);
|
|
1272
1659
|
}
|
|
1273
|
-
}
|
|
1274
|
-
return {
|
|
1275
|
-
generatedAt: new Date().toISOString(),
|
|
1276
|
-
scanned: docs.length,
|
|
1277
|
-
degraded,
|
|
1278
|
-
findings
|
|
1279
1660
|
};
|
|
1280
1661
|
}
|
|
1281
1662
|
|
|
1282
|
-
// packages/standard-plugin/src/
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
var
|
|
1286
|
-
var
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
}
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
after: ["open-pr"]
|
|
1299
|
-
},
|
|
1300
|
-
contributedBy: DOCS_DRIFT_STAGE_ID
|
|
1301
|
-
});
|
|
1302
|
-
var DOCS_DRIFT_CLI_COMMAND = "rig drift [--docs <csv>] [--ignore <csv>] [--fail-on-drift] [--json]";
|
|
1303
|
-
function highConfidenceDriftFindings(report) {
|
|
1304
|
-
return report.findings.filter((finding) => finding.confidence === "high");
|
|
1305
|
-
}
|
|
1306
|
-
function driftGateResult(report, mode = "enforce") {
|
|
1307
|
-
const high = highConfidenceDriftFindings(report);
|
|
1308
|
-
if (mode === "enforce" && high.length > 0) {
|
|
1309
|
-
return { kind: "block", reason: `${high.length} high-confidence documentation drift finding(s).` };
|
|
1310
|
-
}
|
|
1311
|
-
return { kind: "allow" };
|
|
1312
|
-
}
|
|
1313
|
-
function createDocsDriftGateStage(options = {}) {
|
|
1314
|
-
return async (ctx) => {
|
|
1315
|
-
const projectRoot = typeof ctx.metadata?.projectRoot === "string" ? ctx.metadata.projectRoot : process.cwd();
|
|
1316
|
-
const report = await detectDrift({
|
|
1317
|
-
projectRoot,
|
|
1318
|
-
...options.docsGlobs !== undefined ? { docsGlobs: options.docsGlobs } : {},
|
|
1319
|
-
...options.ignoreGlobs !== undefined ? { ignoreGlobs: options.ignoreGlobs } : {}
|
|
1320
|
-
});
|
|
1321
|
-
return driftGateResult(report, options.failOnDrift ? "enforce" : "observe");
|
|
1322
|
-
};
|
|
1323
|
-
}
|
|
1324
|
-
async function runDocsDriftValidation(options) {
|
|
1325
|
-
const report = await detectDrift(options);
|
|
1326
|
-
const high = highConfidenceDriftFindings(report);
|
|
1327
|
-
const passed = options.failOnDrift ? high.length === 0 : true;
|
|
1328
|
-
const findingWord = report.findings.length === 1 ? "finding" : "findings";
|
|
1663
|
+
// packages/standard-plugin/src/plugin.ts
|
|
1664
|
+
init_metadata();
|
|
1665
|
+
init_metadata();
|
|
1666
|
+
var DOCS_HEALTH_PANEL_ID = "docs-health";
|
|
1667
|
+
var STANDARD_PRODUCT_COMMANDS = [
|
|
1668
|
+
{ command: "launch", description: "Open the Rig Cockpit through the OMP collaboration substrate.", usage: "rig [launch] [args...]" },
|
|
1669
|
+
{ command: "join", description: "Join an encrypted Rig/OMP collaborative session.", usage: "rig join <link>" },
|
|
1670
|
+
{ command: "acp", description: "Start the Rig product in OMP ACP mode.", usage: "rig acp [args...]" },
|
|
1671
|
+
{ command: "models", description: "Delegate model management to the Rig product substrate.", usage: "rig models [args...]" },
|
|
1672
|
+
{ command: "mcp", description: "Delegate MCP management to the Rig product substrate.", usage: "rig mcp [args...]" },
|
|
1673
|
+
{ command: "update", description: "Delegate product update handling to the Rig product substrate.", usage: "rig update [args...]" }
|
|
1674
|
+
];
|
|
1675
|
+
function standardProductCliCommandId(command) {
|
|
1676
|
+
return `@rig/standard-plugin:product-entrypoint:${command}`;
|
|
1677
|
+
}
|
|
1678
|
+
function standardProductCliCommandMetadata(descriptor) {
|
|
1329
1679
|
return {
|
|
1330
|
-
id:
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1680
|
+
id: standardProductCliCommandId(descriptor.command),
|
|
1681
|
+
family: descriptor.command,
|
|
1682
|
+
description: descriptor.description,
|
|
1683
|
+
usage: descriptor.usage,
|
|
1684
|
+
projectRequired: false
|
|
1334
1685
|
};
|
|
1335
1686
|
}
|
|
1336
|
-
function
|
|
1687
|
+
function createStandardProductCliCommand(descriptor) {
|
|
1337
1688
|
return {
|
|
1338
|
-
...
|
|
1339
|
-
async
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1689
|
+
...standardProductCliCommandMetadata(descriptor),
|
|
1690
|
+
run: async (context, args) => {
|
|
1691
|
+
const { runRigOmpProductCommand: runRigOmpProductCommand2 } = await Promise.resolve().then(() => (init_product_entrypoint(), exports_product_entrypoint));
|
|
1692
|
+
return runRigOmpProductCommand2({
|
|
1693
|
+
projectRoot: context.projectRoot,
|
|
1694
|
+
command: descriptor.command,
|
|
1695
|
+
args
|
|
1345
1696
|
});
|
|
1346
1697
|
}
|
|
1347
1698
|
};
|
|
1348
1699
|
}
|
|
1349
|
-
function takeOptionValue(args, index, flag) {
|
|
1350
|
-
const value = args[index + 1];
|
|
1351
|
-
if (!value)
|
|
1352
|
-
throw new Error(`${flag} requires a value`);
|
|
1353
|
-
return value;
|
|
1354
|
-
}
|
|
1355
|
-
function takeFlag(args, flag) {
|
|
1356
|
-
const rest = [...args];
|
|
1357
|
-
const index = rest.indexOf(flag);
|
|
1358
|
-
if (index < 0)
|
|
1359
|
-
return { value: false, rest };
|
|
1360
|
-
rest.splice(index, 1);
|
|
1361
|
-
return { value: true, rest };
|
|
1362
|
-
}
|
|
1363
|
-
function takeOption(args, flag) {
|
|
1364
|
-
const rest = [...args];
|
|
1365
|
-
const index = rest.indexOf(flag);
|
|
1366
|
-
if (index < 0)
|
|
1367
|
-
return { rest };
|
|
1368
|
-
const value = rest[index + 1];
|
|
1369
|
-
if (!value || value.startsWith("-"))
|
|
1370
|
-
throw new Error(`${flag} requires a value.`);
|
|
1371
|
-
rest.splice(index, 2);
|
|
1372
|
-
return { value, rest };
|
|
1373
|
-
}
|
|
1374
|
-
function requireNoExtraArgs(args, usage) {
|
|
1375
|
-
if (args.length > 0)
|
|
1376
|
-
throw new Error(`Unexpected argument: ${args[0]}
|
|
1377
|
-
Usage: ${usage}`);
|
|
1378
|
-
}
|
|
1379
|
-
function parseCsv(value) {
|
|
1380
|
-
return value?.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0) ?? [];
|
|
1381
|
-
}
|
|
1382
|
-
function driftSummary(report) {
|
|
1383
|
-
const highConfidence = highConfidenceDriftFindings(report).length;
|
|
1384
|
-
return { total: report.findings.length, highConfidence, degraded: report.degraded };
|
|
1385
|
-
}
|
|
1386
|
-
async function executeDrift(context, args, options = {}) {
|
|
1387
|
-
const json = takeFlag(args, "--json");
|
|
1388
|
-
const docs = takeOption(json.rest, "--docs");
|
|
1389
|
-
const ignore = takeOption(docs.rest, "--ignore");
|
|
1390
|
-
const failOnDrift = takeFlag(ignore.rest, "--fail-on-drift");
|
|
1391
|
-
requireNoExtraArgs(failOnDrift.rest, "rig drift [--docs <csv>] [--ignore <csv>] [--fail-on-drift] [--json]");
|
|
1392
|
-
const docsGlobs = parseCsv(docs.value);
|
|
1393
|
-
const ignoreGlobs = parseCsv(ignore.value);
|
|
1394
|
-
const effectiveDocsGlobs = docsGlobs.length > 0 ? docsGlobs : options.docsGlobs;
|
|
1395
|
-
const effectiveIgnoreGlobs = ignoreGlobs.length > 0 ? ignoreGlobs : options.ignoreGlobs;
|
|
1396
|
-
const effectiveFailOnDrift = failOnDrift.value || options.failOnDrift === true;
|
|
1397
|
-
const report = await detectDrift({
|
|
1398
|
-
projectRoot: context.projectRoot,
|
|
1399
|
-
...effectiveDocsGlobs !== undefined ? { docsGlobs: effectiveDocsGlobs } : {},
|
|
1400
|
-
...effectiveIgnoreGlobs !== undefined ? { ignoreGlobs: effectiveIgnoreGlobs } : {}
|
|
1401
|
-
});
|
|
1402
|
-
const failed = effectiveFailOnDrift && highConfidenceDriftFindings(report).length > 0;
|
|
1403
|
-
const details = { report, summary: driftSummary(report), failOnDrift: effectiveFailOnDrift, failed };
|
|
1404
|
-
if (context.outputMode === "text") {
|
|
1405
|
-
if (json.value)
|
|
1406
|
-
console.log(JSON.stringify(details, null, 2));
|
|
1407
|
-
else
|
|
1408
|
-
console.log(report.findings.length === 0 ? `No drift findings across ${report.scanned} documents.` : report.findings.map((finding) => `${finding.docPath}:${finding.line ?? "?"} ${finding.kind} ${finding.confidence} ${finding.detail}`).join(`
|
|
1409
|
-
`));
|
|
1410
|
-
}
|
|
1411
|
-
return { ok: !failed, group: "drift", command: "scan", details };
|
|
1412
|
-
}
|
|
1413
|
-
function createDocsDriftRuntimeCliCommand(options = {}) {
|
|
1414
|
-
return {
|
|
1415
|
-
id: DOCS_DRIFT_CLI_ID,
|
|
1416
|
-
family: "drift",
|
|
1417
|
-
command: DOCS_DRIFT_CLI_COMMAND,
|
|
1418
|
-
description: "Scan documentation for stale code references.",
|
|
1419
|
-
usage: DOCS_DRIFT_CLI_COMMAND,
|
|
1420
|
-
projectRequired: true,
|
|
1421
|
-
run: (context, args) => executeDrift(context, args, options)
|
|
1422
|
-
};
|
|
1423
|
-
}
|
|
1424
|
-
var DOCS_DRIFT_RUNTIME_CLI_COMMAND = createDocsDriftRuntimeCliCommand();
|
|
1425
|
-
async function runDriftCli(args, options = {}) {
|
|
1426
|
-
const docsGlobs = [];
|
|
1427
|
-
const ignoreGlobs = [];
|
|
1428
|
-
let json = false;
|
|
1429
|
-
let failOnDrift = false;
|
|
1430
|
-
for (let index = 0;index < args.length; index += 1) {
|
|
1431
|
-
const arg = args[index];
|
|
1432
|
-
if (arg === "--json") {
|
|
1433
|
-
json = true;
|
|
1434
|
-
continue;
|
|
1435
|
-
}
|
|
1436
|
-
if (arg === "--fail-on-drift") {
|
|
1437
|
-
failOnDrift = true;
|
|
1438
|
-
continue;
|
|
1439
|
-
}
|
|
1440
|
-
if (arg === "--docs") {
|
|
1441
|
-
docsGlobs.push(takeOptionValue(args, index, arg));
|
|
1442
|
-
index += 1;
|
|
1443
|
-
continue;
|
|
1444
|
-
}
|
|
1445
|
-
if (arg === "--ignore") {
|
|
1446
|
-
ignoreGlobs.push(takeOptionValue(args, index, arg));
|
|
1447
|
-
index += 1;
|
|
1448
|
-
continue;
|
|
1449
|
-
}
|
|
1450
|
-
throw new Error(`Unknown rig drift argument: ${arg}`);
|
|
1451
|
-
}
|
|
1452
|
-
const report = await detectDrift({
|
|
1453
|
-
projectRoot: options.projectRoot ?? process.cwd(),
|
|
1454
|
-
...docsGlobs.length > 0 ? { docsGlobs } : {},
|
|
1455
|
-
...ignoreGlobs.length > 0 ? { ignoreGlobs } : {}
|
|
1456
|
-
});
|
|
1457
|
-
const write = options.write ?? ((message) => console.log(message));
|
|
1458
|
-
if (json) {
|
|
1459
|
-
write(JSON.stringify(report));
|
|
1460
|
-
} else {
|
|
1461
|
-
write(`Scanned ${report.scanned} doc(s); ${report.findings.length} drift finding(s).`);
|
|
1462
|
-
for (const finding of report.findings) {
|
|
1463
|
-
write(`${finding.confidence.toUpperCase()} ${finding.kind} ${finding.docPath}${finding.line ? `:${finding.line}` : ""} ${finding.reference ?? ""} \u2014 ${finding.detail}`);
|
|
1464
|
-
}
|
|
1465
|
-
}
|
|
1466
|
-
const high = highConfidenceDriftFindings(report);
|
|
1467
|
-
if (failOnDrift && high.length > 0) {
|
|
1468
|
-
options.writeError?.(`${high.length} high-confidence drift finding(s).`);
|
|
1469
|
-
return 2;
|
|
1470
|
-
}
|
|
1471
|
-
return 0;
|
|
1472
|
-
}
|
|
1473
|
-
// packages/standard-plugin/src/drift/judge.ts
|
|
1474
|
-
async function judgeDocumentationDrift(provider, input) {
|
|
1475
|
-
const result = await provider.judge(input);
|
|
1476
|
-
return result.mismatches.map((mismatch) => ({
|
|
1477
|
-
kind: "semantic-mismatch",
|
|
1478
|
-
docPath: input.docPath,
|
|
1479
|
-
line: mismatch.line ?? null,
|
|
1480
|
-
reference: mismatch.reference ?? input.reference ?? null,
|
|
1481
|
-
detail: mismatch.detail,
|
|
1482
|
-
confidence: mismatch.confidence ?? "medium"
|
|
1483
|
-
}));
|
|
1484
|
-
}
|
|
1485
|
-
// packages/standard-plugin/src/plugin.ts
|
|
1486
|
-
var DOCS_HEALTH_PANEL_ID = "docs-health";
|
|
1487
1700
|
function requireStringField(config, field, kind) {
|
|
1488
1701
|
const value = config[field];
|
|
1489
1702
|
if (!value) {
|
|
@@ -1539,7 +1752,8 @@ function createDocsHealthPanelProducer(options = {}) {
|
|
|
1539
1752
|
const projectRoot = panelProjectRoot(context);
|
|
1540
1753
|
if (!projectRoot)
|
|
1541
1754
|
return;
|
|
1542
|
-
const
|
|
1755
|
+
const { detectDrift: detectDrift2 } = await Promise.resolve().then(() => (init_detect(), exports_detect));
|
|
1756
|
+
const report = await detectDrift2({
|
|
1543
1757
|
projectRoot,
|
|
1544
1758
|
...options.docsGlobs !== undefined ? { docsGlobs: options.docsGlobs } : {},
|
|
1545
1759
|
...options.ignoreGlobs !== undefined ? { ignoreGlobs: options.ignoreGlobs } : {}
|
|
@@ -1557,24 +1771,46 @@ function createDocsHealthPanelProducer(options = {}) {
|
|
|
1557
1771
|
};
|
|
1558
1772
|
};
|
|
1559
1773
|
}
|
|
1560
|
-
function
|
|
1774
|
+
function createLazyDocsDriftValidator(options = {}) {
|
|
1775
|
+
return {
|
|
1776
|
+
...DOCS_DRIFT_VALIDATOR,
|
|
1777
|
+
async run(ctx) {
|
|
1778
|
+
const { runDocsDriftValidation: runDocsDriftValidation2 } = await Promise.resolve().then(() => (init_plugin(), exports_plugin));
|
|
1779
|
+
return runDocsDriftValidation2({
|
|
1780
|
+
projectRoot: ctx.workspaceRoot,
|
|
1781
|
+
...options.docsGlobs !== undefined ? { docsGlobs: options.docsGlobs } : {},
|
|
1782
|
+
...options.ignoreGlobs !== undefined ? { ignoreGlobs: options.ignoreGlobs } : {},
|
|
1783
|
+
...options.failOnDrift !== undefined ? { failOnDrift: options.failOnDrift } : {}
|
|
1784
|
+
});
|
|
1785
|
+
}
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
function createLazyDocsDriftGateStage(options = {}) {
|
|
1789
|
+
return async (ctx) => {
|
|
1790
|
+
const { createDocsDriftGateStage: createDocsDriftGateStage2 } = await Promise.resolve().then(() => (init_plugin(), exports_plugin));
|
|
1791
|
+
return createDocsDriftGateStage2(options)(ctx);
|
|
1792
|
+
};
|
|
1793
|
+
}
|
|
1794
|
+
function createLazyDocsDriftRuntimeCliCommand(options = {}) {
|
|
1795
|
+
return {
|
|
1796
|
+
id: DOCS_DRIFT_CLI_ID,
|
|
1797
|
+
family: "drift",
|
|
1798
|
+
command: DOCS_DRIFT_CLI_COMMAND,
|
|
1799
|
+
description: "Scan documentation for stale code references.",
|
|
1800
|
+
usage: DOCS_DRIFT_CLI_COMMAND,
|
|
1801
|
+
projectRequired: true,
|
|
1802
|
+
run: async (context, args) => {
|
|
1803
|
+
const { executeDrift: executeDrift2 } = await Promise.resolve().then(() => (init_plugin(), exports_plugin));
|
|
1804
|
+
return executeDrift2(context, args, options);
|
|
1805
|
+
}
|
|
1806
|
+
};
|
|
1807
|
+
}
|
|
1808
|
+
function createStandardDocsDriftPlugin(opts = {}) {
|
|
1561
1809
|
return definePlugin({
|
|
1562
|
-
name: "rig-
|
|
1810
|
+
name: "@rig/standard-plugin:docs-drift",
|
|
1563
1811
|
version: "0.1.0",
|
|
1564
1812
|
contributes: {
|
|
1565
1813
|
validators: [DOCS_DRIFT_VALIDATOR],
|
|
1566
|
-
taskSources: [
|
|
1567
|
-
{
|
|
1568
|
-
id: "std:github-issues",
|
|
1569
|
-
kind: "github-issues",
|
|
1570
|
-
description: "GitHub Issues via gh CLI"
|
|
1571
|
-
},
|
|
1572
|
-
{
|
|
1573
|
-
id: "std:files",
|
|
1574
|
-
kind: "files",
|
|
1575
|
-
description: "JSON files in a local directory"
|
|
1576
|
-
}
|
|
1577
|
-
],
|
|
1578
1814
|
capabilities: [
|
|
1579
1815
|
{ id: DOCS_DRIFT_CAPABILITY_ID, title: "Documentation drift detection", commandId: DOCS_DRIFT_CLI_ID, panelId: DOCS_HEALTH_PANEL_ID }
|
|
1580
1816
|
],
|
|
@@ -1593,15 +1829,47 @@ function standardPlugin(opts = {}) {
|
|
|
1593
1829
|
stageMutations: [DOCS_DRIFT_STAGE_MUTATION]
|
|
1594
1830
|
}
|
|
1595
1831
|
}, {
|
|
1596
|
-
validators: [
|
|
1597
|
-
stages: { [DOCS_DRIFT_STAGE_ID]:
|
|
1832
|
+
validators: [createLazyDocsDriftValidator(opts)],
|
|
1833
|
+
stages: { [DOCS_DRIFT_STAGE_ID]: createLazyDocsDriftGateStage(opts) },
|
|
1598
1834
|
featureCapabilities: [
|
|
1599
1835
|
{ id: DOCS_DRIFT_CAPABILITY_ID, title: "Documentation drift detection", commandId: DOCS_DRIFT_CLI_ID, panelId: DOCS_HEALTH_PANEL_ID }
|
|
1600
1836
|
],
|
|
1601
1837
|
panels: [
|
|
1602
|
-
{ id: DOCS_HEALTH_PANEL_ID, slot: "capability", title: "Documentation drift", capabilityId: DOCS_DRIFT_CAPABILITY_ID, produce: createDocsHealthPanelProducer(opts
|
|
1838
|
+
{ id: DOCS_HEALTH_PANEL_ID, slot: "capability", title: "Documentation drift", capabilityId: DOCS_DRIFT_CAPABILITY_ID, produce: createDocsHealthPanelProducer(opts) }
|
|
1603
1839
|
],
|
|
1604
|
-
cliCommands: [
|
|
1840
|
+
cliCommands: [createLazyDocsDriftRuntimeCliCommand(opts)]
|
|
1841
|
+
});
|
|
1842
|
+
}
|
|
1843
|
+
function createStandardProductEntrypointPlugin() {
|
|
1844
|
+
return definePlugin({
|
|
1845
|
+
name: "@rig/standard-plugin:product-entrypoint",
|
|
1846
|
+
version: "0.1.0",
|
|
1847
|
+
contributes: {
|
|
1848
|
+
cliCommands: STANDARD_PRODUCT_COMMANDS.map(standardProductCliCommandMetadata)
|
|
1849
|
+
}
|
|
1850
|
+
}, {
|
|
1851
|
+
cliCommands: STANDARD_PRODUCT_COMMANDS.map(createStandardProductCliCommand)
|
|
1852
|
+
});
|
|
1853
|
+
}
|
|
1854
|
+
function createStandardTaskSourcesPlugin(opts = {}) {
|
|
1855
|
+
return definePlugin({
|
|
1856
|
+
name: "@rig/standard-plugin:task-sources",
|
|
1857
|
+
version: "0.1.0",
|
|
1858
|
+
contributes: {
|
|
1859
|
+
taskSources: [
|
|
1860
|
+
{
|
|
1861
|
+
id: "std:github-issues",
|
|
1862
|
+
kind: "github-issues",
|
|
1863
|
+
description: "GitHub Issues via gh CLI"
|
|
1864
|
+
},
|
|
1865
|
+
{
|
|
1866
|
+
id: "std:files",
|
|
1867
|
+
kind: "files",
|
|
1868
|
+
description: "JSON files in a local directory"
|
|
1869
|
+
}
|
|
1870
|
+
]
|
|
1871
|
+
}
|
|
1872
|
+
}, {
|
|
1605
1873
|
taskSources: [
|
|
1606
1874
|
{
|
|
1607
1875
|
id: "std:github-issues",
|
|
@@ -1612,7 +1880,7 @@ function standardPlugin(opts = {}) {
|
|
|
1612
1880
|
owner: requireStringField(config, "owner", "github-issues"),
|
|
1613
1881
|
repo: requireStringField(config, "repo", "github-issues")
|
|
1614
1882
|
};
|
|
1615
|
-
const credentialProviderOptions = context?.projectRoot ? { stateDir:
|
|
1883
|
+
const credentialProviderOptions = context?.projectRoot ? { stateDir: resolve5(context.projectRoot, ".rig", "state") } : {};
|
|
1616
1884
|
options.credentialProvider = opts.githubCredentialProvider ?? createStateGitHubCredentialProvider(credentialProviderOptions);
|
|
1617
1885
|
if (opts.githubWorkspaceId)
|
|
1618
1886
|
options.workspaceId = opts.githubWorkspaceId;
|
|
@@ -1659,27 +1927,19 @@ function standardPlugin(opts = {}) {
|
|
|
1659
1927
|
});
|
|
1660
1928
|
}
|
|
1661
1929
|
export {
|
|
1662
|
-
runDriftCli,
|
|
1663
|
-
runDocsDriftValidation,
|
|
1664
|
-
makeDriftGit,
|
|
1665
|
-
judgeDocumentationDrift,
|
|
1666
|
-
extractDriftReferences,
|
|
1667
|
-
executeDrift,
|
|
1668
|
-
driftGateResult,
|
|
1669
|
-
detectStaleAnchors,
|
|
1670
|
-
detectDrift,
|
|
1671
|
-
detectDeletedReferences,
|
|
1672
|
-
standardPlugin as default,
|
|
1673
1930
|
createStateGitHubCredentialProvider,
|
|
1931
|
+
createStandardTaskSourcesPlugin,
|
|
1932
|
+
createStandardProductEntrypointPlugin,
|
|
1933
|
+
createStandardDocsDriftPlugin,
|
|
1674
1934
|
createGitHubIssuesTaskSource,
|
|
1675
1935
|
createFilesTaskSource,
|
|
1676
1936
|
createEnvGitHubCredentialProvider,
|
|
1677
|
-
createDocsDriftValidator,
|
|
1678
|
-
createDocsDriftRuntimeCliCommand,
|
|
1679
1937
|
DOCS_HEALTH_PANEL_ID,
|
|
1680
1938
|
DOCS_DRIFT_VALIDATOR_ID,
|
|
1939
|
+
DOCS_DRIFT_VALIDATOR,
|
|
1940
|
+
DOCS_DRIFT_STAGE_MUTATION,
|
|
1681
1941
|
DOCS_DRIFT_STAGE_ID,
|
|
1682
|
-
DOCS_DRIFT_RUNTIME_CLI_COMMAND,
|
|
1683
1942
|
DOCS_DRIFT_CLI_ID,
|
|
1943
|
+
DOCS_DRIFT_CLI_COMMAND,
|
|
1684
1944
|
DOCS_DRIFT_CAPABILITY_ID
|
|
1685
1945
|
};
|