Skip to content

Rename: skilify -> skillify (fix typo)#116

Open
efenocchi wants to merge 2 commits intomainfrom
feat/rename-skilify-to-skillify
Open

Rename: skilify -> skillify (fix typo)#116
efenocchi wants to merge 2 commits intomainfrom
feat/rename-skilify-to-skillify

Conversation

@efenocchi
Copy link
Copy Markdown
Collaborator

@efenocchi efenocchi commented May 10, 2026

Closes #114.

Summary

Hard switch from skilify to skillify across the tree. The CLI subcommand and module were named skilify but the natural derivation from skill is skillify. No deprecation alias — hivemind skilify ... now returns Unknown command: skilify.

Scope

  • src/skilify/ -> src/skillify/ (incl. skilify-worker.ts, spawn-skilify-worker.ts)
  • src/commands/skilify.ts -> skillify.ts, runSkilifyCommand -> runSkillifyCommand
  • 16 claude-code/tests/skilify-*.test.ts files renamed
  • Tree-wide case-aware text replace across src/, openclaw/, pi/extension-source/, .github/workflows/, README, RELEASE_CHECKLIST, esbuild.config.mjs, vitest.config.ts, hook tests
  • Stale <agent>/bundle/skilify-worker.js artifacts removed; bundles regenerated to <agent>/bundle/skillify-worker.js

State directory auto-migration

New src/skillify/legacy-migration.ts: one-shot renameSync of ~/.deeplake/state/skilify/ -> ~/.deeplake/state/skillify/ on first boot of the renamed version. Wired into all four mkdirSync entry points (state.ts, scope-config.ts, manifest.ts, auto-pull.ts). EXDEV/EPERM falls back to leaving the legacy dir in place.

OpenClaw is a self-contained bundle that can't import from src/skillify/, so the migration is inlined into openclaw/src/index.ts and called before the existing fsMkdir on the state dir — once the new dir exists the migration becomes a no-op and any legacy data would be orphaned.

Tests

New claude-code/tests/skillify-legacy-migration.test.ts (5 tests): legacy-missing, current-already-exists (no clobber), happy-path rename preserving config.json/pulled.json/per-project state, idempotent re-call (module-level attempted flag), and renameSync EXDEV swallowed.

Audit

After the rename, the only skilify references in the tree are 4 intentional string literals (the migration helper's legacy-path target, the inlined openclaw copy, JSDoc, and a test fixture) plus their 18 esbuild compilations in bundle/. Zero filenames, zero camelCase/PascalCase identifiers.

Risk

Users with hivemind skilify aliases / scripts get a hard error after upgrade. State directory auto-migrates so installed-skill manifests, scope config, and per-project state survive without manual intervention. If renameSync fails (cross-device link, perms), the legacy dir stays put and the new dir starts fresh — pull repopulates pulled.json, but unpull of pre-rename installs may need manual cleanup.

Sequencing

Originally planned to wait for #112 to merge to avoid conflicts; #112 has since landed plus follow-ups (autopull throttle removal, maybeAutoPull -> autoPullSkills rename, README condensed into docs/SKILLIFY.md). Branch was based on the pre-merge tip of #112 and merged with origin/main afterward; conflicts in auto-pull.ts, the four session-start.ts files, the CLI USAGE block, two test files, README, and 6 bundle outputs are resolved in 148337d.

Test plan

  • npm test green (2184/2184 in CI)
  • npm run build produces all 12 + 10 + 9 + 9 + 1 + 1 + 1 + 1 bundles
  • After upgrade on a machine with a populated ~/.deeplake/state/skilify/, the directory has been renamed in place to ~/.deeplake/state/skillify/ on the first SessionStart and installed pulled skills still resolve
  • hivemind skilify (legacy spelling) returns Unknown command: skilify
  • hivemind skillify works as documented

Summary by CodeRabbit

  • Bug Fixes

    • Corrected skill management command spelling from skilify to skillify throughout the system.
    • Added automatic legacy state directory migration that renames older ~/.deeplake/state/skilify to ~/.deeplake/state/skillify on first run.
    • Fixed module import paths and worker bundle references to use correct skillify naming.
  • Chores

    • Updated build configuration and test files to reference corrected module paths and naming conventions.

efenocchi added 2 commits May 10, 2026 01:28
Closes #114.

The CLI subcommand and module were named `skilify` but the natural
derivation from `skill` is `skillify`. Hard switch — no `skilify` alias.

- Rename src/skilify/ -> src/skillify/ (incl. skilify-worker.ts and
  spawn-skilify-worker.ts inside)
- Rename src/commands/skilify.ts -> skillify.ts (export
  runSkillifyCommand)
- Rename 16 claude-code/tests/skilify-*.test.ts files
- Tree-wide case-aware text replace across src/, openclaw/,
  pi/extension-source/, .github/workflows/, README, RELEASE_CHECKLIST,
  esbuild.config.mjs, vitest.config.ts, hook tests
- Remove stale <agent>/bundle/skilify-worker.js artifacts; rebuild
  bundles to <agent>/bundle/skillify-worker.js

Add src/skillify/legacy-migration.ts: one-shot renameSync of
~/.deeplake/state/skilify/ -> ~/.deeplake/state/skillify/ on first
boot of the renamed version. Wired into all four mkdirSync entry
points (state.ts, scope-config.ts, manifest.ts, auto-pull.ts).
EXDEV/EPERM falls back to leaving the legacy dir in place.

OpenClaw can't import the shared skillify TS modules (self-contained
bundle), so the migration is inlined into openclaw/src/index.ts and
called before the existing fsMkdir on the state dir — once the new
dir exists the migration becomes a no-op and any legacy data would
be orphaned.

Add claude-code/tests/skillify-legacy-migration.test.ts: 5 tests
covering legacy-missing, current-already-exists (no clobber),
happy-path rename, idempotent re-call (module-level attempted flag),
and renameSync EXDEV failure swallowed.

CLI dispatcher in src/cli/index.ts already only matches "skillify";
"skilify" falls through to "Unknown command: skilify" — no alias
logic to remove.
Resolves conflicts from main's post-#112 follow-ups:
- 5dead1f autopull: throttle removed (run every SessionStart, idempotent)
- 9b54af0 maybeAutoPull -> autoPullSkills
- 26cb2e2 skilify-auto-pull test covers the DeeplakeApi-backed query path
- 7c57d6d / 86d140a docs: skilify -> skillify
- README condensed; details moved to docs/SKILLIFY.md

Resolutions:
- src/skillify/auto-pull.ts: take main's no-throttle body, apply
  skillify path rename. readIntervalMs/readLastRun/writeLastRun and
  the autopull-last-run.json timestamp file are gone. The legacy-state
  migration import is no longer needed in this file (no state-dir
  touchpoints remaining).
- src/hooks/{,codex/,cursor/,hermes/}session-start.ts: take main's
  autoPullSkills callsite + comment, keep src/skillify import path.
- src/cli/index.ts USAGE: take main's no-throttle docstring, keep
  skillify subcommand name.
- claude-code/tests/skillify-auto-pull.test.ts: take main's deletion
  of the throttle test blocks + DeeplakeApi mock setup, apply skillify
  rename to remaining import path.
- claude-code/tests/codex-session-start-hook.test.ts: take main's
  autoPullSkillsMock binding, keep src/skillify import path.
