incremnt 0.8.0 → 0.8.2

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.
package/src/ask-coach.js CHANGED
@@ -399,6 +399,32 @@ function requestedActionForRoute(route, question, { isFollowUp = false, carriedP
399
399
  return byRoute[route] ?? 'answer_training_question';
400
400
  }
401
401
 
402
+ export const ASK_RESPONSE_PROFILES = Object.freeze({
403
+ expansive: 'expansive',
404
+ defensive: 'defensive',
405
+ structured: 'structured'
406
+ });
407
+
408
+ function responseProfileForAskIntent(route, requestedAction, question) {
409
+ if (route === 'program_design' || requestedAction === 'draft_plan') return ASK_RESPONSE_PROFILES.structured;
410
+ const text = String(question ?? '').toLowerCase();
411
+ if (
412
+ requestedAction === 'recommend_action' ||
413
+ requestedAction === 'recommend_next_session' ||
414
+ requestedAction === 'explain_cause' ||
415
+ /\b(should|can i|safe|increase|decrease|deload|swap|change|adjust)\b/.test(text) ||
416
+ /\b(what should i do|do next|next session|up next)\b/.test(text) ||
417
+ /\b(did i hit|hit target|below target|missed?|failed?|why did i fail)\b/.test(text) ||
418
+ // Decision questions phrased without an imperative verb still want a crisp
419
+ // recommendation, not an expansive dashboard.
420
+ /\b(too heavy|too light|too much|too easy|ready to|time to|worth|good idea|enough|move up|add weight|bump up|push (?:harder|up)|stall(?:ing|ed)?|plateau(?:ing|ed)?)\b/.test(text) ||
421
+ /\bam i ready\b/.test(text)
422
+ ) {
423
+ return ASK_RESPONSE_PROFILES.defensive;
424
+ }
425
+ return ASK_RESPONSE_PROFILES.expansive;
426
+ }
427
+
402
428
  function confidenceForIntent({ rawRoute, route, namedExercises, question, previousRoute, isFollowUp }) {
403
429
  let confidence = 0.72;
404
430
  if (route !== 'general') confidence += 0.1;
@@ -456,6 +482,8 @@ function classifyAskIntentWithPrevious(snapshot, question, { previousIntent = nu
456
482
  : current.namedExercises;
457
483
  const sessionLabel = current.sessionLabel ?? (carriedPreviousTopic ? previous?.sessionLabel ?? null : null);
458
484
  const sessionReference = current.sessionReference ?? (carriedPreviousTopic ? previous?.sessionReference ?? null : null);
485
+ const requestedAction = requestedActionForRoute(route, question, { isFollowUp, carriedPreviousTopic });
486
+ const responseProfile = responseProfileForAskIntent(route, requestedAction, question);
459
487
  return {
460
488
  route,
461
489
  effectiveRoute: route === 'exercise_progress' && namedExercises.length === 0 ? 'general' : route,
@@ -476,7 +504,8 @@ function classifyAskIntentWithPrevious(snapshot, question, { previousIntent = nu
476
504
  sessionReference
477
505
  },
478
506
  timeframe: since ? { since } : null,
479
- requestedAction: requestedActionForRoute(route, question, { isFollowUp, carriedPreviousTopic }),
507
+ requestedAction,
508
+ responseProfile,
480
509
  isFollowUp,
481
510
  previousRoute: previous?.route ?? null,
482
511
  ambiguityFlags: ambiguityFlagsForIntent({
@@ -623,6 +652,10 @@ const ASK_ROUTE_REQUIRED_TOOLS = Object.freeze({
623
652
  program_design: ['get_recent_sessions', 'get_goal_status']
624
653
  });
625
654
 
655
+ function requiredToolsForAskRoute(route) {
656
+ return ASK_ROUTE_REQUIRED_TOOLS[route] ?? ASK_ROUTE_REQUIRED_TOOLS.general;
657
+ }
658
+
626
659
  function askObservationCheckPlan({ exclude = new Set(), route } = {}) {
627
660
  if (exclude.has('coach_observations')) return [];
628
661
  const checks = [
@@ -727,7 +760,7 @@ export function planAskEvidence(snapshot, question, {
727
760
  const since = intent.timeframe?.since ?? null;
728
761
  const effectiveRoute = route === 'exercise_progress' && namedExerciseItems.length === 0 ? 'general' : route;
729
762
  const fallbackRoute = effectiveRoute === route ? null : effectiveRoute;
730
- const requiredTools = ASK_ROUTE_REQUIRED_TOOLS[effectiveRoute] ?? ASK_ROUTE_REQUIRED_TOOLS.general;
763
+ const requiredTools = requiredToolsForAskRoute(effectiveRoute);
731
764
  const observationChecks = askObservationCheckPlan({
732
765
  exclude,
733
766
  route: effectiveRoute
@@ -857,11 +890,17 @@ function buildIncrementScoreAskContext(snapshot, { exclude = new Set(), today =
857
890
  lines.push('Increment Score: no snapshots available yet.');
858
891
  } else {
859
892
  const delta = incrementScore.facts.dayOverDayDelta;
860
- const trend = !Number.isFinite(delta) ? 'unknown' : delta > 0 ? 'up' : delta < 0 ? 'down' : 'flat';
861
- lines.push(`Increment Score: ${Math.round(incrementScore.facts.score)}, day-over-day trend ${trend}.`);
893
+ const scoreParts = [`Increment Score: ${Math.round(incrementScore.facts.score)}`];
894
+ if (Number.isFinite(delta)) {
895
+ const trend = delta > 0 ? 'up' : delta < 0 ? 'down' : 'flat';
896
+ scoreParts.push(`day-over-day trend ${trend}`);
897
+ }
898
+ lines.push(`${scoreParts.join(', ')}.`);
862
899
  // Direction word, not a raw daily-score list the model can dump back verbatim.
900
+ // Only steer on the multi-day trend when every point shares the current
901
+ // formula version; a formula change makes "rising/falling" a cross-ruler lie.
863
902
  const recentScores = (incrementScore.facts.recentScores ?? []).filter((s) => typeof s === 'number');
864
- if (recentScores.length > 1) {
903
+ if (incrementScore.facts.trendComparable && recentScores.length > 1) {
865
904
  const span = recentScores[0] - recentScores[recentScores.length - 1];
866
905
  const weekTrend = span > 2 ? 'rising' : span < -2 ? 'falling' : 'steady';
867
906
  lines.push(`Recent score trend (newest first): ${weekTrend}.`);
@@ -883,6 +922,12 @@ function formatRepDelta(delta) {
883
922
  return `${delta > 0 ? '+' : ''}${delta}`;
884
923
  }
885
924
 
925
+ function setWorkUnits(set) {
926
+ const reps = Number(set?.reps ?? 0);
927
+ const weight = Number(set?.weight ?? 0);
928
+ return (weight > 0 ? weight : 1) * reps;
929
+ }
930
+
886
931
  function formatComparableSetDelta(exercise) {
887
932
  const previous = exercise?.previousComparableSession;
888
933
  if (!previous || !Array.isArray(exercise?.sets) || !Array.isArray(previous.sets)) return null;
@@ -891,15 +936,46 @@ function formatComparableSetDelta(exercise) {
891
936
  const comparableCount = Math.min(currentSets.length, previousSets.length);
892
937
  if (comparableCount === 0) return null;
893
938
 
894
- const sameLoad = currentSets.slice(0, comparableCount).every((set, index) => set.weight === previousSets[index].weight);
895
- if (!sameLoad) return null;
896
-
897
- const repDeltas = currentSets
898
- .slice(0, comparableCount)
899
- .map((set, index) => formatRepDelta(set.reps - previousSets[index].reps));
939
+ const currentOverlap = currentSets.slice(0, comparableCount);
940
+ const previousOverlap = previousSets.slice(0, comparableCount);
941
+ const sameLoad = currentOverlap.every((set, index) => Number(set.weight) === Number(previousOverlap[index].weight));
942
+ const repDeltas = currentOverlap.map((set, index) => formatRepDelta(Number(set.reps) - Number(previousOverlap[index].reps)));
900
943
  if (repDeltas.some((delta) => delta == null)) return null;
901
944
 
902
- return `vs previous ${previous.label} on ${previous.date}: same load, reps ${repDeltas.join(', ')}`;
945
+ const currentTotalReps = currentSets.reduce((sum, set) => sum + Number(set.reps ?? 0), 0);
946
+ const previousTotalReps = previousSets.reduce((sum, set) => sum + Number(set.reps ?? 0), 0);
947
+ const currentTotalWork = currentSets.reduce((sum, set) => sum + setWorkUnits(set), 0);
948
+ const previousTotalWork = previousSets.reduce((sum, set) => sum + setWorkUnits(set), 0);
949
+ const topCurrent = currentSets[0] ?? {};
950
+ const topPrevious = previousSets[0] ?? {};
951
+ const topLoadDelta = Number(topCurrent.weight ?? 0) - Number(topPrevious.weight ?? 0);
952
+ const topRepDelta = Number(topCurrent.reps ?? 0) - Number(topPrevious.reps ?? 0);
953
+ const averageCurrentOverlap = currentOverlap.reduce((sum, set) => sum + Number(set.reps ?? 0), 0) / comparableCount;
954
+ const averagePreviousOverlap = previousOverlap.reduce((sum, set) => sum + Number(set.reps ?? 0), 0) / comparableCount;
955
+ const averageRepDelta = averageCurrentOverlap - averagePreviousOverlap;
956
+ // Only flag a regression when the session actually did LESS total work. Without
957
+ // this gate, adding a set (more total reps) or going heavier for slightly fewer
958
+ // reps per set — both textbook progression — tripped the average/top-rep
959
+ // branches and mislabeled a better session "regression".
960
+ const didLessTotalWork = currentTotalWork < previousTotalWork;
961
+ const regressionFlag = didLessTotalWork
962
+ && (averageRepDelta <= -2 || topRepDelta <= -3 || (topLoadDelta > 0 && topRepDelta <= -2));
963
+
964
+ const details = [];
965
+ if (sameLoad) {
966
+ details.push(`same load, reps ${repDeltas.join(', ')}`);
967
+ } else {
968
+ const loadDeltas = currentOverlap.map((set, index) => formatSignedDelta(Number(set.weight ?? 0) - Number(previousOverlap[index].weight ?? 0), 'kg'));
969
+ details.push(`load/reps by overlapping set ${loadDeltas.map((load, index) => `${load ?? '0.0kg'} and reps ${repDeltas[index]}`).join('; ')}`);
970
+ }
971
+ if (currentSets.length !== previousSets.length || currentTotalReps !== previousTotalReps) {
972
+ details.push(`total reps ${currentTotalReps} vs ${previousTotalReps}${currentSets.length !== previousSets.length ? ` across ${currentSets.length} vs ${previousSets.length} sets` : ''}`);
973
+ }
974
+ if (regressionFlag) {
975
+ details.push('regression flag: reps dropped sharply despite the load/set context');
976
+ }
977
+
978
+ return `vs previous ${previous.label} on ${previous.date}: ${details.join('; ')}`;
903
979
  }
904
980
 
905
981
  function appendUserNotesForSession(lines, session) {
@@ -1174,6 +1250,169 @@ function buildRecordsAskContext(snapshot, namedExercises, { exclude = new Set(),
1174
1250
  return { context: lines.join('\n'), sections: ['header', 'records'], tools: [recordsTool], provenance: [coachToolProvenance('records', recordsTool)] };
1175
1251
  }
1176
1252
 
1253
+ function appendIncrementScoreEvidence(lines, incrementScore) {
1254
+ lines.push('');
1255
+ lines.push('Increment Score evidence:');
1256
+ if (incrementScore.facts?.score == null) {
1257
+ lines.push(' No Increment Score snapshot is available.');
1258
+ return;
1259
+ }
1260
+ const facts = incrementScore.facts;
1261
+ const delta = facts.dayOverDayDelta;
1262
+ const scoreParts = [`Current score: ${Math.round(facts.score)}/100`];
1263
+ if (Number.isFinite(delta)) {
1264
+ const trend = delta > 0 ? 'up' : delta < 0 ? 'down' : 'flat';
1265
+ scoreParts.push(`day-over-day ${trend}`);
1266
+ }
1267
+ lines.push(` ${scoreParts.join('; ')}.`);
1268
+ if (facts.summaryText) lines.push(` Summary: ${facts.summaryText}`);
1269
+ if ((facts.topPositiveDrivers ?? []).length > 0) {
1270
+ lines.push(` Top positive drivers: ${facts.topPositiveDrivers.join('; ')}.`);
1271
+ }
1272
+ if ((facts.topNegativeDrivers ?? []).length > 0) {
1273
+ lines.push(` Top negative drivers: ${facts.topNegativeDrivers.join('; ')}.`);
1274
+ }
1275
+ }
1276
+
1277
+ function formatRecentPrDelta(pr) {
1278
+ if (!pr || pr.priorBest == null) {
1279
+ return ' (first logged record for this lift)';
1280
+ }
1281
+ const sign = pr.delta >= 0 ? '+' : '';
1282
+ const priorDate = validDateOnlyString(pr.priorBest.date) ?? 'unknown date';
1283
+ const kindLabel = pr.kind === 'load_pr'
1284
+ ? 'load PR — heavier bar than the prior best'
1285
+ : 'rep PR — more reps at the same or lighter bar than the prior best (load looks flat but strength rose)';
1286
+ return ` (${sign}${pr.delta.toFixed(1)} kg vs prior best ${pr.priorBest.e1rm.toFixed(1)} kg from ${priorDate}; ${kindLabel})`;
1287
+ }
1288
+
1289
+ function appendRecordEvidence(lines, records, { windowStart = null, today = new Date() } = {}) {
1290
+ lines.push('');
1291
+ lines.push('Best estimated 1RM records:');
1292
+ if (records.rows.length === 0) {
1293
+ lines.push(' No weighted completed sets found.');
1294
+ return;
1295
+ }
1296
+ const todayIso = dateOnlyString(today);
1297
+ for (const record of records.rows) {
1298
+ const recordDate = validDateOnlyString(record.date);
1299
+ const inWindow = windowStart && recordDate != null && recordDate >= windowStart && recordDate <= todayIso;
1300
+ lines.push(` ${inWindow ? '★ ' : ''}${record.name}: ${record.e1rm.toFixed(1)} kg (${recordDate ?? 'unknown date'})`);
1301
+ }
1302
+ }
1303
+
1304
+ function appendBodyWeightEvidence(lines, bodyWeight, exclude) {
1305
+ lines.push('');
1306
+ if (exclude.has('bodyWeight')) {
1307
+ lines.push('Body weight sharing is disabled for AI Coach.');
1308
+ } else if (bodyWeight.facts.latestBodyWeightKg != null) {
1309
+ const source = bodyWeight.facts.latestBodyWeightDate
1310
+ ? `latest reading ${bodyWeight.facts.latestBodyWeightDate}`
1311
+ : 'profile';
1312
+ lines.push(`Body weight: ${bodyWeight.facts.latestBodyWeightKg.toFixed(1)} kg (${source}).`);
1313
+ if (bodyWeight.facts.trendKg != null) {
1314
+ const trend = bodyWeight.facts.trendKg >= 0 ? `+${bodyWeight.facts.trendKg.toFixed(1)}` : bodyWeight.facts.trendKg.toFixed(1);
1315
+ lines.push(`Body weight trend, last ${bodyWeight.facts.recentDays} days: ${trend} kg across ${bodyWeight.facts.readingCount} readings.`);
1316
+ }
1317
+ } else {
1318
+ lines.push('No body weight is available in the exported profile or HealthKit body-mass readings.');
1319
+ }
1320
+ }
1321
+
1322
+ function appendGoalStatusEvidence(lines, goalStatus) {
1323
+ if (goalStatus.rows.length === 0) return;
1324
+ lines.push('');
1325
+ lines.push('Goal status:');
1326
+ for (const goal of goalStatus.rows) {
1327
+ const progress = goal.progressPercent != null ? `${goal.progressPercent}%` : 'unknown progress';
1328
+ lines.push(` ${goal.exerciseName}: ${progress}`);
1329
+ }
1330
+ }
1331
+
1332
+ function appendExpansiveEvidenceContextBeforeExcludeNote(lines, snapshot, {
1333
+ exclude = new Set(),
1334
+ today = new Date(),
1335
+ namedExercises = [],
1336
+ existingSections = []
1337
+ } = {}) {
1338
+ const sections = new Set(existingSections);
1339
+ const note = buildExcludeNote(exclude);
1340
+ const noteAtEnd = note && lines.at(-1) === note;
1341
+ if (noteAtEnd) {
1342
+ lines.pop();
1343
+ if (lines.at(-1) === '') lines.pop();
1344
+ }
1345
+
1346
+ const tools = [];
1347
+ const provenance = [];
1348
+ const addedSections = [];
1349
+ const addTool = (section, tool) => {
1350
+ tools.push(tool);
1351
+ provenance.push(coachToolProvenance(section, tool));
1352
+ addedSections.push(section);
1353
+ };
1354
+
1355
+ if (!sections.has('increment_score')) {
1356
+ const incrementScore = executeCoachReadTool(snapshot, 'get_increment_score', { historyDays: 21 });
1357
+ appendIncrementScoreEvidence(lines, incrementScore);
1358
+ addTool('increment_score', incrementScore);
1359
+ }
1360
+
1361
+ if (!sections.has('weekly_volume')) {
1362
+ const weeklyVolume = executeCoachReadTool(snapshot, 'get_weekly_volume', { today });
1363
+ lines.push('');
1364
+ lines.push(`This week strength volume: ${weeklyVolume.facts.currentWeekVolume} kg across ${weeklyVolume.facts.currentWeekSessionCount} session${weeklyVolume.facts.currentWeekSessionCount === 1 ? '' : 's'}.`);
1365
+ lines.push(`Previous week strength volume: ${weeklyVolume.facts.previousWeekVolume} kg across ${weeklyVolume.facts.previousWeekSessionCount} session${weeklyVolume.facts.previousWeekSessionCount === 1 ? '' : 's'}.`);
1366
+ if (weeklyVolume.facts.deltaPct != null) {
1367
+ lines.push(`Week-over-week strength volume change: ${weeklyVolume.facts.deltaPct >= 0 ? '+' : ''}${weeklyVolume.facts.deltaPct}%.`);
1368
+ }
1369
+ addTool('weekly_volume', weeklyVolume);
1370
+ }
1371
+
1372
+ if (!sections.has('records')) {
1373
+ const records = executeCoachReadTool(snapshot, 'get_records', {
1374
+ exercises: namedExercises,
1375
+ limit: namedExercises.length > 0 ? Math.max(5, namedExercises.length) : 10,
1376
+ today
1377
+ });
1378
+ appendRecordEvidence(lines, records, { today });
1379
+ addTool('records', records);
1380
+ }
1381
+
1382
+ if (!sections.has('body_weight')) {
1383
+ const bodyWeight = executeCoachReadTool(snapshot, 'get_body_weight_snapshot', {
1384
+ recentDays: 30,
1385
+ exclude: [...exclude],
1386
+ today
1387
+ });
1388
+ appendBodyWeightEvidence(lines, bodyWeight, exclude);
1389
+ addTool('body_weight', bodyWeight);
1390
+ }
1391
+
1392
+ if (!sections.has('readiness')) {
1393
+ const readiness = executeCoachReadTool(snapshot, 'get_readiness_snapshot', {
1394
+ recentDays: 14,
1395
+ exclude: [...exclude],
1396
+ today
1397
+ });
1398
+ appendReadinessSummary(lines, readiness);
1399
+ addTool('readiness', readiness);
1400
+ }
1401
+
1402
+ if (!sections.has('goal_status')) {
1403
+ const goalStatus = executeCoachReadTool(snapshot, 'get_goal_status', { limit: 5 });
1404
+ appendGoalStatusEvidence(lines, goalStatus);
1405
+ addTool('goal_status', goalStatus);
1406
+ }
1407
+
1408
+ if (noteAtEnd) {
1409
+ lines.push('');
1410
+ lines.push(note);
1411
+ }
1412
+
1413
+ return { sections: addedSections, tools, provenance };
1414
+ }
1415
+
1177
1416
  function rowMatchesSetHint(row, hint) {
1178
1417
  const exercise = (row?.exercises ?? []).find((candidate) => canonicalExerciseName(candidate.name) === hint.exerciseCanonical);
1179
1418
  if (!exercise) return false;
@@ -1428,9 +1667,17 @@ function buildProgressReviewAskContext(snapshot, { exclude = new Set(), since =
1428
1667
  // Records, flagging which were set inside the review window (recent PRs).
1429
1668
  if (records.rows.length > 0) {
1430
1669
  lines.push('');
1670
+ const recentRecords = records.facts.recentRecords ?? [];
1431
1671
  if (recentRecordCount > 0) {
1432
- const names = recentRecordNames.join(', ');
1433
- lines.push(`Recent all-time estimated 1RM PR count in review window: ${recentRecordCount}. Mention this count explicitly in broad progress reviews. Exercises: ${names}.`);
1672
+ lines.push(`Recent all-time estimated 1RM PR count in review window: ${recentRecordCount}. Mention this count explicitly in broad progress reviews.`);
1673
+ if (recentRecords.length > 0) {
1674
+ lines.push('Recent PRs (compared to the prior best, so a rep PR at the same bar weight is not mistaken for a stall):');
1675
+ for (const pr of recentRecords) {
1676
+ lines.push(` ${pr.name}: ${pr.e1rm.toFixed(1)} kg e1RM on ${validDateOnlyString(pr.date) ?? 'unknown date'}${formatRecentPrDelta(pr)}.`);
1677
+ }
1678
+ } else {
1679
+ lines.push(`Exercises: ${recentRecordNames.join(', ')}.`);
1680
+ }
1434
1681
  }
1435
1682
  lines.push('Best estimated 1RM records (★ = set within review window):');
1436
1683
  for (const record of records.rows) {
@@ -1533,9 +1780,17 @@ function buildGeneralAskContext(snapshot, { exclude = new Set(), today = new Dat
1533
1780
  };
1534
1781
  }
1535
1782
 
1536
- function askToolMetadata(tools = [], provenance = []) {
1783
+ function missingDataFlagsForRequiredTools(tools = [], requiredToolNames = []) {
1784
+ const required = new Set(requiredToolNames ?? []);
1785
+ const scopedTools = required.size > 0
1786
+ ? tools.filter((tool) => required.has(tool.toolName))
1787
+ : tools;
1788
+ return uniqueArray(scopedTools.flatMap((tool) => tool.missingDataFlags ?? []));
1789
+ }
1790
+
1791
+ function askToolMetadata(tools = [], provenance = [], { requiredTools = [] } = {}) {
1537
1792
  const sourceTimestamps = tools.map((tool) => tool.sourceTimestamp).filter(Boolean).sort();
1538
- const missingDataFlags = uniqueArray(tools.flatMap((tool) => tool.missingDataFlags ?? []));
1793
+ const missingDataFlags = missingDataFlagsForRequiredTools(tools, requiredTools);
1539
1794
  const noteSourceIds = uniqueArray(tools.flatMap((tool) => tool.facts?.noteSourceIds ?? []));
1540
1795
  return {
1541
1796
  toolsUsed: tools.map((tool) => tool.toolName),
@@ -1583,7 +1838,7 @@ function contextBundleFromParts({
1583
1838
  sessionObservationComparisons = []
1584
1839
  }) {
1585
1840
  const evidenceUsed = evidenceUsedFromProvenance(provenance);
1586
- const missingDataFlags = uniqueArray(tools.flatMap((tool) => tool.missingDataFlags ?? []));
1841
+ const missingDataFlags = missingDataFlagsForRequiredTools(tools, evidencePlan?.requiredTools ?? []);
1587
1842
  return {
1588
1843
  intent,
1589
1844
  evidencePlan,
@@ -1778,6 +2033,8 @@ export function sanitizeAskAnswerVerificationReceipt(value) {
1778
2033
  ...(Number.isFinite(value.retryCount) ? { retryCount: value.retryCount } : {}),
1779
2034
  ...(typeof value.repaired === 'boolean' ? { repaired: value.repaired } : {}),
1780
2035
  ...(typeof value.fallback === 'boolean' ? { fallback: value.fallback } : {}),
2036
+ ...(typeof value.degraded === 'boolean' ? { degraded: value.degraded } : {}),
2037
+ ...(Number.isFinite(value.redactedCount) ? { redactedCount: value.redactedCount } : {}),
1781
2038
  ...(Number.isFinite(value.blockingFailureCount) ? { blockingFailureCount: value.blockingFailureCount } : {}),
1782
2039
  ...(Number.isFinite(value.advisoryFailureCount) ? { advisoryFailureCount: value.advisoryFailureCount } : {}),
1783
2040
  ...(Array.isArray(value.failureKeys) ? { failureKeys } : {})
@@ -1830,6 +2087,7 @@ function appendCoachObservationsContextBeforeExcludeNote(lines, observations, ex
1830
2087
  const section = [
1831
2088
  '',
1832
2089
  'Coach observations (derived from training data, not user-stated facts).',
2090
+ 'These are durable longer-window patterns, not automatic verdicts about the current session.',
1833
2091
  'Each observation separates Facts (raw pattern in the data), Interpretation (what we infer),',
1834
2092
  'and Recommendation (suggested user action). Treat Facts as load-bearing; treat Interpretation',
1835
2093
  'as a hypothesis the user may contradict; Recommendation is a default, not a directive.'
@@ -1888,6 +2146,7 @@ function appendSessionObservationComparisonsBeforeExcludeNote(lines, comparisons
1888
2146
  lines.push('');
1889
2147
  lines.push('Session-to-observation evidence:');
1890
2148
  lines.push('Use this raw session evidence when reconciling the current workout against durable Coach observations.');
2149
+ lines.push('Only call an observation a current-session finding when direction is not "not_comparable"; direction=not_comparable means frame it as a longer-running pattern only.');
1891
2150
  lines.push('Instruction: a single session can qualify a durable multi-week observation, but should not erase it unless the longer-window evidence changes.');
1892
2151
  for (const comparison of usable) {
1893
2152
  lines.push(`- observation-id=${comparison.observationId}; session-id=${comparison.sessionId ?? 'unknown'}; evidence=${comparison.evidenceType}; direction=${comparison.direction ?? 'unknown'}`);
@@ -2310,8 +2569,8 @@ export function askObservationFollowUpContext(snapshot, question, observation, {
2310
2569
  appendExcludeNote(lines, exclude);
2311
2570
  const includedFacts = rankedCoachFactsForAsk(snapshot, question, 'general', { facts: coachFacts });
2312
2571
  const includedCoachFactIds = appendCoachFactsContextBeforeExcludeNote(lines, includedFacts, exclude);
2313
- const toolMetadata = askToolMetadata(tools, provenance);
2314
2572
  const finalizedEvidencePlan = finalizeEvidencePlan(evidencePlan, tools);
2573
+ const toolMetadata = askToolMetadata(tools, provenance, { requiredTools: finalizedEvidencePlan.requiredTools });
2315
2574
  const context = lines.join('\n');
2316
2575
  const includedSections = [
2317
2576
  'header',
@@ -2326,6 +2585,7 @@ export function askObservationFollowUpContext(snapshot, question, observation, {
2326
2585
  const intentMetadata = {
2327
2586
  route: 'coach_observation_followup',
2328
2587
  effectiveRoute: 'coach_observation_followup',
2588
+ responseProfile: ASK_RESPONSE_PROFILES.structured,
2329
2589
  confidence: 0.86,
2330
2590
  entities: {
2331
2591
  exercises: exercises.map((exercise) => ({
@@ -2361,6 +2621,7 @@ export function askObservationFollowUpContext(snapshot, question, observation, {
2361
2621
  route: 'coach_observation_followup',
2362
2622
  effectiveRoute: 'coach_observation_followup',
2363
2623
  fallbackRoute: null,
2624
+ responseProfile: ASK_RESPONSE_PROFILES.structured,
2364
2625
  intent: intentMetadata,
2365
2626
  namedExercises: exercises.map((exercise) => exercise.canonical),
2366
2627
  namedExerciseLabels: exercises.map((exercise) => exercise.displayName),
@@ -2427,6 +2688,7 @@ export function askMissingObservationFollowUpContext(snapshot, _question, reques
2427
2688
  const intentMetadata = {
2428
2689
  route: 'coach_observation_followup',
2429
2690
  effectiveRoute: 'coach_observation_followup_missing',
2691
+ responseProfile: ASK_RESPONSE_PROFILES.structured,
2430
2692
  confidence: 0.5,
2431
2693
  entities: { exercises: [] },
2432
2694
  timeframe: null,
@@ -2452,6 +2714,7 @@ export function askMissingObservationFollowUpContext(snapshot, _question, reques
2452
2714
  route: 'coach_observation_followup',
2453
2715
  effectiveRoute: 'coach_observation_followup_missing',
2454
2716
  fallbackRoute: null,
2717
+ responseProfile: ASK_RESPONSE_PROFILES.structured,
2455
2718
  intent: intentMetadata,
2456
2719
  namedExercises: [],
2457
2720
  namedExerciseLabels: [],
@@ -2475,12 +2738,19 @@ export function askMissingObservationFollowUpContext(snapshot, _question, reques
2475
2738
  };
2476
2739
  }
2477
2740
 
2478
- export function askRoutedContext(snapshot, question, { exclude = new Set(), coachFacts = null, coachObservations = null, history = [], today = new Date() } = {}) {
2741
+ export function askRoutedContext(snapshot, question, { exclude = new Set(), coachFacts = null, coachObservations = null, history = [], today = new Date(), responseProfileOverride = null } = {}) {
2479
2742
  const contextSnapshot = Array.isArray(coachObservations)
2480
2743
  ? { ...snapshot, coachObservations }
2481
2744
  : snapshot;
2482
2745
  const evidencePlan = planAskEvidence(contextSnapshot, question, { exclude, history, today });
2483
2746
  const { route, effectiveRoute, fallbackRoute, namedExercises, namedExerciseLabels, sessionLabel = null, sessionReference = null, since = null } = evidencePlan;
2747
+ // Surfaces that share this context builder but must stay terse (e.g. the weekly
2748
+ // check-in, which runs under WEEKLY_CHECKIN_PROMPT) can force a profile so the
2749
+ // expansive evidence merge and score headline do not bleed in under a
2750
+ // tight-reply prompt.
2751
+ const responseProfile = responseProfileOverride
2752
+ ?? evidencePlan.intent?.responseProfile
2753
+ ?? ASK_RESPONSE_PROFILES.expansive;
2484
2754
  const namedExerciseItems = namedExercises.map((canonical, index) => ({
2485
2755
  canonical,
2486
2756
  displayName: namedExerciseLabels[index] ?? canonical
@@ -2529,10 +2799,27 @@ export function askRoutedContext(snapshot, question, { exclude = new Set(), coac
2529
2799
  } else {
2530
2800
  built = buildGeneralAskContext(contextSnapshot, { exclude, today });
2531
2801
  }
2802
+ const factLines = built.context.split('\n');
2803
+ const expansiveEvidence = responseProfile === ASK_RESPONSE_PROFILES.expansive
2804
+ ? appendExpansiveEvidenceContextBeforeExcludeNote(factLines, contextSnapshot, {
2805
+ exclude,
2806
+ today,
2807
+ namedExercises: namedExerciseItems,
2808
+ existingSections: built.sections
2809
+ })
2810
+ : { sections: [], tools: [], provenance: [] };
2811
+ built = {
2812
+ ...built,
2813
+ context: factLines.join('\n'),
2814
+ sections: [...built.sections, ...expansiveEvidence.sections],
2815
+ tools: [...(built.tools ?? []), ...expansiveEvidence.tools],
2816
+ provenance: [...(built.provenance ?? []), ...expansiveEvidence.provenance]
2817
+ };
2818
+
2532
2819
  const tools = [...(built.tools ?? [])];
2533
2820
  const provenance = [...(built.provenance ?? [])];
2534
2821
 
2535
- const factLines = built.context.split('\n');
2822
+ factLines.splice(0, factLines.length, ...built.context.split('\n'));
2536
2823
  const includedFacts = rankedCoachFactsForAsk(snapshot, question, effectiveRoute, { facts: coachFacts });
2537
2824
  const includedCoachFactIds = appendCoachFactsContextBeforeExcludeNote(factLines, includedFacts, exclude);
2538
2825
  const shouldIncludeCoachObservations = !exclude.has('coach_observations');
@@ -2578,8 +2865,8 @@ export function askRoutedContext(snapshot, question, { exclude = new Set(), coac
2578
2865
  ...(sessionObservationComparisons.length > 0 ? ['session_observation_comparisons'] : [])
2579
2866
  ]
2580
2867
  };
2581
- const toolMetadata = askToolMetadata(tools, provenance);
2582
2868
  const finalizedEvidencePlan = finalizeEvidencePlan(evidencePlan, tools);
2869
+ const toolMetadata = askToolMetadata(tools, provenance, { requiredTools: finalizedEvidencePlan.requiredTools });
2583
2870
  const intent = {
2584
2871
  ...evidencePlan.intent,
2585
2872
  effectiveRoute
@@ -2601,6 +2888,7 @@ export function askRoutedContext(snapshot, question, { exclude = new Set(), coac
2601
2888
  route,
2602
2889
  effectiveRoute,
2603
2890
  fallbackRoute,
2891
+ responseProfile,
2604
2892
  intent,
2605
2893
  namedExercises,
2606
2894
  namedExerciseLabels,
package/src/format.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import chalk from 'chalk';
2
- import { commandSchema, writeCommandSchema } from './contract.js';
2
+ import { agentCommandSchema, commandSchema, writeCommandSchema } from './contract.js';
3
3
 
4
4
  const shortMonths = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
5
5
  const shortDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
@@ -869,6 +869,9 @@ export function formatHelp(opts = {}) {
869
869
  cmd('login --session-file <file>', 'Import session file'),
870
870
  cmd('logout', 'Clear session'),
871
871
  '',
872
+ header('AGENT TOKENS'),
873
+ ...agentCommandSchema.map((c) => cmd(c.usage ?? c.command, c.description)),
874
+ '',
872
875
  header('OTHER'),
873
876
  cmd('browse', 'Interactive Ink browser'),
874
877
  cmd('tui', 'Alias for browse'),