@h-rig/standard-plugin 0.0.6-alpha.14 → 0.0.6-alpha.141

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/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // @bun
2
- // packages/standard-plugin/src/index.ts
2
+ // packages/standard-plugin/src/plugin.ts
3
+ import { resolve as resolve4 } from "path";
3
4
  import { definePlugin } from "@rig/core";
4
5
 
5
6
  // packages/standard-plugin/src/github-issues-source.ts
@@ -14,9 +15,9 @@ function createEnvGitHubCredentialProvider() {
14
15
  return {
15
16
  async resolveGitHubToken(input) {
16
17
  if (input.purpose === "selected-repo") {
17
- return { token: cleanToken(process.env.RIG_GITHUB_SELECTED_TOKEN ?? null) ?? "", source: "signed-in-user" };
18
+ return { token: cleanToken(process.env.RIG_GITHUB_SELECTED_TOKEN ?? process.env.RIG_GITHUB_TOKEN ?? null) ?? "", source: "signed-in-user" };
18
19
  }
19
- const token = cleanToken(process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? null);
20
+ const token = cleanToken(process.env.RIG_GITHUB_TOKEN ?? process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? null);
20
21
  if (!token) {
21
22
  throw new Error("No host GitHub token is configured for admin fallback.");
22
23
  }
@@ -25,34 +26,40 @@ function createEnvGitHubCredentialProvider() {
25
26
  };
26
27
  }
27
28
  function createStateGitHubCredentialProvider(options = {}) {
28
- const resolveStateFile = () => {
29
+ const stateFileCandidates = () => {
30
+ const candidates = [];
29
31
  const explicitFile = options.stateFile ?? process.env.RIG_GITHUB_AUTH_STATE_FILE;
30
32
  if (explicitFile?.trim())
31
- return resolve(explicitFile.trim());
32
- const stateDir = options.stateDir ?? process.env.RIG_STATE_DIR;
33
- return stateDir?.trim() ? resolve(stateDir.trim(), "github-auth.json") : null;
33
+ candidates.push(resolve(explicitFile.trim()));
34
+ for (const dir of [options.stateDir, process.env.RIG_STATE_DIR]) {
35
+ if (dir?.trim())
36
+ candidates.push(resolve(dir.trim(), "github-auth.json"));
37
+ }
38
+ return candidates;
34
39
  };
35
40
  const readToken = () => {
36
- const stateFile = resolveStateFile();
37
- if (!stateFile || !existsSync(stateFile))
38
- return null;
39
- try {
40
- const parsed = JSON.parse(readFileSync(stateFile, "utf8"));
41
- return typeof parsed.token === "string" ? cleanToken(parsed.token) : null;
42
- } catch {
43
- return null;
41
+ for (const stateFile of stateFileCandidates()) {
42
+ if (!existsSync(stateFile))
43
+ continue;
44
+ try {
45
+ const parsed = JSON.parse(readFileSync(stateFile, "utf8"));
46
+ const token = typeof parsed.token === "string" ? cleanToken(parsed.token) : null;
47
+ if (token)
48
+ return token;
49
+ } catch {}
44
50
  }
51
+ return null;
45
52
  };
46
53
  return {
47
54
  async resolveGitHubToken(input) {
48
55
  const token = readToken();
49
56
  if (input.purpose === "selected-repo") {
50
- return { token: token ?? "", source: "signed-in-user" };
57
+ return { token: token ?? cleanToken(process.env.RIG_GITHUB_SELECTED_TOKEN ?? process.env.RIG_GITHUB_TOKEN ?? null) ?? "", source: "signed-in-user" };
51
58
  }
52
59
  if (token) {
53
60
  return { token, source: "signed-in-user" };
54
61
  }
55
- const fallback = cleanToken(process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? null);
62
+ const fallback = cleanToken(process.env.RIG_GITHUB_TOKEN ?? process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? null);
56
63
  if (!fallback) {
57
64
  throw new Error("No signed-in GitHub token is stored for Rig and no host admin fallback token is configured.");
58
65
  }
@@ -86,17 +93,42 @@ function statusFor(issue) {
86
93
  return "cancelled";
87
94
  return "open";
88
95
  }
96
+ function parseIssueRefs(raw) {
97
+ return raw.split(",").map((s) => s.trim()).map((s) => s.replace(/^#/, "").match(/^(\d+)/)?.[1] ?? "").filter((s) => s.length > 0);
98
+ }
99
+ function parseMetadataList(body, key) {
100
+ const block = body.match(/<!-- rig:metadata:start -->\s*([\s\S]*?)\s*<!-- rig:metadata:end -->/);
101
+ if (!block)
102
+ return [];
103
+ const lines = block[1].split(/\r?\n/);
104
+ const values = [];
105
+ for (let index = 0;index < lines.length; index += 1) {
106
+ const line = lines[index];
107
+ const sameLine = line.match(new RegExp(`^${key}:\\s*(.+)$`, "i"));
108
+ if (sameLine) {
109
+ values.push(...parseIssueRefs(sameLine[1]));
110
+ continue;
111
+ }
112
+ if (!new RegExp(`^${key}:\\s*$`, "i").test(line))
113
+ continue;
114
+ for (let cursor = index + 1;cursor < lines.length; cursor += 1) {
115
+ const item = lines[cursor].match(/^\s*-\s*(.+)$/);
116
+ if (!item)
117
+ break;
118
+ values.push(...parseIssueRefs(item[1]));
119
+ }
120
+ }
121
+ return [...new Set(values)];
122
+ }
89
123
  function parseDeps(body) {
90
124
  const match = body.match(/^depends-on:\s*([^\n]+)/im);
91
- if (!match)
92
- return [];
93
- return match[1].split(",").map((s) => s.trim()).map((s) => s.replace(/^#/, "").match(/^(\d+)/)?.[1] ?? "").filter((s) => s.length > 0);
125
+ const bodyRefs = match ? parseIssueRefs(match[1]) : [];
126
+ return [...new Set([...bodyRefs, ...parseMetadataList(body, "depends-on")])];
94
127
  }
95
128
  function parseParents(body) {
96
129
  const match = body.match(/^parents?:\s*([^\n]+)/im);
97
- if (!match)
98
- return [];
99
- return match[1].split(",").map((s) => s.trim()).map((s) => s.replace(/^#/, "").match(/^(\d+)/)?.[1] ?? "").filter((s) => s.length > 0);
130
+ const bodyRefs = match ? parseIssueRefs(match[1]) : [];
131
+ return [...new Set([...bodyRefs, ...parseMetadataList(body, "parents")])];
100
132
  }
101
133
  function issueTypeFor(issue) {
102
134
  const labels = labelNamesFor(issue);
@@ -107,7 +139,7 @@ function issueTypeFor(issue) {
107
139
  return "epic";
108
140
  return "task";
109
141
  }
110
- function issueToTask(issue, repo) {
142
+ function issueToTask(issue, repo, nativeDependencies) {
111
143
  const labelNames = labelNamesFor(issue);
112
144
  const scope = labelNames.filter((l) => l.startsWith("scope:")).map((l) => l.slice("scope:".length));
113
145
  const roleLabel = labelNames.find((l) => l.startsWith("role:"));
@@ -115,10 +147,12 @@ function issueToTask(issue, repo) {
115
147
  const validators = labelNames.filter((l) => l.startsWith("validator:")).map((l) => l.slice("validator:".length));
116
148
  const body = issue.body ?? "";
117
149
  const issueNodeId = issue.id ?? issue.nodeId ?? issue.node_id;
150
+ const parsedDeps = parseDeps(body);
151
+ const deps = nativeDependencies?.deps ? [...new Set([...parsedDeps, ...nativeDependencies.deps])] : parsedDeps;
118
152
  return {
119
153
  id: String(issue.number),
120
154
  ...typeof issueNodeId === "string" && issueNodeId.trim() ? { issueNodeId: issueNodeId.trim() } : {},
121
- deps: parseDeps(body),
155
+ deps,
122
156
  status: statusFor(issue),
123
157
  title: issue.title,
124
158
  body,
@@ -130,6 +164,7 @@ function issueToTask(issue, repo) {
130
164
  sourceIssueId: `${repo}#${issue.number}`,
131
165
  parentChildDeps: parseParents(body),
132
166
  labels: labelNames,
167
+ ...nativeDependencies?.degraded ? { nativeDependenciesDegraded: true, nativeDependenciesError: nativeDependencies.degraded } : {},
133
168
  raw: issue
134
169
  };
135
170
  }
@@ -166,17 +201,41 @@ ${rendered}
166
201
  ` : `${rendered}
167
202
  `;
168
203
  }
204
+ function buildRigStickyStatusComment(input) {
205
+ const lines = [
206
+ RIG_STATUS_COMMENT_MARKER,
207
+ `### Rig status: ${input.status}`,
208
+ "",
209
+ input.summary
210
+ ];
211
+ if (input.runId)
212
+ lines.push("", `- Run: ${input.runId}`);
213
+ if (input.prUrl)
214
+ lines.push(`- PR: ${input.prUrl}`);
215
+ for (const detail of input.details ?? [])
216
+ lines.push(`- ${detail}`);
217
+ return lines.join(`
218
+ `);
219
+ }
169
220
  function isRigStickyStatusComment(body) {
170
221
  return body.includes(RIG_STATUS_COMMENT_MARKER);
171
222
  }
172
223
  function ghSpawnOptions(extraEnv, timeoutMs) {
173
- if (!extraEnv)
174
- return { encoding: "utf-8", timeout: timeoutMs };
175
- return { encoding: "utf-8", timeout: timeoutMs, env: { ...process.env, ...extraEnv } };
224
+ return {
225
+ encoding: "utf-8",
226
+ timeout: timeoutMs,
227
+ env: {
228
+ ...process.env,
229
+ ...process.env.GH_TOKEN !== undefined ? { GH_TOKEN: process.env.GH_TOKEN } : {},
230
+ ...process.env.GITHUB_TOKEN !== undefined ? { GITHUB_TOKEN: process.env.GITHUB_TOKEN } : {},
231
+ ...process.env.RIG_GITHUB_TOKEN !== undefined ? { RIG_GITHUB_TOKEN: process.env.RIG_GITHUB_TOKEN } : {},
232
+ ...extraEnv ?? {}
233
+ }
234
+ };
176
235
  }
177
236
  function credentialEnv(token) {
178
237
  const clean = token?.trim() ?? "";
179
- return { GH_TOKEN: clean, GITHUB_TOKEN: clean };
238
+ return { GH_TOKEN: clean, GITHUB_TOKEN: clean, RIG_GITHUB_TOKEN: clean };
180
239
  }
181
240
  async function resolveCredentialEnv(opts, purpose) {
182
241
  if (!opts.credentialProvider)
@@ -191,28 +250,319 @@ async function resolveCredentialEnv(opts, purpose) {
191
250
  const resolved = await opts.credentialProvider.resolveGitHubToken(input);
192
251
  return credentialEnv(resolved.token);
193
252
  }
253
+ function tokenDiagnostic(value) {
254
+ const clean = value?.trim() ?? "";
255
+ return clean ? `present(len=${clean.length})` : "missing";
256
+ }
194
257
  function runGh(bin, args, spawn, extraEnv, timeoutMs) {
195
- const res = spawn(bin, [...args], ghSpawnOptions(extraEnv, timeoutMs));
196
- assertGhSuccess(args, res);
258
+ const options = ghSpawnOptions(extraEnv, timeoutMs);
259
+ const res = spawn(bin, [...args], options);
260
+ assertGhSuccess(args, res, options.env);
197
261
  if (!res.stdout || res.stdout.trim() === "")
198
262
  return [];
199
263
  return JSON.parse(res.stdout);
200
264
  }
201
265
  function runGhVoid(bin, args, spawn, extraEnv, timeoutMs) {
202
- const res = spawn(bin, [...args], ghSpawnOptions(extraEnv, timeoutMs));
203
- assertGhSuccess(args, res);
266
+ const options = ghSpawnOptions(extraEnv, timeoutMs);
267
+ const res = spawn(bin, [...args], options);
268
+ assertGhSuccess(args, res, options.env);
204
269
  }
205
- function assertGhSuccess(args, res) {
270
+ function assertGhSuccess(args, res, env) {
206
271
  if (res.error) {
207
272
  const msg = res.error.message ?? String(res.error);
208
273
  throw new Error(`gh CLI not available \u2014 install gh (brew install gh / apt install gh): ${msg}`);
209
274
  }
210
275
  if (res.status !== 0) {
211
- throw new Error(`gh ${args.join(" ")} failed (exit ${res.status}): ${res.stderr}`);
276
+ throw new Error(`gh ${args.join(" ")} failed (exit ${res.status}): ${res.stderr}
277
+ [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)}`);
212
278
  }
213
279
  }
280
+ var DEFAULT_PROJECT_STATUSES = {
281
+ todo: "Todo",
282
+ running: "In Progress",
283
+ prOpen: "In Review",
284
+ ciFixing: "In Review",
285
+ merging: "In Review",
286
+ done: "Done",
287
+ needsAttention: "Needs Attention"
288
+ };
289
+ function asProjectRecord(value) {
290
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
291
+ }
292
+ function projectString(value) {
293
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
294
+ }
295
+ function projectLifecycleStatusForTaskStatus(status) {
296
+ const normalized = status?.trim().toLowerCase().replace(/[-\s]+/g, "_");
297
+ switch (normalized) {
298
+ case "draft":
299
+ case "open":
300
+ case "queued":
301
+ case "ready":
302
+ return "todo";
303
+ case "running":
304
+ case "in_progress":
305
+ return "running";
306
+ case "under_review":
307
+ case "review":
308
+ case "pr_open":
309
+ return "prOpen";
310
+ case "ci_fixing":
311
+ case "fixing":
312
+ return "ciFixing";
313
+ case "merging":
314
+ case "merge":
315
+ return "merging";
316
+ case "closed":
317
+ case "completed":
318
+ case "done":
319
+ return "done";
320
+ case "blocked":
321
+ case "cancelled":
322
+ case "failed":
323
+ case "needs_attention":
324
+ return "needsAttention";
325
+ default:
326
+ return null;
327
+ }
328
+ }
329
+ function ghGraphQLFetch(bin, spawnFn, extraEnv, timeoutMs) {
330
+ return async (query, variables) => {
331
+ const args = ["api", "graphql", "-f", `query=${query}`];
332
+ for (const [key, value] of Object.entries(variables)) {
333
+ if (value === undefined || value === null)
334
+ continue;
335
+ args.push("-f", `${key}=${String(value)}`);
336
+ }
337
+ const response = runGh(bin, args, spawnFn, extraEnv, timeoutMs);
338
+ return asProjectRecord(response)?.data ?? response;
339
+ };
340
+ }
341
+ function issueNodeIdFor(issue) {
342
+ const id = issue.id ?? issue.nodeId ?? issue.node_id;
343
+ return typeof id === "string" && id.trim().length > 0 ? id.trim() : null;
344
+ }
345
+ function nativeIssueDependencyRef(value, currentRepo) {
346
+ const record = asProjectRecord(value);
347
+ const number = typeof record?.number === "number" ? String(record.number) : projectString(record?.number);
348
+ if (!number)
349
+ return null;
350
+ const repository = asProjectRecord(record?.repository);
351
+ const owner = projectString(asProjectRecord(repository?.owner)?.login);
352
+ const name = projectString(repository?.name);
353
+ if (!owner || !name || `${owner}/${name}` === currentRepo)
354
+ return number;
355
+ return `${owner}/${name}#${number}`;
356
+ }
357
+ function nativeDependencyRefsFrom(data, currentRepo) {
358
+ const issue = asProjectRecord(asProjectRecord(data)?.node);
359
+ const blockedBy = asProjectRecord(issue?.blockedBy);
360
+ const nodes = Array.isArray(blockedBy?.nodes) ? blockedBy.nodes : [];
361
+ return [...new Set(nodes.flatMap((node) => {
362
+ const ref = nativeIssueDependencyRef(node, currentRepo);
363
+ return ref ? [ref] : [];
364
+ }))];
365
+ }
366
+ async function readNativeDependenciesForIssue(input) {
367
+ const issueId = issueNodeIdFor(input.issue);
368
+ if (!issueId)
369
+ return { deps: [], degraded: "GitHub issue node id is unavailable." };
370
+ const query = `
371
+ query RigIssueNativeDependencies($issueId: ID!) {
372
+ node(id: $issueId) {
373
+ ... on Issue {
374
+ blockedBy(first: 100) {
375
+ nodes {
376
+ number
377
+ repository { name owner { login } }
378
+ }
379
+ }
380
+ }
381
+ }
382
+ }
383
+ `;
384
+ try {
385
+ return {
386
+ deps: nativeDependencyRefsFrom(await input.fetchGraphQL(query, { issueId }, "gh-cli"), input.repo)
387
+ };
388
+ } catch (error) {
389
+ const detail = error instanceof Error ? error.message : String(error);
390
+ return { deps: [], degraded: detail };
391
+ }
392
+ }
393
+ function formatIssueReference(ref) {
394
+ const clean = ref.trim().replace(/^#/, "");
395
+ return /^\d+$/.test(clean) ? `#${clean}` : clean;
396
+ }
397
+ function appendReferenceLines(body, deps, parents) {
398
+ const lines = [];
399
+ const cleanDeps = (deps ?? []).map(formatIssueReference).filter((ref) => ref.length > 0);
400
+ const cleanParents = (parents ?? []).map(formatIssueReference).filter((ref) => ref.length > 0);
401
+ if (cleanDeps.length > 0)
402
+ lines.push(`depends-on: ${cleanDeps.join(", ")}`);
403
+ if (cleanParents.length > 0)
404
+ lines.push(`parents: ${cleanParents.join(", ")}`);
405
+ if (lines.length === 0)
406
+ return body;
407
+ return body.trim().length > 0 ? `${body.trimEnd()}
408
+
409
+ ${lines.join(`
410
+ `)}` : lines.join(`
411
+ `);
412
+ }
413
+ function bodyForCreatedTask(input) {
414
+ const metadata = { ...input.metadata ?? {} };
415
+ if (input.deps && input.deps.length > 0)
416
+ metadata["depends-on"] = input.deps.map(formatIssueReference);
417
+ if (input.parents && input.parents.length > 0)
418
+ metadata.parents = input.parents.map(formatIssueReference);
419
+ return updateRigOwnedMetadataBlock(appendReferenceLines(input.body, input.deps, input.parents), metadata);
420
+ }
421
+ function projectStatusFieldFrom(data, projectId) {
422
+ const fields = asProjectRecord(asProjectRecord(asProjectRecord(data)?.node)?.fields)?.nodes;
423
+ for (const node of Array.isArray(fields) ? fields : []) {
424
+ const record = asProjectRecord(node);
425
+ if (projectString(record?.name)?.toLowerCase() !== "status")
426
+ continue;
427
+ const id = projectString(record?.id);
428
+ if (!id)
429
+ continue;
430
+ const options = Array.isArray(record?.options) ? record.options.flatMap((option) => {
431
+ const optionRecord = asProjectRecord(option);
432
+ const optionId = projectString(optionRecord?.id);
433
+ const name = projectString(optionRecord?.name);
434
+ return optionId && name ? [{ id: optionId, name }] : [];
435
+ }) : [];
436
+ return { id, name: "Status", options };
437
+ }
438
+ throw new Error(`GitHub Project ${projectId} does not expose a Status single-select field.`);
439
+ }
440
+ async function resolveProjectStatusField(input) {
441
+ const query = `
442
+ query RigProjectStatusField($projectId: ID!) {
443
+ node(id: $projectId) {
444
+ ... on ProjectV2 {
445
+ fields(first: 50) {
446
+ nodes {
447
+ ... on ProjectV2FieldCommon { id name }
448
+ ... on ProjectV2SingleSelectField { id name options { id name } }
449
+ }
450
+ }
451
+ }
452
+ }
453
+ }
454
+ `;
455
+ return projectStatusFieldFrom(await input.fetchGraphQL(query, { projectId: input.projectId }, input.token), input.projectId);
456
+ }
457
+ async function ensureIssueProjectItem(input) {
458
+ const query = `
459
+ query RigFindProjectIssueItem($projectId: ID!) {
460
+ node(id: $projectId) {
461
+ ... on ProjectV2 {
462
+ items(first: 100) { nodes { id content { ... on Issue { id } } } }
463
+ }
464
+ }
465
+ }
466
+ `;
467
+ const data = await input.fetchGraphQL(query, { projectId: input.projectId }, input.token);
468
+ const nodes = asProjectRecord(asProjectRecord(asProjectRecord(data)?.node)?.items)?.nodes;
469
+ for (const node of Array.isArray(nodes) ? nodes : []) {
470
+ const record = asProjectRecord(node);
471
+ const content = asProjectRecord(record?.content);
472
+ if (projectString(content?.id) === input.issueNodeId) {
473
+ const id2 = projectString(record?.id);
474
+ if (id2)
475
+ return { id: id2, created: false };
476
+ }
477
+ }
478
+ const mutation = `
479
+ mutation RigAddIssueToProject($projectId: ID!, $contentId: ID!) {
480
+ addProjectV2ItemById(input: { projectId: $projectId, contentId: $contentId }) { item { id } }
481
+ }
482
+ `;
483
+ const created = await input.fetchGraphQL(mutation, { projectId: input.projectId, contentId: input.issueNodeId }, input.token);
484
+ const addResult = asProjectRecord(asProjectRecord(created)?.addProjectV2ItemById);
485
+ const id = projectString(asProjectRecord(addResult?.item)?.id);
486
+ if (!id)
487
+ throw new Error("GitHub Project item creation did not return an item id.");
488
+ return { id, created: true };
489
+ }
490
+ async function updateIssueProjectStatus(input) {
491
+ const mutation = `
492
+ mutation RigUpdateProjectStatus($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
493
+ updateProjectV2ItemFieldValue(input: {
494
+ projectId: $projectId,
495
+ itemId: $itemId,
496
+ fieldId: $fieldId,
497
+ value: { singleSelectOptionId: $optionId }
498
+ }) { projectV2Item { id } }
499
+ }
500
+ `;
501
+ await input.fetchGraphQL(mutation, {
502
+ projectId: input.projectId,
503
+ itemId: input.itemId,
504
+ fieldId: input.fieldId,
505
+ optionId: input.optionId
506
+ }, input.token);
507
+ }
508
+ function fetchIssueNodeId(bin, repo, spawnFn, id, extraEnv, timeoutMs) {
509
+ const issue = runGh(bin, ["issue", "view", String(id), "--repo", repo, "--json", "id"], spawnFn, extraEnv, timeoutMs);
510
+ return projectString(issue.id) ?? projectString(issue.nodeId) ?? projectString(issue.node_id);
511
+ }
512
+ async function syncGitHubProjectStatus(bin, repo, spawnFn, id, status, projects, extraEnv, timeoutMs) {
513
+ if (!projects?.enabled)
514
+ return;
515
+ const projectId = projectString(projects.projectId);
516
+ if (!projectId)
517
+ throw new Error("GitHub Projects status sync is enabled but projectId is missing.");
518
+ const lifecycleStatus = projectLifecycleStatusForTaskStatus(status);
519
+ if (!lifecycleStatus)
520
+ return;
521
+ const issueNodeId = fetchIssueNodeId(bin, repo, spawnFn, id, extraEnv, timeoutMs);
522
+ if (!issueNodeId)
523
+ throw new Error(`GitHub issue ${repo}#${id} did not expose a node id for Projects status sync.`);
524
+ const projectStatus = projectString(projects.statuses?.[lifecycleStatus]) ?? DEFAULT_PROJECT_STATUSES[lifecycleStatus];
525
+ const fetchGraphQL = ghGraphQLFetch(bin, spawnFn, extraEnv, timeoutMs);
526
+ const field = await resolveProjectStatusField({ projectId, token: "gh-cli", fetchGraphQL });
527
+ const option = field.options.find((candidate) => candidate.name.toLowerCase() === projectStatus.toLowerCase() || candidate.id === projectStatus);
528
+ if (!option)
529
+ throw new Error(`GitHub Project ${projectId} Status field does not contain option "${projectStatus}".`);
530
+ const item = await ensureIssueProjectItem({ projectId, issueNodeId, token: "gh-cli", fetchGraphQL });
531
+ await updateIssueProjectStatus({
532
+ projectId,
533
+ itemId: item.id,
534
+ fieldId: projectString(projects.statusFieldId) ?? field.id,
535
+ optionId: option.id,
536
+ token: "gh-cli",
537
+ fetchGraphQL
538
+ });
539
+ }
540
+ var TERMINAL_TASK_STATUSES = new Set(["closed", "completed", "merged", "cancelled", "resolved", "done"]);
541
+ function normalizeTaskStatusToken(status) {
542
+ return status?.trim().toLowerCase().replace(/[-\s]+/g, "_") ?? "";
543
+ }
544
+ function issueUpdatesMode(value) {
545
+ return value === "off" || value === "minimal" || value === "lifecycle" ? value : "lifecycle";
546
+ }
547
+ function isTerminalTaskStatus(status) {
548
+ return TERMINAL_TASK_STATUSES.has(normalizeTaskStatusToken(status));
549
+ }
550
+ function shouldWriteIssueUpdate(mode, status) {
551
+ if (mode === "off")
552
+ return false;
553
+ if (mode === "lifecycle")
554
+ return true;
555
+ return isTerminalTaskStatus(status);
556
+ }
557
+ function isRunningStatus(status) {
558
+ return normalizeTaskStatusToken(status) === "running";
559
+ }
560
+ function assignRunningIssue(bin, repo, spawnFn, id, assignee, extraEnv, timeoutMs) {
561
+ runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, "--add-assignee", assignee?.trim() || "@me"], spawnFn, extraEnv, timeoutMs);
562
+ }
214
563
  function statusLabelFor(status) {
215
564
  switch (status) {
565
+ case "running":
216
566
  case "in_progress":
217
567
  return "in-progress";
218
568
  case "blocked":
@@ -231,6 +581,8 @@ function statusLabelFor(status) {
231
581
  return "under-review";
232
582
  case "needs_attention":
233
583
  return "blocked";
584
+ case "closed":
585
+ case "completed":
234
586
  case "open":
235
587
  return null;
236
588
  default:
@@ -239,11 +591,13 @@ function statusLabelFor(status) {
239
591
  }
240
592
  function rigStatusLabelFor(status) {
241
593
  switch (status) {
594
+ case "running":
242
595
  case "in_progress":
243
596
  return "rig:running";
244
597
  case "under_review":
245
598
  return "rig:pr-open";
246
599
  case "closed":
600
+ case "completed":
247
601
  return "rig:done";
248
602
  case "ci_fixing":
249
603
  return "rig:ci-fixing";
@@ -261,9 +615,10 @@ function rigStatusLabelFor(status) {
261
615
  return null;
262
616
  }
263
617
  }
264
- function applyIssueStatus(bin, repo, spawnFn, id, status, extraEnv, timeoutMs) {
265
- const targetLabel = status === "closed" ? null : statusLabelFor(status);
618
+ async function applyIssueStatus(bin, repo, spawnFn, id, status, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs) {
619
+ const targetLabel = status === "closed" || status === "completed" ? null : statusLabelFor(status);
266
620
  const targetRigLabel = rigStatusLabelFor(status);
621
+ const shouldSyncLifecycle = shouldWriteIssueUpdate(issueUpdates, status);
267
622
  for (const l of [...STATUS_LABELS, ...RIG_STATUS_LABELS]) {
268
623
  if (targetLabel !== null && l === targetLabel)
269
624
  continue;
@@ -285,7 +640,19 @@ function applyIssueStatus(bin, repo, spawnFn, id, status, extraEnv, timeoutMs) {
285
640
  runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, "--add-label", label], spawnFn, extraEnv, timeoutMs);
286
641
  }
287
642
  }
288
- if (status === "closed") {
643
+ if (isRunningStatus(status)) {
644
+ assignRunningIssue(bin, repo, spawnFn, id, runningAssignee, extraEnv, timeoutMs);
645
+ if (shouldSyncLifecycle) {
646
+ upsertRigStickyComment(bin, repo, spawnFn, String(id), buildRigStickyStatusComment({
647
+ status: "running",
648
+ summary: "Rig run started."
649
+ }), extraEnv, timeoutMs);
650
+ }
651
+ }
652
+ if (shouldSyncLifecycle) {
653
+ await syncGitHubProjectStatus(bin, repo, spawnFn, id, status, projects, extraEnv, timeoutMs);
654
+ }
655
+ if (status === "closed" || status === "completed") {
289
656
  runGhVoid(bin, ["issue", "close", String(id), "--repo", repo], spawnFn, extraEnv, timeoutMs);
290
657
  }
291
658
  }
@@ -355,11 +722,11 @@ function applyLabels(bin, repo, spawnFn, id, labels, action, extraEnv, timeoutMs
355
722
  } catch {}
356
723
  }
357
724
  }
358
- function applyIssueUpdate(bin, repo, spawnFn, id, update, extraEnv, timeoutMs) {
725
+ async function applyIssueUpdate(bin, repo, spawnFn, id, update, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs) {
359
726
  if (update.status) {
360
- applyIssueStatus(bin, repo, spawnFn, id, update.status, extraEnv, timeoutMs);
727
+ await applyIssueStatus(bin, repo, spawnFn, id, update.status, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs);
361
728
  }
362
- if (update.comment?.trim()) {
729
+ if (update.comment?.trim() && shouldWriteIssueUpdate(issueUpdates, update.status)) {
363
730
  if (isRigStickyStatusComment(update.comment)) {
364
731
  upsertRigStickyComment(bin, repo, spawnFn, String(id), update.comment, extraEnv, timeoutMs);
365
732
  } else {
@@ -385,6 +752,17 @@ function createGitHubIssuesTaskSource(opts) {
385
752
  const spawnFn = opts.spawn ?? spawnSync;
386
753
  const timeoutMs = Math.max(1000, Math.trunc(opts.timeoutMs ?? DEFAULT_GH_TIMEOUT_MS));
387
754
  const listLimit = Math.max(1, Math.trunc(opts.listLimit ?? DEFAULT_GITHUB_ISSUE_LIST_LIMIT));
755
+ const issueUpdates = issueUpdatesMode(opts.issueUpdates);
756
+ async function issueToTaskWithOptionalNativeDependencies(issue, env) {
757
+ if (!opts.useNativeDependencies)
758
+ return issueToTask(issue, repo);
759
+ const nativeDependencies = await readNativeDependenciesForIssue({
760
+ issue,
761
+ repo,
762
+ fetchGraphQL: ghGraphQLFetch(bin, spawnFn, env, timeoutMs)
763
+ });
764
+ return issueToTask(issue, repo, nativeDependencies);
765
+ }
388
766
  return {
389
767
  id: "std:github-issues",
390
768
  kind: "github-issues",
@@ -411,12 +789,13 @@ function createGitHubIssuesTaskSource(opts) {
411
789
  throw new Error(`GitHub issue list for ${repo} reached the configured limit (${listLimit}); refusing to silently truncate matching issues. Increase taskSource.options.listLimit or narrow labels/state/assignee.`);
412
790
  }
413
791
  const issues = rawIssues.filter((issue) => !issue.pull_request);
414
- return issues.map((i) => issueToTask(i, repo));
792
+ return Promise.all(issues.map((issue) => issueToTaskWithOptionalNativeDependencies(issue, env)));
415
793
  },
416
794
  async get(id) {
795
+ const env = await resolveCredentialEnv(opts, "selected-repo");
796
+ let issue;
417
797
  try {
418
- const env = await resolveCredentialEnv(opts, "selected-repo");
419
- const issue = runGh(bin, [
798
+ issue = runGh(bin, [
420
799
  "issue",
421
800
  "view",
422
801
  String(id),
@@ -425,19 +804,23 @@ function createGitHubIssuesTaskSource(opts) {
425
804
  "--json",
426
805
  "number,title,body,labels,state,url,assignees,id"
427
806
  ], spawnFn, env, timeoutMs);
428
- return issueToTask(issue, repo);
429
- } catch {
430
- return;
807
+ } catch (error) {
808
+ const detail = error instanceof Error ? error.message : String(error);
809
+ if (/could not resolve to (an? )?(issue|pullrequest)|no issues? (found|matched)|404 not found|gh: not found|gh issue view\b[\s\S]*failed \(exit \d+\): not found\b/i.test(detail)) {
810
+ return;
811
+ }
812
+ throw new Error(`Failed to read task ${id} from GitHub repo ${repo}: ${detail}`);
431
813
  }
814
+ return issueToTaskWithOptionalNativeDependencies(issue, env);
432
815
  },
433
816
  async updateStatus(id, status) {
434
817
  const env = await resolveCredentialEnv(opts, "selected-repo");
435
- applyIssueStatus(bin, repo, spawnFn, id, status, env, timeoutMs);
818
+ await applyIssueStatus(bin, repo, spawnFn, id, status, opts.projects, opts.assignee, issueUpdates, env, timeoutMs);
436
819
  notifyTaskChanged(opts.onTaskChanged, repo, id, status);
437
820
  },
438
821
  async updateTask(id, update) {
439
822
  const env = await resolveCredentialEnv(opts, "selected-repo");
440
- applyIssueUpdate(bin, repo, spawnFn, id, update, env, timeoutMs);
823
+ await applyIssueUpdate(bin, repo, spawnFn, id, update, opts.projects, opts.assignee, issueUpdates, env, timeoutMs);
441
824
  notifyTaskChanged(opts.onTaskChanged, repo, id, update.status);
442
825
  },
443
826
  async addLabels(id, labels) {
@@ -452,6 +835,7 @@ function createGitHubIssuesTaskSource(opts) {
452
835
  },
453
836
  async createIssue(input) {
454
837
  const env = await resolveCredentialEnv(opts, "selected-repo");
838
+ const body = input.body ?? "";
455
839
  const args = [
456
840
  "api",
457
841
  "-X",
@@ -460,12 +844,31 @@ function createGitHubIssuesTaskSource(opts) {
460
844
  "-f",
461
845
  `title=${input.title}`,
462
846
  "-f",
463
- `body=${input.body ?? ""}`,
847
+ `body=${body}`,
464
848
  ...(input.labels ?? []).flatMap((label) => ["-f", `labels[]=${label}`])
465
849
  ];
466
850
  const issue = runGh(bin, args, spawnFn, env, timeoutMs);
467
851
  notifyTaskChanged(opts.onTaskChanged, repo, String(issue.number));
468
- return issueToTask(issue, repo);
852
+ return issueToTask({ ...issue, body: issue.body ?? body }, repo);
853
+ },
854
+ async create(input) {
855
+ const env = await resolveCredentialEnv(opts, "selected-repo");
856
+ const body = bodyForCreatedTask(input);
857
+ const args = [
858
+ "api",
859
+ "-X",
860
+ "POST",
861
+ `repos/${repo}/issues`,
862
+ "-f",
863
+ `title=${input.title}`,
864
+ "-f",
865
+ `body=${body}`,
866
+ "-f",
867
+ "labels[]=rig:generated"
868
+ ];
869
+ const issue = runGh(bin, args, spawnFn, env, timeoutMs);
870
+ notifyTaskChanged(opts.onTaskChanged, repo, String(issue.number));
871
+ return issueToTask({ ...issue, body: issue.body ?? body }, repo);
469
872
  },
470
873
  async getIssueBody(id) {
471
874
  const env = await resolveCredentialEnv(opts, "selected-repo");
@@ -476,7 +879,7 @@ function createGitHubIssuesTaskSource(opts) {
476
879
 
477
880
  // packages/standard-plugin/src/files-source.ts
478
881
  import { readFileSync as readFileSync2, readdirSync, existsSync as existsSync2, statSync, writeFileSync } from "fs";
479
- import { join, basename } from "path";
882
+ import { join, basename, isAbsolute, resolve as resolve2 } from "path";
480
883
  var DEFAULT_PATTERN = /\.(task\.)?json$/;
481
884
  function readTaskFile(file, pattern) {
482
885
  const raw = JSON.parse(readFileSync2(file, "utf-8"));
@@ -497,10 +900,11 @@ function readTaskFile(file, pattern) {
497
900
  }
498
901
  function createFilesTaskSource(opts) {
499
902
  const pattern = opts.pattern ?? DEFAULT_PATTERN;
500
- const directory = opts.path ?? opts.dir;
501
- if (!directory) {
903
+ const configured = opts.path ?? opts.dir;
904
+ if (!configured) {
502
905
  throw new Error("createFilesTaskSource: either `path` or `dir` must be provided");
503
906
  }
907
+ const directory = isAbsolute(configured) ? configured : resolve2(opts.projectRoot ?? process.cwd(), configured);
504
908
  const findTaskFile = (id) => {
505
909
  if (!existsSync2(directory))
506
910
  return;
@@ -577,7 +981,509 @@ function createFilesTaskSource(opts) {
577
981
  };
578
982
  }
579
983
 
580
- // packages/standard-plugin/src/index.ts
984
+ // packages/standard-plugin/src/drift/plugin.ts
985
+ import { Schema } from "effect";
986
+ import { StageMutation as StageMutationSchema } from "@rig/contracts";
987
+
988
+ // packages/standard-plugin/src/drift/detect.ts
989
+ import { existsSync as existsSync3 } from "fs";
990
+ import { readdir, readFile, stat } from "fs/promises";
991
+ import { basename as basename2, extname, relative, resolve as resolve3 } from "path";
992
+
993
+ // packages/standard-plugin/src/drift/extract-refs.ts
994
+ var INLINE_CODE = /`([^`\n]+)`/g;
995
+ var MARKDOWN_LINK = /\[[^\]]+\]\(([^)\s]+)\)/g;
996
+ var SYMBOL_REF = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)?$/;
997
+ 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)$/;
998
+ function stripFenceLines(markdown) {
999
+ const lines = markdown.split(/\r?\n/);
1000
+ let fenced = false;
1001
+ return lines.map((line) => {
1002
+ if (/^\s*(```|~~~)/.test(line)) {
1003
+ fenced = !fenced;
1004
+ return "";
1005
+ }
1006
+ return fenced ? "" : line;
1007
+ });
1008
+ }
1009
+ function normalizeToken(raw) {
1010
+ return raw.trim().replace(/^['"]|['"]$/g, "").replace(/[),.;:]+$/g, "").replace(/#L\d+(?:-L\d+)?$/i, "");
1011
+ }
1012
+ function classifyReference(raw) {
1013
+ if (raw.startsWith("@"))
1014
+ return null;
1015
+ if (PATH_REF.test(raw))
1016
+ return "path";
1017
+ if (SYMBOL_REF.test(raw))
1018
+ return "symbol";
1019
+ return null;
1020
+ }
1021
+ function pushReference(refs, seen, raw, line) {
1022
+ const value = normalizeToken(raw);
1023
+ if (!value)
1024
+ return;
1025
+ const kind = classifyReference(value);
1026
+ if (!kind)
1027
+ return;
1028
+ const key = `${kind}:${value}:${line}`;
1029
+ if (seen.has(key))
1030
+ return;
1031
+ seen.add(key);
1032
+ refs.push({ kind, value, line });
1033
+ }
1034
+ function extractDriftReferences(markdown) {
1035
+ const refs = [];
1036
+ const seen = new Set;
1037
+ const lines = stripFenceLines(markdown);
1038
+ for (const [index, line] of lines.entries()) {
1039
+ const lineNumber = index + 1;
1040
+ for (const match of line.matchAll(INLINE_CODE)) {
1041
+ pushReference(refs, seen, match[1] ?? "", lineNumber);
1042
+ }
1043
+ for (const match of line.matchAll(MARKDOWN_LINK)) {
1044
+ pushReference(refs, seen, match[1] ?? "", lineNumber);
1045
+ }
1046
+ }
1047
+ return refs;
1048
+ }
1049
+
1050
+ // packages/standard-plugin/src/drift/git-adapter.ts
1051
+ import { execFile } from "child_process";
1052
+ import { promisify } from "util";
1053
+ var execFileAsync = promisify(execFile);
1054
+ function processError(value) {
1055
+ return value && typeof value === "object" ? value : null;
1056
+ }
1057
+ function lineCount(output) {
1058
+ const trimmed = output.trim();
1059
+ return trimmed ? trimmed.split(/\r?\n/).length : 0;
1060
+ }
1061
+ function makeDriftGit(projectRoot) {
1062
+ async function git(args) {
1063
+ const result = await execFileAsync("git", [...args], {
1064
+ cwd: projectRoot,
1065
+ encoding: "utf8",
1066
+ maxBuffer: 10 * 1024 * 1024
1067
+ });
1068
+ return String(result.stdout);
1069
+ }
1070
+ async function grepCountAt(symbolOrPath, commit) {
1071
+ try {
1072
+ return lineCount(await git(["grep", "-F", "-n", "-e", symbolOrPath, commit, "--"]));
1073
+ } catch (error) {
1074
+ const detail = processError(error);
1075
+ if (detail?.code === 1)
1076
+ return 0;
1077
+ throw error;
1078
+ }
1079
+ }
1080
+ return {
1081
+ async lastCommitTouching(path) {
1082
+ const commit = (await git(["log", "-n", "1", "--format=%H", "--", path])).trim();
1083
+ return commit || "HEAD";
1084
+ },
1085
+ async grepCount(symbolOrPath) {
1086
+ return grepCountAt(symbolOrPath, "HEAD");
1087
+ },
1088
+ async grepCountAtCommit(symbolOrPath, commit) {
1089
+ return grepCountAt(symbolOrPath, commit);
1090
+ },
1091
+ async wasRenamed(symbolOrPath, sinceCommit) {
1092
+ if (!symbolOrPath.includes("/") && !symbolOrPath.includes("."))
1093
+ return false;
1094
+ try {
1095
+ const output = await git(["log", "--name-status", "--format=", `${sinceCommit}..HEAD`]);
1096
+ return output.split(/\r?\n/).some((line) => {
1097
+ const match = line.match(/^R\d*\s+(.+?)\s+(.+)$/);
1098
+ return Boolean(match && (match[1] === symbolOrPath || match[2] === symbolOrPath));
1099
+ });
1100
+ } catch (error) {
1101
+ const detail = processError(error);
1102
+ if (detail?.code === 128)
1103
+ return false;
1104
+ throw error;
1105
+ }
1106
+ }
1107
+ };
1108
+ }
1109
+
1110
+ // packages/standard-plugin/src/drift/detect.ts
1111
+ var DEFAULT_IGNORED_DIRS = {
1112
+ ".git": true,
1113
+ node_modules: true,
1114
+ dist: true,
1115
+ build: true,
1116
+ coverage: true,
1117
+ ".next": true,
1118
+ vendor: true
1119
+ };
1120
+ var SOURCE_EXTENSIONS = {
1121
+ ".ts": true,
1122
+ ".tsx": true,
1123
+ ".js": true,
1124
+ ".jsx": true,
1125
+ ".mjs": true,
1126
+ ".cjs": true,
1127
+ ".rs": true,
1128
+ ".go": true,
1129
+ ".py": true,
1130
+ ".rb": true,
1131
+ ".java": true,
1132
+ ".kt": true,
1133
+ ".swift": true,
1134
+ ".c": true,
1135
+ ".cc": true,
1136
+ ".cpp": true,
1137
+ ".h": true,
1138
+ ".hpp": true,
1139
+ ".json": true,
1140
+ ".toml": true,
1141
+ ".yml": true,
1142
+ ".yaml": true
1143
+ };
1144
+ function globLikeMatch(path, pattern) {
1145
+ if (pattern === path)
1146
+ return true;
1147
+ if (pattern.startsWith("**/*"))
1148
+ return path.endsWith(pattern.slice(4));
1149
+ if (pattern.endsWith("/**"))
1150
+ return path.startsWith(pattern.slice(0, -3));
1151
+ if (pattern.includes("*")) {
1152
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
1153
+ return new RegExp(`^${escaped}$`).test(path);
1154
+ }
1155
+ return path.startsWith(pattern);
1156
+ }
1157
+ function isDefaultDoc(path) {
1158
+ const lower = basename2(path).toLowerCase();
1159
+ return (path.endsWith(".md") || path.endsWith(".mdx")) && !lower.startsWith("changelog") && !lower.includes("generated");
1160
+ }
1161
+ function isIgnored(path, patterns) {
1162
+ return (patterns ?? []).some((pattern) => globLikeMatch(path, pattern));
1163
+ }
1164
+ async function collectFiles(root, options) {
1165
+ const files = [];
1166
+ async function visit(dir) {
1167
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
1168
+ if (entry.isDirectory() && DEFAULT_IGNORED_DIRS[entry.name])
1169
+ continue;
1170
+ const absolute = resolve3(dir, entry.name);
1171
+ const rel = relative(root, absolute).replace(/\\/g, "/");
1172
+ if (isIgnored(rel, options.ignore))
1173
+ continue;
1174
+ if (entry.isDirectory()) {
1175
+ await visit(absolute);
1176
+ continue;
1177
+ }
1178
+ if (!entry.isFile())
1179
+ continue;
1180
+ if (options.docs) {
1181
+ const matchesConfigured = options.patterns && options.patterns.length > 0 ? options.patterns.some((pattern) => globLikeMatch(rel, pattern)) : isDefaultDoc(rel);
1182
+ if (matchesConfigured)
1183
+ files.push(rel);
1184
+ continue;
1185
+ }
1186
+ if (SOURCE_EXTENSIONS[extname(entry.name)])
1187
+ files.push(rel);
1188
+ }
1189
+ }
1190
+ await visit(root);
1191
+ return files.sort();
1192
+ }
1193
+ async function sourceReferenceCount(projectRoot, reference, docPath) {
1194
+ if (reference.kind === "path")
1195
+ return existsSync3(resolve3(projectRoot, reference.value)) ? 1 : 0;
1196
+ let count = 0;
1197
+ const sourceFiles = await collectFiles(projectRoot, { docs: false });
1198
+ for (const sourceFile of sourceFiles) {
1199
+ if (sourceFile === docPath)
1200
+ continue;
1201
+ const text = await readFile(resolve3(projectRoot, sourceFile), "utf8").catch(() => "");
1202
+ if (text.includes(reference.value))
1203
+ count += 1;
1204
+ }
1205
+ return count;
1206
+ }
1207
+ function deletedReferenceFinding(docPath, reference) {
1208
+ return {
1209
+ kind: "deleted-reference",
1210
+ docPath,
1211
+ line: reference.line,
1212
+ reference: reference.value,
1213
+ detail: `Documented reference "${reference.value}" no longer exists in the source tree.`,
1214
+ confidence: "high"
1215
+ };
1216
+ }
1217
+ function staleAnchorFinding(docPath, reference) {
1218
+ return {
1219
+ kind: "stale-anchor",
1220
+ docPath,
1221
+ line: reference.line,
1222
+ reference: reference.value,
1223
+ detail: `Documented path "${reference.value}" changed after this doc was last updated.`,
1224
+ confidence: "medium"
1225
+ };
1226
+ }
1227
+ async function detectDeletedReferences(projectRoot, docPath, git = makeDriftGit(projectRoot)) {
1228
+ const markdown = await readFile(resolve3(projectRoot, docPath), "utf8");
1229
+ const docCommit = await git.lastCommitTouching(docPath);
1230
+ const findings = [];
1231
+ for (const reference of extractDriftReferences(markdown)) {
1232
+ if (await sourceReferenceCount(projectRoot, reference, docPath) > 0)
1233
+ continue;
1234
+ if (await git.wasRenamed(reference.value, docCommit))
1235
+ continue;
1236
+ findings.push(deletedReferenceFinding(docPath, reference));
1237
+ }
1238
+ return findings;
1239
+ }
1240
+ async function detectStaleAnchors(projectRoot, docPath, git = makeDriftGit(projectRoot)) {
1241
+ const markdown = await readFile(resolve3(projectRoot, docPath), "utf8");
1242
+ const docCommit = await git.lastCommitTouching(docPath);
1243
+ const findings = [];
1244
+ for (const reference of extractDriftReferences(markdown).filter((ref) => ref.kind === "path")) {
1245
+ if (!existsSync3(resolve3(projectRoot, reference.value)))
1246
+ continue;
1247
+ const sourceStat = await stat(resolve3(projectRoot, reference.value)).catch(() => null);
1248
+ if (!sourceStat?.isFile())
1249
+ continue;
1250
+ const sourceCommit = await git.lastCommitTouching(reference.value);
1251
+ if (sourceCommit !== docCommit && !await git.wasRenamed(reference.value, docCommit)) {
1252
+ findings.push(staleAnchorFinding(docPath, reference));
1253
+ }
1254
+ }
1255
+ return findings;
1256
+ }
1257
+ async function detectDrift(options) {
1258
+ const git = options.git ?? makeDriftGit(options.projectRoot);
1259
+ const docs = await collectFiles(options.projectRoot, {
1260
+ docs: true,
1261
+ ...options.docsGlobs !== undefined ? { patterns: options.docsGlobs } : {},
1262
+ ...options.ignoreGlobs !== undefined ? { ignore: options.ignoreGlobs } : {}
1263
+ });
1264
+ const findings = [];
1265
+ let degraded = false;
1266
+ for (const docPath of docs) {
1267
+ try {
1268
+ findings.push(...await detectDeletedReferences(options.projectRoot, docPath, git));
1269
+ findings.push(...await detectStaleAnchors(options.projectRoot, docPath, git));
1270
+ } catch {
1271
+ degraded = true;
1272
+ }
1273
+ }
1274
+ return {
1275
+ generatedAt: new Date().toISOString(),
1276
+ scanned: docs.length,
1277
+ degraded,
1278
+ findings
1279
+ };
1280
+ }
1281
+
1282
+ // packages/standard-plugin/src/drift/plugin.ts
1283
+ var DOCS_DRIFT_VALIDATOR_ID = "std:docs-drift";
1284
+ var DOCS_DRIFT_CLI_ID = "std:drift";
1285
+ var DOCS_DRIFT_STAGE_ID = "docs-drift";
1286
+ var DOCS_DRIFT_CAPABILITY_ID = "std:docs-drift-capability";
1287
+ var DOCS_DRIFT_VALIDATOR = {
1288
+ id: DOCS_DRIFT_VALIDATOR_ID,
1289
+ category: "regression",
1290
+ description: "Detect documentation references that drifted from the source tree."
1291
+ };
1292
+ var DOCS_DRIFT_STAGE_MUTATION = Schema.decodeUnknownSync(StageMutationSchema)({
1293
+ op: "insert",
1294
+ stage: {
1295
+ id: DOCS_DRIFT_STAGE_ID,
1296
+ kind: "gate",
1297
+ before: ["merge-gate"],
1298
+ after: ["open-pr"]
1299
+ },
1300
+ contributedBy: DOCS_DRIFT_STAGE_ID
1301
+ });
1302
+ var DOCS_DRIFT_CLI_COMMAND = "rig drift [--docs <csv>] [--ignore <csv>] [--fail-on-drift] [--json]";
1303
+ function highConfidenceDriftFindings(report) {
1304
+ return report.findings.filter((finding) => finding.confidence === "high");
1305
+ }
1306
+ function driftGateResult(report, mode = "enforce") {
1307
+ const high = highConfidenceDriftFindings(report);
1308
+ if (mode === "enforce" && high.length > 0) {
1309
+ return { kind: "block", reason: `${high.length} high-confidence documentation drift finding(s).` };
1310
+ }
1311
+ return { kind: "allow" };
1312
+ }
1313
+ function createDocsDriftGateStage(options = {}) {
1314
+ return async (ctx) => {
1315
+ const projectRoot = typeof ctx.metadata?.projectRoot === "string" ? ctx.metadata.projectRoot : process.cwd();
1316
+ const report = await detectDrift({
1317
+ projectRoot,
1318
+ ...options.docsGlobs !== undefined ? { docsGlobs: options.docsGlobs } : {},
1319
+ ...options.ignoreGlobs !== undefined ? { ignoreGlobs: options.ignoreGlobs } : {}
1320
+ });
1321
+ return driftGateResult(report, options.failOnDrift ? "enforce" : "observe");
1322
+ };
1323
+ }
1324
+ async function runDocsDriftValidation(options) {
1325
+ const report = await detectDrift(options);
1326
+ const high = highConfidenceDriftFindings(report);
1327
+ const passed = options.failOnDrift ? high.length === 0 : true;
1328
+ const findingWord = report.findings.length === 1 ? "finding" : "findings";
1329
+ return {
1330
+ id: DOCS_DRIFT_VALIDATOR_ID,
1331
+ passed,
1332
+ summary: `docs drift scanned ${report.scanned} doc(s), ${report.findings.length} ${findingWord}`,
1333
+ details: JSON.stringify(report)
1334
+ };
1335
+ }
1336
+ function createDocsDriftValidator(options = {}) {
1337
+ return {
1338
+ ...DOCS_DRIFT_VALIDATOR,
1339
+ async run(ctx) {
1340
+ return runDocsDriftValidation({
1341
+ projectRoot: ctx.workspaceRoot,
1342
+ ...options.docsGlobs !== undefined ? { docsGlobs: options.docsGlobs } : {},
1343
+ ...options.ignoreGlobs !== undefined ? { ignoreGlobs: options.ignoreGlobs } : {},
1344
+ ...options.failOnDrift !== undefined ? { failOnDrift: options.failOnDrift } : {}
1345
+ });
1346
+ }
1347
+ };
1348
+ }
1349
+ function takeOptionValue(args, index, flag) {
1350
+ const value = args[index + 1];
1351
+ if (!value)
1352
+ throw new Error(`${flag} requires a value`);
1353
+ return value;
1354
+ }
1355
+ function takeFlag(args, flag) {
1356
+ const rest = [...args];
1357
+ const index = rest.indexOf(flag);
1358
+ if (index < 0)
1359
+ return { value: false, rest };
1360
+ rest.splice(index, 1);
1361
+ return { value: true, rest };
1362
+ }
1363
+ function takeOption(args, flag) {
1364
+ const rest = [...args];
1365
+ const index = rest.indexOf(flag);
1366
+ if (index < 0)
1367
+ return { rest };
1368
+ const value = rest[index + 1];
1369
+ if (!value || value.startsWith("-"))
1370
+ throw new Error(`${flag} requires a value.`);
1371
+ rest.splice(index, 2);
1372
+ return { value, rest };
1373
+ }
1374
+ function requireNoExtraArgs(args, usage) {
1375
+ if (args.length > 0)
1376
+ throw new Error(`Unexpected argument: ${args[0]}
1377
+ Usage: ${usage}`);
1378
+ }
1379
+ function parseCsv(value) {
1380
+ return value?.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0) ?? [];
1381
+ }
1382
+ function driftSummary(report) {
1383
+ const highConfidence = highConfidenceDriftFindings(report).length;
1384
+ return { total: report.findings.length, highConfidence, degraded: report.degraded };
1385
+ }
1386
+ async function executeDrift(context, args, options = {}) {
1387
+ const json = takeFlag(args, "--json");
1388
+ const docs = takeOption(json.rest, "--docs");
1389
+ const ignore = takeOption(docs.rest, "--ignore");
1390
+ const failOnDrift = takeFlag(ignore.rest, "--fail-on-drift");
1391
+ requireNoExtraArgs(failOnDrift.rest, "rig drift [--docs <csv>] [--ignore <csv>] [--fail-on-drift] [--json]");
1392
+ const docsGlobs = parseCsv(docs.value);
1393
+ const ignoreGlobs = parseCsv(ignore.value);
1394
+ const effectiveDocsGlobs = docsGlobs.length > 0 ? docsGlobs : options.docsGlobs;
1395
+ const effectiveIgnoreGlobs = ignoreGlobs.length > 0 ? ignoreGlobs : options.ignoreGlobs;
1396
+ const effectiveFailOnDrift = failOnDrift.value || options.failOnDrift === true;
1397
+ const report = await detectDrift({
1398
+ projectRoot: context.projectRoot,
1399
+ ...effectiveDocsGlobs !== undefined ? { docsGlobs: effectiveDocsGlobs } : {},
1400
+ ...effectiveIgnoreGlobs !== undefined ? { ignoreGlobs: effectiveIgnoreGlobs } : {}
1401
+ });
1402
+ const failed = effectiveFailOnDrift && highConfidenceDriftFindings(report).length > 0;
1403
+ const details = { report, summary: driftSummary(report), failOnDrift: effectiveFailOnDrift, failed };
1404
+ if (context.outputMode === "text") {
1405
+ if (json.value)
1406
+ console.log(JSON.stringify(details, null, 2));
1407
+ else
1408
+ console.log(report.findings.length === 0 ? `No drift findings across ${report.scanned} documents.` : report.findings.map((finding) => `${finding.docPath}:${finding.line ?? "?"} ${finding.kind} ${finding.confidence} ${finding.detail}`).join(`
1409
+ `));
1410
+ }
1411
+ return { ok: !failed, group: "drift", command: "scan", details };
1412
+ }
1413
+ function createDocsDriftRuntimeCliCommand(options = {}) {
1414
+ return {
1415
+ id: DOCS_DRIFT_CLI_ID,
1416
+ family: "drift",
1417
+ command: DOCS_DRIFT_CLI_COMMAND,
1418
+ description: "Scan documentation for stale code references.",
1419
+ usage: DOCS_DRIFT_CLI_COMMAND,
1420
+ projectRequired: true,
1421
+ run: (context, args) => executeDrift(context, args, options)
1422
+ };
1423
+ }
1424
+ var DOCS_DRIFT_RUNTIME_CLI_COMMAND = createDocsDriftRuntimeCliCommand();
1425
+ async function runDriftCli(args, options = {}) {
1426
+ const docsGlobs = [];
1427
+ const ignoreGlobs = [];
1428
+ let json = false;
1429
+ let failOnDrift = false;
1430
+ for (let index = 0;index < args.length; index += 1) {
1431
+ const arg = args[index];
1432
+ if (arg === "--json") {
1433
+ json = true;
1434
+ continue;
1435
+ }
1436
+ if (arg === "--fail-on-drift") {
1437
+ failOnDrift = true;
1438
+ continue;
1439
+ }
1440
+ if (arg === "--docs") {
1441
+ docsGlobs.push(takeOptionValue(args, index, arg));
1442
+ index += 1;
1443
+ continue;
1444
+ }
1445
+ if (arg === "--ignore") {
1446
+ ignoreGlobs.push(takeOptionValue(args, index, arg));
1447
+ index += 1;
1448
+ continue;
1449
+ }
1450
+ throw new Error(`Unknown rig drift argument: ${arg}`);
1451
+ }
1452
+ const report = await detectDrift({
1453
+ projectRoot: options.projectRoot ?? process.cwd(),
1454
+ ...docsGlobs.length > 0 ? { docsGlobs } : {},
1455
+ ...ignoreGlobs.length > 0 ? { ignoreGlobs } : {}
1456
+ });
1457
+ const write = options.write ?? ((message) => console.log(message));
1458
+ if (json) {
1459
+ write(JSON.stringify(report));
1460
+ } else {
1461
+ write(`Scanned ${report.scanned} doc(s); ${report.findings.length} drift finding(s).`);
1462
+ for (const finding of report.findings) {
1463
+ write(`${finding.confidence.toUpperCase()} ${finding.kind} ${finding.docPath}${finding.line ? `:${finding.line}` : ""} ${finding.reference ?? ""} \u2014 ${finding.detail}`);
1464
+ }
1465
+ }
1466
+ const high = highConfidenceDriftFindings(report);
1467
+ if (failOnDrift && high.length > 0) {
1468
+ options.writeError?.(`${high.length} high-confidence drift finding(s).`);
1469
+ return 2;
1470
+ }
1471
+ return 0;
1472
+ }
1473
+ // packages/standard-plugin/src/drift/judge.ts
1474
+ async function judgeDocumentationDrift(provider, input) {
1475
+ const result = await provider.judge(input);
1476
+ return result.mismatches.map((mismatch) => ({
1477
+ kind: "semantic-mismatch",
1478
+ docPath: input.docPath,
1479
+ line: mismatch.line ?? null,
1480
+ reference: mismatch.reference ?? input.reference ?? null,
1481
+ detail: mismatch.detail,
1482
+ confidence: mismatch.confidence ?? "medium"
1483
+ }));
1484
+ }
1485
+ // packages/standard-plugin/src/plugin.ts
1486
+ var DOCS_HEALTH_PANEL_ID = "docs-health";
581
1487
  function requireStringField(config, field, kind) {
582
1488
  const value = config[field];
583
1489
  if (!value) {
@@ -585,11 +1491,78 @@ function requireStringField(config, field, kind) {
585
1491
  }
586
1492
  return value;
587
1493
  }
1494
+ function isRecord(value) {
1495
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
1496
+ }
1497
+ function optionalString(value) {
1498
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
1499
+ }
1500
+ function parseGitHubProjectsOptions(value) {
1501
+ if (!isRecord(value))
1502
+ return;
1503
+ const statusesSource = isRecord(value.statuses) ? value.statuses : undefined;
1504
+ const statuses = {};
1505
+ for (const key of ["todo", "running", "prOpen", "ciFixing", "merging", "done", "needsAttention"]) {
1506
+ const status = optionalString(statusesSource?.[key]);
1507
+ if (status)
1508
+ statuses[key] = status;
1509
+ }
1510
+ const parsed = {};
1511
+ if (typeof value.enabled === "boolean")
1512
+ parsed.enabled = value.enabled;
1513
+ const projectId = optionalString(value.projectId);
1514
+ if (projectId)
1515
+ parsed.projectId = projectId;
1516
+ const statusFieldId = optionalString(value.statusFieldId);
1517
+ if (statusFieldId)
1518
+ parsed.statusFieldId = statusFieldId;
1519
+ if (Object.keys(statuses).length > 0)
1520
+ parsed.statuses = statuses;
1521
+ return parsed;
1522
+ }
1523
+ function githubProjectsOptionsFromConfig(config, context) {
1524
+ const rigConfig = isRecord(context?.rigConfig) ? context.rigConfig : undefined;
1525
+ const github = isRecord(rigConfig?.github) ? rigConfig.github : undefined;
1526
+ return parseGitHubProjectsOptions(config.options?.projects) ?? parseGitHubProjectsOptions(github?.projects);
1527
+ }
1528
+ function booleanOption(value) {
1529
+ return typeof value === "boolean" ? value : undefined;
1530
+ }
1531
+ function panelProjectRoot(context) {
1532
+ return isRecord(context) && typeof context.projectRoot === "string" && context.projectRoot.length > 0 ? context.projectRoot : null;
1533
+ }
1534
+ function driftFindingPanelId(finding, index) {
1535
+ return `${finding.docPath}:${finding.line ?? index}:${finding.kind}`;
1536
+ }
1537
+ function createDocsHealthPanelProducer(options = {}) {
1538
+ return async (context) => {
1539
+ const projectRoot = panelProjectRoot(context);
1540
+ if (!projectRoot)
1541
+ return;
1542
+ const report = await detectDrift({
1543
+ projectRoot,
1544
+ ...options.docsGlobs !== undefined ? { docsGlobs: options.docsGlobs } : {},
1545
+ ...options.ignoreGlobs !== undefined ? { ignoreGlobs: options.ignoreGlobs } : {}
1546
+ });
1547
+ return {
1548
+ findings: report.findings.map((finding, index) => ({
1549
+ id: driftFindingPanelId(finding, index),
1550
+ docPath: finding.docPath,
1551
+ kind: finding.kind,
1552
+ confidence: finding.confidence,
1553
+ summary: finding.detail,
1554
+ taskId: null
1555
+ })),
1556
+ degraded: report.degraded ? "drift scan degraded" : null
1557
+ };
1558
+ };
1559
+ }
588
1560
  function standardPlugin(opts = {}) {
589
1561
  return definePlugin({
590
1562
  name: "rig-standard",
591
1563
  version: "0.1.0",
592
1564
  contributes: {
1565
+ validators: [DOCS_DRIFT_VALIDATOR],
593
1566
  taskSources: [
594
1567
  {
595
1568
  id: "std:github-issues",
@@ -601,21 +1574,46 @@ function standardPlugin(opts = {}) {
601
1574
  kind: "files",
602
1575
  description: "JSON files in a local directory"
603
1576
  }
604
- ]
1577
+ ],
1578
+ capabilities: [
1579
+ { id: DOCS_DRIFT_CAPABILITY_ID, title: "Documentation drift detection", commandId: DOCS_DRIFT_CLI_ID, panelId: DOCS_HEALTH_PANEL_ID }
1580
+ ],
1581
+ panels: [
1582
+ { id: DOCS_HEALTH_PANEL_ID, slot: "capability", title: "Documentation drift", capabilityId: DOCS_DRIFT_CAPABILITY_ID }
1583
+ ],
1584
+ cliCommands: [
1585
+ {
1586
+ id: DOCS_DRIFT_CLI_ID,
1587
+ family: "drift",
1588
+ command: DOCS_DRIFT_CLI_COMMAND,
1589
+ description: "Scan documentation for stale code references.",
1590
+ projectRequired: true
1591
+ }
1592
+ ],
1593
+ stageMutations: [DOCS_DRIFT_STAGE_MUTATION]
605
1594
  }
606
1595
  }, {
1596
+ validators: [createDocsDriftValidator(opts.drift)],
1597
+ stages: { [DOCS_DRIFT_STAGE_ID]: createDocsDriftGateStage(opts.drift) },
1598
+ featureCapabilities: [
1599
+ { id: DOCS_DRIFT_CAPABILITY_ID, title: "Documentation drift detection", commandId: DOCS_DRIFT_CLI_ID, panelId: DOCS_HEALTH_PANEL_ID }
1600
+ ],
1601
+ panels: [
1602
+ { id: DOCS_HEALTH_PANEL_ID, slot: "capability", title: "Documentation drift", capabilityId: DOCS_DRIFT_CAPABILITY_ID, produce: createDocsHealthPanelProducer(opts.drift) }
1603
+ ],
1604
+ cliCommands: [createDocsDriftRuntimeCliCommand(opts.drift)],
607
1605
  taskSources: [
608
1606
  {
609
1607
  id: "std:github-issues",
610
1608
  kind: "github-issues",
611
1609
  description: "GitHub Issues via gh CLI",
612
- factory(config) {
1610
+ factory(config, context) {
613
1611
  const options = {
614
1612
  owner: requireStringField(config, "owner", "github-issues"),
615
1613
  repo: requireStringField(config, "repo", "github-issues")
616
1614
  };
617
- if (opts.githubCredentialProvider)
618
- options.credentialProvider = opts.githubCredentialProvider;
1615
+ const credentialProviderOptions = context?.projectRoot ? { stateDir: resolve4(context.projectRoot, ".rig", "state") } : {};
1616
+ options.credentialProvider = opts.githubCredentialProvider ?? createStateGitHubCredentialProvider(credentialProviderOptions);
619
1617
  if (opts.githubWorkspaceId)
620
1618
  options.workspaceId = opts.githubWorkspaceId;
621
1619
  if (opts.githubUserId)
@@ -637,6 +1635,12 @@ function standardPlugin(opts = {}) {
637
1635
  const listLimit = typeof config.options?.listLimit === "number" ? config.options.listLimit : undefined;
638
1636
  if (listLimit !== undefined)
639
1637
  options.listLimit = listLimit;
1638
+ const projects = githubProjectsOptionsFromConfig(config, context);
1639
+ if (projects)
1640
+ options.projects = projects;
1641
+ const useNativeDependencies = booleanOption(config.options?.useNativeDependencies);
1642
+ if (useNativeDependencies !== undefined)
1643
+ options.useNativeDependencies = useNativeDependencies;
640
1644
  return createGitHubIssuesTaskSource(options);
641
1645
  }
642
1646
  },
@@ -644,19 +1648,55 @@ function standardPlugin(opts = {}) {
644
1648
  id: "std:files",
645
1649
  kind: "files",
646
1650
  description: "JSON files in a local directory",
647
- factory(config) {
1651
+ factory(config, context) {
648
1652
  return createFilesTaskSource({
649
- path: requireStringField(config, "path", "files")
1653
+ path: requireStringField(config, "path", "files"),
1654
+ ...context?.projectRoot ? { projectRoot: context.projectRoot } : {}
650
1655
  });
651
1656
  }
652
1657
  }
653
1658
  ]
654
1659
  });
655
1660
  }
1661
+ // packages/standard-plugin/src/bundle.ts
1662
+ import { createBlockerClassifierPlugin } from "@rig/blocker-classifier-plugin/plugin";
1663
+ import { createDefaultLifecyclePlugin } from "@rig/bundle-default-lifecycle/plugin";
1664
+ import { createDependencyGraphPlugin } from "@rig/dependency-graph-plugin/plugin";
1665
+ import { createPlanningPlugin } from "@rig/planning-plugin/plugin";
1666
+ import { createSupervisorPlugin } from "@rig/supervisor-plugin/plugin";
1667
+ function standardProjectPlugins(options = {}) {
1668
+ return [
1669
+ createDefaultLifecyclePlugin(),
1670
+ createDependencyGraphPlugin(),
1671
+ createBlockerClassifierPlugin(),
1672
+ createPlanningPlugin(),
1673
+ createSupervisorPlugin(),
1674
+ standardPlugin(options.standard)
1675
+ ];
1676
+ }
656
1677
  export {
1678
+ standardProjectPlugins,
1679
+ runDriftCli,
1680
+ runDocsDriftValidation,
1681
+ makeDriftGit,
1682
+ judgeDocumentationDrift,
1683
+ extractDriftReferences,
1684
+ executeDrift,
1685
+ driftGateResult,
1686
+ detectStaleAnchors,
1687
+ detectDrift,
1688
+ detectDeletedReferences,
657
1689
  standardPlugin as default,
658
1690
  createStateGitHubCredentialProvider,
659
1691
  createGitHubIssuesTaskSource,
660
1692
  createFilesTaskSource,
661
- createEnvGitHubCredentialProvider
1693
+ createEnvGitHubCredentialProvider,
1694
+ createDocsDriftValidator,
1695
+ createDocsDriftRuntimeCliCommand,
1696
+ DOCS_HEALTH_PANEL_ID,
1697
+ DOCS_DRIFT_VALIDATOR_ID,
1698
+ DOCS_DRIFT_STAGE_ID,
1699
+ DOCS_DRIFT_RUNTIME_CLI_COMMAND,
1700
+ DOCS_DRIFT_CLI_ID,
1701
+ DOCS_DRIFT_CAPABILITY_ID
662
1702
  };