@dosu/cli 0.53.0-beta.12 → 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 +675 -435
  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.12", 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 } : {},
@@ -8595,13 +8743,24 @@ function setSyncPaused(paused, configDir = getConfigDir()) {
8595
8743
  delete state.paused;
8596
8744
  saveSyncState(state, configDir);
8597
8745
  }
8746
+ function resetSyncState(configDir = getConfigDir()) {
8747
+ const previous = loadSyncState(configDir);
8748
+ const fresh = {
8749
+ schema_version: STATE_SCHEMA_VERSION,
8750
+ watermark: null,
8751
+ consecutive_failures: 0,
8752
+ ...previous.project_filter ? { project_filter: previous.project_filter } : {},
8753
+ ...previous.paused ? { paused: true } : {}
8754
+ };
8755
+ saveSyncState(fresh, configDir);
8756
+ }
8598
8757
  function saveSyncState(state, configDir = getConfigDir()) {
8599
- if (!existsSync10(configDir))
8600
- mkdirSync6(configDir, { recursive: true, mode: 448 });
8758
+ if (!existsSync11(configDir))
8759
+ mkdirSync7(configDir, { recursive: true, mode: 448 });
8601
8760
  const path3 = syncStatePath(configDir);
8602
8761
  const tmp = `${path3}.${process.pid}.tmp`;
8603
- writeFileSync6(tmp, JSON.stringify(state, null, 2), { mode: 384 });
8604
- renameSync2(tmp, path3);
8762
+ writeFileSync7(tmp, JSON.stringify(state, null, 2), { mode: 384 });
8763
+ renameSync3(tmp, path3);
8605
8764
  }
8606
8765
  function backoffUntil(state) {
8607
8766
  if (state.consecutive_failures === 0 || !state.last_attempt_at)
@@ -8645,7 +8804,7 @@ function gateSessions(sessions, watermark, now = new Date, quietPeriodMs = DEFAU
8645
8804
  }
8646
8805
  return { ready, open: open2 };
8647
8806
  }
8648
- 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)";
8649
8808
  var init_watermark = __esm(() => {
8650
8809
  init_config();
8651
8810
  DEFAULT_QUIET_PERIOD_MS = 5 * 60 * 1000;
@@ -8661,20 +8820,34 @@ async function fetchReportNotes(cfg) {
8661
8820
  }
8662
8821
  const { createTypedClient: createTypedClient2 } = await Promise.resolve().then(() => (init_trpc(), exports_trpc));
8663
8822
  const client = createTypedClient2(cfg);
8664
- const { notes } = await client.notes.listMine.query({
8665
- org_id: orgId,
8666
- limit: REPORT_NOTES_LIMIT
8667
- });
8668
- return notes.map((note) => ({
8669
- title: note.title,
8670
- content: note.body,
8671
- at: note.created_at,
8672
- ...note.transcript_id ? { transcript_id: note.transcript_id } : {},
8673
- ...note.repo ? { repo: note.repo } : {},
8674
- ...note.branch ? { branch: note.branch } : {}
8675
- }));
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 };
8676
8849
  }
8677
- var REPORT_NOTES_LIMIT = 500;
8850
+ var REPORT_NOTES_LIMIT = 500, REPORT_NOTES_MAX_PAGES = 40;
8678
8851
 
8679
8852
  // src/report/css.ts
8680
8853
  var REPORT_CSS = `
@@ -8849,38 +9022,6 @@ function applyStatusDefaults(candidates, dryRun) {
8849
9022
  return { ...c2, status: known.has(current) ? current : fallback };
8850
9023
  });
8851
9024
  }
8852
- function tokenTotalsFromCandidates(candidates, inventory) {
8853
- if (candidates.length === 0)
8854
- return null;
8855
- let replaced = 0;
8856
- let hasRediscovery = false;
8857
- let readCost = 0;
8858
- for (const c2 of candidates) {
8859
- const raw = c2.approx_rediscovery_tokens;
8860
- if (raw != null) {
8861
- hasRediscovery = true;
8862
- replaced += Math.max(0, Number(raw) || 0);
8863
- }
8864
- const blob = `${c2.title ?? ""}
8865
- ${c2.content ?? ""}`;
8866
- readCost += Math.round(blob.length / CHARS_PER_TOKEN);
8867
- }
8868
- if (!hasRediscovery)
8869
- return null;
8870
- let baseline = inventory.totals?.learning_tokens ?? 0;
8871
- if (!baseline) {
8872
- baseline = inventory.transcripts.reduce((sum, t2) => sum + (t2.learning_tokens || 0), 0);
8873
- }
8874
- const saved = Math.max(0, replaced - readCost);
8875
- const pct = baseline ? Math.round(1000 * saved / baseline) / 10 : 0;
8876
- return {
8877
- baseline_tokens: baseline,
8878
- replaced_baseline_tokens: replaced,
8879
- read_knowledge_tokens: readCost,
8880
- tokens_saved: saved,
8881
- pct_saved: pct
8882
- };
8883
- }
8884
9025
  function presentationCopy(c2) {
8885
9026
  const idea = (c2.plain_english || "").trim() || (c2.content || "").trim();
8886
9027
  const how = (c2.how_found || "").trim();
@@ -9254,11 +9395,9 @@ function buildReportHtml(options) {
9254
9395
  return bv - av;
9255
9396
  });
9256
9397
  const org = options.orgName || "Your team";
9257
- const repo = options.repo || inventory.cwd || "—";
9258
- const branch = options.branch || "—";
9259
- const summary = options.summary || "Local agent session logs were mined into Dosu notes so the next task can reuse them — reducing rediscovery cost.";
9398
+ const projects = options.projects ?? [];
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.";
9260
9400
  const generated = `${(options.generatedAt ?? new Date).toISOString().replace("T", " ").slice(0, 16)} UTC`;
9261
- const derived = tokenTotalsFromCandidates(candidates, inventory);
9262
9401
  const [notesHeading, notesLede, footerTitle, footerBody] = notesSectionCopy(candidates);
9263
9402
  const candidateRows = candidates.map((c2, i2) => {
9264
9403
  const [idea, work] = presentationCopy(c2);
@@ -9298,27 +9437,19 @@ function buildReportHtml(options) {
9298
9437
  <td>${esc(query)}</td>
9299
9438
  </tr>`;
9300
9439
  }).join("");
9301
- let tokenSection;
9302
- if (derived) {
9303
- 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 = `
9304
9445
  <section>
9305
- <h2>Estimated context savings</h2>
9306
- <p class="lede">Counterfactual: replace rediscovery stretches with a Dosu <code>read_knowledge</code> hit.</p>
9446
+ <h2>This run</h2>
9307
9447
  <div class="stats">
9308
- <div class="stat"><div class="label">Baseline (cost to learn)</div><div class="value">${fmtInt(derived.baseline_tokens)}</div></div>
9309
- <div class="stat"><div class="label">Learning replaced</div><div class="value">${fmtInt(derived.replaced_baseline_tokens)}</div></div>
9310
- <div class="stat"><div class="label">Read cost</div><div class="value">${fmtInt(derived.read_knowledge_tokens)}</div></div>
9311
- <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>
9312
9450
  </div>
9313
9451
  </section>`;
9314
- } else {
9315
- tokenSection = `
9316
- <section>
9317
- <h2>Estimated context savings</h2>
9318
- <p class="muted">No rediscovery estimates on the notes yet — each candidate needs <code>approx_rediscovery_tokens</code>.</p>
9319
- </section>`;
9320
- }
9321
- const notesBody = candidateRows.join("") || '<p class="muted">No write_knowledge payloads yet. Run knowledge sync so the miner extracts learnings.</p>';
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>';
9322
9453
  const lede = notesLede ? `<p class="lede">${esc(notesLede)}</p>` : "";
9323
9454
  return `<!DOCTYPE html>
9324
9455
  <html lang="en">
@@ -9337,9 +9468,8 @@ ${REPORT_CSS}
9337
9468
  <h1>${esc(org)}</h1>
9338
9469
  <p class="lede">${esc(summary)}</p>
9339
9470
  <p class="meta-line">
9340
- Repo <code>${esc(repo)}</code>
9341
- · branch <code>${esc(branch)}</code>
9342
- · 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)" : ""}
9343
9473
  </p>
9344
9474
  <div class="toolbar no-print">
9345
9475
  <button type="button" onclick="window.print()">Print / Save as PDF</button>
@@ -9385,7 +9515,7 @@ ${REPORT_CSS}
9385
9515
  </html>
9386
9516
  `;
9387
9517
  }
