@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,117 @@
|
|
|
1
|
+
import { fileURLToPath } from 'url';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
|
|
4
|
+
interface DayProjection {
|
|
5
|
+
day: number;
|
|
6
|
+
tss: number;
|
|
7
|
+
ctl: number;
|
|
8
|
+
atl: number;
|
|
9
|
+
tsb: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface Projection {
|
|
13
|
+
initial: { ctl: number; atl: number; tsb: number };
|
|
14
|
+
projections: DayProjection[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ProjectionOptions {
|
|
18
|
+
ctlTau?: number;
|
|
19
|
+
atlTau?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function round1(n: number): number {
|
|
23
|
+
return Math.round(n * 10) / 10;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function projectTSB(
|
|
27
|
+
ctl: number,
|
|
28
|
+
atl: number,
|
|
29
|
+
tssSequence: number[],
|
|
30
|
+
options?: ProjectionOptions,
|
|
31
|
+
): Projection {
|
|
32
|
+
const ctlTau = options?.ctlTau ?? 42;
|
|
33
|
+
const atlTau = options?.atlTau ?? 7;
|
|
34
|
+
|
|
35
|
+
const projections: DayProjection[] = [];
|
|
36
|
+
let currentCtl = ctl;
|
|
37
|
+
let currentAtl = atl;
|
|
38
|
+
|
|
39
|
+
for (let i = 0; i < tssSequence.length; i++) {
|
|
40
|
+
const tss = tssSequence[i];
|
|
41
|
+
currentCtl = currentCtl * (1 - 1 / ctlTau) + tss / ctlTau;
|
|
42
|
+
currentAtl = currentAtl * (1 - 1 / atlTau) + tss / atlTau;
|
|
43
|
+
const roundedCtl = round1(currentCtl);
|
|
44
|
+
const roundedAtl = round1(currentAtl);
|
|
45
|
+
projections.push({
|
|
46
|
+
day: i + 1,
|
|
47
|
+
tss,
|
|
48
|
+
ctl: roundedCtl,
|
|
49
|
+
atl: roundedAtl,
|
|
50
|
+
tsb: round1(roundedCtl - roundedAtl),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
initial: { ctl, atl, tsb: round1(ctl - atl) },
|
|
56
|
+
projections,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// --- CLI entry point ---
|
|
61
|
+
function parseArgs(args: string[]): {
|
|
62
|
+
ctl: number;
|
|
63
|
+
atl: number;
|
|
64
|
+
tss: number[];
|
|
65
|
+
ctlTau?: number;
|
|
66
|
+
atlTau?: number;
|
|
67
|
+
} {
|
|
68
|
+
const flagValue = (name: string): string | undefined => {
|
|
69
|
+
const i = args.indexOf(name);
|
|
70
|
+
return i !== -1 && i + 1 < args.length ? args[i + 1] : undefined;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const ctl = flagValue('--ctl');
|
|
74
|
+
const atl = flagValue('--atl');
|
|
75
|
+
const tss = flagValue('--tss');
|
|
76
|
+
|
|
77
|
+
if (ctl === undefined || atl === undefined || tss === undefined) {
|
|
78
|
+
process.stderr.write(
|
|
79
|
+
'Usage: tsb-predict --ctl <number> --atl <number> --tss <n1,n2,...> [--ctl-tau <n>] [--atl-tau <n>]\n',
|
|
80
|
+
);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const ctlNum = Number(ctl);
|
|
85
|
+
const atlNum = Number(atl);
|
|
86
|
+
if (isNaN(ctlNum) || isNaN(atlNum)) {
|
|
87
|
+
process.stderr.write('Error: --ctl and --atl must be numbers\n');
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const tssValues = tss.split(',').map(Number);
|
|
92
|
+
if (tssValues.some(isNaN)) {
|
|
93
|
+
process.stderr.write('Error: --tss values must be numbers (e.g., --tss 100,0,50)\n');
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
ctl: ctlNum,
|
|
99
|
+
atl: atlNum,
|
|
100
|
+
tss: tssValues,
|
|
101
|
+
ctlTau: flagValue('--ctl-tau') ? Number(flagValue('--ctl-tau')) : undefined,
|
|
102
|
+
atlTau: flagValue('--atl-tau') ? Number(flagValue('--atl-tau')) : undefined,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Only run CLI when executed directly (not imported)
|
|
107
|
+
const isDirectExecution =
|
|
108
|
+
process.argv[1] !== undefined &&
|
|
109
|
+
fileURLToPath(import.meta.url) === resolve(process.argv[1]);
|
|
110
|
+
if (isDirectExecution) {
|
|
111
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
112
|
+
const result = projectTSB(parsed.ctl, parsed.atl, parsed.tss, {
|
|
113
|
+
ctlTau: parsed.ctlTau,
|
|
114
|
+
atlTau: parsed.atlTau,
|
|
115
|
+
});
|
|
116
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
117
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* engram-coach ambient capture handler — candidate-only persistence.
|
|
3
|
+
*
|
|
4
|
+
* The extension calls this at awaited OMP `session_stop` with the latest user
|
|
5
|
+
* turn. Extraction itself lives in `engram-coach-ambient-capture.ts`; this
|
|
6
|
+
* module owns what happens to the result:
|
|
7
|
+
*
|
|
8
|
+
* - canonical key derivation that never authorizes replacement;
|
|
9
|
+
* - suppression of ambient candidates the explicit channel already applied
|
|
10
|
+
* in this same turn;
|
|
11
|
+
* - deterministic IDs plus create-only writes, so re-settling a turn cannot
|
|
12
|
+
* duplicate a draft;
|
|
13
|
+
* - a single scoped index refresh.
|
|
14
|
+
*
|
|
15
|
+
* Every record written here is `status: "candidate"` with empty relationship
|
|
16
|
+
* arrays. Ambient extraction proposes; it never retires or supersedes an
|
|
17
|
+
* active record before review. Every failure becomes a warning rather than an
|
|
18
|
+
* exception, so a slow or broken extraction model can never block the
|
|
19
|
+
* coaching response or the explicit Phase 5 path.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { createHash } from "node:crypto";
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
import type {
|
|
25
|
+
JsonObject,
|
|
26
|
+
KnowledgeEnvelope,
|
|
27
|
+
KnowledgeRelationships,
|
|
28
|
+
TurnContext,
|
|
29
|
+
TurnToolCall,
|
|
30
|
+
} from "@isparling/engram-harness/knowledge-types";
|
|
31
|
+
import type { CompletionRequest } from "@isparling/engram-harness/capture-types";
|
|
32
|
+
import type { CaptureSummary } from "./engram-coach-capture-types.ts";
|
|
33
|
+
import {
|
|
34
|
+
extractAmbientCandidates,
|
|
35
|
+
type AmbientCandidate,
|
|
36
|
+
} from "./engram-coach-ambient-capture.ts";
|
|
37
|
+
import { loadEngramCoachConfig, type EngramCoachRuntimeConfig } from "./engram-coach-config.ts";
|
|
38
|
+
import { deriveCanonicalEntityKey } from "./engram-coach-keys.ts";
|
|
39
|
+
import { validateEnvelope } from "./engram-coach-reconciliation.ts";
|
|
40
|
+
import { canonicalJson } from "./engram-coach-structured-capture.ts";
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Host mechanics supplied by the extension. It owns no coaching ontology: it
|
|
44
|
+
* only spawns the isolated completion, confines writes to the records root,
|
|
45
|
+
* and refreshes the scoped index.
|
|
46
|
+
*/
|
|
47
|
+
export type CaptureTools = {
|
|
48
|
+
recordsRoot: string;
|
|
49
|
+
spaceId: string;
|
|
50
|
+
projectRoot: string;
|
|
51
|
+
writeFile(path: string, content: string): Promise<void>;
|
|
52
|
+
refreshIndex(): Promise<void>;
|
|
53
|
+
complete(request: CompletionRequest): Promise<string>;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type { CaptureSummary } from "./engram-coach-capture-types.ts";
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Duplicate suppression from explicit tool provenance
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
63
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function stringArray(value: unknown): string[] {
|
|
67
|
+
return Array.isArray(value)
|
|
68
|
+
? value.filter((item): item is string => typeof item === "string")
|
|
69
|
+
: [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function safeJson(text: string): unknown {
|
|
73
|
+
try {
|
|
74
|
+
return JSON.parse(text);
|
|
75
|
+
} catch {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Entity keys a successful `engram_capture_apply` already committed in this
|
|
82
|
+
* same turn. Membership is decided at runtime over an unbounded key space, so
|
|
83
|
+
* a Set is the right structure here.
|
|
84
|
+
*/
|
|
85
|
+
export function appliedEntityKeys(toolCalls: readonly TurnToolCall[]): Set<string> {
|
|
86
|
+
const keys = new Set<string>();
|
|
87
|
+
for (const call of toolCalls) {
|
|
88
|
+
if (call.tool !== "engram_capture_apply") continue;
|
|
89
|
+
const result = call.result;
|
|
90
|
+
const parsed = typeof result === "string" ? safeJson(result) : result;
|
|
91
|
+
if (!isObject(parsed)) continue;
|
|
92
|
+
const status = parsed.status;
|
|
93
|
+
if (status !== "committed" && status !== "no-change" && status !== "records-committed") continue;
|
|
94
|
+
for (const key of stringArray(parsed.entity_keys)) keys.add(key);
|
|
95
|
+
}
|
|
96
|
+
return keys;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Record construction
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Deterministic ambient record ID. Session, the stable turn index, the
|
|
105
|
+
* candidate index, and canonical candidate content all participate, so
|
|
106
|
+
* re-settling the same turn with the same extraction reproduces the same ID —
|
|
107
|
+
* which is what makes create-only writes idempotent instead of duplicating.
|
|
108
|
+
*/
|
|
109
|
+
export function ambientRecordId(
|
|
110
|
+
sessionId: string,
|
|
111
|
+
turnIndex: number,
|
|
112
|
+
candidateIndex: number,
|
|
113
|
+
candidate: AmbientCandidate,
|
|
114
|
+
): string {
|
|
115
|
+
const digest = createHash("sha256")
|
|
116
|
+
.update(canonicalJson({
|
|
117
|
+
sessionId,
|
|
118
|
+
turnIndex,
|
|
119
|
+
candidateIndex,
|
|
120
|
+
kind: candidate.kind,
|
|
121
|
+
statement: candidate.statement,
|
|
122
|
+
entityType: candidate.entityType,
|
|
123
|
+
keyedEntityType: candidate.keyedEntityType,
|
|
124
|
+
keyComponents: candidate.keyComponents,
|
|
125
|
+
effectiveAt: candidate.effectiveAt,
|
|
126
|
+
}))
|
|
127
|
+
.digest("hex")
|
|
128
|
+
.slice(0, 24);
|
|
129
|
+
return `coach-ambient-${digest}`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const EMPTY_RELATIONSHIPS: KnowledgeRelationships = {
|
|
133
|
+
supports: [],
|
|
134
|
+
contradicts: [],
|
|
135
|
+
refines: [],
|
|
136
|
+
supersedes: [],
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
function serializeDraftRecord(envelope: KnowledgeEnvelope): string {
|
|
140
|
+
return [
|
|
141
|
+
"---",
|
|
142
|
+
"schema_version: 0",
|
|
143
|
+
`id: ${canonicalJson(envelope.id)}`,
|
|
144
|
+
`kind: ${canonicalJson(envelope.kind)}`,
|
|
145
|
+
`status: ${canonicalJson(envelope.status)}`,
|
|
146
|
+
`statement: ${canonicalJson(envelope.statement)}`,
|
|
147
|
+
`details: ${canonicalJson(envelope.details)}`,
|
|
148
|
+
`scope: ${canonicalJson(envelope.scope)}`,
|
|
149
|
+
`pack: ${canonicalJson(envelope.pack)}`,
|
|
150
|
+
`sources: ${canonicalJson(envelope.sources)}`,
|
|
151
|
+
`session: ${canonicalJson(envelope.session)}`,
|
|
152
|
+
`submitted_at: ${canonicalJson(envelope.submittedAt)}`,
|
|
153
|
+
`disposition: ${canonicalJson(envelope.disposition)}`,
|
|
154
|
+
`relationships: ${canonicalJson(EMPTY_RELATIONSHIPS)}`,
|
|
155
|
+
"history: []",
|
|
156
|
+
"---",
|
|
157
|
+
"## Statement",
|
|
158
|
+
"",
|
|
159
|
+
envelope.statement,
|
|
160
|
+
"",
|
|
161
|
+
].join("\n");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function submittedDate(turnTimestamp: string): string {
|
|
165
|
+
return /^\d{4}-\d{2}-\d{2}/.exec(turnTimestamp)?.[0] ?? turnTimestamp;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function isAlreadyPresent(error: unknown): boolean {
|
|
169
|
+
return isObject(error) && error.code === "EEXIST";
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Build the candidate envelope. A derivable key is recorded so review can find
|
|
174
|
+
* the entity it concerns; an underivable one records the diagnostic instead.
|
|
175
|
+
* Either way the record stays a candidate with empty relationships.
|
|
176
|
+
*/
|
|
177
|
+
function buildEnvelope(
|
|
178
|
+
candidate: AmbientCandidate,
|
|
179
|
+
id: string,
|
|
180
|
+
entityKey: string | null,
|
|
181
|
+
bindingError: string | null,
|
|
182
|
+
turn: TurnContext,
|
|
183
|
+
tools: CaptureTools,
|
|
184
|
+
config: EngramCoachRuntimeConfig,
|
|
185
|
+
): KnowledgeEnvelope {
|
|
186
|
+
const details: JsonObject = {
|
|
187
|
+
captureChannel: "ambient",
|
|
188
|
+
entityKey,
|
|
189
|
+
activeProfile: config.activeProfile,
|
|
190
|
+
};
|
|
191
|
+
if (candidate.entityType !== null) details.entityType = candidate.entityType;
|
|
192
|
+
if (candidate.effectiveAt !== null) details.effectiveAt = candidate.effectiveAt;
|
|
193
|
+
if (Object.keys(candidate.keyComponents).length > 0) {
|
|
194
|
+
details.keyComponents = candidate.keyComponents;
|
|
195
|
+
}
|
|
196
|
+
if (bindingError !== null) details.bindingError = bindingError;
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
id,
|
|
200
|
+
kind: candidate.kind,
|
|
201
|
+
status: "candidate",
|
|
202
|
+
statement: candidate.statement,
|
|
203
|
+
details,
|
|
204
|
+
scope: {
|
|
205
|
+
space: tools.spaceId,
|
|
206
|
+
subjects: candidate.subjects,
|
|
207
|
+
topics: candidate.topics,
|
|
208
|
+
contexts: [],
|
|
209
|
+
dimensions: {},
|
|
210
|
+
},
|
|
211
|
+
pack: { id: "engram-coach", version: "0.1.0" },
|
|
212
|
+
sources: [
|
|
213
|
+
{ type: "session", ref: `session:${turn.session.id}/turn:${turn.turnIndex}` },
|
|
214
|
+
{ type: "inference", ref: `llm-inference:${config.capture.model}` },
|
|
215
|
+
],
|
|
216
|
+
session: turn.session,
|
|
217
|
+
submittedAt: submittedDate(turn.timestamp),
|
|
218
|
+
disposition: "new",
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
// Entry point
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
|
|
226
|
+
/** Ambient capture for one settled turn. Always resolves. */
|
|
227
|
+
export async function captureFromTurn(
|
|
228
|
+
turn: TurnContext,
|
|
229
|
+
tools: CaptureTools,
|
|
230
|
+
): Promise<CaptureSummary> {
|
|
231
|
+
const summary: CaptureSummary = { created: [], existing: [], invalid: [], warnings: [] };
|
|
232
|
+
|
|
233
|
+
const userText = turn.narrative.trim();
|
|
234
|
+
if (userText.length === 0) return summary;
|
|
235
|
+
|
|
236
|
+
let config: EngramCoachRuntimeConfig;
|
|
237
|
+
try {
|
|
238
|
+
config = await loadEngramCoachConfig({ projectRoot: tools.projectRoot });
|
|
239
|
+
} catch (error) {
|
|
240
|
+
summary.warnings.push(`ambient capture is not configured: ${String(error)}`);
|
|
241
|
+
return summary;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const extraction = await extractAmbientCandidates(userText, config, tools);
|
|
245
|
+
summary.warnings.push(...extraction.warnings);
|
|
246
|
+
if (extraction.candidates.length === 0) return summary;
|
|
247
|
+
|
|
248
|
+
const alreadyApplied = appliedEntityKeys(turn.toolCalls);
|
|
249
|
+
|
|
250
|
+
for (const [index, candidate] of extraction.candidates.entries()) {
|
|
251
|
+
let entityKey: string | null = null;
|
|
252
|
+
let bindingError: string | null = null;
|
|
253
|
+
if (candidate.keyedEntityType !== null) {
|
|
254
|
+
const derived = deriveCanonicalEntityKey({
|
|
255
|
+
entity_type: candidate.keyedEntityType,
|
|
256
|
+
key_components: candidate.keyComponents,
|
|
257
|
+
});
|
|
258
|
+
if (derived.kind === "bound") entityKey = derived.key;
|
|
259
|
+
else bindingError = derived.reason;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// The explicit channel already captured this key in this same turn, under
|
|
263
|
+
// athlete approval. Unrelated candidates from the turn are untouched.
|
|
264
|
+
if (entityKey !== null && alreadyApplied.has(entityKey)) continue;
|
|
265
|
+
|
|
266
|
+
const id = ambientRecordId(turn.session.id, turn.turnIndex, index, candidate);
|
|
267
|
+
const envelope = buildEnvelope(candidate, id, entityKey, bindingError, turn, tools, config);
|
|
268
|
+
|
|
269
|
+
const validation = validateEnvelope(envelope);
|
|
270
|
+
if (!validation.ok) {
|
|
271
|
+
summary.invalid.push({
|
|
272
|
+
id: envelope.id,
|
|
273
|
+
errors: validation.errors.map((error) => `${error.field ?? "envelope"}: ${error.message}`),
|
|
274
|
+
});
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
await tools.writeFile(
|
|
280
|
+
join(tools.recordsRoot, `${envelope.id}.md`),
|
|
281
|
+
serializeDraftRecord(envelope),
|
|
282
|
+
);
|
|
283
|
+
summary.created.push(envelope.id);
|
|
284
|
+
} catch (error) {
|
|
285
|
+
if (isAlreadyPresent(error)) {
|
|
286
|
+
summary.existing.push(envelope.id);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
summary.warnings.push(`ambient draft write failed for ${envelope.id}: ${String(error)}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (summary.created.length > 0 || summary.existing.length > 0) {
|
|
294
|
+
try {
|
|
295
|
+
await tools.refreshIndex();
|
|
296
|
+
} catch (error) {
|
|
297
|
+
summary.warnings.push(`ambient index refresh failed: ${String(error)}`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return summary;
|
|
301
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Copy this file to config.json and edit values. config.json is gitignored. season is optional: if absent, adapt-plan will ask for it at runtime.",
|
|
3
|
+
"active_profile": "default",
|
|
4
|
+
"profiles": {
|
|
5
|
+
"default": {
|
|
6
|
+
"active_persona": "conservative",
|
|
7
|
+
"coaching_docs_dir": "~/REPLACE_WITH_YOUR_COACHING_DOCS_PATH",
|
|
8
|
+
"prescriptions_dir": "~/REPLACE_WITH_YOUR_PRESCRIPTIONS_PATH",
|
|
9
|
+
"season": "REPLACE_WITH_SEASON_LABEL"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"capture": {
|
|
13
|
+
"model": "REPLACE_WITH_PROVIDER/MODEL",
|
|
14
|
+
"timeout_seconds": 60,
|
|
15
|
+
"max_candidates_per_turn": 3
|
|
16
|
+
},
|
|
17
|
+
"intervals_icu": {
|
|
18
|
+
"api_key": "REPLACE_WITH_YOUR_API_KEY",
|
|
19
|
+
"athlete_id": "REPLACE_WITH_YOUR_ATHLETE_ID"
|
|
20
|
+
}
|
|
21
|
+
}
|