- README.md: take main's condensed Skillify section pointing at
  docs/SKILLIFY.md.
- bundle/* (esbuild outputs): take ours then `npm run build` to
  regenerate from the resolved sources.

Bundles regenerated. Test suite green (2184/2184).
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 10, 2026

📝 Walkthrough

Walkthrough

Project-wide rename from “skilify” to “skillify,” adding a one-time legacy state-directory migration and updating CLI, hooks, bundles, build entries, docs, and tests accordingly.

Changes

Skillify rename and state migration

Layer / File(s) Summary
All changes
src/skillify/*, src/commands/skillify.ts, src/hooks/..., openclaw/..., pi/..., */bundle/*, esbuild.config.mjs, .github/workflows/*, RELEASE_CHECKLIST.md, vitest.config.ts, tests/*
Rename “skilify”→“skillify”; add legacy state-dir migration; update spawn/log paths; rebundle workers; fix imports/help text; adjust build entries and tests.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested reviewers

  • kaghni

Poem

Two L’s align, a tidy hop—
I nudge the names from tip to top.
Old burrows moved, new paths aglow,
logs whisper soft where workers go.
Pull, unpull—skills flutter by;
behold: we skillify. 🐇✨

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/rename-skilify-to-skillify

@claude
Copy link
Copy Markdown

claude Bot commented May 10, 2026

Claude finished @efenocchi's task in 4m 47s —— View job


Claude PR Review

  • Read PR title/body and changed files
  • Inspect rename diff for stragglers / inconsistencies
  • Review src/skillify/legacy-migration.ts
  • Review the openclaw inlined migration (openclaw/src/index.ts:317-334)
  • Review claude-code/tests/skillify-legacy-migration.test.ts
  • Spot-check renamed call sites (state.ts, scope-config.ts, manifest.ts, auto-pull.ts)

No blocking issues found. The rename is consistent end-to-end and the migration is the right shape:

  • Audit confirms the only remaining skilify references are the four intentional ones (legacy path constants in legacy-migration.ts:27, openclaw/src/index.ts:317, the test fixture path in skillify-legacy-migration.test.ts:19, plus their compiled bundle copies).
  • migrateLegacyStateDir is wired ahead of every state-dir mkdirSync in state.ts (write/lock/tryAcquire), scope-config.ts (load/save), and manifest.ts (load/save). auto-pull.ts reaches it transitively via pull.ts → loadManifest/saveManifest.
  • The openclaw inlined copy is functionally equivalent: same legacy exists && !current → rename, else no-op, swallowed on EXDEV/EPERM, called inside tryAcquireOpenclawSkillifyLock before any mkdirSync on the new state dir.
  • hivemind skilify falls through to the existing Unknown command: ${cmd} path at src/cli/index.ts:269 — matches the PR's stated behavior.
  • Test coverage exercises the four meaningful states (legacy-missing, current-already-exists with no clobber, happy rename preserving config.json/pulled.json/per-project state, idempotent re-call via the attempted flag) plus a mocked EXDEV swallow. The vi.resetModules() + per-test process.env.HOME = mkdtempSync(...) pattern is the right way to avoid the module-level attempted cache leaking between cases.

Two minor non-bug observations (no action needed):

  1. PR body says the migration is wired into "all four mkdirSync entry points" including auto-pull.ts, but auto-pull.ts itself doesn't import migrateLegacyStateDir — it calls runPull, which routes through manifest.ts where the migration runs. Description is imprecise but the behavior is correct.
  2. On EXDEV/EPERM, the legacy directory is left in place and the new one starts empty — pulled-skill manifests in the legacy dir get orphaned. The PR risk section already calls this out and pull repopulates pulled.json on next run, so this is a deliberate trade-off, not a regression.
    • Branch: feat/rename-skilify-to-skillify

@coderabbitai coderabbitai Bot requested a review from kaghni May 10, 2026 03:30
@github-actions
Copy link
Copy Markdown
Contributor

Coverage Report

Scope: files changed in this PR. Enforced threshold: 90% per metric (per file via vitest.config.ts).

Status Category Percentage Covered / Total
🟢 Lines 94.93% (🎯 90%) 1441 / 1518
🟢 Statements 94.29% (🎯 90%) 1701 / 1804
🟢 Functions 98.32% (🎯 90%) 176 / 179
🔴 Branches 86.76% (🎯 90%) 1068 / 1231
File Coverage — 31 files changed
File Stmts Branches Functions Lines
src/cli/index.ts 🟢 97.3% 🟢 95.9% 🟢 100.0% 🟢 97.3%
src/cli/install-pi.ts 🟢 100.0% 🟢 100.0% 🟢 100.0% 🟢 100.0%
src/commands/skillify.ts 🟢 90.3% 🔴 83.6% 🟢 95.5% 🔴 88.5%
src/hooks/capture.ts 🟢 98.6% 🟢 90.0% 🟢 100.0% 🟢 100.0%
src/hooks/codex/session-start.ts 🟢 100.0% 🟢 93.8% 🟢 100.0% 🟢 100.0%
src/hooks/codex/stop.ts 🟢 98.5% 🔴 87.0% 🟢 100.0% 🟢 98.3%
src/hooks/cursor/capture.ts 🔴 86.1% 🔴 85.7% 🟢 100.0% 🔴 86.8%
src/hooks/cursor/session-end.ts 🟢 96.0% 🔴 85.7% 🟢 100.0% 🟢 94.4%
src/hooks/cursor/session-start.ts 🟢 98.1% 🟢 90.6% 🟢 100.0% 🟢 97.9%
src/hooks/hermes/capture.ts 🟢 92.8% 🔴 89.5% 🟢 100.0% 🟢 94.3%
src/hooks/hermes/session-end.ts 🟢 96.2% 🔴 85.0% 🟢 100.0% 🟢 94.7%
src/hooks/hermes/session-start.ts 🟢 100.0% 🔴 87.5% 🟢 100.0% 🟢 100.0%
src/hooks/session-end.ts 🟢 100.0% 🟢 100.0% 🟢 100.0% 🟢 100.0%
src/hooks/session-start.ts 🟢 100.0% 🟢 91.2% 🟢 100.0% 🟢 100.0%
src/skillify/agent-roots.ts 🟢 100.0% 🟢 100.0% 🟢 100.0% 🟢 100.0%
src/skillify/auto-pull.ts 🟢 100.0% 🔴 78.3% 🟢 100.0% 🟢 100.0%
src/skillify/autopull-worker.ts 🔴 0.0% 🟢 100.0% 🔴 0.0% 🔴 0.0%
src/skillify/extractors/index.ts 🟢 100.0% 🟢 100.0% 🟢 100.0% 🟢 100.0%
src/skillify/gate-parser.ts 🟢 96.5% 🟢 100.0% 🟢 100.0% 🟢 95.2%
src/skillify/gate-runner.ts 🟢 96.0% 🔴 76.0% 🟢 100.0% 🟢 96.0%
src/skillify/legacy-migration.ts 🟢 100.0% 🔴 87.5% 🟢 100.0% 🟢 100.0%
src/skillify/manifest.ts 🟢 93.3% 🟢 90.4% 🟢 100.0% 🟢 98.2%
src/skillify/pull.ts 🟢 96.1% 🔴 78.5% 🟢 100.0% 🟢 98.6%
src/skillify/scope-config.ts 🟢 100.0% 🟢 100.0% 🟢 100.0% 🟢 100.0%
src/skillify/skill-writer.ts 🟢 98.9% 🔴 88.3% 🟢 100.0% 🟢 100.0%
src/skillify/skillify-worker.ts
src/skillify/skills-table.ts 🟢 93.8% 🔴 75.0% 🟢 100.0% 🟢 100.0%
src/skillify/spawn-skillify-worker.ts
src/skillify/state.ts 🔴 85.4% 🔴 74.3% 🟢 100.0% 🔴 87.8%
src/skillify/triggers.ts 🟢 94.6% 🔴 75.0% 🟢 100.0% 🟢 93.3%
src/skillify/unpull.ts 🔴 82.0% 🔴 83.3% 🔴 80.0% 🔴 85.2%

Generated for commit 9921919.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (7)
openclaw/src/index.ts (1)

346-355: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Release the worker lock on pre-spawn failures.

After Line 410 acquires ${projectKey}.worker.lock, the mkdir, config-write, and spawn-error paths just return. That leaves a stale lock behind, and every later agent_end for the same channel gets skipped as if a worker were still running. A single transient filesystem/spawn error can permanently disable OpenClaw skill mining until the user deletes the lock manually.

Suggested fix
 import {
   existsSync as fsExists, mkdirSync as fsMkdir, openSync as fsOpen,
   closeSync as fsClose, writeFileSync as fsWriteFile, constants as fsConstants,
-  readFileSync as fsReadFile, renameSync as fsRename,
+  readFileSync as fsReadFile, renameSync as fsRename, rmSync as fsRm,
 } from "node:fs";
 ...
-function tryAcquireOpenclawSkillifyLock(projectKey: string): boolean {
+function tryAcquireOpenclawSkillifyLock(projectKey: string): string | null {
   try {
     migrateOpenclawSkillifyLegacyStateDir();
     fsMkdir(OPENCLAW_SKILLIFY_STATE_DIR, { recursive: true });
     const lockPath = joinPath(OPENCLAW_SKILLIFY_STATE_DIR, `${projectKey}.worker.lock`);
     const fd = fsOpen(lockPath, fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_WRONLY);
     fsClose(fd);
-    return true;
-  } catch { return false; }
+    return lockPath;
+  } catch { return null; }
 }
 ...
-  if (!tryAcquireOpenclawSkillifyLock(projectKey)) {
+  const lockPath = tryAcquireOpenclawSkillifyLock(projectKey);
+  if (!lockPath) {
     return;
   }
+  const releaseLock = () => {
+    try { fsRm(lockPath, { force: true }); } catch {}
+  };
   const tmpDir = joinPath(tmpdir(), `deeplake-skillify-openclaw-${projectKey}-${Date.now()}`);
   try { fsMkdir(tmpDir, { recursive: true, mode: 0o700 }); }
-  catch (e: any) { a.loggerWarn?.(`skillify spawn: mkdir failed: ${e?.message ?? e}`); return; }
+  catch (e: any) { releaseLock(); a.loggerWarn?.(`skillify spawn: mkdir failed: ${e?.message ?? e}`); return; }
 ...
   try { fsWriteFile(configPath, JSON.stringify(config), { mode: 0o600 }); }
-  catch (e: any) { a.loggerWarn?.(`skillify spawn: config write failed: ${e?.message ?? e}`); return; }
+  catch (e: any) { releaseLock(); a.loggerWarn?.(`skillify spawn: config write failed: ${e?.message ?? e}`); return; }
 ...
   } catch (e: any) {
+    releaseLock();
     a.loggerWarn?.(`skillify spawn: spawn failed: ${e?.message ?? e}`);
   }

Also applies to: 399-460

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openclaw/src/index.ts` around lines 346 - 355, The lock file created by
tryAcquireOpenclawSkillifyLock can be left behind when subsequent steps (mkdir,
config write, or spawn) fail; update the flow so any path that returns after
acquiring the lock ensures the lock is released and closed (remove the
`${projectKey}.worker.lock` file and close/unlink any fd) — modify
tryAcquireOpenclawSkillifyLock to return both a success flag and a handle or
path (or throw on partial failure) and update the caller code (the OpenClaw
worker spawn/config write paths that run after
migrateOpenclawSkillifyLegacyStateDir and fsMkdir) to call a new release
function that closes and unlinks the lock on any early return or on spawn
errors; use the existing symbols migrateOpenclawSkillifyLegacyStateDir,
OPENCLAW_SKILLIFY_STATE_DIR, fsOpen/fsClose and add a release function like
releaseOpenclawSkillifyLock(lockPath|fd) and invoke it in all error/early-return
branches so no stale lock remains.
codex/bundle/skillify-worker.js (1)

452-461: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Run the legacy-state migration before the first read.

Line 777 calls readState() before any write/lock path runs. On the first post-rename worker run, an existing ~/.deeplake/state/skilify/<project>.json is still invisible here, so lastDate and skillsGenerated look empty and the worker can re-mine old sessions or recreate skills. Move migrateLegacyStateDir() into readState() (or call it before the initial read in main()).

💡 Minimal fix
 function readState(projectKey) {
+  migrateLegacyStateDir();
   const p = statePath(projectKey);
   if (!existsSync4(p))
     return null;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@codex/bundle/skillify-worker.js` around lines 452 - 461, readState currently
reads state files before legacy-state migration runs causing missing legacy
state on first run; call migrateLegacyStateDir() before attempting to read to
ensure old ~/.deeplake/state/skilify/<project>.json is moved/visible. Update the
readState(projectKey) function to first invoke migrateLegacyStateDir() (or
ensure main() calls migrateLegacyStateDir() before the first readState call),
then proceed to compute p via statePath(projectKey) and use
existsSync4/readFileSync2/JSON.parse as before; this guarantees
lastDate/skillsGenerated are populated from migrated files before any read
occurs.
claude-code/tests/skillify-cli.test.ts (1)

38-69: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

This suite now mutates the real ~/.deeplake/state/skillify config.

Line 38 resolves the live production state path up front, and beforeEach() deletes that file. After the rename, an interrupted local test run can clobber a developer’s actual Skillify config. Please run the whole file under a temp HOME and derive the config path from that temp home instead of homedir() at import time.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@claude-code/tests/skillify-cli.test.ts` around lines 38 - 69, The test
currently computes STATE_DIR and CONFIG_PATH at module import using homedir(),
which mutates the real ~/.deeplake/state/skillify; change the setup to compute
those paths at runtime inside beforeEach (or derive them from process.env.HOME)
so tests run under a temporary HOME. Specifically, remove or stop using the
top-level constants STATE_DIR and CONFIG_PATH computed with homedir(), and
instead compute STATE_DIR and CONFIG_PATH inside beforeEach (or read
process.env.HOME) before any rmSync/readFileSync/writeFileSync calls; update
afterEach to use the same runtime-computed variables so temp HOME is
consistently used. Ensure references in beforeEach/afterEach and any helper
mocks (e.g., loadConfigMock) use the new runtime-computed STATE_DIR/CONFIG_PATH
names.
hermes/bundle/capture.js (1)

1148-1189: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Detached spawn failures will leak the worker lock until timeout.

spawn() does not throw synchronously on launch failures (e.g., nohup, node, or the worker file missing); instead, the error emits asynchronously on the child's error event. This code calls .unref() immediately without attaching an error handler, so the caller's try/catch block never observes the failure and never releases the lock. The lock remains stale for up to 10 minutes, suppressing future skillify runs.

Attach an error handler before calling .unref(), or return the ChildProcess and handle the error in the caller alongside the lock release.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hermes/bundle/capture.js` around lines 1148 - 1189, In spawnSkillifyWorker:
the detached child created by spawn3("nohup", ["node", workerPath, configFile])
is unref()d immediately without any 'error' handler so asynchronous spawn
failures can leak the worker lock; fix by capturing the returned ChildProcess
(from spawn3), attach a child.on('error', ...) handler that logs the error and
triggers lock release/cleanup for the given projectKey/currentSessionId, and
only then call .unref() (or alternatively return the ChildProcess to the caller
so the caller can attach the error handler and release the lock). Ensure you
reference workerPath and configFile when logging, and use the same
skillifyLog/error path as existing code to record failures.
src/commands/skillify.ts (1)

50-69: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Status still misreports migrated state files.

After this rename, ~/.deeplake/state/skillify can contain autopull-last-run.json, but this filter only excludes config.json and pulled.json, so state: N project(s) gets inflated. The same block also still prints lastDate, while the state writer stores updatedAt, so active projects show last=never.

Suggested fix
   const files = readdirSync(dir).filter(
-    f => f.endsWith(".json") && f !== "config.json" && f !== "pulled.json",
+    f =>
+      f.endsWith(".json") &&
+      f !== "config.json" &&
+      f !== "pulled.json" &&
+      f !== "autopull-last-run.json",
   );
@@
       const s = JSON.parse(readFileSync(join(dir, f), "utf-8")) as {
-        project: string; counter: number; lastDate: string | null; skillsGenerated: string[];
+        project: string;
+        counter: number;
+        lastDate?: string | null;
+        updatedAt?: number | string | null;
+        skillsGenerated?: string[];
       };
-      const skills = s.skillsGenerated.length === 0 ? "none" : s.skillsGenerated.join(", ");
-      console.log(`  - ${s.project} (counter=${s.counter}, last=${s.lastDate ?? "never"}, skills=${skills})`);
+      const last =
+        typeof s.updatedAt === "number"
+          ? new Date(s.updatedAt).toISOString()
+          : typeof s.updatedAt === "string"
+            ? s.updatedAt
+            : s.lastDate ?? "never";
+      const skills = Array.isArray(s.skillsGenerated) && s.skillsGenerated.length > 0
+        ? s.skillsGenerated.join(", ")
+        : "none";
+      console.log(`  - ${s.project} (counter=${s.counter}, last=${last}, skills=${skills})`);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands/skillify.ts` around lines 50 - 69, The file-filtering and
state-parsing need to be updated: extend the readdirSync filter (the files
variable) to also exclude "autopull-last-run.json" so it doesn't inflate the
tracked-project count, and update the parsed state shape used in the for loop
(the s object) to read updatedAt instead of lastDate and print that (falling
back to "never"); also defensively handle missing/empty skillsGenerated when
computing skills. Update references around files, the readdirSync filter, the s
parsing/type, skillsGenerated, and the console.log that prints last/skills.
codex/bundle/stop.js (2)

965-985: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Initialize state before the first SessionEnd worker run.

This path only calls resetCounter() when readState(projectKey) already exists, but it still spawns the worker when the state file is missing. In the worker bundles in this PR, recordSkill() and advanceWatermark() both return early if readState(projectKey) is null, so the first codex run can mine skills without ever persisting lastDate/lastUuid. That makes the same sessions eligible again on the next stop.

Suggested fix
-    if (readState(projectKey)) {
-      resetCounter(projectKey);
-    }
+    const state = readState(projectKey);
+    if (state) {
+      resetCounter(projectKey);
+    } else {
+      writeState(projectKey, {
+        project,
+        projectKey,
+        counter: 0,
+        lastUuid: null,
+        lastDate: null,
+        skillsGenerated: [],
+        updatedAt: Date.now()
+      });
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@codex/bundle/stop.js` around lines 965 - 985, The code spawns a SessionEnd
worker even when no state exists, causing the worker's
recordSkill/advanceWatermark to skip persisting lastDate/lastUuid; ensure state
is initialized before spawning by checking readState(projectKey) and, if null,
creating/initializing the persisted state (or calling resetCounter(projectKey)
unconditionally) so that spawnSkillifyWorker(...) always runs with a valid
persisted state; update the block around
deriveProjectKey/tryAcquireWorkerLock/readState to initialize the project's
state prior to calling spawnSkillifyWorker to guarantee subsequent
recordSkill/advanceWatermark will persist progress.

731-772: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle async spawn() errors inside spawnSkillifyWorker.

The surrounding try/catch only sees synchronous failures. If nohup, node, or the worker path cannot be executed, spawn2() reports that via the child 'error' event after this helper returns. In that case the code logs a successful spawn and leaves the worker lock held until stale-lock expiry.

Suggested fix
-  skillifyLog(`${reason}: spawning skillify worker for project=${project} key=${projectKey}`);
-  const workerPath = join7(bundleDir, "skillify-worker.js");
-  spawn2("nohup", ["node", workerPath, configFile], {
-    detached: true,
-    stdio: ["ignore", "ignore", "ignore"]
-  }).unref();
-  skillifyLog(`${reason}: spawned skillify worker for ${projectKey}`);
+  skillifyLog(`${reason}: spawning skillify worker for project=${project} key=${projectKey}`);
+  const workerPath = join7(bundleDir, "skillify-worker.js");
+  const child = spawn2("nohup", ["node", workerPath, configFile], {
+    detached: true,
+    stdio: ["ignore", "ignore", "ignore"]
+  });
+  child.once("error", (err) => {
+    skillifyLog(`${reason} spawn failed: ${err.message}`);
+    releaseWorkerLock(projectKey);
+  });
+  child.once("spawn", () => {
+    skillifyLog(`${reason}: spawned skillify worker for ${projectKey}`);
+    child.unref();
+  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@codex/bundle/stop.js` around lines 731 - 772, spawnSkillifyWorker currently
ignores asynchronous child-process startup failures; capture the child returned
from spawn2 (const child = spawn2(...)), call child.unref(), and attach
child.on('error', err => { skillifyLog(`${reason}: failed to spawn skillify
worker for ${projectKey}: ${err.message || err}`); /* perform cleanup: release
the worker/session lock for currentSessionId/projectKey here */ }); to ensure
errors are logged and the worker lock is released — either invoke an existing
unlock function (e.g., releaseSessionLock or clearWorkerLock with
currentSessionId/projectKey) or accept and call an opts.onSpawnError callback if
no unlock helper exists.
🧹 Nitpick comments (1)
vitest.config.ts (1)

