episoda 0.2.97 → 0.2.99

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.
@@ -2786,7 +2786,7 @@ var require_package = __commonJS({
2786
2786
  "package.json"(exports2, module2) {
2787
2787
  module2.exports = {
2788
2788
  name: "episoda",
2789
- version: "0.2.97",
2789
+ version: "0.2.99",
2790
2790
  description: "CLI tool for Episoda local development workflow orchestration",
2791
2791
  main: "dist/index.js",
2792
2792
  types: "dist/index.d.ts",
@@ -4406,6 +4406,48 @@ var WorktreeManager = class _WorktreeManager {
4406
4406
  }
4407
4407
  return { valid, stale, orphaned };
4408
4408
  }
4409
+ /**
4410
+ * EP1190: Clean up non-module worktrees (like 'main')
4411
+ *
4412
+ * In K1273 per-user-per-workspace model, only EP### worktrees should exist.
4413
+ * The 'main' worktree and other non-module worktrees are legacy artifacts
4414
+ * that can contain stale code and should be removed.
4415
+ *
4416
+ * @returns Object with removed paths and errors
4417
+ */
4418
+ async cleanupNonModuleWorktrees() {
4419
+ const removed = [];
4420
+ const errors = [];
4421
+ try {
4422
+ const validation = await this.validateWorktrees();
4423
+ for (const orphanPath of validation.orphaned) {
4424
+ const dirName = path7.basename(orphanPath);
4425
+ const isModuleWorktree = /^EP\d+$/.test(dirName);
4426
+ if (!isModuleWorktree) {
4427
+ console.log(`[WorktreeManager] EP1190: Found non-module worktree: ${dirName}`);
4428
+ try {
4429
+ const removeResult = await this.gitExecutor.execute({
4430
+ action: "worktree_remove",
4431
+ path: orphanPath,
4432
+ force: true
4433
+ // Force removal since it's not tracked
4434
+ }, { cwd: this.bareRepoPath });
4435
+ if (removeResult.success) {
4436
+ removed.push(dirName);
4437
+ console.log(`[WorktreeManager] EP1190: Removed non-module worktree: ${dirName}`);
4438
+ } else {
4439
+ errors.push(`Failed to remove ${dirName}: ${removeResult.output}`);
4440
+ }
4441
+ } catch (removeError) {
4442
+ errors.push(`Error removing ${dirName}: ${removeError.message}`);
4443
+ }
4444
+ }
4445
+ }
4446
+ } catch (error) {
4447
+ errors.push(`Validation error: ${error.message}`);
4448
+ }
4449
+ return { removed, errors };
4450
+ }
4409
4451
  /**
4410
4452
  * Get project configuration
4411
4453
  */
@@ -9425,7 +9467,6 @@ async function fetchEnvVars2() {
9425
9467
  }
9426
9468
  }
9427
9469
  var Daemon = class _Daemon {
9428
- // 60 seconds
9429
9470
  constructor() {
9430
9471
  this.machineId = "";
9431
9472
  this.machineUuid = null;
@@ -9464,6 +9505,9 @@ var Daemon = class _Daemon {
9464
9505
  // Health checks are orthogonal to push-based state sync - they detect dead tunnels
9465
9506
  this.healthCheckInterval = null;
9466
9507
  this.healthCheckInProgress = false;
9508
+ // 60 seconds
9509
+ // EP1190: Worktree cleanup runs every N health checks (5 * 60s = 5 minutes)
9510
+ this.healthCheckCounter = 0;
9467
9511
  this.ipcServer = new IPCServer();
9468
9512
  }
9469
9513
  static {
@@ -9480,6 +9524,9 @@ var Daemon = class _Daemon {
9480
9524
  static {
9481
9525
  this.HEALTH_CHECK_INTERVAL_MS = 6e4;
9482
9526
  }
9527
+ static {
9528
+ this.WORKTREE_CLEANUP_EVERY_N_CHECKS = 5;
9529
+ }
9483
9530
  /**
9484
9531
  * Start the daemon
9485
9532
  */
@@ -10877,130 +10924,8 @@ var Daemon = class _Daemon {
10877
10924
  }
10878
10925
  }
10879
10926
  // EP1025: Removed cleanupModuleWorktree - was dead code (never called).
10880
- // Worktree cleanup is handled by server's cleanupWorktreeAsync which sends
10881
- // worktree_remove command via WebSocket, handled by GitExecutor.executeWorktreeRemove.
10882
- /**
10883
- * EP1047: Process pending cleanup queue entries for this machine
10884
- *
10885
- * @deprecated EP1091: No longer called on connect. Server-side cron now processes
10886
- * the cleanup queue and sends WebSocket commands to connected daemons. This is
10887
- * more reliable than daemon-side polling since it works even if daemon stays connected.
10888
- *
10889
- * Kept for potential manual invocation or debugging purposes.
10890
- *
10891
- * Flow:
10892
- * 1. Query server for pending cleanup tasks for this machine
10893
- * 2. For each task, attempt worktree removal
10894
- * 3. Report success/failure back to server
10895
- */
10896
- async reconcilePendingCleanups(projectId, projectPath) {
10897
- if (!this.machineUuid) {
10898
- console.log("[Daemon] EP1047: Cannot reconcile cleanups - machineUuid not available yet");
10899
- return;
10900
- }
10901
- console.log(`[Daemon] EP1047: Checking for pending cleanup tasks for machine ${this.machineUuid}`);
10902
- try {
10903
- const config = await (0, import_core13.loadConfig)();
10904
- if (!config) {
10905
- console.log("[Daemon] EP1047: No config loaded, skipping cleanup reconciliation");
10906
- return;
10907
- }
10908
- const apiUrl = config.api_url || "https://episoda.dev";
10909
- const controller = new AbortController();
10910
- const timeoutId = setTimeout(() => controller.abort(), 1e4);
10911
- let response;
10912
- try {
10913
- response = await fetchWithAuth(
10914
- `${apiUrl}/api/cli/background-ops?machine_id=${this.machineUuid}`,
10915
- { signal: controller.signal }
10916
- );
10917
- } finally {
10918
- clearTimeout(timeoutId);
10919
- }
10920
- if (!response.ok) {
10921
- console.warn(`[Daemon] EP1051: Failed to fetch background operations: ${response.status}`);
10922
- return;
10923
- }
10924
- const data = await response.json();
10925
- const tasks = data.tasks || [];
10926
- if (tasks.length === 0) {
10927
- console.log("[Daemon] EP1051: No pending background operations");
10928
- return;
10929
- }
10930
- console.log(`[Daemon] EP1051: Processing ${tasks.length} pending operation(s)`);
10931
- const projectRoot = await findProjectRoot(projectPath);
10932
- if (!projectRoot) {
10933
- console.warn("[Daemon] EP1051: Could not find project root, skipping operation reconciliation");
10934
- return;
10935
- }
10936
- const worktreeManager = new WorktreeManager(projectRoot);
10937
- if (!await worktreeManager.initialize()) {
10938
- console.warn("[Daemon] EP1051: Failed to initialize worktree manager");
10939
- return;
10940
- }
10941
- for (const task of tasks) {
10942
- const moduleUid = task.payload.module_uid || task.target_id;
10943
- console.log(`[Daemon] EP1051: Processing ${task.operation_type} for ${moduleUid} (task ${task.id})`);
10944
- try {
10945
- if (task.operation_type === "worktree_cleanup") {
10946
- const result = await worktreeManager.removeWorktree(moduleUid, true);
10947
- if (result.success) {
10948
- console.log(`[Daemon] EP1051: Successfully cleaned up worktree for ${moduleUid}`);
10949
- await this.reportOperationResult(apiUrl, task.id, "complete");
10950
- } else {
10951
- if (result.error?.includes("not found") || result.error?.includes("No worktree found")) {
10952
- console.log(`[Daemon] EP1051: Worktree ${moduleUid} already removed, marking complete`);
10953
- await this.reportOperationResult(apiUrl, task.id, "complete");
10954
- } else {
10955
- console.warn(`[Daemon] EP1051: Cleanup failed for ${moduleUid}: ${result.error}`);
10956
- await this.reportOperationResult(apiUrl, task.id, "retry", {
10957
- code: "OPERATION_ERROR",
10958
- message: result.error || "Unknown error"
10959
- });
10960
- }
10961
- }
10962
- } else {
10963
- console.warn(`[Daemon] EP1051: Unknown operation type: ${task.operation_type}`);
10964
- await this.reportOperationResult(apiUrl, task.id, "fail", {
10965
- code: "UNKNOWN_OPERATION_TYPE",
10966
- message: `Daemon cannot process operation type: ${task.operation_type}`
10967
- });
10968
- }
10969
- } catch (error) {
10970
- console.error(`[Daemon] EP1051: Error processing operation for ${moduleUid}:`, error.message);
10971
- await this.reportOperationResult(apiUrl, task.id, "retry", {
10972
- code: "OPERATION_EXCEPTION",
10973
- message: error.message
10974
- });
10975
- }
10976
- }
10977
- console.log("[Daemon] EP1051: Operation reconciliation complete");
10978
- } catch (error) {
10979
- console.error("[Daemon] EP1051: Operation reconciliation error:", error instanceof Error ? error.message : error);
10980
- throw error;
10981
- }
10982
- }
10983
- /**
10984
- * EP1051: Report background operation result to server
10985
- */
10986
- async reportOperationResult(apiUrl, taskId, action, error) {
10987
- try {
10988
- const response = await fetchWithAuth(`${apiUrl}/api/cli/background-ops`, {
10989
- method: "POST",
10990
- headers: { "Content-Type": "application/json" },
10991
- body: JSON.stringify({
10992
- task_id: taskId,
10993
- action,
10994
- error
10995
- })
10996
- });
10997
- if (!response.ok) {
10998
- console.warn(`[Daemon] EP1051: Failed to report operation result: ${response.status}`);
10999
- }
11000
- } catch (err) {
11001
- console.warn(`[Daemon] EP1051: Error reporting operation result: ${err.message}`);
11002
- }
11003
- }
10927
+ // EP1188: Background ops queue removed - worktree cleanup now handled by Inngest events
10928
+ // (worktree/cleanup) which send WebSocket commands to connected daemons via ws-proxy.
11004
10929
  /**
11005
10930
  * EP1002: Handle worktree_setup command from server
11006
10931
  * This provides a unified setup flow for both local and cloud environments.
@@ -11217,6 +11142,11 @@ var Daemon = class _Daemon {
11217
11142
  if (config?.access_token) {
11218
11143
  await this.performHealthChecks(config);
11219
11144
  }
11145
+ this.healthCheckCounter++;
11146
+ if (this.healthCheckCounter >= _Daemon.WORKTREE_CLEANUP_EVERY_N_CHECKS) {
11147
+ this.healthCheckCounter = 0;
11148
+ await this.auditWorktreesOnStartup();
11149
+ }
11220
11150
  } catch (error) {
11221
11151
  console.error("[Daemon] EP929: Health check error:", error instanceof Error ? error.message : error);
11222
11152
  } finally {
@@ -11403,6 +11333,19 @@ var Daemon = class _Daemon {
11403
11333
  console.warn("[Daemon] EP1035: Failed to prune stale worktrees:", pruneError.message);
11404
11334
  }
11405
11335
  }
11336
+ try {
11337
+ const cleanupResult = await manager.cleanupNonModuleWorktrees();
11338
+ if (cleanupResult.removed.length > 0) {
11339
+ console.log(`[Daemon] EP1190: Cleaned up ${cleanupResult.removed.length} non-module worktree(s): ${cleanupResult.removed.join(", ")}`);
11340
+ }
11341
+ if (cleanupResult.errors.length > 0) {
11342
+ for (const err of cleanupResult.errors) {
11343
+ console.warn(`[Daemon] EP1190: Non-module cleanup error: ${err}`);
11344
+ }
11345
+ }
11346
+ } catch (cleanupError) {
11347
+ console.warn("[Daemon] EP1190: Failed to cleanup non-module worktrees:", cleanupError.message);
11348
+ }
11406
11349
  } catch (error) {
11407
11350
  console.warn(`[Daemon] EP1035: Failed to audit ${projectPath}:`, error);
11408
11351
  }