selftune 0.2.23 → 0.2.25
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/CHANGELOG.md +6 -0
- package/README.md +93 -15
- package/apps/local-dashboard/dist/assets/index-DgY2KGP-.css +1 -0
- package/apps/local-dashboard/dist/assets/index-Dhgv5BQO.js +15 -0
- package/apps/local-dashboard/dist/assets/vendor-react-C5oyHiV1.js +11 -0
- package/apps/local-dashboard/dist/assets/{vendor-table-BIiI3YhS.js → vendor-table-Bc_bbKd8.js} +1 -1
- package/apps/local-dashboard/dist/assets/vendor-ui-B3BPIYy7.js +1 -0
- package/apps/local-dashboard/dist/index.html +5 -5
- package/cli/selftune/adapters/codex/install.ts +310 -78
- package/cli/selftune/adapters/opencode/install.ts +3 -4
- package/cli/selftune/alpha-upload/build-payloads.ts +3 -3
- package/cli/selftune/alpha-upload/stage-canonical.ts +17 -11
- package/cli/selftune/auto-update.ts +200 -8
- package/cli/selftune/canonical-export.ts +55 -25
- package/cli/selftune/command-surface.ts +397 -0
- package/cli/selftune/contribute/contribute.ts +64 -13
- package/cli/selftune/contribution-config.ts +57 -3
- package/cli/selftune/contribution-preferences.ts +117 -0
- package/cli/selftune/contribution-signals.ts +8 -4
- package/cli/selftune/contribution-staging.ts +13 -2
- package/cli/selftune/contributions.ts +55 -121
- package/cli/selftune/creator-contributions.ts +29 -10
- package/cli/selftune/cron/setup.ts +7 -3
- package/cli/selftune/dashboard-contract.ts +73 -0
- package/cli/selftune/dashboard-server.ts +168 -17
- package/cli/selftune/dashboard.ts +350 -17
- package/cli/selftune/eval/baseline.ts +21 -5
- package/cli/selftune/eval/execution-eval.ts +170 -0
- package/cli/selftune/eval/family-overlap.ts +2 -2
- package/cli/selftune/eval/hooks-to-evals.ts +228 -82
- package/cli/selftune/eval/import-skillsbench.ts +2 -2
- package/cli/selftune/eval/invocation-classifier.ts +56 -0
- package/cli/selftune/eval/synthetic-evals.ts +5 -3
- package/cli/selftune/eval/unit-test-cli.ts +7 -4
- package/cli/selftune/evolution/apply-proposal.ts +295 -0
- package/cli/selftune/evolution/engines/replay-engine.ts +79 -57
- package/cli/selftune/evolution/evolve-body.ts +100 -39
- package/cli/selftune/evolution/evolve.ts +244 -52
- package/cli/selftune/evolution/rollback.ts +0 -1
- package/cli/selftune/evolution/validate-body.ts +68 -42
- package/cli/selftune/evolution/validate-host-replay.ts +510 -60
- package/cli/selftune/evolution/validate-proposal.ts +11 -150
- package/cli/selftune/evolution/validate-routing.ts +43 -41
- package/cli/selftune/evolution/validation-contract.ts +91 -0
- package/cli/selftune/grading/auto-grade.ts +11 -7
- package/cli/selftune/grading/grade-session.ts +10 -16
- package/cli/selftune/index.ts +35 -10
- package/cli/selftune/ingestors/claude-replay.ts +15 -10
- package/cli/selftune/ingestors/codex-wrapper.ts +3 -3
- package/cli/selftune/ingestors/opencode-ingest.ts +2 -2
- package/cli/selftune/ingestors/pi-ingest.ts +3 -2
- package/cli/selftune/init.ts +27 -3
- package/cli/selftune/localdb/direct-write.ts +35 -1
- package/cli/selftune/localdb/queries/cron.ts +34 -0
- package/cli/selftune/localdb/queries/dashboard.ts +834 -0
- package/cli/selftune/localdb/queries/evolution.ts +158 -0
- package/cli/selftune/localdb/queries/execution.ts +133 -0
- package/cli/selftune/localdb/queries/json.ts +18 -0
- package/cli/selftune/localdb/queries/monitoring.ts +263 -0
- package/cli/selftune/localdb/queries/raw.ts +95 -0
- package/cli/selftune/localdb/queries/staging.ts +270 -0
- package/cli/selftune/localdb/queries/trust.ts +392 -0
- package/cli/selftune/localdb/queries.ts +60 -2288
- package/cli/selftune/localdb/schema.ts +21 -0
- package/cli/selftune/monitoring/watch.ts +96 -29
- package/cli/selftune/normalization.ts +3 -0
- package/cli/selftune/observability.ts +4 -2
- package/cli/selftune/orchestrate/cli.ts +161 -0
- package/cli/selftune/orchestrate/execute.ts +295 -0
- package/cli/selftune/orchestrate/finalize.ts +157 -0
- package/cli/selftune/orchestrate/locks.ts +40 -0
- package/cli/selftune/orchestrate/plan.ts +131 -0
- package/cli/selftune/orchestrate/post-run.ts +59 -0
- package/cli/selftune/orchestrate/prepare.ts +334 -0
- package/cli/selftune/orchestrate/report.ts +182 -0
- package/cli/selftune/orchestrate/runtime.ts +120 -0
- package/cli/selftune/orchestrate/signals.ts +48 -0
- package/cli/selftune/orchestrate.ts +150 -1173
- package/cli/selftune/repair/skill-usage.ts +5 -2
- package/cli/selftune/routes/overview.ts +5 -2
- package/cli/selftune/routes/skill-report.ts +15 -2
- package/cli/selftune/schedule.ts +5 -5
- package/cli/selftune/status.ts +39 -2
- package/cli/selftune/testing-readiness.ts +597 -0
- package/cli/selftune/types.ts +44 -4
- package/cli/selftune/uninstall.ts +2 -1
- package/cli/selftune/utils/canonical-log.ts +1 -9
- package/cli/selftune/utils/cli-error.ts +9 -0
- package/cli/selftune/utils/llm-call.ts +126 -6
- package/cli/selftune/utils/skill-discovery.ts +2 -0
- package/cli/selftune/workflows/proposals.ts +184 -0
- package/cli/selftune/workflows/skill-scaffold.ts +241 -0
- package/cli/selftune/workflows/workflows.ts +100 -26
- package/node_modules/@selftune/telemetry-contract/fixtures/complete-push.ts +1 -1
- package/node_modules/@selftune/telemetry-contract/fixtures/evidence-only-push.ts +1 -1
- package/node_modules/@selftune/telemetry-contract/fixtures/partial-push-no-sessions.ts +1 -1
- package/node_modules/@selftune/telemetry-contract/fixtures/partial-push-unresolved-parents.ts +1 -1
- package/node_modules/@selftune/telemetry-contract/src/schemas.ts +41 -1
- package/node_modules/@selftune/telemetry-contract/src/types.ts +103 -2
- package/package.json +25 -9
- package/packages/dashboard-core/AGENTS.md +18 -0
- package/packages/dashboard-core/README.md +30 -0
- package/packages/dashboard-core/index.ts +3 -0
- package/packages/dashboard-core/package.json +39 -0
- package/packages/dashboard-core/src/chrome/DashboardChrome.tsx +74 -0
- package/packages/dashboard-core/src/chrome/DashboardHeader.tsx +200 -0
- package/packages/dashboard-core/src/chrome/DashboardSidebar.tsx +219 -0
- package/packages/dashboard-core/src/chrome/RuntimeBadge.tsx +46 -0
- package/packages/dashboard-core/src/chrome/index.ts +14 -0
- package/packages/dashboard-core/src/chrome/types.ts +81 -0
- package/packages/dashboard-core/src/chrome/utils.ts +23 -0
- package/packages/dashboard-core/src/gates/FeatureGate.tsx +11 -0
- package/packages/dashboard-core/src/gates/LockedRoute.tsx +29 -0
- package/packages/dashboard-core/src/gates/UpgradeCard.tsx +89 -0
- package/packages/dashboard-core/src/gates/index.ts +3 -0
- package/packages/dashboard-core/src/host/DashboardHostProvider.tsx +62 -0
- package/packages/dashboard-core/src/host/adapter.ts +47 -0
- package/packages/dashboard-core/src/host/capabilities.ts +55 -0
- package/packages/dashboard-core/src/host/index.ts +3 -0
- package/packages/dashboard-core/src/models/analytics.ts +39 -0
- package/packages/dashboard-core/src/models/index.ts +4 -0
- package/packages/dashboard-core/src/models/overview.ts +98 -0
- package/packages/dashboard-core/src/models/runtime.ts +7 -0
- package/packages/dashboard-core/src/models/skills.ts +34 -0
- package/packages/dashboard-core/src/routes/index.ts +2 -0
- package/packages/dashboard-core/src/routes/manifest.test.ts +70 -0
- package/packages/dashboard-core/src/routes/manifest.ts +451 -0
- package/packages/dashboard-core/src/routes/types.ts +39 -0
- package/packages/dashboard-core/src/screens/analytics/AnalyticsScreen.tsx +278 -0
- package/packages/dashboard-core/src/screens/analytics/index.ts +1 -0
- package/packages/dashboard-core/src/screens/index.ts +37 -0
- package/packages/dashboard-core/src/screens/overview/OverviewComparisonSurface.test.ts +101 -0
- package/packages/dashboard-core/src/screens/overview/OverviewComparisonSurface.tsx +393 -0
- package/packages/dashboard-core/src/screens/overview/OverviewCompositionSurface.test.tsx +113 -0
- package/packages/dashboard-core/src/screens/overview/OverviewCompositionSurface.tsx +72 -0
- package/packages/dashboard-core/src/screens/overview/OverviewCoreSurface.tsx +71 -0
- package/packages/dashboard-core/src/screens/overview/OverviewOnboardingBanner.tsx +90 -0
- package/packages/dashboard-core/src/screens/overview/OverviewRunSummary.tsx +40 -0
- package/packages/dashboard-core/src/screens/overview/index.ts +16 -0
- package/packages/dashboard-core/src/screens/overview/types.ts +13 -0
- package/packages/dashboard-core/src/screens/skill-report/SkillReportDailyBreakdownSection.tsx +99 -0
- package/packages/dashboard-core/src/screens/skill-report/SkillReportDataQualityTabContent.tsx +35 -0
- package/packages/dashboard-core/src/screens/skill-report/SkillReportEvidenceRail.tsx +71 -0
- package/packages/dashboard-core/src/screens/skill-report/SkillReportEvidenceSection.tsx +63 -0
- package/packages/dashboard-core/src/screens/skill-report/SkillReportEvidenceTabContent.tsx +25 -0
- package/packages/dashboard-core/src/screens/skill-report/SkillReportInvocationsSection.tsx +24 -0
- package/packages/dashboard-core/src/screens/skill-report/SkillReportMissedQueriesSection.tsx +79 -0
- package/packages/dashboard-core/src/screens/skill-report/SkillReportScaffold.tsx +150 -0
- package/packages/dashboard-core/src/screens/skill-report/SkillReportSections.test.tsx +224 -0
- package/packages/dashboard-core/src/screens/skill-report/SkillReportTabs.test.tsx +76 -0
- package/packages/dashboard-core/src/screens/skill-report/SkillReportTabs.tsx +88 -0
- package/packages/dashboard-core/src/screens/skill-report/SkillReportTrendSection.tsx +33 -0
- package/packages/dashboard-core/src/screens/skill-report/SkillReportTrustBadge.tsx +67 -0
- package/packages/dashboard-core/src/screens/skill-report/index.ts +45 -0
- package/packages/dashboard-core/src/screens/skills/SkillsLibraryScreen.tsx +162 -0
- package/packages/dashboard-core/src/screens/skills/index.ts +6 -0
- package/packages/telemetry-contract/fixtures/complete-push.ts +1 -1
- package/packages/telemetry-contract/fixtures/evidence-only-push.ts +1 -1
- package/packages/telemetry-contract/fixtures/partial-push-no-sessions.ts +1 -1
- package/packages/telemetry-contract/fixtures/partial-push-unresolved-parents.ts +1 -1
- package/packages/telemetry-contract/src/schemas.ts +41 -1
- package/packages/telemetry-contract/src/types.ts +103 -2
- package/packages/ui/src/components/EvidenceViewer.tsx +80 -25
- package/packages/ui/src/components/OverviewPanels.tsx +67 -26
- package/packages/ui/src/primitives/tabs.tsx +7 -6
- package/packages/ui/src/types.ts +10 -0
- package/skill/SKILL.md +130 -332
- package/skill/agents/diagnosis-analyst.md +3 -3
- package/skill/agents/evolution-reviewer.md +3 -3
- package/skill/agents/integration-guide.md +3 -3
- package/skill/agents/pattern-analyst.md +2 -2
- package/skill/references/cli-quick-reference.md +89 -0
- package/skill/references/creator-playbook.md +131 -0
- package/skill/references/examples.md +48 -0
- package/skill/references/troubleshooting.md +47 -0
- package/skill/references/version-history.md +1 -1
- package/skill/selftune.contribute.json +11 -0
- package/skill/{Workflows → workflows}/Baseline.md +20 -1
- package/skill/{Workflows → workflows}/Contribute.md +23 -10
- package/skill/{Workflows → workflows}/Contributions.md +13 -5
- package/skill/workflows/CreateTestDeploy.md +170 -0
- package/skill/{Workflows → workflows}/CreatorContributions.md +18 -6
- package/skill/{Workflows → workflows}/Cron.md +1 -1
- package/skill/{Workflows → workflows}/Dashboard.md +20 -0
- package/skill/{Workflows → workflows}/Doctor.md +1 -1
- package/skill/{Workflows → workflows}/Evals.md +67 -2
- package/skill/{Workflows → workflows}/Evolve.md +119 -30
- package/skill/{Workflows → workflows}/EvolveBody.md +41 -1
- package/skill/{Workflows → workflows}/Grade.md +1 -1
- package/skill/{Workflows → workflows}/Initialize.md +8 -4
- package/skill/{Workflows → workflows}/Orchestrate.md +13 -3
- package/skill/{Workflows → workflows}/Schedule.md +3 -3
- package/skill/workflows/SignalsDashboard.md +87 -0
- package/skill/{Workflows → workflows}/UnitTest.md +19 -0
- package/skill/{Workflows → workflows}/Watch.md +42 -2
- package/skill/{Workflows → workflows}/Workflows.md +39 -2
- package/apps/local-dashboard/dist/assets/index-CwOtTrUS.css +0 -1
- package/apps/local-dashboard/dist/assets/index-f1HQpbeH.js +0 -59
- package/apps/local-dashboard/dist/assets/vendor-react-CKkiCskZ.js +0 -11
- package/apps/local-dashboard/dist/assets/vendor-ui-jVSaIZey.js +0 -12
- /package/skill/{Workflows → workflows}/AlphaUpload.md +0 -0
- /package/skill/{Workflows → workflows}/AutoActivation.md +0 -0
- /package/skill/{Workflows → workflows}/Badge.md +0 -0
- /package/skill/{Workflows → workflows}/Composability.md +0 -0
- /package/skill/{Workflows → workflows}/EvolutionMemory.md +0 -0
- /package/skill/{Workflows → workflows}/ExportCanonical.md +0 -0
- /package/skill/{Workflows → workflows}/Hook.md +0 -0
- /package/skill/{Workflows → workflows}/ImportSkillsBench.md +0 -0
- /package/skill/{Workflows → workflows}/Ingest.md +0 -0
- /package/skill/{Workflows → workflows}/PlatformHooks.md +0 -0
- /package/skill/{Workflows → workflows}/Quickstart.md +0 -0
- /package/skill/{Workflows → workflows}/Recover.md +0 -0
- /package/skill/{Workflows → workflows}/Registry.md +0 -0
- /package/skill/{Workflows → workflows}/RepairSkillUsage.md +0 -0
- /package/skill/{Workflows → workflows}/Replay.md +0 -0
- /package/skill/{Workflows → workflows}/Rollback.md +0 -0
- /package/skill/{Workflows → workflows}/Sync.md +0 -0
- /package/skill/{Workflows → workflows}/Telemetry.md +0 -0
- /package/skill/{Workflows → workflows}/Uninstall.md +0 -0
|
@@ -19,18 +19,30 @@ import { join } from "node:path";
|
|
|
19
19
|
// Types
|
|
20
20
|
// ---------------------------------------------------------------------------
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
matchers?: string[];
|
|
27
|
-
/** Marker field so selftune can identify its own hooks. */
|
|
22
|
+
type CodexHookEvent = "PreToolUse" | "PostToolUse" | "SessionStart" | "UserPromptSubmit" | "Stop";
|
|
23
|
+
|
|
24
|
+
type CodexHookHandler = Record<string, unknown> & {
|
|
25
|
+
command?: string;
|
|
28
26
|
_selftune?: boolean;
|
|
29
|
-
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type CodexMatcherGroup = Record<string, unknown> & {
|
|
30
|
+
hooks: CodexHookHandler[];
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type CodexHooksByEvent = Record<string, CodexMatcherGroup[]>;
|
|
34
|
+
|
|
35
|
+
type LegacyCodexHookEntry = Record<string, unknown> & {
|
|
36
|
+
event?: unknown;
|
|
37
|
+
command?: unknown;
|
|
38
|
+
timeout_ms?: unknown;
|
|
39
|
+
matchers?: unknown;
|
|
40
|
+
_selftune?: unknown;
|
|
41
|
+
};
|
|
30
42
|
|
|
31
|
-
interface
|
|
32
|
-
|
|
33
|
-
|
|
43
|
+
interface ParsedCodexHooksFile {
|
|
44
|
+
hooksByEvent: CodexHooksByEvent;
|
|
45
|
+
otherFields: Record<string, unknown>;
|
|
34
46
|
}
|
|
35
47
|
|
|
36
48
|
// ---------------------------------------------------------------------------
|
|
@@ -39,40 +51,64 @@ interface CodexHooksFile {
|
|
|
39
51
|
|
|
40
52
|
const DEFAULT_CODEX_HOME = join(homedir(), ".codex");
|
|
41
53
|
const HOOKS_FILENAME = "hooks.json";
|
|
42
|
-
const
|
|
43
|
-
const
|
|
54
|
+
const DEFAULT_TIMEOUT_SEC = 10;
|
|
55
|
+
const SESSION_TIMEOUT_SEC = 30;
|
|
44
56
|
|
|
45
57
|
/** The command Codex will run for each hook event. */
|
|
46
58
|
const HOOK_COMMAND =
|
|
47
59
|
'bash -c \'if [ -n "$SELFTUNE_CLI_PATH" ]; then exec "$SELFTUNE_CLI_PATH" codex hook; else exec npx -y selftune@latest codex hook; fi\'';
|
|
48
60
|
|
|
49
61
|
/** Hook entries selftune installs into Codex. */
|
|
50
|
-
const SELFTUNE_HOOKS:
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
62
|
+
const SELFTUNE_HOOKS: Record<Exclude<CodexHookEvent, "UserPromptSubmit">, CodexMatcherGroup[]> = {
|
|
63
|
+
SessionStart: [
|
|
64
|
+
{
|
|
65
|
+
hooks: [
|
|
66
|
+
{
|
|
67
|
+
type: "command",
|
|
68
|
+
command: HOOK_COMMAND,
|
|
69
|
+
timeout: SESSION_TIMEOUT_SEC,
|
|
70
|
+
_selftune: true,
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
PreToolUse: [
|
|
76
|
+
{
|
|
77
|
+
hooks: [
|
|
78
|
+
{
|
|
79
|
+
type: "command",
|
|
80
|
+
command: HOOK_COMMAND,
|
|
81
|
+
timeout: DEFAULT_TIMEOUT_SEC,
|
|
82
|
+
_selftune: true,
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
PostToolUse: [
|
|
88
|
+
{
|
|
89
|
+
hooks: [
|
|
90
|
+
{
|
|
91
|
+
type: "command",
|
|
92
|
+
command: HOOK_COMMAND,
|
|
93
|
+
timeout: DEFAULT_TIMEOUT_SEC,
|
|
94
|
+
_selftune: true,
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
Stop: [
|
|
100
|
+
{
|
|
101
|
+
hooks: [
|
|
102
|
+
{
|
|
103
|
+
type: "command",
|
|
104
|
+
command: HOOK_COMMAND,
|
|
105
|
+
timeout: SESSION_TIMEOUT_SEC,
|
|
106
|
+
_selftune: true,
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
};
|
|
76
112
|
|
|
77
113
|
// ---------------------------------------------------------------------------
|
|
78
114
|
// Helpers
|
|
@@ -87,51 +123,226 @@ function getCodexHome(): string {
|
|
|
87
123
|
return process.env.CODEX_HOME ?? DEFAULT_CODEX_HOME;
|
|
88
124
|
}
|
|
89
125
|
|
|
126
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
127
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function cloneHooksByEvent(hooksByEvent: CodexHooksByEvent): CodexHooksByEvent {
|
|
131
|
+
return Object.fromEntries(
|
|
132
|
+
Object.entries(hooksByEvent).map(([eventName, groups]) => [
|
|
133
|
+
eventName,
|
|
134
|
+
groups.map((group) => ({
|
|
135
|
+
...group,
|
|
136
|
+
hooks: group.hooks.map((handler) => ({ ...handler })),
|
|
137
|
+
})),
|
|
138
|
+
]),
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function normalizeMatcherGroup(
|
|
143
|
+
value: unknown,
|
|
144
|
+
eventName: string,
|
|
145
|
+
index: number,
|
|
146
|
+
): CodexMatcherGroup {
|
|
147
|
+
if (!isRecord(value)) {
|
|
148
|
+
throw new Error(`Invalid Codex hooks file: hooks.${eventName}[${index}] must be an object`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!Array.isArray(value.hooks)) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
`Invalid Codex hooks file: hooks.${eventName}[${index}].hooks must be an array`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
...value,
|
|
159
|
+
hooks: value.hooks.map((handler, handlerIndex) => {
|
|
160
|
+
if (!isRecord(handler)) {
|
|
161
|
+
throw new Error(
|
|
162
|
+
`Invalid Codex hooks file: hooks.${eventName}[${index}].hooks[${handlerIndex}] must be an object`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
return { ...handler };
|
|
166
|
+
}),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function normalizeEventMapHooks(value: unknown): CodexHooksByEvent {
|
|
171
|
+
if (!isRecord(value)) {
|
|
172
|
+
throw new Error(`Invalid Codex hooks file: "hooks" must be an object or legacy array`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const hooksByEvent: CodexHooksByEvent = {};
|
|
176
|
+
for (const [eventName, groups] of Object.entries(value)) {
|
|
177
|
+
if (!Array.isArray(groups)) {
|
|
178
|
+
throw new Error(`Invalid Codex hooks file: hooks.${eventName} must be an array`);
|
|
179
|
+
}
|
|
180
|
+
hooksByEvent[eventName] = groups.map((group, index) =>
|
|
181
|
+
normalizeMatcherGroup(group, eventName, index),
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
return hooksByEvent;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function convertLegacyHooks(entries: unknown[]): CodexHooksByEvent {
|
|
188
|
+
const hooksByEvent: CodexHooksByEvent = {};
|
|
189
|
+
|
|
190
|
+
for (const [index, entry] of entries.entries()) {
|
|
191
|
+
if (!isRecord(entry) || typeof entry.event !== "string" || typeof entry.command !== "string") {
|
|
192
|
+
throw new Error(
|
|
193
|
+
`Invalid Codex hooks file: legacy hooks[${index}] must include string event and command`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const legacyEntry = entry as LegacyCodexHookEntry;
|
|
198
|
+
const handler: CodexHookHandler = {
|
|
199
|
+
type: "command",
|
|
200
|
+
command: legacyEntry.command as string,
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
if (typeof legacyEntry.timeout_ms === "number" && Number.isFinite(legacyEntry.timeout_ms)) {
|
|
204
|
+
handler.timeout = Math.max(1, Math.ceil((legacyEntry.timeout_ms as number) / 1000));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (legacyEntry._selftune === true) {
|
|
208
|
+
handler._selftune = true;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const matchers =
|
|
212
|
+
Array.isArray(legacyEntry.matchers) &&
|
|
213
|
+
legacyEntry.matchers.every((matcher) => typeof matcher === "string")
|
|
214
|
+
? (legacyEntry.matchers as string[])
|
|
215
|
+
: [];
|
|
216
|
+
|
|
217
|
+
const groups = hooksByEvent[legacyEntry.event as string] ?? [];
|
|
218
|
+
if (matchers.length === 0) {
|
|
219
|
+
groups.push({ hooks: [{ ...handler }] });
|
|
220
|
+
} else {
|
|
221
|
+
for (const matcher of matchers) {
|
|
222
|
+
groups.push({ matcher, hooks: [{ ...handler }] });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
hooksByEvent[legacyEntry.event as string] = groups;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return hooksByEvent;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function serializeHooksByEvent(hooksByEvent: CodexHooksByEvent): CodexHooksByEvent {
|
|
232
|
+
return Object.fromEntries(
|
|
233
|
+
Object.entries(hooksByEvent).map(([eventName, groups]) => [
|
|
234
|
+
eventName,
|
|
235
|
+
groups.map((group) => {
|
|
236
|
+
const { hooks, ...rest } = group;
|
|
237
|
+
return {
|
|
238
|
+
...rest,
|
|
239
|
+
hooks: hooks.map((handler) => {
|
|
240
|
+
const { _selftune, ...serialized } = handler;
|
|
241
|
+
return serialized;
|
|
242
|
+
}),
|
|
243
|
+
};
|
|
244
|
+
}),
|
|
245
|
+
]),
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
90
249
|
/** Read and parse existing hooks.json, or return empty structure. */
|
|
91
|
-
function readHooksFile(path: string):
|
|
92
|
-
if (!existsSync(path)) return {
|
|
250
|
+
function readHooksFile(path: string): ParsedCodexHooksFile {
|
|
251
|
+
if (!existsSync(path)) return { hooksByEvent: {}, otherFields: {} };
|
|
93
252
|
try {
|
|
94
253
|
const raw = readFileSync(path, "utf-8").trim();
|
|
95
|
-
if (!raw) return {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
254
|
+
if (!raw) return { hooksByEvent: {}, otherFields: {} };
|
|
255
|
+
|
|
256
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
257
|
+
if (!isRecord(parsed)) {
|
|
258
|
+
throw new Error(`Invalid Codex hooks file: root must be an object`);
|
|
99
259
|
}
|
|
100
|
-
|
|
101
|
-
|
|
260
|
+
|
|
261
|
+
const { hooks, ...otherFields } = parsed;
|
|
262
|
+
if (hooks === undefined) {
|
|
263
|
+
return { hooksByEvent: {}, otherFields };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (Array.isArray(hooks)) {
|
|
267
|
+
return { hooksByEvent: convertLegacyHooks(hooks), otherFields };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return { hooksByEvent: normalizeEventMapHooks(hooks), otherFields };
|
|
102
271
|
} catch (err) {
|
|
103
|
-
throw new Error(
|
|
272
|
+
throw new Error(
|
|
273
|
+
`Failed to parse ${path}: ${err instanceof Error ? err.message : String(err)}`,
|
|
274
|
+
{
|
|
275
|
+
cause: err,
|
|
276
|
+
},
|
|
277
|
+
);
|
|
104
278
|
}
|
|
105
279
|
}
|
|
106
280
|
|
|
107
281
|
/** Legacy command strings that identify selftune-installed hooks (before the _selftune marker). */
|
|
108
|
-
const LEGACY_SELFTUNE_COMMANDS = [
|
|
282
|
+
const LEGACY_SELFTUNE_COMMANDS = new Set([
|
|
109
283
|
"npx selftune codex hook",
|
|
110
284
|
"npx -y selftune@latest codex hook",
|
|
111
285
|
"npx -y selftune codex hook",
|
|
112
|
-
];
|
|
286
|
+
]);
|
|
113
287
|
|
|
114
288
|
/** Check if a hook entry was installed by selftune. */
|
|
115
|
-
function isSelftuneHook(entry:
|
|
289
|
+
function isSelftuneHook(entry: CodexHookHandler): boolean {
|
|
116
290
|
if (entry._selftune === true) return true;
|
|
117
291
|
// Exact match against known legacy commands only
|
|
118
|
-
|
|
292
|
+
if (typeof entry.command !== "string") return false;
|
|
293
|
+
return entry.command === HOOK_COMMAND || LEGACY_SELFTUNE_COMMANDS.has(entry.command);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function stripSelftuneHooks(existing: CodexHooksByEvent): {
|
|
297
|
+
hooksByEvent: CodexHooksByEvent;
|
|
298
|
+
removedCount: number;
|
|
299
|
+
} {
|
|
300
|
+
const hooksByEvent: CodexHooksByEvent = {};
|
|
301
|
+
let removedCount = 0;
|
|
302
|
+
|
|
303
|
+
for (const [eventName, groups] of Object.entries(existing)) {
|
|
304
|
+
const cleanedGroups: CodexMatcherGroup[] = [];
|
|
305
|
+
|
|
306
|
+
for (const group of groups) {
|
|
307
|
+
const preservedHooks = group.hooks.filter((handler) => !isSelftuneHook(handler));
|
|
308
|
+
removedCount += group.hooks.length - preservedHooks.length;
|
|
309
|
+
if (preservedHooks.length > 0) {
|
|
310
|
+
cleanedGroups.push({
|
|
311
|
+
...group,
|
|
312
|
+
hooks: preservedHooks.map((handler) => ({ ...handler })),
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (cleanedGroups.length > 0) {
|
|
318
|
+
hooksByEvent[eventName] = cleanedGroups;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return { hooksByEvent, removedCount };
|
|
119
323
|
}
|
|
120
324
|
|
|
121
325
|
/** Merge selftune hooks into existing hooks, replacing any previous selftune entries. */
|
|
122
326
|
export function mergeHooks(
|
|
123
|
-
existing:
|
|
124
|
-
incoming:
|
|
125
|
-
):
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
327
|
+
existing: CodexHooksByEvent,
|
|
328
|
+
incoming: CodexHooksByEvent,
|
|
329
|
+
): CodexHooksByEvent {
|
|
330
|
+
const { hooksByEvent } = stripSelftuneHooks(existing);
|
|
331
|
+
const merged = cloneHooksByEvent(hooksByEvent);
|
|
332
|
+
|
|
333
|
+
for (const [eventName, groups] of Object.entries(incoming)) {
|
|
334
|
+
merged[eventName] = [
|
|
335
|
+
...(merged[eventName] ?? []),
|
|
336
|
+
...cloneHooksByEvent({ [eventName]: groups })[eventName],
|
|
337
|
+
];
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return merged;
|
|
130
341
|
}
|
|
131
342
|
|
|
132
343
|
/** Remove all selftune hooks from the list. */
|
|
133
|
-
export function removeSelftuneHooks(existing:
|
|
134
|
-
return existing.
|
|
344
|
+
export function removeSelftuneHooks(existing: CodexHooksByEvent): CodexHooksByEvent {
|
|
345
|
+
return stripSelftuneHooks(existing).hooksByEvent;
|
|
135
346
|
}
|
|
136
347
|
|
|
137
348
|
// ---------------------------------------------------------------------------
|
|
@@ -150,13 +361,14 @@ export function installHooks(options: { dryRun?: boolean } = {}): InstallResult
|
|
|
150
361
|
const hooksPath = getCodexHooksPath();
|
|
151
362
|
const codexHome = getCodexHome();
|
|
152
363
|
const hooksFile = readHooksFile(hooksPath);
|
|
153
|
-
const existingHooks = hooksFile.
|
|
154
|
-
|
|
364
|
+
const existingHooks = hooksFile.hooksByEvent;
|
|
155
365
|
const merged = mergeHooks(existingHooks, SELFTUNE_HOOKS);
|
|
366
|
+
const serializedExisting = serializeHooksByEvent(existingHooks);
|
|
367
|
+
const serializedMerged = serializeHooksByEvent(merged);
|
|
156
368
|
|
|
157
|
-
//
|
|
158
|
-
const existingJson = JSON.stringify(
|
|
159
|
-
const mergedJson = JSON.stringify(
|
|
369
|
+
// Compare the persisted shape; _selftune markers are internal only.
|
|
370
|
+
const existingJson = JSON.stringify(serializedExisting);
|
|
371
|
+
const mergedJson = JSON.stringify(serializedMerged);
|
|
160
372
|
|
|
161
373
|
if (existingJson === mergedJson) {
|
|
162
374
|
return {
|
|
@@ -172,15 +384,27 @@ export function installHooks(options: { dryRun?: boolean } = {}): InstallResult
|
|
|
172
384
|
if (!existsSync(codexHome)) {
|
|
173
385
|
mkdirSync(codexHome, { recursive: true });
|
|
174
386
|
}
|
|
175
|
-
|
|
176
|
-
|
|
387
|
+
writeFileSync(
|
|
388
|
+
hooksPath,
|
|
389
|
+
JSON.stringify(
|
|
390
|
+
{
|
|
391
|
+
...hooksFile.otherFields,
|
|
392
|
+
hooks: serializedMerged,
|
|
393
|
+
},
|
|
394
|
+
null,
|
|
395
|
+
2,
|
|
396
|
+
) + "\n",
|
|
397
|
+
"utf-8",
|
|
398
|
+
);
|
|
177
399
|
}
|
|
178
400
|
|
|
401
|
+
const { removedCount } = stripSelftuneHooks(existingHooks);
|
|
402
|
+
|
|
179
403
|
return {
|
|
180
404
|
hooksPath,
|
|
181
405
|
action: "installed",
|
|
182
|
-
hooksWritten: SELFTUNE_HOOKS.length,
|
|
183
|
-
hooksRemoved:
|
|
406
|
+
hooksWritten: Object.keys(SELFTUNE_HOOKS).length,
|
|
407
|
+
hooksRemoved: removedCount,
|
|
184
408
|
dryRun: options.dryRun ?? false,
|
|
185
409
|
};
|
|
186
410
|
}
|
|
@@ -188,10 +412,8 @@ export function installHooks(options: { dryRun?: boolean } = {}): InstallResult
|
|
|
188
412
|
export function uninstallHooks(options: { dryRun?: boolean } = {}): InstallResult {
|
|
189
413
|
const hooksPath = getCodexHooksPath();
|
|
190
414
|
const hooksFile = readHooksFile(hooksPath);
|
|
191
|
-
const existingHooks = hooksFile.
|
|
192
|
-
|
|
193
|
-
const cleaned = removeSelftuneHooks(existingHooks);
|
|
194
|
-
const removedCount = existingHooks.length - cleaned.length;
|
|
415
|
+
const existingHooks = hooksFile.hooksByEvent;
|
|
416
|
+
const { hooksByEvent: cleaned, removedCount } = stripSelftuneHooks(existingHooks);
|
|
195
417
|
|
|
196
418
|
if (removedCount === 0) {
|
|
197
419
|
return {
|
|
@@ -204,8 +426,18 @@ export function uninstallHooks(options: { dryRun?: boolean } = {}): InstallResul
|
|
|
204
426
|
}
|
|
205
427
|
|
|
206
428
|
if (!options.dryRun) {
|
|
207
|
-
|
|
208
|
-
|
|
429
|
+
writeFileSync(
|
|
430
|
+
hooksPath,
|
|
431
|
+
JSON.stringify(
|
|
432
|
+
{
|
|
433
|
+
...hooksFile.otherFields,
|
|
434
|
+
hooks: serializeHooksByEvent(cleaned),
|
|
435
|
+
},
|
|
436
|
+
null,
|
|
437
|
+
2,
|
|
438
|
+
) + "\n",
|
|
439
|
+
"utf-8",
|
|
440
|
+
);
|
|
209
441
|
}
|
|
210
442
|
|
|
211
443
|
return {
|
|
@@ -225,9 +457,9 @@ export function uninstallHooks(options: { dryRun?: boolean } = {}): InstallResul
|
|
|
225
457
|
* CLI entry point for `selftune codex install`.
|
|
226
458
|
*/
|
|
227
459
|
export async function cliMain(): Promise<void> {
|
|
228
|
-
const args = process.argv.slice(2);
|
|
229
|
-
const dryRun = args.
|
|
230
|
-
const uninstall = args.
|
|
460
|
+
const args = new Set(process.argv.slice(2));
|
|
461
|
+
const dryRun = args.has("--dry-run");
|
|
462
|
+
const uninstall = args.has("--uninstall");
|
|
231
463
|
|
|
232
464
|
try {
|
|
233
465
|
if (uninstall) {
|
|
@@ -140,6 +140,7 @@ interface OpenCodeAgentConfig {
|
|
|
140
140
|
|
|
141
141
|
interface OpenCodeConfig {
|
|
142
142
|
agent?: Record<string, OpenCodeAgentConfig>;
|
|
143
|
+
plugin?: string[];
|
|
143
144
|
[key: string]: unknown;
|
|
144
145
|
}
|
|
145
146
|
|
|
@@ -405,7 +406,7 @@ function doInstall(options: InstallOptions): void {
|
|
|
405
406
|
// Clean up any legacy plugin array entries from previous installer versions
|
|
406
407
|
if (Array.isArray(config.plugin)) {
|
|
407
408
|
const before = config.plugin.length;
|
|
408
|
-
config.plugin =
|
|
409
|
+
config.plugin = config.plugin.filter((p) => !p.includes(PLUGIN_FILENAME));
|
|
409
410
|
if (config.plugin.length === 0) {
|
|
410
411
|
delete config.plugin;
|
|
411
412
|
}
|
|
@@ -446,9 +447,7 @@ function doUninstall(options: InstallOptions): void {
|
|
|
446
447
|
// Remove legacy plugin array entries
|
|
447
448
|
if (Array.isArray(config.plugin)) {
|
|
448
449
|
const before = config.plugin.length;
|
|
449
|
-
config.plugin =
|
|
450
|
-
(p: string) => !p.includes(PLUGIN_FILENAME),
|
|
451
|
-
);
|
|
450
|
+
config.plugin = config.plugin.filter((p) => !p.includes(PLUGIN_FILENAME));
|
|
452
451
|
if (config.plugin.length === 0) {
|
|
453
452
|
delete config.plugin;
|
|
454
453
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import type { Database } from "bun:sqlite";
|
|
13
13
|
|
|
14
|
-
import type { CanonicalRecord } from "@selftune/telemetry-contract";
|
|
14
|
+
import type { CanonicalRecord, PushPayloadV2 } from "@selftune/telemetry-contract/types";
|
|
15
15
|
|
|
16
16
|
import { buildPushPayloadV2 } from "../canonical-export.js";
|
|
17
17
|
import type { EvolutionEvidenceEntry } from "../types.js";
|
|
@@ -19,7 +19,7 @@ import type { EvolutionEvidenceEntry } from "../types.js";
|
|
|
19
19
|
// -- Types --------------------------------------------------------------------
|
|
20
20
|
|
|
21
21
|
export interface BuildV2Result {
|
|
22
|
-
payload: Record<string,
|
|
22
|
+
payload: PushPayloadV2 & { content_hashes?: Record<string, string> };
|
|
23
23
|
lastSeq: number;
|
|
24
24
|
}
|
|
25
25
|
|
|
@@ -152,7 +152,7 @@ export function buildV2PushPayload(
|
|
|
152
152
|
return null;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
const payload = buildPushPayloadV2(
|
|
155
|
+
const payload: BuildV2Result["payload"] = buildPushPayloadV2(
|
|
156
156
|
canonicalRecords,
|
|
157
157
|
evidenceEntries,
|
|
158
158
|
orchestrateRuns,
|
|
@@ -68,6 +68,12 @@ export function generateSignalId(record: Record<string, unknown>): string {
|
|
|
68
68
|
return `sig_${createHash("sha256").update(key).digest("hex").slice(0, 16)}`;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
function addOptional(record: Record<string, unknown>, key: string, value: unknown): void {
|
|
72
|
+
if (value !== undefined && value !== null) {
|
|
73
|
+
record[key] = value;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
71
77
|
/**
|
|
72
78
|
* Enrich a raw parsed record: if it is an execution_fact missing
|
|
73
79
|
* execution_fact_id, inject a deterministic one.
|
|
@@ -128,7 +134,7 @@ function extractRecordId(record: CanonicalRecord): string {
|
|
|
128
134
|
* Extract session_id from a canonical record (if the record has one).
|
|
129
135
|
*/
|
|
130
136
|
function extractSessionId(record: CanonicalRecord): string | null {
|
|
131
|
-
if ("session_id" in record) return record.session_id;
|
|
137
|
+
if ("session_id" in record && typeof record.session_id === "string") return record.session_id;
|
|
132
138
|
return null;
|
|
133
139
|
}
|
|
134
140
|
|
|
@@ -136,7 +142,7 @@ function extractSessionId(record: CanonicalRecord): string | null {
|
|
|
136
142
|
* Extract prompt_id from a canonical record (if the record has one).
|
|
137
143
|
*/
|
|
138
144
|
function extractPromptId(record: CanonicalRecord): string | null {
|
|
139
|
-
if ("prompt_id" in record) return record.prompt_id;
|
|
145
|
+
if ("prompt_id" in record && typeof record.prompt_id === "string") return record.prompt_id;
|
|
140
146
|
return null;
|
|
141
147
|
}
|
|
142
148
|
|
|
@@ -213,19 +219,19 @@ export function stageCanonicalRecords(db: Database, logPath: string = CANONICAL_
|
|
|
213
219
|
for (const entry of evidence) {
|
|
214
220
|
const evidenceRecord: Record<string, unknown> = {
|
|
215
221
|
skill_name: entry.skill_name,
|
|
216
|
-
skill_path: entry.skill_path,
|
|
217
|
-
proposal_id: entry.proposal_id,
|
|
218
222
|
target: entry.target,
|
|
219
223
|
stage: entry.stage,
|
|
220
|
-
rationale: entry.rationale,
|
|
221
|
-
confidence: entry.confidence,
|
|
222
|
-
details: entry.details,
|
|
223
|
-
original_text: entry.original_text,
|
|
224
|
-
proposed_text: entry.proposed_text,
|
|
225
|
-
eval_set_json: entry.eval_set,
|
|
226
|
-
validation_json: entry.validation,
|
|
227
224
|
timestamp: entry.timestamp,
|
|
228
225
|
};
|
|
226
|
+
addOptional(evidenceRecord, "skill_path", entry.skill_path);
|
|
227
|
+
addOptional(evidenceRecord, "proposal_id", entry.proposal_id);
|
|
228
|
+
addOptional(evidenceRecord, "rationale", entry.rationale);
|
|
229
|
+
addOptional(evidenceRecord, "confidence", entry.confidence);
|
|
230
|
+
addOptional(evidenceRecord, "details", entry.details);
|
|
231
|
+
addOptional(evidenceRecord, "original_text", entry.original_text);
|
|
232
|
+
addOptional(evidenceRecord, "proposed_text", entry.proposed_text);
|
|
233
|
+
addOptional(evidenceRecord, "eval_set_json", entry.eval_set);
|
|
234
|
+
addOptional(evidenceRecord, "validation_json", entry.validation);
|
|
229
235
|
// Generate deterministic evidence_id if not already present
|
|
230
236
|
const evidenceId = generateEvidenceId(evidenceRecord);
|
|
231
237
|
evidenceRecord.evidence_id = evidenceId;
|