selftune 0.2.8 → 0.2.9

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.
Files changed (37) hide show
  1. package/apps/local-dashboard/dist/assets/{index-CRtLkBTi.css → index-Bs3Y4ixf.css} +1 -1
  2. package/apps/local-dashboard/dist/assets/index-C4UYGWKr.js +15 -0
  3. package/apps/local-dashboard/dist/index.html +2 -2
  4. package/cli/selftune/activation-rules.ts +36 -18
  5. package/cli/selftune/agent-guidance.ts +16 -16
  6. package/cli/selftune/alpha-identity.ts +1 -2
  7. package/cli/selftune/alpha-upload/flush.ts +2 -2
  8. package/cli/selftune/alpha-upload/stage-canonical.ts +12 -3
  9. package/cli/selftune/contribute/bundle.ts +1 -0
  10. package/cli/selftune/dashboard-contract.ts +1 -1
  11. package/cli/selftune/dashboard-server.ts +10 -52
  12. package/cli/selftune/eval/hooks-to-evals.ts +12 -6
  13. package/cli/selftune/grading/auto-grade.ts +1 -0
  14. package/cli/selftune/grading/grade-session.ts +1 -0
  15. package/cli/selftune/hooks/auto-activate.ts +5 -0
  16. package/cli/selftune/hooks/evolution-guard.ts +11 -14
  17. package/cli/selftune/init.ts +80 -71
  18. package/cli/selftune/localdb/direct-write.ts +54 -12
  19. package/cli/selftune/localdb/queries.ts +157 -0
  20. package/cli/selftune/localdb/schema.ts +44 -1
  21. package/cli/selftune/monitoring/watch.ts +1 -0
  22. package/cli/selftune/normalization.ts +3 -0
  23. package/cli/selftune/observability.ts +13 -7
  24. package/cli/selftune/orchestrate.ts +15 -37
  25. package/cli/selftune/repair/skill-usage.ts +6 -3
  26. package/cli/selftune/sync.ts +1 -0
  27. package/cli/selftune/types.ts +2 -2
  28. package/package.json +2 -1
  29. package/packages/telemetry-contract/package.json +2 -2
  30. package/packages/telemetry-contract/src/index.ts +0 -1
  31. package/packages/telemetry-contract/src/schemas.ts +5 -24
  32. package/skill/SKILL.md +3 -3
  33. package/skill/Workflows/Dashboard.md +8 -13
  34. package/skill/Workflows/Doctor.md +7 -7
  35. package/skill/Workflows/Initialize.md +35 -62
  36. package/skill/references/logs.md +6 -0
  37. package/apps/local-dashboard/dist/assets/index-Bk9vSHHd.js +0 -15
@@ -16,12 +16,12 @@ import { parseArgs } from "node:util";
16
16
 
17
17
  import { readAlphaIdentity } from "./alpha-identity.js";
18
18
  import type { UploadCycleSummary } from "./alpha-upload/index.js";
19
- import { ORCHESTRATE_LOCK, SELFTUNE_CONFIG_PATH, SIGNAL_LOG } from "./constants.js";
19
+ import { ORCHESTRATE_LOCK, SELFTUNE_CONFIG_PATH } from "./constants.js";
20
20
  import type { OrchestrateRunReport, OrchestrateRunSkillAction } from "./dashboard-contract.js";
21
21
  import type { EvolveResult } from "./evolution/evolve.js";
22
22
  import { readGradingResultsForSkill } from "./grading/results.js";
23
23
  import { getDb } from "./localdb/db.js";
