@youdie006/prodex 0.39.5 → 0.40.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.
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Rank what actually blocks consults, across every bridge root on the machine.
3
+ *
4
+ * Half of every consult here ends in a blocker, and until now asking WHICH
5
+ * failure dominates meant writing a throwaway script: `pro list` reads a single
6
+ * repo, and the registry that knows where the others are had no reader. Pure
7
+ * on purpose - the reading lives in the command, so the ranking can be tested
8
+ * without a filesystem.
9
+ */
10
+ /**
11
+ * Codes that describe how a send died rather than why. Counting these whole
12
+ * reports "the catch-all is biggest" and names nothing to fix - measured, one
13
+ * of them held 165 of 267 blockers - so their message decides the group.
14
+ */
15
+ const CATCH_ALL_CODES = new Set(["browser_send_failed", "consult_failed", "unknown_error"]);
16
+ /**
17
+ * The part of a message that identifies the failure, with the varying parts
18
+ * removed: the same picker failure is written once with "Pro" and once with a
19
+ * model name, and those are one cause, not two.
20
+ */
21
+ export function blockerCause(message) {
22
+ const firstSentence = /^(.*?)(?:\.\s|\.$|$)/.exec(message.trim())?.[1] ?? message.trim();
23
+ return firstSentence
24
+ .replace(/"[^"]*"/g, '"..."')
25
+ .replace(/\d+/g, "N")
26
+ .replace(/\s+/g, " ")
27
+ .trim();
28
+ }
29
+ function groupKey(code, message) {
30
+ if (!CATCH_ALL_CODES.has(code))
31
+ return code;
32
+ const cause = blockerCause(message);
33
+ return cause ? `${code}: ${cause}` : code;
34
+ }
35
+ export function buildBlockerReport(input) {
36
+ const cutoff = input.since ? Date.parse(input.since) : undefined;
37
+ const inWindow = input.consults.filter((c) => {
38
+ if (cutoff === undefined)
39
+ return true;
40
+ if (!c.createdAt)
41
+ return false;
42
+ const at = Date.parse(c.createdAt);
43
+ return Number.isFinite(at) && at >= cutoff;
44
+ });
45
+ const groups = new Map();
46
+ let blocked = 0;
47
+ for (const consult of inWindow) {
48
+ if (!consult.blocker)
49
+ continue;
50
+ blocked += 1;
51
+ const key = groupKey(consult.blocker.code, consult.blocker.message);
52
+ const at = consult.createdAt ?? "";
53
+ const existing = groups.get(key);
54
+ if (existing) {
55
+ existing.count += 1;
56
+ if (at > existing.lastSeen)
57
+ existing.lastSeen = at;
58
+ existing.repos.set(consult.repo, (existing.repos.get(consult.repo) ?? 0) + 1);
59
+ }
60
+ else {
61
+ groups.set(key, {
62
+ count: 1,
63
+ lastSeen: at,
64
+ example: consult.blocker.message,
65
+ repos: new Map([[consult.repo, 1]])
66
+ });
67
+ }
68
+ }
69
+ const ranked = [...groups.entries()]
70
+ .sort((a, b) => b[1].count - a[1].count || a[0].localeCompare(b[0]))
71
+ .slice(0, input.limit ?? Number.POSITIVE_INFINITY)
72
+ .map(([code, g]) => ({
73
+ code,
74
+ count: g.count,
75
+ share: blocked > 0 ? g.count / blocked : 0,
76
+ lastSeen: g.lastSeen,
77
+ example: g.example,
78
+ repos: [...g.repos.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([repo]) => repo)
79
+ }));
80
+ return { totalConsults: inWindow.length, blocked, roots: input.roots, groups: ranked };
81
+ }
package/dist/cli-help.js CHANGED
@@ -28,6 +28,7 @@ Ask / consult commands:
28
28
  prodex pro browser recover [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--port 9333] --target-url <thread-url> [--timeout-ms 60000] # recover a finished answer from a thread whose send timed out
29
29
  prodex pro browser ask [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 300000] [--busy-wait-ms 600000] [--target-url url --confirm-target] [--new-chat] [--temporary] [--allow-model-fallback] [--stdin] [--json] [--auto-login|--no-auto-login] [--file path] [--attach path] [--tool deep-research|web-search|create-image] [--model Pro] [--pro-mode 기본|확장] [--effort 즉시|중간|높음|"매우 높음"|Max|Ultra|Pro] [--project "name" | --project-new "name"] "prompt" # explicit visible-browser send
