@isparling/engram-coach 0.1.0 → 0.2.0

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 (51) hide show
  1. package/README.md +85 -18
  2. package/SETUP.md +559 -0
  3. package/SKILL_PACK.md +75 -0
  4. package/analyses/catalog.md +257 -0
  5. package/analysis-tools/hrv-trend.ts +592 -0
  6. package/analysis-tools/migrate-structured-capture.ts +234 -0
  7. package/analysis-tools/race-context.ts +96 -0
  8. package/analysis-tools/stream-analyze.ts +1008 -0
  9. package/analysis-tools/tsb-predict.ts +117 -0
  10. package/capture-handler.ts +301 -0
  11. package/config.json.example +21 -0
  12. package/engram-coach-ambient-capture.ts +336 -0
  13. package/engram-coach-capture-types.ts +185 -0
  14. package/engram-coach-config.ts +268 -0
  15. package/engram-coach-domain.ts +7 -2
  16. package/engram-coach-keys.ts +189 -0
  17. package/engram-coach-materialization.ts +638 -0
  18. package/engram-coach-migration.ts +1078 -0
  19. package/engram-coach-pack.ts +17 -12
  20. package/engram-coach-presentation.ts +10 -1
  21. package/engram-coach-reconciliation.ts +305 -2
  22. package/engram-coach-structured-capture.ts +622 -0
  23. package/package.json +39 -6
  24. package/personas/aggressive-monitoring.md +121 -0
  25. package/personas/aggressive.json +85 -0
  26. package/personas/conservative-monitoring.md +133 -0
  27. package/personas/conservative.json +93 -0
  28. package/personas/polarized-monitoring.md +112 -0
  29. package/personas/polarized.json +72 -0
  30. package/personas/volume-monitoring.md +85 -0
  31. package/personas/volume.json +108 -0
  32. package/shared/retrieval.md +71 -0
  33. package/shared/setup.md +207 -0
  34. package/skills/.gitkeep +0 -0
  35. package/skills/adapt-plan/SKILL.md +263 -0
  36. package/skills/block-review/SKILL.md +275 -0
  37. package/skills/consult/SKILL.md +176 -0
  38. package/skills/intake/SKILL.md +315 -0
  39. package/skills/lactate-analyze/SKILL.md +230 -0
  40. package/skills/lessons-rollup/SKILL.md +196 -0
  41. package/skills/monitoring-rollup/SKILL.md +208 -0
  42. package/skills/race-analysis/SKILL.md +219 -0
  43. package/skills/season-retrospective/SKILL.md +200 -0
  44. package/skills/set-goal/SKILL.md +297 -0
  45. package/templates/base.md +55 -0
  46. package/templates/build-1.md +57 -0
  47. package/templates/build-2.md +62 -0
  48. package/templates/race-report.md +51 -0
  49. package/templates/race-specificity.md +62 -0
  50. package/templates/season-review.md +40 -0
  51. package/engram-coach-extractor.ts +0 -295
