@ghostystudio/cli 0.54.0 → 0.56.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.
Files changed (3) hide show
  1. package/README.md +12 -1
  2. package/dist/index.js +99 -18
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -109,6 +109,17 @@ Every command has its own help with examples: `ghosty <command> --help` or `ghos
109
109
 
110
110
  Exit codes: `0` ok · `1` API error · `2` usage error · `3` not signed in.
111
111
 
112
+ ### Changes in 0.56.0
113
+
114
+ - `factory runs why <run> [--refresh]`: why a run got stuck or failed — category, impact, the critical step with its evidence and what to change next time (a healthy run doesn't call the model).
115
+ - `factory runs show --timing` lists stuck patterns (a command open 8+ min, the same command repeated, errors in a row, going back and forth, apt install in a box); `runs stats` counts runs with stuck alerts.
116
+
117
+ ### Changes in 0.55.0
118
+
119
+ - `factory runs show <run> --timing`: where a run's time and money went — each turn of each role (critic, @plan, @build, @check) with minutes, minutes in tools, model and USD; the clock split into working, waiting for a person and the rest (CI, queues); and the commands that took 60 s or more.
120
+ - `factory runs stats [--since 30d] [--repo owner/repo]`: runs, % merged, median minutes to PR and to merge, @check→@build loops, red CI per run, and cost per run and per merged PR.
121
+ - `factory runs ls` shows each run's time and cost.
122
+
112
123
  ### Changes in 0.54.0
113
124
 
114
125
  - `agents embed <id> --copy '{json}'|none` and `--copy-file texts.json`: the phrases the web widget shows while the agent works (`waking`, `connecting`, `thinking` — 1–8 that rotate —, `stillWorking`, `cantStart`, `interrupted`, `retryLabel`, plain text ≤ 80 chars) and whether it shows the duration and the Copy button (`showDuration`, `showCopyButton`). Patch per key; `null` = Ghosty's phrase. `agents embed <id> theme` exports `copy` too, and `--out` bakes it into the component.
@@ -134,7 +145,7 @@ Exit codes: `0` ok · `1` API error · `2` usage error · `3` not signed in.
134
145
  - `channels whatsapp integrations` shows who answered each number last; `--orphans` and `channels messenger ls [--orphans]` list numbers/pages nobody answers today that had turns.
135
146
  - `agents set --prompt-patch overlay.json` / `--prompt-append FILE [--dry-run]`: change one piece of the prompt on the server's current copy (dry run prints the diff).
136
147
  - `audit ls [--workspace] [--agent] [--actor] [--action] [--since]`: the audit log. The agent page's Save and channel changes are logged now.
137
- - `factory runs ls|show|rename` (a workspace's Software Factory runs) and `agents ls --handle build`.
148
+ - `factory runs ls|show|rename` (a workspace's Software Factory runs) and `agents ls --handle build`. Since 0.55: `show --timing` and `stats`.
138
149
 
139
150
  ### Changes in 0.49.0
140
151
 
package/dist/index.js CHANGED
@@ -8468,7 +8468,7 @@ async function postLong(path, body, timeoutMs = 9e5) {
8468
8468
  const req = httpsRequest(`${base}${path}`, {
8469
8469
  method: "POST",
8470
8470
  timeout: timeoutMs,
8471
- headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.54.0"}`, "content-type": "application/json", "content-length": Buffer.byteLength(data) }
8471
+ headers: { authorization: `Bearer ${token}`, "user-agent": `ghosty-cli/${"0.56.0"}`, "content-type": "application/json", "content-length": Buffer.byteLength(data) }
8472
8472
  }, (res) => {
8473
8473
  let buf = "";
8474
8474
  res.on("data", (c) => buf += c);
@@ -8510,7 +8510,7 @@ async function request(path, opts = {}, retried = false) {
8510
8510
  method: opts.method ?? (opts.body !== void 0 || opts.raw !== void 0 ? "POST" : "GET"),
8511
8511
  headers: {
8512
8512
  authorization: `Bearer ${token}`,
8513
- "user-agent": `ghosty-cli/${"0.54.0"}`,
8513
+ "user-agent": `ghosty-cli/${"0.56.0"}`,
8514
8514
  "accept-language": lang(),
8515
8515
  ...opts.body !== void 0 ? { "content-type": "application/json" } : {},
8516
8516
  ...opts.headers
@@ -10404,7 +10404,7 @@ async function call(path, v, init = {}) {
10404
10404
  "x-ghosty-ts": ts,
10405
10405
  "x-ghosty-sig": sig,
10406
10406
  "content-type": "application/json",
10407
- "user-agent": `ghosty-cli/${"0.54.0"}`
10407
+ "user-agent": `ghosty-cli/${"0.56.0"}`
10408
10408
  },
10409
10409
  body: method === "GET" ? void 0 : raw
10410
10410
  });
@@ -12492,7 +12492,7 @@ var HELPS_ES = {
12492
12492
  ]
12493
12493
  },