30
30
  prodex pro latest [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--json]
31
+ prodex pro blockers [--cwd /absolute/path/to/repo] [--since 7d] [--limit 10] [--json] # what actually blocks consults, ranked, across every bridge root on this machine
31
32
  prodex pro list [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--json]
32
33
  prodex pro show <task-id|latest> [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--json]
33
34
  prodex pro report-issue [--cwd /absolute/path/to/repo] [--task <task-id>] [--repo owner/name] [--confirm] # bug report from a failed consult's receipt; previews unless --confirm, never carries the prompt or the answer
@@ -171,6 +172,7 @@ Commands:
171
172
  prodex pro browser models [--source-cli /absolute/path/to/dist/cli.js]
172
173
  prodex pro browser ask [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--target-url url --confirm-target] [--new-chat] [--temporary] [--allow-model-fallback] [--stdin] [--json] [--auto-login|--no-auto-login] [--file path] [--attach path] [--tool deep-research|web-search|create-image] [--model Pro] [--pro-mode 기본|확장] [--effort 즉시|중간|높음|"매우 높음"|Max|Ultra|Pro] [--project "name" | --project-new "name"] "prompt"
173
174
  prodex pro latest [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--json]
175
+ prodex pro blockers [--cwd /absolute/path/to/repo] [--since 7d] [--limit 10] [--json] # what actually blocks consults, ranked, across every bridge root on this machine
174
176
  prodex pro list [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--json]
175
177
  prodex pro show <task-id|latest> [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--json]
176
178
  prodex pro report-issue [--cwd /absolute/path/to/repo] [--task <task-id>] [--repo owner/name] [--confirm] # bug report from a failed consult's receipt; previews unless --confirm, never carries the prompt or the answer
package/dist/cli-pro.js CHANGED
@@ -10,6 +10,8 @@ import { formatBrowserDefaults, redactServerUrl } from "./cli-server.js";
10
10
  import { errorMessage, firstLine, formatBlockedConsultRecordedMessage, formatProLatestCommand, formatBrowserCheckCommand, formatBrowserLoginCommand, formatBrowserSmokeCommand, formatBrowserTargetAskCommand, formatInitCommand, formatSetupCommand, isMissingFileError, computeSendPacingWaitMs, isUntrustedResultError, resolveMinSendIntervalMs, sourceAwareBrowserBlocker, sourceAwareBrowserNextStep, sourceAwareResultError, sourceAwareResultMessage, sourceAwareSetupMessage } from "./cli-shared.js";
11
11
  import { getTokenExpiryStatus, loadBrowserDefaults, loadLocalConfig } from "./config.js";
12
12
  import { withBrowserSendLock } from "./browser-send-lock.js";
13
+ import { blockerCause, buildBlockerReport } from "./blocker-report.js";
14
+ import { readBridgeRoots } from "./registry.js";
13
15
  import { BridgeStore, MAX_FETCHABLE_RESULT_ARTIFACT_BYTES } from "./store.js";
14
16
  import { CLI_VERSION } from "./cli-help.js";
15
17
  import { PRODEX_ISSUE_REPO, buildIssueReport, fileGitHubIssue } from "./issue-report.js";
@@ -732,6 +734,50 @@ export async function runProCommand(rest, io, runCliFn) {
732
734
  // a two-hop dead end that made an agent abandon the diagnosis.
733
735
  throw new Error(`Use \`prodex pro browser ${legacyBrowserSubcommandReplacement(subcommand)}\` for explicit browser automation.`);
734
736
  }
