@h-rig/standard-plugin 0.0.6-alpha.9 → 0.0.6-alpha.91

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.
@@ -0,0 +1,18 @@
1
+ import type { RegisteredTaskSource } from "@rig/contracts";
2
+ export interface FilesTaskSourceOptions {
3
+ /**
4
+ * Directory containing one JSON file per task. Use either `path` (matches
5
+ * the `taskSource.path` field in rig.config.ts) or `dir` (back-compat).
6
+ */
7
+ path?: string;
8
+ dir?: string;
9
+ pattern?: RegExp;
10
+ /**
11
+ * Root a relative `path`/`dir` resolves against. The serving process's cwd
12
+ * is NOT the project (workspace-spawned servers run from the engine
13
+ * checkout), so without this a relative path silently reads the WRONG
14
+ * repo's tasks. Defaults to cwd for direct programmatic use.
15
+ */
16
+ projectRoot?: string;
17
+ }
18
+ export declare function createFilesTaskSource(opts: FilesTaskSourceOptions): RegisteredTaskSource;
@@ -1,7 +1,7 @@
1
1
  // @bun
2
2
  // packages/standard-plugin/src/files-source.ts
3
3
  import { readFileSync, readdirSync, existsSync, statSync, writeFileSync } from "fs";
4
- import { join, basename } from "path";
4
+ import { join, basename, isAbsolute, resolve } from "path";
5
5
  var DEFAULT_PATTERN = /\.(task\.)?json$/;
