incremnt 0.8.7 → 0.8.8

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 (60) hide show
  1. package/README.md +2 -1
  2. package/SKILL.md +2 -1
  3. package/package.json +4 -1
  4. package/src/ask-answer-verifier.js +23 -2
  5. package/src/ask-coach/contexts.js +1472 -0
  6. package/src/ask-coach/evidence-plan.js +342 -0
  7. package/src/ask-coach/observations.js +8 -0
  8. package/src/ask-coach/orchestrator.js +1641 -0
  9. package/src/ask-coach/renderers.js +4 -0
  10. package/src/ask-coach/routing.js +610 -0
  11. package/src/ask-coach/structured-response.js +5 -0
  12. package/src/ask-coach-routing.js +4 -0
  13. package/src/ask-coach.js +26 -3506
  14. package/src/ask-replay.js +70 -4
  15. package/src/ask-starter-prompts.js +206 -0
  16. package/src/auth.js +26 -0
  17. package/src/coach-advice.js +32 -0
  18. package/src/coach-facts.js +214 -0
  19. package/src/coach-prompt-layers.js +26 -30
  20. package/src/contract.js +27 -7
  21. package/src/exercise-aliases.js +6 -0
  22. package/src/format.js +17 -0
  23. package/src/index.js +1 -1
  24. package/src/lib.js +41 -36
  25. package/src/mcp.js +1 -1
  26. package/src/openrouter.js +279 -161
  27. package/src/plan-changeset.js +14 -8
  28. package/src/program-draft.js +97 -1
  29. package/src/prompt-changelog.js +16 -0
  30. package/src/prompt-security.js +34 -3
  31. package/src/promptfoo-evals.js +2 -0
  32. package/src/promptfoo-langfuse-scores.js +10 -0
  33. package/src/prompts/ask.js +22 -0
  34. package/src/prompts/checkpoint.js +14 -0
  35. package/src/prompts/coach-facts.js +24 -0
  36. package/src/prompts/cycle.js +23 -0
  37. package/src/prompts/starter-graph.js +7 -0
  38. package/src/prompts/vitals.js +9 -0
  39. package/src/prompts/weekly-checkin.js +20 -0
  40. package/src/prompts/workout.js +47 -0
  41. package/src/queries/coach-observations.js +8 -0
  42. package/src/queries/coach-read-tools.js +6 -0
  43. package/src/queries/commands.js +3 -0
  44. package/src/queries/common.js +9 -0
  45. package/src/queries/core.js +6354 -0
  46. package/src/queries/exercise-identity.js +6 -0
  47. package/src/queries/health.js +12 -0
  48. package/src/queries/programs.js +17 -0
  49. package/src/queries/records-progress.js +10 -0
  50. package/src/queries/sessions.js +12 -0
  51. package/src/queries/weekly.js +5 -0
  52. package/src/queries.js +10 -6283
  53. package/src/remote.js +51 -0
  54. package/src/score-context.js +58 -3
  55. package/src/summary-evals.js +201 -45
  56. package/src/sync-service.js +1118 -104
  57. package/src/training-language-public-terms.json +97 -0
  58. package/src/training-language.js +94 -0
  59. package/src/transport.js +7 -1
  60. package/src/validate.js +11 -1
package/src/remote.js CHANGED
@@ -335,6 +335,7 @@ function endpointForCommand(baseUrl, normalizedCommand, options) {
335
335
  if (options.from) url.searchParams.set('from', options.from);
336
336
  if (options.to) url.searchParams.set('to', options.to);
337
337
  if (options.limit) url.searchParams.set('limit', options.limit);
338
+ if (options['include-signals']) url.searchParams.set('includeSignals', 'true');
338
339
  return url;
339
340
  }