254-272: ⚡ Quick win

Add src/skillify/legacy-migration.ts to the per-file coverage gate.

This PR adds a new load-bearing file, but it is not listed in coverage.thresholds. Since this config is the repo’s mechanism for enforcing touched-file coverage, the migration path can now regress without tripping CI.

♻️ Suggested change
         "src/skillify/auto-pull.ts":         { statements: 90, branches: 70, functions: 90, lines: 90 },
         "src/skillify/extractors/index.ts":  { statements: 90, branches: 90, functions: 90, lines: 90 },
         "src/skillify/gate-parser.ts":       { statements: 90, branches: 90, functions: 90, lines: 90 },
         "src/skillify/gate-runner.ts":       { statements: 90, branches: 60, functions: 90, lines: 90 },
+        "src/skillify/legacy-migration.ts":  { statements: 80, branches: 80, functions: 80, lines: 80 },
         "src/skillify/pull.ts":              { statements: 90, branches: 75, functions: 90, lines: 90 },
         "src/skillify/scope-config.ts":      { statements: 90, branches: 90, functions: 90, lines: 90 },
         "src/skillify/skill-writer.ts":      { statements: 90, branches: 80, functions: 90, lines: 90 },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@vitest.config.ts` around lines 254 - 272, Add the new file
"src/skillify/legacy-migration.ts" to the coverage.thresholds map in
vitest.config.ts so CI enforces per-file coverage for this load-bearing
migration; add an entry similar to the other skillify files, e.g.
"src/skillify/legacy-migration.ts": { statements: 90, branches: 70, functions:
90, lines: 90 }, ensuring the thresholds object includes that key.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@bundle/cli.js`:
- Around line 4737-4753: The catch in migrateLegacyStateDir currently swallows
all renameSync errors; change it to only suppress the documented fallback cases
(cross-device and permission errors) by checking err.code and handling only
'EXDEV' (cross-device) and the permission codes ('EACCES' and 'EPERM'), logging
the fallback for those; for any other err.code rethrow the error so callers see
unexpected failures (keep references to attempted, legacy, current, renameSync,
and dlog in your edit).

