agileflow 2.89.0 → 2.89.1

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/CHANGELOG.md CHANGED
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.89.1] - 2026-01-14
11
+
12
+ ### Fixed
13
+ - Fix active_commands preservation after conversation compact
14
+
10
15
  ## [2.89.0] - 2026-01-14
11
16
 
12
17
  ### Added
package/lib/file-cache.js CHANGED
@@ -130,9 +130,10 @@ class LRUCache {
130
130
  ...this.stats,
131
131
  size: this.cache.size,
132
132
  maxSize: this.maxSize,
133
- hitRate: this.stats.hits + this.stats.misses > 0
134
- ? (this.stats.hits / (this.stats.hits + this.stats.misses) * 100).toFixed(1) + '%'
135
- : '0%',
133
+ hitRate:
134
+ this.stats.hits + this.stats.misses > 0
135
+ ? ((this.stats.hits / (this.stats.hits + this.stats.misses)) * 100).toFixed(1) + '%'
136
+ : '0%',
136
137
  };
137
138
  }
138
139
 
package/lib/progress.js CHANGED
@@ -168,7 +168,8 @@ class Spinner {
168
168
  }
169
169
 
170
170
  const elapsed = this.startTime ? Date.now() - this.startTime : 0;
171
- const suffix = elapsed > DOHERTY_THRESHOLD_MS ? ` ${c.dim}(${formatDuration(elapsed)})${c.reset}` : '';
171
+ const suffix =
172
+ elapsed > DOHERTY_THRESHOLD_MS ? ` ${c.dim}(${formatDuration(elapsed)})${c.reset}` : '';
172
173
 
173
174
  console.log(`${color}${symbol}${c.reset} ${message}${suffix}`);
174
175
  return this;
@@ -179,7 +180,7 @@ class Spinner {
179
180
  * @returns {boolean}
180
181
  */
181
182
  wasFast() {
182
- return this.startTime && (Date.now() - this.startTime) < DOHERTY_THRESHOLD_MS;
183
+ return this.startTime && Date.now() - this.startTime < DOHERTY_THRESHOLD_MS;
183
184
  }
184
185
  }
185
186
 
@@ -263,7 +264,7 @@ class ProgressBar {
263
264
  return this;
264
265
  }
265
266
 
266
- const percent = this.total > 0 ? (current / this.total) : 0;
267
+ const percent = this.total > 0 ? current / this.total : 0;
267
268
  const filled = Math.round(this.width * percent);
268
269
  const empty = this.width - filled;
269
270
 
@@ -300,7 +301,8 @@ class ProgressBar {
300
301
  }
301
302
 
302
303
  const elapsed = Date.now() - this.startTime;
303
- const suffix = elapsed > DOHERTY_THRESHOLD_MS ? ` ${c.dim}(${formatDuration(elapsed)})${c.reset}` : '';
304
+ const suffix =
305
+ elapsed > DOHERTY_THRESHOLD_MS ? ` ${c.dim}(${formatDuration(elapsed)})${c.reset}` : '';
304
306
  const msg = message || `${this.label} complete`;
305
307
 
306
308
  console.log(`${c.green}✓${c.reset} ${msg} (${this.total} items)${suffix}`);
