@dosu/cli 0.55.1 → 0.56.1

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 +271 -69
  2. package/package.json +1 -1
package/bin/dosu.js CHANGED
@@ -1050,7 +1050,7 @@ function isNpxInvocation(channel = INSTALL_CHANNEL, env = process.env) {
1050
1050
  function getVersionString() {
1051
1051
  return `v${VERSION}`;
1052
1052
  }
1053
- var VERSION = "0.55.1", INSTALL_CHANNEL = "npm";
1053
+ var VERSION = "0.56.1", INSTALL_CHANNEL = "npm";
1054
1054
 
1055
1055
  // src/debug/logger.ts
1056
1056
  import {
@@ -1902,6 +1902,7 @@ function loadSyncState(configDir = getConfigDir()) {
1902
1902
  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) => ({
1903
1903
  at: record.at,
1904
1904
  session: record.session,
1905
+ ...typeof record.updated === "string" ? { updated: record.updated } : {},
1905
1906
  ...typeof record.project === "string" ? { project: record.project } : {}
1906
1907
  })) : [];
1907
1908
  const rawRefusal = raw.last_refusal;
@@ -13429,6 +13430,27 @@ function batchWatermark(batch) {
13429
13430
  }
13430
13431
  return newest;
13431
13432
  }
13433
+ function sessionKey(session) {
13434
+ return `${session.harness}/${session.id}`;
13435
+ }
13436
+ function studiedRecords(sessions, at) {
13437
+ return sessions.map((s) => ({
13438
+ at,
13439
+ session: sessionKey(s),
13440
+ updated: s.updated,
13441
+ ...s.project ? { project: s.project } : {}
13442
+ }));
13443
+ }
13444
+ function lastStudiedSnapshot(history) {
13445
+ const latest = new Map;
13446
+ for (const record of history ?? []) {
13447
+ const updated = record.updated ? Date.parse(record.updated) : Number.NaN;
13448
+ if (!Number.isNaN(updated) && updated > (latest.get(record.session) ?? Number.NEGATIVE_INFINITY)) {
13449
+ latest.set(record.session, updated);
13450
+ }
13451
+ }
13452
+ return latest;
13453
+ }
13432
13454
  async function runKnowledgeSync(options = {}) {
13433
13455
  const deps = options.deps ?? {};
13434
13456
  const loadState = deps.loadState ?? loadSyncState;
@@ -13511,14 +13533,20 @@ async function runKnowledgeSync(options = {}) {
13511
13533
  }
13512
13534
  const worthStudying = deps.worthStudying ?? isWorthStudying;
13513
13535
  const isIncognito = deps.isIncognito ?? isIncognitoSession;
13536
+ const studiedSnapshot = lastStudiedSnapshot(state.mined_sessions);
13514
13537
  const examined = [];
13515
13538
  const batch = [];
13516
13539
  let trivial = 0;
13517
13540
  let incognito = 0;
13541
+ let alreadyStudied = 0;
13518
13542
  for (let i = ready.length - 1;i >= 0 && batch.length < MINE_BATCH_LIMIT; i--) {
13519
13543
  const candidate = ready[i];
13520
13544
  examined.push(candidate);
13521
- if (isIncognito(candidate)) {
13545
+ const key = sessionKey(candidate);
13546
+ if (Date.parse(candidate.updated) <= (studiedSnapshot.get(key) ?? Number.NEGATIVE_INFINITY)) {
13547
+ alreadyStudied += 1;
13548
+ logger.debug("sync", `skipping already-studied session ${key}`);
13549
+ } else if (isIncognito(candidate)) {
13522
13550
  incognito += 1;
13523
13551
  logger.debug("sync", `skipping incognito session ${candidate.harness}/${candidate.id}`);
13524
13552
  } else if (worthStudying(candidate)) {
@@ -13527,7 +13555,7 @@ async function runKnowledgeSync(options = {}) {
13527
13555
  trivial += 1;
13528
13556
  }
13529
13557
  }
13530
- const skippedNote = `${trivial} trivial, ${incognito} incognito skipped`;
13558
+ const skippedNote = `${trivial} trivial, ${incognito} incognito${alreadyStudied > 0 ? `, ${alreadyStudied} already studied` : ""} skipped`;
13531
13559
  if (batch.length === 0) {
13532
13560
  saveState({
13533
13561
  ...state,
@@ -13546,29 +13574,41 @@ async function runKnowledgeSync(options = {}) {
13546
13574
  }
13547
13575
  logger.debug("sync", `studying ${batch.length} of ${ready.length} ready sessions (${skippedNote})`);
13548
13576
  const learner = await deps.mine(batch);
13577
+ const sessionTokens = deps.sessionTokens ?? estimateSessionTokens;
13578
+ const noted = new Set(learner.notedSessions ?? []);
13579
+ const notedBatch = batch.filter((s) => noted.has(sessionKey(s)));
13580
+ const partialProgress = (at) => {
13581
+ if (notedBatch.length === 0)
13582
+ return {};
13583
+ let tokens = 0;
13584
+ for (const s of notedBatch)
13585
+ tokens += sessionTokens(s);
13586
+ logger.debug("sync", `recorded ${notedBatch.length} noted sessions from the ${learner.outcome} run`);
13587
+ return {
13588
+ mined_sessions: [...state.mined_sessions ?? [], ...studiedRecords(notedBatch, at)].slice(-STUDIED_HISTORY_LIMIT),
13589
+ total_mined: (state.total_mined ?? 0) + notedBatch.length,
13590
+ total_notes: (state.total_notes ?? 0) + learner.notesWritten,
13591
+ total_learning_tokens: (state.total_learning_tokens ?? 0) + tokens
13592
+ };
13593
+ };
13549
13594
  switch (learner.outcome) {
13550
13595
  case "completed": {
13551
13596
  for (const s of batch) {
13552
13597
  logger.debug("sync", `studied session ${s.harness}/${s.id}`);
13553
13598
  }
13554
- const studiedAt = now().toISOString();
13599
+ const completedAt = now().toISOString();
13555
13600
  const history = [
13556
13601
  ...state.mined_sessions ?? [],
13557
- ...batch.map((s) => ({
13558
- at: studiedAt,
13559
- session: `${s.harness}/${s.id}`,
13560
- ...s.project ? { project: s.project } : {}
13561
- }))
13602
+ ...studiedRecords(batch, completedAt)
13562
13603
  ].slice(-STUDIED_HISTORY_LIMIT);
13563
13604
  const watermark = batchWatermark(examined);
13564
- const sessionTokens = deps.sessionTokens ?? estimateSessionTokens;
13565
13605
  let batchTokens = 0;
13566
13606
  for (const s of batch)
13567
13607
  batchTokens += sessionTokens(s);
13568
13608
  saveState({
13569
13609
  ...state,
13570
13610
  watermark,
13571
- last_attempt_at: studiedAt,
13611
+ last_attempt_at: completedAt,
13572
13612
  consecutive_failures: 0,
13573
13613
  mined_sessions: history,
13574
13614
  total_mined: (state.total_mined ?? 0) + batch.length,
@@ -13588,10 +13628,12 @@ async function runKnowledgeSync(options = {}) {
13588
13628
  }
13589
13629
  case "consent_off":
13590
13630
  case "credit_limit":
13591
- case "quota_exceeded": {
13631
+ case "quota_exceeded":
13632
+ case "claude_code_missing": {
13592
13633
  const at = now().toISOString();
13593
13634
  saveState({
13594
13635
  ...state,
13636
+ ...partialProgress(at),
13595
13637
  last_attempt_at: at,
13596
13638
  consecutive_failures: 0,
13597
13639
  last_refusal: {
@@ -13600,13 +13642,14 @@ async function runKnowledgeSync(options = {}) {
13600
13642
  message: learner.message ?? "Studying unavailable right now."
13601
13643
  }
13602
13644
  });
13603
- logger.debug("sync", `studying skipped by gateway: ${learner.outcome}`);
13645
+ logger.debug("sync", `studying skipped: ${learner.outcome}`);
13604
13646
  return { status: "skipped-gateway", ...base, studiedSessions: 0, learner };
13605
13647
  }
13606
13648
  default: {
13607
13649
  const at = now().toISOString();
13608
13650
  saveState({
13609
13651
  ...state,
13652
+ ...partialProgress(at),
13610
13653
  last_attempt_at: at,
13611
13654
  consecutive_failures: state.consecutive_failures + 1,
13612
13655
  ...learner.outcome === "gateway_rejected" && learner.message ? { last_refusal: { at, outcome: learner.outcome, message: learner.message } } : {}
@@ -13637,7 +13680,7 @@ var init_sync = __esm(() => {
13637
13680
  });
13638
13681
 
13639
13682
  // src/telemetry/debug-id.ts
13640
- var BUNDLE_DEBUG_ID_PLACEHOLDER = "737a8f86-5677-f74d-6475-6e2164756e21";
13683
+ var BUNDLE_DEBUG_ID_PLACEHOLDER = "93db7db7-907f-4d14-6475-6e2164756e21";
13641
13684
 
13642
13685
  // src/telemetry/telemetry.ts
13643
13686
  import { randomUUID as randomUUID2 } from "node:crypto";
@@ -13862,12 +13905,19 @@ function consumeCommandFacets() {
13862
13905
  function allowlisted(value, allowed) {
13863
13906
  return typeof value === "string" && allowed.has(value) ? value : undefined;
13864
13907
  }
13908
+ function matching(value, pattern) {
13909
+ return typeof value === "string" && pattern.test(value) ? value : undefined;
13910
+ }
13865
13911
  function sanitizeFacets(facets) {
13866
13912
  if (!facets || typeof facets !== "object")
13867
13913
  return {};
13868
13914
  const trigger = allowlisted(facets.sync_trigger, SYNC_TRIGGERS);
13869
13915
  const status = allowlisted(facets.sync_status, SYNC_STATUSES);
13870
13916
  const learner = allowlisted(facets.learner_outcome, LEARNER_OUTCOMES);
13917
+ const gatewayReason = allowlisted(facets.gateway_reason, GATEWAY_REASONS);
13918
+ const claudeSource = allowlisted(facets.claude_code_source, CLAUDE_CODE_SOURCES);
13919
+ const claudeVersion = matching(facets.claude_code_version, CLAUDE_CODE_VERSION_PATTERN);
13920
+ const model = matching(facets.learner_model, LEARNER_MODEL_PATTERN);
13871
13921
  const backfill = allowlisted(facets.backfill_offer, BACKFILL_OFFERS);
13872
13922
  return {
13873
13923
  ...trigger ? { sync_trigger: trigger } : {},
@@ -13875,6 +13925,10 @@ function sanitizeFacets(facets) {
13875
13925
  ...facets.sessions_studied !== undefined ? { sessions_studied: countBucket(facets.sessions_studied) } : {},
13876
13926
  ...facets.notes_written !== undefined ? { notes_written: countBucket(facets.notes_written) } : {},
13877
13927
  ...learner ? { learner_outcome: learner } : {},
13928
+ ...gatewayReason ? { gateway_reason: gatewayReason } : {},
13929
+ ...claudeSource ? { claude_code_source: claudeSource } : {},
13930
+ ...claudeVersion ? { claude_code_version: claudeVersion } : {},
13931
+ ...model ? { learner_model: model } : {},
13878
13932
  ...backfill ? { backfill_offer: backfill } : {}
13879
13933
  };
13880
13934
  }
@@ -13964,7 +14018,13 @@ function parseSentryDsn(value) {
13964
14018
  return null;
13965
14019
  }
13966
14020
  }
13967
- function sentryTags(command, context, runtime, error) {
14021
+ function sentryTags(command, context, runtime, error, facets) {
14022
+ const facetTags = {};
14023
+ for (const key of SENTRY_FACET_TAGS) {
14024
+ const value = facets[key];
14025
+ if (value)
14026
+ facetTags[key] = value;
14027
+ }
13968
14028
  return {
13969
14029
  schema_version: "1",
13970
14030
  command,
@@ -13980,9 +14040,21 @@ function sentryTags(command, context, runtime, error) {
13980
14040
  is_authenticated: String(context.isAuthenticated),
13981
14041
  ...error.code ? { error_code: error.code } : {},
13982
14042
  ...error.status ? { http_status: String(error.status) } : {},
13983
- ...error.exitCode === undefined ? {} : { exit_code: String(error.exitCode) }
14043
+ ...error.exitCode === undefined ? {} : { exit_code: String(error.exitCode) },
14044
+ ...facetTags
13984
14045
  };
13985
14046
  }
14047
+ function failedLearnerOutcome(facets) {
14048
+ return facets.learner_outcome === "completed" ? undefined : facets.learner_outcome;
14049
+ }
14050
+ function exceptionSummary(command, error, facets) {
14051
+ if (facets.learner_outcome === "completed")
14052
+ return error.code ?? error.type;
14053
+ const outcome = facets.learner_outcome ?? facets.sync_status;
14054
+ if (!outcome)
14055
+ return error.code ?? error.type;
14056
+ return `${command}: ${outcome}${facets.gateway_reason ? ` (${facets.gateway_reason})` : ""}`;
14057
+ }
13986
14058
  function buildSentryEnvelope(input) {
13987
14059
  const parsedDsn = parseSentryDsn(input.dsn);
13988
14060
  if (!parsedDsn || !validEventId(input.eventId))
@@ -13990,6 +14062,7 @@ function buildSentryEnvelope(input) {
13990
14062
  const context = normalizeContext(input.context);
13991
14063
  const runtime = normalizeRuntime(input.runtime);
13992
14064
  const command = canonicalCommand(input.command);
14065
+ const facets = sanitizeFacets(input.facets);
13993
14066
  const debugId = validDebugId(input.debugId) ? input.debugId.toLowerCase() : undefined;
13994
14067
  const error = {
13995
14068
  type: validErrorType(input.error.type) ? input.error.type : "Error",
@@ -14009,7 +14082,7 @@ function buildSentryEnvelope(input) {
14009
14082
  };
14010
14083
  const exceptionValue = {
14011
14084
  type: error.type,
14012
- value: error.code ?? error.type,
14085
+ value: exceptionSummary(command, error, facets),
14013
14086
  ...error.frames.length > 0 ? { stacktrace: { frames: error.frames } } : {}
14014
14087
  };
14015
14088
  const newestFrame = error.frames.at(-1);
@@ -14022,7 +14095,7 @@ function buildSentryEnvelope(input) {
14022
14095
  platform: "node",
14023
14096
  level: "error",
14024
14097
  release: `dosu-cli@${runtime.version}`,
14025
- tags: sentryTags(command, context, runtime, error),
14098
+ tags: sentryTags(command, context, runtime, error, facets),
14026
14099
  ...context.user ? {
14027
14100
  user: {
14028
14101
  id: context.user.id,
@@ -14034,7 +14107,15 @@ function buildSentryEnvelope(input) {
14034
14107
  images: [{ type: "sourcemap", code_file: BUNDLE_CODE_FILE, debug_id: debugId }]
14035
14108
  }
14036
14109
  } : {},
14037
- fingerprint: ["dosu-cli", command, error.type, error.code ?? "unknown", safeCallsite],
14110
+ fingerprint: [
14111
+ "dosu-cli",
14112
+ command,
14113
+ error.type,
14114
+ error.code ?? "unknown",
14115
+ safeCallsite,
14116
+ ...failedLearnerOutcome(facets) ? [facets.learner_outcome] : [],
14117
+ ...facets.gateway_reason ? [facets.gateway_reason] : []
14118
+ ],
14038
14119
  exception: { values: [exceptionValue] }
14039
14120
  };
14040
14121
  const envelopeHeader = {
@@ -14275,15 +14356,17 @@ function createCommandTelemetry(settings, dependencies = {}) {
14275
14356
  async function dispatch(result, exitCode, error) {
14276
14357
  try {
14277
14358
  const tasks = [];
14359
+ let facets;
14360
+ if (!disabled) {
14361
+ try {
14362
+ facets = resolveFacets();
14363
+ } catch {
14364
+ facets = undefined;
14365
+ }
14366
+ }
14278
14367
  if (!disabled && analyticsToken) {
14279
14368
  const id = context.user?.id ?? installId();
14280
14369
  if (id) {
14281
- let facets;
14282
- try {
14283
- facets = resolveFacets();
14284
- } catch {
14285
- facets = undefined;
14286
- }
14287
14370
  const payload = buildPostHogPayload({
14288
14371
  apiKey: analyticsToken,
14289
14372
  ...context.user ? {} : { installId: id },
@@ -14311,14 +14394,16 @@ function createCommandTelemetry(settings, dependencies = {}) {
14311
14394
  }
14312
14395
  }
14313
14396
  }
14314
- if (!disabled && result === "failure" && error && shouldSendToSentry(error) && sentryDsn) {
14397
+ const sentryError = result === "failure" && error && shouldSendToSentry(error) ? error : result === "success" && sanitizeFacets(facets).sync_status === "mine-failed" ? { type: "LearnerRunFailed", frames: [] } : undefined;
14398
+ if (!disabled && sentryError && sentryDsn) {
14315
14399
  const id = eventId(generateUuid);
14316
14400
  const envelope = id ? buildSentryEnvelope({
14317
14401
  dsn: sentryDsn,
14318
14402
  command,
14319
14403
  context,
14320
14404
  runtime,
14321
- error,
14405
+ error: sentryError,
14406
+ ...facets ? { facets } : {},
14322
14407
  eventId: id,
14323
14408
  timestampMs: safeNow(now),
14324
14409
  debugId: BUNDLE_DEBUG_ID_PLACEHOLDER
@@ -14370,7 +14455,7 @@ function createCommandTelemetry(settings, dependencies = {}) {
14370
14455
  function shouldSendToSentry(error) {
14371
14456
  return !["SESSION_EXPIRED", "SESSION_PERSISTENCE_ERROR"].includes(error.code ?? "");
14372
14457
  }
14373
- var REQUEST_TIMEOUT_MS = 500, MAX_RESPONSE_BYTES, MAX_STACK_FRAMES = 20, UNKNOWN_COMMAND = "unknown", FALLBACK_INSTALL_ID = "00000000-0000-4000-8000-000000000000", BUNDLE_CODE_FILE = "app:///bin/dosu.js", CLI_PACKAGE_ROOT, SAFE_ERROR_TYPES, SAFE_ERROR_CODES, SYNC_TRIGGERS, SYNC_STATUSES, LEARNER_OUTCOMES, BACKFILL_OFFERS, pendingFacets;
14458
+ var REQUEST_TIMEOUT_MS = 500, MAX_RESPONSE_BYTES, MAX_STACK_FRAMES = 20, UNKNOWN_COMMAND = "unknown", FALLBACK_INSTALL_ID = "00000000-0000-4000-8000-000000000000", BUNDLE_CODE_FILE = "app:///bin/dosu.js", CLI_PACKAGE_ROOT, SAFE_ERROR_TYPES, SAFE_ERROR_CODES, SYNC_TRIGGERS, SYNC_STATUSES, LEARNER_OUTCOMES, GATEWAY_REASONS, CLAUDE_CODE_SOURCES, CLAUDE_CODE_VERSION_PATTERN, LEARNER_MODEL_PATTERN, BACKFILL_OFFERS, SENTRY_FACET_TAGS, pendingFacets;
14374
14459
  var init_telemetry = __esm(() => {
14375
14460
  MAX_RESPONSE_BYTES = 1024 * 1024;
14376
14461
  CLI_PACKAGE_ROOT = detectCliPackageRoot();
@@ -14381,6 +14466,7 @@ var init_telemetry = __esm(() => {
14381
14466
  "CliUsageError",
14382
14467
  "CommandExitError",
14383
14468
  "Error",
14469
+ "LearnerRunFailed",
14384
14470
  "OAuthCallbackError",
14385
14471
  "RangeError",
14386
14472
  "ReferenceError",
@@ -14474,8 +14560,21 @@ var init_telemetry = __esm(() => {
14474
14560
  "credit_limit",
14475
14561
  "quota_exceeded",
14476
14562
  "gateway_rejected",
14563
+ "claude_code_missing",
14477
14564
  "error"
14478
14565
  ]);
14566
+ GATEWAY_REASONS = new Set([
14567
+ "system_role_unsupported",
14568
+ "adaptive_thinking_unsupported",
14569
+ "effort_unsupported",
14570
+ "unsupported_request",
14571
+ "max_tokens",
14572
+ "context_length",
14573
+ "other"
14574
+ ]);
14575
+ CLAUDE_CODE_SOURCES = new Set(["sdk", "system", "missing"]);
14576
+ CLAUDE_CODE_VERSION_PATTERN = /^\d{1,4}\.\d{1,4}\.\d{1,6}(?:-[0-9A-Za-z]{1,16}(?:\.[0-9A-Za-z]{1,16}){0,3})?$/;
14577
+ LEARNER_MODEL_PATTERN = /^claude-[a-z0-9.-]{1,56}$/;
14479
14578
  BACKFILL_OFFERS = new Set([
14480
14579
  "not-offered",
14481
14580
  "accepted",
@@ -14483,6 +14582,15 @@ var init_telemetry = __esm(() => {
14483
14582
  "cancelled",
14484
14583
  "spawn-failed"
14485
14584
  ]);
14585
+ SENTRY_FACET_TAGS = [
14586
+ "sync_trigger",
14587
+ "sync_status",
14588
+ "learner_outcome",
14589
+ "gateway_reason",
14590
+ "claude_code_source",
14591
+ "claude_code_version",
14592
+ "learner_model"
14593
+ ];
14486
14594
  });
14487
14595
 
14488
14596
  // src/commands/agent-select.ts
@@ -15073,12 +15181,14 @@ function buildLearnerEnv(options) {
15073
15181
  const headers = [
15074
15182
  `x-dosu-run-id: ${options.runID}`,
15075
15183
  `x-dosu-trigger: ${options.trigger}`,
15076
- `x-dosu-cli-version: ${options.cliVersion}`
15184
+ `x-dosu-cli-version: ${options.cliVersion}`,
15185
+ `x-dosu-expected-model: ${options.model}`
15077
15186
  ];
15078
15187
  if (options.deploymentID)
15079
15188
  headers.push(`x-dosu-deployment-id: ${options.deploymentID}`);
15080
15189
  env.ANTHROPIC_BASE_URL = options.gatewayURL;
15081
15190
  env.ANTHROPIC_AUTH_TOKEN = options.apiKey;
15191
+ env.ANTHROPIC_MODEL = options.model;
15082
15192
  env.ANTHROPIC_CUSTOM_HEADERS = headers.join(`
15083
15193
  `);
15084
15194
  env.CLAUDE_CONFIG_DIR = options.configDir;
@@ -15136,11 +15246,44 @@ function findSystemClaude(env = process.env, homeDir = homedir6()) {
15136
15246
  function resolveClaudeExecutable(options = {}) {
15137
15247
  const hasSdkBinary = options.sdkBinaryExists ?? sdkNativeBinaryExists;
15138
15248
  if (hasSdkBinary())
15139
- return;
15140
- return findSystemClaude(options.env, options.homeDir);
15249
+ return { kind: "sdk" };
15250
+ const path = findSystemClaude(options.env, options.homeDir);
15251
+ return path ? { kind: "system", path } : { kind: "missing" };
15141
15252
  }
15142
15253
  var init_executable = () => {};
15143
15254
 
15255
+ // src/learner/model.ts
15256
+ async function resolveServedModel(options) {
15257
+ const fetchImpl = options.fetchImpl ?? fetch;
15258
+ try {
15259
+ const base = options.gatewayURL.replace(/\/+$/, "");
15260
+ const resp = await fetchImpl(`${base}/capabilities`, {
15261
+ method: "GET",
15262
+ headers: { Authorization: `Bearer ${options.apiKey}` },
15263
+ signal: AbortSignal.timeout(options.timeoutMs ?? CAPABILITIES_TIMEOUT_MS)
15264
+ });
15265
+ if (!resp.ok) {
15266
+ logger.debug("learner", `gateway capabilities returned ${resp.status}; using default model`);
15267
+ return DEFAULT_LEARNER_MODEL;
15268
+ }
15269
+ const body = await resp.json();
15270
+ const model = body?.model;
15271
+ if (typeof model === "string" && CLAUDE_MODEL_PATTERN.test(model))
15272
+ return model;
15273
+ logger.debug("learner", "gateway capabilities named no usable model; using default model");
15274
+ return DEFAULT_LEARNER_MODEL;
15275
+ } catch (error) {
15276
+ const text = error instanceof Error ? error.message : String(error);
15277
+ logger.debug("learner", `gateway capabilities failed (${text}); using default model`);
15278
+ return DEFAULT_LEARNER_MODEL;
15279
+ }
15280
+ }
15281
+ var DEFAULT_LEARNER_MODEL = "claude-haiku-4-5", CAPABILITIES_TIMEOUT_MS = 5000, CLAUDE_MODEL_PATTERN;
15282
+ var init_model = __esm(() => {
15283
+ init_logger();
15284
+ CLAUDE_MODEL_PATTERN = /^claude-[a-z0-9.-]+$/;
15285
+ });
15286
+
15144
15287
  // src/learner/prompt.ts
15145
15288
  function buildLearnerSystemPrompt(coreRules) {
15146
15289
  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.
@@ -58104,6 +58247,13 @@ function snippet(value) {
58104
58247
  const flat = (text ?? "").replace(/\s+/g, " ").trim();
58105
58248
  return flat.length > TRACE_SNIPPET_LIMIT ? `${flat.slice(0, TRACE_SNIPPET_LIMIT)}…` : flat;
58106
58249
  }
58250
+ function classifyRejectionReason(detail) {
58251
+ for (const [pattern, reason] of REJECTION_REASONS) {
58252
+ if (pattern.test(detail))
58253
+ return reason;
58254
+ }
58255
+ return "other";
58256
+ }
58107
58257
  function classifyGatewayRejection(text) {
58108
58258
  const match = API_400_PATTERN.exec(text);
58109
58259
  if (!match)
@@ -58116,7 +58266,8 @@ function classifyGatewayRejection(text) {
58116
58266
  } catch {}
58117
58267
  return {
58118
58268
  outcome: "gateway_rejected",
58119
- message: `LLM gateway rejected the study run: ${snippet(detail)}`
58269
+ message: `LLM gateway rejected the study run: ${snippet(detail)}`,
58270
+ reason: classifyRejectionReason(detail.trim())
58120
58271
  };
58121
58272
  }
58122
58273
  function traceAgentMessage(message) {
@@ -58164,11 +58315,26 @@ async function runLearner(options) {
58164
58315
  message: "LLM gateway URL is not set. From source, use `bun run dev` (production endpoints) or `bun run dev:local` (local stack)."
58165
58316
  };
58166
58317
  }
58167
- await Promise.resolve().then(() => init_sdk());
58168
58318
  const claudeExecutable = resolveClaudeExecutable();
58169
- if (claudeExecutable) {
58170
- logger.debug("learner", `using system Claude Code executable: ${claudeExecutable}`);
58319
+ if (claudeExecutable.kind === "missing") {
58320
+ return {
58321
+ outcome: "claude_code_missing",
58322
+ notesWritten: 0,
58323
+ turns: 0,
58324
+ claudeCodeSource: "missing",
58325
+ message: "Studying runs on Claude Code, which isn't installed here. Install Claude Code, then run `dosu knowledge sync`."
58326
+ };
58171
58327
  }
58328
+ if (claudeExecutable.kind === "system") {
58329
+ logger.debug("learner", `using system Claude Code executable: ${claudeExecutable.path}`);
58330
+ }
58331
+ const model = await resolveServedModel({ gatewayURL, apiKey: options.apiKey });
58332
+ logger.debug("learner", `study run model: ${model}`);
58333
+ const diagnostics = {
58334
+ claudeCodeSource: claudeExecutable.kind,
58335
+ model
58336
+ };
58337
+ await Promise.resolve().then(() => init_sdk());
58172
58338
  const configDir = createRunConfigDir();
58173
58339
  const runID = options.runID ?? crypto.randomUUID();
58174
58340
  const sessionStartedAt = options.sessions.map((s) => s.updated).sort()[0];
@@ -58179,7 +58345,19 @@ async function runLearner(options) {
58179
58345
  let notesWritten = 0;
58180
58346
  let currentSession;
58181
58347
  const readsSinceWrite = new Set;
58348
+ const sessionKeys = new Map(options.sessions.map((s) => [s.id, `${s.harness}/${s.id}`]));
58349
+ const notedSessions = new Set;
58350
+ const pendingNotes = new Map;
58182
58351
  let turns = 0;
58352
+ const finish = (outcome, message, gatewayReason) => ({
58353
+ outcome,
58354
+ notesWritten,
58355
+ notedSessions: [...notedSessions],
58356
+ turns,
58357
+ ...message === undefined ? {} : { message },
58358
+ ...gatewayReason ? { gatewayReason } : {},
58359
+ ...diagnostics
58360
+ });
58183
58361
  const env = buildLearnerEnv({
58184
58362
  apiKey: options.apiKey,
58185
58363
  gatewayURL,
@@ -58187,19 +58365,21 @@ async function runLearner(options) {
58187
58365
  runID,
58188
58366
  trigger: options.trigger,
58189
58367
  cliVersion: getVersionString(),
58190
- deploymentID: options.deploymentID
58368
+ deploymentID: options.deploymentID,
58369
+ model
58191
58370
  });
58192
58371
  try {
58193
58372
  const run = PGt({
58194
58373
  prompt: buildLearnerPrompt(options.sessions),
58195
58374
  options: {
58196
58375
  systemPrompt: buildLearnerSystemPrompt(LEARNER_CORE_RULES),
58376
+ model,
58197
58377
  env,
58198
58378
  abortController: abort,
58199
58379
  maxTurns: options.maxTurns ?? DEFAULT_MAX_TURNS,
58200
58380
  settingSources: [],
58201
58381
  persistSession: false,
58202
- ...claudeExecutable ? { pathToClaudeCodeExecutable: claudeExecutable } : {},
58382
+ ...claudeExecutable.kind === "system" ? { pathToClaudeCodeExecutable: claudeExecutable.path } : {},
58203
58383
  sandbox: { enabled: true, failIfUnavailable: false },
58204
58384
  mcpServers: {
58205
58385
  [SESSIONS_SERVER_NAME]: createSessionToolsServer(options.sessions),
@@ -58215,7 +58395,7 @@ async function runLearner(options) {
58215
58395
  }
58216
58396
  },
58217
58397
  stderr: (data) => logger.debug("learner", `[sdk] ${data}`),
58218
- canUseTool: async (toolName, input) => {
58398
+ canUseTool: async (toolName, input, { toolUseID }) => {
58219
58399
  if (!allowed.has(toolName)) {
58220
58400
  return {
58221
58401
  behavior: "deny",
@@ -58246,6 +58426,9 @@ async function runLearner(options) {
58246
58426
  }
58247
58427
  notesWritten += 1;
58248
58428
  readsSinceWrite.clear();
58429
+ const noted = currentSession && sessionKeys.get(currentSession);
58430
+ if (noted)
58431
+ pendingNotes.set(toolUseID, noted);
58249
58432
  const { transcript_id: _authoredByModel, ...clean } = input;
58250
58433
  return {
58251
58434
  behavior: "allow",
@@ -58258,56 +58441,46 @@ async function runLearner(options) {
58258
58441
  });
58259
58442
  for await (const message of run) {
58260
58443
  traceAgentMessage(message);
58444
+ if (message.type === "user" && Array.isArray(message.message.content)) {
58445
+ for (const block of message.message.content) {
58446
+ const noted = block.type === "tool_result" && pendingNotes.get(block.tool_use_id);
58447
+ if (noted && block.is_error !== true)
58448
+ notedSessions.add(noted);
58449
+ }
58450
+ }
58451
+ if (message.type === "system" && message.subtype === "init" && typeof message.claude_code_version === "string") {
58452
+ diagnostics.claudeCodeVersion = message.claude_code_version;
58453
+ }
58261
58454
  if (message.type === "result") {
58262
58455
  turns = message.num_turns;
58263
58456
  const text = message.subtype === "success" ? message.result : message.subtype;
58264
58457
  const gatewayError = classifyGatewayError(text ?? "") ?? (message.is_error ? classifyGatewayRejection(text ?? "") : null);
58265
58458
  if (gatewayError) {
58266
- return {
58267
- outcome: gatewayError.outcome,
58268
- notesWritten,
58269
- turns,
58270
- message: gatewayError.message
58271
- };
58459
+ return finish(gatewayError.outcome, gatewayError.message, gatewayError.reason);
58272
58460
  }
58273
58461
  if (message.subtype !== "success" || message.is_error) {
58274
58462
  logger.debug("learner", `run ${runID} failed: ${text}`);
58275
- return {
58276
- outcome: "error",
58277
- notesWritten,
58278
- turns,
58279
- message: "Study run failed; see debug log for details."
58280
- };
58463
+ return finish("error", "Study run failed; see debug log for details.");
58281
58464
  }
58282
58465
  logger.debug("learner", `run ${runID} completed: ${turns} turns, ${notesWritten} suggested pages`);
58283
- return { outcome: "completed", notesWritten, turns, message: text };
58466
+ return finish("completed", text);
58284
58467
  }
58285
58468
  }
58286
- return {
58287
- outcome: "error",
58288
- notesWritten,
58289
- turns,
58290
- message: "Study run ended without a result."
58291
- };
58469
+ return finish("error", "Study run ended without a result.");
58292
58470
  } catch (error) {
58293
58471
  const text = error instanceof Error ? error.message : String(error);
58294
58472
  const gatewayError = classifyGatewayError(text) ?? classifyGatewayRejection(text);
58295
58473
  if (gatewayError) {
58296
- return { outcome: gatewayError.outcome, notesWritten, turns, message: gatewayError.message };
58474
+ return finish(gatewayError.outcome, gatewayError.message, gatewayError.reason);
58297
58475
  }
58298
58476
  logger.debug("learner", `run ${runID} threw: ${text}`);
58299
- return {
58300
- outcome: "error",
58301
- notesWritten,
58302
- turns,
58303
- message: abort.signal.aborted ? "Study run timed out and was aborted." : "Study run failed; see debug log for details."
58304
- };
58477
+ return finish("error", abort.signal.aborted ? "Study run timed out and was aborted." : "Study run failed; see debug log for details.");
58305
58478
  } finally {
58306
58479
  clearTimeout(timer);
58307
58480
  configDir.cleanup();
58308
58481
  }
58309
58482
  }
58310
- var DEFAULT_MAX_TURNS = 160, DEFAULT_MAX_NOTES = 80, DEFAULT_TIMEOUT_MS2, KNOWLEDGE_SERVER_NAME = "dosu", GATEWAY_ERRORS, TRACE_SNIPPET_LIMIT = 400, API_400_PATTERN;
58483
+ var DEFAULT_MAX_TURNS = 160, DEFAULT_MAX_NOTES = 80, DEFAULT_TIMEOUT_MS2, KNOWLEDGE_SERVER_NAME = "dosu", GATEWAY_ERRORS, TRACE_SNIPPET_LIMIT = 400, REJECTION_REASONS, API_400_PATTERN;
58311
58484
  var init_runner = __esm(() => {
58312
58485
  init_logger();
58313
58486
  init_config_helpers();
@@ -58316,6 +58489,7 @@ var init_runner = __esm(() => {
58316
58489
  init_conflicts();
58317
58490
  init_env();
58318
58491
  init_executable();
58492
+ init_model();
58319
58493
  init_tools();
58320
58494
  DEFAULT_TIMEOUT_MS2 = 30 * 60 * 1000;
58321
58495
  GATEWAY_ERRORS = {
@@ -58332,6 +58506,14 @@ var init_runner = __esm(() => {
58332
58506
  message: "Daily Dosu Remote Sessions budget reached; runs resume tomorrow."
58333
58507
  }
58334
58508
  };
58509
+ REJECTION_REASONS = [
58510
+ [/^dosu_unsupported_request:/, "unsupported_request"],
58511
+ [/\beffort\b|output_config/i, "effort_unsupported"],
58512
+ [/\brole\b.*\bsystem\b|\bsystem\b.*\brole\b|messages\.\d+\.role/i, "system_role_unsupported"],
58513
+ [/\badaptive\b/i, "adaptive_thinking_unsupported"],
58514
+ [/prompt is too long|context (?:limit|length|window)/i, "context_length"],
58515
+ [/max_tokens/i, "max_tokens"]
58516
+ ];
58335
58517
  API_400_PATTERN = /API Error: 400\b\s*(.*)/s;
58336
58518
  });
58337
58519
 
@@ -58495,7 +58677,7 @@ ${results.length - opts.limit} more results not shown.`));
58495
58677
  sync_status: outcome.status,
58496
58678
  sessions_studied: sessionsStudied,
58497
58679
  notes_written: notesWritten,
58498
- ...outcome.learner ? { learner_outcome: outcome.learner.outcome } : {}
58680
+ ...outcome.learner ? learnerFacets(outcome.learner) : {}
58499
58681
  });
58500
58682
  if (opts.quiet)
58501
58683
  return;
@@ -58555,6 +58737,15 @@ ${results.length - opts.limit} more results not shown.`));
58555
58737
  cmd.addCommand(statuslineCommand2());
58556
58738
  return cmd;
58557
58739
  }
58740
+ function learnerFacets(learner) {
58741
+ return {
58742
+ learner_outcome: learner.outcome,
58743
+ ...learner.gatewayReason ? { gateway_reason: learner.gatewayReason } : {},
58744
+ ...learner.claudeCodeSource ? { claude_code_source: learner.claudeCodeSource } : {},
58745
+ ...learner.claudeCodeVersion ? { claude_code_version: learner.claudeCodeVersion } : {},
58746
+ ...learner.model ? { learner_model: learner.model } : {}
58747
+ };
58748
+ }
58558
58749
  function buildLearner(trigger) {
58559
58750
  const cfg = loadConfig();
58560
58751
  if (cfg.mode === "oss")
@@ -62592,10 +62783,18 @@ function allowlistedWorkflowProperties(properties) {
62592
62783
  if (Array.isArray(input.providers)) {
62593
62784
  safe.providers = input.providers.filter((provider) => typeof provider === "string" && /^[a-z][a-z0-9-]{0,31}$/.test(provider)).slice(0, 50);
62594
62785
  }
62595
- for (const key of ["completed_mcp", "completed_skill", "completed_agents_md"]) {
62786
+ for (const key of [
62787
+ "completed_mcp",
62788
+ "completed_skill",
62789
+ "completed_agents_md",
62790
+ "completed_hooks"
62791
+ ]) {
62596
62792
  if (typeof input[key] === "boolean")
62597
62793
  safe[key] = input[key];
62598
62794
  }
62795
+ if (typeof input.hook_count === "number" && Number.isInteger(input.hook_count) && input.hook_count >= 0 && input.hook_count <= 50) {
62796
+ safe.hook_count = input.hook_count;
62797
+ }
62599
62798
  return safe;
62600
62799
  }
62601
62800
  function safeIdentifier(value, maxLength) {
@@ -63272,10 +63471,13 @@ async function runSetupFlow(opts = {}) {
63272
63471
  agentsMdCompleted = await stepUpdateAgentsMd();
63273
63472
  }
63274
63473
  if (mcpCompleted || skillCompleted || agentsMdCompleted) {
63474
+ const hookCount = configuredProviders.filter((result) => result.hook).length;
63275
63475
  trackInBackground(trackCliOnboardingEvent(cfg, onboardingRunID, "cli_onboarding_completed", {
63276
63476
  completed_mcp: mcpCompleted,
63277
63477
  completed_skill: skillCompleted,
63278
- completed_agents_md: agentsMdCompleted
63478
+ completed_agents_md: agentsMdCompleted,
63479
+ completed_hooks: hookCount > 0,
63480
+ hook_count: hookCount
63279
63481
  }));
63280
63482
  }
63281
63483
  if (mcpCompleted && cfg.mode !== MODE_OSS) {
@@ -66405,4 +66607,4 @@ main().catch((err) => {
66405
66607
  process.exit(1);
66406
66608
  });
66407
66609
 
66408
- //# debugId=737a8f86-5677-f74d-6475-6e2164756e21
66610
+ //# debugId=93db7db7-907f-4d14-6475-6e2164756e21
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dosu/cli",
3
- "version": "0.55.1",
3
+ "version": "0.56.1",
4
4
  "type": "module",
5
5
  "description": "Dosu CLI - Manage MCP servers for AI tools",
6
6
  "license": "MIT",