In `@claude-code/bundle/session-start.js`:
- Around line 777-793: The migration currently swallows all errors from
renameSync in migrateLegacyStateDir (symbols: migrateLegacyStateDir, renameSync,
dlog, attempted, legacy, current, existsSync5), which hides real I/O failures;
change the catch to inspect err.code and only treat the documented fallback
codes (e.g., "EXDEV" and "EPERM") as benign (log and leave legacy dir in place),
but for any other error codes (e.g., "EIO", "ENOSPC", etc.) rethrow or log as an
actual error (so the failure is visible) instead of silently continuing.

In `@claude-code/tests/skillify-legacy-migration.test.ts`:
- Around line 28-41: The tests currently only set process.env.HOME (sandboxHome)
in beforeEach but do not stub node:os.homedir(), so on Windows the code under
test can still resolve the real user homedir; update beforeEach to also mock/spy
on os.homedir() to return sandboxHome (store the original os.homedir reference),
and update afterEach to restore the original os.homedir; keep the existing
prevHome logic and cleanup (chmodSync/rmSync) but ensure the os.homedir mock is
removed in afterEach so subsequent tests are unaffected.

In `@RELEASE_CHECKLIST.md`:
- Around line 84-95: Update the contradictory statement that OpenClaw “has none”
for mining-style features so it matches the rest of the doc: change the line
that claims mining doesn’t apply to OpenClaw to state that OpenClaw does capture
sessions and can be mined (referencing the agent_end hook and the
spawnOpenclawSkillifyWorker wiring in openclaw/src/index.ts and the produced
bundle openclaw/dist/skillify-worker.js); ensure the text mentions the agent_end
capture (see openclaw/src/index.ts:agent_end/line ~903) and that the mining
worker is spawned from spawnOpenclawSkillifyWorker and uses
openclaw/dist/skillify-worker.js so the checklist is consistent.