340
341
  case 'coach-observations-current': {
@@ -486,6 +487,20 @@ export async function buildWriteRequest(commandId, options, sessionState) {
486
487
  }
487
488
 
488
489
  switch (commandId) {
490
+ case 'programs-create': {
491
+ if (!options.file) {
492
+ const error = new Error('--file is required for programs create.');
493
+ error.code = 'MISSING_OPTION';
494
+ throw error;
495
+ }
496
+ const raw = await fs.readFile(options.file, 'utf8');
497
+ const draft = JSON.parse(raw);
498
+ return {
499
+ method: 'POST',
500
+ url: resolveServiceUrl(baseUrl, '/cli/programs').toString(),
501
+ body: draft
502
+ };
503
+ }
489
504
  case 'programs-propose': {
490
505
  if (!options.file) {
491
506
  const error = new Error('--file is required for programs propose.');
@@ -632,6 +647,42 @@ export async function buildWriteRequest(commandId, options, sessionState) {
632
647
  }
633
648
 
634
649
  const remoteWriteCommandHandlers = {
650
+ 'programs-create': async (options, sessionState) => {
651
+ const baseUrl = sessionState.session?.transport?.baseUrl;
652
+ if (!baseUrl) throw notImplementedError();
653
+
654
+ const filePath = options.file;
655
+ if (!filePath) {
656
+ const error = new Error('--file is required for programs create.');
657
+ error.code = 'MISSING_OPTION';
658
+ throw error;
659
+ }
660
+
661
+ const raw = await fs.readFile(filePath, 'utf8');
662
+ const draft = JSON.parse(raw);
663
+
664
+ const endpoint = resolveServiceUrl(baseUrl, '/cli/programs');
665
+ const response = await fetch(endpoint, {
666
+ method: 'POST',
667
+ headers: {
668
+ 'Content-Type': 'application/json',
669
+ Authorization: `Bearer ${sessionState.session?.auth?.accessToken ?? ''}`
670
+ },
671
+ body: JSON.stringify(draft)
672
+ });
673
+
674
+ if (response.status === 401) throw authenticationFailedError();
675
+ if (response.status === 403) await throwForbiddenResponse(response, sessionState, 'programs-create');
676
+ if (!response.ok) {
677
+ const payload = await response.json().catch(() => null);
678
+ const error = new Error(payload?.error ?? `Unexpected error (HTTP ${response.status}).`);
679
+ error.code = 'REMOTE_HTTP_ERROR';
680
+ throw error;
681
+ }
682
+
683
+ return response.json();
684
+ },
685
+
635
686
  'programs-propose': async (options, sessionState) => {
636
687
  const baseUrl = sessionState.session?.transport?.baseUrl;
637
688
  if (!baseUrl) throw notImplementedError();
@@ -4,6 +4,11 @@
4
4
  // They are NOT persisted — pure projections of (current snapshot, previous
5
5
  // snapshot) into agent-friendly explanatory shape. See GitHub issue #498.
6
6
 
7
+ import {
8
+ publicTrainingLanguageViolations,
9
+ replacePublicTrainingLanguageTerms
10
+ } from './training-language.js';
11
+
7
12
  // Score bands. Inclusive lower bound, exclusive upper bound (except 'peak').
8
13
  // 0..40 weak
9
14
  // 40..60 developing
@@ -29,7 +34,7 @@ export function computeScoreBand(score) {
29
34
  // Component-keyed action templates surfaced as `recommendedNextActions` for
30
35
  // each top-2 negative driver. Keep these short, imperative, single-line.
31
36
  const COMPONENT_ACTIONS = {
32
- coverage: 'Add the missing muscle work before adding more of what is already covered.',
37
+ coverage: 'Train the missing muscles before adding more of what is already trained.',
33
38
  recovery: 'Go easier next session unless sleep and soreness feel clearly better.',
34
39
  stimulus: 'Put the short muscle groups earlier next time.',
35
40
  execution: 'Follow the planned work more closely next session.',
@@ -56,8 +61,12 @@ function actionForDriver(driver) {
56
61
 
57
62
  function driverDisplayMessage(driver) {
58
63
  if (!driver || typeof driver !== 'object') return null;
59
- if (typeof driver.message === 'string' && driver.message.trim()) return driver.message;
60
- if (typeof driver.label === 'string' && driver.label.trim()) return driver.label;
64
+ if (typeof driver.message === 'string' && driver.message.trim()) {
65
+ return replacePublicTrainingLanguageTerms(driver.message);
66
+ }
67
+ if (typeof driver.label === 'string' && driver.label.trim()) {
68
+ return replacePublicTrainingLanguageTerms(driver.label);
69
+ }
61
70
  return null;
62
71
  }
63
72
 
@@ -152,6 +161,22 @@ export function enrichScoreSnapshots(snapshots) {
152
161
  });
153
162
  }
154
163
 
164
+ function includeSignalsOption(options = {}) {
165
+ const value = options.includeSignals ?? options['include-signals'];
166
+ return value === true || value === 'true' || value === '1';
167
+ }
168
+
169
+ export function compactScoreHistorySnapshots(snapshots, options = {}) {
170
+ if (!Array.isArray(snapshots)) return [];
171
+ if (includeSignalsOption(options)) return snapshots;
172
+ return snapshots.map((snapshot) => {
173
+ if (!snapshot || typeof snapshot !== 'object') return snapshot;
174
+ const compact = { ...snapshot };
175
+ delete compact.signals;
176
+ return compact;
177
+ });
178
+ }
179
+
155
180
  export function enrichScoreSnapshot(current, previous) {
156
181
  if (!current || typeof current !== 'object') return current;
157
182
 
@@ -187,3 +212,33 @@ export function enrichScoreSnapshot(current, previous) {
187
212
  enriched.summaryText = computeSummaryText(enriched);
188
213
  return enriched;
189
214
  }
215
+
216
+ export function scoreContextPublicLanguageViolations(enriched) {
217
+ if (!enriched || typeof enriched !== 'object') return [];
218
+ const publicTexts = [];
219
+ if (typeof enriched.summaryText === 'string') {
220
+ publicTexts.push({ field: 'summaryText', text: enriched.summaryText });
221
+ }
222
+ if (Array.isArray(enriched.recommendedNextActions)) {
223
+ enriched.recommendedNextActions.forEach((action, index) => {
224
+ if (typeof action?.driverMessage === 'string') {
225
+ publicTexts.push({
226
+ field: `recommendedNextActions[${index}].driverMessage`,
227
+ text: action.driverMessage
228
+ });
229
+ }
230
+ if (typeof action?.action === 'string') {
231
+ publicTexts.push({
232
+ field: `recommendedNextActions[${index}].action`,
233
+ text: action.action
234
+ });
235
+ }
236
+ });
237
+ }
238
+
239
+ return publicTexts
240
+ .filter(({ text }) => text.trim())
241
+ .flatMap(({ field, text }) => (
242
+ publicTrainingLanguageViolations(text).map((violation) => ({ ...violation, field }))
243
+ ));
244
+ }
@@ -17,6 +17,7 @@ import { formatIncrementScorePrelude, isScoreQuestion } from './score-prelude.js
17
17
  import {
18
18
  AI_PROMPT_VERSIONS,
19
19
  generateAskAnswer,
20
+ generateAskAnswerStarterGraph,
20
21
  generateCheckpointSummary,
21
22
  generateCoachingSummary,
22
23
  generateVitalsSummary,
@@ -25,7 +26,14 @@ import {
25
26
  import { computeScoreBand } from './score-context.js';
26
27
  import { stripXMLTagBlocks } from './prompt-security.js';
27
28
  import { extractAskProgramDraft, hasProgramDraftBlock } from './program-draft.js';
29
+ import { extractCoachAdvice } from './coach-advice.js';
28
30
  import { findAskAnswerExerciseMentions, verifyAskAnswer } from './ask-answer-verifier.js';
31
+ import {
32
+ STARTER_GRAPH_VERSION,
33
+ applyStarterPromptStructuredIntentHint,
34
+ starterPromptToolPlan
35
+ } from './ask-starter-prompts.js';
36
+ import { publicTrainingLanguageViolations } from './training-language.js';
29
37
 
30
38
  const __filename = fileURLToPath(import.meta.url);
31
39
  const __dirname = path.dirname(__filename);
@@ -120,8 +128,9 @@ export function buildSummaryEvalContext(snapshot, testCase) {
120
128
  const question = testCase.context?.question ?? testCase.question ?? '';
121
129
  const today = testCase.context?.today ?? testCase.today ?? null;
122
130
  const history = Array.isArray(testCase.context?.history) ? testCase.context.history : [];
131
+ const routeToday = today ? new Date(today) : new Date();
123
132
  const routed = question
124
- ? askRoutedContext(snapshot, question, { exclude: new Set(testCase.exclude ?? []), history, today: today ?? new Date() })
133
+ ? askRoutedContext(snapshot, question, { exclude: new Set(testCase.exclude ?? []), history, today: routeToday })
125
134
  : null;
126
135
  // Mirror production: the live /cli/ask path prepends the Increment Score
127
136
  // prelude to the routed context. Including it here means a live eval feeds
@@ -132,10 +141,11 @@ export function buildSummaryEvalContext(snapshot, testCase) {
132
141
  const routedContext = routed?.context ?? null;
133
142
  const trainingData = testCase.context?.trainingData
134
143
  ?? (prelude && routedContext ? `${prelude}\n\n${routedContext}` : (routedContext ?? prelude));
144
+ const routedMetadata = applyStarterPromptEvalMetadata(routed?.metadata ?? null, testCase);
135
145
  return {
136
146
  ...(testCase.context ?? {}),
137
147
  trainingData,
138
- routedMetadata: routed?.metadata ?? null
148
+ routedMetadata
139
149
  };
140
150
  }
141
151
  case 'scoreCommentary':
@@ -152,25 +162,101 @@ function summaryEvalGenerationMetadata(result) {
152
162
  model: result.model,
153
163
  durationMs: result.durationMs,
154
164
  fallback: result.fallback,
165
+ promptSurface: result.promptSurface,
166
+ promptVersion: result.promptVersion,
167
+ graphVersion: result.graph?.version,
168
+ starterPromptFamily: result.graph?.family,
169
+ graphNodes: result.graph?.nodes,
170
+ graphToolNames: result.graph?.toolNames,
155
171
  langfuseTraceId: result.langfuseTraceId,
156
172
  langfuseObservationId: result.langfuseObservationId
157
173
  }).filter(([, value]) => value !== undefined && value !== null)
158
174
  );
159
175
  }
160
176
 
177
+ function evalEvidenceLabel(section, toolName) {
178
+ const cleaned = String(section || toolName || 'evidence')
179
+ .replace(/^observation_/, '')
180
+ .replace(/_/g, ' ')
181
+ .trim();
182
+ return cleaned ? cleaned.replace(/\b\w/g, (char) => char.toUpperCase()) : 'Evidence';
183
+ }
184
+
185
+ function applyStarterPromptEvalMetadata(metadata, testCase) {
186
+ const starterPrompt = testCase.starterPrompt;
187
+ if (!starterPrompt?.family || !metadata || typeof metadata !== 'object') return metadata;
188
+
189
+ const hintedMetadata = applyStarterPromptStructuredIntentHint(metadata, starterPrompt);
190
+ const plannedCalls = starterPromptToolPlan(starterPrompt);
191
+ if (plannedCalls.length === 0) return metadata;
192
+
193
+ const names = plannedCalls.map((call) => call.name).filter(Boolean);
194
+ const toolParams = Object.fromEntries(plannedCalls.map((call) => [call.name, call.params ?? {}]));
195
+ const provenanceEntries = plannedCalls.map((call) => ({
196
+ section: call.name,
197
+ toolName: call.name,
198
+ sourceIds: []
199
+ }));
200
+ const contextBundle = hintedMetadata.contextBundle && typeof hintedMetadata.contextBundle === 'object'
201
+ ? hintedMetadata.contextBundle
202
+ : null;
203
+
204
+ return {
205
+ ...hintedMetadata,
206
+ starterPromptFamily: starterPrompt.family,
207
+ graphVersion: STARTER_GRAPH_VERSION,
208
+ graphNodes: ['prepare_evidence', 'synthesize'],
209
+ graphToolNames: names,
210
+ toolsUsed: [...new Set([...(hintedMetadata.toolsUsed ?? []), ...names])],
211
+ toolParams: {
212
+ ...(hintedMetadata.toolParams ?? {}),
213
+ ...toolParams
214
+ },
215
+ evidencePlan: hintedMetadata.evidencePlan && typeof hintedMetadata.evidencePlan === 'object'
216
+ ? {
217
+ ...hintedMetadata.evidencePlan,
218
+ executedTools: [...new Set([...(hintedMetadata.evidencePlan.executedTools ?? []), ...names])]
219
+ }
220
+ : hintedMetadata.evidencePlan,
221
+ provenance: [...(hintedMetadata.provenance ?? []), ...provenanceEntries],
222
+ ...(contextBundle
223
+ ? {
224
+ contextBundle: {
225
+ ...contextBundle,
226
+ executedTools: [...new Set([...(contextBundle.executedTools ?? []), ...names])],
227
+ evidenceUsed: [
228
+ ...(contextBundle.evidenceUsed ?? []),
229
+ ...provenanceEntries.map((entry) => ({
230
+ label: evalEvidenceLabel(entry.section, entry.toolName),
231
+ section: entry.section,
232
+ toolName: entry.toolName,
233
+ sourceTimestamp: null,
234
+ sourceIds: [],
235
+ noteSourceIds: [],
236
+ missingDataFlags: []
237
+ }))
238
+ ]
239
+ }
240
+ }
241
+ : {})
242
+ };
243
+ }
244
+
161
245
  function buildAskEvalStructuredMetadata(testCase, context, output) {
162
246
  if (testCase.surface !== 'ask') return {};
163
247
  const parsedAsk = extractAskProgramDraft(output, {
164
248
  canonicalizeExerciseName: canonicalExerciseName
165
249
  });
166
- const answer = stripXMLTagBlocks(parsedAsk.answerText);
250
+ const adviceParsed = extractCoachAdvice(parsedAsk.answerText);
251
+ const answer = stripXMLTagBlocks(adviceParsed.answerText);
167
252
  const question = context?.question ?? testCase.context?.question ?? testCase.question ?? '';
168
253
  const routingMetadata = context?.routedMetadata ?? null;
169
254
  return {
170
255
  routingMetadata,
171
256
  structured: buildAskStructuredResponse(answer, routingMetadata ?? {}, {
172
257
  programDraft: askStructuredProgramDraft(parsedAsk, routingMetadata),
173
- question
258
+ question,
259
+ coachAdvice: adviceParsed.coachAdvice ?? testCase.coachAdviceCandidate ?? null
174
260
  })
175
261
  };
176
262
  }
@@ -215,13 +301,24 @@ export async function generateSummaryEvalOutputWithMetadata(testCase, context, s
215
301
  const trainingContext = context.trainingData
216
302
  ?? askRoutedContext(snapshot, question, { exclude: new Set(testCase.exclude ?? []) }).context
217
303
  ?? askContext(snapshot, { exclude: new Set(testCase.exclude ?? []) });
218
- result = await generateAskAnswer(trainingContext, question, {
219
- apiKey,
220
- history: context.history ?? [],
221
- tone: context.tone,
222
- model: context.model,
223
- routingMetadata: context.routedMetadata ?? undefined
224
- });
304
+ result = testCase.starterPrompt?.family
305
+ ? await generateAskAnswerStarterGraph(trainingContext, question, {
306
+ apiKey,
307
+ history: context.history ?? [],
308
+ tone: context.tone,
309
+ model: context.model,
310
+ snapshot,
311
+ starterPrompt: testCase.starterPrompt,
312
+ routingMetadata: context.routedMetadata ?? undefined,
313
+ exclude: testCase.exclude ?? []
314
+ })
315
+ : await generateAskAnswer(trainingContext, question, {
316
+ apiKey,
317
+ history: context.history ?? [],
318
+ tone: context.tone,
319
+ model: context.model,
320
+ routingMetadata: context.routedMetadata ?? undefined
321
+ });
225
322
  break;
226
323
  }
227
324
  default:
@@ -1185,7 +1282,10 @@ function hasUnqualifiedDeclineLanguage(window) {
1185
1282
  const text = normalizeText(window);
1186
1283
  const decline = /\b(drop(?:ped|ping|s)?(?: off)?|drop-off|declin(?:e|ed|ing)|regress(?:ed|ion|ing)?|fell|fall(?:ing)?|decreas(?:e|ed|ing)|lower|worse|slid|slipped)\b/i;
1187
1284
  if (!decline.test(text)) return false;
1188
- if (/\b(?:no|not|isn'?t|wasn'?t|without|rather than)\b.{0,45}\b(drop(?:ped|ping|s)?(?: off)?|drop-off|declin(?:e|ed|ing)?|decreas(?:e|ed|ing)?|regress(?:ed|ion|ing)?|fall(?:ing)?|fell|lower|worse|slid|slipped)\b/i.test(text)) return false;
1285
+ if (/\b(?:volume|tonnage|work rate|work done|weekly work)\b.{0,80}\b(drop(?:ped|ping|s)?(?: off)?|drop-off|declin(?:e|ed|ing)?|decreas(?:e|ed|ing)?|fell|fall(?:ing)?|lower|down|dip(?:ped)?|pullback)\b/i.test(text)) return false;
1286
+ if (/\b(drop(?:ped|ping|s)?(?: off)?|drop-off|declin(?:e|ed|ing)?|decreas(?:e|ed|ing)?|fell|fall(?:ing)?|lower|down|dip(?:ped)?|pullback)\b.{0,80}\b(?:volume|tonnage|work rate|work done|weekly work)\b/i.test(text)) return false;
1287
+ if (/\b(?:no|not|isn'?t|wasn'?t|weren'?t|doesn'?t|don'?t|didn'?t|without|rather than)\b.{0,60}\b(drop(?:ped|ping|s)?(?: off)?|drop-off|declin(?:e|ed|ing)?|decreas(?:e|ed|ing)?|regress(?:ed|ion|ing)?|fall(?:ing)?|fell|lower|worse|slid|slipped)\b/i.test(text)) return false;
1288
+ if (/\b(drop(?:ped|ping|s)?(?: off)?|drop-off|declin(?:e|ed|ing)?|decreas(?:e|ed|ing)?|regress(?:ed|ion|ing)?|fall(?:ing)?|fell|lower|worse|slid|slipped)\b.{0,60}\b(?:no|not|isn'?t|wasn'?t|weren'?t|doesn'?t|don'?t|didn'?t)\b/i.test(text)) return false;
1189
1289
  if (/\b(?:rep|reps)\b.{0,20}\b(drop(?:ped|ping|s)?(?: off)?|drop-off|slip(?:ped|ping)?|fell|fall(?:ing)?|lower|declin(?:e|ed|ing)?|decreas(?:e|ed|ing)?|worse)\b/i.test(text)) return false;
1190
1290
  if (/\b(drop(?:ped|ping|s)?(?: off)?|drop-off|slip(?:ped|ping)?|fell|fall(?:ing)?|lower|declin(?:e|ed|ing)?|decreas(?:e|ed|ing)?|worse)\b.{0,20}\b(?:rep|reps)\b/i.test(text)) return false;
1191
1291
  return true;
@@ -1338,13 +1438,26 @@ export function evaluateAskScoreVoice(output, testCase) {
1338
1438
  hits.add('day-over-day delta number');
1339
1439
  }
1340
1440
 
1341
- return {
1441
+ const rawScoreResult = {
1342
1442
  key: 'ask_score_voice',
1343
1443
  passed: hits.size === 0,
1344
1444
  reason: hits.size === 0
1345
1445
  ? 'Ask answer does not recite raw Increment Score component sub-scores.'
1346
1446
  : `Ask answer recites raw score internals: ${[...hits].join(', ')}. Speak in training reality, not raw sub-scores.`
1347
1447
  };
1448
+
1449
+ if (!rawScoreResult.passed) return rawScoreResult;
1450
+
1451
+ const publicLanguageHits = publicTrainingLanguageViolations(text).map((violation) => violation.term);
1452
+ if (publicLanguageHits.length > 0) {
1453
+ return {
1454
+ key: 'ask_score_voice',
1455
+ passed: false,
1456
+ reason: `Ask answer uses internal training language: ${publicLanguageHits.join(', ')}. Speak in normal training language.`
1457
+ };
1458
+ }
1459
+
1460
+ return rawScoreResult;
1348
1461
  }
1349
1462
 
1350
1463
  function relevantSessionsForStaleness(snapshot, testCase) {
@@ -1591,8 +1704,10 @@ function outputCallsStaleEvidenceRecent(outputText, row) {
1591
1704
  const normalized = normalizeText(outputText);
1592
1705
  const claimsRecent = /\brecent(?:ly)?\b/i.test(normalized);
1593
1706
  if (!claimsRecent) return false;
1594
- const explicitlyNotRecent = /\b(?:not|isn'?t|wasn'?t|no longer)\s+(?:a\s+)?recent\b/i.test(normalized)
1595
- || /\brecent\b.{0,20}\b(?:not|isn'?t|wasn'?t)\b/i.test(normalized);
1707
+ const explicitlyNotRecent = /\b(?:not|isn'?t|wasn'?t|no longer)\s+(?:\w+\s+){0,4}?recent\b/i.test(normalized)
1708
+ || /\b(?:no|not enough|without|missing|lack(?:ing)?|insufficient|don'?t have enough|do not have enough)\s+(?:\w+\s+){0,4}?recent\s+(?:work|sessions?|data|logs?|evidence|history)\b/i.test(normalized)
1709
+ || /\brecent\b.{0,45}\b(?:not|isn'?t|wasn'?t|stale|old|older|latest logged|last logged)\b/i.test(normalized)
1710
+ || /\b(?:only|latest|last)\s+logged\b.{0,80}\b(?:stale|old|older|days?\s+ago)\b/i.test(normalized);
1596
1711
  if (explicitlyNotRecent) return false;
1597
1712
  const daysAgo = Number(row?.daysAgo);
1598
1713
  return !Number.isFinite(daysAgo) || !new RegExp(`\\b${daysAgo}\\s+days?\\s+ago\\b`, 'i').test(normalized);
@@ -1746,12 +1861,19 @@ function evaluateAskEvidencePlan(_output, context, testCase) {
1746
1861
  }
1747
1862
  }
1748
1863
 
1749
- for (const key of ['requiredTools', 'optionalTools', 'executedTools', 'evidenceGaps']) {
1864
+ for (const key of ['requiredTools', 'optionalTools', 'executedTools', 'executedDynamicTools', 'evidenceGaps']) {
1750
1865
  if (Array.isArray(expected[key]) && !arrayEquals(plan[key], expected[key])) {
1751
1866
  failures.push(`Expected evidencePlan.${key} to equal ${expected[key].join(', ')}; got ${(plan[key] ?? []).join(', ')}.`);
1752
1867
  }
1753
1868
  }
1754
1869
 
1870
+ if (Array.isArray(expected.dynamicTools)) {
1871
+ const actualDynamicTools = (plan.dynamicTools ?? []).map((item) => item.toolName);
1872
+ if (!arrayEquals(actualDynamicTools, expected.dynamicTools)) {
1873
+ failures.push(`Expected evidencePlan.dynamicTools to equal ${expected.dynamicTools.join(', ')}; got ${actualDynamicTools.join(', ')}.`);
1874
+ }
1875
+ }
1876
+
1755
1877
  if (Array.isArray(expected.excludedExecutedTools)) {
1756
1878
  const executed = new Set(plan.executedTools ?? []);
1757
1879
  const hits = expected.excludedExecutedTools.filter((toolName) => executed.has(toolName));
@@ -1955,6 +2077,35 @@ function evaluateAskStructuredResponse(_output, context, testCase, structured) {
1955
2077
  failures.push(`Expected programDraft present=${expected.programDraftPresent}, got ${hasProgramDraft}.`);
1956
2078
  }
1957
2079
  }
2080
+
2081
+ if (typeof expected.coachAdvicePresent === 'boolean') {
2082
+ const hasCoachAdvice = structured.coachAdvice != null;
2083
+ if (hasCoachAdvice !== expected.coachAdvicePresent) {
2084
+ failures.push(`Expected coachAdvice present=${expected.coachAdvicePresent}, got ${hasCoachAdvice}.`);
2085
+ }
2086
+ }
2087
+
2088
+ if (expected.coachAdviceKind && structured.coachAdvice?.kind !== expected.coachAdviceKind) {
2089
+ failures.push(`Expected coachAdvice kind ${expected.coachAdviceKind}, got ${structured.coachAdvice?.kind ?? 'null'}.`);
2090
+ }
2091
+
2092
+ if (expected.coachAdviceTarget && structured.coachAdvice?.target !== expected.coachAdviceTarget) {
2093
+ failures.push(`Expected coachAdvice target ${expected.coachAdviceTarget}, got ${structured.coachAdvice?.target ?? 'null'}.`);
2094
+ }
2095
+
2096
+ requireStructuredStrings(
2097
+ structured.coachAdvice?.evidenceRefs,
2098
+ expected.requiredCoachAdviceEvidenceRefs,
2099
+ 'coachAdvice evidence ref(s)',
2100
+ failures
2101
+ );
2102
+
2103
+ requireStructuredStrings(
2104
+ structured.coachAdvice?.outcomeCriteria,
2105
+ expected.requiredCoachAdviceOutcomeCriteria,
2106
+ 'coachAdvice outcome criteria',
2107
+ failures
2108
+ );
1958
2109
  }
1959
2110
 
1960
2111
  return {
@@ -2278,42 +2429,47 @@ export function evaluateSummaryOutputFromSnapshot(testCase, snapshot, output) {
2278
2429
  ? extractAskProgramDraft(output, { canonicalizeExerciseName: canonicalExerciseName })
2279
2430
  : null;
2280
2431
  const visibleOutput = parsedAsk
2281
- ? stripXMLTagBlocks(parsedAsk.answerText)
2432
+ ? parsedAsk.answerText
2282
2433
  : output;
2434
+ const adviceParsed = testCase.surface === 'ask'
2435
+ ? extractCoachAdvice(visibleOutput)
2436
+ : { answerText: visibleOutput, coachAdvice: null };
2437
+ const finalVisibleOutput = stripXMLTagBlocks(adviceParsed.answerText);
2283
2438
  const structuredAsk = testCase.surface === 'ask'
2284
- ? buildAskStructuredResponse(visibleOutput, context.routedMetadata ?? {}, {
2439
+ ? buildAskStructuredResponse(finalVisibleOutput, context.routedMetadata ?? {}, {
2285
2440
  programDraft: askStructuredProgramDraft(structuredParsedAsk, context.routedMetadata),
2286
- question: context.question ?? testCase.context?.question ?? testCase.question ?? ''
2441
+ question: context.question ?? testCase.context?.question ?? testCase.question ?? '',
2442
+ coachAdvice: adviceParsed.coachAdvice ?? testCase.coachAdviceCandidate ?? null
2287
2443
  })
2288
2444
  : null;
2289
2445
 
2290
2446
  const checks = [
2291
- evaluateNoInsight(visibleOutput, testCase),
2292
- evaluateShape(visibleOutput, testCase, context),
2293
- evaluateRequiredMentions(visibleOutput, testCase),
2294
- evaluateAnyOfMentions(visibleOutput, testCase),
2295
- evaluateForbiddenPhrases(visibleOutput, testCase),
2296
- evaluateForbiddenMentions(visibleOutput, testCase),
2297
- evaluateExerciseMentions(visibleOutput, snapshot, context, testCase.surface, testCase),
2298
- evaluateWorkoutClaims(visibleOutput, context, testCase),
2299
- evaluateAskClaims(visibleOutput, snapshot, testCase, context),
2300
- evaluateAskDirectionalConsistency(visibleOutput, snapshot, testCase),
2301
- evaluateAskScoreVoice(visibleOutput, testCase),
2302
- evaluateAskSelfReference(visibleOutput, testCase),
2303
- evaluateAskVolunteeredScore(visibleOutput, testCase, context),
2304
- evaluateAskStaleness(visibleOutput, snapshot, testCase),
2305
- evaluateAskToolProvenance(visibleOutput, context, testCase, snapshot),
2306
- evaluateFormulaVersion(visibleOutput, snapshot, testCase),
2307
- evaluateAskEvidencePlan(visibleOutput, context, testCase),
2308
- evaluateAskMetadata(visibleOutput, context, testCase),
2309
- evaluateAskStructuredResponse(visibleOutput, context, testCase, structuredAsk),
2310
- evaluateScoreCommentaryAction(visibleOutput, context, testCase),
2311
- evaluateScoreCommentarySynthesis(visibleOutput, context, testCase),
2312
- evaluateScoreCommentaryExerciseInvention(visibleOutput, snapshot, context, testCase),
2313
- evaluateScoreCommentaryBand(visibleOutput, context, testCase),
2314
- evaluateScoreCommentaryTone(visibleOutput, testCase),
2315
- evaluateScoreCommentaryLength(visibleOutput, testCase),
2316
- evaluatePersonaMotivation(visibleOutput, context, testCase),
2447
+ evaluateNoInsight(finalVisibleOutput, testCase),
2448
+ evaluateShape(finalVisibleOutput, testCase, context),
2449
+ evaluateRequiredMentions(finalVisibleOutput, testCase),
2450
+ evaluateAnyOfMentions(finalVisibleOutput, testCase),
2451
+ evaluateForbiddenPhrases(finalVisibleOutput, testCase),
2452
+ evaluateForbiddenMentions(finalVisibleOutput, testCase),
2453
+ evaluateExerciseMentions(finalVisibleOutput, snapshot, context, testCase.surface, testCase),
2454
+ evaluateWorkoutClaims(finalVisibleOutput, context, testCase),
2455
+ evaluateAskClaims(finalVisibleOutput, snapshot, testCase, context),
2456
+ evaluateAskDirectionalConsistency(finalVisibleOutput, snapshot, testCase),
2457
+ evaluateAskScoreVoice(finalVisibleOutput, testCase),
2458
+ evaluateAskSelfReference(finalVisibleOutput, testCase),
2459
+ evaluateAskVolunteeredScore(finalVisibleOutput, testCase, context),
2460
+ evaluateAskStaleness(finalVisibleOutput, snapshot, testCase),
2461
+ evaluateAskToolProvenance(finalVisibleOutput, context, testCase, snapshot),
2462
+ evaluateFormulaVersion(finalVisibleOutput, snapshot, testCase),
2463
+ evaluateAskEvidencePlan(finalVisibleOutput, context, testCase),
2464
+ evaluateAskMetadata(finalVisibleOutput, context, testCase),
2465
+ evaluateAskStructuredResponse(finalVisibleOutput, context, testCase, structuredAsk),
2466
+ evaluateScoreCommentaryAction(finalVisibleOutput, context, testCase),
2467
+ evaluateScoreCommentarySynthesis(finalVisibleOutput, context, testCase),
2468
+ evaluateScoreCommentaryExerciseInvention(finalVisibleOutput, snapshot, context, testCase),
2469
+ evaluateScoreCommentaryBand(finalVisibleOutput, context, testCase),
2470
+ evaluateScoreCommentaryTone(finalVisibleOutput, testCase),
2471
+ evaluateScoreCommentaryLength(finalVisibleOutput, testCase),
2472
+ evaluatePersonaMotivation(finalVisibleOutput, context, testCase),
2317
2473
  evaluateProgramDraft(output, testCase, parsedAsk)
2318
2474
  ];
2319
2475