9388
- 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;
9389
9519
  var init_html = __esm(() => {
9390
9520
  SQL_DISPLAY = new Set(["execute_sql", "query_run", "list_tables"]);
9391
9521
  LOGS_DISPLAY = new Set(["query_logs"]);
@@ -9395,7 +9525,7 @@ var init_html = __esm(() => {
9395
9525
  });
9396
9526
 
9397
9527
  // src/sessions/read.ts
9398
- import { readFileSync as readFileSync10 } from "node:fs";
9528
+ import { readFileSync as readFileSync11 } from "node:fs";
9399
9529
  function asRecord(value) {
9400
9530
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
9401
9531
  }
@@ -9511,11 +9641,11 @@ function readSessionTurns(session) {
9511
9641
  try {
9512
9642
  switch (session.harness) {
9513
9643
  case "claude":
9514
- return readClaude(readFileSync10(session.path, "utf8"));
9644
+ return readClaude(readFileSync11(session.path, "utf8"));
9515
9645
  case "cursor":
9516
- return readCursor(readFileSync10(session.path, "utf8"));
9646
+ return readCursor(readFileSync11(session.path, "utf8"));
9517
9647
  case "codex":
9518
- return readCodex(readFileSync10(session.path, "utf8"));
9648
+ return readCodex(readFileSync11(session.path, "utf8"));
9519
9649
  case "opencode":
9520
9650
  return readOpencode(session.path, session.id);
9521
9651
  }
@@ -9543,7 +9673,7 @@ function countRediscoveryToolCalls(session) {
9543
9673
  case "claude":
9544
9674
  case "cursor": {
9545
9675
  let n2 = 0;
9546
- for (const record of jsonlRecords(readFileSync10(session.path, "utf8"))) {
9676
+ for (const record of jsonlRecords(readFileSync11(session.path, "utf8"))) {
9547
9677
  const message = asRecord(record.message);
9548
9678
  for (const name of toolNamesFromContent(message?.content)) {
9549
9679
  if (isRediscoveryTool(name))
@@ -9554,7 +9684,7 @@ function countRediscoveryToolCalls(session) {
9554
9684
  }
9555
9685
  case "codex": {
9556
9686
  let n2 = 0;
9557
- for (const record of jsonlRecords(readFileSync10(session.path, "utf8"))) {
9687
+ for (const record of jsonlRecords(readFileSync11(session.path, "utf8"))) {
9558
9688
  const payload = asRecord(record.payload);
9559
9689
  if (payload?.type === "function_call" && typeof payload.name === "string") {
9560
9690
  if (isRediscoveryTool(payload.name))
@@ -9574,9 +9704,9 @@ function estimateSessionTokens(session) {
9574
9704
  let chars = 0;
9575
9705
  for (const turn of readSessionTurns(session))
9576
9706
  chars += turn.text.length;
9577
- return Math.round(chars / CHARS_PER_TOKEN2);
9707
+ return Math.round(chars / CHARS_PER_TOKEN);
9578
9708
  }
9579
- function isWorthMining(session) {
9709
+ function isWorthStudying(session) {
9580
9710
  const turns = readSessionTurns(session);
9581
9711
  if (turns.length < MIN_WORTH_TURNS)
9582
9712
  return false;
@@ -9588,7 +9718,7 @@ function isWorthMining(session) {
9588
9718
  }
9589
9719
  return false;
9590
9720
  }
9591
- 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;
9592
9722
  var init_read = __esm(() => {
9593
9723
  init_scan();
9594
9724
  PLAIN_TEXT = new Set(["text"]);
@@ -9645,12 +9775,12 @@ var init_queries = __esm(() => {
9645
9775
  });
9646
9776
 
9647
9777
  // src/report/digest.ts
9648
- import { readFileSync as readFileSync11 } from "node:fs";
9778
+ import { readFileSync as readFileSync12 } from "node:fs";
9649
9779
  function asRecord2(value) {
9650
9780
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
9651
9781
  }
9652
9782
  function estimateTokens(text) {
9653
- return Math.round(text.length / CHARS_PER_TOKEN3);
9783
+ return Math.round(text.length / CHARS_PER_TOKEN2);
9654
9784
  }
9655
9785
  function digestEstTokens(texts, tools, resultChars) {
9656
9786
  const countable = tools.filter((t2) => t2.name !== "tool_result");
@@ -9660,7 +9790,7 @@ function digestEstTokens(texts, tools, resultChars) {
9660
9790
  payload += JSON.stringify(countable);
9661
9791
  let tokens = estimateTokens(payload);
9662
9792
  if (resultChars > 0)
9663
- tokens += Math.max(1, Math.round(resultChars / CHARS_PER_TOKEN3));
9793
+ tokens += Math.max(1, Math.round(resultChars / CHARS_PER_TOKEN2));
9664
9794
  return tokens;
9665
9795
  }
9666
9796
  function copyToolInputFields(entry, inp) {
@@ -9876,16 +10006,16 @@ function sessionToDigest(session) {
9876
10006
  try {
9877
10007
  switch (session.harness) {
9878
10008
  case "claude":
9879
- return { turns: digestClaude(readFileSync11(session.path, "utf8")) };
10009
+ return { turns: digestClaude(readFileSync12(session.path, "utf8")) };
9880
10010
  case "cursor":
9881
- return { turns: digestCursor(readFileSync11(session.path, "utf8")) };
10011
+ return { turns: digestCursor(readFileSync12(session.path, "utf8")) };
9882
10012
  case "codex":
9883
- return { turns: digestCodex(readFileSync11(session.path, "utf8")) };
10013
+ return { turns: digestCodex(readFileSync12(session.path, "utf8")) };
9884
10014
  case "opencode": {
9885
10015
  const turns = readSessionTurns(session).map((turn, index) => ({
9886
10016
  role: turn.role,
9887
10017
  line: index + 1,
9888
- est_tokens: Math.round(turn.text.length / CHARS_PER_TOKEN3),
10018
+ est_tokens: Math.round(turn.text.length / CHARS_PER_TOKEN2),
9889
10019
  text: [turn.text],
9890
10020
  tools: []
9891
10021
  }));
@@ -9910,7 +10040,7 @@ function digestTurnText(turn) {
9910
10040
  return texts.filter(Boolean).join(`
9911
10041
  `);
9912
10042
  }
9913
- 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;
9914
10044
  var init_digest = __esm(() => {
9915
10045
  init_read();
9916
10046
  init_queries();
@@ -10101,17 +10231,17 @@ var init_notes = __esm(() => {
10101
10231
  });
10102
10232
 
10103
10233
  // src/report/write.ts
10104
- import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "node:fs";
10234
+ import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "node:fs";
10105
10235
  import { tmpdir } from "node:os";
10106
- import { dirname as dirname4, join as join10, resolve } from "node:path";
10236
+ import { dirname as dirname4, join as join11, resolve } from "node:path";
10107
10237
  import { pathToFileURL } from "node:url";
10108
10238
  function defaultReportPath() {
10109
- return join10(tmpdir(), "dosu-knowledge-report.html");
10239
+ return join11(tmpdir(), "dosu-knowledge-report.html");
10110
10240
  }
10111
10241
  async function writeAndOpenReport(options) {
10112
10242
  const out = resolve(options.out ?? defaultReportPath());
10113
- mkdirSync7(dirname4(out), { recursive: true });
10114
- writeFileSync7(out, options.html, "utf8");
10243
+ mkdirSync8(dirname4(out), { recursive: true });
10244
+ writeFileSync8(out, options.html, "utf8");
10115
10245
  if (options.open !== false) {
10116
10246
  const url = pathToFileURL(out).href;
10117
10247
  if (options.openUrl) {
@@ -10126,17 +10256,26 @@ async function writeAndOpenReport(options) {
10126
10256
  var init_write = () => {};
10127
10257
 
10128
10258
  // src/report/generate.ts
10259
+ import { basename as basename2 } from "node:path";
10129
10260
  function sessionsForNotes(notes, sessions) {
10130
10261
  const ids = new Set(notes.map((n2) => n2.transcript_id).filter((id) => Boolean(id)));
10131
10262
  return sessions.filter((s) => ids.has(s.id));
10132
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
+ }
10133
10271
  function noteTime(note) {
10134
10272
  const parsed = Date.parse(note.at ?? "");
10135
10273
  return Number.isNaN(parsed) ? 0 : parsed;
10136
10274
  }
10137
10275
  async function emitKnowledgeReport(options = {}) {
10138
10276
  const cfg = loadConfig();
10139
- 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));
10140
10279
  const scanned = options.sessions ?? scanAgentSessions();
10141
10280
  const sessions = sessionsForNotes(notes, scanned);
10142
10281
  const candidates = attributeRediscovery(notes, sessions);
@@ -10145,12 +10284,16 @@ async function emitKnowledgeReport(options = {}) {
10145
10284
  if (inventory.totals && state.total_learning_tokens && !options.sessions) {
10146
10285
  inventory.totals.learning_tokens = state.total_learning_tokens;
10147
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
+ ];
10148
10291
  const html = buildReportHtml({
10149
10292
  inventory,
10150
10293
  candidates,
10151
10294
  orgName: options.orgName ?? cfg.active_account?.target?.org_name ?? "Your team",
10152
- repo: options.repo ?? notes.find((n2) => n2.repo)?.repo,
10153
- branch: options.branch ?? notes.find((n2) => n2.branch)?.branch,
10295
+ projects,
10296
+ truncated: fetched.truncated,
10154
10297
  generatedAt: options.generatedAt,
10155
10298
  digests: digestsForSessions(sessions)
10156
10299
  });
@@ -10163,6 +10306,7 @@ async function emitKnowledgeReport(options = {}) {
10163
10306
  }
10164
10307
  var init_generate = __esm(() => {
10165
10308
  init_config();
10309
+ init_project_dir();
10166
10310
  init_scan();
10167
10311
  init_watermark();
10168
10312
  init_html();
@@ -10170,151 +10314,6 @@ var init_generate = __esm(() => {
10170
10314
  init_write();
10171
10315
  });
10172
10316
 
10173
- // src/sessions/project-dir.ts
10174
- import {
10175
- closeSync as closeSync2,
10176
- existsSync as existsSync11,
10177
- mkdirSync as mkdirSync8,
10178
- openSync as openSync2,
10179
- readFileSync as readFileSync12,
10180
- readSync,
10181
- renameSync as renameSync3,
10182
- statSync as statSync4,
10183
- writeFileSync as writeFileSync8
10184
- } from "node:fs";
10185
- import { join as join11 } from "node:path";
10186
- function cachePath(configDir) {
10187
- return join11(configDir, CACHE_FILENAME2);
10188
- }
10189
- function loadCacheFile(configDir) {
10190
- try {
10191
- const raw = JSON.parse(readFileSync12(cachePath(configDir), "utf-8"));
10192
- if (raw.schema_version !== CACHE_SCHEMA_VERSION || typeof raw.entries !== "object")
10193
- return {};
10194
- return raw.entries;
10195
- } catch {
10196
- return {};
10197
- }
10198
- }
10199
- function saveCacheFile(configDir, entries) {
10200
- try {
10201
- if (!existsSync11(configDir))
10202
- mkdirSync8(configDir, { recursive: true, mode: 448 });
10203
- const path3 = cachePath(configDir);
10204
- const tmp = `${path3}.${process.pid}.tmp`;
10205
- const file = { schema_version: CACHE_SCHEMA_VERSION, entries };
10206
- writeFileSync8(tmp, JSON.stringify(file), { mode: 384 });
10207
- renameSync3(tmp, path3);
10208
- } catch {}
10209
- }
10210
- function readHead(path3, bytes = HEAD_BYTES) {
10211
- let fd;
10212
- try {
10213
- fd = openSync2(path3, "r");
10214
- } catch {
10215
- return null;
10216
- }
10217
- try {
10218
- const buf = Buffer.alloc(bytes);
10219
- const read = readSync(fd, buf, 0, bytes, 0);
10220
- return buf.subarray(0, read).toString("utf-8");
10221
- } catch {
10222
- return null;
10223
- } finally {
10224
- closeSync2(fd);
10225
- }
10226
- }
10227
- function cwdFromJsonlHead(text) {
10228
- for (const line of text.split(`
10229
- `).slice(0, HEAD_LINES)) {
10230
- if (!line.includes('"cwd"'))
10231
- continue;
10232
- try {
10233
- const obj = JSON.parse(line);
10234
- const cwd = obj.cwd ?? obj.payload?.cwd;
10235
- if (typeof cwd === "string" && cwd.startsWith("/"))
10236
- return cwd;
10237
- } catch {}
10238
- }
10239
- return null;
10240
- }
10241
- function unmungeSlug(slug, exists = existsSync11) {
10242
- const tokens = slug.replace(/^-/, "").split("-");
10243
- if (tokens.length === 0 || tokens[0] === "")
10244
- return null;
10245
- let budget = 5000;
10246
- const walk = (prefix, index) => {
10247
- if (budget-- <= 0)
10248
- return null;
10249
- if (index === tokens.length)
10250
- return exists(prefix) ? prefix : null;
10251
- const asSegment = prefix === "" || exists(prefix) ? walk(`${prefix}/${tokens[index]}`, index + 1) : null;
10252
- if (asSegment)
10253
- return asSegment;
10254
- return prefix === "" ? null : walk(`${prefix}-${tokens[index]}`, index + 1);
10255
- };
10256
- return walk("", 0);
10257
- }
10258
- function fileMtime(path3) {
10259
- try {
10260
- return statSync4(path3).mtime.toISOString();
10261
- } catch {
10262
- return "";
10263
- }
10264
- }
10265
- function createProjectDirResolver(configDir = getConfigDir(), deps = {}) {
10266
- const exists = deps.exists ?? existsSync11;
10267
- const head = deps.readHead ?? readHead;
10268
- const mtime = deps.mtime ?? fileMtime;
10269
- const entries = loadCacheFile(configDir);
10270
- let dirty = false;
10271
- const compute = (session) => {
10272
- switch (session.harness) {
10273
- case "opencode":
10274
- return session.project ?? null;
10275
- case "claude": {
10276
- const text = head(session.path);
10277
- const cwd = text ? cwdFromJsonlHead(text) : null;
10278
- if (cwd)
10279
- return cwd;
10280
- return session.project ? unmungeSlug(session.project, exists) : null;
10281
- }
10282
- case "codex": {
10283
- const text = head(session.path);
10284
- return text ? cwdFromJsonlHead(text) : null;
10285
- }
10286
- case "cursor":
10287
- return session.project ? unmungeSlug(session.project, exists) : null;
10288
- default:
10289
- return null;
10290
- }
10291
- };
10292
- return {
10293
- resolve(session) {
10294
- const key = `${session.harness}/${session.id}`;
10295
- const cached = entries[key];
10296
- if (cached && (cached.dir !== null || cached.mtime === mtime(session.path))) {
10297
- return cached.dir;
10298
- }
10299
- const dir = compute(session);
10300
- entries[key] = { dir, mtime: mtime(session.path) };
10301
- dirty = true;
10302
- return dir;
10303
- },
10304
- flush() {
10305
- if (!dirty)
10306
- return;
10307
- saveCacheFile(configDir, entries);
10308
- dirty = false;
10309
- }
10310
- };
10311
- }
10312
- var CACHE_FILENAME2 = "project-dirs.json", CACHE_SCHEMA_VERSION = 1, HEAD_BYTES, HEAD_LINES = 50;
10313
- var init_project_dir = __esm(() => {
10314
- init_config();
10315
- HEAD_BYTES = 128 * 1024;
10316
- });
10317
-
10318
10317
  // src/sync/backlog.ts
10319
10318
  function listSessionBacklog() {
10320
10319
  try {
@@ -10459,7 +10458,7 @@ function readLockFile(configDir) {
10459
10458
  }
10460
10459
  function recentSyncActivity(log3) {
10461
10460
  return log3.split(`
10462
- `).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)\]/, ""));
10463
10462
  }
10464
10463
  function getSyncStatus(deps = {}) {
10465
10464
  const configDir = deps.configDir ?? getConfigDir();
@@ -10508,7 +10507,7 @@ async function runKnowledgeSync(options = {}) {
10508
10507
  const state = loadState();
10509
10508
  if (options.quiet) {
10510
10509
  if (state.paused) {
10511
- logger.debug("sync", "skipping quiet sync: mining is paused");
10510
+ logger.debug("sync", "skipping quiet sync: studying is paused");
10512
10511
  return { status: "skipped-paused", readySessions: 0, inFlightSessions: 0, sessions: [] };
10513
10512
  }
10514
10513
  const retryAt = backoffUntil(state);
@@ -10518,7 +10517,7 @@ async function runKnowledgeSync(options = {}) {
10518
10517
  }
10519
10518
  } else if (state.paused) {
10520
10519
  delete state.paused;
10521
- logger.debug("sync", "manual sync resumes paused mining");
10520
+ logger.debug("sync", "manual sync resumes paused studying");
10522
10521
  }
10523
10522
  let ready;
10524
10523
  let open2;
@@ -10580,14 +10579,14 @@ async function runKnowledgeSync(options = {}) {
10580
10579
  baseline_mined: state.total_mined ?? 0
10581
10580
  };
10582
10581
  }
10583
- const worthMining = deps.worthMining ?? isWorthMining;
10582
+ const worthStudying = deps.worthStudying ?? isWorthStudying;
10584
10583
  const examined = [];
10585
10584
  const batch = [];
10586
10585
  let trivial = 0;
10587
10586
  for (let i2 = ready.length - 1;i2 >= 0 && batch.length < MINE_BATCH_LIMIT; i2--) {
10588
10587
  const candidate = ready[i2];
10589
10588
  examined.push(candidate);
10590
- if (worthMining(candidate)) {
10589
+ if (worthStudying(candidate)) {
10591
10590
  batch.push(candidate);
10592
10591
  } else {
10593
10592
  trivial += 1;
@@ -10604,26 +10603,26 @@ async function runKnowledgeSync(options = {}) {
10604
10603
  return {
10605
10604
  status: "nothing-new",
10606
10605
  ...base,
10607
- minedSessions: 0,
10606
+ studiedSessions: 0,
10608
10607
  trivialSessions: trivial
10609
10608
  };
10610
10609
  }
10611
- logger.debug("sync", `mining ${batch.length} of ${ready.length} ready sessions (${trivial} trivial skipped)`);
10612
- const miner = await deps.mine(batch);
10613
- switch (miner.outcome) {
10610
+ logger.debug("sync", `studying ${batch.length} of ${ready.length} ready sessions (${trivial} trivial skipped)`);
10611
+ const learner = await deps.mine(batch);
10612
+ switch (learner.outcome) {
10614
10613
  case "completed": {
10615
10614
  for (const s of batch) {
10616
- logger.debug("sync", `mined session ${s.harness}/${s.id}`);
10615
+ logger.debug("sync", `studied session ${s.harness}/${s.id}`);
10617
10616
  }
10618
- const minedAt = now().toISOString();
10617
+ const studiedAt = now().toISOString();
10619
10618
  const history = [
10620
10619
  ...state.mined_sessions ?? [],
10621
10620
  ...batch.map((s) => ({
10622
- at: minedAt,
10621
+ at: studiedAt,
10623
10622
  session: `${s.harness}/${s.id}`,
10624
10623
  ...s.project ? { project: s.project } : {}
10625
10624
  }))
10626
- ].slice(-MINED_HISTORY_LIMIT);
10625
+ ].slice(-STUDIED_HISTORY_LIMIT);
10627
10626
  const watermark = batchWatermark(examined);
10628
10627
  const sessionTokens = deps.sessionTokens ?? estimateSessionTokens;
10629
10628
  let batchTokens = 0;
@@ -10632,21 +10631,21 @@ async function runKnowledgeSync(options = {}) {
10632
10631
  saveState({
10633
10632
  ...state,
10634
10633
  watermark,
10635
- last_attempt_at: minedAt,
10634
+ last_attempt_at: studiedAt,
10636
10635
  consecutive_failures: 0,
10637
10636
  mined_sessions: history,
10638
10637
  total_mined: (state.total_mined ?? 0) + batch.length,
10639
- total_notes: (state.total_notes ?? 0) + miner.notesWritten,
10638
+ total_notes: (state.total_notes ?? 0) + learner.notesWritten,
10640
10639
  total_learning_tokens: (state.total_learning_tokens ?? 0) + batchTokens,
10641
10640
  last_refusal: undefined
10642
10641
  });
10643
- logger.debug("sync", `mined ${batch.length} sessions, ${miner.notesWritten} suggested pages; watermark → ${watermark}`);
10642
+ logger.debug("sync", `studied ${batch.length} sessions, ${learner.notesWritten} suggested pages; watermark → ${watermark}`);
10644
10643
  return {
10645
- status: "mined",
10644
+ status: "studied",
10646
10645
  ...base,
10647
- minedSessions: batch.length,
10646
+ studiedSessions: batch.length,
10648
10647
  trivialSessions: trivial,
10649
- miner
10648
+ learner
10650
10649
  };
10651
10650
  }
10652
10651
  case "consent_off":
@@ -10659,12 +10658,12 @@ async function runKnowledgeSync(options = {}) {
10659
10658
  consecutive_failures: 0,
10660
10659
  last_refusal: {
10661
10660
  at,
10662
- outcome: miner.outcome,
10663
- message: miner.message ?? "Mining unavailable right now."
10661
+ outcome: learner.outcome,
10662
+ message: learner.message ?? "Studying unavailable right now."
10664
10663
  }
10665
10664
  });
10666
- logger.debug("sync", `mining skipped by gateway: ${miner.outcome}`);
10667
- 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 };
10668
10667
  }
10669
10668
  default: {
10670
10669
  saveState({
@@ -10672,13 +10671,13 @@ async function runKnowledgeSync(options = {}) {
10672
10671
  last_attempt_at: now().toISOString(),
10673
10672
  consecutive_failures: state.consecutive_failures + 1
10674
10673
  });
10675
- logger.debug("sync", `mining failed: ${miner.outcome}; ${miner.message ?? ""}`);
10674
+ logger.debug("sync", `studying failed: ${learner.outcome}; ${learner.message ?? ""}`);
10676
10675
  return {
10677
10676
  status: "mine-failed",
10678
10677
  ...base,
10679
- minedSessions: 0,
10680
- miner,
10681
- error: miner.message
10678
+ studiedSessions: 0,
10679
+ learner,
10680
+ error: learner.message
10682
10681
  };
10683
10682
  }
10684
10683
  }
@@ -10824,12 +10823,12 @@ var init_backfill_run = __esm(() => {
10824
10823
  init_backfill();
10825
10824
  });
10826
10825
 
10827
- // src/miner/config-dir.ts
10826
+ // src/learner/config-dir.ts
10828
10827
  import { mkdtempSync, rmSync as rmSync3, writeFileSync as writeFileSync10 } from "node:fs";
10829
10828
  import { tmpdir as tmpdir2 } from "node:os";
10830
10829
  import { join as join13 } from "node:path";
10831
10830
  function createRunConfigDir() {
10832
- const path3 = mkdtempSync(join13(tmpdir2(), "dosu-miner-"));
10831
+ const path3 = mkdtempSync(join13(tmpdir2(), "dosu-learner-"));
10833
10832
  writeFileSync10(join13(path3, ".claude.json"), JSON.stringify({ hasCompletedOnboarding: true }));
10834
10833
  return {
10835
10834
  path: path3,
@@ -10840,7 +10839,7 @@ function createRunConfigDir() {
10840
10839
  }
10841
10840
  var init_config_dir = () => {};
10842
10841
 
10843
- // src/miner/conflicts.ts
10842
+ // src/learner/conflicts.ts
10844
10843
  import { existsSync as existsSync13, readFileSync as readFileSync15 } from "node:fs";
10845
10844
  function managedSettingsPaths(platform3 = process.platform) {
10846
10845
  switch (platform3) {
@@ -10898,11 +10897,11 @@ var init_conflicts = __esm(() => {
10898
10897
  CONFLICTING_ENV_PREFIXES = ["ANTHROPIC_", "CLAUDE_CODE_", "AWS_"];
10899
10898
  });
10900
10899
 
10901
- // src/miner/env.ts
10900
+ // src/learner/env.ts
10902
10901
  function isStripped(name) {
10903
10902
  return STRIPPED_NAMES.has(name) || STRIPPED_PREFIXES.some((p2) => name.startsWith(p2));
10904
10903
  }
10905
- function buildMinerEnv(options) {
10904
+ function buildLearnerEnv(options) {
10906
10905
  const base = options.baseEnv ?? process.env;
10907
10906
  const env = {};
10908
10907
  for (const [name, value] of Object.entries(base)) {
@@ -10932,7 +10931,7 @@ var init_env = __esm(() => {
10932
10931
  STRIPPED_NAMES = new Set(["CLAUDE_CONFIG_DIR", "CLAUDECODE", "AWS_BEARER_TOKEN_BEDROCK"]);
10933
10932
  });
10934
10933
 
10935
- // src/miner/executable.ts
10934
+ // src/learner/executable.ts
10936
10935
  import { accessSync, constants, existsSync as existsSync14 } from "node:fs";
10937
10936
  import { createRequire as createRequire3 } from "node:module";
10938
10937
  import { homedir as homedir4 } from "node:os";
@@ -10979,9 +10978,9 @@ function resolveClaudeExecutable(options = {}) {
10979
10978
  }
10980
10979
  var init_executable = () => {};
10981
10980
 
10982
- // src/miner/prompt.ts
10983
- function buildMinerSystemPrompt(coreRules) {
10984
- 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.
10985
10984
 
10986
10985
  Available tools (the only tools you may use, by these exact names):
10987
10986
  - mcp__sessions__list_sessions / mcp__sessions__read_session: the session transcripts in scope for this run.
@@ -10989,7 +10988,7 @@ Available tools (the only tools you may use, by these exact names):
10989
10988
 
10990
10989
  ${coreRules}`;
10991
10990
  }
10992
- function buildMinerPrompt(sessions) {
10991
+ function buildLearnerPrompt(sessions) {
10993
10992
  const list = sessions.map((s) => `- ${s.id} (${s.harness})`).join(`
10994
10993
  `);
10995
10994
  return `Mine the following ${sessions.length} coding-agent session(s) for durable knowledge:
@@ -10999,8 +10998,8 @@ ${list}
10999
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.`;
11000
10999
  }
11001
11000
 
11002
- // src/miner/prompt-core.ts
11003
- var MINER_CORE_RULES = `Rules — non-negotiable:
11001
+ // src/learner/prompt-core.ts
11002
+ var LEARNER_CORE_RULES = `Rules — non-negotiable:
11004
11003
  1. Before writing anything, call read_knowledge with the candidate topic. If the same fact is
11005
11004
  already recorded, skip it — never write a duplicate or near-duplicate. If the topic exists but
11006
11005
  the session shows it has changed or been superseded, do write: a note stating the current state
@@ -11028,10 +11027,9 @@ useful.
11028
11027
  6. Only pass repo/branch to write_knowledge when the session itself verifies them (an explicit
11029
11028
  cwd, git remote, or branch mentioned in the transcript). Never infer or guess a repo. When not
11030
11029
  verified, omit both.
11031
- 7. Populate write_knowledge metadata with source_agent and session_id for every note.
11032
- 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
11033
11031
  long verbatim transcript spans. Summarize in your own words.
11034
- 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.`;
11035
11033
 
11036
11034
  // node_modules/@anthropic-ai/claude-agent-sdk/sdk.mjs
11037
11035
  var exports_sdk = {};
@@ -72782,7 +72780,7 @@ var init_redact = __esm(() => {
72782
72780
  ]);
72783
72781
  });
72784
72782
 
72785
- // src/miner/tools.ts
72783
+ // src/learner/tools.ts
72786
72784
  function textResult(text) {
72787
72785
  return { content: [{ type: "text", text }] };
72788
72786
  }
@@ -72841,7 +72839,7 @@ function createSessionToolsServer(sessions) {
72841
72839
  name: SESSIONS_SERVER_NAME,
72842
72840
  version: "1.0.0",
72843
72841
  tools: [
72844
- 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))),
72845
72843
  O6e("read_session", "Read the conversation turns of one in-scope session. Responses are " + "paginated; pass the returned offset to continue reading.", {
72846
72844
  id: exports_external.string().describe("Session id from list_sessions"),
72847
72845
  offset: exports_external.number().int().min(0).optional().describe("Turn index to start from")
@@ -72867,11 +72865,11 @@ var init_tools = __esm(() => {
72867
72865
  init_redact();
72868
72866
  });
72869
72867
 
72870
- // src/miner/runner.ts
72868
+ // src/learner/runner.ts
72871
72869
  var exports_runner = {};
72872
72870
  __export(exports_runner, {
72873
72871
  traceAgentMessage: () => traceAgentMessage,
72874
- runMiner: () => runMiner,
72872
+ runLearner: () => runLearner,
72875
72873
  classifyGatewayError: () => classifyGatewayError
72876
72874
  });
72877
72875
  function classifyGatewayError(text) {
@@ -72893,12 +72891,12 @@ function traceAgentMessage(message) {
72893
72891
  return;
72894
72892
  for (const block2 of content) {
72895
72893
  if (msg.type === "assistant" && block2.type === "text") {
72896
- logger.debug("miner", `[agent] ${snippet(block2.text)}`);
72894
+ logger.debug("learner", `[agent] ${snippet(block2.text)}`);
72897
72895
  } else if (msg.type === "assistant" && block2.type === "tool_use") {
72898
- logger.debug("miner", `[agent] → ${block2.name} ${snippet(block2.input)}`);
72896
+ logger.debug("learner", `[agent] → ${block2.name} ${snippet(block2.input)}`);
72899
72897
  } else if (msg.type === "user" && block2.type === "tool_result") {
72900
72898
  const full = JSON.stringify(block2.content ?? "");
72901
- 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)}`);
72902
72900
  }
72903
72901
  }
72904
72902
  }
@@ -72911,7 +72909,7 @@ function allowedToolNames() {
72911
72909
  `mcp__${KNOWLEDGE_SERVER_NAME}__finalize_session_knowledge`
72912
72910
  ]);
72913
72911
  }
72914
- async function runMiner(options) {
72912
+ async function runLearner(options) {
72915
72913
  const conflicts = detectSettingsConflicts();
72916
72914
  if (conflicts.length > 0) {
72917
72915
  const detail = conflicts.map((c5) => `${c5.file} (${c5.keys.join(", ")})`).join("; ");
@@ -72919,7 +72917,7 @@ async function runMiner(options) {
72919
72917
  outcome: "settings_conflict",
72920
72918
  notesWritten: 0,
72921
72919
  turns: 0,
72922
- message: `Refusing to run: conflicting Claude Code settings would override the miner's auth (${detail})`
72920
+ message: `Refusing to run: conflicting Claude Code settings would override the study run's auth (${detail})`
72923
72921
  };
72924
72922
  }
72925
72923
  const gatewayURL = options.gatewayURL ?? getLlmGatewayURL();
@@ -72934,7 +72932,7 @@ async function runMiner(options) {
72934
72932
  const { query } = await Promise.resolve().then(() => (init_sdk(), exports_sdk));
72935
72933
  const claudeExecutable = resolveClaudeExecutable();
72936
72934
  if (claudeExecutable) {
72937
- logger.debug("miner", `using system Claude Code executable: ${claudeExecutable}`);
72935
+ logger.debug("learner", `using system Claude Code executable: ${claudeExecutable}`);
72938
72936
  }
72939
72937
  const configDir = createRunConfigDir();
72940
72938
  const runID = options.runID ?? crypto.randomUUID();
@@ -72947,7 +72945,7 @@ async function runMiner(options) {
72947
72945
  let currentSession;
72948
72946
  const readsSinceWrite = new Set;
72949
72947
  let turns = 0;
72950
- const env = buildMinerEnv({
72948
+ const env = buildLearnerEnv({
72951
72949
  apiKey: options.apiKey,
72952
72950
  gatewayURL,
72953
72951
  configDir: configDir.path,
@@ -72958,9 +72956,9 @@ async function runMiner(options) {
72958
72956
  });
72959
72957
  try {
72960
72958
  const run2 = query({
72961
- prompt: buildMinerPrompt(options.sessions),
72959
+ prompt: buildLearnerPrompt(options.sessions),
72962
72960
  options: {
72963
- systemPrompt: buildMinerSystemPrompt(MINER_CORE_RULES),
72961
+ systemPrompt: buildLearnerSystemPrompt(LEARNER_CORE_RULES),
72964
72962
  env,
72965
72963
  abortController: abort,
72966
72964
  maxTurns: options.maxTurns ?? DEFAULT_MAX_TURNS,
@@ -72976,17 +72974,17 @@ async function runMiner(options) {
72976
72974
  headers: {
72977
72975
  ...mcpHeaders(options.apiKey),
72978
72976
  "X-Dosu-Session-Id": runID,
72979
- "X-Dosu-Client": `dosu-cli-miner/${getVersionString()}`,
72977
+ "X-Dosu-Client": `dosu-cli-learner/${getVersionString()}`,
72980
72978
  ...sessionStartedAt ? { "X-Dosu-Session-Started-At": sessionStartedAt } : {}
72981
72979
  }
72982
72980
  }
72983
72981
  },
72984
- stderr: (data) => logger.debug("miner", `[sdk] ${data}`),
72982
+ stderr: (data) => logger.debug("learner", `[sdk] ${data}`),
72985
72983
  canUseTool: async (toolName, input2) => {
72986
72984
  if (!allowed.has(toolName)) {
72987
72985
  return {
72988
72986
  behavior: "deny",
72989
- message: `Tool ${toolName} is not permitted in mining runs.`
72987
+ message: `Tool ${toolName} is not permitted in study runs.`
72990
72988
  };
72991
72989
  }
72992
72990
  if (toolName === `mcp__${SESSIONS_SERVER_NAME}__read_session`) {
@@ -73038,15 +73036,15 @@ async function runMiner(options) {
73038
73036
  };
73039
73037
  }
73040
73038
  if (message.subtype !== "success" || message.is_error) {
73041
- logger.debug("miner", `run ${runID} failed: ${text}`);
73039
+ logger.debug("learner", `run ${runID} failed: ${text}`);
73042
73040
  return {
73043
73041
  outcome: "error",
73044
73042
  notesWritten,
73045
73043
  turns,
73046
- message: "Mining run failed; see debug log for details."
73044
+ message: "Study run failed; see debug log for details."
73047
73045
  };
73048
73046
  }
73049
- logger.debug("miner", `run ${runID} completed: ${turns} turns, ${notesWritten} suggested pages`);
73047
+ logger.debug("learner", `run ${runID} completed: ${turns} turns, ${notesWritten} suggested pages`);
73050
73048
  return { outcome: "completed", notesWritten, turns, message: text };
73051
73049
  }
73052
73050
  }
@@ -73054,7 +73052,7 @@ async function runMiner(options) {
73054
73052
  outcome: "error",
73055
73053
  notesWritten,
73056
73054
  turns,
73057
- message: "Mining run ended without a result."
73055
+ message: "Study run ended without a result."
73058
73056
  };
73059
73057
  } catch (error61) {
73060
73058
  const text = error61 instanceof Error ? error61.message : String(error61);
@@ -73062,12 +73060,12 @@ async function runMiner(options) {
73062
73060
  if (gatewayError) {
73063
73061
  return { outcome: gatewayError.outcome, notesWritten, turns, message: gatewayError.message };
73064
73062
  }
73065
- logger.debug("miner", `run ${runID} threw: ${text}`);
73063
+ logger.debug("learner", `run ${runID} threw: ${text}`);
73066
73064
  return {
73067
73065
  outcome: "error",
73068
73066
  notesWritten,
73069
73067
  turns,
73070
- message: abort.signal.aborted ? "Mining run timed out and was aborted." : "Mining run failed; see debug log for details."
73068
+ message: abort.signal.aborted ? "Study run timed out and was aborted." : "Study run failed; see debug log for details."
73071
73069
  };
73072
73070
  } finally {
73073
73071
  clearTimeout(timer);
@@ -73537,7 +73535,7 @@ var init_settings = __esm(() => {
73537
73535
  });
73538
73536
 
73539
73537
  // src/telemetry/debug-id.ts
73540
- var BUNDLE_DEBUG_ID_PLACEHOLDER = "acac034b-488b-746c-6475-6e2164756e21";
73538
+ var BUNDLE_DEBUG_ID_PLACEHOLDER = "a3c20325-c0f7-811f-6475-6e2164756e21";
73541
73539
 
73542
73540
  // src/telemetry/telemetry.ts
73543
73541
  import { randomUUID as randomUUID4 } from "node:crypto";
@@ -75413,6 +75411,140 @@ var init_errors3 = __esm(() => {
75413
75411
  // src/mcp/constants.ts
75414
75412
  var MCP_PROVIDER_SLUG2 = "dosu_mcp";
75415
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
+
75416
75548
  // src/tui/alt-screen.ts
75417
75549
  function enterAltScreen(output2) {
75418
75550
  depth += 1;
@@ -75436,7 +75568,8 @@ var init_alt_screen = __esm(() => {
75436
75568
  });
75437
75569
 
75438
75570
  // src/tui/activity-view.ts
75439
- 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";
75440
75573
  function reduceActivityViewKey(key) {
75441
75574
  if (key === "q" || key === ESC7 || key === CTRL_C4)
75442
75575
  return "back";
@@ -75452,6 +75585,8 @@ function reduceActivityViewKey(key) {
75452
75585
  return "sync";
75453
75586
  if (key === "f")
75454
75587
  return "full";
75588
+ if (key === "c")
75589
+ return "clear";
75455
75590
  return "none";
75456
75591
  }
75457
75592
  function cycleTab(tab, delta = 1) {
@@ -75482,13 +75617,13 @@ function formatActivityLine(line, width, full = false) {
75482
75617
  }
75483
75618
  function appendSyncActivity(buffer, chunk, max = ACTIVITY_VIEW_BUFFER_LINES) {
75484
75619
  const fresh = chunk.split(`
75485
- `).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)\]/, ""));
75486
75621
  return [...buffer, ...fresh].slice(-max);
75487
75622
  }
75488
75623
  function clip(text, max) {
75489
75624
  return text.length <= max ? text : `${text.slice(0, max - 1)}…`;
75490
75625
  }
75491
- function formatMinedRow(record2, full = false) {
75626
+ function formatStudiedRow(record2, full = false, name) {
75492
75627
  const match = record2.at.match(/^\d{4}-(\d{2}-\d{2})T(\d{2}:\d{2})/);
75493
75628
  const stamp = match ? `${match[1]} ${match[2]}` : record2.at;
75494
75629
  const slash = record2.session.indexOf("/");
@@ -75496,13 +75631,15 @@ function formatMinedRow(record2, full = false) {
75496
75631
  const id2 = slash > 0 ? record2.session.slice(slash + 1) : record2.session;
75497
75632
  const rawProject = record2.project ?? "-";
75498
75633
  const project = full ? rawProject : clip(rawProject, 28);
75499
- 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}`;
75500
75636
  }
75501
- function formatQueuedRow(session, full = false) {
75637
+ function formatQueuedRow(session, full = false, name) {
75502
75638
  const stamp = session.updated.replace("T", " ").slice(5, 16);
75503
75639
  const rawProject = session.project ?? "-";
75504
75640
  const project = full ? rawProject : clip(rawProject, 28);
75505
- 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}`;
75506
75643
  }
75507
75644
  function wrapRow(text, width, indent = " ") {
75508
75645
  if (text.length <= width)
@@ -75542,23 +75679,23 @@ function foldRunProgress(progress, chunk) {
75542
75679
  let current = progress;
75543
75680
  for (const line of chunk.split(`
75544
75681
  `)) {
75545
- const start = line.match(/\[sync\] mining (\d+) of \d+ ready sessions/);
75682
+ const start = line.match(/\[sync\] (?:studying|mining) (\d+) of \d+ ready sessions/);
75546
75683
  if (start) {
75547
75684
  current = { batch: Number.parseInt(start[1], 10), read: new Set, notes: 0 };
75548
75685
  continue;
75549
75686
  }
75550
- if (/\[sync\] (mined \d+ sessions|mining failed|mining skipped)/.test(line)) {
75687
+ if (/\[sync\] (?:studied|mined) \d+ sessions|\[sync\] (?:studying|mining) (?:failed|skipped)/.test(line)) {
75551
75688
  current = null;
75552
75689
  continue;
75553
75690
  }
75554
75691
  if (!current)
75555
75692
  continue;
75556
- const read = line.match(/\[miner\] \[agent\] → mcp__sessions__read_session .*?"id":"([^"]+)"/);
75693
+ const read = line.match(/\[learner\] \[agent\] → mcp__sessions__read_session .*?"id":"([^"]+)"/);
75557
75694
  if (read) {
75558
75695
  current.read.add(read[1]);
75559
75696
  continue;
75560
75697
  }
75561
- if (line.includes("[miner] [agent] → mcp__dosu__write_knowledge")) {
75698
+ if (line.includes("[learner] [agent] → mcp__dosu__write_knowledge")) {
75562
75699
  current.notes += 1;
75563
75700
  }
75564
75701
  }
@@ -75572,15 +75709,15 @@ function statusLine(status) {
75572
75709
  if (status.running) {
75573
75710
  const since = status.startedAt ? ` · since ${localTime(status.startedAt)}` : "";
75574
75711
  const pid = status.pid !== undefined ? ` (pid ${status.pid})` : "";
75575
- return `⛏️ ${import_picocolors30.default.bold(brand("Mining sessions..."))}${import_picocolors30.default.dim(`${pid}${since}`)}`;
75712
+ return `\uD83D\uDCDA ${import_picocolors30.default.bold(brand("Studying sessions..."))}${import_picocolors30.default.dim(`${pid}${since}`)}`;
75576
75713
  }
75577
75714
  if (status.state.paused) {
75578
- return `${import_picocolors30.default.yellow("○")} ${import_picocolors30.default.bold("Paused")} ${import_picocolors30.default.dim("· mining stays off until you resume")}`;
75715
+ return `${import_picocolors30.default.yellow("○")} ${import_picocolors30.default.bold("Paused")} ${import_picocolors30.default.dim("· studying stays off until you resume")}`;
75579
75716
  }
75580
75717
  if (status.staleLock) {
75581
75718
  return `${import_picocolors30.default.yellow("○")} ${import_picocolors30.default.bold("Not running")} ${import_picocolors30.default.dim("· a previous run exited without cleaning up")}`;
75582
75719
  }
75583
- return `${import_picocolors30.default.dim("○")} ${import_picocolors30.default.bold("Idle")} ${import_picocolors30.default.dim("· hooks mine new sessions automatically")}`;
75720
+ return `${import_picocolors30.default.dim("○")} ${import_picocolors30.default.bold("Idle")} ${import_picocolors30.default.dim("· hooks study new sessions automatically")}`;
75584
75721
  }
75585
75722
  function progressLine(done, ready, width, notes = 0) {
75586
75723
  const total = done + ready;
@@ -75592,12 +75729,12 @@ function progressLine(done, ready, width, notes = 0) {
75592
75729
  const bar = brand("█".repeat(filled)) + import_picocolors30.default.dim("░".repeat(cells - filled));
75593
75730
  const pct = Math.floor(ratio * 100);
75594
75731
  const suffix = notes > 0 ? ` · ${notes} suggested page${notes === 1 ? "" : "s"}` : "";
75595
- return `${bar} ${done}/${total} mined · ${pct}%${suffix}`;
75732
+ return `${bar} ${done}/${total} studied · ${pct}%${suffix}`;
75596
75733
  }
75597
- function tabBar(tab, queuedCount, openCount, minedCount, width) {
75734
+ function tabBar(tab, queuedCount, openCount, studiedCount, width) {
75598
75735
  return tabStrip([
75599
75736
  ["activity", "Activity"],
75600
- ["mined", `Mined (${minedCount})`],
75737
+ ["studied", `Studied (${studiedCount})`],
75601
75738
  ["queued", `Queued (${queuedCount})`],
75602
75739
  ["open", `Open (${openCount})`]
75603
75740
  ], tab, width);
@@ -75621,10 +75758,11 @@ function wrapLine(text, width, indent = " ") {
75621
75758
  return out;
75622
75759
  }
75623
75760
  function confirmBox(queuedCount, backlog, width, mode = "start") {
75624
- const inFlight = backlog && backlog.inFlight > 0 ? ` (+${backlog.inFlight} open, mined once ${backlog.inFlight === 1 ? "it goes" : "they go"} quiet)` : "";
75625
- const scope = mode === "stop" ? "the run is killed mid-batch · mining stays paused until you resume" : queuedCount > 0 ? `${queuedCount} session${queuedCount === 1 ? "" : "s"} queued${inFlight} · runs in the background` : `queue empty${inFlight} · a run would only pick up sessions that finish from here`;
75626
- const title = mode === "stop" ? "Stop mining?" : mode === "resume" ? "Resume mining?" : "Start mining now?";
75627
- const verb = mode === "stop" ? "stop" : mode === "resume" ? "resume" : "start";
75761
+ const inFlight = backlog && backlog.inFlight > 0 ? ` (+${backlog.inFlight} still open; ${backlog.inFlight === 1 ? "it joins" : "they join"} once quiet)` : "";
75762
+ const sessions = `${queuedCount} session${queuedCount === 1 ? "" : "s"}`;
75763
+ const scope = mode === "clear" ? "Forgets which local sessions Dosu has already studied, so the next run reads them all again. Use this to rebuild knowledge from scratch. Notes already saved in Dosu are kept." : mode === "stop" ? "Kills the run mid-batch and pauses studying, so new sessions pile up in the queue instead of being read. Nothing is lost; press s again to resume from where it left off." : mode === "resume" ? `Studying picks up where it stopped: ${sessions} queued${inFlight}. Runs in the background; new notes appear in Dosu as each batch finishes.` : queuedCount > 0 ? `Dosu reads ${sessions}${inFlight} and distills the durable decisions and gotchas into team knowledge. Runs in the background; new notes appear in Dosu as each batch finishes.` : `Queue is empty${inFlight}. A run now would only pick up sessions that finish from here; hooks already do that automatically.`;
75764
+ const title = mode === "clear" ? "Clear study history?" : mode === "stop" ? "Stop studying?" : mode === "resume" ? "Resume studying?" : "Start studying now?";
75765
+ const verb = mode === "clear" ? "clear" : mode === "stop" ? "stop" : mode === "resume" ? "resume" : "start";
75628
75766
  const maxInner = Math.max(20, Math.min(width, contentWidth()) - 4);
75629
75767
  const rows = [
75630
75768
  import_picocolors30.default.bold(brand(title)),
@@ -75640,28 +75778,41 @@ function confirmBox(queuedCount, backlog, width, mode = "start") {
75640
75778
  import_picocolors30.default.dim(`╰${"─".repeat(inner + 2)}╯`)
75641
75779
  ];
75642
75780
  }
75643
- function renderActivityFrame(status, activity, width, backlog = null, pane = DEFAULT_PANE, queued = [], minedBeforeRun = 0, open5 = [], runProgress = null) {
75644
- const mined = status.state.watermark ? `Mined sessions up to ${localTime(status.state.watermark)}` : "Nothing mined 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";
75645
75783
  const queueDetail = [];
75646
75784
  if (status.backoffUntil) {
75647
75785
  queueDetail.push(`retrying after ${localTime(status.backoffUntil)} · s syncs now`);
75648
75786
  }
75649
75787
  let progress = null;
75650
75788
  if (status.running && backlog) {
75651
- const minedDelta = Math.max(0, (status.state.total_mined ?? 0) - minedBeforeRun);
75789
+ const studiedDelta = Math.max(0, (status.state.total_mined ?? 0) - studiedBeforeRun);
75652
75790
  const stepDone = runProgress ? Math.min(Math.max(0, runProgress.read.size - 1), runProgress.batch - 1, backlog.ready) : 0;
75653
- 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);
75654
75792
  }
75655
75793
  const refusal = !status.running && status.state.last_refusal;
75656
- const refusalLines = refusal ? wrapLine(`! Mining paused: ${refusal.message} (${localTime(refusal.at)})`, width).map((line) => import_picocolors30.default.yellow(line)) : [];
75794
+ const refusalLines = refusal ? wrapLine(`! Studying paused: ${refusal.message} (${localTime(refusal.at)})`, width).map((line) => import_picocolors30.default.yellow(line)) : [];
75657
75795
  const fullRows = Boolean(pane.fullRows);
75658
- const minedRows = (status.state.mined_sessions ?? []).map((record2) => fullRows ? formatMinedRow(record2, true) : formatActivityLine(formatMinedRow(record2), width));
75659
- 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
+ };
75660
75811
  const queuedRows = queued.map(sessionRow);
75661
75812
  const openRows = open5.map(sessionRow);
75662
- const source = pane.tab === "activity" ? activity.map((line) => formatActivityLine(line, width, fullRows)) : pane.tab === "queued" ? queuedRows : pane.tab === "open" ? openRows : minedRows;
75663
- const emptyMined = status.state.watermark ? "No sessions recorded yet. History starts with the next mining run." : "No mined sessions yet.";
75664
- 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;
75665
75816
  const height = fullRows ? ACTIVITY_VIEW_FULL_LIST_ROWS : ACTIVITY_VIEW_LIST_LINES;
75666
75817
  const { visible, above, below } = windowList(source, pane.scroll, height);
75667
75818
  const display = fullRows ? visible.flatMap((row) => wrapRow(row, width)) : visible;
@@ -75675,31 +75826,95 @@ function renderActivityFrame(status, activity, width, backlog = null, pane = DEF
75675
75826
  breadcrumb(["Home", "Activity"], width),
75676
75827
  "",
75677
75828
  statusLine(status),
75678
- import_picocolors30.default.dim(mined),
75829
+ import_picocolors30.default.dim(studied),
75679
75830
  ...queueDetail.length > 0 ? wrapLine(queueDetail.join(" · "), width).map((line) => import_picocolors30.default.dim(line)) : [],
75680
75831
  ...progress ? [progress] : [],
75681
75832
  ...refusalLines,
75682
75833
  "",
75683
- ...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),
75684
75835
  ...listRows,
75685
75836
  ...scrollParts.length > 0 ? [import_picocolors30.default.dim(clip(scrollParts.join(" · "), width))] : [],
75686
75837
  "",
75687
75838
  ...pane.confirm ? centerBlock(confirmBox(queued.length, backlog, width, pane.confirm), width) : [
75688
- import_picocolors30.default.dim([
75839
+ ...wrapLine([
75689
75840
  "tab switch",
75690
75841
  "↑↓ scroll",
75691
75842
  fullRows ? "f clip" : "f full rows",
75692
75843
  status.running ? "s stop" : status.state.paused ? "s resume" : "s sync now",
75844
+ ...!status.running && status.state.watermark ? ["c clear"] : [],
75693
75845
  "esc back"
75694
- ].join(" · "))
75846
+ ].join(" · "), width, "").map((line) => import_picocolors30.default.dim(line))
75695
75847
  ]
75696
75848
  ];
75697
75849
  return lines.join(`
75698
75850
  `);
75699
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
+ }
75700
75915
  function defaultReadLog2() {
75701
75916
  try {
75702
- return readFileSync24(logger.getLogPath(), "utf-8");
75917
+ return readFileSync25(logger.getLogPath(), "utf-8");
75703
75918
  } catch {
75704
75919
  return "";
75705
75920
  }
@@ -75715,7 +75930,9 @@ function runActivityView(io2 = {}) {
75715
75930
  const startSync = io2.startSync ?? (() => spawnDetachedSelf(["knowledge", "sync", "--bootstrap"]));
75716
75931
  const stopSync = io2.stopSync ?? stopSyncRun;
75717
75932
  const setPaused = io2.setPaused ?? setSyncPaused;
75933
+ const clearHistory = io2.clearHistory ?? resetSyncState;
75718
75934
  const listBacklog = io2.listBacklog ?? listSessionBacklog;
75935
+ const rowNamesFor = io2.rowNames ?? createRowNamer();
75719
75936
  const pollMs = io2.pollMs ?? ACTIVITY_VIEW_POLL_MS;
75720
75937
  const seed = readLog();
75721
75938
  let activity = appendSyncActivity([], seed);
@@ -75744,29 +75961,30 @@ function runActivityView(io2 = {}) {
75744
75961
  };
75745
75962
  const listHeight = () => fullRows ? ACTIVITY_VIEW_FULL_LIST_ROWS : ACTIVITY_VIEW_LIST_LINES;
75746
75963
  let lastFrame = null;
75747
- let minedBeforeRun = null;
75964
+ let studiedBeforeRun = null;
75748
75965
  const draw = () => {
75749
75966
  status = getStatus();
75750
75967
  if (status.running) {
75751
75968
  const run2 = status.state.run;
75752
75969
  if (run2 && run2.pid === status.pid) {
75753
- minedBeforeRun = run2.baseline_mined;
75970
+ studiedBeforeRun = run2.baseline_mined;
75754
75971
  } else {
75755
- minedBeforeRun ??= status.state.total_mined ?? 0;
75972
+ studiedBeforeRun ??= status.state.total_mined ?? 0;
75756
75973
  }
75757
75974
  } else {
75758
- minedBeforeRun = null;
75975
+ studiedBeforeRun = null;
75759
75976
  }
75760
75977
  if (status.state.watermark !== queuedWatermark) {
75761
75978
  queuedWatermark = status.state.watermark;
75762
75979
  sessions = listBacklog();
75763
75980
  }
75764
- if ((confirmSync === "start" || confirmSync === "resume") && status.running)
75981
+ if (confirmSync !== null && confirmSync !== "stop" && status.running)
75765
75982
  confirmSync = null;
75766
75983
  if (confirmSync === "stop" && !status.running)
75767
75984
  confirmSync = null;
75768
75985
  const width = activityWidth(output2.columns ?? 80);
75769
- 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);
75770
75988
  if (frame === lastFrame)
75771
75989
  return;
75772
75990
  lastFrame = frame;
@@ -75813,11 +76031,15 @@ ${CLEAR_BELOW}`);
75813
76031
  const mode = confirmSync;
75814
76032
  confirmSync = null;
75815
76033
  let note;
75816
- if (mode === "stop") {
76034
+ if (mode === "clear") {
76035
+ clearHistory();
76036
+ studiedBeforeRun = null;
76037
+ note = "[sync] study history cleared · the next run reads every local session again";
76038
+ } else if (mode === "stop") {
75817
76039
  const ok2 = status.pid !== undefined && stopSync(status.pid);
75818
76040
  if (ok2)
75819
76041
  setPaused(true);
75820
- note = ok2 ? "[sync] mining stopped · paused until you resume" : "[sync] could not stop the run · it may have just finished";
76042
+ note = ok2 ? "[sync] studying stopped · paused until you resume" : "[sync] could not stop the run · it may have just finished";
75821
76043
  } else {
75822
76044
  if (mode === "resume")
75823
76045
  setPaused(false);
@@ -75841,6 +76063,11 @@ ${CLEAR_BELOW}`);
75841
76063
  if (action === "sync") {
75842
76064
  confirmSync = syncConfirmMode(status);
75843
76065
  draw();
76066
+ } else if (action === "clear") {
76067
+ if (!status.running && status.state.watermark) {
76068
+ confirmSync = "clear";
76069
+ draw();
76070
+ }
75844
76071
  } else if (action === "tab" || action === "tab-back") {
75845
76072
  tab = cycleTab(tab, action === "tab" ? 1 : -1);
75846
76073
  scroll = 0;
@@ -75868,10 +76095,12 @@ ${CLEAR_BELOW}`);
75868
76095
  input2.on("data", onData);
75869
76096
  });
75870
76097
  }
75871
- 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;
75872
76099
  var init_activity_view = __esm(() => {
75873
76100
  init_follow();
75874
76101
  init_logger();
76102
+ init_project_dir();
76103
+ init_session_title();
75875
76104
  init_styles();
75876
76105
  init_backlog();
75877
76106
  init_detach();
@@ -75893,7 +76122,7 @@ var init_activity_view = __esm(() => {
75893
76122
  CURSOR_HOME = `${ESC7}[H`;
75894
76123
  CLEAR_BELOW = `${ESC7}[0J`;
75895
76124
  CLEAR_EOL = `${ESC7}[K`;
75896
- ACTIVITY_VIEW_TABS = ["activity", "mined", "queued", "open"];
76125
+ ACTIVITY_VIEW_TABS = ["activity", "studied", "queued", "open"];
75897
76126
  DEFAULT_PANE = { tab: "activity", scroll: 0 };
75898
76127
  });
75899
76128
 
@@ -76726,23 +76955,31 @@ async function stepOfferInitialSync(cfg) {
76726
76955
  s.start("Checking for recent agent sessions...");
76727
76956
  const outcome = await runKnowledgeSync({ bootstrap: true });
76728
76957
  if (outcome.status !== "backlog" || outcome.readySessions === 0) {
76729
- s.stop("No unmined agent sessions found. Dosu will learn as you work.");
76958
+ s.stop("No past agent sessions to study. Dosu will learn from new ones as you work.");
76730
76959
  return;
76731
76960
  }
76732
76961
  const n5 = outcome.readySessions;
76733
- s.stop(`Found ${n5} unmined agent session${n5 === 1 ? "" : "s"} on this machine.`);
76962
+ const them = n5 === 1 ? "it" : "them";
76963
+ s.stop(`Found ${n5} past agent session${n5 === 1 ? "" : "s"} on this machine that Dosu hasn't studied.`);
76964
+ log2.message([
76965
+ `Dosu can read ${them} and pull out the durable knowledge: decisions, gotchas, and`,
76966
+ "how-things-work that your team and agents would otherwise rediscover.",
76967
+ "Only distilled notes are saved to Dosu; your session logs stay on this machine.",
76968
+ "It runs in the background a few sessions at a time; notes appear in Dosu as each batch finishes."
76969
+ ].join(`
76970
+ `));
76734
76971
  const mineNow = await confirm2({
76735
- message: `Mine ${n5 === 1 ? "it" : "them"} for team knowledge now? (runs in the background)`,
76736
- active: "Mine now ⛏️",
76737
- inactive: "Skip",
76972
+ message: `Study ${them} now?`,
76973
+ active: "Study now \uD83D\uDCDA",
76974
+ inactive: "Skip for now",
76738
76975
  initialValue: true
76739
76976
  });
76740
76977
  if (isCancel2(mineNow) || !mineNow) {
76741
- log2.info(`Skipped. Dosu picks sessions up in the background as you work, or run ${info("dosu knowledge sync")} anytime.`);
76978
+ log2.info(`Skipped. Dosu studies new sessions in the background as you work; run ${info("dosu knowledge sync")} anytime to study these too.`);
76742
76979
  return;
76743
76980
  }
76744
76981
  if (spawnDetachedSelf(["knowledge", "sync", "--quiet", "--bootstrap"])) {
76745
- log2.success("⛏️ Currently mining... Dosu is distilling your past sessions into team knowledge, a few at a time in the background.");
76982
+ log2.success(`\uD83D\uDCDA Studying ${n5} session${n5 === 1 ? "" : "s"} in the background. Watch progress on the Activity screen; when it finishes, the new notes are in Dosu and agents connected to this MCP can use them.`);
76746
76983
  const watch = await confirm2({
76747
76984
  message: "What next?",
76748
76985
  active: "Watch it work",
@@ -77567,12 +77804,15 @@ function loadPageStatsFromConfig() {
77567
77804
  }
77568
77805
  }
77569
77806
  function overviewRows(state) {
77570
- const minedTotal = state.total_mined ?? 0;
77807
+ const studiedTotal = state.total_mined ?? 0;
77571
77808
  const notes = state.total_notes ?? 0;
77572
77809
  const tokens = state.total_learning_tokens ?? 0;
77573
- if (minedTotal === 0 && notes === 0)
77810
+ if (studiedTotal === 0 && notes === 0)
77574
77811
  return [];
77575
- const rows = [`${label("Sessions mined")}${minedTotal}`, `${label("Suggested pages")}${notes}`];
77812
+ const rows = [
77813
+ `${label("Sessions studied")}${studiedTotal}`,
77814
+ `${label("Suggested pages")}${notes}`
77815
+ ];
77576
77816
  if (tokens > 0) {
77577
77817
  rows.push(`${label("Investigation distilled")}${formatTokenCount(tokens)} tokens`);
77578
77818
  }
@@ -77621,7 +77861,7 @@ function analyticsTabRows(tab, state, pageStats) {
77621
77861
  function renderAnalyticsFrame(state, tab, scroll, pageStats = null, pagesPending = false, width = contentWidth()) {
77622
77862
  const rows = analyticsTabRows(tab, state, pageStats);
77623
77863
  const { visible, above, below } = windowReport(rows, scroll);
77624
- const empty = tab === "overview" ? "No analytics yet. They appear after the first mining run." : tab === "projects" ? "No per-project history yet. It fills in as sessions are mined." : pagesPending ? "Loading page analytics..." : "No page analytics yet.";
77864
+ const empty = tab === "overview" ? "No analytics yet. They appear after the first study run." : tab === "projects" ? "No per-project history yet. It fills in as sessions are studied." : pagesPending ? "Loading page analytics..." : "No page analytics yet.";
77625
77865
  const listRows = visible.length > 0 ? visible.map((row) => import_picocolors32.default.dim(row)) : [import_picocolors32.default.dim(empty)];
77626
77866
  const scrollParts = [];
77627
77867
  if (above > 0)
@@ -77797,10 +78037,10 @@ function checklistRows(ctx) {
77797
78037
  rows.push(["agents", `${on2} ${ctx.agents.join(` ${DOT3} `)}`]);
77798
78038
  if (missing.has("hooks"))
77799
78039
  rows.push(["hooks", warnRow]);
77800
- if (ctx.mining) {
78040
+ if (ctx.studying) {
77801
78041
  rows.push([
77802
78042
  "sync",
77803
- `⛏️ ${brand("mining sessions...")} ${import_picocolors33.default.dim(`${DOT3} see Activity`)}`
78043
+ `\uD83D\uDCDA ${brand("studying sessions...")} ${import_picocolors33.default.dim(`${DOT3} see Activity`)}`
77804
78044
  ]);
77805
78045
  }
77806
78046
  if (ctx.update) {
@@ -78190,8 +78430,8 @@ __export(exports_tui, {
78190
78430
  runTUI: () => runTUI,
78191
78431
  handleLogout: () => handleLogout
78192
78432
  });
78193
- import { homedir as homedir10 } from "node:os";
78194
- import { basename as basename4 } from "node:path";
78433
+ import { homedir as homedir11 } from "node:os";
78434
+ import { basename as basename6 } from "node:path";
78195
78435
  async function probeSession(cfg) {
78196
78436
  if (!isAuthenticated(cfg) || !isTokenExpired(cfg))
78197
78437
  return { expired: false };
@@ -78218,7 +78458,7 @@ function bannerContext(cfg, session) {
78218
78458
  return {
78219
78459
  version: getVersionString(),
78220
78460
  webAppHost,
78221
- directory: basename4(process.cwd()),
78461
+ directory: basename6(process.cwd()),
78222
78462
  signedIn: isAuthenticated(cfg),
78223
78463
  sessionExpired: session.expired,
78224
78464
  deploymentName: cfg.active_account?.target?.deployment_name,
@@ -78226,7 +78466,7 @@ function bannerContext(cfg, session) {
78226
78466
  setupMissing: isAuthenticated(cfg) && !session.expired ? missingSetupSteps(cfg) : [],
78227
78467
  ...inGitWorkTree() ? { repoAgentsMd: dosuAgentsSectionState() } : {},
78228
78468
  agents,
78229
- mining: isMining(),
78469
+ studying: isStudying(),
78230
78470
  ...latest ? { update: { version: latest, hint: buildUpdateHint(INSTALL_CHANNEL, isNpxInvocation()) } } : {}
78231
78471
  };
78232
78472
  }
@@ -78293,7 +78533,7 @@ function setupHint(cfg) {
78293
78533
  const started = Boolean(cfg.active_account?.target);
78294
78534
  return import_picocolors35.default.yellow(started ? `incomplete · missing ${missingSetupSteps(cfg).join(" + ")}` : "not set up yet");
78295
78535
  }
78296
- function isMining() {
78536
+ function isStudying() {
78297
78537
  try {
78298
78538
  return getSyncStatus({ readLog: () => "" }).running;
78299
78539
  } catch {
@@ -78341,10 +78581,10 @@ async function runMainMenu() {
78341
78581
  { label: "Exit", value: "exit" }
78342
78582
  ];
78343
78583
  }
78344
- const mining = isMining();
78584
+ const studying = isStudying();
78345
78585
  return [
78346
78586
  {
78347
- label: mining ? `Activity ⛏️ ${brand("mining sessions...")}` : "Activity",
78587
+ label: studying ? `Activity \uD83D\uDCDA ${brand("studying sessions...")}` : "Activity",
78348
78588
  value: "sync"
78349
78589
  },
78350
78590
  { label: "Knowledge report", hint: "(opens in browser)", value: "report" },
@@ -78420,11 +78660,11 @@ async function runSettings(cfg) {
78420
78660
  const target = cfg.active_account?.target;
78421
78661
  const library = target?.library_name ?? target?.deployment_name ?? "not configured";
78422
78662
  const filter = loadSyncState().project_filter;
78423
- 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`;
78424
78664
  const action = await menuSelect("Settings", [
78425
78665
  { label: "Switch organization", hint: target?.org_name, value: "switch-org" },
78426
78666
  { label: "Switch Library", hint: library, value: "switch-library" },
78427
- { label: "Mining scope", hint: scope, value: "projects" },
78667
+ { label: "Study scope", hint: scope, value: "projects" },
78428
78668
  { label: "Run setup", hint: "rerun the setup wizard", value: "setup" },
78429
78669
  { label: "Log out", hint: "clear stored credentials", value: "logout" },
78430
78670
  { label: "Back", value: "back" }
@@ -78432,7 +78672,7 @@ async function runSettings(cfg) {
78432
78672
  if (action === null || action === "back")
78433
78673
  return;
78434
78674
  if (action === "projects") {
78435
- await runMiningProjectsSetting();
78675
+ await runStudyingProjectsSetting();
78436
78676
  continue;
78437
78677
  }
78438
78678
  if (action === "setup") {
@@ -78455,7 +78695,7 @@ async function runSettings(cfg) {
78455
78695
  }
78456
78696
  }
78457
78697
  function displayDir(dir) {
78458
- const home = homedir10();
78698
+ const home = homedir11();
78459
78699
  return dir === home ? "~" : dir.startsWith(`${home}/`) ? `~${dir.slice(home.length)}` : dir;
78460
78700
  }
78461
78701
  function discoverProjectDirs() {
@@ -78470,7 +78710,7 @@ function discoverProjectDirs() {
78470
78710
  } catch {}
78471
78711
  return [...counts.entries()].sort((a9, b2) => b2[1] - a9[1]).map(([dir]) => dir);
78472
78712
  }
78473
- async function runMiningProjectsSetting() {
78713
+ async function runStudyingProjectsSetting() {
78474
78714
  const dirs = discoverProjectDirs();
78475
78715
  if (dirs.length === 0) {
78476
78716
  log2.info("No local agent sessions found yet; nothing to scope.");
@@ -78478,7 +78718,7 @@ async function runMiningProjectsSetting() {
78478
78718
  }
78479
78719
  const current = loadSyncState().project_filter;
78480
78720
  const selected = await multiselect2({
78481
- message: "Mine sessions from which folders?",
78721
+ message: "Study sessions from which folders?",
78482
78722
  options: dirs.map((dir) => ({
78483
78723
  label: dir === UNKNOWN_PROJECT ? "(unknown folder)" : displayDir(dir),
78484
78724
  value: dir
@@ -78493,7 +78733,7 @@ async function runMiningProjectsSetting() {
78493
78733
  const all = selected.length === dirs.length;
78494
78734
  saveSyncState(all ? state : { ...state, project_filter: [...selected] });
78495
78735
  const scope = all ? "all folders" : selected.map(displayDir).join(", ");
78496
- log2.success(`Mining scope ${dim(`· ${scope}`)}`);
78736
+ log2.success(`Study scope ${dim(`· ${scope}`)}`);
78497
78737
  }
78498
78738
  async function handleAuthenticate(cfg) {
78499
78739
  const shouldLogin = await confirm2({ message: "Open browser to log in?" });
@@ -79197,7 +79437,7 @@ import {
79197
79437
  constants as constants3,
79198
79438
  fstatSync,
79199
79439
  openSync as openSync5,
79200
- readFileSync as readFileSync25,
79440
+ readFileSync as readFileSync26,
79201
79441
  readSync as readSync4,
79202
79442
  unlinkSync as unlinkSync5
79203
79443
  } from "node:fs";
@@ -82399,23 +82639,23 @@ ${results.length - opts.limit} more results not shown.`));
82399
82639
  console.log(` ID: ${store.id}`);
82400
82640
  console.log(` Space ID: ${store.space_id}`);
82401
82641
  });
82402
- 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 mining").option("--open", "Only live sessions still inside the quiet period").option("--mined", "Only recent mined-session history").option("--json", "Output as JSON").action((opts) => {
82403
- 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;
82404
82644
  const wantQueued = all || Boolean(opts.queued);
82405
82645
  const wantOpen = all || Boolean(opts.open);
82406
- const wantMined = all || Boolean(opts.mined);
82646
+ const wantStudied = all || Boolean(opts.studied);
82407
82647
  const backlog = wantQueued || wantOpen ? listSessionBacklog() : { queued: [], open: [] };
82408
- const mined = wantMined ? loadSyncState().mined_sessions ?? [] : [];
82648
+ const studied = wantStudied ? loadSyncState().mined_sessions ?? [] : [];
82409
82649
  if (opts.json) {
82410
82650
  printResult({
82411
82651
  ...wantQueued ? { queued: backlog.queued } : {},
82412
82652
  ...wantOpen ? { open: backlog.open } : {},
82413
- ...wantMined ? { mined } : {}
82653
+ ...wantStudied ? { studied } : {}
82414
82654
  }, opts);
82415
82655
  return;
82416
82656
  }
82417
82657
  const sessionRows = (sessions) => sessions.map((s) => [s.harness, s.updated, s.project ?? "-", s.id]);
82418
- const minedRows = mined.map((record2) => {
82658
+ const studiedRows = studied.map((record2) => {
82419
82659
  const slash = record2.session.indexOf("/");
82420
82660
  const harness = slash > 0 ? record2.session.slice(0, slash) : "-";
82421
82661
  const id2 = slash > 0 ? record2.session.slice(slash + 1) : record2.session;
@@ -82439,11 +82679,11 @@ ${results.length - opts.limit} more results not shown.`));
82439
82679
  if (wantOpen) {
82440
82680
  section("Open", sessionRows(backlog.open), "Updated", "No open sessions. Live agent sessions sit here until they go quiet.");
82441
82681
  }
82442
- if (wantMined) {
82443
- section("Mined", minedRows, "Mined at", "No mined sessions recorded yet.");
82682
+ if (wantStudied) {
82683
+ section("Studied", studiedRows, "Studied at", "No studied sessions recorded yet.");
82444
82684
  }
82445
82685
  });
82446
- cmd.command("sync").description("Scan local agent session history and report the mining 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: mine 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) => {
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) => {
82447
82687
  if (opts.status) {
82448
82688
  const status = getSyncStatus();
82449
82689
  if (opts.json) {
@@ -82464,7 +82704,7 @@ ${results.length - opts.limit} more results not shown.`));
82464
82704
  ]);
82465
82705
  return;
82466
82706
  }
82467
- const deps = { mine: buildMiner(opts.quiet ? "hook" : "manual") };
82707
+ const deps = { mine: buildLearner(opts.quiet ? "hook" : "manual") };
82468
82708
  let outcome = await runKnowledgeSync({
82469
82709
  quiet: opts.quiet,
82470
82710
  bootstrap: opts.bootstrap,
@@ -82472,7 +82712,7 @@ ${results.length - opts.limit} more results not shown.`));
82472
82712
  });
82473
82713
  if (opts.bootstrap && deps.mine) {
82474
82714
  const maxRounds = Math.ceil(outcome.readySessions / MINE_BATCH_LIMIT) + 2;
82475
- for (let round = 1;outcome.status === "mined" && round < maxRounds; round++) {
82715
+ for (let round = 1;outcome.status === "studied" && round < maxRounds; round++) {
82476
82716
  if (!opts.quiet && !opts.json)
82477
82717
  printSyncOutcome(outcome);
82478
82718
  outcome = await runKnowledgeSync({ quiet: opts.quiet, bootstrap: true, deps });
@@ -82529,12 +82769,12 @@ ${results.length - opts.limit} more results not shown.`));
82529
82769
  console.log("All your notes already have a transcript — nothing to backfill.");
82530
82770
  return;
82531
82771
  }
82532
- console.log(`Attributed ${result.updated} of ${result.candidates} notes (${result.ambiguous} ambiguous, ${result.noBatch} without a local mining batch). Run 'dosu knowledge report' to see their traces.`);
82772
+ console.log(`Attributed ${result.updated} of ${result.candidates} notes (${result.ambiguous} ambiguous, ${result.noBatch} without a local study batch). Run 'dosu knowledge report' to see their traces.`);
82533
82773
  });
82534
82774
  cmd.addCommand(hooksCommand());
82535
82775
  return cmd;
82536
82776
  }
82537
- function buildMiner(trigger) {
82777
+ function buildLearner(trigger) {
82538
82778
  const cfg = loadConfig();
82539
82779
  if (cfg.mode === "oss")
82540
82780
  return;
@@ -82543,8 +82783,8 @@ function buildMiner(trigger) {
82543
82783
  return;
82544
82784
  const { api_key, deployment_id } = target;
82545
82785
  return async (sessions) => {
82546
- const { runMiner: runMiner2 } = await Promise.resolve().then(() => (init_runner(), exports_runner));
82547
- 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 });
82548
82788
  };
82549
82789
  }
82550
82790
  function formatAge(iso, now) {
@@ -82570,14 +82810,14 @@ function printSyncStatus(status, now = new Date) {
82570
82810
  console.log("○ No sync running.");
82571
82811
  }
82572
82812
  if (status.state.paused) {
82573
- console.log(import_picocolors18.default.yellow(" Mining paused: stopped by you. Resume from the Activity screen or run 'dosu knowledge sync'."));
82813
+ console.log(import_picocolors18.default.yellow(" Studying paused: stopped by you. Resume from the Activity screen or run 'dosu knowledge sync'."));
82574
82814
  }
82575
82815
  const wm2 = status.state.watermark;
82576
- console.log(` Mined through: ${wm2 ? `${wm2} (${formatAge(wm2, now)})` : "nothing mined yet"}`);
82816
+ console.log(` Studied through: ${wm2 ? `${wm2} (${formatAge(wm2, now)})` : "nothing studied yet"}`);
82577
82817
  if (status.state.project_filter?.length) {
82578
82818
  const home = homedir5();
82579
82819
  const scope = status.state.project_filter.map((dir) => dir.startsWith(`${home}/`) ? `~${dir.slice(home.length)}` : dir).join(", ");
82580
- console.log(` Mining scope: ${scope}`);
82820
+ console.log(` Study scope: ${scope}`);
82581
82821
  }
82582
82822
  if ((status.state.total_notes ?? 0) > 0) {
82583
82823
  const tokens = status.state.total_learning_tokens ?? 0;
@@ -82592,7 +82832,7 @@ function printSyncStatus(status, now = new Date) {
82592
82832
  console.log(import_picocolors18.default.yellow(` Backing off after ${n5} failure${n5 === 1 ? "" : "s"}; background syncs retry after ${status.backoffUntil}.`));
82593
82833
  }
82594
82834
  if (status.state.last_refusal) {
82595
- console.log(import_picocolors18.default.yellow(` Mining paused: ${status.state.last_refusal.message} (${formatAge(status.state.last_refusal.at, now)})`));
82835
+ console.log(import_picocolors18.default.yellow(` Studying paused: ${status.state.last_refusal.message} (${formatAge(status.state.last_refusal.at, now)})`));
82596
82836
  }
82597
82837
  if (status.recentActivity.length > 0) {
82598
82838
  console.log(`
@@ -82608,25 +82848,25 @@ function printSyncOutcome(outcome) {
82608
82848
  switch (outcome.status) {
82609
82849
  case "backlog": {
82610
82850
  const inFlight = outcome.inFlightSessions > 0 ? import_picocolors18.default.dim(` (${outcome.inFlightSessions} more still in progress)`) : "";
82611
- console.log(`✓ Scanned. ${outcome.readySessions} new session${outcome.readySessions === 1 ? "" : "s"} ready to mine${inFlight}.`);
82612
- console.log(import_picocolors18.default.dim("Sign in with 'dosu setup' to enable mining."));
82851
+ console.log(`✓ Scanned. ${outcome.readySessions} new session${outcome.readySessions === 1 ? "" : "s"} ready to study${inFlight}.`);
82852
+ console.log(import_picocolors18.default.dim("Sign in with 'dosu setup' to enable studying."));
82613
82853
  break;
82614
82854
  }
82615
- case "mined": {
82616
- const notes = outcome.miner?.notesWritten ?? 0;
82617
- const remaining = outcome.readySessions - (outcome.minedSessions ?? 0);
82618
- console.log(`✓ Mined ${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.`);
82619
82859
  if (remaining > 0) {
82620
82860
  console.log(import_picocolors18.default.dim(`${remaining} more in the backlog; run sync again to continue.`));
82621
82861
  }
82622
82862
  break;
82623
82863
  }
82624
82864
  case "skipped-gateway": {
82625
- console.log(import_picocolors18.default.yellow(outcome.miner?.message ?? "Mining unavailable right now."));
82865
+ console.log(import_picocolors18.default.yellow(outcome.learner?.message ?? "Studying unavailable right now."));
82626
82866
  break;
82627
82867
  }
82628
82868
  case "mine-failed": {
82629
- console.error(import_picocolors18.default.red(outcome.miner?.message ?? "Mining run failed."));
82869
+ console.error(import_picocolors18.default.red(outcome.learner?.message ?? "Study run failed."));
82630
82870
  process.exitCode = 1;
82631
82871
  break;
82632
82872
  }
@@ -82649,7 +82889,7 @@ function printSyncOutcome(outcome) {
82649
82889
  break;
82650
82890
  }
82651
82891
  case "skipped-paused": {
82652
- console.log(import_picocolors18.default.dim("Skipped: mining is paused. Run 'dosu knowledge sync' to resume."));
82892
+ console.log(import_picocolors18.default.dim("Skipped: studying is paused. Run 'dosu knowledge sync' to resume."));
82653
82893
  break;
82654
82894
  }
82655
82895
  }
@@ -84567,7 +84807,7 @@ Use 'dosu mcp add <agent>' to add Dosu MCP to a tool.`);
84567
84807
  }
84568
84808
  if (opts.follow) {
84569
84809
  try {
84570
- const lines = readFileSync25(logPath, "utf-8").split(`
84810
+ const lines = readFileSync26(logPath, "utf-8").split(`
84571
84811
  `);
84572
84812
  if (lines.at(-1) === "")
84573
84813
  lines.pop();
@@ -84583,7 +84823,7 @@ Use 'dosu mcp add <agent>' to add Dosu MCP to a tool.`);
84583
84823
  if (opts.tail !== undefined) {
84584
84824
  const n5 = typeof opts.tail === "string" ? parseInt(opts.tail, 10) || 50 : 50;
84585
84825
  try {
84586
- const content = readFileSync25(logPath, "utf-8");
84826
+ const content = readFileSync26(logPath, "utf-8");
84587
84827
  const lines = content.split(`
84588
84828
  `);
84589
84829
  console.log(lines.slice(-n5).join(`
@@ -84654,4 +84894,4 @@ execute().catch((err) => {
84654
84894
  process.exit(1);
84655
84895
  });
84656
84896
 
84657
- //# debugId=acac034b-488b-746c-6475-6e2164756e21
84897
+ //# debugId=a3c20325-c0f7-811f-6475-6e2164756e21