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