nightytidy 0.2.8 → 0.2.10

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nightytidy",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
4
4
  "description": "Automated overnight codebase improvement through Claude Code",
5
5
  "license": "MIT",
6
6
  "author": "Dorian Spitz",
@@ -76,7 +76,7 @@ export class CliBridge {
76
76
  }
77
77
 
78
78
  _run(args, onOutput, opts = {}) {
79
- return new Promise((resolve, reject) => {
79
+ return new Promise((resolve) => {
80
80
  const binPath = path.resolve(import.meta.dirname, '../../bin/nightytidy.js');
81
81
  const proc = spawn('node', [binPath, ...args], {
82
82
  cwd: this.projectDir,
@@ -87,15 +87,40 @@ export class CliBridge {
87
87
  let stdout = '';
88
88
  let stderr = '';
89
89
  let killed = false;
90
+ let settled = false;
91
+
92
+ const settle = (result) => {
93
+ if (settled) return;
94
+ settled = true;
95
+ if (timer) clearTimeout(timer);
96
+ if (killTimer) clearTimeout(killTimer);
97
+ resolve(result);
98
+ };
90
99
 
91
100
  // Timeout — kill the process if it takes too long
92
101
  let timer = null;
102
+ let killTimer = null;
93
103
  if (opts.timeout) {
94
104
  timer = setTimeout(() => {
95
105
  killed = true;
96
106
  const timeoutSec = Math.round(opts.timeout / 1000);
97
107
  warn(`CLI process timed out after ${timeoutSec}s: ${args.join(' ')}`);
98
108
  this.kill();
109
+ // On Windows, taskkill is fire-and-forget — the 'close' event may
110
+ // never fire. Force-resolve after 5s to prevent the agent from
111
+ // hanging forever.
112
+ killTimer = setTimeout(() => {
113
+ warn(`CLI process did not exit within 5s after kill — force-resolving`);
114
+ this.activeProcess = null;
115
+ settle({
116
+ success: false,
117
+ exitCode: -1,
118
+ stdout,
119
+ stderr: `Process timed out after ${timeoutSec}s — Claude Code may be unavailable`,
120
+ parsed: CliBridge.parseOutput(stdout),
121
+ timedOut: true,
122
+ });
123
+ }, 5000);
99
124
  }, opts.timeout);
100
125
  }
101
126
 
@@ -127,26 +152,23 @@ export class CliBridge {
127
152
  });
128
153
 
129
154
  proc.on('close', (code) => {
130
- if (timer) clearTimeout(timer);
131
155
  this.activeProcess = null;
132
- const parsed = CliBridge.parseOutput(stdout);
133
- resolve({
156
+ settle({
134
157
  success: code === 0 && !killed,
135
158
  exitCode: code,
136
159
  stdout,
137
160
  stderr: killed
138
161
  ? `Process timed out after ${Math.round(opts.timeout / 1000)}s — Claude Code may be unavailable`
139
162
  : stderr,
140
- parsed,
163
+ parsed: CliBridge.parseOutput(stdout),
141
164
  timedOut: killed,
142
165
  });
143
166
  });
144
167
 
145
168
  proc.on('error', (err) => {
146
- if (timer) clearTimeout(timer);
147
169
  this.activeProcess = null;
148
170
  logError(`CLI process error: ${err.message}`);
149
- resolve({
171
+ settle({
150
172
  success: false,
151
173
  exitCode: -1,
152
174
  stdout,
@@ -378,6 +378,12 @@ export async function startAgent() {
378
378
 
379
379
  case 'cancel-queued': {
380
380
  runQueue.cancel(msg.runId);
381
+ // Also notify Firestore — the run may exist there even if not in local queue
382
+ // (e.g. orphaned after a timeout/crash where the agent already discarded it)
383
+ dispatchWithQueue('run_failed', {
384
+ projectId: msg.projectId || '',
385
+ run: { id: msg.runId },
386
+ }, []);
381
387
  wsServer.broadcast({ type: 'queue-updated', queue: runQueue.getQueue() });
382
388
  reply({ type: 'queue-updated', queue: runQueue.getQueue() });
383
389
  break;
@@ -1080,22 +1086,35 @@ export async function startAgent() {
1080
1086
  const progress = interrupted.lastProgress || {};
1081
1087
  const completed = progress.completedCount || 0;
1082
1088
  const total = interrupted.steps?.length || 0;
1083
- info(`Found interrupted run: ${projName} (${completed}/${total} steps completed)`);
1084
- info(` Run ID: ${interrupted.id}`);
1085
- info(` Use the web app to Resume, Finish with Partial Results, or Discard`);
1086
1089
 
1087
- // Best-effort: notify Firestore that this run is interrupted
1088
- // (in case the shutdown webhook didn't make it)
1089
- if (proj) {
1090
- dispatchWithQueue('run_interrupted', {
1091
- projectId: proj.id,
1092
- run: {
1093
- id: interrupted.id,
1094
- completedSteps: completed,
1095
- failedSteps: progress.failedCount || 0,
1096
- totalCost: progress.totalCost || 0,
1097
- },
1090
+ if (completed === 0) {
1091
+ // Run never actually started (init timed out or crashed before any steps).
1092
+ // Auto-discard — there's nothing to resume or finish, and blocking the
1093
+ // queue for user action is pointless.
1094
+ info(`Auto-discarding interrupted run with 0 completed steps: ${projName} (${interrupted.id})`);
1095
+ runQueue.clearInterrupted();
1096
+ dispatchWithQueue('run_failed', {
1097
+ projectId: proj?.id || interrupted.projectId,
1098
+ run: { id: interrupted.id },
1098
1099
  }, []);
1100
+ } else {
1101
+ info(`Found interrupted run: ${projName} (${completed}/${total} steps completed)`);
1102
+ info(` Run ID: ${interrupted.id}`);
1103
+ info(` Use the web app to Resume, Finish with Partial Results, or Discard`);
1104
+
1105
+ // Best-effort: notify Firestore that this run is interrupted
1106
+ // (in case the shutdown webhook didn't make it)
1107
+ if (proj) {
1108
+ dispatchWithQueue('run_interrupted', {
1109
+ projectId: proj.id,
1110
+ run: {
1111
+ id: interrupted.id,
1112
+ completedSteps: completed,
1113
+ failedSteps: progress.failedCount || 0,
1114
+ totalCost: progress.totalCost || 0,
1115
+ },
1116
+ }, []);
1117
+ }
1099
1118
  }
1100
1119
  }
1101
1120
 
@@ -1103,8 +1122,13 @@ export async function startAgent() {
1103
1122
  // (agent died without graceful shutdown, so markInterrupted was never called)
1104
1123
  const current = runQueue.getCurrent();
1105
1124
  if (current && current.status === 'running' && !activeBridge) {
1106
- info(`Found orphaned running run: ${current.id}marking as interrupted`);
1107
- runQueue.markInterrupted({ completedCount: 0, failedCount: 0, totalCost: 0, stepList: [], currentStepNum: null });
1125
+ // Orphaned run with no progress auto-discard instead of blocking the queue
1126
+ info(`Found orphaned running run: ${current.id} auto-discarding (0 steps completed)`);
1127
+ runQueue.completeCurrent({ success: false });
1128
+ dispatchWithQueue('run_failed', {
1129
+ projectId: current.projectId,
1130
+ run: { id: current.id },
1131
+ }, []);
1108
1132
  }
1109
1133
 
1110
1134
  // Process any queued runs left from a previous session