@h-rig/standard-plugin 0.0.6-alpha.15 → 0.0.6-alpha.151

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.
Files changed (45) hide show
  1. package/dist/src/blocker-classifier.d.ts +1 -0
  2. package/dist/src/blocker-classifier.js +18 -0
  3. package/dist/src/bundle.d.ts +7 -0
  4. package/dist/src/bundle.js +1859 -0
  5. package/dist/src/cli-surface.d.ts +1 -0
  6. package/dist/src/cli-surface.js +12 -0
  7. package/dist/src/default-lifecycle.d.ts +2 -0
  8. package/dist/src/default-lifecycle.js +12 -0
  9. package/dist/src/dependency-graph.d.ts +1 -0
  10. package/dist/src/dependency-graph.js +22 -0
  11. package/dist/src/drift/__fixtures__/temp-repo.d.ts +9 -0
  12. package/dist/src/drift/__fixtures__/temp-repo.js +41 -0
  13. package/dist/src/drift/detect.d.ts +11 -0
  14. package/dist/src/drift/detect.js +299 -0
  15. package/dist/src/drift/extract-refs.d.ts +7 -0
  16. package/dist/src/drift/extract-refs.js +60 -0
  17. package/dist/src/drift/git-adapter.d.ts +7 -0
  18. package/dist/src/drift/git-adapter.js +63 -0
  19. package/dist/src/drift/judge.d.ts +19 -0
  20. package/dist/src/drift/judge.js +16 -0
  21. package/dist/src/drift/metadata.d.ts +13 -0
  22. package/dist/src/drift/metadata.js +33 -0
  23. package/dist/src/drift/plugin.d.ts +53 -0
  24. package/dist/src/drift/plugin.js +507 -0
  25. package/dist/src/files-source.d.ts +18 -0
  26. package/dist/src/files-source.js +4 -3
  27. package/dist/src/github-issues-source.d.ts +80 -0
  28. package/dist/src/github-issues-source.js +482 -53
  29. package/dist/src/index.d.ts +13 -0
  30. package/dist/src/index.js +1369 -68
  31. package/dist/src/lifecycle-closeout.d.ts +2 -0
  32. package/dist/src/lifecycle-closeout.js +6 -0
  33. package/dist/src/planning.d.ts +1 -0
  34. package/dist/src/planning.js +14 -0
  35. package/dist/src/plugin.d.ts +24 -0
  36. package/dist/src/plugin.js +1814 -0
  37. package/dist/src/product-plugin.d.ts +3 -0
  38. package/dist/src/product-plugin.js +18 -0
  39. package/dist/src/run-worker-panels.d.ts +15 -0
  40. package/dist/src/run-worker-panels.js +53 -0
  41. package/dist/src/supervisor.d.ts +1 -0
  42. package/dist/src/supervisor.js +12 -0
  43. package/dist/src/task-cli.d.ts +1 -0
  44. package/dist/src/task-cli.js +14 -0
  45. package/package.json +67 -5
