@dosu/cli 0.53.0-beta.13 → 0.53.0-beta.14

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 (2) hide show
  1. package/bin/dosu.js +595 -388
  2. package/package.json +1 -1
package/bin/dosu.js CHANGED
@@ -5018,7 +5018,7 @@ function isNpxInvocation(channel = INSTALL_CHANNEL, env = process.env) {
5018
5018
  function getVersionString() {
5019
5019
  return `v${VERSION}`;
5020
5020
  }
5021
- var VERSION = "0.53.0-beta.13", INSTALL_CHANNEL = "npm";
5021
+ var VERSION = "0.53.0-beta.14", INSTALL_CHANNEL = "npm";
5022
5022
 
5023
5023
  // src/debug/logger.ts
5024
5024
  import {
@@ -8376,16 +8376,164 @@ var init_agents = __esm(() => {
8376
8376
  init_formats();
8377
8377
  });
8378
8378
 
8379
+ // src/sessions/project-dir.ts
8380
+ import {
8381
+ closeSync as closeSync2,
8382
+ existsSync as existsSync9,
8383
+ mkdirSync as mkdirSync6,
8384
+ openSync as openSync2,
8385
+ readFileSync as readFileSync9,
8386
+ readSync,
8387
+ renameSync as renameSync2,
8388
+ statSync as statSync3,
8389
+ writeFileSync as writeFileSync6
8390
+ } from "node:fs";
8391
+ import { join as join8 } from "node:path";
8392
+ function cachePath(configDir) {
8393
+ return join8(configDir, CACHE_FILENAME2);
8394
+ }
8395
+ function loadCacheFile(configDir) {
8396
+ try {
8397
+ const raw = JSON.parse(readFileSync9(cachePath(configDir), "utf-8"));
8398
+ if (raw.schema_version !== CACHE_SCHEMA_VERSION || typeof raw.entries !== "object")
8399
+ return {};
8400
+ return raw.entries;
8401
+ } catch {
8402
+ return {};
8403
+ }
8404
+ }
8405
+ function saveCacheFile(configDir, entries) {
8406
+ try {
8407
+ if (!existsSync9(configDir))
8408
+ mkdirSync6(configDir, { recursive: true, mode: 448 });
8409
+ const path3 = cachePath(configDir);
8410
+ const tmp = `${path3}.${process.pid}.tmp`;
8411
+ const file = { schema_version: CACHE_SCHEMA_VERSION, entries };
8412
+ writeFileSync6(tmp, JSON.stringify(file), { mode: 384 });
8413
+ renameSync2(tmp, path3);
8414
+ } catch {}
8415
+ }
8416
+ function readHead(path3, bytes = HEAD_BYTES) {
8417
+ let fd;
8418
+ try {
8419
+ fd = openSync2(path3, "r");
8420
+ } catch {
8421
+ return null;
8422
+ }
8423
+ try {
8424
+ const buf = Buffer.alloc(bytes);
8425
+ const read = readSync(fd, buf, 0, bytes, 0);
8426
+ return buf.subarray(0, read).toString("utf-8");
8427
+ } catch {
8428
+ return null;
8429
+ } finally {
8430
+ closeSync2(fd);
8431
+ }
8432
+ }
8433
+ function cwdFromJsonlHead(text) {
8434
+ for (const line of text.split(`
8435
+ `).slice(0, HEAD_LINES)) {
8436
+ if (!line.includes('"cwd"'))
8437
+ continue;
8438
+ try {
8439
+ const obj = JSON.parse(line);
8440
+ const cwd = obj.cwd ?? obj.payload?.cwd;
8441
+ if (typeof cwd === "string" && cwd.startsWith("/"))
8442
+ return cwd;
8443
+ } catch {}
8444
+ }
8445
+ return null;
8446
+ }
8447
+ function unmungeSlug(slug, exists = existsSync9) {
8448
+ const tokens = slug.replace(/^-/, "").split("-");
8449
+ if (tokens.length === 0 || tokens[0] === "")
8450
+ return null;
8451
+ let budget = 5000;
8452
+ const walk = (prefix, index) => {
8453
+ if (budget-- <= 0)
8454
+ return null;
8455
+ if (index === tokens.length)
8456
+ return exists(prefix) ? prefix : null;
8457
+ const asSegment = prefix === "" || exists(prefix) ? walk(`${prefix}/${tokens[index]}`, index + 1) : null;
8458
+ if (asSegment)
8459
+ return asSegment;
8460
+ return prefix === "" ? null : walk(`${prefix}-${tokens[index]}`, index + 1);
8461
+ };
8462
+ return walk("", 0);
8463
+ }
8464
+ function fileMtime(path3) {
8465
+ try {
8466
+ return statSync3(path3).mtime.toISOString();
8467
+ } catch {
8468
+ return "";
8469
+ }
8470
+ }
8471
+ function createProjectDirResolver(configDir = getConfigDir(), deps = {}) {
8472
+ const exists = deps.exists ?? existsSync9;
8473
+ const head = deps.readHead ?? readHead;
8474
+ const mtime = deps.mtime ?? fileMtime;
8475
+ const entries = loadCacheFile(configDir);
8476
+ let dirty = false;
8477
+ const compute = (session) => {
8478
+ switch (session.harness) {
8479
+ case "opencode":
8480
+ return session.project ?? null;
8481
+ case "claude": {
8482
+ const text = head(session.path);
8483
+ const cwd = text ? cwdFromJsonlHead(text) : null;
8484
+ if (cwd)
8485
+ return cwd;
8486
+ return session.project ? unmungeSlug(session.project, exists) : null;
8487
+ }
8488
+ case "codex": {
8489
+ const text = head(session.path);
8490
+ return text ? cwdFromJsonlHead(text) : null;
8491
+ }
8492
+ case "cursor":
8493
+ return session.project ? unmungeSlug(session.project, exists) : null;
8494
+ default:
8495
+ return null;
8496
+ }
8497
+ };
8498
+ return {
8499
+ cached(key) {
8500
+ return entries[key]?.dir ?? null;
8501
+ },
8502
+ resolve(session) {
8503
+ const key = `${session.harness}/${session.id}`;
8504
+ const cached = entries[key];
8505
+ if (cached && (cached.dir !== null || cached.mtime === mtime(session.path))) {
8506
+ return cached.dir;
8507
+ }
8508
+ const dir = compute(session);
8509
+ entries[key] = { dir, mtime: mtime(session.path) };
8510
+ dirty = true;
8511
+ return dir;
8512
+ },
8513
+ flush() {
8514
+ if (!dirty)
8515
+ return;
8516
+ saveCacheFile(configDir, entries);
8517
+ dirty = false;
8518
+ }
8519
+ };
8520
+ }
8521
+ var CACHE_FILENAME2 = "project-dirs.json", CACHE_SCHEMA_VERSION = 1, HEAD_BYTES, HEAD_LINES = 50;
8522
+ var init_project_dir = __esm(() => {
8523
+ init_config();
8524
+ HEAD_BYTES = 128 * 1024;
8525
+ });
8526
+
8379
8527
  // src/sessions/scan.ts
8380
- import { existsSync as existsSync9, readdirSync, statSync as statSync3 } from "node:fs";
8528
+ import { existsSync as existsSync10, readdirSync, statSync as statSync4 } from "node:fs";
8381
8529
  import { createRequire as createRequire2 } from "node:module";
8382
8530
  import { homedir as homedir3 } from "node:os";
8383
- import { basename, join as join8 } from "node:path";
8531
+ import { basename, join as join9 } from "node:path";
8384
8532
  function listDir(dir) {
8385
8533
  try {
8386
8534
  return readdirSync(dir, { withFileTypes: true }).map((entry) => ({
8387
8535
  name: entry.name,
8388
- path: join8(dir, entry.name),
8536
+ path: join9(dir, entry.name),
8389
8537
  isDir: entry.isDirectory()
8390
8538
  }));
8391
8539
  } catch {
@@ -8398,7 +8546,7 @@ function sessionFromFile(filePath, harness, project) {
8398
8546
  return null;
8399
8547
  let mtime;
8400
8548
  try {
8401
- mtime = statSync3(filePath).mtime;
8549
+ mtime = statSync4(filePath).mtime;
8402
8550
  } catch {
8403
8551
  return null;
8404
8552
  }
@@ -8412,7 +8560,7 @@ function sessionFromFile(filePath, harness, project) {
8412
8560
  }
8413
8561
  function scanClaude(home) {
8414
8562
  const sessions = [];
8415
- for (const project of listDir(join8(home, ".claude", "projects"))) {
8563
+ for (const project of listDir(join9(home, ".claude", "projects"))) {
8416
8564
  if (!project.isDir)
8417
8565
  continue;
8418
8566
  for (const entry of listDir(project.path)) {
@@ -8427,10 +8575,10 @@ function scanClaude(home) {
8427
8575
  }
8428
8576
  function scanCursor(home) {
8429
8577
  const sessions = [];
8430
- for (const project of listDir(join8(home, ".cursor", "projects"))) {
8578
+ for (const project of listDir(join9(home, ".cursor", "projects"))) {
8431
8579
  if (!project.isDir)
8432
8580
  continue;
8433
- for (const entry of listDir(join8(project.path, "agent-transcripts"))) {
8581
+ for (const entry of listDir(join9(project.path, "agent-transcripts"))) {
8434
8582
  const files = entry.isDir ? listDir(entry.path).filter((f2) => !f2.isDir) : [entry];
8435
8583
  for (const file of files) {
8436
8584
  const session = sessionFromFile(file.path, "cursor", project.name);
@@ -8442,9 +8590,9 @@ function scanCursor(home) {
8442
8590
  return sessions;
8443
8591
  }
8444
8592
  function scanCodex(home, env) {
8445
- const codexHome2 = env.CODEX_HOME ?? join8(home, ".codex");
8593
+ const codexHome2 = env.CODEX_HOME ?? join9(home, ".codex");
8446
8594
  const sessions = [];
8447
- for (const year of listDir(join8(codexHome2, "sessions"))) {
8595
+ for (const year of listDir(join9(codexHome2, "sessions"))) {
8448
8596
  if (!year.isDir)
8449
8597
  continue;
8450
8598
  for (const month of listDir(year.path)) {
@@ -8500,9 +8648,9 @@ function querySqlite(dbPath, sql) {
8500
8648
  }
8501
8649
  }
8502
8650
  function scanOpencode(home, env) {
8503
- const dataDir = env.XDG_DATA_HOME ?? join8(home, ".local", "share");
8504
- const dbPath = join8(dataDir, "opencode", "opencode.db");
8505
- if (!existsSync9(dbPath))
8651
+ const dataDir = env.XDG_DATA_HOME ?? join9(home, ".local", "share");
8652
+ const dbPath = join9(dataDir, "opencode", "opencode.db");
8653
+ if (!existsSync10(dbPath))
8506
8654
  return [];
8507
8655
  const rows = querySqlite(dbPath, "SELECT id, directory, time_updated FROM session WHERE parent_id IS NULL");
8508
8656
  if (!rows)
@@ -8540,10 +8688,10 @@ function scanAgentSessions(options = {}) {
8540
8688
  var init_scan = () => {};
8541
8689
 
8542
8690
  // src/sync/watermark.ts
8543
- import { existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync9, renameSync as renameSync2, writeFileSync as writeFileSync6 } from "node:fs";
8544
- import { join as join9 } from "node:path";
8691
+ import { existsSync as existsSync11, mkdirSync as mkdirSync7, readFileSync as readFileSync10, renameSync as renameSync3, writeFileSync as writeFileSync7 } from "node:fs";
8692
+ import { join as join10 } from "node:path";
8545
8693
  function syncStatePath(configDir = getConfigDir()) {
8546
- return join9(configDir, STATE_FILENAME);
8694
+ return join10(configDir, STATE_FILENAME);
8547
8695
  }
8548
8696
  function loadSyncState(configDir = getConfigDir()) {
8549
8697
  const empty = {
@@ -8552,13 +8700,13 @@ function loadSyncState(configDir = getConfigDir()) {
8552
8700
  consecutive_failures: 0
8553
8701
  };
8554
8702
  const path3 = syncStatePath(configDir);
8555
- if (!existsSync10(path3))
8703
+ if (!existsSync11(path3))
8556
8704
  return empty;
8557
8705
  try {
8558
- const raw = JSON.parse(readFileSync9(path3, "utf-8"));
8706
+ const raw = JSON.parse(readFileSync10(path3, "utf-8"));
8559
8707
  if (raw.schema_version !== STATE_SCHEMA_VERSION)
8560
8708
  return empty;
8561
- const minedSessions = Array.isArray(raw.mined_sessions) ? raw.mined_sessions.filter((record) => typeof record === "object" && record !== null && typeof record.at === "string" && typeof record.session === "string").map((record) => ({
8709
+ const studiedSessions = Array.isArray(raw.mined_sessions) ? raw.mined_sessions.filter((record) => typeof record === "object" && record !== null && typeof record.at === "string" && typeof record.session === "string").map((record) => ({
8562
8710
  at: record.at,
8563
8711
  session: record.session,
8564
8712
  ...typeof record.project === "string" ? { project: record.project } : {}
@@ -8572,8 +8720,8 @@ function loadSyncState(configDir = getConfigDir()) {
8572
8720
  watermark: typeof raw.watermark === "string" ? raw.watermark : null,
8573
8721
  last_attempt_at: typeof raw.last_attempt_at === "string" ? raw.last_attempt_at : undefined,
8574
8722
  consecutive_failures: typeof raw.consecutive_failures === "number" && raw.consecutive_failures >= 0 ? raw.consecutive_failures : 0,
8575
- mined_sessions: minedSessions,
8576
- total_mined: typeof raw.total_mined === "number" && raw.total_mined >= 0 ? raw.total_mined : minedSessions.length,
8723
+ mined_sessions: studiedSessions,
8724
+ total_mined: typeof raw.total_mined === "number" && raw.total_mined >= 0 ? raw.total_mined : studiedSessions.length,
8577
8725
  total_notes: typeof raw.total_notes === "number" && raw.total_notes >= 0 ? raw.total_notes : 0,
8578
8726
  total_learning_tokens: typeof raw.total_learning_tokens === "number" && raw.total_learning_tokens >= 0 ? raw.total_learning_tokens : 0,
8579
8727
  ...lastRefusal ? { last_refusal: lastRefusal } : {},
@@ -8607,12 +8755,12 @@ function resetSyncState(configDir = getConfigDir()) {
8607
8755
  saveSyncState(fresh, configDir);
8608
8756
  }
8609
8757
  function saveSyncState(state, configDir = getConfigDir()) {
8610
- if (!existsSync10(configDir))
8611
- mkdirSync6(configDir, { recursive: true, mode: 448 });
8758
+ if (!existsSync11(configDir))
8759
+ mkdirSync7(configDir, { recursive: true, mode: 448 });
8612
8760
  const path3 = syncStatePath(configDir);
8613
8761
  const tmp = `${path3}.${process.pid}.tmp`;
8614
- writeFileSync6(tmp, JSON.stringify(state, null, 2), { mode: 384 });
8615
- renameSync2(tmp, path3);
8762
+ writeFileSync7(tmp, JSON.stringify(state, null, 2), { mode: 384 });
8763
+ renameSync3(tmp, path3);
8616
8764
  }
8617
8765
  function backoffUntil(state) {
8618
8766
  if (state.consecutive_failures === 0 || !state.last_attempt_at)
@@ -8656,7 +8804,7 @@ function gateSessions(sessions, watermark, now = new Date, quietPeriodMs = DEFAU
8656
8804
  }
8657
8805
  return { ready, open: open2 };
8658
8806
  }
8659
- var STATE_FILENAME = "knowledge-sync.json", STATE_SCHEMA_VERSION = 1, DEFAULT_QUIET_PERIOD_MS, BACKOFF_BASE_MS, BACKOFF_MAX_MS, MINED_HISTORY_LIMIT = 500, UNKNOWN_PROJECT = "(unknown)";
8807
+ var STATE_FILENAME = "knowledge-sync.json", STATE_SCHEMA_VERSION = 1, DEFAULT_QUIET_PERIOD_MS, BACKOFF_BASE_MS, BACKOFF_MAX_MS, STUDIED_HISTORY_LIMIT = 500, UNKNOWN_PROJECT = "(unknown)";
8660
8808
  var init_watermark = __esm(() => {
8661
8809
  init_config();
8662
8810
  DEFAULT_QUIET_PERIOD_MS = 5 * 60 * 1000;
@@ -8672,20 +8820,34 @@ async function fetchReportNotes(cfg) {
8672
8820
  }
8673
8821
  const { createTypedClient: createTypedClient2 } = await Promise.resolve().then(() => (init_trpc(), exports_trpc));
8674
8822
  const client = createTypedClient2(cfg);
8675
- const { notes } = await client.notes.listMine.query({
8676
- org_id: orgId,
8677
- limit: REPORT_NOTES_LIMIT
8678
- });
8679
- return notes.map((note) => ({
8680
- title: note.title,
8681
- content: note.body,
8682
- at: note.created_at,
8683
- ...note.transcript_id ? { transcript_id: note.transcript_id } : {},
8684
- ...note.repo ? { repo: note.repo } : {},
8685
- ...note.branch ? { branch: note.branch } : {}
8686
- }));
8823
+ const notes = [];
8824
+ let cursor3;
8825
+ let truncated = false;
8826
+ for (let page = 0;page < REPORT_NOTES_MAX_PAGES; page++) {
8827
+ const result = await client.notes.listMine.query({
8828
+ org_id: orgId,
8829
+ limit: REPORT_NOTES_LIMIT,
8830
+ ...cursor3 ? { cursor: cursor3 } : {}
8831
+ });
8832
+ notes.push(...result.notes.map((note) => ({
8833
+ title: note.title,
8834
+ content: note.body,
8835
+ at: note.created_at,
8836
+ ...note.transcript_id ? { transcript_id: note.transcript_id } : {},
8837
+ ...note.repo ? { repo: note.repo } : {},
8838
+ ...note.branch ? { branch: note.branch } : {}
8839
+ })));
8840
+ cursor3 = result.next_cursor;
8841
+ if (!cursor3) {
8842
+ truncated = result.notes.length === REPORT_NOTES_LIMIT;
8843
+ break;
8844
+ }
8845
+ if (page === REPORT_NOTES_MAX_PAGES - 1)
8846
+ truncated = true;
8847
+ }
8848
+ return { notes, truncated };
8687
8849
  }
8688
- var REPORT_NOTES_LIMIT = 500;
8850
+ var REPORT_NOTES_LIMIT = 500, REPORT_NOTES_MAX_PAGES = 40;
8689
8851
 
8690
8852
  // src/report/css.ts
8691
8853
  var REPORT_CSS = `
@@ -8860,38 +9022,6 @@ function applyStatusDefaults(candidates, dryRun) {
8860
9022
  return { ...c2, status: known.has(current) ? current : fallback };
8861
9023
  });
8862
9024
  }
8863
- function tokenTotalsFromCandidates(candidates, inventory) {
8864
- if (candidates.length === 0)
8865
- return null;
8866
- let replaced = 0;
8867
- let hasRediscovery = false;
8868
- let readCost = 0;
8869
- for (const c2 of candidates) {
8870
- const raw = c2.approx_rediscovery_tokens;
8871
- if (raw != null) {
8872
- hasRediscovery = true;
8873
- replaced += Math.max(0, Number(raw) || 0);
8874
- }
8875
- const blob = `${c2.title ?? ""}
8876
- ${c2.content ?? ""}`;
8877
- readCost += Math.round(blob.length / CHARS_PER_TOKEN);
8878
- }
8879
- if (!hasRediscovery)
8880
- return null;
8881
- let baseline = inventory.totals?.learning_tokens ?? 0;
8882
- if (!baseline) {
8883
- baseline = inventory.transcripts.reduce((sum, t2) => sum + (t2.learning_tokens || 0), 0);
8884
- }
8885
- const saved = Math.max(0, replaced - readCost);
8886
- const pct = baseline ? Math.round(1000 * saved / baseline) / 10 : 0;
8887
- return {
8888
- baseline_tokens: baseline,
8889
- replaced_baseline_tokens: replaced,
8890
- read_knowledge_tokens: readCost,
8891
- tokens_saved: saved,
8892
- pct_saved: pct
8893
- };
8894
- }
8895
9025
  function presentationCopy(c2) {
8896
9026
  const idea = (c2.plain_english || "").trim() || (c2.content || "").trim();
8897
9027
  const how = (c2.how_found || "").trim();
@@ -9265,11 +9395,9 @@ function buildReportHtml(options) {
9265
9395
  return bv - av;
9266
9396
  });
9267
9397
  const org = options.orgName || "Your team";
9268
- const repo = options.repo || inventory.cwd || "—";
9269
- const branch = options.branch || "—";
9398
+ const projects = options.projects ?? [];
9270
9399
  const summary = options.summary || "Local agent session logs were studied and distilled into Dosu notes so the next task can reuse them — reducing rediscovery cost.";
9271
9400
  const generated = `${(options.generatedAt ?? new Date).toISOString().replace("T", " ").slice(0, 16)} UTC`;
9272
- const derived = tokenTotalsFromCandidates(candidates, inventory);
9273
9401
  const [notesHeading, notesLede, footerTitle, footerBody] = notesSectionCopy(candidates);
9274
9402
  const candidateRows = candidates.map((c2, i2) => {
9275
9403
  const [idea, work] = presentationCopy(c2);
@@ -9309,26 +9437,18 @@ function buildReportHtml(options) {
9309
9437
  <td>${esc(query)}</td>
9310
9438
  </tr>`;
9311
9439
  }).join("");
9312
- let tokenSection;
9313
- if (derived) {
9314
- tokenSection = `
9440
+ const scannedTokens = inventory.totals?.learning_tokens || transcripts.reduce((sum, t2) => sum + (t2.learning_tokens || 0), 0);
9441
+ const writtenCount = candidates.filter((c2) => (c2.status || "written").toLowerCase() === "written").length;
9442
+ const writtenLabel = options.dryRun ? "Notes proposed (dry run)" : "Notes written to Dosu";
9443
+ const writtenValue = options.dryRun ? candidates.length : writtenCount;
9444
+ const tokenSection = `
9315
9445
  <section>
9316
- <h2>Estimated context savings</h2>
9317
- <p class="lede">Counterfactual: replace rediscovery stretches with a Dosu <code>read_knowledge</code> hit.</p>
9446
+ <h2>This run</h2>
9318
9447
  <div class="stats">
9319
- <div class="stat"><div class="label">Baseline (cost to learn)</div><div class="value">${fmtInt(derived.baseline_tokens)}</div></div>
9320
- <div class="stat"><div class="label">Learning replaced</div><div class="value">${fmtInt(derived.replaced_baseline_tokens)}</div></div>
9321
- <div class="stat"><div class="label">Read cost</div><div class="value">${fmtInt(derived.read_knowledge_tokens)}</div></div>
9322
- <div class="stat highlight"><div class="label">Est. tokens saved</div><div class="value">${fmtInt(derived.tokens_saved)} <span class="pct">(${esc(`${derived.pct_saved}%`)})</span></div></div>
9448
+ <div class="stat"><div class="label">Tokens scanned</div><div class="value">${fmtInt(scannedTokens)}</div></div>
9449
+ <div class="stat highlight"><div class="label">${esc(writtenLabel)}</div><div class="value">${fmtInt(writtenValue)}</div></div>
9323
9450
  </div>
9324
9451
  </section>`;
9325
- } else {
9326
- tokenSection = `
9327
- <section>
9328
- <h2>Estimated context savings</h2>
9329
- <p class="muted">No rediscovery estimates on the notes yet — each candidate needs <code>approx_rediscovery_tokens</code>.</p>
9330
- </section>`;
9331
- }
9332
9452
  const notesBody = candidateRows.join("") || '<p class="muted">No write_knowledge payloads yet. Run knowledge sync so Dosu studies your sessions and extracts learnings.</p>';
9333
9453
  const lede = notesLede ? `<p class="lede">${esc(notesLede)}</p>` : "";
9334
9454
  return `<!DOCTYPE html>
@@ -9348,9 +9468,8 @@ ${REPORT_CSS}
9348
9468
  <h1>${esc(org)}</h1>
9349
9469
  <p class="lede">${esc(summary)}</p>
9350
9470
  <p class="meta-line">
9351
- Repo <code>${esc(repo)}</code>
9352
- · branch <code>${esc(branch)}</code>
9353
- · generated ${esc(generated)}
9471
+ ${projects.length > 0 ? `Projects ${projects.map((p2) => `<code>${esc(p2)}</code>`).join(", ")} · ` : ""}${transcripts.length} session${transcripts.length === 1 ? "" : "s"} with notes
9472
+ · generated ${esc(generated)}${options.truncated ? " · newest notes only (server capped the fetch)" : ""}
9354
9473
  </p>
9355
9474
  <div class="toolbar no-print">
9356
9475
  <button type="button" onclick="window.print()">Print / Save as PDF</button>
@@ -9396,7 +9515,7 @@ ${REPORT_CSS}
9396
9515
  </html>
9397
9516
  `;
9398
9517
  }
9399
- var CHARS_PER_TOKEN = 4, MAX_TRACE_STEPS = 50, PREVIEW_CHARS = 80, SQL_DISPLAY, LOGS_DISPLAY, SKIP_TOOL_NAMES, TAG_RE, WS_RE;
9518
+ var MAX_TRACE_STEPS = 50, PREVIEW_CHARS = 80, SQL_DISPLAY, LOGS_DISPLAY, SKIP_TOOL_NAMES, TAG_RE, WS_RE;
9400
9519
  var init_html = __esm(() => {
9401
9520
  SQL_DISPLAY = new Set(["execute_sql", "query_run", "list_tables"]);
9402
9521
  LOGS_DISPLAY = new Set(["query_logs"]);
@@ -9406,7 +9525,7 @@ var init_html = __esm(() => {
9406
9525
  });
9407
9526
 
9408
9527
  // src/sessions/read.ts
9409
- import { readFileSync as readFileSync10 } from "node:fs";
9528
+ import { readFileSync as readFileSync11 } from "node:fs";
9410
9529
  function asRecord(value) {
9411
9530
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
9412
9531
  }
@@ -9522,11 +9641,11 @@ function readSessionTurns(session) {
9522
9641
  try {
9523
9642
  switch (session.harness) {
9524
9643
  case "claude":
9525
- return readClaude(readFileSync10(session.path, "utf8"));
9644
+ return readClaude(readFileSync11(session.path, "utf8"));
9526
9645
  case "cursor":
9527
- return readCursor(readFileSync10(session.path, "utf8"));
9646
+ return readCursor(readFileSync11(session.path, "utf8"));
9528
9647
  case "codex":
9529
- return readCodex(readFileSync10(session.path, "utf8"));
9648
+ return readCodex(readFileSync11(session.path, "utf8"));
9530
9649
  case "opencode":
9531
9650
  return readOpencode(session.path, session.id);
9532
9651
  }
@@ -9554,7 +9673,7 @@ function countRediscoveryToolCalls(session) {
9554
9673
  case "claude":
9555
9674
  case "cursor": {
9556
9675
  let n2 = 0;
9557
- for (const record of jsonlRecords(readFileSync10(session.path, "utf8"))) {
9676
+ for (const record of jsonlRecords(readFileSync11(session.path, "utf8"))) {
9558
9677
  const message = asRecord(record.message);
9559
9678
  for (const name of toolNamesFromContent(message?.content)) {
9560
9679
  if (isRediscoveryTool(name))
@@ -9565,7 +9684,7 @@ function countRediscoveryToolCalls(session) {
9565
9684
  }
9566
9685
  case "codex": {
9567
9686
  let n2 = 0;
9568
- for (const record of jsonlRecords(readFileSync10(session.path, "utf8"))) {
9687
+ for (const record of jsonlRecords(readFileSync11(session.path, "utf8"))) {
9569
9688
  const payload = asRecord(record.payload);
9570
9689
  if (payload?.type === "function_call" && typeof payload.name === "string") {
9571
9690
  if (isRediscoveryTool(payload.name))
@@ -9585,9 +9704,9 @@ function estimateSessionTokens(session) {
9585
9704
  let chars = 0;
9586
9705
  for (const turn of readSessionTurns(session))
9587
9706
  chars += turn.text.length;
9588
- return Math.round(chars / CHARS_PER_TOKEN2);
9707
+ return Math.round(chars / CHARS_PER_TOKEN);
9589
9708
  }
9590
- function isWorthMining(session) {
9709
+ function isWorthStudying(session) {
9591
9710
  const turns = readSessionTurns(session);
9592
9711
  if (turns.length < MIN_WORTH_TURNS)
9593
9712
  return false;
@@ -9599,7 +9718,7 @@ function isWorthMining(session) {
9599
9718
  }
9600
9719
  return false;
9601
9720
  }
9602
- var PLAIN_TEXT, CODEX_TEXT, REDISCOVERY_TOOLS, CHARS_PER_TOKEN2 = 4, MIN_WORTH_TURNS = 4, MIN_WORTH_CHARS = 2000;
9721
+ var PLAIN_TEXT, CODEX_TEXT, REDISCOVERY_TOOLS, CHARS_PER_TOKEN = 4, MIN_WORTH_TURNS = 4, MIN_WORTH_CHARS = 2000;
9603
9722
  var init_read = __esm(() => {
9604
9723
  init_scan();
9605
9724
  PLAIN_TEXT = new Set(["text"]);
@@ -9656,12 +9775,12 @@ var init_queries = __esm(() => {
9656
9775
  });
9657
9776
 
9658
9777
  // src/report/digest.ts
9659
- import { readFileSync as readFileSync11 } from "node:fs";
9778
+ import { readFileSync as readFileSync12 } from "node:fs";
9660
9779
  function asRecord2(value) {
9661
9780
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
9662
9781
  }
9663
9782
  function estimateTokens(text) {
9664
- return Math.round(text.length / CHARS_PER_TOKEN3);
9783
+ return Math.round(text.length / CHARS_PER_TOKEN2);
9665
9784
  }
9666
9785
  function digestEstTokens(texts, tools, resultChars) {
9667
9786
  const countable = tools.filter((t2) => t2.name !== "tool_result");
@@ -9671,7 +9790,7 @@ function digestEstTokens(texts, tools, resultChars) {
9671
9790
  payload += JSON.stringify(countable);
9672
9791
  let tokens = estimateTokens(payload);
9673
9792
  if (resultChars > 0)
9674
- tokens += Math.max(1, Math.round(resultChars / CHARS_PER_TOKEN3));
9793
+ tokens += Math.max(1, Math.round(resultChars / CHARS_PER_TOKEN2));
9675
9794
  return tokens;
9676
9795
  }
9677
9796
  function copyToolInputFields(entry, inp) {
@@ -9887,16 +10006,16 @@ function sessionToDigest(session) {
9887
10006
  try {
9888
10007
  switch (session.harness) {
9889
10008
  case "claude":
9890
- return { turns: digestClaude(readFileSync11(session.path, "utf8")) };
10009
+ return { turns: digestClaude(readFileSync12(session.path, "utf8")) };
9891
10010
  case "cursor":
9892
- return { turns: digestCursor(readFileSync11(session.path, "utf8")) };
10011
+ return { turns: digestCursor(readFileSync12(session.path, "utf8")) };
9893
10012
  case "codex":
9894
- return { turns: digestCodex(readFileSync11(session.path, "utf8")) };
10013
+ return { turns: digestCodex(readFileSync12(session.path, "utf8")) };
9895
10014
  case "opencode": {
9896
10015
  const turns = readSessionTurns(session).map((turn, index) => ({
9897
10016
  role: turn.role,
9898
10017
  line: index + 1,
9899
- est_tokens: Math.round(turn.text.length / CHARS_PER_TOKEN3),
10018
+ est_tokens: Math.round(turn.text.length / CHARS_PER_TOKEN2),
9900
10019
  text: [turn.text],
9901
10020
  tools: []
9902
10021
  }));
@@ -9921,7 +10040,7 @@ function digestTurnText(turn) {
9921
10040
  return texts.filter(Boolean).join(`
9922
10041
  `);
9923
10042
  }
9924
- var CHARS_PER_TOKEN3 = 4, MAX_ASSISTANT_CHARS = 4000, MCP_DIGEST_TOOLS;
10043
+ var CHARS_PER_TOKEN2 = 4, MAX_ASSISTANT_CHARS = 4000, MCP_DIGEST_TOOLS;
9925
10044
  var init_digest = __esm(() => {
9926
10045
  init_read();
9927
10046
  init_queries();
@@ -10112,17 +10231,17 @@ var init_notes = __esm(() => {
10112
10231
  });
10113
10232
 
10114
10233
  // src/report/write.ts
10115
- import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "node:fs";
10234
+ import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "node:fs";
10116
10235
  import { tmpdir } from "node:os";
10117
- import { dirname as dirname4, join as join10, resolve } from "node:path";
10236
+ import { dirname as dirname4, join as join11, resolve } from "node:path";
10118
10237
  import { pathToFileURL } from "node:url";
10119
10238
  function defaultReportPath() {
10120
- return join10(tmpdir(), "dosu-knowledge-report.html");
10239
+ return join11(tmpdir(), "dosu-knowledge-report.html");
10121
10240
  }
10122
10241
  async function writeAndOpenReport(options) {
10123
10242
  const out = resolve(options.out ?? defaultReportPath());
10124
- mkdirSync7(dirname4(out), { recursive: true });
10125
- writeFileSync7(out, options.html, "utf8");
10243
+ mkdirSync8(dirname4(out), { recursive: true });
10244
+ writeFileSync8(out, options.html, "utf8");
10126
10245
  if (options.open !== false) {
10127
10246
  const url = pathToFileURL(out).href;
10128
10247
  if (options.openUrl) {
@@ -10137,17 +10256,26 @@ async function writeAndOpenReport(options) {
10137
10256
  var init_write = () => {};
10138
10257
 
10139
10258
  // src/report/generate.ts
10259
+ import { basename as basename2 } from "node:path";
10140
10260
  function sessionsForNotes(notes, sessions) {
10141
10261
  const ids = new Set(notes.map((n2) => n2.transcript_id).filter((id) => Boolean(id)));
10142
10262
  return sessions.filter((s) => ids.has(s.id));
10143
10263
  }
10264
+ function defaultProjectName() {
10265
+ const resolver = createProjectDirResolver();
10266
+ return (session) => {
10267
+ const dir = resolver.resolve(session);
10268
+ return dir ? basename2(dir) : null;
10269
+ };
10270
+ }
10144
10271
  function noteTime(note) {
10145
10272
  const parsed = Date.parse(note.at ?? "");
10146
10273
  return Number.isNaN(parsed) ? 0 : parsed;
10147
10274
  }
10148
10275
  async function emitKnowledgeReport(options = {}) {
10149
10276
  const cfg = loadConfig();
10150
- const notes = [...options.notes ?? await (options.fetchNotes ?? fetchReportNotes)(cfg)].sort((a3, b2) => noteTime(a3) - noteTime(b2));
10277
+ const fetched = options.notes ? { notes: options.notes, truncated: false } : await (options.fetchNotes ?? fetchReportNotes)(cfg);
10278
+ const notes = [...fetched.notes].sort((a3, b2) => noteTime(a3) - noteTime(b2));
10151
10279
  const scanned = options.sessions ?? scanAgentSessions();
10152
10280
  const sessions = sessionsForNotes(notes, scanned);
10153
10281
  const candidates = attributeRediscovery(notes, sessions);
@@ -10156,12 +10284,16 @@ async function emitKnowledgeReport(options = {}) {
10156
10284
  if (inventory.totals && state.total_learning_tokens && !options.sessions) {
10157
10285
  inventory.totals.learning_tokens = state.total_learning_tokens;
10158
10286
  }
10287
+ const projectName = options.projectName ?? defaultProjectName();
10288
+ const projects = [
10289
+ ...new Set(sessions.map((s) => projectName(s) ?? s.project).filter((name) => Boolean(name)))
10290
+ ];
10159
10291
  const html = buildReportHtml({
10160
10292
  inventory,
10161
10293
  candidates,
10162
10294
  orgName: options.orgName ?? cfg.active_account?.target?.org_name ?? "Your team",
10163
- repo: options.repo ?? notes.find((n2) => n2.repo)?.repo,
10164
- branch: options.branch ?? notes.find((n2) => n2.branch)?.branch,
10295
+ projects,
10296
+ truncated: fetched.truncated,
10165
10297
  generatedAt: options.generatedAt,
10166
10298
  digests: digestsForSessions(sessions)
10167
10299
  });
@@ -10174,6 +10306,7 @@ async function emitKnowledgeReport(options = {}) {
10174
10306
  }
10175
10307
  var init_generate = __esm(() => {
10176
10308
  init_config();
10309
+ init_project_dir();
10177
10310
  init_scan();
10178
10311
  init_watermark();
10179
10312
  init_html();
@@ -10181,151 +10314,6 @@ var init_generate = __esm(() => {
10181
10314
  init_write();
10182
10315
  });
10183
10316
 
10184
- // src/sessions/project-dir.ts
10185
- import {
10186
- closeSync as closeSync2,
10187
- existsSync as existsSync11,
10188
- mkdirSync as mkdirSync8,
10189
- openSync as openSync2,
10190
- readFileSync as readFileSync12,
10191
- readSync,
10192
- renameSync as renameSync3,
10193
- statSync as statSync4,
10194
- writeFileSync as writeFileSync8
10195
- } from "node:fs";
10196
- import { join as join11 } from "node:path";
10197
- function cachePath(configDir) {
10198
- return join11(configDir, CACHE_FILENAME2);
10199
- }
10200
- function loadCacheFile(configDir) {
10201
- try {
10202
- const raw = JSON.parse(readFileSync12(cachePath(configDir), "utf-8"));
10203
- if (raw.schema_version !== CACHE_SCHEMA_VERSION || typeof raw.entries !== "object")
10204
- return {};
10205
- return raw.entries;
10206
- } catch {
10207
- return {};
10208
- }
10209
- }
10210
- function saveCacheFile(configDir, entries) {
10211
- try {
10212
- if (!existsSync11(configDir))
10213
- mkdirSync8(configDir, { recursive: true, mode: 448 });
10214
- const path3 = cachePath(configDir);
10215
- const tmp = `${path3}.${process.pid}.tmp`;
10216
- const file = { schema_version: CACHE_SCHEMA_VERSION, entries };
10217
- writeFileSync8(tmp, JSON.stringify(file), { mode: 384 });
10218
- renameSync3(tmp, path3);
10219
- } catch {}
10220
- }
10221
- function readHead(path3, bytes = HEAD_BYTES) {
10222
- let fd;
10223
- try {
10224
- fd = openSync2(path3, "r");
10225
- } catch {
10226
- return null;
10227
- }
10228
- try {
10229
- const buf = Buffer.alloc(bytes);
10230
- const read = readSync(fd, buf, 0, bytes, 0);
10231
- return buf.subarray(0, read).toString("utf-8");
10232
- } catch {
10233
- return null;
10234
- } finally {
10235
- closeSync2(fd);
10236
- }
10237
- }
10238
- function cwdFromJsonlHead(text) {
10239
- for (const line of text.split(`
10240
- `).slice(0, HEAD_LINES)) {
10241
- if (!line.includes('"cwd"'))
10242
- continue;
10243
- try {
10244
- const obj = JSON.parse(line);
10245
- const cwd = obj.cwd ?? obj.payload?.cwd;
10246
- if (typeof cwd === "string" && cwd.startsWith("/"))
10247
- return cwd;
10248
- } catch {}
10249
- }
10250
- return null;
10251
- }
10252
- function unmungeSlug(slug, exists = existsSync11) {
10253
- const tokens = slug.replace(/^-/, "").split("-");
10254
- if (tokens.length === 0 || tokens[0] === "")
10255
- return null;
10256
- let budget = 5000;
10257
- const walk = (prefix, index) => {
10258
- if (budget-- <= 0)
10259
- return null;
10260
- if (index === tokens.length)
10261
- return exists(prefix) ? prefix : null;
10262
- const asSegment = prefix === "" || exists(prefix) ? walk(`${prefix}/${tokens[index]}`, index + 1) : null;
10263
- if (asSegment)
10264
- return asSegment;
10265
- return prefix === "" ? null : walk(`${prefix}-${tokens[index]}`, index + 1);
10266
- };
10267
- return walk("", 0);
10268
- }
10269
- function fileMtime(path3) {
10270
- try {
10271
- return statSync4(path3).mtime.toISOString();
10272
- } catch {
10273
- return "";
10274
- }
10275
- }
10276
- function createProjectDirResolver(configDir = getConfigDir(), deps = {}) {
10277
- const exists = deps.exists ?? existsSync11;
10278
- const head = deps.readHead ?? readHead;
10279
- const mtime = deps.mtime ?? fileMtime;
10280
- const entries = loadCacheFile(configDir);
10281
- let dirty = false;
10282
- const compute = (session) => {
10283
- switch (session.harness) {
10284
- case "opencode":
10285
- return session.project ?? null;
10286
- case "claude": {
10287
- const text = head(session.path);
10288
- const cwd = text ? cwdFromJsonlHead(text) : null;
10289
- if (cwd)
10290
- return cwd;
10291
- return session.project ? unmungeSlug(session.project, exists) : null;
10292
- }
10293
- case "codex": {
10294
- const text = head(session.path);
10295
- return text ? cwdFromJsonlHead(text) : null;
10296
- }
10297
- case "cursor":
10298
- return session.project ? unmungeSlug(session.project, exists) : null;
10299
- default:
10300
- return null;
10301
- }
10302
- };
10303
- return {
10304
- resolve(session) {
10305
- const key = `${session.harness}/${session.id}`;
10306
- const cached = entries[key];
10307
- if (cached && (cached.dir !== null || cached.mtime === mtime(session.path))) {
10308
- return cached.dir;
10309
- }
10310
- const dir = compute(session);
10311
- entries[key] = { dir, mtime: mtime(session.path) };
10312
- dirty = true;
10313
- return dir;
10314
- },
10315
- flush() {
10316
- if (!dirty)
10317
- return;
10318
- saveCacheFile(configDir, entries);
10319
- dirty = false;
10320
- }
10321
- };
10322
- }
10323
- var CACHE_FILENAME2 = "project-dirs.json", CACHE_SCHEMA_VERSION = 1, HEAD_BYTES, HEAD_LINES = 50;
10324
- var init_project_dir = __esm(() => {
10325
- init_config();
10326
- HEAD_BYTES = 128 * 1024;
10327
- });
10328
-
10329
10317
  // src/sync/backlog.ts
10330
10318
  function listSessionBacklog() {
10331
10319
  try {
@@ -10470,7 +10458,7 @@ function readLockFile(configDir) {
10470
10458
  }
10471
10459
  function recentSyncActivity(log3) {
10472
10460
  return log3.split(`
10473
- `).filter((line) => line.includes("[sync]") || line.includes("[miner]")).slice(-ACTIVITY_LINES).map((line) => line.replace(/ \[(DEBUG|INFO|WARN|ERROR)\]/, ""));
10461
+ `).filter((line) => line.includes("[sync]") || line.includes("[learner]")).slice(-ACTIVITY_LINES).map((line) => line.replace(/ \[(DEBUG|INFO|WARN|ERROR)\]/, ""));
10474
10462
  }
10475
10463
  function getSyncStatus(deps = {}) {
10476
10464
  const configDir = deps.configDir ?? getConfigDir();
@@ -10591,14 +10579,14 @@ async function runKnowledgeSync(options = {}) {
10591
10579
  baseline_mined: state.total_mined ?? 0
10592
10580
  };
10593
10581
  }
10594
- const worthMining = deps.worthMining ?? isWorthMining;
10582
+ const worthStudying = deps.worthStudying ?? isWorthStudying;
10595
10583
  const examined = [];
10596
10584
  const batch = [];
10597
10585
  let trivial = 0;
10598
10586
  for (let i2 = ready.length - 1;i2 >= 0 && batch.length < MINE_BATCH_LIMIT; i2--) {
10599
10587
  const candidate = ready[i2];
10600
10588
  examined.push(candidate);
10601
- if (worthMining(candidate)) {
10589
+ if (worthStudying(candidate)) {
10602
10590
  batch.push(candidate);
10603
10591
  } else {
10604
10592
  trivial += 1;
@@ -10615,26 +10603,26 @@ async function runKnowledgeSync(options = {}) {
10615
10603
  return {
10616
10604
  status: "nothing-new",
10617
10605
  ...base,
10618
- minedSessions: 0,
10606
+ studiedSessions: 0,
10619
10607
  trivialSessions: trivial
10620
10608
  };
10621
10609
  }
10622
10610
  logger.debug("sync", `studying ${batch.length} of ${ready.length} ready sessions (${trivial} trivial skipped)`);
10623
- const miner = await deps.mine(batch);
10624
- switch (miner.outcome) {
10611
+ const learner = await deps.mine(batch);
10612
+ switch (learner.outcome) {
10625
10613
  case "completed": {
10626
10614
  for (const s of batch) {
10627
10615
  logger.debug("sync", `studied session ${s.harness}/${s.id}`);
10628
10616
  }
10629
- const minedAt = now().toISOString();
10617
+ const studiedAt = now().toISOString();
10630
10618
  const history = [
10631
10619
  ...state.mined_sessions ?? [],
10632
10620
  ...batch.map((s) => ({
10633
- at: minedAt,
10621
+ at: studiedAt,
10634
10622
  session: `${s.harness}/${s.id}`,
10635
10623
  ...s.project ? { project: s.project } : {}
10636
10624
  }))
10637
- ].slice(-MINED_HISTORY_LIMIT);
10625
+ ].slice(-STUDIED_HISTORY_LIMIT);
10638
10626
  const watermark = batchWatermark(examined);
10639
10627
  const sessionTokens = deps.sessionTokens ?? estimateSessionTokens;
10640
10628
  let batchTokens = 0;
@@ -10643,21 +10631,21 @@ async function runKnowledgeSync(options = {}) {
10643
10631
  saveState({
10644
10632
  ...state,
10645
10633
  watermark,
10646
- last_attempt_at: minedAt,
10634
+ last_attempt_at: studiedAt,
10647
10635
  consecutive_failures: 0,
10648
10636
  mined_sessions: history,
10649
10637
  total_mined: (state.total_mined ?? 0) + batch.length,
10650
- total_notes: (state.total_notes ?? 0) + miner.notesWritten,
10638
+ total_notes: (state.total_notes ?? 0) + learner.notesWritten,
10651
10639
  total_learning_tokens: (state.total_learning_tokens ?? 0) + batchTokens,
10652
10640
  last_refusal: undefined
10653
10641
  });
10654
- logger.debug("sync", `studied ${batch.length} sessions, ${miner.notesWritten} suggested pages; watermark → ${watermark}`);
10642
+ logger.debug("sync", `studied ${batch.length} sessions, ${learner.notesWritten} suggested pages; watermark → ${watermark}`);
10655
10643
  return {
10656
- status: "mined",
10644
+ status: "studied",
10657
10645
  ...base,
10658
- minedSessions: batch.length,
10646
+ studiedSessions: batch.length,
10659
10647
  trivialSessions: trivial,
10660
- miner
10648
+ learner
10661
10649
  };
10662
10650
  }
10663
10651
  case "consent_off":
@@ -10670,12 +10658,12 @@ async function runKnowledgeSync(options = {}) {
10670
10658
  consecutive_failures: 0,
10671
10659
  last_refusal: {
10672
10660
  at,
10673
- outcome: miner.outcome,
10674
- message: miner.message ?? "Studying unavailable right now."
10661
+ outcome: learner.outcome,
10662
+ message: learner.message ?? "Studying unavailable right now."
10675
10663
  }
10676
10664
  });
10677
- logger.debug("sync", `studying skipped by gateway: ${miner.outcome}`);
10678
- return { status: "skipped-gateway", ...base, minedSessions: 0, miner };
10665
+ logger.debug("sync", `studying skipped by gateway: ${learner.outcome}`);
10666
+ return { status: "skipped-gateway", ...base, studiedSessions: 0, learner };
10679
10667
  }
10680
10668
  default: {
10681
10669
  saveState({
@@ -10683,13 +10671,13 @@ async function runKnowledgeSync(options = {}) {
10683
10671
  last_attempt_at: now().toISOString(),
10684
10672
  consecutive_failures: state.consecutive_failures + 1
10685
10673
  });
10686
- logger.debug("sync", `studying failed: ${miner.outcome}; ${miner.message ?? ""}`);
10674
+ logger.debug("sync", `studying failed: ${learner.outcome}; ${learner.message ?? ""}`);
10687
10675
  return {
10688
10676
  status: "mine-failed",
10689
10677
  ...base,
10690
- minedSessions: 0,
10691
- miner,
10692
- error: miner.message
10678
+ studiedSessions: 0,
10679
+ learner,
10680
+ error: learner.message
10693
10681
  };
10694
10682
  }
10695
10683
  }
@@ -10835,12 +10823,12 @@ var init_backfill_run = __esm(() => {
10835
10823
  init_backfill();
10836
10824
  });
10837
10825
 
10838
- // src/miner/config-dir.ts
10826
+ // src/learner/config-dir.ts
10839
10827
  import { mkdtempSync, rmSync as rmSync3, writeFileSync as writeFileSync10 } from "node:fs";
10840
10828
  import { tmpdir as tmpdir2 } from "node:os";
10841
10829
  import { join as join13 } from "node:path";
10842
10830
  function createRunConfigDir() {
10843
- const path3 = mkdtempSync(join13(tmpdir2(), "dosu-miner-"));
10831
+ const path3 = mkdtempSync(join13(tmpdir2(), "dosu-learner-"));
10844
10832
  writeFileSync10(join13(path3, ".claude.json"), JSON.stringify({ hasCompletedOnboarding: true }));
10845
10833
  return {
10846
10834
  path: path3,
@@ -10851,7 +10839,7 @@ function createRunConfigDir() {
10851
10839
  }
10852
10840
  var init_config_dir = () => {};
10853
10841
 
10854
- // src/miner/conflicts.ts
10842
+ // src/learner/conflicts.ts
10855
10843
  import { existsSync as existsSync13, readFileSync as readFileSync15 } from "node:fs";
10856
10844
  function managedSettingsPaths(platform3 = process.platform) {
10857
10845
  switch (platform3) {
@@ -10909,11 +10897,11 @@ var init_conflicts = __esm(() => {
10909
10897
  CONFLICTING_ENV_PREFIXES = ["ANTHROPIC_", "CLAUDE_CODE_", "AWS_"];
10910
10898
  });
10911
10899
 
10912
- // src/miner/env.ts
10900
+ // src/learner/env.ts
10913
10901
  function isStripped(name) {
10914
10902
  return STRIPPED_NAMES.has(name) || STRIPPED_PREFIXES.some((p2) => name.startsWith(p2));
10915
10903
  }
10916
- function buildMinerEnv(options) {
10904
+ function buildLearnerEnv(options) {
10917
10905
  const base = options.baseEnv ?? process.env;
10918
10906
  const env = {};
10919
10907
  for (const [name, value] of Object.entries(base)) {
@@ -10943,7 +10931,7 @@ var init_env = __esm(() => {
10943
10931
  STRIPPED_NAMES = new Set(["CLAUDE_CONFIG_DIR", "CLAUDECODE", "AWS_BEARER_TOKEN_BEDROCK"]);
10944
10932
  });
10945
10933
 
10946
- // src/miner/executable.ts
10934
+ // src/learner/executable.ts
10947
10935
  import { accessSync, constants, existsSync as existsSync14 } from "node:fs";
10948
10936
  import { createRequire as createRequire3 } from "node:module";
10949
10937
  import { homedir as homedir4 } from "node:os";
@@ -10990,9 +10978,9 @@ function resolveClaudeExecutable(options = {}) {
10990
10978
  }
10991
10979
  var init_executable = () => {};
10992
10980
 
10993
- // src/miner/prompt.ts
10994
- function buildMinerSystemPrompt(coreRules) {
10995
- return `You are Dosu's knowledge miner. You read a developer's recent coding-agent sessions and save the durable, non-obvious findings to the team's shared knowledge base so future teammates and agents do not have to rediscover them.
10981
+ // src/learner/prompt.ts
10982
+ function buildLearnerSystemPrompt(coreRules) {
10983
+ return `You are Dosu's knowledge learner. You read a developer's recent coding-agent sessions and save the durable, non-obvious findings to the team's shared knowledge base so future teammates and agents do not have to rediscover them.
10996
10984
 
10997
10985
  Available tools (the only tools you may use, by these exact names):
10998
10986
  - mcp__sessions__list_sessions / mcp__sessions__read_session: the session transcripts in scope for this run.
@@ -11000,7 +10988,7 @@ Available tools (the only tools you may use, by these exact names):
11000
10988
 
11001
10989
  ${coreRules}`;
11002
10990
  }
11003
- function buildMinerPrompt(sessions) {
10991
+ function buildLearnerPrompt(sessions) {
11004
10992
  const list = sessions.map((s) => `- ${s.id} (${s.harness})`).join(`
11005
10993
  `);
11006
10994
  return `Mine the following ${sessions.length} coding-agent session(s) for durable knowledge:
@@ -11010,8 +10998,8 @@ ${list}
11010
10998
  Work through them ONE AT A TIME: read a single session with read_session, decide what (if anything) is durable per your rules, dedupe against read_knowledge, and write that session's notes with write_knowledge BEFORE moving on to the next session. Each note is attributed to the session you read just before writing it, so do not read the next session until you have written this one's notes. When you are done, reply with a one-line summary: how many sessions you read and how many notes you wrote.`;
11011
10999
  }
11012
11000
 
11013
- // src/miner/prompt-core.ts
11014
- var MINER_CORE_RULES = `Rules — non-negotiable:
11001
+ // src/learner/prompt-core.ts
11002
+ var LEARNER_CORE_RULES = `Rules — non-negotiable:
11015
11003
  1. Before writing anything, call read_knowledge with the candidate topic. If the same fact is
11016
11004
  already recorded, skip it — never write a duplicate or near-duplicate. If the topic exists but
11017
11005
  the session shows it has changed or been superseded, do write: a note stating the current state
@@ -11039,10 +11027,9 @@ useful.
11039
11027
  6. Only pass repo/branch to write_knowledge when the session itself verifies them (an explicit
11040
11028
  cwd, git remote, or branch mentioned in the transcript). Never infer or guess a repo. When not
11041
11029
  verified, omit both.
11042
- 7. Populate write_knowledge metadata with source_agent and session_id for every note.
11043
- 8. Never quote credentials, tokens, or secrets — even redacted placeholders — and never include
11030
+ 7. Never quote credentials, tokens, or secrets — even redacted placeholders — and never include
11044
11031
  long verbatim transcript spans. Summarize in your own words.
11045
- 9. A trivial session with no real user query is normal: skip it silently.`;
11032
+ 8. A trivial session with no real user query is normal: skip it silently.`;
11046
11033
 
11047
11034
  // node_modules/@anthropic-ai/claude-agent-sdk/sdk.mjs
11048
11035
  var exports_sdk = {};
@@ -72793,7 +72780,7 @@ var init_redact = __esm(() => {
72793
72780
  ]);
72794
72781
  });
72795
72782
 
72796
- // src/miner/tools.ts
72783
+ // src/learner/tools.ts
72797
72784
  function textResult(text) {
72798
72785
  return { content: [{ type: "text", text }] };
72799
72786
  }
@@ -72852,7 +72839,7 @@ function createSessionToolsServer(sessions) {
72852
72839
  name: SESSIONS_SERVER_NAME,
72853
72840
  version: "1.0.0",
72854
72841
  tools: [
72855
- O6e("list_sessions", "List the coding-agent sessions in scope for this mining run.", {}, async () => textResult(formatSessionList(sessions))),
72842
+ O6e("list_sessions", "List the coding-agent sessions in scope for this study run.", {}, async () => textResult(formatSessionList(sessions))),
72856
72843
  O6e("read_session", "Read the conversation turns of one in-scope session. Responses are " + "paginated; pass the returned offset to continue reading.", {
72857
72844
  id: exports_external.string().describe("Session id from list_sessions"),
72858
72845
  offset: exports_external.number().int().min(0).optional().describe("Turn index to start from")
@@ -72878,11 +72865,11 @@ var init_tools = __esm(() => {
72878
72865
  init_redact();
72879
72866
  });
72880
72867
 
72881
- // src/miner/runner.ts
72868
+ // src/learner/runner.ts
72882
72869
  var exports_runner = {};
72883
72870
  __export(exports_runner, {
72884
72871
  traceAgentMessage: () => traceAgentMessage,
72885
- runMiner: () => runMiner,
72872
+ runLearner: () => runLearner,
72886
72873
  classifyGatewayError: () => classifyGatewayError
72887
72874
  });
72888
72875
  function classifyGatewayError(text) {
@@ -72904,12 +72891,12 @@ function traceAgentMessage(message) {
72904
72891
  return;
72905
72892
  for (const block2 of content) {
72906
72893
  if (msg.type === "assistant" && block2.type === "text") {
72907
- logger.debug("miner", `[agent] ${snippet(block2.text)}`);
72894
+ logger.debug("learner", `[agent] ${snippet(block2.text)}`);
72908
72895
  } else if (msg.type === "assistant" && block2.type === "tool_use") {
72909
- logger.debug("miner", `[agent] → ${block2.name} ${snippet(block2.input)}`);
72896
+ logger.debug("learner", `[agent] → ${block2.name} ${snippet(block2.input)}`);
72910
72897
  } else if (msg.type === "user" && block2.type === "tool_result") {
72911
72898
  const full = JSON.stringify(block2.content ?? "");
72912
- logger.debug("miner", `[agent] ← result ${full.length} chars${block2.is_error ? " (error)" : ""}: ${snippet(block2.content).slice(0, 120)}`);
72899
+ logger.debug("learner", `[agent] ← result ${full.length} chars${block2.is_error ? " (error)" : ""}: ${snippet(block2.content).slice(0, 120)}`);
72913
72900
  }
72914
72901
  }
72915
72902
  }
@@ -72922,7 +72909,7 @@ function allowedToolNames() {
72922
72909
  `mcp__${KNOWLEDGE_SERVER_NAME}__finalize_session_knowledge`
72923
72910
  ]);
72924
72911
  }
72925
- async function runMiner(options) {
72912
+ async function runLearner(options) {
72926
72913
  const conflicts = detectSettingsConflicts();
72927
72914
  if (conflicts.length > 0) {
72928
72915
  const detail = conflicts.map((c5) => `${c5.file} (${c5.keys.join(", ")})`).join("; ");
@@ -72945,7 +72932,7 @@ async function runMiner(options) {
72945
72932
  const { query } = await Promise.resolve().then(() => (init_sdk(), exports_sdk));
72946
72933
  const claudeExecutable = resolveClaudeExecutable();
72947
72934
  if (claudeExecutable) {
72948
- logger.debug("miner", `using system Claude Code executable: ${claudeExecutable}`);
72935
+ logger.debug("learner", `using system Claude Code executable: ${claudeExecutable}`);
72949
72936
  }
72950
72937
  const configDir = createRunConfigDir();
72951
72938
  const runID = options.runID ?? crypto.randomUUID();
@@ -72958,7 +72945,7 @@ async function runMiner(options) {
72958
72945
  let currentSession;
72959
72946
  const readsSinceWrite = new Set;
72960
72947
  let turns = 0;
72961
- const env = buildMinerEnv({
72948
+ const env = buildLearnerEnv({
72962
72949
  apiKey: options.apiKey,
72963
72950
  gatewayURL,
72964
72951
  configDir: configDir.path,
@@ -72969,9 +72956,9 @@ async function runMiner(options) {
72969
72956
  });
72970
72957
  try {
72971
72958
  const run2 = query({
72972
- prompt: buildMinerPrompt(options.sessions),
72959
+ prompt: buildLearnerPrompt(options.sessions),
72973
72960
  options: {
72974
- systemPrompt: buildMinerSystemPrompt(MINER_CORE_RULES),
72961
+ systemPrompt: buildLearnerSystemPrompt(LEARNER_CORE_RULES),
72975
72962
  env,
72976
72963
  abortController: abort,
72977
72964
  maxTurns: options.maxTurns ?? DEFAULT_MAX_TURNS,
@@ -72987,12 +72974,12 @@ async function runMiner(options) {
72987
72974
  headers: {
72988
72975
  ...mcpHeaders(options.apiKey),
72989
72976
  "X-Dosu-Session-Id": runID,
72990
- "X-Dosu-Client": `dosu-cli-miner/${getVersionString()}`,
72977
+ "X-Dosu-Client": `dosu-cli-learner/${getVersionString()}`,
72991
72978
  ...sessionStartedAt ? { "X-Dosu-Session-Started-At": sessionStartedAt } : {}
72992
72979
  }
72993
72980
  }
72994
72981
  },
72995
- stderr: (data) => logger.debug("miner", `[sdk] ${data}`),
72982
+ stderr: (data) => logger.debug("learner", `[sdk] ${data}`),
72996
72983
  canUseTool: async (toolName, input2) => {
72997
72984
  if (!allowed.has(toolName)) {
72998
72985
  return {
@@ -73049,7 +73036,7 @@ async function runMiner(options) {
73049
73036
  };
73050
73037
  }
73051
73038
  if (message.subtype !== "success" || message.is_error) {
73052
- logger.debug("miner", `run ${runID} failed: ${text}`);
73039
+ logger.debug("learner", `run ${runID} failed: ${text}`);
73053
73040
  return {
73054
73041
  outcome: "error",
73055
73042
  notesWritten,
@@ -73057,7 +73044,7 @@ async function runMiner(options) {
73057
73044
  message: "Study run failed; see debug log for details."
73058
73045
  };
73059
73046
  }
73060
- logger.debug("miner", `run ${runID} completed: ${turns} turns, ${notesWritten} suggested pages`);
73047
+ logger.debug("learner", `run ${runID} completed: ${turns} turns, ${notesWritten} suggested pages`);
73061
73048
  return { outcome: "completed", notesWritten, turns, message: text };
73062
73049
  }
73063
73050
  }
@@ -73073,7 +73060,7 @@ async function runMiner(options) {
73073
73060
  if (gatewayError) {
73074
73061
  return { outcome: gatewayError.outcome, notesWritten, turns, message: gatewayError.message };
73075
73062
  }
73076
- logger.debug("miner", `run ${runID} threw: ${text}`);
73063
+ logger.debug("learner", `run ${runID} threw: ${text}`);
73077
73064
  return {
73078
73065
  outcome: "error",
73079
73066
  notesWritten,
@@ -73548,7 +73535,7 @@ var init_settings = __esm(() => {
73548
73535
  });
73549
73536
 
73550
73537
  // src/telemetry/debug-id.ts
73551
- var BUNDLE_DEBUG_ID_PLACEHOLDER = "6b750395-441b-6515-6475-6e2164756e21";
73538
+ var BUNDLE_DEBUG_ID_PLACEHOLDER = "a3c20325-c0f7-811f-6475-6e2164756e21";
73552
73539
 
73553
73540
  // src/telemetry/telemetry.ts
73554
73541
  import { randomUUID as randomUUID4 } from "node:crypto";
@@ -75424,6 +75411,140 @@ var init_errors3 = __esm(() => {
75424
75411
  // src/mcp/constants.ts
75425
75412
  var MCP_PROVIDER_SLUG2 = "dosu_mcp";
75426
75413
 
75414
+ // src/sessions/session-title.ts
75415
+ import { existsSync as existsSync25, mkdirSync as mkdirSync14, readFileSync as readFileSync24, renameSync as renameSync6, statSync as statSync9, writeFileSync as writeFileSync16 } from "node:fs";
75416
+ import { homedir as homedir10 } from "node:os";
75417
+ import { join as join40 } from "node:path";
75418
+ function cachePath2(configDir) {
75419
+ return join40(configDir, CACHE_FILENAME5);
75420
+ }
75421
+ function loadCacheFile2(configDir) {
75422
+ try {
75423
+ const raw = JSON.parse(readFileSync24(cachePath2(configDir), "utf-8"));
75424
+ if (raw.schema_version !== CACHE_SCHEMA_VERSION2 || typeof raw.entries !== "object")
75425
+ return {};
75426
+ return raw.entries;
75427
+ } catch {
75428
+ return {};
75429
+ }
75430
+ }
75431
+ function saveCacheFile2(configDir, entries) {
75432
+ try {
75433
+ if (!existsSync25(configDir))
75434
+ mkdirSync14(configDir, { recursive: true, mode: 448 });
75435
+ const path3 = cachePath2(configDir);
75436
+ const tmp = `${path3}.${process.pid}.tmp`;
75437
+ const file2 = { schema_version: CACHE_SCHEMA_VERSION2, entries };
75438
+ writeFileSync16(tmp, JSON.stringify(file2), { mode: 384 });
75439
+ renameSync6(tmp, path3);
75440
+ } catch {}
75441
+ }
75442
+ function extractUserQuery(text) {
75443
+ const tagged = text.match(/<user_query>([\s\S]*?)<\/user_query>/);
75444
+ if (tagged)
75445
+ return tagged[1];
75446
+ const named = text.match(/^\s*<[\w-]+\s[^>]*\bname="([^"]+)"/);
75447
+ if (named)
75448
+ return named[1];
75449
+ return text.replace(/^(\s*<([\w-]+)(?:\s[^>]*)?>[\s\S]*?<\/\2>\s*)+/, "");
75450
+ }
75451
+ function cleanSessionName(text) {
75452
+ const line = text.replace(/\s+/g, " ").trim();
75453
+ if (!line)
75454
+ return null;
75455
+ return line.length > SESSION_NAME_MAX ? `${line.slice(0, SESSION_NAME_MAX - 1)}…` : line;
75456
+ }
75457
+ function claudeSummaryFromHead(text) {
75458
+ for (const line of text.split(`
75459
+ `).slice(0, HEAD_LINES2)) {
75460
+ if (!line.includes('"summary"'))
75461
+ continue;
75462
+ try {
75463
+ const obj = JSON.parse(line);
75464
+ if (obj.type === "summary" && typeof obj.summary === "string") {
75465
+ return cleanSessionName(obj.summary);
75466
+ }
75467
+ } catch {}
75468
+ }
75469
+ return null;
75470
+ }
75471
+ function reconstructSession(harness, id2, slug, home = homedir10(), exists = existsSync25) {
75472
+ if (!slug)
75473
+ return null;
75474
+ const candidates = [];
75475
+ if (harness === "claude") {
75476
+ candidates.push(join40(home, ".claude", "projects", slug, `${id2}.jsonl`));
75477
+ } else if (harness === "cursor") {
75478
+ const project = join40(home, ".cursor", "projects", slug);
75479
+ candidates.push(join40(project, "agent-transcripts", id2, `${id2}.jsonl`), join40(project, "agent-transcripts", `${id2}.jsonl`));
75480
+ } else {
75481
+ return null;
75482
+ }
75483
+ for (const path3 of candidates) {
75484
+ if (exists(path3))
75485
+ return { id: id2, harness, path: path3, project: slug, updated: "" };
75486
+ }
75487
+ return null;
75488
+ }
75489
+ function readHead2(path3) {
75490
+ try {
75491
+ return readFileSync24(path3, "utf-8").slice(0, 128 * 1024);
75492
+ } catch {
75493
+ return null;
75494
+ }
75495
+ }
75496
+ function fileMtime2(path3) {
75497
+ try {
75498
+ return statSync9(path3).mtime.toISOString();
75499
+ } catch {
75500
+ return "";
75501
+ }
75502
+ }
75503
+ function createSessionTitleResolver(configDir = getConfigDir(), deps = {}) {
75504
+ const head = deps.readHead ?? readHead2;
75505
+ const turns = deps.readTurns ?? readSessionTurns;
75506
+ const mtime = deps.mtime ?? fileMtime2;
75507
+ const entries = loadCacheFile2(configDir);
75508
+ let dirty = false;
75509
+ const compute = (session) => {
75510
+ if (session.harness === "claude") {
75511
+ const text = head(session.path);
75512
+ const summary = text ? claudeSummaryFromHead(text) : null;
75513
+ if (summary)
75514
+ return summary;
75515
+ }
75516
+ const firstUser = turns(session).find((t9) => t9.role === "user");
75517
+ return firstUser ? cleanSessionName(extractUserQuery(firstUser.text)) : null;
75518
+ };
75519
+ return {
75520
+ cached(key) {
75521
+ return entries[key]?.title ?? null;
75522
+ },
75523
+ resolve(session) {
75524
+ const key = `${session.harness}/${session.id}`;
75525
+ const cached2 = entries[key];
75526
+ if (cached2 && (cached2.title !== null || cached2.mtime === mtime(session.path))) {
75527
+ return cached2.title;
75528
+ }
75529
+ const title = compute(session);
75530
+ entries[key] = { title, mtime: mtime(session.path) };
75531
+ dirty = true;
75532
+ return title;
75533
+ },
75534
+ flush() {
75535
+ if (!dirty)
75536
+ return;
75537
+ saveCacheFile2(configDir, entries);
75538
+ dirty = false;
75539
+ }
75540
+ };
75541
+ }
75542
+ var CACHE_FILENAME5 = "session-titles.json", CACHE_SCHEMA_VERSION2 = 3, HEAD_LINES2 = 50, SESSION_NAME_MAX = 80;
75543
+ var init_session_title = __esm(() => {
75544
+ init_config();
75545
+ init_read();
75546
+ });
75547
+
75427
75548
  // src/tui/alt-screen.ts
75428
75549
  function enterAltScreen(output2) {
75429
75550
  depth += 1;
@@ -75447,7 +75568,8 @@ var init_alt_screen = __esm(() => {
75447
75568
  });
75448
75569
 
75449
75570
  // src/tui/activity-view.ts
75450
- import { readFileSync as readFileSync24 } from "node:fs";
75571
+ import { existsSync as existsSync26, readFileSync as readFileSync25 } from "node:fs";
75572
+ import { basename as basename5 } from "node:path";
75451
75573
  function reduceActivityViewKey(key) {
75452
75574
  if (key === "q" || key === ESC7 || key === CTRL_C4)
75453
75575
  return "back";
@@ -75495,13 +75617,13 @@ function formatActivityLine(line, width, full = false) {
75495
75617
  }
75496
75618
  function appendSyncActivity(buffer, chunk, max = ACTIVITY_VIEW_BUFFER_LINES) {
75497
75619
  const fresh = chunk.split(`
75498
- `).filter((line) => line.includes("[sync]") || line.includes("[miner]")).map((line) => line.replace(/ \[(DEBUG|INFO|WARN|ERROR)\]/, ""));
75620
+ `).filter((line) => line.includes("[sync]") || line.includes("[learner]")).map((line) => line.replace(/ \[(DEBUG|INFO|WARN|ERROR)\]/, ""));
75499
75621
  return [...buffer, ...fresh].slice(-max);
75500
75622
  }
75501
75623
  function clip(text, max) {
75502
75624
  return text.length <= max ? text : `${text.slice(0, max - 1)}…`;
75503
75625
  }
75504
- function formatMinedRow(record2, full = false) {
75626
+ function formatStudiedRow(record2, full = false, name) {
75505
75627
  const match = record2.at.match(/^\d{4}-(\d{2}-\d{2})T(\d{2}:\d{2})/);
75506
75628
  const stamp = match ? `${match[1]} ${match[2]}` : record2.at;
75507
75629
  const slash = record2.session.indexOf("/");
@@ -75509,13 +75631,15 @@ function formatMinedRow(record2, full = false) {
75509
75631
  const id2 = slash > 0 ? record2.session.slice(slash + 1) : record2.session;
75510
75632
  const rawProject = record2.project ?? "-";
75511
75633
  const project = full ? rawProject : clip(rawProject, 28);
75512
- return `${harness.padEnd(8)} ${stamp} ${project} ${full ? id2 : clip(id2, 24)}`;
75634
+ const label = full ? name ? `${name} · ${id2}` : id2 : clip(name ?? id2, 44);
75635
+ return `${harness.padEnd(8)} ${stamp} ${project} ${label}`;
75513
75636
  }
75514
- function formatQueuedRow(session, full = false) {
75637
+ function formatQueuedRow(session, full = false, name) {
75515
75638
  const stamp = session.updated.replace("T", " ").slice(5, 16);
75516
75639
  const rawProject = session.project ?? "-";
75517
75640
  const project = full ? rawProject : clip(rawProject, 28);
75518
- return `${session.harness.padEnd(8)} ${stamp} ${project} ${full ? session.id : clip(session.id, 24)}`;
75641
+ const label = full ? name ? `${name} · ${session.id}` : session.id : clip(name ?? session.id, 44);
75642
+ return `${session.harness.padEnd(8)} ${stamp} ${project} ${label}`;
75519
75643
  }
75520
75644
  function wrapRow(text, width, indent = " ") {
75521
75645
  if (text.length <= width)
@@ -75566,12 +75690,12 @@ function foldRunProgress(progress, chunk) {
75566
75690
  }
75567
75691
  if (!current)
75568
75692
  continue;
75569
- const read = line.match(/\[miner\] \[agent\] → mcp__sessions__read_session .*?"id":"([^"]+)"/);
75693
+ const read = line.match(/\[learner\] \[agent\] → mcp__sessions__read_session .*?"id":"([^"]+)"/);
75570
75694
  if (read) {
75571
75695
  current.read.add(read[1]);
75572
75696
  continue;
75573
75697
  }
75574
- if (line.includes("[miner] [agent] → mcp__dosu__write_knowledge")) {
75698
+ if (line.includes("[learner] [agent] → mcp__dosu__write_knowledge")) {
75575
75699
  current.notes += 1;
75576
75700
  }
75577
75701
  }
@@ -75607,10 +75731,10 @@ function progressLine(done, ready, width, notes = 0) {
75607
75731
  const suffix = notes > 0 ? ` · ${notes} suggested page${notes === 1 ? "" : "s"}` : "";
75608
75732
  return `${bar} ${done}/${total} studied · ${pct}%${suffix}`;
75609
75733
  }
75610
- function tabBar(tab, queuedCount, openCount, minedCount, width) {
75734
+ function tabBar(tab, queuedCount, openCount, studiedCount, width) {
75611
75735
  return tabStrip([
75612
75736
  ["activity", "Activity"],
75613
- ["mined", `Studied (${minedCount})`],
75737
+ ["studied", `Studied (${studiedCount})`],
75614
75738
  ["queued", `Queued (${queuedCount})`],
75615
75739
  ["open", `Open (${openCount})`]
75616
75740
  ], tab, width);
@@ -75654,28 +75778,41 @@ function confirmBox(queuedCount, backlog, width, mode = "start") {
75654
75778
  import_picocolors30.default.dim(`╰${"─".repeat(inner + 2)}╯`)
75655
75779
  ];
75656
75780
  }
75657
- function renderActivityFrame(status, activity, width, backlog = null, pane = DEFAULT_PANE, queued = [], minedBeforeRun = 0, open5 = [], runProgress = null) {
75658
- const mined = status.state.watermark ? `Studied sessions up to ${localTime(status.state.watermark)}` : "Nothing studied yet";
75781
+ function renderActivityFrame(status, activity, width, backlog = null, pane = DEFAULT_PANE, queued = [], studiedBeforeRun = 0, open5 = [], runProgress = null, projectNames = {}, sessionNames = {}) {
75782
+ const studied = status.state.watermark ? `Studied sessions up to ${localTime(status.state.watermark)}` : "Nothing studied yet";
75659
75783
  const queueDetail = [];
75660
75784
  if (status.backoffUntil) {
75661
75785
  queueDetail.push(`retrying after ${localTime(status.backoffUntil)} · s syncs now`);
75662
75786
  }
75663
75787
  let progress = null;
75664
75788
  if (status.running && backlog) {
75665
- const minedDelta = Math.max(0, (status.state.total_mined ?? 0) - minedBeforeRun);
75789
+ const studiedDelta = Math.max(0, (status.state.total_mined ?? 0) - studiedBeforeRun);
75666
75790
  const stepDone = runProgress ? Math.min(Math.max(0, runProgress.read.size - 1), runProgress.batch - 1, backlog.ready) : 0;
75667
- progress = progressLine(minedDelta + stepDone, Math.max(0, backlog.ready - stepDone), width, runProgress?.notes ?? 0);
75791
+ progress = progressLine(studiedDelta + stepDone, Math.max(0, backlog.ready - stepDone), width, runProgress?.notes ?? 0);
75668
75792
  }
75669
75793
  const refusal = !status.running && status.state.last_refusal;
75670
75794
  const refusalLines = refusal ? wrapLine(`! Studying paused: ${refusal.message} (${localTime(refusal.at)})`, width).map((line) => import_picocolors30.default.yellow(line)) : [];
75671
75795
  const fullRows = Boolean(pane.fullRows);
75672
- const minedRows = (status.state.mined_sessions ?? []).map((record2) => fullRows ? formatMinedRow(record2, true) : formatActivityLine(formatMinedRow(record2), width));
75673
- const sessionRow = (session) => fullRows ? formatQueuedRow(session, true) : formatActivityLine(formatQueuedRow(session), width);
75796
+ const withName = (item, key) => projectNames[key] ? { ...item, project: projectNames[key] } : item;
75797
+ const latestPass = new Map;
75798
+ for (const r9 of status.state.mined_sessions ?? [])
75799
+ latestPass.set(r9.session, r9);
75800
+ const studiedRows = [...latestPass.values()].map((r9) => {
75801
+ const record2 = withName(r9, r9.session);
75802
+ const name = sessionNames[r9.session];
75803
+ return fullRows ? formatStudiedRow(record2, true, name) : formatActivityLine(formatStudiedRow(record2, false, name), width);
75804
+ });
75805
+ const sessionRow = (session) => {
75806
+ const key = `${session.harness}/${session.id}`;
75807
+ const s = withName(session, key);
75808
+ const name = sessionNames[key];
75809
+ return fullRows ? formatQueuedRow(s, true, name) : formatActivityLine(formatQueuedRow(s, false, name), width);
75810
+ };
75674
75811
  const queuedRows = queued.map(sessionRow);
75675
75812
  const openRows = open5.map(sessionRow);
75676
- const source = pane.tab === "activity" ? activity.map((line) => formatActivityLine(line, width, fullRows)) : pane.tab === "queued" ? queuedRows : pane.tab === "open" ? openRows : minedRows;
75677
- const emptyMined = status.state.watermark ? "No sessions recorded yet. History starts with the next study run." : "No studied sessions yet.";
75678
- const empty = pane.tab === "activity" ? "No sync activity in the log yet." : pane.tab === "queued" ? "Queue empty. Finished agent sessions appear here." : pane.tab === "open" ? "No open sessions. Live agent sessions sit here until they go quiet." : emptyMined;
75813
+ const source = pane.tab === "activity" ? activity.map((line) => formatActivityLine(line, width, fullRows)) : pane.tab === "queued" ? queuedRows : pane.tab === "open" ? openRows : studiedRows;
75814
+ const emptyStudied = status.state.watermark ? "No sessions recorded yet. History starts with the next study run." : "No studied sessions yet.";
75815
+ const empty = pane.tab === "activity" ? "No sync activity in the log yet." : pane.tab === "queued" ? "Queue empty. Finished agent sessions appear here." : pane.tab === "open" ? "No open sessions. Live agent sessions sit here until they go quiet." : emptyStudied;
75679
75816
  const height = fullRows ? ACTIVITY_VIEW_FULL_LIST_ROWS : ACTIVITY_VIEW_LIST_LINES;
75680
75817
  const { visible, above, below } = windowList(source, pane.scroll, height);
75681
75818
  const display = fullRows ? visible.flatMap((row) => wrapRow(row, width)) : visible;
@@ -75689,12 +75826,12 @@ function renderActivityFrame(status, activity, width, backlog = null, pane = DEF
75689
75826
  breadcrumb(["Home", "Activity"], width),
75690
75827
  "",
75691
75828
  statusLine(status),
75692
- import_picocolors30.default.dim(mined),
75829
+ import_picocolors30.default.dim(studied),
75693
75830
  ...queueDetail.length > 0 ? wrapLine(queueDetail.join(" · "), width).map((line) => import_picocolors30.default.dim(line)) : [],
75694
75831
  ...progress ? [progress] : [],
75695
75832
  ...refusalLines,
75696
75833
  "",
75697
- ...tabBar(pane.tab, queued.length, open5.length, status.state.total_mined ?? minedRows.length, width),
75834
+ ...tabBar(pane.tab, queued.length, open5.length, latestPass.size, width),
75698
75835
  ...listRows,
75699
75836
  ...scrollParts.length > 0 ? [import_picocolors30.default.dim(clip(scrollParts.join(" · "), width))] : [],
75700
75837
  "",
@@ -75712,9 +75849,72 @@ function renderActivityFrame(status, activity, width, backlog = null, pane = DEF
75712
75849
  return lines.join(`
75713
75850
  `);
75714
75851
  }
75852
+ function createRowNamer() {
75853
+ const dirs = createProjectDirResolver();
75854
+ const titles = createSessionTitleResolver();
75855
+ const bySlug = new Map;
75856
+ const fromSlug = (slug) => {
75857
+ if (!bySlug.has(slug))
75858
+ bySlug.set(slug, unmungeSlug(slug));
75859
+ return bySlug.get(slug) ?? null;
75860
+ };
75861
+ return (status, backlog) => {
75862
+ const names = { projects: {}, titles: {} };
75863
+ let budget = TITLE_READS_PER_DRAW;
75864
+ let resolved = false;
75865
+ const title = (key, session) => {
75866
+ const hit = titles.cached(key);
75867
+ if (hit) {
75868
+ names.titles[key] = hit;
75869
+ return;
75870
+ }
75871
+ if (budget <= 0)
75872
+ return;
75873
+ budget--;
75874
+ const t9 = titles.resolve(session);
75875
+ resolved = true;
75876
+ if (t9)
75877
+ names.titles[key] = t9;
75878
+ };
75879
+ for (const s of [...backlog.queued, ...backlog.open]) {
75880
+ if (!existsSync26(s.path))
75881
+ continue;
75882
+ const key = `${s.harness}/${s.id}`;
75883
+ const dir = dirs.resolve(s);
75884
+ if (dir)
75885
+ names.projects[key] = basename5(dir);
75886
+ title(key, s);
75887
+ resolved = true;
75888
+ }
75889
+ for (const r9 of [...status.state.mined_sessions ?? []].reverse()) {
75890
+ const key = r9.session;
75891
+ if (!names.projects[key]) {
75892
+ const dir = dirs.cached(key) ?? (r9.project ? fromSlug(r9.project) : null);
75893
+ if (dir)
75894
+ names.projects[key] = basename5(dir);
75895
+ }
75896
+ if (!names.titles[key]) {
75897
+ const hit = titles.cached(key);
75898
+ if (hit) {
75899
+ names.titles[key] = hit;
75900
+ } else if (budget > 0) {
75901
+ const slash = key.indexOf("/");
75902
+ const session = reconstructSession(key.slice(0, slash), key.slice(slash + 1), r9.project);
75903
+ if (session)
75904
+ title(key, session);
75905
+ }
75906
+ }
75907
+ }
75908
+ if (resolved) {
75909
+ dirs.flush();
75910
+ titles.flush();
75911
+ }
75912
+ return names;
75913
+ };
75914
+ }
75715
75915
  function defaultReadLog2() {
75716
75916
  try {
75717
- return readFileSync24(logger.getLogPath(), "utf-8");
75917
+ return readFileSync25(logger.getLogPath(), "utf-8");
75718
75918
  } catch {
75719
75919
  return "";
75720
75920
  }
@@ -75732,6 +75932,7 @@ function runActivityView(io2 = {}) {
75732
75932
  const setPaused = io2.setPaused ?? setSyncPaused;
75733
75933
  const clearHistory = io2.clearHistory ?? resetSyncState;
75734
75934
  const listBacklog = io2.listBacklog ?? listSessionBacklog;
75935
+ const rowNamesFor = io2.rowNames ?? createRowNamer();
75735
75936
  const pollMs = io2.pollMs ?? ACTIVITY_VIEW_POLL_MS;
75736
75937
  const seed = readLog();
75737
75938
  let activity = appendSyncActivity([], seed);
@@ -75760,18 +75961,18 @@ function runActivityView(io2 = {}) {
75760
75961
  };
75761
75962
  const listHeight = () => fullRows ? ACTIVITY_VIEW_FULL_LIST_ROWS : ACTIVITY_VIEW_LIST_LINES;
75762
75963
  let lastFrame = null;
75763
- let minedBeforeRun = null;
75964
+ let studiedBeforeRun = null;
75764
75965
  const draw = () => {
75765
75966
  status = getStatus();
75766
75967
  if (status.running) {
75767
75968
  const run2 = status.state.run;
75768
75969
  if (run2 && run2.pid === status.pid) {
75769
- minedBeforeRun = run2.baseline_mined;
75970
+ studiedBeforeRun = run2.baseline_mined;
75770
75971
  } else {
75771
- minedBeforeRun ??= status.state.total_mined ?? 0;
75972
+ studiedBeforeRun ??= status.state.total_mined ?? 0;
75772
75973
  }
75773
75974
  } else {
75774
- minedBeforeRun = null;
75975
+ studiedBeforeRun = null;
75775
75976
  }
75776
75977
  if (status.state.watermark !== queuedWatermark) {
75777
75978
  queuedWatermark = status.state.watermark;
@@ -75782,7 +75983,8 @@ function runActivityView(io2 = {}) {
75782
75983
  if (confirmSync === "stop" && !status.running)
75783
75984
  confirmSync = null;
75784
75985
  const width = activityWidth(output2.columns ?? 80);
75785
- const frame = renderActivityFrame(status, activity, width, backlog, { tab, scroll, confirm: confirmSync ?? undefined, fullRows }, sessions.queued, minedBeforeRun ?? 0, sessions.open, runProgress);
75986
+ const rowNames = rowNamesFor(status, sessions);
75987
+ const frame = renderActivityFrame(status, activity, width, backlog, { tab, scroll, confirm: confirmSync ?? undefined, fullRows }, sessions.queued, studiedBeforeRun ?? 0, sessions.open, runProgress, rowNames.projects, rowNames.titles);
75786
75988
  if (frame === lastFrame)
75787
75989
  return;
75788
75990
  lastFrame = frame;
@@ -75831,7 +76033,7 @@ ${CLEAR_BELOW}`);
75831
76033
  let note;
75832
76034
  if (mode === "clear") {
75833
76035
  clearHistory();
75834
- minedBeforeRun = null;
76036
+ studiedBeforeRun = null;
75835
76037
  note = "[sync] study history cleared · the next run reads every local session again";
75836
76038
  } else if (mode === "stop") {
75837
76039
  const ok2 = status.pid !== undefined && stopSync(status.pid);
@@ -75893,10 +76095,12 @@ ${CLEAR_BELOW}`);
75893
76095
  input2.on("data", onData);
75894
76096
  });
75895
76097
  }
75896
- var import_picocolors30, ESC7, HIDE_CURSOR3, SHOW_CURSOR3, CTRL_C4, KEY_UP4, KEY_DOWN4, KEY_RIGHT3, KEY_LEFT3, CURSOR_HOME, CLEAR_BELOW, CLEAR_EOL, ACTIVITY_VIEW_POLL_MS = 500, ACTIVITY_VIEW_LIST_LINES = 10, ACTIVITY_VIEW_FULL_LIST_ROWS = 5, ACTIVITY_VIEW_BUFFER_LINES = 200, ACTIVITY_VIEW_TABS, DEFAULT_PANE;
76098
+ var import_picocolors30, ESC7, HIDE_CURSOR3, SHOW_CURSOR3, CTRL_C4, KEY_UP4, KEY_DOWN4, KEY_RIGHT3, KEY_LEFT3, CURSOR_HOME, CLEAR_BELOW, CLEAR_EOL, ACTIVITY_VIEW_POLL_MS = 500, ACTIVITY_VIEW_LIST_LINES = 10, ACTIVITY_VIEW_FULL_LIST_ROWS = 5, ACTIVITY_VIEW_BUFFER_LINES = 200, ACTIVITY_VIEW_TABS, DEFAULT_PANE, TITLE_READS_PER_DRAW = 25;
75897
76099
  var init_activity_view = __esm(() => {
75898
76100
  init_follow();
75899
76101
  init_logger();
76102
+ init_project_dir();
76103
+ init_session_title();
75900
76104
  init_styles();
75901
76105
  init_backlog();
75902
76106
  init_detach();
@@ -75918,7 +76122,7 @@ var init_activity_view = __esm(() => {
75918
76122
  CURSOR_HOME = `${ESC7}[H`;
75919
76123
  CLEAR_BELOW = `${ESC7}[0J`;
75920
76124
  CLEAR_EOL = `${ESC7}[K`;
75921
- ACTIVITY_VIEW_TABS = ["activity", "mined", "queued", "open"];
76125
+ ACTIVITY_VIEW_TABS = ["activity", "studied", "queued", "open"];
75922
76126
  DEFAULT_PANE = { tab: "activity", scroll: 0 };
75923
76127
  });
75924
76128
 
@@ -77600,12 +77804,15 @@ function loadPageStatsFromConfig() {
77600
77804
  }
77601
77805
  }
77602
77806
  function overviewRows(state) {
77603
- const minedTotal = state.total_mined ?? 0;
77807
+ const studiedTotal = state.total_mined ?? 0;
77604
77808
  const notes = state.total_notes ?? 0;
77605
77809
  const tokens = state.total_learning_tokens ?? 0;
77606
- if (minedTotal === 0 && notes === 0)
77810
+ if (studiedTotal === 0 && notes === 0)
77607
77811
  return [];
77608
- const rows = [`${label("Sessions studied")}${minedTotal}`, `${label("Suggested pages")}${notes}`];
77812
+ const rows = [
77813
+ `${label("Sessions studied")}${studiedTotal}`,
77814
+ `${label("Suggested pages")}${notes}`
77815
+ ];
77609
77816
  if (tokens > 0) {
77610
77817
  rows.push(`${label("Investigation distilled")}${formatTokenCount(tokens)} tokens`);
77611
77818
  }
@@ -77830,7 +78037,7 @@ function checklistRows(ctx) {
77830
78037
  rows.push(["agents", `${on2} ${ctx.agents.join(` ${DOT3} `)}`]);
77831
78038
  if (missing.has("hooks"))
77832
78039
  rows.push(["hooks", warnRow]);
77833
- if (ctx.mining) {
78040
+ if (ctx.studying) {
77834
78041
  rows.push([
77835
78042
  "sync",
77836
78043
  `\uD83D\uDCDA ${brand("studying sessions...")} ${import_picocolors33.default.dim(`${DOT3} see Activity`)}`
@@ -78223,8 +78430,8 @@ __export(exports_tui, {
78223
78430
  runTUI: () => runTUI,
78224
78431
  handleLogout: () => handleLogout
78225
78432
  });
78226
- import { homedir as homedir10 } from "node:os";
78227
- import { basename as basename4 } from "node:path";
78433
+ import { homedir as homedir11 } from "node:os";
78434
+ import { basename as basename6 } from "node:path";
78228
78435
  async function probeSession(cfg) {
78229
78436
  if (!isAuthenticated(cfg) || !isTokenExpired(cfg))
78230
78437
  return { expired: false };
@@ -78251,7 +78458,7 @@ function bannerContext(cfg, session) {
78251
78458
  return {
78252
78459
  version: getVersionString(),
78253
78460
  webAppHost,
78254
- directory: basename4(process.cwd()),
78461
+ directory: basename6(process.cwd()),
78255
78462
  signedIn: isAuthenticated(cfg),
78256
78463
  sessionExpired: session.expired,
78257
78464
  deploymentName: cfg.active_account?.target?.deployment_name,
@@ -78259,7 +78466,7 @@ function bannerContext(cfg, session) {
78259
78466
  setupMissing: isAuthenticated(cfg) && !session.expired ? missingSetupSteps(cfg) : [],
78260
78467
  ...inGitWorkTree() ? { repoAgentsMd: dosuAgentsSectionState() } : {},
78261
78468
  agents,
78262
- mining: isMining(),
78469
+ studying: isStudying(),
78263
78470
  ...latest ? { update: { version: latest, hint: buildUpdateHint(INSTALL_CHANNEL, isNpxInvocation()) } } : {}
78264
78471
  };
78265
78472
  }
@@ -78326,7 +78533,7 @@ function setupHint(cfg) {
78326
78533
  const started = Boolean(cfg.active_account?.target);
78327
78534
  return import_picocolors35.default.yellow(started ? `incomplete · missing ${missingSetupSteps(cfg).join(" + ")}` : "not set up yet");
78328
78535
  }
78329
- function isMining() {
78536
+ function isStudying() {
78330
78537
  try {
78331
78538
  return getSyncStatus({ readLog: () => "" }).running;
78332
78539
  } catch {
@@ -78374,10 +78581,10 @@ async function runMainMenu() {
78374
78581
  { label: "Exit", value: "exit" }
78375
78582
  ];
78376
78583
  }
78377
- const mining = isMining();
78584
+ const studying = isStudying();
78378
78585
  return [
78379
78586
  {
78380
- label: mining ? `Activity \uD83D\uDCDA ${brand("studying sessions...")}` : "Activity",
78587
+ label: studying ? `Activity \uD83D\uDCDA ${brand("studying sessions...")}` : "Activity",
78381
78588
  value: "sync"
78382
78589
  },
78383
78590
  { label: "Knowledge report", hint: "(opens in browser)", value: "report" },
@@ -78453,7 +78660,7 @@ async function runSettings(cfg) {
78453
78660
  const target = cfg.active_account?.target;
78454
78661
  const library = target?.library_name ?? target?.deployment_name ?? "not configured";
78455
78662
  const filter = loadSyncState().project_filter;
78456
- const scope = !filter?.length ? "all projects" : filter.length <= 2 ? filter.map((dir) => dir === UNKNOWN_PROJECT ? "unknown" : basename4(dir)).join(", ") : `${filter.length} projects`;
78663
+ const scope = !filter?.length ? "all projects" : filter.length <= 2 ? filter.map((dir) => dir === UNKNOWN_PROJECT ? "unknown" : basename6(dir)).join(", ") : `${filter.length} projects`;
78457
78664
  const action = await menuSelect("Settings", [
78458
78665
  { label: "Switch organization", hint: target?.org_name, value: "switch-org" },
78459
78666
  { label: "Switch Library", hint: library, value: "switch-library" },
@@ -78465,7 +78672,7 @@ async function runSettings(cfg) {
78465
78672
  if (action === null || action === "back")
78466
78673
  return;
78467
78674
  if (action === "projects") {
78468
- await runMiningProjectsSetting();
78675
+ await runStudyingProjectsSetting();
78469
78676
  continue;
78470
78677
  }
78471
78678
  if (action === "setup") {
@@ -78488,7 +78695,7 @@ async function runSettings(cfg) {
78488
78695
  }
78489
78696
  }
78490
78697
  function displayDir(dir) {
78491
- const home = homedir10();
78698
+ const home = homedir11();
78492
78699
  return dir === home ? "~" : dir.startsWith(`${home}/`) ? `~${dir.slice(home.length)}` : dir;
78493
78700
  }
78494
78701
  function discoverProjectDirs() {
@@ -78503,7 +78710,7 @@ function discoverProjectDirs() {
78503
78710
  } catch {}
78504
78711
  return [...counts.entries()].sort((a9, b2) => b2[1] - a9[1]).map(([dir]) => dir);
78505
78712
  }
78506
- async function runMiningProjectsSetting() {
78713
+ async function runStudyingProjectsSetting() {
78507
78714
  const dirs = discoverProjectDirs();
78508
78715
  if (dirs.length === 0) {
78509
78716
  log2.info("No local agent sessions found yet; nothing to scope.");
@@ -79230,7 +79437,7 @@ import {
79230
79437
  constants as constants3,
79231
79438
  fstatSync,
79232
79439
  openSync as openSync5,
79233
- readFileSync as readFileSync25,
79440
+ readFileSync as readFileSync26,
79234
79441
  readSync as readSync4,
79235
79442
  unlinkSync as unlinkSync5
79236
79443
  } from "node:fs";
@@ -82432,23 +82639,23 @@ ${results.length - opts.limit} more results not shown.`));
82432
82639
  console.log(` ID: ${store.id}`);
82433
82640
  console.log(` Space ID: ${store.space_id}`);
82434
82641
  });
82435
- cmd.command("sessions").description("List local agent sessions with full project and session ids (the untruncated view of the Activity screen's tabs)").option("--queued", "Only sessions queued for studying").option("--open", "Only live sessions still inside the quiet period").option("--mined", "Only recent studied-session history").option("--json", "Output as JSON").action((opts) => {
82436
- const all = !opts.queued && !opts.open && !opts.mined;
82642
+ cmd.command("sessions").description("List local agent sessions with full project and session ids (the untruncated view of the Activity screen's tabs)").option("--queued", "Only sessions queued for studying").option("--open", "Only live sessions still inside the quiet period").option("--studied", "Only recent studied-session history").option("--json", "Output as JSON").action((opts) => {
82643
+ const all = !opts.queued && !opts.open && !opts.studied;
82437
82644
  const wantQueued = all || Boolean(opts.queued);
82438
82645
  const wantOpen = all || Boolean(opts.open);
82439
- const wantMined = all || Boolean(opts.mined);
82646
+ const wantStudied = all || Boolean(opts.studied);
82440
82647
  const backlog = wantQueued || wantOpen ? listSessionBacklog() : { queued: [], open: [] };
82441
- const mined = wantMined ? loadSyncState().mined_sessions ?? [] : [];
82648
+ const studied = wantStudied ? loadSyncState().mined_sessions ?? [] : [];
82442
82649
  if (opts.json) {
82443
82650
  printResult({
82444
82651
  ...wantQueued ? { queued: backlog.queued } : {},
82445
82652
  ...wantOpen ? { open: backlog.open } : {},
82446
- ...wantMined ? { mined } : {}
82653
+ ...wantStudied ? { studied } : {}
82447
82654
  }, opts);
82448
82655
  return;
82449
82656
  }
82450
82657
  const sessionRows = (sessions) => sessions.map((s) => [s.harness, s.updated, s.project ?? "-", s.id]);
82451
- const minedRows = mined.map((record2) => {
82658
+ const studiedRows = studied.map((record2) => {
82452
82659
  const slash = record2.session.indexOf("/");
82453
82660
  const harness = slash > 0 ? record2.session.slice(0, slash) : "-";
82454
82661
  const id2 = slash > 0 ? record2.session.slice(slash + 1) : record2.session;
@@ -82472,8 +82679,8 @@ ${results.length - opts.limit} more results not shown.`));
82472
82679
  if (wantOpen) {
82473
82680
  section("Open", sessionRows(backlog.open), "Updated", "No open sessions. Live agent sessions sit here until they go quiet.");
82474
82681
  }
82475
- if (wantMined) {
82476
- section("Studied", minedRows, "Studied at", "No studied sessions recorded yet.");
82682
+ if (wantStudied) {
82683
+ section("Studied", studiedRows, "Studied at", "No studied sessions recorded yet.");
82477
82684
  }
82478
82685
  });
82479
82686
  cmd.command("sync").description("Scan local agent session history and report the study backlog").option("--quiet", "Background mode for hooks: honor backoff, exit 0, print nothing").option("--detach", "Re-spawn detached and return immediately (used by agent hooks)").option("--bootstrap", "Backfill mode: study the full local session history regardless of age and drain the backlog (used by setup)").option("--status", "Show whether a sync is running now, plus watermark and recent activity").option("--report", "Write the same HTML harvest report as the log-to-dosu-knowledge skill and open it").option("--out <path>", "HTML report path (default: tmp/dosu-knowledge-report.html)").option("--json", "Output as JSON").action(async (opts) => {
@@ -82497,7 +82704,7 @@ ${results.length - opts.limit} more results not shown.`));
82497
82704
  ]);
82498
82705
  return;
82499
82706
  }
82500
- const deps = { mine: buildMiner(opts.quiet ? "hook" : "manual") };
82707
+ const deps = { mine: buildLearner(opts.quiet ? "hook" : "manual") };
82501
82708
  let outcome = await runKnowledgeSync({
82502
82709
  quiet: opts.quiet,
82503
82710
  bootstrap: opts.bootstrap,
@@ -82505,7 +82712,7 @@ ${results.length - opts.limit} more results not shown.`));
82505
82712
  });
82506
82713
  if (opts.bootstrap && deps.mine) {
82507
82714
  const maxRounds = Math.ceil(outcome.readySessions / MINE_BATCH_LIMIT) + 2;
82508
- for (let round = 1;outcome.status === "mined" && round < maxRounds; round++) {
82715
+ for (let round = 1;outcome.status === "studied" && round < maxRounds; round++) {
82509
82716
  if (!opts.quiet && !opts.json)
82510
82717
  printSyncOutcome(outcome);
82511
82718
  outcome = await runKnowledgeSync({ quiet: opts.quiet, bootstrap: true, deps });
@@ -82567,7 +82774,7 @@ ${results.length - opts.limit} more results not shown.`));
82567
82774
  cmd.addCommand(hooksCommand());
82568
82775
  return cmd;
82569
82776
  }
82570
- function buildMiner(trigger) {
82777
+ function buildLearner(trigger) {
82571
82778
  const cfg = loadConfig();
82572
82779
  if (cfg.mode === "oss")
82573
82780
  return;
@@ -82576,8 +82783,8 @@ function buildMiner(trigger) {
82576
82783
  return;
82577
82784
  const { api_key, deployment_id } = target;
82578
82785
  return async (sessions) => {
82579
- const { runMiner: runMiner2 } = await Promise.resolve().then(() => (init_runner(), exports_runner));
82580
- return runMiner2({ sessions, apiKey: api_key, deploymentID: deployment_id, trigger });
82786
+ const { runLearner: runLearner2 } = await Promise.resolve().then(() => (init_runner(), exports_runner));
82787
+ return runLearner2({ sessions, apiKey: api_key, deploymentID: deployment_id, trigger });
82581
82788
  };
82582
82789
  }
82583
82790
  function formatAge(iso, now) {
@@ -82645,21 +82852,21 @@ function printSyncOutcome(outcome) {
82645
82852
  console.log(import_picocolors18.default.dim("Sign in with 'dosu setup' to enable studying."));
82646
82853
  break;
82647
82854
  }
82648
- case "mined": {
82649
- const notes = outcome.miner?.notesWritten ?? 0;
82650
- const remaining = outcome.readySessions - (outcome.minedSessions ?? 0);
82651
- console.log(`✓ Studied ${outcome.minedSessions} session${outcome.minedSessions === 1 ? "" : "s"}, ${notes} suggested page${notes === 1 ? "" : "s"} created.`);
82855
+ case "studied": {
82856
+ const notes = outcome.learner?.notesWritten ?? 0;
82857
+ const remaining = outcome.readySessions - (outcome.studiedSessions ?? 0);
82858
+ console.log(`✓ Studied ${outcome.studiedSessions} session${outcome.studiedSessions === 1 ? "" : "s"}, ${notes} suggested page${notes === 1 ? "" : "s"} created.`);
82652
82859
  if (remaining > 0) {
82653
82860
  console.log(import_picocolors18.default.dim(`${remaining} more in the backlog; run sync again to continue.`));
82654
82861
  }
82655
82862
  break;
82656
82863
  }
82657
82864
  case "skipped-gateway": {
82658
- console.log(import_picocolors18.default.yellow(outcome.miner?.message ?? "Studying unavailable right now."));
82865
+ console.log(import_picocolors18.default.yellow(outcome.learner?.message ?? "Studying unavailable right now."));
82659
82866
  break;
82660
82867
  }
82661
82868
  case "mine-failed": {
82662
- console.error(import_picocolors18.default.red(outcome.miner?.message ?? "Study run failed."));
82869
+ console.error(import_picocolors18.default.red(outcome.learner?.message ?? "Study run failed."));
82663
82870
  process.exitCode = 1;
82664
82871
  break;
82665
82872
  }
@@ -84600,7 +84807,7 @@ Use 'dosu mcp add <agent>' to add Dosu MCP to a tool.`);
84600
84807
  }
84601
84808
  if (opts.follow) {
84602
84809
  try {
84603
- const lines = readFileSync25(logPath, "utf-8").split(`
84810
+ const lines = readFileSync26(logPath, "utf-8").split(`
84604
84811
  `);
84605
84812
  if (lines.at(-1) === "")
84606
84813
  lines.pop();
@@ -84616,7 +84823,7 @@ Use 'dosu mcp add <agent>' to add Dosu MCP to a tool.`);
84616
84823
  if (opts.tail !== undefined) {
84617
84824
  const n5 = typeof opts.tail === "string" ? parseInt(opts.tail, 10) || 50 : 50;
84618
84825
  try {
84619
- const content = readFileSync25(logPath, "utf-8");
84826
+ const content = readFileSync26(logPath, "utf-8");
84620
84827
  const lines = content.split(`
84621
84828
  `);
84622
84829
  console.log(lines.slice(-n5).join(`
@@ -84687,4 +84894,4 @@ execute().catch((err) => {
84687
84894
  process.exit(1);
84688
84895
  });
84689
84896
 
84690
- //# debugId=6b750395-441b-6515-6475-6e2164756e21
84897
+ //# debugId=a3c20325-c0f7-811f-6475-6e2164756e21