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

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.
@@ -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
  }
@@ -83,17 +89,42 @@ function statusFor(issue) {
83
89
  return "cancelled";
84
90
  return "open";
85
91
  }
92
+ function parseIssueRefs(raw) {
93
+ return raw.split(",").map((s) => s.trim()).map((s) => s.replace(/^#/, "").match(/^(\d+)/)?.[1] ?? "").filter((s) => s.length > 0);
94
+ }
95
+ function parseMetadataList(body, key) {
96
+ const block = body.match(/<!-- rig:metadata:start -->\s*([\s\S]*?)\s*<!-- rig:metadata:end -->/);
97
+ if (!block)
98
+ return [];
99
+ const lines = block[1].split(/\r?\n/);
100
+ const values = [];
101
+ for (let index = 0;index < lines.length; index += 1) {
102
+ const line = lines[index];
103
+ const sameLine = line.match(new RegExp(`^${key}:\\s*(.+)$`, "i"));
104
+ if (sameLine) {
105
+ values.push(...parseIssueRefs(sameLine[1]));
106
+ continue;
107
+ }
108
+ if (!new RegExp(`^${key}:\\s*$`, "i").test(line))
109
+ continue;
110
+ for (let cursor = index + 1;cursor < lines.length; cursor += 1) {
111
+ const item = lines[cursor].match(/^\s*-\s*(.+)$/);
112
+ if (!item)
113
+ break;
114
+ values.push(...parseIssueRefs(item[1]));
115
+ }
116
+ }
117
+ return [...new Set(values)];
118
+ }
86
119
  function parseDeps(body) {
87
120
  const match = body.match(/^depends-on:\s*([^\n]+)/im);
88
- if (!match)
89
- return [];
90
- return match[1].split(",").map((s) => s.trim()).map((s) => s.replace(/^#/, "").match(/^(\d+)/)?.[1] ?? "").filter((s) => s.length > 0);
121
+ const bodyRefs = match ? parseIssueRefs(match[1]) : [];
122
+ return [...new Set([...bodyRefs, ...parseMetadataList(body, "depends-on")])];
91
123
  }
92
124
  function parseParents(body) {
93
125
  const match = body.match(/^parents?:\s*([^\n]+)/im);
94
- if (!match)
95
- return [];
96
- return match[1].split(",").map((s) => s.trim()).map((s) => s.replace(/^#/, "").match(/^(\d+)/)?.[1] ?? "").filter((s) => s.length > 0);
126
+ const bodyRefs = match ? parseIssueRefs(match[1]) : [];
127
+ return [...new Set([...bodyRefs, ...parseMetadataList(body, "parents")])];
97
128
  }
98
129
  function issueTypeFor(issue) {
99
130
  const labels = labelNamesFor(issue);
@@ -104,7 +135,7 @@ function issueTypeFor(issue) {
104
135
  return "epic";
105
136
  return "task";
106
137
  }
107
- function issueToTask(issue, repo) {
138
+ function issueToTask(issue, repo, nativeDependencies) {
108
139
  const labelNames = labelNamesFor(issue);
109
140
  const scope = labelNames.filter((l) => l.startsWith("scope:")).map((l) => l.slice("scope:".length));
110
141
  const roleLabel = labelNames.find((l) => l.startsWith("role:"));
@@ -112,10 +143,12 @@ function issueToTask(issue, repo) {
112
143
  const validators = labelNames.filter((l) => l.startsWith("validator:")).map((l) => l.slice("validator:".length));
113
144
  const body = issue.body ?? "";
114
145
  const issueNodeId = issue.id ?? issue.nodeId ?? issue.node_id;
146
+ const parsedDeps = parseDeps(body);
147
+ const deps = nativeDependencies?.deps ? [...new Set([...parsedDeps, ...nativeDependencies.deps])] : parsedDeps;
115
148
  return {
116
149
  id: String(issue.number),
117
150
  ...typeof issueNodeId === "string" && issueNodeId.trim() ? { issueNodeId: issueNodeId.trim() } : {},
118
- deps: parseDeps(body),
151
+ deps,
119
152
  status: statusFor(issue),
120
153
  title: issue.title,
121
154
  body,
@@ -127,6 +160,7 @@ function issueToTask(issue, repo) {
127
160
  sourceIssueId: `${repo}#${issue.number}`,
128
161
  parentChildDeps: parseParents(body),
129
162
  labels: labelNames,
163
+ ...nativeDependencies?.degraded ? { nativeDependenciesDegraded: true, nativeDependenciesError: nativeDependencies.degraded } : {},
130
164
  raw: issue
131
165
  };
132
166
  }
@@ -183,13 +217,21 @@ function isRigStickyStatusComment(body) {
183
217
  return body.includes(RIG_STATUS_COMMENT_MARKER);
184
218
  }
185
219
  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 } };
220
+ return {
221
+ encoding: "utf-8",
222
+ timeout: timeoutMs,
223
+ env: {
224
+ ...process.env,
225
+ ...process.env.GH_TOKEN !== undefined ? { GH_TOKEN: process.env.GH_TOKEN } : {},
226
+ ...process.env.GITHUB_TOKEN !== undefined ? { GITHUB_TOKEN: process.env.GITHUB_TOKEN } : {},
227
+ ...process.env.RIG_GITHUB_TOKEN !== undefined ? { RIG_GITHUB_TOKEN: process.env.RIG_GITHUB_TOKEN } : {},
228
+ ...extraEnv ?? {}
229
+ }
230
+ };
189
231
  }
190
232
  function credentialEnv(token) {
191
233
  const clean = token?.trim() ?? "";
192
- return { GH_TOKEN: clean, GITHUB_TOKEN: clean };
234
+ return { GH_TOKEN: clean, GITHUB_TOKEN: clean, RIG_GITHUB_TOKEN: clean };
193
235
  }
194
236
  async function resolveCredentialEnv(opts, purpose) {
195
237
  if (!opts.credentialProvider)
@@ -204,28 +246,319 @@ async function resolveCredentialEnv(opts, purpose) {
204
246
  const resolved = await opts.credentialProvider.resolveGitHubToken(input);
205
247
  return credentialEnv(resolved.token);
206
248
  }
249
+ function tokenDiagnostic(value) {
250
+ const clean = value?.trim() ?? "";
251
+ return clean ? `present(len=${clean.length})` : "missing";
252
+ }
207
253
  function runGh(bin, args, spawn, extraEnv, timeoutMs) {
208
- const res = spawn(bin, [...args], ghSpawnOptions(extraEnv, timeoutMs));
209
- assertGhSuccess(args, res);
254
+ const options = ghSpawnOptions(extraEnv, timeoutMs);
255
+ const res = spawn(bin, [...args], options);
256
+ assertGhSuccess(args, res, options.env);
210
257
  if (!res.stdout || res.stdout.trim() === "")
211
258
  return [];
212
259
  return JSON.parse(res.stdout);
213
260
  }
214
261
  function runGhVoid(bin, args, spawn, extraEnv, timeoutMs) {
215
- const res = spawn(bin, [...args], ghSpawnOptions(extraEnv, timeoutMs));
216
- assertGhSuccess(args, res);
262
+ const options = ghSpawnOptions(extraEnv, timeoutMs);
263
+ const res = spawn(bin, [...args], options);
264
+ assertGhSuccess(args, res, options.env);
217
265
  }
218
- function assertGhSuccess(args, res) {
266
+ function assertGhSuccess(args, res, env) {
219
267
  if (res.error) {
220
268
  const msg = res.error.message ?? String(res.error);
221
269
  throw new Error(`gh CLI not available \u2014 install gh (brew install gh / apt install gh): ${msg}`);
222
270
  }
223
271
  if (res.status !== 0) {
224
- throw new Error(`gh ${args.join(" ")} failed (exit ${res.status}): ${res.stderr}`);
272
+ throw new Error(`gh ${args.join(" ")} failed (exit ${res.status}): ${res.stderr}
273
+ [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)}`);
225
274
  }
226
275
  }
276
+ var DEFAULT_PROJECT_STATUSES = {
277
+ todo: "Todo",
278
+ running: "In Progress",
279
+ prOpen: "In Review",
280
+ ciFixing: "In Review",
281
+ merging: "In Review",
282
+ done: "Done",
283
+ needsAttention: "Needs Attention"
284
+ };
285
+ function asProjectRecord(value) {
286
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
287
+ }
288
+ function projectString(value) {
289
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
290
+ }
291
+ function projectLifecycleStatusForTaskStatus(status) {
292
+ const normalized = status?.trim().toLowerCase().replace(/[-\s]+/g, "_");
293
+ switch (normalized) {
294
+ case "draft":
295
+ case "open":
296
+ case "queued":
297
+ case "ready":
298
+ return "todo";
299
+ case "running":
300
+ case "in_progress":
301
+ return "running";
302
+ case "under_review":
303
+ case "review":
304
+ case "pr_open":
305
+ return "prOpen";
306
+ case "ci_fixing":
307
+ case "fixing":
308
+ return "ciFixing";
309
+ case "merging":
310
+ case "merge":
311
+ return "merging";
312
+ case "closed":
313
+ case "completed":
314
+ case "done":
315
+ return "done";
316
+ case "blocked":
317
+ case "cancelled":
318
+ case "failed":
319
+ case "needs_attention":
320
+ return "needsAttention";
321
+ default:
322
+ return null;
323
+ }
324
+ }
325
+ function ghGraphQLFetch(bin, spawnFn, extraEnv, timeoutMs) {
326
+ return async (query, variables) => {
327
+ const args = ["api", "graphql", "-f", `query=${query}`];
328
+ for (const [key, value] of Object.entries(variables)) {
329
+ if (value === undefined || value === null)
330
+ continue;
331
+ args.push("-f", `${key}=${String(value)}`);
332
+ }
333
+ const response = runGh(bin, args, spawnFn, extraEnv, timeoutMs);
334
+ return asProjectRecord(response)?.data ?? response;
335
+ };
336
+ }
337
+ function issueNodeIdFor(issue) {
338
+ const id = issue.id ?? issue.nodeId ?? issue.node_id;
339
+ return typeof id === "string" && id.trim().length > 0 ? id.trim() : null;
340
+ }
341
+ function nativeIssueDependencyRef(value, currentRepo) {
342
+ const record = asProjectRecord(value);
343
+ const number = typeof record?.number === "number" ? String(record.number) : projectString(record?.number);
344
+ if (!number)
345
+ return null;
346
+ const repository = asProjectRecord(record?.repository);
347
+ const owner = projectString(asProjectRecord(repository?.owner)?.login);
348
+ const name = projectString(repository?.name);
349
+ if (!owner || !name || `${owner}/${name}` === currentRepo)
350
+ return number;
351
+ return `${owner}/${name}#${number}`;
352
+ }
353
+ function nativeDependencyRefsFrom(data, currentRepo) {
354
+ const issue = asProjectRecord(asProjectRecord(data)?.node);
355
+ const blockedBy = asProjectRecord(issue?.blockedBy);
356
+ const nodes = Array.isArray(blockedBy?.nodes) ? blockedBy.nodes : [];
357
+ return [...new Set(nodes.flatMap((node) => {
358
+ const ref = nativeIssueDependencyRef(node, currentRepo);
359
+ return ref ? [ref] : [];
360
+ }))];
361
+ }
362
+ async function readNativeDependenciesForIssue(input) {
363
+ const issueId = issueNodeIdFor(input.issue);
364
+ if (!issueId)
365
+ return { deps: [], degraded: "GitHub issue node id is unavailable." };
366
+ const query = `
367
+ query RigIssueNativeDependencies($issueId: ID!) {
368
+ node(id: $issueId) {
369
+ ... on Issue {
370
+ blockedBy(first: 100) {
371
+ nodes {
372
+ number
373
+ repository { name owner { login } }
374
+ }
375
+ }
376
+ }
377
+ }
378
+ }
379
+ `;
380
+ try {
381
+ return {
382
+ deps: nativeDependencyRefsFrom(await input.fetchGraphQL(query, { issueId }, "gh-cli"), input.repo)
383
+ };
384
+ } catch (error) {
385
+ const detail = error instanceof Error ? error.message : String(error);
386
+ return { deps: [], degraded: detail };
387
+ }
388
+ }
389
+ function formatIssueReference(ref) {
390
+ const clean = ref.trim().replace(/^#/, "");
391
+ return /^\d+$/.test(clean) ? `#${clean}` : clean;
392
+ }
393
+ function appendReferenceLines(body, deps, parents) {
394
+ const lines = [];
395
+ const cleanDeps = (deps ?? []).map(formatIssueReference).filter((ref) => ref.length > 0);
396
+ const cleanParents = (parents ?? []).map(formatIssueReference).filter((ref) => ref.length > 0);
397
+ if (cleanDeps.length > 0)
398
+ lines.push(`depends-on: ${cleanDeps.join(", ")}`);
399
+ if (cleanParents.length > 0)
400
+ lines.push(`parents: ${cleanParents.join(", ")}`);
401
+ if (lines.length === 0)
402
+ return body;
403
+ return body.trim().length > 0 ? `${body.trimEnd()}
404
+
405
+ ${lines.join(`
406
+ `)}` : lines.join(`
407
+ `);
408
+ }
409
+ function bodyForCreatedTask(input) {
410
+ const metadata = { ...input.metadata ?? {} };
411
+ if (input.deps && input.deps.length > 0)
412
+ metadata["depends-on"] = input.deps.map(formatIssueReference);
413
+ if (input.parents && input.parents.length > 0)
414
+ metadata.parents = input.parents.map(formatIssueReference);
415
+ return updateRigOwnedMetadataBlock(appendReferenceLines(input.body, input.deps, input.parents), metadata);
416
+ }
417
+ function projectStatusFieldFrom(data, projectId) {
418
+ const fields = asProjectRecord(asProjectRecord(asProjectRecord(data)?.node)?.fields)?.nodes;
419
+ for (const node of Array.isArray(fields) ? fields : []) {
420
+ const record = asProjectRecord(node);
421
+ if (projectString(record?.name)?.toLowerCase() !== "status")
422
+ continue;
423
+ const id = projectString(record?.id);
424
+ if (!id)
425
+ continue;
426
+ const options = Array.isArray(record?.options) ? record.options.flatMap((option) => {
427
+ const optionRecord = asProjectRecord(option);
428
+ const optionId = projectString(optionRecord?.id);
429
+ const name = projectString(optionRecord?.name);
430
+ return optionId && name ? [{ id: optionId, name }] : [];
431
+ }) : [];
432
+ return { id, name: "Status", options };
433
+ }
434
+ throw new Error(`GitHub Project ${projectId} does not expose a Status single-select field.`);
435
+ }
436
+ async function resolveProjectStatusField(input) {
437
+ const query = `
438
+ query RigProjectStatusField($projectId: ID!) {
439
+ node(id: $projectId) {
440
+ ... on ProjectV2 {
441
+ fields(first: 50) {
442
+ nodes {
443
+ ... on ProjectV2FieldCommon { id name }
444
+ ... on ProjectV2SingleSelectField { id name options { id name } }
445
+ }
446
+ }
447
+ }
448
+ }
449
+ }
450
+ `;
451
+ return projectStatusFieldFrom(await input.fetchGraphQL(query, { projectId: input.projectId }, input.token), input.projectId);
452
+ }
453
+ async function ensureIssueProjectItem(input) {
454
+ const query = `
455
+ query RigFindProjectIssueItem($projectId: ID!) {
456
+ node(id: $projectId) {
457
+ ... on ProjectV2 {
458
+ items(first: 100) { nodes { id content { ... on Issue { id } } } }
459
+ }
460
+ }
461
+ }
462
+ `;
463
+ const data = await input.fetchGraphQL(query, { projectId: input.projectId }, input.token);
464
+ const nodes = asProjectRecord(asProjectRecord(asProjectRecord(data)?.node)?.items)?.nodes;
465
+ for (const node of Array.isArray(nodes) ? nodes : []) {
466
+ const record = asProjectRecord(node);
467
+ const content = asProjectRecord(record?.content);
468
+ if (projectString(content?.id) === input.issueNodeId) {
469
+ const id2 = projectString(record?.id);
470
+ if (id2)
471
+ return { id: id2, created: false };
472
+ }
473
+ }
474
+ const mutation = `
475
+ mutation RigAddIssueToProject($projectId: ID!, $contentId: ID!) {
476
+ addProjectV2ItemById(input: { projectId: $projectId, contentId: $contentId }) { item { id } }
477
+ }
478
+ `;
479
+ const created = await input.fetchGraphQL(mutation, { projectId: input.projectId, contentId: input.issueNodeId }, input.token);
480
+ const addResult = asProjectRecord(asProjectRecord(created)?.addProjectV2ItemById);
481
+ const id = projectString(asProjectRecord(addResult?.item)?.id);
482
+ if (!id)
483
+ throw new Error("GitHub Project item creation did not return an item id.");
484
+ return { id, created: true };
485
+ }
486
+ async function updateIssueProjectStatus(input) {
487
+ const mutation = `
488
+ mutation RigUpdateProjectStatus($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
489
+ updateProjectV2ItemFieldValue(input: {
490
+ projectId: $projectId,
491
+ itemId: $itemId,
492
+ fieldId: $fieldId,
493
+ value: { singleSelectOptionId: $optionId }
494
+ }) { projectV2Item { id } }
495
+ }
496
+ `;
497
+ await input.fetchGraphQL(mutation, {
498
+ projectId: input.projectId,
499
+ itemId: input.itemId,
500
+ fieldId: input.fieldId,
501
+ optionId: input.optionId
502
+ }, input.token);
503
+ }
504
+ function fetchIssueNodeId(bin, repo, spawnFn, id, extraEnv, timeoutMs) {
505
+ const issue = runGh(bin, ["issue", "view", String(id), "--repo", repo, "--json", "id"], spawnFn, extraEnv, timeoutMs);
506
+ return projectString(issue.id) ?? projectString(issue.nodeId) ?? projectString(issue.node_id);
507
+ }
508
+ async function syncGitHubProjectStatus(bin, repo, spawnFn, id, status, projects, extraEnv, timeoutMs) {
509
+ if (!projects?.enabled)
510
+ return;
511
+ const projectId = projectString(projects.projectId);
512
+ if (!projectId)
513
+ throw new Error("GitHub Projects status sync is enabled but projectId is missing.");
514
+ const lifecycleStatus = projectLifecycleStatusForTaskStatus(status);
515
+ if (!lifecycleStatus)
516
+ return;
517
+ const issueNodeId = fetchIssueNodeId(bin, repo, spawnFn, id, extraEnv, timeoutMs);
518
+ if (!issueNodeId)
519
+ throw new Error(`GitHub issue ${repo}#${id} did not expose a node id for Projects status sync.`);
520
+ const projectStatus = projectString(projects.statuses?.[lifecycleStatus]) ?? DEFAULT_PROJECT_STATUSES[lifecycleStatus];
521
+ const fetchGraphQL = ghGraphQLFetch(bin, spawnFn, extraEnv, timeoutMs);
522
+ const field = await resolveProjectStatusField({ projectId, token: "gh-cli", fetchGraphQL });
523
+ const option = field.options.find((candidate) => candidate.name.toLowerCase() === projectStatus.toLowerCase() || candidate.id === projectStatus);
524
+ if (!option)
525
+ throw new Error(`GitHub Project ${projectId} Status field does not contain option "${projectStatus}".`);
526
+ const item = await ensureIssueProjectItem({ projectId, issueNodeId, token: "gh-cli", fetchGraphQL });
527
+ await updateIssueProjectStatus({
528
+ projectId,
529
+ itemId: item.id,
530
+ fieldId: projectString(projects.statusFieldId) ?? field.id,
531
+ optionId: option.id,
532
+ token: "gh-cli",
533
+ fetchGraphQL
534
+ });
535
+ }
536
+ var TERMINAL_TASK_STATUSES = new Set(["closed", "completed", "merged", "cancelled", "resolved", "done"]);
537
+ function normalizeTaskStatusToken(status) {
538
+ return status?.trim().toLowerCase().replace(/[-\s]+/g, "_") ?? "";
539
+ }
540
+ function issueUpdatesMode(value) {
541
+ return value === "off" || value === "minimal" || value === "lifecycle" ? value : "lifecycle";
542
+ }
543
+ function isTerminalTaskStatus(status) {
544
+ return TERMINAL_TASK_STATUSES.has(normalizeTaskStatusToken(status));
545
+ }
546
+ function shouldWriteIssueUpdate(mode, status) {
547
+ if (mode === "off")
548
+ return false;
549
+ if (mode === "lifecycle")
550
+ return true;
551
+ return isTerminalTaskStatus(status);
552
+ }
553
+ function isRunningStatus(status) {
554
+ return normalizeTaskStatusToken(status) === "running";
555
+ }
556
+ function assignRunningIssue(bin, repo, spawnFn, id, assignee, extraEnv, timeoutMs) {
557
+ runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, "--add-assignee", assignee?.trim() || "@me"], spawnFn, extraEnv, timeoutMs);
558
+ }
227
559
  function statusLabelFor(status) {
228
560
  switch (status) {
561
+ case "running":
229
562
  case "in_progress":
230
563
  return "in-progress";
231
564
  case "blocked":
@@ -244,6 +577,8 @@ function statusLabelFor(status) {
244
577
  return "under-review";
245
578
  case "needs_attention":
246
579
  return "blocked";
580
+ case "closed":
581
+ case "completed":
247
582
  case "open":
248
583
  return null;
249
584
  default:
@@ -252,11 +587,13 @@ function statusLabelFor(status) {
252
587
  }
253
588
  function rigStatusLabelFor(status) {
254
589
  switch (status) {
590
+ case "running":
255
591
  case "in_progress":
256
592
  return "rig:running";
257
593
  case "under_review":
258
594
  return "rig:pr-open";
259
595
  case "closed":
596
+ case "completed":
260
597
  return "rig:done";
261
598
  case "ci_fixing":
262
599
  return "rig:ci-fixing";
@@ -274,9 +611,10 @@ function rigStatusLabelFor(status) {
274
611
  return null;
275
612
  }
276
613
  }
277
- function applyIssueStatus(bin, repo, spawnFn, id, status, extraEnv, timeoutMs) {
278
- const targetLabel = status === "closed" ? null : statusLabelFor(status);
614
+ async function applyIssueStatus(bin, repo, spawnFn, id, status, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs) {
615
+ const targetLabel = status === "closed" || status === "completed" ? null : statusLabelFor(status);
279
616
  const targetRigLabel = rigStatusLabelFor(status);
617
+ const shouldSyncLifecycle = shouldWriteIssueUpdate(issueUpdates, status);
280
618
  for (const l of [...STATUS_LABELS, ...RIG_STATUS_LABELS]) {
281
619
  if (targetLabel !== null && l === targetLabel)
282
620
  continue;
@@ -298,7 +636,19 @@ function applyIssueStatus(bin, repo, spawnFn, id, status, extraEnv, timeoutMs) {
298
636
  runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, "--add-label", label], spawnFn, extraEnv, timeoutMs);
299
637
  }
300
638
  }
301
- if (status === "closed") {
639
+ if (isRunningStatus(status)) {
640
+ assignRunningIssue(bin, repo, spawnFn, id, runningAssignee, extraEnv, timeoutMs);
641
+ if (shouldSyncLifecycle) {
642
+ upsertRigStickyComment(bin, repo, spawnFn, String(id), buildRigStickyStatusComment({
643
+ status: "running",
644
+ summary: "Rig run started."
645
+ }), extraEnv, timeoutMs);
646
+ }
647
+ }
648
+ if (shouldSyncLifecycle) {
649
+ await syncGitHubProjectStatus(bin, repo, spawnFn, id, status, projects, extraEnv, timeoutMs);
650
+ }
651
+ if (status === "closed" || status === "completed") {
302
652
  runGhVoid(bin, ["issue", "close", String(id), "--repo", repo], spawnFn, extraEnv, timeoutMs);
303
653
  }
304
654
  }
@@ -368,11 +718,11 @@ function applyLabels(bin, repo, spawnFn, id, labels, action, extraEnv, timeoutMs
368
718
  } catch {}
369
719
  }
370
720
  }
371
- function applyIssueUpdate(bin, repo, spawnFn, id, update, extraEnv, timeoutMs) {
721
+ async function applyIssueUpdate(bin, repo, spawnFn, id, update, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs) {
372
722
  if (update.status) {
373
- applyIssueStatus(bin, repo, spawnFn, id, update.status, extraEnv, timeoutMs);
723
+ await applyIssueStatus(bin, repo, spawnFn, id, update.status, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs);
374
724
  }
375
- if (update.comment?.trim()) {
725
+ if (update.comment?.trim() && shouldWriteIssueUpdate(issueUpdates, update.status)) {
376
726
  if (isRigStickyStatusComment(update.comment)) {
377
727
  upsertRigStickyComment(bin, repo, spawnFn, String(id), update.comment, extraEnv, timeoutMs);
378
728
  } else {
@@ -398,6 +748,17 @@ function createGitHubIssuesTaskSource(opts) {
398
748
  const spawnFn = opts.spawn ?? spawnSync;
399
749
  const timeoutMs = Math.max(1000, Math.trunc(opts.timeoutMs ?? DEFAULT_GH_TIMEOUT_MS));
400
750
  const listLimit = Math.max(1, Math.trunc(opts.listLimit ?? DEFAULT_GITHUB_ISSUE_LIST_LIMIT));
751
+ const issueUpdates = issueUpdatesMode(opts.issueUpdates);
752
+ async function issueToTaskWithOptionalNativeDependencies(issue, env) {
753
+ if (!opts.useNativeDependencies)
754
+ return issueToTask(issue, repo);
755
+ const nativeDependencies = await readNativeDependenciesForIssue({
756
+ issue,
757
+ repo,
758
+ fetchGraphQL: ghGraphQLFetch(bin, spawnFn, env, timeoutMs)
759
+ });
760
+ return issueToTask(issue, repo, nativeDependencies);
761
+ }
401
762
  return {
402
763
  id: "std:github-issues",
403
764
  kind: "github-issues",
@@ -424,12 +785,13 @@ function createGitHubIssuesTaskSource(opts) {
424
785
  throw new Error(`GitHub issue list for ${repo} reached the configured limit (${listLimit}); refusing to silently truncate matching issues. Increase taskSource.options.listLimit or narrow labels/state/assignee.`);
425
786
  }
426
787
  const issues = rawIssues.filter((issue) => !issue.pull_request);
427
- return issues.map((i) => issueToTask(i, repo));
788
+ return Promise.all(issues.map((issue) => issueToTaskWithOptionalNativeDependencies(issue, env)));
428
789
  },
429
790
  async get(id) {
791
+ const env = await resolveCredentialEnv(opts, "selected-repo");
792
+ let issue;
430
793
  try {
431
- const env = await resolveCredentialEnv(opts, "selected-repo");
432
- const issue = runGh(bin, [
794
+ issue = runGh(bin, [
433
795
  "issue",
434
796
  "view",
435
797
  String(id),
@@ -438,19 +800,23 @@ function createGitHubIssuesTaskSource(opts) {
438
800
  "--json",
439
801
  "number,title,body,labels,state,url,assignees,id"
440
802
  ], spawnFn, env, timeoutMs);
441
- return issueToTask(issue, repo);
442
- } catch {
443
- return;
803
+ } catch (error) {
804
+ const detail = error instanceof Error ? error.message : String(error);
805
+ if (/could not resolve to (an? )?(issue|pullrequest)|no issues? (found|matched)|404 not found|gh: not found|gh issue view\b[\s\S]*failed \(exit \d+\): not found\b/i.test(detail)) {
806
+ return;
807
+ }
808
+ throw new Error(`Failed to read task ${id} from GitHub repo ${repo}: ${detail}`);
444
809
  }
810
+ return issueToTaskWithOptionalNativeDependencies(issue, env);
445
811
  },
446
812
  async updateStatus(id, status) {
447
813
  const env = await resolveCredentialEnv(opts, "selected-repo");
448
- applyIssueStatus(bin, repo, spawnFn, id, status, env, timeoutMs);
814
+ await applyIssueStatus(bin, repo, spawnFn, id, status, opts.projects, opts.assignee, issueUpdates, env, timeoutMs);
449
815
  notifyTaskChanged(opts.onTaskChanged, repo, id, status);
450
816
  },
451
817
  async updateTask(id, update) {
452
818
  const env = await resolveCredentialEnv(opts, "selected-repo");
453
- applyIssueUpdate(bin, repo, spawnFn, id, update, env, timeoutMs);
819
+ await applyIssueUpdate(bin, repo, spawnFn, id, update, opts.projects, opts.assignee, issueUpdates, env, timeoutMs);
454
820
  notifyTaskChanged(opts.onTaskChanged, repo, id, update.status);
455
821
  },
456
822
  async addLabels(id, labels) {
@@ -465,6 +831,7 @@ function createGitHubIssuesTaskSource(opts) {
465
831
  },
466
832
  async createIssue(input) {
467
833
  const env = await resolveCredentialEnv(opts, "selected-repo");
834
+ const body = input.body ?? "";
468
835
  const args = [
469
836
  "api",
470
837
  "-X",
@@ -473,12 +840,31 @@ function createGitHubIssuesTaskSource(opts) {
473
840
  "-f",
474
841
  `title=${input.title}`,
475
842
  "-f",
476
- `body=${input.body ?? ""}`,
843
+ `body=${body}`,
477
844
  ...(input.labels ?? []).flatMap((label) => ["-f", `labels[]=${label}`])
478
845
  ];
479
846
  const issue = runGh(bin, args, spawnFn, env, timeoutMs);
480
847
  notifyTaskChanged(opts.onTaskChanged, repo, String(issue.number));
481
- return issueToTask(issue, repo);
848
+ return issueToTask({ ...issue, body: issue.body ?? body }, repo);
849
+ },
850
+ async create(input) {
851
+ const env = await resolveCredentialEnv(opts, "selected-repo");
852
+ const body = bodyForCreatedTask(input);
853
+ const args = [
854
+ "api",
855
+ "-X",
856
+ "POST",
857
+ `repos/${repo}/issues`,
858
+ "-f",
859
+ `title=${input.title}`,
860
+ "-f",
861
+ `body=${body}`,
862
+ "-f",
863
+ "labels[]=rig:generated"
864
+ ];
865
+ const issue = runGh(bin, args, spawnFn, env, timeoutMs);
866
+ notifyTaskChanged(opts.onTaskChanged, repo, String(issue.number));
867
+ return issueToTask({ ...issue, body: issue.body ?? body }, repo);
482
868
  },
483
869
  async getIssueBody(id) {
484
870
  const env = await resolveCredentialEnv(opts, "selected-repo");
@@ -0,0 +1,3 @@
1
+ export * from "./plugin";
2
+ export { default } from "./plugin";
3
+ export { standardProjectPlugins, type StandardProjectPluginsOptions } from "./bundle";