selftune 0.2.22 → 0.2.23

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 (94) hide show
  1. package/README.md +4 -2
  2. package/apps/local-dashboard/dist/assets/index-CwOtTrUS.css +1 -0
  3. package/apps/local-dashboard/dist/assets/index-f1HQpbeH.js +59 -0
  4. package/apps/local-dashboard/dist/assets/vendor-ui-jVSaIZey.js +12 -0
  5. package/apps/local-dashboard/dist/index.html +3 -3
  6. package/cli/selftune/adapters/pi/hook.ts +273 -0
  7. package/cli/selftune/adapters/pi/install.ts +207 -0
  8. package/cli/selftune/constants.ts +10 -1
  9. package/cli/selftune/dashboard-contract.ts +14 -0
  10. package/cli/selftune/evolution/engines/judge-engine.ts +96 -0
  11. package/cli/selftune/evolution/engines/replay-engine.ts +158 -0
  12. package/cli/selftune/evolution/evidence.ts +2 -6
  13. package/cli/selftune/evolution/evolve-body.ts +73 -20
  14. package/cli/selftune/evolution/validate-body.ts +78 -42
  15. package/cli/selftune/evolution/validate-routing.ts +45 -104
  16. package/cli/selftune/hooks/skill-eval.ts +2 -1
  17. package/cli/selftune/hooks-shared/types.ts +1 -0
  18. package/cli/selftune/index.ts +23 -5
  19. package/cli/selftune/ingestors/pi-ingest.ts +726 -0
  20. package/cli/selftune/init.ts +11 -1
  21. package/cli/selftune/localdb/direct-write.ts +85 -0
  22. package/cli/selftune/localdb/materialize.ts +6 -7
  23. package/cli/selftune/localdb/queries.ts +126 -0
  24. package/cli/selftune/localdb/schema.ts +38 -0
  25. package/cli/selftune/observability.ts +8 -1
  26. package/cli/selftune/orchestrate.ts +43 -0
  27. package/cli/selftune/registry/client.ts +74 -0
  28. package/cli/selftune/registry/history.ts +54 -0
  29. package/cli/selftune/registry/index.ts +90 -0
  30. package/cli/selftune/registry/install.ts +141 -0
  31. package/cli/selftune/registry/list.ts +44 -0
  32. package/cli/selftune/registry/push.ts +171 -0
  33. package/cli/selftune/registry/rollback.ts +49 -0
  34. package/cli/selftune/registry/status.ts +62 -0
  35. package/cli/selftune/registry/sync.ts +125 -0
  36. package/cli/selftune/repair/skill-usage.ts +4 -1
  37. package/cli/selftune/status.ts +31 -0
  38. package/cli/selftune/sync.ts +127 -23
  39. package/cli/selftune/types.ts +2 -1
  40. package/cli/selftune/utils/jsonl.ts +1 -30
  41. package/cli/selftune/utils/skill-discovery.ts +22 -0
  42. package/node_modules/@selftune/telemetry-contract/fixtures/evidence-only-push.ts +1 -1
  43. package/node_modules/@selftune/telemetry-contract/fixtures/golden.test.ts +0 -1
  44. package/node_modules/@selftune/telemetry-contract/fixtures/partial-push-unresolved-parents.ts +1 -1
  45. package/node_modules/@selftune/telemetry-contract/package.json +1 -1
  46. package/node_modules/@selftune/telemetry-contract/src/index.ts +1 -0
  47. package/node_modules/@selftune/telemetry-contract/src/schemas.ts +22 -4
  48. package/node_modules/@selftune/telemetry-contract/src/types.ts +1 -12
  49. package/node_modules/@selftune/telemetry-contract/tests/compatibility.test.ts +0 -1
  50. package/package.json +1 -1
  51. package/packages/telemetry-contract/fixtures/evidence-only-push.ts +1 -1
  52. package/packages/telemetry-contract/fixtures/golden.test.ts +0 -1
  53. package/packages/telemetry-contract/fixtures/partial-push-unresolved-parents.ts +1 -1
  54. package/packages/telemetry-contract/package.json +1 -1
  55. package/packages/telemetry-contract/src/index.ts +1 -0
  56. package/packages/telemetry-contract/src/schemas.ts +22 -4
  57. package/packages/telemetry-contract/src/types.ts +1 -12
  58. package/packages/telemetry-contract/tests/compatibility.test.ts +0 -1
  59. package/packages/ui/AGENTS.md +16 -0
  60. package/packages/ui/README.md +1 -1
  61. package/packages/ui/package.json +1 -1
  62. package/packages/ui/src/components/ActivityTimeline.tsx +152 -168
  63. package/packages/ui/src/components/AnalyticsCharts.tsx +344 -0
  64. package/packages/ui/src/components/EvidenceViewer.tsx +153 -443
  65. package/packages/ui/src/components/EvolutionTimeline.tsx +34 -87
  66. package/packages/ui/src/components/InfoTip.tsx +1 -2
  67. package/packages/ui/src/components/InvocationsPanel.tsx +413 -0
  68. package/packages/ui/src/components/JobHistoryTimeline.tsx +156 -0
  69. package/packages/ui/src/components/OrchestrateRunsPanel.tsx +18 -36
  70. package/packages/ui/src/components/OverviewPanels.tsx +652 -0
  71. package/packages/ui/src/components/PipelineStatusBar.tsx +65 -0
  72. package/packages/ui/src/components/SkillReportGuide.tsx +215 -0
  73. package/packages/ui/src/components/SkillReportPanels.tsx +919 -0
  74. package/packages/ui/src/components/SkillsLibrary.tsx +437 -0
  75. package/packages/ui/src/components/index.ts +56 -1
  76. package/packages/ui/src/components/section-cards.tsx +18 -35
  77. package/packages/ui/src/components/skill-health-grid.tsx +47 -37
  78. package/packages/ui/src/lib/constants.tsx +0 -1
  79. package/packages/ui/src/primitives/card.tsx +1 -1
  80. package/packages/ui/src/primitives/checkbox.tsx +1 -1
  81. package/packages/ui/src/primitives/dropdown-menu.tsx +2 -2
  82. package/packages/ui/src/primitives/select.tsx +2 -2
  83. package/packages/ui/src/types.ts +172 -4
  84. package/skill/SKILL.md +18 -4
  85. package/skill/Workflows/Ingest.md +60 -2
  86. package/skill/Workflows/Initialize.md +8 -5
  87. package/skill/Workflows/PlatformHooks.md +19 -3
  88. package/skill/Workflows/Registry.md +99 -0
  89. package/skill/Workflows/Sync.md +3 -1
  90. package/apps/local-dashboard/dist/assets/index-D8O-RG1I.js +0 -60
  91. package/apps/local-dashboard/dist/assets/index-_EcLywDg.css +0 -1
  92. package/apps/local-dashboard/dist/assets/vendor-ui-CGEmUayx.js +0 -12
  93. package/cli/selftune/utils/html.ts +0 -27
  94. package/packages/ui/src/components/RecentActivityFeed.tsx +0 -117
