selftune 0.2.18 → 0.2.19
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 +9 -4
- package/apps/local-dashboard/dist/assets/index-DnhnXQm6.js +60 -0
- package/apps/local-dashboard/dist/assets/index-_EcLywDg.css +1 -0
- package/apps/local-dashboard/dist/assets/vendor-table-BIiI3YhS.js +1 -0
- package/apps/local-dashboard/dist/assets/vendor-ui-CGEmUayx.js +12 -0
- package/apps/local-dashboard/dist/index.html +5 -5
- package/cli/selftune/alpha-upload/stage-canonical.ts +7 -6
- package/cli/selftune/constants.ts +10 -0
- package/cli/selftune/contribute/contribute.ts +30 -2
- package/cli/selftune/contribution-config.ts +249 -0
- package/cli/selftune/contribution-relay.ts +177 -0
- package/cli/selftune/contribution-signals.ts +219 -0
- package/cli/selftune/contribution-staging.ts +147 -0
- package/cli/selftune/contributions.ts +532 -0
- package/cli/selftune/creator-contributions.ts +333 -0
- package/cli/selftune/dashboard-contract.ts +205 -1
- package/cli/selftune/dashboard-server.ts +45 -11
- package/cli/selftune/eval/family-overlap.ts +395 -0
- package/cli/selftune/eval/hooks-to-evals.ts +182 -28
- package/cli/selftune/eval/synthetic-evals.ts +298 -11
- package/cli/selftune/export.ts +2 -2
- package/cli/selftune/index.ts +41 -5
- package/cli/selftune/ingestors/codex-rollout.ts +31 -35
- package/cli/selftune/ingestors/codex-wrapper.ts +32 -24
- package/cli/selftune/localdb/db.ts +2 -2
- package/cli/selftune/localdb/queries.ts +701 -30
- package/cli/selftune/localdb/schema.ts +20 -0
- package/cli/selftune/recover.ts +153 -0
- package/cli/selftune/repair/skill-usage.ts +363 -4
- package/cli/selftune/routes/actions.ts +35 -1
- package/cli/selftune/routes/analytics.ts +14 -0
- package/cli/selftune/routes/index.ts +1 -0
- package/cli/selftune/routes/overview.ts +112 -4
- package/cli/selftune/routes/skill-report.ts +569 -10
- package/cli/selftune/status.ts +81 -2
- package/cli/selftune/sync.ts +56 -2
- package/cli/selftune/trust-model.ts +66 -0
- package/cli/selftune/types.ts +49 -0
- package/cli/selftune/utils/skill-detection.ts +43 -0
- package/cli/selftune/watchlist.ts +65 -0
- package/package.json +1 -1
- package/packages/ui/src/components/ActivityTimeline.tsx +165 -150
- package/packages/ui/src/components/EvidenceViewer.tsx +335 -144
- package/packages/ui/src/components/EvolutionTimeline.tsx +58 -28
- package/packages/ui/src/components/OrchestrateRunsPanel.tsx +33 -16
- package/packages/ui/src/components/RecentActivityFeed.tsx +72 -41
- package/packages/ui/src/components/section-cards.tsx +12 -9
- package/packages/ui/src/primitives/card.tsx +1 -1
- package/skill/SKILL.md +11 -1
- package/skill/Workflows/AlphaUpload.md +4 -0
- package/skill/Workflows/Composability.md +64 -0
- package/skill/Workflows/Contribute.md +6 -3
- package/skill/Workflows/Contributions.md +97 -0
- package/skill/Workflows/CreatorContributions.md +74 -0
- package/skill/Workflows/Dashboard.md +31 -0
- package/skill/Workflows/Evals.md +57 -8
- package/skill/Workflows/Ingest.md +7 -0
- package/skill/Workflows/Initialize.md +20 -1
- package/skill/Workflows/Recover.md +84 -0
- package/skill/Workflows/RepairSkillUsage.md +12 -4
- package/skill/Workflows/Sync.md +18 -12
- package/apps/local-dashboard/dist/assets/index-BMIS6uUh.css +0 -2
- package/apps/local-dashboard/dist/assets/index-DOu3iLD9.js +0 -16
- package/apps/local-dashboard/dist/assets/vendor-table-pHbDxq36.js +0 -8
- package/apps/local-dashboard/dist/assets/vendor-ui-DIwlrGlb.js +0 -12
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { parseArgs } from "node:util";
|
|
4
|
+
|
|
5
|
+
import { readAlphaIdentity } from "./alpha-identity.js";
|
|
6
|
+
import { SELFTUNE_CONFIG_PATH } from "./constants.js";
|
|
7
|
+
import {
|
|
8
|
+
type CreatorContributionConfig,
|
|
9
|
+
discoverCreatorContributionConfigs,
|
|
10
|
+
findCreatorContributionConfig,
|
|
11
|
+
getContributionConfigSearchRoots,
|
|
12
|
+
removeCreatorContributionConfig,
|
|
13
|
+
resolveContributionSkillPath,
|
|
14
|
+
writeCreatorContributionConfig,
|
|
15
|
+
} from "./contribution-config.js";
|
|
16
|
+
import { CLIError } from "./utils/cli-error.js";
|
|
17
|
+
import { handleCLIError } from "./utils/cli-error.js";
|
|
18
|
+
import { findInstalledSkillNames } from "./utils/skill-discovery.js";
|
|
19
|
+
|
|
20
|
+
function inferCreatorId(explicitCreatorId?: string): string | null {
|
|
21
|
+
if (explicitCreatorId?.trim()) return explicitCreatorId.trim();
|
|
22
|
+
const alpha = readAlphaIdentity(SELFTUNE_CONFIG_PATH);
|
|
23
|
+
return alpha?.cloud_user_id?.trim() || null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function printConfig(config: CreatorContributionConfig): void {
|
|
27
|
+
console.log(`${config.skill_name}`);
|
|
28
|
+
console.log(` creator_id: ${config.creator_id}`);
|
|
29
|
+
console.log(` skill_path: ${config.skill_path}`);
|
|
30
|
+
console.log(` signals: ${config.contribution.signals.join(", ")}`);
|
|
31
|
+
if (config.contribution.message) {
|
|
32
|
+
console.log(` message: ${config.contribution.message}`);
|
|
33
|
+
}
|
|
34
|
+
if (config.contribution.privacy_url) {
|
|
35
|
+
console.log(` privacy_url: ${config.contribution.privacy_url}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function printStatus(skillName?: string): void {
|
|
40
|
+
const searchRoots = getContributionConfigSearchRoots();
|
|
41
|
+
const installedSkills = [...findInstalledSkillNames(searchRoots)].sort();
|
|
42
|
+
const configuredSkillNames = new Set(
|
|
43
|
+
discoverCreatorContributionConfigs(searchRoots).map((c) => c.skill_name),
|
|
44
|
+
);
|
|
45
|
+
if (skillName) {
|
|
46
|
+
const config = findCreatorContributionConfig(skillName);
|
|
47
|
+
if (!config) {
|
|
48
|
+
console.log(`No creator contribution config found for "${skillName}".`);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
console.log("Creator contribution config:");
|
|
52
|
+
printConfig(config);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const configs = discoverCreatorContributionConfigs();
|
|
57
|
+
if (configs.length === 0) {
|
|
58
|
+
console.log("No creator contribution configs discovered.");
|
|
59
|
+
console.log("Use `selftune creator-contributions enable --skill <name>` to add one.");
|
|
60
|
+
} else {
|
|
61
|
+
console.log("Discovered creator contribution configs:");
|
|
62
|
+
for (const config of configs) {
|
|
63
|
+
printConfig(config);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const missingInstalled = installedSkills.filter((skill) => !configuredSkillNames.has(skill));
|
|
68
|
+
if (missingInstalled.length > 0) {
|
|
69
|
+
console.log("Installed skills without creator contribution config:");
|
|
70
|
+
for (const skill of missingInstalled) {
|
|
71
|
+
console.log(` ${skill}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface BulkEnableSkip {
|
|
77
|
+
skill_name: string;
|
|
78
|
+
reason: "already_configured" | "skill_path_not_found";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface BulkEnableResult {
|
|
82
|
+
written: string[];
|
|
83
|
+
skipped: BulkEnableSkip[];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function enableCreatorContributionConfigs(options: {
|
|
87
|
+
skillName?: string;
|
|
88
|
+
all?: boolean;
|
|
89
|
+
prefix?: string;
|
|
90
|
+
explicitSkillPath?: string;
|
|
91
|
+
explicitCreatorId?: string;
|
|
92
|
+
signals: string[];
|
|
93
|
+
message?: string;
|
|
94
|
+
privacyUrl?: string;
|
|
95
|
+
}): BulkEnableResult {
|
|
96
|
+
const creatorId = inferCreatorId(options.explicitCreatorId);
|
|
97
|
+
if (!creatorId) {
|
|
98
|
+
throw new CLIError(
|
|
99
|
+
"Creator ID is required.",
|
|
100
|
+
"MISSING_FLAG",
|
|
101
|
+
"Pass --creator-id <id> or enroll alpha so cloud_user_id is available.",
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const searchRoots = getContributionConfigSearchRoots();
|
|
106
|
+
const targetSkills = options.all
|
|
107
|
+
? [...findInstalledSkillNames(searchRoots)]
|
|
108
|
+
.filter((name) => !options.prefix || name.startsWith(options.prefix))
|
|
109
|
+
.sort()
|
|
110
|
+
: options.skillName
|
|
111
|
+
? [options.skillName]
|
|
112
|
+
: [];
|
|
113
|
+
|
|
114
|
+
if (targetSkills.length === 0) {
|
|
115
|
+
throw new CLIError(
|
|
116
|
+
options.all
|
|
117
|
+
? `No installed skills found${options.prefix ? ` with prefix "${options.prefix}"` : ""}.`
|
|
118
|
+
: "Skill name is required.",
|
|
119
|
+
options.all ? "FILE_NOT_FOUND" : "MISSING_FLAG",
|
|
120
|
+
options.all
|
|
121
|
+
? "selftune creator-contributions status"
|
|
122
|
+
: "selftune creator-contributions enable --skill <name>",
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const result: BulkEnableResult = { written: [], skipped: [] };
|
|
127
|
+
for (const skillName of targetSkills) {
|
|
128
|
+
if (findCreatorContributionConfig(skillName, searchRoots)) {
|
|
129
|
+
result.skipped.push({ skill_name: skillName, reason: "already_configured" });
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const skillPath = resolveContributionSkillPath(
|
|
133
|
+
skillName,
|
|
134
|
+
options.all ? undefined : options.explicitSkillPath,
|
|
135
|
+
searchRoots,
|
|
136
|
+
);
|
|
137
|
+
if (!skillPath) {
|
|
138
|
+
result.skipped.push({ skill_name: skillName, reason: "skill_path_not_found" });
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
writeCreatorContributionConfig({
|
|
143
|
+
creator_id: creatorId,
|
|
144
|
+
skill_name: skillName,
|
|
145
|
+
skill_path: skillPath,
|
|
146
|
+
signals: options.signals,
|
|
147
|
+
message: options.message,
|
|
148
|
+
privacy_url: options.privacyUrl,
|
|
149
|
+
});
|
|
150
|
+
result.written.push(skillName);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export async function cliMain(): Promise<void> {
|
|
157
|
+
const sub = process.argv[2];
|
|
158
|
+
const rest = process.argv.slice(3);
|
|
159
|
+
|
|
160
|
+
if (sub === "--help" || sub === "-h") {
|
|
161
|
+
console.log(`selftune creator-contributions — Manage creator-side contribution configs
|
|
162
|
+
|
|
163
|
+
Usage:
|
|
164
|
+
selftune creator-contributions
|
|
165
|
+
selftune creator-contributions status [--skill <name>]
|
|
166
|
+
selftune creator-contributions enable --skill <name> [--skill-path <path>] [--creator-id <id>] [--signals a,b,c]
|
|
167
|
+
selftune creator-contributions enable --all [--prefix <prefix>] [--creator-id <id>] [--signals a,b,c]
|
|
168
|
+
selftune creator-contributions disable --skill <name> [--skill-path <path>]
|
|
169
|
+
|
|
170
|
+
Purpose:
|
|
171
|
+
Manage the local selftune.contribute.json file that a skill creator bundles
|
|
172
|
+
with a skill package. This is separate from:
|
|
173
|
+
selftune contributions End-user sharing preferences
|
|
174
|
+
selftune contribute Community export bundle`);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const normalizedSub = sub ?? "status";
|
|
179
|
+
|
|
180
|
+
switch (normalizedSub) {
|
|
181
|
+
case "status": {
|
|
182
|
+
const { values } = parseArgs({
|
|
183
|
+
args: rest,
|
|
184
|
+
options: {
|
|
185
|
+
skill: { type: "string" },
|
|
186
|
+
help: { type: "boolean", short: "h", default: false },
|
|
187
|
+
},
|
|
188
|
+
strict: true,
|
|
189
|
+
});
|
|
190
|
+
if (values.help) {
|
|
191
|
+
console.log("Usage: selftune creator-contributions status [--skill <name>]");
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
printStatus(values.skill);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
case "enable": {
|
|
198
|
+
const { values } = parseArgs({
|
|
199
|
+
args: rest,
|
|
200
|
+
options: {
|
|
201
|
+
skill: { type: "string" },
|
|
202
|
+
all: { type: "boolean", default: false },
|
|
203
|
+
prefix: { type: "string" },
|
|
204
|
+
"skill-path": { type: "string" },
|
|
205
|
+
"creator-id": { type: "string" },
|
|
206
|
+
signals: { type: "string", default: "trigger,grade,miss_category" },
|
|
207
|
+
message: { type: "string" },
|
|
208
|
+
"privacy-url": { type: "string" },
|
|
209
|
+
help: { type: "boolean", short: "h", default: false },
|
|
210
|
+
},
|
|
211
|
+
strict: true,
|
|
212
|
+
});
|
|
213
|
+
if (values.help) {
|
|
214
|
+
console.log(
|
|
215
|
+
"Usage: selftune creator-contributions enable (--skill <name> [--skill-path <path>] | --all [--prefix <prefix>]) [--creator-id <id>]",
|
|
216
|
+
);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (!values.all && !values.skill?.trim()) {
|
|
221
|
+
throw new CLIError(
|
|
222
|
+
"Pass either --skill <name> or --all.",
|
|
223
|
+
"MISSING_FLAG",
|
|
224
|
+
"selftune creator-contributions enable --skill <name>",
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const signals = (values.signals ?? "trigger,grade,miss_category")
|
|
229
|
+
.split(",")
|
|
230
|
+
.map((signal) => signal.trim())
|
|
231
|
+
.filter(Boolean);
|
|
232
|
+
const outcome = enableCreatorContributionConfigs({
|
|
233
|
+
skillName: values.skill?.trim(),
|
|
234
|
+
all: values.all,
|
|
235
|
+
prefix: values.prefix?.trim(),
|
|
236
|
+
explicitSkillPath: values["skill-path"],
|
|
237
|
+
explicitCreatorId: values["creator-id"],
|
|
238
|
+
signals,
|
|
239
|
+
message: values.message,
|
|
240
|
+
privacyUrl: values["privacy-url"],
|
|
241
|
+
});
|
|
242
|
+
if (values.all) {
|
|
243
|
+
console.log(
|
|
244
|
+
`Enabled creator contribution config for ${outcome.written.length} skills${values.prefix ? ` with prefix "${values.prefix}"` : ""}.`,
|
|
245
|
+
);
|
|
246
|
+
if (outcome.written.length > 0) {
|
|
247
|
+
for (const skill of outcome.written) {
|
|
248
|
+
const config = findCreatorContributionConfig(skill);
|
|
249
|
+
if (config) printConfig(config);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (outcome.skipped.length > 0) {
|
|
253
|
+
console.log(
|
|
254
|
+
`Skipped ${outcome.skipped.length} skills: ${outcome.skipped.map((entry) => entry.skill_name).join(", ")}`,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const skillName = values.skill!.trim();
|
|
260
|
+
if (outcome.written.length === 0) {
|
|
261
|
+
const skip = outcome.skipped[0];
|
|
262
|
+
if (skip?.reason === "already_configured") {
|
|
263
|
+
throw new CLIError(
|
|
264
|
+
`A creator contribution config already exists for "${skillName}".`,
|
|
265
|
+
"FILE_EXISTS",
|
|
266
|
+
"Run `selftune creator-contributions status --skill <name>` to inspect it.",
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
throw new CLIError(
|
|
270
|
+
`Could not resolve SKILL.md for "${skillName}".`,
|
|
271
|
+
"FILE_NOT_FOUND",
|
|
272
|
+
"Pass --skill-path /path/to/SKILL.md",
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
const config = findCreatorContributionConfig(skillName);
|
|
276
|
+
console.log(`Enabled creator contribution config for "${skillName}".`);
|
|
277
|
+
if (config) printConfig(config);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
case "disable": {
|
|
281
|
+
const { values } = parseArgs({
|
|
282
|
+
args: rest,
|
|
283
|
+
options: {
|
|
284
|
+
skill: { type: "string" },
|
|
285
|
+
"skill-path": { type: "string" },
|
|
286
|
+
help: { type: "boolean", short: "h", default: false },
|
|
287
|
+
},
|
|
288
|
+
strict: true,
|
|
289
|
+
});
|
|
290
|
+
if (values.help) {
|
|
291
|
+
console.log(
|
|
292
|
+
"Usage: selftune creator-contributions disable --skill <name> [--skill-path <path>]",
|
|
293
|
+
);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const skillName = values.skill?.trim();
|
|
298
|
+
if (!skillName) {
|
|
299
|
+
throw new CLIError(
|
|
300
|
+
"Skill name is required.",
|
|
301
|
+
"MISSING_FLAG",
|
|
302
|
+
"selftune creator-contributions disable --skill <name>",
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
const skillPath = resolveContributionSkillPath(skillName, values["skill-path"]);
|
|
306
|
+
if (!skillPath) {
|
|
307
|
+
throw new CLIError(
|
|
308
|
+
`Could not resolve SKILL.md for "${skillName}".`,
|
|
309
|
+
"FILE_NOT_FOUND",
|
|
310
|
+
"Pass --skill-path /path/to/SKILL.md",
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const removed = removeCreatorContributionConfig(skillPath);
|
|
315
|
+
if (!removed) {
|
|
316
|
+
console.log(`No creator contribution config found for "${skillName}".`);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
console.log(`Disabled creator contribution config for "${skillName}".`);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
default:
|
|
323
|
+
throw new CLIError(
|
|
324
|
+
`Unknown creator-contributions subcommand: ${normalizedSub}`,
|
|
325
|
+
"UNKNOWN_COMMAND",
|
|
326
|
+
"selftune creator-contributions --help",
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (import.meta.main) {
|
|
332
|
+
cliMain().catch(handleCLIError);
|
|
333
|
+
}
|
|
@@ -134,6 +134,65 @@ export interface SkillSummary {
|
|
|
134
134
|
unique_sessions: number;
|
|
135
135
|
last_seen: string | null;
|
|
136
136
|
has_evidence: boolean;
|
|
137
|
+
routing_confidence: number | null;
|
|
138
|
+
confidence_coverage: number;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// -- Autonomy-first overview types -------------------------------------------
|
|
142
|
+
|
|
143
|
+
export type AutonomyStatusLevel = "healthy" | "watching" | "needs_review" | "blocked";
|
|
144
|
+
|
|
145
|
+
export interface AutonomyStatus {
|
|
146
|
+
level: AutonomyStatusLevel;
|
|
147
|
+
summary: string;
|
|
148
|
+
last_run: string | null;
|
|
149
|
+
skills_observed: number;
|
|
150
|
+
pending_reviews: number;
|
|
151
|
+
attention_required: number;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export type AttentionCategory =
|
|
155
|
+
| "needs_review"
|
|
156
|
+
| "regression"
|
|
157
|
+
| "low_trust"
|
|
158
|
+
| "polluted"
|
|
159
|
+
| "blocked";
|
|
160
|
+
|
|
161
|
+
export interface AttentionItem {
|
|
162
|
+
skill_name: string;
|
|
163
|
+
category: AttentionCategory;
|
|
164
|
+
severity: "critical" | "warning" | "info";
|
|
165
|
+
reason: string;
|
|
166
|
+
recommended_action: string;
|
|
167
|
+
timestamp: string;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export type TrustBucket = "at_risk" | "improving" | "uncertain" | "stable";
|
|
171
|
+
|
|
172
|
+
export interface TrustWatchlistEntry {
|
|
173
|
+
skill_name: string;
|
|
174
|
+
bucket: TrustBucket;
|
|
175
|
+
trust_state: TrustState;
|
|
176
|
+
reason: string;
|
|
177
|
+
pass_rate: number | null;
|
|
178
|
+
checks: number;
|
|
179
|
+
last_seen: string | null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export type DecisionKind =
|
|
183
|
+
| "proposal_created"
|
|
184
|
+
| "proposal_rejected"
|
|
185
|
+
| "validation_failed"
|
|
186
|
+
| "proposal_deployed"
|
|
187
|
+
| "rollback_triggered"
|
|
188
|
+
| "regression_found";
|
|
189
|
+
|
|
190
|
+
export interface AutonomousDecision {
|
|
191
|
+
timestamp: string;
|
|
192
|
+
kind: DecisionKind;
|
|
193
|
+
skill_name: string;
|
|
194
|
+
proposal_id?: string;
|
|
195
|
+
summary: string;
|
|
137
196
|
}
|
|
138
197
|
|
|
139
198
|
export interface OverviewPayload {
|
|
@@ -158,6 +217,11 @@ export interface OverviewResponse {
|
|
|
158
217
|
overview: OverviewPayload;
|
|
159
218
|
skills: SkillSummary[];
|
|
160
219
|
version?: string;
|
|
220
|
+
watched_skills: string[];
|
|
221
|
+
autonomy_status: AutonomyStatus;
|
|
222
|
+
attention_queue: AttentionItem[];
|
|
223
|
+
trust_watchlist: TrustWatchlistEntry[];
|
|
224
|
+
recent_decisions: AutonomousDecision[];
|
|
161
225
|
}
|
|
162
226
|
|
|
163
227
|
export interface EvidenceEntry {
|
|
@@ -188,6 +252,8 @@ export interface CanonicalInvocation {
|
|
|
188
252
|
source?: string | null;
|
|
189
253
|
skill_path?: string | null;
|
|
190
254
|
skill_scope?: string | null;
|
|
255
|
+
observation_kind?: ObservationKind;
|
|
256
|
+
historical_context?: HistoricalContext | null;
|
|
191
257
|
}
|
|
192
258
|
|
|
193
259
|
export interface PromptSample {
|
|
@@ -266,6 +332,48 @@ export interface OrchestrateRunsResponse {
|
|
|
266
332
|
runs: OrchestrateRunReport[];
|
|
267
333
|
}
|
|
268
334
|
|
|
335
|
+
// -- Performance analytics response -------------------------------------------
|
|
336
|
+
|
|
337
|
+
export interface AnalyticsResponse {
|
|
338
|
+
/** Daily pass rate trend (last 90 days, bucketed by day) */
|
|
339
|
+
pass_rate_trend: Array<{
|
|
340
|
+
date: string;
|
|
341
|
+
pass_rate: number;
|
|
342
|
+
total_checks: number;
|
|
343
|
+
}>;
|
|
344
|
+
|
|
345
|
+
/** Skills ranked by pass rate with trend direction */
|
|
346
|
+
skill_rankings: Array<{
|
|
347
|
+
skill_name: string;
|
|
348
|
+
pass_rate: number;
|
|
349
|
+
total_checks: number;
|
|
350
|
+
triggered_count: number;
|
|
351
|
+
}>;
|
|
352
|
+
|
|
353
|
+
/** Daily check counts for heatmap (last 84 days / 12 weeks) */
|
|
354
|
+
daily_activity: Array<{
|
|
355
|
+
date: string;
|
|
356
|
+
checks: number;
|
|
357
|
+
}>;
|
|
358
|
+
|
|
359
|
+
/** Evolution impact — before/after pass rates for deployed evolutions */
|
|
360
|
+
evolution_impact: Array<{
|
|
361
|
+
skill_name: string;
|
|
362
|
+
proposal_id: string;
|
|
363
|
+
deployed_at: string;
|
|
364
|
+
pass_rate_before: number;
|
|
365
|
+
pass_rate_after: number;
|
|
366
|
+
}>;
|
|
367
|
+
|
|
368
|
+
/** Aggregate summary */
|
|
369
|
+
summary: {
|
|
370
|
+
total_evolutions: number;
|
|
371
|
+
avg_improvement: number;
|
|
372
|
+
total_checks_30d: number;
|
|
373
|
+
active_skills: number;
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
269
377
|
// -- Health endpoint response -------------------------------------------------
|
|
270
378
|
|
|
271
379
|
export interface HealthResponse {
|
|
@@ -318,7 +426,103 @@ export interface CommitSummary {
|
|
|
318
426
|
recent_commits: Array<{ sha: string; title: string; branch: string; timestamp: string }>;
|
|
319
427
|
}
|
|
320
428
|
|
|
321
|
-
|
|
429
|
+
// -- Trust-oriented types for skill report ------------------------------------
|
|
430
|
+
|
|
431
|
+
export type TrustState =
|
|
432
|
+
| "low_sample"
|
|
433
|
+
| "observed"
|
|
434
|
+
| "watch"
|
|
435
|
+
| "validated"
|
|
436
|
+
| "deployed"
|
|
437
|
+
| "rolled_back";
|
|
438
|
+
|
|
439
|
+
export type ObservationKind =
|
|
440
|
+
| "canonical"
|
|
441
|
+
| "repaired_trigger"
|
|
442
|
+
| "repaired_contextual_miss"
|
|
443
|
+
| "legacy_materialized";
|
|
444
|
+
|
|
445
|
+
export type HistoricalContext = "previously_missed";
|
|
446
|
+
|
|
447
|
+
export interface ExampleRow {
|
|
448
|
+
timestamp: string | null;
|
|
449
|
+
session_id: string;
|
|
450
|
+
query_text: string;
|
|
451
|
+
triggered: boolean;
|
|
452
|
+
confidence: number | null;
|
|
453
|
+
invocation_mode: string | null;
|
|
454
|
+
prompt_kind: string | null;
|
|
455
|
+
source: string | null;
|
|
456
|
+
platform: string | null;
|
|
457
|
+
workspace_path: string | null;
|
|
458
|
+
query_origin: "inline_query" | "matched_prompt" | "missing";
|
|
459
|
+
is_system_like: boolean;
|
|
460
|
+
observation_kind: ObservationKind;
|
|
461
|
+
historical_context?: HistoricalContext | null;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export interface TrustFields {
|
|
465
|
+
trust: {
|
|
466
|
+
state: TrustState;
|
|
467
|
+
summary: string;
|
|
468
|
+
};
|
|
469
|
+
coverage: {
|
|
470
|
+
checks: number;
|
|
471
|
+
sessions: number;
|
|
472
|
+
workspaces: number;
|
|
473
|
+
first_seen: string | null;
|
|
474
|
+
last_seen: string | null;
|
|
475
|
+
};
|
|
476
|
+
evidence_quality: {
|
|
477
|
+
prompt_link_rate: number;
|
|
478
|
+
inline_query_rate: number;
|
|
479
|
+
user_prompt_rate: number;
|
|
480
|
+
meta_prompt_rate: number;
|
|
481
|
+
internal_prompt_rate: number;
|
|
482
|
+
no_prompt_rate: number;
|
|
483
|
+
system_like_rate: number;
|
|
484
|
+
invocation_mode_coverage: number;
|
|
485
|
+
confidence_coverage: number;
|
|
486
|
+
source_coverage: number;
|
|
487
|
+
scope_coverage: number;
|
|
488
|
+
};
|
|
489
|
+
routing_quality: {
|
|
490
|
+
missed_triggers: number;
|
|
491
|
+
miss_rate: number;
|
|
492
|
+
avg_confidence: number | null;
|
|
493
|
+
confidence_coverage: number;
|
|
494
|
+
low_confidence_rate: number | null;
|
|
495
|
+
};
|
|
496
|
+
evolution_state: {
|
|
497
|
+
has_evidence: boolean;
|
|
498
|
+
has_pending_proposals: boolean;
|
|
499
|
+
latest_action: string | null;
|
|
500
|
+
latest_timestamp: string | null;
|
|
501
|
+
evidence_rows: number;
|
|
502
|
+
evolution_rows: number;
|
|
503
|
+
};
|
|
504
|
+
data_hygiene: {
|
|
505
|
+
naming_variants: string[];
|
|
506
|
+
source_breakdown: Array<{ source: string; count: number }>;
|
|
507
|
+
prompt_kind_breakdown: Array<{ kind: string; count: number }>;
|
|
508
|
+
observation_breakdown: Array<{ kind: ObservationKind; count: number }>;
|
|
509
|
+
raw_checks: number;
|
|
510
|
+
operational_checks: number;
|
|
511
|
+
internal_prompt_rows: number;
|
|
512
|
+
internal_prompt_rate: number;
|
|
513
|
+
legacy_rows: number;
|
|
514
|
+
legacy_rate: number;
|
|
515
|
+
repaired_rows: number;
|
|
516
|
+
repaired_rate: number;
|
|
517
|
+
};
|
|
518
|
+
examples: {
|
|
519
|
+
good: ExampleRow[];
|
|
520
|
+
missed: ExampleRow[];
|
|
521
|
+
noisy: ExampleRow[];
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
export interface SkillReportResponse extends SkillReportPayload, TrustFields {
|
|
322
526
|
evolution: EvolutionEntry[];
|
|
323
527
|
pending_proposals: PendingProposal[];
|
|
324
528
|
token_usage: {
|
|
@@ -8,10 +8,12 @@
|
|
|
8
8
|
* GET /api/health — Dashboard server health probe
|
|
9
9
|
* GET /api/v2/doctor — System health diagnostics (config, logs, hooks, evolution)
|
|
10
10
|
* GET /api/v2/overview — SQLite-backed overview payload
|
|
11
|
+
* GET /api/v2/analytics — Performance analytics (trends, rankings, heatmap)
|
|
11
12
|
* GET /api/v2/skills/:name — SQLite-backed per-skill report
|
|
12
13
|
* POST /api/actions/watch — Trigger `selftune watch` for a skill
|
|
13
14
|
* POST /api/actions/evolve — Trigger `selftune evolve` for a skill
|
|
14
15
|
* POST /api/actions/rollback — Trigger `selftune rollback` for a skill
|
|
16
|
+
* POST /api/actions/watchlist — Persist creator watchlist preferences
|
|
15
17
|
* GET /badge/:name — Skill health badge
|
|
16
18
|
* GET /report/:name — Skill health report HTML
|
|
17
19
|
*/
|
|
@@ -29,7 +31,6 @@ import type {
|
|
|
29
31
|
} from "./dashboard-contract.js";
|
|
30
32
|
import { readEvidenceTrail } from "./evolution/evidence.js";
|
|
31
33
|
import { closeSingleton, DB_PATH, getDb } from "./localdb/db.js";
|
|
32
|
-
import { materializeIncremental } from "./localdb/materialize.js";
|
|
33
34
|
import {
|
|
34
35
|
queryEvolutionAudit,
|
|
35
36
|
queryQueryLog,
|
|
@@ -40,6 +41,7 @@ import { doctor } from "./observability.js";
|
|
|
40
41
|
import type { ActionRunner } from "./routes/index.js";
|
|
41
42
|
import {
|
|
42
43
|
handleAction,
|
|
44
|
+
handleAnalytics,
|
|
43
45
|
handleBadge,
|
|
44
46
|
handleDoctor,
|
|
45
47
|
handleOrchestrateRuns,
|
|
@@ -109,6 +111,16 @@ function decodePathSegment(segment: string): string | null {
|
|
|
109
111
|
}
|
|
110
112
|
}
|
|
111
113
|
|
|
114
|
+
function allowedDashboardOrigins(hostname: string, port: number): Set<string> {
|
|
115
|
+
const origins = new Set<string>([`http://${hostname}:${port}`]);
|
|
116
|
+
if (hostname === "localhost") {
|
|
117
|
+
origins.add(`http://127.0.0.1:${port}`);
|
|
118
|
+
} else if (hostname === "127.0.0.1") {
|
|
119
|
+
origins.add(`http://localhost:${port}`);
|
|
120
|
+
}
|
|
121
|
+
return origins;
|
|
122
|
+
}
|
|
123
|
+
|
|
112
124
|
const MIME_TYPES: Record<string, string> = {
|
|
113
125
|
".html": "text/html; charset=utf-8",
|
|
114
126
|
".js": "application/javascript; charset=utf-8",
|
|
@@ -187,24 +199,21 @@ export async function startDashboardServer(
|
|
|
187
199
|
if (needsDb) {
|
|
188
200
|
try {
|
|
189
201
|
db = getDb();
|
|
190
|
-
// Materializer runs once at startup to backfill any JSONL data not yet in SQLite.
|
|
191
|
-
// After startup, hooks write directly to SQLite so re-materialization is unnecessary.
|
|
192
|
-
materializeIncremental(db);
|
|
193
202
|
} catch (error: unknown) {
|
|
194
203
|
const message = error instanceof Error ? error.message : String(error);
|
|
195
204
|
console.error(`V2 dashboard data unavailable: ${message}`);
|
|
196
205
|
}
|
|
197
206
|
}
|
|
198
207
|
|
|
199
|
-
// Hooks write directly to SQLite, so periodic
|
|
200
|
-
//
|
|
201
|
-
//
|
|
208
|
+
// Hooks and ingestors write directly to SQLite, so periodic materialization is
|
|
209
|
+
// not part of normal runtime. These remain no-ops because they are invoked
|
|
210
|
+
// from several shared request and watcher paths.
|
|
202
211
|
function refreshV2Data(): void {
|
|
203
|
-
// No-op:
|
|
212
|
+
// No-op: SQLite is already authoritative at runtime
|
|
204
213
|
}
|
|
205
214
|
|
|
206
215
|
function refreshV2DataImmediate(): void {
|
|
207
|
-
// No-op:
|
|
216
|
+
// No-op: SQLite is already authoritative at runtime
|
|
208
217
|
}
|
|
209
218
|
|
|
210
219
|
// -- SSE (Server-Sent Events) live update layer -----------------------------
|
|
@@ -259,6 +268,7 @@ export async function startDashboardServer(
|
|
|
259
268
|
let lastStatusCacheRefreshAt = 0;
|
|
260
269
|
let statusRefreshPromise: Promise<void> | null = null;
|
|
261
270
|
const STATUS_CACHE_TTL_MS = 30_000;
|
|
271
|
+
let boundPort = port;
|
|
262
272
|
|
|
263
273
|
async function refreshStatusCache(force = false): Promise<void> {
|
|
264
274
|
const cacheIsFresh =
|
|
@@ -383,8 +393,20 @@ export async function startDashboardServer(
|
|
|
383
393
|
});
|
|
384
394
|
}
|
|
385
395
|
|
|
386
|
-
// ---- POST /api/actions/{watch,evolve,rollback} ----
|
|
396
|
+
// ---- POST /api/actions/{watch,evolve,rollback,watchlist} ----
|
|
387
397
|
if (url.pathname.startsWith("/api/actions/") && req.method === "POST") {
|
|
398
|
+
const trustedActionOrigins = allowedDashboardOrigins(hostname, boundPort);
|
|
399
|
+
const origin = req.headers.get("origin");
|
|
400
|
+
if (!origin || !trustedActionOrigins.has(origin)) {
|
|
401
|
+
return Response.json(
|
|
402
|
+
{
|
|
403
|
+
success: false,
|
|
404
|
+
error:
|
|
405
|
+
"Dashboard actions only accept same-origin requests from the local dashboard UI.",
|
|
406
|
+
},
|
|
407
|
+
{ status: 403, headers: corsHeaders() },
|
|
408
|
+
);
|
|
409
|
+
}
|
|
388
410
|
const action = url.pathname.slice("/api/actions/".length);
|
|
389
411
|
let body: Record<string, unknown> = {};
|
|
390
412
|
try {
|
|
@@ -469,6 +491,18 @@ export async function startDashboardServer(
|
|
|
469
491
|
return withCors(handleOrchestrateRuns(db, limit));
|
|
470
492
|
}
|
|
471
493
|
|
|
494
|
+
// ---- GET /api/v2/analytics ----
|
|
495
|
+
if (url.pathname === "/api/v2/analytics" && req.method === "GET") {
|
|
496
|
+
if (!db) {
|
|
497
|
+
return Response.json(
|
|
498
|
+
{ error: "V2 data unavailable" },
|
|
499
|
+
{ status: 503, headers: corsHeaders() },
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
refreshV2Data();
|
|
503
|
+
return withCors(handleAnalytics(db));
|
|
504
|
+
}
|
|
505
|
+
|
|
472
506
|
// ---- GET /api/v2/skills/:name ----
|
|
473
507
|
if (url.pathname.startsWith("/api/v2/skills/") && req.method === "GET") {
|
|
474
508
|
const skillName = decodePathSegment(url.pathname.slice("/api/v2/skills/".length));
|
|
@@ -510,7 +544,7 @@ export async function startDashboardServer(
|
|
|
510
544
|
},
|
|
511
545
|
});
|
|
512
546
|
|
|
513
|
-
|
|
547
|
+
boundPort = server.port;
|
|
514
548
|
|
|
515
549
|
if (openBrowser) {
|
|
516
550
|
const url = `http://${hostname}:${boundPort}`;
|