omp-conductor 0.3.17 → 0.3.18

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omp-conductor",
3
- "version": "0.3.17",
3
+ "version": "0.3.18",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "A 24/7 dispatcher that takes ready GitHub issues to green, mergeable PRs using omp coding sessions, with tiered escalation first to an orchestrator session and then to a human.",
@@ -53,6 +53,12 @@ const CLOSERS_QUERY = `query($owner:String!,$repo:String!,$n:Int!){
53
53
  }
54
54
  }`;
55
55
 
56
+ const PARENT_QUERY = `query($owner:String!,$repo:String!,$n:Int!){
57
+ repository(owner:$owner,name:$repo){
58
+ issue(number:$n){ parent{ number } }
59
+ }
60
+ }`;
61
+
56
62
  /** The `gh api graphql` envelope for {@link CLOSERS_QUERY}. Every level is
57
63
  * nullable: a deleted or wrong-numbered issue answers `null`, not an error. */
58
64
  interface ClosersResponse {
@@ -190,27 +196,30 @@ export function prStateFrom(raw: string): PrState | undefined {
190
196
  }
191
197
 
192
198
  /**
193
- * Parent number from a `gh issue view --json parent` payload, or undefined
194
- * when the issue has no parent. Throws on a payload that claims a parent but
195
- * does not carry a usable number admission would otherwise treat garbage as
196
- * "no parent" and skip the epic soft-cap.
199
+ * Parent number from a raw GraphQL response, or undefined when the issue has
200
+ * no parent. Throws when the issue or claimed parent is malformed — admission
201
+ * must not turn an unknown relationship into permission to run siblings.
197
202
  */
198
203
  export function parentNumberFrom(raw: string): number | undefined {
199
- const parsed = JSON.parse(raw) as { parent: { number?: unknown } | null };
200
- if (parsed.parent == null) return undefined;
201
- const n = parsed.parent.number;
204
+ const parsed = JSON.parse(raw) as {
205
+ data?: { repository?: { issue?: { parent?: { number?: unknown } | null } | null } | null };
206
+ };
207
+ const issue = parsed.data?.repository?.issue;
208
+ if (issue == null) throw new Error("unexpected missing issue in parent response");
209
+ if (issue.parent == null) return undefined;
210
+ const n = issue.parent.number;
202
211
  if (typeof n !== "number" || !Number.isInteger(n) || n <= 0) {
203
212
  throw new Error(`unexpected parent number ${JSON.stringify(n)}`);
204
213
  }
205
214
  return n;
206
215
  }
207
216
 
208
- export function makeTracker(p: ProjectConfig): Tracker {
217
+ export function makeTracker(p: ProjectConfig, runGh: typeof gh = gh): Tracker {
209
218
  const repo = p.tracker.repo;
210
219
 
211
220
  return {
212
221
  async listReady(): Promise<ReadyIssue[]> {
213
- const raw = await gh([
222
+ const raw = await runGh([
214
223
  "issue",
215
224
  "list",
216
225
  "--repo",
@@ -246,7 +255,7 @@ export function makeTracker(p: ProjectConfig): Tracker {
246
255
 
247
256
  async addLabel(issue: number, label: string): Promise<void> {
248
257
  try {
249
- await gh(["issue", "edit", String(issue), "--repo", repo, "--add-label", label]);
258
+ await runGh(["issue", "edit", String(issue), "--repo", repo, "--add-label", label]);
250
259
  } catch (err) {
251
260
  if (!isLabelNoop(err, "add")) throw err;
252
261
  }
@@ -254,7 +263,7 @@ export function makeTracker(p: ProjectConfig): Tracker {
254
263
 
255
264
  async removeLabel(issue: number, label: string): Promise<void> {
256
265
  try {
257
- await gh(["issue", "edit", String(issue), "--repo", repo, "--remove-label", label]);
266
+ await runGh(["issue", "edit", String(issue), "--repo", repo, "--remove-label", label]);
258
267
  } catch (err) {
259
268
  if (!isLabelNoop(err, "remove")) throw err;
260
269
  }
@@ -263,25 +272,37 @@ export function makeTracker(p: ProjectConfig): Tracker {
263
272
  async comment(issue: number, body: string): Promise<void> {
264
273
  // `--body-file -` reads stdin, so the body is never shell- or argv-
265
274
  // mangled and has no length limit worth worrying about.
266
- await gh(["issue", "comment", String(issue), "--repo", repo, "--body-file", "-"], body);
275
+ await runGh(["issue", "comment", String(issue), "--repo", repo, "--body-file", "-"], body);
267
276
  },
268
277
 
269
278
  async close(issue: number): Promise<void> {
270
- await gh(["issue", "close", String(issue), "--repo", repo]);
279
+ await runGh(["issue", "close", String(issue), "--repo", repo]);
271
280
  },
272
281
 
273
282
  async linkParent(child: number, parent: number): Promise<void> {
274
283
  // Native sub-issue linkage rather than a body mention: it is what the
275
284
  // repo's own epic rollups read, so a human sees the split without us
276
285
  // maintaining a second index of it.
277
- await gh(["issue", "edit", String(parent), "--repo", repo, "--add-sub-issue", String(child)]);
286
+ await runGh(["issue", "edit", String(parent), "--repo", repo, "--add-sub-issue", String(child)]);
278
287
  },
279
288
 
280
289
  async parentOf(issue: number): Promise<number | undefined> {
281
- // Native sub-issue parent, not a body scrape. Verified on gh 2.97.0:
282
- // `parent` is null with no link, otherwise `{ number, title, url, ... }`.
290
+ // gh 2.86 cannot expose `parent` through `issue view --json`; its raw
291
+ // GraphQL command can, and is already the adapter's path for PR closers.
292
+ const [owner = "", name = ""] = repo.split("/");
283
293
  return parentNumberFrom(
284
- await gh(["issue", "view", String(issue), "--repo", repo, "--json", "parent"]),
294
+ await runGh([
295
+ "api",
296
+ "graphql",
297
+ "-f",
298
+ `query=${PARENT_QUERY}`,
299
+ "-F",
300
+ `owner=${owner}`,
301
+ "-F",
302
+ `repo=${name}`,
303
+ "-F",
304
+ `n=${issue}`,
305
+ ]),
285
306
  );
286
307
  },
287
308
 
@@ -290,7 +311,7 @@ export function makeTracker(p: ProjectConfig): Tracker {
290
311
  // that spelling, so an empty half means a hand-edited config: `gh` then
291
312
  // errors and the caller holds the candidate rather than guessing.
292
313
  const [owner = "", name = ""] = repo.split("/");
293
- const raw = await gh([
314
+ const raw = await runGh([
294
315
  "api",
295
316
  "graphql",
296
317
  "-f",
@@ -316,7 +337,7 @@ export function makeTracker(p: ProjectConfig): Tracker {
316
337
  // what the URL already says.
317
338
  if (!PR_URL.test(url)) return undefined;
318
339
  try {
319
- return prStateFrom(await gh(["pr", "view", url, "--json", "state", "--jq", ".state"]));
340
+ return prStateFrom(await runGh(["pr", "view", url, "--json", "state", "--jq", ".state"]));
320
341
  } catch {
321
342
  // Never throws, per the port's contract. A deleted PR, a revoked token
322
343
  // and a flaky network all mean "could not tell", and the caller's whole