incremnt 0.8.4 → 0.8.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "incremnt",
3
- "version": "0.8.4",
3
+ "version": "0.8.6",
4
4
  "description": "Command-line tool for querying your incremnt strength training data",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -786,6 +786,30 @@ function checkObservationFollowupVoice(answer, route) {
786
786
  }];
787
787
  }
788
788
 
789
+ const ASK_REPORT_VOICE_PATTERNS = [
790
+ { label: 'What I see', pattern: /\bWhat I see\b/i },
791
+ { label: 'What that means', pattern: /\bWhat that means\b/i },
792
+ { label: 'Recent pattern', pattern: /\bRecent pattern\b/i },
793
+ { label: 'Facts:', pattern: /^\s*Facts:/im },
794
+ { label: 'Interpretation:', pattern: /^\s*Interpretation(?:\s*\[[^\]]+\])?:/im },
795
+ { label: 'Recommendation:', pattern: /^\s*Recommendation(?:\s*\[[^\]]+\])?:/im },
796
+ { label: 'coach observation', pattern: /\bcoach observations?\b/i },
797
+ { label: 'planning check', pattern: /\bplanning check\b/i }
798
+ ];
799
+
800
+ function checkAskReportVoice(answer, route) {
801
+ if (route === 'coach_observation_followup') return [];
802
+ const hits = uniqueStrings(ASK_REPORT_VOICE_PATTERNS
803
+ .filter(({ pattern }) => pattern.test(answer))
804
+ .map(({ label }) => label));
805
+ if (hits.length === 0) return [];
806
+ return [{
807
+ key: 'ask_report_voice',
808
+ severity: 'advisory',
809
+ reason: `Ask answer used report/artifact phrasing instead of coach voice: ${hits.join(', ')}.`
810
+ }];
811
+ }
812
+
789
813
  function checkExpansiveCompleteness(answer, snapshot, routingMetadata, { executeTool = executeCoachReadTool } = {}) {
790
814
  const responseProfile = routingMetadata?.responseProfile ?? routingMetadata?.intent?.responseProfile;
791
815
  if (responseProfile !== 'expansive') return [];
@@ -916,6 +940,7 @@ export function verifyAskAnswer({
916
940
 
917
941
  const failures = [
918
942
  ...voiceFailures,
943
+ ...checkAskReportVoice(normalized, route),
919
944
  ...checkSnapshotClaims(normalized, snapshot, routingMetadata, { today, exclude }),
920
945
  ...checkToolProvenance(normalized, snapshot, routingMetadata, { today, exclude, strictMentionProvenance, executeTool }),
921
946
  ...checkSessionObservationProvenance(normalized, routingMetadata),
package/src/ask-coach.js CHANGED
@@ -278,7 +278,53 @@ function inferredSinceDate(question, today = new Date()) {
278
278
  return candidate > dateOnlyString(today) ? `${year - 1}-${monthPart}-01` : candidate;
279
279
  }
280
280
 
281
- function routeAskQuestion(snapshot, question, { today = new Date() } = {}) {
281
+ function deloadScheduleContextFromText(text) {
282
+ const raw = String(text ?? '');
283
+ if (!/\bd(?:e)?load\b/i.test(raw)) return null;
284
+ const match = raw.match(/\b(this|next|coming)\s+(?:training\s+)?week\b/i);
285
+ if (!match) return null;
286
+ return {
287
+ week: match[1].toLowerCase() === 'this' ? 'this' : 'next'
288
+ };
289
+ }
290
+
291
+ function latestDeloadScheduleContext(history = []) {
292
+ const messages = (Array.isArray(history) ? history : []).slice(-2);
293
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
294
+ const message = messages[index];
295
+ if (!['user', 'assistant'].includes(message?.role) || typeof message.content !== 'string') continue;
296
+ const context = deloadScheduleContextFromText(message.content);
297
+ if (context) return context;
298
+ }
299
+ return null;
300
+ }
301
+
302
+ function isDeloadScheduleConfirmation(question) {
303
+ const text = String(question ?? '').trim().toLowerCase();
304
+ if (!text) return false;
305
+ if (/\b(no|don'?t|do not|cancel|undo|stop|never mind|nevermind)\b/i.test(text)) return false;
306
+ const wordCount = text.split(/\s+/).filter(Boolean).length;
307
+ if (wordCount > 8) return false;
308
+ return /^(yes|yeah|yep|ok|okay|sure)\b/i.test(text) ||
309
+ /\b(do it|schedule it|set it|make it happen|schedule that|set that)\b/i.test(text);
310
+ }
311
+
312
+ function hasProgramHistoryLanguage(question, { previousRoute = null, isFollowUp = false } = {}) {
313
+ const text = String(question ?? '').toLowerCase();
314
+ const hasProgramLanguage = /\b(program|plan|routine|split)\b/i.test(text);
315
+ if (/\bwhat changed\b[\s\S]{0,80}\b(program|plan|routine|split)\b/i.test(text) ||
316
+ /\b(program|plan|routine|split)\b[\s\S]{0,80}\bwhat changed\b/i.test(text) ||
317
+ /\bwhy\b[\s\S]{0,80}\b(program|plan|routine|split)\b[\s\S]{0,80}\bchang/i.test(text)) {
318
+ return true;
319
+ }
320
+
321
+ const restoreLanguage = /\b(undo that|undo it|revert that|revert it|restore that|restore it|change it back|previous version|last change)\b/i.test(text);
322
+ if (!restoreLanguage) return false;
323
+ if (hasProgramLanguage) return true;
324
+ return isFollowUp && ['program_history', 'program_progress', 'program_schedule_action'].includes(previousRoute);
325
+ }
326
+
327
+ function routeAskQuestion(snapshot, question, { today = new Date(), previousRoute = null, isFollowUp = false } = {}) {
282
328
  const normalizedQuestion = normalizeExerciseName(question ?? '');
283
329
  const namedExercises = namedExercisesFromQuestion(snapshot, question);
284
330
  const sessionReference = referencedSessionFromQuestion(snapshot, question);
@@ -287,6 +333,11 @@ function routeAskQuestion(snapshot, question, { today = new Date() } = {}) {
287
333
  const progressLanguage = /\b(progress|progressing|improve|improved|improvement|better|stronger|moved|moving|stalled|flat|since)\b/i.test(question ?? '');
288
334
  const profileLanguage = /\b(know about me|about me as a lifter|me as a lifter|lifter profile|training profile)\b/i.test(question ?? '');
289
335
  const programLanguage = /\b(program|plan|split|routine|full gym|ppl|upper lower)\b/i.test(question ?? '');
336
+ const deloadWord = 'd(?:e)?load';
337
+ const deloadScheduleContext = deloadScheduleContextFromText(question);
338
+ const deloadScheduleVerb = '(?:make|schedule|set|program|turn|change|adjust)';
339
+ const deloadScheduleLanguage = new RegExp(`\\b${deloadScheduleVerb}\\b[\\s\\S]{0,120}\\b(?:this|next|coming)\\s+(?:training\\s+)?week\\b[\\s\\S]{0,120}\\b${deloadWord}\\b`, 'i').test(question ?? '') ||
340
+ new RegExp(`\\b${deloadScheduleVerb}\\b[\\s\\S]{0,120}\\b${deloadWord}\\b[\\s\\S]{0,120}\\b(?:this|next|coming)\\s+(?:training\\s+)?week\\b`, 'i').test(question ?? '');
290
341
  const windowDays = inferredRelativeWindowDays(question);
291
342
  const reviewVerb = /\b(doing|going|look(?:s|ing|ed)?|progress\w*|train(?:s|ing)?|workouts?|fitness)\b/i.test(question ?? '');
292
343
  const explicitReview = /\b(on (?:the )?(?:right )?track|right track|making (?:good )?progress|overall progress|review (?:my|the)\b|check (?:out\s+)?my\b|how(?:'?s| is| has| have)\s+(?:my|the)\s+(?:training|progress|fitness))\b/i.test(question ?? '');
@@ -303,6 +354,13 @@ function routeAskQuestion(snapshot, question, { today = new Date() } = {}) {
303
354
  /\b(pr|prs|record|records|max|maxes|1rm|one rep max|one-rep max|strongest)\b/i.test(question ?? '') ||
304
355
  /\b(next|tomorrow|up next|coming session|do next|what should i do)\b/i.test(question ?? '');
305
356
 
357
+ if (deloadScheduleLanguage) {
358
+ return { route: 'program_schedule_action', namedExercises, deloadScheduleContext };
359
+ }
360
+ if (hasProgramHistoryLanguage(question, { previousRoute, isFollowUp })) {
361
+ return { route: 'program_history', namedExercises, deloadScheduleContext };
362
+ }
363
+
306
364
  // Broad "how am I doing / on the right track / last N weeks" reviews fan out to
307
365
  // sessions + volume + records + body weight rather than a single narrow route.
308
366
  // Requires an explicit review cue, or a relative window paired with a review verb.
@@ -311,16 +369,16 @@ function routeAskQuestion(snapshot, question, { today = new Date() } = {}) {
311
369
  (!narrowSingleTopic || compositeReviewCue) &&
312
370
  broadReviewIntent
313
371
  ) {
314
- return { route: 'progress_review', namedExercises, since };
372
+ return { route: 'progress_review', namedExercises, since, deloadScheduleContext };
315
373
  }
316
374
  if (/\b(body ?weight|weigh|weight trends?|current weight|my weight)\b/i.test(question ?? '')) {
317
- return { route: 'body_weight', namedExercises };
375
+ return { route: 'body_weight', namedExercises, deloadScheduleContext };
318
376
  }
319
377
  if (profileLanguage) {
320
- return { route: 'training_profile', namedExercises, since };
378
+ return { route: 'training_profile', namedExercises, since, deloadScheduleContext };
321
379
  }
322
380
  if (programLanguage && progressLanguage) {
323
- return { route: 'program_progress', namedExercises, since };
381
+ return { route: 'program_progress', namedExercises, since, deloadScheduleContext };
324
382
  }
325
383
  if (since && progressLanguage) {
326
384
  return { route: 'exercise_progress_summary', namedExercises, since };
@@ -374,6 +432,9 @@ function isTerseFollowUpQuestion(question) {
374
432
 
375
433
  function requestedActionForRoute(route, question, { isFollowUp = false, carriedPreviousTopic = false } = {}) {
376
434
  const text = String(question ?? '').toLowerCase();
435
+ if (route === 'program_schedule_action') return 'schedule_program_action';
436
+ if (route === 'program_history' && /\b(undo|revert|restore|change it back|previous version)\b/.test(text)) return 'explain_restore_status';
437
+ if (route === 'program_history') return 'explain_program_history';
377
438
  if (/\b(why|how come)\b/.test(text)) return 'explain_cause';
378
439
  if (/\b(build|create|make|generate|draft|rewrite|revise|update)\b/.test(text)) return 'draft_plan';
379
440
  if (/\badjust\b/.test(text) && /\b(program|plan|split|routine)\b/.test(text)) return 'draft_plan';
@@ -388,6 +449,7 @@ function requestedActionForRoute(route, question, { isFollowUp = false, carriedP
388
449
  exercise_progress: 'explain_exercise',
389
450
  volume: 'explain_volume',
390
451
  next_session: 'recommend_next_session',
452
+ program_history: 'explain_program_history',
391
453
  recovery: 'explain_recovery',
392
454
  score: 'explain_score',
393
455
  records: 'summarize_records',
@@ -406,6 +468,7 @@ export const ASK_RESPONSE_PROFILES = Object.freeze({
406
468
  });
407
469
 
408
470
  function responseProfileForAskIntent(route, requestedAction, question) {
471
+ if (route === 'program_schedule_action' || requestedAction === 'schedule_program_action') return ASK_RESPONSE_PROFILES.structured;
409
472
  if (route === 'program_design' || requestedAction === 'draft_plan') return ASK_RESPONSE_PROFILES.structured;
410
473
  const text = String(question ?? '').toLowerCase();
411
474
  if (
@@ -445,23 +508,36 @@ function ambiguityFlagsForIntent({ route, namedExercises, question, isFollowUp,
445
508
  return flags;
446
509
  }
447
510
 
448
- function classifyAskIntentWithPrevious(snapshot, question, { previousIntent = null, today = new Date() } = {}) {
449
- const current = routeAskQuestion(snapshot, question, { today });
511
+ function classifyAskIntentWithPrevious(snapshot, question, { previousIntent = null, previousDeloadScheduleContext = null, today = new Date() } = {}) {
450
512
  const previous = previousIntent
451
513
  ? {
452
514
  route: previousIntent.route,
453
515
  since: previousIntent.timeframe?.since ?? null,
454
516
  namedExercises: previousIntent.entities?.exercises ?? [],
455
517
  sessionLabel: previousIntent.entities?.sessionLabel ?? null,
456
- sessionReference: previousIntent.entities?.sessionReference ?? null
518
+ sessionReference: previousIntent.entities?.sessionReference ?? null,
519
+ deloadScheduleContext: previousIntent.deloadScheduleContext ?? null
457
520
  }
458
521
  : null;
459
- const isFollowUp = Boolean(previous && isTerseFollowUpQuestion(question));
522
+ const isFollowUp = Boolean((previous || previousDeloadScheduleContext) && isTerseFollowUpQuestion(question));
523
+ const current = routeAskQuestion(snapshot, question, {
524
+ today,
525
+ previousRoute: previous?.route ?? null,
526
+ isFollowUp
527
+ });
528
+ const currentDeloadScheduleContext = current.deloadScheduleContext ?? deloadScheduleContextFromText(question);
529
+ const carriedDeloadScheduleContext = currentDeloadScheduleContext ?? previousDeloadScheduleContext ?? previous?.deloadScheduleContext ?? null;
530
+ const isDeloadScheduleFollowUp = current.route !== 'program_schedule_action' &&
531
+ Boolean(carriedDeloadScheduleContext) &&
532
+ isDeloadScheduleConfirmation(question);
460
533
  let route = current.route;
461
534
  let since = current.since ?? null;
462
535
  let carriedPreviousTopic = false;
463
536
 
464
- if (
537
+ if (isDeloadScheduleFollowUp) {
538
+ route = 'program_schedule_action';
539
+ since = null;
540
+ } else if (
465
541
  isFollowUp &&
466
542
  previous?.route &&
467
543
  current.namedExercises.length > 0 &&
@@ -508,6 +584,9 @@ function classifyAskIntentWithPrevious(snapshot, question, { previousIntent = nu
508
584
  responseProfile,
509
585
  isFollowUp,
510
586
  previousRoute: previous?.route ?? null,
587
+ deloadScheduleContext: route === 'program_schedule_action'
588
+ ? carriedDeloadScheduleContext
589
+ : currentDeloadScheduleContext,
511
590
  ambiguityFlags: ambiguityFlagsForIntent({
512
591
  route,
513
592
  namedExercises,
@@ -523,7 +602,11 @@ export function classifyAskIntent(snapshot, question, { history = [], today = ne
523
602
  for (const previousQuestion of historyUserQuestions(history)) {
524
603
  previousIntent = classifyAskIntentWithPrevious(snapshot, previousQuestion, { previousIntent, today });
525
604
  }
526
- return classifyAskIntentWithPrevious(snapshot, question, { previousIntent, today });
605
+ return classifyAskIntentWithPrevious(snapshot, question, {
606
+ previousIntent,
607
+ previousDeloadScheduleContext: latestDeloadScheduleContext(history),
608
+ today
609
+ });
527
610
  }
528
611
 
529
612
  function pushAskContextHeader(lines, snapshot, today = new Date()) {
@@ -542,6 +625,7 @@ const ASK_FACT_KIND_BY_ROUTE = Object.freeze({
542
625
  exercise_progress: ['goal_signal', 'injury', 'constraint', 'preference', 'tone'],
543
626
  exercise_progress_summary: ['goal_signal', 'injury', 'constraint', 'preference', 'tone'],
544
627
  program_progress: ['goal_signal', 'preference', 'constraint', 'injury', 'tone'],
628
+ program_schedule_action: ['goal_signal', 'preference', 'constraint', 'injury', 'tone'],
545
629
  training_profile: ['goal_signal', 'preference', 'constraint', 'injury', 'tone'],
546
630
  program_design: ['goal_signal', 'preference', 'constraint', 'injury', 'tone'],
547
631
  next_session: ['constraint', 'injury', 'preference', 'goal_signal', 'tone'],
@@ -650,6 +734,8 @@ const ASK_ROUTE_REQUIRED_TOOLS = Object.freeze({
650
734
  exercise_progress: ['get_exercise_history'],
651
735
  exercise_progress_summary: ['get_exercise_progress_summary'],
652
736
  program_progress: ['get_program_progress', 'get_exercise_progress_summary', 'get_cycle_progression_summary'],
737
+ program_history: ['get_program_change_history'],
738
+ program_schedule_action: ['get_program_progress', 'get_recent_sessions', 'get_readiness_snapshot'],
653
739
  training_profile: ['get_training_profile'],
654
740
  records: ['get_records'],
655
741
  recent_session: ['get_recent_sessions'],
@@ -1123,6 +1209,83 @@ function buildNextSessionAskContext(snapshot, { exclude = new Set(), today = new
1123
1209
  return { context: lines.join('\n'), sections, tools: [nextSession], provenance: [coachToolProvenance('next_session_plan', nextSession)] };
1124
1210
  }
1125
1211
 
1212
+ function mondayWeekStartDateOnly(today = new Date(), weekOffset = 0) {
1213
+ const base = new Date(`${dateOnlyString(today)}T00:00:00.000Z`);
1214
+ const day = base.getUTCDay();
1215
+ const daysSinceMonday = (day + 6) % 7;
1216
+ base.setUTCDate(base.getUTCDate() - daysSinceMonday + weekOffset * 7);
1217
+ return base.toISOString().slice(0, 10);
1218
+ }
1219
+
1220
+ function deloadScheduleStartDate(question, today = new Date(), scheduleContext = null) {
1221
+ if (/\bthis\s+(?:training\s+)?week\b/i.test(question ?? '')) {
1222
+ return mondayWeekStartDateOnly(today, 0);
1223
+ }
1224
+ if (/\b(?:next|coming)\s+(?:training\s+)?week\b/i.test(question ?? '')) {
1225
+ return mondayWeekStartDateOnly(today, 1);
1226
+ }
1227
+ return scheduleContext?.week === 'this'
1228
+ ? mondayWeekStartDateOnly(today, 0)
1229
+ : mondayWeekStartDateOnly(today, 1);
1230
+ }
1231
+
1232
+ function appendProgramScheduleActionRequest(lines, { startDate, hasActiveProgram }) {
1233
+ lines.push('');
1234
+ lines.push('Program schedule action request:');
1235
+ if (!hasActiveProgram) {
1236
+ lines.push(' No active program is available. Do not append a <program_schedule_action> block.');
1237
+ return;
1238
+ }
1239
+ lines.push(' The user wants to schedule a one-week whole-program deload. This is not a new program draft and not an exercise-specific plan changeset.');
1240
+ lines.push(' If the request is clear, keep prose to 1-2 short sentences and append exactly one trailing <program_schedule_action>{JSON}</program_schedule_action>.');
1241
+ lines.push(` Use this exact JSON shape: {"action":"schedule_deload_week","startDate":"${startDate}","durationWeeks":1,"rationale":"..."}.`);
1242
+ lines.push(' The app will compute the weight changes when the week starts. Do not include weights, reps, set counts, deltas, targets, or any extra JSON keys.');
1243
+ lines.push(' Do not append a <program_draft> or <plan_changeset> block.');
1244
+ }
1245
+
1246
+ function buildProgramScheduleActionAskContext(snapshot, question, { exclude = new Set(), today = new Date(), scheduleContext = null } = {}) {
1247
+ const lines = [];
1248
+ const programProgress = executeCoachReadTool(snapshot, 'get_program_progress', { since: null, today, limitExercises: 10 });
1249
+ const recentSessions = executeCoachReadTool(snapshot, 'get_recent_sessions', { limit: 5, today });
1250
+ const readiness = executeCoachReadTool(snapshot, 'get_readiness_snapshot', { today });
1251
+ const program = activeProgram(snapshot);
1252
+ const startDate = deloadScheduleStartDate(question, today, scheduleContext);
1253
+
1254
+ pushAskContextHeader(lines, snapshot, today);
1255
+ lines.push('');
1256
+ lines.push(`Program schedule target: ${program?.name ?? 'No active program'}.`);
1257
+ const trainingLoad = programProgress.facts?.trainingLoad;
1258
+ if (trainingLoad) {
1259
+ const readinessBand = trainingLoad.readiness?.readinessBand ? `, readiness ${trainingLoad.readiness.readinessBand}` : '';
1260
+ lines.push(`Training-load signal: status ${trainingLoad.status ?? 'unknown'}${readinessBand}.`);
1261
+ }
1262
+ if (readiness.facts?.readinessBand) {
1263
+ lines.push(`Readiness signal: ${readiness.facts.readinessBand}.`);
1264
+ }
1265
+ appendActiveProgramScheduleContext(lines, snapshot);
1266
+ if (recentSessions.rows?.length > 0) {
1267
+ lines.push('');
1268
+ lines.push('Recent sessions:');
1269
+ for (const row of recentSessions.rows.slice(0, 5)) {
1270
+ const title = row.title ?? row.dayName ?? row.programDayTitle ?? 'Workout';
1271
+ lines.push(` ${row.date ?? 'unknown date'} - ${title}`);
1272
+ }
1273
+ }
1274
+ appendProgramScheduleActionRequest(lines, { startDate, hasActiveProgram: Boolean(program) });
1275
+ appendExcludeNote(lines, exclude);
1276
+ return {
1277
+ context: lines.join('\n'),
1278
+ sections: ['header', 'program_schedule_action', 'current_program_schedule', 'recent_sessions'],
1279
+ programScheduleActionStartDate: startDate,
1280
+ tools: [programProgress, recentSessions, readiness],
1281
+ provenance: [
1282
+ coachToolProvenance('program_schedule_program_progress', programProgress),
1283
+ coachToolProvenance('program_schedule_recent_sessions', recentSessions),
1284
+ coachToolProvenance('program_schedule_readiness', readiness)
1285
+ ]
1286
+ };
1287
+ }
1288
+
1126
1289
  function buildExerciseProgressAskContext(snapshot, namedExercises, { exclude = new Set(), today = new Date() } = {}) {
1127
1290
  const lines = [];
1128
1291
  const exerciseHistoryTool = executeCoachReadTool(snapshot, 'get_exercise_history', { exercises: namedExercises, limit: 6, today });
@@ -1286,6 +1449,37 @@ function buildProgramProgressAskContext(snapshot, { exclude = new Set(), since =
1286
1449
  };
1287
1450
  }
1288
1451
 
1452
+ function buildProgramHistoryAskContext(snapshot, { exclude = new Set(), today = new Date() } = {}) {
1453
+ const lines = [];
1454
+ const history = executeCoachReadTool(snapshot, 'get_program_change_history', { limit: 20 });
1455
+ pushAskContextHeader(lines, snapshot, today);
1456
+ lines.push('');
1457
+ lines.push(`Program change history: ${history.facts?.programName ?? 'No active program'}.`);
1458
+ if (history.rows.length === 0) {
1459
+ lines.push(' No durable program change records are available yet.');
1460
+ } else {
1461
+ for (const change of history.rows.slice(0, 10)) {
1462
+ const when = change.createdAt ? String(change.createdAt).slice(0, 10) : 'unknown date';
1463
+ const source = change.source ? `, source ${change.source}` : '';
1464
+ const status = change.status ? `, status ${change.status}` : '';
1465
+ lines.push(` ${when}: ${change.summary ?? change.kind ?? 'Program changed'} (${change.kind ?? 'unknown'}${source}${status}, id ${change.id ?? 'unknown'}).`);
1466
+ const exercises = change.affectedExercises?.length ? `exercises ${change.affectedExercises.slice(0, 4).join(', ')}` : null;
1467
+ const fields = change.affectedFields?.length ? `fields ${change.affectedFields.slice(0, 5).join(', ')}` : null;
1468
+ if (exercises || fields) lines.push(` Affected: ${[exercises, fields].filter(Boolean).join('; ')}.`);
1469
+ if (change.rationale) lines.push(` Rationale: ${change.rationale}`);
1470
+ }
1471
+ }
1472
+ lines.push('');
1473
+ lines.push('Restore status: automatic restore is not available in this slice. You may identify the likely change to restore, but do not claim the app restored or changed the program.');
1474
+ appendExcludeNote(lines, exclude);
1475
+ return {
1476
+ context: lines.join('\n'),
1477
+ sections: ['header', 'program_change_history'],
1478
+ tools: [history],
1479
+ provenance: [coachToolProvenance('program_change_history', history)]
1480
+ };
1481
+ }
1482
+
1289
1483
  function buildTrainingProfileAskContext(snapshot, { exclude = new Set(), since = null, today = new Date() } = {}) {
1290
1484
  const lines = [];
1291
1485
  const trainingProfile = executeCoachReadTool(snapshot, 'get_training_profile', { since, today });
@@ -2037,10 +2231,13 @@ function confidenceBand(intentConfidence, missingDataFlags = []) {
2037
2231
  return 'high';
2038
2232
  }
2039
2233
 
2040
- function recommendedActionsForAsk(route, requestedAction, programDraft) {
2234
+ function recommendedActionsForAsk(route, requestedAction, programDraft, programScheduleAction = null) {
2041
2235
  if (programDraft) {
2042
2236
  return [{ id: 'review-program-draft', label: 'Review drafted plan', kind: 'program_draft' }];
2043
2237
  }
2238
+ if (programScheduleAction) {
2239
+ return [{ id: 'review-program-schedule-action', label: 'Review scheduled deload', kind: 'program_schedule_action' }];
2240
+ }
2044
2241
  if (requestedAction === 'draft_plan') {
2045
2242
  return [{ id: 'ask-for-plan-draft', label: 'Ask for a plan draft', kind: 'follow_up' }];
2046
2243
  }
@@ -2124,6 +2321,7 @@ function followUpSuggestionsForAsk(route, intent, { question = '', missingDataFl
2124
2321
  body_weight: bodyWeightFollowUpCandidates(missingDataFlags),
2125
2322
  score: ['What is pulling my score down?', 'What should I focus on this week?'],
2126
2323
  program_progress: ['Pull this block summary.', 'Break down a specific lift.', 'What is the next decision?'],
2324
+ program_history: ['What changed most recently?', 'Why did that change happen?', 'Can that be restored later?'],
2127
2325
  program_design: ['Make this plan more conservative.', 'Explain the main changes.']
2128
2326
  };
2129
2327
  let candidates;
@@ -2178,7 +2376,7 @@ export function sanitizeAskAnswerVerificationReceipt(value) {
2178
2376
  return Object.keys(result).length > 0 ? result : null;
2179
2377
  }
2180
2378
 
2181
- export function buildAskStructuredResponse(answer, routingMetadata = {}, { programDraft = null, planChangeset = null, question = '' } = {}) {
2379
+ export function buildAskStructuredResponse(answer, routingMetadata = {}, { programDraft = null, planChangeset = null, programScheduleAction = null, question = '' } = {}) {
2182
2380
  const contextBundle = routingMetadata.contextBundle ?? {};
2183
2381
  const intent = routingMetadata.intent ?? contextBundle.intent ?? {};
2184
2382
  const answerVerification = sanitizeAskAnswerVerificationReceipt(routingMetadata.answerVerification);
@@ -2192,12 +2390,13 @@ export function buildAskStructuredResponse(answer, routingMetadata = {}, { progr
2192
2390
  answer,
2193
2391
  confidence,
2194
2392
  evidenceUsed: contextBundle.evidenceUsed ?? evidenceUsedFromProvenance(routingMetadata.provenance ?? [], routingMetadata.tools ?? []),
2195
- recommendedActions: recommendedActionsForAsk(intent.route ?? routingMetadata.route, intent.requestedAction, programDraft),
2393
+ recommendedActions: recommendedActionsForAsk(intent.route ?? routingMetadata.route, intent.requestedAction, programDraft, programScheduleAction),
2196
2394
  followUpSuggestions: followUpSuggestionsForAsk(intent.route ?? routingMetadata.route, intent, { question, missingDataFlags }),
2197
2395
  limitations: missingDataFlags.filter((flag) => !HIDDEN_LIMITATION_FLAGS.has(flag)).map(limitationText),
2198
2396
  answerVerification,
2199
2397
  programDraft: programDraft ?? null,
2200
- planChangeset: planChangeset ?? null
2398
+ planChangeset: planChangeset ?? null,
2399
+ programScheduleAction: programScheduleAction ?? null
2201
2400
  };
2202
2401
  }
2203
2402
 
@@ -2222,29 +2421,30 @@ function appendCoachObservationsContextBeforeExcludeNote(lines, observations, ex
2222
2421
  }
2223
2422
  const section = [
2224
2423
  '',
2225
- 'Coach observations (derived from training data, not user-stated facts).',
2226
- 'These are durable longer-window patterns, not automatic verdicts about the current session.',
2227
- 'Each observation separates Facts (raw pattern in the data), Interpretation (what we infer),',
2228
- 'and Recommendation (suggested user action). Treat Facts as load-bearing; treat Interpretation',
2229
- 'as a hypothesis the user may contradict; Recommendation is a default, not a directive.'
2424
+ 'Longer-window training patterns (derived from training data, not user-stated facts).',
2425
+ 'Use these as background unless session evidence below says the current workout directly supports them.',
2426
+ 'Treat Evidence as load-bearing. Treat Coach read as a grounded read the user may contradict.',
2427
+ 'Treat Next move as a default coaching nudge, not a directive.'
2230
2428
  ];
2231
2429
  for (const observation of usable) {
2430
+ const title = typeof observation.title === 'string' && observation.title.trim()
2431
+ ? observation.title.trim()
2432
+ : null;
2232
2433
  const header = [
2233
- `- [${observation.kind ?? 'observation'}]`,
2234
- observation.sourceComponent ? `component=${observation.sourceComponent}` : null,
2235
- observation.sourceExercise ? `exercise=${observation.sourceExercise}` : null,
2434
+ `- pattern-id=${observation.id}`,
2435
+ observation.kind ? `kind=${observation.kind}` : null,
2436
+ observation.sourceComponent ? `source-component=${observation.sourceComponent}` : null,
2437
+ observation.sourceExercise ? `source-exercise=${observation.sourceExercise}` : null,
2236
2438
  `confidence=${Number(observation.confidence ?? 0).toFixed(2)}`,
2237
- `observation-id=${observation.id}`
2238
2439
  ].filter(Boolean).join(' ');
2239
2440
  section.push(header);
2240
- section.push(` Facts: ${observation.summary}`);
2441
+ if (title) section.push(` Pattern: ${title}`);
2442
+ section.push(` Evidence: ${observation.summary}`);
2241
2443
  if (observation.interpretationText) {
2242
- const tag = observation.interpretationKind ? ` [${observation.interpretationKind}]` : '';
2243
- section.push(` Interpretation${tag}: ${observation.interpretationText}`);
2444
+ section.push(` Coach read: ${observation.interpretationText}`);
2244
2445
  }
2245
2446
  if (observation.actionText) {
2246
- const tag = observation.recommendationKind ? ` [${observation.recommendationKind}]` : '';
2247
- section.push(` Recommendation${tag}: ${observation.actionText}`);
2447
+ section.push(` Next move: ${observation.actionText}`);
2248
2448
  }
2249
2449
  if (observation.outcomeStatus) {
2250
2450
  const observedAt = observation.outcomeObservedAt ? ` observed ${observation.outcomeObservedAt}` : '';
@@ -2387,6 +2587,14 @@ function appendAskAnswerContract(lines, {
2387
2587
  contract.push(' End with this goal-clarifying question when no clear goal decides the tradeoff: "What are we measuring this against - size, strength, or staying lean?"');
2388
2588
  }
2389
2589
 
2590
+ if (route === 'program_history') {
2591
+ contract.push('Answer contract: program change history.');
2592
+ contract.push(' Answer only from the durable program change history evidence.');
2593
+ contract.push(' If the user asks to undo, revert, restore, or change it back, say automatic restore is not available yet in this slice.');
2594
+ contract.push(' Identify the likely target change by date, summary, kind, and id when the evidence supports it.');
2595
+ contract.push(' Do not claim the program was changed, restored, reverted, or updated.');
2596
+ }
2597
+
2390
2598
  if (route === 'recent_session' && sessionObservationComparisons.length > 0) {
2391
2599
  contract.push('Answer contract: current session plus durable observations.');
2392
2600
  contract.push(' Say what improved in the current session first.');
@@ -2581,7 +2789,7 @@ function humanObservationEvidenceRows(observation) {
2581
2789
 
2582
2790
  function appendCoachPatternToRecheck(lines, observation) {
2583
2791
  lines.push('');
2584
- lines.push('Coach pattern I previously flagged; re-check it before answering:');
2792
+ lines.push('Training pattern I previously flagged; re-check it before answering:');
2585
2793
  lines.push(` Pattern: ${observation.title}`);
2586
2794
  lines.push(` pattern-id=${observation.id}; kind=${observation.kind}; confidence=${observation.confidence.toFixed(2)}`);
2587
2795
  if (observation.windowStart || observation.windowEnd) {
@@ -2593,14 +2801,12 @@ function appendCoachPatternToRecheck(lines, observation) {
2593
2801
  observation.sourceExercise ? `exercise=${observation.sourceExercise}` : null
2594
2802
  ].filter(Boolean).join('; ')}`);
2595
2803
  }
2596
- lines.push(` Facts: ${observation.summary}`);
2804
+ lines.push(` Evidence: ${observation.summary}`);
2597
2805
  if (observation.interpretationText) {
2598
- const tag = observation.interpretationKind ? ` [${observation.interpretationKind}]` : '';
2599
- lines.push(` Interpretation${tag}: ${observation.interpretationText}`);
2806
+ lines.push(` Coach read: ${observation.interpretationText}`);
2600
2807
  }
2601
2808
  if (observation.actionText) {
2602
- const tag = observation.recommendationKind ? ` [${observation.recommendationKind}]` : '';
2603
- lines.push(` Recommendation${tag}: ${observation.actionText}`);
2809
+ lines.push(` Next move: ${observation.actionText}`);
2604
2810
  }
2605
2811
  if (observation.outcomeStatus || observation.outcomeObservedAt || observation.outcomeNotes) {
2606
2812
  lines.push(` Stored outcome: ${[
@@ -2845,7 +3051,7 @@ export function askObservationFollowUpContext(snapshot, question, observation, {
2845
3051
  pushAskContextHeader(lines, snapshot, today);
2846
3052
  appendCoachPatternToRecheck(lines, target);
2847
3053
  lines.push('');
2848
- lines.push('Follow-up voice rule: answer as the coach who flagged the pattern. Never use artifact phrases like "the coach observation", "this note", "the card", or "this system". Use first-person coaching language such as "I flagged...", "your data shows...", or "I would change...".');
3054
+ lines.push('Follow-up voice rule: answer as the coach who flagged the pattern. Do not name the product artifact, card, note, system, or tooling. Use first-person coaching language such as "I flagged...", "your data shows...", or "I would change...".');
2849
3055
  lines.push('Outcome rule: treat the prior pattern as a hypothesis. If current evidence still supports it, say it is still active. If the evidence is improving but not clean, say it is partly true. If current evidence contradicts it or it is stale, say you would retire it now before giving advice.');
2850
3056
  appendSessionObservationComparisonsBeforeExcludeNote(lines, comparisonTool.rows, exclude);
2851
3057
  for (const tool of [scoreTool, recentTool, exerciseTool, readinessTool, bodyWeightTool].filter(Boolean)) {
@@ -2954,7 +3160,7 @@ export function askMissingObservationFollowUpContext(snapshot, _question, reques
2954
3160
  const lines = [];
2955
3161
  pushAskContextHeader(lines, snapshot, today);
2956
3162
  lines.push('');
2957
- lines.push('Requested coach observation follow-up:');
3163
+ lines.push('Requested training-pattern follow-up:');
2958
3164
  lines.push(` observation-id=${String(requestedObservation?.id ?? '').trim() || 'unknown'}; status=missing_current_server_observation`);
2959
3165
  lines.push(' The client requested an observation follow-up, but the observation did not match current server observations.');
2960
3166
  if (followUpIntent === 'successor_plan') {
@@ -3067,6 +3273,14 @@ export function askRoutedContext(snapshot, question, { exclude = new Set(), coac
3067
3273
  built = buildExerciseProgressSummaryAskContext(contextSnapshot, namedExerciseItems, { exclude, since, today });
3068
3274
  } else if (route === 'program_progress') {
3069
3275
  built = buildProgramProgressAskContext(contextSnapshot, { exclude, since, today });
3276
+ } else if (route === 'program_history') {
3277
+ built = buildProgramHistoryAskContext(contextSnapshot, { exclude, today });
3278
+ } else if (route === 'program_schedule_action') {
3279
+ built = buildProgramScheduleActionAskContext(contextSnapshot, question, {
3280
+ exclude,
3281
+ today,
3282
+ scheduleContext: evidencePlan.intent?.deloadScheduleContext ?? null
3283
+ });
3070
3284
  } else if (route === 'training_profile') {
3071
3285
  built = buildTrainingProfileAskContext(contextSnapshot, { exclude, since, today });
3072
3286
  } else if (route === 'records') {
@@ -3171,7 +3385,8 @@ export function askRoutedContext(snapshot, question, { exclude = new Set(), coac
3171
3385
  ...(includedFacts.length > 0 ? ['coach_facts'] : []),
3172
3386
  ...(includedCoachObservationIds.length > 0 ? ['coach_observations'] : []),
3173
3387
  ...(sessionObservationComparisons.length > 0 ? ['session_observation_comparisons'] : [])
3174
- ]
3388
+ ],
3389
+ programScheduleActionStartDate: built.programScheduleActionStartDate
3175
3390
  };
3176
3391
  const finalizedEvidencePlan = finalizeEvidencePlan(evidencePlan, tools);
3177
3392
  const toolMetadata = askToolMetadata(tools, provenance, { requiredTools: finalizedEvidencePlan.requiredTools });
@@ -3211,6 +3426,7 @@ export function askRoutedContext(snapshot, question, { exclude = new Set(), coac
3211
3426
  includedCoachObservationIds,
3212
3427
  coachObservationIds: includedCoachObservationIds,
3213
3428
  currentSessionIds,
3429
+ ...(built.programScheduleActionStartDate ? { programScheduleActionStartDate: built.programScheduleActionStartDate } : {}),
3214
3430
  sessionObservationComparisons,
3215
3431
  evidencePlan: finalizedEvidencePlan,
3216
3432
  contextBundle: contextBundleForMetadata(contextBundle),
@@ -0,0 +1,34 @@
1
+ import { SECURITY_PREAMBLE } from './prompt-security.js';
2
+ import {
3
+ ASK_COACH_INTRO,
4
+ ASK_CORE_RULES,
5
+ ASK_DEFENSIVE_RULES,
6
+ ASK_EXPANSIVE_RULES,
7
+ ASK_STRUCTURED_RULES,
8
+ COACH_SOUL
9
+ } from './coach-prompt-layers.js';
10
+
11
+ export function composeAskPrompt(profile = 'expansive') {
12
+ const profileRules = profile === 'structured'
13
+ ? `${ASK_DEFENSIVE_RULES}\n\n${ASK_STRUCTURED_RULES}`
14
+ : profile === 'defensive'
15
+ ? ASK_DEFENSIVE_RULES
16
+ : ASK_EXPANSIVE_RULES;
17
+ return `${SECURITY_PREAMBLE}${ASK_COACH_INTRO}
18
+
19
+ ${COACH_SOUL}
20
+
21
+ ${ASK_CORE_RULES}
22
+
23
+ ${profileRules}`;
24
+ }
25
+
26
+ export const ASK_PROMPT = composeAskPrompt('expansive');
27
+ export const ASK_DEFENSIVE_PROMPT = composeAskPrompt('defensive');
28
+ export const ASK_STRUCTURED_PROMPT = composeAskPrompt('structured');
29
+
30
+ export function askPromptForResponseProfile(responseProfile) {
31
+ if (responseProfile === 'structured') return ASK_STRUCTURED_PROMPT;
32
+ if (responseProfile === 'defensive') return ASK_DEFENSIVE_PROMPT;
33
+ return ASK_PROMPT;
34
+ }