@@ -0,0 +1,40 @@
1
+ # {Season Name} — Season Review
2
+ **Span:** {start_date} → {end_date} **Goal at season start:** {from intake}
3
+ **Persona:** {start} → {end if changed} **Outcome:** {one-line outcome vs goal}
4
+
5
+ ## Season Arc
6
+ {Narrative: how blocks connected, where load came from, peak fitness window, taper into target event. Reference CTL trajectory milestones from Phase 1 wellness data.}
7
+
8
+ ## Block Sequence Summary
9
+ | Block | Dates | Peak CTL | Key outcome |
10
+ |---|---|---|---|
11
+ {One row per block. Source: each block's SUMMARY.md.}
12
+
13
+ ## Race Outcomes
14
+ | Date | Event | Goal | Result | Report |
15
+ |---|---|---|---|---|
16
+ {One entry per race report read in Phase 1. Goal vs result. Link path to RACE_REPORT.md.}
17
+
18
+ ## What Worked
19
+ {Phase 2 question 2 athlete answer + Phase 3 synthesis. Concrete: structures, recovery rhythms, fueling approaches, persona-fit observations.}
20
+
21
+ ## What Didn't
22
+ {Phase 2 question 3 athlete answer + Phase 3 synthesis. Concrete: items to change, with reasoning.}
23
+
24
+ ## Goal vs Outcome Diagnosis
25
+ {If outcome missed goal: what was the limiter, evidenced by what data?}
26
+ {If outcome exceeded goal: what overperformed, and is it durable or anomalous?}
27
+
28
+ ## Persona-Fit Assessment
29
+ {Cross-block insight: did the active persona match the athlete's actual response patterns?}
30
+ {If yes: cite supporting evidence from cross-block patterns.}
31
+ {If no: which persona's philosophy would have produced an equal or better outcome? Recommendation for next season.}
32
+
33
+ ## Cross-Block Patterns
34
+ {Items that appeared in 2+ block summaries' "Calibration Points for Future Blocks" sections. List with sources.}
35
+
36
+ ## Forward Intent
37
+ {Phase 2 question 4: next goal and shape. Implications for next season's persona, block structure, and volume.}
38
+
39
+ ## Calibration Points to Promote
40
+ {Explicit list of bullets passed to lessons-rollup as --source=season:{name} promotions. Higher bar than block-level points — only items confirmed across multiple blocks or by race execution.}
@@ -1,295 +0,0 @@
1
- /**
2
- * engram-coach LLM-powered knowledge extractor.
3
- *
4
- * Implements the KnowledgeExtractor interface using the Engram harness LLM helper
5
- * when available. For each turn, the LLM analyzes the conversation and
6
- * produces structured coaching-aware candidates classified into engram-coach's
7
- * domain ontology (entity types, decision kinds, training signals, phases,
8
- * personas). Falls back to deterministic keyword matching when the LLM
9
- * helper is absent.
10
- *
11
- * @module engram-coach-extractor
12
- */
13
-
14
- import type { TurnContext, PackHelpers } from "@isparling/engram-harness/knowledge-types";
15
- import {
16
- ENGRAM_COACH_ENTITY_TYPES,
17
- ENGRAM_COACH_DECISION_KINDS,
18
- ENGRAM_COACH_TRAINING_SIGNALS,
19
- ENGRAM_COACH_TRAINING_PHASES,
20
- ENGRAM_COACH_PERSONAS,
21
- COACHING_TOPIC_HINTS,
22
- type EngramCoachDetails,
23
- type EngramCoachEntityType,
24
- type EngramCoachDecisionKind,
25
- } from "./engram-coach-domain.ts";
26
-
27
- export const engramCoachPackId = "engram-coach";
28
- export const engramCoachPackVersion = "0.1.0";
29
-
30
- /**
31
- * System prompt for the LLM-powered extractor.
32
- *
33
- * Instructs the LLM to analyze a conversation turn and produce structured
34
- * coaching observations classified into the engram-coach domain ontology.
35
- */
36
- const EXTRACTION_SYSTEM_PROMPT = `You are a coaching domain classifier embedded in the engram-coach coaching system. Your task is to analyze conversation turns and extract structured knowledge candidates about coaching decisions.
37
-
38
- ## Domain ontology
39
-
40
- ### Entity types — what kind of coaching artifact or concept this turn is about:
41
- ${ENGRAM_COACH_ENTITY_TYPES.map((t) => `- ${t}`).join("\n")}
42
-
43
- ### Decision kinds — what type of coaching judgment is being rendered:
44
- ${ENGRAM_COACH_DECISION_KINDS.map((k) => `- ${k}`).join("\n")}
45
-
46
- ### Training signals — physiological metrics mentioned:
47
- ${ENGRAM_COACH_TRAINING_SIGNALS.map((s) => `- ${s}`).join("\n")}
48
-
49
- ### Training phases:
50
- ${ENGRAM_COACH_TRAINING_PHASES.map((p) => `- ${p}`).join("\n")}
51
-
52
- ### Personas:
53
- ${ENGRAM_COACH_PERSONAS.map((p) => `- ${p}`).join("\n")}
54
-
55
- ## Classification rules
56
-
57
- 1. Return an empty JSON array \`[]\` if this turn contains no coaching-relevant content. Coaching-relevant means: training data, workout execution, adaptation decisions, race/event planning, consultation/advice, health signals (illness/injury/sleep/nutrition), monitoring concerns, goal/arc planning, coaching setup (intake/persona), lactate analysis, or block/season reviews.
58
-
59
- 2. Each candidate must be a JSON object with these fields:
60
- - \`entityType\`: one of the entity types above
61
- - \`decisionKind\`: one of the decision kinds above
62
- - \`statement\`: a concise, factual one-sentence statement of what was observed or decided (max 300 chars)
63
- - \`trainingSignals\`: array of signal names mentioned (or empty array)
64
- - \`trainingPhase\`: training phase if identifiable, else null
65
- - \`persona\`: persona if mentioned or obvious from context, else null
66
- - \`topics\`: 1-4 topic labels useful for retrieval (e.g., "interval fade", "sweet spot prescription", "LT2 update", "HRV suppression")
67
-
68
- 3. For \`statement\`, prefer concrete observations over generic ones:
69
- - GOOD: "Athlete reported RPE 8.5 on final interval with power fade >10%"
70
- - BAD: "Athlete had a hard workout"
71
- - GOOD: "Athlete switched persona from conservative to aggressive for build phase"
72
- - BAD: "Persona change"
73
-
74
- 4. Return at most 3 candidates per turn. Prioritize candidates that represent decisions or new observations over routine status updates.
75
-
76
- 5. Return ONLY the JSON array. No preamble, no explanation, no markdown.`;
77
-
78
- /**
79
- * Deterministic fallback: check if the turn narrative mentions coaching
80
- * topics. Returns the most likely entity type based on matched terms.
81
- */
82
- function coachingRelevantEntityType(text: string): EngramCoachEntityType | null {
83
- const lower = text.toLowerCase();
84
-
85
- // Simple greedy: return null if no topic matched
86
- if (!COACHING_TOPIC_HINTS.some((hint) => lower.includes(hint))) {
87
- return null;
88
- }
89
-
90
- // Map topics to entity types
91
- // Arc-planning: goal+arc/target is most specific — check first
92
- if (
93
- (lower.includes("goal") || lower.includes("arc")) &&
94
- (lower.includes("target") || lower.includes("methodology"))
95
- ) {
96
- return "arc-plan";
97
- }
98
- if (lower.includes("race") && (lower.includes("report") || lower.includes("analysis"))) {
99
- return "race-report";
100
- }
101
- if (lower.includes("block") && (lower.includes("review") || lower.includes("summary"))) {
102
- return "block-review";
103
- }
104
- if (lower.includes("season") && (lower.includes("review") || lower.includes("retrospective"))) {
105
- return "season-review";
106
- }
107
- if (lower.includes("consult") || lower.includes("advice") || lower.includes("question about")) {
108
- return "consultation";
109
- }
110
- if (lower.includes("intake") || lower.includes("setup") || lower.includes("onboard")) {
111
- return "intake-record";
112
- }
113
- if (lower.includes("adapt") || lower.includes("modify") || lower.includes("delta") || lower.includes("prescription")) {
114
- return "workout-adaptation";
115
- }
116
- if (lower.includes("lactate") || lower.includes("threshold") || lower.includes("lt1") || lower.includes("lt2")) {
117
- return "lactate-test";
118
- }
119
- if (lower.includes("monitor") || lower.includes("concern") || lower.includes("symptom") || lower.includes("tracking")) {
120
- return "monitoring-capture";
121
- }
122
- if (lower.includes("persona") || lower.includes("philosophy")) {
123
- return "persona-fit";
124
- }
125
- if (lower.includes("lesson") || lower.includes("calibration") || lower.includes("profile") || lower.includes("rollup")) {
126
- return "calibration-point";
127
- }
128
- if (lower.includes("race") || lower.includes("event")) {
129
- return "race-report";
130
- }
131
-
132
- // Generic coaching observation
133
- return "session-execution";
134
- }
135
-
136
- function deterministicDecisionKind(entityType: EngramCoachEntityType): EngramCoachDecisionKind {
137
- const map: Record<EngramCoachEntityType, EngramCoachDecisionKind> = {
138
- "workout-adaptation": "workout-adaptation",
139
- consultation: "consultation-advice",
140
- "block-review": "block-restructure",
141
- "race-report": "workout-adaptation",
142
- "season-review": "arc-planning",
143
- "arc-plan": "arc-planning",
144
- "lactate-test": "threshold-update",
145
- "monitoring-capture": "monitoring-capture",
146
- "intake-record": "setup-decision",
147
- prescription: "arc-planning",
148
- "persona-fit": "persona-change",
149
- "calibration-point": "profile-claim",
150
- methodology: "arc-planning",
151
- "session-execution": "workout-adaptation",
152
- };
153
- return map[entityType];
154
- }
155
-
156
- /**
157
- * Parse the LLM's JSON response into candidate objects.
158
- * Returns empty array on any parse failure.
159
- */
160
- function parseLlmResponse(raw: string): Record<string, unknown>[] {
161
- const trimmed = raw.trim();
162
-
163
- // Strip markdown code fences if present
164
- const jsonStr = trimmed.replace(/^```(?:json)?\s*\n?/, "").replace(/\n?```\s*$/, "");
165
-
166
- try {
167
- const parsed = JSON.parse(jsonStr);
168
- if (!Array.isArray(parsed)) return [];
169
- return parsed;
170
- } catch {
171
- return [];
172
- }
173
- }
174
-
175
- /**
176
- * The engram-coach extractor facet.
177
- *
178
- * Uses LLM when available (via PackHelpers.llm.complete) to produce
179
- * coaching-aware candidates classified into the domain ontology.
180
- * Falls back to deterministic keyword matching without LLM.
181
- */
182
- export const engramCoachExtractor = {
183
- id: engramCoachPackId,
184
- version: engramCoachPackVersion,
185
-
186
- async extractCandidates(
187
- turn: TurnContext,
188
- helpers: PackHelpers,
189
- ): Promise<Record<string, unknown>[]> {
190
- const narrative = turn.narrative?.trim() ?? "";
191
- if (!narrative) return [];
192
-
193
- // -------------------------------------------------------------------------
194
- // LLM path
195
- // -------------------------------------------------------------------------
196
- if (helpers.llm?.complete) {
197
- try {
198
- const raw = await helpers.llm.complete(
199
- `Analyze this conversation turn and extract coaching knowledge candidates as JSON:\n\n${narrative}`,
200
- { system: EXTRACTION_SYSTEM_PROMPT },
201
- );
202
-
203
- const candidates = parseLlmResponse(raw);
204
- if (candidates.length > 0) {
205
- // Enrich each candidate with pack metadata
206
- return candidates.map((c, i) => {
207
- const entityType = (c.entityType as EngramCoachEntityType) ?? "session-execution";
208
- const decisionKind = (c.decisionKind as EngramCoachDecisionKind) ?? "workout-adaptation";
209
- const statement = (c.statement as string) ?? narrative.slice(0, 300);
210
- const topics = (c.topics as string[]) ?? ["coaching:observation"];
211
-
212
- const details: EngramCoachDetails = {
213
- skill: undefined,
214
- entityType,
215
- decisionKind,
216
- trainingSignals: c.trainingSignals as EngramCoachDetails["trainingSignals"],
217
- trainingPhase: c.trainingPhase as EngramCoachDetails["trainingPhase"],
218
- persona: c.persona as EngramCoachDetails["persona"],
219
- turnIndex: turn.turnIndex,
220
- extractionConfidence: "high",
221
- };
222
-
223
- return {
224
- id: `engram-coach-turn-${turn.turnIndex}-${i}-${Date.now()}`,
225
- kind: "decision",
226
- status: "candidate",
227
- disposition: "new",
228
- scope: {
229
- space: engramCoachPackId,
230
- subjects: [],
231
- topics,
232
- contexts: [],
233
- dimensions: {},
234
- },
235
- pack: { id: engramCoachPackId, version: engramCoachPackVersion },
236
- sources: [
237
- { type: "engram-coach-extractor", ref: `session:${turn.session.id}` },
238
- { type: "llm-inference", ref: `turn:${turn.turnIndex}` },
239
- ],
240
- session: turn.session,
241
- submittedAt: turn.timestamp,
242
- details: details as unknown as Record<string, unknown>,
243
- statement: statement.slice(0, 512),
244
- };
245
- });
246
- }
247
- } catch {
248
- // LLM call failed — fall through to deterministic path
249
- }
250
- }
251
-
252
- // -------------------------------------------------------------------------
253
- // Deterministic fallback path
254
- // -------------------------------------------------------------------------
255
- if (narrative.length === 0) return [];
256
-
257
- const entityType = coachingRelevantEntityType(narrative);
258
- if (!entityType) return [];
259
-
260
- const decisionKind = deterministicDecisionKind(entityType);
261
-
262
- const details: EngramCoachDetails = {
263
- skill: undefined,
264
- entityType,
265
- decisionKind,
266
- turnIndex: turn.turnIndex,
267
- extractionConfidence: "low",
268
- };
269
-
270
- const id = `engram-coach-turn-${turn.turnIndex}-${Date.now()}`;
271
- return [
272
- {
273
- id,
274
- kind: "decision",
275
- status: "candidate",
276
- disposition: "new",
277
- scope: {
278
- space: engramCoachPackId,
279
- subjects: [],
280
- topics: ["coaching:observation"],
281
- contexts: [],
282
- dimensions: {},
283
- },
284
- pack: { id: engramCoachPackId, version: engramCoachPackVersion },
285
- sources: [
286
- { type: "engram-coach-extractor", ref: `session:${turn.session.id}` },
287
- ],
288
- session: turn.session,
289
- submittedAt: turn.timestamp,
290
- details: details as unknown as Record<string, unknown>,
291
- statement: narrative.slice(0, 300),
292
- },
293
- ];
294
- },
295
- };