In `@src/cli/index.ts`:
- Around line 231-233: The skillify command branch currently calls
runSkillifyCommand(args.slice(1)) without awaiting it, which can cause unhandled
rejections; change the branch to await the dispatcher (await
runSkillifyCommand(args.slice(1))) so any rejection flows into the surrounding
main().catch(...) path and ensure the enclosing function is async (or already
is) so awaiting is valid; update the branch that checks if (cmd === "skillify")
and replace the non-awaited call with an awaited call to runSkillifyCommand.

---

Outside diff comments:
In `@claude-code/tests/skillify-cli.test.ts`:
- Around line 38-69: The test currently computes STATE_DIR and CONFIG_PATH at
module import using homedir(), which mutates the real
~/.deeplake/state/skillify; change the setup to compute those paths at runtime
inside beforeEach (or derive them from process.env.HOME) so tests run under a
temporary HOME. Specifically, remove or stop using the top-level constants
STATE_DIR and CONFIG_PATH computed with homedir(), and instead compute STATE_DIR
and CONFIG_PATH inside beforeEach (or read process.env.HOME) before any
rmSync/readFileSync/writeFileSync calls; update afterEach to use the same
runtime-computed variables so temp HOME is consistently used. Ensure references
in beforeEach/afterEach and any helper mocks (e.g., loadConfigMock) use the new
runtime-computed STATE_DIR/CONFIG_PATH names.

In `@codex/bundle/skillify-worker.js`:
- Around line 452-461: readState currently reads state files before legacy-state
migration runs causing missing legacy state on first run; call
migrateLegacyStateDir() before attempting to read to ensure old
~/.deeplake/state/skilify/<project>.json is moved/visible. Update the
readState(projectKey) function to first invoke migrateLegacyStateDir() (or
ensure main() calls migrateLegacyStateDir() before the first readState call),
then proceed to compute p via statePath(projectKey) and use
existsSync4/readFileSync2/JSON.parse as before; this guarantees
lastDate/skillsGenerated are populated from migrated files before any read
occurs.

In `@codex/bundle/stop.js`:
- Around line 965-985: The code spawns a SessionEnd worker even when no state
exists, causing the worker's recordSkill/advanceWatermark to skip persisting
lastDate/lastUuid; ensure state is initialized before spawning by checking
readState(projectKey) and, if null, creating/initializing the persisted state
(or calling resetCounter(projectKey) unconditionally) so that
spawnSkillifyWorker(...) always runs with a valid persisted state; update the
block around deriveProjectKey/tryAcquireWorkerLock/readState to initialize the
project's state prior to calling spawnSkillifyWorker to guarantee subsequent
recordSkill/advanceWatermark will persist progress.
- Around line 731-772: spawnSkillifyWorker currently ignores asynchronous
child-process startup failures; capture the child returned from spawn2 (const
child = spawn2(...)), call child.unref(), and attach child.on('error', err => {
skillifyLog(`${reason}: failed to spawn skillify worker for ${projectKey}:
${err.message || err}`); /* perform cleanup: release the worker/session lock for
currentSessionId/projectKey here */ }); to ensure errors are logged and the
worker lock is released — either invoke an existing unlock function (e.g.,
releaseSessionLock or clearWorkerLock with currentSessionId/projectKey) or
accept and call an opts.onSpawnError callback if no unlock helper exists.

In `@hermes/bundle/capture.js`:
- Around line 1148-1189: In spawnSkillifyWorker: the detached child created by
spawn3("nohup", ["node", workerPath, configFile]) is unref()d immediately
without any 'error' handler so asynchronous spawn failures can leak the worker
lock; fix by capturing the returned ChildProcess (from spawn3), attach a
child.on('error', ...) handler that logs the error and triggers lock
release/cleanup for the given projectKey/currentSessionId, and only then call
.unref() (or alternatively return the ChildProcess to the caller so the caller
can attach the error handler and release the lock). Ensure you reference
workerPath and configFile when logging, and use the same skillifyLog/error path
as existing code to record failures.

