selftune 0.1.4 → 0.2.1
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/.claude/agents/diagnosis-analyst.md +156 -0
- package/.claude/agents/evolution-reviewer.md +180 -0
- package/.claude/agents/integration-guide.md +212 -0
- package/.claude/agents/pattern-analyst.md +160 -0
- package/CHANGELOG.md +46 -1
- package/README.md +105 -257
- package/apps/local-dashboard/dist/assets/geist-cyrillic-wght-normal-CHSlOQsW.woff2 +0 -0
- package/apps/local-dashboard/dist/assets/geist-latin-ext-wght-normal-DMtmJ5ZE.woff2 +0 -0
- package/apps/local-dashboard/dist/assets/geist-latin-wght-normal-Dm3htQBi.woff2 +0 -0
- package/apps/local-dashboard/dist/assets/index-C4EOTFZ2.js +15 -0
- package/apps/local-dashboard/dist/assets/index-bl-Webyd.css +1 -0
- package/apps/local-dashboard/dist/assets/vendor-react-U7zYD9Rg.js +60 -0
- package/apps/local-dashboard/dist/assets/vendor-table-B7VF2Ipl.js +26 -0
- package/apps/local-dashboard/dist/assets/vendor-ui-D7_zX_qy.js +346 -0
- package/apps/local-dashboard/dist/favicon.png +0 -0
- package/apps/local-dashboard/dist/index.html +17 -0
- package/apps/local-dashboard/dist/logo.png +0 -0
- package/apps/local-dashboard/dist/logo.svg +9 -0
- package/assets/BeforeAfter.gif +0 -0
- package/assets/FeedbackLoop.gif +0 -0
- package/assets/logo.svg +9 -0
- package/assets/skill-health-badge.svg +20 -0
- package/cli/selftune/activation-rules.ts +171 -0
- package/cli/selftune/badge/badge-data.ts +108 -0
- package/cli/selftune/badge/badge-svg.ts +212 -0
- package/cli/selftune/badge/badge.ts +99 -0
- package/cli/selftune/canonical-export.ts +183 -0
- package/cli/selftune/constants.ts +103 -1
- package/cli/selftune/contribute/bundle.ts +314 -0
- package/cli/selftune/contribute/contribute.ts +214 -0
- package/cli/selftune/contribute/sanitize.ts +162 -0
- package/cli/selftune/cron/setup.ts +266 -0
- package/cli/selftune/dashboard-contract.ts +202 -0
- package/cli/selftune/dashboard-server.ts +1049 -0
- package/cli/selftune/dashboard.ts +43 -156
- package/cli/selftune/eval/baseline.ts +248 -0
- package/cli/selftune/eval/composability-v2.ts +273 -0
- package/cli/selftune/eval/composability.ts +117 -0
- package/cli/selftune/eval/generate-unit-tests.ts +143 -0
- package/cli/selftune/eval/hooks-to-evals.ts +101 -16
- package/cli/selftune/eval/import-skillsbench.ts +221 -0
- package/cli/selftune/eval/synthetic-evals.ts +172 -0
- package/cli/selftune/eval/unit-test-cli.ts +152 -0
- package/cli/selftune/eval/unit-test.ts +196 -0
- package/cli/selftune/evolution/deploy-proposal.ts +142 -1
- package/cli/selftune/evolution/evidence.ts +26 -0
- package/cli/selftune/evolution/evolve-body.ts +586 -0
- package/cli/selftune/evolution/evolve.ts +825 -116
- package/cli/selftune/evolution/extract-patterns.ts +105 -16
- package/cli/selftune/evolution/pareto.ts +314 -0
- package/cli/selftune/evolution/propose-body.ts +171 -0
- package/cli/selftune/evolution/propose-description.ts +100 -2
- package/cli/selftune/evolution/propose-routing.ts +166 -0
- package/cli/selftune/evolution/refine-body.ts +141 -0
- package/cli/selftune/evolution/rollback.ts +21 -4
- package/cli/selftune/evolution/validate-body.ts +254 -0
- package/cli/selftune/evolution/validate-proposal.ts +257 -35
- package/cli/selftune/evolution/validate-routing.ts +177 -0
- package/cli/selftune/grading/auto-grade.ts +200 -0
- package/cli/selftune/grading/grade-session.ts +513 -42
- package/cli/selftune/grading/pre-gates.ts +104 -0
- package/cli/selftune/grading/results.ts +42 -0
- package/cli/selftune/hooks/auto-activate.ts +185 -0
- package/cli/selftune/hooks/evolution-guard.ts +165 -0
- package/cli/selftune/hooks/prompt-log.ts +172 -2
- package/cli/selftune/hooks/session-stop.ts +123 -3
- package/cli/selftune/hooks/skill-change-guard.ts +112 -0
- package/cli/selftune/hooks/skill-eval.ts +119 -3
- package/cli/selftune/index.ts +415 -48
- package/cli/selftune/ingestors/claude-replay.ts +377 -0
- package/cli/selftune/ingestors/codex-rollout.ts +345 -46
- package/cli/selftune/ingestors/codex-wrapper.ts +207 -39
- package/cli/selftune/ingestors/openclaw-ingest.ts +573 -0
- package/cli/selftune/ingestors/opencode-ingest.ts +193 -17
- package/cli/selftune/init.ts +376 -16
- package/cli/selftune/last.ts +14 -5
- package/cli/selftune/localdb/db.ts +63 -0
- package/cli/selftune/localdb/materialize.ts +428 -0
- package/cli/selftune/localdb/queries.ts +376 -0
- package/cli/selftune/localdb/schema.ts +204 -0
- package/cli/selftune/memory/writer.ts +447 -0
- package/cli/selftune/monitoring/watch.ts +90 -16
- package/cli/selftune/normalization.ts +682 -0
- package/cli/selftune/observability.ts +19 -44
- package/cli/selftune/orchestrate.ts +1073 -0
- package/cli/selftune/quickstart.ts +203 -0
- package/cli/selftune/repair/skill-usage.ts +576 -0
- package/cli/selftune/schedule.ts +561 -0
- package/cli/selftune/status.ts +59 -33
- package/cli/selftune/sync.ts +627 -0
- package/cli/selftune/types.ts +525 -5
- package/cli/selftune/utils/canonical-log.ts +45 -0
- package/cli/selftune/utils/frontmatter.ts +217 -0
- package/cli/selftune/utils/hooks.ts +41 -0
- package/cli/selftune/utils/html.ts +27 -0
- package/cli/selftune/utils/llm-call.ts +103 -19
- package/cli/selftune/utils/math.ts +10 -0
- package/cli/selftune/utils/query-filter.ts +139 -0
- package/cli/selftune/utils/skill-discovery.ts +340 -0
- package/cli/selftune/utils/skill-log.ts +68 -0
- package/cli/selftune/utils/skill-usage-confidence.ts +18 -0
- package/cli/selftune/utils/transcript.ts +307 -26
- package/cli/selftune/utils/trigger-check.ts +89 -0
- package/cli/selftune/utils/tui.ts +156 -0
- package/cli/selftune/workflows/discover.ts +254 -0
- package/cli/selftune/workflows/skill-md-writer.ts +288 -0
- package/cli/selftune/workflows/workflows.ts +188 -0
- package/package.json +28 -11
- package/packages/telemetry-contract/README.md +11 -0
- package/packages/telemetry-contract/fixtures/golden.json +87 -0
- package/packages/telemetry-contract/fixtures/golden.test.ts +42 -0
- package/packages/telemetry-contract/index.ts +1 -0
- package/packages/telemetry-contract/package.json +19 -0
- package/packages/telemetry-contract/src/index.ts +2 -0
- package/packages/telemetry-contract/src/types.ts +163 -0
- package/packages/telemetry-contract/src/validators.ts +109 -0
- package/skill/SKILL.md +180 -33
- package/skill/Workflows/AutoActivation.md +145 -0
- package/skill/Workflows/Badge.md +124 -0
- package/skill/Workflows/Baseline.md +144 -0
- package/skill/Workflows/Composability.md +107 -0
- package/skill/Workflows/Contribute.md +94 -0
- package/skill/Workflows/Cron.md +132 -0
- package/skill/Workflows/Dashboard.md +214 -0
- package/skill/Workflows/Doctor.md +63 -14
- package/skill/Workflows/Evals.md +110 -18
- package/skill/Workflows/EvolutionMemory.md +154 -0
- package/skill/Workflows/Evolve.md +181 -21
- package/skill/Workflows/EvolveBody.md +159 -0
- package/skill/Workflows/Grade.md +36 -31
- package/skill/Workflows/ImportSkillsBench.md +117 -0
- package/skill/Workflows/Ingest.md +142 -21
- package/skill/Workflows/Initialize.md +91 -23
- package/skill/Workflows/Orchestrate.md +139 -0
- package/skill/Workflows/Replay.md +91 -0
- package/skill/Workflows/Rollback.md +23 -4
- package/skill/Workflows/Schedule.md +61 -0
- package/skill/Workflows/Sync.md +88 -0
- package/skill/Workflows/UnitTest.md +150 -0
- package/skill/Workflows/Watch.md +33 -1
- package/skill/Workflows/Workflows.md +129 -0
- package/skill/assets/activation-rules-default.json +26 -0
- package/skill/assets/multi-skill-settings.json +63 -0
- package/skill/assets/single-skill-settings.json +57 -0
- package/skill/references/invocation-taxonomy.md +2 -2
- package/skill/references/logs.md +164 -2
- package/skill/references/setup-patterns.md +65 -0
- package/skill/references/version-history.md +40 -0
- package/skill/settings_snippet.json +23 -0
- package/templates/activation-rules-default.json +27 -0
- package/templates/multi-skill-settings.json +64 -0
- package/templates/single-skill-settings.json +58 -0
- package/dashboard/index.html +0 -1119
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical telemetry normalization helpers.
|
|
3
|
+
*
|
|
4
|
+
* This module provides shared functions that all platform adapters call
|
|
5
|
+
* to produce canonical records alongside their raw JSONL output.
|
|
6
|
+
*
|
|
7
|
+
* Contract rules (from telemetry-field-map.md):
|
|
8
|
+
* 1. Normalization is additive — raw capture is preserved separately.
|
|
9
|
+
* 2. Every canonical record includes platform, capture_mode,
|
|
10
|
+
* source_session_kind, session_id, and raw_source_ref.
|
|
11
|
+
* 3. prompt_kind, is_actionable, invocation_mode, and confidence are
|
|
12
|
+
* normalization outputs, not downstream heuristics.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { createHash } from "node:crypto";
|
|
16
|
+
import {
|
|
17
|
+
appendFileSync,
|
|
18
|
+
existsSync,
|
|
19
|
+
mkdirSync,
|
|
20
|
+
readFileSync,
|
|
21
|
+
renameSync,
|
|
22
|
+
rmSync,
|
|
23
|
+
statSync,
|
|
24
|
+
writeFileSync,
|
|
25
|
+
} from "node:fs";
|
|
26
|
+
import { basename, dirname } from "node:path";
|
|
27
|
+
import { CANONICAL_LOG, canonicalSessionStatePath } from "./constants.js";
|
|
28
|
+
import {
|
|
29
|
+
CANONICAL_SCHEMA_VERSION,
|
|
30
|
+
type CanonicalCaptureMode,
|
|
31
|
+
type CanonicalCompletionStatus,
|
|
32
|
+
type CanonicalExecutionFactRecord,
|
|
33
|
+
type CanonicalInvocationMode,
|
|
34
|
+
type CanonicalPlatform,
|
|
35
|
+
type CanonicalPromptKind,
|
|
36
|
+
type CanonicalPromptRecord,
|
|
37
|
+
type CanonicalRawSourceRef,
|
|
38
|
+
type CanonicalRecord,
|
|
39
|
+
type CanonicalSessionRecord,
|
|
40
|
+
type CanonicalSessionRecordBase,
|
|
41
|
+
type CanonicalSkillInvocationRecord,
|
|
42
|
+
type CanonicalSourceSessionKind,
|
|
43
|
+
} from "./types.js";
|
|
44
|
+
import { isActionableQueryText } from "./utils/query-filter.js";
|
|
45
|
+
|
|
46
|
+
/** Current normalizer version. Bump on logic changes. */
|
|
47
|
+
export const NORMALIZER_VERSION = "1.0.0";
|
|
48
|
+
|
|
49
|
+
interface CanonicalPromptSessionState {
|
|
50
|
+
session_id: string;
|
|
51
|
+
next_prompt_index: number;
|
|
52
|
+
last_prompt_id?: string;
|
|
53
|
+
last_actionable_prompt_id?: string;
|
|
54
|
+
updated_at: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface PromptStateLockMetadata {
|
|
58
|
+
owner_id: string;
|
|
59
|
+
pid: number;
|
|
60
|
+
acquired_at: string;
|
|
61
|
+
heartbeat_at: string;
|
|
62
|
+
state_path: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const PROMPT_STATE_LOCK_TIMEOUT_MS = 30_000;
|
|
66
|
+
const PROMPT_STATE_LOCK_POLL_MS = 25;
|
|
67
|
+
const PROMPT_STATE_LOCK_SAB = new SharedArrayBuffer(4);
|
|
68
|
+
const PROMPT_STATE_LOCK_VIEW = new Int32Array(PROMPT_STATE_LOCK_SAB);
|
|
69
|
+
|
|
70
|
+
function sleepSync(ms: number): void {
|
|
71
|
+
Atomics.wait(PROMPT_STATE_LOCK_VIEW, 0, 0, ms);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function defaultPromptSessionState(sessionId: string): CanonicalPromptSessionState {
|
|
75
|
+
return {
|
|
76
|
+
session_id: sessionId,
|
|
77
|
+
next_prompt_index: 0,
|
|
78
|
+
updated_at: new Date().toISOString(),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function derivePromptSessionStateFromCanonicalLog(
|
|
83
|
+
sessionId: string,
|
|
84
|
+
canonicalLogPath: string = CANONICAL_LOG,
|
|
85
|
+
): CanonicalPromptSessionState {
|
|
86
|
+
const recovered = defaultPromptSessionState(sessionId);
|
|
87
|
+
let maxPromptIndex = -1;
|
|
88
|
+
let maxActionablePromptIndex = -1;
|
|
89
|
+
|
|
90
|
+
if (!existsSync(canonicalLogPath)) {
|
|
91
|
+
return recovered;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let content = "";
|
|
95
|
+
try {
|
|
96
|
+
content = readFileSync(canonicalLogPath, "utf-8");
|
|
97
|
+
} catch {
|
|
98
|
+
return recovered;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (const rawLine of content.split("\n")) {
|
|
102
|
+
const line = rawLine.trim();
|
|
103
|
+
if (!line) continue;
|
|
104
|
+
|
|
105
|
+
let parsed: Record<string, unknown>;
|
|
106
|
+
try {
|
|
107
|
+
parsed = JSON.parse(line);
|
|
108
|
+
} catch {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (parsed.record_kind !== "prompt" || parsed.session_id !== sessionId) continue;
|
|
113
|
+
|
|
114
|
+
const promptId = typeof parsed.prompt_id === "string" ? parsed.prompt_id : undefined;
|
|
115
|
+
let promptIndex =
|
|
116
|
+
typeof parsed.prompt_index === "number" && Number.isFinite(parsed.prompt_index)
|
|
117
|
+
? parsed.prompt_index
|
|
118
|
+
: undefined;
|
|
119
|
+
|
|
120
|
+
if (promptIndex === undefined && promptId) {
|
|
121
|
+
const match = /:p(\d+)$/.exec(promptId);
|
|
122
|
+
if (match) {
|
|
123
|
+
promptIndex = Number.parseInt(match[1], 10);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (promptIndex === undefined || !Number.isFinite(promptIndex)) continue;
|
|
128
|
+
|
|
129
|
+
if (promptIndex >= maxPromptIndex) {
|
|
130
|
+
maxPromptIndex = promptIndex;
|
|
131
|
+
recovered.last_prompt_id = promptId ?? derivePromptId(sessionId, promptIndex);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (parsed.is_actionable === true && promptIndex >= maxActionablePromptIndex) {
|
|
135
|
+
maxActionablePromptIndex = promptIndex;
|
|
136
|
+
recovered.last_actionable_prompt_id = promptId ?? derivePromptId(sessionId, promptIndex);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
recovered.next_prompt_index = maxPromptIndex >= 0 ? maxPromptIndex + 1 : 0;
|
|
141
|
+
return recovered;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function archiveCorruptPromptSessionState(path: string): void {
|
|
145
|
+
if (!existsSync(path)) return;
|
|
146
|
+
const archivedPath = `${path}.corrupt-${Date.now()}`;
|
|
147
|
+
renameSync(path, archivedPath);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function joinPromptStateLockPath(path: string): string {
|
|
151
|
+
return `${path}.lock`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function joinPromptStateLockMetadataPath(lockPath: string): string {
|
|
155
|
+
return `${lockPath}/owner.json`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function writePromptStateLockMetadata(lockPath: string, ownerId: string, statePath: string): void {
|
|
159
|
+
const now = new Date().toISOString();
|
|
160
|
+
const metadataPath = joinPromptStateLockMetadataPath(lockPath);
|
|
161
|
+
const metadata: PromptStateLockMetadata = {
|
|
162
|
+
owner_id: ownerId,
|
|
163
|
+
pid: process.pid,
|
|
164
|
+
acquired_at: now,
|
|
165
|
+
heartbeat_at: now,
|
|
166
|
+
state_path: statePath,
|
|
167
|
+
};
|
|
168
|
+
writeFileSync(metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function readPromptStateLockMetadata(lockPath: string): PromptStateLockMetadata | null {
|
|
172
|
+
const metadataPath = joinPromptStateLockMetadataPath(lockPath);
|
|
173
|
+
if (!existsSync(metadataPath)) return null;
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
const parsed = JSON.parse(readFileSync(metadataPath, "utf-8")) as PromptStateLockMetadata;
|
|
177
|
+
if (
|
|
178
|
+
typeof parsed.owner_id === "string" &&
|
|
179
|
+
typeof parsed.pid === "number" &&
|
|
180
|
+
typeof parsed.heartbeat_at === "string"
|
|
181
|
+
) {
|
|
182
|
+
return parsed;
|
|
183
|
+
}
|
|
184
|
+
} catch {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function touchPromptStateLock(lockPath: string, ownerId: string, statePath: string): void {
|
|
192
|
+
const metadataPath = joinPromptStateLockMetadataPath(lockPath);
|
|
193
|
+
const current = readPromptStateLockMetadata(lockPath);
|
|
194
|
+
if (current && current.owner_id !== ownerId) return;
|
|
195
|
+
|
|
196
|
+
const now = new Date().toISOString();
|
|
197
|
+
const metadata: PromptStateLockMetadata = {
|
|
198
|
+
owner_id: ownerId,
|
|
199
|
+
pid: process.pid,
|
|
200
|
+
acquired_at: current?.acquired_at ?? now,
|
|
201
|
+
heartbeat_at: now,
|
|
202
|
+
state_path: statePath,
|
|
203
|
+
};
|
|
204
|
+
writeFileSync(metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function loadPromptSessionState(
|
|
208
|
+
path: string,
|
|
209
|
+
sessionId: string,
|
|
210
|
+
canonicalLogPath: string = CANONICAL_LOG,
|
|
211
|
+
options?: { archiveCorrupt?: boolean },
|
|
212
|
+
): CanonicalPromptSessionState {
|
|
213
|
+
if (!existsSync(path)) {
|
|
214
|
+
return derivePromptSessionStateFromCanonicalLog(sessionId, canonicalLogPath);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8")) as CanonicalPromptSessionState;
|
|
219
|
+
if (
|
|
220
|
+
parsed.session_id === sessionId &&
|
|
221
|
+
typeof parsed.next_prompt_index === "number" &&
|
|
222
|
+
Number.isFinite(parsed.next_prompt_index)
|
|
223
|
+
) {
|
|
224
|
+
return parsed;
|
|
225
|
+
}
|
|
226
|
+
} catch {
|
|
227
|
+
// fall through to canonical-log recovery
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (options?.archiveCorrupt) {
|
|
231
|
+
try {
|
|
232
|
+
archiveCorruptPromptSessionState(path);
|
|
233
|
+
} catch {
|
|
234
|
+
// Ignore archive failures and recover from canonical log instead.
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return derivePromptSessionStateFromCanonicalLog(sessionId, canonicalLogPath);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function savePromptSessionState(path: string, state: CanonicalPromptSessionState): void {
|
|
242
|
+
const dir = dirname(path);
|
|
243
|
+
if (!existsSync(dir)) {
|
|
244
|
+
mkdirSync(dir, { recursive: true });
|
|
245
|
+
}
|
|
246
|
+
const tempPath = joinTempStatePath(path);
|
|
247
|
+
writeFileSync(tempPath, JSON.stringify(state, null, 2), "utf-8");
|
|
248
|
+
renameSync(tempPath, path);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function joinTempStatePath(path: string): string {
|
|
252
|
+
return `${dirname(path)}/.${basename(path)}.tmp-${process.pid}-${Date.now()}`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function isStaleLock(lockPath: string): boolean {
|
|
256
|
+
const metadata = readPromptStateLockMetadata(lockPath);
|
|
257
|
+
try {
|
|
258
|
+
const heartbeatAt = metadata ? Date.parse(metadata.heartbeat_at) : statSync(lockPath).mtimeMs;
|
|
259
|
+
if (!Number.isFinite(heartbeatAt)) return false;
|
|
260
|
+
return Date.now() - heartbeatAt > PROMPT_STATE_LOCK_TIMEOUT_MS;
|
|
261
|
+
} catch {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function withPromptStateLock<T>(statePath: string, fn: () => T): T {
|
|
267
|
+
const dir = dirname(statePath);
|
|
268
|
+
if (!existsSync(dir)) {
|
|
269
|
+
mkdirSync(dir, { recursive: true });
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const lockPath = joinPromptStateLockPath(statePath);
|
|
273
|
+
const deadline = Date.now() + PROMPT_STATE_LOCK_TIMEOUT_MS;
|
|
274
|
+
const ownerId = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
275
|
+
|
|
276
|
+
while (true) {
|
|
277
|
+
try {
|
|
278
|
+
mkdirSync(lockPath);
|
|
279
|
+
writePromptStateLockMetadata(lockPath, ownerId, statePath);
|
|
280
|
+
break;
|
|
281
|
+
} catch (error) {
|
|
282
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
283
|
+
if (code !== "EEXIST") throw error;
|
|
284
|
+
|
|
285
|
+
if (isStaleLock(lockPath)) {
|
|
286
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (Date.now() >= deadline) {
|
|
291
|
+
throw new Error(`Timed out acquiring prompt state lock for ${statePath}`);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
sleepSync(PROMPT_STATE_LOCK_POLL_MS);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
touchPromptStateLock(lockPath, ownerId, statePath);
|
|
300
|
+
return fn();
|
|
301
|
+
} finally {
|
|
302
|
+
const metadata = readPromptStateLockMetadata(lockPath);
|
|
303
|
+
if (!metadata || metadata.owner_id === ownerId) {
|
|
304
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export interface CanonicalPromptIdentity {
|
|
310
|
+
prompt_id: string;
|
|
311
|
+
prompt_index: number;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function reservePromptIdentity(
|
|
315
|
+
sessionId: string,
|
|
316
|
+
isActionable: boolean,
|
|
317
|
+
statePath: string = canonicalSessionStatePath(sessionId),
|
|
318
|
+
canonicalLogPath: string = CANONICAL_LOG,
|
|
319
|
+
): CanonicalPromptIdentity {
|
|
320
|
+
return withPromptStateLock(statePath, () => {
|
|
321
|
+
const state = loadPromptSessionState(statePath, sessionId, canonicalLogPath, {
|
|
322
|
+
archiveCorrupt: true,
|
|
323
|
+
});
|
|
324
|
+
const promptIndex = state.next_prompt_index;
|
|
325
|
+
const promptId = derivePromptId(sessionId, promptIndex);
|
|
326
|
+
|
|
327
|
+
state.next_prompt_index = promptIndex + 1;
|
|
328
|
+
state.last_prompt_id = promptId;
|
|
329
|
+
if (isActionable) state.last_actionable_prompt_id = promptId;
|
|
330
|
+
state.updated_at = new Date().toISOString();
|
|
331
|
+
savePromptSessionState(statePath, state);
|
|
332
|
+
|
|
333
|
+
return { prompt_id: promptId, prompt_index: promptIndex };
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export function getLatestPromptIdentity(
|
|
338
|
+
sessionId: string,
|
|
339
|
+
statePath: string = canonicalSessionStatePath(sessionId),
|
|
340
|
+
canonicalLogPath: string = CANONICAL_LOG,
|
|
341
|
+
): { last_prompt_id?: string; last_actionable_prompt_id?: string } {
|
|
342
|
+
const state = loadPromptSessionState(statePath, sessionId, canonicalLogPath);
|
|
343
|
+
return {
|
|
344
|
+
last_prompt_id: state.last_prompt_id,
|
|
345
|
+
last_actionable_prompt_id: state.last_actionable_prompt_id,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export function appendCanonicalRecord(
|
|
350
|
+
record: CanonicalRecord,
|
|
351
|
+
logPath: string = CANONICAL_LOG,
|
|
352
|
+
): void {
|
|
353
|
+
const dir = dirname(logPath);
|
|
354
|
+
if (!existsSync(dir)) {
|
|
355
|
+
mkdirSync(dir, { recursive: true });
|
|
356
|
+
}
|
|
357
|
+
appendFileSync(logPath, `${JSON.stringify(record)}\n`, "utf-8");
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function appendCanonicalRecords(
|
|
361
|
+
records: CanonicalRecord[],
|
|
362
|
+
logPath: string = CANONICAL_LOG,
|
|
363
|
+
): void {
|
|
364
|
+
for (const record of records) appendCanonicalRecord(record, logPath);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// ---------------------------------------------------------------------------
|
|
368
|
+
// Prompt classification
|
|
369
|
+
// ---------------------------------------------------------------------------
|
|
370
|
+
|
|
371
|
+
const META_PREFIXES = [
|
|
372
|
+
"<system_instruction>",
|
|
373
|
+
"<system-instruction>",
|
|
374
|
+
"<local-command-caveat>",
|
|
375
|
+
"<local-command-stdout>",
|
|
376
|
+
"<local-command-stderr>",
|
|
377
|
+
"<command-name>",
|
|
378
|
+
"Tool loaded.",
|
|
379
|
+
"You are an evaluation assistant.",
|
|
380
|
+
"You are a skill description optimizer",
|
|
381
|
+
"CONTEXT:",
|
|
382
|
+
"Base directory for this skill:",
|
|
383
|
+
"USER'S CURRENT MESSAGE (summarize THIS):",
|
|
384
|
+
];
|
|
385
|
+
|
|
386
|
+
const CONTINUATION_PREFIXES = [
|
|
387
|
+
"This session is being continued from a previous conversation",
|
|
388
|
+
"Continue from where you left off.",
|
|
389
|
+
];
|
|
390
|
+
|
|
391
|
+
const TASK_NOTIFICATION_PREFIXES = ["<task-notification>", "Completing task"];
|
|
392
|
+
|
|
393
|
+
const TEAMMATE_MESSAGE_PREFIXES = ["<teammate-message"];
|
|
394
|
+
|
|
395
|
+
const TOOL_OUTPUT_PREFIXES = ["<tool_result", "<function_result"];
|
|
396
|
+
|
|
397
|
+
const SYSTEM_INSTRUCTION_PREFIXES = [
|
|
398
|
+
"[Automated",
|
|
399
|
+
"[System",
|
|
400
|
+
"[Request interrupted by user for tool use]",
|
|
401
|
+
"[Request interrupted by user]",
|
|
402
|
+
];
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Classify a prompt into a canonical prompt kind.
|
|
406
|
+
* Order matters — more specific prefixes checked first.
|
|
407
|
+
*/
|
|
408
|
+
export function classifyPromptKind(text: string): CanonicalPromptKind {
|
|
409
|
+
if (typeof text !== "string") return "unknown";
|
|
410
|
+
const trimmed = text.trim();
|
|
411
|
+
if (!trimmed) return "unknown";
|
|
412
|
+
|
|
413
|
+
if (TOOL_OUTPUT_PREFIXES.some((p) => trimmed.startsWith(p))) return "tool_output";
|
|
414
|
+
if (SYSTEM_INSTRUCTION_PREFIXES.some((p) => trimmed.startsWith(p))) return "system_instruction";
|
|
415
|
+
if (TASK_NOTIFICATION_PREFIXES.some((p) => trimmed.startsWith(p))) return "task_notification";
|
|
416
|
+
if (TEAMMATE_MESSAGE_PREFIXES.some((p) => trimmed.startsWith(p))) return "teammate_message";
|
|
417
|
+
if (CONTINUATION_PREFIXES.some((p) => trimmed.startsWith(p))) return "continuation";
|
|
418
|
+
if (META_PREFIXES.some((p) => trimmed.startsWith(p))) return "meta";
|
|
419
|
+
|
|
420
|
+
return "user";
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Determine if a prompt is actionable (real user work vs meta/system noise).
|
|
425
|
+
* Delegates to the existing query-filter logic for backward compatibility.
|
|
426
|
+
*/
|
|
427
|
+
export function classifyIsActionable(text: string): boolean {
|
|
428
|
+
return isActionableQueryText(text);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ---------------------------------------------------------------------------
|
|
432
|
+
// Invocation mode
|
|
433
|
+
// ---------------------------------------------------------------------------
|
|
434
|
+
|
|
435
|
+
export interface InvocationClassification {
|
|
436
|
+
invocation_mode: CanonicalInvocationMode;
|
|
437
|
+
confidence: number;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Classify how a skill was invoked.
|
|
442
|
+
*/
|
|
443
|
+
export function deriveInvocationMode(opts: {
|
|
444
|
+
has_skill_tool_call?: boolean;
|
|
445
|
+
has_skill_md_read?: boolean;
|
|
446
|
+
is_text_mention_only?: boolean;
|
|
447
|
+
is_repaired?: boolean;
|
|
448
|
+
}): InvocationClassification {
|
|
449
|
+
if (opts.is_repaired) return { invocation_mode: "repaired", confidence: 0.9 };
|
|
450
|
+
if (opts.has_skill_tool_call) return { invocation_mode: "explicit", confidence: 1.0 };
|
|
451
|
+
if (opts.has_skill_md_read) return { invocation_mode: "implicit", confidence: 0.7 };
|
|
452
|
+
if (opts.is_text_mention_only) return { invocation_mode: "inferred", confidence: 0.4 };
|
|
453
|
+
return { invocation_mode: "inferred", confidence: 0.4 };
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// ---------------------------------------------------------------------------
|
|
457
|
+
// Prompt hashing
|
|
458
|
+
// ---------------------------------------------------------------------------
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Derive a deterministic prompt hash for dedupe and privacy-safe analytics.
|
|
462
|
+
*/
|
|
463
|
+
export function hashPrompt(text: string): string {
|
|
464
|
+
return createHash("sha256").update(text).digest("hex").slice(0, 16);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Derive a deterministic prompt ID from session + index.
|
|
469
|
+
*/
|
|
470
|
+
export function derivePromptId(sessionId: string, index: number): string {
|
|
471
|
+
return `${sessionId}:p${index}`;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Derive a deterministic skill invocation ID.
|
|
476
|
+
*/
|
|
477
|
+
export function deriveSkillInvocationId(
|
|
478
|
+
sessionId: string,
|
|
479
|
+
skillName: string,
|
|
480
|
+
index: number,
|
|
481
|
+
): string {
|
|
482
|
+
return `${sessionId}:s:${skillName}:${index}`;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// ---------------------------------------------------------------------------
|
|
486
|
+
// Canonical record builders
|
|
487
|
+
// ---------------------------------------------------------------------------
|
|
488
|
+
|
|
489
|
+
export interface CanonicalBaseInput {
|
|
490
|
+
platform: CanonicalPlatform;
|
|
491
|
+
capture_mode: CanonicalCaptureMode;
|
|
492
|
+
source_session_kind: CanonicalSourceSessionKind;
|
|
493
|
+
session_id: string;
|
|
494
|
+
raw_source_ref: CanonicalRawSourceRef;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function makeBase(
|
|
498
|
+
record_kind: CanonicalSessionRecordBase["record_kind"],
|
|
499
|
+
input: CanonicalBaseInput,
|
|
500
|
+
): CanonicalSessionRecordBase {
|
|
501
|
+
return {
|
|
502
|
+
record_kind,
|
|
503
|
+
schema_version: CANONICAL_SCHEMA_VERSION,
|
|
504
|
+
normalizer_version: NORMALIZER_VERSION,
|
|
505
|
+
normalized_at: new Date().toISOString(),
|
|
506
|
+
platform: input.platform,
|
|
507
|
+
capture_mode: input.capture_mode,
|
|
508
|
+
source_session_kind: input.source_session_kind,
|
|
509
|
+
session_id: input.session_id,
|
|
510
|
+
raw_source_ref: input.raw_source_ref,
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
export interface BuildSessionInput extends CanonicalBaseInput {
|
|
515
|
+
started_at?: string;
|
|
516
|
+
ended_at?: string;
|
|
517
|
+
external_session_id?: string;
|
|
518
|
+
parent_session_id?: string;
|
|
519
|
+
agent_id?: string;
|
|
520
|
+
agent_type?: string;
|
|
521
|
+
agent_cli?: string;
|
|
522
|
+
session_key?: string;
|
|
523
|
+
channel?: string;
|
|
524
|
+
workspace_path?: string;
|
|
525
|
+
repo_root?: string;
|
|
526
|
+
repo_remote?: string;
|
|
527
|
+
branch?: string;
|
|
528
|
+
commit_sha?: string;
|
|
529
|
+
permission_mode?: string;
|
|
530
|
+
approval_policy?: string;
|
|
531
|
+
sandbox_policy?: string;
|
|
532
|
+
provider?: string;
|
|
533
|
+
model?: string;
|
|
534
|
+
completion_status?: CanonicalCompletionStatus;
|
|
535
|
+
end_reason?: string;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
export function buildCanonicalSession(input: BuildSessionInput): CanonicalSessionRecord {
|
|
539
|
+
const base = makeBase("session", input);
|
|
540
|
+
const record: CanonicalSessionRecord = { ...base, record_kind: "session" };
|
|
541
|
+
|
|
542
|
+
// Copy optional fields only when present
|
|
543
|
+
if (input.started_at !== undefined) record.started_at = input.started_at;
|
|
544
|
+
if (input.ended_at !== undefined) record.ended_at = input.ended_at;
|
|
545
|
+
if (input.external_session_id !== undefined)
|
|
546
|
+
record.external_session_id = input.external_session_id;
|
|
547
|
+
if (input.parent_session_id !== undefined) record.parent_session_id = input.parent_session_id;
|
|
548
|
+
if (input.agent_id !== undefined) record.agent_id = input.agent_id;
|
|
549
|
+
if (input.agent_type !== undefined) record.agent_type = input.agent_type;
|
|
550
|
+
if (input.agent_cli !== undefined) record.agent_cli = input.agent_cli;
|
|
551
|
+
if (input.session_key !== undefined) record.session_key = input.session_key;
|
|
552
|
+
if (input.channel !== undefined) record.channel = input.channel;
|
|
553
|
+
if (input.workspace_path !== undefined) record.workspace_path = input.workspace_path;
|
|
554
|
+
if (input.repo_root !== undefined) record.repo_root = input.repo_root;
|
|
555
|
+
if (input.repo_remote !== undefined) record.repo_remote = input.repo_remote;
|
|
556
|
+
if (input.branch !== undefined) record.branch = input.branch;
|
|
557
|
+
if (input.commit_sha !== undefined) record.commit_sha = input.commit_sha;
|
|
558
|
+
if (input.permission_mode !== undefined) record.permission_mode = input.permission_mode;
|
|
559
|
+
if (input.approval_policy !== undefined) record.approval_policy = input.approval_policy;
|
|
560
|
+
if (input.sandbox_policy !== undefined) record.sandbox_policy = input.sandbox_policy;
|
|
561
|
+
if (input.provider !== undefined) record.provider = input.provider;
|
|
562
|
+
if (input.model !== undefined) record.model = input.model;
|
|
563
|
+
if (input.completion_status !== undefined) record.completion_status = input.completion_status;
|
|
564
|
+
if (input.end_reason !== undefined) record.end_reason = input.end_reason;
|
|
565
|
+
|
|
566
|
+
return record;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
export interface BuildPromptInput extends CanonicalBaseInput {
|
|
570
|
+
prompt_id: string;
|
|
571
|
+
occurred_at: string;
|
|
572
|
+
prompt_text: string;
|
|
573
|
+
prompt_kind?: CanonicalPromptKind;
|
|
574
|
+
is_actionable?: boolean;
|
|
575
|
+
prompt_hash?: string;
|
|
576
|
+
prompt_index?: number;
|
|
577
|
+
parent_prompt_id?: string;
|
|
578
|
+
source_message_id?: string;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
export function buildCanonicalPrompt(input: BuildPromptInput): CanonicalPromptRecord {
|
|
582
|
+
const base = makeBase("prompt", input);
|
|
583
|
+
const kind = input.prompt_kind ?? classifyPromptKind(input.prompt_text);
|
|
584
|
+
const actionable = input.is_actionable ?? classifyIsActionable(input.prompt_text);
|
|
585
|
+
|
|
586
|
+
const record: CanonicalPromptRecord = {
|
|
587
|
+
...base,
|
|
588
|
+
record_kind: "prompt",
|
|
589
|
+
prompt_id: input.prompt_id,
|
|
590
|
+
occurred_at: input.occurred_at,
|
|
591
|
+
prompt_text: input.prompt_text,
|
|
592
|
+
prompt_hash: input.prompt_hash ?? hashPrompt(input.prompt_text),
|
|
593
|
+
prompt_kind: kind,
|
|
594
|
+
is_actionable: actionable,
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
if (input.prompt_index !== undefined) record.prompt_index = input.prompt_index;
|
|
598
|
+
if (input.parent_prompt_id !== undefined) record.parent_prompt_id = input.parent_prompt_id;
|
|
599
|
+
if (input.source_message_id !== undefined) record.source_message_id = input.source_message_id;
|
|
600
|
+
|
|
601
|
+
return record;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
export interface BuildSkillInvocationInput extends CanonicalBaseInput {
|
|
605
|
+
skill_invocation_id: string;
|
|
606
|
+
occurred_at: string;
|
|
607
|
+
matched_prompt_id?: string;
|
|
608
|
+
skill_name: string;
|
|
609
|
+
skill_path?: string;
|
|
610
|
+
skill_version_hash?: string;
|
|
611
|
+
invocation_mode: CanonicalInvocationMode;
|
|
612
|
+
triggered: boolean;
|
|
613
|
+
confidence: number;
|
|
614
|
+
tool_name?: string;
|
|
615
|
+
tool_call_id?: string;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
export function buildCanonicalSkillInvocation(
|
|
619
|
+
input: BuildSkillInvocationInput,
|
|
620
|
+
): CanonicalSkillInvocationRecord {
|
|
621
|
+
const base = makeBase("skill_invocation", input);
|
|
622
|
+
|
|
623
|
+
const record: CanonicalSkillInvocationRecord = {
|
|
624
|
+
...base,
|
|
625
|
+
record_kind: "skill_invocation",
|
|
626
|
+
skill_invocation_id: input.skill_invocation_id,
|
|
627
|
+
occurred_at: input.occurred_at,
|
|
628
|
+
skill_name: input.skill_name,
|
|
629
|
+
invocation_mode: input.invocation_mode,
|
|
630
|
+
triggered: input.triggered,
|
|
631
|
+
confidence: input.confidence,
|
|
632
|
+
};
|
|
633
|
+
|
|
634
|
+
if (input.matched_prompt_id !== undefined) record.matched_prompt_id = input.matched_prompt_id;
|
|
635
|
+
if (input.skill_path !== undefined) record.skill_path = input.skill_path;
|
|
636
|
+
if (input.skill_version_hash !== undefined) record.skill_version_hash = input.skill_version_hash;
|
|
637
|
+
if (input.tool_name !== undefined) record.tool_name = input.tool_name;
|
|
638
|
+
if (input.tool_call_id !== undefined) record.tool_call_id = input.tool_call_id;
|
|
639
|
+
|
|
640
|
+
return record;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
export interface BuildExecutionFactInput extends CanonicalBaseInput {
|
|
644
|
+
occurred_at: string;
|
|
645
|
+
prompt_id?: string;
|
|
646
|
+
tool_calls_json: Record<string, number>;
|
|
647
|
+
total_tool_calls: number;
|
|
648
|
+
bash_commands_redacted: string[];
|
|
649
|
+
assistant_turns: number;
|
|
650
|
+
errors_encountered: number;
|
|
651
|
+
input_tokens?: number;
|
|
652
|
+
output_tokens?: number;
|
|
653
|
+
duration_ms?: number;
|
|
654
|
+
completion_status?: CanonicalCompletionStatus;
|
|
655
|
+
end_reason?: string;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
export function buildCanonicalExecutionFact(
|
|
659
|
+
input: BuildExecutionFactInput,
|
|
660
|
+
): CanonicalExecutionFactRecord {
|
|
661
|
+
const base = makeBase("execution_fact", input);
|
|
662
|
+
|
|
663
|
+
const record: CanonicalExecutionFactRecord = {
|
|
664
|
+
...base,
|
|
665
|
+
record_kind: "execution_fact",
|
|
666
|
+
occurred_at: input.occurred_at,
|
|
667
|
+
tool_calls_json: input.tool_calls_json,
|
|
668
|
+
total_tool_calls: input.total_tool_calls,
|
|
669
|
+
bash_commands_redacted: input.bash_commands_redacted,
|
|
670
|
+
assistant_turns: input.assistant_turns,
|
|
671
|
+
errors_encountered: input.errors_encountered,
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
if (input.prompt_id !== undefined) record.prompt_id = input.prompt_id;
|
|
675
|
+
if (input.input_tokens !== undefined) record.input_tokens = input.input_tokens;
|
|
676
|
+
if (input.output_tokens !== undefined) record.output_tokens = input.output_tokens;
|
|
677
|
+
if (input.duration_ms !== undefined) record.duration_ms = input.duration_ms;
|
|
678
|
+
if (input.completion_status !== undefined) record.completion_status = input.completion_status;
|
|
679
|
+
if (input.end_reason !== undefined) record.end_reason = input.end_reason;
|
|
680
|
+
|
|
681
|
+
return record;
|
|
682
|
+
}
|