@deeplake/hivemind 0.7.45 → 0.7.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +64 -0
  4. package/bundle/cli.js +22702 -7775
  5. package/codex/bundle/capture.js +228 -0
  6. package/codex/bundle/commands/auth-login.js +228 -0
  7. package/codex/bundle/graph-pull-worker.js +1370 -0
  8. package/codex/bundle/pre-tool-use.js +228 -0
  9. package/codex/bundle/session-start-setup.js +228 -0
  10. package/codex/bundle/session-start.js +255 -4
  11. package/codex/bundle/shell/deeplake-shell.js +1028 -28
  12. package/codex/bundle/skillify-worker.js +94 -3
  13. package/codex/bundle/stop.js +282 -50
  14. package/codex/skills/hivemind-goals/SKILL.md +157 -0
  15. package/cursor/bundle/capture.js +282 -50
  16. package/cursor/bundle/commands/auth-login.js +228 -0
  17. package/cursor/bundle/graph-pull-worker.js +1370 -0
  18. package/cursor/bundle/pre-tool-use.js +228 -0
  19. package/cursor/bundle/session-end.js +65 -44
  20. package/cursor/bundle/session-start.js +662 -6
  21. package/cursor/bundle/shell/deeplake-shell.js +1028 -28
  22. package/cursor/bundle/skillify-worker.js +94 -3
  23. package/hermes/bundle/capture.js +282 -50
  24. package/hermes/bundle/commands/auth-login.js +228 -0
  25. package/hermes/bundle/graph-pull-worker.js +1370 -0
  26. package/hermes/bundle/pre-tool-use.js +228 -0
  27. package/hermes/bundle/session-end.js +65 -44
  28. package/hermes/bundle/session-start.js +662 -6
  29. package/hermes/bundle/shell/deeplake-shell.js +1028 -28
  30. package/hermes/bundle/skillify-worker.js +94 -3
  31. package/mcp/bundle/server.js +228 -0
  32. package/openclaw/dist/chunks/config-FH6JYSJW.js +53 -0
  33. package/openclaw/dist/index.js +307 -2
  34. package/openclaw/dist/skillify-worker.js +94 -3
  35. package/openclaw/openclaw.plugin.json +4 -2
  36. package/openclaw/package.json +1 -1
  37. package/openclaw/skills/hivemind-goals/SKILL.md +30 -0
  38. package/package.json +4 -1
  39. package/openclaw/dist/chunks/config-XEK4MJJS.js +0 -36