737
+ if (subcommand === "blockers") {
738
+ if (printHelpIfRequested(proArgs, "pro blockers", io.stdout, printProHelp, { valueFlags: ["--cwd", "--since", "--limit"] }))
739
+ return 0;
740
+ assertOnlyOptions(proArgs, "pro blockers", ["--cwd", "--since", "--limit"], ["--json"]);
741
+ const scopedToOneRepo = readFlag(proArgs, "--cwd") !== undefined;
742
+ // Default to every bridge root: the failures are spread across the repos
743
+ // consults were run from, and `pro list` already covers just this one.
744
+ const roots = scopedToOneRepo ? [resolveCwdFlag(io.cwd, proArgs)] : await readBridgeRoots();
745
+ const since = readSinceFlag(proArgs);
746
+ const limit = readPositiveIntegerFlag(proArgs, "--limit") ?? 10;
747
+ const consults = [];
748
+ let readable = 0;
749
+ for (const root of roots) {
750
+ try {
751
+ const results = await new BridgeStore(root).listResultsReadOnly();
752
+ readable += 1;
753
+ for (const result of results) {
754
+ if (!result.task_id.includes("gpt-pro-consult"))
755
+ continue;
756
+ consults.push({
757
+ repo: path.basename(root),
758
+ createdAt: result.created_at,
759
+ ...(result.blocker ? { blocker: { code: result.blocker.code, message: result.blocker.message } } : {})
760
+ });
761
+ }
762
+ }
763
+ catch {
764
+ // A root that has been deleted or is unreadable is not a failure of
765
+ // the report; it just has nothing to contribute.
766
+ }
767
+ }
768
+ const report = buildBlockerReport({
769
+ consults,
770
+ roots: readable,
771
+ ...(since ? { since } : {}),
772
+ limit
773
+ });
774
+ if (proArgs.includes("--json")) {
775
+ io.stdout(JSON.stringify(report, null, 2));
776
+ return 0;
777
+ }
778
+ io.stdout(formatBlockerReport(report, { since, scopedToOneRepo }));
779
+ return 0;
780
+ }
735
781
  if (subcommand === "list") {
736
782
  if (printHelpIfRequested(proArgs, "pro list", io.stdout, printProHelp, { valueFlags: ["--cwd", "--source-cli"] }))
737
783
  return 0;
@@ -879,7 +925,7 @@ export async function runProCommand(rest, io, runCliFn) {
879
925
  io.stdout(formatDebatePrompt({ topic: readFlag(proArgs, "--topic"), rounds, sourceCli }));
880
926
  return 0;
881
927
  }
882
- throw unknownSubcommandError("pro", subcommand, ["ask", "browser", "debate-prompt", "list", "latest", "show"]);
928
+ throw unknownSubcommandError("pro", subcommand, ["ask", "blockers", "browser", "debate-prompt", "list", "latest", "show"]);
883
929
  }
