selftune 0.2.6 → 0.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/apps/local-dashboard/dist/assets/index-Bk9vSHHd.js +15 -0
- package/apps/local-dashboard/dist/assets/index-CRtLkBTi.css +1 -0
- package/apps/local-dashboard/dist/assets/vendor-react-BQH_6WrG.js +60 -0
- package/apps/local-dashboard/dist/assets/{vendor-table-B7VF2Ipl.js → vendor-table-dK1QMLq9.js} +1 -1
- package/apps/local-dashboard/dist/assets/{vendor-ui-r2k_Ku_V.js → vendor-ui-CO2mrx6e.js} +60 -65
- package/apps/local-dashboard/dist/index.html +5 -5
- package/cli/selftune/activation-rules.ts +30 -9
- package/cli/selftune/agent-guidance.ts +96 -0
- package/cli/selftune/alpha-identity.ts +157 -0
- package/cli/selftune/alpha-upload/build-payloads.ts +151 -0
- package/cli/selftune/alpha-upload/client.ts +113 -0
- package/cli/selftune/alpha-upload/flush.ts +191 -0
- package/cli/selftune/alpha-upload/index.ts +194 -0
- package/cli/selftune/alpha-upload/queue.ts +252 -0
- package/cli/selftune/alpha-upload/stage-canonical.ts +242 -0
- package/cli/selftune/alpha-upload-contract.ts +52 -0
- package/cli/selftune/auth/device-code.ts +110 -0
- package/cli/selftune/auto-update.ts +130 -0
- package/cli/selftune/badge/badge.ts +19 -9
- package/cli/selftune/canonical-export.ts +16 -3
- package/cli/selftune/constants.ts +28 -8
- package/cli/selftune/contribute/bundle.ts +32 -5
- package/cli/selftune/dashboard-contract.ts +32 -1
- package/cli/selftune/dashboard-server.ts +256 -692
- package/cli/selftune/dashboard.ts +1 -1
- package/cli/selftune/eval/baseline.ts +11 -7
- package/cli/selftune/eval/hooks-to-evals.ts +27 -9
- package/cli/selftune/eval/synthetic-evals.ts +54 -1
- package/cli/selftune/evolution/audit.ts +24 -19
- package/cli/selftune/evolution/constitutional.ts +176 -0
- package/cli/selftune/evolution/evidence.ts +18 -13
- package/cli/selftune/evolution/evolve-body.ts +104 -7
- package/cli/selftune/evolution/evolve.ts +195 -22
- package/cli/selftune/evolution/propose-body.ts +18 -1
- package/cli/selftune/evolution/propose-description.ts +27 -2
- package/cli/selftune/evolution/rollback.ts +11 -15
- package/cli/selftune/export.ts +84 -0
- package/cli/selftune/grading/auto-grade.ts +13 -4
- package/cli/selftune/grading/grade-session.ts +16 -6
- package/cli/selftune/hooks/evolution-guard.ts +26 -9
- package/cli/selftune/hooks/prompt-log.ts +23 -9
- package/cli/selftune/hooks/session-stop.ts +78 -15
- package/cli/selftune/hooks/skill-eval.ts +189 -10
- package/cli/selftune/index.ts +274 -2
- package/cli/selftune/ingestors/claude-replay.ts +48 -21
- package/cli/selftune/init.ts +249 -47
- package/cli/selftune/last.ts +7 -7
- package/cli/selftune/localdb/db.ts +90 -10
- package/cli/selftune/localdb/direct-write.ts +531 -0
- package/cli/selftune/localdb/materialize.ts +296 -42
- package/cli/selftune/localdb/queries.ts +325 -32
- package/cli/selftune/localdb/schema.ts +109 -0
- package/cli/selftune/monitoring/watch.ts +26 -8
- package/cli/selftune/normalization.ts +85 -15
- package/cli/selftune/observability.ts +248 -2
- package/cli/selftune/orchestrate.ts +165 -20
- package/cli/selftune/quickstart.ts +34 -10
- package/cli/selftune/repair/skill-usage.ts +12 -2
- package/cli/selftune/routes/actions.ts +77 -0
- package/cli/selftune/routes/badge.ts +66 -0
- package/cli/selftune/routes/doctor.ts +12 -0
- package/cli/selftune/routes/index.ts +14 -0
- package/cli/selftune/routes/orchestrate-runs.ts +13 -0
- package/cli/selftune/routes/overview.ts +14 -0
- package/cli/selftune/routes/report.ts +293 -0
- package/cli/selftune/routes/skill-report.ts +230 -0
- package/cli/selftune/status.ts +203 -7
- package/cli/selftune/sync.ts +13 -1
- package/cli/selftune/types.ts +50 -0
- package/cli/selftune/utils/jsonl.ts +58 -1
- package/cli/selftune/utils/selftune-meta.ts +38 -0
- package/cli/selftune/utils/skill-log.ts +30 -4
- package/cli/selftune/utils/transcript.ts +15 -0
- package/cli/selftune/workflows/workflows.ts +7 -6
- package/package.json +10 -6
- package/packages/telemetry-contract/fixtures/complete-push.ts +184 -0
- package/packages/telemetry-contract/fixtures/evidence-only-push.ts +58 -0
- package/packages/telemetry-contract/fixtures/golden.json +1 -0
- package/packages/telemetry-contract/fixtures/index.ts +4 -0
- package/packages/telemetry-contract/fixtures/partial-push-no-sessions.ts +40 -0
- package/packages/telemetry-contract/fixtures/partial-push-unresolved-parents.ts +79 -0
- package/packages/telemetry-contract/package.json +6 -1
- package/packages/telemetry-contract/src/index.ts +1 -0
- package/packages/telemetry-contract/src/schemas.ts +215 -0
- package/packages/telemetry-contract/src/types.ts +3 -1
- package/packages/telemetry-contract/src/validators.ts +3 -1
- package/packages/telemetry-contract/tests/compatibility.test.ts +144 -0
- package/packages/ui/package.json +4 -0
- package/packages/ui/src/components/ActivityTimeline.tsx +61 -29
- package/packages/ui/src/components/section-cards.tsx +31 -14
- package/packages/ui/src/types.ts +1 -0
- package/skill/SKILL.md +214 -174
- package/skill/Workflows/AlphaUpload.md +45 -0
- package/skill/Workflows/Baseline.md +18 -12
- package/skill/Workflows/Composability.md +3 -3
- package/skill/Workflows/Dashboard.md +44 -91
- package/skill/Workflows/Doctor.md +93 -66
- package/skill/Workflows/Evals.md +49 -40
- package/skill/Workflows/Evolve.md +76 -28
- package/skill/Workflows/EvolveBody.md +37 -38
- package/skill/Workflows/Initialize.md +172 -26
- package/skill/Workflows/Orchestrate.md +11 -2
- package/skill/Workflows/Sync.md +23 -0
- package/skill/Workflows/Watch.md +2 -5
- package/skill/agents/diagnosis-analyst.md +163 -0
- package/skill/agents/evolution-reviewer.md +149 -0
- package/skill/agents/integration-guide.md +154 -0
- package/skill/agents/pattern-analyst.md +149 -0
- package/skill/assets/multi-skill-settings.json +1 -1
- package/skill/assets/single-skill-settings.json +1 -1
- package/skill/references/interactive-config.md +39 -0
- package/skill/references/invocation-taxonomy.md +34 -0
- package/skill/references/logs.md +9 -1
- package/skill/references/setup-patterns.md +3 -3
- package/skill/settings_snippet.json +1 -1
- package/apps/local-dashboard/dist/assets/index-C75H1Q3n.css +0 -1
- package/apps/local-dashboard/dist/assets/index-axE4kz3Q.js +0 -15
- package/apps/local-dashboard/dist/assets/vendor-react-U7zYD9Rg.js +0 -60
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Canonical telemetry normalization helpers.
|
|
3
3
|
*
|
|
4
4
|
* This module provides shared functions that all platform adapters call
|
|
5
|
-
* to produce canonical records
|
|
5
|
+
* to produce canonical records written to SQLite via writeCanonicalToDb().
|
|
6
6
|
*
|
|
7
7
|
* Contract rules (from telemetry-field-map.md):
|
|
8
8
|
* 1. Normalization is additive — raw capture is preserved separately.
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
} from "node:fs";
|
|
26
26
|
import { basename, dirname } from "node:path";
|
|
27
27
|
import { CANONICAL_LOG, canonicalSessionStatePath } from "./constants.js";
|
|
28
|
+
import { writeCanonicalBatchToDb, writeCanonicalToDb } from "./localdb/direct-write.js";
|
|
28
29
|
import {
|
|
29
30
|
CANONICAL_SCHEMA_VERSION,
|
|
30
31
|
type CanonicalCaptureMode,
|
|
@@ -81,9 +82,46 @@ function defaultPromptSessionState(sessionId: string): CanonicalPromptSessionSta
|
|
|
81
82
|
|
|
82
83
|
function derivePromptSessionStateFromCanonicalLog(
|
|
83
84
|
sessionId: string,
|
|
84
|
-
|
|
85
|
+
_canonicalLogPath: string = CANONICAL_LOG,
|
|
85
86
|
): CanonicalPromptSessionState {
|
|
86
87
|
const recovered = defaultPromptSessionState(sessionId);
|
|
88
|
+
|
|
89
|
+
// Try SQLite first — canonical records now go to the local DB.
|
|
90
|
+
// Uses dynamic require + try/catch so this remains fail-safe during
|
|
91
|
+
// hook execution when the DB module may not be loadable.
|
|
92
|
+
try {
|
|
93
|
+
const { getDb } = require("./localdb/db.js") as {
|
|
94
|
+
getDb: () => import("bun:sqlite").Database;
|
|
95
|
+
};
|
|
96
|
+
const db = getDb();
|
|
97
|
+
const rows = db
|
|
98
|
+
.query(
|
|
99
|
+
"SELECT prompt_id, prompt_index, is_actionable FROM prompts WHERE session_id = ? ORDER BY prompt_index DESC LIMIT 1",
|
|
100
|
+
)
|
|
101
|
+
.all(sessionId) as Array<{
|
|
102
|
+
prompt_id: string;
|
|
103
|
+
prompt_index: number;
|
|
104
|
+
is_actionable: number;
|
|
105
|
+
}>;
|
|
106
|
+
if (rows.length > 0) {
|
|
107
|
+
const row = rows[0];
|
|
108
|
+
recovered.next_prompt_index = row.prompt_index + 1;
|
|
109
|
+
recovered.last_prompt_id = row.prompt_id;
|
|
110
|
+
// Get last actionable
|
|
111
|
+
const actionable = db
|
|
112
|
+
.query(
|
|
113
|
+
"SELECT prompt_id, prompt_index FROM prompts WHERE session_id = ? AND is_actionable = 1 ORDER BY prompt_index DESC LIMIT 1",
|
|
114
|
+
)
|
|
115
|
+
.get(sessionId) as { prompt_id: string; prompt_index: number } | null;
|
|
116
|
+
if (actionable) recovered.last_actionable_prompt_id = actionable.prompt_id;
|
|
117
|
+
return recovered;
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
// DB unavailable — fall through to JSONL recovery below.
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Fallback: scan canonical JSONL log (legacy path or DB unavailable).
|
|
124
|
+
const canonicalLogPath = _canonicalLogPath;
|
|
87
125
|
let maxPromptIndex = -1;
|
|
88
126
|
let maxActionablePromptIndex = -1;
|
|
89
127
|
|
|
@@ -346,22 +384,32 @@ export function getLatestPromptIdentity(
|
|
|
346
384
|
};
|
|
347
385
|
}
|
|
348
386
|
|
|
349
|
-
export function appendCanonicalRecord(
|
|
350
|
-
record
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
mkdirSync(dir, { recursive: true });
|
|
387
|
+
export function appendCanonicalRecord(record: CanonicalRecord, logPath?: string): void {
|
|
388
|
+
writeCanonicalToDb(record);
|
|
389
|
+
// JSONL append — best-effort backup for prompt state recovery
|
|
390
|
+
try {
|
|
391
|
+
const path = logPath ?? CANONICAL_LOG;
|
|
392
|
+
const dir = dirname(path);
|
|
393
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
394
|
+
appendFileSync(path, `${JSON.stringify(record)}\n`, "utf-8");
|
|
395
|
+
} catch {
|
|
396
|
+
/* best-effort only */
|
|
356
397
|
}
|
|
357
|
-
appendFileSync(logPath, `${JSON.stringify(record)}\n`, "utf-8");
|
|
358
398
|
}
|
|
359
399
|
|
|
360
|
-
export function appendCanonicalRecords(
|
|
361
|
-
records
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
400
|
+
export function appendCanonicalRecords(records: CanonicalRecord[], logPath?: string): void {
|
|
401
|
+
writeCanonicalBatchToDb(records);
|
|
402
|
+
// JSONL append — best-effort backup for prompt state recovery
|
|
403
|
+
try {
|
|
404
|
+
const path = logPath ?? CANONICAL_LOG;
|
|
405
|
+
const dir = dirname(path);
|
|
406
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
407
|
+
for (const record of records) {
|
|
408
|
+
appendFileSync(path, `${JSON.stringify(record)}\n`, "utf-8");
|
|
409
|
+
}
|
|
410
|
+
} catch {
|
|
411
|
+
/* best-effort only */
|
|
412
|
+
}
|
|
365
413
|
}
|
|
366
414
|
|
|
367
415
|
// ---------------------------------------------------------------------------
|
|
@@ -439,14 +487,34 @@ export interface InvocationClassification {
|
|
|
439
487
|
|
|
440
488
|
/**
|
|
441
489
|
* Classify how a skill was invoked.
|
|
490
|
+
*
|
|
491
|
+
* When `hook_invocation_type` is provided (from the skill-eval hook's
|
|
492
|
+
* classifyInvocationType), it takes precedence over the legacy heuristics:
|
|
493
|
+
* - "explicit" → user typed /skill (slash command) → explicit, confidence 1.0
|
|
494
|
+
* - "implicit" → user named the skill, Claude invoked it → implicit, confidence 0.85
|
|
495
|
+
* - "inferred" → Claude chose skill autonomously → inferred, confidence 0.6
|
|
496
|
+
* - "contextual" → SKILL.md was read (Read tool, not Skill tool) → inferred, confidence 0.5
|
|
442
497
|
*/
|
|
443
498
|
export function deriveInvocationMode(opts: {
|
|
444
499
|
has_skill_tool_call?: boolean;
|
|
445
500
|
has_skill_md_read?: boolean;
|
|
446
501
|
is_text_mention_only?: boolean;
|
|
447
502
|
is_repaired?: boolean;
|
|
503
|
+
hook_invocation_type?: "explicit" | "implicit" | "inferred" | "contextual";
|
|
448
504
|
}): InvocationClassification {
|
|
449
505
|
if (opts.is_repaired) return { invocation_mode: "repaired", confidence: 0.9 };
|
|
506
|
+
|
|
507
|
+
// Prefer hook-level classification when available
|
|
508
|
+
if (opts.hook_invocation_type === "explicit")
|
|
509
|
+
return { invocation_mode: "explicit", confidence: 1.0 };
|
|
510
|
+
if (opts.hook_invocation_type === "implicit")
|
|
511
|
+
return { invocation_mode: "implicit", confidence: 0.85 };
|
|
512
|
+
if (opts.hook_invocation_type === "inferred")
|
|
513
|
+
return { invocation_mode: "inferred", confidence: 0.6 };
|
|
514
|
+
if (opts.hook_invocation_type === "contextual")
|
|
515
|
+
return { invocation_mode: "inferred", confidence: 0.5 };
|
|
516
|
+
|
|
517
|
+
// Legacy fallback for callers that don't pass hook_invocation_type
|
|
450
518
|
if (opts.has_skill_tool_call) return { invocation_mode: "explicit", confidence: 1.0 };
|
|
451
519
|
if (opts.has_skill_md_read) return { invocation_mode: "implicit", confidence: 0.7 };
|
|
452
520
|
if (opts.is_text_mention_only) return { invocation_mode: "inferred", confidence: 0.4 };
|
|
@@ -613,6 +681,7 @@ export interface BuildSkillInvocationInput extends CanonicalBaseInput {
|
|
|
613
681
|
confidence: number;
|
|
614
682
|
tool_name?: string;
|
|
615
683
|
tool_call_id?: string;
|
|
684
|
+
agent_type?: string;
|
|
616
685
|
}
|
|
617
686
|
|
|
618
687
|
export function buildCanonicalSkillInvocation(
|
|
@@ -636,6 +705,7 @@ export function buildCanonicalSkillInvocation(
|
|
|
636
705
|
if (input.skill_version_hash !== undefined) record.skill_version_hash = input.skill_version_hash;
|
|
637
706
|
if (input.tool_name !== undefined) record.tool_name = input.tool_name;
|
|
638
707
|
if (input.tool_call_id !== undefined) record.tool_call_id = input.tool_call_id;
|
|
708
|
+
if (input.agent_type !== undefined) record.agent_type = input.agent_type;
|
|
639
709
|
|
|
640
710
|
return record;
|
|
641
711
|
}
|
|
@@ -11,8 +11,18 @@
|
|
|
11
11
|
import { existsSync, readFileSync } from "node:fs";
|
|
12
12
|
import { homedir } from "node:os";
|
|
13
13
|
import { join } from "node:path";
|
|
14
|
+
import { getAlphaGuidance } from "./agent-guidance.js";
|
|
15
|
+
import { getAlphaLinkState, readAlphaIdentity } from "./alpha-identity.js";
|
|
14
16
|
import { LOG_DIR, REQUIRED_FIELDS, SELFTUNE_CONFIG_PATH } from "./constants.js";
|
|
15
|
-
import
|
|
17
|
+
import { DB_PATH, getDb } from "./localdb/db.js";
|
|
18
|
+
import type {
|
|
19
|
+
AlphaIdentity,
|
|
20
|
+
AlphaLinkState,
|
|
21
|
+
DoctorResult,
|
|
22
|
+
HealthCheck,
|
|
23
|
+
HealthStatus,
|
|
24
|
+
SelftuneConfig,
|
|
25
|
+
} from "./types.js";
|
|
16
26
|
import { missingClaudeCodeHookKeys } from "./utils/hooks.js";
|
|
17
27
|
|
|
18
28
|
const VALID_AGENT_TYPES = new Set(["claude_code", "codex", "opencode", "openclaw", "unknown"]);
|
|
@@ -116,6 +126,13 @@ export function checkHookInstallation(): HealthCheck[] {
|
|
|
116
126
|
if (!existsSync(settingsPath)) {
|
|
117
127
|
settingsCheck.status = "warn";
|
|
118
128
|
settingsCheck.message = "Claude Code settings.json not found";
|
|
129
|
+
settingsCheck.guidance = {
|
|
130
|
+
code: "hook_settings_missing",
|
|
131
|
+
message: "Claude Code settings.json is missing. Re-run init to install the selftune hooks.",
|
|
132
|
+
next_command: "selftune init --force",
|
|
133
|
+
suggested_commands: ["selftune doctor"],
|
|
134
|
+
blocking: true,
|
|
135
|
+
};
|
|
119
136
|
} else {
|
|
120
137
|
try {
|
|
121
138
|
const raw = readFileSync(settingsPath, "utf-8");
|
|
@@ -124,11 +141,25 @@ export function checkHookInstallation(): HealthCheck[] {
|
|
|
124
141
|
if (!hooks || typeof hooks !== "object") {
|
|
125
142
|
settingsCheck.status = "warn";
|
|
126
143
|
settingsCheck.message = "No hooks section in settings.json";
|
|
144
|
+
settingsCheck.guidance = {
|
|
145
|
+
code: "hook_settings_missing",
|
|
146
|
+
message: "The Claude Code hooks are not configured yet.",
|
|
147
|
+
next_command: "selftune init --force",
|
|
148
|
+
suggested_commands: ["selftune doctor"],
|
|
149
|
+
blocking: true,
|
|
150
|
+
};
|
|
127
151
|
} else {
|
|
128
152
|
const missing = missingClaudeCodeHookKeys(hooks as Record<string, unknown>);
|
|
129
153
|
if (missing.length > 0) {
|
|
130
154
|
settingsCheck.status = "warn";
|
|
131
155
|
settingsCheck.message = `Selftune hooks not configured for: ${missing.join(", ")}`;
|
|
156
|
+
settingsCheck.guidance = {
|
|
157
|
+
code: "hook_settings_incomplete",
|
|
158
|
+
message: "Some Claude Code hooks are missing.",
|
|
159
|
+
next_command: "selftune init --force",
|
|
160
|
+
suggested_commands: ["selftune doctor"],
|
|
161
|
+
blocking: true,
|
|
162
|
+
};
|
|
132
163
|
} else {
|
|
133
164
|
settingsCheck.status = "pass";
|
|
134
165
|
settingsCheck.message = "All selftune hooks configured in settings.json";
|
|
@@ -165,6 +196,18 @@ export function checkEvolutionHealth(): HealthCheck[] {
|
|
|
165
196
|
return [check];
|
|
166
197
|
}
|
|
167
198
|
|
|
199
|
+
export function checkDashboardIntegrityHealth(): HealthCheck[] {
|
|
200
|
+
const check: HealthCheck = {
|
|
201
|
+
name: "dashboard_freshness_mode",
|
|
202
|
+
path: DB_PATH,
|
|
203
|
+
status: "warn",
|
|
204
|
+
message:
|
|
205
|
+
"Dashboard reads SQLite, but live refresh still relies on JSONL watcher invalidation instead of SQLite WAL. Expect freshness gaps for SQLite-only writes and export before destructive recovery.",
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
return [check];
|
|
209
|
+
}
|
|
210
|
+
|
|
168
211
|
export function checkConfigHealth(): HealthCheck[] {
|
|
169
212
|
const check: HealthCheck = {
|
|
170
213
|
name: "config",
|
|
@@ -176,6 +219,13 @@ export function checkConfigHealth(): HealthCheck[] {
|
|
|
176
219
|
if (!existsSync(SELFTUNE_CONFIG_PATH)) {
|
|
177
220
|
check.status = "warn";
|
|
178
221
|
check.message = "Config not found. Run 'selftune init' to bootstrap.";
|
|
222
|
+
check.guidance = {
|
|
223
|
+
code: "config_missing",
|
|
224
|
+
message: "selftune is not initialized yet.",
|
|
225
|
+
next_command: "selftune init",
|
|
226
|
+
suggested_commands: ["selftune doctor"],
|
|
227
|
+
blocking: true,
|
|
228
|
+
};
|
|
179
229
|
} else {
|
|
180
230
|
try {
|
|
181
231
|
const raw = readFileSync(SELFTUNE_CONFIG_PATH, "utf-8");
|
|
@@ -190,6 +240,13 @@ export function checkConfigHealth(): HealthCheck[] {
|
|
|
190
240
|
if (errors.length > 0) {
|
|
191
241
|
check.status = "fail";
|
|
192
242
|
check.message = errors.join("; ");
|
|
243
|
+
check.guidance = {
|
|
244
|
+
code: "config_invalid",
|
|
245
|
+
message: "The selftune config is invalid and needs to be regenerated.",
|
|
246
|
+
next_command: "selftune init --force",
|
|
247
|
+
suggested_commands: ["selftune doctor"],
|
|
248
|
+
blocking: true,
|
|
249
|
+
};
|
|
193
250
|
} else {
|
|
194
251
|
check.status = "pass";
|
|
195
252
|
check.message = `agent_type=${config.agent_type}, llm_mode=${config.llm_mode}`;
|
|
@@ -197,6 +254,13 @@ export function checkConfigHealth(): HealthCheck[] {
|
|
|
197
254
|
} catch {
|
|
198
255
|
check.status = "fail";
|
|
199
256
|
check.message = "Config file exists but is not valid JSON";
|
|
257
|
+
check.guidance = {
|
|
258
|
+
code: "config_invalid_json",
|
|
259
|
+
message: "The selftune config file is corrupt JSON.",
|
|
260
|
+
next_command: "selftune init --force",
|
|
261
|
+
suggested_commands: ["selftune doctor"],
|
|
262
|
+
blocking: true,
|
|
263
|
+
};
|
|
200
264
|
}
|
|
201
265
|
}
|
|
202
266
|
|
|
@@ -249,6 +313,13 @@ export async function checkVersionHealth(): Promise<HealthCheck[]> {
|
|
|
249
313
|
} else {
|
|
250
314
|
check.status = "warn";
|
|
251
315
|
check.message = `v${currentVersion} installed, v${latestVersion} available. Run: npx skills add selftune-dev/selftune`;
|
|
316
|
+
check.guidance = {
|
|
317
|
+
code: "version_update_available",
|
|
318
|
+
message: "A newer selftune release is available.",
|
|
319
|
+
next_command: "npx skills add selftune-dev/selftune",
|
|
320
|
+
suggested_commands: ["selftune doctor"],
|
|
321
|
+
blocking: false,
|
|
322
|
+
};
|
|
252
323
|
}
|
|
253
324
|
} else {
|
|
254
325
|
check.message = `v${currentVersion} (unable to check npm registry)`;
|
|
@@ -263,24 +334,199 @@ export async function checkVersionHealth(): Promise<HealthCheck[]> {
|
|
|
263
334
|
return [check];
|
|
264
335
|
}
|
|
265
336
|
|
|
337
|
+
// ---------------------------------------------------------------------------
|
|
338
|
+
// Alpha upload queue health checks
|
|
339
|
+
// ---------------------------------------------------------------------------
|
|
340
|
+
|
|
341
|
+
const ALPHA_STUCK_THRESHOLD_SECONDS = 3600; // 1 hour
|
|
342
|
+
const ALPHA_FAILURE_THRESHOLD = 50;
|
|
343
|
+
|
|
344
|
+
export interface AlphaQueueCheckOptions {
|
|
345
|
+
stuckThresholdSeconds?: number;
|
|
346
|
+
failureThreshold?: number;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Check alpha upload queue health.
|
|
351
|
+
* Returns empty array when not enrolled (checks are skipped).
|
|
352
|
+
*/
|
|
353
|
+
export async function checkAlphaQueueHealth(
|
|
354
|
+
db: import("bun:sqlite").Database,
|
|
355
|
+
enrolled: boolean,
|
|
356
|
+
opts?: AlphaQueueCheckOptions,
|
|
357
|
+
): Promise<HealthCheck[]> {
|
|
358
|
+
if (!enrolled) return [];
|
|
359
|
+
|
|
360
|
+
const { getQueueStats } = await import("./alpha-upload/queue.js");
|
|
361
|
+
const { getOldestPendingAge } = await import("./localdb/queries.js");
|
|
362
|
+
|
|
363
|
+
const checks: HealthCheck[] = [];
|
|
364
|
+
const stuckThreshold = opts?.stuckThresholdSeconds ?? ALPHA_STUCK_THRESHOLD_SECONDS;
|
|
365
|
+
const failureThreshold = opts?.failureThreshold ?? ALPHA_FAILURE_THRESHOLD;
|
|
366
|
+
|
|
367
|
+
// Check for stuck pending items
|
|
368
|
+
const stuckCheck: HealthCheck = {
|
|
369
|
+
name: "alpha_queue_stuck",
|
|
370
|
+
path: "upload_queue",
|
|
371
|
+
status: "pass",
|
|
372
|
+
message: "",
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const oldestAge = getOldestPendingAge(db);
|
|
376
|
+
if (oldestAge !== null && oldestAge > stuckThreshold) {
|
|
377
|
+
stuckCheck.status = "warn";
|
|
378
|
+
const hours = Math.floor(oldestAge / 3600);
|
|
379
|
+
const minutes = Math.floor((oldestAge % 3600) / 60);
|
|
380
|
+
stuckCheck.message = `Oldest pending upload is ${hours}h ${minutes}m old (threshold: ${Math.floor(stuckThreshold / 3600)}h)`;
|
|
381
|
+
stuckCheck.guidance = {
|
|
382
|
+
code: "alpha_queue_stuck",
|
|
383
|
+
message: "The alpha upload queue has pending items that are not draining.",
|
|
384
|
+
next_command: "selftune alpha upload",
|
|
385
|
+
suggested_commands: ["selftune doctor", "selftune status"],
|
|
386
|
+
blocking: false,
|
|
387
|
+
};
|
|
388
|
+
} else {
|
|
389
|
+
stuckCheck.message =
|
|
390
|
+
oldestAge !== null
|
|
391
|
+
? `Oldest pending item: ${Math.floor(oldestAge / 60)}m old`
|
|
392
|
+
: "No pending items";
|
|
393
|
+
}
|
|
394
|
+
checks.push(stuckCheck);
|
|
395
|
+
|
|
396
|
+
// Check for excessive failures
|
|
397
|
+
const failCheck: HealthCheck = {
|
|
398
|
+
name: "alpha_queue_failures",
|
|
399
|
+
path: "upload_queue",
|
|
400
|
+
status: "pass",
|
|
401
|
+
message: "",
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
const stats = getQueueStats(db);
|
|
405
|
+
if (stats.failed > failureThreshold) {
|
|
406
|
+
failCheck.status = "warn";
|
|
407
|
+
failCheck.message = `${stats.failed} failed uploads (threshold: ${failureThreshold})`;
|
|
408
|
+
failCheck.guidance = {
|
|
409
|
+
code: "alpha_queue_failures",
|
|
410
|
+
message: "The alpha upload queue has accumulated too many failures.",
|
|
411
|
+
next_command: "selftune alpha upload",
|
|
412
|
+
suggested_commands: ["selftune doctor", "selftune status"],
|
|
413
|
+
blocking: false,
|
|
414
|
+
};
|
|
415
|
+
} else {
|
|
416
|
+
failCheck.message = `${stats.failed} failed uploads`;
|
|
417
|
+
}
|
|
418
|
+
checks.push(failCheck);
|
|
419
|
+
|
|
420
|
+
return checks;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export function checkSkillVersionSync(): HealthCheck[] {
|
|
424
|
+
const check: HealthCheck = {
|
|
425
|
+
name: "skill_version_sync",
|
|
426
|
+
path: "skill/SKILL.md",
|
|
427
|
+
status: "pass",
|
|
428
|
+
message: "",
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
try {
|
|
432
|
+
const pkgPath = join(import.meta.dir, "../../package.json");
|
|
433
|
+
const pkgVersion: string = JSON.parse(readFileSync(pkgPath, "utf-8")).version;
|
|
434
|
+
|
|
435
|
+
const skillPath = join(import.meta.dir, "../../skill/SKILL.md");
|
|
436
|
+
if (!existsSync(skillPath)) {
|
|
437
|
+
check.status = "warn";
|
|
438
|
+
check.message = "skill/SKILL.md not found (may be running from installed package)";
|
|
439
|
+
return [check];
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const skillContent = readFileSync(skillPath, "utf-8");
|
|
443
|
+
const versionMatch = skillContent.match(/^\s*version:\s*(.+)$/m);
|
|
444
|
+
if (!versionMatch) {
|
|
445
|
+
check.status = "warn";
|
|
446
|
+
check.message = "No version field found in SKILL.md frontmatter";
|
|
447
|
+
return [check];
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const skillVersion = versionMatch[1].trim();
|
|
451
|
+
if (skillVersion === pkgVersion) {
|
|
452
|
+
check.message = `v${pkgVersion} (in sync)`;
|
|
453
|
+
} else {
|
|
454
|
+
check.status = "warn";
|
|
455
|
+
check.message = `SKILL.md has v${skillVersion} but package.json has v${pkgVersion}. Run: bun run sync-version`;
|
|
456
|
+
check.guidance = {
|
|
457
|
+
code: "skill_version_out_of_sync",
|
|
458
|
+
message: "The packaged skill version does not match package.json.",
|
|
459
|
+
next_command: "bun run sync-version",
|
|
460
|
+
suggested_commands: ["selftune doctor"],
|
|
461
|
+
blocking: false,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
} catch {
|
|
465
|
+
check.status = "warn";
|
|
466
|
+
check.message = "Unable to compare versions";
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return [check];
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// ---------------------------------------------------------------------------
|
|
473
|
+
// Cloud link health checks
|
|
474
|
+
// ---------------------------------------------------------------------------
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Check cloud link health for alpha users.
|
|
478
|
+
* Returns [] for non-alpha users (identity is null).
|
|
479
|
+
*/
|
|
480
|
+
const CLOUD_LINK_CHECKS: Record<AlphaLinkState, { status: HealthStatus; message: string }> = {
|
|
481
|
+
not_linked: { status: "warn", message: "Not linked to cloud account (cloud_user_id missing)" },
|
|
482
|
+
linked_not_enrolled: { status: "warn", message: "Linked but not enrolled" },
|
|
483
|
+
enrolled_no_credential: {
|
|
484
|
+
status: "warn",
|
|
485
|
+
message: "Enrolled but api_key missing — uploads will fail",
|
|
486
|
+
},
|
|
487
|
+
ready: { status: "pass", message: "Cloud link ready" },
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
export function checkCloudLinkHealth(identity: AlphaIdentity | null): HealthCheck[] {
|
|
491
|
+
if (!identity) return [];
|
|
492
|
+
const state = getAlphaLinkState(identity);
|
|
493
|
+
const { status, message } = CLOUD_LINK_CHECKS[state];
|
|
494
|
+
return [
|
|
495
|
+
{
|
|
496
|
+
name: "cloud_link",
|
|
497
|
+
path: SELFTUNE_CONFIG_PATH,
|
|
498
|
+
status,
|
|
499
|
+
message,
|
|
500
|
+
guidance: getAlphaGuidance(identity),
|
|
501
|
+
},
|
|
502
|
+
];
|
|
503
|
+
}
|
|
504
|
+
|
|
266
505
|
export async function doctor(): Promise<DoctorResult> {
|
|
506
|
+
const alphaIdentity = readAlphaIdentity(SELFTUNE_CONFIG_PATH);
|
|
507
|
+
const db = getDb();
|
|
267
508
|
const allChecks = [
|
|
268
509
|
...checkConfigHealth(),
|
|
269
510
|
...checkLogHealth(),
|
|
270
511
|
...checkHookInstallation(),
|
|
271
512
|
...checkEvolutionHealth(),
|
|
513
|
+
...checkDashboardIntegrityHealth(),
|
|
514
|
+
...checkSkillVersionSync(),
|
|
272
515
|
...(await checkVersionHealth()),
|
|
516
|
+
...checkCloudLinkHealth(alphaIdentity),
|
|
517
|
+
...(await checkAlphaQueueHealth(db, alphaIdentity?.enrolled === true)),
|
|
273
518
|
];
|
|
274
519
|
const passed = allChecks.filter((c) => c.status === "pass").length;
|
|
275
520
|
const failed = allChecks.filter((c) => c.status === "fail").length;
|
|
276
521
|
const warned = allChecks.filter((c) => c.status === "warn").length;
|
|
522
|
+
const hasBlockingGuidance = allChecks.some((c) => c.guidance?.blocking === true);
|
|
277
523
|
|
|
278
524
|
return {
|
|
279
525
|
command: "doctor",
|
|
280
526
|
timestamp: new Date().toISOString(),
|
|
281
527
|
checks: allChecks,
|
|
282
528
|
summary: { pass: passed, fail: failed, warn: warned, total: allChecks.length },
|
|
283
|
-
healthy: failed === 0,
|
|
529
|
+
healthy: failed === 0 && !hasBlockingGuidance,
|
|
284
530
|
};
|
|
285
531
|
}
|
|
286
532
|
|