agent-control-plane 0.1.13 → 0.1.16

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/SKILL.md CHANGED
@@ -22,7 +22,7 @@ external profile registry, not inside this repository.
22
22
  - installed project profiles in `~/.agent-runtime/control-plane/profiles/*/control-plane.yaml`
23
23
  - installed profile notes in `~/.agent-runtime/control-plane/profiles/*/README.md`
24
24
  - workflow catalog in `assets/workflow-catalog.json`
25
- - worker dashboard in `tools/dashboard/` with launchers in `tools/bin/render-dashboard-snapshot.py`
25
+ - worker dashboard in `tools/dashboard/` with launcher at `tools/dashboard/dashboard_snapshot.py`
26
26
  and `tools/bin/serve-dashboard.sh`
27
27
  - dashboard autostart helpers in `tools/bin/dashboard-launchd-bootstrap.sh` and
28
28
  `tools/bin/install-dashboard-launchd.sh`
@@ -95,6 +95,7 @@ heartbeat_open_agent_pr_issue_ids() {
95
95
  local pr_issue_ids_json=""
96
96
  pr_issue_ids_json="$(
97
97
  flow_github_pr_list_json "$REPO_SLUG" open 100 \
98
+ 2>/dev/null \
98
99
  | jq --argjson agentPrPrefixes "${AGENT_PR_PREFIXES_JSON}" --arg handoffLabel "${AGENT_PR_HANDOFF_LABEL}" --arg branchIssueRegex "${AGENT_PR_ISSUE_CAPTURE_REGEX}" '
99
100
  map(
100
101
  . as $pr
@@ -119,7 +120,7 @@ heartbeat_open_agent_pr_issue_ids() {
119
120
  | select(. != null and . != "")
120
121
  )
121
122
  | unique
122
- '
123
+ ' 2>/dev/null || true
123
124
  )"
124
125
 
125
126
  if [[ -z "${pr_issue_ids_json:-}" ]]; then
@@ -136,6 +137,7 @@ heartbeat_list_ready_issue_ids() {
136
137
 
137
138
  ready_issue_rows="$(
138
139
  flow_github_issue_list_json "$REPO_SLUG" open 100 \
140
+ 2>/dev/null \
139
141
  | jq -r --argjson openAgentPrIssueIds "${open_agent_pr_issue_ids}" '
140
142
  map(select(
141
143
  (any(.labels[]?; .name == "agent-running") | not)
@@ -145,7 +147,7 @@ heartbeat_list_ready_issue_ids() {
145
147
  | .[]
146
148
  | [.number, (any(.labels[]?; .name == "agent-blocked"))]
147
149
  | @tsv
148
- '
150
+ ' 2>/dev/null || true
149
151
  )"
150
152
 
151
153
  while IFS=$'\t' read -r issue_id is_blocked; do
@@ -170,6 +172,7 @@ heartbeat_list_blocked_recovery_issue_ids() {
170
172
 
171
173
  blocked_issue_rows="$(
172
174
  flow_github_issue_list_json "$REPO_SLUG" open 100 \
175
+ 2>/dev/null \
173
176
  | jq -r --argjson openAgentPrIssueIds "${open_agent_pr_issue_ids}" '
174
177
  map(select(
175
178
  any(.labels[]?; .name == "agent-blocked")
@@ -178,7 +181,7 @@ heartbeat_list_blocked_recovery_issue_ids() {
178
181
  ))
179
182
  | sort_by(.createdAt, .number)
180
183
  | .[].number
181
- '
184
+ ' 2>/dev/null || true
182
185
  )"
183
186
 
184
187
  while IFS= read -r issue_id; do
@@ -268,6 +271,7 @@ heartbeat_list_exclusive_issue_ids() {
268
271
  open_agent_pr_issue_ids="$(heartbeat_open_agent_pr_issue_ids)"
269
272
 
270
273
  flow_github_issue_list_json "$REPO_SLUG" open 100 \
274
+ 2>/dev/null \
271
275
  | jq -r --arg exclusiveLabel "${AGENT_EXCLUSIVE_LABEL}" --argjson openAgentPrIssueIds "${open_agent_pr_issue_ids}" '
272
276
  map(select(
273
277
  any(.labels[]?; .name == $exclusiveLabel)
@@ -277,20 +281,22 @@ heartbeat_list_exclusive_issue_ids() {
277
281
  ))
278
282
  | sort_by(.createdAt, .number)
279
283
  | .[].number
280
- '
284
+ ' 2>/dev/null || true
281
285
  }
282
286
 
283
287
  heartbeat_list_running_issue_ids() {
284
288
  flow_github_issue_list_json "$REPO_SLUG" open 100 \
289
+ 2>/dev/null \
285
290
  | jq -r '
286
291
  map(select(any(.labels[]?; .name == "agent-running")))
287
292
  | sort_by(.createdAt, .number)
288
293
  | .[].number
289
- '
294
+ ' 2>/dev/null || true
290
295
  }
291
296
 
292
297
  heartbeat_list_open_agent_pr_ids() {
293
298
  flow_github_pr_list_json "$REPO_SLUG" open 100 \
299
+ 2>/dev/null \
294
300
  | jq -r --argjson agentPrPrefixes "${AGENT_PR_PREFIXES_JSON}" --arg handoffLabel "${AGENT_PR_HANDOFF_LABEL}" '
295
301
  map(select(
296
302
  . as $pr
@@ -302,11 +308,12 @@ heartbeat_list_open_agent_pr_ids() {
302
308
  ))
303
309
  | sort_by(.createdAt)
304
310
  | .[].number
305
- '
311
+ ' 2>/dev/null || true
306
312
  }
307
313
 
308
314
  heartbeat_list_exclusive_pr_ids() {
309
315
  flow_github_pr_list_json "$REPO_SLUG" open 100 \
316
+ 2>/dev/null \
310
317
  | jq -r --argjson agentPrPrefixes "${AGENT_PR_PREFIXES_JSON}" --arg handoffLabel "${AGENT_PR_HANDOFF_LABEL}" --arg exclusiveLabel "${AGENT_EXCLUSIVE_LABEL}" '
311
318
  map(select(
312
319
  . as $pr
@@ -319,7 +326,7 @@ heartbeat_list_exclusive_pr_ids() {
319
326
  ))
320
327
  | sort_by(.createdAt)
321
328
  | .[].number
322
- '
329
+ ' 2>/dev/null || true
323
330
  }
324
331
 
325
332
  heartbeat_issue_is_heavy() {
@@ -438,9 +445,9 @@ heartbeat_mark_issue_running() {
438
445
  local issue_id="${1:?issue id required}"
439
446
  local is_heavy="${2:-no}"
440
447
  if [[ "$is_heavy" == "yes" ]]; then
441
- bash "${FLOW_TOOLS_DIR}/agent-github-update-labels" --repo-slug "$REPO_SLUG" --number "$issue_id" --remove agent-ready --remove agent-blocked --add agent-running --add agent-e2e-heavy >/dev/null
448
+ bash "${FLOW_TOOLS_DIR}/agent-github-update-labels" --repo-slug "$REPO_SLUG" --number "$issue_id" --remove agent-ready --remove agent-blocked --add agent-running --add agent-e2e-heavy >/dev/null || true
442
449
  else
443
- bash "${FLOW_TOOLS_DIR}/agent-github-update-labels" --repo-slug "$REPO_SLUG" --number "$issue_id" --remove agent-ready --remove agent-blocked --add agent-running >/dev/null
450
+ bash "${FLOW_TOOLS_DIR}/agent-github-update-labels" --repo-slug "$REPO_SLUG" --number "$issue_id" --remove agent-ready --remove agent-blocked --add agent-running >/dev/null || true
444
451
  fi
445
452
  }
446
453
 
@@ -22,6 +22,7 @@ Commands:
22
22
  help Show this help
23
23
  version Print package version
24
24
  setup Guided setup flow for one repo profile
25
+ onboard Alias for setup
25
26
  sync Publish the packaged runtime into ~/.agent-runtime
26
27
  install Alias for sync
27
28
  init Scaffold and adopt a project profile
@@ -503,6 +504,20 @@ function createPromptInterface() {
503
504
  });
504
505
  }
505
506
 
507
+ function printWizardBanner() {
508
+ console.log("============================================================");
509
+ console.log(" Agent Control Plane — Setup Wizard");
510
+ console.log("============================================================");
511
+ console.log("");
512
+ console.log("This wizard will guide you through setting up one repo profile.");
513
+ console.log("Press Enter at any prompt to accept the value shown in [brackets].");
514
+ console.log("");
515
+ }
516
+
517
+ function printWizardStep(step, total, title) {
518
+ console.log(`\n[${step}/${total}] ${title}`);
519
+ }
520
+
506
521
  function question(rl, prompt) {
507
522
  return new Promise((resolve) => rl.question(prompt, resolve));
508
523
  }
@@ -1341,7 +1356,9 @@ function printSetupDryRunPlan(context, config, plan) {
1341
1356
  }
1342
1357
  console.log(`- GitHub auth step: ${plan.githubAuthAction.status}${plan.githubAuthAction.reason ? ` (${plan.githubAuthAction.reason})` : ""}`);
1343
1358
  console.log(`- runtime start: ${plan.runtimeStartAction.status}${plan.runtimeStartAction.reason ? ` (${plan.runtimeStartAction.reason})` : ""}`);
1344
- console.log(`- launchd install: ${plan.launchdAction.status}${plan.launchdAction.reason ? ` (${plan.launchdAction.reason})` : ""}`);
1359
+ if (process.platform === "darwin") {
1360
+ console.log(`- launchd install: ${plan.launchdAction.status}${plan.launchdAction.reason ? ` (${plan.launchdAction.reason})` : ""}`);
1361
+ }
1345
1362
  }
1346
1363
 
1347
1364
  function buildSetupResultPayload(params) {
@@ -1457,6 +1474,23 @@ async function maybeRunFinalSetupFixups(options, scopedContext, config, currentS
1457
1474
  console.log(`- ${issue}`);
1458
1475
  }
1459
1476
 
1477
+ // Always show actionable hints so operators know what to fix,
1478
+ // even when running non-interactively (--yes / --json / CI).
1479
+ if (!currentState.prereq.coreToolsOk) {
1480
+ const missing = currentState.prereq.missingRequired.join(", ");
1481
+ console.log(` Fix: install missing core tools (${missing})`);
1482
+ }
1483
+ if (!currentState.prereq.workerAvailable) {
1484
+ const worker = currentState.prereq.workerCommand;
1485
+ if (worker === "codex") console.log(" Fix: npm install -g @openai/codex && codex login");
1486
+ else if (worker === "openclaw") console.log(" Fix: npm install -g openclaw && openclaw setup");
1487
+ else if (worker === "claude") console.log(" Fix: npm install -g @anthropic-ai/claude-code && claude auth login");
1488
+ else console.log(` Fix: install ${worker} and add it to PATH`);
1489
+ }
1490
+ if (!currentState.prereq.ghAuthOk) {
1491
+ console.log(" Fix: run gh auth login");
1492
+ }
1493
+
1460
1494
  if (!options.interactive) {
1461
1495
  return {
1462
1496
  status: "skipped",
@@ -1597,19 +1631,20 @@ async function collectSetupConfig(options, context) {
1597
1631
  throw new Error("setup could not detect --repo-slug automatically; pass --repo-slug <owner/repo> or run interactively inside a git checkout with origin set");
1598
1632
  }
1599
1633
  } else {
1634
+ printWizardBanner();
1600
1635
  const rl = createPromptInterface();
1601
1636
  try {
1602
- console.log("ACP setup will guide one repo profile from install to operator-ready defaults.");
1603
- console.log("Press Enter to accept the suggested value shown in brackets.\n");
1637
+ printWizardStep(1, 4, "Project details");
1604
1638
 
1605
1639
  repoRoot = path.resolve(await promptText(rl, "Local repo root", detectedRepoRoot));
1606
1640
  repoSlug = await promptText(rl, "GitHub repo slug", repoSlug || "");
1607
1641
  profileId = sanitizeProfileId(await promptText(rl, "Profile id", profileId));
1608
- codingWorker = await promptText(rl, "Coding worker (codex|claude|openclaw)", codingWorker);
1609
1642
 
1610
- if (!["codex", "claude", "openclaw"].includes(codingWorker)) {
1611
- throw new Error(`unsupported coding worker: ${codingWorker}`);
1643
+ let workerInput = codingWorker;
1644
+ while (!["codex", "claude", "openclaw"].includes(workerInput)) {
1645
+ workerInput = await promptText(rl, "Coding worker (codex / claude / openclaw)", codingWorker || "openclaw");
1612
1646
  }
1647
+ codingWorker = workerInput;
1613
1648
  } finally {
1614
1649
  rl.close();
1615
1650
  }
@@ -1630,6 +1665,9 @@ async function collectSetupConfig(options, context) {
1630
1665
  prereq
1631
1666
  };
1632
1667
 
1668
+ if (options.interactive) {
1669
+ printWizardStep(2, 4, "Review plan");
1670
+ }
1633
1671
  renderSetupSummary(config);
1634
1672
 
1635
1673
  if (options.interactive) {
@@ -1641,7 +1679,7 @@ async function collectSetupConfig(options, context) {
1641
1679
  }
1642
1680
  const shouldContinue = await promptYesNo(rl, "Continue with these values", true);
1643
1681
  if (!shouldContinue) {
1644
- throw new Error("setup cancelled");
1682
+ return null;
1645
1683
  }
1646
1684
  if (options.startRuntime === null) {
1647
1685
  options.startRuntime = await promptYesNo(rl, "Start the runtime after setup", true);
@@ -1733,6 +1771,10 @@ async function runSetupFlow(forwardedArgs) {
1733
1771
 
1734
1772
  try {
1735
1773
  const config = await collectSetupConfig(options, context);
1774
+ if (config === null) {
1775
+ console.log("\nSetup cancelled. Run again when you are ready.");
1776
+ return 0;
1777
+ }
1736
1778
  if (options.dryRun) {
1737
1779
  const plan = buildSetupDryRunPlan(options, context, config);
1738
1780
  printSetupDryRunPlan(context, config, plan);
@@ -1869,6 +1911,10 @@ async function runSetupFlow(forwardedArgs) {
1869
1911
  return 1;
1870
1912
  }
1871
1913
 
1914
+ if (options.interactive) {
1915
+ printWizardStep(3, 4, "Prerequisites");
1916
+ }
1917
+
1872
1918
  let prereq = config.prereq;
1873
1919
  let dependencyInstall = await maybeInstallMissingDependencies(options, prereq);
1874
1920
  if (dependencyInstall.status === "failed") {
@@ -1886,6 +1932,35 @@ async function runSetupFlow(forwardedArgs) {
1886
1932
  let workerSetupStep = await maybeShowWorkerSetupGuide(options, prereq);
1887
1933
  prereq = collectPrereqStatus(config.codingWorker);
1888
1934
 
1935
+ // Check OpenRouter API key when openclaw is selected
1936
+ if (config.codingWorker === "openclaw" && !process.env.OPENROUTER_API_KEY) {
1937
+ console.log("\nOpenClaw requires an OpenRouter API key (OPENROUTER_API_KEY).");
1938
+ console.log("- Get a free key at: https://openrouter.ai/keys");
1939
+ if (options.interactive) {
1940
+ const rl = createPromptInterface();
1941
+ let apiKey = "";
1942
+ try {
1943
+ apiKey = (await promptText(rl, "OpenRouter API key (Enter to skip)", "")).trim();
1944
+ } finally {
1945
+ rl.close();
1946
+ }
1947
+ if (apiKey) {
1948
+ process.env.OPENROUTER_API_KEY = apiKey;
1949
+ console.log("API key set for this session.");
1950
+ console.log("To persist it, add the following to your shell profile (~/.zshrc or ~/.bashrc):");
1951
+ console.log(` export OPENROUTER_API_KEY=${JSON.stringify(apiKey)}`);
1952
+ } else {
1953
+ console.log("Skipped. Set OPENROUTER_API_KEY before starting the runtime.");
1954
+ }
1955
+ } else {
1956
+ console.log("Set OPENROUTER_API_KEY in your environment before starting the runtime.");
1957
+ }
1958
+ }
1959
+
1960
+ if (options.interactive) {
1961
+ printWizardStep(4, 4, "Install");
1962
+ }
1963
+
1889
1964
  const scopedContext = buildScopedContext(context, config.profileId);
1890
1965
  const anchorSync = buildAnchorSyncDecision(options, config.paths.sourceRepoRoot);
1891
1966
 
@@ -2047,12 +2122,45 @@ async function runSetupFlow(forwardedArgs) {
2047
2122
 
2048
2123
  if (options.json) {
2049
2124
  emitSetupJsonPayload(runPayload);
2125
+ } else if (options.interactive) {
2126
+ // Human-friendly summary for interactive terminal runs
2127
+ console.log("\n============================================================");
2128
+ console.log(" Setup complete!");
2129
+ console.log("============================================================");
2130
+ console.log(` Profile : ${config.profileId}`);
2131
+ console.log(` Repo : ${config.repoSlug}`);
2132
+ console.log(` Worker : ${config.codingWorker}`);
2133
+ console.log(` Runtime : ${context.runtimeHome}`);
2134
+
2135
+ const pendingItems = [];
2136
+ if (!prereq.ghAuthOk) pendingItems.push("GitHub CLI not authenticated — run: gh auth login");
2137
+ if (!prereq.workerAvailable) pendingItems.push(`${config.codingWorker} not found on PATH — install it before starting`);
2138
+ if (config.codingWorker === "openclaw" && !process.env.OPENROUTER_API_KEY) {
2139
+ pendingItems.push("OPENROUTER_API_KEY not set — required for openclaw workers");
2140
+ }
2141
+ if (anchorSync.status !== "ok") pendingItems.push(`Anchor repo sync deferred (${anchorSync.reason}) — fix git access and re-run setup`);
2142
+ if ((doctorKv.DOCTOR_STATUS || "") !== "ok") pendingItems.push(`Doctor check flagged issues — run: npx agent-control-plane@latest doctor`);
2143
+
2144
+ if (pendingItems.length > 0) {
2145
+ console.log("\n Pending items before starting:");
2146
+ for (const item of pendingItems) {
2147
+ console.log(` - ${item}`);
2148
+ }
2149
+ }
2150
+
2151
+ console.log("\n Next commands:");
2152
+ if (runtimeStartStatus !== "ok") {
2153
+ console.log(` npx agent-control-plane@latest runtime start --profile-id ${config.profileId}`);
2154
+ }
2155
+ console.log(` npx agent-control-plane@latest runtime status --profile-id ${config.profileId}`);
2156
+ console.log(` npx agent-control-plane@latest doctor`);
2157
+ console.log("");
2050
2158
  } else {
2159
+ // Machine-readable KV output for non-interactive / scripted runs
2051
2160
  console.log("\nSetup complete.");
2052
2161
  console.log(`- profile: ${config.profileId}`);
2053
2162
  console.log(`- repo: ${config.repoSlug}`);
2054
2163
  console.log(`- runtime home: ${context.runtimeHome}`);
2055
- console.log(`- next status command: npx agent-control-plane@latest runtime status --profile-id ${config.profileId}`);
2056
2164
 
2057
2165
  console.log(`SETUP_STATUS=ok`);
2058
2166
  console.log(`PROFILE_ID=${config.profileId}`);
@@ -2170,6 +2278,7 @@ async function main() {
2170
2278
  console.log(packageJson.version);
2171
2279
  return 0;
2172
2280
  case "setup":
2281
+ case "onboard":
2173
2282
  return runSetupFlow(forwardedArgs);
2174
2283
  case "sync":
2175
2284
  case "install":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-control-plane",
3
- "version": "0.1.13",
3
+ "version": "0.1.16",
4
4
  "description": "Help a repo keep GitHub-driven coding agents running reliably without constant human babysitting",
5
5
  "homepage": "https://github.com/ducminhnguyen0319/agent-control-plane",
6
6
  "bugs": {
@@ -32,12 +32,14 @@
32
32
  "tools/bin",
33
33
  "!tools/bin/audit-*.sh",
34
34
  "!tools/bin/check-skill-contracts.sh",
35
+ "!tools/bin/render-dashboard-snapshot.py",
35
36
  "tools/dashboard/app.js",
36
37
  "tools/dashboard/dashboard_snapshot.py",
37
38
  "tools/dashboard/index.html",
38
39
  "tools/dashboard/server.py",
39
40
  "tools/dashboard/styles.css",
40
41
  "tools/templates",
42
+ "!tools/templates/legacy/",
41
43
  "tools/vendor/codex-quota/LICENSE",
42
44
  "tools/vendor/codex-quota/codex-quota.js",
43
45
  "tools/vendor/codex-quota/lib",
@@ -85,7 +85,7 @@ tools/bin/uninstall-project-launchd.sh --profile-id <id>
85
85
  tools/bin/project-remove.sh --profile-id <id>
86
86
  tools/bin/project-remove.sh --profile-id <id> --purge-paths
87
87
  tools/bin/sync-shared-agent-home.sh
88
- python3 tools/bin/render-dashboard-snapshot.py --pretty
88
+ python3 tools/dashboard/dashboard_snapshot.py --pretty
89
89
  bash tools/bin/serve-dashboard.sh --host 127.0.0.1 --port 8765
90
90
  bash tools/bin/install-dashboard-launchd.sh --host 127.0.0.1 --port 8765
91
91
  ```
@@ -97,7 +97,7 @@ prompts live under `tools/templates/`.
97
97
  ## Dashboard
98
98
 
99
99
  ```bash
100
- python3 tools/bin/render-dashboard-snapshot.py --pretty
100
+ python3 tools/dashboard/dashboard_snapshot.py --pretty
101
101
  bash tools/bin/serve-dashboard.sh --host 127.0.0.1 --port 8765
102
102
  bash tools/bin/install-dashboard-launchd.sh --host 127.0.0.1 --port 8765
103
103
  ```
@@ -64,7 +64,7 @@ roots, labels, worker preferences, prompts, and project-specific guardrails.
64
64
  before scheduler use.
65
65
  - `tools/bin/test-smoke.sh`
66
66
  Runs the main shared-package smoke gates in one operator-facing command.
67
- - `tools/bin/render-dashboard-snapshot.py`
67
+ - `tools/dashboard/dashboard_snapshot.py`
68
68
  Emits a JSON snapshot of active runs, resident controllers, cooldown state,
69
69
  queue depth, and scheduled issues across installed profiles.
70
70
  - `tools/bin/serve-dashboard.sh`
@@ -307,12 +307,14 @@ schedule_provider_quota_cooldown() {
307
307
  local reason="${1:-provider-quota-limit}"
308
308
  [[ "${reason}" == "provider-quota-limit" ]] || return 0
309
309
  [[ -x "${provider_cooldown_script}" ]] || return 0
310
+ [[ "${CODING_WORKER:-}" == "codex" ]] && return 0
310
311
 
311
312
  "${provider_cooldown_script}" schedule "${reason}" >/dev/null || true
312
313
  }
313
314
 
314
315
  clear_provider_quota_cooldown() {
315
316
  [[ -x "${provider_cooldown_script}" ]] || return 0
317
+ [[ "${CODING_WORKER:-}" == "codex" ]] && return 0
316
318
 
317
319
  "${provider_cooldown_script}" clear >/dev/null || true
318
320
  }
@@ -357,6 +359,16 @@ infer_issue_runtime_failure_from_log() {
357
359
  return 0
358
360
  fi
359
361
 
362
+ if grep -Eiq 'stale-run no-agent-output-before-stall-threshold|no-agent-output-before-stall-threshold' "${log_file}" 2>/dev/null; then
363
+ printf 'no-agent-output-before-stall-threshold\n'
364
+ return 0
365
+ fi
366
+
367
+ if grep -Eiq 'stale-run no-agent-progress-before-stall-threshold|no-agent-progress-before-stall-threshold' "${log_file}" 2>/dev/null; then
368
+ printf 'no-agent-progress-before-stall-threshold\n'
369
+ return 0
370
+ fi
371
+
360
372
  if grep -Eiq 'Ignoring invalid cwd .* No such file or directory|/tmp is absolute|Custom tool call output is missing' "${log_file}" 2>/dev/null; then
361
373
  printf 'worker-environment-blocked\n'
362
374
  return 0
@@ -985,6 +997,17 @@ if (explicitFailureReason) {
985
997
  reason = 'scope-guard-blocked';
986
998
  } else if (/^# Blocker: Provider quota is currently exhausted$/im.test(body)) {
987
999
  reason = 'provider-quota-limit';
1000
+ } else if (
1001
+ /blocked on external network access/i.test(body) &&
1002
+ (/What I ran:/i.test(body) ||
1003
+ /`pnpm audit`/i.test(body) ||
1004
+ /`gh issue view`/i.test(body)) &&
1005
+ (/failed with `ENOTFOUND`/i.test(body) ||
1006
+ /Exact failure:/i.test(body) ||
1007
+ /registry\.npmjs\.org/i.test(body) ||
1008
+ /api\.github\.com/i.test(body))
1009
+ ) {
1010
+ reason = 'worker-preflight-network-blocked';
988
1011
  } else if (
989
1012
  /blocked on external network access/i.test(body) ||
990
1013
  /could not perform a safe offline bump/i.test(body) ||