Skip to content
This repository was archived by the owner on May 25, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "gradata",
"version": "0.1.0",
"description": "AI that learns your judgment - auto-captures corrections and injects graduated rules",
"author": {
"name": "Gradata",
"email": "oliver@gradata.ai"
},
"hooks": "./hooks/hooks.json",
"skills": "./skills"
}
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,10 @@ node ~/.gradata/plugin/setup/doctor.js

- **Claude Code** — installer also creates `~/.claude/plugins/gradata`
symlinking the checkout, so `/gradata` slash-commands work out of the box.
- **Codex / OpenCode / Hermes** — pick up the Gradata block from `AGENTS.md`
- **Codex** — installer adds a managed Gradata hook block to
`~/.codex/config.toml` so session lifecycle events fire graduation and
AGENTS.md maintenance hooks.
- **OpenCode / Hermes** — pick up the Gradata block from `AGENTS.md`
automatically. The `gradata-quickstart` skill provides the full reference;
the doctor command is the universal health check:
`node ~/.gradata/plugin/setup/doctor.js`.
Expand Down
63 changes: 63 additions & 0 deletions hooks/hooks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
{
"hooks": {
"SessionStart": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "node hooks/session-start.js"
}
]
}
],
"UserPromptSubmit": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "node hooks/user-prompt.js"
}
]
}
],
"PostToolUse": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "node hooks/post-edit.js"
},
{
"type": "command",
"command": "node hooks/post-tool-extended.js"
}
]
}
],
"PreCompact": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "node hooks/pre-compact.js"
}
]
}
],
"Stop": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "node hooks/session-stop.js"
}
]
}
]
}
}
90 changes: 90 additions & 0 deletions hooks/lib/telemetry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// gradata-plugin/hooks/lib/telemetry.js — Lab experiment telemetry
// Best-effort event emission. Never throws, never blocks critical paths.
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const { callDaemon } = require('./daemon-client.js');

const HOME = process.env.HOME || process.env.USERPROFILE || '~';
const GRADATA_HOME = process.env.GRADATA_HOME || path.join(HOME, '.gradata');

/** Path to persisted install_id. */
function installIdPath() {
return path.join(GRADATA_HOME, 'install_id');
}

/** Generate a unique install ID (32-char hex). */
function generateInstallId() {
const id = crypto.randomBytes(16).toString('hex');
try {
fs.mkdirSync(GRADATA_HOME, { recursive: true });
fs.writeFileSync(installIdPath(), id, 'utf8');
} catch { /* best-effort */ }
return id;
}

/** Read persisted install_id. Returns empty string if not present. */
function getInstallId() {
try {
return fs.readFileSync(installIdPath(), 'utf8').trim();
} catch { return ''; }
}

/**
* Deterministic cohort assignment: hash installId, first hex char < '8' → control.
* Returns 'control' or 'treatment'.
*/
function getCohort(installId) {
const h = crypto.createHash('sha256').update(installId).digest('hex');
return h[0] < '8' ? 'control' : 'treatment';
}

/** Emit install_completed event to daemon. Best-effort. */
async function emitInstallCompleted(installId, cohort) {
try {
await callDaemon('/telemetry/install-completed', {
install_id: installId,
cohort,
timestamp: new Date().toISOString(),
}, 3000);
} catch { /* telemetry is best-effort */ }
}

/** Guard flag path for first_rule_injected — one per install. */
function firstRuleFlagPath(installId) {
return path.join(GRADATA_HOME, `.first_rule_injected_${installId}`);
}

/** Check if first_rule_injected has already been emitted for this install. */
function isFirstRuleInjectedEmitted(installId) {
try {
return fs.existsSync(firstRuleFlagPath(installId));
} catch { return false; }
}

/** Mark first_rule_injected as emitted (touch guard file). */
function markFirstRuleInjectedEmitted(installId) {
try {
fs.writeFileSync(firstRuleFlagPath(installId), new Date().toISOString(), 'utf8');
} catch { /* best-effort */ }
}

/** Emit first_rule_injected event to daemon. Best-effort, fires once per install. */
async function emitFirstRuleInjected(installId) {
if (isFirstRuleInjectedEmitted(installId)) return;
markFirstRuleInjectedEmitted(installId);
try {
await callDaemon('/telemetry/first-rule-injected', {
install_id: installId,
timestamp: new Date().toISOString(),
}, 3000);
} catch { /* telemetry is best-effort */ }
}

module.exports = {
generateInstallId,
getInstallId,
getCohort,
emitInstallCompleted,
emitFirstRuleInjected,
};
6 changes: 6 additions & 0 deletions hooks/session-stop.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env node
const { callDaemon } = require('./lib/daemon-client.js');
const { readHookInput } = require('./lib/hook-input.js');
const { emitFirstRuleInjected, getInstallId } = require('./lib/telemetry.js');
(async () => {
try {
const eventData = readHookInput();
Expand All @@ -14,6 +15,11 @@ const { readHookInput } = require('./lib/hook-input.js');
if (c > 0 || g > 0) {
process.stderr.write(`[gradata] Session end: ${c} corrections, ${i} instructions, ${g} graduated\n`);
}
// Emit first_rule_injected if rules are active (graduation happened OR active rules present)
if (g > 0 || (endResult.active_rules || 0) > 0) {
const installId = endResult.install_id || getInstallId();
if (installId) emitFirstRuleInjected(installId);
}
}

const maintainResult = await callDaemon('/maintain', { tasks: ['manifest', 'patterns'] }, 10000);
Expand Down
Loading