@h-rig/standard-plugin 0.0.6-alpha.13 → 0.0.6-alpha.130

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
  }
@@ -22,34 +22,40 @@ function createEnvGitHubCredentialProvider() {
22
22
  };
23
23
  }
24
24
  function createStateGitHubCredentialProvider(options = {}) {
25
- const resolveStateFile = () => {
25
+ const stateFileCandidates = () => {
26
+ const candidates = [];
26
27
  const explicitFile = options.stateFile ?? process.env.RIG_GITHUB_AUTH_STATE_FILE;
27
28
  if (explicitFile?.trim())
28
- return resolve(explicitFile.trim());
29
- const stateDir = options.stateDir ?? process.env.RIG_STATE_DIR;
30
- return stateDir?.trim() ? resolve(stateDir.trim(), "github-auth.json") : null;
29
+ candidates.push(resolve(explicitFile.trim()));
30
+ for (const dir of [options.stateDir, process.env.RIG_STATE_DIR]) {
31
+ if (dir?.trim())
32
+ candidates.push(resolve(dir.trim(), "github-auth.json"));
33
+ }
34
+ return candidates;
31
35
  };
32
36
  const readToken = () => {
33
- const stateFile = resolveStateFile();
34
- if (!stateFile || !existsSync(stateFile))
35
- return null;
36
- try {
37
- const parsed = JSON.parse(readFileSync(stateFile, "utf8"));
38
- return typeof parsed.token === "string" ? cleanToken(parsed.token) : null;
39
- } catch {
40
- return null;
37
+ for (const stateFile of stateFileCandidates()) {
38
+ if (!existsSync(stateFile))
39
+ continue;
40
+ try {
41
+ const parsed = JSON.parse(readFileSync(stateFile, "utf8"));
42
+ const token = typeof parsed.token === "string" ? cleanToken(parsed.token) : null;
43
+ if (token)
44
+ return token;
45
+ } catch {}
41
46
  }
47
+ return null;
42
48
  };
43
49
  return {
44
50
  async resolveGitHubToken(input) {
45
51
  const token = readToken();
46
52
  if (input.purpose === "selected-repo") {
47
- return { token: token ?? "", source: "signed-in-user" };
53
+ return { token: token ?? cleanToken(process.env.RIG_GITHUB_SELECTED_TOKEN ?? process.env.RIG_GITHUB_TOKEN ?? null) ?? "", source: "signed-in-user" };
48
54
  }
49
55
  if (token) {
50
56
  return { token, source: "signed-in-user" };
51
57
  }
52
- const fallback = cleanToken(process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? null);
58
+ const fallback = cleanToken(process.env.RIG_GITHUB_TOKEN ?? process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? null);
53
59
  if (!fallback) {
54
60
  throw new Error("No signed-in GitHub token is stored for Rig and no host admin fallback token is configured.");
55
61
  }
@@ -183,13 +189,21 @@ function isRigStickyStatusComment(body) {
183
189
  return body.includes(RIG_STATUS_COMMENT_MARKER);
184
190
  }
185
191
  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 } };
192
+ return {
193
+ encoding: "utf-8",
194
+ timeout: timeoutMs,
195
+ env: {
196
+ ...process.env,
197
+ ...process.env.GH_TOKEN !== undefined ? { GH_TOKEN: process.env.GH_TOKEN } : {},
198
+ ...process.env.GITHUB_TOKEN !== undefined ? { GITHUB_TOKEN: process.env.GITHUB_TOKEN } : {},
199
+ ...process.env.RIG_GITHUB_TOKEN !== undefined ? { RIG_GITHUB_TOKEN: process.env.RIG_GITHUB_TOKEN } : {},
200
+ ...extraEnv ?? {}
201
+ }
202
+ };
189
203
  }
190
204
  function credentialEnv(token) {
191
205
  const clean = token?.trim() ?? "";
192
- return { GH_TOKEN: clean, GITHUB_TOKEN: clean };
206
+ return { GH_TOKEN: clean, GITHUB_TOKEN: clean, RIG_GITHUB_TOKEN: clean };
193
207
  }
194
208
  async function resolveCredentialEnv(opts, purpose) {
195
209
  if (!opts.credentialProvider)
@@ -204,28 +218,239 @@ async function resolveCredentialEnv(opts, purpose) {
204
218
  const resolved = await opts.credentialProvider.resolveGitHubToken(input);
205
219
  return credentialEnv(resolved.token);
206
220
  }
221
+ function tokenDiagnostic(value) {
222
+ const clean = value?.trim() ?? "";
223
+ return clean ? `present(len=${clean.length})` : "missing";
224
+ }
207
225
  function runGh(bin, args, spawn, extraEnv, timeoutMs) {
208
- const res = spawn(bin, [...args], ghSpawnOptions(extraEnv, timeoutMs));
209
- assertGhSuccess(args, res);
226
+ const options = ghSpawnOptions(extraEnv, timeoutMs);
227
+ const res = spawn(bin, [...args], options);
228
+ assertGhSuccess(args, res, options.env);
210
229
  if (!res.stdout || res.stdout.trim() === "")
211
230
  return [];
212
231
  return JSON.parse(res.stdout);
213
232
  }
214
233
  function runGhVoid(bin, args, spawn, extraEnv, timeoutMs) {
215
- const res = spawn(bin, [...args], ghSpawnOptions(extraEnv, timeoutMs));
216
- assertGhSuccess(args, res);
234
+ const options = ghSpawnOptions(extraEnv, timeoutMs);
235
+ const res = spawn(bin, [...args], options);
236
+ assertGhSuccess(args, res, options.env);
217
237
  }
218
- function assertGhSuccess(args, res) {
238
+ function assertGhSuccess(args, res, env) {
219
239
  if (res.error) {
220
240
  const msg = res.error.message ?? String(res.error);
221
241
  throw new Error(`gh CLI not available \u2014 install gh (brew install gh / apt install gh): ${msg}`);
222
242
  }
223
243
  if (res.status !== 0) {
224
- throw new Error(`gh ${args.join(" ")} failed (exit ${res.status}): ${res.stderr}`);
244
+ throw new Error(`gh ${args.join(" ")} failed (exit ${res.status}): ${res.stderr}
245
+ [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)}`);
246
+ }
247
+ }
248
+ var DEFAULT_PROJECT_STATUSES = {
249
+ todo: "Todo",
250
+ running: "In Progress",
251
+ prOpen: "In Review",
252
+ ciFixing: "In Review",
253
+ merging: "In Review",
254
+ done: "Done",
255
+ needsAttention: "Needs Attention"
256
+ };
257
+ function asProjectRecord(value) {
258
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
259
+ }
260
+ function projectString(value) {
261
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
262
+ }
263
+ function projectLifecycleStatusForTaskStatus(status) {
264
+ const normalized = status?.trim().toLowerCase().replace(/[-\s]+/g, "_");
265
+ switch (normalized) {
266
+ case "draft":
267
+ case "open":
268
+ case "queued":
269
+ case "ready":
270
+ return "todo";
271
+ case "running":
272
+ case "in_progress":
273
+ return "running";
274
+ case "under_review":
275
+ case "review":
276
+ case "pr_open":
277
+ return "prOpen";
278
+ case "ci_fixing":
279
+ case "fixing":
280
+ return "ciFixing";
281
+ case "merging":
282
+ case "merge":
283
+ return "merging";
284
+ case "closed":
285
+ case "completed":
286
+ case "done":
287
+ return "done";
288
+ case "blocked":
289
+ case "cancelled":
290
+ case "failed":
291
+ case "needs_attention":
292
+ return "needsAttention";
293
+ default:
294
+ return null;
225
295
  }
226
296
  }
297
+ function ghGraphQLFetch(bin, spawnFn, extraEnv, timeoutMs) {
298
+ return async (query, variables) => {
299
+ const args = ["api", "graphql", "-f", `query=${query}`];
300
+ for (const [key, value] of Object.entries(variables)) {
301
+ if (value === undefined || value === null)
302
+ continue;
303
+ args.push("-f", `${key}=${String(value)}`);
304
+ }
305
+ const response = runGh(bin, args, spawnFn, extraEnv, timeoutMs);
306
+ return asProjectRecord(response)?.data ?? response;
307
+ };
308
+ }
309
+ function projectStatusFieldFrom(data, projectId) {
310
+ const fields = asProjectRecord(asProjectRecord(asProjectRecord(data)?.node)?.fields)?.nodes;
311
+ for (const node of Array.isArray(fields) ? fields : []) {
312
+ const record = asProjectRecord(node);
313
+ if (projectString(record?.name)?.toLowerCase() !== "status")
314
+ continue;
315
+ const id = projectString(record?.id);
316
+ if (!id)
317
+ continue;
318
+ const options = Array.isArray(record?.options) ? record.options.flatMap((option) => {
319
+ const optionRecord = asProjectRecord(option);
320
+ const optionId = projectString(optionRecord?.id);
321
+ const name = projectString(optionRecord?.name);
322
+ return optionId && name ? [{ id: optionId, name }] : [];
323
+ }) : [];
324
+ return { id, name: "Status", options };
325
+ }
326
+ throw new Error(`GitHub Project ${projectId} does not expose a Status single-select field.`);
327
+ }
328
+ async function resolveProjectStatusField(input) {
329
+ const query = `
330
+ query RigProjectStatusField($projectId: ID!) {
331
+ node(id: $projectId) {
332
+ ... on ProjectV2 {
333
+ fields(first: 50) {
334
+ nodes {
335
+ ... on ProjectV2FieldCommon { id name }
336
+ ... on ProjectV2SingleSelectField { id name options { id name } }
337
+ }
338
+ }
339
+ }
340
+ }
341
+ }
342
+ `;
343
+ return projectStatusFieldFrom(await input.fetchGraphQL(query, { projectId: input.projectId }, input.token), input.projectId);
344
+ }
345
+ async function ensureIssueProjectItem(input) {
346
+ const query = `
347
+ query RigFindProjectIssueItem($projectId: ID!) {
348
+ node(id: $projectId) {
349
+ ... on ProjectV2 {
350
+ items(first: 100) { nodes { id content { ... on Issue { id } } } }
351
+ }
352
+ }
353
+ }
354
+ `;
355
+ const data = await input.fetchGraphQL(query, { projectId: input.projectId }, input.token);
356
+ const nodes = asProjectRecord(asProjectRecord(asProjectRecord(data)?.node)?.items)?.nodes;
357
+ for (const node of Array.isArray(nodes) ? nodes : []) {
358
+ const record = asProjectRecord(node);
359
+ const content = asProjectRecord(record?.content);
360
+ if (projectString(content?.id) === input.issueNodeId) {
361
+ const id2 = projectString(record?.id);
362
+ if (id2)
363
+ return { id: id2, created: false };
364
+ }
365
+ }
366
+ const mutation = `
367
+ mutation RigAddIssueToProject($projectId: ID!, $contentId: ID!) {
368
+ addProjectV2ItemById(input: { projectId: $projectId, contentId: $contentId }) { item { id } }
369
+ }
370
+ `;
371
+ const created = await input.fetchGraphQL(mutation, { projectId: input.projectId, contentId: input.issueNodeId }, input.token);
372
+ const addResult = asProjectRecord(asProjectRecord(created)?.addProjectV2ItemById);
373
+ const id = projectString(asProjectRecord(addResult?.item)?.id);
374
+ if (!id)
375
+ throw new Error("GitHub Project item creation did not return an item id.");
376
+ return { id, created: true };
377
+ }
378
+ async function updateIssueProjectStatus(input) {
379
+ const mutation = `
380
+ mutation RigUpdateProjectStatus($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
381
+ updateProjectV2ItemFieldValue(input: {
382
+ projectId: $projectId,
383
+ itemId: $itemId,
384
+ fieldId: $fieldId,
385
+ value: { singleSelectOptionId: $optionId }
386
+ }) { projectV2Item { id } }
387
+ }
388
+ `;
389
+ await input.fetchGraphQL(mutation, {
390
+ projectId: input.projectId,
391
+ itemId: input.itemId,
392
+ fieldId: input.fieldId,
393
+ optionId: input.optionId
394
+ }, input.token);
395
+ }
396
+ function fetchIssueNodeId(bin, repo, spawnFn, id, extraEnv, timeoutMs) {
397
+ const issue = runGh(bin, ["issue", "view", String(id), "--repo", repo, "--json", "id"], spawnFn, extraEnv, timeoutMs);
398
+ return projectString(issue.id) ?? projectString(issue.nodeId) ?? projectString(issue.node_id);
399
+ }
400
+ async function syncGitHubProjectStatus(bin, repo, spawnFn, id, status, projects, extraEnv, timeoutMs) {
401
+ if (!projects?.enabled)
402
+ return;
403
+ const projectId = projectString(projects.projectId);
404
+ if (!projectId)
405
+ throw new Error("GitHub Projects status sync is enabled but projectId is missing.");
406
+ const lifecycleStatus = projectLifecycleStatusForTaskStatus(status);
407
+ if (!lifecycleStatus)
408
+ return;
409
+ const issueNodeId = fetchIssueNodeId(bin, repo, spawnFn, id, extraEnv, timeoutMs);
410
+ if (!issueNodeId)
411
+ throw new Error(`GitHub issue ${repo}#${id} did not expose a node id for Projects status sync.`);
412
+ const projectStatus = projectString(projects.statuses?.[lifecycleStatus]) ?? DEFAULT_PROJECT_STATUSES[lifecycleStatus];
413
+ const fetchGraphQL = ghGraphQLFetch(bin, spawnFn, extraEnv, timeoutMs);
414
+ const field = await resolveProjectStatusField({ projectId, token: "gh-cli", fetchGraphQL });
415
+ const option = field.options.find((candidate) => candidate.name.toLowerCase() === projectStatus.toLowerCase() || candidate.id === projectStatus);
416
+ if (!option)
417
+ throw new Error(`GitHub Project ${projectId} Status field does not contain option "${projectStatus}".`);
418
+ const item = await ensureIssueProjectItem({ projectId, issueNodeId, token: "gh-cli", fetchGraphQL });
419
+ await updateIssueProjectStatus({
420
+ projectId,
421
+ itemId: item.id,
422
+ fieldId: projectString(projects.statusFieldId) ?? field.id,
423
+ optionId: option.id,
424
+ token: "gh-cli",
425
+ fetchGraphQL
426
+ });
427
+ }
428
+ var TERMINAL_TASK_STATUSES = new Set(["closed", "completed", "merged", "cancelled", "resolved", "done"]);
429
+ function normalizeTaskStatusToken(status) {
430
+ return status?.trim().toLowerCase().replace(/[-\s]+/g, "_") ?? "";
431
+ }
432
+ function issueUpdatesMode(value) {
433
+ return value === "off" || value === "minimal" || value === "lifecycle" ? value : "lifecycle";
434
+ }
435
+ function isTerminalTaskStatus(status) {
436
+ return TERMINAL_TASK_STATUSES.has(normalizeTaskStatusToken(status));
437
+ }
438
+ function shouldWriteIssueUpdate(mode, status) {
439
+ if (mode === "off")
440
+ return false;
441
+ if (mode === "lifecycle")
442
+ return true;
443
+ return isTerminalTaskStatus(status);
444
+ }
445
+ function isRunningStatus(status) {
446
+ return normalizeTaskStatusToken(status) === "running";
447
+ }
448
+ function assignRunningIssue(bin, repo, spawnFn, id, assignee, extraEnv, timeoutMs) {
449
+ runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, "--add-assignee", assignee?.trim() || "@me"], spawnFn, extraEnv, timeoutMs);
450
+ }
227
451
  function statusLabelFor(status) {
228
452
  switch (status) {
453
+ case "running":
229
454
  case "in_progress":
230
455
  return "in-progress";
231
456
  case "blocked":
@@ -244,6 +469,8 @@ function statusLabelFor(status) {
244
469
  return "under-review";
245
470
  case "needs_attention":
246
471
  return "blocked";
472
+ case "closed":
473
+ case "completed":
247
474
  case "open":
248
475
  return null;
249
476
  default:
@@ -252,11 +479,13 @@ function statusLabelFor(status) {
252
479
  }
253
480
  function rigStatusLabelFor(status) {
254
481
  switch (status) {
482
+ case "running":
255
483
  case "in_progress":
256
484
  return "rig:running";
257
485
  case "under_review":
258
486
  return "rig:pr-open";
259
487
  case "closed":
488
+ case "completed":
260
489
  return "rig:done";
261
490
  case "ci_fixing":
262
491
  return "rig:ci-fixing";
@@ -274,9 +503,10 @@ function rigStatusLabelFor(status) {
274
503
  return null;
275
504
  }
276
505
  }
277
- function applyIssueStatus(bin, repo, spawnFn, id, status, extraEnv, timeoutMs) {
278
- const targetLabel = status === "closed" ? null : statusLabelFor(status);
506
+ async function applyIssueStatus(bin, repo, spawnFn, id, status, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs) {
507
+ const targetLabel = status === "closed" || status === "completed" ? null : statusLabelFor(status);
279
508
  const targetRigLabel = rigStatusLabelFor(status);
509
+ const shouldSyncLifecycle = shouldWriteIssueUpdate(issueUpdates, status);
280
510
  for (const l of [...STATUS_LABELS, ...RIG_STATUS_LABELS]) {
281
511
  if (targetLabel !== null && l === targetLabel)
282
512
  continue;
@@ -298,7 +528,19 @@ function applyIssueStatus(bin, repo, spawnFn, id, status, extraEnv, timeoutMs) {
298
528
  runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, "--add-label", label], spawnFn, extraEnv, timeoutMs);
299
529
  }
300
530
  }
301
- if (status === "closed") {
531
+ if (isRunningStatus(status)) {
532
+ assignRunningIssue(bin, repo, spawnFn, id, runningAssignee, extraEnv, timeoutMs);
533
+ if (shouldSyncLifecycle) {
534
+ upsertRigStickyComment(bin, repo, spawnFn, String(id), buildRigStickyStatusComment({
535
+ status: "running",
536
+ summary: "Rig run started."
537
+ }), extraEnv, timeoutMs);
538
+ }
539
+ }
540
+ if (shouldSyncLifecycle) {
541
+ await syncGitHubProjectStatus(bin, repo, spawnFn, id, status, projects, extraEnv, timeoutMs);
542
+ }
543
+ if (status === "closed" || status === "completed") {
302
544
  runGhVoid(bin, ["issue", "close", String(id), "--repo", repo], spawnFn, extraEnv, timeoutMs);
303
545
  }
304
546
  }
@@ -368,11 +610,11 @@ function applyLabels(bin, repo, spawnFn, id, labels, action, extraEnv, timeoutMs
368
610
  } catch {}
369
611
  }
370
612
  }
371
- function applyIssueUpdate(bin, repo, spawnFn, id, update, extraEnv, timeoutMs) {
613
+ async function applyIssueUpdate(bin, repo, spawnFn, id, update, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs) {
372
614
  if (update.status) {
373
- applyIssueStatus(bin, repo, spawnFn, id, update.status, extraEnv, timeoutMs);
615
+ await applyIssueStatus(bin, repo, spawnFn, id, update.status, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs);
374
616
  }
375
- if (update.comment?.trim()) {
617
+ if (update.comment?.trim() && shouldWriteIssueUpdate(issueUpdates, update.status)) {
376
618
  if (isRigStickyStatusComment(update.comment)) {
377
619
  upsertRigStickyComment(bin, repo, spawnFn, String(id), update.comment, extraEnv, timeoutMs);
378
620
  } else {
@@ -398,6 +640,7 @@ function createGitHubIssuesTaskSource(opts) {
398
640
  const spawnFn = opts.spawn ?? spawnSync;
399
641
  const timeoutMs = Math.max(1000, Math.trunc(opts.timeoutMs ?? DEFAULT_GH_TIMEOUT_MS));
400
642
  const listLimit = Math.max(1, Math.trunc(opts.listLimit ?? DEFAULT_GITHUB_ISSUE_LIST_LIMIT));
643
+ const issueUpdates = issueUpdatesMode(opts.issueUpdates);
401
644
  return {
402
645
  id: "std:github-issues",
403
646
  kind: "github-issues",
@@ -427,9 +670,10 @@ function createGitHubIssuesTaskSource(opts) {
427
670
  return issues.map((i) => issueToTask(i, repo));
428
671
  },
429
672
  async get(id) {
673
+ const env = await resolveCredentialEnv(opts, "selected-repo");
674
+ let issue;
430
675
  try {
431
- const env = await resolveCredentialEnv(opts, "selected-repo");
432
- const issue = runGh(bin, [
676
+ issue = runGh(bin, [
433
677
  "issue",
434
678
  "view",
435
679
  String(id),
@@ -438,19 +682,23 @@ function createGitHubIssuesTaskSource(opts) {
438
682
  "--json",
439
683
  "number,title,body,labels,state,url,assignees,id"
440
684
  ], spawnFn, env, timeoutMs);
441
- return issueToTask(issue, repo);
442
- } catch {
443
- return;
685
+ } catch (error) {
686
+ const detail = error instanceof Error ? error.message : String(error);
687
+ if (/could not resolve to (an? )?(issue|pullrequest)|no issues? (found|matched)|404 not found|gh: not found/i.test(detail)) {
688
+ return;
689
+ }
690
+ throw new Error(`Failed to read task ${id} from GitHub repo ${repo}: ${detail}`);
444
691
  }
692
+ return issueToTask(issue, repo);
445
693
  },
446
694
  async updateStatus(id, status) {
447
695
  const env = await resolveCredentialEnv(opts, "selected-repo");
448
- applyIssueStatus(bin, repo, spawnFn, id, status, env, timeoutMs);
696
+ await applyIssueStatus(bin, repo, spawnFn, id, status, opts.projects, opts.assignee, issueUpdates, env, timeoutMs);
449
697
  notifyTaskChanged(opts.onTaskChanged, repo, id, status);
450
698
  },
451
699
  async updateTask(id, update) {
452
700
  const env = await resolveCredentialEnv(opts, "selected-repo");
453
- applyIssueUpdate(bin, repo, spawnFn, id, update, env, timeoutMs);
701
+ await applyIssueUpdate(bin, repo, spawnFn, id, update, opts.projects, opts.assignee, issueUpdates, env, timeoutMs);
454
702
  notifyTaskChanged(opts.onTaskChanged, repo, id, update.status);
455
703
  },
456
704
  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
  }
@@ -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
  }
@@ -166,17 +173,41 @@ ${rendered}
166
173
  ` : `${rendered}
167
174
  `;
168
175
  }
176
+ function buildRigStickyStatusComment(input) {
177
+ const lines = [
178
+ RIG_STATUS_COMMENT_MARKER,
179
+ `### Rig status: ${input.status}`,
180
+ "",
181
+ input.summary
182
+ ];
183
+ if (input.runId)
184
+ lines.push("", `- Run: ${input.runId}`);
185
+ if (input.prUrl)
186
+ lines.push(`- PR: ${input.prUrl}`);
187
+ for (const detail of input.details ?? [])
188
+ lines.push(`- ${detail}`);
189
+ return lines.join(`
190
+ `);
191
+ }
169
192
  function isRigStickyStatusComment(body) {
170
193
  return body.includes(RIG_STATUS_COMMENT_MARKER);
171
194
  }
172
195
  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 } };
196
+ return {
197
+ encoding: "utf-8",
198
+ timeout: timeoutMs,
199
+ env: {
200
+ ...process.env,
201
+ ...process.env.GH_TOKEN !== undefined ? { GH_TOKEN: process.env.GH_TOKEN } : {},
202
+ ...process.env.GITHUB_TOKEN !== undefined ? { GITHUB_TOKEN: process.env.GITHUB_TOKEN } : {},
203
+ ...process.env.RIG_GITHUB_TOKEN !== undefined ? { RIG_GITHUB_TOKEN: process.env.RIG_GITHUB_TOKEN } : {},
204
+ ...extraEnv ?? {}
205
+ }
206
+ };
176
207
  }
177
208
  function credentialEnv(token) {
178
209
  const clean = token?.trim() ?? "";
179
- return { GH_TOKEN: clean, GITHUB_TOKEN: clean };
210
+ return { GH_TOKEN: clean, GITHUB_TOKEN: clean, RIG_GITHUB_TOKEN: clean };
180
211
  }
181
212
  async function resolveCredentialEnv(opts, purpose) {
182
213
  if (!opts.credentialProvider)
@@ -191,28 +222,239 @@ async function resolveCredentialEnv(opts, purpose) {
191
222
  const resolved = await opts.credentialProvider.resolveGitHubToken(input);
192
223
  return credentialEnv(resolved.token);
193
224
  }
225
+ function tokenDiagnostic(value) {
226
+ const clean = value?.trim() ?? "";
227
+ return clean ? `present(len=${clean.length})` : "missing";
228
+ }
194
229
  function runGh(bin, args, spawn, extraEnv, timeoutMs) {
195
- const res = spawn(bin, [...args], ghSpawnOptions(extraEnv, timeoutMs));
196
- assertGhSuccess(args, res);
230
+ const options = ghSpawnOptions(extraEnv, timeoutMs);
231
+ const res = spawn(bin, [...args], options);
232
+ assertGhSuccess(args, res, options.env);
197
233
  if (!res.stdout || res.stdout.trim() === "")
198
234
  return [];
199
235
  return JSON.parse(res.stdout);
200
236
  }
201
237
  function runGhVoid(bin, args, spawn, extraEnv, timeoutMs) {
202
- const res = spawn(bin, [...args], ghSpawnOptions(extraEnv, timeoutMs));
203
- assertGhSuccess(args, res);
238
+ const options = ghSpawnOptions(extraEnv, timeoutMs);
239
+ const res = spawn(bin, [...args], options);
240
+ assertGhSuccess(args, res, options.env);
204
241
  }
205
- function assertGhSuccess(args, res) {
242
+ function assertGhSuccess(args, res, env) {
206
243
  if (res.error) {
207
244
  const msg = res.error.message ?? String(res.error);
208
245
  throw new Error(`gh CLI not available \u2014 install gh (brew install gh / apt install gh): ${msg}`);
209
246
  }
210
247
  if (res.status !== 0) {
211
- throw new Error(`gh ${args.join(" ")} failed (exit ${res.status}): ${res.stderr}`);
248
+ throw new Error(`gh ${args.join(" ")} failed (exit ${res.status}): ${res.stderr}
249
+ [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)}`);
250
+ }
251
+ }
252
+ var DEFAULT_PROJECT_STATUSES = {
253
+ todo: "Todo",
254
+ running: "In Progress",
255
+ prOpen: "In Review",
256
+ ciFixing: "In Review",
257
+ merging: "In Review",
258
+ done: "Done",
259
+ needsAttention: "Needs Attention"
260
+ };
261
+ function asProjectRecord(value) {
262
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
263
+ }
264
+ function projectString(value) {
265
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
266
+ }
267
+ function projectLifecycleStatusForTaskStatus(status) {
268
+ const normalized = status?.trim().toLowerCase().replace(/[-\s]+/g, "_");
269
+ switch (normalized) {
270
+ case "draft":
271
+ case "open":
272
+ case "queued":
273
+ case "ready":
274
+ return "todo";
275
+ case "running":
276
+ case "in_progress":
277
+ return "running";
278
+ case "under_review":
279
+ case "review":
280
+ case "pr_open":
281
+ return "prOpen";
282
+ case "ci_fixing":
283
+ case "fixing":
284
+ return "ciFixing";
285
+ case "merging":
286
+ case "merge":
287
+ return "merging";
288
+ case "closed":
289
+ case "completed":
290
+ case "done":
291
+ return "done";
292
+ case "blocked":
293
+ case "cancelled":
294
+ case "failed":
295
+ case "needs_attention":
296
+ return "needsAttention";
297
+ default:
298
+ return null;
212
299
  }
213
300
  }
301
+ function ghGraphQLFetch(bin, spawnFn, extraEnv, timeoutMs) {
302
+ return async (query, variables) => {
303
+ const args = ["api", "graphql", "-f", `query=${query}`];
304
+ for (const [key, value] of Object.entries(variables)) {
305
+ if (value === undefined || value === null)
306
+ continue;
307
+ args.push("-f", `${key}=${String(value)}`);
308
+ }
309
+ const response = runGh(bin, args, spawnFn, extraEnv, timeoutMs);
310
+ return asProjectRecord(response)?.data ?? response;
311
+ };
312
+ }
313
+ function projectStatusFieldFrom(data, projectId) {
314
+ const fields = asProjectRecord(asProjectRecord(asProjectRecord(data)?.node)?.fields)?.nodes;
315
+ for (const node of Array.isArray(fields) ? fields : []) {
316
+ const record = asProjectRecord(node);
317
+ if (projectString(record?.name)?.toLowerCase() !== "status")
318
+ continue;
319
+ const id = projectString(record?.id);
320
+ if (!id)
321
+ continue;
322
+ const options = Array.isArray(record?.options) ? record.options.flatMap((option) => {
323
+ const optionRecord = asProjectRecord(option);
324
+ const optionId = projectString(optionRecord?.id);
325
+ const name = projectString(optionRecord?.name);
326
+ return optionId && name ? [{ id: optionId, name }] : [];
327
+ }) : [];
328
+ return { id, name: "Status", options };
329
+ }
330
+ throw new Error(`GitHub Project ${projectId} does not expose a Status single-select field.`);
331
+ }
332
+ async function resolveProjectStatusField(input) {
333
+ const query = `
334
+ query RigProjectStatusField($projectId: ID!) {
335
+ node(id: $projectId) {
336
+ ... on ProjectV2 {
337
+ fields(first: 50) {
338
+ nodes {
339
+ ... on ProjectV2FieldCommon { id name }
340
+ ... on ProjectV2SingleSelectField { id name options { id name } }
341
+ }
342
+ }
343
+ }
344
+ }
345
+ }
346
+ `;
347
+ return projectStatusFieldFrom(await input.fetchGraphQL(query, { projectId: input.projectId }, input.token), input.projectId);
348
+ }
349
+ async function ensureIssueProjectItem(input) {
350
+ const query = `
351
+ query RigFindProjectIssueItem($projectId: ID!) {
352
+ node(id: $projectId) {
353
+ ... on ProjectV2 {
354
+ items(first: 100) { nodes { id content { ... on Issue { id } } } }
355
+ }
356
+ }
357
+ }
358
+ `;
359
+ const data = await input.fetchGraphQL(query, { projectId: input.projectId }, input.token);
360
+ const nodes = asProjectRecord(asProjectRecord(asProjectRecord(data)?.node)?.items)?.nodes;
361
+ for (const node of Array.isArray(nodes) ? nodes : []) {
362
+ const record = asProjectRecord(node);
363
+ const content = asProjectRecord(record?.content);
364
+ if (projectString(content?.id) === input.issueNodeId) {
365
+ const id2 = projectString(record?.id);
366
+ if (id2)
367
+ return { id: id2, created: false };
368
+ }
369
+ }
370
+ const mutation = `
371
+ mutation RigAddIssueToProject($projectId: ID!, $contentId: ID!) {
372
+ addProjectV2ItemById(input: { projectId: $projectId, contentId: $contentId }) { item { id } }
373
+ }
374
+ `;
375
+ const created = await input.fetchGraphQL(mutation, { projectId: input.projectId, contentId: input.issueNodeId }, input.token);
376
+ const addResult = asProjectRecord(asProjectRecord(created)?.addProjectV2ItemById);
377
+ const id = projectString(asProjectRecord(addResult?.item)?.id);
378
+ if (!id)
379
+ throw new Error("GitHub Project item creation did not return an item id.");
380
+ return { id, created: true };
381
+ }
382
+ async function updateIssueProjectStatus(input) {
383
+ const mutation = `
384
+ mutation RigUpdateProjectStatus($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
385
+ updateProjectV2ItemFieldValue(input: {
386
+ projectId: $projectId,
387
+ itemId: $itemId,
388
+ fieldId: $fieldId,
389
+ value: { singleSelectOptionId: $optionId }
390
+ }) { projectV2Item { id } }
391
+ }
392
+ `;
393
+ await input.fetchGraphQL(mutation, {
394
+ projectId: input.projectId,
395
+ itemId: input.itemId,
396
+ fieldId: input.fieldId,
397
+ optionId: input.optionId
398
+ }, input.token);
399
+ }
400
+ function fetchIssueNodeId(bin, repo, spawnFn, id, extraEnv, timeoutMs) {
401
+ const issue = runGh(bin, ["issue", "view", String(id), "--repo", repo, "--json", "id"], spawnFn, extraEnv, timeoutMs);
402
+ return projectString(issue.id) ?? projectString(issue.nodeId) ?? projectString(issue.node_id);
403
+ }
404
+ async function syncGitHubProjectStatus(bin, repo, spawnFn, id, status, projects, extraEnv, timeoutMs) {
405
+ if (!projects?.enabled)
406
+ return;
407
+ const projectId = projectString(projects.projectId);
408
+ if (!projectId)
409
+ throw new Error("GitHub Projects status sync is enabled but projectId is missing.");
410
+ const lifecycleStatus = projectLifecycleStatusForTaskStatus(status);
411
+ if (!lifecycleStatus)
412
+ return;
413
+ const issueNodeId = fetchIssueNodeId(bin, repo, spawnFn, id, extraEnv, timeoutMs);
414
+ if (!issueNodeId)
415
+ throw new Error(`GitHub issue ${repo}#${id} did not expose a node id for Projects status sync.`);
416
+ const projectStatus = projectString(projects.statuses?.[lifecycleStatus]) ?? DEFAULT_PROJECT_STATUSES[lifecycleStatus];
417
+ const fetchGraphQL = ghGraphQLFetch(bin, spawnFn, extraEnv, timeoutMs);
418
+ const field = await resolveProjectStatusField({ projectId, token: "gh-cli", fetchGraphQL });
419
+ const option = field.options.find((candidate) => candidate.name.toLowerCase() === projectStatus.toLowerCase() || candidate.id === projectStatus);
420
+ if (!option)
421
+ throw new Error(`GitHub Project ${projectId} Status field does not contain option "${projectStatus}".`);
422
+ const item = await ensureIssueProjectItem({ projectId, issueNodeId, token: "gh-cli", fetchGraphQL });
423
+ await updateIssueProjectStatus({
424
+ projectId,
425
+ itemId: item.id,
426
+ fieldId: projectString(projects.statusFieldId) ?? field.id,
427
+ optionId: option.id,
428
+ token: "gh-cli",
429
+ fetchGraphQL
430
+ });
431
+ }
432
+ var TERMINAL_TASK_STATUSES = new Set(["closed", "completed", "merged", "cancelled", "resolved", "done"]);
433
+ function normalizeTaskStatusToken(status) {
434
+ return status?.trim().toLowerCase().replace(/[-\s]+/g, "_") ?? "";
435
+ }
436
+ function issueUpdatesMode(value) {
437
+ return value === "off" || value === "minimal" || value === "lifecycle" ? value : "lifecycle";
438
+ }
439
+ function isTerminalTaskStatus(status) {
440
+ return TERMINAL_TASK_STATUSES.has(normalizeTaskStatusToken(status));
441
+ }
442
+ function shouldWriteIssueUpdate(mode, status) {
443
+ if (mode === "off")
444
+ return false;
445
+ if (mode === "lifecycle")
446
+ return true;
447
+ return isTerminalTaskStatus(status);
448
+ }
449
+ function isRunningStatus(status) {
450
+ return normalizeTaskStatusToken(status) === "running";
451
+ }
452
+ function assignRunningIssue(bin, repo, spawnFn, id, assignee, extraEnv, timeoutMs) {
453
+ runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, "--add-assignee", assignee?.trim() || "@me"], spawnFn, extraEnv, timeoutMs);
454
+ }
214
455
  function statusLabelFor(status) {
215
456
  switch (status) {
457
+ case "running":
216
458
  case "in_progress":
217
459
  return "in-progress";
218
460
  case "blocked":
@@ -231,6 +473,8 @@ function statusLabelFor(status) {
231
473
  return "under-review";
232
474
  case "needs_attention":
233
475
  return "blocked";
476
+ case "closed":
477
+ case "completed":
234
478
  case "open":
235
479
  return null;
236
480
  default:
@@ -239,11 +483,13 @@ function statusLabelFor(status) {
239
483
  }
240
484
  function rigStatusLabelFor(status) {
241
485
  switch (status) {
486
+ case "running":
242
487
  case "in_progress":
243
488
  return "rig:running";
244
489
  case "under_review":
245
490
  return "rig:pr-open";
246
491
  case "closed":
492
+ case "completed":
247
493
  return "rig:done";
248
494
  case "ci_fixing":
249
495
  return "rig:ci-fixing";
@@ -261,9 +507,10 @@ function rigStatusLabelFor(status) {
261
507
  return null;
262
508
  }
263
509
  }
264
- function applyIssueStatus(bin, repo, spawnFn, id, status, extraEnv, timeoutMs) {
265
- const targetLabel = status === "closed" ? null : statusLabelFor(status);
510
+ async function applyIssueStatus(bin, repo, spawnFn, id, status, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs) {
511
+ const targetLabel = status === "closed" || status === "completed" ? null : statusLabelFor(status);
266
512
  const targetRigLabel = rigStatusLabelFor(status);
513
+ const shouldSyncLifecycle = shouldWriteIssueUpdate(issueUpdates, status);
267
514
  for (const l of [...STATUS_LABELS, ...RIG_STATUS_LABELS]) {
268
515
  if (targetLabel !== null && l === targetLabel)
269
516
  continue;
@@ -285,7 +532,19 @@ function applyIssueStatus(bin, repo, spawnFn, id, status, extraEnv, timeoutMs) {
285
532
  runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, "--add-label", label], spawnFn, extraEnv, timeoutMs);
286
533
  }
287
534
  }
288
- if (status === "closed") {
535
+ if (isRunningStatus(status)) {
536
+ assignRunningIssue(bin, repo, spawnFn, id, runningAssignee, extraEnv, timeoutMs);
537
+ if (shouldSyncLifecycle) {
538
+ upsertRigStickyComment(bin, repo, spawnFn, String(id), buildRigStickyStatusComment({
539
+ status: "running",
540
+ summary: "Rig run started."
541
+ }), extraEnv, timeoutMs);
542
+ }
543
+ }
544
+ if (shouldSyncLifecycle) {
545
+ await syncGitHubProjectStatus(bin, repo, spawnFn, id, status, projects, extraEnv, timeoutMs);
546
+ }
547
+ if (status === "closed" || status === "completed") {
289
548
  runGhVoid(bin, ["issue", "close", String(id), "--repo", repo], spawnFn, extraEnv, timeoutMs);
290
549
  }
291
550
  }
@@ -355,11 +614,11 @@ function applyLabels(bin, repo, spawnFn, id, labels, action, extraEnv, timeoutMs
355
614
  } catch {}
356
615
  }
357
616
  }
358
- function applyIssueUpdate(bin, repo, spawnFn, id, update, extraEnv, timeoutMs) {
617
+ async function applyIssueUpdate(bin, repo, spawnFn, id, update, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs) {
359
618
  if (update.status) {
360
- applyIssueStatus(bin, repo, spawnFn, id, update.status, extraEnv, timeoutMs);
619
+ await applyIssueStatus(bin, repo, spawnFn, id, update.status, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs);
361
620
  }
362
- if (update.comment?.trim()) {
621
+ if (update.comment?.trim() && shouldWriteIssueUpdate(issueUpdates, update.status)) {
363
622
  if (isRigStickyStatusComment(update.comment)) {
364
623
  upsertRigStickyComment(bin, repo, spawnFn, String(id), update.comment, extraEnv, timeoutMs);
365
624
  } else {
@@ -385,6 +644,7 @@ function createGitHubIssuesTaskSource(opts) {
385
644
  const spawnFn = opts.spawn ?? spawnSync;
386
645
  const timeoutMs = Math.max(1000, Math.trunc(opts.timeoutMs ?? DEFAULT_GH_TIMEOUT_MS));
387
646
  const listLimit = Math.max(1, Math.trunc(opts.listLimit ?? DEFAULT_GITHUB_ISSUE_LIST_LIMIT));
647
+ const issueUpdates = issueUpdatesMode(opts.issueUpdates);
388
648
  return {
389
649
  id: "std:github-issues",
390
650
  kind: "github-issues",
@@ -414,9 +674,10 @@ function createGitHubIssuesTaskSource(opts) {
414
674
  return issues.map((i) => issueToTask(i, repo));
415
675
  },
416
676
  async get(id) {
677
+ const env = await resolveCredentialEnv(opts, "selected-repo");
678
+ let issue;
417
679
  try {
418
- const env = await resolveCredentialEnv(opts, "selected-repo");
419
- const issue = runGh(bin, [
680
+ issue = runGh(bin, [
420
681
  "issue",
421
682
  "view",
422
683
  String(id),
@@ -425,19 +686,23 @@ function createGitHubIssuesTaskSource(opts) {
425
686
  "--json",
426
687
  "number,title,body,labels,state,url,assignees,id"
427
688
  ], spawnFn, env, timeoutMs);
428
- return issueToTask(issue, repo);
429
- } catch {
430
- return;
689
+ } catch (error) {
690
+ const detail = error instanceof Error ? error.message : String(error);
691
+ if (/could not resolve to (an? )?(issue|pullrequest)|no issues? (found|matched)|404 not found|gh: not found/i.test(detail)) {
692
+ return;
693
+ }
694
+ throw new Error(`Failed to read task ${id} from GitHub repo ${repo}: ${detail}`);
431
695
  }
696
+ return issueToTask(issue, repo);
432
697
  },
433
698
  async updateStatus(id, status) {
434
699
  const env = await resolveCredentialEnv(opts, "selected-repo");
435
- applyIssueStatus(bin, repo, spawnFn, id, status, env, timeoutMs);
700
+ await applyIssueStatus(bin, repo, spawnFn, id, status, opts.projects, opts.assignee, issueUpdates, env, timeoutMs);
436
701
  notifyTaskChanged(opts.onTaskChanged, repo, id, status);
437
702
  },
438
703
  async updateTask(id, update) {
439
704
  const env = await resolveCredentialEnv(opts, "selected-repo");
440
- applyIssueUpdate(bin, repo, spawnFn, id, update, env, timeoutMs);
705
+ await applyIssueUpdate(bin, repo, spawnFn, id, update, opts.projects, opts.assignee, issueUpdates, env, timeoutMs);
441
706
  notifyTaskChanged(opts.onTaskChanged, repo, id, update.status);
442
707
  },
443
708
  async addLabels(id, labels) {
@@ -476,7 +741,7 @@ function createGitHubIssuesTaskSource(opts) {
476
741
 
477
742
  // packages/standard-plugin/src/files-source.ts
478
743
  import { readFileSync as readFileSync2, readdirSync, existsSync as existsSync2, statSync, writeFileSync } from "fs";
479
- import { join, basename } from "path";
744
+ import { join, basename, isAbsolute, resolve as resolve2 } from "path";
480
745
  var DEFAULT_PATTERN = /\.(task\.)?json$/;
481
746
  function readTaskFile(file, pattern) {
482
747
  const raw = JSON.parse(readFileSync2(file, "utf-8"));
@@ -497,10 +762,11 @@ function readTaskFile(file, pattern) {
497
762
  }
498
763
  function createFilesTaskSource(opts) {
499
764
  const pattern = opts.pattern ?? DEFAULT_PATTERN;
500
- const directory = opts.path ?? opts.dir;
501
- if (!directory) {
765
+ const configured = opts.path ?? opts.dir;
766
+ if (!configured) {
502
767
  throw new Error("createFilesTaskSource: either `path` or `dir` must be provided");
503
768
  }
769
+ const directory = isAbsolute(configured) ? configured : resolve2(opts.projectRoot ?? process.cwd(), configured);
504
770
  const findTaskFile = (id) => {
505
771
  if (!existsSync2(directory))
506
772
  return;
@@ -585,6 +851,40 @@ function requireStringField(config, field, kind) {
585
851
  }
586
852
  return value;
587
853
  }
854
+ function isRecord(value) {
855
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
856
+ }
857
+ function optionalString(value) {
858
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
859
+ }
860
+ function parseGitHubProjectsOptions(value) {
861
+ if (!isRecord(value))
862
+ return;
863
+ const statusesSource = isRecord(value.statuses) ? value.statuses : undefined;
864
+ const statuses = {};
865
+ for (const key of ["todo", "running", "prOpen", "ciFixing", "merging", "done", "needsAttention"]) {
866
+ const status = optionalString(statusesSource?.[key]);
867
+ if (status)
868
+ statuses[key] = status;
869
+ }
870
+ const parsed = {};
871
+ if (typeof value.enabled === "boolean")
872
+ parsed.enabled = value.enabled;
873
+ const projectId = optionalString(value.projectId);
874
+ if (projectId)
875
+ parsed.projectId = projectId;
876
+ const statusFieldId = optionalString(value.statusFieldId);
877
+ if (statusFieldId)
878
+ parsed.statusFieldId = statusFieldId;
879
+ if (Object.keys(statuses).length > 0)
880
+ parsed.statuses = statuses;
881
+ return parsed;
882
+ }
883
+ function githubProjectsOptionsFromConfig(config, context) {
884
+ const rigConfig = isRecord(context?.rigConfig) ? context.rigConfig : undefined;
885
+ const github = isRecord(rigConfig?.github) ? rigConfig.github : undefined;
886
+ return parseGitHubProjectsOptions(config.options?.projects) ?? parseGitHubProjectsOptions(github?.projects);
887
+ }
588
888
  function standardPlugin(opts = {}) {
589
889
  return definePlugin({
590
890
  name: "rig-standard",
@@ -609,13 +909,13 @@ function standardPlugin(opts = {}) {
609
909
  id: "std:github-issues",
610
910
  kind: "github-issues",
611
911
  description: "GitHub Issues via gh CLI",
612
- factory(config) {
912
+ factory(config, context) {
613
913
  const options = {
614
914
  owner: requireStringField(config, "owner", "github-issues"),
615
915
  repo: requireStringField(config, "repo", "github-issues")
616
916
  };
617
- if (opts.githubCredentialProvider)
618
- options.credentialProvider = opts.githubCredentialProvider;
917
+ const credentialProviderOptions = context?.projectRoot ? { stateDir: resolve3(context.projectRoot, ".rig", "state") } : {};
918
+ options.credentialProvider = opts.githubCredentialProvider ?? createStateGitHubCredentialProvider(credentialProviderOptions);
619
919
  if (opts.githubWorkspaceId)
620
920
  options.workspaceId = opts.githubWorkspaceId;
621
921
  if (opts.githubUserId)
@@ -637,6 +937,9 @@ function standardPlugin(opts = {}) {
637
937
  const listLimit = typeof config.options?.listLimit === "number" ? config.options.listLimit : undefined;
638
938
  if (listLimit !== undefined)
639
939
  options.listLimit = listLimit;
940
+ const projects = githubProjectsOptionsFromConfig(config, context);
941
+ if (projects)
942
+ options.projects = projects;
640
943
  return createGitHubIssuesTaskSource(options);
641
944
  }
642
945
  },
@@ -644,9 +947,10 @@ function standardPlugin(opts = {}) {
644
947
  id: "std:files",
645
948
  kind: "files",
646
949
  description: "JSON files in a local directory",
647
- factory(config) {
950
+ factory(config, context) {
648
951
  return createFilesTaskSource({
649
- path: requireStringField(config, "path", "files")
952
+ path: requireStringField(config, "path", "files"),
953
+ ...context?.projectRoot ? { projectRoot: context.projectRoot } : {}
650
954
  });
651
955
  }
652
956
  }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@h-rig/standard-plugin",
3
- "version": "0.0.6-alpha.13",
3
+ "version": "0.0.6-alpha.130",
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.13",
23
- "@rig/core": "npm:@h-rig/core@0.0.6-alpha.13",
24
+ "@rig/contracts": "npm:@h-rig/contracts@0.0.6-alpha.130",
25
+ "@rig/core": "npm:@h-rig/core@0.0.6-alpha.130",
24
26
  "effect": "4.0.0-beta.78"
25
27
  }
26
28
  }