24
- import { writeOrchestrateRunToDb } from "./localdb/direct-write.js";
24
+ import { updateSignalConsumed, writeOrchestrateRunToDb } from "./localdb/direct-write.js";
25
25
  import {
26
26
  queryEvolutionAudit,
27
27
  queryImprovementSignals,
@@ -36,13 +36,13 @@ import { computeStatus } from "./status.js";
36
36
  import type { SyncResult } from "./sync.js";
37
37
  import { createDefaultSyncOptions, syncSources } from "./sync.js";
38
38
  import type {
39
+ AlphaIdentity,
39
40
  EvolutionAuditEntry,
40
41
  ImprovementSignalRecord,
41
42
  QueryLogRecord,
42
43
  SessionTelemetryRecord,
43
44
  SkillUsageRecord,
44
45
  } from "./types.js";
45
- import { readJsonl } from "./utils/jsonl.js";
46
46
  import { detectAgent } from "./utils/llm-call.js";
47
47
  import { getSelftuneVersion, readConfiguredAgentType } from "./utils/selftune-meta.js";
48
48
  import {
@@ -123,42 +123,17 @@ export function groupSignalsBySkill(signals: ImprovementSignalRecord[]): Map<str
123
123
  return map;
124
124
  }
125
125
 
126
- export function markSignalsConsumed(
127
- signals: ImprovementSignalRecord[],
128
- runId: string,
129
- signalLogPath: string = SIGNAL_LOG,
130
- ): void {
126
+ export function markSignalsConsumed(signals: ImprovementSignalRecord[], runId: string): void {
131
127
  try {
132
128
  if (signals.length === 0) return;
133
- if (!existsSync(signalLogPath)) return;
134
-
135
- // Build lookup set for matching pending signals
136
- const pendingKeys = new Set(signals.map((s) => `${s.timestamp}|${s.session_id}`));
137
-
138
- const allRecords = readJsonl<ImprovementSignalRecord>(signalLogPath);
139
- const now = new Date().toISOString();
140
- const updated = allRecords.map((record) => {
141
- const key = `${record.timestamp}|${record.session_id}`;
142
- if (pendingKeys.has(key) && !record.consumed) {
143
- return {
144
- ...record,
145
- consumed: true,
146
- consumed_at: now,
147
- consumed_by_run: runId,
148
- };
129
+ for (const signal of signals) {
130
+ const ok = updateSignalConsumed(signal.session_id, signal.query, signal.signal_type, runId);
131
+ if (!ok) {
132
+ console.error(
133
+ `[orchestrate] failed to mark signal consumed: session_id=${signal.session_id}, signal_type=${signal.signal_type}`,
134
+ );
149
135
  }
150
- return record;
151
- });
152
-
153
- // Re-read to capture any signals appended between our read and write
154
- const freshRecords = readJsonl<ImprovementSignalRecord>(signalLogPath);
155
- const existingKeys = new Set(updated.map((r) => `${r.timestamp}|${r.session_id}`));
156
- const newlyAppended = freshRecords.filter(
157
- (r) => !existingKeys.has(`${r.timestamp}|${r.session_id}`),
158
- );
159
- const merged = [...updated, ...newlyAppended];
160
-
161
- writeFileSync(signalLogPath, `${merged.map((r) => JSON.stringify(r)).join("\n")}\n`);
136
+ }
162
137
  } catch {
163
138
  // Silent on errors
164
139
  }
@@ -412,6 +387,7 @@ export interface OrchestrateDeps {
412
387
  resolveSkillPath?: (skillName: string) => string | undefined;
413
388
  readGradingResults?: (skillName: string) => ReturnType<typeof readGradingResultsForSkill>;
414
389
  readSignals?: () => ImprovementSignalRecord[];
390
+ readAlphaIdentity?: () => AlphaIdentity | null;
415
391
  }
416
392
 
417
393
  // ---------------------------------------------------------------------------
@@ -729,6 +705,8 @@ export async function orchestrate(
729
705
  });
730
706
  const _resolveSkillPath = deps.resolveSkillPath ?? defaultResolveSkillPath;
731
707
  const _readGradingResults = deps.readGradingResults ?? readGradingResultsForSkill;
708
+ const _readAlphaIdentity =
709
+ deps.readAlphaIdentity ?? (() => readAlphaIdentity(SELFTUNE_CONFIG_PATH));
732
710
 
733
711
  // Lazy-load evolve and watch to avoid circular imports
734
712
  const _evolve = deps.evolve ?? (await import("./evolution/evolve.js")).evolve;
@@ -1001,7 +979,7 @@ export async function orchestrate(
1001
979
  // -------------------------------------------------------------------------
1002
980
  // Step 9: Alpha upload (fail-open — never blocks the orchestrate loop)
1003
981
  // -------------------------------------------------------------------------
1004
- const alphaIdentity = readAlphaIdentity(SELFTUNE_CONFIG_PATH);
982
+ const alphaIdentity = _readAlphaIdentity();
1005
983
  if (alphaIdentity?.enrolled) {
1006
984
  try {
1007
985
  console.error("[orchestrate] Running alpha upload cycle...");
@@ -511,14 +511,17 @@ Options:
511
511
  since,
512
512
  );
513
513
  const rolloutPaths = findRolloutFiles(values["codex-home"] ?? DEFAULT_CODEX_HOME, since);
514
+ // SQLite-first: default paths read from SQLite; JSONL only for custom --skill-log overrides
514
515
  let rawSkillRecords: SkillUsageRecord[];
515
516
  let queryRecords: QueryLogRecord[];
516
- try {
517
+ const skillLogPath = values["skill-log"] ?? SKILL_LOG;
518
+ if (skillLogPath === SKILL_LOG) {
517
519
  const db = getDb();
518
520
  rawSkillRecords = querySkillUsageRecords(db) as SkillUsageRecord[];
519
521
  queryRecords = queryQueryLog(db) as QueryLogRecord[];
520
- } catch {
521
- rawSkillRecords = readJsonl<SkillUsageRecord>(values["skill-log"] ?? SKILL_LOG);
522
+ } else {
523
+ // test/custom-path fallback
524
+ rawSkillRecords = readJsonl<SkillUsageRecord>(skillLogPath);
522
525
  queryRecords = readJsonl<QueryLogRecord>(QUERY_LOG);
523
526
  }
524
527
  const { repairedRecords, repairedSessionIds } = rebuildSkillUsageFromTranscripts(
@@ -367,6 +367,7 @@ function rebuildSkillUsageOverlay(
367
367
  rawSkillRecords = readJsonl<SkillUsageRecord>(options.skillLogPath);
368
368
  }
369
369
  } else {
370
+ // Intentional JSONL fallback: custom --skill-log path overrides SQLite reads
370
371
  rawSkillRecords = readJsonl<SkillUsageRecord>(options.skillLogPath);
371
372
  }
372
373
  const { repairedRecords, repairedSessionIds } = rebuildSkillUsageFromTranscripts(
@@ -125,7 +125,7 @@ export type {
125
125
  CanonicalSessionRecord,
126
126
  CanonicalSkillInvocationRecord,
127
127
  CanonicalSourceSessionKind,
128
- } from "@selftune/telemetry-contract";
128
+ } from "@selftune/telemetry-contract/types";
129
129
  // ---------------------------------------------------------------------------
130
130
  // Canonical normalization types (local + cloud projection layer)
131
131
  // ---------------------------------------------------------------------------
@@ -138,7 +138,7 @@ export {
138
138
  CANONICAL_RECORD_KINDS,
139
139
  CANONICAL_SCHEMA_VERSION,
140
140
  CANONICAL_SOURCE_SESSION_KINDS,
141
- } from "@selftune/telemetry-contract";
141
+ } from "@selftune/telemetry-contract/types";
142
142
 
143
143
  // ---------------------------------------------------------------------------
144
144
  // Transcript parsing
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "selftune",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "Self-improving skills CLI for AI agents",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -60,6 +60,7 @@
60
60
  "test:fast": "bun test $(find tests -name '*.test.ts' ! -name 'evolve.test.ts' ! -name 'integration.test.ts' ! -name 'dashboard-server.test.ts' ! -path '*/blog-proof/*')",
61
61
  "test:slow": "bun test tests/evolution/evolve.test.ts tests/evolution/integration.test.ts tests/monitoring/integration.test.ts tests/dashboard/dashboard-server.test.ts",
62
62
  "build:dashboard": "cd apps/local-dashboard && bunx vite build",
63
+ "link:claude-workspace": "bash scripts/link-claude-workspace.sh",
63
64
  "sync-version": "bun run scripts/sync-skill-version.ts",
64
65
  "validate:subagents": "bun run scripts/validate-subagent-docs.ts",
65
66
  "prepublishOnly": "bun run sync-version && bun run build:dashboard",
@@ -13,12 +13,12 @@
13
13
  },
14
14
  "exports": {
15
15
  ".": "./index.ts",
16
+ "./schemas": "./src/schemas.ts",
16
17
  "./types": "./src/types.ts",
17
18
  "./validators": "./src/validators.ts",
18
- "./schemas": "./src/schemas.ts",
19
19
  "./fixtures": "./fixtures/index.ts"
20
20
  },
21
21
  "dependencies": {
22
- "zod": "^3.24.0"
22
+ "zod": "^4.3.6"
23
23
  }
24
24
  }
@@ -1,3 +1,2 @@
1
- export * from "./schemas.js";
2
1
  export * from "./types.js";
3
2
  export * from "./validators.js";
@@ -1,12 +1,3 @@
1
- /**
2
- * Zod validation schemas for all canonical telemetry record types
3
- * and the PushPayloadV2 envelope.
4
- *
5
- * This is the single source of truth -- cloud consumers should import
6
- * from @selftune/telemetry-contract/schemas instead of maintaining
7
- * their own copies.
8
- */
9
-
10
1
  import { z } from "zod";
11
2
  import {
12
3
  CANONICAL_CAPTURE_MODES,
@@ -19,8 +10,6 @@ import {
19
10
  CANONICAL_SOURCE_SESSION_KINDS,
20
11
  } from "./types.js";
21
12
 
22
- // ---------- Shared enum schemas ----------
23
-
24
13
  export const canonicalPlatformSchema = z.enum(CANONICAL_PLATFORMS);
25
14
  export const captureModeSchema = z.enum(CANONICAL_CAPTURE_MODES);
26
15
  export const sourceSessionKindSchema = z.enum(CANONICAL_SOURCE_SESSION_KINDS);
@@ -29,14 +18,12 @@ export const invocationModeSchema = z.enum(CANONICAL_INVOCATION_MODES);
29
18
  export const completionStatusSchema = z.enum(CANONICAL_COMPLETION_STATUSES);
30
19
  export const recordKindSchema = z.enum(CANONICAL_RECORD_KINDS);
31
20
 
32
- // ---------- Shared structural schemas ----------
33
-
34
21
  export const rawSourceRefSchema = z.object({
35
22
  path: z.string().optional(),
36
23
  line: z.number().int().nonnegative().optional(),
37
24
  event_type: z.string().optional(),
38
25
  raw_id: z.string().optional(),
39
- metadata: z.record(z.unknown()).optional(),
26
+ metadata: z.record(z.string(), z.unknown()).optional(),
40
27
  });
41
28
 
42
29
  export const canonicalRecordBaseSchema = z.object({
@@ -54,8 +41,6 @@ export const canonicalSessionRecordBaseSchema = canonicalRecordBaseSchema.extend
54
41
  session_id: z.string().min(1),
55
42
  });
56
43
 
57
- // ---------- Canonical record schemas ----------
58
-
59
44
  export const CanonicalSessionRecordSchema = canonicalSessionRecordBaseSchema.extend({
60
45
  record_kind: z.literal("session"),
61
46
  external_session_id: z.string().optional(),
@@ -115,7 +100,7 @@ export const CanonicalExecutionFactRecordSchema = canonicalSessionRecordBaseSche
115
100
  execution_fact_id: z.string().min(1),
116
101
  occurred_at: z.string().datetime(),
117
102
  prompt_id: z.string().optional(),
118
- tool_calls_json: z.record(z.number().finite()),
103
+ tool_calls_json: z.record(z.string(), z.number().finite()),
119
104
  total_tool_calls: z.number().int().nonnegative(),
120
105
  bash_commands_redacted: z.array(z.string()).optional(),
121
106
  assistant_turns: z.number().int().nonnegative(),
@@ -151,8 +136,6 @@ export const CanonicalEvolutionEvidenceRecordSchema = z.object({
151
136
  raw_source_ref: rawSourceRefSchema.optional(),
152
137
  });
153
138
 
154
- // ---------- Orchestrate run schemas ----------
155
-
156
139
  export const OrchestrateRunSkillActionSchema = z.object({
157
140
  skill: z.string().min(1),
158
141
  action: z.enum(["evolve", "watch", "skip"]),
@@ -179,12 +162,12 @@ export const PushOrchestrateRunRecordSchema = z.object({
179
162
  skill_actions: z.array(OrchestrateRunSkillActionSchema),
180
163
  });
181
164
 
182
- // ---------- Push V2 envelope ----------
183
-
184
165
  export const PushPayloadV2Schema = z.object({
185
166
  schema_version: z.literal("2.0"),
186
167
  client_version: z.string().min(1),
187
- push_id: z.string().uuid(),
168
+ // Queue-generated push IDs are typically UUIDs, but the wire contract only
169
+ // requires a stable non-empty idempotency key.
170
+ push_id: z.string().min(1),
188
171
  normalizer_version: z.string().min(1),
189
172
  canonical: z.object({
190
173
  sessions: z.array(CanonicalSessionRecordSchema).min(0),
@@ -197,8 +180,6 @@ export const PushPayloadV2Schema = z.object({
197
180
  }),
198
181
  });
199
182
 
200
- // ---------- Inferred types from Zod schemas ----------
201
-
202
183
  export type PushPayloadV2 = z.infer<typeof PushPayloadV2Schema>;
203
184
  export type ZodCanonicalSessionRecord = z.infer<typeof CanonicalSessionRecordSchema>;
204
185
  export type ZodCanonicalPromptRecord = z.infer<typeof CanonicalPromptRecordSchema>;
package/skill/SKILL.md CHANGED
@@ -12,7 +12,7 @@ description: >
12
12
  even if they don't say "selftune" explicitly.
13
13
  metadata:
14
14
  author: selftune-dev
15
- version: 0.2.8
15
+ version: 0.2.9
16
16
  category: developer-tools
17
17
  ---
18
18
 
@@ -104,8 +104,8 @@ selftune cron remove [--dry-run]
104
104
  selftune telemetry [status|enable|disable]
105
105
  selftune export [TABLE...] [--output/-o DIR] [--since DATE]
106
106
 
107
- # Alpha enrollment (cloud app is control-plane only, not the main UX)
108
- selftune init --alpha --alpha-email <email> --alpha-key <st_live_key>
107
+ # Alpha enrollment (device-code flow browser opens automatically)
108
+ selftune init --alpha --alpha-email <email>
109
109
  selftune alpha upload [--dry-run]
110
110
  selftune status # shows cloud link state + upload readiness
111
111
  ```
@@ -11,14 +11,11 @@ selftune dashboard
11
11
  ```
12
12
 
13
13
  Starts a Bun HTTP server with a React SPA dashboard and opens it in the
14
- default browser. The dashboard reads SQLite directly, but the current
15
- live-update invalidation path still watches JSONL logs and pushes
16
- updates via Server-Sent Events (SSE). That means the dashboard usually
17
- refreshes quickly, but SQLite-only writes can still lag until the WAL
18
- cutover lands. TanStack Query polling (60s) acts as a fallback. Action
19
- buttons trigger selftune commands directly from the dashboard. Use
20
- `selftune export` to generate JSONL from SQLite for debugging or
21
- offline analysis.
14
+ default browser. The dashboard reads SQLite directly and uses WAL-based
15
+ invalidation to push live updates via Server-Sent Events (SSE).
16
+ TanStack Query polling (60s) acts as a fallback. Action buttons trigger
17
+ selftune commands directly from the dashboard. Use `selftune export` to
18
+ generate JSONL from SQLite for debugging or offline analysis.
22
19
 
23
20
  ## Options
24
21
 
@@ -56,11 +53,9 @@ override.
56
53
  ### Live Updates (SSE)
57
54
 
58
55
  The dashboard connects to `/api/v2/events` via Server-Sent Events.
59
- When watched JSONL log files change on disk, the server broadcasts an
60
- `update` event. The SPA invalidates all cached queries, triggering
61
- immediate refetches. New data usually appears quickly, but the runtime
62
- footer and Status page will warn when the server is still in this
63
- legacy JSONL watcher mode.
56
+ The server watches the SQLite WAL file for changes and broadcasts an
57
+ `update` event when new data is written. The SPA invalidates all cached
58
+ queries, triggering immediate refetches (~1s latency).
64
59
 
65
60
  TanStack Query polling (60s) acts as a fallback safety net in case the
66
61
  SSE connection drops. Data also refreshes on window focus.
@@ -40,14 +40,14 @@ None. Doctor runs all checks unconditionally.
40
40
  },
41
41
  {
42
42
  "name": "dashboard_freshness_mode",
43
- "status": "warn",
44
- "message": "Dashboard still uses legacy JSONL watcher invalidation"
43
+ "status": "pass",
44
+ "message": "Dashboard reads SQLite and watches WAL for live updates"
45
45
  }
46
46
  ],
47
47
  "summary": {
48
- "pass": 8,
48
+ "pass": 9,
49
49
  "fail": 1,
50
- "warn": 1,
50
+ "warn": 0,
51
51
  "total": 10
52
52
  },
53
53
  "healthy": false
@@ -183,9 +183,9 @@ for root cause analysis.
183
183
  **Diagnostic steps:**
184
184
  1. Check `selftune status` — look at "Alpha Upload" and "Cloud link" lines
185
185
  2. If `doctor` includes a `cloud_link` or alpha queue warning, prefer `.checks[].guidance.next_command`
186
- 3. If "not enrolled" or "not linked": run `selftune init --alpha --alpha-email <email> --alpha-key <key>`
187
- 4. If "enrolled (missing credential)": re-run `selftune init --alpha --alpha-email <email> --alpha-key <credential> --force`
188
- 5. If "api_key has invalid format": credential must start with `st_live_` or `st_test_`
186
+ 3. If "not enrolled" or "not linked": run `selftune init --alpha --alpha-email <email>` (opens browser for device-code auth)
187
+ 4. If "enrolled (missing credential)": re-run `selftune init --alpha --alpha-email <email> --force` (re-authenticates via browser)
188
+ 5. If "api_key has invalid format": re-run init with `--alpha --force` to re-authenticate
189
189
 
190
190
  **Resolution:** Follow the setup sequence in Initialize workflow → Alpha Enrollment section.
191
191
 
@@ -25,11 +25,10 @@ selftune init --no-alpha [--force]
25
25
  | `--force` | Reinitialize even if config already exists | Off |
26
26
  | `--enable-autonomy` | Enable autonomous scheduling during init | Off |
27
27
  | `--schedule-format <fmt>` | Schedule format: `cron`, `launchd`, `systemd` | Auto-detected |
28
- | `--alpha` | Enroll in the selftune alpha program | Off |
28
+ | `--alpha` | Enroll in the selftune alpha program (opens browser for device-code auth) | Off |
29
29
  | `--no-alpha` | Unenroll from the alpha program (preserves user_id) | Off |
30
30
  | `--alpha-email <email>` | Email for alpha enrollment (required with `--alpha`) | - |
31
31
  | `--alpha-name <name>` | Display name for alpha enrollment | - |
32
- | `--alpha-key <key>` | API key for cloud uploads (`st_live_*` format) | - |
33
32
 
34
33
  ## Output Format
35
34
 
@@ -46,9 +45,12 @@ Creates `~/.selftune/config.json`:
46
45
  "alpha": {
47
46
  "enrolled": true,
48
47
  "user_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
48
+ "cloud_user_id": "cloud-uuid-...",
49
+ "cloud_org_id": "org-uuid-...",
49
50
  "email": "user@example.com",
50
51
  "display_name": "User Name",
51
- "consent_timestamp": "2026-02-28T10:00:00Z"
52
+ "consent_timestamp": "2026-02-28T10:00:00Z",
53
+ "api_key": "<provisioned automatically via device-code flow>"
52
54
  }
53
55
  }
54
56
  ```
@@ -66,9 +68,12 @@ Creates `~/.selftune/config.json`:
66
68
  | `alpha` | object? | Alpha program enrollment (present only if enrolled) |
67
69
  | `alpha.enrolled` | boolean | Whether the user is currently enrolled |
68
70
  | `alpha.user_id` | string | Stable UUID, generated once, preserved across reinits |
71
+ | `alpha.cloud_user_id` | string? | Cloud account UUID (set by device-code flow) |
72
+ | `alpha.cloud_org_id` | string? | Cloud organization UUID (set by device-code flow) |
69
73
  | `alpha.email` | string? | Email provided at enrollment |
70
74
  | `alpha.display_name` | string? | Optional display name |
71
75
  | `alpha.consent_timestamp` | string | ISO 8601 timestamp of consent |
76
+ | `alpha.api_key` | string? | Upload credential (provisioned automatically by device-code flow) |
72
77
 
73
78
  ## Steps
74
79
 
@@ -195,79 +200,61 @@ Before running the alpha command:
195
200
  The CLI stays non-interactive. The agent is responsible for collecting consent
196
201
  and the required `--alpha-email` value before invoking the command.
197
202
 
198
- ## Alpha Enrollment (Agent-First Flow)
203
+ ## Alpha Enrollment (Device-Code Flow)
199
204
 
200
205
  The alpha program sends canonical telemetry to the selftune cloud for analysis.
201
- Setup is agent-firstthe cloud app is a one-time control-plane handoff, not the main UX.
206
+ Enrollment uses a device-code flow one command, one browser approval, fully automatic.
202
207
 
203
208
  ### Setup Sequence
204
209
 
205
210
  1. **Check local config**: Run `selftune status` — look for the "Alpha Upload" section
206
- 2. **If not linked**: Tell the user:
207
- > To join the selftune alpha program, you need to create an account at https://app.selftune.dev and issue an upload credential. This is a one-time step — afterwards everything runs locally through the CLI.
208
- 3. **User completes cloud enrollment**: Signs in, enrolls, copies the `st_live_*` credential
209
- 4. **Store credential locally**:
211
+ 2. **If not linked**: Collect the user's email and run:
210
212
 
211
213
  ```bash
212
- selftune init --alpha --alpha-email <user-email> --alpha-key <st_live_credential>
214
+ selftune init --alpha --alpha-email <user-email> --force
213
215
  ```
214
216
 
215
- 5. **Verify readiness**: The init command prints a readiness check. If all checks pass, alpha upload is active.
216
- The readiness JSON now includes a `guidance` object with:
217
+ 3. **Browser opens automatically**: The CLI requests a device code, opens the verification URL in the browser with the code pre-filled, and polls for approval.
218
+ 4. **User approves in browser**: One click to authorize.
219
+ 5. **CLI receives credentials**: API key, cloud_user_id, and org_id are automatically provisioned and stored in `~/.selftune/config.json` with `0600` permissions.
220
+ 6. **Verify readiness**: The init command prints a readiness check. If all checks pass, alpha upload is active.
221
+ The readiness JSON includes a `guidance` object with:
217
222
  - `message`
218
223
  - `next_command`
219
224
  - `suggested_commands[]`
220
225
  - `blocking`
221
- 6. **If readiness fails**: Run `selftune doctor` to diagnose. Common issues:
222
- - `api_key not set` → re-run init with `--alpha-key`
223
- - `api_key has invalid format` credential must start with `st_live_` or `st_test_`
224
- - `not enrolled` → re-run init with `--alpha --alpha-email <email> --alpha-key <key>`
226
+ 7. **If readiness fails**: Run `selftune doctor` to diagnose. Common issues:
227
+ - `not enrolled` → re-run `selftune init --alpha --alpha-email <email> --force`
228
+ - Device-code expired re-run the init command (codes expire after ~15 minutes)
225
229
 
226
230
  ### Key Principle
227
231
 
228
- The cloud app is used **only** for:
229
- - Sign-in
230
- - Alpha enrollment
231
- - Upload credential issuance
232
-
233
- All other selftune operations happen through the local CLI and this agent.
232
+ The cloud app is used **only** for the one-time browser approval during device-code auth. All other selftune operations happen through the local CLI and this agent.
234
233
 
235
234
  ### Enroll
236
235
 
237
236
  ```bash
238
237
  selftune init --alpha --alpha-email user@example.com --alpha-name "User Name" --force
239
- selftune init --alpha-key st_live_abc123... # after enrollment, store the API key
240
238
  ```
241
239
 
242
240
  The `--alpha-email` flag is required. The command will:
243
241
  1. Generate a stable UUID (preserved across reinits)
244
- 2. Write the alpha block to `~/.selftune/config.json`
245
- 3. Print an `alpha_enrolled` JSON message to stdout
246
- 4. Print the consent notice to stderr
247
- 5. If an `--alpha-key` is provided, chmod `~/.selftune/config.json` to `0600`
242
+ 2. Request a device code from the cloud API
243
+ 3. Open the browser to the verification URL
244
+ 4. Poll until the user approves
245
+ 5. Receive and store the API key, cloud_user_id, and org_id automatically
246
+ 6. Write the alpha block to `~/.selftune/config.json` with `0600` permissions
247
+ 7. Print an `alpha_enrolled` JSON message to stdout
248
+ 8. Print the consent notice to stderr
248
249
 
249
250
  The consent notice explicitly states that the friendly alpha cohort shares raw
250
251
  prompt/query text in addition to skill/session/evolution metadata.
251
252
 
252
- ### API Key Provisioning
253
-
254
- After enrollment, users need to configure an API key for cloud uploads:
255
-
256
- 1. Create a cloud account at the selftune web app
257
- 2. Generate an API key (format: `st_live_*`)
258
- 3. Store the key locally:
259
-
260
- ```bash
261
- selftune init --alpha --alpha-email <email> --alpha-key st_live_abc123... --force
262
- ```
263
-
264
- Without an API key, alpha enrollment is recorded locally but no uploads are attempted. When a key is stored, selftune tightens the local config file permissions to `0600`.
265
-
266
253
  ### Upload Behavior
267
254
 
268
- Once enrolled and an API key is configured, `selftune orchestrate` automatically
269
- uploads new session, invocation, and evolution data to the cloud API at the end of
270
- each run. This upload step is fail-open -- errors never block the orchestrate loop.
255
+ Once enrolled, `selftune orchestrate` automatically uploads new session,
256
+ invocation, and evolution data to the cloud API at the end of each run.
257
+ This upload step is fail-open -- errors never block the orchestrate loop.
271
258
  Use `selftune alpha upload` for manual uploads or `selftune alpha upload --dry-run`
272
259
  to preview what would be sent.
273
260
 
@@ -298,23 +285,9 @@ If `--alpha` is passed without `--alpha-email`, the CLI throws a JSON error:
298
285
  }
299
286
  ```
300
287
 
301
- When alpha readiness is evaluated after `selftune init --alpha`, the CLI emits:
302
-
303
- ```json
304
- {
305
- "alpha_readiness": {
306
- "ready": false,
307
- "missing": ["api_key not set"],
308
- "guidance": {
309
- "code": "alpha_credential_required",
310
- "message": "Alpha enrollment exists, but the local upload credential is missing or invalid.",
311
- "next_command": "selftune init --alpha --alpha-email user@example.com --alpha-key <st_live_key> --force",
312
- "suggested_commands": ["selftune status", "selftune doctor"],
313
- "blocking": true
314
- }
315
- }
316
- }
317
- ```
288
+ If the device-code flow fails (network error, timeout, user denied), the CLI throws
289
+ with a descriptive error message. The agent should relay this to the user and suggest
290
+ retrying with `selftune init --alpha --alpha-email <email> --force`.
318
291
 
319
292
  ## Common Patterns
320
293
 
@@ -326,7 +299,7 @@ When alpha readiness is evaluated after `selftune init --alpha`, the CLI emits:
326
299
  **User wants alpha enrollment**
327
300
  > Ask whether they want to opt into alpha data sharing. If yes, collect email
328
301
  > and optional display name, then run `selftune init --alpha --alpha-email ...`.
329
- > If no, continue with plain `selftune init`.
302
+ > The browser opens automatically for approval. No manual key management needed.
330
303
 
331
304
  **Hooks not capturing data**
332
305
  > Run `selftune doctor` to check hook installation. Parse the JSON output
@@ -4,6 +4,12 @@ selftune writes raw legacy logs plus a canonical event log. This reference
4
4
  describes each format in detail for the skill to use when parsing sessions,
5
5
  audit trails, and cloud-ingest exports.
6
6
 
7
+ > **Note:** JSONL files are now backup/recovery only. SQLite (`~/.selftune/selftune.db`)
8
+ > is the sole operational store for all runtime reads. JSONL writes are retained for
9
+ > append-only durability, but all dashboard queries, hook reads, grading, monitoring,
10
+ > and upload staging read from SQLite. JSONL reads only occur when custom log paths
11
+ > are provided (e.g., `--telemetry-log`, `--skill-log`) for test isolation.
12
+
7
13
  ---
8
14
 
9
15
  ## ~/.claude/session_telemetry_log.jsonl