12494
12494
  factory: {
12495
- summary: "Los pedidos de la Software Factory de un espacio (viven en su Teams): listarlos, ver uno (plan vigente, PR, preview, \xFAltimos eventos, qu\xE9 rol trabaja) y corregir el t\xEDtulo de un pedido.",
12495
+ summary: "Los pedidos de la Software Factory de un espacio (viven en su Teams): listarlos con tiempo y costo, ver uno (plan vigente, PR, preview, \xFAltimos eventos, qu\xE9 rol trabaja), ver en qu\xE9 se fue su tiempo y su dinero y por qu\xE9 se ator\xF3 o fall\xF3, los totales del espacio y corregir el t\xEDtulo de un pedido.",
12496
12496
  replace: [['"Title"', '"T\xEDtulo"']],
12497
12497
  flags: [
12498
12498
  ["--workspace SLUG", "el espacio (default: el de ghosty use)"],
@@ -12500,12 +12500,20 @@ var HELPS_ES = {
12500
12500
  ["--status S", "ls: s\xF3lo esa etapa (planning, building, checking, pr_review, merged\u2026)"],
12501
12501
  ["--limit N", "ls: cu\xE1ntos (default 30)"],
12502
12502
  ["--full", "show: imprime el plan vigente"],
12503
+ ["--timing", "show: cada turno de cada rol (cr\xEDtico, @plan, @build, @check) con minutos, minutos en tools, modelo y USD; el reloj del pedido partido en trabajando, esperando a una persona (plan o PR por aprobar) y lo dem\xE1s (CI, colas); y los comandos de 60 s o m\xE1s"],
12504
+ ["--refresh", "why: rehace el an\xE1lisis (se guarda la primera vez)"],
12505
+ ["--since 30d", "stats: cu\xE1nto hacia atr\xE1s (d\xEDas, default 30)"],
12506
+ ["--repo OWNER/REPO", "stats: s\xF3lo ese repo (due\xF1o/repo)"],
12503
12507
  ["-n, --dry-run", "rename: ense\xF1a el cambio, no cambia nada"],
12504
12508
  ["--yes", "rename: se salta la confirmaci\xF3n (obligatorio sin terminal)"]
12505
12509
  ],
12506
12510
  notes: [
12507
12511
  "Leer pide ser miembro del espacio; renombrar, ser due\xF1o, admin o staff de Ghosty, y queda en bit\xE1cora (factory.run_rename). La tarjeta del room toma el t\xEDtulo nuevo en su siguiente refresco.",
12508
- "Qui\xE9n es @plan o @build: ghosty agents ls --handle plan --workspace SLUG."
12512
+ "Qui\xE9n es @plan o @build: ghosty agents ls --handle plan --workspace SLUG.",
12513
+ "stats: pedidos, % mezclados, mediana de minutos hasta el PR y hasta el merge, vueltas @check\u2192@build y CI rojo por pedido, y costo por pedido y por PR mezclado. El costo incluye el primer turno de @plan en el hilo, antes de que naciera el pedido.",
12514
+ "--timing lee la transcripci\xF3n de cada rol: los motores sin una (ACP) salen con minutos y costo, pero sin comandos. Tambi\xE9n lista los atascos: un comando abierto 8+ min, el mismo comando 4 veces, el mismo error 3 veces, ir y venir entre dos acciones, o apt install dentro de una caja.",
12515
+ "why: si el pedido no fue sano (atascos, @build de m\xE1s de 30 min, vueltas, CI rojo, escalado), una categor\xEDa (missing_service, unresolved_build_or_tests, fixed_strategy\u2026), el paso cr\xEDtico con su evidencia y qu\xE9 cambiar la pr\xF3xima vez. Un pedido sano no llama al modelo. Cuando un pedido cierra sin ser sano, el room recibe este resumen en su hilo.",
12516
+ "Mientras un rol trabaja, si se atora el room recibe un \u23F3 en el hilo y al rol se le dice qu\xE9 revisar; desde la segunda vez en el mismo pedido, quien lo pidi\xF3 recibe un push."
12509
12517
  ]
12510
12518
  }
12511
12519
  };