@@ -0,0 +1,157 @@
1
+ ---
2
+ name: hivemind-goals
3
+ 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.
4
+ allowed-tools: Bash
5
+ ---
6
+
7
+ # Hivemind Goals
8
+
9
+ 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.
10
+
11
+ ## When to use this skill
12
+
13
+ Activate when the user expresses any of:
14
+ - "I want to track X / aim for X / track my progress on Y"
15
+ - "add a goal", "add a KPI", "what are my goals?"
16
+ - "mark this as done", "close that goal"
17
+ - "shipping X by Friday", "5 PRs this week", any measurable target
18
+
19
+ For "list my goals" → run `ls ~/.deeplake/memory/goal/<userName>/opened/` and `ls ~/.deeplake/memory/goal/<userName>/in_progress/`. If empty, ask the user if they want to create one.
20
+
21
+ ## Path conventions (LEARN THESE)
22
+
23
+ ```
24
+ ~/.deeplake/memory/goal/<owner>/<status>/<goal_id>.md
25
+ ~/.deeplake/memory/kpi/<goal_id>/<kpi_id>.md
26
+ ```
27
+
28
+ - `<owner>` — user identifier (use the userName from `hivemind whoami` or the credentials)
29
+ - `<status>` — one of `opened`, `in_progress`, `closed`
30
+ - `<goal_id>` — UUIDv4 you generate at create time
31
+ - `<kpi_id>` — short slug like `k-prs` or `k-demos`
32
+
33
+ **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.
34
+
35
+ ## File body format
36
+
37
+ Goal file body — plain markdown, free form:
38
+ ```
39
+ ship the goals-graph feature
40
+
41
+ Notes: focus on KPI tracking via VFS, no separate CLI.
42
+ Due: 2026-05-30.
43
+ ```
44
+
45
+ KPI file body — markdown with a few mandatory key:value lines so the commit-driven auto-progress worker can parse and bump:
46
+ ```
47
+ PRs merged
48
+
49
+ - target: 5
50
+ - current: 2
51
+ - unit: count
52
+ ```
53
+
54
+ 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.
55
+
56
+ ## Operations
57
+
58
+ ### 1. Create a new goal
59
+
60
+ When the user expresses a new goal:
61
+
62
+ 1. Get the current owner with `hivemind whoami` (use the userName, e.g. `emanuele.fenocchi`).
63
+ 2. Generate a UUIDv4: `uuidgen` or `node -e 'console.log(crypto.randomUUID())'`.
64
+ 3. Write the goal file at `~/.deeplake/memory/goal/<owner>/opened/<uuid>.md` with the goal description as body.
65
+ 4. Respond to the user that the goal is created.
66
+
67
+ **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/<goal_id>/<kpi-slug>.md` with the body format documented above.
68
+
69
+ ### 2. List goals
70
+
71
+ ```bash
72
+ ls ~/.deeplake/memory/goal/<owner>/opened/
73
+ ls ~/.deeplake/memory/goal/<owner>/in_progress/
74
+ ```
75
+
76
+ Then `cat` each `<uuid>.md` to read the body. Optionally `ls ~/.deeplake/memory/kpi/<uuid>/` and `cat` each KPI to surface progress.
77
+
78
+ ### 3. Edit a goal description
79
+
80
+ ```bash
81
+ # Use Read + Edit (or Write) on the existing file. The VFS handles
82
+ # version-bumping — every write produces a fresh row in the
83
+ # hivemind_goals table.
84
+ ```
85
+
86
+ ### 4. Move a goal to in_progress
87
+
88
+ ```bash
89
+ mv ~/.deeplake/memory/goal/<owner>/opened/<uuid>.md ~/.deeplake/memory/goal/<owner>/in_progress/<uuid>.md
90
+ ```
91
+
92
+ `mv` between status folders is an atomic version-bump. The file body carries over unchanged.
93
+
94
+ ### 5. Close a goal
95
+
96
+ Two equivalent ways:
97
+
98
+ ```bash
99
+ # Explicit mv to closed (recommended — clearest intent)
100
+ mv ~/.deeplake/memory/goal/<owner>/in_progress/<uuid>.md ~/.deeplake/memory/goal/<owner>/closed/<uuid>.md
101
+
102
+ # Or: rm (the VFS interprets rm on a goal path as a soft-close)
103
+ rm ~/.deeplake/memory/goal/<owner>/opened/<uuid>.md
104
+ ```
105
+
106
+ **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.
107
+
108
+ ### 6. Add a KPI manually
109
+
110
+ ```bash
111
+ Write the file at ~/.deeplake/memory/kpi/<uuid>/<kpi-slug>.md with:
112
+ <KPI name>
113
+
114
+ - target: <N>
115
+ - current: 0
116
+ - unit: <unit>
117
+ ```
118
+
119
+ ### 7. Record progress on a KPI
120
+
121
+ Read the KPI file, increment the `current:` line, write it back:
122
+
123
+ ```
124
+ <KPI name>
125
+
126
+ - target: 5
127
+ - current: 3 ← incremented from 2
128
+ - unit: count
129
+ ```
130
+
131
+ Use the Edit tool for the most surgical change (just the line with `current:`).
132
+
133
+ ### 8. Reassign a goal (transfer ownership)
134
+
135
+ ```bash
136
+ mv ~/.deeplake/memory/goal/<old-owner>/<status>/<uuid>.md ~/.deeplake/memory/goal/<new-owner>/<status>/<uuid>.md
137
+ ```
138
+
139
+ Goal ownership lives in the path. KPI files do NOT have an owner segment — they are linked to the goal by `<uuid>`, so they need no change when a goal is reassigned.
140
+
141
+ ## Constraints — DO NOT do these
142
+
143
+ - 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.
144
+ - Do NOT use status values other than `opened`, `in_progress`, `closed`.
145
+ - Do NOT rename the goal_id (the UUID in the filename) via `mv`. The VFS rejects goal_id renames.
146
+ - Do NOT block on the KPI generator subprocess — always spawn it detached (`nohup … &`).
147
+ - Do NOT use the old `hivemind tasks` CLI — that belonged to an earlier design and is being removed.
148
+
149
+ ## Auto-progress from `git commit`
150
+
151
+ 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.
152
+
153
+ To disable globally: `HIVEMIND_AUTO_KPI_FROM_COMMITS=false`.
154
+
155
+ ## Team visibility
156
+
157
+ 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.
@@ -54,13 +54,13 @@ var init_index_marker_store = __esm({
54
54
 
55
55
  // dist/src/utils/stdin.js
56
56
  function readStdin() {
57
- return new Promise((resolve2, reject) => {
57
+ return new Promise((resolve3, reject) => {
58
58
  let data = "";
59
59
  process.stdin.setEncoding("utf-8");
60
60
  process.stdin.on("data", (chunk) => data += chunk);
61
61
  process.stdin.on("end", () => {
62
62
  try {
63
- resolve2(JSON.parse(data));
63
+ resolve3(JSON.parse(data));
64
64
  } catch (err) {
65
65
  reject(new Error(`Failed to parse hook input: ${err}`));
66
66
  }
@@ -98,6 +98,23 @@ function loadConfig() {
98
98
  tableName: process.env.HIVEMIND_TABLE ?? "memory",
99
99
  sessionsTableName: process.env.HIVEMIND_SESSIONS_TABLE ?? "sessions",
100
100
  skillsTableName: process.env.HIVEMIND_SKILLS_TABLE ?? "skills",
101
+ // Defaults match the table name written into the SQL — keep aligned
102
+ // with RULES_COLUMNS / TASKS_COLUMNS / TASK_EVENTS_COLUMNS in
103
+ // deeplake-schema.ts and with the e2e test-org override convention
104
+ // (memory_test / sessions_test → goals_test, etc.) documented in
105
+ // CLAUDE.md.
106
+ rulesTableName: process.env.HIVEMIND_RULES_TABLE ?? "hivemind_rules",
107
+ tasksTableName: process.env.HIVEMIND_TASKS_TABLE ?? "hivemind_tasks",
108
+ taskEventsTableName: process.env.HIVEMIND_TASK_EVENTS_TABLE ?? "hivemind_task_events",
109
+ // Goals + KPIs (refined design — VFS path classifier maps
110
+ // memory/goal/<user>/<status>/<uuid>.md → hivemind_goals row
111
+ // memory/kpi/<uuid>/<kpi_id>.md → hivemind_kpis row
112
+ // See src/shell/deeplake-fs.ts for the translation logic and
113
+ // GOALS_COLUMNS / KPIS_COLUMNS in deeplake-schema.ts for the
114
+ // table shape.
115
+ goalsTableName: process.env.HIVEMIND_GOALS_TABLE ?? "hivemind_goals",
116
+ kpisTableName: process.env.HIVEMIND_KPIS_TABLE ?? "hivemind_kpis",
117
+ codebaseTableName: process.env.HIVEMIND_CODEBASE_TABLE ?? "codebase",
101
118
  memoryPath: process.env.HIVEMIND_MEMORY_PATH ?? join(home, ".deeplake", "memory")
102
119
  };
103
120
  }
@@ -198,6 +215,65 @@ var SKILLS_COLUMNS = Object.freeze([
198
215
  { name: "created_at", sql: "TEXT NOT NULL DEFAULT ''" },
199
216
  { name: "updated_at", sql: "TEXT NOT NULL DEFAULT ''" }
200
217
  ]);
218
+ var RULES_COLUMNS = Object.freeze([
219
+ { name: "id", sql: "TEXT NOT NULL DEFAULT ''" },
220
+ { name: "rule_id", sql: "TEXT NOT NULL DEFAULT ''" },
221
+ { name: "text", sql: "TEXT NOT NULL DEFAULT ''" },
222
+ { name: "scope", sql: "TEXT NOT NULL DEFAULT 'team'" },
223
+ { name: "status", sql: "TEXT NOT NULL DEFAULT 'active'" },
224
+ { name: "assigned_by", sql: "TEXT NOT NULL DEFAULT ''" },
225
+ { name: "version", sql: "BIGINT NOT NULL DEFAULT 1" },
226
+ { name: "created_at", sql: "TEXT NOT NULL DEFAULT ''" },
227
+ { name: "agent", sql: "TEXT NOT NULL DEFAULT 'manual'" },
228
+ { name: "plugin_version", sql: "TEXT NOT NULL DEFAULT ''" }
229
+ ]);
230
+ var TASKS_COLUMNS = Object.freeze([
231
+ { name: "id", sql: "TEXT NOT NULL DEFAULT ''" },
232
+ { name: "task_id", sql: "TEXT NOT NULL DEFAULT ''" },
233
+ { name: "text", sql: "TEXT NOT NULL DEFAULT ''" },
234
+ { name: "scope", sql: "TEXT NOT NULL DEFAULT 'me'" },
235
+ { name: "status", sql: "TEXT NOT NULL DEFAULT 'active'" },
236
+ { name: "assigned_to", sql: "TEXT NOT NULL DEFAULT ''" },
237
+ { name: "assigned_by", sql: "TEXT NOT NULL DEFAULT ''" },
238
+ { name: "kpis", sql: "JSONB" },
239
+ { name: "version", sql: "BIGINT NOT NULL DEFAULT 1" },
240
+ { name: "created_at", sql: "TEXT NOT NULL DEFAULT ''" },
241
+ { name: "agent", sql: "TEXT NOT NULL DEFAULT 'manual'" },
242
+ { name: "plugin_version", sql: "TEXT NOT NULL DEFAULT ''" }
243
+ ]);
244
+ var TASK_EVENTS_COLUMNS = Object.freeze([
245
+ { name: "id", sql: "TEXT NOT NULL DEFAULT ''" },
246
+ { name: "task_id", sql: "TEXT NOT NULL DEFAULT ''" },
247
+ { name: "task_version", sql: "BIGINT NOT NULL DEFAULT 1" },
248
+ { name: "kpi_id", sql: "TEXT NOT NULL DEFAULT ''" },
249
+ { name: "value", sql: "BIGINT NOT NULL DEFAULT 0" },
250
+ { name: "note", sql: "TEXT NOT NULL DEFAULT ''" },
251
+ { name: "source", sql: "TEXT NOT NULL DEFAULT 'user'" },
252
+ { name: "agent", sql: "TEXT NOT NULL DEFAULT ''" },
253
+ { name: "ts", sql: "TEXT NOT NULL DEFAULT ''" },
254
+ { name: "plugin_version", sql: "TEXT NOT NULL DEFAULT ''" }
255
+ ]);
256
+ var GOALS_COLUMNS = Object.freeze([
257
+ { name: "id", sql: "TEXT NOT NULL DEFAULT ''" },
258
+ { name: "goal_id", sql: "TEXT NOT NULL DEFAULT ''" },
259
+ { name: "owner", sql: "TEXT NOT NULL DEFAULT ''" },
260
+ { name: "status", sql: "TEXT NOT NULL DEFAULT 'opened'" },
261
+ { name: "content", sql: "TEXT NOT NULL DEFAULT ''" },
262
+ { name: "version", sql: "BIGINT NOT NULL DEFAULT 1" },
263
+ { name: "created_at", sql: "TEXT NOT NULL DEFAULT ''" },
264
+ { name: "agent", sql: "TEXT NOT NULL DEFAULT 'manual'" },
265
+ { name: "plugin_version", sql: "TEXT NOT NULL DEFAULT ''" }
266
+ ]);
267
+ var KPIS_COLUMNS = Object.freeze([
268
+ { name: "id", sql: "TEXT NOT NULL DEFAULT ''" },
269
+ { name: "goal_id", sql: "TEXT NOT NULL DEFAULT ''" },
270
+ { name: "kpi_id", sql: "TEXT NOT NULL DEFAULT ''" },
271
+ { name: "content", sql: "TEXT NOT NULL DEFAULT ''" },
272
+ { name: "version", sql: "BIGINT NOT NULL DEFAULT 1" },
273
+ { name: "created_at", sql: "TEXT NOT NULL DEFAULT ''" },
274
+ { name: "agent", sql: "TEXT NOT NULL DEFAULT 'manual'" },
275
+ { name: "plugin_version", sql: "TEXT NOT NULL DEFAULT ''" }
276
+ ]);
201
277
  function validateSchema(label, cols) {
202
278
  const seen = /* @__PURE__ */ new Set();
203
279
  for (const col of cols) {
@@ -215,9 +291,38 @@ function validateSchema(label, cols) {
215
291
  }
216
292
  }
217
293
  }
294
+ var CODEBASE_COLUMNS = Object.freeze([
295
+ // Identity key (matches the PK below)
296
+ { name: "org_id", sql: "TEXT NOT NULL DEFAULT ''" },
297
+ { name: "workspace_id", sql: "TEXT NOT NULL DEFAULT ''" },
298
+ { name: "repo_slug", sql: "TEXT NOT NULL DEFAULT ''" },
299
+ { name: "user_id", sql: "TEXT NOT NULL DEFAULT ''" },
300
+ { name: "worktree_id", sql: "TEXT NOT NULL DEFAULT ''" },
301
+ { name: "commit_sha", sql: "TEXT NOT NULL DEFAULT ''" },
302
+ // Observation metadata
303
+ { name: "parent_sha", sql: "TEXT NOT NULL DEFAULT ''" },
304
+ { name: "branch", sql: "TEXT NOT NULL DEFAULT ''" },
305
+ { name: "ts", sql: "TIMESTAMP" },
306
+ { name: "pushed_by", sql: "TEXT NOT NULL DEFAULT ''" },
307
+ // Snapshot payload
308
+ { name: "snapshot_sha256", sql: "TEXT NOT NULL DEFAULT ''" },
309
+ { name: "snapshot_jsonb", sql: "TEXT NOT NULL DEFAULT ''" },
310
+ { name: "node_count", sql: "BIGINT NOT NULL DEFAULT 0" },
311
+ { name: "edge_count", sql: "BIGINT NOT NULL DEFAULT 0" },
312
+ // Generator metadata (for drift diagnostics — what hivemind version produced this?)
313
+ { name: "generator", sql: "TEXT NOT NULL DEFAULT 'hivemind-graph'" },
314
+ { name: "generator_version", sql: "TEXT NOT NULL DEFAULT ''" },
315
+ { name: "schema_version", sql: "BIGINT NOT NULL DEFAULT 1" }
316
+ ]);
218
317
  validateSchema("MEMORY_COLUMNS", MEMORY_COLUMNS);
219
318
  validateSchema("SESSIONS_COLUMNS", SESSIONS_COLUMNS);
220
319
  validateSchema("SKILLS_COLUMNS", SKILLS_COLUMNS);
320
+ validateSchema("RULES_COLUMNS", RULES_COLUMNS);
321
+ validateSchema("TASKS_COLUMNS", TASKS_COLUMNS);
322
+ validateSchema("TASK_EVENTS_COLUMNS", TASK_EVENTS_COLUMNS);
323
+ validateSchema("GOALS_COLUMNS", GOALS_COLUMNS);
324
+ validateSchema("KPIS_COLUMNS", KPIS_COLUMNS);
325
+ validateSchema("CODEBASE_COLUMNS", CODEBASE_COLUMNS);
221
326
  function buildCreateTableSql(tableName, cols) {
222
327
  const safe = sqlIdent(tableName);
223
328
  const colSql = cols.map((c) => `${c.name} ${c.sql}`).join(", ");
@@ -442,7 +547,7 @@ function getQueryTimeoutMs() {
442
547
  return Number(process.env.HIVEMIND_QUERY_TIMEOUT_MS ?? 1e4);
443
548
  }
444
549
  function sleep2(ms) {
445
- return new Promise((resolve2) => setTimeout(resolve2, ms));
550
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
446
551
  }
447
552
  function isTimeoutError(error) {
448
553
  const name = error instanceof Error ? error.name.toLowerCase() : "";
@@ -472,7 +577,7 @@ var Semaphore = class {
472
577
  this.active++;
473
578
  return;
474
579
  }
475
- await new Promise((resolve2) => this.waiting.push(resolve2));
580
+ await new Promise((resolve3) => this.waiting.push(resolve3));
476
581
  }
477
582
  release() {
478
583
  this.active--;
@@ -782,6 +887,24 @@ var DeeplakeApi = class {
782
887
  * This sidesteps the Deeplake UPDATE-coalescing quirk that bit the wiki
783
888
  * worker.
784
889
  */
890
+ /**
891
+ * Create the codebase table. One row per (org, workspace, repo, user,
892
+ * worktree, commit) — see CODEBASE_COLUMNS for the schema. Healing
893
+ * + index follow the same pattern as ensureSessionsTable.
894
+ */
895
+ async ensureCodebaseTable(name) {
896
+ const safe = sqlIdent(name);
897
+ const tables = await this.listTables();
898
+ if (!tables.includes(safe)) {
899
+ log3(`table "${safe}" not found, creating`);
900
+ await this.createTableWithRetry(buildCreateTableSql(safe, CODEBASE_COLUMNS), safe);
901
+ log3(`table "${safe}" created`);
902
+ if (!tables.includes(safe))
903
+ this._tablesCache = [...tables, safe];
904
+ }
905
+ await this.healSchema(safe, CODEBASE_COLUMNS);
906
+ await this.ensureLookupIndex(safe, "codebase_identity", `("org_id", "workspace_id", "repo_slug", "user_id", "worktree_id", "commit_sha")`);
907
+ }
785
908
  async ensureSkillsTable(name) {
786
909
  const safe = sqlIdent(name);
787
910
  const tables = await this.listTables();
@@ -795,6 +918,111 @@ var DeeplakeApi = class {
795
918
  await this.healSchema(safe, SKILLS_COLUMNS);
796
919
  await this.ensureLookupIndex(safe, "project_key_name", `("project_key", "name")`);
797
920
  }
921
+ /**
922
+ * Create the rules table.
923
+ *
924
+ * One row per rule version (same write pattern as skills): edits INSERT
925
+ * a fresh row with version+1, reads pick latest per rule_id via
926
+ * `ORDER BY version DESC LIMIT 1`. Sidesteps the Deeplake
927
+ * UPDATE-coalescing quirk by never UPDATEing.
928
+ */
929
+ async ensureRulesTable(name) {
930
+ const safe = sqlIdent(name);
931
+ const tables = await this.listTables();
932
+ if (!tables.includes(safe)) {
933
+ log3(`table "${safe}" not found, creating`);
934
+ await this.createTableWithRetry(buildCreateTableSql(safe, RULES_COLUMNS), safe);
935
+ log3(`table "${safe}" created`);
936
+ if (!tables.includes(safe))
937
+ this._tablesCache = [...tables, safe];
938
+ }
939
+ await this.healSchema(safe, RULES_COLUMNS);
940
+ await this.ensureLookupIndex(safe, "rule_id_version", `("rule_id", "version")`);
941
+ }
942
+ /**
943
+ * Create the tasks table.
944
+ *
945
+ * Same write pattern as rules + skills. `kpis` is a nullable JSONB
946
+ * column with the agent's KPI metadata; KPI current values come from
947
+ * `task_events` (SUM(value)), not this snapshot.
948
+ */
949
+ async ensureTasksTable(name) {
950
+ const safe = sqlIdent(name);
951
+ const tables = await this.listTables();
952
+ if (!tables.includes(safe)) {
953
+ log3(`table "${safe}" not found, creating`);
954
+ await this.createTableWithRetry(buildCreateTableSql(safe, TASKS_COLUMNS), safe);
955
+ log3(`table "${safe}" created`);
956
+ if (!tables.includes(safe))
957
+ this._tablesCache = [...tables, safe];
958
+ }
959
+ await this.healSchema(safe, TASKS_COLUMNS);
960
+ await this.ensureLookupIndex(safe, "task_id_version", `("task_id", "version")`);
961
+ }
962
+ /**
963
+ * Create the task-events table.
964
+ *
965
+ * Append-only. Every INSERT is a fresh row; never UPDATE. KPI current
966
+ * value is `SUM(value) WHERE task_id=? AND kpi_id=?`. Index on
967
+ * (task_id, kpi_id) is the canonical aggregation key.
968
+ */
969
+ async ensureTaskEventsTable(name) {
970
+ const safe = sqlIdent(name);
971
+ const tables = await this.listTables();
972
+ if (!tables.includes(safe)) {
973
+ log3(`table "${safe}" not found, creating`);
974
+ await this.createTableWithRetry(buildCreateTableSql(safe, TASK_EVENTS_COLUMNS), safe);
975
+ log3(`table "${safe}" created`);
976
+ if (!tables.includes(safe))
977
+ this._tablesCache = [...tables, safe];
978
+ }
979
+ await this.healSchema(safe, TASK_EVENTS_COLUMNS);
980
+ await this.ensureLookupIndex(safe, "task_id_kpi_id", `("task_id", "kpi_id")`);
981
+ }
982
+ /**
983
+ * Create the goals table.
984
+ *
985
+ * Backed by the VFS path convention memory/goal/<owner>/<status>/<goal_id>.md.
986
+ * INSERT-only version-bumped: rm and mv operations translate to fresh
987
+ * v=N+1 rows (status flips for mv → closed; rm is the same soft-close).
988
+ * The (goal_id, version) index lets the VFS dispatch a cheap latest-row
989
+ * read on cat / Read of a single goal.
990
+ */
991
+ async ensureGoalsTable(name) {
992
+ const safe = sqlIdent(name);
993
+ const tables = await this.listTables();
994
+ if (!tables.includes(safe)) {
995
+ log3(`table "${safe}" not found, creating`);
996
+ await this.createTableWithRetry(buildCreateTableSql(safe, GOALS_COLUMNS), safe);
997
+ log3(`table "${safe}" created`);
998
+ if (!tables.includes(safe))
999
+ this._tablesCache = [...tables, safe];
1000
+ }
1001
+ await this.healSchema(safe, GOALS_COLUMNS);
1002
+ await this.ensureLookupIndex(safe, "goal_id_version", `("goal_id", "version")`);
1003
+ await this.ensureLookupIndex(safe, "owner_status", `("owner", "status")`);
1004
+ }
1005
+ /**
1006
+ * Create the kpis table.
1007
+ *
1008
+ * Backed by memory/kpi/<goal_id>/<kpi_id>.md. KPI rows do NOT carry
1009
+ * owner — ownership derives from the parent goal via logical join on
1010
+ * goal_id. INSERT-only version-bumped. (goal_id, kpi_id) index is the
1011
+ * canonical lookup the VFS uses on Read and Write.
1012
+ */
1013
+ async ensureKpisTable(name) {
1014
+ const safe = sqlIdent(name);
1015
+ const tables = await this.listTables();
1016
+ if (!tables.includes(safe)) {
1017
+ log3(`table "${safe}" not found, creating`);
1018
+ await this.createTableWithRetry(buildCreateTableSql(safe, KPIS_COLUMNS), safe);
1019
+ log3(`table "${safe}" created`);
1020
+ if (!tables.includes(safe))
1021
+ this._tablesCache = [...tables, safe];
1022
+ }
1023
+ await this.healSchema(safe, KPIS_COLUMNS);
1024
+ await this.ensureLookupIndex(safe, "goal_id_kpi_id", `("goal_id", "kpi_id")`);
1025
+ }
798
1026
  };
799
1027
 
800
1028
  // dist/src/utils/session-path.js
@@ -1057,7 +1285,7 @@ var EmbedClient = class {
1057
1285
  }
1058
1286
  }
1059
1287
  connectOnce() {
1060
- return new Promise((resolve2, reject) => {
1288
+ return new Promise((resolve3, reject) => {
1061
1289
  const sock = connect(this.socketPath);
1062
1290
  const to = setTimeout(() => {
1063
1291
  sock.destroy();
@@ -1065,7 +1293,7 @@ var EmbedClient = class {
1065
1293
  }, this.timeoutMs);
1066
1294
  sock.once("connect", () => {
1067
1295
  clearTimeout(to);
1068
- resolve2(sock);
1296
+ resolve3(sock);
1069
1297
  });
1070
1298
  sock.once("error", (e) => {
1071
1299
  clearTimeout(to);
@@ -1147,7 +1375,7 @@ var EmbedClient = class {
1147
1375
  throw new Error("daemon did not become ready within spawnWaitMs");
1148
1376
  }
1149
1377
  sendAndWait(sock, req) {
1150
- return new Promise((resolve2, reject) => {
1378
+ return new Promise((resolve3, reject) => {
1151
1379
  let buf = "";
1152
1380
  const to = setTimeout(() => {
1153
1381
  sock.destroy();
@@ -1162,7 +1390,7 @@ var EmbedClient = class {
1162
1390
  const line = buf.slice(0, nl);
1163
1391
  clearTimeout(to);
1164
1392
  try {
1165
- resolve2(JSON.parse(line));
1393
+ resolve3(JSON.parse(line));
1166
1394
  } catch (e) {
1167
1395
  reject(e);
1168
1396
  }
@@ -1818,9 +2046,54 @@ function spawnSkillifyWorker(opts) {
1818
2046
 
1819
2047
  // dist/src/skillify/state.js
1820
2048
  import { readFileSync as readFileSync9, writeFileSync as writeFileSync8, writeSync as writeSync3, mkdirSync as mkdirSync10, renameSync as renameSync6, rmdirSync, existsSync as existsSync9, lstatSync as lstatSync2, unlinkSync as unlinkSync5, openSync as openSync4, closeSync as closeSync4 } from "node:fs";
2049
+ import { join as join18 } from "node:path";
2050
+
2051
+ // dist/src/utils/repo-identity.js
1821
2052
  import { execSync as execSync2 } from "node:child_process";
1822
2053
  import { createHash } from "node:crypto";
1823
- import { join as join18, basename as basename2 } from "node:path";
2054
+ import { basename as basename2, resolve as resolve2 } from "node:path";
2055
+ var DEFAULT_PORTS = {
2056
+ http: "80",
2057
+ https: "443",
2058
+ ssh: "22",
2059
+ git: "9418"
2060
+ };
2061
+ function normalizeGitRemoteUrl(url) {
2062
+ let s = url.trim();
2063
+ const schemeMatch = s.match(/^([a-z][a-z0-9+.-]*):\/\//i);
2064
+ const scheme = schemeMatch ? schemeMatch[1].toLowerCase() : null;
2065
+ if (schemeMatch)
2066
+ s = s.slice(schemeMatch[0].length);
2067
+ if (!scheme) {
2068
+ const scp = s.match(/^(?:[^@/\s]+@)?([^:/\s]+):(.+)$/);
2069
+ if (scp)
2070
+ s = `${scp[1]}/${scp[2]}`;
2071
+ }
2072
+ s = s.replace(/^[^@/]+@/, "");
2073
+ if (scheme && DEFAULT_PORTS[scheme]) {
2074
+ s = s.replace(new RegExp(`^([^/]+):${DEFAULT_PORTS[scheme]}(/|$)`), "$1$2");
2075
+ }
2076
+ s = s.replace(/\.git\/?$/i, "");
2077
+ s = s.replace(/\/+$/, "");
2078
+ return s.toLowerCase();
2079
+ }
2080
+ function deriveProjectKey(cwd) {
2081
+ const absCwd = resolve2(cwd);
2082
+ const project = basename2(absCwd) || "unknown";
2083
+ let signature = null;
2084
+ try {
2085
+ const raw = execSync2("git config --get remote.origin.url", {
2086
+ cwd: absCwd,
2087
+ encoding: "utf-8",
2088
+ stdio: ["ignore", "pipe", "ignore"]
2089
+ }).trim();
2090
+ signature = raw ? normalizeGitRemoteUrl(raw) : null;
2091
+ } catch {
2092
+ }
2093
+ const input = signature ?? absCwd;
2094
+ const key = createHash("sha1").update(input).digest("hex").slice(0, 16);
2095
+ return { key, project };
2096
+ }
1824
2097
 
1825
2098
  // dist/src/skillify/legacy-migration.js
1826
2099
  import { existsSync as existsSync8, renameSync as renameSync5 } from "node:fs";
@@ -1875,47 +2148,6 @@ function statePath2(projectKey) {
1875
2148
  function lockPath3(projectKey) {
1876
2149
  return join18(getStateDir(), `${projectKey}.lock`);
1877
2150
  }
1878
- var DEFAULT_PORTS = {
1879
- http: "80",
1880
- https: "443",
1881
- ssh: "22",
1882
- git: "9418"
1883
- };
1884
- function normalizeGitRemoteUrl(url) {
1885
- let s = url.trim();
1886
- const schemeMatch = s.match(/^([a-z][a-z0-9+.-]*):\/\//i);
1887
- const scheme = schemeMatch ? schemeMatch[1].toLowerCase() : null;
1888
- if (schemeMatch)
1889
- s = s.slice(schemeMatch[0].length);
1890
- if (!scheme) {
1891
- const scp = s.match(/^(?:[^@/\s]+@)?([^:/\s]+):(.+)$/);
1892
- if (scp)
1893
- s = `${scp[1]}/${scp[2]}`;
1894
- }
1895
- s = s.replace(/^[^@/]+@/, "");
1896
- if (scheme && DEFAULT_PORTS[scheme]) {
1897
- s = s.replace(new RegExp(`^([^/]+):${DEFAULT_PORTS[scheme]}(/|$)`), "$1$2");
1898
- }
1899
- s = s.replace(/\.git\/?$/i, "");
1900
- s = s.replace(/\/+$/, "");
1901
- return s.toLowerCase();
1902
- }
1903
- function deriveProjectKey(cwd) {
1904
- const project = basename2(cwd) || "unknown";
1905
- let signature = null;
1906
- try {
1907
- const raw = execSync2("git config --get remote.origin.url", {
1908
- cwd,
1909
- encoding: "utf-8",
1910
- stdio: ["ignore", "pipe", "ignore"]
1911
- }).trim();
1912
- signature = raw ? normalizeGitRemoteUrl(raw) : null;
1913
- } catch {
1914
- }
1915
- const input = signature ?? cwd;
1916
- const key = createHash("sha1").update(input).digest("hex").slice(0, 16);
1917
- return { key, project };
1918
- }
1919
2151
  function readState2(projectKey) {
1920
2152
  migrateLegacyStateDir();
1921
2153
  const p = statePath2(projectKey);