wogiflow 2.8.0 → 2.9.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.
@@ -21,6 +21,7 @@ const fs = require('node:fs');
21
21
  const { getConfig, PATHS, safeJsonParse, writeJson, withLock, validateTaskId, archiveCompletedTasksToLog } = require('../../flow-utils');
22
22
  const { resetPhase, isPhaseGateEnabled } = require('./phase-gate');
23
23
  const { clearOnTaskComplete } = require('../../flow-hook-status');
24
+ const { checkGateLatch, clearGateLatch } = require('../../flow-gate-latch');
24
25
 
25
26
  /**
26
27
  * Check if task completed handling is enabled
@@ -90,6 +91,22 @@ async function handleTaskCompleted(input) {
90
91
  }
91
92
  result.taskId = completedTask.id;
92
93
 
94
+ // Gate latch check — verify quality gates have passed before allowing completion.
95
+ // Without this, agents can call TaskUpdate and bypass all quality gates.
96
+ // The latch is written by flow-done.js after gates pass.
97
+ const config = getConfig();
98
+ const requireGateLatch = config.enforcement?.requireGateLatch !== false;
99
+ if (requireGateLatch) {
100
+ const latchResult = checkGateLatch(completedTask.id);
101
+ if (!latchResult.valid) {
102
+ result.message = `BLOCKED: ${latchResult.reason} ` +
103
+ 'Quality gates must pass before a task can be completed. ' +
104
+ 'Run the full /wogi-start pipeline (Step 4: Quality Gates) or `flow done` first.';
105
+ result.gateBlocked = true;
106
+ return;
107
+ }
108
+ }
109
+
93
110
  // Move task to recentlyCompleted
94
111
  completedTask.status = 'completed';
95
112
  completedTask.completedAt = new Date().toISOString();
@@ -158,6 +175,15 @@ async function handleTaskCompleted(input) {
158
175
  }
159
176
  }
160
177
 
178
+ // Clear gate latch after successful completion
179
+ if (result.completed) {
180
+ try {
181
+ clearGateLatch();
182
+ } catch (_err) {
183
+ // Non-critical
184
+ }
185
+ }
186
+
161
187
  // Clear progress tracker state on task completion
162
188
  if (result.completed) {
163
189
  try {
@@ -285,6 +311,83 @@ async function handleTaskCompleted(input) {
285
311
  // Workspace notifications are handled by the Stop hook (via HTTP to manager port).
286
312
  // Removed duplicate file-based notification here to prevent double messages (finding-004).
287
313
 
314
+ // Compound from success — capture positive patterns (fire-and-forget)
315
+ if (result.completed) {
316
+ try {
317
+ const config = getConfig();
318
+ if (config.skillLearning?.enabled) {
319
+ const { writeToFeedbackPatterns } = require('../../flow-learning-orchestrator');
320
+ const taskType = completedTask.type || 'unknown';
321
+ const changedFiles = input.changedFiles || [];
322
+ const criteriaCount = input.scenarioCount || completedTask.criteria || 0;
323
+ const firstPass = input.firstAttemptPass !== false;
324
+
325
+ // Only record success patterns for non-trivial tasks that passed on first attempt
326
+ if (firstPass && changedFiles.length >= 2 && criteriaCount >= 2) {
327
+ const today = new Date().toISOString().split('T')[0];
328
+ const filesSummary = changedFiles.slice(0, 5).map(f => path.basename(f)).join(', ');
329
+ const entryText = `success-pattern: ${taskType} task (${criteriaCount} criteria, ${changedFiles.length} files) completed first-pass. Files: ${filesSummary}`;
330
+ const tableRow = `| ${today} | ${entryText} | 1 | - | #success |`;
331
+
332
+ writeToFeedbackPatterns({
333
+ content: tableRow,
334
+ entryText,
335
+ caller: 'task-completed-success',
336
+ }).catch(() => {
337
+ // Non-critical
338
+ });
339
+ }
340
+ }
341
+ } catch (_err) {
342
+ // Non-critical — success pattern capture may not be available
343
+ }
344
+ }
345
+
346
+ // Skill learning extraction (fire-and-forget)
347
+ if (result.completed) {
348
+ try {
349
+ const { isLearningEnabled, extractLearningContext, matchFilesToSkills, appendLearning, discoverSkills, ensureKnowledgeDir, formatSemanticChanges } = require('../../flow-skill-learn');
350
+ const config = getConfig();
351
+ if (isLearningEnabled(config, 'task')) {
352
+ const changedFiles = input.changedFiles || [];
353
+ if (changedFiles.length > 0) {
354
+ const skills = discoverSkills();
355
+ const { matches: skillMap } = matchFilesToSkills(changedFiles, skills);
356
+ const context = extractLearningContext(changedFiles, 'task');
357
+
358
+ // Enrich context with task info
359
+ context.summary = `Task ${completedTask.id}: ${completedTask.title || ''}`;
360
+ context.taskType = completedTask.type || 'unknown';
361
+
362
+ for (const [skillName, matchedFiles] of skillMap) {
363
+ if (matchedFiles.length > 0) {
364
+ const skill = skills.find(s => s.name === skillName);
365
+ const skillDir = skill?.path;
366
+ if (skillDir) {
367
+ ensureKnowledgeDir(skillDir);
368
+ const entry = [
369
+ `### ${context.summary}`,
370
+ `**Type**: ${context.type} | **Trigger**: task-complete`,
371
+ `**Files**: ${matchedFiles.join(', ')}`,
372
+ ];
373
+ if (context.semanticChanges.length > 0) {
374
+ entry.push(`**Changes**: ${formatSemanticChanges(context.semanticChanges).slice(0, 200)}`);
375
+ }
376
+ entry.push('');
377
+ appendLearning(skillDir, entry.join('\n'));
378
+ }
379
+ }
380
+ }
381
+ }
382
+ }
383
+ } catch (_err) {
384
+ // Non-critical — skill learning may not be available
385
+ if (process.env.DEBUG) {
386
+ console.error(`[Task Completed] Skill learning failed: ${_err.message}`);
387
+ }
388
+ }
389
+ }
390
+
288
391
  // Check pending queue — notify user if items are waiting
289
392
  try {
290
393
  const { getPendingCount } = require('../../flow-pending');
@@ -928,6 +928,26 @@ function main() {
928
928
  // so the correct hooks are already in place when stale ones are removed.
929
929
  migrateStaleLocalHooks();
930
930
 
931
+ // Migrate config.json defaults when they change between versions.
932
+ // Without this, existing users keep stale defaults forever because
933
+ // deepMerge(defaults, userConfig) lets user values win.
934
+ // Only upgrades keys that still match the OLD default — user-customized values are preserved.
935
+ try {
936
+ const configPath = path.join(WORKFLOW_DIR, 'config.json');
937
+ if (fs.existsSync(configPath)) {
938
+ const { migrateConfigFile } = require('./flow-config-migrate');
939
+ const migrationResult = migrateConfigFile(configPath);
940
+ if (migrationResult.migrated && !shouldBeSilent()) {
941
+ process.stderr.write(`\x1b[36mWogiFlow:\x1b[0m Migrated config (v${migrationResult.fromVersion} → v${migrationResult.toVersion}): ${migrationResult.applied.length} change(s).\n`);
942
+ }
943
+ }
944
+ } catch (err) {
945
+ // Non-fatal — config migration should never fail installation
946
+ if (process.env.DEBUG) {
947
+ console.error(`[postinstall] Config migration failed: ${err.message}`);
948
+ }
949
+ }
950
+
931
951
  // NOTE: Scripts and .workflow/ managed dirs (bridges, templates, agents, lib) are
932
952
  // NO LONGER copied to the project. They live exclusively in the wogiflow package
933
953
  // under node_modules/wogiflow/. This keeps user projects clean.