884
930
  export async function runConsultsCommand(rest, io) {
885
931
  throw new Error("The legacy `consults` alias is retired. Use `prodex pro list`, `prodex pro latest`, or `prodex pro show <task-id|latest>`.");
@@ -1934,6 +1980,63 @@ export function formatProConsultArtifact(consult) {
1934
1980
  lines.push("## Answer", "", consult.answer.trim(), "");
1935
1981
  return lines.join("\n");
1936
1982
  }
1983
+ /**
1984
+ * `--since 7d`, `--since 24h`, or a plain date. A window is how you ask whether
1985
+ * a fix landed, so it has to be quick to type.
1986
+ */
1987
+ export function readSinceFlag(args) {
1988
+ const raw = readFlag(args, "--since");
1989
+ if (raw === undefined)
1990
+ return undefined;
1991
+ const relative = /^(\d+)\s*([dh])$/i.exec(raw.trim());
1992
+ if (relative) {
1993
+ const amount = Number(relative[1]);
1994
+ const hours = relative[2]?.toLowerCase() === "d" ? amount * 24 : amount;
1995
+ return new Date(Date.now() - hours * 3_600_000).toISOString();
1996
+ }
1997
+ const parsed = Date.parse(raw);
1998
+ if (!Number.isFinite(parsed)) {
1999
+ throw new Error(`--since expects a date or an age like 7d or 24h, not "${raw}".`);
2000
+ }
2001
+ return new Date(parsed).toISOString();
2002
+ }
2003
+ /** The ranked report as a table, widest column first so the counts line up. */
2004
+ export function formatBlockerReport(report, options = {}) {
2005
+ const scope = options.scopedToOneRepo ? "this repo" : `${report.roots} bridge root${report.roots === 1 ? "" : "s"}`;
2006
+ const window = options.since ? ` since ${options.since.slice(0, 10)}` : "";
2007
+ if (report.totalConsults === 0) {
2008
+ return `No consults recorded in ${scope}${window}.`;
2009
+ }
2010
+ const share = report.totalConsults > 0 ? Math.round((report.blocked / report.totalConsults) * 100) : 0;
2011
+ const head = `Consult blockers: ${report.blocked} of ${report.totalConsults} consults (${share}%) across ${scope}${window}`;
2012
+ if (report.groups.length === 0)
2013
+ return `${head}\n\nNothing was blocked.`;
2014
+ // A split catch-all carries the first sentence in its own name, so the
2015
+ // example would repeat it; only a bare code needs one.
2016
+ const clip = (text, max) => (text.length > max ? `${text.slice(0, max - 1)}\u2026` : text);
2017
+ const rows = report.groups.map((group) => {
2018
+ const example = group.example.replace(/\s+/g, " ").trim();
2019
+ // Compare through the same normalisation the cause went through: the code
2020
+ // says `has no "..." step` where the message says `has no "Pro" step`.
2021
+ const causePart = group.code.includes(": ") ? group.code.split(": ").slice(1).join(": ") : undefined;
2022
+ const redundant = causePart !== undefined && blockerCause(example) === causePart;
2023
+ return {
2024
+ count: String(group.count),
2025
+ share: `${Math.round(group.share * 100)}%`,
2026
+ code: clip(group.code, 58),
2027
+ lastSeen: group.lastSeen ? group.lastSeen.slice(0, 10) : "-",
2028
+ example: redundant ? "" : clip(example, 58)
2029
+ };
2030
+ });
2031
+ const width = (pick, header) => Math.max(header.length, ...rows.map((row) => pick(row).length));
2032
+ const wCount = width((r) => r.count, "COUNT");
2033
+ const wShare = width((r) => r.share, "SHARE");
2034
+ const wCode = width((r) => r.code, "CAUSE");
2035
+ const wLast = width((r) => r.lastSeen, "LAST SEEN");
2036
+ const line = (count, sharePct, code, lastSeen, example) => ` ${count.padStart(wCount)} ${sharePct.padStart(wShare)} ${code.padEnd(wCode)} ${lastSeen.padEnd(wLast)} ${example}`;
2037
+ const body = rows.map((row) => line(row.count, row.share, row.code, row.lastSeen, row.example).trimEnd());
2038
+ return [head, "", line("COUNT", "SHARE", "CAUSE", "LAST SEEN", "EXAMPLE"), ...body].join("\n");
2039
+ }
1937
2040
  export function formatProListSummary(consult, sourceCli, options = {}) {
1938
2041
  const blocker = sourceAwareProAnswerBlocker(consult, sourceCli, options);
1939
2042
  return firstLine(sourceAwareProAnswerSummary(consult.result.summary, consult.result.blocker, blocker));
package/dist/registry.js CHANGED
@@ -44,6 +44,38 @@ async function directoryExists(dir) {
44
44
  return false;
45
45
  }
46
46
  }
47
+ /**
48
+ * Every bridge root this machine knows about. The registry has only ever been
49
+ * written; reading it is what lets a report span the repos where the failures
50
+ * actually are, instead of the one the command happens to run in.
51
+ *
52
+ * Advisory like the rest of the registry: a missing or corrupt file is an
53
+ * empty list, not an error, and roots that have since been deleted are
54
+ * dropped rather than reported.
55
+ */
56
+ export async function readBridgeRoots() {
57
+ let parsed;
58
+ try {
59
+ parsed = JSON.parse(await fs.readFile(bridgesRegistryPath(), "utf8"));
60
+ }
61
+ catch {
62
+ return [];
63
+ }
64
+ const raw = parsed?.roots;
65
+ if (!Array.isArray(raw))
66
+ return [];
67
+ const roots = [];
68
+ for (const entry of raw.slice(0, MAX_REGISTRY_ROOTS)) {
69
+ const value = typeof entry === "string" ? entry : entry?.path;
70
+ if (typeof value !== "string" || value.length === 0)
71
+ continue;
72
+ if (!(await directoryExists(path.join(value, ".bridge"))))
73
+ continue;
74
+ if (!roots.includes(value))
75
+ roots.push(value);
76
+ }
77
+ return roots;
78
+ }
47
79
  export function registerBridgeRoot(root) {
48
80
  const next = registryQueue.then(() => registerBridgeRootInner(root));
49
81
  // Keep the chain alive even if an inner registration rejects.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youdie006/prodex",
3
- "version": "0.39.5",
3
+ "version": "0.40.0",
4
4
  "description": "Local receipt bus for coordinating Codex execution with ChatGPT Pro/Projects consultation.",
5
5
  "author": "youdie006",
6
6
  "license": "MIT",