@@ -13239,10 +13247,13 @@ HELPS.audit = {
13239
13247
  learn: `${REF}#bitacora`
13240
13248
  };
13241
13249
  HELPS.factory = {
13242
- summary: "A workspace's Software Factory runs (they live in its Teams): list them, see one (current plan, PR, preview, latest events, which role is working) and fix a run's title.",
13250
+ summary: "A workspace's Software Factory runs (they live in its Teams): list them with time and cost, see one (current plan, PR, preview, latest events, which role is working), see where its time and money went and why it got stuck or failed, totals for the workspace, and fix a run's title.",
13243
13251
  usage: [
13244
13252
  "ghosty factory runs ls [--workspace SLUG] [--room R] [--status S] [--limit 30]",
13245
13253
  "ghosty factory runs show <run> [--workspace SLUG] [--full]",
13254
+ "ghosty factory runs show <run> --timing [--workspace SLUG]",
13255
+ "ghosty factory runs why <run> [--refresh] [--workspace SLUG]",
13256
+ "ghosty factory runs stats [--since 30d] [--repo OWNER/REPO] [--workspace SLUG]",
13246
13257
  'ghosty factory runs rename <run> "Title" [--workspace SLUG] [--dry-run] [--yes]'
13247
13258
  ],
13248
13259
  flags: [
@@ -13251,11 +13262,15 @@ HELPS.factory = {
13251
13262
  ["--status S", "ls: only that stage (planning, building, checking, pr_review, merged\u2026)"],
13252
13263
  ["--limit N", "ls: how many (default 30)"],
13253
13264
  ["--full", "show: print the current plan"],
13265
+ ["--timing", "show: each turn of each role (critic, @plan, @build, @check) with minutes, minutes in tools, model and USD; the run's clock split into working, waiting for a person (plan or PR to approve) and the rest (CI, queues); and the commands that took 60 s or more"],
13266
+ ["--refresh", "why: redo the analysis (it's saved the first time)"],
13267
+ ["--since 30d", "stats: how far back (days, default 30)"],
13268
+ ["--repo OWNER/REPO", "stats: only that repo"],
13254
13269
  ["-n, --dry-run", "rename: show the change, change nothing"],
13255
13270
  ["--yes", "rename: skip the confirmation (required without a terminal)"]
13256
13271
  ],
13257
- examples: ["ghosty factory runs ls --workspace business --room mailmask", "ghosty factory runs show 15 --workspace business", 'ghosty factory runs rename 15 "Docs del CLI al d\xEDa" --workspace business --dry-run'],
13258
- notes: ["Reading needs membership in the workspace; rename needs owner, admin or Ghosty staff, and is logged (factory.run_rename). The room's card picks the new title up on its next refresh.", "Who is @plan or @build: ghosty agents ls --handle plan --workspace SLUG."],
13272
+ examples: ["ghosty factory runs ls --workspace business --room mailmask", "ghosty factory runs show 15 --workspace business", "ghosty factory runs show 13 --timing --workspace mercadito-verde", "ghosty factory runs why 13 --workspace mercadito-verde", "ghosty factory runs stats --since 7d --workspace mercadito-verde", 'ghosty factory runs rename 15 "Docs del CLI al d\xEDa" --workspace business --dry-run'],
13273
+ notes: ["Reading needs membership in the workspace; rename needs owner, admin or Ghosty staff, and is logged (factory.run_rename). The room's card picks the new title up on its next refresh.", "Who is @plan or @build: ghosty agents ls --handle plan --workspace SLUG.", "stats: runs, % merged, median minutes to PR and to merge, @check\u2192@build loops and red CI per run, and cost per run and per merged PR. The cost counts @plan's first turn in the thread, before the run existed.", "--timing reads each role's transcript: engines without one (ACP) show minutes and cost but no commands. It also lists stuck patterns: a command open 8+ min, the same command 4 times, the same error 3 times, going back and forth, or apt install inside a box.", "why: if the run wasn't healthy (stuck patterns, @build over 30 min, loops, red CI, escalated), a category (missing_service, unresolved_build_or_tests, fixed_strategy\u2026), the critical step with its evidence and what to change next time. A healthy run doesn't call the model. When a run closes unhealthy, the room gets this summary in its thread.", "While a role works, if it gets stuck the room gets a \u23F3 message in the thread and the role is told what to check; from the second time in the same run, whoever asked gets a push."],
13259
13274
  learn: `${REF}#fabrica`
13260
13275
  };
13261
13276
  function helpText(cmd) {
@@ -13613,11 +13628,11 @@ async function planCmd(sub, slug, v, context) {
13613
13628
  if (!slug) throw usage(t("Missing workspace.", "Falta el espacio."), "ghosty plan apply <workspace> [--dry-run] (or ghosty use <workspace>)");
13614
13629
  const path = `/api/v2/workspaces/${encodeURIComponent(slug)}/plan/apply`;
13615
13630
  const plan = await api(path, { method: "POST", body: { dryRun: true } });
13616
- const money = (n) => `$${n.toLocaleString("es-MX")} MXN`;
13631
+ const money2 = (n) => `$${n.toLocaleString("es-MX")} MXN`;
13617
13632
  const text = [
13618
13633
  `${bold(plan.workspace)} ${dim(plan.scheduledFor ? t(`scheduled for ${plan.scheduledFor.slice(0, 10)}`, `programado para el ${plan.scheduledFor.slice(0, 10)}`) : "")}`,
13619
13634
  ...plan.changes.map((c) => ` ~ ${c.label}: ${String(c.from)} \u2192 ${String(c.to)}`),
13620
- ` ${t("monthly", "al mes")}: ${money(plan.monthlyTotal.from)} \u2192 ${money(plan.monthlyTotal.to)}`
13635
+ ` ${t("monthly", "al mes")}: ${money2(plan.monthlyTotal.from)} \u2192 ${money2(plan.monthlyTotal.to)}`
13621
13636
  ].join("\n");
13622
13637
  if (v["dry-run"]) return out(plan, () => `${text}
13623
13638
  ${dim(t("Dry run: nothing applied.", "Simulaci\xF3n: no se aplic\xF3 nada."))}`);
@@ -14110,21 +14125,84 @@ ${dim(t("Times in UTC. --full for the whole detail; --json for everything.", "Ho
14110
14125
 
14111
14126
  // src/commands/factory.ts
14112
14127
  var enc12 = encodeURIComponent;
14128
+ var money = (n) => n === null || n === void 0 ? "\u2014" : `$${n.toFixed(2)}`;
14129
+ var mins = (n) => n === null || n === void 0 ? "\u2014" : n >= 120 ? `${(n / 60).toFixed(1)} h` : `${n} min`;
14130
+ var hhmm = (iso) => new Date(iso).toLocaleTimeString("es-MX", { hour: "2-digit", minute: "2-digit", hour12: false });
14113
14131
  var short2 = (s2) => (s2 ?? "").slice(0, 16).replace("T", " ");
14114
14132
  async function factoryCmd(sub, verb, ref, title, v, context) {
14115
- if (sub !== "runs") throw usage(t(`Unknown factory subcommand: ${sub ?? "(none)"}.`, `Subcomando de factory desconocido: ${sub ?? "(ninguno)"}.`), "ghosty factory runs ls|show|rename \u2026");
14133
+ if (sub !== "runs") throw usage(t(`Unknown factory subcommand: ${sub ?? "(none)"}.`, `Subcomando de factory desconocido: ${sub ?? "(ninguno)"}.`), "ghosty factory runs ls|show|why|stats|rename \u2026");
14116
14134
  const ws = v.workspace ?? context;
14117
14135
  if (!ws) throw usage(t("Which workspace?", "\xBFQu\xE9 espacio?"), t("Add --workspace SLUG (or set one with ghosty use SLUG).", "Agrega --workspace SLUG (o fija uno con ghosty use SLUG)."));
14118
14136
  const base = `/api/v2/workspaces/${enc12(ws)}/factory/runs`;
14119
14137
  if (!verb || verb === "ls" || verb === "list") {
14120
14138
  const qs = new URLSearchParams({ ...v.room ? { room: v.room } : {}, ...v.status ? { status: v.status } : {}, ...v.limit ? { limit: v.limit } : {} });
14121
14139
  const r = await api(`${base}?${qs}`);
14122
- const rows = r.runs.map((x) => ({ run: `#${x.id}`, title: x.title.slice(0, 60), status: x.status, room: x.room ? `#${x.room}` : "", pr: x.pr ? x.pr.replace(/^https:\/\/github\.com\//, "") : "", updated: short2(x.updatedAt) }));
14123
- return out(r, () => rows.length ? `${table(rows, ["run", "title", "status", "room", "pr", "updated"])}
14140
+ const rows = r.runs.map((x) => ({ run: `#${x.id}`, title: x.title.slice(0, 60), status: x.status, room: x.room ? `#${x.room}` : "", pr: x.pr ? x.pr.replace(/^https:\/\/github\.com\//, "") : "", time: mins(x.durationMin), usd: money(x.usd), updated: short2(x.updatedAt) }));
14141
+ return out(r, () => rows.length ? `${table(rows, ["run", "title", "status", "room", "pr", "time", "usd", "updated"])}
14124
14142
  ${dim(`${t("Detail", "Detalle")}: ghosty factory runs show <run> --workspace ${r.workspace}`)}` : dim(t(`No factory runs in ${r.workspace}.`, `No hay pedidos de la f\xE1brica en ${r.workspace}.`)));
14125
14143
  }
14144
+ if (verb === "stats") {
14145
+ const qs = new URLSearchParams({ since: v.since ?? "30d", ...v.repo ? { repo: v.repo } : {} });
14146
+ const { stats: s2, workspace } = await api(`/api/v2/workspaces/${enc12(ws)}/factory/stats?${qs}`);
14147
+ return out({ workspace, stats: s2 }, () => {
14148
+ if (!s2.runs) return dim(t(`No factory runs in ${workspace} in the last ${s2.sinceDays} days.`, `No hay pedidos de la f\xE1brica en ${workspace} en los \xFAltimos ${s2.sinceDays} d\xEDas.`));
14149
+ return [
14150
+ bold(t(`${workspace} \xB7 last ${s2.sinceDays} days${s2.repo ? ` \xB7 ${s2.repo}` : ""}`, `${workspace} \xB7 \xFAltimos ${s2.sinceDays} d\xEDas${s2.repo ? ` \xB7 ${s2.repo}` : ""}`)),
14151
+ `${t("runs", "pedidos")}: ${s2.runs} \xB7 ${t("merged", "mezclados")}: ${s2.merged} (${s2.mergedPct ?? "\u2014"} %) ${dim(s2.byStatus.map((b) => `${b.status} ${b.runs}`).join(" \xB7 "))}`,
14152
+ `${t("median to PR", "mediana hasta el PR")}: ${mins(s2.medianMinutesToPr)} \xB7 ${t("to merge", "hasta el merge")}: ${mins(s2.medianMinutesToMerge)}`,
14153
+ `${t("@check\u2192@build loops per run", "vueltas @check\u2192@build por pedido")}: ${s2.avgLoops ?? "\u2014"} \xB7 ${t("red CI per run", "CI rojo por pedido")}: ${s2.avgCiFails ?? "\u2014"}${s2.runsWithStuck !== void 0 ? ` \xB7 ${t("runs with stuck alerts", "pedidos con atascos")}: ${s2.runsWithStuck}` : ""}`,
14154
+ `${t("cost", "costo")}: ${money(s2.usd)} USD \xB7 ${t("per run", "por pedido")} ${money(s2.usdPerRun)} \xB7 ${bold(t(`per merged PR ${money(s2.usdPerMergedPr)}`, `por PR mezclado ${money(s2.usdPerMergedPr)}`))}`,
14155
+ ...s2.byRepo.length > 1 ? ["", table(s2.byRepo.map((b) => ({ repo: b.repo, runs: b.runs, merged: b.merged, usd: money(b.usd) })), ["repo", "runs", "merged", "usd"])] : []
14156
+ ].join("\n");
14157
+ });
14158
+ }
14126
14159
  if (!ref) throw usage(t("Missing run.", "Falta el pedido."), `ghosty factory runs ${verb} <run> --workspace ${ws}`);
14127
14160
  const path = `${base}/${enc12(ref.replace(/^#/, ""))}`;
14161
+ if (verb === "show" && v.timing) {
14162
+ const { timing: tm, workspace } = await api(`${path}/timing`);
14163
+ return out({ workspace, timing: tm }, () => {
14164
+ const c = tm.clock;
14165
+ const head = [
14166
+ `${bold(`#${tm.run}`)} ${dim(`\xB7 ${tm.status} \xB7 ${t("loops", "vueltas")} ${tm.loops} \xB7 ${t("red CI", "CI rojo")} ${tm.ciFails}`)} \xB7 ${bold(money(tm.usd))} USD`,
14167
+ `${t("clock", "reloj")}: ${mins(c.totalMinutes)} = ${t("working", "trabajando")} ${mins(c.workingMinutes)} \xB7 ${t("waiting for a person", "esperando a una persona")} ${mins(c.waitingHumanMinutes)} \xB7 ${t("other (CI, queues)", "lo dem\xE1s (CI, colas)")} ${mins(c.otherMinutes)}`,
14168
+ dim(c.byState.map((b) => `${b.state} ${mins(b.minutes)}`).join(" \u2192 ")),
14169
+ ""
14170
+ ];
14171
+ const rows = tm.steps.map((s2) => ({
14172
+ [t("at", "hora")]: hhmm(s2.startedAt),
14173
+ [t("step", "paso")]: `@${s2.role ?? "?"}${s2.critic ? t(" (critic)", " (cr\xEDtico)") : ""}${s2.prePlan ? t(" (thread)", " (hilo)") : ""}`,
14174
+ [t("time", "tiempo")]: s2.state === "running" ? `${mins(s2.minutes)} \u2026` : mins(s2.minutes),
14175
+ [t("in tools", "en tools")]: mins(s2.toolMinutes),
14176
+ model: (s2.model ?? "\u2014").replace(/^claude-/, ""),
14177
+ usd: money(s2.usd),
14178
+ [t("state", "estado")]: s2.state === "error" ? red(s2.state) : s2.state
14179
+ }));
14180
+ const cols = Object.keys(rows[0] ?? {});
14181
+ const slow = tm.steps.flatMap((s2) => s2.slow.map((x) => ({ ...x, role: s2.role })));
14182
+ return [
14183
+ ...head,
14184
+ rows.length ? table(rows, cols) : dim(t("No turns found for this run.", "No hay turnos de este pedido.")),
14185
+ ...slow.length ? ["", bold(t("Slowest commands (\u2265 60 s)", "Comandos m\xE1s lentos (\u2265 60 s)")), ...slow.map((x) => ` ${hhmm(x.at)} @${x.role} ${x.secs === null ? red(t("no result", "sin resultado")) : mins(Math.round(x.secs / 6) / 10)} ${dim(`${x.tool}: ${x.input}`)}`)] : [],
14186
+ ...tm.steps.some((s2) => s2.flags?.length) ? ["", bold(t("Stuck patterns", "Atascos")), ...tm.steps.flatMap((s2) => (s2.flags ?? []).map((f) => ` ${hhmm(f.at)} @${s2.role} ${red(f.rule)}${f.count > 1 ? ` \xD7${f.count}` : ""}${f.secs ? ` ${mins(Math.round(f.secs / 6) / 10)}` : ""} ${dim(f.detail.slice(0, 120))}`)), dim(t(`Why: ghosty factory runs why ${tm.run} --workspace ${workspace}`, `Por qu\xE9: ghosty factory runs why ${tm.run} --workspace ${workspace}`))] : [],
14187
+ ...tm.steps.filter((s2) => s2.why).map((s2) => dim(` @${s2.role}: ${t("no timeline", "sin l\xEDnea de tiempo")} (${s2.why})`))
14188
+ ].join("\n");
14189
+ });
14190
+ }
14191
+ if (verb === "why") {
14192
+ const { why: w, workspace } = await api(`${path}/why`, v.refresh ? { body: { refresh: true } } : void 0);
14193
+ return out({ workspace, why: w }, () => {
14194
+ if (w.healthy) return `${lavender("\u2713")} #${w.runId} ${t("was healthy: no stuck patterns, no long @build, no loops, no red CI.", "fue sano: sin atascos, sin @build largo, sin vueltas ni CI rojo.")}`;
14195
+ const impact = { high: t("high", "alto"), medium: t("medium", "medio"), low: t("low", "bajo") }[w.impact] ?? w.impact;
14196
+ return [
14197
+ `${bold(`#${w.runId}`)} ${red(w.category)} ${dim(`\xB7 ${t("impact", "impacto")} ${impact} \xB7 ${w.source === "model" ? w.model ?? "model" : t("rules", "reglas")} \xB7 ${short2(w.createdAt)}`)}`,
14198
+ w.summary,
14199
+ dim(`${t("signals", "se\xF1ales")}: ${w.reasons.join("; ")}`),
14200
+ ...w.criticalStep ? ["", bold(t("Critical step", "Paso cr\xEDtico")), ` ${w.criticalStep.at ? hhmm(w.criticalStep.at) : "\u2014"} @${w.criticalStep.role ?? "?"} ${dim(w.criticalStep.command ?? "")}`, ` ${w.criticalStep.evidence}`] : [],
14201
+ ...w.actions.length ? ["", bold(t("Next time", "Para la pr\xF3xima")), ...w.actions.map((a) => ` ${lavender(a.type)} ${a.text}`)] : [],
14202
+ ...v.refresh ? [] : ["", dim(t("Saved analysis; --refresh redoes it.", "An\xE1lisis guardado; --refresh lo rehace."))]
14203
+ ].join("\n");
14204
+ });
14205
+ }
14128
14206
  if (verb === "show") {
14129
14207
  const { run: r } = await api(path);
14130
14208
  const lines = [
@@ -14152,7 +14230,7 @@ ${dim(t("Dry run: nothing changed.", "Simulacro: no se cambi\xF3 nada."))}`);
14152
14230
  const r = await api(path, { body: { title } });
14153
14231
  return out(r, () => `${lavender("\u2713")} ${line2} ${dim(t("(logged; the room card picks it up on its next refresh)", "(queda en bit\xE1cora; la tarjeta del room lo toma en su siguiente refresco)"))}`);
14154
14232
  }
14155
- throw usage(t(`Unknown runs subcommand: ${verb}.`, `Subcomando de runs desconocido: ${verb}.`), 'ghosty factory runs ls | show <run> | rename <run> "title"');
14233
+ throw usage(t(`Unknown runs subcommand: ${verb}.`, `Subcomando de runs desconocido: ${verb}.`), 'ghosty factory runs ls | show <run> [--timing] | why <run> [--refresh] | stats [--since 30d] | rename <run> "title"');
14156
14234
  }
14157
14235
  async function agentsByHandle(handle, workspace) {
14158
14236
  const qs = new URLSearchParams({ handle, ...workspace ? { workspace } : {} });
@@ -14444,7 +14522,7 @@ ${bold("Workspaces")} ${dim("(changing them: Ghosty staff)")}
14444
14522
  staff ls [staff-email] [client-email] Ghosty staff assigned to client accounts (staff) [--all: also revoked]
14445
14523
  staff assign|unassign <staff> <client> Staff sees and runs that account's agents, documents, databases and boards as themselves (no keys, billing or transfers; logged) [--reason]
14446
14524
  audit ls The audit log: who changed what (agent page, channels, pauses, replies, transfers, "log in as") [--workspace SLUG] [--agent ID] [--actor EMAIL] [--action A] [--since 24h] [--limit N]
14447
- factory runs ls|show|rename A workspace's Software Factory runs: list, detail (plan, PR, preview, who is working), fix a title [--workspace SLUG] [--room R] [--status S]
14525
+ factory runs ls|show|why|stats|rename A workspace's Software Factory runs: list (time and cost), detail (plan, PR, preview, who is working), where the time and money went, why it got stuck or failed, totals, fix a title [--workspace SLUG] [--room R] [--status S] [show --timing] [why --refresh] [stats --since 30d --repo R]
14448
14526
 
14449
14527
  ${bold("Agents")}
14450
14528
  agents ls List your agents [--engine E] [--ids: only ids, for xargs] \xB7 --user EMAIL --app all|android|ios|desktop: as their app sees it (staff)
@@ -14608,7 +14686,7 @@ ${bold("Espacios")} ${dim("(cambiarlos: staff de Ghosty)")}
14608
14686
  staff ls [correo-staff] [correo-cliente] Staff de Ghosty asignado a cuentas cliente (staff) [--all: tambi\xE9n revocadas]
14609
14687
  staff assign|unassign <staff> <cliente> El staff ve y opera agentes, documentos, bases y tableros de esa cuenta como s\xED mismo (sin llaves, cobro ni traspasos; queda en bit\xE1cora) [--reason]
14610
14688
  audit ls La bit\xE1cora: qui\xE9n cambi\xF3 qu\xE9 (ficha, canales, pausas, respuestas, traspasos, \xABentrar como\xBB) [--workspace SLUG] [--agent ID] [--actor CORREO] [--action A] [--since 24h] [--limit N]
14611
- factory runs ls|show|rename Los pedidos de la Software Factory de un espacio: lista, detalle (plan, PR, preview, qui\xE9n trabaja), corregir un t\xEDtulo [--workspace SLUG] [--room R] [--status S]
14689
+ factory runs ls|show|why|stats|rename Los pedidos de la Software Factory de un espacio: lista (tiempo y costo), detalle (plan, PR, preview, qui\xE9n trabaja), en qu\xE9 se fue el tiempo y el dinero, por qu\xE9 se ator\xF3 o fall\xF3, totales, corregir un t\xEDtulo [--workspace SLUG] [--room R] [--status S] [show --timing] [why --refresh] [stats --since 30d --repo R]
14612
14690
 
14613
14691
  ${bold("Agentes")}
14614
14692
  agents ls Lista tus agentes [--engine E] [--ids: s\xF3lo ids, para xargs] \xB7 --user CORREO --app all|android|ios|desktop: como lo ve su app (staff)
@@ -14868,6 +14946,9 @@ async function main() {
14868
14946
  action: { type: "string" },
14869
14947
  verbose: { type: "boolean" },
14870
14948
  full: { type: "boolean" },
14949
+ timing: { type: "boolean" },
14950
+ refresh: { type: "boolean" },
14951
+ repo: { type: "string" },
14871
14952
  "as-formmy": { type: "boolean" },
14872
14953
  "key-column": { type: "string" },
14873
14954
  "unset-env": { type: "string" },
@@ -15004,7 +15085,7 @@ async function main() {
15004
15085
  const tokens = splitTokens(pos, v);
15005
15086
  globals.token = tokens.session;
15006
15087
  globals.profile = v.profile;
15007
- if (v.version) return out({ version: "0.54.0" }, () => "0.54.0");
15088
+ if (v.version) return out({ version: "0.56.0" }, () => "0.56.0");
15008
15089
  for (const n of normalizeArgs(pos, v)) if (!globals.json) info(dim(n));
15009
15090
  const context = (await readConfig()).workspace;
15010
15091
  if (!explicitToken()?.startsWith("gat_")) {
@@ -15170,7 +15251,7 @@ async function main() {
15170
15251
  throw unknown(cmd, cmd, COMMANDS, "ghosty --help");
15171
15252
  }
15172
15253
  }
15173
- var notice = updateNotice("0.54.0").catch(() => null);
15254
+ var notice = updateNotice("0.56.0").catch(() => null);
15174
15255
  main().then(
15175
15256
  async () => {
15176
15257
  const n = await notice;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghostystudio/cli",
3
- "version": "0.54.0",
3
+ "version": "0.56.0",
4
4
  "description": "Official Ghosty Studio CLI — configure, test and chat with your agents from the terminal. Built for humans and coding agents.",
5
5
  "type": "module",
6
6
  "bin": {