@h-rig/standard-plugin 0.0.6-alpha.15 → 0.0.6-alpha.151

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.
Files changed (45) hide show
  1. package/dist/src/blocker-classifier.d.ts +1 -0
  2. package/dist/src/blocker-classifier.js +18 -0
  3. package/dist/src/bundle.d.ts +7 -0
  4. package/dist/src/bundle.js +1859 -0
  5. package/dist/src/cli-surface.d.ts +1 -0
  6. package/dist/src/cli-surface.js +12 -0
  7. package/dist/src/default-lifecycle.d.ts +2 -0
  8. package/dist/src/default-lifecycle.js +12 -0
  9. package/dist/src/dependency-graph.d.ts +1 -0
  10. package/dist/src/dependency-graph.js +22 -0
  11. package/dist/src/drift/__fixtures__/temp-repo.d.ts +9 -0
  12. package/dist/src/drift/__fixtures__/temp-repo.js +41 -0
  13. package/dist/src/drift/detect.d.ts +11 -0
  14. package/dist/src/drift/detect.js +299 -0
  15. package/dist/src/drift/extract-refs.d.ts +7 -0
  16. package/dist/src/drift/extract-refs.js +60 -0
  17. package/dist/src/drift/git-adapter.d.ts +7 -0
  18. package/dist/src/drift/git-adapter.js +63 -0
  19. package/dist/src/drift/judge.d.ts +19 -0
  20. package/dist/src/drift/judge.js +16 -0
  21. package/dist/src/drift/metadata.d.ts +13 -0
  22. package/dist/src/drift/metadata.js +33 -0
  23. package/dist/src/drift/plugin.d.ts +53 -0
  24. package/dist/src/drift/plugin.js +507 -0
  25. package/dist/src/files-source.d.ts +18 -0
  26. package/dist/src/files-source.js +4 -3
  27. package/dist/src/github-issues-source.d.ts +80 -0
  28. package/dist/src/github-issues-source.js +482 -53
  29. package/dist/src/index.d.ts +13 -0
  30. package/dist/src/index.js +1369 -68
  31. package/dist/src/lifecycle-closeout.d.ts +2 -0
  32. package/dist/src/lifecycle-closeout.js +6 -0
  33. package/dist/src/planning.d.ts +1 -0
  34. package/dist/src/planning.js +14 -0
  35. package/dist/src/plugin.d.ts +24 -0
  36. package/dist/src/plugin.js +1814 -0
  37. package/dist/src/product-plugin.d.ts +3 -0
  38. package/dist/src/product-plugin.js +18 -0
  39. package/dist/src/run-worker-panels.d.ts +15 -0
  40. package/dist/src/run-worker-panels.js +53 -0
  41. package/dist/src/supervisor.d.ts +1 -0
  42. package/dist/src/supervisor.js +12 -0
  43. package/dist/src/task-cli.d.ts +1 -0
  44. package/dist/src/task-cli.js +14 -0
  45. package/package.json +67 -5
