@h-rig/standard-plugin 0.0.6-alpha.1 → 0.0.6-alpha.100

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
  }
@@ -111,8 +111,10 @@ function issueToTask(issue, repo) {
111
111
  const role = roleLabel ? roleLabel.slice("role:".length) : undefined;
112
112
  const validators = labelNames.filter((l) => l.startsWith("validator:")).map((l) => l.slice("validator:".length));
113
113
  const body = issue.body ?? "";
114
+ const issueNodeId = issue.id ?? issue.nodeId ?? issue.node_id;
114
115
  return {
115
116
  id: String(issue.number),
117
+ ...typeof issueNodeId === "string" && issueNodeId.trim() ? { issueNodeId: issueNodeId.trim() } : {},
116
118
  deps: parseDeps(body),
117
119
  status: statusFor(issue),
118
120
  title: issue.title,
@@ -181,13 +183,21 @@ function isRigStickyStatusComment(body) {
181
183
  return body.includes(RIG_STATUS_COMMENT_MARKER);
182
184
  }
183
185
  function ghSpawnOptions(extraEnv, timeoutMs) {
184
- if (!extraEnv)
185
- return { encoding: "utf-8", timeout: timeoutMs };
186
- 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
+ };
187
197
  }
188
198
  function credentialEnv(token) {
189
199
  const clean = token?.trim() ?? "";
190
- return { GH_TOKEN: clean, GITHUB_TOKEN: clean };
200
+ return { GH_TOKEN: clean, GITHUB_TOKEN: clean, RIG_GITHUB_TOKEN: clean };
191
201
  }
192
202
  async function resolveCredentialEnv(opts, purpose) {
193
203
  if (!opts.credentialProvider)
@@ -202,28 +212,239 @@ async function resolveCredentialEnv(opts, purpose) {
202
212
  const resolved = await opts.credentialProvider.resolveGitHubToken(input);
203
213
  return credentialEnv(resolved.token);
204
214
  }
215
+ function tokenDiagnostic(value) {
216
+ const clean = value?.trim() ?? "";
217
+ return clean ? `present(len=${clean.length})` : "missing";
218
+ }
205
219
  function runGh(bin, args, spawn, extraEnv, timeoutMs) {
206
- const res = spawn(bin, [...args], ghSpawnOptions(extraEnv, timeoutMs));
207
- assertGhSuccess(args, res);
220
+ const options = ghSpawnOptions(extraEnv, timeoutMs);
221
+ const res = spawn(bin, [...args], options);
222
+ assertGhSuccess(args, res, options.env);
208
223
  if (!res.stdout || res.stdout.trim() === "")
209
224
  return [];
210
225
  return JSON.parse(res.stdout);
211
226
  }
212
227
  function runGhVoid(bin, args, spawn, extraEnv, timeoutMs) {
213
- const res = spawn(bin, [...args], ghSpawnOptions(extraEnv, timeoutMs));
214
- assertGhSuccess(args, res);
228
+ const options = ghSpawnOptions(extraEnv, timeoutMs);
229
+ const res = spawn(bin, [...args], options);
230
+ assertGhSuccess(args, res, options.env);
215
231
  }
216
- function assertGhSuccess(args, res) {
232
+ function assertGhSuccess(args, res, env) {
217
233
  if (res.error) {
218
234
  const msg = res.error.message ?? String(res.error);
219
235
  throw new Error(`gh CLI not available \u2014 install gh (brew install gh / apt install gh): ${msg}`);
220
236
  }
221
237
  if (res.status !== 0) {
222
- 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;
223
289
  }
224
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
+ }
225
445
  function statusLabelFor(status) {
226
446
  switch (status) {
447
+ case "running":
227
448
  case "in_progress":
228
449
  return "in-progress";
229
450
  case "blocked":
@@ -242,6 +463,8 @@ function statusLabelFor(status) {
242
463
  return "under-review";
243
464
  case "needs_attention":
244
465
  return "blocked";
466
+ case "closed":
467
+ case "completed":
245
468
  case "open":
246
469
  return null;
247
470
  default:
@@ -250,11 +473,13 @@ function statusLabelFor(status) {
250
473
  }
251
474
  function rigStatusLabelFor(status) {
252
475
  switch (status) {
476
+ case "running":
253
477
  case "in_progress":
254
478
  return "rig:running";
255
479
  case "under_review":
256
480
  return "rig:pr-open";
257
481
  case "closed":
482
+ case "completed":
258
483
  return "rig:done";
259
484
  case "ci_fixing":
260
485
  return "rig:ci-fixing";
@@ -272,9 +497,10 @@ function rigStatusLabelFor(status) {
272
497
  return null;
273
498
  }
274
499
  }
275
- function applyIssueStatus(bin, repo, spawnFn, id, status, extraEnv, timeoutMs) {
276
- 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);
277
502
  const targetRigLabel = rigStatusLabelFor(status);
503
+ const shouldSyncLifecycle = shouldWriteIssueUpdate(issueUpdates, status);
278
504
  for (const l of [...STATUS_LABELS, ...RIG_STATUS_LABELS]) {
279
505
  if (targetLabel !== null && l === targetLabel)
280
506
  continue;
@@ -296,7 +522,19 @@ function applyIssueStatus(bin, repo, spawnFn, id, status, extraEnv, timeoutMs) {
296
522
  runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, "--add-label", label], spawnFn, extraEnv, timeoutMs);
297
523
  }
298
524
  }
299
- 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") {
300
538
  runGhVoid(bin, ["issue", "close", String(id), "--repo", repo], spawnFn, extraEnv, timeoutMs);
301
539
  }
302
540
  }
@@ -366,11 +604,11 @@ function applyLabels(bin, repo, spawnFn, id, labels, action, extraEnv, timeoutMs
366
604
  } catch {}
367
605
  }
368
606
  }
369
- function applyIssueUpdate(bin, repo, spawnFn, id, update, extraEnv, timeoutMs) {
607
+ async function applyIssueUpdate(bin, repo, spawnFn, id, update, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs) {
370
608
  if (update.status) {
371
- applyIssueStatus(bin, repo, spawnFn, id, update.status, extraEnv, timeoutMs);
609
+ await applyIssueStatus(bin, repo, spawnFn, id, update.status, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs);
372
610
  }
373
- if (update.comment?.trim()) {
611
+ if (update.comment?.trim() && shouldWriteIssueUpdate(issueUpdates, update.status)) {
374
612
  if (isRigStickyStatusComment(update.comment)) {
375
613
  upsertRigStickyComment(bin, repo, spawnFn, String(id), update.comment, extraEnv, timeoutMs);
376
614
  } else {
@@ -396,6 +634,7 @@ function createGitHubIssuesTaskSource(opts) {
396
634
  const spawnFn = opts.spawn ?? spawnSync;
397
635
  const timeoutMs = Math.max(1000, Math.trunc(opts.timeoutMs ?? DEFAULT_GH_TIMEOUT_MS));
398
636
  const listLimit = Math.max(1, Math.trunc(opts.listLimit ?? DEFAULT_GITHUB_ISSUE_LIST_LIMIT));
637
+ const issueUpdates = issueUpdatesMode(opts.issueUpdates);
399
638
  return {
400
639
  id: "std:github-issues",
401
640
  kind: "github-issues",
@@ -414,7 +653,7 @@ function createGitHubIssuesTaskSource(opts) {
414
653
  "--limit",
415
654
  String(listLimit),
416
655
  "--json",
417
- "number,title,body,labels,state,url,assignees"
656
+ "number,title,body,labels,state,url,assignees,id"
418
657
  ];
419
658
  const env = await resolveCredentialEnv(opts, "selected-repo");
420
659
  const rawIssues = runGh(bin, args, spawnFn, env, timeoutMs);
@@ -434,7 +673,7 @@ function createGitHubIssuesTaskSource(opts) {
434
673
  "--repo",
435
674
  repo,
436
675
  "--json",
437
- "number,title,body,labels,state,url,assignees"
676
+ "number,title,body,labels,state,url,assignees,id"
438
677
  ], spawnFn, env, timeoutMs);
439
678
  return issueToTask(issue, repo);
440
679
  } catch {
@@ -443,12 +682,12 @@ function createGitHubIssuesTaskSource(opts) {
443
682
  },
444
683
  async updateStatus(id, status) {
445
684
  const env = await resolveCredentialEnv(opts, "selected-repo");
446
- applyIssueStatus(bin, repo, spawnFn, id, status, env, timeoutMs);
685
+ await applyIssueStatus(bin, repo, spawnFn, id, status, opts.projects, opts.assignee, issueUpdates, env, timeoutMs);
447
686
  notifyTaskChanged(opts.onTaskChanged, repo, id, status);
448
687
  },
449
688
  async updateTask(id, update) {
450
689
  const env = await resolveCredentialEnv(opts, "selected-repo");
451
- applyIssueUpdate(bin, repo, spawnFn, id, update, env, timeoutMs);
690
+ await applyIssueUpdate(bin, repo, spawnFn, id, update, opts.projects, opts.assignee, issueUpdates, env, timeoutMs);
452
691
  notifyTaskChanged(opts.onTaskChanged, repo, id, update.status);
453
692
  },
454
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
  }
@@ -114,8 +115,10 @@ function issueToTask(issue, repo) {
114
115
  const role = roleLabel ? roleLabel.slice("role:".length) : undefined;
115
116
  const validators = labelNames.filter((l) => l.startsWith("validator:")).map((l) => l.slice("validator:".length));
116
117
  const body = issue.body ?? "";
118
+ const issueNodeId = issue.id ?? issue.nodeId ?? issue.node_id;
117
119
  return {
118
120
  id: String(issue.number),
121
+ ...typeof issueNodeId === "string" && issueNodeId.trim() ? { issueNodeId: issueNodeId.trim() } : {},
119
122
  deps: parseDeps(body),
120
123
  status: statusFor(issue),
121
124
  title: issue.title,
@@ -164,17 +167,41 @@ ${rendered}
164
167
  ` : `${rendered}
165
168
  `;
166
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
+ }
167
186
  function isRigStickyStatusComment(body) {
168
187
  return body.includes(RIG_STATUS_COMMENT_MARKER);
169
188
  }
170
189
  function ghSpawnOptions(extraEnv, timeoutMs) {
171
- if (!extraEnv)
172
- return { encoding: "utf-8", timeout: timeoutMs };
173
- 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
+ };
174
201
  }
175
202
  function credentialEnv(token) {
176
203
  const clean = token?.trim() ?? "";
177
- return { GH_TOKEN: clean, GITHUB_TOKEN: clean };
204
+ return { GH_TOKEN: clean, GITHUB_TOKEN: clean, RIG_GITHUB_TOKEN: clean };
178
205
  }
179
206
  async function resolveCredentialEnv(opts, purpose) {
180
207
  if (!opts.credentialProvider)
@@ -189,28 +216,239 @@ async function resolveCredentialEnv(opts, purpose) {
189
216
  const resolved = await opts.credentialProvider.resolveGitHubToken(input);
190
217
  return credentialEnv(resolved.token);
191
218
  }
219
+ function tokenDiagnostic(value) {
220
+ const clean = value?.trim() ?? "";
221
+ return clean ? `present(len=${clean.length})` : "missing";
222
+ }
192
223
  function runGh(bin, args, spawn, extraEnv, timeoutMs) {
193
- const res = spawn(bin, [...args], ghSpawnOptions(extraEnv, timeoutMs));
194
- assertGhSuccess(args, res);
224
+ const options = ghSpawnOptions(extraEnv, timeoutMs);
225
+ const res = spawn(bin, [...args], options);
226
+ assertGhSuccess(args, res, options.env);
195
227
  if (!res.stdout || res.stdout.trim() === "")
196
228
  return [];
197
229
  return JSON.parse(res.stdout);
198
230
  }
199
231
  function runGhVoid(bin, args, spawn, extraEnv, timeoutMs) {
200
- const res = spawn(bin, [...args], ghSpawnOptions(extraEnv, timeoutMs));
201
- assertGhSuccess(args, res);
232
+ const options = ghSpawnOptions(extraEnv, timeoutMs);
233
+ const res = spawn(bin, [...args], options);
234
+ assertGhSuccess(args, res, options.env);
202
235
  }
203
- function assertGhSuccess(args, res) {
236
+ function assertGhSuccess(args, res, env) {
204
237
  if (res.error) {
205
238
  const msg = res.error.message ?? String(res.error);
206
239
  throw new Error(`gh CLI not available \u2014 install gh (brew install gh / apt install gh): ${msg}`);
207
240
  }
208
241
  if (res.status !== 0) {
209
- 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 };
210
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);
211
448
  }
212
449
  function statusLabelFor(status) {
213
450
  switch (status) {
451
+ case "running":
214
452
  case "in_progress":
215
453
  return "in-progress";
216
454
  case "blocked":
@@ -229,6 +467,8 @@ function statusLabelFor(status) {
229
467
  return "under-review";
230
468
  case "needs_attention":
231
469
  return "blocked";
470
+ case "closed":
471
+ case "completed":
232
472
  case "open":
233
473
  return null;
234
474
  default:
@@ -237,11 +477,13 @@ function statusLabelFor(status) {
237
477
  }
238
478
  function rigStatusLabelFor(status) {
239
479
  switch (status) {
480
+ case "running":
240
481
  case "in_progress":
241
482
  return "rig:running";
242
483
  case "under_review":
243
484
  return "rig:pr-open";
244
485
  case "closed":
486
+ case "completed":
245
487
  return "rig:done";
246
488
  case "ci_fixing":
247
489
  return "rig:ci-fixing";
@@ -259,9 +501,10 @@ function rigStatusLabelFor(status) {
259
501
  return null;
260
502
  }
261
503
  }
262
- function applyIssueStatus(bin, repo, spawnFn, id, status, extraEnv, timeoutMs) {
263
- 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);
264
506
  const targetRigLabel = rigStatusLabelFor(status);
507
+ const shouldSyncLifecycle = shouldWriteIssueUpdate(issueUpdates, status);
265
508
  for (const l of [...STATUS_LABELS, ...RIG_STATUS_LABELS]) {
266
509
  if (targetLabel !== null && l === targetLabel)
267
510
  continue;
@@ -283,7 +526,19 @@ function applyIssueStatus(bin, repo, spawnFn, id, status, extraEnv, timeoutMs) {
283
526
  runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, "--add-label", label], spawnFn, extraEnv, timeoutMs);
284
527
  }
285
528
  }
286
- 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") {
287
542
  runGhVoid(bin, ["issue", "close", String(id), "--repo", repo], spawnFn, extraEnv, timeoutMs);
288
543
  }
289
544
  }
@@ -353,11 +608,11 @@ function applyLabels(bin, repo, spawnFn, id, labels, action, extraEnv, timeoutMs
353
608
  } catch {}
354
609
  }
355
610
  }
356
- function applyIssueUpdate(bin, repo, spawnFn, id, update, extraEnv, timeoutMs) {
611
+ async function applyIssueUpdate(bin, repo, spawnFn, id, update, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs) {
357
612
  if (update.status) {
358
- applyIssueStatus(bin, repo, spawnFn, id, update.status, extraEnv, timeoutMs);
613
+ await applyIssueStatus(bin, repo, spawnFn, id, update.status, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs);
359
614
  }
360
- if (update.comment?.trim()) {
615
+ if (update.comment?.trim() && shouldWriteIssueUpdate(issueUpdates, update.status)) {
361
616
  if (isRigStickyStatusComment(update.comment)) {
362
617
  upsertRigStickyComment(bin, repo, spawnFn, String(id), update.comment, extraEnv, timeoutMs);
363
618
  } else {
@@ -383,6 +638,7 @@ function createGitHubIssuesTaskSource(opts) {
383
638
  const spawnFn = opts.spawn ?? spawnSync;
384
639
  const timeoutMs = Math.max(1000, Math.trunc(opts.timeoutMs ?? DEFAULT_GH_TIMEOUT_MS));
385
640
  const listLimit = Math.max(1, Math.trunc(opts.listLimit ?? DEFAULT_GITHUB_ISSUE_LIST_LIMIT));
641
+ const issueUpdates = issueUpdatesMode(opts.issueUpdates);
386
642
  return {
387
643
  id: "std:github-issues",
388
644
  kind: "github-issues",
@@ -401,7 +657,7 @@ function createGitHubIssuesTaskSource(opts) {
401
657
  "--limit",
402
658
  String(listLimit),
403
659
  "--json",
404
- "number,title,body,labels,state,url,assignees"
660
+ "number,title,body,labels,state,url,assignees,id"
405
661
  ];
406
662
  const env = await resolveCredentialEnv(opts, "selected-repo");
407
663
  const rawIssues = runGh(bin, args, spawnFn, env, timeoutMs);
@@ -421,7 +677,7 @@ function createGitHubIssuesTaskSource(opts) {
421
677
  "--repo",
422
678
  repo,
423
679
  "--json",
424
- "number,title,body,labels,state,url,assignees"
680
+ "number,title,body,labels,state,url,assignees,id"
425
681
  ], spawnFn, env, timeoutMs);
426
682
  return issueToTask(issue, repo);
427
683
  } catch {
@@ -430,12 +686,12 @@ function createGitHubIssuesTaskSource(opts) {
430
686
  },
431
687
  async updateStatus(id, status) {
432
688
  const env = await resolveCredentialEnv(opts, "selected-repo");
433
- applyIssueStatus(bin, repo, spawnFn, id, status, env, timeoutMs);
689
+ await applyIssueStatus(bin, repo, spawnFn, id, status, opts.projects, opts.assignee, issueUpdates, env, timeoutMs);
434
690
  notifyTaskChanged(opts.onTaskChanged, repo, id, status);
435
691
  },
436
692
  async updateTask(id, update) {
437
693
  const env = await resolveCredentialEnv(opts, "selected-repo");
438
- applyIssueUpdate(bin, repo, spawnFn, id, update, env, timeoutMs);
694
+ await applyIssueUpdate(bin, repo, spawnFn, id, update, opts.projects, opts.assignee, issueUpdates, env, timeoutMs);
439
695
  notifyTaskChanged(opts.onTaskChanged, repo, id, update.status);
440
696
  },
441
697
  async addLabels(id, labels) {
@@ -474,7 +730,7 @@ function createGitHubIssuesTaskSource(opts) {
474
730
 
475
731
  // packages/standard-plugin/src/files-source.ts
476
732
  import { readFileSync as readFileSync2, readdirSync, existsSync as existsSync2, statSync, writeFileSync } from "fs";
477
- import { join, basename } from "path";
733
+ import { join, basename, isAbsolute, resolve as resolve2 } from "path";
478
734
  var DEFAULT_PATTERN = /\.(task\.)?json$/;
479
735
  function readTaskFile(file, pattern) {
480
736
  const raw = JSON.parse(readFileSync2(file, "utf-8"));
@@ -495,10 +751,11 @@ function readTaskFile(file, pattern) {
495
751
  }
496
752
  function createFilesTaskSource(opts) {
497
753
  const pattern = opts.pattern ?? DEFAULT_PATTERN;
498
- const directory = opts.path ?? opts.dir;
499
- if (!directory) {
754
+ const configured = opts.path ?? opts.dir;
755
+ if (!configured) {
500
756
  throw new Error("createFilesTaskSource: either `path` or `dir` must be provided");
501
757
  }
758
+ const directory = isAbsolute(configured) ? configured : resolve2(opts.projectRoot ?? process.cwd(), configured);
502
759
  const findTaskFile = (id) => {
503
760
  if (!existsSync2(directory))
504
761
  return;
@@ -583,6 +840,40 @@ function requireStringField(config, field, kind) {
583
840
  }
584
841
  return value;
585
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
+ }
586
877
  function standardPlugin(opts = {}) {
587
878
  return definePlugin({
588
879
  name: "rig-standard",
@@ -607,13 +898,13 @@ function standardPlugin(opts = {}) {
607
898
  id: "std:github-issues",
608
899
  kind: "github-issues",
609
900
  description: "GitHub Issues via gh CLI",
610
- factory(config) {
901
+ factory(config, context) {
611
902
  const options = {
612
903
  owner: requireStringField(config, "owner", "github-issues"),
613
904
  repo: requireStringField(config, "repo", "github-issues")
614
905
  };
615
- if (opts.githubCredentialProvider)
616
- options.credentialProvider = opts.githubCredentialProvider;
906
+ const credentialProviderOptions = context?.projectRoot ? { stateDir: resolve3(context.projectRoot, ".rig", "state") } : {};
907
+ options.credentialProvider = opts.githubCredentialProvider ?? createStateGitHubCredentialProvider(credentialProviderOptions);
617
908
  if (opts.githubWorkspaceId)
618
909
  options.workspaceId = opts.githubWorkspaceId;
619
910
  if (opts.githubUserId)
@@ -635,6 +926,9 @@ function standardPlugin(opts = {}) {
635
926
  const listLimit = typeof config.options?.listLimit === "number" ? config.options.listLimit : undefined;
636
927
  if (listLimit !== undefined)
637
928
  options.listLimit = listLimit;
929
+ const projects = githubProjectsOptionsFromConfig(config, context);
930
+ if (projects)
931
+ options.projects = projects;
638
932
  return createGitHubIssuesTaskSource(options);
639
933
  }
640
934
  },
@@ -642,9 +936,10 @@ function standardPlugin(opts = {}) {
642
936
  id: "std:files",
643
937
  kind: "files",
644
938
  description: "JSON files in a local directory",
645
- factory(config) {
939
+ factory(config, context) {
646
940
  return createFilesTaskSource({
647
- path: requireStringField(config, "path", "files")
941
+ path: requireStringField(config, "path", "files"),
942
+ ...context?.projectRoot ? { projectRoot: context.projectRoot } : {}
648
943
  });
649
944
  }
650
945
  }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@h-rig/standard-plugin",
3
- "version": "0.0.6-alpha.1",
3
+ "version": "0.0.6-alpha.100",
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.1",
23
- "@rig/core": "npm:@h-rig/core@0.0.6-alpha.1",
24
+ "@rig/contracts": "npm:@h-rig/contracts@0.0.6-alpha.100",
25
+ "@rig/core": "npm:@h-rig/core@0.0.6-alpha.100",
24
26
  "effect": "4.0.0-beta.78"
25
27
  }
26
28
  }