@@ -13,7 +13,7 @@ function createEnvGitHubCredentialProvider() {
13
13
  if (input.purpose === "selected-repo") {
14
14
  return { token: cleanToken(process.env.RIG_GITHUB_SELECTED_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,60 @@ function createEnvGitHubCredentialProvider() {
22
22
  };
23
23
  }
24
24
  function createStateGitHubCredentialProvider(options = {}) {
25
- const resolveStateFile = () => {
26
- const explicitFile = options.stateFile ?? process.env.RIG_GITHUB_AUTH_STATE_FILE;
27
- 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;
25
+ const addCandidate = (candidates, path) => {
26
+ const trimmed = path?.trim();
27
+ if (!trimmed)
28
+ return;
29
+ const resolved = resolve(trimmed);
30
+ if (!candidates.includes(resolved))
31
+ candidates.push(resolved);
32
+ };
33
+ const addStateDir = (candidates, dir) => {
34
+ const trimmed = dir?.trim();
35
+ if (!trimmed)
36
+ return;
37
+ addCandidate(candidates, resolve(trimmed, "github-auth.json"));
38
+ };
39
+ const addProjectStateDir = (candidates, root) => {
40
+ const trimmed = root?.trim();
41
+ if (!trimmed)
42
+ return;
43
+ addStateDir(candidates, resolve(trimmed, ".rig", "state"));
44
+ };
45
+ const stateFileCandidates = () => {
46
+ const candidates = [];
47
+ addCandidate(candidates, options.stateFile ?? process.env.RIG_GITHUB_AUTH_STATE_FILE);
48
+ addStateDir(candidates, options.stateDir);
49
+ addStateDir(candidates, process.env.RIG_STATE_DIR);
50
+ addProjectStateDir(candidates, process.env.PROJECT_RIG_ROOT);
51
+ addProjectStateDir(candidates, process.env.RIG_PROJECT_ROOT);
52
+ addProjectStateDir(candidates, process.env.RIG_HOST_PROJECT_ROOT);
53
+ addProjectStateDir(candidates, process.cwd());
54
+ return candidates;
31
55
  };
32
56
  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;
57
+ for (const stateFile of stateFileCandidates()) {
58
+ if (!existsSync(stateFile))
59
+ continue;
60
+ try {
61
+ const parsed = JSON.parse(readFileSync(stateFile, "utf8"));
62
+ const token = typeof parsed.token === "string" ? cleanToken(parsed.token) : null;
63
+ if (token)
64
+ return token;
65
+ } catch {}
41
66
  }
67
+ return null;
42
68
  };
43
69
  return {
44
70
  async resolveGitHubToken(input) {
45
71
  const token = readToken();
46
72
  if (input.purpose === "selected-repo") {
47
- return { token: token ?? "", source: "signed-in-user" };
73
+ return { token: token ?? cleanToken(process.env.RIG_GITHUB_SELECTED_TOKEN ?? null) ?? "", source: "signed-in-user" };
48
74
  }
49
75
  if (token) {
50
76
  return { token, source: "signed-in-user" };
51
77
  }
52
- const fallback = cleanToken(process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? null);
78
+ const fallback = cleanToken(process.env.RIG_GITHUB_TOKEN ?? process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN ?? null);
53
79
  if (!fallback) {
54
80
  throw new Error("No signed-in GitHub token is stored for Rig and no host admin fallback token is configured.");
55
81
  }
@@ -83,17 +109,57 @@ function statusFor(issue) {
83
109
  return "cancelled";
84
110
  return "open";
85
111
  }
86
- function parseDeps(body) {
87
- const match = body.match(/^depends-on:\s*([^\n]+)/im);
88
- if (!match)
112
+ function parseIssueRefs(raw) {
113
+ const refs = [...raw.matchAll(/(?:^|[^\w/.-])(?:[\w.-]+\/[\w.-]+#|#)?(\d+)\b/g)].map((match) => match[1]).filter((value) => Boolean(value));
114
+ return [...new Set(refs)];
115
+ }
116
+ function metadataKeyPattern(keys) {
117
+ return new RegExp(`^(?:${keys.map((key) => key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")}):\\s*(.*)$`, "i");
118
+ }
119
+ function parseMetadataList(body, keys) {
120
+ const block = body.match(/<!-- rig:metadata:start -->\s*([\s\S]*?)\s*<!-- rig:metadata:end -->/);
121
+ if (!block)
89
122
  return [];
90
- return match[1].split(",").map((s) => s.trim()).map((s) => s.replace(/^#/, "").match(/^(\d+)/)?.[1] ?? "").filter((s) => s.length > 0);
123
+ const lines = block[1].split(/\r?\n/);
124
+ const values = [];
125
+ const keyPattern = metadataKeyPattern(keys);
126
+ for (let index = 0;index < lines.length; index += 1) {
127
+ const line = lines[index];
128
+ const sameLine = line.match(keyPattern);
129
+ if (!sameLine)
130
+ continue;
131
+ const inlineValue = sameLine[1]?.trim() ?? "";
132
+ if (inlineValue) {
133
+ values.push(...parseIssueRefs(inlineValue));
134
+ continue;
135
+ }
136
+ for (let cursor = index + 1;cursor < lines.length; cursor += 1) {
137
+ const item = lines[cursor].match(/^\s*-\s*(.+)$/);
138
+ if (!item)
139
+ break;
140
+ values.push(...parseIssueRefs(item[1]));
141
+ }
142
+ }
143
+ return [...new Set(values)];
144
+ }
145
+ function bodyWithoutRigMetadataBlock(body) {
146
+ return body.replace(/<!-- rig:metadata:start -->\s*[\s\S]*?\s*<!-- rig:metadata:end -->/g, "");
147
+ }
148
+ function parseBodyKeyRefs(body, keys) {
149
+ const keyPattern = metadataKeyPattern(keys);
150
+ const values = bodyWithoutRigMetadataBlock(body).split(/\r?\n/).flatMap((line) => {
151
+ const match = line.match(keyPattern);
152
+ return match?.[1] ? parseIssueRefs(match[1]) : [];
153
+ });
154
+ return [...new Set(values)];
155
+ }
156
+ function parseDeps(body) {
157
+ const keys = ["depends-on", "deps", "blocked-by", "blocked_by"];
158
+ return [...new Set([...parseBodyKeyRefs(body, keys), ...parseMetadataList(body, keys)])];
91
159
  }
92
160
  function parseParents(body) {
93
- 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);
161
+ const keys = ["parents", "parent"];
162
+ return [...new Set([...parseBodyKeyRefs(body, keys), ...parseMetadataList(body, keys)])];
97
163
  }
98
164
  function issueTypeFor(issue) {
99
165
  const labels = labelNamesFor(issue);
@@ -104,7 +170,7 @@ function issueTypeFor(issue) {
104
170
  return "epic";
105
171
  return "task";
106
172
  }
107
- function issueToTask(issue, repo) {
173
+ function issueToTask(issue, repo, nativeDependencies) {
108
174
  const labelNames = labelNamesFor(issue);
109
175
  const scope = labelNames.filter((l) => l.startsWith("scope:")).map((l) => l.slice("scope:".length));
110
176
  const roleLabel = labelNames.find((l) => l.startsWith("role:"));
@@ -112,10 +178,12 @@ function issueToTask(issue, repo) {
112
178
  const validators = labelNames.filter((l) => l.startsWith("validator:")).map((l) => l.slice("validator:".length));
113
179
  const body = issue.body ?? "";
114
180
  const issueNodeId = issue.id ?? issue.nodeId ?? issue.node_id;
181
+ const parsedDeps = parseDeps(body);
182
+ const deps = nativeDependencies?.deps ? [...new Set([...parsedDeps, ...nativeDependencies.deps])] : parsedDeps;
115
183
  return {
116
184
  id: String(issue.number),
117
185
  ...typeof issueNodeId === "string" && issueNodeId.trim() ? { issueNodeId: issueNodeId.trim() } : {},
118
- deps: parseDeps(body),
186
+ deps,
119
187
  status: statusFor(issue),
120
188
  title: issue.title,
121
189
  body,
@@ -127,6 +195,7 @@ function issueToTask(issue, repo) {
127
195
  sourceIssueId: `${repo}#${issue.number}`,
128
196
  parentChildDeps: parseParents(body),
129
197
  labels: labelNames,
198
+ ...nativeDependencies?.degraded ? { nativeDependenciesDegraded: true, nativeDependenciesError: nativeDependencies.degraded } : {},
130
199
  raw: issue
131
200
  };
132
201
  }
@@ -183,13 +252,29 @@ function isRigStickyStatusComment(body) {
183
252
  return body.includes(RIG_STATUS_COMMENT_MARKER);
184
253
  }
185
254
  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 } };
255
+ const env = {
256
+ ...process.env,
257
+ ...process.env.GH_TOKEN !== undefined ? { GH_TOKEN: process.env.GH_TOKEN } : {},
258
+ ...process.env.GITHUB_TOKEN !== undefined ? { GITHUB_TOKEN: process.env.GITHUB_TOKEN } : {},
259
+ ...process.env.RIG_GITHUB_TOKEN !== undefined ? { RIG_GITHUB_TOKEN: process.env.RIG_GITHUB_TOKEN } : {}
260
+ };
261
+ for (const [key, value] of Object.entries(extraEnv ?? {})) {
262
+ if (value === undefined)
263
+ delete env[key];
264
+ else
265
+ env[key] = value;
266
+ }
267
+ return {
268
+ encoding: "utf-8",
269
+ timeout: timeoutMs,
270
+ env
271
+ };
189
272
  }
190
273
  function credentialEnv(token) {
191
274
  const clean = token?.trim() ?? "";
192
- return { GH_TOKEN: clean, GITHUB_TOKEN: clean };
275
+ if (clean)
276
+ return { GH_TOKEN: clean, GITHUB_TOKEN: clean, RIG_GITHUB_TOKEN: clean };
277
+ return { GH_TOKEN: undefined, GITHUB_TOKEN: undefined, RIG_GITHUB_TOKEN: undefined };
193
278
  }
194
279
  async function resolveCredentialEnv(opts, purpose) {
195
280
  if (!opts.credentialProvider)
@@ -204,28 +289,319 @@ async function resolveCredentialEnv(opts, purpose) {
204
289
  const resolved = await opts.credentialProvider.resolveGitHubToken(input);
205
290
  return credentialEnv(resolved.token);
206
291
  }
292
+ function tokenDiagnostic(value) {
293
+ const clean = value?.trim() ?? "";
294
+ return clean ? `present(len=${clean.length})` : "missing";
295
+ }
207
296
  function runGh(bin, args, spawn, extraEnv, timeoutMs) {
208
- const res = spawn(bin, [...args], ghSpawnOptions(extraEnv, timeoutMs));
209
- assertGhSuccess(args, res);
297
+ const options = ghSpawnOptions(extraEnv, timeoutMs);
298
+ const res = spawn(bin, [...args], options);
299
+ assertGhSuccess(args, res, options.env);
210
300
  if (!res.stdout || res.stdout.trim() === "")
211
301
  return [];
212
302
  return JSON.parse(res.stdout);
213
303
  }
214
304
  function runGhVoid(bin, args, spawn, extraEnv, timeoutMs) {
215
- const res = spawn(bin, [...args], ghSpawnOptions(extraEnv, timeoutMs));
216
- assertGhSuccess(args, res);
305
+ const options = ghSpawnOptions(extraEnv, timeoutMs);
306
+ const res = spawn(bin, [...args], options);
307
+ assertGhSuccess(args, res, options.env);
217
308
  }
218
- function assertGhSuccess(args, res) {
309
+ function assertGhSuccess(args, res, env) {
219
310
  if (res.error) {
220
311
  const msg = res.error.message ?? String(res.error);
221
312
  throw new Error(`gh CLI not available \u2014 install gh (brew install gh / apt install gh): ${msg}`);
222
313
  }
223
314
  if (res.status !== 0) {
224
- throw new Error(`gh ${args.join(" ")} failed (exit ${res.status}): ${res.stderr}`);
315
+ throw new Error(`gh ${args.join(" ")} failed (exit ${res.status}): ${res.stderr}
316
+ [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)}`);
317
+ }
318
+ }
319
+ var DEFAULT_PROJECT_STATUSES = {
320
+ todo: "Todo",
321
+ running: "In Progress",
322
+ prOpen: "In Review",
323
+ ciFixing: "In Review",
324
+ merging: "In Review",
325
+ done: "Done",
326
+ needsAttention: "Needs Attention"
327
+ };
328
+ function asProjectRecord(value) {
329
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
330
+ }
331
+ function projectString(value) {
332
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
333
+ }
334
+ function projectLifecycleStatusForTaskStatus(status) {
335
+ const normalized = status?.trim().toLowerCase().replace(/[-\s]+/g, "_");
336
+ switch (normalized) {
337
+ case "draft":
338
+ case "open":
339
+ case "queued":
340
+ case "ready":
341
+ return "todo";
342
+ case "running":
343
+ case "in_progress":
344
+ return "running";
345
+ case "under_review":
346
+ case "review":
347
+ case "pr_open":
348
+ return "prOpen";
349
+ case "ci_fixing":
350
+ case "fixing":
351
+ return "ciFixing";
352
+ case "merging":
353
+ case "merge":
354
+ return "merging";
355
+ case "closed":
356
+ case "completed":
357
+ case "done":
358
+ return "done";
359
+ case "blocked":
360
+ case "cancelled":
361
+ case "failed":
362
+ case "needs_attention":
363
+ return "needsAttention";
364
+ default:
365
+ return null;
366
+ }
367
+ }
368
+ function ghGraphQLFetch(bin, spawnFn, extraEnv, timeoutMs) {
369
+ return async (query, variables) => {
370
+ const args = ["api", "graphql", "-f", `query=${query}`];
371
+ for (const [key, value] of Object.entries(variables)) {
372
+ if (value === undefined || value === null)
373
+ continue;
374
+ args.push("-f", `${key}=${String(value)}`);
375
+ }
376
+ const response = runGh(bin, args, spawnFn, extraEnv, timeoutMs);
377
+ return asProjectRecord(response)?.data ?? response;
378
+ };
379
+ }
380
+ function issueNodeIdFor(issue) {
381
+ const id = issue.id ?? issue.nodeId ?? issue.node_id;
382
+ return typeof id === "string" && id.trim().length > 0 ? id.trim() : null;
383
+ }
384
+ function nativeIssueDependencyRef(value, currentRepo) {
385
+ const record = asProjectRecord(value);
386
+ const number = typeof record?.number === "number" ? String(record.number) : projectString(record?.number);
387
+ if (!number)
388
+ return null;
389
+ const repository = asProjectRecord(record?.repository);
390
+ const owner = projectString(asProjectRecord(repository?.owner)?.login);
391
+ const name = projectString(repository?.name);
392
+ if (!owner || !name || `${owner}/${name}` === currentRepo)
393
+ return number;
394
+ return `${owner}/${name}#${number}`;
395
+ }
396
+ function nativeDependencyRefsFrom(data, currentRepo) {
397
+ const issue = asProjectRecord(asProjectRecord(data)?.node);
398
+ const blockedBy = asProjectRecord(issue?.blockedBy);
399
+ const nodes = Array.isArray(blockedBy?.nodes) ? blockedBy.nodes : [];
400
+ return [...new Set(nodes.flatMap((node) => {
401
+ const ref = nativeIssueDependencyRef(node, currentRepo);
402
+ return ref ? [ref] : [];
403
+ }))];
404
+ }
405
+ async function readNativeDependenciesForIssue(input) {
406
+ const issueId = issueNodeIdFor(input.issue);
407
+ if (!issueId)
408
+ return { deps: [], degraded: "GitHub issue node id is unavailable." };
409
+ const query = `
410
+ query RigIssueNativeDependencies($issueId: ID!) {
411
+ node(id: $issueId) {
412
+ ... on Issue {
413
+ blockedBy(first: 100) {
414
+ nodes {
415
+ number
416
+ repository { name owner { login } }
417
+ }
418
+ }
419
+ }
420
+ }
421
+ }
422
+ `;
423
+ try {
424
+ return {
425
+ deps: nativeDependencyRefsFrom(await input.fetchGraphQL(query, { issueId }, "gh-cli"), input.repo)
426
+ };
427
+ } catch (error) {
428
+ const detail = error instanceof Error ? error.message : String(error);
429
+ return { deps: [], degraded: detail };
225
430
  }
226
431
  }
432
+ function formatIssueReference(ref) {
433
+ const clean = ref.trim().replace(/^#/, "");
434
+ return /^\d+$/.test(clean) ? `#${clean}` : clean;
435
+ }
436
+ function appendReferenceLines(body, deps, parents) {
437
+ const lines = [];
438
+ const cleanDeps = (deps ?? []).map(formatIssueReference).filter((ref) => ref.length > 0);
439
+ const cleanParents = (parents ?? []).map(formatIssueReference).filter((ref) => ref.length > 0);
440
+ if (cleanDeps.length > 0)
441
+ lines.push(`depends-on: ${cleanDeps.join(", ")}`);
442
+ if (cleanParents.length > 0)
443
+ lines.push(`parents: ${cleanParents.join(", ")}`);
444
+ if (lines.length === 0)
445
+ return body;
446
+ return body.trim().length > 0 ? `${body.trimEnd()}
447
+
448
+ ${lines.join(`
449
+ `)}` : lines.join(`
450
+ `);
451
+ }
452
+ function bodyForCreatedTask(input) {
453
+ const metadata = { ...input.metadata ?? {} };
454
+ if (input.deps && input.deps.length > 0)
455
+ metadata["depends-on"] = input.deps.map(formatIssueReference);
456
+ if (input.parents && input.parents.length > 0)
457
+ metadata.parents = input.parents.map(formatIssueReference);
458
+ return updateRigOwnedMetadataBlock(appendReferenceLines(input.body, input.deps, input.parents), metadata);
459
+ }
460
+ function projectStatusFieldFrom(data, projectId) {
461
+ const fields = asProjectRecord(asProjectRecord(asProjectRecord(data)?.node)?.fields)?.nodes;
462
+ for (const node of Array.isArray(fields) ? fields : []) {
463
+ const record = asProjectRecord(node);
464
+ if (projectString(record?.name)?.toLowerCase() !== "status")
465
+ continue;
466
+ const id = projectString(record?.id);
467
+ if (!id)
468
+ continue;
469
+ const options = Array.isArray(record?.options) ? record.options.flatMap((option) => {
470
+ const optionRecord = asProjectRecord(option);
471
+ const optionId = projectString(optionRecord?.id);
472
+ const name = projectString(optionRecord?.name);
473
+ return optionId && name ? [{ id: optionId, name }] : [];
474
+ }) : [];
475
+ return { id, name: "Status", options };
476
+ }
477
+ throw new Error(`GitHub Project ${projectId} does not expose a Status single-select field.`);
478
+ }
479
+ async function resolveProjectStatusField(input) {
480
+ const query = `
481
+ query RigProjectStatusField($projectId: ID!) {
482
+ node(id: $projectId) {
483
+ ... on ProjectV2 {
484
+ fields(first: 50) {
485
+ nodes {
486
+ ... on ProjectV2FieldCommon { id name }
487
+ ... on ProjectV2SingleSelectField { id name options { id name } }
488
+ }
489
+ }
490
+ }
491
+ }
492
+ }
493
+ `;
494
+ return projectStatusFieldFrom(await input.fetchGraphQL(query, { projectId: input.projectId }, input.token), input.projectId);
495
+ }
496
+ async function ensureIssueProjectItem(input) {
497
+ const query = `
498
+ query RigFindProjectIssueItem($projectId: ID!) {
499
+ node(id: $projectId) {
500
+ ... on ProjectV2 {
501
+ items(first: 100) { nodes { id content { ... on Issue { id } } } }
502
+ }
503
+ }
504
+ }
505
+ `;
506
+ const data = await input.fetchGraphQL(query, { projectId: input.projectId }, input.token);
507
+ const nodes = asProjectRecord(asProjectRecord(asProjectRecord(data)?.node)?.items)?.nodes;
508
+ for (const node of Array.isArray(nodes) ? nodes : []) {
509
+ const record = asProjectRecord(node);
510
+ const content = asProjectRecord(record?.content);
511
+ if (projectString(content?.id) === input.issueNodeId) {
512
+ const id2 = projectString(record?.id);
513
+ if (id2)
514
+ return { id: id2, created: false };
515
+ }
516
+ }
517
+ const mutation = `
518
+ mutation RigAddIssueToProject($projectId: ID!, $contentId: ID!) {
519
+ addProjectV2ItemById(input: { projectId: $projectId, contentId: $contentId }) { item { id } }
520
+ }
521
+ `;
522
+ const created = await input.fetchGraphQL(mutation, { projectId: input.projectId, contentId: input.issueNodeId }, input.token);
523
+ const addResult = asProjectRecord(asProjectRecord(created)?.addProjectV2ItemById);
524
+ const id = projectString(asProjectRecord(addResult?.item)?.id);
525
+ if (!id)
526
+ throw new Error("GitHub Project item creation did not return an item id.");
527
+ return { id, created: true };
528
+ }
529
+ async function updateIssueProjectStatus(input) {
530
+ const mutation = `
531
+ mutation RigUpdateProjectStatus($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
532
+ updateProjectV2ItemFieldValue(input: {
533
+ projectId: $projectId,
534
+ itemId: $itemId,
535
+ fieldId: $fieldId,
536
+ value: { singleSelectOptionId: $optionId }
537
+ }) { projectV2Item { id } }
538
+ }
539
+ `;
540
+ await input.fetchGraphQL(mutation, {
541
+ projectId: input.projectId,
542
+ itemId: input.itemId,
543
+ fieldId: input.fieldId,
544
+ optionId: input.optionId
545
+ }, input.token);
546
+ }
547
+ function fetchIssueNodeId(bin, repo, spawnFn, id, extraEnv, timeoutMs) {
548
+ const issue = runGh(bin, ["issue", "view", String(id), "--repo", repo, "--json", "id"], spawnFn, extraEnv, timeoutMs);
549
+ return projectString(issue.id) ?? projectString(issue.nodeId) ?? projectString(issue.node_id);
550
+ }
551
+ async function syncGitHubProjectStatus(bin, repo, spawnFn, id, status, projects, extraEnv, timeoutMs) {
552
+ if (!projects?.enabled)
553
+ return;
554
+ const projectId = projectString(projects.projectId);
555
+ if (!projectId)
556
+ throw new Error("GitHub Projects status sync is enabled but projectId is missing.");
557
+ const lifecycleStatus = projectLifecycleStatusForTaskStatus(status);
558
+ if (!lifecycleStatus)
559
+ return;
560
+ const issueNodeId = fetchIssueNodeId(bin, repo, spawnFn, id, extraEnv, timeoutMs);
561
+ if (!issueNodeId)
562
+ throw new Error(`GitHub issue ${repo}#${id} did not expose a node id for Projects status sync.`);
563
+ const projectStatus = projectString(projects.statuses?.[lifecycleStatus]) ?? DEFAULT_PROJECT_STATUSES[lifecycleStatus];
564
+ const fetchGraphQL = ghGraphQLFetch(bin, spawnFn, extraEnv, timeoutMs);
565
+ const field = await resolveProjectStatusField({ projectId, token: "gh-cli", fetchGraphQL });
566
+ const option = field.options.find((candidate) => candidate.name.toLowerCase() === projectStatus.toLowerCase() || candidate.id === projectStatus);
567
+ if (!option)
568
+ throw new Error(`GitHub Project ${projectId} Status field does not contain option "${projectStatus}".`);
569
+ const item = await ensureIssueProjectItem({ projectId, issueNodeId, token: "gh-cli", fetchGraphQL });
570
+ await updateIssueProjectStatus({
571
+ projectId,
572
+ itemId: item.id,
573
+ fieldId: projectString(projects.statusFieldId) ?? field.id,
574
+ optionId: option.id,
575
+ token: "gh-cli",
576
+ fetchGraphQL
577
+ });
578
+ }
579
+ var TERMINAL_TASK_STATUSES = new Set(["closed", "completed", "merged", "cancelled", "resolved", "done"]);
580
+ function normalizeTaskStatusToken(status) {
581
+ return status?.trim().toLowerCase().replace(/[-\s]+/g, "_") ?? "";
582
+ }
583
+ function issueUpdatesMode(value) {
584
+ return value === "off" || value === "minimal" || value === "lifecycle" ? value : "lifecycle";
585
+ }
586
+ function isTerminalTaskStatus(status) {
587
+ return TERMINAL_TASK_STATUSES.has(normalizeTaskStatusToken(status));
588
+ }
589
+ function shouldWriteIssueUpdate(mode, status) {
590
+ if (mode === "off")
591
+ return false;
592
+ if (mode === "lifecycle")
593
+ return true;
594
+ return isTerminalTaskStatus(status);
595
+ }
596
+ function isRunningStatus(status) {
597
+ return normalizeTaskStatusToken(status) === "running";
598
+ }
599
+ function assignRunningIssue(bin, repo, spawnFn, id, assignee, extraEnv, timeoutMs) {
600
+ runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, "--add-assignee", assignee?.trim() || "@me"], spawnFn, extraEnv, timeoutMs);
601
+ }
227
602
  function statusLabelFor(status) {
228
603
  switch (status) {
604
+ case "running":
229
605
  case "in_progress":
230
606
  return "in-progress";
231
607
  case "blocked":
@@ -244,6 +620,8 @@ function statusLabelFor(status) {
244
620
  return "under-review";
245
621
  case "needs_attention":
246
622
  return "blocked";
623
+ case "closed":
624
+ case "completed":
247
625
  case "open":
248
626
  return null;
249
627
  default:
@@ -252,11 +630,13 @@ function statusLabelFor(status) {
252
630
  }
253
631
  function rigStatusLabelFor(status) {
254
632
  switch (status) {
633
+ case "running":
255
634
  case "in_progress":
256
635
  return "rig:running";
257
636
  case "under_review":
258
637
  return "rig:pr-open";
259
638
  case "closed":
639
+ case "completed":
260
640
  return "rig:done";
261
641
  case "ci_fixing":
262
642
  return "rig:ci-fixing";
@@ -274,9 +654,10 @@ function rigStatusLabelFor(status) {
274
654
  return null;
275
655
  }
276
656
  }
277
- function applyIssueStatus(bin, repo, spawnFn, id, status, extraEnv, timeoutMs) {
278
- const targetLabel = status === "closed" ? null : statusLabelFor(status);
657
+ async function applyIssueStatus(bin, repo, spawnFn, id, status, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs) {
658
+ const targetLabel = status === "closed" || status === "completed" ? null : statusLabelFor(status);
279
659
  const targetRigLabel = rigStatusLabelFor(status);
660
+ const shouldSyncLifecycle = shouldWriteIssueUpdate(issueUpdates, status);
280
661
  for (const l of [...STATUS_LABELS, ...RIG_STATUS_LABELS]) {
281
662
  if (targetLabel !== null && l === targetLabel)
282
663
  continue;
@@ -298,7 +679,19 @@ function applyIssueStatus(bin, repo, spawnFn, id, status, extraEnv, timeoutMs) {
298
679
  runGhVoid(bin, ["issue", "edit", String(id), "--repo", repo, "--add-label", label], spawnFn, extraEnv, timeoutMs);
299
680
  }
300
681
  }
301
- if (status === "closed") {
682
+ if (isRunningStatus(status)) {
683
+ assignRunningIssue(bin, repo, spawnFn, id, runningAssignee, extraEnv, timeoutMs);
684
+ if (shouldSyncLifecycle) {
685
+ upsertRigStickyComment(bin, repo, spawnFn, String(id), buildRigStickyStatusComment({
686
+ status: "running",
687
+ summary: "Rig run started."
688
+ }), extraEnv, timeoutMs);
689
+ }
690
+ }
691
+ if (shouldSyncLifecycle) {
692
+ await syncGitHubProjectStatus(bin, repo, spawnFn, id, status, projects, extraEnv, timeoutMs);
693
+ }
694
+ if (status === "closed" || status === "completed") {
302
695
  runGhVoid(bin, ["issue", "close", String(id), "--repo", repo], spawnFn, extraEnv, timeoutMs);
303
696
  }
304
697
  }
@@ -368,11 +761,11 @@ function applyLabels(bin, repo, spawnFn, id, labels, action, extraEnv, timeoutMs
368
761
  } catch {}
369
762
  }
370
763
  }
371
- function applyIssueUpdate(bin, repo, spawnFn, id, update, extraEnv, timeoutMs) {
764
+ async function applyIssueUpdate(bin, repo, spawnFn, id, update, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs) {
372
765
  if (update.status) {
373
- applyIssueStatus(bin, repo, spawnFn, id, update.status, extraEnv, timeoutMs);
766
+ await applyIssueStatus(bin, repo, spawnFn, id, update.status, projects, runningAssignee, issueUpdates, extraEnv, timeoutMs);
374
767
  }
375
- if (update.comment?.trim()) {
768
+ if (update.comment?.trim() && shouldWriteIssueUpdate(issueUpdates, update.status)) {
376
769
  if (isRigStickyStatusComment(update.comment)) {
377
770
  upsertRigStickyComment(bin, repo, spawnFn, String(id), update.comment, extraEnv, timeoutMs);
378
771
  } else {
@@ -398,6 +791,17 @@ function createGitHubIssuesTaskSource(opts) {
398
791
  const spawnFn = opts.spawn ?? spawnSync;
399
792
  const timeoutMs = Math.max(1000, Math.trunc(opts.timeoutMs ?? DEFAULT_GH_TIMEOUT_MS));
400
793
  const listLimit = Math.max(1, Math.trunc(opts.listLimit ?? DEFAULT_GITHUB_ISSUE_LIST_LIMIT));
794
+ const issueUpdates = issueUpdatesMode(opts.issueUpdates);
795
+ async function issueToTaskWithOptionalNativeDependencies(issue, env) {
796
+ if (!opts.useNativeDependencies)
797
+ return issueToTask(issue, repo);
798
+ const nativeDependencies = await readNativeDependenciesForIssue({
799
+ issue,
800
+ repo,
801
+ fetchGraphQL: ghGraphQLFetch(bin, spawnFn, env, timeoutMs)
802
+ });
803
+ return issueToTask(issue, repo, nativeDependencies);
804
+ }
401
805
  return {
402
806
  id: "std:github-issues",
403
807
  kind: "github-issues",
@@ -424,12 +828,13 @@ function createGitHubIssuesTaskSource(opts) {
424
828
  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
829
  }
426
830
  const issues = rawIssues.filter((issue) => !issue.pull_request);
427
- return issues.map((i) => issueToTask(i, repo));
831
+ return Promise.all(issues.map((issue) => issueToTaskWithOptionalNativeDependencies(issue, env)));
428
832
  },
429
833
  async get(id) {
834
+ const env = await resolveCredentialEnv(opts, "selected-repo");
835
+ let issue;
430
836
  try {
431
- const env = await resolveCredentialEnv(opts, "selected-repo");
432
- const issue = runGh(bin, [
837
+ issue = runGh(bin, [
433
838
  "issue",
434
839
  "view",
435
840
  String(id),
@@ -438,19 +843,23 @@ function createGitHubIssuesTaskSource(opts) {
438
843
  "--json",
439
844
  "number,title,body,labels,state,url,assignees,id"
440
845
  ], spawnFn, env, timeoutMs);
441
- return issueToTask(issue, repo);
442
- } catch {
443
- return;
846
+ } catch (error) {
847
+ const detail = error instanceof Error ? error.message : String(error);
848
+ 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)) {
849
+ return;
850
+ }
851
+ throw new Error(`Failed to read task ${id} from GitHub repo ${repo}: ${detail}`);
444
852
  }
853
+ return issueToTaskWithOptionalNativeDependencies(issue, env);
445
854
  },
446
855
  async updateStatus(id, status) {
447
856
  const env = await resolveCredentialEnv(opts, "selected-repo");
448
- applyIssueStatus(bin, repo, spawnFn, id, status, env, timeoutMs);
857
+ await applyIssueStatus(bin, repo, spawnFn, id, status, opts.projects, opts.assignee, issueUpdates, env, timeoutMs);
449
858
  notifyTaskChanged(opts.onTaskChanged, repo, id, status);
450
859
  },
451
860
  async updateTask(id, update) {
452
861
  const env = await resolveCredentialEnv(opts, "selected-repo");
453
- applyIssueUpdate(bin, repo, spawnFn, id, update, env, timeoutMs);
862
+ await applyIssueUpdate(bin, repo, spawnFn, id, update, opts.projects, opts.assignee, issueUpdates, env, timeoutMs);
454
863
  notifyTaskChanged(opts.onTaskChanged, repo, id, update.status);
455
864
  },
456
865
  async addLabels(id, labels) {
@@ -465,6 +874,7 @@ function createGitHubIssuesTaskSource(opts) {
465
874
  },
466
875
  async createIssue(input) {
467
876
  const env = await resolveCredentialEnv(opts, "selected-repo");
877
+ const body = input.body ?? "";
468
878
  const args = [
469
879
  "api",
470
880
  "-X",
@@ -473,12 +883,31 @@ function createGitHubIssuesTaskSource(opts) {
473
883
  "-f",
474
884
  `title=${input.title}`,
475
885
  "-f",
476
- `body=${input.body ?? ""}`,
886
+ `body=${body}`,
477
887
  ...(input.labels ?? []).flatMap((label) => ["-f", `labels[]=${label}`])
478
888
  ];
479
889
  const issue = runGh(bin, args, spawnFn, env, timeoutMs);
480
890
  notifyTaskChanged(opts.onTaskChanged, repo, String(issue.number));
481
- return issueToTask(issue, repo);
891
+ return issueToTask({ ...issue, body: issue.body ?? body }, repo);
892
+ },
893
+ async create(input) {
894
+ const env = await resolveCredentialEnv(opts, "selected-repo");
895
+ const body = bodyForCreatedTask(input);
896
+ const args = [
897
+ "api",
898
+ "-X",
899
+ "POST",
900
+ `repos/${repo}/issues`,
901
+ "-f",
902
+ `title=${input.title}`,
903
+ "-f",
904
+ `body=${body}`,
905
+ "-f",
906
+ "labels[]=rig:generated"
907
+ ];
908
+ const issue = runGh(bin, args, spawnFn, env, timeoutMs);
909
+ notifyTaskChanged(opts.onTaskChanged, repo, String(issue.number));
910
+ return issueToTask({ ...issue, body: issue.body ?? body }, repo);
482
911
  },
483
912
  async getIssueBody(id) {
484
913
  const env = await resolveCredentialEnv(opts, "selected-repo");