6
6
  function readTaskFile(file, pattern) {
7
7
  const raw = JSON.parse(readFileSync(file, "utf-8"));
@@ -22,10 +22,11 @@ function readTaskFile(file, pattern) {
22
22
  }
23
23
  function createFilesTaskSource(opts) {
24
24
  const pattern = opts.pattern ?? DEFAULT_PATTERN;
25
- const directory = opts.path ?? opts.dir;
26
- if (!directory) {
25
+ const configured = opts.path ?? opts.dir;
26
+ if (!configured) {
27
27
  throw new Error("createFilesTaskSource: either `path` or `dir` must be provided");
28
28
  }
29
+ const directory = isAbsolute(configured) ? configured : resolve(opts.projectRoot ?? process.cwd(), configured);
29
30
  const findTaskFile = (id) => {
30
31
  if (!existsSync(directory))
31
32
  return;
@@ -0,0 +1,78 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import type { RegisteredTaskSource, TaskRecord } from "@rig/contracts";
3
+ export type GitHubCredentialPurpose = "selected-repo" | "admin-fallback";
4
+ export type GitHubIssueUpdatesMode = "lifecycle" | "minimal" | "off";
5
+ export interface GitHubCredentialProvider {
6
+ resolveGitHubToken(input: {
7
+ owner: string;
8
+ repo: string;
9
+ workspaceId: string;
10
+ userId?: string;
11
+ purpose: GitHubCredentialPurpose;
12
+ }): Promise<{
13
+ token: string;
14
+ source: "signed-in-user" | "host-admin-fallback";
15
+ }>;
16
+ }
17
+ export type GitHubProjectLifecycleStatus = "todo" | "running" | "prOpen" | "ciFixing" | "merging" | "done" | "needsAttention";
18
+ export interface GitHubProjectsOptions {
19
+ enabled?: boolean;
20
+ projectId?: string;
21
+ statusFieldId?: string;
22
+ statuses?: Partial<Record<GitHubProjectLifecycleStatus, string>>;
23
+ }
24
+ export interface GitHubIssuesOptions {
25
+ owner: string;
26
+ repo: string;
27
+ labels?: readonly string[];
28
+ state?: "open" | "closed" | "all";
29
+ assignee?: string;
30
+ ghBinary?: string;
31
+ workspaceId?: string;
32
+ userId?: string;
33
+ credentialProvider?: GitHubCredentialProvider;
34
+ issueUpdates?: GitHubIssueUpdatesMode;
35
+ /** Timeout for every gh CLI call. Defaults to 15 seconds. */
36
+ timeoutMs?: number;
37
+ /** Maximum issue-list rows before Rig fails loudly instead of silently truncating. Defaults to 1,000. */
38
+ listLimit?: number;
39
+ /** @internal — for testing. Override the spawnSync used by the adapter. */
40
+ spawn?: typeof spawnSync;
41
+ /** Notify the host that issue-backed task state changed and snapshots should refresh. */
42
+ onTaskChanged?: (event: {
43
+ repo: string;
44
+ id: string;
45
+ status?: string;
46
+ reason: "github-issue-updated";
47
+ }) => void;
48
+ /** Optional GitHub Projects (v2) status-field sync mapped from Rig task status. */
49
+ projects?: GitHubProjectsOptions;
50
+ }
51
+ export interface GitHubIssueCreateInput {
52
+ title: string;
53
+ body?: string;
54
+ labels?: readonly string[];
55
+ }
56
+ export interface GitHubIssuesTaskSource extends RegisteredTaskSource {
57
+ addLabels(id: string, labels: readonly string[]): Promise<void>;
58
+ removeLabels(id: string, labels: readonly string[]): Promise<void>;
59
+ createIssue(input: GitHubIssueCreateInput): Promise<TaskRecord>;
60
+ getIssueBody(id: string): Promise<string | undefined>;
61
+ }
62
+ export declare function createEnvGitHubCredentialProvider(): GitHubCredentialProvider;
63
+ export declare function createStateGitHubCredentialProvider(options?: {
64
+ stateFile?: string;
65
+ stateDir?: string;
66
+ }): GitHubCredentialProvider;
67
+ export declare const RIG_STATUS_COMMENT_MARKER = "<!-- rig:status-comment -->";
68
+ export declare const RIG_METADATA_START = "<!-- rig:metadata:start -->";
69
+ export declare const RIG_METADATA_END = "<!-- rig:metadata:end -->";
70
+ export declare function updateRigOwnedMetadataBlock(body: string, metadata: Record<string, unknown>): string;
71
+ export declare function buildRigStickyStatusComment(input: {
72
+ status: "running" | "prOpen" | "ciFixing" | "done" | "needsAttention" | string;
73
+ summary: string;
74
+ runId?: string;
75
+ prUrl?: string;
76
+ details?: readonly string[];
77
+ }): string;
78
+ export declare function createGitHubIssuesTaskSource(opts: GitHubIssuesOptions): GitHubIssuesTaskSource;
@@ -11,9 +11,9 @@ function createEnvGitHubCredentialProvider() {
11
11
  return {
12
12
  async resolveGitHubToken(input) {
13
13
  if (input.purpose === "selected-repo") {
14
- return { token: cleanToken(process.env.RIG_GITHUB_SELECTED_TOKEN ?? null) ?? "", source: "signed-in-user" };
14
+ return { token: cleanToken(process.env.RIG_GITHUB_SELECTED_TOKEN ?? process.env.RIG_GITHUB_TOKEN ?? null) ?? "", source: "signed-in-user" };
15
15
  }
16
- const token = cleanToken(process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? null);
16
+ const token = cleanToken(process.env.RIG_GITHUB_TOKEN ?? process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? null);
17
17
  if (!token) {
18
18
  throw new Error("No host GitHub token is configured for admin fallback.");
19
19
  }
@@ -44,12 +44,12 @@ function createStateGitHubCredentialProvider(options = {}) {
44
44
  async resolveGitHubToken(input) {
45
45
  const token = readToken();
46
46
  if (input.purpose === "selected-repo") {
47
- return { token: token ?? "", source: "signed-in-user" };
47
+ return { token: token ?? cleanToken(process.env.RIG_GITHUB_SELECTED_TOKEN ?? process.env.RIG_GITHUB_TOKEN ?? null) ?? "", source: "signed-in-user" };
48
48
  }
49
49
  if (token) {
50
50
  return { token, source: "signed-in-user" };
51
51
  }
52
- const fallback = cleanToken(process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? null);
52
+ const fallback = cleanToken(process.env.RIG_GITHUB_TOKEN ?? process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? null);
53
53
  if (!fallback) {
54
54
  throw new Error("No signed-in GitHub token is stored for Rig and no host admin fallback token is configured.");
55
55
  }
@@ -183,13 +183,21 @@ function isRigStickyStatusComment(body) {
183
183
  return body.includes(RIG_STATUS_COMMENT_MARKER);
184
184
  }
185
185
  function ghSpawnOptions(extraEnv, timeoutMs) {
186
- if (!extraEnv)
187
- return { encoding: "utf-8", timeout: timeoutMs };
188
- return { encoding: "utf-8", timeout: timeoutMs, env: { ...process.env, ...extraEnv } };
186
+ return {
187
+ encoding: "utf-8",
188
+ timeout: timeoutMs,
189
+ env: {
190
+ ...process.env,
191
+ ...process.env.GH_TOKEN !== undefined ? { GH_TOKEN: process.env.GH_TOKEN } : {},
192
+ ...process.env.GITHUB_TOKEN !== undefined ? { GITHUB_TOKEN: process.env.GITHUB_TOKEN } : {},
193
+ ...process.env.RIG_GITHUB_TOKEN !== undefined ? { RIG_GITHUB_TOKEN: process.env.RIG_GITHUB_TOKEN } : {},
194
+ ...extraEnv ?? {}
195
+ }
196
+ };
189
197
  }
190
198
  function credentialEnv(token) {
191
199
  const clean = token?.trim() ?? "";
192
- return { GH_TOKEN: clean, GITHUB_TOKEN: clean };
200
+ return { GH_TOKEN: clean, GITHUB_TOKEN: clean, RIG_GITHUB_TOKEN: clean };
193
201
  }
194
202
  async function resolveCredentialEnv(opts, purpose) {
195
203
  if (!opts.credentialProvider)
@@ -204,28 +212,239 @@ async function resolveCredentialEnv(opts, purpose) {
204
212
  const resolved = await opts.credentialProvider.resolveGitHubToken(input);
205
213
  return credentialEnv(resolved.token);
206
214
  }
215
+ function tokenDiagnostic(value) {
216
+ const clean = value?.trim() ?? "";
217
+ return clean ? `present(len=${clean.length})` : "missing";
218
+ }
207
219
  function runGh(bin, args, spawn, extraEnv, timeoutMs) {
208
- const res = spawn(bin, [...args], ghSpawnOptions(extraEnv, timeoutMs));
209
- assertGhSuccess(args, res);
220
+ const options = ghSpawnOptions(extraEnv, timeoutMs);
221
+ const res = spawn(bin, [...args], options);
222
+ assertGhSuccess(args, res, options.env);
210
223
  if (!res.stdout || res.stdout.trim() === "")
211
224
  return [];
212
225
  return JSON.parse(res.stdout);
213
226
  }
214
227
  function runGhVoid(bin, args, spawn, extraEnv, timeoutMs) {
215
- const res = spawn(bin, [...args], ghSpawnOptions(extraEnv, timeoutMs));
216
- assertGhSuccess(args, res);
228
+ const options = ghSpawnOptions(extraEnv, timeoutMs);
229
+ const res = spawn(bin, [...args], options);
230
+ assertGhSuccess(args, res, options.env);
217
231
  }
218
- function assertGhSuccess(args, res) {
232
+ function assertGhSuccess(args, res, env) {
219
233
  if (res.error) {
220
234
  const msg = res.error.message ?? String(res.error);
221
235
  throw new Error(`gh CLI not available \u2014 install gh (brew install gh / apt install gh): ${msg}`);
222
236
  }
223
237
  if (res.status !== 0) {
224
- throw new Error(`gh ${args.join(" ")} failed (exit ${res.status}): ${res.stderr}`);
238
+ throw new Error(`gh ${args.join(" ")} failed (exit ${res.status}): ${res.stderr}
239
+ [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)}`);
240
+ }
241
+ }
242
+ var DEFAULT_PROJECT_STATUSES = {
243
+ todo: "Todo",
244
+ running: "In Progress",
245
+ prOpen: "In Review",
246
+ ciFixing: "In Review",
247
+ merging: "In Review",
248
+ done: "Done",
249
+ needsAttention: "Needs Attention"
250
+ };
251
+ function asProjectRecord(value) {
252
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
253
+ }
254
+ function projectString(value) {
255
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
256
+ }
257
+ function projectLifecycleStatusForTaskStatus(status) {
258
+ const normalized = status?.trim().toLowerCase().replace(/[-\s]+/g, "_");
259
+ switch (normalized) {
260
+ case "draft":
261
+ case "open":
262
+ case "queued":
263
+ case "ready":
264
+ return "todo";
265
+ case "running":
266
+ case "in_progress":
267
+ return "running";
268
+ case "under_review":
269
+ case "review":
270
+ case "pr_open":
271
+ return "prOpen";
272
+ case "ci_fixing":
273
+ case "fixing":
274
+ return "ciFixing";
275
+ case "merging":
276
+ case "merge":
277
+ return "merging";
278
+ case "closed":
279
+ case "completed":
280
+ case "done":
281
+ return "done";
282
+ case "blocked":
283
+ case "cancelled":
284
+ case "failed":
285
+ case "needs_attention":
286
+ return "needsAttention";
287
+ default:
288
+ return null;
225
289
  }
226
290
  }
291
+ function ghGraphQLFetch(bin, spawnFn, extraEnv, timeoutMs) {
292
+ return async (query, variables) => {
293
+ const args = ["api", "graphql", "-f", `query=${query}`];
294
+ for (const [key, value] of Object.entries(variables)) {
295
+ if (value === undefined || value === null)
296
+ continue;
297
+ args.push("-f", `${key}=${String(value)}`);
298
+ }
299
+ const response = runGh(bin, args, spawnFn, extraEnv, timeoutMs);
300
+ return asProjectRecord(response)?.data ?? response;
301
+ };
302
+ }
303
+ function projectStatusFieldFrom(data, projectId) {
304
+ const fields = asProjectRecord(asProjectRecord(asProjectRecord(data)?.node)?.fields)?.nodes;
305
+ for (const node of Array.isArray(fields) ? fields : []) {
306
+ const record = asProjectRecord(node);
307
+ if (projectString(record?.name)?.toLowerCase() !== "status")
308
+ continue;
309
+ const id = projectString(record?.id);
310
+ if (!id)
311
+ continue;
312
+ const options = Array.isArray(record?.options) ? record.options.flatMap((option) => {
313
+ const optionRecord = asProjectRecord(option);
314
+ const optionId = projectString(optionRecord?.id);
315
+ const name = projectString(optionRecord?.name);
316
+ return optionId && name ? [{ id: optionId, name }] : [];
317
+ }) : [];
318
+ return { id, name: "Status", options };
319
+ }
320
+ throw new Error(`GitHub Project ${projectId} does not expose a Status single-select field.`);
321
+ }
322
+ async function resolveProjectStatusField(input) {
323
+ const query = `
324
+ query RigProjectStatusField($projectId: ID!) {
325
+ node(id: $projectId) {
326
+ ... on ProjectV2 {
327
+ fields(first: 50) {
328
+ nodes {
329
+ ... on ProjectV2FieldCommon { id name }
330
+ ... on ProjectV2SingleSelectField { id name options { id name } }
331
+ }
332
+ }
333
+ }
334
+ }
335
+ }
336
+ `;
337
+ return projectStatusFieldFrom(await input.fetchGraphQL(query, { projectId: input.projectId }, input.token), input.projectId);
338
+ }
339
+ async function ensureIssueProjectItem(input) {
340
+ const query = `
341
+ query RigFindProjectIssueItem($projectId: ID!) {
342
+ node(id: $projectId) {
343
+ ... on ProjectV2 {
344
+ items(first: 100) { nodes { id content { ... on Issue { id } } } }
345
+ }
346
+ }
347
+ }
348
+ `;
349
+ const data = await input.fetchGraphQL(query, { projectId: input.projectId }, input.token);
350
+ const nodes = asProjectRecord(asProjectRecord(asProjectRecord(data)?.node)?.items)?.nodes;
351
+ for (const node of Array.isArray(nodes) ? nodes : []) {
352
+ const record = asProjectRecord(node);
353
+ const content = asProjectRecord(record?.content);
354
+ if (projectString(content?.id) === input.issueNodeId) {
355
+ const id2 = projectString(record?.id);
356
+ if (id2)
357
+ return { id: id2, created: false };
358
+ }
359
+ }
360
+ const mutation = `
361
+ mutation RigAddIssueToProject($projectId: ID!, $contentId: ID!) {
362
+ addProjectV2ItemById(input: { projectId: $projectId, contentId: $contentId }) { item { id } }
363
+ }
364
+ `;
365
+ const created = await input.fetchGraphQL(mutation, { projectId: input.projectId, contentId: input.issueNodeId }, input.token);
366
+ const addResult = asProjectRecord(asProjectRecord(created)?.addProjectV2ItemById);
367
+ const id = projectString(asProjectRecord(addResult?.item)?.id);
368
+ if (!id)
369
+ throw new Error("GitHub Project item creation did not return an item id.");
370
+ return { id, created: true };
371
+ }
372
+ async function updateIssueProjectStatus(input) {
373
+ const mutation = `
374
+ mutation RigUpdateProjectStatus($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
375
+ updateProjectV2ItemFieldValue(input: {
376
+ projectId: $projectId,
377
+ itemId: $itemId,
378
+ fieldId: $fieldId,
379
+ value: { singleSelectOptionId: $optionId }
380
+ }) { projectV2Item { id } }
381
+ }
382
+ `;
383
+ await input.fetchGraphQL(mutation, {
384
+ projectId: input.projectId,
385
+ itemId: input.itemId,
386
+ fieldId: input.fieldId,
387
+ optionId: input.optionId
388
+ }, input.token);
389
+ }
390
+ function fetchIssueNodeId(bin, repo, spawnFn, id, extraEnv, timeoutMs) {
391
+ const issue = runGh(bin, ["issue", "view", String(id), "--repo", repo, "--json", "id"], spawnFn, extraEnv, timeoutMs);
392
+ return projectString(issue.id) ?? projectString(issue.nodeId) ?? projectString(issue.node_id);
393
+ }
394
+ async function syncGitHubProjectStatus(bin, repo, spawnFn, id, status, projects, extraEnv, timeoutMs) {
395
+ if (!projects?.enabled)
396
+ return;
397
+ const projectId = projectString(projects.projectId);
398
+ if (!projectId)
399
+ throw new Error("GitHub Projects status sync is enabled but projectId is missing.");
400
+ const lifecycleStatus = projectLifecycleStatusForTaskStatus(status);
401
+ if (!lifecycleStatus)
402
+ return;
403
+ const issueNodeId = fetchIssueNodeId(bin, repo, spawnFn, id, extraEnv, timeoutMs);
404
+ if (!issueNodeId)
405
+ throw new Error(`GitHub issue ${repo}#${id} did not expose a node id for Projects status sync.`);
406
+ const projectStatus = projectString(projects.statuses?.[lifecycleStatus]) ?? DEFAULT_PROJECT_STATUSES[lifecycleStatus];
407
+ const fetchGraphQL = ghGraphQLFetch(bin, spawnFn, extraEnv, timeoutMs);
408
+ const field = await resolveProjectStatusField({ projectId, token: "gh-cli", fetchGraphQL });
409
+ const option = field.options.find((candidate) => candidate.name.toLowerCase() === projectStatus.toLowerCase() || candidate.id === projectStatus);
410
+ if (!option)
411
+ throw new Error(`GitHub Project ${projectId} Status field does not contain option "${projectStatus}".`);
412
+ const item = await ensureIssueProjectItem({ projectId, issueNodeId, token: "gh-cli", fetchGraphQL });
413
+ await updateIssueProjectStatus({
414
+ projectId,
415
+ itemId: item.id,
416
+ fieldId: projectString(projects.statusFieldId) ?? field.id,
417
+ optionId: option.id,
418
+ token: "gh-cli",
419
+ fetchGraphQL
420
+ });
421
+ }
422
+ var TERMINAL_TASK_STATUSES = new Set(["closed", "completed", "merged", "cancelled", "resolved", "done"]);
423
+ function normalizeTaskStatusToken(status) {
424
+ return status?.trim().toLowerCase().replace(/[-\s]+/g, "_") ?? "";
425
+ }
426
+ function issueUpdatesMode(value) {
427
+ return value === "off" || value === "minimal" || value === "lifecycle" ? value : "lifecycle";
428
+ }
429
+ function isTerminalTaskStatus(status) {
430
+ return TERMINAL_TASK_STATUSES.has(normalizeTaskStatusToken(status));
431
+ }
432
+ function shouldWriteIssueUpdate(mode, status) {
433
+ if (mode === "off")
434
+ return false;
435
+ if (mode === "lifecycle")
436
+ return true;
437
+ return isTerminalTaskStatus(status);
438
+ }
439
+ function isRunningStatus(status) {
440
+ return normalizeTaskStatusToken(status) === "running";
441
+ }
442
+ function assignRunningIssue(bin, repo, spawnFn, id, assignee, extraEnv, timeoutMs) {
443
+ runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, "--add-assignee", assignee?.trim() || "@me"], spawnFn, extraEnv, timeoutMs);
444
+ }
227
445
  function statusLabelFor(status) {
228
446
  switch (status) {
447
+ case "running":
229
448
  case "in_progress":
230
449
  return "in-progress";
231
450
  case "blocked":
@@ -244,6 +463,8 @@ function statusLabelFor(status) {
244
463
  return "under-review";
245
464
  case "needs_attention":
246
465
  return "blocked";
466
+ case "closed":
467
+ case "completed":
247
468
  case "open":
248
469
  return null;
249
470
  default:
@@ -252,11 +473,13 @@ function statusLabelFor(status) {
252
473
  }
253
474
  function rigStatusLabelFor(status) {
254
475
  switch (status) {
476
+ case "running":
255
477
  case "in_progress":
256
478
  return "rig:running";
257
479
  case "under_review":
258
480
  return "rig:pr-open";
259
481
  case "closed":
482
+ case "completed":
260
483
  return "rig:done";
261
484
  case "ci_fixing":
262
485
  return "rig:ci-fixing";
@@ -274,9 +497,10 @@ function rigStatusLabelFor(status) {
274
497
  return null;
275
498
  }
276
499
  }
277
- function applyIssueStatus(bin, repo, spawnFn, id, status, extraEnv, timeoutMs) {
278
- const targetLabel = status === "closed" ? null : statusLabelFor(status);
500
+ async function applyIssueStatus(bin, repo, spawnFn, id, status, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs) {
501
+ const targetLabel = status === "closed" || status === "completed" ? null : statusLabelFor(status);
279
502
  const targetRigLabel = rigStatusLabelFor(status);
503
+ const shouldSyncLifecycle = shouldWriteIssueUpdate(issueUpdates, status);
280
504
  for (const l of [...STATUS_LABELS, ...RIG_STATUS_LABELS]) {
281
505
  if (targetLabel !== null && l === targetLabel)
282
506
  continue;
@@ -298,7 +522,19 @@ function applyIssueStatus(bin, repo, spawnFn, id, status, extraEnv, timeoutMs) {
298
522
  runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, "--add-label", label], spawnFn, extraEnv, timeoutMs);
299
523
  }
300
524
  }
301
- if (status === "closed") {
525
+ if (isRunningStatus(status)) {
526
+ assignRunningIssue(bin, repo, spawnFn, id, runningAssignee, extraEnv, timeoutMs);
527
+ if (shouldSyncLifecycle) {
528
+ upsertRigStickyComment(bin, repo, spawnFn, String(id), buildRigStickyStatusComment({
529
+ status: "running",
530
+ summary: "Rig run started."
531
+ }), extraEnv, timeoutMs);
532
+ }
533
+ }
534
+ if (shouldSyncLifecycle) {
535
+ await syncGitHubProjectStatus(bin, repo, spawnFn, id, status, projects, extraEnv, timeoutMs);
536
+ }
537
+ if (status === "closed" || status === "completed") {
302
538
  runGhVoid(bin, ["issue", "close", String(id), "--repo", repo], spawnFn, extraEnv, timeoutMs);
303
539
  }
304
540
  }
@@ -368,11 +604,11 @@ function applyLabels(bin, repo, spawnFn, id, labels, action, extraEnv, timeoutMs
368
604
  } catch {}
369
605
  }
370
606
  }
371
- function applyIssueUpdate(bin, repo, spawnFn, id, update, extraEnv, timeoutMs) {
607
+ async function applyIssueUpdate(bin, repo, spawnFn, id, update, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs) {
372
608
  if (update.status) {
373
- applyIssueStatus(bin, repo, spawnFn, id, update.status, extraEnv, timeoutMs);
609
+ await applyIssueStatus(bin, repo, spawnFn, id, update.status, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs);
374
610
  }
375
- if (update.comment?.trim()) {
611
+ if (update.comment?.trim() && shouldWriteIssueUpdate(issueUpdates, update.status)) {
376
612
  if (isRigStickyStatusComment(update.comment)) {
377
613
  upsertRigStickyComment(bin, repo, spawnFn, String(id), update.comment, extraEnv, timeoutMs);
378
614
  } else {
@@ -398,6 +634,7 @@ function createGitHubIssuesTaskSource(opts) {
398
634
  const spawnFn = opts.spawn ?? spawnSync;
399
635
  const timeoutMs = Math.max(1000, Math.trunc(opts.timeoutMs ?? DEFAULT_GH_TIMEOUT_MS));
400
636
  const listLimit = Math.max(1, Math.trunc(opts.listLimit ?? DEFAULT_GITHUB_ISSUE_LIST_LIMIT));
637
+ const issueUpdates = issueUpdatesMode(opts.issueUpdates);
401
638
  return {
402
639
  id: "std:github-issues",
403
640
  kind: "github-issues",
@@ -445,12 +682,12 @@ function createGitHubIssuesTaskSource(opts) {
445
682
  },
446
683
  async updateStatus(id, status) {
447
684
  const env = await resolveCredentialEnv(opts, "selected-repo");
448
- applyIssueStatus(bin, repo, spawnFn, id, status, env, timeoutMs);
685
+ await applyIssueStatus(bin, repo, spawnFn, id, status, opts.projects, opts.assignee, issueUpdates, env, timeoutMs);
449
686
  notifyTaskChanged(opts.onTaskChanged, repo, id, status);
450
687
  },
451
688
  async updateTask(id, update) {
452
689
  const env = await resolveCredentialEnv(opts, "selected-repo");
453
- applyIssueUpdate(bin, repo, spawnFn, id, update, env, timeoutMs);
690
+ await applyIssueUpdate(bin, repo, spawnFn, id, update, opts.projects, opts.assignee, issueUpdates, env, timeoutMs);
454
691
  notifyTaskChanged(opts.onTaskChanged, repo, id, update.status);
455
692
  },
456
693
  async addLabels(id, labels) {
@@ -0,0 +1,14 @@
1
+ import { type RigPluginWithRuntime } from "@rig/core";
2
+ import { createEnvGitHubCredentialProvider, createStateGitHubCredentialProvider, createGitHubIssuesTaskSource, type GitHubCredentialProvider, type GitHubIssuesOptions } from "./github-issues-source";
3
+ import { createFilesTaskSource } from "./files-source";
4
+ export { createGitHubIssuesTaskSource, createEnvGitHubCredentialProvider, createStateGitHubCredentialProvider, createFilesTaskSource };
5
+ export type { GitHubIssuesOptions } from "./github-issues-source";
6
+ export type { FilesTaskSourceOptions } from "./files-source";
7
+ export interface StandardPluginOptions {
8
+ githubCredentialProvider?: GitHubCredentialProvider;
9
+ githubWorkspaceId?: string;
10
+ githubUserId?: string;
11
+ githubSpawn?: GitHubIssuesOptions["spawn"];
12
+ onGitHubTaskChanged?: GitHubIssuesOptions["onTaskChanged"];
13
+ }
14
+ export default function standardPlugin(opts?: StandardPluginOptions): RigPluginWithRuntime;
package/dist/src/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // @bun
2
2
  // packages/standard-plugin/src/index.ts
3
+ import { resolve as resolve3 } 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
  }
@@ -47,12 +48,12 @@ function createStateGitHubCredentialProvider(options = {}) {
47
48
  async resolveGitHubToken(input) {
48
49
  const token = readToken();
49
50
  if (input.purpose === "selected-repo") {
50
- return { token: token ?? "", source: "signed-in-user" };
51
+ return { token: token ?? cleanToken(process.env.RIG_GITHUB_SELECTED_TOKEN ?? process.env.RIG_GITHUB_TOKEN ?? null) ?? "", source: "signed-in-user" };
51
52
  }
52
53
  if (token) {
53
54
  return { token, source: "signed-in-user" };
54
55
  }
55
- const fallback = cleanToken(process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? null);
56
+ const fallback = cleanToken(process.env.RIG_GITHUB_TOKEN ?? process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? null);
56
57
  if (!fallback) {
57
58
  throw new Error("No signed-in GitHub token is stored for Rig and no host admin fallback token is configured.");
58
59
  }
@@ -166,17 +167,41 @@ ${rendered}
166
167
  ` : `${rendered}
167
168
  `;
168
169
  }
170
+ function buildRigStickyStatusComment(input) {
171
+ const lines = [
172
+ RIG_STATUS_COMMENT_MARKER,
173
+ `### Rig status: ${input.status}`,
174
+ "",
175
+ input.summary
176
+ ];
177
+ if (input.runId)
178
+ lines.push("", `- Run: ${input.runId}`);
179
+ if (input.prUrl)
180
+ lines.push(`- PR: ${input.prUrl}`);
181
+ for (const detail of input.details ?? [])
182
+ lines.push(`- ${detail}`);
183
+ return lines.join(`
184
+ `);
185
+ }
169
186
  function isRigStickyStatusComment(body) {
170
187
  return body.includes(RIG_STATUS_COMMENT_MARKER);
171
188
  }
172
189
  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 } };
190
+ return {
191
+ encoding: "utf-8",
192
+ timeout: timeoutMs,
193
+ env: {
194
+ ...process.env,
195
+ ...process.env.GH_TOKEN !== undefined ? { GH_TOKEN: process.env.GH_TOKEN } : {},
196
+ ...process.env.GITHUB_TOKEN !== undefined ? { GITHUB_TOKEN: process.env.GITHUB_TOKEN } : {},
197
+ ...process.env.RIG_GITHUB_TOKEN !== undefined ? { RIG_GITHUB_TOKEN: process.env.RIG_GITHUB_TOKEN } : {},
198
+ ...extraEnv ?? {}
199
+ }
200
+ };
176
201
  }
177
202
  function credentialEnv(token) {
178
203
  const clean = token?.trim() ?? "";
179
- return { GH_TOKEN: clean, GITHUB_TOKEN: clean };
204
+ return { GH_TOKEN: clean, GITHUB_TOKEN: clean, RIG_GITHUB_TOKEN: clean };
180
205
  }
181
206
  async function resolveCredentialEnv(opts, purpose) {
182
207
  if (!opts.credentialProvider)
@@ -191,28 +216,239 @@ async function resolveCredentialEnv(opts, purpose) {
191
216
  const resolved = await opts.credentialProvider.resolveGitHubToken(input);
192
217
  return credentialEnv(resolved.token);
193
218
  }
219
+ function tokenDiagnostic(value) {
220
+ const clean = value?.trim() ?? "";
221
+ return clean ? `present(len=${clean.length})` : "missing";
222
+ }
194
223
  function runGh(bin, args, spawn, extraEnv, timeoutMs) {
195
- const res = spawn(bin, [...args], ghSpawnOptions(extraEnv, timeoutMs));
196
- assertGhSuccess(args, res);
224
+ const options = ghSpawnOptions(extraEnv, timeoutMs);
225
+ const res = spawn(bin, [...args], options);
226
+ assertGhSuccess(args, res, options.env);
197
227
  if (!res.stdout || res.stdout.trim() === "")
198
228
  return [];
199
229
  return JSON.parse(res.stdout);
200
230
  }
201
231
  function runGhVoid(bin, args, spawn, extraEnv, timeoutMs) {
202
- const res = spawn(bin, [...args], ghSpawnOptions(extraEnv, timeoutMs));
203
- assertGhSuccess(args, res);
232
+ const options = ghSpawnOptions(extraEnv, timeoutMs);
233
+ const res = spawn(bin, [...args], options);
234
+ assertGhSuccess(args, res, options.env);
204
235
  }
205
- function assertGhSuccess(args, res) {
236
+ function assertGhSuccess(args, res, env) {
206
237
  if (res.error) {
207
238
  const msg = res.error.message ?? String(res.error);
208
239
  throw new Error(`gh CLI not available \u2014 install gh (brew install gh / apt install gh): ${msg}`);
209
240
  }
210
241
  if (res.status !== 0) {
211
- throw new Error(`gh ${args.join(" ")} failed (exit ${res.status}): ${res.stderr}`);
242
+ throw new Error(`gh ${args.join(" ")} failed (exit ${res.status}): ${res.stderr}
243
+ [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)}`);
244
+ }
245
+ }
246
+ var DEFAULT_PROJECT_STATUSES = {
247
+ todo: "Todo",
248
+ running: "In Progress",
249
+ prOpen: "In Review",
250
+ ciFixing: "In Review",
251
+ merging: "In Review",
252
+ done: "Done",
253
+ needsAttention: "Needs Attention"
254
+ };
255
+ function asProjectRecord(value) {
256
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
257
+ }
258
+ function projectString(value) {
259
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
260
+ }
261
+ function projectLifecycleStatusForTaskStatus(status) {
262
+ const normalized = status?.trim().toLowerCase().replace(/[-\s]+/g, "_");
263
+ switch (normalized) {
264
+ case "draft":
265
+ case "open":
266
+ case "queued":
267
+ case "ready":
268
+ return "todo";
269
+ case "running":
270
+ case "in_progress":
271
+ return "running";
272
+ case "under_review":
273
+ case "review":
274
+ case "pr_open":
275
+ return "prOpen";
276
+ case "ci_fixing":
277
+ case "fixing":
278
+ return "ciFixing";
279
+ case "merging":
280
+ case "merge":
281
+ return "merging";
282
+ case "closed":
283
+ case "completed":
284
+ case "done":
285
+ return "done";
286
+ case "blocked":
287
+ case "cancelled":
288
+ case "failed":
289
+ case "needs_attention":
290
+ return "needsAttention";
291
+ default:
292
+ return null;
293
+ }
294
+ }
295
+ function ghGraphQLFetch(bin, spawnFn, extraEnv, timeoutMs) {
296
+ return async (query, variables) => {
297
+ const args = ["api", "graphql", "-f", `query=${query}`];
298
+ for (const [key, value] of Object.entries(variables)) {
299
+ if (value === undefined || value === null)
300
+ continue;
301
+ args.push("-f", `${key}=${String(value)}`);
302
+ }
303
+ const response = runGh(bin, args, spawnFn, extraEnv, timeoutMs);
304
+ return asProjectRecord(response)?.data ?? response;
305
+ };
306
+ }
307
+ function projectStatusFieldFrom(data, projectId) {
308
+ const fields = asProjectRecord(asProjectRecord(asProjectRecord(data)?.node)?.fields)?.nodes;
309
+ for (const node of Array.isArray(fields) ? fields : []) {
310
+ const record = asProjectRecord(node);
311
+ if (projectString(record?.name)?.toLowerCase() !== "status")
312
+ continue;
313
+ const id = projectString(record?.id);
314
+ if (!id)
315
+ continue;
316
+ const options = Array.isArray(record?.options) ? record.options.flatMap((option) => {
317
+ const optionRecord = asProjectRecord(option);
318
+ const optionId = projectString(optionRecord?.id);
319
+ const name = projectString(optionRecord?.name);
320
+ return optionId && name ? [{ id: optionId, name }] : [];
321
+ }) : [];
322
+ return { id, name: "Status", options };
212
323
  }
324
+ throw new Error(`GitHub Project ${projectId} does not expose a Status single-select field.`);
325
+ }
326
+ async function resolveProjectStatusField(input) {
327
+ const query = `
328
+ query RigProjectStatusField($projectId: ID!) {
329
+ node(id: $projectId) {
330
+ ... on ProjectV2 {
331
+ fields(first: 50) {
332
+ nodes {
333
+ ... on ProjectV2FieldCommon { id name }
334
+ ... on ProjectV2SingleSelectField { id name options { id name } }
335
+ }
336
+ }
337
+ }
338
+ }
339
+ }
340
+ `;
341
+ return projectStatusFieldFrom(await input.fetchGraphQL(query, { projectId: input.projectId }, input.token), input.projectId);
342
+ }
343
+ async function ensureIssueProjectItem(input) {
344
+ const query = `
345
+ query RigFindProjectIssueItem($projectId: ID!) {
346
+ node(id: $projectId) {
347
+ ... on ProjectV2 {
348
+ items(first: 100) { nodes { id content { ... on Issue { id } } } }
349
+ }
350
+ }
351
+ }
352
+ `;
353
+ const data = await input.fetchGraphQL(query, { projectId: input.projectId }, input.token);
354
+ const nodes = asProjectRecord(asProjectRecord(asProjectRecord(data)?.node)?.items)?.nodes;
355
+ for (const node of Array.isArray(nodes) ? nodes : []) {
356
+ const record = asProjectRecord(node);
357
+ const content = asProjectRecord(record?.content);
358
+ if (projectString(content?.id) === input.issueNodeId) {
359
+ const id2 = projectString(record?.id);
360
+ if (id2)
361
+ return { id: id2, created: false };
362
+ }
363
+ }
364
+ const mutation = `
365
+ mutation RigAddIssueToProject($projectId: ID!, $contentId: ID!) {
366
+ addProjectV2ItemById(input: { projectId: $projectId, contentId: $contentId }) { item { id } }
367
+ }
368
+ `;
369
+ const created = await input.fetchGraphQL(mutation, { projectId: input.projectId, contentId: input.issueNodeId }, input.token);
370
+ const addResult = asProjectRecord(asProjectRecord(created)?.addProjectV2ItemById);
371
+ const id = projectString(asProjectRecord(addResult?.item)?.id);
372
+ if (!id)
373
+ throw new Error("GitHub Project item creation did not return an item id.");
374
+ return { id, created: true };
375
+ }
376
+ async function updateIssueProjectStatus(input) {
377
+ const mutation = `
378
+ mutation RigUpdateProjectStatus($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
379
+ updateProjectV2ItemFieldValue(input: {
380
+ projectId: $projectId,
381
+ itemId: $itemId,
382
+ fieldId: $fieldId,
383
+ value: { singleSelectOptionId: $optionId }
384
+ }) { projectV2Item { id } }
385
+ }
386
+ `;
387
+ await input.fetchGraphQL(mutation, {
388
+ projectId: input.projectId,
389
+ itemId: input.itemId,
390
+ fieldId: input.fieldId,
391
+ optionId: input.optionId
392
+ }, input.token);
393
+ }
394
+ function fetchIssueNodeId(bin, repo, spawnFn, id, extraEnv, timeoutMs) {
395
+ const issue = runGh(bin, ["issue", "view", String(id), "--repo", repo, "--json", "id"], spawnFn, extraEnv, timeoutMs);
396
+ return projectString(issue.id) ?? projectString(issue.nodeId) ?? projectString(issue.node_id);
397
+ }
398
+ async function syncGitHubProjectStatus(bin, repo, spawnFn, id, status, projects, extraEnv, timeoutMs) {
399
+ if (!projects?.enabled)
400
+ return;
401
+ const projectId = projectString(projects.projectId);
402
+ if (!projectId)
403
+ throw new Error("GitHub Projects status sync is enabled but projectId is missing.");
404
+ const lifecycleStatus = projectLifecycleStatusForTaskStatus(status);
405
+ if (!lifecycleStatus)
406
+ return;
407
+ const issueNodeId = fetchIssueNodeId(bin, repo, spawnFn, id, extraEnv, timeoutMs);
408
+ if (!issueNodeId)
409
+ throw new Error(`GitHub issue ${repo}#${id} did not expose a node id for Projects status sync.`);
410
+ const projectStatus = projectString(projects.statuses?.[lifecycleStatus]) ?? DEFAULT_PROJECT_STATUSES[lifecycleStatus];
411
+ const fetchGraphQL = ghGraphQLFetch(bin, spawnFn, extraEnv, timeoutMs);
412
+ const field = await resolveProjectStatusField({ projectId, token: "gh-cli", fetchGraphQL });
413
+ const option = field.options.find((candidate) => candidate.name.toLowerCase() === projectStatus.toLowerCase() || candidate.id === projectStatus);
414
+ if (!option)
415
+ throw new Error(`GitHub Project ${projectId} Status field does not contain option "${projectStatus}".`);
416
+ const item = await ensureIssueProjectItem({ projectId, issueNodeId, token: "gh-cli", fetchGraphQL });
417
+ await updateIssueProjectStatus({
418
+ projectId,
419
+ itemId: item.id,
420
+ fieldId: projectString(projects.statusFieldId) ?? field.id,
421
+ optionId: option.id,
422
+ token: "gh-cli",
423
+ fetchGraphQL
424
+ });
425
+ }
426
+ var TERMINAL_TASK_STATUSES = new Set(["closed", "completed", "merged", "cancelled", "resolved", "done"]);
427
+ function normalizeTaskStatusToken(status) {
428
+ return status?.trim().toLowerCase().replace(/[-\s]+/g, "_") ?? "";
429
+ }
430
+ function issueUpdatesMode(value) {
431
+ return value === "off" || value === "minimal" || value === "lifecycle" ? value : "lifecycle";
432
+ }
433
+ function isTerminalTaskStatus(status) {
434
+ return TERMINAL_TASK_STATUSES.has(normalizeTaskStatusToken(status));
435
+ }
436
+ function shouldWriteIssueUpdate(mode, status) {
437
+ if (mode === "off")
438
+ return false;
439
+ if (mode === "lifecycle")
440
+ return true;
441
+ return isTerminalTaskStatus(status);
442
+ }
443
+ function isRunningStatus(status) {
444
+ return normalizeTaskStatusToken(status) === "running";
445
+ }
446
+ function assignRunningIssue(bin, repo, spawnFn, id, assignee, extraEnv, timeoutMs) {
447
+ runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, "--add-assignee", assignee?.trim() || "@me"], spawnFn, extraEnv, timeoutMs);
213
448
  }
214
449
  function statusLabelFor(status) {
215
450
  switch (status) {
451
+ case "running":
216
452
  case "in_progress":
217
453
  return "in-progress";
218
454
  case "blocked":
@@ -231,6 +467,8 @@ function statusLabelFor(status) {
231
467
  return "under-review";
232
468
  case "needs_attention":
233
469
  return "blocked";
470
+ case "closed":
471
+ case "completed":
234
472
  case "open":
235
473
  return null;
236
474
  default:
@@ -239,11 +477,13 @@ function statusLabelFor(status) {
239
477
  }
240
478
  function rigStatusLabelFor(status) {
241
479
  switch (status) {
480
+ case "running":
242
481
  case "in_progress":
243
482
  return "rig:running";
244
483
  case "under_review":
245
484
  return "rig:pr-open";
246
485
  case "closed":
486
+ case "completed":
247
487
  return "rig:done";
248
488
  case "ci_fixing":
249
489
  return "rig:ci-fixing";
@@ -261,9 +501,10 @@ function rigStatusLabelFor(status) {
261
501
  return null;
262
502
  }
263
503
  }
264
- function applyIssueStatus(bin, repo, spawnFn, id, status, extraEnv, timeoutMs) {
265
- const targetLabel = status === "closed" ? null : statusLabelFor(status);
504
+ async function applyIssueStatus(bin, repo, spawnFn, id, status, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs) {
505
+ const targetLabel = status === "closed" || status === "completed" ? null : statusLabelFor(status);
266
506
  const targetRigLabel = rigStatusLabelFor(status);
507
+ const shouldSyncLifecycle = shouldWriteIssueUpdate(issueUpdates, status);
267
508
  for (const l of [...STATUS_LABELS, ...RIG_STATUS_LABELS]) {
268
509
  if (targetLabel !== null && l === targetLabel)
269
510
  continue;
@@ -285,7 +526,19 @@ function applyIssueStatus(bin, repo, spawnFn, id, status, extraEnv, timeoutMs) {
285
526
  runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, "--add-label", label], spawnFn, extraEnv, timeoutMs);
286
527
  }
287
528
  }
288
- if (status === "closed") {
529
+ if (isRunningStatus(status)) {
530
+ assignRunningIssue(bin, repo, spawnFn, id, runningAssignee, extraEnv, timeoutMs);
531
+ if (shouldSyncLifecycle) {
532
+ upsertRigStickyComment(bin, repo, spawnFn, String(id), buildRigStickyStatusComment({
533
+ status: "running",
534
+ summary: "Rig run started."
535
+ }), extraEnv, timeoutMs);
536
+ }
537
+ }
538
+ if (shouldSyncLifecycle) {
539
+ await syncGitHubProjectStatus(bin, repo, spawnFn, id, status, projects, extraEnv, timeoutMs);
540
+ }
541
+ if (status === "closed" || status === "completed") {
289
542
  runGhVoid(bin, ["issue", "close", String(id), "--repo", repo], spawnFn, extraEnv, timeoutMs);
290
543
  }
291
544
  }
@@ -355,11 +608,11 @@ function applyLabels(bin, repo, spawnFn, id, labels, action, extraEnv, timeoutMs
355
608
  } catch {}
356
609
  }
357
610
  }
358
- function applyIssueUpdate(bin, repo, spawnFn, id, update, extraEnv, timeoutMs) {
611
+ async function applyIssueUpdate(bin, repo, spawnFn, id, update, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs) {
359
612
  if (update.status) {
360
- applyIssueStatus(bin, repo, spawnFn, id, update.status, extraEnv, timeoutMs);
613
+ await applyIssueStatus(bin, repo, spawnFn, id, update.status, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs);
361
614
  }
362
- if (update.comment?.trim()) {
615
+ if (update.comment?.trim() && shouldWriteIssueUpdate(issueUpdates, update.status)) {
363
616
  if (isRigStickyStatusComment(update.comment)) {
364
617
  upsertRigStickyComment(bin, repo, spawnFn, String(id), update.comment, extraEnv, timeoutMs);
365
618
  } else {
@@ -385,6 +638,7 @@ function createGitHubIssuesTaskSource(opts) {
385
638
  const spawnFn = opts.spawn ?? spawnSync;
386
639
  const timeoutMs = Math.max(1000, Math.trunc(opts.timeoutMs ?? DEFAULT_GH_TIMEOUT_MS));
387
640
  const listLimit = Math.max(1, Math.trunc(opts.listLimit ?? DEFAULT_GITHUB_ISSUE_LIST_LIMIT));
641
+ const issueUpdates = issueUpdatesMode(opts.issueUpdates);
388
642
  return {
389
643
  id: "std:github-issues",
390
644
  kind: "github-issues",
@@ -432,12 +686,12 @@ function createGitHubIssuesTaskSource(opts) {
432
686
  },
433
687
  async updateStatus(id, status) {
434
688
  const env = await resolveCredentialEnv(opts, "selected-repo");
435
- applyIssueStatus(bin, repo, spawnFn, id, status, env, timeoutMs);
689
+ await applyIssueStatus(bin, repo, spawnFn, id, status, opts.projects, opts.assignee, issueUpdates, env, timeoutMs);
436
690
  notifyTaskChanged(opts.onTaskChanged, repo, id, status);
437
691
  },
438
692
  async updateTask(id, update) {
439
693
  const env = await resolveCredentialEnv(opts, "selected-repo");
440
- applyIssueUpdate(bin, repo, spawnFn, id, update, env, timeoutMs);
694
+ await applyIssueUpdate(bin, repo, spawnFn, id, update, opts.projects, opts.assignee, issueUpdates, env, timeoutMs);
441
695
  notifyTaskChanged(opts.onTaskChanged, repo, id, update.status);
442
696
  },
443
697
  async addLabels(id, labels) {
@@ -476,7 +730,7 @@ function createGitHubIssuesTaskSource(opts) {
476
730
 
477
731
  // packages/standard-plugin/src/files-source.ts
478
732
  import { readFileSync as readFileSync2, readdirSync, existsSync as existsSync2, statSync, writeFileSync } from "fs";
479
- import { join, basename } from "path";
733
+ import { join, basename, isAbsolute, resolve as resolve2 } from "path";
480
734
  var DEFAULT_PATTERN = /\.(task\.)?json$/;
481
735
  function readTaskFile(file, pattern) {
482
736
  const raw = JSON.parse(readFileSync2(file, "utf-8"));
@@ -497,10 +751,11 @@ function readTaskFile(file, pattern) {
497
751
  }
498
752
  function createFilesTaskSource(opts) {
499
753
  const pattern = opts.pattern ?? DEFAULT_PATTERN;
500
- const directory = opts.path ?? opts.dir;
501
- if (!directory) {
754
+ const configured = opts.path ?? opts.dir;
755
+ if (!configured) {
502
756
  throw new Error("createFilesTaskSource: either `path` or `dir` must be provided");
503
757
  }
758
+ const directory = isAbsolute(configured) ? configured : resolve2(opts.projectRoot ?? process.cwd(), configured);
504
759
  const findTaskFile = (id) => {
505
760
  if (!existsSync2(directory))
506
761
  return;
@@ -585,6 +840,40 @@ function requireStringField(config, field, kind) {
585
840
  }
586
841
  return value;
587
842
  }
843
+ function isRecord(value) {
844
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
845
+ }
846
+ function optionalString(value) {
847
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
848
+ }
849
+ function parseGitHubProjectsOptions(value) {
850
+ if (!isRecord(value))
851
+ return;
852
+ const statusesSource = isRecord(value.statuses) ? value.statuses : undefined;
853
+ const statuses = {};
854
+ for (const key of ["todo", "running", "prOpen", "ciFixing", "merging", "done", "needsAttention"]) {
855
+ const status = optionalString(statusesSource?.[key]);
856
+ if (status)
857
+ statuses[key] = status;
858
+ }
859
+ const parsed = {};
860
+ if (typeof value.enabled === "boolean")
861
+ parsed.enabled = value.enabled;
862
+ const projectId = optionalString(value.projectId);
863
+ if (projectId)
864
+ parsed.projectId = projectId;
865
+ const statusFieldId = optionalString(value.statusFieldId);
866
+ if (statusFieldId)
867
+ parsed.statusFieldId = statusFieldId;
868
+ if (Object.keys(statuses).length > 0)
869
+ parsed.statuses = statuses;
870
+ return parsed;
871
+ }
872
+ function githubProjectsOptionsFromConfig(config, context) {
873
+ const rigConfig = isRecord(context?.rigConfig) ? context.rigConfig : undefined;
874
+ const github = isRecord(rigConfig?.github) ? rigConfig.github : undefined;
875
+ return parseGitHubProjectsOptions(config.options?.projects) ?? parseGitHubProjectsOptions(github?.projects);
876
+ }
588
877
  function standardPlugin(opts = {}) {
589
878
  return definePlugin({
590
879
  name: "rig-standard",
@@ -609,13 +898,13 @@ function standardPlugin(opts = {}) {
609
898
  id: "std:github-issues",
610
899
  kind: "github-issues",
611
900
  description: "GitHub Issues via gh CLI",
612
- factory(config) {
901
+ factory(config, context) {
613
902
  const options = {
614
903
  owner: requireStringField(config, "owner", "github-issues"),
615
904
  repo: requireStringField(config, "repo", "github-issues")
616
905
  };
617
- if (opts.githubCredentialProvider)
618
- options.credentialProvider = opts.githubCredentialProvider;
906
+ const credentialProviderOptions = context?.projectRoot ? { stateDir: resolve3(context.projectRoot, ".rig", "state") } : {};
907
+ options.credentialProvider = opts.githubCredentialProvider ?? createStateGitHubCredentialProvider(credentialProviderOptions);
619
908
  if (opts.githubWorkspaceId)
620
909
  options.workspaceId = opts.githubWorkspaceId;
621
910
  if (opts.githubUserId)
@@ -637,6 +926,9 @@ function standardPlugin(opts = {}) {
637
926
  const listLimit = typeof config.options?.listLimit === "number" ? config.options.listLimit : undefined;
638
927
  if (listLimit !== undefined)
639
928
  options.listLimit = listLimit;
929
+ const projects = githubProjectsOptionsFromConfig(config, context);
930
+ if (projects)
931
+ options.projects = projects;
640
932
  return createGitHubIssuesTaskSource(options);
641
933
  }
642
934
  },
@@ -644,9 +936,10 @@ function standardPlugin(opts = {}) {
644
936
  id: "std:files",
645
937
  kind: "files",
646
938
  description: "JSON files in a local directory",
647
- factory(config) {
939
+ factory(config, context) {
648
940
  return createFilesTaskSource({
649
- path: requireStringField(config, "path", "files")
941
+ path: requireStringField(config, "path", "files"),
942
+ ...context?.projectRoot ? { projectRoot: context.projectRoot } : {}
650
943
  });
651
944
  }
652
945
  }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@h-rig/standard-plugin",
3
- "version": "0.0.6-alpha.9",
3
+ "version": "0.0.6-alpha.91",
4
4
  "type": "module",
5
- "description": "Rig package",
5
+ "description": "First-party contribution bundle for Rig's OMP extension plugin graph; not a standalone plugin runtime.",
6
6
  "license": "UNLICENSED",
7
7
  "files": [
8
8
  "dist",
@@ -10,6 +10,7 @@
10
10
  ],
11
11
  "exports": {
12
12
  ".": {
13
+ "types": "./dist/src/index.d.ts",
13
14
  "import": "./dist/src/index.js"
14
15
  }
15
16
  },
@@ -18,9 +19,10 @@
18
19
  },
19
20
  "main": "./dist/src/index.js",
20
21
  "module": "./dist/src/index.js",
22
+ "types": "./dist/src/index.d.ts",
21
23
  "dependencies": {
22
- "@rig/contracts": "npm:@h-rig/contracts@0.0.6-alpha.9",
23
- "@rig/core": "npm:@h-rig/core@0.0.6-alpha.9",
24
+ "@rig/contracts": "npm:@h-rig/contracts@0.0.6-alpha.91",
25
+ "@rig/core": "npm:@h-rig/core@0.0.6-alpha.91",
24
26
  "effect": "4.0.0-beta.78"
25
27
  }
26
28
  }