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
|
@@ -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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
// ============================================================================
|
package/scripts/flow-start.js
CHANGED
|
@@ -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,
|