In `@openclaw/src/index.ts`:
- Around line 346-355: The lock file created by tryAcquireOpenclawSkillifyLock
can be left behind when subsequent steps (mkdir, config write, or spawn) fail;
update the flow so any path that returns after acquiring the lock ensures the
lock is released and closed (remove the `${projectKey}.worker.lock` file and
close/unlink any fd) — modify tryAcquireOpenclawSkillifyLock to return both a
success flag and a handle or path (or throw on partial failure) and update the
caller code (the OpenClaw worker spawn/config write paths that run after
migrateOpenclawSkillifyLegacyStateDir and fsMkdir) to call a new release
function that closes and unlinks the lock on any early return or on spawn
errors; use the existing symbols migrateOpenclawSkillifyLegacyStateDir,
OPENCLAW_SKILLIFY_STATE_DIR, fsOpen/fsClose and add a release function like
releaseOpenclawSkillifyLock(lockPath|fd) and invoke it in all error/early-return
branches so no stale lock remains.

In `@src/commands/skillify.ts`:
- Around line 50-69: The file-filtering and state-parsing need to be updated:
extend the readdirSync filter (the files variable) to also exclude
"autopull-last-run.json" so it doesn't inflate the tracked-project count, and
update the parsed state shape used in the for loop (the s object) to read
updatedAt instead of lastDate and print that (falling back to "never"); also
defensively handle missing/empty skillsGenerated when computing skills. Update
references around files, the readdirSync filter, the s parsing/type,
skillsGenerated, and the console.log that prints last/skills.

---

Nitpick comments:
In `@vitest.config.ts`:
- Around line 254-272: Add the new file "src/skillify/legacy-migration.ts" to
the coverage.thresholds map in vitest.config.ts so CI enforces per-file coverage
for this load-bearing migration; add an entry similar to the other skillify
files, e.g. "src/skillify/legacy-migration.ts": { statements: 90, branches: 70,
functions: 90, lines: 90 }, ensuring the thresholds object includes that key.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b4da8c25-9cac-4de5-9758-997cb1b0f25b

📥 Commits

Reviewing files that changed from the base of the PR and between ed7634b and 148337d.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (80)
  • .github/workflows/ci.yml
  • .github/workflows/release.yml
  • RELEASE_CHECKLIST.md
  • bundle/cli.js
  • claude-code/bundle/capture.js
  • claude-code/bundle/session-end.js
  • claude-code/bundle/session-start.js
  • claude-code/bundle/skillify-worker.js
  • claude-code/tests/codex-session-start-hook.test.ts
  • claude-code/tests/cursor-session-start-hook.test.ts
  • claude-code/tests/hermes-session-start-hook.test.ts
  • claude-code/tests/session-start-hook.test.ts
  • claude-code/tests/sessions-table.test.ts
  • claude-code/tests/skillify-agent-roots.test.ts
  • claude-code/tests/skillify-auto-pull.test.ts
  • claude-code/tests/skillify-bundle-scan.test.ts
  • claude-code/tests/skillify-cli.test.ts
  • claude-code/tests/skillify-extractor.test.ts
  • claude-code/tests/skillify-gate-parser.test.ts
  • claude-code/tests/skillify-gate-runner.test.ts
  • claude-code/tests/skillify-legacy-migration.test.ts
  • claude-code/tests/skillify-manifest.test.ts
  • claude-code/tests/skillify-pull.test.ts
  • claude-code/tests/skillify-scope-config.test.ts
  • claude-code/tests/skillify-session-start-injection.test.ts
  • claude-code/tests/skillify-skill-writer.test.ts
  • claude-code/tests/skillify-skills-table.test.ts
  • claude-code/tests/skillify-state.test.ts
  • claude-code/tests/skillify-triggers.test.ts
  • claude-code/tests/skillify-unpull.test.ts
  • codex/bundle/session-start.js
  • codex/bundle/skillify-worker.js
  • codex/bundle/stop.js
  • cursor/bundle/capture.js
  • cursor/bundle/session-end.js
  • cursor/bundle/session-start.js
  • cursor/bundle/skillify-worker.js
  • esbuild.config.mjs
  • hermes/bundle/capture.js
  • hermes/bundle/session-end.js
  • hermes/bundle/session-start.js
  • hermes/bundle/skillify-worker.js
  • openclaw/skills/SKILL.md
  • openclaw/src/index.ts
  • pi/bundle/autopull-worker.js
  • pi/bundle/skilify-worker.js
  • pi/bundle/skillify-worker.js
  • pi/extension-source/hivemind.ts
  • src/cli/index.ts
  • src/cli/install-pi.ts
  • src/commands/skillify.ts
  • src/hooks/capture.ts
  • src/hooks/codex/session-start.ts
  • src/hooks/codex/stop.ts
  • src/hooks/cursor/capture.ts
  • src/hooks/cursor/session-end.ts
  • src/hooks/cursor/session-start.ts
  • src/hooks/hermes/capture.ts
  • src/hooks/hermes/session-end.ts
  • src/hooks/hermes/session-start.ts
  • src/hooks/session-end.ts
  • src/hooks/session-start.ts
  • src/skillify/agent-roots.ts
  • src/skillify/auto-pull.ts
  • src/skillify/autopull-worker.ts
  • src/skillify/extractors/index.ts
  • src/skillify/gate-parser.ts
  • src/skillify/gate-runner.ts
  • src/skillify/legacy-migration.ts
  • src/skillify/manifest.ts
  • src/skillify/pull.ts
  • src/skillify/scope-config.ts
  • src/skillify/skill-writer.ts
  • src/skillify/skillify-worker.ts
  • src/skillify/skills-table.ts
  • src/skillify/spawn-skillify-worker.ts
  • src/skillify/state.ts
  • src/skillify/triggers.ts
  • src/skillify/unpull.ts
  • vitest.config.ts
💤 Files with no reviewable changes (1)
  • pi/bundle/skilify-worker.js

