From 81cd50a2a66b27dad952dbfc03c48837c4821f3a Mon Sep 17 00:00:00 2001 From: Emanuele Fenocchi Date: Wed, 16 Sep 2026 21:50:24 +0000 Subject: [PATCH 1/4] chore(kpi): remove the goal-KPI table, CLI and VFS routing Drop the hivemind_kpis table, the `hivemind kpi add|list|bump` CLI, the memory/kpi//.md VFS routing, the openclaw hivemind_kpi_add tool and the unreferenced commit-kpi-extract hook. Goals (table, CLI, VFS paths, SessionStart instructions) are unchanged. memory/kpi/ paths now fall through to the generic memory table like any other path. --- esbuild.config.mjs | 1 - harnesses/openclaw/openclaw.plugin.json | 1 - harnesses/openclaw/src/index.ts | 54 ------ harnesses/openclaw/src/setup-config.ts | 3 +- src/cli/index.ts | 7 +- src/commands/goal.ts | 128 +------------- src/config.ts | 8 +- src/deeplake-api.ts | 22 --- src/deeplake-schema.ts | 32 +--- src/hooks/capture.ts | 9 - src/hooks/commit-kpi-extract.ts | 212 ------------------------ src/hooks/cursor/session-start.ts | 4 +- src/hooks/memory-path-utils.ts | 2 +- src/hooks/shared/goals-instructions.ts | 38 ++--- src/shell/deeplake-fs.ts | 105 ++---------- src/shell/deeplake-shell.ts | 3 +- src/shell/goal-paths.ts | 73 ++------ 17 files changed, 49 insertions(+), 653 deletions(-) delete mode 100644 src/hooks/commit-kpi-extract.ts diff --git a/esbuild.config.mjs b/esbuild.config.mjs index 53a00eb1b..f88f98dea 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -654,7 +654,6 @@ const openclawGraphWorkerDefine = { "process.env.HIVEMIND_SKILLS_TABLE": "globalThis.__hivemind_tuning__.HIVEMIND_SKILLS_TABLE", "process.env.HIVEMIND_RULES_TABLE": "globalThis.__hivemind_tuning__.HIVEMIND_RULES_TABLE", "process.env.HIVEMIND_GOALS_TABLE": "globalThis.__hivemind_tuning__.HIVEMIND_GOALS_TABLE", - "process.env.HIVEMIND_KPIS_TABLE": "globalThis.__hivemind_tuning__.HIVEMIND_KPIS_TABLE", "process.env.HIVEMIND_MEMORY_PATH": "globalThis.__hivemind_tuning__.HIVEMIND_MEMORY_PATH", "process.env.HIVEMIND_GRAPH_PUSH": "globalThis.__hivemind_tuning__.HIVEMIND_GRAPH_PUSH", "process.env.HIVEMIND_GRAPHS_HOME": "globalThis.__hivemind_tuning__.HIVEMIND_GRAPHS_HOME", diff --git a/harnesses/openclaw/openclaw.plugin.json b/harnesses/openclaw/openclaw.plugin.json index 603a263de..e630059ff 100644 --- a/harnesses/openclaw/openclaw.plugin.json +++ b/harnesses/openclaw/openclaw.plugin.json @@ -11,7 +11,6 @@ "hivemind_read", "hivemind_index", "hivemind_goal_add", - "hivemind_kpi_add", "hivemind_graph_search", "hivemind_graph_neighborhood" ], diff --git a/harnesses/openclaw/src/index.ts b/harnesses/openclaw/src/index.ts index 78cb7976f..e164a707b 100644 --- a/harnesses/openclaw/src/index.ts +++ b/harnesses/openclaw/src/index.ts @@ -420,7 +420,6 @@ let sessionsTable = "sessions"; let memoryTable = "memory"; let skillsTable = "skills"; // lazy-created on first INSERT by the worker let goalsTable = "hivemind_goals"; // lazy-created by hivemind_goal_add tool -let kpisTable = "hivemind_kpis"; // lazy-created by hivemind_kpi_add tool let captureEnabled = true; const capturedCounts = new Map(); const fallbackSessionId = crypto.randomUUID(); @@ -730,7 +729,6 @@ async function getApi(): Promise { memoryTable = config.tableName; skillsTable = config.skillsTableName; goalsTable = config.goalsTableName; - kpisTable = config.kpisTableName; // Build the api in a local variable and only commit it to the module-level // cache after both ensureX calls succeed. If a transient network failure @@ -1259,58 +1257,6 @@ export default definePluginEntry({ }, }); - pluginApi.registerTool({ - name: "hivemind_kpi_add", - label: "Hivemind KPI Add", - description: - "Add a measurable KPI to an existing Hivemind goal. Persists to the org-shared hivemind_kpis table. Only call after the user has explicitly asked for KPIs — do NOT auto-generate them.", - parameters: { - type: "object", - additionalProperties: false, - properties: { - goal_id: { type: "string", minLength: 1, description: "Existing goal_id (UUID) returned by hivemind_goal_add." }, - kpi_id: { type: "string", minLength: 1, description: "Short slug for this KPI (e.g. 'k-prs')." }, - target: { type: "integer", minimum: 1, description: "Positive integer target." }, - unit: { type: "string", minLength: 1, description: "Unit label (e.g. 'count', 'PRs', 'lines')." }, - name: { type: "string", description: "Optional human-readable name. Defaults to kpi_id." }, - }, - required: ["goal_id", "kpi_id", "target", "unit"], - }, - execute: async (_toolCallId, rawParams) => { - const params = rawParams as { goal_id: string; kpi_id: string; target: number; unit: string; name?: string }; - const dl = await getApi(); - if (!dl) { - return { content: [{ type: "text", text: "Not logged in. Run /hivemind_login first." }] }; - } - try { - await dl.ensureKpisTable(kpisTable); - const name = params.name ?? params.kpi_id; - const content = `${name}\n\n- target: ${params.target}\n- current: 0\n- unit: ${params.unit}`; - const ts = new Date().toISOString(); - const safe = kpisTable.replace(/[^A-Za-z0-9_]/g, ""); - await dl.query( - `INSERT INTO "${safe}" (id, goal_id, kpi_id, content, version, created_at, updated_at, agent, plugin_version) VALUES (` + - `'${crypto.randomUUID()}', ` + - `'${sqlStr(params.goal_id)}', ` + - `'${sqlStr(params.kpi_id)}', ` + - `E'${sqlStr(content)}', ` + - `1, ` + - `'${sqlStr(ts)}', ` + - `'${sqlStr(ts)}', ` + - `'openclaw', ` + - `''` + - `)` - ); - pluginApi.logger.info?.(`hivemind_kpi_add → ${params.goal_id}/${params.kpi_id}`); - return { content: [{ type: "text", text: `KPI added.\ngoal_id: ${params.goal_id}\nkpi_id: ${params.kpi_id}\ntarget: ${params.target} ${params.unit}` }] }; - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - pluginApi.logger.error(`hivemind_kpi_add failed: ${msg}`); - return { content: [{ type: "text", text: `KPI add failed: ${msg}` }] }; - } - }, - }); - // Memory-corpus supplement: if the host runs a `memory_search` tool (e.g. // from memory-core), it federates queries to all registered supplements. // Non-exclusive — coexists with any other corpus. diff --git a/harnesses/openclaw/src/setup-config.ts b/harnesses/openclaw/src/setup-config.ts index 7613e51c3..5053f8234 100644 --- a/harnesses/openclaw/src/setup-config.ts +++ b/harnesses/openclaw/src/setup-config.ts @@ -14,14 +14,13 @@ export const HIVEMIND_TOOL_NAMES = [ "hivemind_read", "hivemind_index", "hivemind_goal_add", - "hivemind_kpi_add", "hivemind_graph_search", "hivemind_graph_neighborhood", ]; /** * Core memory tools whose per-name alsoAllow entry counts as full suite - * coverage. Niche tools (goals/KPIs/graph) must NOT — a user who allowlists + * coverage. Niche tools (goals/graph) must NOT — a user who allowlists * only hivemind_graph_search still needs "hivemind" added by /hivemind_setup. */ export const HIVEMIND_CORE_ALLOWLIST_TOOLS = [ diff --git a/src/cli/index.ts b/src/cli/index.ts index 21161096d..029a44d7f 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -26,7 +26,7 @@ import { runAuthCommand } from "../commands/auth-login.js"; import { runDashboardCommand } from "../commands/dashboard.js"; import { runSkillifyCommand } from "../commands/skillify.js"; import { runRulesCommand } from "../commands/rules.js"; -import { runGoalCommand, runKpiCommand } from "../commands/goal.js"; +import { runGoalCommand } from "../commands/goal.js"; import { runDocsCommand } from "../commands/docs.js"; import { runContextCommand } from "../commands/context.js"; import { runBackfillMemory } from "../commands/backfill-memory.js"; @@ -532,11 +532,6 @@ async function main(): Promise { return; } - if (cmd === "kpi" || cmd === "kpis") { - await runKpiCommand(args.slice(1)); - return; - } - if (cmd === "docs" || cmd === "doc") { await runDocsCommand(args.slice(1)); return; diff --git a/src/commands/goal.ts b/src/commands/goal.ts index 0583362c7..53a13190a 100644 --- a/src/commands/goal.ts +++ b/src/commands/goal.ts @@ -1,5 +1,5 @@ /** - * CLI surface for `hivemind goal` / `hivemind kpi`. + * CLI surface for `hivemind goal`. * * Why this exists: cursor and hermes intercept ONLY Shell-style * tool invocations in their pre-tool-use hook (see @@ -15,7 +15,7 @@ * command runs as a normal subprocess (cursor's hook lets * non-memory-touching commands pass through), and this code talks * directly to the Deeplake API. End result: a row in - * hivemind_goals (or hivemind_kpis) regardless of which agent + * hivemind_goals regardless of which agent * called it. * * Subcommands: @@ -24,11 +24,6 @@ * hivemind goal list [--all|--mine] list goal_id + text + status * hivemind goal done flip status -> closed * hivemind goal progress flip status to any value - * hivemind kpi add [name] - * create a KPI on an existing goal - * hivemind kpi list list KPIs for a goal - * hivemind kpi bump - * add (int, +/-) to current * * Output is intentionally compact and machine-parsable on the * happy path so the agent can pipe it into follow-up commands. @@ -198,106 +193,6 @@ async function goalProgress(goalId: string, status: string): Promise { process.stdout.write(`${goalId} -> ${status}\n`); } -// ── kpi subcommands ───────────────────────────────────────────────────────── - -async function kpiAdd(args: string[]): Promise { - const [goalId, kpiId, targetStr, unit, ...nameParts] = args; - if (!goalId || !kpiId || !targetStr || !unit) { - process.stderr.write("usage: hivemind kpi add [name]\n"); - process.exit(1); - } - const target = Number.parseInt(targetStr, 10); - if (!Number.isFinite(target) || target <= 0) { - process.stderr.write(`invalid target: ${targetStr} (must be positive integer)\n`); - process.exit(1); - } - const name = nameParts.length > 0 ? nameParts.join(" ") : kpiId; - const cfg = loadRoutedConfig(); - if (!cfg) { process.stderr.write("not logged in\n"); process.exit(1); } - const { api, query } = loadApiOrDie(cfg.kpisTableName); - await api.ensureKpisTable(cfg.kpisTableName); - const safe = sqlIdent(cfg.kpisTableName); - const content = `${name}\n\n- target: ${target}\n- current: 0\n- unit: ${unit}`; - const ts = new Date().toISOString(); - await query( - `INSERT INTO "${safe}" (id, goal_id, kpi_id, content, version, created_at, updated_at, agent, plugin_version) VALUES (` + - `'${randomUUID()}', ` + - `'${sqlStr(goalId)}', ` + - `'${sqlStr(kpiId)}', ` + - `E'${sqlStr(content)}', ` + - `1, ` + - `'${sqlStr(ts)}', ` + - `'${sqlStr(ts)}', ` + - `'manual', ` + - `''` + - `)` - ); - process.stdout.write(`${goalId}/${kpiId}\n`); -} - -async function kpiList(goalId: string): Promise { - if (!goalId) { process.stderr.write("usage: hivemind kpi list \n"); process.exit(1); } - const cfg = loadRoutedConfig(); - if (!cfg) { process.stderr.write("not logged in\n"); process.exit(1); } - const { query } = loadApiOrDie(cfg.kpisTableName); - const safe = sqlIdent(cfg.kpisTableName); - try { - const rows = await query( - `SELECT kpi_id, content FROM "${safe}" WHERE goal_id = '${sqlStr(goalId)}' ORDER BY created_at ASC LIMIT 50` - ); - if (rows.length === 0) { process.stdout.write("(no kpis)\n"); return; } - for (const r of rows) { - const firstLine = String(r.content ?? "").split(/\r?\n/)[0].trim(); - process.stdout.write(`${r.kpi_id}\t${firstLine}\n`); - } - } catch (e: unknown) { - process.stderr.write(`hivemind kpi list: ${(e as Error).message}\n`); - process.exit(1); - } -} - -async function kpiBump(goalId: string, kpiId: string, deltaStr: string): Promise { - if (!goalId || !kpiId || !deltaStr) { - process.stderr.write("usage: hivemind kpi bump \n"); - process.exit(1); - } - const delta = Number.parseInt(deltaStr, 10); - if (!Number.isFinite(delta)) { - process.stderr.write(`invalid delta: ${deltaStr}\n`); - process.exit(1); - } - const cfg = loadRoutedConfig(); - if (!cfg) { process.stderr.write("not logged in\n"); process.exit(1); } - const { api, query } = loadApiOrDie(cfg.kpisTableName); - // Heal the schema before the UPDATE — same reason as goalProgress: a - // preexisting KPIs table may not yet have the `updated_at` column. - await api.ensureKpisTable(cfg.kpisTableName); - const safe = sqlIdent(cfg.kpisTableName); - // Read current content - const rows = await query( - `SELECT content FROM "${safe}" WHERE goal_id = '${sqlStr(goalId)}' AND kpi_id = '${sqlStr(kpiId)}' LIMIT 1` - ); - if (rows.length === 0) { - process.stderr.write(`kpi not found: ${goalId}/${kpiId}\n`); - process.exit(1); - } - const content = String(rows[0].content ?? ""); - // Find and bump the `current:` line - const newContent = content.replace( - /^(\s*-?\s*current\s*:\s*)(-?\d+)(\s*)$/m, - (_m, prefix, n, suffix) => `${prefix}${Number.parseInt(n, 10) + delta}${suffix}` - ); - if (newContent === content) { - process.stderr.write(`could not find 'current:' line in kpi ${goalId}/${kpiId}\n`); - process.exit(1); - } - const ts = new Date().toISOString(); - await query( - `UPDATE "${safe}" SET content = E'${sqlStr(newContent)}', updated_at = '${sqlStr(ts)}' WHERE goal_id = '${sqlStr(goalId)}' AND kpi_id = '${sqlStr(kpiId)}'` - ); - process.stdout.write(`${goalId}/${kpiId} +${delta}\n`); -} - // ── dispatchers ───────────────────────────────────────────────────────────── const USAGE_GOAL = ` @@ -349,22 +244,3 @@ export async function runGoalCommand(args: string[]): Promise { process.stderr.write(`unknown goal subcommand: ${sub}\n${USAGE_GOAL}\n`); process.exit(1); } - -const USAGE_KPI = ` -hivemind kpi — manage goal KPIs - -Usage: - hivemind kpi add [name] - hivemind kpi list - hivemind kpi bump -`.trim(); - -export async function runKpiCommand(args: string[]): Promise { - const sub = args[0]; - if (!sub || sub === "--help" || sub === "-h") { process.stdout.write(USAGE_KPI + "\n"); return; } - if (sub === "add") { await kpiAdd(args.slice(1)); return; } - if (sub === "list") { await kpiList(args[1]); return; } - if (sub === "bump") { await kpiBump(args[1], args[2], args[3]); return; } - process.stderr.write(`unknown kpi subcommand: ${sub}\n${USAGE_KPI}\n`); - process.exit(1); -} diff --git a/src/config.ts b/src/config.ts index 8f4b6770b..be4e4e8fd 100644 --- a/src/config.ts +++ b/src/config.ts @@ -15,7 +15,6 @@ export interface Config { skillsTableName: string; rulesTableName: string; goalsTableName: string; - kpisTableName: string; docsTableName: string; codebaseTableName: string; memoryPath: string; @@ -74,14 +73,11 @@ export function loadConfig(): Config | null { // override convention (memory_test / sessions_test → goals_test, etc.) // documented in CLAUDE.md. rulesTableName: process.env.HIVEMIND_RULES_TABLE ?? "hivemind_rules", - // Goals + KPIs (refined design — VFS path classifier maps + // Goals (VFS path classifier maps // memory/goal///.md → hivemind_goals row - // memory/kpi//.md → hivemind_kpis row // See src/shell/deeplake-fs.ts for the translation logic and - // GOALS_COLUMNS / KPIS_COLUMNS in deeplake-schema.ts for the - // table shape. + // GOALS_COLUMNS in deeplake-schema.ts for the table shape). goalsTableName: process.env.HIVEMIND_GOALS_TABLE ?? "hivemind_goals", - kpisTableName: process.env.HIVEMIND_KPIS_TABLE ?? "hivemind_kpis", // Per-file documentation kept fresh on code deltas. INSERT-only // version-bumped table (see DOCS_COLUMNS in deeplake-schema.ts). // Phase 1: written/read through the `hivemind docs` CLI + worker via the diff --git a/src/deeplake-api.ts b/src/deeplake-api.ts index ca3450256..e2065a200 100644 --- a/src/deeplake-api.ts +++ b/src/deeplake-api.ts @@ -10,7 +10,6 @@ import { SKILLS_COLUMNS, RULES_COLUMNS, GOALS_COLUMNS, - KPIS_COLUMNS, DOCS_COLUMNS, buildCreateTableSql, healMissingColumns, @@ -727,27 +726,6 @@ export class DeeplakeApi { await this.ensureLookupIndex(safe, "owner_status", `("owner", "status")`); } - /** - * Create the kpis table. - * - * Backed by memory/kpi//.md. KPI rows do NOT carry - * owner — ownership derives from the parent goal via logical join on - * goal_id. INSERT-only version-bumped. (goal_id, kpi_id) index is the - * canonical lookup the VFS uses on Read and Write. - */ - async ensureKpisTable(name: string): Promise { - const safe = sqlIdent(name); - const tables = await this.listTables(); - if (!tables.includes(safe)) { - log(`table "${safe}" not found, creating`); - await this.createTableWithRetry(buildCreateTableSql(safe, KPIS_COLUMNS), safe); - log(`table "${safe}" created`); - if (!tables.includes(safe)) this._tablesCache = [...tables, safe]; - } - await this.healSchema(safe, KPIS_COLUMNS); - await this.ensureLookupIndex(safe, "goal_id_kpi_id", `("goal_id", "kpi_id")`); - } - /** * Create the docs table — per-file documentation kept fresh on code deltas. * diff --git a/src/deeplake-schema.ts b/src/deeplake-schema.ts index 81083d950..6be9c58a5 100644 --- a/src/deeplake-schema.ts +++ b/src/deeplake-schema.ts @@ -130,8 +130,7 @@ export const RULES_COLUMNS: readonly ColumnDef[] = Object.freeze([ * audit trail preserved). * * Status enum: 'opened' | 'in_progress' | 'closed' — mirrors the path - * folder names. KPIs link via shared `goal_id` (no FK enforcement on - * Deeplake; logical join only). + * folder names. */ export const GOALS_COLUMNS: readonly ColumnDef[] = Object.freeze([ { name: "id", sql: "TEXT NOT NULL DEFAULT ''" }, @@ -146,34 +145,6 @@ export const GOALS_COLUMNS: readonly ColumnDef[] = Object.freeze([ { name: "plugin_version", sql: "TEXT NOT NULL DEFAULT ''" }, ]); -/** - * KPIs table — markdown bodies describing target / current / unit for - * one KPI on one goal. Backed by VFS path - * `memory/kpi//.md`. Path encodes the (goal_id, - * kpi_id) pair; the content column stores the body (free markdown, - * by convention with `target:` / `current:` / `unit:` lines for the - * commit-extract worker to mutate). - * - * Owner is intentionally NOT stored here — it is derived from the - * parent goal (logical join on goal_id). This avoids the - * reassign-races scenario where moving a goal between owners would - * otherwise force a multi-file cascade move on the KPI files. - * - * Same version-bump pattern: every write INSERTs v=N+1; deleting a - * KPI conceptually means writing a tombstone version, deferred to v1.1. - */ -export const KPIS_COLUMNS: readonly ColumnDef[] = Object.freeze([ - { name: "id", sql: "TEXT NOT NULL DEFAULT ''" }, - { name: "goal_id", sql: "TEXT NOT NULL DEFAULT ''" }, - { name: "kpi_id", sql: "TEXT NOT NULL DEFAULT ''" }, - { name: "content", sql: "TEXT NOT NULL DEFAULT ''" }, - { name: "version", sql: "BIGINT NOT NULL DEFAULT 1" }, - { name: "created_at", sql: "TEXT NOT NULL DEFAULT ''" }, - { name: "updated_at", sql: "TEXT NOT NULL DEFAULT ''" }, - { name: "agent", sql: "TEXT NOT NULL DEFAULT 'manual'" }, - { name: "plugin_version", sql: "TEXT NOT NULL DEFAULT ''" }, -]); - /** * Docs table — per-file internal documentation kept fresh on code deltas. * @@ -294,7 +265,6 @@ validateSchema("SESSIONS_COLUMNS", SESSIONS_COLUMNS); validateSchema("SKILLS_COLUMNS", SKILLS_COLUMNS); validateSchema("RULES_COLUMNS", RULES_COLUMNS); validateSchema("GOALS_COLUMNS", GOALS_COLUMNS); -validateSchema("KPIS_COLUMNS", KPIS_COLUMNS); validateSchema("DOCS_COLUMNS", DOCS_COLUMNS); validateSchema("CODEBASE_COLUMNS", CODEBASE_COLUMNS); diff --git a/src/hooks/capture.ts b/src/hooks/capture.ts index 06359f7c4..eed7f8b65 100644 --- a/src/hooks/capture.ts +++ b/src/hooks/capture.ts @@ -232,15 +232,6 @@ async function main(): Promise { // source of truth. Only reached after a successful INSERT above. appendSessionEvent(input.session_id, line); - // Commit-driven KPI auto-extract is disabled for now — the - // fire-and-forget sub-agent spawned per `git commit` (see - // src/hooks/commit-kpi-extract.ts) consumed a high amount of tokens - // on the user's claude/codex plan (every commit triggered a full - // goal/KPI scan + reasoning pass over the diff). The module is - // kept on disk for future re-wiring once we add: sha-dedup, - // empty-goals prefilter, debounce, and a hard timeout. Re-enable - // by restoring the import + try block here. - maybeTriggerPeriodicSummary(input.session_id, input.cwd ?? "", config); // SkillOpt: the user prompt is the reaction to a recently-used org skill. Swallowed. diff --git a/src/hooks/commit-kpi-extract.ts b/src/hooks/commit-kpi-extract.ts deleted file mode 100644 index 8d81770b9..000000000 --- a/src/hooks/commit-kpi-extract.ts +++ /dev/null @@ -1,212 +0,0 @@ -/** - * Auto-extract per `git commit` — design pivot dalla white-list regex - * (gh pr merge) a un'analisi LLM del diff vs i goal attivi dell'utente. - * - * Flow: - * 1. PostToolUse hook intercetta Bash `git commit` (success only). - * 2. Cattura il diff via `git show HEAD --no-color` (cap a N kB). - * 3. Spawna l'LLM nativo dell'agent in background (`claude -p` per - * claude-code, `codex exec` per codex) con un prompt che gli dice - * di leggere `~/.deeplake/memory/goals/*.json`, filtrare ai goal - * dell'utente attivi, e bumpare qualunque KPI il diff abbia - * avanzato. Tutto detached, fire-and-forget. - * 4. L'LLM scrive il file aggiornato → VFS → tabella memory → - * visibile al team al prossimo SessionStart. - * - * NO dedup commit→KPI in v1: se l'utente fa amend o se il commit-extract - * gira due volte sullo stesso sha, il bump è doppio. L'utente lo - * correggerà a mano editando il file. - * - * Env var `HIVEMIND_AUTO_KPI_FROM_COMMITS=false` disattiva globalmente. - */ - -import { spawn } from "node:child_process"; -import { execSync } from "node:child_process"; - -const ENV_DISABLE = "HIVEMIND_AUTO_KPI_FROM_COMMITS"; -const DIFF_MAX_CHARS = 16_000; // cap for prompt size; bigger diffs get truncated with a marker - -const GIT_COMMIT_RE = /^\s*git\s+commit\b/; - -export interface CommitExtractInput { - hook_event_name?: string; - tool_name?: string; - tool_input?: Record; - tool_response?: Record; -} - -export interface CommitExtractOptions { - /** Agent identifier — picks the LLM CLI to spawn. */ - agent: "claude-code" | "codex" | "cursor" | "hermes" | string; - /** user_email of the current Hivemind user. Used to scope goals. */ - currentUser: string; - /** Optional explicit cwd (defaults to process.cwd()). */ - cwd?: string; - /** Optional logger for tracing (defaults to no-op). */ - log?: (msg: string) => void; -} - -/** - * Returns true if the hook input is a successful `git commit` Bash - * call. False otherwise — including when the env var disables the - * feature. - */ -export function shouldExtract(input: CommitExtractInput): boolean { - if (process.env[ENV_DISABLE] === "false" || process.env[ENV_DISABLE] === "0") { - return false; - } - if (input.hook_event_name !== "PostToolUse") return false; - if (input.tool_name !== "Bash") return false; - const cmd = (input.tool_input as { command?: unknown } | undefined)?.command; - if (typeof cmd !== "string") return false; - if (!GIT_COMMIT_RE.test(cmd)) return false; - return isSuccess(input.tool_response); -} - -/** - * Main entry — call this from the capture hook on every PostToolUse. - * Returns immediately; the LLM work happens in a detached child - * process. The function is safe to await but never blocks meaningfully - * (the spawn itself is sync, the analysis is async outside our - * process tree). - * - * Throws are intentionally not raised — any failure here must NOT - * break the session-row INSERT. Caller can ignore the return value. - */ -export async function tryCommitKpiExtract( - input: CommitExtractInput, - options: CommitExtractOptions, -): Promise<"emitted" | "skipped" | "error"> { - const log = options.log ?? (() => { /* noop */ }); - try { - if (!shouldExtract(input)) return "skipped"; - - const cwd = options.cwd ?? process.cwd(); - - // git show HEAD — captures the just-committed diff. We pipe through - // a char cap so a 5 MB autogenerated commit doesn't blow up the - // prompt. Truncated diffs are marked so the LLM doesn't hallucinate - // missing context. - let diff = ""; - try { - diff = execSync("git show HEAD --no-color --pretty=fuller", { - cwd, - encoding: "utf8", - maxBuffer: 32 * 1024 * 1024, - stdio: ["ignore", "pipe", "ignore"], - }); - } catch (err: unknown) { - log(`commit-kpi-extract: git show failed: ${(err as Error).message}`); - return "error"; - } - if (diff.length > DIFF_MAX_CHARS) { - diff = diff.slice(0, DIFF_MAX_CHARS) + `\n\n[... diff truncated, original ${diff.length} chars ...]`; - } - - const prompt = buildPrompt({ - currentUser: options.currentUser, - diff, - }); - - const cli = cliForAgent(options.agent); - if (!cli) { - log(`commit-kpi-extract: no LLM CLI mapped for agent '${options.agent}', skipping`); - return "skipped"; - } - - // Fire-and-forget. detached + unref so the worker survives the - // hook exit, and stdio fully ignored so it doesn't keep the parent - // alive. The LLM will read goal files via the VFS and rewrite the - // relevant ones on its own. - const child = spawn(cli.bin, [...cli.args, prompt], { - cwd, - detached: true, - stdio: "ignore", - // CREATE_NO_WINDOW: detached summarizer CLI spawn — without it Windows - // pops a visible console window for the claude/codex child. No-op on POSIX. - windowsHide: true, - env: process.env, - }); - child.unref(); - log(`commit-kpi-extract: spawned ${cli.bin} pid=${child.pid}`); - return "emitted"; - } catch (err: unknown) { - (options.log ?? (() => { /* noop */ }))(`commit-kpi-extract: unexpected: ${(err as Error).message}`); - return "error"; - } -} - -interface CliSpec { - bin: string; - args: string[]; -} - -/** - * Map the agent identifier to the right LLM CLI invocation. Each - * agent uses its own credentials/plan — no Anthropic API key needed - * in the plugin. - * - * Return null when the agent has no batch CLI we can dispatch to - * (e.g. cursor/hermes/pi/openclaw don't currently ship one). - */ -function cliForAgent(agent: string): CliSpec | null { - switch (agent) { - case "claude-code": - case "claude": - return { bin: "claude", args: ["-p"] }; - case "codex": - return { - bin: "codex", - args: ["exec", "--dangerously-bypass-approvals-and-sandbox", "-"], - }; - default: - return null; - } -} - -function buildPrompt(args: { currentUser: string; diff: string }): string { - const user = args.currentUser; - return [ - `A git commit just landed. Update any KPI advanced by this commit for goals owned by ${user}.`, - "", - "Storage layout (Deeplake VFS, both opened and in-progress goals are eligible):", - ` ~/.deeplake/memory/goal/${user}/opened/.md ← active goals`, - ` ~/.deeplake/memory/goal/${user}/in_progress/.md ← in-progress goals`, - ` ~/.deeplake/memory/kpi//.md ← KPI files for that goal`, - "", - "Each KPI file is plain markdown with mandatory key:value lines:", - " ", - " - target: ", - " - current: ", - " - unit: ", - "", - "Steps (sub-agent, run silently, no user-visible output):", - `1. ls ~/.deeplake/memory/goal/${user}/opened/ and ~/.deeplake/memory/goal/${user}/in_progress/. Collect every .md.`, - "2. For each goal_id, ls ~/.deeplake/memory/kpi// and read each KPI body.", - "3. Judge ONLY from clear, direct evidence in the diff whether the commit advanced any KPI. Be conservative — if unsure, do NOTHING for that KPI.", - "4. If a KPI should be bumped: use the Edit tool on the matching kpi file to increment the `current:` line. DO NOT modify target, unit, name, or any other line. DO NOT change the path or the goal_id.", - "5. The VFS persists every write as a version-bump in hivemind_kpis. Multiple bumps across multiple KPIs are fine — just keep each Edit surgical.", - "", - "Constraints:", - "- Do NOT create new goals or new KPI files.", - "- Do NOT touch goal description files under memory/goal/...", - "- Do NOT close, mv, or rm any goal.", - "- Do NOT emit anything to stdout. This worker is fire-and-forget.", - "", - "Commit diff (truncated if oversized):", - "```", - args.diff, - "```", - ].join("\n"); -} - -function isSuccess(tool_response: Record | undefined): boolean { - if (!tool_response) return true; // unknown shape — assume success - if (tool_response.is_error === true) return false; - if (tool_response.error && typeof tool_response.error === "string" && tool_response.error.length > 0) { - return false; - } - if (tool_response.interrupted === true) return false; - if (typeof tool_response.exit_code === "number" && tool_response.exit_code !== 0) return false; - return true; -} diff --git a/src/hooks/cursor/session-start.ts b/src/hooks/cursor/session-start.ts index 33d05b746..c7ea65357 100644 --- a/src/hooks/cursor/session-start.ts +++ b/src/hooks/cursor/session-start.ts @@ -244,8 +244,8 @@ async function main(): Promise { // Cursor cannot route Write/Edit through hivemind hooks (its // pre-tool-use only intercepts Shell). So the agent here uses // the CLI variant — `hivemind goal add/list/...` invoked as - // shell commands. Same end state (rows in hivemind_goals / - // hivemind_kpis), different code path inside the agent. + // shell commands. Same end state (rows in hivemind_goals), + // different code path inside the agent. const baseWithGoals = creds?.token ? `${baseContext}\n\n${GOALS_INSTRUCTIONS_CLI}` : baseContext; const withRules = rulesBlock ? `${baseWithGoals}\n\n${rulesBlock}` diff --git a/src/hooks/memory-path-utils.ts b/src/hooks/memory-path-utils.ts index 215b04a30..192be5fc9 100644 --- a/src/hooks/memory-path-utils.ts +++ b/src/hooks/memory-path-utils.ts @@ -37,7 +37,7 @@ export const SAFE_BUILTINS = new Set([ ]); // A quoted heredoc (`<<'EOF'` / `<<"EOF"`) disables shell expansion, so its -// body is inert literal data — a goal/KPI description, not commands. Drop the +// body is inert literal data — a goal description, not commands. Drop the // body and its closing delimiter so they are never validated as command stages // or tripped over by the substitution guard. Unquoted heredocs keep their body // (bash would expand it), so they still fall through to full validation. diff --git a/src/hooks/shared/goals-instructions.ts b/src/hooks/shared/goals-instructions.ts index 4c0112baf..8146b2945 100644 --- a/src/hooks/shared/goals-instructions.ts +++ b/src/hooks/shared/goals-instructions.ts @@ -1,5 +1,5 @@ /** - * Inline goal/KPI instructions appended to every agent's + * Inline goal instructions appended to every agent's * SessionStart context. * * TWO VARIANTS because the underlying runtimes differ: @@ -7,7 +7,7 @@ * - GOALS_INSTRUCTIONS (VFS variant): for claude-code and codex, * whose pre-tool-use hook can REWRITE the Write/Edit tool * calls and route them through deeplake-shell into the - * hivemind_goals / hivemind_kpis tables. The agent uses + * hivemind_goals table. The agent uses * native Write/Edit on memory paths. * * - GOALS_INSTRUCTIONS_CLI (CLI variant): for cursor/hermes/pi, @@ -15,36 +15,33 @@ * commands (not Write tool). The Write tool on those runtimes * would land on the host filesystem and never reach Deeplake. * The agent instead invokes `hivemind goal add/list/done/...` - * and `hivemind kpi add/list/bump` as plain shell commands. + * as plain shell commands. * The `hivemind` CLI talks directly to the Deeplake API. * - * Both variants end up writing to the same hivemind_goals / - * hivemind_kpis tables. Team visibility is identical. Only the - * code path inside the agent differs. + * Both variants end up writing to the same hivemind_goals table. + * Team visibility is identical. Only the code path inside the agent + * differs. * * Single source of truth lives here so we never drift between * the per-agent session-start.ts forks. */ -export const GOALS_INSTRUCTIONS = `HIVEMIND GOALS — track team goals via the virtual filesystem at \`~/.deeplake/memory/goal/\` and \`~/.deeplake/memory/kpi/\`. Writes auto-persist to the org-shared \`hivemind_goals\` / \`hivemind_kpis\` tables. +export const GOALS_INSTRUCTIONS = `HIVEMIND GOALS — track team goals via the virtual filesystem at \`~/.deeplake/memory/goal/\`. Writes auto-persist to the org-shared \`hivemind_goals\` table. Path convention (path encoding is the source of truth — do NOT duplicate fields in the file body): - Goal: \`~/.deeplake/memory/goal///.md\` with body = free markdown describing the goal -- KPI: \`~/.deeplake/memory/kpi//.md\` with body = '\\n\\n- target: \\n- current: \\n- unit: ' -\`\` = userName from \`hivemind whoami\`. \`\` ∈ {opened, in_progress, closed}. \`\` = UUIDv4 you generate at create time. \`\` = short slug (e.g. \`k-prs\`). +\`\` = userName from \`hivemind whoami\`. \`\` ∈ {opened, in_progress, closed}. \`\` = UUIDv4 you generate at create time. Operations: -- Create goal: Write file at \`goal//opened/.md\`. Do NOT auto-generate KPIs. +- Create goal: Write file at \`goal//opened/.md\`. - Edit goal text: Edit/Write the same path. - Move status: \`mv goal//opened/.md goal//in_progress/.md\` (atomic UPDATE). - Soft-close: \`rm goal///.md\` — VFS interprets rm as status-flip to 'closed' (no hard delete; row stays for audit). -- Add KPI (ONLY when user explicitly asks): Write file at \`kpi//.md\` with the body format above. -- Update KPI progress: Edit only the \`current:\` line. -When the user mentions a goal / objective / target / KPI / measurable milestone — OR a task / todo / work item / "remind me to X" — use this convention. The goals system absorbed the legacy \`hivemind tasks\` CLI, so "task" and "goal" map to the same Hivemind row. Do NOT spawn background workers to generate KPIs unsolicited — wait for the user to ask.`; +When the user mentions a goal / objective / target / measurable milestone — OR a task / todo / work item / "remind me to X" — use this convention. The goals system absorbed the legacy \`hivemind tasks\` CLI, so "task" and "goal" map to the same Hivemind row.`; -export const GOALS_INSTRUCTIONS_CLI = `HIVEMIND GOALS — track team goals via the \`hivemind\` CLI on this runtime. Your Write/Edit tools do NOT route to the team-shared tables here, so use these shell commands instead. All commands persist to the org-shared \`hivemind_goals\` / \`hivemind_kpis\` tables — other team members see your goals at SessionStart. +export const GOALS_INSTRUCTIONS_CLI = `HIVEMIND GOALS — track team goals via the \`hivemind\` CLI on this runtime. Your Write/Edit tools do NOT route to the team-shared tables here, so use these shell commands instead. All commands persist to the org-shared \`hivemind_goals\` table — other team members see your goals at SessionStart. Commands (invoke via your Shell / terminal / Bash tool): @@ -60,19 +57,8 @@ Commands (invoke via your Shell / terminal / Bash tool): hivemind goal progress Flip a goal to any status. - hivemind kpi add [name...] - Add a KPI to an existing goal. = short slug (e.g. k-prs). - = positive integer. [name] defaults to the kpi_id. - - hivemind kpi list - Tab-separated list of (kpi_id, first-line-of-content). - - hivemind kpi bump - Increment (positive int) or decrement (negative) the current value of one KPI. - Workflow when the user expresses a goal OR a task / todo / work item (they are the same thing — the goals system absorbed the legacy hivemind tasks CLI): 1. \`hivemind goal add ""\` — capture stdout as goal_id. - 2. ONLY if the user explicitly asks for KPIs: \`hivemind kpi add \` per KPI. - 3. Tell the user the goal_id and that it is now visible to the team. + 2. Tell the user the goal_id and that it is now visible to the team. Do NOT use Write/Edit on \`~/.deeplake/memory/goal/...\` here — on this runtime those tool calls write to the host filesystem only, not the shared table.`; diff --git a/src/shell/deeplake-fs.ts b/src/shell/deeplake-fs.ts index 257091aa4..58e4ac9d5 100644 --- a/src/shell/deeplake-fs.ts +++ b/src/shell/deeplake-fs.ts @@ -16,9 +16,7 @@ import { buildVirtualIndexContent, INDEX_LIMIT_PER_SECTION } from "../hooks/virt import { classifyPath, composeGoalPath, - composeKpiPath, decomposeGoalPath, - decomposeKpiPath, type PathKind, } from "./goal-paths.js"; import { handleGraphVfs } from "../graph/vfs-handler.js"; @@ -222,9 +220,8 @@ export class DeeplakeFs implements IFileSystem { // Path-routed structured tables. When non-null, the VFS classifies // each path (see ./goal-paths.ts) and dispatches reads/writes to // the right table instead of the generic memory table. Null means - // the goal/kpi routing is disabled (test or legacy configurations). + // the goal routing is disabled (test or legacy configurations). private goalsTable: string | null = null; - private kpisTable: string | null = null; // Per-file docs table, for /docs/ VFS reads + docs/find search. Null = off. private docsTable: string | null = null; /** Project scope for docs reads on shared tables (legacy '' rows included). */ @@ -247,15 +244,14 @@ export class DeeplakeFs implements IFileSystem { table: string, mount = "/memory", sessionsTable?: string, - extra?: { goalsTable?: string; kpisTable?: string; docsTable?: string; docsProject?: string }, + extra?: { goalsTable?: string; docsTable?: string; docsProject?: string }, ): Promise { const fs = new DeeplakeFs(client, table, mount); fs.sessionsTable = sessionsTable ?? null; fs.goalsTable = extra?.goalsTable ?? null; - fs.kpisTable = extra?.kpisTable ?? null; fs.docsTable = extra?.docsTable ?? null; fs.docsProject = extra?.docsProject ?? null; - // Ensure the memory table + goal/kpi tables exist before + // Ensure the memory table + goals table exist before // bootstrapping. Each ensure call is idempotent and lazy-heals // any column drift from prior schema versions. Failures bubble // up; the shell will report them but stay alive (the @@ -265,10 +261,6 @@ export class DeeplakeFs implements IFileSystem { try { await client.ensureGoalsTable(fs.goalsTable); } catch { /* keep bootstrap moving — goal routing degrades gracefully */ } } - if (fs.kpisTable) { - try { await client.ensureKpisTable(fs.kpisTable); } - catch { /* same — degrade gracefully */ } - } // Bootstrap memory + sessions metadata in parallel. let sessionSyncOk = true; @@ -278,8 +270,8 @@ export class DeeplakeFs implements IFileSystem { const rows = await client.query(sql); for (const row of rows) { const p = row["path"] as string; - // Goal/KPI-shaped paths belong exclusively to the dedicated - // hivemind_goals / hivemind_kpis tables. Pre-routing hook + // Goal-shaped paths belong exclusively to the dedicated + // hivemind_goals table. Pre-routing hook // versions (<=0.7.4) wrote goals to the generic memory table // as plain files; surfacing those here re-injects phantom // goals into the VFS goal namespace — visible in `ls /goal/...` @@ -289,7 +281,7 @@ export class DeeplakeFs implements IFileSystem { // configured, goal routing is off and these rows are the only // copy, so we keep them.) const kind = classifyPath(p); - if ((kind === "goal" && fs.goalsTable) || (kind === "kpi" && fs.kpisTable)) { + if (kind === "goal" && fs.goalsTable) { continue; } fs.files.set(p, null); @@ -334,8 +326,8 @@ export class DeeplakeFs implements IFileSystem { } })() : Promise.resolve(); - // Goals + KPIs bootstrap — read the latest version of each row in - // the structured tables and synthesize VFS paths for the cache. + // Goals bootstrap — read the latest version of each row in the + // structured table and synthesize VFS paths for the cache. // ls / cat then work naturally against the file map, while // writes route to upsertRow which dispatches by path classifier. const goalsBootstrap = fs.goalsTable ? (async () => { @@ -368,34 +360,7 @@ export class DeeplakeFs implements IFileSystem { } })() : Promise.resolve(); - const kpisBootstrap = fs.kpisTable ? (async () => { - try { - const kpiRows = await client.query( - // One row per (goal_id, kpi_id) (UPDATE-or-INSERT model). - `SELECT goal_id, kpi_id, content, created_at ` + - `FROM "${fs.kpisTable}" ORDER BY created_at DESC` - ); - for (const row of kpiRows) { - const goal_id = String(row["goal_id"] ?? ""); - const kpi_id = String(row["kpi_id"] ?? ""); - if (!goal_id || !kpi_id) continue; - const p = composeKpiPath({ goal_id, kpi_id }); - const content = String(row["content"] ?? ""); - fs.files.set(p, Buffer.from(content, "utf-8")); - fs.meta.set(p, { - size: Buffer.byteLength(content, "utf-8"), - mime: "text/markdown", - mtime: new Date(), - }); - fs.addToTree(p); - fs.flushed.add(p); - } - } catch { - // KPIs table may not exist yet — start empty. - } - })() : Promise.resolve(); - - await Promise.all([memoryBootstrap, sessionsBootstrap, goalsBootstrap, kpisBootstrap]); + await Promise.all([memoryBootstrap, sessionsBootstrap, goalsBootstrap]); return fs; } @@ -475,7 +440,7 @@ export class DeeplakeFs implements IFileSystem { } private async upsertRow(r: PendingRow, embedding: number[] | null): Promise { - // Path-routed structured tables: dispatch goal / kpi writes to + // Path-routed structured tables: dispatch goal writes to // the dedicated table with INSERT-only version-bump semantics. // The generic memory path falls through to the existing UPDATE / // INSERT shape below. Failures here propagate up to the flush @@ -485,10 +450,6 @@ export class DeeplakeFs implements IFileSystem { await this.upsertGoalRow(r); return; } - if (kind === "kpi" && this.kpisTable) { - await this.upsertKpiRow(r); - return; - } const text = esc(r.contentText); const p = esc(r.path); @@ -579,52 +540,6 @@ export class DeeplakeFs implements IFileSystem { this.flushed.add(r.path); } - /** - * UPDATE-or-INSERT for a KPI row, keyed by (goal_id, kpi_id). - * Same trade-off as upsertGoalRow — one row per KPI forever, - * no version proliferation. Progress bumps (Edit on the `current:` - * line) and any other content edits mutate the same row in place. - */ - private async upsertKpiRow(r: PendingRow): Promise { - if (!this.kpisTable) throw new Error("kpisTable not configured"); - const parts = decomposeKpiPath(r.path); - const safe = this.kpisTable; - const now = new Date().toISOString(); - const createdAt = r.creationDate ?? now; - const updatedAt = r.lastUpdateDate ?? createdAt; - const existing = await this.client.query( - `SELECT id FROM "${safe}" ` + - `WHERE goal_id = '${esc(parts.goal_id)}' AND kpi_id = '${esc(parts.kpi_id)}' LIMIT 1` - ); - if (existing.length > 0) { - // Preserve created_at — KPI progress edits keep their original - // creation time so the KPI list stays in stable creation order - // (created_at ASC). Edit time goes to updated_at. - await this.client.query( - `UPDATE "${safe}" SET ` + - `content = E'${esc(r.contentText)}', ` + - `updated_at = '${esc(updatedAt)}' ` + - `WHERE goal_id = '${esc(parts.goal_id)}' AND kpi_id = '${esc(parts.kpi_id)}'` - ); - } else { - const id = randomUUID(); - await this.client.query( - `INSERT INTO "${safe}" (id, goal_id, kpi_id, content, version, created_at, updated_at, agent, plugin_version) VALUES (` + - `'${id}', ` + - `'${esc(parts.goal_id)}', ` + - `'${esc(parts.kpi_id)}', ` + - `E'${esc(r.contentText)}', ` + - `1, ` + - `'${esc(createdAt)}', ` + - `'${esc(updatedAt)}', ` + - `'manual', ` + - `''` + - `)` - ); - } - this.flushed.add(r.path); - } - // ── Virtual index.md generation ──────────────────────────────────────────── private async generateVirtualIndex(): Promise { diff --git a/src/shell/deeplake-shell.ts b/src/shell/deeplake-shell.ts index 43710635f..24acb24ee 100644 --- a/src/shell/deeplake-shell.ts +++ b/src/shell/deeplake-shell.ts @@ -59,7 +59,6 @@ async function main(): Promise { const table = process.env["HIVEMIND_TABLE"] ?? "memory"; const sessionsTable = process.env["HIVEMIND_SESSIONS_TABLE"] ?? "sessions"; const goalsTable = process.env["HIVEMIND_GOALS_TABLE"] ?? config.goalsTableName; - const kpisTable = process.env["HIVEMIND_KPIS_TABLE"] ?? config.kpisTableName; const docsTable = process.env["HIVEMIND_DOCS_TABLE"] ?? config.docsTableName; const mount = process.env["HIVEMIND_MOUNT"] ?? "/"; @@ -71,7 +70,7 @@ async function main(): Promise { process.stderr.write(`Connecting to deeplake://${config.workspaceId}/${table} ...\n`); } - const fs = await DeeplakeFs.create(client, table, mount, sessionsTable, { goalsTable, kpisTable, docsTable, docsProject: deriveProjectKey(process.cwd()).key }); + const fs = await DeeplakeFs.create(client, table, mount, sessionsTable, { goalsTable, docsTable, docsProject: deriveProjectKey(process.cwd()).key }); if (!isOneShot) { const fileCount = fs.getAllPaths().filter(p => !!p).length; diff --git a/src/shell/goal-paths.ts b/src/shell/goal-paths.ts index 5a18ea955..24348b76b 100644 --- a/src/shell/goal-paths.ts +++ b/src/shell/goal-paths.ts @@ -1,36 +1,34 @@ /** - * Path classifier + decompose/compose helpers for goal and KPI paths - * inside the Deeplake VFS. + * Path classifier + decompose/compose helpers for goal paths inside + * the Deeplake VFS. * * The agent operates on a normal-looking filesystem under the memory * mount; the VFS in deeplake-fs.ts uses these helpers to detect goal - * and KPI paths and dispatch reads/writes to the dedicated - * hivemind_goals / hivemind_kpis tables instead of the generic memory - * table. Path encoding is the source of truth for owner / status / - * goal_id / kpi_id; the row `content` column stores only the - * descriptive markdown body. + * paths and dispatch reads/writes to the dedicated hivemind_goals + * table instead of the generic memory table. Path encoding is the + * source of truth for owner / status / goal_id; the row `content` + * column stores only the descriptive markdown body. * - * Path conventions (always absolute, always start with the memory + * Path convention (always absolute, always start with the memory * mount; the mount prefix itself is stripped before classification): * * /memory/goal///.md - * /memory/kpi//.md * * status ∈ {opened, in_progress, closed}. owner is the user_email or - * userName the agent reports. goal_id and kpi_id are stable UUID-ish - * slugs the agent generates at create time. + * userName the agent reports. goal_id is a stable UUID-ish slug the + * agent generates at create time. * - * Anything that does not match these prefixes is classified as - * "memory" and handled by the existing VFS code path. Malformed - * goal/kpi paths (wrong number of segments, missing .md extension, - * unknown status value) are also "memory" — the caller can decide to - * reject them at the write boundary if desired. + * Anything that does not match this prefix is classified as "memory" + * and handled by the existing VFS code path. Malformed goal paths + * (wrong number of segments, missing .md extension, unknown status + * value) are also "memory" — the caller can decide to reject them at + * the write boundary if desired. */ const VALID_STATUS = new Set(["opened", "in_progress", "closed"]); /** Classification result for a VFS path. */ -export type PathKind = "goal" | "kpi" | "memory"; +export type PathKind = "goal" | "memory"; export interface GoalPathParts { owner: string; @@ -38,11 +36,6 @@ export interface GoalPathParts { goal_id: string; } -export interface KpiPathParts { - goal_id: string; - kpi_id: string; -} - /** * Strip any leading mount prefix and split the remainder into path * segments. Returns `null` for empty paths. @@ -79,7 +72,7 @@ function segmentsUnderMemory(p: string): string[] | null { } /** - * Classify a VFS path into "goal", "kpi", or "memory". Performs the + * Classify a VFS path into "goal" or "memory". Performs the * minimum validation needed to dispatch — full validation (status * enum, segment count, .md extension) happens via decompose helpers. */ @@ -93,13 +86,6 @@ export function classifyPath(p: string): PathKind { } return "memory"; } - if (segs[0] === "kpi") { - // /memory/kpi//.md → 3 segs - if (segs.length === 3 && segs[2].endsWith(".md")) { - return "kpi"; - } - return "memory"; - } return "memory"; } @@ -128,25 +114,6 @@ export function decomposeGoalPath(p: string): GoalPathParts { }; } -/** - * Decompose a kpi path into (goal_id, kpi_id). Throws on malformed - * paths. - */ -export function decomposeKpiPath(p: string): KpiPathParts { - const segs = segmentsUnderMemory(p); - if (!segs || segs.length !== 3 || segs[0] !== "kpi") { - throw new Error(`Not a kpi path: ${p}`); - } - const filename = segs[2]; - if (!filename.endsWith(".md")) { - throw new Error(`KPI path must end with .md: ${p}`); - } - return { - goal_id: segs[1], - kpi_id: filename.slice(0, -".md".length), - }; -} - /** * Build the canonical goal path from its parts. Output matches the * VFS-internal form (no mount prefix) because that is what @@ -155,11 +122,3 @@ export function decomposeKpiPath(p: string): KpiPathParts { export function composeGoalPath(parts: GoalPathParts): string { return `/goal/${parts.owner}/${parts.status}/${parts.goal_id}.md`; } - -/** - * Build the canonical kpi path from its parts. Output matches the - * VFS-internal form (no mount prefix). - */ -export function composeKpiPath(parts: KpiPathParts): string { - return `/kpi/${parts.goal_id}/${parts.kpi_id}.md`; -} From dbc9a3a0b70ad926b3f658e781a8c644dd9131d3 Mon Sep 17 00:00:00 2001 From: Emanuele Fenocchi Date: Wed, 16 Sep 2026 21:55:47 +0000 Subject: [PATCH 2/4] test: drop KPI cases and config fixtures Remove the kpi CLI, path-classifier, VFS routing, ensureKpisTable and openclaw hivemind_kpi_add cases, drop kpisTableName from hand-built Config fixtures, and swap the tilde-path deny example to a plain memory path. --- tests/claude-code/cli-docs.test.ts | 2 +- tests/claude-code/cli-goal.test.ts | 180 +----------------- .../claude-code/deeplake-fs-coverage.test.ts | 94 ++------- tests/claude-code/deeplake-fs.test.ts | 18 +- tests/claude-code/flush-memory-wiring.test.ts | 2 +- tests/claude-code/flush-memory.test.ts | 2 +- ...inner-cli-spawn-windowshide-source.test.ts | 4 - .../claude-code/legacy-cap-migration.test.ts | 2 +- tests/claude-code/pre-tool-use.test.ts | 2 +- tests/claude-code/skillify-auto-pull.test.ts | 1 - tests/claude-code/spawn-wiki-worker.test.ts | 1 - tests/openclaw/hivemind-tools.test.ts | 79 +------- tests/shared/deeplake-api.test.ts | 59 ------ tests/shared/dir-config.test.ts | 1 - tests/shared/goal-paths.test.ts | 60 +----- tests/shared/graph/deeplake-pull.test.ts | 1 - tests/shared/graph/deeplake-push.test.ts | 1 - 17 files changed, 31 insertions(+), 478 deletions(-) diff --git a/tests/claude-code/cli-docs.test.ts b/tests/claude-code/cli-docs.test.ts index 014cac041..744c36aaf 100644 --- a/tests/claude-code/cli-docs.test.ts +++ b/tests/claude-code/cli-docs.test.ts @@ -74,7 +74,7 @@ const VALID_CONFIG = { token: "tok", orgId: "org", orgName: "OrgName", userName: "alice@activeloop.ai", workspaceId: "ws", apiUrl: "https://api", tableName: "memory", sessionsTableName: "sessions", skillsTableName: "skills", rulesTableName: "hivemind_rules", - goalsTableName: "g", kpisTableName: "k", docsTableName: "hivemind_docs", + goalsTableName: "g", docsTableName: "hivemind_docs", codebaseTableName: "codebase", memoryPath: "/tmp/mem", }; diff --git a/tests/claude-code/cli-goal.test.ts b/tests/claude-code/cli-goal.test.ts index 6ba674ce5..ab806a126 100644 --- a/tests/claude-code/cli-goal.test.ts +++ b/tests/claude-code/cli-goal.test.ts @@ -1,7 +1,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; /** - * CLI handler tests for `hivemind goal` / `hivemind kpi`. + * CLI handler tests for `hivemind goal`. * * Path B (CLI) is the only goal-write path that cursor / hermes / pi can * reach (their plugin hooks can't rewrite Write tool calls — see @@ -16,7 +16,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; */ const ensureGoalsTableMock = vi.fn(); -const ensureKpisTableMock = vi.fn(); const queryMock = vi.fn(); vi.mock("../../src/config.js", () => ({ @@ -33,12 +32,11 @@ vi.mock("../../src/deeplake-api.js", () => ({ _tableName: string, ) { /* nothing */ } ensureGoalsTable(name: string) { return ensureGoalsTableMock(name); } - ensureKpisTable(name: string) { return ensureKpisTableMock(name); } query(sql: string) { return queryMock(sql); } }, })); -import { runGoalCommand, runKpiCommand } from "../../src/commands/goal.js"; +import { runGoalCommand } from "../../src/commands/goal.js"; import { loadConfig } from "../../src/config.js"; const loadConfigMock = loadConfig as unknown as ReturnType; @@ -54,7 +52,6 @@ const VALID_CONFIG = { skillsTableName: "skills", rulesTableName: "hivemind_rules", goalsTableName: "hivemind_goals_test", - kpisTableName: "hivemind_kpis_test", memoryPath: "/tmp/mem", }; @@ -68,7 +65,6 @@ beforeEach(() => { stdout = []; stderr = []; ensureGoalsTableMock.mockReset().mockResolvedValue(undefined); - ensureKpisTableMock.mockReset().mockResolvedValue(undefined); queryMock.mockReset().mockResolvedValue([]); loadConfigMock.mockReset().mockReturnValue(VALID_CONFIG); stdoutSpy = vi.spyOn(process.stdout, "write").mockImplementation(((chunk: string | Uint8Array) => { @@ -122,18 +118,6 @@ describe("runGoalCommand — help & unknown sub", () => { }); }); -describe("runKpiCommand — help & unknown sub", () => { - it("prints kpi usage with no subcommand", async () => { - await runKpiCommand([]); - expect(allOut()).toContain("hivemind kpi — manage goal KPIs"); - }); - - it("exits 1 on unknown subcommand", async () => { - await expectExit(1, () => runKpiCommand(["wat"])); - expect(allErr()).toContain("unknown kpi subcommand: wat"); - }); -}); - // ── login gating ──────────────────────────────────────────────────────────── describe("runGoalCommand — requires login", () => { @@ -158,12 +142,6 @@ describe("runGoalCommand — requires login", () => { await expectExit(1, () => runGoalCommand(["done", "abc"])); expect(queryMock).not.toHaveBeenCalled(); }); - - it("kpi add also gates on login", async () => { - loadConfigMock.mockReturnValue(null); - await expectExit(1, () => runKpiCommand(["add", "g", "k", "5", "PRs"])); - expect(queryMock).not.toHaveBeenCalled(); - }); }); // ── goal add ──────────────────────────────────────────────────────────────── @@ -389,150 +367,9 @@ describe("runGoalCommand — done & progress", () => { }); }); -// ── kpi add ───────────────────────────────────────────────────────────────── - -describe("runKpiCommand — add", () => { - it("INSERTs a v1 row into the KPIs table with content carrying name/target/unit", async () => { - await runKpiCommand(["add", "g-uuid", "k-prs", "5", "PRs", "Pull requests shipped"]); - expect(ensureKpisTableMock).toHaveBeenCalledExactlyOnceWith("hivemind_kpis_test"); - expect(queryMock).toHaveBeenCalledTimes(1); - const sql = queryMock.mock.calls[0][0] as string; - expect(sql).toMatch(/^INSERT INTO "hivemind_kpis_test" \(id, goal_id, kpi_id, content, version, created_at, updated_at, agent, plugin_version\)/); - expect(sql).toContain("'g-uuid'"); - expect(sql).toContain("'k-prs'"); - expect(sql).toContain("'manual'"); - expect(sql).toContain(", 1, "); - // content body — source builds it with real "\n" (template literal) - expect(sql).toContain("Pull requests shipped\n\n- target: 5\n- current: 0\n- unit: PRs"); - expect(allOut()).toContain("g-uuid/k-prs"); - }); - - it("defaults the human-readable name to kpi_id when no [name] is given", async () => { - await runKpiCommand(["add", "g-uuid", "k-prs", "5", "PRs"]); - const sql = queryMock.mock.calls[0][0] as string; - expect(sql).toContain("k-prs\n\n- target: 5\n- current: 0\n- unit: PRs"); - }); - - it("rejects non-positive integer targets (silent skip would create a /-1 KPI)", async () => { - await expectExit(1, () => runKpiCommand(["add", "g", "k", "0", "x"])); - expect(allErr()).toContain("invalid target: 0"); - expect(queryMock).not.toHaveBeenCalled(); - - await expectExit(1, () => runKpiCommand(["add", "g", "k", "-3", "x"])); - expect(allErr()).toContain("invalid target: -3"); - - await expectExit(1, () => runKpiCommand(["add", "g", "k", "abc", "x"])); - expect(allErr()).toContain("invalid target: abc"); - }); - - it("rejects missing args with the usage line", async () => { - await expectExit(1, () => runKpiCommand(["add", "g", "k", "5"])); - expect(allErr()).toContain("usage: hivemind kpi add"); - expect(queryMock).not.toHaveBeenCalled(); - }); -}); - -// ── kpi list ──────────────────────────────────────────────────────────────── - -describe("runKpiCommand — list", () => { - it("SELECTs by goal_id, prints kpi_id + first content line as TSV", async () => { - queryMock.mockResolvedValueOnce([ - { kpi_id: "k1", content: "PRs shipped\n\n- target: 5" }, - { kpi_id: "k2", content: "Lines reviewed\n\n- target: 100" }, - ]); - await runKpiCommand(["list", "g-uuid"]); - const sql = queryMock.mock.calls[0][0] as string; - expect(sql).toContain(`WHERE goal_id = 'g-uuid'`); - expect(sql).toContain("ORDER BY created_at ASC LIMIT 50"); - expect(allOut()).toContain("k1\tPRs shipped\n"); - expect(allOut()).toContain("k2\tLines reviewed\n"); - }); - - it("prints '(no kpis)' on empty result", async () => { - queryMock.mockResolvedValueOnce([]); - await runKpiCommand(["list", "g-uuid"]); - expect(allOut()).toContain("(no kpis)"); - }); - - it("exits 1 with usage when goal_id is missing", async () => { - await expectExit(1, () => runKpiCommand(["list"])); - expect(allErr()).toContain("usage: hivemind kpi list"); - }); - - it("exits 1 with the API error message on query failure", async () => { - queryMock.mockRejectedValueOnce(new Error("read timeout")); - await expectExit(1, () => runKpiCommand(["list", "g-uuid"])); - expect(allErr()).toContain("hivemind kpi list: read timeout"); - }); -}); - -// ── kpi bump ──────────────────────────────────────────────────────────────── - -describe("runKpiCommand — bump", () => { - it("reads current content, rewrites the `- current: N` line, then UPDATEs", async () => { - queryMock - .mockResolvedValueOnce([ - { content: "PRs shipped\n\n- target: 5\n- current: 2\n- unit: PRs" }, - ]) - // UPDATE — empty result - .mockResolvedValueOnce([]); - await runKpiCommand(["bump", "g-uuid", "k-prs", "1"]); - // Heals the schema first so a preexisting table without `updated_at` can't fail. - expect(ensureKpisTableMock).toHaveBeenCalledExactlyOnceWith("hivemind_kpis_test"); - expect(queryMock).toHaveBeenCalledTimes(2); - const select = queryMock.mock.calls[0][0] as string; - expect(select).toMatch(/^SELECT content FROM "hivemind_kpis_test"/); - expect(select).toContain(`WHERE goal_id = 'g-uuid' AND kpi_id = 'k-prs'`); - const update = queryMock.mock.calls[1][0] as string; - expect(update).toMatch(/^UPDATE "hivemind_kpis_test"/); - expect(update).toContain("- current: 3"); - // make sure we didn't mistakenly clobber target / unit - expect(update).toContain("- target: 5"); - expect(update).toContain("- unit: PRs"); - expect(allOut()).toContain("g-uuid/k-prs +1"); - }); - - it("handles negative deltas (bump -2 should decrement)", async () => { - queryMock - .mockResolvedValueOnce([{ content: "x\n\n- current: 10\n- unit: count" }]) - .mockResolvedValueOnce([]); - await runKpiCommand(["bump", "g", "k", "-2"]); - const update = queryMock.mock.calls[1][0] as string; - expect(update).toContain("- current: 8"); - }); - - it("exits 1 when the KPI row doesn't exist (no UPDATE issued)", async () => { - queryMock.mockResolvedValueOnce([]); // SELECT returns nothing - await expectExit(1, () => runKpiCommand(["bump", "g", "k", "1"])); - expect(allErr()).toContain("kpi not found: g/k"); - // SELECT was issued once but no UPDATE - expect(queryMock).toHaveBeenCalledTimes(1); - }); - - it("exits 1 when the content has no `current:` line (no UPDATE issued)", async () => { - queryMock.mockResolvedValueOnce([ - { content: "PRs shipped\n\n- target: 5\n- unit: PRs" }, // missing `current:` - ]); - await expectExit(1, () => runKpiCommand(["bump", "g", "k", "1"])); - expect(allErr()).toContain("could not find 'current:' line"); - expect(queryMock).toHaveBeenCalledTimes(1); - }); - - it("rejects non-numeric delta", async () => { - await expectExit(1, () => runKpiCommand(["bump", "g", "k", "lots"])); - expect(allErr()).toContain("invalid delta: lots"); - expect(queryMock).not.toHaveBeenCalled(); - }); - - it("exits 1 with usage when args are missing", async () => { - await expectExit(1, () => runKpiCommand(["bump", "g", "k"])); - expect(allErr()).toContain("usage: hivemind kpi bump"); - }); -}); - // ── negative SQL patterns: UPDATE coalescing guard ────────────────────────── -describe("goal/kpi CLI — does NOT issue back-to-back UPDATEs on the same row", () => { +describe("goal CLI — does NOT issue back-to-back UPDATEs on the same row", () => { // Backend coalesces two rapid UPDATEs against the same row, silently // dropping one (see CLAUDE.md "UPDATE coalescing" note). The CLI must // never split a single logical mutation into two UPDATEs. @@ -541,15 +378,4 @@ describe("goal/kpi CLI — does NOT issue back-to-back UPDATEs on the same row", const updates = queryMock.mock.calls.filter(c => /^UPDATE\b/.test(c[0])); expect(updates).toHaveLength(1); }); - - it("`kpi bump` issues one SELECT + one UPDATE — never a second UPDATE on a side column", async () => { - queryMock - .mockResolvedValueOnce([{ content: "x\n\n- current: 1\n- unit: y" }]) - .mockResolvedValueOnce([]); - await runKpiCommand(["bump", "g", "k", "1"]); - const updates = queryMock.mock.calls.filter(c => /^UPDATE\b/.test(c[0])); - expect(updates).toHaveLength(1); - // sanity: SELECT preceded UPDATE - expect(/^SELECT/.test(queryMock.mock.calls[0][0])).toBe(true); - }); }); diff --git a/tests/claude-code/deeplake-fs-coverage.test.ts b/tests/claude-code/deeplake-fs-coverage.test.ts index a772bc884..5aa1db827 100644 --- a/tests/claude-code/deeplake-fs-coverage.test.ts +++ b/tests/claude-code/deeplake-fs-coverage.test.ts @@ -24,22 +24,19 @@ afterEach(() => { // ── Mock clients ────────────────────────────────────────────────────────────── interface GoalRow { id?: string; goal_id: string; owner: string; status: string; content: string; created_at?: string } -interface KpiRow { id?: string; goal_id: string; kpi_id: string; content: string; created_at?: string } -/** Stateful client backing the goal/kpi structured tables plus a generic +/** Stateful client backing the goals structured table plus a generic * memory table. Maintains in-memory arrays so UPDATE-vs-INSERT, bootstrap, * rm soft-close and mv status-transition all exercise real SQL shapes. */ -function makeGoalClient(init: { goals?: GoalRow[]; kpis?: KpiRow[]; memory?: string[] } = {}) { +function makeGoalClient(init: { goals?: GoalRow[]; memory?: string[] } = {}) { const goals: GoalRow[] = (init.goals ?? []).map(g => ({ id: g.id ?? `seed-${g.goal_id}`, ...g })); - const kpis: KpiRow[] = (init.kpis ?? []).map(k => ({ id: k.id ?? `seed-${k.goal_id}-${k.kpi_id}`, ...k })); const memory = [...(init.memory ?? [])]; const client = { applyStorageCreds: vi.fn().mockResolvedValue(undefined), ensureTable: vi.fn().mockResolvedValue(undefined), ensureGoalsTable: vi.fn().mockResolvedValue(undefined), - ensureKpisTable: vi.fn().mockResolvedValue(undefined), - listTables: vi.fn().mockResolvedValue(["memory", "goals", "kpis"]), + listTables: vi.fn().mockResolvedValue(["memory", "goals"]), query: vi.fn(async (sql: string) => { // ── bootstrap ── if (sql.includes("SELECT path, size_bytes, mime_type")) { @@ -48,9 +45,6 @@ function makeGoalClient(init: { goals?: GoalRow[]; kpis?: KpiRow[]; memory?: str if (sql.includes("SELECT goal_id, owner, status, content, created_at")) { return goals.map(g => ({ goal_id: g.goal_id, owner: g.owner, status: g.status, content: g.content, created_at: g.created_at ?? "2026-01-01" })); } - if (sql.includes("SELECT goal_id, kpi_id, content, created_at")) { - return kpis.map(k => ({ goal_id: k.goal_id, kpi_id: k.kpi_id, content: k.content, created_at: k.created_at ?? "2026-01-01" })); - } // ── goal upsert ── if (sql.startsWith("SELECT id") && sql.includes('"goals"')) { const gid = sql.match(/goal_id = '([^']+)'/)?.[1]; @@ -71,37 +65,17 @@ function makeGoalClient(init: { goals?: GoalRow[]; kpis?: KpiRow[]; memory?: str if (m) goals.push({ id: m[1], goal_id: m[2], owner: m[3], status: m[4], content: m[5].replace(/''/g, "'") }); return []; } - // ── kpi upsert ── - if (sql.startsWith("SELECT id") && sql.includes('"kpis"')) { - const gid = sql.match(/goal_id = '([^']+)'/)?.[1]; - const kid = sql.match(/kpi_id = '([^']+)'/)?.[1]; - return kpis.filter(k => k.goal_id === gid && k.kpi_id === kid).map(k => ({ id: k.id })); - } - if (sql.startsWith("UPDATE") && sql.includes('"kpis"')) { - const gid = sql.match(/WHERE goal_id = '([^']+)'/)?.[1]; - const kid = sql.match(/kpi_id = '([^']+)'/)?.[1]; - const row = kpis.find(k => k.goal_id === gid && k.kpi_id === kid); - if (row) row.content = (sql.match(/content = E'((?:[^']|'')*)'/)?.[1] ?? row.content).replace(/''/g, "'"); - return []; - } - if (sql.startsWith("INSERT") && sql.includes('"kpis"')) { - const m = sql.match(/VALUES \(\s*'([^']*)',\s*'([^']*)',\s*'([^']*)',\s*E'((?:[^']|'')*)'/); - if (m) kpis.push({ id: m[1], goal_id: m[2], kpi_id: m[3], content: m[4].replace(/''/g, "'") }); - return []; - } return []; }), _goals: goals, - _kpis: kpis, }; return client; } -async function makeGoalFs(init: { goals?: GoalRow[]; kpis?: KpiRow[]; memory?: string[] } = {}) { +async function makeGoalFs(init: { goals?: GoalRow[]; memory?: string[] } = {}) { const client = makeGoalClient(init); const fs = await DeeplakeFs.create(client as never, "memory", "/", "sessions", { goalsTable: "goals", - kpisTable: "kpis", }); return { fs, client }; } @@ -245,54 +219,29 @@ describe("goals bootstrap", () => { }); // ── Bootstrap null-coalescing (defensive ?? "" paths) ──────────────────────── -describe("goals/kpis bootstrap with null columns", () => { - function rawClient(goalRows: Record[], kpiRows: Record[]) { +describe("goals bootstrap with null columns", () => { + function rawClient(goalRows: Record[]) { return { applyStorageCreds: vi.fn().mockResolvedValue(undefined), ensureTable: vi.fn().mockResolvedValue(undefined), ensureGoalsTable: vi.fn().mockResolvedValue(undefined), - ensureKpisTable: vi.fn().mockResolvedValue(undefined), query: vi.fn(async (sql: string) => { if (sql.includes("SELECT goal_id, owner, status, content, created_at")) return goalRows; - if (sql.includes("SELECT goal_id, kpi_id, content, created_at")) return kpiRows; return []; }), }; } - it("coalesces null goal/kpi columns and keeps only well-formed rows", async () => { - const client = rawClient( - [ - { goal_id: null, owner: null, status: null, content: null }, // every field null → skipped - { goal_id: "g1", owner: "alice", status: "opened", content: null }, // valid path, null content → "" - ], - [ - { goal_id: null, kpi_id: null, content: null }, // skipped - { goal_id: "g1", kpi_id: "k1", content: null }, // valid, null content → "" - ], - ); + it("coalesces null goal columns and keeps only well-formed rows", async () => { + const client = rawClient([ + { goal_id: null, owner: null, status: null, content: null }, // every field null → skipped + { goal_id: "g1", owner: "alice", status: "opened", content: null }, // valid path, null content → "" + ]); const fs = await DeeplakeFs.create(client as never, "memory", "/", "sessions", { goalsTable: "goals", - kpisTable: "kpis", }); expect(await fs.readdir("/goal/alice/opened")).toEqual(["g1.md"]); expect(await fs.readFile("/goal/alice/opened/g1.md")).toBe(""); - expect(await fs.readdir("/kpi/g1")).toEqual(["k1.md"]); - expect(await fs.readFile("/kpi/g1/k1.md")).toBe(""); - }); -}); - -// ── KPIs bootstrap ────────────────────────────────────────────────────────── -describe("kpis bootstrap", () => { - it("synthesizes kpi paths and skips rows missing ids", async () => { - const { fs } = await makeGoalFs({ - kpis: [ - { goal_id: "g1", kpi_id: "k1", content: "kpi-body" }, - { goal_id: "", kpi_id: "k2", content: "skip" }, // skip (327) - ], - }); - expect(await fs.readdir("/kpi/g1")).toEqual(["k1.md"]); - expect(await fs.readFile("/kpi/g1/k1.md")).toBe("kpi-body"); }); }); @@ -321,27 +270,6 @@ describe("goal write routing", () => { }); }); -// ── KPI write routing (upsertRow → upsertKpiRow) ───────────────────────────── -describe("kpi write routing", () => { - it("INSERTs a new kpi into the kpis table on flush", async () => { - const { fs, client } = await makeGoalFs({}); - await fs.writeFile("/kpi/g1/k1.md", "metric"); - await fs.flush(); - expect(client._kpis).toContainEqual(expect.objectContaining({ goal_id: "g1", kpi_id: "k1", content: "metric" })); - }); - - it("UPDATEs an existing kpi row in place", async () => { - const { fs, client } = await makeGoalFs({ - kpis: [{ goal_id: "g1", kpi_id: "k1", content: "0" }], - }); - await fs.writeFile("/kpi/g1/k1.md", "42"); - await fs.flush(); - expect(client._kpis.find(k => k.kpi_id === "k1")!.content).toBe("42"); - const updates = (client.query.mock.calls as [string][]).filter(c => c[0].startsWith("UPDATE") && c[0].includes('"kpis"')); - expect(updates.length).toBe(1); - }); -}); - // ── rm goal soft-close ──────────────────────────────────────────────────────── describe("rm goal soft-close", () => { it("moves an opened goal to closed/ instead of deleting", async () => { diff --git a/tests/claude-code/deeplake-fs.test.ts b/tests/claude-code/deeplake-fs.test.ts index 1213c42bf..82b9e447c 100644 --- a/tests/claude-code/deeplake-fs.test.ts +++ b/tests/claude-code/deeplake-fs.test.ts @@ -234,7 +234,7 @@ async function makeFs(seed: Record = {}, mount = "/memo return { fs, client }; } -// ── goal/kpi namespace isolation from the generic memory table ──────────────── +// ── goal namespace isolation from the generic memory table ──────────────────── // Regression for the VFS↔goals-table version skew: pre-routing hook versions // (<=0.7.4) wrote goals as plain files into the generic memory table. The // bootstrap must NOT re-surface those goal-shaped memory rows into the VFS goal @@ -248,8 +248,7 @@ function makeSkewClient(opts: { applyStorageCreds: vi.fn().mockResolvedValue(undefined), ensureTable: vi.fn().mockResolvedValue(undefined), ensureGoalsTable: vi.fn().mockResolvedValue(undefined), - ensureKpisTable: vi.fn().mockResolvedValue(undefined), - listTables: vi.fn().mockResolvedValue(["memory", "goals", "kpis"]), + listTables: vi.fn().mockResolvedValue(["memory", "goals"]), query: vi.fn().mockImplementation(async (sql: string) => { if (sql.includes("SELECT path, size_bytes, mime_type")) { return opts.memoryPaths.map(p => ({ path: p, size_bytes: 1, mime_type: "text/markdown" })); @@ -262,7 +261,7 @@ function makeSkewClient(opts: { }; } -describe("DeeplakeFs goal/kpi namespace isolation", () => { +describe("DeeplakeFs goal namespace isolation", () => { it("excludes legacy goal-shaped rows from the memory table when goalsTable is set", async () => { const client = makeSkewClient({ // Legacy phantom goal written to the generic memory table by the old hook, @@ -273,7 +272,6 @@ describe("DeeplakeFs goal/kpi namespace isolation", () => { }); const fs = await DeeplakeFs.create(client as never, "memory", "/", "sessions", { goalsTable: "goals", - kpisTable: "kpis", }); const opened = await fs.readdir("/goal/alice/opened"); expect(opened).toContain("real.md"); // structured goal surfaces @@ -297,8 +295,7 @@ describe("DeeplakeFs goal/kpi namespace isolation", () => { applyStorageCreds: vi.fn().mockResolvedValue(undefined), ensureTable: vi.fn().mockResolvedValue(undefined), ensureGoalsTable: vi.fn().mockResolvedValue(undefined), - ensureKpisTable: vi.fn().mockResolvedValue(undefined), - listTables: vi.fn().mockResolvedValue(["memory", "goals", "kpis"]), + listTables: vi.fn().mockResolvedValue(["memory", "goals"]), query: vi.fn().mockImplementation(async (q: string) => { sql.push(q); if (q.includes("SELECT path, size_bytes, mime_type")) return []; @@ -312,7 +309,6 @@ describe("DeeplakeFs goal/kpi namespace isolation", () => { }; const fs = await DeeplakeFs.create(client as never, "memory", "/", "sessions", { goalsTable: "goals", - kpisTable: "kpis", }); await fs.mv("/goal/alice/opened/g1.md", "/goal/alice/closed/g1.md"); @@ -329,8 +325,7 @@ describe("DeeplakeFs goal/kpi namespace isolation", () => { applyStorageCreds: vi.fn().mockResolvedValue(undefined), ensureTable: vi.fn().mockResolvedValue(undefined), ensureGoalsTable: vi.fn().mockResolvedValue(undefined), - ensureKpisTable: vi.fn().mockResolvedValue(undefined), - listTables: vi.fn().mockResolvedValue(["memory", "goals", "kpis"]), + listTables: vi.fn().mockResolvedValue(["memory", "goals"]), query: vi.fn().mockImplementation(async (q: string) => { sql.push(q); // upsertGoalRow existence check → no row, take the INSERT branch. @@ -339,7 +334,6 @@ describe("DeeplakeFs goal/kpi namespace isolation", () => { }; const fs = await DeeplakeFs.create(client as never, "memory", "/", "sessions", { goalsTable: "goals", - kpisTable: "kpis", }); await fs.writeFileWithMeta("/goal/alice/opened/g2.md", "do it later", { @@ -1345,7 +1339,7 @@ describe("docs VFS routing in the shell", () => { return { query: vi.fn(async (sql: string) => onQuery(sql)), ensureTable: async () => {}, - ensureGoalsTable: async () => {}, ensureKpisTable: async () => {}, + ensureGoalsTable: async () => {}, }; } diff --git a/tests/claude-code/flush-memory-wiring.test.ts b/tests/claude-code/flush-memory-wiring.test.ts index ab93df4e4..b9b0550bd 100644 --- a/tests/claude-code/flush-memory-wiring.test.ts +++ b/tests/claude-code/flush-memory-wiring.test.ts @@ -39,7 +39,7 @@ const NOW = "2026-06-16T00:00:00.000Z"; const cfg: Config = { token: "t", orgId: "o", orgName: "Org", userName: "u", workspaceId: "w", apiUrl: "http://x", tableName: "mem", sessionsTableName: "s", skillsTableName: "sk", rulesTableName: "r", - goalsTableName: "g", kpisTableName: "k", docsTableName: "d", codebaseTableName: "c", memoryPath: "/m", + goalsTableName: "g", docsTableName: "d", codebaseTableName: "c", memoryPath: "/m", }; beforeEach(() => { diff --git a/tests/claude-code/flush-memory.test.ts b/tests/claude-code/flush-memory.test.ts index 458cee017..7ede1d743 100644 --- a/tests/claude-code/flush-memory.test.ts +++ b/tests/claude-code/flush-memory.test.ts @@ -25,7 +25,7 @@ const NOW = "2026-06-16T00:00:00.000Z"; const fakeConfig: Config = { token: "t", orgId: "o", orgName: "OrgName", userName: "user", workspaceId: "w", apiUrl: "http://x", tableName: "memtable", sessionsTableName: "s", skillsTableName: "sk", - rulesTableName: "r", goalsTableName: "g", kpisTableName: "k", docsTableName: "d", codebaseTableName: "c", + rulesTableName: "r", goalsTableName: "g", docsTableName: "d", codebaseTableName: "c", memoryPath: "/m", }; diff --git a/tests/claude-code/inner-cli-spawn-windowshide-source.test.ts b/tests/claude-code/inner-cli-spawn-windowshide-source.test.ts index 759184a47..73da1928c 100644 --- a/tests/claude-code/inner-cli-spawn-windowshide-source.test.ts +++ b/tests/claude-code/inner-cli-spawn-windowshide-source.test.ts @@ -33,10 +33,6 @@ describe("inner CLI spawn windowsHide — source guards", () => { expect(src("src/skillify/claude-model.ts")).toMatch(/spawn\(\s*findAgentBin\([^;]*windowsHide:\s*true/); }); - it("commit-kpi-extract detached CLI spawn passes windowsHide", () => { - expect(src("src/hooks/commit-kpi-extract.ts")).toMatch(/spawn\(\s*cli\.bin[^)]*windowsHide:\s*true/); - }); - // The helper LOOKUPS, not the CLI spawns. These run `where.exe` on Windows // on the way to launching a detached worker, so without CREATE_NO_WINDOW // each one allocates its own visible window — the same flash the CLI spawns diff --git a/tests/claude-code/legacy-cap-migration.test.ts b/tests/claude-code/legacy-cap-migration.test.ts index 158c80300..e807e31d1 100644 --- a/tests/claude-code/legacy-cap-migration.test.ts +++ b/tests/claude-code/legacy-cap-migration.test.ts @@ -565,7 +565,7 @@ describe("autoPullSkills — invokes legacy-cap migration before the network", ( token: "tok", orgId: "org", orgName: "O", userName: "u", workspaceId: "default", apiUrl: "https://api.deeplake.ai", tableName: "memory", sessionsTableName: "sessions", skillsTableName: "skills", - rulesTableName: "r", goalsTableName: "g", kpisTableName: "k", + rulesTableName: "r", goalsTableName: "g", docsTableName: "d", codebaseTableName: "c", memoryPath: join(fakeHome, ".deeplake", "memory"), }) as any; diff --git a/tests/claude-code/pre-tool-use.test.ts b/tests/claude-code/pre-tool-use.test.ts index c1aa123c2..b076896b6 100644 --- a/tests/claude-code/pre-tool-use.test.ts +++ b/tests/claude-code/pre-tool-use.test.ts @@ -457,7 +457,7 @@ describe("pre-tool-use: Write / Edit on memory paths are denied with Bash guidan it("denies Write with tilde-prefixed memory path", () => { const r = runPreToolUse("Write", { - file_path: "~/.deeplake/memory/kpi/g/k.md", + file_path: "~/.deeplake/memory/notes/g/k.md", content: "x", }); expect(r.empty).toBe(false); diff --git a/tests/claude-code/skillify-auto-pull.test.ts b/tests/claude-code/skillify-auto-pull.test.ts index 56c7b0456..928be8fd8 100644 --- a/tests/claude-code/skillify-auto-pull.test.ts +++ b/tests/claude-code/skillify-auto-pull.test.ts @@ -72,7 +72,6 @@ function makeConfig(): Config { skillsTableName: "skills", rulesTableName: "hivemind_rules", goalsTableName: "hivemind_goals", - kpisTableName: "hivemind_kpis", docsTableName: "hivemind_docs", codebaseTableName: "codebase", memoryPath: join(tmpHome, ".deeplake", "memory"), diff --git a/tests/claude-code/spawn-wiki-worker.test.ts b/tests/claude-code/spawn-wiki-worker.test.ts index 7857c0d5f..98e0ea26d 100644 --- a/tests/claude-code/spawn-wiki-worker.test.ts +++ b/tests/claude-code/spawn-wiki-worker.test.ts @@ -128,7 +128,6 @@ function fakeConfig(): Config { skillsTableName: "skills", rulesTableName: "hivemind_rules", goalsTableName: "hivemind_goals", - kpisTableName: "hivemind_kpis", docsTableName: "hivemind_docs", codebaseTableName: "codebase", memoryPath: "/tmp/fake-memory", diff --git a/tests/openclaw/hivemind-tools.test.ts b/tests/openclaw/hivemind-tools.test.ts index f7b03ff36..243a9494e 100644 --- a/tests/openclaw/hivemind-tools.test.ts +++ b/tests/openclaw/hivemind-tools.test.ts @@ -4,14 +4,14 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; * Integration tests for the agent-facing tools registered by the openclaw * hivemind plugin: * - hivemind_search / hivemind_read / hivemind_index (read-side) - * - hivemind_goal_add / hivemind_kpi_add (write-side, team-shared - * goals + KPIs — openclaw can't intercept Write tool calls so it must + * - hivemind_goal_add (write-side, team-shared + * goals — openclaw can't intercept Write tool calls so it must * expose explicit tools instead of the VFS Path A used by claude-code / * codex; see PR #193 body, section "runtime intercept scope"). * * Tests mock DeeplakeApi at the SQL-query boundary and assert that: * 1. read-side queries target BOTH memory + sessions tables - * 2. write-side INSERTs into the goal/kpi tables under the expected shape + * 2. write-side INSERTs into the goals table under the expected shape */ const queryMock = vi.fn(); @@ -19,7 +19,6 @@ const listTablesMock = vi.fn(); const ensureSessionsTableMock = vi.fn(); const ensureTableMock = vi.fn(); const ensureGoalsTableMock = vi.fn(); -const ensureKpisTableMock = vi.fn(); const loadConfigMock = vi.fn(); const loadCredsMock = vi.fn(); const handleGraphVfsMock = vi.fn(); @@ -58,7 +57,6 @@ vi.mock("../../src/deeplake-api.js", () => ({ ensureSessionsTable(n: string) { return ensureSessionsTableMock(n); } ensureTable() { return ensureTableMock(); } ensureGoalsTable(n: string) { return ensureGoalsTableMock(n); } - ensureKpisTable(n: string) { return ensureKpisTableMock(n); } }, })); @@ -95,7 +93,6 @@ beforeEach(() => { ensureSessionsTableMock.mockReset().mockResolvedValue(undefined); ensureTableMock.mockReset().mockResolvedValue(undefined); ensureGoalsTableMock.mockReset().mockResolvedValue(undefined); - ensureKpisTableMock.mockReset().mockResolvedValue(undefined); loadCredsMock.mockReset().mockReturnValue({ token: "tok", orgId: "o", orgName: "acme", userName: "alice", }); @@ -110,7 +107,6 @@ beforeEach(() => { sessionsTableName: "sessions", skillsTableName: "skills", goalsTableName: "hivemind_goals_test", - kpisTableName: "hivemind_kpis_test", memoryPath: "/tmp/mem", }); }); @@ -123,7 +119,6 @@ describe("openclaw hivemind tools — registration", () => { "hivemind_graph_neighborhood", "hivemind_graph_search", "hivemind_index", - "hivemind_kpi_add", "hivemind_read", "hivemind_search", ]); @@ -372,7 +367,7 @@ describe("hivemind_goal_add (Path C — write-side via registered tool)", () => expect(sql).toMatch(/E'ship the goals feature'/); // result echoes the generated goal_id back to the agent so it can use it - // in a follow-up hivemind_kpi_add call + // in follow-up calls const text = result.content[0].text; expect(text).toContain("Goal created"); expect(text).toContain("owner: alice"); @@ -409,72 +404,6 @@ describe("hivemind_goal_add (Path C — write-side via registered tool)", () => }); }); -describe("hivemind_kpi_add (Path C — write-side via registered tool)", () => { - it("INSERTs into the configured KPIs table with content carrying name/target/unit", async () => { - queryMock.mockResolvedValue([]); - const { tools } = await loadPluginWithTools(); - const kpiAdd = tools.find(t => t.name === "hivemind_kpi_add")!; - const result = await kpiAdd.execute("call-kpi-1", { - goal_id: "11111111-2222-3333-4444-555555555555", - kpi_id: "k-prs", - target: 5, - unit: "PRs", - name: "Pull requests shipped", - }); - - expect(ensureKpisTableMock).toHaveBeenCalledWith("hivemind_kpis_test"); - const kpiInserts = queryMock.mock.calls.filter(c => /INSERT INTO "hivemind_kpis_test"/.test(c[0])); - expect(kpiInserts).toHaveLength(1); - const sql = kpiInserts[0][0] as string; - - expect(sql).toMatch(/INSERT INTO "hivemind_kpis_test" \(id, goal_id, kpi_id, content, version, created_at, updated_at, agent, plugin_version\)/); - expect(sql).toContain("'11111111-2222-3333-4444-555555555555'"); - expect(sql).toContain("'k-prs'"); - expect(sql).toContain("'openclaw'"); - // content is a markdown body with target/current/unit lines — the source - // builds it with real "\n" characters via template literals, so the SQL - // text contains literal newlines (NOT backslash-n escape sequences). - expect(sql).toContain("Pull requests shipped\n\n- target: 5\n- current: 0\n- unit: PRs"); - - expect(result.content[0].text).toContain("KPI added"); - expect(result.content[0].text).toContain("target: 5 PRs"); - }); - - it("defaults the human-readable name to kpi_id when name is omitted", async () => { - queryMock.mockResolvedValue([]); - const { tools } = await loadPluginWithTools(); - const kpiAdd = tools.find(t => t.name === "hivemind_kpi_add")!; - await kpiAdd.execute("call-kpi-2", { - goal_id: "abc", kpi_id: "k-noname", target: 1, unit: "count", - }); - const sql = (queryMock.mock.calls.find(c => /INSERT INTO/.test(c[0]))![0]) as string; - expect(sql).toContain("k-noname\n\n- target: 1\n- current: 0\n- unit: count"); - }); - - it("returns a friendly error and logs when the INSERT throws", async () => { - queryMock.mockRejectedValue(new Error("table missing")); - const { tools, mockApi } = await loadPluginWithTools(); - const kpiAdd = tools.find(t => t.name === "hivemind_kpi_add")!; - const result = await kpiAdd.execute("call-kpi-3", { - goal_id: "g", kpi_id: "k", target: 1, unit: "x", - }); - expect(result.content[0].text).toMatch(/KPI add failed: table missing/); - expect(mockApi.logger.error).toHaveBeenCalled(); - }); - - it("returns 'Not logged in' (no INSERT) when config is missing", async () => { - loadConfigMock.mockReturnValue(null); - const { tools } = await loadPluginWithTools(); - const kpiAdd = tools.find(t => t.name === "hivemind_kpi_add")!; - const result = await kpiAdd.execute("call-kpi-4", { - goal_id: "g", kpi_id: "k", target: 1, unit: "x", - }); - expect(result.content[0].text).toMatch(/Not logged in/); - expect(queryMock).not.toHaveBeenCalled(); - expect(ensureKpisTableMock).not.toHaveBeenCalled(); - }); -}); - describe("hivemind_graph_search", () => { it("queries graph/query/ via handleGraphVfs", async () => { const { tools } = await loadPluginWithTools(); diff --git a/tests/shared/deeplake-api.test.ts b/tests/shared/deeplake-api.test.ts index 361faccea..8555db3d8 100644 --- a/tests/shared/deeplake-api.test.ts +++ b/tests/shared/deeplake-api.test.ts @@ -507,7 +507,6 @@ import { SKILLS_COLUMNS, RULES_COLUMNS, GOALS_COLUMNS, - KPIS_COLUMNS, DOCS_COLUMNS, CODEBASE_COLUMNS, } from "../../src/deeplake-schema.js"; @@ -1160,64 +1159,6 @@ describe("DeeplakeApi.ensureDocsTable", () => { }); }); -// ── ensureKpisTable ───────────────────────────────────────────────────────── - -describe("DeeplakeApi.ensureKpisTable", () => { - it("creates kpis table when missing; heals after CREATE; emits (goal_id, kpi_id) lookup index", async () => { - mockFetch.mockResolvedValueOnce({ - ok: true, status: 200, - json: async () => ({ tables: [] }), - }); - mockFetch.mockResolvedValueOnce(jsonResponse({})); // CREATE TABLE - mockFetch.mockResolvedValueOnce(infoSchemaResponse(allOf(KPIS_COLUMNS))); // post-CREATE heal SELECT - mockFetch.mockResolvedValueOnce(jsonResponse({})); // CREATE INDEX - const api = makeApi(); - await api.ensureKpisTable("hivemind_kpis"); - expect(mockFetch).toHaveBeenCalledTimes(4); - - const createSql = JSON.parse(mockFetch.mock.calls[1][1].body).query; - expect(createSql).toContain(`CREATE TABLE IF NOT EXISTS "hivemind_kpis"`); - expect(createSql).toContain("goal_id TEXT NOT NULL DEFAULT ''"); - expect(createSql).toContain("kpi_id TEXT NOT NULL DEFAULT ''"); - // KPIs do NOT carry owner — ownership derives from the parent goal. - expect(createSql).not.toMatch(/\bowner TEXT/); - - const indexSql = JSON.parse(mockFetch.mock.calls[3][1].body).query; - expect(indexSql).toContain(`"hivemind_kpis"`); - expect(indexSql).toContain(`("goal_id", "kpi_id")`); - }); - - it("heals after CREATE: missing kpi_id column gets ALTERed before returning", async () => { - mockFetch.mockResolvedValueOnce({ - ok: true, status: 200, - json: async () => ({ tables: [] }), - }); - mockFetch.mockResolvedValueOnce(jsonResponse({})); // CREATE - const legacy = allOf(KPIS_COLUMNS).filter(c => c !== "kpi_id"); - mockFetch.mockResolvedValueOnce(infoSchemaResponse(legacy)); // heal SELECT - mockFetch.mockResolvedValueOnce(jsonResponse({})); // ALTER kpi_id - mockFetch.mockResolvedValueOnce(jsonResponse({})); // CREATE INDEX - const api = makeApi(); - await api.ensureKpisTable("hivemind_kpis"); - const alterSql = JSON.parse(mockFetch.mock.calls[3][1].body).query; - expect(alterSql).toBe(`ALTER TABLE "hivemind_kpis" ADD COLUMN kpi_id TEXT NOT NULL DEFAULT ''`); - }); - - it("on existing kpis table fully up-to-date: no ALTER fires", async () => { - mockFetch.mockResolvedValueOnce({ - ok: true, status: 200, - json: async () => ({ tables: [{ table_name: "hivemind_kpis" }] }), - }); - mockFetch.mockResolvedValueOnce(infoSchemaResponse(allOf(KPIS_COLUMNS))); - mockFetch.mockResolvedValueOnce(jsonResponse({})); // CREATE INDEX - const api = makeApi(); - await api.ensureKpisTable("hivemind_kpis"); - const allSql = mockFetch.mock.calls.filter(c => c[1]?.body).map(c => JSON.parse(c[1].body).query).join(" | "); - expect(allSql).not.toContain("ALTER TABLE"); - expect(allSql).not.toContain("CREATE TABLE"); - }); -}); - // ── traceSql coverage ───────────────────────────────────────────────────── describe("traceSql (indirect, via query() with trace env set)", () => { const stderrSpy = vi.spyOn(process.stderr, "write"); diff --git a/tests/shared/dir-config.test.ts b/tests/shared/dir-config.test.ts index cac61de70..22a1267c1 100644 --- a/tests/shared/dir-config.test.ts +++ b/tests/shared/dir-config.test.ts @@ -24,7 +24,6 @@ function base(): Config { skillsTableName: "skills", rulesTableName: "hivemind_rules", goalsTableName: "hivemind_goals", - kpisTableName: "hivemind_kpis", codebaseTableName: "codebase", docsTableName: "docs", memoryPath: "/tmp/mem", diff --git a/tests/shared/goal-paths.test.ts b/tests/shared/goal-paths.test.ts index 34512b7bb..d37bed561 100644 --- a/tests/shared/goal-paths.test.ts +++ b/tests/shared/goal-paths.test.ts @@ -3,14 +3,12 @@ import { describe, expect, it } from "vitest"; import { classifyPath, composeGoalPath, - composeKpiPath, decomposeGoalPath, - decomposeKpiPath, } from "../../src/shell/goal-paths.js"; /** * Pure classifier — no I/O, no DB — but it's the dispatch boundary - * between the goals/kpis tables and the generic memory table inside + * between the goals table and the generic memory table inside * the VFS. A regression here silently routes goal writes back into the * memory table (no `WHERE goal_id` queryability ever) so the * cross-agent rollout starts losing rows without any explicit error. @@ -55,27 +53,8 @@ describe("classifyPath", () => { }); }); - describe("kpi paths", () => { - it("classifies the canonical mount-relative form", () => { - expect(classifyPath("/kpi/g-uuid/k-prs.md")).toBe("kpi"); - }); - - it("classifies the /memory/ host-FS form", () => { - expect(classifyPath("/home/emanuele/.deeplake/memory/kpi/g-uuid/k-prs.md")).toBe("kpi"); - }); - - it("rejects missing .md", () => { - expect(classifyPath("/kpi/g-uuid/k-prs")).toBe("memory"); - }); - - it("rejects wrong segment count", () => { - expect(classifyPath("/kpi/g-uuid")).toBe("memory"); - expect(classifyPath("/kpi/g-uuid/k-prs/extra.md")).toBe("memory"); - }); - }); - describe("memory paths", () => { - it("treats anything outside goal/ and kpi/ as memory", () => { + it("treats anything outside goal/ as memory", () => { expect(classifyPath("/summaries/alice/abc.md")).toBe("memory"); expect(classifyPath("/foo/bar.md")).toBe("memory"); expect(classifyPath("/")).toBe("memory"); @@ -90,7 +69,6 @@ describe("classifyPath", () => { it("strips trailing slashes consistently", () => { expect(classifyPath("/goal/alice/opened/uuid.md/")).toBe("goal"); - expect(classifyPath("/kpi/g/k.md/")).toBe("kpi"); }); }); }); @@ -119,7 +97,6 @@ describe("decomposeGoalPath", () => { it("throws on a non-goal path so callers can't accidentally treat memory rows as goals", () => { expect(() => decomposeGoalPath("/summaries/alice/abc.md")).toThrow(/Not a goal path/); - expect(() => decomposeGoalPath("/kpi/g/k.md")).toThrow(/Not a goal path/); }); it("throws on an invalid status (no row should ever land with status='wat')", () => { @@ -131,31 +108,6 @@ describe("decomposeGoalPath", () => { }); }); -describe("decomposeKpiPath", () => { - it("extracts goal_id / kpi_id from a canonical path", () => { - expect(decomposeKpiPath("/kpi/g-uuid/k-prs.md")).toEqual({ - goal_id: "g-uuid", - kpi_id: "k-prs", - }); - }); - - it("handles the host-FS /memory/ prefix", () => { - expect(decomposeKpiPath("/home/x/.deeplake/memory/kpi/g/k.md")).toEqual({ - goal_id: "g", - kpi_id: "k", - }); - }); - - it("throws on non-kpi paths", () => { - expect(() => decomposeKpiPath("/goal/o/opened/uuid.md")).toThrow(/Not a kpi path/); - expect(() => decomposeKpiPath("/summaries/x.md")).toThrow(/Not a kpi path/); - }); - - it("throws when the leaf is missing .md", () => { - expect(() => decomposeKpiPath("/kpi/g/k")).toThrow(/must end with \.md/); - }); -}); - describe("compose round-trip", () => { it("composeGoalPath ↔ decomposeGoalPath is identity for valid parts", () => { const original = { owner: "alice@activeloop.ai", status: "in_progress" as const, goal_id: "u-1" }; @@ -164,15 +116,7 @@ describe("compose round-trip", () => { expect(decomposeGoalPath(p)).toEqual(original); }); - it("composeKpiPath ↔ decomposeKpiPath is identity", () => { - const original = { goal_id: "g-1", kpi_id: "k-prs" }; - const p = composeKpiPath(original); - expect(p).toBe("/kpi/g-1/k-prs.md"); - expect(decomposeKpiPath(p)).toEqual(original); - }); - it("composed paths always classify as their kind", () => { expect(classifyPath(composeGoalPath({ owner: "x", status: "opened", goal_id: "u" }))).toBe("goal"); - expect(classifyPath(composeKpiPath({ goal_id: "g", kpi_id: "k" }))).toBe("kpi"); }); }); diff --git a/tests/shared/graph/deeplake-pull.test.ts b/tests/shared/graph/deeplake-pull.test.ts index 4c8f48c79..ccd9f87fd 100644 --- a/tests/shared/graph/deeplake-pull.test.ts +++ b/tests/shared/graph/deeplake-pull.test.ts @@ -30,7 +30,6 @@ function makeConfig(): Config { skillsTableName: "skills", rulesTableName: "hivemind_rules", goalsTableName: "hivemind_goals", - kpisTableName: "hivemind_kpis", docsTableName: "hivemind_docs", codebaseTableName: "codebase_test", memoryPath: "/tmp/mem", diff --git a/tests/shared/graph/deeplake-push.test.ts b/tests/shared/graph/deeplake-push.test.ts index 11ba4aab8..806d40236 100644 --- a/tests/shared/graph/deeplake-push.test.ts +++ b/tests/shared/graph/deeplake-push.test.ts @@ -19,7 +19,6 @@ function makeConfig(): Config { skillsTableName: "skills", rulesTableName: "hivemind_rules", goalsTableName: "hivemind_goals", - kpisTableName: "hivemind_kpis", docsTableName: "hivemind_docs", codebaseTableName: "codebase_test", memoryPath: "/tmp/mem", From dbdfe9b1bf6aee1d9e70a8c0098eb21dcae0e5d2 Mon Sep 17 00:00:00 2001 From: Emanuele Fenocchi Date: Wed, 16 Sep 2026 22:01:20 +0000 Subject: [PATCH 3/4] docs: remove KPIs from the goals skill, README and knowledge docs Strip the KPI sections, allowed-tools entry and commit auto-progress notes from the four hivemind-goals skills, retitle the README section to Goals, and drop the kpis table, kpi path routing and hivemind_kpi_add tool from the knowledge docs. The dashboard KPI cards are a different feature and are untouched. --- README.md | 8 +- .../skills/hivemind-goals/SKILL.md | 73 +++---------------- harnesses/codex/SUBMISSION.md | 2 +- .../codex/skills/hivemind-goals/SKILL.md | 67 +++-------------- .../hermes/skills/hivemind-goals/SKILL.md | 16 ++-- .../openclaw/skills/hivemind-goals/SKILL.md | 11 +-- .../private/architecture/system-overview.md | 4 +- .../private/data/deeplake-tables-schema.md | 35 ++------- .../private/data/memory-virtual-filesystem.md | 25 +++---- .../multi-tenant/org-workspace-model.md | 1 - library/knowledge/private/overview.md | 2 +- .../private/plugins/integration-model.md | 4 +- .../plugins/mcp-and-extension-surfaces.md | 5 +- src/shell/deeplake-fs.ts | 3 +- 14 files changed, 60 insertions(+), 196 deletions(-) diff --git a/README.md b/README.md index e168c894c..325200a97 100644 --- a/README.md +++ b/README.md @@ -495,7 +495,7 @@ Generation shells out to a host agent's own CLI (`claude -p`, `codex exec`, …) ## Rules (cross-agent team principles) -Hivemind **shares team rules across every agent in the org**, injected at SessionStart so every claude-code / cursor / hermes session starts knowing them. For personal or team work items with progress tracking, use [Goals + KPIs](#goals--kpis) (VFS-backed) instead. +Hivemind **shares team rules across every agent in the org**, injected at SessionStart so every claude-code / cursor / hermes session starts knowing them. For personal or team work items with progress tracking, use [Goals](#goals) (VFS-backed) instead. ```bash hivemind rules add "no DROP TABLE on prod creds" @@ -525,9 +525,9 @@ fall back to `hivemind context`): - `HIVEMIND_RULES_TABLE`: table name (default `hivemind_rules`). - `HIVEMIND_CAPTURE=false`: full read-only mode. Skips placeholder + ensure DDL; renderer still injects. -## Goals + KPIs +## Goals -Personal / team objectives + measurable targets live in the Deeplake virtual filesystem under `~/.deeplake/memory/goal///.md` and `~/.deeplake/memory/kpi//.md`. Path encodes structure (owner, status, goal_id); the file body holds the human-readable description. +Personal / team objectives live in the Deeplake virtual filesystem under `~/.deeplake/memory/goal///.md`. Path encodes structure (owner, status, goal_id); the file body holds the human-readable description. ```bash # CLI fallback for runtimes that can't route VFS writes (cursor/hermes/pi) @@ -537,7 +537,7 @@ hivemind goal done hivemind goal progress opened|in_progress|closed ``` -For VFS-capable runtimes (claude-code/codex) the `hivemind-goals` skill creates and edits goals/KPIs directly via Bash heredoc against the VFS path. `mv` between `opened/`, `in_progress/`, and `closed/` is the canonical status transition. KPIs are manual files; the body format is documented in the skill (`target:`, `current:`, `unit:`). +For VFS-capable runtimes (claude-code/codex) the `hivemind-goals` skill creates and edits goals directly via Bash heredoc against the VFS path. `mv` between `opened/`, `in_progress/`, and `closed/` is the canonical status transition. ## Architecture diff --git a/harnesses/claude-code/skills/hivemind-goals/SKILL.md b/harnesses/claude-code/skills/hivemind-goals/SKILL.md index 4bf77a717..8346ae471 100644 --- a/harnesses/claude-code/skills/hivemind-goals/SKILL.md +++ b/harnesses/claude-code/skills/hivemind-goals/SKILL.md @@ -1,18 +1,18 @@ --- name: hivemind-goals -description: Create, track and update team goals + KPIs via the Deeplake virtual filesystem at memory/goal/ and memory/kpi/. Use whenever the user mentions a goal, objective, KPI, target, milestone, or asks to track progress on something measurable. ALSO use when the user says "task", "todo", "work item", "remind me to", "fix X", or any actionable work item — the goal system replaced the legacy `hivemind tasks` CLI and now covers both objectives and tasks. +description: Create, track and update team goals via the Deeplake virtual filesystem at memory/goal/. Use whenever the user mentions a goal, objective, target, milestone, or asks to track progress on something measurable. ALSO use when the user says "task", "todo", "work item", "remind me to", "fix X", or any actionable work item — the goal system replaced the legacy `hivemind tasks` CLI and now covers both objectives and tasks. allowed-tools: Read Bash --- # Hivemind Goals -Track goals and KPIs as Markdown files inside the Deeplake virtual filesystem. Each file is one row in a dedicated team-shared table — the path encodes the structural metadata, the file body holds the human-readable description. +Track goals as Markdown files inside the Deeplake virtual filesystem. Each file is one row in a dedicated team-shared table — the path encodes the structural metadata, the file body holds the human-readable description. ## When to use this skill Activate when the user expresses any of: - "I want to track X / aim for X / track my progress on Y" -- "add a goal", "add a KPI", "what are my goals?" +- "add a goal", "what are my goals?" - "mark this as done", "close that goal" - "shipping X by Friday", "5 PRs this week", any measurable target - "create a task", "add a todo", "remind me to fix X", any work item (the goals system absorbs the old `hivemind tasks` CLI — there is no separate task store) @@ -23,15 +23,13 @@ For "list my goals" → run `ls ~/.deeplake/memory/goal//opened/` and ``` ~/.deeplake/memory/goal///.md -~/.deeplake/memory/kpi//.md ``` - `` — user identifier (use the userName from `hivemind whoami` or the credentials) - `` — one of `opened`, `in_progress`, `closed` - `` — UUIDv4 you generate at create time -- `` — short slug like `k-prs` or `k-demos` -**Path encoding is the source of truth.** The owner, status, goal_id, and kpi_id come from the path — NOT from the file body. Do NOT write owner/status/goal_id/kpi_id inside the file content. +**Path encoding is the source of truth.** The owner, status, and goal_id come from the path — NOT from the file body. Do NOT write owner/status/goal_id inside the file content. ## File body format @@ -39,20 +37,11 @@ Goal file body — plain markdown, free form: ``` ship the goals-graph feature -Notes: focus on KPI tracking via VFS, no separate CLI. +Notes: route every write through the VFS, no separate CLI. Due: 2026-05-30. ``` -KPI file body — markdown with a few mandatory key:value lines so the commit-driven auto-progress worker can parse and bump: -``` -PRs merged - -- target: 5 -- current: 2 -- unit: count -``` - -The `target:`, `current:`, `unit:` lines must stay on a single line each. The first line is the human-readable name. Anything else is free notes. +The first line is the human-readable label (what `goal list` and the SessionStart banner show). Anything else is free notes. ## Operations @@ -71,8 +60,6 @@ When the user expresses a new goal: For a single-line goal, `echo '' > ~/.deeplake/memory/goal//opened/.md` is equivalent. 4. Respond to the user that the goal is created. -**Do NOT auto-generate KPIs.** A goal is created with zero KPI files by default. Generate KPIs ONLY when the user explicitly asks you to ("aggiungi KPI per …", "add metrics for this goal", "track these metrics: …"). When the user asks, write each KPI as a separate file at `~/.deeplake/memory/kpi//.md` with the body format documented above. - ### 1a. Capture a task for later (with resumable context) Use this when the user **parks a tangential task** mid-session — "save this for later", "remind me to …", "don't let me forget …", "let's do X later", "capture this in Hivemind". The value is NOT the one-liner — it's storing enough **context to resume cold** in a future session without the user re-explaining anything. @@ -110,7 +97,7 @@ ls ~/.deeplake/memory/goal//opened/ ls ~/.deeplake/memory/goal//in_progress/ ``` -Then `cat` each `.md` to read the body. Optionally `ls ~/.deeplake/memory/kpi//` and `cat` each KPI to surface progress. +Then `cat` each `.md` to read the body. ### 3. Edit a goal description @@ -147,58 +134,20 @@ rm ~/.deeplake/memory/goal//opened/.md **Important:** `rm` does NOT actually delete the goal. It is a soft-close — the VFS writes a new version with status=closed. The goal remains in the team-shared table for audit. There is no hard-delete in v1. -### 6. Add a KPI manually - -```bash -cat > ~/.deeplake/memory/kpi//.md <<'EOF' - - -- target: -- current: 0 -- unit: -EOF -``` - -### 7. Record progress on a KPI - -Read the KPI file, increment the `current:` line, write it back via Bash. The -Edit tool is denied on memory paths — overwrite the full file via heredoc: - -```bash -cat ~/.deeplake/memory/kpi//.md # read current -cat > ~/.deeplake/memory/kpi//.md <<'EOF' - - -- target: 5 -- current: 3 -- unit: count -EOF -``` - -A surgical `sed -i 's/^- current: .*/- current: 3/'` also works since `sed` -is an allowed builtin under the VFS path. - -### 8. Reassign a goal (transfer ownership) +### 6. Reassign a goal (transfer ownership) ```bash mv ~/.deeplake/memory/goal///.md ~/.deeplake/memory/goal///.md ``` -Goal ownership lives in the path. KPI files do NOT have an owner segment — they are linked to the goal by ``, so they need no change when a goal is reassigned. +Goal ownership lives in the path; the file body carries over unchanged. ## Constraints — DO NOT do these -- Do NOT put `owner`, `status`, `goal_id`, or `kpi_id` inside the file body. The path is the source of truth — duplicating in the body causes drift. +- Do NOT put `owner`, `status`, or `goal_id` inside the file body. The path is the source of truth — duplicating in the body causes drift. - Do NOT use status values other than `opened`, `in_progress`, `closed`. - Do NOT rename the goal_id (the UUID in the filename) via `mv`. The VFS rejects goal_id renames. -- Do NOT block on the KPI generator subprocess — always spawn it detached (`nohup … &`). - -## Auto-progress from `git commit` - -A PostToolUse hook listens for `git commit`. When it fires, it spawns the agent's native LLM in the background with the commit diff + the list of the current user's open goals. The LLM reads each goal + its KPIs, judges whether the commit advanced any KPI, and edits the relevant KPI file to bump `current:`. This is fire-and-forget; the user does not block on it. - -To disable globally: `HIVEMIND_AUTO_KPI_FROM_COMMITS=false`. ## Team visibility -Every write goes to a team-shared table on Deeplake (`hivemind_goals` or `hivemind_kpis`). Other team members see your goals in their SessionStart context and via direct `ls` / `cat` on the same paths in their own VFS. No explicit sharing step needed. +Every write goes to a team-shared table on Deeplake (`hivemind_goals`). Other team members see your goals in their SessionStart context and via direct `ls` / `cat` on the same paths in their own VFS. No explicit sharing step needed. diff --git a/harnesses/codex/SUBMISSION.md b/harnesses/codex/SUBMISSION.md index b36f53a31..5d44c991b 100644 --- a/harnesses/codex/SUBMISSION.md +++ b/harnesses/codex/SUBMISSION.md @@ -62,7 +62,7 @@ Each includes the prompt, expected behavior, and any required test data. - Expected: hivemind-graph skill queries `memory/graph/query/...`; returns callers/callees. - Test data: a built graph snapshot for the repo. -5. **Goal/KPI tracking** +5. **Goal tracking** - Prompt: "Track a goal: ship the Codex plugin submission this month." - Expected: hivemind-goals skill writes to `memory/goal/`; confirms creation. - Test data: authenticated workspace. diff --git a/harnesses/codex/skills/hivemind-goals/SKILL.md b/harnesses/codex/skills/hivemind-goals/SKILL.md index be6592b24..d4c62ed09 100644 --- a/harnesses/codex/skills/hivemind-goals/SKILL.md +++ b/harnesses/codex/skills/hivemind-goals/SKILL.md @@ -1,18 +1,18 @@ --- name: hivemind-goals -description: Create, track and update team goals + KPIs via the Deeplake virtual filesystem at memory/goal/ and memory/kpi/. Use whenever the user mentions a goal, objective, KPI, target, milestone, or asks to track progress on something measurable. ALSO use when the user says "task", "todo", "work item", "remind me to", "fix X", or any actionable work item — the goal system replaced the legacy `hivemind tasks` CLI and now covers both objectives and tasks. +description: Create, track and update team goals via the Deeplake virtual filesystem at memory/goal/. Use whenever the user mentions a goal, objective, target, milestone, or asks to track progress on something measurable. ALSO use when the user says "task", "todo", "work item", "remind me to", "fix X", or any actionable work item — the goal system replaced the legacy `hivemind tasks` CLI and now covers both objectives and tasks. allowed-tools: Bash --- # Hivemind Goals -Track goals and KPIs as Markdown files inside the Deeplake virtual filesystem. Each file is one row in a dedicated team-shared table — the path encodes the structural metadata, the file body holds the human-readable description. +Track goals as Markdown files inside the Deeplake virtual filesystem. Each file is one row in a dedicated team-shared table — the path encodes the structural metadata, the file body holds the human-readable description. ## When to use this skill Activate when the user expresses any of: - "I want to track X / aim for X / track my progress on Y" -- "add a goal", "add a KPI", "what are my goals?" +- "add a goal", "what are my goals?" - "mark this as done", "close that goal" - "shipping X by Friday", "5 PRs this week", any measurable target - "create a task", "add a todo", "remind me to fix X", any work item (the goals system absorbs the old `hivemind tasks` CLI — there is no separate task store) @@ -23,15 +23,13 @@ For "list my goals" → run `ls ~/.deeplake/memory/goal//opened/` and ``` ~/.deeplake/memory/goal///.md -~/.deeplake/memory/kpi//.md ``` - `` — user identifier (use the userName from `hivemind whoami` or the credentials) - `` — one of `opened`, `in_progress`, `closed` - `` — UUIDv4 you generate at create time -- `` — short slug like `k-prs` or `k-demos` -**Path encoding is the source of truth.** The owner, status, goal_id, and kpi_id come from the path — NOT from the file body. Do NOT write owner/status/goal_id/kpi_id inside the file content. +**Path encoding is the source of truth.** The owner, status, and goal_id come from the path — NOT from the file body. Do NOT write owner/status/goal_id inside the file content. ## File body format @@ -39,20 +37,11 @@ Goal file body — plain markdown, free form: ``` ship the goals-graph feature -Notes: focus on KPI tracking via VFS, no separate CLI. +Notes: route every write through the VFS, no separate CLI. Due: 2026-05-30. ``` -KPI file body — markdown with a few mandatory key:value lines so the commit-driven auto-progress worker can parse and bump: -``` -PRs merged - -- target: 5 -- current: 2 -- unit: count -``` - -The `target:`, `current:`, `unit:` lines must stay on a single line each. The first line is the human-readable name. Anything else is free notes. +The first line is the human-readable label (what `goal list` and the SessionStart banner show). Anything else is free notes. ## Operations @@ -65,8 +54,6 @@ When the user expresses a new goal: 3. Write the goal file at `~/.deeplake/memory/goal//opened/.md` with the goal description as body. 4. Respond to the user that the goal is created. -**Do NOT auto-generate KPIs.** A goal is created with zero KPI files by default. Generate KPIs ONLY when the user explicitly asks you to ("aggiungi KPI per …", "add metrics for this goal", "track these metrics: …"). When the user asks, write each KPI as a separate file at `~/.deeplake/memory/kpi//.md` with the body format documented above. - ### 1a. Capture a task for later (with resumable context) Use this when the user **parks a tangential task** mid-session — "save this for later", "remind me to …", "don't let me forget …", "let's do X later". The value is NOT the one-liner — it's storing enough **context to resume cold** in a future session without the user re-explaining anything. @@ -104,7 +91,7 @@ ls ~/.deeplake/memory/goal//opened/ ls ~/.deeplake/memory/goal//in_progress/ ``` -Then `cat` each `.md` to read the body. Optionally `ls ~/.deeplake/memory/kpi//` and `cat` each KPI to surface progress. +Then `cat` each `.md` to read the body. ### 3. Edit a goal description @@ -136,52 +123,20 @@ rm ~/.deeplake/memory/goal//opened/.md **Important:** `rm` does NOT actually delete the goal. It is a soft-close — the VFS writes a new version with status=closed. The goal remains in the team-shared table for audit. There is no hard-delete in v1. -### 6. Add a KPI manually - -```bash -Write the file at ~/.deeplake/memory/kpi//.md with: - - - - target: - - current: 0 - - unit: -``` - -### 7. Record progress on a KPI - -Read the KPI file, increment the `current:` line, write it back: - -``` - - -- target: 5 -- current: 3 ← incremented from 2 -- unit: count -``` - -Use the Edit tool for the most surgical change (just the line with `current:`). - -### 8. Reassign a goal (transfer ownership) +### 6. Reassign a goal (transfer ownership) ```bash mv ~/.deeplake/memory/goal///.md ~/.deeplake/memory/goal///.md ``` -Goal ownership lives in the path. KPI files do NOT have an owner segment — they are linked to the goal by ``, so they need no change when a goal is reassigned. +Goal ownership lives in the path; the file body carries over unchanged. ## Constraints — DO NOT do these -- Do NOT put `owner`, `status`, `goal_id`, or `kpi_id` inside the file body. The path is the source of truth — duplicating in the body causes drift. +- Do NOT put `owner`, `status`, or `goal_id` inside the file body. The path is the source of truth — duplicating in the body causes drift. - Do NOT use status values other than `opened`, `in_progress`, `closed`. - Do NOT rename the goal_id (the UUID in the filename) via `mv`. The VFS rejects goal_id renames. -- Do NOT block on the KPI generator subprocess — always spawn it detached (`nohup … &`). - -## Auto-progress from `git commit` - -A PostToolUse hook listens for `git commit`. When it fires, it spawns the agent's native LLM in the background with the commit diff + the list of the current user's open goals. The LLM reads each goal + its KPIs, judges whether the commit advanced any KPI, and edits the relevant KPI file to bump `current:`. This is fire-and-forget; the user does not block on it. - -To disable globally: `HIVEMIND_AUTO_KPI_FROM_COMMITS=false`. ## Team visibility -Every write goes to a team-shared table on Deeplake (`hivemind_goals` or `hivemind_kpis`). Other team members see your goals in their SessionStart context and via direct `ls` / `cat` on the same paths in their own VFS. No explicit sharing step needed. +Every write goes to a team-shared table on Deeplake (`hivemind_goals`). Other team members see your goals in their SessionStart context and via direct `ls` / `cat` on the same paths in their own VFS. No explicit sharing step needed. diff --git a/harnesses/hermes/skills/hivemind-goals/SKILL.md b/harnesses/hermes/skills/hivemind-goals/SKILL.md index f273dc722..7f9a051f1 100644 --- a/harnesses/hermes/skills/hivemind-goals/SKILL.md +++ b/harnesses/hermes/skills/hivemind-goals/SKILL.md @@ -1,14 +1,14 @@ --- name: hivemind-goals -description: Create, track and update team goals + KPIs in Hivemind via the `hivemind` CLI. Use whenever the user mentions a goal, objective, KPI, target, milestone, or asks to track progress on something measurable. ALSO use when the user says "task", "todo", "work item", "remind me to", "fix X", or any actionable work item — the goal system replaced the legacy `hivemind tasks` CLI and now covers both objectives and tasks. +description: Create, track and update team goals in Hivemind via the `hivemind` CLI. Use whenever the user mentions a goal, objective, target, milestone, or asks to track progress on something measurable. ALSO use when the user says "task", "todo", "work item", "remind me to", "fix X", or any actionable work item — the goal system replaced the legacy `hivemind tasks` CLI and now covers both objectives and tasks. allowed-tools: terminal --- # Hivemind Goals — CLI only (Hermes) -⚠️ **CRITICAL: On this runtime (Hermes), you MUST use the `hivemind` shell CLI for goals + KPIs. DO NOT use `write_file` on `~/.deeplake/memory/goal/...` paths — those writes go to the local filesystem and never reach the team-shared `hivemind_goals` table. Other team members will NOT see them.** +⚠️ **CRITICAL: On this runtime (Hermes), you MUST use the `hivemind` shell CLI for goals. DO NOT use `write_file` on `~/.deeplake/memory/goal/...` paths — those writes go to the local filesystem and never reach the team-shared `hivemind_goals` table. Other team members will NOT see them.** -The hivemind-memory skill describes a generic memory layout — it does NOT apply to goals/KPIs. For goals/KPIs, use the CLI below. +The hivemind-memory skill describes a generic memory layout — it does NOT apply to goals. For goals, use the CLI below. ## Commands (invoke via terminal tool) @@ -17,17 +17,12 @@ hivemind goal add "" # create goal, print hivemind goal list [--mine|--all] # list (default --mine) hivemind goal done # mark closed hivemind goal progress - -hivemind kpi add [name] # add KPI to goal -hivemind kpi list # list KPIs for goal -hivemind kpi bump # increment current (int) ``` ## Workflow when the user expresses a goal 1. `hivemind goal add ""` — capture stdout, that's the `goal_id` (UUID). -2. If the user explicitly asks for KPIs: `hivemind kpi add ` per KPI. -3. Tell the user the goal_id and that it is now team-visible in Deeplake. +2. Tell the user the goal_id and that it is now team-visible in Deeplake. ## Capture a task for later (with resumable context) @@ -56,8 +51,7 @@ When the user says "let's work on that task / goal" or "pick up the `` task": ## What NOT to do -- Do NOT call `write_file` on any path under `~/.deeplake/memory/goal/` or `~/.deeplake/memory/kpi/`. +- Do NOT call `write_file` on any path under `~/.deeplake/memory/goal/`. - Do NOT do `mkdir` / `cat >` to create those files manually via terminal. -- Do NOT auto-generate KPIs unless the user explicitly asks. If the user wants to inspect goals you created, run `hivemind goal list --mine` (terminal) and present the output. diff --git a/harnesses/openclaw/skills/hivemind-goals/SKILL.md b/harnesses/openclaw/skills/hivemind-goals/SKILL.md index d83388419..52ed3996c 100644 --- a/harnesses/openclaw/skills/hivemind-goals/SKILL.md +++ b/harnesses/openclaw/skills/hivemind-goals/SKILL.md @@ -1,17 +1,16 @@ --- name: hivemind-goals -description: Create, track, and read team goals + KPIs via Hivemind from openclaw. Use whenever the user mentions a goal, objective, KPI, target, milestone, or asks to track progress on something measurable. ALSO use when the user says "task", "todo", "work item", "remind me to", "fix X", or any actionable work item — the goal system replaced the legacy `hivemind tasks` CLI and now covers both objectives and tasks. -allowed-tools: hivemind_search, hivemind_read, hivemind_index, hivemind_goal_add, hivemind_kpi_add +description: Create, track, and read team goals via Hivemind from openclaw. Use whenever the user mentions a goal, objective, target, milestone, or asks to track progress on something measurable. ALSO use when the user says "task", "todo", "work item", "remind me to", "fix X", or any actionable work item — the goal system replaced the legacy `hivemind tasks` CLI and now covers both objectives and tasks. +allowed-tools: hivemind_search, hivemind_read, hivemind_index, hivemind_goal_add --- # Hivemind Goals (openclaw) -OpenClaw exposes purpose-built tools for goals + KPIs. Use them directly — do NOT try to write files via the host filesystem. +OpenClaw exposes purpose-built tools for goals. Use them directly — do NOT try to write files via the host filesystem. ## Tools - `hivemind_goal_add({ text })` — create a new goal. Returns `goal_id` (UUID). Status starts at `opened`. -- `hivemind_kpi_add({ goal_id, kpi_id, target, unit, name? })` — add a KPI to an existing goal. Only call when the user explicitly asks for KPIs; do NOT auto-generate. - `hivemind_search({ query })` — search Hivemind shared memory (summaries + sessions). Use this when the user asks "what's already there" before creating a duplicate. - `hivemind_read({ path })` — read the full content of a specific Hivemind path. - `hivemind_index({})` — list everything in memory. @@ -20,8 +19,7 @@ OpenClaw exposes purpose-built tools for goals + KPIs. Use them directly — do 1. (Optional) `hivemind_search` first to surface any existing related goal. 2. `hivemind_goal_add({ text: "" })` — capture the returned `goal_id`. -3. ONLY if the user asks for KPIs: `hivemind_kpi_add` once per KPI with `goal_id` + `kpi_id` (short slug like `k-prs`) + `target` (positive int) + `unit`. -4. Confirm to the user with the goal_id and that the goal is team-visible. +3. Confirm to the user with the goal_id and that the goal is team-visible. ## Capture a task for later (with resumable context) @@ -51,5 +49,4 @@ When the user says "let's work on that task / goal" or "pick up the `` task": ## What NOT to do - Do NOT write files anywhere under `~/.deeplake/memory/`. OpenClaw's runtime does not route filesystem writes to the Deeplake tables — only the `hivemind_*` tools above do. -- Do NOT call `hivemind_kpi_add` unsolicited. Wait for the user to ask. - Do NOT use `hivemind_search` to *create* anything — it's read-only. diff --git a/library/knowledge/private/architecture/system-overview.md b/library/knowledge/private/architecture/system-overview.md index 64a53c59b..1d66b467d 100644 --- a/library/knowledge/private/architecture/system-overview.md +++ b/library/knowledge/private/architecture/system-overview.md @@ -86,7 +86,7 @@ flowchart TB sessionsTable["sessions table"] memoryTable["memory table + VFS"] skillsTable["skills table"] - rulesGoals["rules / goals / kpis"] + rulesGoals["rules / goals"] codebaseTable["codebase table"] end @@ -134,7 +134,7 @@ The differences are real but shallow: event names and payload field names vary, ## State and storage -All durable state lives in Deeplake tables defined in `src/deeplake-schema.ts`. The `sessions` table holds raw per-event traces with an optional `message_embedding` vector. The `memory` table holds wiki summaries plus the virtual filesystem entries and their `summary_embedding`. Separate tables back skills, rules, goals, KPIs, and the codebase graph. Rules, skills, goals, and KPIs all use the same immutable, version-bumped write pattern (every edit INSERTs version N+1 and reads take the highest version) to sidestep a Deeplake UPDATE-coalescing quirk that previously dropped concurrent writes. +All durable state lives in Deeplake tables defined in `src/deeplake-schema.ts`. The `sessions` table holds raw per-event traces with an optional `message_embedding` vector. The `memory` table holds wiki summaries plus the virtual filesystem entries and their `summary_embedding`. Separate tables back skills, rules, goals, and the codebase graph. Rules and skills use an immutable, version-bumped write pattern (every edit INSERTs version N+1 and reads take the highest version) to sidestep a Deeplake UPDATE-coalescing quirk that previously dropped concurrent writes. Goals are the deliberate exception: `upsertGoalRow` keeps one row per `goal_id` and mutates it in place (UPDATE-or-INSERT, `version` fixed at 1), trading the audit trail for a one-row-per-goal table view. Tenant isolation is enforced at the storage layer, not just the API: org and workspace boundaries mean sessions never share a row, partition, or index across workspaces. Credentials live on disk with mode `0600` and the config directory with mode `0700`, and the device-flow login keeps tokens out of the environment and out of source. diff --git a/library/knowledge/private/data/deeplake-tables-schema.md b/library/knowledge/private/data/deeplake-tables-schema.md index 3b49f7c9f..4a48f32ad 100644 --- a/library/knowledge/private/data/deeplake-tables-schema.md +++ b/library/knowledge/private/data/deeplake-tables-schema.md @@ -24,14 +24,13 @@ Two cross-cutting facts shape every table below. First, Deeplake's HTTP query en --- -## The seven tables at a glance +## The six tables at a glance -Hivemind owns seven tables. Their logical relationships (Deeplake enforces no foreign keys; all joins are logical) look like this: +Hivemind owns six tables. Their logical relationships (Deeplake enforces no foreign keys; all joins are logical) look like this: ```mermaid erDiagram sessions ||--o{ memory : "summarized into" - goals ||--o{ kpis : "goal_id" skills }o--|| project : "project_key" rules }o--|| org : "scope" codebase }o--|| repo : "repo_slug" @@ -63,11 +62,6 @@ erDiagram text goal_id text status } - kpis { - text id - text goal_id - text kpi_id - } codebase { text commit_sha text snapshot_sha256 @@ -82,7 +76,6 @@ erDiagram | `skills` | Mined `SKILL.md` versions | Append-only, version-bumped | | `rules` | Org-wide principles | Append-only, version-bumped | | `goals` | User-tracked objectives | UPDATE-or-INSERT keyed by `goal_id` | -| `kpis` | Metrics attached to a goal | UPDATE-or-INSERT keyed by `(goal_id, kpi_id)` | | `codebase` | Code-graph snapshots | SELECT-before-INSERT, per identity key | --- @@ -182,9 +175,9 @@ A rule edit INSERTs version+1; the latest per `rule_id` wins. Rules feed the Ses --- -## Goals and KPIs: path-encoded, UPDATE-or-INSERT +## Goals: path-encoded, UPDATE-or-INSERT -Goals and KPIs are backed by the virtual filesystem path conventions, and the path is the source of truth for their structural fields. A goal lives at `memory/goal///.md`; a KPI lives at `memory/kpi//.md`. The `content` column stores only the human-readable markdown body, so there is nothing to drift between the path-encoded fields and the row body. +Goals are backed by the virtual filesystem path convention, and the path is the source of truth for their structural fields. A goal lives at `memory/goal///.md`. The `content` column stores only the human-readable markdown body, so there is nothing to drift between the path-encoded fields and the row body. ```sql CREATE TABLE IF NOT EXISTS "goals" ( @@ -201,26 +194,10 @@ CREATE TABLE IF NOT EXISTS "goals" ( ) USING deeplake; ``` -Unlike skills and rules, the goals and KPIs tables hold one row per logical key forever. A status transition, an owner reassignment, or a body edit mutates the same row in place via UPDATE rather than inserting a new version. The `version` column survives as a vestigial `1`, kept so the audit-trail pattern can be reinstated without a migration. This is a deliberate v1 trade-off: one row per goal makes the Deeplake table view obvious and bootstrap queries simple, at the cost of no audit trail and exposure to the UPDATE-coalescing quirk for two writes that hit the same row within microseconds. For the single-user and small-team workflow this was an accepted choice. +Unlike skills and rules, the goals table holds one row per logical key forever. A status transition, an owner reassignment, or a body edit mutates the same row in place via UPDATE rather than inserting a new version. The `version` column survives as a vestigial `1`, kept so the audit-trail pattern can be reinstated without a migration. This is a deliberate v1 trade-off: one row per goal makes the Deeplake table view obvious and bootstrap queries simple, at the cost of no audit trail and exposure to the UPDATE-coalescing quirk for two writes that hit the same row within microseconds. For the single-user and small-team workflow this was an accepted choice. The status enum is `opened`, `in_progress`, or `closed`, mirroring the path folder names. The `created_at` timestamp is preserved across edits (a status change records its time in `updated_at`) so goals stay in stable creation order in listings. -```sql -CREATE TABLE IF NOT EXISTS "kpis" ( - id TEXT NOT NULL DEFAULT '', - goal_id TEXT NOT NULL DEFAULT '', - kpi_id TEXT NOT NULL DEFAULT '', - content TEXT NOT NULL DEFAULT '', - version BIGINT NOT NULL DEFAULT 1, - created_at TEXT NOT NULL DEFAULT '', - updated_at TEXT NOT NULL DEFAULT '', - agent TEXT NOT NULL DEFAULT 'manual', - plugin_version TEXT NOT NULL DEFAULT '' -) USING deeplake; -``` - -A KPI is keyed by `(goal_id, kpi_id)`. Owner is intentionally not stored on the KPI; it is derived from the parent goal by a logical join on `goal_id`, which avoids a multi-file cascade move whenever a goal is reassigned between owners. The body is free markdown, by convention carrying `target:`, `current:`, and `unit:` lines that the commit-extract worker mutates. - How these path conventions are parsed and dispatched is detailed in [`memory-virtual-filesystem.md`](memory-virtual-filesystem.md). --- @@ -316,7 +293,7 @@ The read patterns follow directly from the write patterns: - `memory`: read the row for a `path` directly (`SELECT summary FROM "memory" WHERE path = '...'`). - `sessions`: read all rows for a `path` ordered by `creation_date` and concatenate the messages. - `skills` and `rules`: take the highest `version` per logical key. -- `goals` and `kpis`: read the single row per key, ordered by `created_at DESC` at bootstrap. +- `goals`: read the single row per key, ordered by `created_at DESC` at bootstrap. - `codebase`: SELECT by the identity key; the pull path relaxes the key to drop `worktree_id` and takes `ORDER BY ts DESC LIMIT 1` for the freshest snapshot of a commit. These conventions keep every table internally consistent under concurrent hook processes without relying on database transactions, which Deeplake does not expose at this layer. diff --git a/library/knowledge/private/data/memory-virtual-filesystem.md b/library/knowledge/private/data/memory-virtual-filesystem.md index 671f4c105..8bde8ac76 100644 --- a/library/knowledge/private/data/memory-virtual-filesystem.md +++ b/library/knowledge/private/data/memory-virtual-filesystem.md @@ -2,7 +2,7 @@ > Category: Data | Version: 1.0 | Date: June 2026 | Status: Active -How Hivemind makes a team-shared Deeplake database look like an ordinary directory at `~/.deeplake/memory/`: the `DeeplakeFs` intercept, path-routed dispatch to the goals and KPIs tables, batched writes with debounced flush, the synthesized `index.md`, and the read-only sessions and graph bridges. +How Hivemind makes a team-shared Deeplake database look like an ordinary directory at `~/.deeplake/memory/`: the `DeeplakeFs` intercept, path-routed dispatch to the goals table, batched writes with debounced flush, the synthesized `index.md`, and the read-only sessions and graph bridges. **Related:** - [`deeplake-tables-schema.md`](deeplake-tables-schema.md) @@ -17,7 +17,7 @@ How Hivemind makes a team-shared Deeplake database look like an ordinary directo ## Why a filesystem over a database -Coding agents already know how to `cat`, `ls`, `grep`, and `find`. Hivemind leans on that fluency: instead of teaching every assistant a new recall API, it presents memory as files under `~/.deeplake/memory/` and intercepts the shell commands that touch that mount. From the agent's point of view it is browsing files; underneath, each operation is a SQL query against the `sessions`, `memory`, `goals`, and `kpis` tables described in [`deeplake-tables-schema.md`](deeplake-tables-schema.md). +Coding agents already know how to `cat`, `ls`, `grep`, and `find`. Hivemind leans on that fluency: instead of teaching every assistant a new recall API, it presents memory as files under `~/.deeplake/memory/` and intercepts the shell commands that touch that mount. From the agent's point of view it is browsing files; underneath, each operation is a SQL query against the `sessions`, `memory`, and `goals` tables described in [`deeplake-tables-schema.md`](deeplake-tables-schema.md). There are two consumers of this intercept. The PreToolUse hook rewrites Claude Code Bash, Read, Grep, and Glob commands one-shot and stateless. The standalone deeplake-shell exposes the same mount through a long-lived `DeeplakeFs` object that implements the `IFileSystem` interface from `just-bash`. Both produce the same view; this document focuses on the `DeeplakeFs` implementation in `src/shell/deeplake-fs.ts`, which is the richer of the two. @@ -40,32 +40,29 @@ At construction the factory `create()` bootstraps four sources in parallel befor ```mermaid flowchart TD - create["DeeplakeFs.create()"] --> ensure["ensureTable + ensureGoalsTable + ensureKpisTable"] + create["DeeplakeFs.create()"] --> ensure["ensureTable + ensureGoalsTable"] ensure --> parallel["Promise.all bootstrap"] parallel --> mem["memory rows: SELECT path, size_bytes, mime_type"] parallel --> sess["sessions rows: GROUP BY path, MAX(size_bytes)"] parallel --> goals["goals rows: latest per goal_id"] - parallel --> kpis["kpis rows: latest per goal_id, kpi_id"] mem --> tree["populate files/meta/dirs maps"] sess --> tree goals --> tree - kpis --> tree ``` -The memory bootstrap reads `path, size_bytes, mime_type` ordered by path and registers each row as an unfetched file (`files.set(p, null)`). Crucially, it skips any goal-shaped or KPI-shaped path when the dedicated tables are configured, because those rows belong exclusively to the structured tables. Surfacing the generic-table copies would re-inject phantom goals into the VFS namespace that the `hivemind goal list` CLI (which reads only the structured table) would not see. +The memory bootstrap reads `path, size_bytes, mime_type` ordered by path and registers each row as an unfetched file (`files.set(p, null)`). Crucially, it skips any goal-shaped path when the dedicated table is configured, because those rows belong exclusively to the structured table. Surfacing the generic-table copies would re-inject phantom goals into the VFS namespace that the `hivemind goal list` CLI (which reads only the structured table) would not see. The sessions bootstrap groups by `path` and takes `MAX(size_bytes)`, a workaround for a Deeplake behavior where `SUM(size_bytes)` returns NULL when combined with `GROUP BY path`. For the single-row-per-file layout MAX equals SUM; for multi-row layouts it under-reports but stays positive so files never look like empty placeholders. --- -## Path classification: three destinations +## Path classification: two destinations -Every read and write is first classified by `classifyPath` (from `src/shell/goal-paths.ts`) into one of three kinds: +Every read and write is first classified by `classifyPath` (from `src/shell/goal-paths.ts`) into one of two kinds: | Kind | Path shape | Backing table | |---|---|---| | `goal` | `memory/goal///.md` | `goals` | -| `kpi` | `memory/kpi//.md` | `kpis` | | `memory` | anything else | `memory` | The classifier strips any leading mount prefix by finding the last `/memory/` occurrence in the path, which lets it accept every shape an agent might produce: a mount-relative `/goal/...`, a test mount `/memory/goal/...`, a shell redirect `~/.deeplake/memory/goal/...`, or a host-absolute `/home//.deeplake/memory/goal/...`. The status component must be one of `opened`, `in_progress`, or `closed`, and the filename must end in `.md`; anything malformed falls back to `memory` so the generic path handles it. @@ -80,15 +77,11 @@ export function classifyPath(p: string): PathKind { } return "memory"; } - if (segs[0] === "kpi") { - if (segs.length === 3 && segs[2].endsWith(".md")) return "kpi"; - return "memory"; - } return "memory"; } ``` -The path encoding is the source of truth: `decomposeGoalPath` extracts `owner`, `status`, and `goal_id` from the path, and the row's `content` column stores only the markdown body. `composeGoalPath` and `composeKpiPath` rebuild the canonical mount-relative path (no mount prefix) that both the cache and the DB rows use. +The path encoding is the source of truth: `decomposeGoalPath` extracts `owner`, `status`, and `goal_id` from the path, and the row's `content` column stores only the markdown body. `composeGoalPath` rebuilds the canonical mount-relative path (no mount prefix) that both the cache and the DB rows use. --- @@ -119,7 +112,7 @@ sequenceDiagram The flush is serialized through a promise chain (`flushChain`) so two flushes never interleave. `_doFlush` drains the pending map, computes embeddings for the batch (skipping the daemon hop entirely when embeddings are globally disabled, writing NULL for the vector columns), and upserts every row in parallel via `Promise.allSettled`. Any row that fails is re-queued for the next flush unless a newer version was written in the meantime, and the flush throws so callers know some writes were deferred. -`upsertRow` dispatches by path kind. Goal and KPI writes route to `upsertGoalRow` / `upsertKpiRow`, which do their own SELECT-then-UPDATE-or-INSERT keyed by `goal_id` (or `goal_id, kpi_id`). The generic memory path branches on the `flushed` set: a path already flushed gets an UPDATE of `summary`, `summary_embedding`, `mime_type`, `size_bytes`, and `last_update_date` (plus optional `project` and `description`); a fresh path gets a full INSERT with a new UUID. Text bodies are escaped with `sqlStr` and written with the `E'...'` literal form (see [`deeplake-tables-schema.md`](deeplake-tables-schema.md)). +`upsertRow` dispatches by path kind. Goal writes route to `upsertGoalRow`, which does its own SELECT-then-UPDATE-or-INSERT keyed by `goal_id`. The generic memory path branches on the `flushed` set: a path already flushed gets an UPDATE of `summary`, `summary_embedding`, `mime_type`, `size_bytes`, and `last_update_date` (plus optional `project` and `description`); a fresh path gets a full INSERT with a new UUID. Text bodies are escaped with `sqlStr` and written with the `E'...'` literal form (see [`deeplake-tables-schema.md`](deeplake-tables-schema.md)). `appendFile` takes a fast path that avoids a read-back: when the file already exists it issues a SQL-level concatenation (`summary = summary || E'...'`) and invalidates the content cache so the next read fetches fresh data. This makes append O(1) per call rather than read-modify-write. @@ -179,4 +172,4 @@ The bridge keeps the FS contract honest. The `no-graph` result (no snapshot buil ## What the agent never sees -The intercept hides three things the agent would otherwise trip over. It hides write batching: a `cat` immediately after a `Write` reads from the pending buffer, so the agent sees its own write even before it reaches Deeplake. It hides the multi-row session layout: a session "file" is dozens of rows concatenated transparently. And it hides the goals and KPIs structured tables behind plain markdown files, so the agent manages objectives with `Write` and `mv` while the CLI reads the same state from typed columns. The result is that recall feels like browsing a directory while every operation is really a query against a team-shared, multi-tenant database. +The intercept hides three things the agent would otherwise trip over. It hides write batching: a `cat` immediately after a `Write` reads from the pending buffer, so the agent sees its own write even before it reaches Deeplake. It hides the multi-row session layout: a session "file" is dozens of rows concatenated transparently. And it hides the goals structured table behind plain markdown files, so the agent manages objectives with `Write` and `mv` while the CLI reads the same state from typed columns. The result is that recall feels like browsing a directory while every operation is really a query against a team-shared, multi-tenant database. diff --git a/library/knowledge/private/multi-tenant/org-workspace-model.md b/library/knowledge/private/multi-tenant/org-workspace-model.md index 460db1089..257d1a00f 100644 --- a/library/knowledge/private/multi-tenant/org-workspace-model.md +++ b/library/knowledge/private/multi-tenant/org-workspace-model.md @@ -110,7 +110,6 @@ The heal never throws: a failed re-mint logs a warning and returns the original | `skillsTableName` | `"skills"` | `HIVEMIND_SKILLS_TABLE` | | `rulesTableName` | `"hivemind_rules"` | `HIVEMIND_RULES_TABLE` | | `goalsTableName` | `"hivemind_goals"` | `HIVEMIND_GOALS_TABLE` | -| `kpisTableName` | `"hivemind_kpis"` | `HIVEMIND_KPIS_TABLE` | | `codebaseTableName` | `"codebase"` | `HIVEMIND_CODEBASE_TABLE` | | `memoryPath` | `~/.deeplake/memory` | `HIVEMIND_MEMORY_PATH` | diff --git a/library/knowledge/private/overview.md b/library/knowledge/private/overview.md index 0e12e7744..10d4a9aff 100644 --- a/library/knowledge/private/overview.md +++ b/library/knowledge/private/overview.md @@ -32,7 +32,7 @@ Hivemind has four moving parts that recur across every domain. **The shared core (`src/`).** The Deeplake API client (`src/deeplake-api.ts`), the table schemas (`src/deeplake-schema.ts`), config loading (`src/config.ts`), credential handling (`src/commands/auth.ts`), and the SQL-safety utilities are all agent-agnostic. The per-agent hooks are thin wrappers over this core. -**Deeplake as the substrate.** All durable state lives in Deeplake tables: `sessions` (raw per-event traces), `memory` (wiki summaries plus the virtual filesystem), `skills`, `rules`, `goals`, `kpis`, and `codebase` (the code graph). Org and workspace boundaries are enforced at the storage layer, so two workspaces never share a row, partition, or index. +**Deeplake as the substrate.** All durable state lives in Deeplake tables: `sessions` (raw per-event traces), `memory` (wiki summaries plus the virtual filesystem), `skills`, `rules`, `goals`, and `codebase` (the code graph). Org and workspace boundaries are enforced at the storage layer, so two workspaces never share a row, partition, or index. **The virtual filesystem (VFS).** Agents read and write memory through ordinary shell commands (`cat`, `ls`, `grep`) against `~/.deeplake/memory/`. A PreToolUse hook intercepts those commands and routes them to SQL queries instead of the real disk, which is how recall feels like browsing files while actually hitting a team-shared database. diff --git a/library/knowledge/private/plugins/integration-model.md b/library/knowledge/private/plugins/integration-model.md index 056f0d5ee..81aef0841 100644 --- a/library/knowledge/private/plugins/integration-model.md +++ b/library/knowledge/private/plugins/integration-model.md @@ -78,7 +78,7 @@ The extension wires two hook events via `pluginApi.on(event, handler)`: - `before_agent_start`: handles the login nudge (device-flow URL) and the post-auth welcome banner. - `agent_end`: captures new messages from the conversation into the `sessions` table and fires the skillify worker. -OpenClaw has no PreToolUse analog. Instead, the extension registers three agent-facing tools (`hivemind_search`, `hivemind_read`, `hivemind_index`) plus two write tools (`hivemind_goal_add`, `hivemind_kpi_add`) via `pluginApi.registerTool`. The SKILL.md body embedded at build time (`__HIVEMIND_SKILL__` constant) instructs the agent to call `hivemind_search` before answering questions about past work. OpenClaw also registers a `MemoryCorpusSupplement` so other OpenClaw plugins that expose a `memory_search` tool can federate queries into Hivemind automatically. +OpenClaw has no PreToolUse analog. Instead, the extension registers three agent-facing tools (`hivemind_search`, `hivemind_read`, `hivemind_index`) plus one write tool (`hivemind_goal_add`) via `pluginApi.registerTool`. The SKILL.md body embedded at build time (`__HIVEMIND_SKILL__` constant) instructs the agent to call `hivemind_search` before answering questions about past work. OpenClaw also registers a `MemoryCorpusSupplement` so other OpenClaw plugins that expose a `memory_search` tool can federate queries into Hivemind automatically. Because OpenClaw's bundle scanner treats any `process.env` access in a file that also calls `fetch()` as `env-harvesting`, all `HIVEMIND_*` environment reads are rewritten by esbuild's `define` to `globalThis.__hivemind_tuning__?.HIVEMIND_X`, and `applyOpenclawTuning` bridges the user's `openclaw.json` plugin config into that global. @@ -110,4 +110,4 @@ Despite the mechanism differences, every integration shares the same invariants - Capture is gated by `HIVEMIND_CAPTURE !== "false"`. When that flag is set, the hook runs read-only: no DDL, no INSERTs. - User-facing notices go through the SessionStart banner channel. Hooks never write error text into `additionalContext`, because arbitrary text in context is a prompt-injection risk. - Each INSERT writes exactly one row per event, never concatenating events into a shared row, to prevent write races. -- All writes use the immutable version-bumped pattern for rules, skills, goals, and KPIs to avoid Deeplake's UPDATE-coalescing quirk. +- Rules and skills use the immutable version-bumped pattern to avoid Deeplake's UPDATE-coalescing quirk; goals are UPDATE-or-INSERT, one row per `goal_id`. diff --git a/library/knowledge/private/plugins/mcp-and-extension-surfaces.md b/library/knowledge/private/plugins/mcp-and-extension-surfaces.md index d13d2e00c..97632bd36 100644 --- a/library/knowledge/private/plugins/mcp-and-extension-surfaces.md +++ b/library/knowledge/private/plugins/mcp-and-extension-surfaces.md @@ -81,7 +81,7 @@ The OpenClaw plugin manifest at `harnesses/openclaw/openclaw.plugin.json` declar "contracts": { "tools": [ "hivemind_search", "hivemind_read", "hivemind_index", - "hivemind_goal_add", "hivemind_kpi_add" + "hivemind_goal_add" ], "commands": [ "hivemind_login", "hivemind_capture", "hivemind_whoami", @@ -101,10 +101,9 @@ The `memoryCorpusSupplements: true` declaration tells OpenClaw's runtime that th The three recall tools (`hivemind_search`, `hivemind_read`, `hivemind_index`) mirror the MCP server tools but use the OpenClaw `AgentTool` interface and accept richer parameters. `hivemind_search` additionally supports `path`, `regex`, and `ignoreCase` fields. All three call the same `searchDeeplakeTables` and `readVirtualPathContent` functions from the shared core. -Two write tools are also registered: +One write tool is also registered: - **`hivemind_goal_add`** creates a new goal row in the `hivemind_goals` table with `agent: "openclaw"` provenance. It mirrors the `hivemind goal add --agent capture` CLI path. -- **`hivemind_kpi_add`** creates a KPI row in the `hivemind_kpis` table linked to an existing goal by `goal_id`. ### Commands diff --git a/src/shell/deeplake-fs.ts b/src/shell/deeplake-fs.ts index 58e4ac9d5..023e8013c 100644 --- a/src/shell/deeplake-fs.ts +++ b/src/shell/deeplake-fs.ts @@ -441,7 +441,8 @@ export class DeeplakeFs implements IFileSystem { private async upsertRow(r: PendingRow, embedding: number[] | null): Promise { // Path-routed structured tables: dispatch goal writes to - // the dedicated table with INSERT-only version-bump semantics. + // the dedicated table with UPDATE-or-INSERT semantics (one row + // per goal_id, see upsertGoalRow). // The generic memory path falls through to the existing UPDATE / // INSERT shape below. Failures here propagate up to the flush // chain which re-queues the row on the next tick. From 1d766f8c6c53f7ab5fe9df82be85835f14a355fb Mon Sep 17 00:00:00 2001 From: Emanuele Fenocchi Date: Wed, 16 Sep 2026 22:31:42 +0000 Subject: [PATCH 4/4] test: cover flush re-queue, pending readFile and prefetch branches in deeplake-fs Removing the KPI routing dropped deeplake-fs.ts branch coverage to 89.77% against its 90% floor. Cover existing branches instead of lowering it: a write landing mid-flush survives the re-queue of its failed predecessor, an empty flush issues no query, readFile serves pending rows and maps a missing/NULL summary row, and prefetch skips unregistered paths. --- .../claude-code/deeplake-fs-coverage.test.ts | 48 ++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/tests/claude-code/deeplake-fs-coverage.test.ts b/tests/claude-code/deeplake-fs-coverage.test.ts index 5aa1db827..a93f7e76c 100644 --- a/tests/claude-code/deeplake-fs-coverage.test.ts +++ b/tests/claude-code/deeplake-fs-coverage.test.ts @@ -37,7 +37,7 @@ function makeGoalClient(init: { goals?: GoalRow[]; memory?: string[] } = {}) { ensureTable: vi.fn().mockResolvedValue(undefined), ensureGoalsTable: vi.fn().mockResolvedValue(undefined), listTables: vi.fn().mockResolvedValue(["memory", "goals"]), - query: vi.fn(async (sql: string) => { + query: vi.fn(async (sql: string): Promise[]> => { // ── bootstrap ── if (sql.includes("SELECT path, size_bytes, mime_type")) { return memory.map(p => ({ path: p, size_bytes: 1, mime_type: "text/markdown" })); @@ -334,6 +334,30 @@ describe("flush re-queue on failure", () => { // the failure so the caller knows the write did not land. await expect(fs.flush()).rejects.toThrow(/writes failed and were re-queued/); }); + + it("keeps a newer write over the re-queued stale row when the write lands mid-flush", async () => { + const { fs, client } = await makeGoalFs({}); + let rejectInsert!: (e: Error) => void; + const insertStarted = new Promise((started) => { + client.query.mockImplementationOnce(() => new Promise((_, reject) => { rejectInsert = reject; started(); })); + }); + await fs.writeFile("/notes/x.md", "v1"); + const flushing = fs.flush(); + await insertStarted; + // The v1 INSERT is in flight; the caller overwrites the same path. + await fs.writeFile("/notes/x.md", "v2"); + rejectInsert(new Error("backend down")); + await expect(flushing).rejects.toThrow(/1\/1 writes failed/); + // v2 must survive the re-queue — the stale v1 row must not clobber it. + expect(await fs.readFile("/notes/x.md")).toBe("v2"); + }); + + it("flush with nothing pending issues no query", async () => { + const { fs, client } = await makeGoalFs({}); + client.query.mockClear(); + await fs.flush(); + expect(client.query).not.toHaveBeenCalled(); + }); }); // ── embeddings-disabled flush path ──────────────────────────────────────────── @@ -437,6 +461,28 @@ describe("read branches", () => { expect(Buffer.from(buf).toString("utf-8")).toBe("cached"); }); + it("readFile (text) mirrors readFileBuffer: ENOENT on a missing row, '' on a NULL summary", async () => { + const { fs, client } = await makeGoalFs({ memory: ["/notes/gone.md", "/notes/null.md"] }); + client.query.mockImplementation(async (sql: string) => sql.includes("/notes/null.md") ? [{ summary: null }] : []); + await expect(fs.readFile("/notes/gone.md")).rejects.toMatchObject({ code: "ENOENT" }); + expect(await fs.readFile("/notes/null.md")).toBe(""); + }); + + it("readFile (text) serves a pending unflushed write without a query", async () => { + const { fs, client } = await makeGoalFs({}); + await fs.writeFile("/notes/pending.md", "not flushed yet"); + client.query.mockClear(); + expect(await fs.readFile("/notes/pending.md")).toBe("not flushed yet"); + expect(client.query).not.toHaveBeenCalled(); + }); + + it("prefetch skips unknown paths and issues no query for them", async () => { + const { fs, client } = await makeGoalFs({}); + client.query.mockClear(); + await fs.prefetch(["/notes/never-registered.md"]); + expect(client.query).not.toHaveBeenCalled(); + }); + it("readFileBuffer throws ENOENT when the SQL row is absent", async () => { const { fs, client } = await makeGoalFs({ memory: ["/notes/gone.md"] }); // The bootstrap registered the path (files map → null) but the summary read returns nothing.