wogiflow 1.0.40 → 1.0.41

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": "wogiflow",
3
- "version": "1.0.40",
3
+ "version": "1.0.41",
4
4
  "description": "AI-powered development workflow management system with multi-model support",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -145,10 +145,55 @@ async function createDurableSessionAsync(taskId, taskType, steps = [], options =
145
145
  // Check if session already exists for this task (inside lock)
146
146
  const existing = loadDurableSession();
147
147
  if (existing && existing.taskId === taskId) {
148
- // Return existing session for resume
148
+ // v4.1: If existing is a bulk session and we're starting a task, upgrade it
149
+ // This preserves the task queue when transitioning from /wogi-bulk to /wogi-start
150
+ if (existing.taskType === 'bulk' && taskType === 'task') {
151
+ if (process.env.DEBUG) {
152
+ console.log(`[Session] Upgrading bulk session to task session for ${taskId}`);
153
+ }
154
+ // Preserve queue data
155
+ const preservedQueue = existing.taskQueue;
156
+
157
+ // Create new task session
158
+ const session = createSessionObject(taskId, taskType, steps, options);
159
+
160
+ // Restore queue data
161
+ if (preservedQueue && preservedQueue.enabled) {
162
+ session.taskQueue = preservedQueue;
163
+ }
164
+
165
+ saveDurableSession(session);
166
+ return session;
167
+ }
168
+
169
+ // Return existing session for resume (non-bulk case)
149
170
  return existing;
150
171
  }
151
172
 
173
+ // v4.1: Check if there's an existing session with a different taskId but an active queue
174
+ // that includes this taskId - preserve the queue data
175
+ if (existing && existing.taskQueue?.enabled) {
176
+ const queuedTasks = existing.taskQueue.tasks || [];
177
+ if (queuedTasks.includes(taskId)) {
178
+ if (process.env.DEBUG) {
179
+ console.log(`[Session] Preserving queue data when switching to task ${taskId}`);
180
+ }
181
+ // Create new session for new task but preserve queue
182
+ const preservedQueue = { ...existing.taskQueue };
183
+ // Update queue index to point to this task
184
+ const taskIndex = queuedTasks.indexOf(taskId);
185
+ if (taskIndex >= 0) {
186
+ preservedQueue.currentIndex = taskIndex;
187
+ }
188
+
189
+ cleanupLegacyHybridSession();
190
+ const session = createSessionObject(taskId, taskType, steps, options);
191
+ session.taskQueue = preservedQueue;
192
+ saveDurableSession(session);
193
+ return session;
194
+ }
195
+ }
196
+
152
197
  // Clean up legacy hybrid-session.json if present (migration to v2.0)
153
198
  cleanupLegacyHybridSession();
154
199
 
@@ -1243,9 +1288,20 @@ function getSessionStats() {
1243
1288
  * @returns {Object} Updated session
1244
1289
  */
1245
1290
  function initTaskQueue(taskIds, source = 'manual') {
1246
- const session = loadDurableSession();
1291
+ let session = loadDurableSession();
1292
+
1293
+ // v4.1: Create a bulk session if none exists
1294
+ // This fixes the chicken-and-egg problem where /wogi-bulk needs to
1295
+ // initialize the queue BEFORE starting the first task
1247
1296
  if (!session) {
1248
- throw new Error('No active session to initialize queue');
1297
+ if (!taskIds || taskIds.length === 0) {
1298
+ throw new Error('Cannot initialize empty queue without active session');
1299
+ }
1300
+ // Create a minimal bulk session for the first task
1301
+ session = createSessionObject(taskIds[0], 'bulk', [], {});
1302
+ if (process.env.DEBUG) {
1303
+ console.log(`[Queue] Created bulk session for first task: ${taskIds[0]}`);
1304
+ }
1249
1305
  }
1250
1306
 
1251
1307
  session.taskQueue = {
@@ -1257,9 +1313,13 @@ function initTaskQueue(taskIds, source = 'manual') {
1257
1313
  completedTasks: []
1258
1314
  };
1259
1315
 
1260
- // Ensure first task matches current session
1316
+ // Ensure first task matches current session (if session existed)
1261
1317
  if (taskIds[0] && session.taskId !== taskIds[0]) {
1262
- console.warn(`[Queue] First task ${taskIds[0]} doesn't match current session ${session.taskId}`);
1318
+ // Update session to match first task in queue
1319
+ session.taskId = taskIds[0];
1320
+ if (process.env.DEBUG) {
1321
+ console.log(`[Queue] Updated session taskId to match first queued task: ${taskIds[0]}`);
1322
+ }
1263
1323
  }
1264
1324
 
1265
1325
  session.updatedAt = new Date().toISOString();
@@ -1416,6 +1476,139 @@ function checkQueueContinuation() {
1416
1476
  };
1417
1477
  }
1418
1478
 
1479
+ // ============================================================================
1480
+ // Skill Execution Tracking (v4.1)
1481
+ // ============================================================================
1482
+
1483
+ const PENDING_SKILL_FILE = 'pending-skill.json';
1484
+
1485
+ /**
1486
+ * Get path to pending skill state file
1487
+ */
1488
+ function getPendingSkillPath() {
1489
+ const projectRoot = getProjectRoot();
1490
+ return path.join(projectRoot, '.workflow', 'state', PENDING_SKILL_FILE);
1491
+ }
1492
+
1493
+ /**
1494
+ * Mark a skill as pending execution
1495
+ * Called when a skill is loaded but not yet executed
1496
+ * @param {string} skillName - Name of the skill (e.g., "wogi-bulk", "wogi-start")
1497
+ * @param {Object} context - Optional context (taskIds for bulk, etc.)
1498
+ * @returns {boolean} Success
1499
+ */
1500
+ function markSkillPending(skillName, context = {}) {
1501
+ if (!skillName || typeof skillName !== 'string') {
1502
+ return false;
1503
+ }
1504
+
1505
+ const pendingPath = getPendingSkillPath();
1506
+
1507
+ try {
1508
+ ensureDir(path.dirname(pendingPath));
1509
+ const state = {
1510
+ skillName,
1511
+ markedAt: new Date().toISOString(),
1512
+ context,
1513
+ status: 'pending'
1514
+ };
1515
+ writeJson(pendingPath, state);
1516
+ if (process.env.DEBUG) {
1517
+ console.log(`[Skill] Marked ${skillName} as pending`);
1518
+ }
1519
+ return true;
1520
+ } catch (err) {
1521
+ if (process.env.DEBUG) {
1522
+ console.error(`[Skill] Failed to mark skill pending: ${err.message}`);
1523
+ }
1524
+ return false;
1525
+ }
1526
+ }
1527
+
1528
+ /**
1529
+ * Mark a skill as executed (clear pending state)
1530
+ * Called when skill execution begins (e.g., when /wogi-start creates a session)
1531
+ * @returns {boolean} Success
1532
+ */
1533
+ function clearPendingSkill() {
1534
+ const pendingPath = getPendingSkillPath();
1535
+
1536
+ try {
1537
+ if (fs.existsSync(pendingPath)) {
1538
+ fs.unlinkSync(pendingPath);
1539
+ if (process.env.DEBUG) {
1540
+ console.log('[Skill] Cleared pending skill state');
1541
+ }
1542
+ }
1543
+ return true;
1544
+ } catch (err) {
1545
+ if (process.env.DEBUG) {
1546
+ console.error(`[Skill] Failed to clear pending skill: ${err.message}`);
1547
+ }
1548
+ return false;
1549
+ }
1550
+ }
1551
+
1552
+ /**
1553
+ * Check if a skill is pending execution
1554
+ * Used by stop hook to prevent premature exit
1555
+ * @returns {Object|null} Pending skill info or null
1556
+ */
1557
+ function getPendingSkill() {
1558
+ const pendingPath = getPendingSkillPath();
1559
+
1560
+ if (!fs.existsSync(pendingPath)) {
1561
+ return null;
1562
+ }
1563
+
1564
+ try {
1565
+ const state = safeJsonParse(pendingPath, null);
1566
+ if (!state || state.status !== 'pending') {
1567
+ return null;
1568
+ }
1569
+
1570
+ // Check for stale pending state (older than 5 minutes = likely abandoned)
1571
+ const markedAt = new Date(state.markedAt);
1572
+ const ageMs = Date.now() - markedAt.getTime();
1573
+ const maxAgeMs = 5 * 60 * 1000; // 5 minutes
1574
+
1575
+ if (ageMs > maxAgeMs) {
1576
+ // Stale - clean up and return null
1577
+ if (process.env.DEBUG) {
1578
+ console.log(`[Skill] Cleaning up stale pending skill (age: ${Math.round(ageMs / 1000)}s)`);
1579
+ }
1580
+ clearPendingSkill();
1581
+ return null;
1582
+ }
1583
+
1584
+ return state;
1585
+ } catch (err) {
1586
+ if (process.env.DEBUG) {
1587
+ console.error(`[Skill] Failed to read pending skill: ${err.message}`);
1588
+ }
1589
+ return null;
1590
+ }
1591
+ }
1592
+
1593
+ /**
1594
+ * Check if execution should be blocked due to pending skill
1595
+ * @returns {Object} { hasPendingSkill, skillName, message }
1596
+ */
1597
+ function checkPendingSkillExecution() {
1598
+ const pending = getPendingSkill();
1599
+
1600
+ if (!pending) {
1601
+ return { hasPendingSkill: false };
1602
+ }
1603
+
1604
+ return {
1605
+ hasPendingSkill: true,
1606
+ skillName: pending.skillName,
1607
+ context: pending.context,
1608
+ message: `Skill /${pending.skillName} is pending execution. Complete it before stopping.`
1609
+ };
1610
+ }
1611
+
1419
1612
  // ============================================================================
1420
1613
  // Exports
1421
1614
  // ============================================================================
@@ -1481,7 +1674,13 @@ module.exports = {
1481
1674
  getQueueStatus,
1482
1675
  advanceTaskQueue,
1483
1676
  clearTaskQueue,
1484
- checkQueueContinuation
1677
+ checkQueueContinuation,
1678
+
1679
+ // Skill Execution Tracking (v4.1)
1680
+ markSkillPending,
1681
+ clearPendingSkill,
1682
+ getPendingSkill,
1683
+ checkPendingSkillExecution
1485
1684
  };
1486
1685
 
1487
1686
  // ============================================================================
@@ -63,7 +63,8 @@ const {
63
63
  getSuspensionStatus,
64
64
  resumeSession,
65
65
  isSuspended,
66
- STEP_STATUS
66
+ STEP_STATUS,
67
+ clearPendingSkill // v4.1: Clear pending skill state when task starts
67
68
  } = require('./flow-durable-session');
68
69
 
69
70
  // Spec loader for scope enforcement (optional - graceful degradation)
@@ -320,6 +321,10 @@ async function main() {
320
321
  filesToChange
321
322
  });
322
323
 
324
+ // v4.1: Clear pending skill state now that task has started
325
+ // This signals to the stop hook that the skill has been executed
326
+ clearPendingSkill();
327
+
323
328
  if (steps.length > 0) {
324
329
  console.log(color('cyan', `📋 Durable session initialized with ${steps.length} steps`));
325
330
  } else if (process.env.DEBUG) {
@@ -14,7 +14,7 @@ const fs = require('fs');
14
14
 
15
15
  // Import from parent scripts directory
16
16
  const { getConfig, PATHS } = require('../../flow-utils');
17
- const { checkQueueContinuation, advanceTaskQueue } = require('../../flow-durable-session');
17
+ const { checkQueueContinuation, advanceTaskQueue, checkPendingSkillExecution } = require('../../flow-durable-session');
18
18
 
19
19
  /**
20
20
  * Check if loop enforcement is enabled
@@ -113,6 +113,19 @@ function checkLoopExit() {
113
113
  const session = getActiveLoopSession();
114
114
 
115
115
  if (!session) {
116
+ // v4.1: Check if a skill is pending execution before allowing exit
117
+ // This fixes the bug where /wogi-bulk loads but Claude stops before executing
118
+ const pendingSkill = checkPendingSkillExecution();
119
+ if (pendingSkill.hasPendingSkill) {
120
+ return {
121
+ canExit: false,
122
+ blocked: true,
123
+ message: pendingSkill.message,
124
+ reason: 'skill_pending_execution',
125
+ skillName: pendingSkill.skillName
126
+ };
127
+ }
128
+
116
129
  return {
117
130
  canExit: true,
118
131
  blocked: false,
@@ -13,6 +13,7 @@ const { checkScopeGate } = require('../../core/scope-gate');
13
13
  const { checkComponentReuse } = require('../../core/component-check');
14
14
  const { checkTodoWriteGate } = require('../../core/todowrite-gate');
15
15
  const { claudeCodeAdapter } = require('../../adapters/claude-code');
16
+ const { markSkillPending } = require('../../../flow-durable-session');
16
17
 
17
18
  // Maximum stdin size to prevent DoS (100KB should be enough for tool inputs)
18
19
  const MAX_STDIN_SIZE = 100 * 1024;
@@ -94,6 +95,18 @@ async function main() {
94
95
  }
95
96
  }
96
97
 
98
+ // v4.1: Skill execution tracking (for Skill tool)
99
+ // Catches natural language skill invocations (e.g., "do the bulk tasks")
100
+ if (toolName === 'Skill') {
101
+ const skillName = toolInput.skill;
102
+ if (typeof skillName === 'string' && /^wogi-(bulk|start)$/i.test(skillName)) {
103
+ markSkillPending(skillName.toLowerCase(), { args: toolInput.args });
104
+ if (process.env.DEBUG) {
105
+ console.error(`[Hook] Marked skill ${skillName} as pending (via Skill tool)`);
106
+ }
107
+ }
108
+ }
109
+
97
110
  // Component reuse check (for Write only)
98
111
  if (toolName === 'Write' && filePath) {
99
112
  const componentResult = checkComponentReuse({
@@ -9,6 +9,7 @@
9
9
 
10
10
  const { checkImplementationGate } = require('../../core/implementation-gate');
11
11
  const { claudeCodeAdapter } = require('../../adapters/claude-code');
12
+ const { markSkillPending } = require('../../../flow-durable-session');
12
13
 
13
14
  // Maximum stdin size to prevent DoS (100KB should be more than enough for prompts)
14
15
  const MAX_STDIN_SIZE = 100 * 1024;
@@ -51,6 +52,19 @@ async function main() {
51
52
  const prompt = parsedInput.prompt;
52
53
  const source = parsedInput.source;
53
54
 
55
+ // v4.1: Detect skill commands that need execution tracking
56
+ // This prevents premature exit when /wogi-bulk or /wogi-start is entered
57
+ if (typeof prompt === 'string') {
58
+ const skillMatch = prompt.match(/^\/(wogi-bulk|wogi-start)\b/i);
59
+ if (skillMatch) {
60
+ const skillName = skillMatch[1].toLowerCase();
61
+ markSkillPending(skillName, { prompt });
62
+ if (process.env.DEBUG) {
63
+ console.error(`[Hook] Marked /${skillName} as pending execution`);
64
+ }
65
+ }
66
+ }
67
+
54
68
  // Check implementation gate
55
69
  const coreResult = checkImplementationGate({
56
70
  prompt,