Comment thread bundle/cli.js
Comment on lines +4737 to +4753
function migrateLegacyStateDir() {
if (attempted)
return;
attempted = true;
const root = join15(homedir5(), ".deeplake", "state");
const legacy = join15(root, "skilify");
const current = join15(root, "skillify");
if (!existsSync12(legacy))
return;
if (existsSync12(current))
return;
try {
renameSync(legacy, current);
dlog(`migrated ${legacy} -> ${current}`);
} catch (err) {
dlog(`migration failed (${err.code ?? "unknown"}); leaving legacy dir in place`);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Restrict the migration fallback to the documented fs errors.

This currently suppresses every renameSync failure and silently proceeds with a fresh skillify state dir. Unexpected failures here will look like state loss and be very hard to diagnose. Please only downgrade the cross-device / permission cases called out in the migration plan, and rethrow everything else from the source helper before regenerating this bundle.

Suggested fix
 function migrateLegacyStateDir() {
   if (attempted)
     return;
   attempted = true;
   const root = join15(homedir5(), ".deeplake", "state");
   const legacy = join15(root, "skilify");
   const current = join15(root, "skillify");
   if (!existsSync12(legacy))
     return;
   if (existsSync12(current))
     return;
   try {
     renameSync(legacy, current);
     dlog(`migrated ${legacy} -> ${current}`);
   } catch (err) {
-    dlog(`migration failed (${err.code ?? "unknown"}); leaving legacy dir in place`);
+    const code = err?.code;
+    if (code === "EXDEV" || code === "EPERM" || code === "EACCES") {
+      dlog(`migration failed (${code}); leaving legacy dir in place`);
+      return;
+    }
+    throw err;
   }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bundle/cli.js` around lines 4737 - 4753, The catch in migrateLegacyStateDir
currently swallows all renameSync errors; change it to only suppress the
documented fallback cases (cross-device and permission errors) by checking
err.code and handling only 'EXDEV' (cross-device) and the permission codes
('EACCES' and 'EPERM'), logging the fallback for those; for any other err.code
rethrow the error so callers see unexpected failures (keep references to
attempted, legacy, current, renameSync, and dlog in your edit).

Comment on lines +777 to +793
function migrateLegacyStateDir() {
if (attempted)
return;
attempted = true;
const root = join9(homedir5(), ".deeplake", "state");
const legacy = join9(root, "skilify");
const current = join9(root, "skillify");
if (!existsSync5(legacy))
return;
if (existsSync5(current))
return;
try {
renameSync(legacy, current);
dlog(`migrated ${legacy} -> ${current}`);
} catch (err) {
dlog(`migration failed (${err.code ?? "unknown"}); leaving legacy dir in place`);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Only swallow the documented fallback errors.

This helper currently treats every renameSync() failure as benign. That matches EXDEV/EPERM, but it also hides unexpected I/O failures like EIO or ENOSPC, which leaves users on a fresh skillify state dir while their legacy manifest/state is silently ignored.

Suggested fix
   try {
     renameSync(legacy, current);
     dlog(`migrated ${legacy} -> ${current}`);
   } catch (err) {
-    dlog(`migration failed (${err.code ?? "unknown"}); leaving legacy dir in place`);
+    const code = err && typeof err === "object" && "code" in err ? err.code : "unknown";
+    if (code === "EXDEV" || code === "EPERM") {
+      dlog(`migration failed (${code}); leaving legacy dir in place`);
+      return;
+    }
+    throw err;
   }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@claude-code/bundle/session-start.js` around lines 777 - 793, The migration
currently swallows all errors from renameSync in migrateLegacyStateDir (symbols:
migrateLegacyStateDir, renameSync, dlog, attempted, legacy, current,
existsSync5), which hides real I/O failures; change the catch to inspect
err.code and only treat the documented fallback codes (e.g., "EXDEV" and
"EPERM") as benign (log and leave legacy dir in place), but for any other error
codes (e.g., "EIO", "ENOSPC", etc.) rethrow or log as an actual error (so the
failure is visible) instead of silently continuing.

Comment on lines +28 to +41
beforeEach(() => {
sandboxHome = mkdtempSync(join(tmpdir(), "skillify-migration-"));
prevHome = process.env.HOME;
process.env.HOME = sandboxHome;
});

afterEach(() => {
if (prevHome === undefined) delete process.env.HOME;
else process.env.HOME = prevHome;
// chmodSync the sandbox readable in case a test removed perms; otherwise
// rmSync hits EACCES on cleanup and leaks the temp dir across runs.
try { chmodSync(sandboxHome, 0o755); } catch { /* nothing */ }
rmSync(sandboxHome, { recursive: true, force: true });
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

# First, let's find and examine the test file
find . -name "skillify-legacy-migration.test.ts" -type f

Repository: activeloopai/hivemind

Length of output: 119


🏁 Script executed:

# Also find the source file being tested
find . -name "legacy-migration.ts" -path "*/skillify/*" -type f

Repository: activeloopai/hivemind

Length of output: 100


🏁 Script executed:

# Search for homedir usage in the codebase
rg "homedir" --type ts --type js

Repository: activeloopai/hivemind

Length of output: 50377


🏁 Script executed:

# Read the test file to see the current setup
cat -n ./claude-code/tests/skillify-legacy-migration.test.ts | head -50

Repository: activeloopai/hivemind

Length of output: 2296


🏁 Script executed:

# Also check the source file being tested
cat -n ./src/skillify/legacy-migration.ts

Repository: activeloopai/hivemind

Length of output: 1621


🏁 Script executed:

# Let me verify Node.js os.homedir() behavior on Windows
web_search

Repository: activeloopai/hivemind

Length of output: 116


🌐 Web query:

Node.js os.homedir() implementation Windows USERPROFILE environment variables

💡 Result:

On Windows, Node.js os.homedir() returns the current user’s home directory by delegating to libuv’s uv_os_homedir(), which (1) checks the USERPROFILE environment variable first and (2) if it’s not set, falls back to querying the Windows profile directory for the current user (via the Windows API used by libuv). [1][2][3] More detail: 1) Node.js implementation - Node’s os.homedir() implementation is a thin wrapper: it calls uv_os_homedir(buf, &len) to fill a buffer and returns the resulting path string. [2] 2) libuv Windows implementation and USERPROFILE - In libuv’s Windows code for uv_os_homedir, it first attempts to read the USERPROFILE environment variable (uv_os_getenv("USERPROFILE", ...)). [4] - If USERPROFILE is not found, it falls back to obtaining the profile/home directory for the current user via uv_os_get_passwd(&pwd), and then copies pwd.homedir into the output buffer. [4] 3) Node.js public documentation summary - Node’s os.homedir() docs state: on Windows, it uses the USERPROFILE environment variable if defined; otherwise it uses the path to the current user’s profile directory. [3] Practical implication - If process.env.USERPROFILE is set in the environment of the Node process, os.homedir() will use it; changing HOME typically won’t affect os.homedir() on Windows because the documented/preferred variable is USERPROFILE on that platform. [3][4]

Citations:


🏁 Script executed:

# Check how other tests handle homedir() mocking
rg -A 5 "vi.mock.*homedir" claude-code/tests/ --type ts

Repository: activeloopai/hivemind

Length of output: 47


🏁 Script executed:

# Also check if there are environment variable mocking patterns for USERPROFILE
rg "USERPROFILE|HOMEDRIVE|HOMEPATH" --type ts

Repository: activeloopai/hivemind

Length of output: 47


🏁 Script executed:

# Check how cli-auth.test.ts handles homedir mocking (mentioned in the homedir search results)
cat -n ./claude-code/tests/cli-auth.test.ts | head -50

Repository: activeloopai/hivemind

Length of output: 2359


🏁 Script executed:

# Check auth-creds.test.ts which was also mentioned in the search
rg -B 5 -A 10 "homedir" ./claude-code/tests/auth-creds.test.ts | head -40

Repository: activeloopai/hivemind

Length of output: 1341


🏁 Script executed:

# Let's verify what platform this project is expected to run on
cat ./package.json | grep -A 5 -B 5 "engines\|os"

Repository: activeloopai/hivemind

Length of output: 536


🏁 Script executed:

# Also check if there are CI configs that might indicate Windows testing
find . -name "*.yml" -o -name "*.yaml" | grep -E "(workflow|ci|github)" | head -5

Repository: activeloopai/hivemind

Length of output: 236


🏁 Script executed:

# Check the CI workflow to see what OS platforms are tested
cat ./.github/workflows/ci.yml | head -40

Repository: activeloopai/hivemind

Length of output: 1279


🏁 Script executed:

# Check if there's any mention of Windows or cross-platform testing
grep -r "windows\|WIN32\|platform" ./.github/workflows/ --include="*.yml" --include="*.yaml"

Repository: activeloopai/hivemind

Length of output: 47


🏁 Script executed:

# Check more of the CI workflow
cat ./.github/workflows/ci.yml | grep -A 20 "runs-on"

Repository: activeloopai/hivemind

Length of output: 1381


Sandbox os.homedir() on Windows as well.

These tests only override HOME, but the code under test calls node:os.homedir(). On Windows that resolves from USERPROFILE / HOMEDRIVE / HOMEPATH, so this suite can hit the real ~/.deeplake/state and mutate real user data when run on Windows systems.

🛡️ Suggested fix
 let sandboxHome: string;
 let prevHome: string | undefined;
+let prevUserProfile: string | undefined;
+let prevHomeDrive: string | undefined;
+let prevHomePath: string | undefined;

 beforeEach(() => {
   sandboxHome = mkdtempSync(join(tmpdir(), "skillify-migration-"));
   prevHome = process.env.HOME;
+  prevUserProfile = process.env.USERPROFILE;
+  prevHomeDrive = process.env.HOMEDRIVE;
+  prevHomePath = process.env.HOMEPATH;
   process.env.HOME = sandboxHome;
+  process.env.USERPROFILE = sandboxHome;
+  delete process.env.HOMEDRIVE;
+  delete process.env.HOMEPATH;
 });

 afterEach(() => {
   if (prevHome === undefined) delete process.env.HOME;
   else process.env.HOME = prevHome;
+  if (prevUserProfile === undefined) delete process.env.USERPROFILE;
+  else process.env.USERPROFILE = prevUserProfile;
+  if (prevHomeDrive === undefined) delete process.env.HOMEDRIVE;
+  else process.env.HOMEDRIVE = prevHomeDrive;
+  if (prevHomePath === undefined) delete process.env.HOMEPATH;
+  else process.env.HOMEPATH = prevHomePath;
   // chmodSync the sandbox readable in case a test removed perms; otherwise
   // rmSync hits EACCES on cleanup and leaks the temp dir across runs.
   try { chmodSync(sandboxHome, 0o755); } catch { /* nothing */ }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@claude-code/tests/skillify-legacy-migration.test.ts` around lines 28 - 41,
The tests currently only set process.env.HOME (sandboxHome) in beforeEach but do
not stub node:os.homedir(), so on Windows the code under test can still resolve
the real user homedir; update beforeEach to also mock/spy on os.homedir() to
return sandboxHome (store the original os.homedir reference), and update
afterEach to restore the original os.homedir; keep the existing prevHome logic
and cleanup (chmodSync/rmSync) but ensure the os.homedir mock is removed in
afterEach so subsequent tests are unaffected.

Comment thread RELEASE_CHECKLIST.md
Comment on lines +84 to 95
| **Pi** | `pi/extension-source/hivemind.ts` (raw .ts, no bundle) | full (session_start, input, tool_result, message_end, session_shutdown) | ✅ via `pi/bundle/skillify-worker.js` spawned from session_shutdown | ✅ inline in `CONTEXT_PREAMBLE` | self-contained extension; pi compiles the .ts at load time |
| **OpenClaw**| `openclaw/src/index.ts`, `openclaw/skills/SKILL.md` | `before_prompt_build`, `before_agent_start`, `agent_end` | ✅ via `openclaw/dist/skillify-worker.js` spawned from `agent_end` | ✅ in `openclaw/skills/SKILL.md` | gateway plugin: captures sessions to the same `sessions` table; bypasses esbuild's `child_process` stub via `createRequire(import.meta.url)` |

**Mining (worker firing on session end)** applies wherever the agent
captures sessions — which is all six. Earlier drafts of this section
incorrectly described OpenClaw mining as "N/A by design"; OpenClaw
does in fact capture sessions to the same `sessions` Deeplake table
(see `openclaw/src/index.ts:903` agent_end hook), and the worker can
mine them just like any other agent. The wiring lives at
`openclaw/src/index.ts:spawnOpenclawSkilifyWorker` and fires from the
agent_end hook after each capture. Bundle: `openclaw/dist/skilify-worker.js`
`openclaw/src/index.ts:spawnOpenclawSkillifyWorker` and fires from the
agent_end hook after each capture. Bundle: `openclaw/dist/skillify-worker.js`
(separate esbuild entry, isolated from the gateway's child_process
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Resolve OpenClaw mining guidance contradiction.

This updated section says OpenClaw mining is wired and active, but Line 113 still states mining-style features don’t apply because OpenClaw “has none.” Please align those statements to avoid misleading future checklist runs.

Suggested doc fix (outside this changed hunk)
-- [ ] **OpenClaw**: if the feature has any agent-facing surface (commands, tools, discoverability text), update `openclaw/skills/SKILL.md` so the host agent learns about it. Mining-style features that need session lifecycle don't apply (openclaw has none) — document the limitation in the SKILL.md instead of pretending parity
+- [ ] **OpenClaw**: if the feature has any agent-facing surface (commands, tools, discoverability text), update `openclaw/skills/SKILL.md` so the host agent learns about it. For mining/session-capture features, validate the `agent_end` capture + worker path in `openclaw/src/index.ts` and confirm bundle wiring.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@RELEASE_CHECKLIST.md` around lines 84 - 95, Update the contradictory
statement that OpenClaw “has none” for mining-style features so it matches the
rest of the doc: change the line that claims mining doesn’t apply to OpenClaw to
state that OpenClaw does capture sessions and can be mined (referencing the
agent_end hook and the spawnOpenclawSkillifyWorker wiring in
openclaw/src/index.ts and the produced bundle openclaw/dist/skillify-worker.js);
ensure the text mentions the agent_end capture (see
openclaw/src/index.ts:agent_end/line ~903) and that the mining worker is spawned
from spawnOpenclawSkillifyWorker and uses openclaw/dist/skillify-worker.js so
the checklist is consistent.

Comment thread src/cli/index.ts
Comment on lines +231 to 233
if (cmd === "skillify") {
runSkillifyCommand(args.slice(1));
return;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Await the skillify dispatcher.

This is the only command branch that doesn’t await its handler. If runSkillifyCommand() rejects, the failure bypasses main().catch(...) and can surface as an unhandled rejection instead of the normal CLI error path.

🐛 Suggested fix
   if (cmd === "skillify") {
-    runSkillifyCommand(args.slice(1));
+    await runSkillifyCommand(args.slice(1));
     return;
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (cmd === "skillify") {
runSkillifyCommand(args.slice(1));
return;
if (cmd === "skillify") {
await runSkillifyCommand(args.slice(1));
return;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/index.ts` around lines 231 - 233, The skillify command branch
currently calls runSkillifyCommand(args.slice(1)) without awaiting it, which can
cause unhandled rejections; change the branch to await the dispatcher (await
runSkillifyCommand(args.slice(1))) so any rejection flows into the surrounding
main().catch(...) path and ensure the enclosing function is async (or already
is) so awaiting is valid; update the branch that checks if (cmd === "skillify")
and replace the non-awaited call with an awaited call to runSkillifyCommand.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Rename: skilifyskillify (fix typo)

1 participant