@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.
- package/README.md +85 -18
- package/SETUP.md +559 -0
- package/SKILL_PACK.md +75 -0
- package/analyses/catalog.md +257 -0
- package/analysis-tools/hrv-trend.ts +592 -0
- package/analysis-tools/migrate-structured-capture.ts +234 -0
- package/analysis-tools/race-context.ts +96 -0
- package/analysis-tools/stream-analyze.ts +1008 -0
- package/analysis-tools/tsb-predict.ts +117 -0
- package/capture-handler.ts +301 -0
- package/config.json.example +21 -0
- package/engram-coach-ambient-capture.ts +336 -0
- package/engram-coach-capture-types.ts +185 -0
- package/engram-coach-config.ts +268 -0
- package/engram-coach-domain.ts +7 -2
- package/engram-coach-keys.ts +189 -0
- package/engram-coach-materialization.ts +638 -0
- package/engram-coach-migration.ts +1078 -0
- package/engram-coach-pack.ts +17 -12
- package/engram-coach-presentation.ts +10 -1
- package/engram-coach-reconciliation.ts +305 -2
- package/engram-coach-structured-capture.ts +622 -0
- package/package.json +39 -6
- package/personas/aggressive-monitoring.md +121 -0
- package/personas/aggressive.json +85 -0
- package/personas/conservative-monitoring.md +133 -0
- package/personas/conservative.json +93 -0
- package/personas/polarized-monitoring.md +112 -0
- package/personas/polarized.json +72 -0
- package/personas/volume-monitoring.md +85 -0
- package/personas/volume.json +108 -0
- package/shared/retrieval.md +71 -0
- package/shared/setup.md +207 -0
- package/skills/.gitkeep +0 -0
- package/skills/adapt-plan/SKILL.md +263 -0
- package/skills/block-review/SKILL.md +275 -0
- package/skills/consult/SKILL.md +176 -0
- package/skills/intake/SKILL.md +315 -0
- package/skills/lactate-analyze/SKILL.md +230 -0
- package/skills/lessons-rollup/SKILL.md +196 -0
- package/skills/monitoring-rollup/SKILL.md +208 -0
- package/skills/race-analysis/SKILL.md +219 -0
- package/skills/season-retrospective/SKILL.md +200 -0
- package/skills/set-goal/SKILL.md +297 -0
- package/templates/base.md +55 -0
- package/templates/build-1.md +57 -0
- package/templates/build-2.md +62 -0
- package/templates/race-report.md +51 -0
- package/templates/race-specificity.md +62 -0
- package/templates/season-review.md +40 -0
- 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
|
-
};
|