@@ -7,6 +7,7 @@
7
7
  * - Codex rollout logs
8
8
  * - OpenCode session history
9
9
  * - OpenClaw session history
10
+ * - Pi session history
10
11
  *
11
12
  * After syncing raw session/query/telemetry records, it rebuilds the repaired
12
13
  * skill-usage overlay from Claude transcripts and Codex rollouts so monitoring,
@@ -25,6 +26,8 @@ import {
25
26
  OPENCLAW_AGENTS_DIR,
26
27
  OPENCLAW_INGEST_MARKER,
27
28
  OPENCODE_INGEST_MARKER,
29
+ PI_INGEST_MARKER,
30
+ PI_SESSIONS_DIR,
28
31
  QUERY_LOG,
29
32
  REPAIRED_SKILL_LOG,
30
33
  REPAIRED_SKILL_SESSIONS_MARKER,
@@ -56,7 +59,14 @@ import {
56
59
  readSessionsFromSqlite,
57
60
  writeSession as writeOpenCodeSession,
58
61
  } from "./ingestors/opencode-ingest.js";
62
+ import {
63
+ findPiSessions,
64
+ findPiSkillNames,
65
+ parsePiSession,
66
+ writeSession as writePiSession,
67
+ } from "./ingestors/pi-ingest.js";
59
68
  import { getDb } from "./localdb/db.js";
69
+ import { writeCronRunToDb } from "./localdb/direct-write.js";
60
70
  import { querySkillUsageRecords } from "./localdb/queries.js";
61
71
  import {
62
72
  persistRepairedSkillUsageToDb,
@@ -91,6 +101,7 @@ export interface SyncResult {
91
101
  codex: SyncStepResult;
92
102
  opencode: SyncStepResult;
93
103
  openclaw: SyncStepResult;
104
+ pi: SyncStepResult;
94
105
  };
95
106
  repair: {
96
107
  ran: boolean;
@@ -113,6 +124,7 @@ export interface SyncOptions {
113
124
  codexHome: string;
114
125
  opencodeDataDir: string;
115
126
  openclawAgentsDir: string;
127
+ piSessionsDir: string;
116
128
  skillLogPath: string;
117
129
  repairedSkillLogPath: string;
118
130
  repairedSessionsPath: string;
@@ -123,6 +135,7 @@ export interface SyncOptions {
123
135
  syncCodex: boolean;
124
136
  syncOpenCode: boolean;
125
137
  syncOpenClaw: boolean;
138
+ syncPi: boolean;
126
139
  rebuildSkillUsage: boolean;
127
140
  }
128
141
 
@@ -133,6 +146,7 @@ export interface SyncDeps {
133
146
  syncCodex?: (options: SyncOptions) => SyncStepResult;
134
147
  syncOpenCode?: (options: SyncOptions) => SyncStepResult;
135
148
  syncOpenClaw?: (options: SyncOptions) => SyncStepResult;
149
+ syncPi?: (options: SyncOptions) => SyncStepResult;
136
150
  rebuildSkillUsage?: (options: SyncOptions) => {
137
151
  repairedSessions: number;
138
152
  repairedRecords: number;
@@ -154,6 +168,7 @@ export function createDefaultSyncOptions(overrides: Partial<SyncOptions> = {}):
154
168
  codexHome: DEFAULT_CODEX_HOME,
155
169
  opencodeDataDir: DEFAULT_OPENCODE_DATA_DIR,
156
170
  openclawAgentsDir: OPENCLAW_AGENTS_DIR,
171
+ piSessionsDir: PI_SESSIONS_DIR,
157
172
  skillLogPath: SKILL_LOG,
158
173
  repairedSkillLogPath: REPAIRED_SKILL_LOG,
159
174
  repairedSessionsPath: REPAIRED_SKILL_SESSIONS_MARKER,
@@ -163,6 +178,7 @@ export function createDefaultSyncOptions(overrides: Partial<SyncOptions> = {}):
163
178
  syncCodex: true,
164
179
  syncOpenCode: true,
165
180
  syncOpenClaw: true,
181
+ syncPi: true,
166
182
  rebuildSkillUsage: true,
167
183
  ...overrides,
168
184
  };
@@ -356,6 +372,45 @@ function syncOpenClawSource(
356
372
  };
357
373
  }
358
374
 
375
+ function syncPiSource(options: SyncOptions, onProgress?: SyncProgressCallback): SyncStepResult {
376
+ if (!existsSync(options.piSessionsDir)) {
377
+ return { available: false, scanned: 0, synced: 0, skipped: 0 };
378
+ }
379
+
380
+ onProgress?.("scanning Pi sessions...");
381
+ const sinceTs = options.since ? options.since.getTime() : null;
382
+ const allSessions = findPiSessions(options.piSessionsDir, sinceTs);
383
+ const skillNames = findPiSkillNames();
384
+ const alreadyIngested = options.force ? new Set<string>() : loadMarker(PI_INGEST_MARKER);
385
+ const pending = allSessions.filter((session) => !alreadyIngested.has(session.sessionId));
386
+ onProgress?.(`found ${allSessions.length} sessions, ${pending.length} pending`);
387
+ const newIngested = new Set<string>();
388
+ let synced = 0;
389
+ let skipped = 0;
390
+
391
+ for (const sessionFile of pending) {
392
+ const session = parsePiSession(sessionFile.filePath, skillNames);
393
+ if (!session.session_id || !session.timestamp) {
394
+ skipped += 1;
395
+ continue;
396
+ }
397
+ writePiSession(session, options.dryRun);
398
+ newIngested.add(sessionFile.sessionId);
399
+ synced += 1;
400
+ }
401
+
402
+ if (!options.dryRun && newIngested.size > 0) {
403
+ saveMarker(PI_INGEST_MARKER, new Set([...alreadyIngested, ...newIngested]));
404
+ }
405
+
406
+ return {
407
+ available: true,
408
+ scanned: allSessions.length,
409
+ synced,
410
+ skipped,
411
+ };
412
+ }
413
+
359
414
  function rebuildSkillUsageOverlay(
360
415
  options: SyncOptions,
361
416
  onProgress?: SyncProgressCallback,
@@ -445,6 +500,7 @@ export function syncSources(
445
500
  const runCodex = deps.syncCodex;
446
501
  const runOpenCode = deps.syncOpenCode;
447
502
  const runOpenClaw = deps.syncOpenClaw;
503
+ const runPi = deps.syncPi;
448
504
  const runRepair = deps.rebuildSkillUsage;
449
505
  const runCreatorContributions = deps.stageCreatorContributions;
450
506
  const db = getDb();
@@ -485,6 +541,10 @@ export function syncSources(
485
541
  )
486
542
  : disabledStep;
487
543
 
544
+ const pi = options.syncPi
545
+ ? timePhase("pi", () => (runPi ? runPi(options) : syncPiSource(options, onProgress)), timings)
546
+ : disabledStep;
547
+
488
548
  const repair = options.rebuildSkillUsage
489
549
  ? timePhase(
490
550
  "repair",
@@ -512,10 +572,10 @@ export function syncSources(
512
572
 
513
573
  const totalElapsed = Math.round(performance.now() - totalStart);
514
574
 
515
- return {
575
+ const syncResult: SyncResult = {
516
576
  since: options.since ? options.since.toISOString() : null,
517
577
  dry_run: options.dryRun,
518
- sources: { claude, codex, opencode, openclaw },
578
+ sources: { claude, codex, opencode, openclaw, pi },
519
579
  repair: {
520
580
  ran: options.rebuildSkillUsage,
521
581
  repaired_sessions: repair.repairedSessions,
@@ -526,6 +586,8 @@ export function syncSources(
526
586
  timings,
527
587
  total_elapsed_ms: totalElapsed,
528
588
  };
589
+
590
+ return syncResult;
529
591
  }
530
592
 
531
593
  function formatMs(ms: number): string {
@@ -549,6 +611,7 @@ export async function cliMain(): Promise<void> {
549
611
  "codex-home": { type: "string", default: DEFAULT_CODEX_HOME },
550
612
  "opencode-data-dir": { type: "string", default: DEFAULT_OPENCODE_DATA_DIR },
551
613
  "openclaw-agents-dir": { type: "string", default: OPENCLAW_AGENTS_DIR },
614
+ "pi-sessions-dir": { type: "string", default: PI_SESSIONS_DIR },
552
615
  "skill-log": { type: "string", default: SKILL_LOG },
553
616
  "repaired-skill-log": { type: "string", default: REPAIRED_SKILL_LOG },
554
617
  "repaired-sessions-marker": { type: "string", default: REPAIRED_SKILL_SESSIONS_MARKER },
@@ -559,6 +622,7 @@ export async function cliMain(): Promise<void> {
559
622
  "no-codex": { type: "boolean", default: false },
560
623
  "no-opencode": { type: "boolean", default: false },
561
624
  "no-openclaw": { type: "boolean", default: false },
625
+ "no-pi": { type: "boolean", default: false },
562
626
  "no-repair": { type: "boolean", default: false },
563
627
  json: { type: "boolean", default: false },
564
628
  help: { type: "boolean", short: "h", default: false },
@@ -577,6 +641,7 @@ Options:
577
641
  --codex-home <dir> Codex home directory (default: ~/.codex)
578
642
  --opencode-data-dir <dir> OpenCode data directory
579
643
  --openclaw-agents-dir <dir> OpenClaw agents directory
644
+ --pi-sessions-dir <dir> Pi sessions directory
580
645
  --skill-log <path> Raw skill usage log path
581
646
  --repaired-skill-log <path> Repaired overlay log path
582
647
  --repaired-sessions-marker <p> Repaired session marker path
@@ -587,6 +652,7 @@ Options:
587
652
  --no-codex Skip Codex rollout ingest
588
653
  --no-opencode Skip OpenCode ingest
589
654
  --no-openclaw Skip OpenClaw ingest
655
+ --no-pi Skip Pi ingest
590
656
  --no-repair Skip rebuilt skill-usage overlay
591
657
  --json Output raw JSON instead of human-readable summary
592
658
  -h, --help Show this help`);
@@ -622,27 +688,64 @@ Options:
622
688
  process.stderr.write(`selftune sync${flags.length ? ` ${flags.join(" ")}` : ""}\n`);
623
689
  }
624
690
 
625
- const result = syncSources(
626
- createDefaultSyncOptions({
627
- projectsDir: values["projects-dir"] ?? CLAUDE_CODE_PROJECTS_DIR,
628
- codexHome: values["codex-home"] ?? DEFAULT_CODEX_HOME,
629
- opencodeDataDir: values["opencode-data-dir"] ?? DEFAULT_OPENCODE_DATA_DIR,
630
- openclawAgentsDir: values["openclaw-agents-dir"] ?? OPENCLAW_AGENTS_DIR,
631
- skillLogPath: values["skill-log"] ?? SKILL_LOG,
632
- repairedSkillLogPath: values["repaired-skill-log"] ?? REPAIRED_SKILL_LOG,
633
- repairedSessionsPath: values["repaired-sessions-marker"] ?? REPAIRED_SKILL_SESSIONS_MARKER,
634
- since,
635
- dryRun: values["dry-run"] ?? false,
636
- force: values.force ?? false,
637
- syncClaude: !(values["no-claude"] ?? false),
638
- syncCodex: !(values["no-codex"] ?? false),
639
- syncOpenCode: !(values["no-opencode"] ?? false),
640
- syncOpenClaw: !(values["no-openclaw"] ?? false),
641
- rebuildSkillUsage: !(values["no-repair"] ?? false),
642
- }),
643
- {},
644
- onProgress,
645
- );
691
+ const syncStartedAt = new Date();
692
+ const syncStart = performance.now();
693
+ let result: SyncResult;
694
+ try {
695
+ result = syncSources(
696
+ createDefaultSyncOptions({
697
+ projectsDir: values["projects-dir"] ?? CLAUDE_CODE_PROJECTS_DIR,
698
+ codexHome: values["codex-home"] ?? DEFAULT_CODEX_HOME,
699
+ opencodeDataDir: values["opencode-data-dir"] ?? DEFAULT_OPENCODE_DATA_DIR,
700
+ openclawAgentsDir: values["openclaw-agents-dir"] ?? OPENCLAW_AGENTS_DIR,
701
+ piSessionsDir: values["pi-sessions-dir"] ?? PI_SESSIONS_DIR,
702
+ skillLogPath: values["skill-log"] ?? SKILL_LOG,
703
+ repairedSkillLogPath: values["repaired-skill-log"] ?? REPAIRED_SKILL_LOG,
704
+ repairedSessionsPath: values["repaired-sessions-marker"] ?? REPAIRED_SKILL_SESSIONS_MARKER,
705
+ since,
706
+ dryRun: values["dry-run"] ?? false,
707
+ force: values.force ?? false,
708
+ syncClaude: !(values["no-claude"] ?? false),
709
+ syncCodex: !(values["no-codex"] ?? false),
710
+ syncOpenCode: !(values["no-opencode"] ?? false),
711
+ syncOpenClaw: !(values["no-openclaw"] ?? false),
712
+ syncPi: !(values["no-pi"] ?? false),
713
+ rebuildSkillUsage: !(values["no-repair"] ?? false),
714
+ }),
715
+ {},
716
+ onProgress,
717
+ );
718
+ } catch (err) {
719
+ const syncElapsed = Math.round(performance.now() - syncStart);
720
+ const message = err instanceof Error ? err.message : String(err);
721
+ writeCronRunToDb(getDb(), {
722
+ jobName: "sync",
723
+ startedAt: syncStartedAt.toISOString(),
724
+ elapsedMs: syncElapsed,
725
+ status: "error",
726
+ error: message,
727
+ });
728
+ throw err;
729
+ }
730
+
731
+ // Log successful sync run to unified cron_runs timeline
732
+ const syncElapsed = Math.round(performance.now() - syncStart);
733
+ const s = result.sources;
734
+ writeCronRunToDb(getDb(), {
735
+ jobName: "sync",
736
+ startedAt: syncStartedAt.toISOString(),
737
+ elapsedMs: syncElapsed,
738
+ status: "success",
739
+ metrics: {
740
+ total_synced:
741
+ s.claude.synced + s.codex.synced + s.opencode.synced + s.openclaw.synced + s.pi.synced,
742
+ claude_synced: s.claude.synced,
743
+ codex_synced: s.codex.synced,
744
+ opencode_synced: s.opencode.synced,
745
+ openclaw_synced: s.openclaw.synced,
746
+ pi_synced: s.pi.synced,
747
+ },
748
+ });
646
749
 
647
750
  if (jsonOutput) {
648
751
  console.log(JSON.stringify(result, null, 2));
@@ -662,6 +765,7 @@ Options:
662
765
  process.stderr.write(
663
766
  `${formatStepLine("OpenClaw", result.sources.openclaw, timingMap.get("openclaw"))}\n`,
664
767
  );
768
+ process.stderr.write(`${formatStepLine("Pi", result.sources.pi, timingMap.get("pi"))}\n`);
665
769
 
666
770
  if (result.repair.ran) {
667
771
  const repairTiming = timingMap.get("repair");
@@ -34,7 +34,7 @@ export type AlphaLinkState =
34
34
  | "ready";
35
35
 
36
36
  export interface SelftuneConfig {
37
- agent_type: "claude_code" | "codex" | "opencode" | "openclaw" | "unknown";
37
+ agent_type: "claude_code" | "codex" | "opencode" | "openclaw" | "pi" | "unknown";
38
38
  cli_path: string;
39
39
  llm_mode: "agent";
40
40
  agent_cli: string | null;
@@ -738,6 +738,7 @@ export interface BodyValidationResult {
738
738
  before_pass_rate?: number;
739
739
  after_pass_rate?: number;
740
740
  per_entry_results?: RoutingReplayEntryResult[];
741
+ before_entry_results?: RoutingReplayEntryResult[];
741
742
  }
742
743
 
743
744
  /** Configuration for which LLM model a role should use. */
@@ -1,9 +1,8 @@
1
1
  /**
2
- * JSONL read/write/append utilities.
2
+ * JSONL read utilities and marker file helpers.
3
3
  */
4
4
 
5
5
  import {
6
- appendFileSync,
7
6
  closeSync,
8
7
  existsSync,
9
8
  fstatSync,
@@ -15,10 +14,6 @@ import {
15
14
  } from "node:fs";
16
15
  import { dirname } from "node:path";
17
16
 
18
- import { createLogger } from "./logging.js";
19
- import type { LogType } from "./schema-validator.js";
20
- import { validateRecord } from "./schema-validator.js";
21
-
22
17
  /**
23
18
  * Read a JSONL file and return parsed records.
24
19
  * Skips blank lines and lines that fail to parse.
@@ -86,30 +81,6 @@ export function readJsonlFrom<T = Record<string, unknown>>(
86
81
  }
87
82
  }
88
83
 
89
- /**
90
- * Append a single record to a JSONL file. Creates parent directories if needed.
91
- * When logType is provided, validates the record and logs warnings on failure
92
- * but still writes the record (fail-open: hooks must never block).
93
- *
94
- * @deprecated Phase 3: JSONL writes removed. Retained for materializer/test utilities only.
95
- */
96
- export function appendJsonl(path: string, record: unknown, logType?: LogType): void {
97
- if (logType) {
98
- const result = validateRecord(record, logType);
99
- if (!result.valid) {
100
- const logger = createLogger("jsonl");
101
- for (const error of result.errors) {
102
- logger.warn(`Validation warning for ${logType}: ${error}`);
103
- }
104
- }
105
- }
106
- const dir = dirname(path);
107
- if (!existsSync(dir)) {
108
- mkdirSync(dir, { recursive: true });
109
- }
110
- appendFileSync(path, `${JSON.stringify(record)}\n`, "utf-8");
111
- }
112
-
113
84
  /**
114
85
  * Load a marker file (JSON array of strings) for idempotent ingestion.
115
86
  */
@@ -263,6 +263,28 @@ export function classifySkillPath(
263
263
  return { skill_scope: "unknown" };
264
264
  }
265
265
 
266
+ const TEST_PATH_SEGMENTS = [
267
+ "/tests/",
268
+ "/__tests__/",
269
+ "/test/",
270
+ "/fixtures/",
271
+ "/sandbox/",
272
+ "/test-data/",
273
+ "/testdata/",
274
+ "/mock/",
275
+ "/mocks/",
276
+ ];
277
+
278
+ /**
279
+ * Check if a skill path is inside a test/fixture directory.
280
+ * Used to prevent test fixture skills from leaking into production data.
281
+ */
282
+ export function isTestFixturePath(skillPath: string): boolean {
283
+ if (!skillPath) return false;
284
+ const normalized = skillPath.toLowerCase();
285
+ return TEST_PATH_SEGMENTS.some((seg) => normalized.includes(seg));
286
+ }
287
+
266
288
  export function extractSkillNamesFromInstructions(
267
289
  text: string,
268
290
  knownSkillNames?: Iterable<string>,
@@ -7,7 +7,7 @@ import type { PushPayloadV2 } from "../src/schemas.js";
7
7
  export const evidenceOnlyPush: PushPayloadV2 = {
8
8
  schema_version: "2.0",
9
9
  client_version: "0.9.0",
10
- push_id: "d4e5f6a7-b8c9-0123-defa-234567890123",
10
+ push_id: "d4e5f6a7-b8c9-8123-9efa-234567890123",
11
11
  normalizer_version: "0.2.1",
12
12
  canonical: {
13
13
  sessions: [],
@@ -1,7 +1,6 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import { readFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
-
5
4
  import { CANONICAL_SCHEMA_VERSION } from "../src/types.js";
6
5
  import { isCanonicalRecord } from "../src/validators.js";
7
6
 
@@ -10,7 +10,7 @@ import type { PushPayloadV2 } from "../src/schemas.js";
10
10
  export const partialPushUnresolvedParents: PushPayloadV2 = {
11
11
  schema_version: "2.0",
12
12
  client_version: "0.9.0",
13
- push_id: "c3d4e5f6-a7b8-9012-cdef-123456789012",
13
+ push_id: "c3d4e5f6-a7b8-8012-8def-123456789012",
14
14
  normalizer_version: "0.2.1",
15
15
  canonical: {
16
16
  sessions: [],
@@ -13,9 +13,9 @@
13
13
  "type": "module",
14
14
  "exports": {
15
15
  ".": "./index.ts",
16
- "./schemas": "./src/schemas.ts",
17
16
  "./types": "./src/types.ts",
18
17
  "./validators": "./src/validators.ts",
18
+ "./schemas": "./src/schemas.ts",
19
19
  "./fixtures": "./fixtures/index.ts"
20
20
  },
21
21
  "dependencies": {
@@ -1,2 +1,3 @@
1
+ export * from "./schemas.js";
1
2
  export * from "./types.js";
2
3
  export * from "./validators.js";
@@ -1,5 +1,13 @@
1
- import { z } from "zod";
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
+ */
2
9
 
10
+ import { z } from "zod";
3
11
  import {
4
12
  CANONICAL_CAPTURE_MODES,
5
13
  CANONICAL_COMPLETION_STATUSES,
@@ -11,6 +19,8 @@ import {
11
19
  CANONICAL_SOURCE_SESSION_KINDS,
12
20
  } from "./types.js";
13
21
 
22
+ // ---------- Shared enum schemas ----------
23
+
14
24
  export const canonicalPlatformSchema = z.enum(CANONICAL_PLATFORMS);
15
25
  export const captureModeSchema = z.enum(CANONICAL_CAPTURE_MODES);
16
26
  export const sourceSessionKindSchema = z.enum(CANONICAL_SOURCE_SESSION_KINDS);
@@ -19,6 +29,8 @@ export const invocationModeSchema = z.enum(CANONICAL_INVOCATION_MODES);
19
29
  export const completionStatusSchema = z.enum(CANONICAL_COMPLETION_STATUSES);
20
30
  export const recordKindSchema = z.enum(CANONICAL_RECORD_KINDS);
21
31
 
32
+ // ---------- Shared structural schemas ----------
33
+
22
34
  export const rawSourceRefSchema = z.object({
23
35
  path: z.string().optional(),
24
36
  line: z.number().int().nonnegative().optional(),
@@ -42,6 +54,8 @@ export const canonicalSessionRecordBaseSchema = canonicalRecordBaseSchema.extend
42
54
  session_id: z.string().min(1),
43
55
  });
44
56
 
57
+ // ---------- Canonical record schemas ----------
58
+
45
59
  export const CanonicalSessionRecordSchema = canonicalSessionRecordBaseSchema.extend({
46
60
  record_kind: z.literal("session"),
47
61
  external_session_id: z.string().optional(),
@@ -137,6 +151,8 @@ export const CanonicalEvolutionEvidenceRecordSchema = z.object({
137
151
  raw_source_ref: rawSourceRefSchema.optional(),
138
152
  });
139
153
 
154
+ // ---------- Orchestrate run schemas ----------
155
+
140
156
  export const OrchestrateRunSkillActionSchema = z.object({
141
157
  skill: z.string().min(1),
142
158
  action: z.enum(["evolve", "watch", "skip"]),
@@ -163,12 +179,12 @@ export const PushOrchestrateRunRecordSchema = z.object({
163
179
  skill_actions: z.array(OrchestrateRunSkillActionSchema),
164
180
  });
165
181
 
182
+ // ---------- Push V2 envelope ----------
183
+
166
184
  export const PushPayloadV2Schema = z.object({
167
185
  schema_version: z.literal("2.0"),
168
186
  client_version: z.string().min(1),
169
- // Queue-generated push IDs are typically UUIDs, but the wire contract only
170
- // requires a stable non-empty idempotency key.
171
- push_id: z.string().min(1),
187
+ push_id: z.string().uuid(),
172
188
  normalizer_version: z.string().min(1),
173
189
  canonical: z.object({
174
190
  sessions: z.array(CanonicalSessionRecordSchema).min(0),
@@ -181,6 +197,8 @@ export const PushPayloadV2Schema = z.object({
181
197
  }),
182
198
  });
183
199
 
200
+ // ---------- Inferred types from Zod schemas ----------
201
+
184
202
  export type PushPayloadV2 = z.infer<typeof PushPayloadV2Schema>;
185
203
  export type ZodCanonicalSessionRecord = z.infer<typeof CanonicalSessionRecordSchema>;
186
204
  export type ZodCanonicalPromptRecord = z.infer<typeof CanonicalPromptRecordSchema>;
@@ -1,7 +1,7 @@
1
1
  export const CANONICAL_SCHEMA_VERSION = "2.0" as const;
2
2
  export type CanonicalSchemaVersion = typeof CANONICAL_SCHEMA_VERSION;
3
3
 
4
- export const CANONICAL_PLATFORMS = ["claude_code", "codex", "opencode", "openclaw"] as const;
4
+ export const CANONICAL_PLATFORMS = ["claude_code", "codex", "opencode", "openclaw", "pi"] as const;
5
5
  export type CanonicalPlatform = (typeof CANONICAL_PLATFORMS)[number];
6
6
 
7
7
  export const CANONICAL_CAPTURE_MODES = [
@@ -143,18 +143,7 @@ export interface CanonicalExecutionFactRecord extends CanonicalSessionRecordBase
143
143
  errors_encountered: number;
144
144
  input_tokens?: number;
145
145
  output_tokens?: number;
146
- cached_input_tokens?: number;
147
- reasoning_output_tokens?: number;
148
- cost_usd?: number;
149
146
  duration_ms?: number;
150
- files_changed?: number;
151
- lines_added?: number;
152
- lines_removed?: number;
153
- lines_modified?: number;
154
- /** Count of output-producing tool calls (Write, Edit, WebFetch, WebSearch, Skill, Agent). */
155
- artifact_count?: number;
156
- /** Inferred session type based on tool distribution. */
157
- session_type?: "dev" | "research" | "content" | "mixed";
158
147
  completion_status?: CanonicalCompletionStatus;
159
148
  end_reason?: string;
160
149
  }
@@ -1,5 +1,4 @@
1
1
  import { describe, expect, test } from "bun:test";
2
-
3
2
  import { completePush } from "../fixtures/complete-push.js";
4
3
  import { evidenceOnlyPush } from "../fixtures/evidence-only-push.js";
5
4
  import { partialPushNoSessions } from "../fixtures/partial-push-no-sessions.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "selftune",
3
- "version": "0.2.22",
3
+ "version": "0.2.23",
4
4
  "description": "Self-improving skills CLI for AI agents",
5
5
  "keywords": [
6
6
  "agent",
@@ -7,7 +7,7 @@ import type { PushPayloadV2 } from "../src/schemas.js";
7
7
  export const evidenceOnlyPush: PushPayloadV2 = {
8
8
  schema_version: "2.0",
9
9
  client_version: "0.9.0",
10
- push_id: "d4e5f6a7-b8c9-0123-defa-234567890123",
10
+ push_id: "d4e5f6a7-b8c9-8123-9efa-234567890123",
11
11
  normalizer_version: "0.2.1",
12
12
  canonical: {
13
13
  sessions: [],
@@ -1,7 +1,6 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import { readFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
-
5
4
  import { CANONICAL_SCHEMA_VERSION } from "../src/types.js";
6
5
  import { isCanonicalRecord } from "../src/validators.js";
7
6
 
@@ -10,7 +10,7 @@ import type { PushPayloadV2 } from "../src/schemas.js";
10
10
  export const partialPushUnresolvedParents: PushPayloadV2 = {
11
11
  schema_version: "2.0",
12
12
  client_version: "0.9.0",
13
- push_id: "c3d4e5f6-a7b8-9012-cdef-123456789012",
13
+ push_id: "c3d4e5f6-a7b8-8012-8def-123456789012",
14
14
  normalizer_version: "0.2.1",
15
15
  canonical: {
16
16
  sessions: [],
@@ -13,9 +13,9 @@
13
13
  "type": "module",
14
14
  "exports": {
15
15
  ".": "./index.ts",
16
- "./schemas": "./src/schemas.ts",
17
16
  "./types": "./src/types.ts",
18
17
  "./validators": "./src/validators.ts",
18
+ "./schemas": "./src/schemas.ts",
19
19
  "./fixtures": "./fixtures/index.ts"
20
20
  },
21
21
  "dependencies": {
@@ -1,2 +1,3 @@
1
+ export * from "./schemas.js";
1
2
  export * from "./types.js";
2
3
  export * from "./validators.js";
@@ -1,5 +1,13 @@
1
- import { z } from "zod";
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
+ */
2
9
 
10
+ import { z } from "zod";
3
11
  import {
4
12
  CANONICAL_CAPTURE_MODES,
5
13
  CANONICAL_COMPLETION_STATUSES,
@@ -11,6 +19,8 @@ import {
11
19
  CANONICAL_SOURCE_SESSION_KINDS,
12
20
  } from "./types.js";
13
21
 
22
+ // ---------- Shared enum schemas ----------
23
+
14
24
  export const canonicalPlatformSchema = z.enum(CANONICAL_PLATFORMS);
15
25
  export const captureModeSchema = z.enum(CANONICAL_CAPTURE_MODES);
16
26
  export const sourceSessionKindSchema = z.enum(CANONICAL_SOURCE_SESSION_KINDS);
@@ -19,6 +29,8 @@ export const invocationModeSchema = z.enum(CANONICAL_INVOCATION_MODES);
19
29
  export const completionStatusSchema = z.enum(CANONICAL_COMPLETION_STATUSES);
20
30
  export const recordKindSchema = z.enum(CANONICAL_RECORD_KINDS);
21
31
 
32
+ // ---------- Shared structural schemas ----------
33
+
22
34
  export const rawSourceRefSchema = z.object({
23
35
  path: z.string().optional(),
24
36
  line: z.number().int().nonnegative().optional(),
@@ -42,6 +54,8 @@ export const canonicalSessionRecordBaseSchema = canonicalRecordBaseSchema.extend
42
54
  session_id: z.string().min(1),
43
55
  });
44
56
 
57
+ // ---------- Canonical record schemas ----------
58
+
45
59
  export const CanonicalSessionRecordSchema = canonicalSessionRecordBaseSchema.extend({
46
60
  record_kind: z.literal("session"),
47
61
  external_session_id: z.string().optional(),
@@ -137,6 +151,8 @@ export const CanonicalEvolutionEvidenceRecordSchema = z.object({
137
151
  raw_source_ref: rawSourceRefSchema.optional(),
138
152
  });
139
153
 
154
+ // ---------- Orchestrate run schemas ----------
155
+
140
156
  export const OrchestrateRunSkillActionSchema = z.object({
141
157
  skill: z.string().min(1),
142
158
  action: z.enum(["evolve", "watch", "skip"]),
@@ -163,12 +179,12 @@ export const PushOrchestrateRunRecordSchema = z.object({
163
179
  skill_actions: z.array(OrchestrateRunSkillActionSchema),
164
180
  });
165
181
 
182
+ // ---------- Push V2 envelope ----------
183
+
166
184
  export const PushPayloadV2Schema = z.object({
167
185
  schema_version: z.literal("2.0"),
168
186
  client_version: z.string().min(1),
169
- // Queue-generated push IDs are typically UUIDs, but the wire contract only
170
- // requires a stable non-empty idempotency key.
171
- push_id: z.string().min(1),
187
+ push_id: z.string().uuid(),
172
188
  normalizer_version: z.string().min(1),
173
189
  canonical: z.object({
174
190
  sessions: z.array(CanonicalSessionRecordSchema).min(0),
@@ -181,6 +197,8 @@ export const PushPayloadV2Schema = z.object({
181
197
  }),
182
198
  });
183
199
 
200
+ // ---------- Inferred types from Zod schemas ----------
201
+
184
202
  export type PushPayloadV2 = z.infer<typeof PushPayloadV2Schema>;
185
203
  export type ZodCanonicalSessionRecord = z.infer<typeof CanonicalSessionRecordSchema>;
186
204
  export type ZodCanonicalPromptRecord = z.infer<typeof CanonicalPromptRecordSchema>;