package/lib/validate.js CHANGED
@@ -427,8 +427,7 @@ function validatePath(inputPath, baseDir, options = {}) {
427
427
  ? normalizedBase
428
428
  : normalizedBase + path.sep;
429
429
 
430
- const isWithinBase =
431
- resolvedPath === normalizedBase || resolvedPath.startsWith(baseWithSep);
430
+ const isWithinBase = resolvedPath === normalizedBase || resolvedPath.startsWith(baseWithSep);
432
431
 
433
432
  if (!isWithinBase) {
434
433
  return {
package/lib/yaml-utils.js CHANGED
@@ -53,11 +53,7 @@ function safeLoadAll(content, options = {}) {
53
53
  }
54
54
 
55
55
  const documents = [];
56
- yaml.loadAll(
57
- content,
58
- (doc) => documents.push(doc),
59
- { schema: yaml.DEFAULT_SCHEMA, ...options }
60
- );
56
+ yaml.loadAll(content, doc => documents.push(doc), { schema: yaml.DEFAULT_SCHEMA, ...options });
61
57
  return documents;
62
58
  }
63
59
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agileflow",
3
- "version": "2.89.0",
3
+ "version": "2.89.1",
4
4
  "description": "AI-driven agile development system for Claude Code, Cursor, Windsurf, and more",
5
5
  "keywords": [
6
6
  "agile",
@@ -274,7 +274,7 @@ function runArchival(rootDir, cache = null) {
274
274
  }
275
275
 
276
276
  function clearActiveCommands(rootDir, cache = null) {
277
- const result = { ran: false, cleared: 0, commandNames: [] };
277
+ const result = { ran: false, cleared: 0, commandNames: [], preserved: false };
278
278
 
279
279
  try {
280
280
  const sessionStatePath = path.join(rootDir, 'docs/09-agents/session-state.json');
@@ -291,6 +291,31 @@ function clearActiveCommands(rootDir, cache = null) {
291
291
  result.ran = true;
292
292
  }
293
293
 
294
+ // Check if PreCompact just ran (within last 30 seconds)
295
+ // If so, preserve active_commands instead of clearing them (post-compact session start)
296
+ if (state.last_precompact_at) {
297
+ const precompactTime = new Date(state.last_precompact_at).getTime();
298
+ const now = Date.now();
299
+ const secondsSincePrecompact = (now - precompactTime) / 1000;
300
+
301
+ if (secondsSincePrecompact < 30) {
302
+ // This is a post-compact session start - preserve active commands
303
+ result.preserved = true;
304
+ // Capture command names for display (but don't clear)
305
+ if (state.active_commands && state.active_commands.length > 0) {
306
+ for (const cmd of state.active_commands) {
307
+ if (cmd.name) result.commandNames.push(cmd.name);
308
+ }
309
+ }
310
+ // Clear the precompact timestamp so next true session start will clear
311
+ delete state.last_precompact_at;
312
+ fs.writeFileSync(sessionStatePath, JSON.stringify(state, null, 2) + '\n');
313
+ return result;
314
+ }
315
+ // Precompact was too long ago - clear as normal
316
+ delete state.last_precompact_at;
317
+ }
318
+
294
319
  // Handle new array format (active_commands)
295
320
  if (state.active_commands && state.active_commands.length > 0) {
296
321
  result.cleared = state.active_commands.length;
@@ -1061,10 +1086,18 @@ function formatTable(
1061
1086
  }
1062
1087
 
1063
1088
  // Session cleanup
1064
- const sessionStatus = session.cleared > 0 ? `cleared ${session.cleared} command(s)` : `clean`;
1065
- lines.push(
1066
- row('Session state', sessionStatus, c.lavender, session.cleared > 0 ? c.mintGreen : c.dim)
1067
- );
1089
+ let sessionStatus, sessionColor;
1090
+ if (session.preserved) {
1091
+ sessionStatus = `preserved ${session.commandNames.length} command(s)`;
1092
+ sessionColor = c.mintGreen;
1093
+ } else if (session.cleared > 0) {
1094
+ sessionStatus = `cleared ${session.cleared} command(s)`;
1095
+ sessionColor = c.mintGreen;
1096
+ } else {
1097
+ sessionStatus = `clean`;
1098
+ sessionColor = c.dim;
1099
+ }
1100
+ lines.push(row('Session state', sessionStatus, c.lavender, sessionColor));
1068
1101
 
1069
1102
  // PreCompact status with version check
1070
1103
  if (precompact.configured && precompact.scriptExists) {
@@ -1269,12 +1302,14 @@ async function main() {
1269
1302
  if (parallelSessions.cleaned > 0 && parallelSessions.cleanedSessions) {
1270
1303
  console.log('');
1271
1304
  console.log(`${c.amber}📋 Cleaned ${parallelSessions.cleaned} inactive session(s):${c.reset}`);
1272
- parallelSessions.cleanedSessions.forEach((sess) => {
1305
+ parallelSessions.cleanedSessions.forEach(sess => {
1273
1306
  const name = sess.nickname ? `${sess.id} "${sess.nickname}"` : `Session ${sess.id}`;
1274
1307
  const reason = sess.reason === 'pid_dead' ? 'process ended' : sess.reason;
1275
1308
  console.log(` ${c.dim}└─ ${name} (${reason}, PID ${sess.pid})${c.reset}`);
1276
1309
  });
1277
- console.log(` ${c.slate}Sessions are cleaned when their Claude Code process is no longer running.${c.reset}`);
1310
+ console.log(
1311
+ ` ${c.slate}Sessions are cleaned when their Claude Code process is no longer running.${c.reset}`
1312
+ );
1278
1313
  }
1279
1314
 
1280
1315
  // Story claiming: cleanup stale claims and show warnings
@@ -46,11 +46,14 @@ function saveSessionState(rootDir, state) {
46
46
  async function resolveGlob(pattern, rootDir) {
47
47
  // Use bash globbing for pattern expansion
48
48
  try {
49
- const result = execSync(`bash -c 'shopt -s nullglob; for f in ${pattern}; do echo "$f"; done'`, {
50
- cwd: rootDir,
51
- encoding: 'utf8',
52
- timeout: 10000,
53
- });
49
+ const result = execSync(
50
+ `bash -c 'shopt -s nullglob; for f in ${pattern}; do echo "$f"; done'`,
51
+ {
52
+ cwd: rootDir,
53
+ encoding: 'utf8',
54
+ timeout: 10000,
55
+ }
56
+ );
54
57
  const files = result
55
58
  .split('\n')
56
59
  .filter(f => f.trim())
@@ -218,7 +221,9 @@ function handleBatchLoop(rootDir) {
218
221
  );
219
222
  console.log('');
220
223
  console.log(`${c.green}Pattern: ${loop.pattern}${c.reset}`);
221
- console.log(`${c.dim}${summary.completed} items completed in ${iteration - 1} iterations${c.reset}`);
224
+ console.log(
225
+ `${c.dim}${summary.completed} items completed in ${iteration - 1} iterations${c.reset}`
226
+ );
222
227
  return;
223
228
  }
224
229
 
@@ -287,7 +292,9 @@ function handleBatchLoop(rootDir) {
287
292
  console.log(`${c.cyan}━━━ Next Item ━━━${c.reset}`);
288
293
  console.log(`${c.bold}${nextItem}${c.reset}`);
289
294
  console.log('');
290
- console.log(`${c.dim}Progress: ${summary.completed}/${summary.total} items complete${c.reset}`);
295
+ console.log(
296
+ `${c.dim}Progress: ${summary.completed}/${summary.total} items complete${c.reset}`
297
+ );
291
298
  console.log('');
292
299
  console.log(`${c.brand}▶ Implement "${loop.action}" for this file${c.reset}`);
293
300
  console.log(`${c.dim} Run tests when ready. Loop will validate and continue.${c.reset}`);
@@ -310,7 +317,9 @@ function handleBatchLoop(rootDir) {
310
317
  console.log('');
311
318
  console.log(`${c.green}Pattern: ${loop.pattern}${c.reset}`);
312
319
  console.log(`${c.green}Action: ${loop.action}${c.reset}`);
313
- console.log(`${c.dim}${summary.completed} items completed in ${iteration} iterations${c.reset}`);
320
+ console.log(
321
+ `${c.dim}${summary.completed} items completed in ${iteration} iterations${c.reset}`
322
+ );
314
323
  }
315
324
  } else {
316
325
  // Tests failed - continue iterating
@@ -353,9 +362,7 @@ async function handleInit(args, rootDir) {
353
362
  }
354
363
 
355
364
  const pattern = patternArg.split('=').slice(1).join('=').replace(/"/g, '');
356
- const action = actionArg
357
- ? actionArg.split('=').slice(1).join('=').replace(/"/g, '')
358
- : 'process';
365
+ const action = actionArg ? actionArg.split('=').slice(1).join('=').replace(/"/g, '') : 'process';
359
366
  const maxIterations = parseIntBounded(maxArg ? maxArg.split('=')[1] : null, 50, 1, 200);
360
367
 
361
368
  // Resolve glob pattern
@@ -432,7 +439,9 @@ function handleStatus(rootDir) {
432
439
  console.log(` Pattern: ${loop.pattern}`);
433
440
  console.log(` Action: ${loop.action}`);
434
441
  console.log(` Current Item: ${loop.current_item || 'none'}`);
435
- console.log(` Progress: ${summary.completed}/${summary.total} (${summary.in_progress} in progress)`);
442
+ console.log(
443
+ ` Progress: ${summary.completed}/${summary.total} (${summary.in_progress} in progress)`
444
+ );
436
445
  console.log(` Iteration: ${loop.iteration || 0}/${loop.max_iterations || 50}`);
437
446
  }
438
447
 
@@ -461,10 +461,10 @@ function generateFullContent() {
461
461
  'loop-mode': 'Autonomous epic execution (MODE=loop)',
462
462
  'multi-session': 'Multi-session coordination detected',
463
463
  'visual-e2e': 'Visual screenshot verification (VISUAL=true)',
464
- 'delegation': 'Expert spawning patterns (load when spawning)',
465
- 'stuck': 'Research prompt guidance (load after 2 failures)',
464
+ delegation: 'Expert spawning patterns (load when spawning)',
465
+ stuck: 'Research prompt guidance (load after 2 failures)',
466
466
  'plan-mode': 'Planning workflow details (load when entering plan mode)',
467
- 'tools': 'Tool usage guidance (load when needed)',
467
+ tools: 'Tool usage guidance (load when needed)',
468
468
  };
469
469
 
470
470
  content += `\n${C.dim}Section meanings:${C.reset}\n`;
@@ -121,3 +121,17 @@ cat << EOF
121
121
  3. Review docs/02-practices/ for implementation patterns
122
122
  4. Check git log for recent changes
123
123
  EOF
124
+
125
+ # Mark that PreCompact just ran - tells SessionStart to preserve active_commands
126
+ # This prevents the welcome script from clearing commands right after compact
127
+ if [ -f "docs/09-agents/session-state.json" ]; then
128
+ node -e "
129
+ const fs = require('fs');
130
+ const path = 'docs/09-agents/session-state.json';
131
+ try {
132
+ const state = JSON.parse(fs.readFileSync(path, 'utf8'));
133
+ state.last_precompact_at = new Date().toISOString();
134
+ fs.writeFileSync(path, JSON.stringify(state, null, 2) + '\n');
135
+ } catch (e) {}
136
+ " 2>/dev/null
137
+ fi
@@ -228,9 +228,8 @@ function registerSession(nickname = null, threadType = null) {
228
228
  registry.next_id++;
229
229
 
230
230
  const isMain = cwd === ROOT;
231
- const detectedType = threadType && THREAD_TYPES.includes(threadType)
232
- ? threadType
233
- : detectThreadType(null, !isMain);
231
+ const detectedType =
232
+ threadType && THREAD_TYPES.includes(threadType) ? threadType : detectThreadType(null, !isMain);
234
233
 
235
234
  registry.sessions[sessionId] = {
236
235
  path: cwd,
@@ -750,10 +749,10 @@ function getSessionPhase(session) {
750
749
 
751
750
  // Count commits since branch diverged from main
752
751
  const mainBranch = getMainBranch();
753
- const commitCount = execSync(
754
- `git rev-list --count ${mainBranch}..HEAD 2>/dev/null || echo 0`,
755
- { cwd: sessionPath, encoding: 'utf8' }
756
- ).trim();
752
+ const commitCount = execSync(`git rev-list --count ${mainBranch}..HEAD 2>/dev/null || echo 0`, {
753
+ cwd: sessionPath,
754
+ encoding: 'utf8',
755
+ }).trim();
757
756
 
758
757
  const commits = parseInt(commitCount, 10);
759
758
 
@@ -1037,7 +1036,9 @@ function main() {
1037
1036
  if (nickname) registry.sessions[sessionId].nickname = nickname;
1038
1037
  // Ensure thread_type exists (migration for old sessions)
1039
1038
  if (!registry.sessions[sessionId].thread_type) {
1040
- registry.sessions[sessionId].thread_type = registry.sessions[sessionId].is_main ? 'base' : 'parallel';
1039
+ registry.sessions[sessionId].thread_type = registry.sessions[sessionId].is_main
1040
+ ? 'base'
1041
+ : 'parallel';
1041
1042
  }
1042
1043
  writeLock(sessionId, pid);
1043
1044
  } else {
@@ -1218,7 +1219,9 @@ function main() {
1218
1219
  const sessionId = args[2];
1219
1220
  const threadType = args[3];
1220
1221
  if (!sessionId || !threadType) {
1221
- console.log(JSON.stringify({ success: false, error: 'Usage: thread-type set <sessionId> <type>' }));
1222
+ console.log(
1223
+ JSON.stringify({ success: false, error: 'Usage: thread-type set <sessionId> <type>' })
1224
+ );
1222
1225
  return;
1223
1226
  }
1224
1227
  const result = setSessionThreadType(sessionId, threadType);
@@ -1910,7 +1913,10 @@ function getSessionThreadType(sessionId = null) {
1910
1913
  */
1911
1914
  function setSessionThreadType(sessionId, threadType) {
1912
1915
  if (!THREAD_TYPES.includes(threadType)) {
1913
- return { success: false, error: `Invalid thread type: ${threadType}. Valid: ${THREAD_TYPES.join(', ')}` };
1916
+ return {
1917
+ success: false,
1918
+ error: `Invalid thread type: ${threadType}. Valid: ${THREAD_TYPES.join(', ')}`,
1919
+ };
1914
1920
  }
1915
1921
 
1916
1922
  const registry = loadRegistry();
@@ -63,8 +63,8 @@ console.log(`File Being Edited: ${normalizedFile}`);
63
63
  console.log('');
64
64
 
65
65
  // Check if file is within active session path
66
- const isInsideSession = normalizedFile.startsWith(normalizedActive + path.sep) ||
67
- normalizedFile === normalizedActive;
66
+ const isInsideSession =
67
+ normalizedFile.startsWith(normalizedActive + path.sep) || normalizedFile === normalizedActive;
68
68
 
69
69
  if (isInsideSession) {
70
70
  console.log('✅ ALLOWED - File is inside the active session directory');
@@ -130,11 +130,7 @@ async function handleGet(status, key) {
130
130
  const handler = new ErrorHandler('config');
131
131
 
132
132
  if (!key) {
133
- handler.warning(
134
- 'Missing key',
135
- 'Provide a config key to get',
136
- 'npx agileflow config get <key>'
137
- );
133
+ handler.warning('Missing key', 'Provide a config key to get', 'npx agileflow config get <key>');
138
134
  }
139
135
 
140
136
  const validKeys = ['userName', 'ides', 'agileflowFolder', 'docsFolder', 'version'];
@@ -103,11 +103,7 @@ module.exports = {
103
103
 
104
104
  if (!coreResult.success) {
105
105
  const handler = new ErrorHandler('setup');
106
- handler.warning(
107
- 'Core setup failed',
108
- 'Check directory permissions',
109
- 'npx agileflow doctor'
110
- );
106
+ handler.warning('Core setup failed', 'Check directory permissions', 'npx agileflow doctor');
111
107
  }
112
108
 
113
109
  success(`Installed ${coreResult.counts.agents} agents`);
@@ -195,11 +195,7 @@ class BaseIdeSetup {
195
195
  `Permission denied: ${error.message}`
196
196
  );
197
197
  }
198
- throw new CleanupError(
199
- this.displayName,
200
- agileflowPath,
201
- error.message
202
- );
198
+ throw new CleanupError(this.displayName, agileflowPath, error.message);
203
199
  }
204
200
  }
205
201
  }
@@ -332,11 +328,7 @@ class BaseIdeSetup {
332
328
  try {
333
329
  content = this.injectDynamicContent(content, agileflowDir);
334
330
  } catch (injectionError) {
335
- throw new ContentInjectionError(
336
- this.displayName,
337
- sourcePath,
338
- injectionError.message
339
- );
331
+ throw new ContentInjectionError(this.displayName, sourcePath, injectionError.message);
340
332
  }
341
333
  }
342
334
 
@@ -350,12 +342,10 @@ class BaseIdeSetup {
350
342
  if (error.name && error.name.includes('Error') && error.ideName) {
351
343
  throw error;
352
344
  }
353
- throw new CommandInstallationError(
354
- this.displayName,
355
- entry.name,
356
- error.message,
357
- { sourcePath, targetPath }
358
- );
345
+ throw new CommandInstallationError(this.displayName, entry.name, error.message, {
346
+ sourcePath,
347
+ targetPath,
348
+ });
359
349
  }
360
350
  } else if (entry.isDirectory()) {
361
351
  // Recursively process subdirectory
@@ -7,6 +7,7 @@
7
7
 
8
8
  const path = require('node:path');
9
9
  const fs = require('fs-extra');
10
+ const chalk = require('chalk');
10
11
  const { BaseIdeSetup } = require('./_base-ide');
11
12
 
12
13
  /**
@@ -7,6 +7,7 @@
7
7
 
8
8
  const path = require('node:path');
9
9
  const fs = require('fs-extra');
10
+ const chalk = require('chalk');
10
11
  const { BaseIdeSetup } = require('./_base-ide');
11
12
 
12
13
  /**