forgedock 1.38.1 → 1.39.0

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.
@@ -14,12 +14,16 @@ export const MAX_LEASE_TTL_MS = 3 * 600_000;
14
14
  const PROVENANCE_QUERY = "query($owner:String!,$name:String!,$number:Int!){repository(owner:$owner,name:$name){"
15
15
  + "issue(number:$number){body authorAssociation author{login __typename} editor{login __typename}}}}";
16
16
 
17
- export function makeProjector(io) {
17
+ export function makeProjector(io, { repo } = {}) {
18
18
  const gh = io.gh;
19
19
 
20
+ // Throws on an unreadable body: an empty string would make writeState wipe the issue (#4860).
20
21
  async function getBody(issue) {
21
- const out = await gh(["issue", "view", String(issue), "--json", "body"]);
22
- try { return JSON.parse(out).body ?? ""; } catch { return ""; }
22
+ const out = await gh(["issue", "view", String(issue), "--json", "body", ...(repo ? ["-R", repo] : [])]);
23
+ let body;
24
+ try { body = JSON.parse(out).body; } catch { throw new Error(`issue #${issue}: body read returned non-JSON output`); }
25
+ if (typeof body !== "string") throw new Error(`issue #${issue}: body read returned no body`);
26
+ return body;
23
27
  }
24
28
 
25
29
  /**
@@ -64,16 +68,21 @@ export function makeProjector(io) {
64
68
  },
65
69
 
66
70
  async writeState(issue, index) {
67
- const body = upsertStateBlock(await getBody(issue), index);
68
- await gh(["issue", "edit", String(issue), "--body", body]);
71
+ const old = await getBody(issue);
72
+ const body = upsertStateBlock(old, index);
73
+ // Each run of issue text around a FORGE:STATE block must survive on its own: the block is replaced in place, so text
74
+ // before and after it stays apart and a joined string would never match (#4871).
75
+ const dropped = old.split(/<!-- FORGE:STATE\s*[\s\S]*?-->/).map((t) => t.trim()).filter(Boolean).some((t) => !body.includes(t));
76
+ if (dropped) throw new Error(`issue #${issue}: refusing a state write that drops existing issue text`);
77
+ await gh(["issue", "edit", String(issue), "--body", body, ...(repo ? ["-R", repo] : [])]);
69
78
  },
70
79
 
71
80
  async setLabel(issue, label) {
72
- await gh(["issue", "edit", String(issue), "--add-label", label]);
81
+ await gh(["issue", "edit", String(issue), "--add-label", label, ...(repo ? ["-R", repo] : [])]);
73
82
  },
74
83
 
75
84
  async removeLabel(issue, label) {
76
- await gh(["issue", "edit", String(issue), "--remove-label", label]);
85
+ await gh(["issue", "edit", String(issue), "--remove-label", label, ...(repo ? ["-R", repo] : [])]);
77
86
  },
78
87
  };
79
88
  }
@@ -9,6 +9,7 @@
9
9
  * builder_tier/class/model/escalated 0|1, forge#4394)
10
10
  * SANDBOX_USAGE -> `sandbox_usage` (provider, sandbox_id, minutes, cost_usd, batch; one per settled remote lane, forge#4255)
11
11
  * DECISION_ENGINE -> `decision_engine` (decided, escalated, vetoed, reversed, mode, needs_human_before/after; forge#4766)
12
+ * PRODWATCH_METRICS -> `prodwatch_metrics` (source, sample, invalid, fp_rate, fix_ineffective_rate, recurrence_rate, signal_to_issue/merge/verified_ms; forge#4837)
12
13
  * STRUCTURE_DELTA -> `structure_delta` (edges_added/removed, import_files_added/removed, boundary_changed/loosened 0|1; forge#4159)
13
14
  * lean's merge-time record (forge#4165) adds milestone, the pr and string lists: deps_added and
14
15
  * new_edges / removed_edges as "from:to" comma-joined, top_growth "file+N", decisions "K1:lint",
@@ -84,6 +85,11 @@ export function mapRecord(rec, wallMs) {
84
85
  return { event: "decision_engine", phase: null, payload: compact({ decided: rec.decided, escalated: rec.escalated, vetoed: rec.vetoed, reversed: rec.reversed, mode: rec.mode,
85
86
  needs_human_before: rec.needs_human_before, needs_human_after: rec.needs_human_after }) };
86
87
  }
88
+ if (rec.event === "PRODWATCH_METRICS") { // #4837
89
+ return { event: "prodwatch_metrics", phase: null, payload: compact({ source: rec.source, sample: rec.sample, invalid: rec.invalid, fix_ineffective: rec.fix_ineffective, recurred: rec.recurred,
90
+ fp_rate: rec.fp_rate, fix_ineffective_rate: rec.fix_ineffective_rate, recurrence_rate: rec.recurrence_rate,
91
+ signal_to_issue_ms: rec.signal_to_issue_ms, signal_to_merge_ms: rec.signal_to_merge_ms, signal_to_verified_ms: rec.signal_to_verified_ms }) };
92
+ }
87
93
  if (rec.event === "CI_PENDING_WAIT") {
88
94
  return { event: "ci_pending_wait", phase: rec.phase ?? "review", payload: compact({ pr: rec.pr, minutes_waited: rec.minutes_waited, result: rec.result, polls: rec.polls, rate_limit_sleeps: rec.rate_limit_sleeps }) };
89
95
  }
@@ -27,6 +27,12 @@ describe("engine telemetry seam", () => {
27
27
  assert.equal("baseline_tokens" in q, false); assert.equal("lane_profile" in q, false);
28
28
  });
29
29
 
30
+ it("forge#4837: PRODWATCH_METRICS maps to prodwatch_metrics, absent values omitted", () => {
31
+ const m = mapRecord({ event: "PRODWATCH_METRICS", source: "memo", sample: 7, fp_rate: 0.25, signal_to_issue_ms: 1000, signal_to_merge_ms: null }, 0);
32
+ assert.equal(m.event, "prodwatch_metrics");
33
+ assert.deepEqual(m.payload, { source: "memo", sample: 7, fp_rate: 0.25, signal_to_issue_ms: 1000 });
34
+ });
35
+
30
36
  it("forge#4257: phase_attempt carries the backend, omitted when unreported", () => {
31
37
  assert.equal(mapRecord({ event: "PHASE_ATTEMPT_USAGE", phase: "build", attempt: 1, backend: "api" }, 0).payload.backend, "api");
32
38
  // #4407: the per-attempt cost and the session running total ride along; absent fields are omitted
@@ -23,6 +23,7 @@ import { makeEventEmitter, probeSweepEvents, experimentEvent } from "./engine/ev
23
23
  import { readExperiment } from "./experiment.mjs";
24
24
  import { readBranches, resolveLane, isDefaultLane } from "./lean/lane.mjs";
25
25
  import { parseVerify } from "./lean/verify.mjs";
26
+ import { viewIssue } from "./lean/gh-assoc.mjs";
26
27
  import { parseExecution, detectNeeds, placementText } from "./lean/execution.mjs";
27
28
  import { relayPrefetchCommands } from "./lean/relay.mjs";
28
29
  import { parseTurnBudgets, formatTurnSummary, TURN_BUDGET_DEFAULTS } from "./lean/budget.mjs";
@@ -1038,7 +1039,7 @@ export async function resumeStalledFromCli(argv, deps = {}) {
1038
1039
  await assertRepoMatchesCwd(io, repo, { sleep: deps.sleep });
1039
1040
  await assertLaneNotDefault(io, lane, repo);
1040
1041
  const dispatch = deps.dispatch ?? runFromCli;
1041
- const projector = makeProjector(io);
1042
+ const projector = makeProjector(io, { repo: repo ?? undefined });
1042
1043
  const now = Date.now();
1043
1044
 
1044
1045
  // Collect candidate issue numbers from all non-terminal workflow labels
@@ -1254,7 +1255,7 @@ async function addToBatch({ id, tokens, root, io, viaDaemon = null, print = (l)
1254
1255
  for (const n of new Set(ints)) {
1255
1256
  let st = null;
1256
1257
  let v = null;
1257
- try { v = JSON.parse(await io.gh(["issue", "view", String(n), ...(repo ? ["-R", repo] : []), "--json", "state,body,author,authorAssociation"])); st = v?.state ?? null; } catch { st = null; }
1258
+ try { v = await viewIssue(io.gh, repo, n, "state,body,author"); st = v?.state ?? null; } catch { st = null; }
1258
1259
  state.set(n, st); records.set(n, v);
1259
1260
  }
1260
1261
  const open = [];
@@ -1861,8 +1862,8 @@ export async function runBatch({ argv, cwd: callerCwd = process.cwd(), env: call
1861
1862
  sink.line(`cross-lane: #${e.issue} (${e.lane}) depends on #${e.dependency} (${e.dependencyLane}); its fix will not be on ${e.lane}: re-milestone #${e.dependency} or wait for a milestone sync`);
1862
1863
  },
1863
1864
  readBody: maint || registry || hybrid
1864
- ? async (n) => { const v = JSON.parse(await leanIo.gh(["issue", "view", String(n), "-R", repo, "--json", "body,labels,comments,author,authorAssociation"])); trustedOf.set(n, bodyTrusted(v)); labelsOf.set(n, (v.labels ?? []).map((x) => x.name)); needsOf.set(n, detectNeeds(placementText(v), execForNeeds)); if (execForNeeds?.relay) prefetchOf.set(n, relayPrefetchCommands(v, execForNeeds.relay)); return v.body; }
1865
- : async (n) => { const v = JSON.parse(await leanIo.gh(["issue", "view", String(n), "-R", repo, "--json", "body,comments,author,authorAssociation"])); trustedOf.set(n, bodyTrusted(v)); needsOf.set(n, detectNeeds(placementText(v), execForNeeds)); if (execForNeeds?.relay) prefetchOf.set(n, relayPrefetchCommands(v, execForNeeds.relay)); return v.body; }, // #4785: decisions and the frozen contract's validate commands too
1865
+ ? async (n) => { const v = await viewIssue(leanIo.gh, repo, n, "body,labels,comments,author"); trustedOf.set(n, bodyTrusted(v)); labelsOf.set(n, (v.labels ?? []).map((x) => x.name)); needsOf.set(n, detectNeeds(placementText(v), execForNeeds)); if (execForNeeds?.relay) prefetchOf.set(n, relayPrefetchCommands(v, execForNeeds.relay)); return v.body; }
1866
+ : async (n) => { const v = await viewIssue(leanIo.gh, repo, n, "body,comments,author"); trustedOf.set(n, bodyTrusted(v)); needsOf.set(n, detectNeeds(placementText(v), execForNeeds)); if (execForNeeds?.relay) prefetchOf.set(n, relayPrefetchCommands(v, execForNeeds.relay)); return v.body; }, // #4785: decisions and the frozen contract's validate commands too
1866
1867
  onUpdate: (r) => sink.row(r),
1867
1868
  });
1868
1869
  } finally { for (const [sig, f] of Object.entries(handlers)) off(sig, f); }
@@ -2226,7 +2227,7 @@ export async function schedFromCli(argv, deps = {}) {
2226
2227
  throw new Error(usage);
2227
2228
  }
2228
2229
 
2229
- /** #4773: is the author of a `gh issue view --json author,authorAssociation` record trusted (the engine admission's `isTrustedActor`)? */
2230
+ /** #4773: is the author of a `viewIssue` record (author plus the REST association, #4859) trusted (the engine admission's `isTrustedActor`)? */
2230
2231
  const bodyTrusted = (v) => isTrustedActor({ association: v?.authorAssociation, type: v?.author?.is_bot ? "Bot" : "User", login: v?.author?.login });
2231
2232
 
2232
2233
  /**
package/bin/forgedock.mjs CHANGED
@@ -3360,6 +3360,7 @@ function help() {
3360
3360
  ["npx forgedock milestone train <slug...> [--parallel-milestones N] | --resume <id> | --insert <id> <slug> [--before s|--after s] | --show <id>", "Unattended multi-milestone train: one durable state file (order, active set, lifecycle), typed STOP reasons, resumable; never edits forge.yaml"],
3361
3361
  ["npx forgedock guards list [--path p] [--json] | backfill --since <date> [--dry-run]", "Fix guards: list the FORGE:GUARD records (test files + protected hunks of merged fixes) covering a path, or build them for already-merged lean lanes"],
3362
3362
  ["npx forgedock issue-watch run [--name <w>] [--repo owner/repo] [--once] [--dry-run] | status [--name <w>]", "Named, scoped, capped watches (forge.yaml issue_watch.watches) that claim matching open issues into lanes or one long-lived orchestrate batch, or (mode: release) pool them and cut themed releases; see docs/ISSUE-WATCH.md"],
3363
+ ["npx forgedock prod-watch run [--repo owner/repo] [--once] [--interval <s>] [--dry-run] | status", "Production-side watcher (forge.yaml prod_watch): read-only signal sources -> triage -> governor -> issues, kill-switch file, owner digest; see docs/PROD-WATCH.md"],
3363
3364
  ["npx forgedock limits show | clear <id>|--all", "Show or clear per-account usage-limit holds shared by orchestrate batches"],
3364
3365
  ["npx forgedock decisions list [--stale] [--refresh]", "List anchored decisions (FORGE:DECISION); --stale shows those with an anchor missing on the base"],
3365
3366
  ["npx forgedock sched show | cap <N>|--clear | weight <owner/repo> <N>|--clear", "Show the host lane registry, set the host-wide lane cap shared by orchestrate batches, or set a repo's share of slots within a priority (default equal)"],
@@ -3946,7 +3947,7 @@ async function watch() {
3946
3947
  const SPLASH_COMMANDS = new Set(["run", "bootstrap", "run-issue", "orchestrate", "triage", "probe", "gc", "resume-stalled", "demo", "doctor", "watch", "help", "--help", "-h"]);
3947
3948
  const KNOWN_COMMANDS = new Set([
3948
3949
  "install", "init", "enable", "disable", "status", "uninstall", "update",
3949
- "run", "bootstrap", "backend-check", "run-issue", "orchestrate", "milestone", "milestone-sync", "guards", "triage", "probe", "review-staging", "gc", "resume-stalled", "demo", "doctor", "watch", "issue-watch", "labels", "config", "stats", "limits", "decisions", "sched", "schedd", "pool", "check", "help", "--help", "-h",
3950
+ "run", "bootstrap", "backend-check", "run-issue", "orchestrate", "milestone", "milestone-sync", "guards", "triage", "probe", "review-staging", "gc", "resume-stalled", "demo", "doctor", "watch", "issue-watch", "prod-watch", "labels", "config", "stats", "limits", "decisions", "sched", "schedd", "pool", "check", "help", "--help", "-h",
3950
3951
  "version", "--version", "-v",
3951
3952
  ]);
3952
3953
  if (SPLASH_COMMANDS.has(command) || !KNOWN_COMMANDS.has(command)) splash(command);
@@ -4217,6 +4218,16 @@ switch (command) {
4217
4218
  }
4218
4219
  break;
4219
4220
  }
4221
+ case "prod-watch": {
4222
+ const { prodWatchFromCli } = await import("./lean/prodwatch.mjs");
4223
+ try {
4224
+ await prodWatchFromCli(restArgs);
4225
+ } catch (err) {
4226
+ process.stderr.write(`${RED}${err.message}${RESET}\n`);
4227
+ exitCode = 1;
4228
+ }
4229
+ break;
4230
+ }
4220
4231
  case "limits": {
4221
4232
  const { limitsFromCli } = await import("./engine-cli.mjs");
4222
4233
  try {
@@ -538,7 +538,7 @@ describe("#4408: issues added to a running batch (forgedock orchestrate --add)",
538
538
  assert.deepEqual(b.readAdded(), [2, 3, 5]);
539
539
  assert.deepEqual(b.unrunAdds(), [2, 3, 5]);
540
540
  const views = [];
541
- const io = { gh: async (a) => { views.push(a); if (a[2] === "8") throw new Error("gh down"); return JSON.stringify({ state: a[2] === "7" ? "CLOSED" : "OPEN" }); } };
541
+ const io = { gh: async (a) => { if (a[0] === "api") return "OWNER\n"; views.push(a); if (a[2] === "8") throw new Error("gh down"); return JSON.stringify({ state: a[2] === "7" ? "CLOSED" : "OPEN" }); } };
542
542
  const res = await quiet(() => orchestrateFromCli(["--add", "b1", "6", "7", "8", "3", "x"], { batchRoot: root, io }));
543
543
  assert.deepEqual(res.added, [6]);
544
544
  assert.deepEqual(res.skipped.map((x) => [x.issue, x.reason]).sort(), [[3, "already in batch"], [7, "closed"], [8, "unreadable"], ["x", "not an integer"]]);
@@ -553,7 +553,7 @@ describe("#4408: issues added to a running batch (forgedock orchestrate --add)",
553
553
  b.writePlan({ argv: [], repo: "o/r" }); // no issues: the resume stops with the usage error after its adds
554
554
  b.store.save({ order: [1] });
555
555
  const views = [];
556
- const io = { gh: async (a) => { views.push(a[2]); return JSON.stringify({ state: "OPEN" }); } };
556
+ const io = { gh: async (a) => { if (a[0] === "api") return "OWNER\n"; views.push(a[2]); return JSON.stringify({ state: "OPEN" }); } };
557
557
  await assert.rejects(quiet(() => orchestrateFromCli(["--resume", "b1", "--parallel", "5"], { batchRoot: root, io })), /usage/);
558
558
  assert.deepEqual(views, [], "no issue read for a flag value");
559
559
  assert.deepEqual(b.readAdded(), []);
@@ -298,7 +298,7 @@ export function makeLeanIo({ repo, subagentModel = readSubagentModel() }) {
298
298
  const run = (bin) => async (args, opts = {}) => (await pexec(bin, args, { cwd: opts.cwd, maxBuffer: 100 * 1024 * 1024, timeout: 120000, ...(opts.env ? { env: { ...process.env, ...opts.env } } : {}) })).stdout;
299
299
  return {
300
300
  ghCache: (root) => makeGhCache({ root }),
301
- gh: run("gh"), git: run("git"), docker: run("docker"), killGroup: async (pid, marker) => killLedgerPid(pid, marker), ledger: (opts) => makeLedger(opts), runTmp: makeRunTmp(), host: makeHostProbe(), heavy: makeHeavyIo({ host: makeHostProbe() }), projector: makeProjector({ gh: base.gh }), runner: makeLeanRunner({ subagentModel }), waitCiGreen: base.waitCiGreen,
301
+ gh: run("gh"), git: run("git"), docker: run("docker"), killGroup: async (pid, marker) => killLedgerPid(pid, marker), ledger: (opts) => makeLedger(opts), runTmp: makeRunTmp(), host: makeHostProbe(), heavy: makeHeavyIo({ host: makeHostProbe() }), projector: makeProjector({ gh: base.gh }, { repo }), runner: makeLeanRunner({ subagentModel }), waitCiGreen: base.waitCiGreen,
302
302
  /** Shared dependency cache (#4220): the repo's own package manager, argv only, bounded by the cache's timeout. */
303
303
  deps: makeDepCache({ exec: async (cmd, args, { cwd, env, timeoutMs } = {}) => String((await pexec(cmd, args, { cwd, env: { ...process.env, ...(env ?? {}) }, timeout: timeoutMs ?? 0, maxBuffer: 100 * 1024 * 1024 })).stdout) }),
304
304
  /** Names of the checks the base branch requires: classic protection plus rulesets (#4158). [] when neither has any. */
@@ -0,0 +1,85 @@
1
+ {
2
+ "memos": 70,
3
+ "clusters": [
4
+ {
5
+ "name": "pg-conn",
6
+ "class": "defect",
7
+ "count": 15
8
+ },
9
+ {
10
+ "name": "api-5xx",
11
+ "class": "incident",
12
+ "count": 1
13
+ },
14
+ {
15
+ "name": "queue-lag",
16
+ "class": "defect",
17
+ "count": 1
18
+ },
19
+ {
20
+ "name": "cert",
21
+ "class": "defect",
22
+ "count": 1
23
+ },
24
+ {
25
+ "name": "disk",
26
+ "class": "defect",
27
+ "count": 1
28
+ },
29
+ {
30
+ "name": "deploy",
31
+ "class": "deploy-fallout",
32
+ "count": 1
33
+ },
34
+ {
35
+ "name": "mon-bug",
36
+ "class": "monitoring-bug",
37
+ "count": 1
38
+ },
39
+ {
40
+ "name": "mem-leak",
41
+ "class": "defect",
42
+ "count": 1
43
+ },
44
+ {
45
+ "name": "t1",
46
+ "class": "transient",
47
+ "count": 1
48
+ },
49
+ {
50
+ "name": "t2",
51
+ "class": "transient",
52
+ "count": 1
53
+ },
54
+ {
55
+ "name": "t3",
56
+ "class": "transient",
57
+ "count": 1
58
+ },
59
+ {
60
+ "name": "t4",
61
+ "class": "transient",
62
+ "count": 1
63
+ },
64
+ {
65
+ "name": "e1",
66
+ "class": "expected-class",
67
+ "count": 11
68
+ },
69
+ {
70
+ "name": "e2",
71
+ "class": "expected-class",
72
+ "count": 11
73
+ },
74
+ {
75
+ "name": "e3",
76
+ "class": "expected-class",
77
+ "count": 11
78
+ },
79
+ {
80
+ "name": "e4",
81
+ "class": "expected-class",
82
+ "count": 11
83
+ }
84
+ ]
85
+ }
@@ -0,0 +1,282 @@
1
+ [
2
+ {
3
+ "id": "memo-001",
4
+ "text": "# Postgres connection pool exhausted on db-2.internal.example\nseverity: P1\nowner: db\nseen: 2026-10-29T01:01:00Z\n\nPool at 17 of 17 connections, request 3c6ef362 waited 17s; queries are queuing.\n"
5
+ },
6
+ {
7
+ "id": "memo-002",
8
+ "text": "# Postgres connection pool exhausted on db-3.internal.example\nseverity: P1\nowner: db\nseen: 2026-10-29T02:02:00Z\n\nPool at 24 of 24 connections, request 78dde6c4 waited 24s; queries are queuing.\n"
9
+ },
10
+ {
11
+ "id": "memo-003",
12
+ "text": "# Postgres connection pool exhausted on db-4.internal.example\nseverity: P1\nowner: db\nseen: 2026-10-29T03:03:00Z\n\nPool at 31 of 31 connections, request b54cda26 waited 31s; queries are queuing.\n"
13
+ },
14
+ {
15
+ "id": "memo-004",
16
+ "text": "# Postgres connection pool exhausted on db-5.internal.example\nseverity: P1\nowner: db\nseen: 2026-10-29T04:04:00Z\n\nPool at 38 of 38 connections, request f1bbcd88 waited 38s; queries are queuing.\n"
17
+ },
18
+ {
19
+ "id": "memo-005",
20
+ "text": "# Postgres connection pool exhausted on db-1.internal.example\nseverity: P1\nowner: db\nseen: 2026-10-29T05:05:00Z\n\nPool at 45 of 45 connections, request 2e2ac0ea waited 45s; queries are queuing.\n"
21
+ },
22
+ {
23
+ "id": "memo-006",
24
+ "text": "# Postgres connection pool exhausted on db-2.internal.example\nseverity: P1\nowner: db\nseen: 2026-10-29T06:06:00Z\n\nPool at 52 of 52 connections, request 6a99b44c waited 52s; queries are queuing.\n"
25
+ },
26
+ {
27
+ "id": "memo-007",
28
+ "text": "# Postgres connection pool exhausted on db-3.internal.example\nseverity: P1\nowner: db\nseen: 2026-10-29T07:07:00Z\n\nPool at 59 of 59 connections, request a708a7ae waited 59s; queries are queuing.\n"
29
+ },
30
+ {
31
+ "id": "memo-008",
32
+ "text": "# Postgres connection pool exhausted on db-4.internal.example\nseverity: P1\nowner: db\nseen: 2026-10-29T08:08:00Z\n\nPool at 66 of 66 connections, request e3779b10 waited 66s; queries are queuing.\n"
33
+ },
34
+ {
35
+ "id": "memo-009",
36
+ "text": "# Postgres connection pool exhausted on db-5.internal.example\nseverity: P1\nowner: db\nseen: 2026-10-29T09:09:00Z\n\nPool at 73 of 73 connections, request 1fe68e72 waited 73s; queries are queuing.\n"
37
+ },
38
+ {
39
+ "id": "memo-010",
40
+ "text": "# Postgres connection pool exhausted on db-1.internal.example\nseverity: P1\nowner: db\nseen: 2026-10-29T10:10:00Z\n\nPool at 80 of 80 connections, request 5c5581d4 waited 80s; queries are queuing.\n"
41
+ },
42
+ {
43
+ "id": "memo-011",
44
+ "text": "# Postgres connection pool exhausted on db-2.internal.example\nseverity: P1\nowner: db\nseen: 2026-10-29T11:11:00Z\n\nPool at 87 of 87 connections, request 98c47536 waited 87s; queries are queuing.\n"
45
+ },
46
+ {
47
+ "id": "memo-012",
48
+ "text": "# Postgres connection pool exhausted on db-3.internal.example\nseverity: P1\nowner: db\nseen: 2026-10-29T12:12:00Z\n\nPool at 94 of 94 connections, request d5336898 waited 94s; queries are queuing.\n"
49
+ },
50
+ {
51
+ "id": "memo-013",
52
+ "text": "# Postgres connection pool exhausted on db-4.internal.example\nseverity: P1\nowner: db\nseen: 2026-10-29T13:13:00Z\n\nPool at 101 of 101 connections, request 11a25bfa waited 101s; queries are queuing.\n"
53
+ },
54
+ {
55
+ "id": "memo-014",
56
+ "text": "# Postgres connection pool exhausted on db-5.internal.example\nseverity: P1\nowner: db\nseen: 2026-10-29T14:14:00Z\n\nPool at 108 of 108 connections, request 4e114f5c waited 108s; queries are queuing.\n"
57
+ },
58
+ {
59
+ "id": "memo-015",
60
+ "text": "# Postgres connection pool exhausted on db-1.internal.example\nseverity: P1\nowner: db\nseen: 2026-10-29T15:15:00Z\n\nPool at 115 of 115 connections, request 8a8042be waited 115s; queries are queuing.\n"
61
+ },
62
+ {
63
+ "id": "memo-016",
64
+ "text": "# Public API unavailable for tenant group 38\nseverity: P0\nowner: api\nseen: 2026-10-29T04:04:00Z\n\nService down: gateway returned 503 for 38 requests, trace 5c5581d4.\n"
65
+ },
66
+ {
67
+ "id": "memo-017",
68
+ "text": "# Job queue lag growing in worker pool\nseverity: P2\nowner: jobs\nseen: 2026-10-29T07:07:00Z\n\nOldest job is 59 minutes old, worker d5336898 idle.\n"
69
+ },
70
+ {
71
+ "id": "memo-018",
72
+ "text": "# TLS certificate for edge host nearing expiry\nseverity: P2\nowner: infra\nseen: 2026-10-29T10:10:00Z\n\nCertificate expires in 80 days on db-1.internal.example.\n"
73
+ },
74
+ {
75
+ "id": "memo-019",
76
+ "text": "# Disk usage above threshold on storage node\nseverity: P1\nowner: infra\nseen: 2026-10-29T13:13:00Z\n\nVolume at 101 percent on db-4.internal.example.\n"
77
+ },
78
+ {
79
+ "id": "memo-020",
80
+ "text": "# Error rate up after the deploy of release v1.122\nseverity: P1\nowner: web\nseen: 2026-10-29T16:16:00Z\n\nErrors in checkout handler 3fcd1ce4 began after the deploy.\n"
81
+ },
82
+ {
83
+ "id": "memo-021",
84
+ "text": "# Disk alert is a false positive\nseverity: P2\nowner: obs\nseen: 2026-10-29T19:19:00Z\n\nAlert fires although the metric source b8ab03a8 reports a stale metric of 143.\n"
85
+ },
86
+ {
87
+ "id": "memo-022",
88
+ "text": "# Memory growth in indexer process\nseverity: P2\nowner: search\nseen: 2026-10-29T22:22:00Z\n\nRSS grew 164 MB over the last hour, pid 3188ea6c.\n"
89
+ },
90
+ {
91
+ "id": "memo-023",
92
+ "text": "# Transient timeout calling vendor API\nseverity: P3\nowner: api\nseen: 2026-10-29T01:25:00Z\n\nRetry succeeded after 185ms, request aa66d130.\n"
93
+ },
94
+ {
95
+ "id": "memo-024",
96
+ "text": "# Transient DNS lookup failure\nseverity: P3\nowner: infra\nseen: 2026-10-29T04:28:00Z\n\nSelf-recovered after 206 seconds on db-4.internal.example.\n"
97
+ },
98
+ {
99
+ "id": "memo-025",
100
+ "text": "# Transient lock wait on billing table\nseverity: P3\nowner: db\nseen: 2026-10-29T07:31:00Z\n\nRetry succeeded, wait 227ms, tx 9c229eb8.\n"
101
+ },
102
+ {
103
+ "id": "memo-026",
104
+ "text": "# Transient cache eviction spike\nseverity: P3\nowner: cache\nseen: 2026-10-29T10:34:00Z\n\nSelf-recovered within 248 seconds, node 1500857c.\n"
105
+ },
106
+ {
107
+ "id": "memo-027",
108
+ "text": "# Nightly backup completed\nseverity: P3\nowner: db\nseen: 2026-10-29T13:37:00Z\n\nExpected: backup 8dde6c40 completed in 269 minutes.\n"
109
+ },
110
+ {
111
+ "id": "memo-028",
112
+ "text": "# Nightly backup completed\nseverity: P3\nowner: db\nseen: 2026-10-29T14:38:00Z\n\nExpected: backup ca4d5fa2 completed in 276 minutes.\n"
113
+ },
114
+ {
115
+ "id": "memo-029",
116
+ "text": "# Nightly backup completed\nseverity: P3\nowner: db\nseen: 2026-10-29T15:39:00Z\n\nExpected: backup 06bc5304 completed in 283 minutes.\n"
117
+ },
118
+ {
119
+ "id": "memo-030",
120
+ "text": "# Nightly backup completed\nseverity: P3\nowner: db\nseen: 2026-10-29T16:40:00Z\n\nExpected: backup 432b4666 completed in 290 minutes.\n"
121
+ },
122
+ {
123
+ "id": "memo-031",
124
+ "text": "# Nightly backup completed\nseverity: P3\nowner: db\nseen: 2026-10-29T17:41:00Z\n\nExpected: backup 7f9a39c8 completed in 297 minutes.\n"
125
+ },
126
+ {
127
+ "id": "memo-032",
128
+ "text": "# Nightly backup completed\nseverity: P3\nowner: db\nseen: 2026-10-29T18:42:00Z\n\nExpected: backup bc092d2a completed in 304 minutes.\n"
129
+ },
130
+ {
131
+ "id": "memo-033",
132
+ "text": "# Nightly backup completed\nseverity: P3\nowner: db\nseen: 2026-10-29T19:43:00Z\n\nExpected: backup f878208c completed in 311 minutes.\n"
133
+ },
134
+ {
135
+ "id": "memo-034",
136
+ "text": "# Nightly backup completed\nseverity: P3\nowner: db\nseen: 2026-10-29T20:44:00Z\n\nExpected: backup 34e713ee completed in 318 minutes.\n"
137
+ },
138
+ {
139
+ "id": "memo-035",
140
+ "text": "# Nightly backup completed\nseverity: P3\nowner: db\nseen: 2026-10-29T21:45:00Z\n\nExpected: backup 71560750 completed in 325 minutes.\n"
141
+ },
142
+ {
143
+ "id": "memo-036",
144
+ "text": "# Nightly backup completed\nseverity: P3\nowner: db\nseen: 2026-10-29T22:46:00Z\n\nExpected: backup adc4fab2 completed in 332 minutes.\n"
145
+ },
146
+ {
147
+ "id": "memo-037",
148
+ "text": "# Nightly backup completed\nseverity: P3\nowner: db\nseen: 2026-10-29T23:47:00Z\n\nExpected: backup ea33ee14 completed in 339 minutes.\n"
149
+ },
150
+ {
151
+ "id": "memo-038",
152
+ "text": "# Scheduled maintenance window heartbeat\nseverity: P3\nowner: infra\nseen: 2026-10-29T16:40:00Z\n\nRoutine heartbeat 34e713ee from db-1.internal.example, no action needed.\n"
153
+ },
154
+ {
155
+ "id": "memo-039",
156
+ "text": "# Scheduled maintenance window heartbeat\nseverity: P3\nowner: infra\nseen: 2026-10-29T17:41:00Z\n\nRoutine heartbeat 71560750 from db-2.internal.example, no action needed.\n"
157
+ },
158
+ {
159
+ "id": "memo-040",
160
+ "text": "# Scheduled maintenance window heartbeat\nseverity: P3\nowner: infra\nseen: 2026-10-29T18:42:00Z\n\nRoutine heartbeat adc4fab2 from db-3.internal.example, no action needed.\n"
161
+ },
162
+ {
163
+ "id": "memo-041",
164
+ "text": "# Scheduled maintenance window heartbeat\nseverity: P3\nowner: infra\nseen: 2026-10-29T19:43:00Z\n\nRoutine heartbeat ea33ee14 from db-4.internal.example, no action needed.\n"
165
+ },
166
+ {
167
+ "id": "memo-042",
168
+ "text": "# Scheduled maintenance window heartbeat\nseverity: P3\nowner: infra\nseen: 2026-10-29T20:44:00Z\n\nRoutine heartbeat 26a2e176 from db-5.internal.example, no action needed.\n"
169
+ },
170
+ {
171
+ "id": "memo-043",
172
+ "text": "# Scheduled maintenance window heartbeat\nseverity: P3\nowner: infra\nseen: 2026-10-29T21:45:00Z\n\nRoutine heartbeat 6311d4d8 from db-1.internal.example, no action needed.\n"
173
+ },
174
+ {
175
+ "id": "memo-044",
176
+ "text": "# Scheduled maintenance window heartbeat\nseverity: P3\nowner: infra\nseen: 2026-10-29T22:46:00Z\n\nRoutine heartbeat 9f80c83a from db-2.internal.example, no action needed.\n"
177
+ },
178
+ {
179
+ "id": "memo-045",
180
+ "text": "# Scheduled maintenance window heartbeat\nseverity: P3\nowner: infra\nseen: 2026-10-29T23:47:00Z\n\nRoutine heartbeat dbefbb9c from db-3.internal.example, no action needed.\n"
181
+ },
182
+ {
183
+ "id": "memo-046",
184
+ "text": "# Scheduled maintenance window heartbeat\nseverity: P3\nowner: infra\nseen: 2026-10-29T00:48:00Z\n\nRoutine heartbeat 185eaefe from db-4.internal.example, no action needed.\n"
185
+ },
186
+ {
187
+ "id": "memo-047",
188
+ "text": "# Scheduled maintenance window heartbeat\nseverity: P3\nowner: infra\nseen: 2026-10-29T01:49:00Z\n\nRoutine heartbeat 54cda260 from db-5.internal.example, no action needed.\n"
189
+ },
190
+ {
191
+ "id": "memo-048",
192
+ "text": "# Scheduled maintenance window heartbeat\nseverity: P3\nowner: infra\nseen: 2026-10-29T02:50:00Z\n\nRoutine heartbeat 913c95c2 from db-1.internal.example, no action needed.\n"
193
+ },
194
+ {
195
+ "id": "memo-049",
196
+ "text": "# Expected rate-limit rejections from load test\nseverity: P3\nowner: api\nseen: 2026-10-29T19:43:00Z\n\nRoutine: 311 requests rejected, client dbefbb9c.\n"
197
+ },
198
+ {
199
+ "id": "memo-050",
200
+ "text": "# Expected rate-limit rejections from load test\nseverity: P3\nowner: api\nseen: 2026-10-29T20:44:00Z\n\nRoutine: 318 requests rejected, client 185eaefe.\n"
201
+ },
202
+ {
203
+ "id": "memo-051",
204
+ "text": "# Expected rate-limit rejections from load test\nseverity: P3\nowner: api\nseen: 2026-10-29T21:45:00Z\n\nRoutine: 325 requests rejected, client 54cda260.\n"
205
+ },
206
+ {
207
+ "id": "memo-052",
208
+ "text": "# Expected rate-limit rejections from load test\nseverity: P3\nowner: api\nseen: 2026-10-29T22:46:00Z\n\nRoutine: 332 requests rejected, client 913c95c2.\n"
209
+ },
210
+ {
211
+ "id": "memo-053",
212
+ "text": "# Expected rate-limit rejections from load test\nseverity: P3\nowner: api\nseen: 2026-10-29T23:47:00Z\n\nRoutine: 339 requests rejected, client cdab8924.\n"
213
+ },
214
+ {
215
+ "id": "memo-054",
216
+ "text": "# Expected rate-limit rejections from load test\nseverity: P3\nowner: api\nseen: 2026-10-29T00:48:00Z\n\nRoutine: 346 requests rejected, client 0a1a7c86.\n"
217
+ },
218
+ {
219
+ "id": "memo-055",
220
+ "text": "# Expected rate-limit rejections from load test\nseverity: P3\nowner: api\nseen: 2026-10-29T01:49:00Z\n\nRoutine: 353 requests rejected, client 46896fe8.\n"
221
+ },
222
+ {
223
+ "id": "memo-056",
224
+ "text": "# Expected rate-limit rejections from load test\nseverity: P3\nowner: api\nseen: 2026-10-29T02:50:00Z\n\nRoutine: 360 requests rejected, client 82f8634a.\n"
225
+ },
226
+ {
227
+ "id": "memo-057",
228
+ "text": "# Expected rate-limit rejections from load test\nseverity: P3\nowner: api\nseen: 2026-10-29T03:51:00Z\n\nRoutine: 367 requests rejected, client bf6756ac.\n"
229
+ },
230
+ {
231
+ "id": "memo-058",
232
+ "text": "# Expected rate-limit rejections from load test\nseverity: P3\nowner: api\nseen: 2026-10-29T04:52:00Z\n\nRoutine: 374 requests rejected, client fbd64a0e.\n"
233
+ },
234
+ {
235
+ "id": "memo-059",
236
+ "text": "# Expected rate-limit rejections from load test\nseverity: P3\nowner: api\nseen: 2026-10-29T05:53:00Z\n\nRoutine: 381 requests rejected, client 38453d70.\n"
237
+ },
238
+ {
239
+ "id": "memo-060",
240
+ "text": "# Scheduled report job finished\nseverity: P3\nowner: data\nseen: 2026-10-29T22:46:00Z\n\nExpected output size 332 rows, run 82f8634a.\n"
241
+ },
242
+ {
243
+ "id": "memo-061",
244
+ "text": "# Scheduled report job finished\nseverity: P3\nowner: data\nseen: 2026-10-29T23:47:00Z\n\nExpected output size 339 rows, run bf6756ac.\n"
245
+ },
246
+ {
247
+ "id": "memo-062",
248
+ "text": "# Scheduled report job finished\nseverity: P3\nowner: data\nseen: 2026-10-29T00:48:00Z\n\nExpected output size 346 rows, run fbd64a0e.\n"
249
+ },
250
+ {
251
+ "id": "memo-063",
252
+ "text": "# Scheduled report job finished\nseverity: P3\nowner: data\nseen: 2026-10-29T01:49:00Z\n\nExpected output size 353 rows, run 38453d70.\n"
253
+ },
254
+ {
255
+ "id": "memo-064",
256
+ "text": "# Scheduled report job finished\nseverity: P3\nowner: data\nseen: 2026-10-29T02:50:00Z\n\nExpected output size 360 rows, run 74b430d2.\n"
257
+ },
258
+ {
259
+ "id": "memo-065",
260
+ "text": "# Scheduled report job finished\nseverity: P3\nowner: data\nseen: 2026-10-29T03:51:00Z\n\nExpected output size 367 rows, run b1232434.\n"
261
+ },
262
+ {
263
+ "id": "memo-066",
264
+ "text": "# Scheduled report job finished\nseverity: P3\nowner: data\nseen: 2026-10-29T04:52:00Z\n\nExpected output size 374 rows, run ed921796.\n"
265
+ },
266
+ {
267
+ "id": "memo-067",
268
+ "text": "# Scheduled report job finished\nseverity: P3\nowner: data\nseen: 2026-10-29T05:53:00Z\n\nExpected output size 381 rows, run 2a010af8.\n"
269
+ },
270
+ {
271
+ "id": "memo-068",
272
+ "text": "# Scheduled report job finished\nseverity: P3\nowner: data\nseen: 2026-10-29T06:54:00Z\n\nExpected output size 388 rows, run 666ffe5a.\n"
273
+ },
274
+ {
275
+ "id": "memo-069",
276
+ "text": "# Scheduled report job finished\nseverity: P3\nowner: data\nseen: 2026-10-29T07:55:00Z\n\nExpected output size 395 rows, run a2def1bc.\n"
277
+ },
278
+ {
279
+ "id": "memo-070",
280
+ "text": "# Scheduled report job finished\nseverity: P3\nowner: data\nseen: 2026-10-29T08:56:00Z\n\nExpected output size 402 rows, run df4de51e.\n"
281
+ }
282
+ ]
@@ -0,0 +1,37 @@
1
+ // SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+ // #4859: `gh issue view|list --json` has no `authorAssociation` field (gh 2.97: `Unknown JSON field`); it exists on
4
+ // comments and in REST as `author_association`. Trust reads take it from REST and fail closed (undefined = untrusted).
5
+
6
+ /** The fields `gh issue view|list --json` lists in gh 2.97. `authorAssociation` is deliberately absent. */
7
+ export const ISSUE_JSON_FIELDS = Object.freeze([
8
+ "assignees", "author", "body", "closed", "closedAt", "closedByPullRequestsReferences", "comments", "createdAt", "id",
9
+ "isPinned", "labels", "milestone", "number", "projectCards", "projectItems", "reactionGroups", "state", "stateReason",
10
+ "title", "updatedAt", "url",
11
+ ]);
12
+
13
+ const repoPath = (repo) => repo || "{owner}/{repo}"; // gh api fills the placeholders from the cwd checkout
14
+
15
+ /** The issue author's association (OWNER, MEMBER, ...) via REST, or undefined when it cannot be read. */
16
+ export async function issueAssociation(gh, repo, n) {
17
+ try { return String(await gh(["api", `repos/${repoPath(repo)}/issues/${n}`, "--jq", ".author_association"])).trim() || undefined; } catch { return undefined; }
18
+ }
19
+
20
+ /** Map issue number to author association for the repo's open issues (one paginated REST list); empty on failure. */
21
+ export async function listAssociations(gh, repo) {
22
+ const out = new Map();
23
+ try {
24
+ const text = String(await gh(["api", `repos/${repoPath(repo)}/issues?state=open&per_page=100`, "--paginate", "--jq", '.[] | "\\(.number)\\t\\(.author_association)"']));
25
+ for (const line of text.split("\n")) { const [n, a] = line.trim().split("\t"); if (/^\d+$/.test(n) && a) out.set(Number(n), a); }
26
+ } catch { /* unreadable: every author is untrusted */ }
27
+ return out;
28
+ }
29
+
30
+ /** `gh issue view --json <fields>` parsed, with `authorAssociation` merged in from REST. A failed view throws; a failed association leaves it unset. */
31
+ export async function viewIssue(gh, repo, n, fields) {
32
+ const [raw, authorAssociation] = await Promise.all([
33
+ gh(["issue", "view", String(n), ...(repo ? ["-R", repo] : []), "--json", fields]),
34
+ issueAssociation(gh, repo, n),
35
+ ]);
36
+ return { ...JSON.parse(raw), authorAssociation };
37
+ }