@@ -0,0 +1,1859 @@
1
+ // @bun
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);
17
+
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."
27
+ };
28
+ DOCS_DRIFT_STAGE_MUTATION = Schema.decodeUnknownSync(StageMutationSchema)({
29
+ op: "insert",
30
+ stage: {
31
+ id: DOCS_DRIFT_STAGE_ID,
32
+ kind: "gate",
33
+ before: ["merge-gate"],
34
+ after: ["open-pr"]
35
+ },
36
+ contributedBy: DOCS_DRIFT_STAGE_ID
37
+ });
38
+ });
39
+
40
+ // packages/standard-plugin/src/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 "";
48
+ }
49
+ return fenced ? "" : line;
50
+ });
51
+ }
52
+ function normalizeToken(raw) {
53
+ return raw.trim().replace(/^['"]|['"]$/g, "").replace(/[),.;:]+$/g, "").replace(/#L\d+(?:-L\d+)?$/i, "");
54
+ }
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;
63
+ }
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);
85
+ }
86
+ for (const match of line.matchAll(MARKDOWN_LINK)) {
87
+ pushReference(refs, seen, match[1] ?? "", lineNumber);
88
+ }
89
+ }
90
+ return refs;
91
+ }
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;
105
+ }
106
+ function lineCount(output) {
107
+ const trimmed = output.trim();
108
+ return trimmed ? trimmed.split(/\r?\n/).length : 0;
109
+ }
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
+ }
129
+ return {
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
+ }
156
+ };
157
+ }
158
+ var execFileAsync;
159
+ var init_git_adapter = __esm(() => {
160
+ execFileAsync = promisify(execFile);
161
+ });
162
+
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);
185
+ }
186
+ function isDefaultDoc(path) {
187
+ const lower = basename2(path).toLowerCase();
188
+ return (path.endsWith(".md") || path.endsWith(".mdx")) && !lower.startsWith("changelog") && !lower.includes("generated");
189
+ }
190
+ function isIgnored(path, patterns) {
191
+ return (patterns ?? []).some((pattern) => globLikeMatch(path, pattern));
192
+ }
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);
217
+ }
218
+ }
219
+ await visit(root);
220
+ return files.sort();
221
+ }
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;
235
+ }
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"
244
+ };
245
+ }
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
+ };
255
+ }
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));
266
+ }
267
+ return findings;
268
+ }
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
+ }
283
+ }
284
+ return findings;
285
+ }
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;
301
+ }
302
+ }
303
+ return {
304
+ generatedAt: new Date().toISOString(),
305
+ scanned: docs.length,
306
+ degraded,
307
+ findings
308
+ };
309
+ }
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");
371
+ }
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" };
378
+ }
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
+ });
412
+ }
413
+ };
414
+ }
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;
420
+ }
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 };
428
+ }
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 };
439
+ }
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
+ `));
476
+ }
477
+ return { ok: !failed, group: "drift", command: "scan", details };
478
+ }
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
+ };
489
+ }
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;
500
+ }
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;
514
+ }
515
+ throw new Error(`Unknown rig drift argument: ${arg}`);
516
+ }
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}`);
529
+ }
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
+ };
580
+ }
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/cli-surface.ts
599
+ import {
600
+ STANDARD_CLI_SURFACE_PLUGIN_NAME,
601
+ createStandardCliSurfacePlugin,
602
+ standardCliSurfacePlugin
603
+ } from "@rig/cli-surface-plugin/plugin";
604
+
605
+ // packages/standard-plugin/src/plugin.ts
606
+ import { resolve as resolve4 } from "path";
607
+ import { definePlugin } from "@rig/core/config";
608
+
609
+ // packages/standard-plugin/src/github-issues-source.ts
610
+ import { spawnSync } from "child_process";
611
+ import { existsSync, readFileSync } from "fs";
612
+ import { resolve } from "path";
613
+ function cleanToken(value) {
614
+ const trimmed = value?.trim() ?? "";
615
+ return trimmed.length > 0 ? trimmed : null;
616
+ }
617
+ function createStateGitHubCredentialProvider(options = {}) {
618
+ const addCandidate = (candidates, path) => {
619
+ const trimmed = path?.trim();
620
+ if (!trimmed)
621
+ return;
622
+ const resolved = resolve(trimmed);
623
+ if (!candidates.includes(resolved))
624
+ candidates.push(resolved);
625
+ };
626
+ const addStateDir = (candidates, dir) => {
627
+ const trimmed = dir?.trim();
628
+ if (!trimmed)
629
+ return;
630
+ addCandidate(candidates, resolve(trimmed, "github-auth.json"));
631
+ };
632
+ const addProjectStateDir = (candidates, root) => {
633
+ const trimmed = root?.trim();
634
+ if (!trimmed)
635
+ return;
636
+ addStateDir(candidates, resolve(trimmed, ".rig", "state"));
637
+ };
638
+ const stateFileCandidates = () => {
639
+ const candidates = [];
640
+ addCandidate(candidates, options.stateFile ?? process.env.RIG_GITHUB_AUTH_STATE_FILE);
641
+ addStateDir(candidates, options.stateDir);
642
+ addStateDir(candidates, process.env.RIG_STATE_DIR);
643
+ addProjectStateDir(candidates, process.env.PROJECT_RIG_ROOT);
644
+ addProjectStateDir(candidates, process.env.RIG_PROJECT_ROOT);
645
+ addProjectStateDir(candidates, process.env.RIG_HOST_PROJECT_ROOT);
646
+ addProjectStateDir(candidates, process.cwd());
647
+ return candidates;
648
+ };
649
+ const readToken = () => {
650
+ for (const stateFile of stateFileCandidates()) {
651
+ if (!existsSync(stateFile))
652
+ continue;
653
+ try {
654
+ const parsed = JSON.parse(readFileSync(stateFile, "utf8"));
655
+ const token = typeof parsed.token === "string" ? cleanToken(parsed.token) : null;
656
+ if (token)
657
+ return token;
658
+ } catch {}
659
+ }
660
+ return null;
661
+ };
662
+ return {
663
+ async resolveGitHubToken(input) {
664
+ const token = readToken();
665
+ if (input.purpose === "selected-repo") {
666
+ return { token: token ?? cleanToken(process.env.RIG_GITHUB_SELECTED_TOKEN ?? null) ?? "", source: "signed-in-user" };
667
+ }
668
+ if (token) {
669
+ return { token, source: "signed-in-user" };
670
+ }
671
+ const fallback = cleanToken(process.env.RIG_GITHUB_TOKEN ?? process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? null);
672
+ if (!fallback) {
673
+ throw new Error("No signed-in GitHub token is stored for Rig and no host admin fallback token is configured.");
674
+ }
675
+ return { token: fallback, source: "host-admin-fallback" };
676
+ }
677
+ };
678
+ }
679
+ var STATUS_LABELS = new Set(["ready", "blocked", "in-progress", "under-review", "failed", "cancelled"]);
680
+ var RIG_STATUS_COMMENT_MARKER = "<!-- rig:status-comment -->";
681
+ var RIG_METADATA_START = "<!-- rig:metadata:start -->";
682
+ var RIG_METADATA_END = "<!-- rig:metadata:end -->";
683
+ var RIG_STATUS_LABELS = new Set(["rig:running", "rig:pr-open", "rig:ci-fixing", "rig:merging", "rig:done", "rig:needs-attention"]);
684
+ var DEFAULT_GH_TIMEOUT_MS = 15000;
685
+ var DEFAULT_GITHUB_ISSUE_LIST_LIMIT = 1000;
686
+ function statusFor(issue) {
687
+ const state = (issue.state ?? "").toUpperCase();
688
+ if (state === "CLOSED")
689
+ return "closed";
690
+ const labelNames = labelNamesFor(issue);
691
+ if (labelNames.includes("in-progress"))
692
+ return "in_progress";
693
+ if (labelNames.includes("blocked"))
694
+ return "blocked";
695
+ if (labelNames.includes("ready"))
696
+ return "ready";
697
+ if (labelNames.includes("under-review"))
698
+ return "under_review";
699
+ if (labelNames.includes("failed"))
700
+ return "failed";
701
+ if (labelNames.includes("cancelled"))
702
+ return "cancelled";
703
+ return "open";
704
+ }
705
+ function parseIssueRefs(raw) {
706
+ const refs = [...raw.matchAll(/(?:^|[^\w/.-])(?:[\w.-]+\/[\w.-]+#|#)?(\d+)\b/g)].map((match) => match[1]).filter((value) => Boolean(value));
707
+ return [...new Set(refs)];
708
+ }
709
+ function metadataKeyPattern(keys) {
710
+ return new RegExp(`^(?:${keys.map((key) => key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")}):\\s*(.*)$`, "i");
711
+ }
712
+ function parseMetadataList(body, keys) {
713
+ const block = body.match(/<!-- rig:metadata:start -->\s*([\s\S]*?)\s*<!-- rig:metadata:end -->/);
714
+ if (!block)
715
+ return [];
716
+ const lines = block[1].split(/\r?\n/);
717
+ const values = [];
718
+ const keyPattern = metadataKeyPattern(keys);
719
+ for (let index = 0;index < lines.length; index += 1) {
720
+ const line = lines[index];
721
+ const sameLine = line.match(keyPattern);
722
+ if (!sameLine)
723
+ continue;
724
+ const inlineValue = sameLine[1]?.trim() ?? "";
725
+ if (inlineValue) {
726
+ values.push(...parseIssueRefs(inlineValue));
727
+ continue;
728
+ }
729
+ for (let cursor = index + 1;cursor < lines.length; cursor += 1) {
730
+ const item = lines[cursor].match(/^\s*-\s*(.+)$/);
731
+ if (!item)
732
+ break;
733
+ values.push(...parseIssueRefs(item[1]));
734
+ }
735
+ }
736
+ return [...new Set(values)];
737
+ }
738
+ function bodyWithoutRigMetadataBlock(body) {
739
+ return body.replace(/<!-- rig:metadata:start -->\s*[\s\S]*?\s*<!-- rig:metadata:end -->/g, "");
740
+ }
741
+ function parseBodyKeyRefs(body, keys) {
742
+ const keyPattern = metadataKeyPattern(keys);
743
+ const values = bodyWithoutRigMetadataBlock(body).split(/\r?\n/).flatMap((line) => {
744
+ const match = line.match(keyPattern);
745
+ return match?.[1] ? parseIssueRefs(match[1]) : [];
746
+ });
747
+ return [...new Set(values)];
748
+ }
749
+ function parseDeps(body) {
750
+ const keys = ["depends-on", "deps", "blocked-by", "blocked_by"];
751
+ return [...new Set([...parseBodyKeyRefs(body, keys), ...parseMetadataList(body, keys)])];
752
+ }
753
+ function parseParents(body) {
754
+ const keys = ["parents", "parent"];
755
+ return [...new Set([...parseBodyKeyRefs(body, keys), ...parseMetadataList(body, keys)])];
756
+ }
757
+ function issueTypeFor(issue) {
758
+ const labels = labelNamesFor(issue);
759
+ const typed = labels.find((l) => l.startsWith("type:"));
760
+ if (typed)
761
+ return typed.slice("type:".length);
762
+ if (labels.includes("epic"))
763
+ return "epic";
764
+ return "task";
765
+ }
766
+ function issueToTask(issue, repo, nativeDependencies) {
767
+ const labelNames = labelNamesFor(issue);
768
+ const scope = labelNames.filter((l) => l.startsWith("scope:")).map((l) => l.slice("scope:".length));
769
+ const roleLabel = labelNames.find((l) => l.startsWith("role:"));
770
+ const role = roleLabel ? roleLabel.slice("role:".length) : undefined;
771
+ const validators = labelNames.filter((l) => l.startsWith("validator:")).map((l) => l.slice("validator:".length));
772
+ const body = issue.body ?? "";
773
+ const issueNodeId = issue.id ?? issue.nodeId ?? issue.node_id;
774
+ const parsedDeps = parseDeps(body);
775
+ const deps = nativeDependencies?.deps ? [...new Set([...parsedDeps, ...nativeDependencies.deps])] : parsedDeps;
776
+ return {
777
+ id: String(issue.number),
778
+ ...typeof issueNodeId === "string" && issueNodeId.trim() ? { issueNodeId: issueNodeId.trim() } : {},
779
+ deps,
780
+ status: statusFor(issue),
781
+ title: issue.title,
782
+ body,
783
+ scope,
784
+ role,
785
+ validators,
786
+ url: issue.url ?? issue.html_url,
787
+ issueType: issueTypeFor(issue),
788
+ sourceIssueId: `${repo}#${issue.number}`,
789
+ parentChildDeps: parseParents(body),
790
+ labels: labelNames,
791
+ ...nativeDependencies?.degraded ? { nativeDependenciesDegraded: true, nativeDependenciesError: nativeDependencies.degraded } : {},
792
+ raw: issue
793
+ };
794
+ }
795
+ function labelNamesFor(issue) {
796
+ return (issue.labels ?? []).flatMap((label) => {
797
+ if (typeof label === "string")
798
+ return [label];
799
+ return typeof label.name === "string" ? [label.name] : [];
800
+ });
801
+ }
802
+ function yamlScalar(value) {
803
+ if (Array.isArray(value)) {
804
+ return value.length === 0 ? "[]" : `
805
+ ${value.map((entry) => ` - ${String(entry)}`).join(`
806
+ `)}`;
807
+ }
808
+ if (value && typeof value === "object")
809
+ return JSON.stringify(value);
810
+ return String(value);
811
+ }
812
+ function updateRigOwnedMetadataBlock(body, metadata) {
813
+ const rendered = [
814
+ RIG_METADATA_START,
815
+ ...Object.entries(metadata).map(([key, value]) => Array.isArray(value) ? `${key}:${yamlScalar(value)}` : `${key}: ${yamlScalar(value)}`),
816
+ RIG_METADATA_END
817
+ ].join(`
818
+ `);
819
+ const pattern = new RegExp(`${RIG_METADATA_START.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*[\\s\\S]*?\\s*${RIG_METADATA_END.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`);
820
+ if (pattern.test(body))
821
+ return body.replace(pattern, rendered);
822
+ return body.trim().length > 0 ? `${body.trimEnd()}
823
+
824
+ ${rendered}
825
+ ` : `${rendered}
826
+ `;
827
+ }
828
+ function buildRigStickyStatusComment(input) {
829
+ const lines = [
830
+ RIG_STATUS_COMMENT_MARKER,
831
+ `### Rig status: ${input.status}`,
832
+ "",
833
+ input.summary
834
+ ];
835
+ if (input.runId)
836
+ lines.push("", `- Run: ${input.runId}`);
837
+ if (input.prUrl)
838
+ lines.push(`- PR: ${input.prUrl}`);
839
+ for (const detail of input.details ?? [])
840
+ lines.push(`- ${detail}`);
841
+ return lines.join(`
842
+ `);
843
+ }
844
+ function isRigStickyStatusComment(body) {
845
+ return body.includes(RIG_STATUS_COMMENT_MARKER);
846
+ }
847
+ function ghSpawnOptions(extraEnv, timeoutMs) {
848
+ const env = {
849
+ ...process.env,
850
+ ...process.env.GH_TOKEN !== undefined ? { GH_TOKEN: process.env.GH_TOKEN } : {},
851
+ ...process.env.GITHUB_TOKEN !== undefined ? { GITHUB_TOKEN: process.env.GITHUB_TOKEN } : {},
852
+ ...process.env.RIG_GITHUB_TOKEN !== undefined ? { RIG_GITHUB_TOKEN: process.env.RIG_GITHUB_TOKEN } : {}
853
+ };
854
+ for (const [key, value] of Object.entries(extraEnv ?? {})) {
855
+ if (value === undefined)
856
+ delete env[key];
857
+ else
858
+ env[key] = value;
859
+ }
860
+ return {
861
+ encoding: "utf-8",
862
+ timeout: timeoutMs,
863
+ env
864
+ };
865
+ }
866
+ function credentialEnv(token) {
867
+ const clean = token?.trim() ?? "";
868
+ if (clean)
869
+ return { GH_TOKEN: clean, GITHUB_TOKEN: clean, RIG_GITHUB_TOKEN: clean };
870
+ return { GH_TOKEN: undefined, GITHUB_TOKEN: undefined, RIG_GITHUB_TOKEN: undefined };
871
+ }
872
+ async function resolveCredentialEnv(opts, purpose) {
873
+ if (!opts.credentialProvider)
874
+ return;
875
+ const input = {
876
+ owner: opts.owner,
877
+ repo: opts.repo,
878
+ workspaceId: opts.workspaceId ?? `${opts.owner}/${opts.repo}`,
879
+ ...opts.userId ? { userId: opts.userId } : {},
880
+ purpose
881
+ };
882
+ const resolved = await opts.credentialProvider.resolveGitHubToken(input);
883
+ return credentialEnv(resolved.token);
884
+ }
885
+ function tokenDiagnostic(value) {
886
+ const clean = value?.trim() ?? "";
887
+ return clean ? `present(len=${clean.length})` : "missing";
888
+ }
889
+ function runGh(bin, args, spawn, extraEnv, timeoutMs) {
890
+ const options = ghSpawnOptions(extraEnv, timeoutMs);
891
+ const res = spawn(bin, [...args], options);
892
+ assertGhSuccess(args, res, options.env);
893
+ if (!res.stdout || res.stdout.trim() === "")
894
+ return [];
895
+ return JSON.parse(res.stdout);
896
+ }
897
+ function runGhVoid(bin, args, spawn, extraEnv, timeoutMs) {
898
+ const options = ghSpawnOptions(extraEnv, timeoutMs);
899
+ const res = spawn(bin, [...args], options);
900
+ assertGhSuccess(args, res, options.env);
901
+ }
902
+ function assertGhSuccess(args, res, env) {
903
+ if (res.error) {
904
+ const msg = res.error.message ?? String(res.error);
905
+ throw new Error(`gh CLI not available \u2014 install gh (brew install gh / apt install gh): ${msg}`);
906
+ }
907
+ if (res.status !== 0) {
908
+ throw new Error(`gh ${args.join(" ")} failed (exit ${res.status}): ${res.stderr}
909
+ [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)}`);
910
+ }
911
+ }
912
+ var DEFAULT_PROJECT_STATUSES = {
913
+ todo: "Todo",
914
+ running: "In Progress",
915
+ prOpen: "In Review",
916
+ ciFixing: "In Review",
917
+ merging: "In Review",
918
+ done: "Done",
919
+ needsAttention: "Needs Attention"
920
+ };
921
+ function asProjectRecord(value) {
922
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
923
+ }
924
+ function projectString(value) {
925
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
926
+ }
927
+ function projectLifecycleStatusForTaskStatus(status) {
928
+ const normalized = status?.trim().toLowerCase().replace(/[-\s]+/g, "_");
929
+ switch (normalized) {
930
+ case "draft":
931
+ case "open":
932
+ case "queued":
933
+ case "ready":
934
+ return "todo";
935
+ case "running":
936
+ case "in_progress":
937
+ return "running";
938
+ case "under_review":
939
+ case "review":
940
+ case "pr_open":
941
+ return "prOpen";
942
+ case "ci_fixing":
943
+ case "fixing":
944
+ return "ciFixing";
945
+ case "merging":
946
+ case "merge":
947
+ return "merging";
948
+ case "closed":
949
+ case "completed":
950
+ case "done":
951
+ return "done";
952
+ case "blocked":
953
+ case "cancelled":
954
+ case "failed":
955
+ case "needs_attention":
956
+ return "needsAttention";
957
+ default:
958
+ return null;
959
+ }
960
+ }
961
+ function ghGraphQLFetch(bin, spawnFn, extraEnv, timeoutMs) {
962
+ return async (query, variables) => {
963
+ const args = ["api", "graphql", "-f", `query=${query}`];
964
+ for (const [key, value] of Object.entries(variables)) {
965
+ if (value === undefined || value === null)
966
+ continue;
967
+ args.push("-f", `${key}=${String(value)}`);
968
+ }
969
+ const response = runGh(bin, args, spawnFn, extraEnv, timeoutMs);
970
+ return asProjectRecord(response)?.data ?? response;
971
+ };
972
+ }
973
+ function issueNodeIdFor(issue) {
974
+ const id = issue.id ?? issue.nodeId ?? issue.node_id;
975
+ return typeof id === "string" && id.trim().length > 0 ? id.trim() : null;
976
+ }
977
+ function nativeIssueDependencyRef(value, currentRepo) {
978
+ const record = asProjectRecord(value);
979
+ const number = typeof record?.number === "number" ? String(record.number) : projectString(record?.number);
980
+ if (!number)
981
+ return null;
982
+ const repository = asProjectRecord(record?.repository);
983
+ const owner = projectString(asProjectRecord(repository?.owner)?.login);
984
+ const name = projectString(repository?.name);
985
+ if (!owner || !name || `${owner}/${name}` === currentRepo)
986
+ return number;
987
+ return `${owner}/${name}#${number}`;
988
+ }
989
+ function nativeDependencyRefsFrom(data, currentRepo) {
990
+ const issue = asProjectRecord(asProjectRecord(data)?.node);
991
+ const blockedBy = asProjectRecord(issue?.blockedBy);
992
+ const nodes = Array.isArray(blockedBy?.nodes) ? blockedBy.nodes : [];
993
+ return [...new Set(nodes.flatMap((node) => {
994
+ const ref = nativeIssueDependencyRef(node, currentRepo);
995
+ return ref ? [ref] : [];
996
+ }))];
997
+ }
998
+ async function readNativeDependenciesForIssue(input) {
999
+ const issueId = issueNodeIdFor(input.issue);
1000
+ if (!issueId)
1001
+ return { deps: [], degraded: "GitHub issue node id is unavailable." };
1002
+ const query = `
1003
+ query RigIssueNativeDependencies($issueId: ID!) {
1004
+ node(id: $issueId) {
1005
+ ... on Issue {
1006
+ blockedBy(first: 100) {
1007
+ nodes {
1008
+ number
1009
+ repository { name owner { login } }
1010
+ }
1011
+ }
1012
+ }
1013
+ }
1014
+ }
1015
+ `;
1016
+ try {
1017
+ return {
1018
+ deps: nativeDependencyRefsFrom(await input.fetchGraphQL(query, { issueId }, "gh-cli"), input.repo)
1019
+ };
1020
+ } catch (error) {
1021
+ const detail = error instanceof Error ? error.message : String(error);
1022
+ return { deps: [], degraded: detail };
1023
+ }
1024
+ }
1025
+ function formatIssueReference(ref) {
1026
+ const clean = ref.trim().replace(/^#/, "");
1027
+ return /^\d+$/.test(clean) ? `#${clean}` : clean;
1028
+ }
1029
+ function appendReferenceLines(body, deps, parents) {
1030
+ const lines = [];
1031
+ const cleanDeps = (deps ?? []).map(formatIssueReference).filter((ref) => ref.length > 0);
1032
+ const cleanParents = (parents ?? []).map(formatIssueReference).filter((ref) => ref.length > 0);
1033
+ if (cleanDeps.length > 0)
1034
+ lines.push(`depends-on: ${cleanDeps.join(", ")}`);
1035
+ if (cleanParents.length > 0)
1036
+ lines.push(`parents: ${cleanParents.join(", ")}`);
1037
+ if (lines.length === 0)
1038
+ return body;
1039
+ return body.trim().length > 0 ? `${body.trimEnd()}
1040
+
1041
+ ${lines.join(`
1042
+ `)}` : lines.join(`
1043
+ `);
1044
+ }
1045
+ function bodyForCreatedTask(input) {
1046
+ const metadata = { ...input.metadata ?? {} };
1047
+ if (input.deps && input.deps.length > 0)
1048
+ metadata["depends-on"] = input.deps.map(formatIssueReference);
1049
+ if (input.parents && input.parents.length > 0)
1050
+ metadata.parents = input.parents.map(formatIssueReference);
1051
+ return updateRigOwnedMetadataBlock(appendReferenceLines(input.body, input.deps, input.parents), metadata);
1052
+ }
1053
+ function projectStatusFieldFrom(data, projectId) {
1054
+ const fields = asProjectRecord(asProjectRecord(asProjectRecord(data)?.node)?.fields)?.nodes;
1055
+ for (const node of Array.isArray(fields) ? fields : []) {
1056
+ const record = asProjectRecord(node);
1057
+ if (projectString(record?.name)?.toLowerCase() !== "status")
1058
+ continue;
1059
+ const id = projectString(record?.id);
1060
+ if (!id)
1061
+ continue;
1062
+ const options = Array.isArray(record?.options) ? record.options.flatMap((option) => {
1063
+ const optionRecord = asProjectRecord(option);
1064
+ const optionId = projectString(optionRecord?.id);
1065
+ const name = projectString(optionRecord?.name);
1066
+ return optionId && name ? [{ id: optionId, name }] : [];
1067
+ }) : [];
1068
+ return { id, name: "Status", options };
1069
+ }
1070
+ throw new Error(`GitHub Project ${projectId} does not expose a Status single-select field.`);
1071
+ }
1072
+ async function resolveProjectStatusField(input) {
1073
+ const query = `
1074
+ query RigProjectStatusField($projectId: ID!) {
1075
+ node(id: $projectId) {
1076
+ ... on ProjectV2 {
1077
+ fields(first: 50) {
1078
+ nodes {
1079
+ ... on ProjectV2FieldCommon { id name }
1080
+ ... on ProjectV2SingleSelectField { id name options { id name } }
1081
+ }
1082
+ }
1083
+ }
1084
+ }
1085
+ }
1086
+ `;
1087
+ return projectStatusFieldFrom(await input.fetchGraphQL(query, { projectId: input.projectId }, input.token), input.projectId);
1088
+ }
1089
+ async function ensureIssueProjectItem(input) {
1090
+ const query = `
1091
+ query RigFindProjectIssueItem($projectId: ID!) {
1092
+ node(id: $projectId) {
1093
+ ... on ProjectV2 {
1094
+ items(first: 100) { nodes { id content { ... on Issue { id } } } }
1095
+ }
1096
+ }
1097
+ }
1098
+ `;
1099
+ const data = await input.fetchGraphQL(query, { projectId: input.projectId }, input.token);
1100
+ const nodes = asProjectRecord(asProjectRecord(asProjectRecord(data)?.node)?.items)?.nodes;
1101
+ for (const node of Array.isArray(nodes) ? nodes : []) {
1102
+ const record = asProjectRecord(node);
1103
+ const content = asProjectRecord(record?.content);
1104
+ if (projectString(content?.id) === input.issueNodeId) {
1105
+ const id2 = projectString(record?.id);
1106
+ if (id2)
1107
+ return { id: id2, created: false };
1108
+ }
1109
+ }
1110
+ const mutation = `
1111
+ mutation RigAddIssueToProject($projectId: ID!, $contentId: ID!) {
1112
+ addProjectV2ItemById(input: { projectId: $projectId, contentId: $contentId }) { item { id } }
1113
+ }
1114
+ `;
1115
+ const created = await input.fetchGraphQL(mutation, { projectId: input.projectId, contentId: input.issueNodeId }, input.token);
1116
+ const addResult = asProjectRecord(asProjectRecord(created)?.addProjectV2ItemById);
1117
+ const id = projectString(asProjectRecord(addResult?.item)?.id);
1118
+ if (!id)
1119
+ throw new Error("GitHub Project item creation did not return an item id.");
1120
+ return { id, created: true };
1121
+ }
1122
+ async function updateIssueProjectStatus(input) {
1123
+ const mutation = `
1124
+ mutation RigUpdateProjectStatus($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
1125
+ updateProjectV2ItemFieldValue(input: {
1126
+ projectId: $projectId,
1127
+ itemId: $itemId,
1128
+ fieldId: $fieldId,
1129
+ value: { singleSelectOptionId: $optionId }
1130
+ }) { projectV2Item { id } }
1131
+ }
1132
+ `;
1133
+ await input.fetchGraphQL(mutation, {
1134
+ projectId: input.projectId,
1135
+ itemId: input.itemId,
1136
+ fieldId: input.fieldId,
1137
+ optionId: input.optionId
1138
+ }, input.token);
1139
+ }
1140
+ function fetchIssueNodeId(bin, repo, spawnFn, id, extraEnv, timeoutMs) {
1141
+ const issue = runGh(bin, ["issue", "view", String(id), "--repo", repo, "--json", "id"], spawnFn, extraEnv, timeoutMs);
1142
+ return projectString(issue.id) ?? projectString(issue.nodeId) ?? projectString(issue.node_id);
1143
+ }
1144
+ async function syncGitHubProjectStatus(bin, repo, spawnFn, id, status, projects, extraEnv, timeoutMs) {
1145
+ if (!projects?.enabled)
1146
+ return;
1147
+ const projectId = projectString(projects.projectId);
1148
+ if (!projectId)
1149
+ throw new Error("GitHub Projects status sync is enabled but projectId is missing.");
1150
+ const lifecycleStatus = projectLifecycleStatusForTaskStatus(status);
1151
+ if (!lifecycleStatus)
1152
+ return;
1153
+ const issueNodeId = fetchIssueNodeId(bin, repo, spawnFn, id, extraEnv, timeoutMs);
1154
+ if (!issueNodeId)
1155
+ throw new Error(`GitHub issue ${repo}#${id} did not expose a node id for Projects status sync.`);
1156
+ const projectStatus = projectString(projects.statuses?.[lifecycleStatus]) ?? DEFAULT_PROJECT_STATUSES[lifecycleStatus];
1157
+ const fetchGraphQL = ghGraphQLFetch(bin, spawnFn, extraEnv, timeoutMs);
1158
+ const field = await resolveProjectStatusField({ projectId, token: "gh-cli", fetchGraphQL });
1159
+ const option = field.options.find((candidate) => candidate.name.toLowerCase() === projectStatus.toLowerCase() || candidate.id === projectStatus);
1160
+ if (!option)
1161
+ throw new Error(`GitHub Project ${projectId} Status field does not contain option "${projectStatus}".`);
1162
+ const item = await ensureIssueProjectItem({ projectId, issueNodeId, token: "gh-cli", fetchGraphQL });
1163
+ await updateIssueProjectStatus({
1164
+ projectId,
1165
+ itemId: item.id,
1166
+ fieldId: projectString(projects.statusFieldId) ?? field.id,
1167
+ optionId: option.id,
1168
+ token: "gh-cli",
1169
+ fetchGraphQL
1170
+ });
1171
+ }
1172
+ var TERMINAL_TASK_STATUSES = new Set(["closed", "completed", "merged", "cancelled", "resolved", "done"]);
1173
+ function normalizeTaskStatusToken(status) {
1174
+ return status?.trim().toLowerCase().replace(/[-\s]+/g, "_") ?? "";
1175
+ }
1176
+ function issueUpdatesMode(value) {
1177
+ return value === "off" || value === "minimal" || value === "lifecycle" ? value : "lifecycle";
1178
+ }
1179
+ function isTerminalTaskStatus(status) {
1180
+ return TERMINAL_TASK_STATUSES.has(normalizeTaskStatusToken(status));
1181
+ }
1182
+ function shouldWriteIssueUpdate(mode, status) {
1183
+ if (mode === "off")
1184
+ return false;
1185
+ if (mode === "lifecycle")
1186
+ return true;
1187
+ return isTerminalTaskStatus(status);
1188
+ }
1189
+ function isRunningStatus(status) {
1190
+ return normalizeTaskStatusToken(status) === "running";
1191
+ }
1192
+ function assignRunningIssue(bin, repo, spawnFn, id, assignee, extraEnv, timeoutMs) {
1193
+ runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, "--add-assignee", assignee?.trim() || "@me"], spawnFn, extraEnv, timeoutMs);
1194
+ }
1195
+ function statusLabelFor(status) {
1196
+ switch (status) {
1197
+ case "running":
1198
+ case "in_progress":
1199
+ return "in-progress";
1200
+ case "blocked":
1201
+ return "blocked";
1202
+ case "ready":
1203
+ return "ready";
1204
+ case "under_review":
1205
+ return "under-review";
1206
+ case "failed":
1207
+ return "failed";
1208
+ case "cancelled":
1209
+ return "cancelled";
1210
+ case "ci_fixing":
1211
+ return "under-review";
1212
+ case "merging":
1213
+ return "under-review";
1214
+ case "needs_attention":
1215
+ return "blocked";
1216
+ case "closed":
1217
+ case "completed":
1218
+ case "open":
1219
+ return null;
1220
+ default:
1221
+ throw new Error(`unsupported status: ${status}`);
1222
+ }
1223
+ }
1224
+ function rigStatusLabelFor(status) {
1225
+ switch (status) {
1226
+ case "running":
1227
+ case "in_progress":
1228
+ return "rig:running";
1229
+ case "under_review":
1230
+ return "rig:pr-open";
1231
+ case "closed":
1232
+ case "completed":
1233
+ return "rig:done";
1234
+ case "ci_fixing":
1235
+ return "rig:ci-fixing";
1236
+ case "merging":
1237
+ return "rig:merging";
1238
+ case "needs_attention":
1239
+ case "failed":
1240
+ case "blocked":
1241
+ return "rig:needs-attention";
1242
+ case "ready":
1243
+ case "cancelled":
1244
+ case "open":
1245
+ return null;
1246
+ default:
1247
+ return null;
1248
+ }
1249
+ }
1250
+ async function applyIssueStatus(bin, repo, spawnFn, id, status, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs) {
1251
+ const targetLabel = status === "closed" || status === "completed" ? null : statusLabelFor(status);
1252
+ const targetRigLabel = rigStatusLabelFor(status);
1253
+ const shouldSyncLifecycle = shouldWriteIssueUpdate(issueUpdates, status);
1254
+ for (const l of [...STATUS_LABELS, ...RIG_STATUS_LABELS]) {
1255
+ if (targetLabel !== null && l === targetLabel)
1256
+ continue;
1257
+ if (targetRigLabel !== null && l === targetRigLabel)
1258
+ continue;
1259
+ try {
1260
+ runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, "--remove-label", l], spawnFn, extraEnv, timeoutMs);
1261
+ } catch {}
1262
+ }
1263
+ for (const label of [targetLabel, targetRigLabel].filter((value) => Boolean(value))) {
1264
+ try {
1265
+ runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, "--add-label", label], spawnFn, extraEnv, timeoutMs);
1266
+ } catch (error) {
1267
+ const message = error instanceof Error ? error.message : String(error);
1268
+ if (!/not found/i.test(message)) {
1269
+ throw error;
1270
+ }
1271
+ ensureStatusLabel(bin, repo, spawnFn, label, extraEnv, timeoutMs);
1272
+ runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, "--add-label", label], spawnFn, extraEnv, timeoutMs);
1273
+ }
1274
+ }
1275
+ if (isRunningStatus(status)) {
1276
+ assignRunningIssue(bin, repo, spawnFn, id, runningAssignee, extraEnv, timeoutMs);
1277
+ if (shouldSyncLifecycle) {
1278
+ upsertRigStickyComment(bin, repo, spawnFn, String(id), buildRigStickyStatusComment({
1279
+ status: "running",
1280
+ summary: "Rig run started."
1281
+ }), extraEnv, timeoutMs);
1282
+ }
1283
+ }
1284
+ if (shouldSyncLifecycle) {
1285
+ await syncGitHubProjectStatus(bin, repo, spawnFn, id, status, projects, extraEnv, timeoutMs);
1286
+ }
1287
+ if (status === "closed" || status === "completed") {
1288
+ runGhVoid(bin, ["issue", "close", String(id), "--repo", repo], spawnFn, extraEnv, timeoutMs);
1289
+ }
1290
+ }
1291
+ function ensureStatusLabel(bin, repo, spawnFn, label, extraEnv, timeoutMs) {
1292
+ try {
1293
+ runGhVoid(bin, [
1294
+ "label",
1295
+ "create",
1296
+ label,
1297
+ "--repo",
1298
+ repo,
1299
+ "--color",
1300
+ "6f42c1",
1301
+ "--description",
1302
+ "Task status managed by Rig"
1303
+ ], spawnFn, extraEnv, timeoutMs);
1304
+ } catch (error) {
1305
+ const message = error instanceof Error ? error.message : String(error);
1306
+ if (!/already exists/i.test(message)) {
1307
+ throw error;
1308
+ }
1309
+ }
1310
+ }
1311
+ function upsertRigStickyComment(bin, repo, spawnFn, id, body, extraEnv, timeoutMs) {
1312
+ const comments = runGh(bin, ["api", `repos/${repo}/issues/${id}/comments`, "--paginate"], spawnFn, extraEnv, timeoutMs);
1313
+ const existing = comments.find((comment) => typeof comment.body === "string" && comment.body.includes(RIG_STATUS_COMMENT_MARKER));
1314
+ if (existing) {
1315
+ runGhVoid(bin, ["api", "-X", "PATCH", `repos/${repo}/issues/comments/${existing.id}`, "-f", `body=${body}`], spawnFn, extraEnv, timeoutMs);
1316
+ return;
1317
+ }
1318
+ runGhVoid(bin, ["api", "-X", "POST", `repos/${repo}/issues/${id}/comments`, "-f", `body=${body}`], spawnFn, extraEnv, timeoutMs);
1319
+ }
1320
+ function notifyTaskChanged(onTaskChanged, repo, id, status) {
1321
+ onTaskChanged?.({ repo, id, ...status ? { status } : {}, reason: "github-issue-updated" });
1322
+ }
1323
+ function fetchIssueBody(bin, repo, spawnFn, id, extraEnv, timeoutMs) {
1324
+ const issue = runGh(bin, [
1325
+ "issue",
1326
+ "view",
1327
+ String(id),
1328
+ "--repo",
1329
+ repo,
1330
+ "--json",
1331
+ "body"
1332
+ ], spawnFn, extraEnv, timeoutMs);
1333
+ return typeof issue.body === "string" ? issue.body : undefined;
1334
+ }
1335
+ function applyLabels(bin, repo, spawnFn, id, labels, action, extraEnv, timeoutMs) {
1336
+ for (const rawLabel of labels) {
1337
+ const label = rawLabel.trim();
1338
+ if (!label)
1339
+ continue;
1340
+ if (action === "--add-label") {
1341
+ try {
1342
+ runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, action, label], spawnFn, extraEnv, timeoutMs);
1343
+ } catch (error) {
1344
+ const message = error instanceof Error ? error.message : String(error);
1345
+ if (!/not found/i.test(message))
1346
+ throw error;
1347
+ ensureStatusLabel(bin, repo, spawnFn, label, extraEnv, timeoutMs);
1348
+ runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, action, label], spawnFn, extraEnv, timeoutMs);
1349
+ }
1350
+ continue;
1351
+ }
1352
+ try {
1353
+ runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, action, label], spawnFn, extraEnv, timeoutMs);
1354
+ } catch {}
1355
+ }
1356
+ }
1357
+ async function applyIssueUpdate(bin, repo, spawnFn, id, update, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs) {
1358
+ if (update.status) {
1359
+ await applyIssueStatus(bin, repo, spawnFn, id, update.status, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs);
1360
+ }
1361
+ if (update.comment?.trim() && shouldWriteIssueUpdate(issueUpdates, update.status)) {
1362
+ if (isRigStickyStatusComment(update.comment)) {
1363
+ upsertRigStickyComment(bin, repo, spawnFn, String(id), update.comment, extraEnv, timeoutMs);
1364
+ } else {
1365
+ runGhVoid(bin, ["issue", "comment", String(id), "--repo", repo, "--body", update.comment], spawnFn, extraEnv, timeoutMs);
1366
+ }
1367
+ }
1368
+ const editArgs = ["issue", "edit", String(id), "--repo", repo];
1369
+ if (update.title?.trim()) {
1370
+ editArgs.push("--title", update.title.trim());
1371
+ }
1372
+ const nextBody = update.metadata ? updateRigOwnedMetadataBlock(update.body ?? fetchIssueBody(bin, repo, spawnFn, id, extraEnv, timeoutMs) ?? "", update.metadata) : update.body;
1373
+ if (nextBody !== undefined) {
1374
+ editArgs.push("--body", nextBody);
1375
+ }
1376
+ if (editArgs.length > 5) {
1377
+ runGhVoid(bin, editArgs, spawnFn, extraEnv, timeoutMs);
1378
+ }
1379
+ }
1380
+ function createGitHubIssuesTaskSource(opts) {
1381
+ const bin = opts.ghBinary ?? "gh";
1382
+ const state = opts.state ?? "open";
1383
+ const repo = `${opts.owner}/${opts.repo}`;
1384
+ const spawnFn = opts.spawn ?? spawnSync;
1385
+ const timeoutMs = Math.max(1000, Math.trunc(opts.timeoutMs ?? DEFAULT_GH_TIMEOUT_MS));
1386
+ const listLimit = Math.max(1, Math.trunc(opts.listLimit ?? DEFAULT_GITHUB_ISSUE_LIST_LIMIT));
1387
+ const issueUpdates = issueUpdatesMode(opts.issueUpdates);
1388
+ async function issueToTaskWithOptionalNativeDependencies(issue, env) {
1389
+ if (!opts.useNativeDependencies)
1390
+ return issueToTask(issue, repo);
1391
+ const nativeDependencies = await readNativeDependenciesForIssue({
1392
+ issue,
1393
+ repo,
1394
+ fetchGraphQL: ghGraphQLFetch(bin, spawnFn, env, timeoutMs)
1395
+ });
1396
+ return issueToTask(issue, repo, nativeDependencies);
1397
+ }
1398
+ return {
1399
+ id: "std:github-issues",
1400
+ kind: "github-issues",
1401
+ async list() {
1402
+ const labelArg = opts.labels && opts.labels.length > 0 ? ["--label", opts.labels.join(",")] : [];
1403
+ const assigneeArg = opts.assignee?.trim() ? ["--assignee", opts.assignee.trim()] : [];
1404
+ const args = [
1405
+ "issue",
1406
+ "list",
1407
+ "--repo",
1408
+ repo,
1409
+ ...labelArg,
1410
+ ...assigneeArg,
1411
+ "--state",
1412
+ state,
1413
+ "--limit",
1414
+ String(listLimit),
1415
+ "--json",
1416
+ "number,title,body,labels,state,url,assignees,id"
1417
+ ];
1418
+ const env = await resolveCredentialEnv(opts, "selected-repo");
1419
+ const rawIssues = runGh(bin, args, spawnFn, env, timeoutMs);
1420
+ if (rawIssues.length >= listLimit) {
1421
+ 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.`);
1422
+ }
1423
+ const issues = rawIssues.filter((issue) => !issue.pull_request);
1424
+ return Promise.all(issues.map((issue) => issueToTaskWithOptionalNativeDependencies(issue, env)));
1425
+ },
1426
+ async get(id) {
1427
+ const env = await resolveCredentialEnv(opts, "selected-repo");
1428
+ let issue;
1429
+ try {
1430
+ issue = runGh(bin, [
1431
+ "issue",
1432
+ "view",
1433
+ String(id),
1434
+ "--repo",
1435
+ repo,
1436
+ "--json",
1437
+ "number,title,body,labels,state,url,assignees,id"
1438
+ ], spawnFn, env, timeoutMs);
1439
+ } catch (error) {
1440
+ const detail = error instanceof Error ? error.message : String(error);
1441
+ 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)) {
1442
+ return;
1443
+ }
1444
+ throw new Error(`Failed to read task ${id} from GitHub repo ${repo}: ${detail}`);
1445
+ }
1446
+ return issueToTaskWithOptionalNativeDependencies(issue, env);
1447
+ },
1448
+ async updateStatus(id, status) {
1449
+ const env = await resolveCredentialEnv(opts, "selected-repo");
1450
+ await applyIssueStatus(bin, repo, spawnFn, id, status, opts.projects, opts.assignee, issueUpdates, env, timeoutMs);
1451
+ notifyTaskChanged(opts.onTaskChanged, repo, id, status);
1452
+ },
1453
+ async updateTask(id, update) {
1454
+ const env = await resolveCredentialEnv(opts, "selected-repo");
1455
+ await applyIssueUpdate(bin, repo, spawnFn, id, update, opts.projects, opts.assignee, issueUpdates, env, timeoutMs);
1456
+ notifyTaskChanged(opts.onTaskChanged, repo, id, update.status);
1457
+ },
1458
+ async addLabels(id, labels) {
1459
+ const env = await resolveCredentialEnv(opts, "selected-repo");
1460
+ applyLabels(bin, repo, spawnFn, id, labels, "--add-label", env, timeoutMs);
1461
+ notifyTaskChanged(opts.onTaskChanged, repo, id);
1462
+ },
1463
+ async removeLabels(id, labels) {
1464
+ const env = await resolveCredentialEnv(opts, "selected-repo");
1465
+ applyLabels(bin, repo, spawnFn, id, labels, "--remove-label", env, timeoutMs);
1466
+ notifyTaskChanged(opts.onTaskChanged, repo, id);
1467
+ },
1468
+ async createIssue(input) {
1469
+ const env = await resolveCredentialEnv(opts, "selected-repo");
1470
+ const body = input.body ?? "";
1471
+ const args = [
1472
+ "api",
1473
+ "-X",
1474
+ "POST",
1475
+ `repos/${repo}/issues`,
1476
+ "-f",
1477
+ `title=${input.title}`,
1478
+ "-f",
1479
+ `body=${body}`,
1480
+ ...(input.labels ?? []).flatMap((label) => ["-f", `labels[]=${label}`])
1481
+ ];
1482
+ const issue = runGh(bin, args, spawnFn, env, timeoutMs);
1483
+ notifyTaskChanged(opts.onTaskChanged, repo, String(issue.number));
1484
+ return issueToTask({ ...issue, body: issue.body ?? body }, repo);
1485
+ },
1486
+ async create(input) {
1487
+ const env = await resolveCredentialEnv(opts, "selected-repo");
1488
+ const body = bodyForCreatedTask(input);
1489
+ const args = [
1490
+ "api",
1491
+ "-X",
1492
+ "POST",
1493
+ `repos/${repo}/issues`,
1494
+ "-f",
1495
+ `title=${input.title}`,
1496
+ "-f",
1497
+ `body=${body}`,
1498
+ "-f",
1499
+ "labels[]=rig:generated"
1500
+ ];
1501
+ const issue = runGh(bin, args, spawnFn, env, timeoutMs);
1502
+ notifyTaskChanged(opts.onTaskChanged, repo, String(issue.number));
1503
+ return issueToTask({ ...issue, body: issue.body ?? body }, repo);
1504
+ },
1505
+ async getIssueBody(id) {
1506
+ const env = await resolveCredentialEnv(opts, "selected-repo");
1507
+ return fetchIssueBody(bin, repo, spawnFn, id, env, timeoutMs);
1508
+ }
1509
+ };
1510
+ }
1511
+
1512
+ // packages/standard-plugin/src/files-source.ts
1513
+ import { readFileSync as readFileSync2, readdirSync, existsSync as existsSync2, statSync, writeFileSync } from "fs";
1514
+ import { join, basename, isAbsolute, resolve as resolve2 } from "path";
1515
+ var DEFAULT_PATTERN = /\.(task\.)?json$/;
1516
+ function readTaskFile(file, pattern) {
1517
+ const raw = JSON.parse(readFileSync2(file, "utf-8"));
1518
+ const inferredId = basename(file).replace(pattern, "");
1519
+ const labels = Array.isArray(raw.labels) ? raw.labels.filter((label) => typeof label === "string") : [];
1520
+ const scope = labels.filter((label) => label.startsWith("scope:")).map((label) => label.slice("scope:".length));
1521
+ const validators = labels.filter((label) => label.startsWith("validator:")).map((label) => label.slice("validator:".length));
1522
+ const roleLabel = labels.find((label) => label.startsWith("role:"));
1523
+ return {
1524
+ id: raw["id"] ?? inferredId,
1525
+ deps: raw["deps"] ?? raw["depends_on"] ?? [],
1526
+ status: raw["status"] ?? "ready",
1527
+ ...scope.length > 0 ? { scope } : {},
1528
+ ...roleLabel ? { role: roleLabel.slice("role:".length) } : {},
1529
+ ...validators.length > 0 ? { validators, validation: validators } : {},
1530
+ ...raw
1531
+ };
1532
+ }
1533
+ function createFilesTaskSource(opts) {
1534
+ const pattern = opts.pattern ?? DEFAULT_PATTERN;
1535
+ const configured = opts.path ?? opts.dir;
1536
+ if (!configured) {
1537
+ throw new Error("createFilesTaskSource: either `path` or `dir` must be provided");
1538
+ }
1539
+ const directory = isAbsolute(configured) ? configured : resolve2(opts.projectRoot ?? process.cwd(), configured);
1540
+ const findTaskFile = (id) => {
1541
+ if (!existsSync2(directory))
1542
+ return;
1543
+ for (const name of readdirSync(directory)) {
1544
+ if (!pattern.test(name))
1545
+ continue;
1546
+ const p = join(directory, name);
1547
+ try {
1548
+ if (!statSync(p).isFile())
1549
+ continue;
1550
+ if (readTaskFile(p, pattern).id === id)
1551
+ return p;
1552
+ } catch {}
1553
+ }
1554
+ return;
1555
+ };
1556
+ const applyUpdate = (id, update) => {
1557
+ const file = findTaskFile(id);
1558
+ if (!file) {
1559
+ throw new Error(`files task not found: ${id}`);
1560
+ }
1561
+ const raw = JSON.parse(readFileSync2(file, "utf-8"));
1562
+ if (update.status)
1563
+ raw.status = update.status;
1564
+ if (update.title !== undefined)
1565
+ raw.title = update.title;
1566
+ if (update.body !== undefined)
1567
+ raw.body = update.body;
1568
+ if (update.comment?.trim()) {
1569
+ const existing = Array.isArray(raw.comments) ? raw.comments : [];
1570
+ raw.comments = [
1571
+ ...existing,
1572
+ {
1573
+ body: update.comment,
1574
+ createdAt: new Date().toISOString(),
1575
+ source: "rig"
1576
+ }
1577
+ ];
1578
+ }
1579
+ writeFileSync(file, `${JSON.stringify(raw, null, 2)}
1580
+ `, "utf-8");
1581
+ };
1582
+ return {
1583
+ id: "std:files",
1584
+ kind: "files",
1585
+ async list() {
1586
+ if (!existsSync2(directory))
1587
+ return [];
1588
+ const out = [];
1589
+ for (const name of readdirSync(directory)) {
1590
+ if (!pattern.test(name))
1591
+ continue;
1592
+ const p = join(directory, name);
1593
+ try {
1594
+ if (!statSync(p).isFile())
1595
+ continue;
1596
+ out.push(readTaskFile(p, pattern));
1597
+ } catch (err) {
1598
+ console.warn(`[files-source] skipped ${name}: ${err.message}`);
1599
+ }
1600
+ }
1601
+ return out;
1602
+ },
1603
+ async get(id) {
1604
+ const all = await this.list();
1605
+ return all.find((t) => t.id === id);
1606
+ },
1607
+ async updateStatus(id, status) {
1608
+ applyUpdate(id, { status });
1609
+ },
1610
+ async updateTask(id, update) {
1611
+ applyUpdate(id, update);
1612
+ }
1613
+ };
1614
+ }
1615
+
1616
+ // packages/standard-plugin/src/plugin.ts
1617
+ init_metadata();
1618
+ init_metadata();
1619
+ import { createStandardProductEntrypointPlugin, standardProductEntrypointPlugin } from "@rig/product-entrypoint-plugin/plugin";
1620
+ import { createStandardTaskCliPlugin, standardTaskCliPlugin } from "@rig/task-cli-plugin/plugin";
1621
+ var DOCS_HEALTH_PANEL_ID = "docs-health";
1622
+ function requireStringField(config, field, kind) {
1623
+ const value = config[field];
1624
+ if (!value) {
1625
+ throw new Error(`task source ${kind}: ${field} is required`);
1626
+ }
1627
+ return value;
1628
+ }
1629
+ function isRecord(value) {
1630
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
1631
+ }
1632
+ function optionalString(value) {
1633
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
1634
+ }
1635
+ function parseGitHubProjectsOptions(value) {
1636
+ if (!isRecord(value))
1637
+ return;
1638
+ const statusesSource = isRecord(value.statuses) ? value.statuses : undefined;
1639
+ const statuses = {};
1640
+ for (const key of ["todo", "running", "prOpen", "ciFixing", "merging", "done", "needsAttention"]) {
1641
+ const status = optionalString(statusesSource?.[key]);
1642
+ if (status)
1643
+ statuses[key] = status;
1644
+ }
1645
+ const parsed = {};
1646
+ if (typeof value.enabled === "boolean")
1647
+ parsed.enabled = value.enabled;
1648
+ const projectId = optionalString(value.projectId);
1649
+ if (projectId)
1650
+ parsed.projectId = projectId;
1651
+ const statusFieldId = optionalString(value.statusFieldId);
1652
+ if (statusFieldId)
1653
+ parsed.statusFieldId = statusFieldId;
1654
+ if (Object.keys(statuses).length > 0)
1655
+ parsed.statuses = statuses;
1656
+ return parsed;
1657
+ }
1658
+ function githubProjectsOptionsFromConfig(config, context) {
1659
+ const rigConfig = isRecord(context?.rigConfig) ? context.rigConfig : undefined;
1660
+ const github = isRecord(rigConfig?.github) ? rigConfig.github : undefined;
1661
+ return parseGitHubProjectsOptions(config.options?.projects) ?? parseGitHubProjectsOptions(github?.projects);
1662
+ }
1663
+ function booleanOption(value) {
1664
+ return typeof value === "boolean" ? value : undefined;
1665
+ }
1666
+ function panelProjectRoot(context) {
1667
+ return isRecord(context) && typeof context.projectRoot === "string" && context.projectRoot.length > 0 ? context.projectRoot : null;
1668
+ }
1669
+ function driftFindingPanelId(finding, index) {
1670
+ return `${finding.docPath}:${finding.line ?? index}:${finding.kind}`;
1671
+ }
1672
+ function createDocsHealthPanelProducer(options = {}) {
1673
+ return async (context) => {
1674
+ const projectRoot = panelProjectRoot(context);
1675
+ if (!projectRoot)
1676
+ return;
1677
+ const { detectDrift: detectDrift2 } = await Promise.resolve().then(() => (init_detect(), exports_detect));
1678
+ const report = await detectDrift2({
1679
+ projectRoot,
1680
+ ...options.docsGlobs !== undefined ? { docsGlobs: options.docsGlobs } : {},
1681
+ ...options.ignoreGlobs !== undefined ? { ignoreGlobs: options.ignoreGlobs } : {}
1682
+ });
1683
+ return {
1684
+ findings: report.findings.map((finding, index) => ({
1685
+ id: driftFindingPanelId(finding, index),
1686
+ docPath: finding.docPath,
1687
+ kind: finding.kind,
1688
+ confidence: finding.confidence,
1689
+ summary: finding.detail,
1690
+ taskId: null
1691
+ })),
1692
+ degraded: report.degraded ? "drift scan degraded" : null
1693
+ };
1694
+ };
1695
+ }
1696
+ function createLazyDocsDriftValidator(options = {}) {
1697
+ return {
1698
+ ...DOCS_DRIFT_VALIDATOR,
1699
+ async run(ctx) {
1700
+ const { runDocsDriftValidation: runDocsDriftValidation2 } = await Promise.resolve().then(() => (init_plugin(), exports_plugin));
1701
+ return runDocsDriftValidation2({
1702
+ projectRoot: ctx.workspaceRoot,
1703
+ ...options.docsGlobs !== undefined ? { docsGlobs: options.docsGlobs } : {},
1704
+ ...options.ignoreGlobs !== undefined ? { ignoreGlobs: options.ignoreGlobs } : {},
1705
+ ...options.failOnDrift !== undefined ? { failOnDrift: options.failOnDrift } : {}
1706
+ });
1707
+ }
1708
+ };
1709
+ }
1710
+ function createLazyDocsDriftGateStage(options = {}) {
1711
+ return async (ctx) => {
1712
+ const { createDocsDriftGateStage: createDocsDriftGateStage2 } = await Promise.resolve().then(() => (init_plugin(), exports_plugin));
1713
+ return createDocsDriftGateStage2(options)(ctx);
1714
+ };
1715
+ }
1716
+ function createLazyDocsDriftRuntimeCliCommand(options = {}) {
1717
+ return {
1718
+ id: DOCS_DRIFT_CLI_ID,
1719
+ family: "drift",
1720
+ command: DOCS_DRIFT_CLI_COMMAND,
1721
+ description: "Scan documentation for stale code references.",
1722
+ usage: DOCS_DRIFT_CLI_COMMAND,
1723
+ projectRequired: true,
1724
+ run: async (context, args) => {
1725
+ const { executeDrift: executeDrift2 } = await Promise.resolve().then(() => (init_plugin(), exports_plugin));
1726
+ return executeDrift2(context, args, options);
1727
+ }
1728
+ };
1729
+ }
1730
+ function createStandardDocsDriftPlugin(opts = {}) {
1731
+ return definePlugin({
1732
+ name: "@rig/standard-plugin:docs-drift",
1733
+ version: "0.1.0",
1734
+ contributes: {
1735
+ validators: [DOCS_DRIFT_VALIDATOR],
1736
+ capabilities: [
1737
+ { id: DOCS_DRIFT_CAPABILITY_ID, title: "Documentation drift detection", commandId: DOCS_DRIFT_CLI_ID, panelId: DOCS_HEALTH_PANEL_ID }
1738
+ ],
1739
+ panels: [
1740
+ { id: DOCS_HEALTH_PANEL_ID, slot: "capability", title: "Documentation drift", capabilityId: DOCS_DRIFT_CAPABILITY_ID }
1741
+ ],
1742
+ cliCommands: [
1743
+ {
1744
+ id: DOCS_DRIFT_CLI_ID,
1745
+ family: "drift",
1746
+ command: DOCS_DRIFT_CLI_COMMAND,
1747
+ description: "Scan documentation for stale code references.",
1748
+ projectRequired: true
1749
+ }
1750
+ ],
1751
+ stageMutations: [DOCS_DRIFT_STAGE_MUTATION]
1752
+ }
1753
+ }, {
1754
+ validators: [createLazyDocsDriftValidator(opts)],
1755
+ stages: { [DOCS_DRIFT_STAGE_ID]: createLazyDocsDriftGateStage(opts) },
1756
+ featureCapabilities: [
1757
+ { id: DOCS_DRIFT_CAPABILITY_ID, title: "Documentation drift detection", commandId: DOCS_DRIFT_CLI_ID, panelId: DOCS_HEALTH_PANEL_ID }
1758
+ ],
1759
+ panels: [
1760
+ { id: DOCS_HEALTH_PANEL_ID, slot: "capability", title: "Documentation drift", capabilityId: DOCS_DRIFT_CAPABILITY_ID, produce: createDocsHealthPanelProducer(opts) }
1761
+ ],
1762
+ cliCommands: [createLazyDocsDriftRuntimeCliCommand(opts)]
1763
+ });
1764
+ }
1765
+ function createStandardTaskSourcesPlugin(opts = {}) {
1766
+ return definePlugin({
1767
+ name: "@rig/standard-plugin:task-sources",
1768
+ version: "0.1.0",
1769
+ contributes: {
1770
+ taskSources: [
1771
+ {
1772
+ id: "std:github-issues",
1773
+ kind: "github-issues",
1774
+ description: "GitHub Issues via gh CLI"
1775
+ },
1776
+ {
1777
+ id: "std:files",
1778
+ kind: "files",
1779
+ description: "JSON files in a local directory"
1780
+ }
1781
+ ]
1782
+ }
1783
+ }, {
1784
+ taskSources: [
1785
+ {
1786
+ id: "std:github-issues",
1787
+ kind: "github-issues",
1788
+ description: "GitHub Issues via gh CLI",
1789
+ factory(config, context) {
1790
+ const options = {
1791
+ owner: requireStringField(config, "owner", "github-issues"),
1792
+ repo: requireStringField(config, "repo", "github-issues")
1793
+ };
1794
+ const credentialProviderOptions = context?.projectRoot ? { stateDir: resolve4(context.projectRoot, ".rig", "state") } : {};
1795
+ options.credentialProvider = opts.githubCredentialProvider ?? createStateGitHubCredentialProvider(credentialProviderOptions);
1796
+ if (opts.githubWorkspaceId)
1797
+ options.workspaceId = opts.githubWorkspaceId;
1798
+ if (opts.githubUserId)
1799
+ options.userId = opts.githubUserId;
1800
+ if (opts.githubSpawn)
1801
+ options.spawn = opts.githubSpawn;
1802
+ if (opts.onGitHubTaskChanged)
1803
+ options.onTaskChanged = opts.onGitHubTaskChanged;
1804
+ if (config.labels !== undefined)
1805
+ options.labels = config.labels;
1806
+ if (config.state !== undefined)
1807
+ options.state = config.state;
1808
+ const assignee = typeof config.options?.assignee === "string" ? config.options.assignee : process.env.RIG_GITHUB_ASSIGNEE;
1809
+ if (assignee?.trim())
1810
+ options.assignee = assignee.trim();
1811
+ const timeoutMs = typeof config.options?.timeoutMs === "number" ? config.options.timeoutMs : undefined;
1812
+ if (timeoutMs !== undefined)
1813
+ options.timeoutMs = timeoutMs;
1814
+ const listLimit = typeof config.options?.listLimit === "number" ? config.options.listLimit : undefined;
1815
+ if (listLimit !== undefined)
1816
+ options.listLimit = listLimit;
1817
+ const projects = githubProjectsOptionsFromConfig(config, context);
1818
+ if (projects)
1819
+ options.projects = projects;
1820
+ const useNativeDependencies = booleanOption(config.options?.useNativeDependencies);
1821
+ if (useNativeDependencies !== undefined)
1822
+ options.useNativeDependencies = useNativeDependencies;
1823
+ return createGitHubIssuesTaskSource(options);
1824
+ }
1825
+ },
1826
+ {
1827
+ id: "std:files",
1828
+ kind: "files",
1829
+ description: "JSON files in a local directory",
1830
+ factory(config, context) {
1831
+ return createFilesTaskSource({
1832
+ path: requireStringField(config, "path", "files"),
1833
+ ...context?.projectRoot ? { projectRoot: context.projectRoot } : {}
1834
+ });
1835
+ }
1836
+ }
1837
+ ]
1838
+ });
1839
+ }
1840
+
1841
+ // packages/standard-plugin/src/bundle.ts
1842
+ function standardPlugins(options = {}) {
1843
+ return [
1844
+ createDefaultLifecyclePlugin(),
1845
+ createDependencyGraphPlugin(),
1846
+ createBlockerClassifierPlugin(),
1847
+ createPlanningPlugin(),
1848
+ createSupervisorPlugin(),
1849
+ standardCliSurfacePlugin,
1850
+ RUN_WORKER_PANEL_PLUGIN,
1851
+ createStandardTaskSourcesPlugin(options.taskSources),
1852
+ createStandardTaskCliPlugin(),
1853
+ createStandardDocsDriftPlugin(options.drift),
1854
+ createStandardProductEntrypointPlugin()
1855
+ ];
1856
+ }
1857
+ export {
1858
+ standardPlugins
1859
+ };