wogiflow 2.1.3 → 2.3.0

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.
@@ -8,13 +8,21 @@
8
8
  *
9
9
  * Purpose: Restore critical state that may have been lost during compaction.
10
10
  * - Re-inject durable session context (current task, completed steps, remaining work)
11
- * - Re-inject active decisions and routing state
11
+ * - Re-inject acceptance criteria with completion status
12
+ * - Re-inject changed files and last request-log entry
12
13
  * - Ensure routing-pending flag is set (compaction = new context, needs re-routing)
13
14
  *
14
15
  * This hook is non-blocking (fail-open). Compaction should never be prevented
15
16
  * by a state restoration failure.
17
+ *
18
+ * v2.0: Hoisted shared requires, added criteria/files/log restoration,
19
+ * fixed criteria done status to read from scenarios.completed[]
16
20
  */
17
21
 
22
+ const path = require('node:path');
23
+ const fs = require('node:fs');
24
+ const { PATHS, safeJsonParse, getReadyData } = require('../../flow-utils');
25
+
18
26
  /**
19
27
  * Sanitize a string value before injecting into AI context.
20
28
  * Strips markdown heading markers and truncates to prevent prompt manipulation.
@@ -65,7 +73,6 @@ function handlePostCompact() {
65
73
 
66
74
  // 2. Check for active task in ready.json (fallback if no durable session)
67
75
  try {
68
- const { getReadyData } = require('../../flow-utils');
69
76
  const readyData = getReadyData();
70
77
  if (Array.isArray(readyData.inProgress) && readyData.inProgress.length > 0) {
71
78
  const task = readyData.inProgress[0];
@@ -82,6 +89,77 @@ function handlePostCompact() {
82
89
  }
83
90
  }
84
91
 
92
+ // 2b. Load acceptance criteria and changed files from task checkpoint
93
+ try {
94
+ const checkpointPath = path.join(PATHS.state, 'task-checkpoint.json');
95
+ const checkpoint = safeJsonParse(checkpointPath, null);
96
+ if (checkpoint && checkpoint.taskId) {
97
+ // Inject acceptance criteria with completion status
98
+ // Derive done status from scenarios.completed[] (the authoritative source)
99
+ // rather than criteria[].done (which is never updated after initialization)
100
+ const completedIndices = new Set(
101
+ (checkpoint.scenarios?.completed || []).map(s => s.index)
102
+ );
103
+
104
+ if (Array.isArray(checkpoint.criteria) && checkpoint.criteria.length > 0) {
105
+ const criteriaLines = checkpoint.criteria.slice(0, 15).map((c, i) => {
106
+ const isDone = completedIndices.has(i);
107
+ const status = isDone ? '✓' : '○';
108
+ return ` ${status} ${sanitize(c.text || c.description || c.id, 120)}`;
109
+ });
110
+ const done = checkpoint.criteria.filter((_, i) => completedIndices.has(i)).length;
111
+ contextParts.push(`**Acceptance Criteria** (${done}/${checkpoint.criteria.length} done):\n${criteriaLines.join('\n')}`);
112
+ }
113
+
114
+ // Inject changed files list
115
+ if (Array.isArray(checkpoint.changedFiles) && checkpoint.changedFiles.length > 0) {
116
+ const files = checkpoint.changedFiles.slice(0, 20).map(f => ` - ${sanitize(f, 100)}`);
117
+ contextParts.push(`**Changed files this session** (${checkpoint.changedFiles.length}):\n${files.join('\n')}`);
118
+ }
119
+ }
120
+
121
+ // Fallback: get changed files from git if checkpoint doesn't have them
122
+ if (!checkpoint || !checkpoint.changedFiles || checkpoint.changedFiles.length === 0) {
123
+ try {
124
+ const { execFileSync } = require('node:child_process');
125
+ const gitFiles = execFileSync('git', ['diff', '--name-only', 'HEAD'], {
126
+ encoding: 'utf-8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe']
127
+ }).trim();
128
+ if (gitFiles) {
129
+ const files = gitFiles.split('\n').filter(Boolean).slice(0, 20);
130
+ contextParts.push(`**Uncommitted changes** (${files.length} files):\n${files.map(f => ` - ${f}`).join('\n')}`);
131
+ }
132
+ } catch (_err) {
133
+ // git not available or no changes — skip silently
134
+ }
135
+ }
136
+ } catch (err) {
137
+ if (process.env.DEBUG) {
138
+ console.error(`[post-compact] Criteria/files restore failed: ${err.message}`);
139
+ }
140
+ }
141
+
142
+ // 2c. Load last request-log entry number
143
+ try {
144
+ const logPath = path.join(PATHS.state, 'request-log.md');
145
+ if (fs.existsSync(logPath)) {
146
+ const content = fs.readFileSync(logPath, 'utf-8');
147
+ // Find the last R-NNN entry
148
+ const matches = content.match(/^### R-(\d+)/gm);
149
+ if (matches && matches.length > 0) {
150
+ const lastEntry = matches[0]; // First match = most recent (file is reverse-chronological)
151
+ const num = lastEntry.match(/R-(\d+)/)?.[1];
152
+ if (num) {
153
+ contextParts.push(`**Last request-log entry**: R-${num} (next entry should be R-${parseInt(num, 10) + 1})`);
154
+ }
155
+ }
156
+ }
157
+ } catch (err) {
158
+ if (process.env.DEBUG) {
159
+ console.error(`[post-compact] Request-log read failed: ${err.message}`);
160
+ }
161
+ }
162
+
85
163
  // 3. Re-set routing-pending flag
86
164
  // After compaction, the AI has fresh context and may try to act without routing.
87
165
  // Setting routing-pending ensures the next tool use goes through routing checks.
@@ -99,8 +177,6 @@ function handlePostCompact() {
99
177
 
100
178
  // 4. Load current workflow phase
101
179
  try {
102
- const { PATHS, safeJsonParse } = require('../../flow-utils');
103
- const path = require('node:path');
104
180
  const phasePath = path.join(PATHS.state, 'workflow-phase.json');
105
181
  const phaseData = safeJsonParse(phasePath, {});
106
182
  if (phaseData.phase && phaseData.phase !== 'idle') {
@@ -116,9 +192,6 @@ function handlePostCompact() {
116
192
  // Claude Code stops auto-compaction after 3 consecutive failures.
117
193
  // If we detect repeated compactions in quick succession, warn about potential issues.
118
194
  try {
119
- const path = require('node:path');
120
- const fs = require('node:fs');
121
- const { PATHS, safeJsonParse } = require('../../flow-utils');
122
195
  const compactStatePath = path.join(PATHS.state, '.compact-tracker.json');
123
196
  const tracker = safeJsonParse(compactStatePath, { count: 0, lastAt: null });
124
197
  const now = Date.now();
@@ -136,7 +209,7 @@ function handlePostCompact() {
136
209
  fs.writeFileSync(compactStatePath, JSON.stringify(tracker, null, 2));
137
210
 
138
211
  if (tracker.count >= 3) {
139
- contextParts.push('**WARNING**: Multiple compactions detected in quick succession. Claude Code 2.1.76+ stops auto-compaction after 3 consecutive failures. If context keeps growing, consider starting a new session or committing progress and using `/wogi-pre-compact`.');
212
+ contextParts.push('**WARNING**: Multiple compactions detected in quick succession. Claude Code 2.1.76+ stops auto-compaction after 3 consecutive failures. If context keeps growing, consider starting a new session.');
140
213
  }
141
214
  } catch (err) {
142
215
  if (process.env.DEBUG) {
@@ -93,6 +93,12 @@ async function handleTaskCompleted(input) {
93
93
  completedTask.status = 'completed';
94
94
  completedTask.completedAt = new Date().toISOString();
95
95
 
96
+ // Strip progress prefix from title (e.g., "[3/5] Title" → "Title")
97
+ // Done inside the lock to avoid race conditions with progress tracker
98
+ if (completedTask.title) {
99
+ completedTask.title = completedTask.title.replace(/^\[\d+\/\d+\]\s*/, '');
100
+ }
101
+
96
102
  // Remove from inProgress
97
103
  ready.inProgress = ready.inProgress.filter(t =>
98
104
  (typeof t === 'string' ? t : t.id) !== completedTask.id
@@ -140,6 +146,19 @@ async function handleTaskCompleted(input) {
140
146
  }
141
147
  }
142
148
 
149
+ // Clear progress tracker state on task completion
150
+ if (result.completed) {
151
+ try {
152
+ const { clearProgress } = require('../../flow-progress-tracker');
153
+ clearProgress();
154
+ } catch (err) {
155
+ // Non-fatal — progress tracker may not exist in older installs
156
+ if (process.env.DEBUG) {
157
+ console.error(`[Task Completed] Progress clear failed: ${err.message}`);
158
+ }
159
+ }
160
+ }
161
+
143
162
  // Update durable history if it exists (under lock to prevent concurrent corruption)
144
163
  if (result.completed) {
145
164
  try {
@@ -113,6 +113,26 @@ async function main() {
113
113
  if (process.env.DEBUG) console.error(`[post-tool-use] setCurrentTask: ${err.message}`);
114
114
  }
115
115
 
116
+ // v7.0: Initialize task checkpoint with criteria for PostCompact recovery
117
+ try {
118
+ const { saveCheckpoint } = require('../../../flow-task-checkpoint');
119
+ const criteriaList = (task.acceptanceCriteria || task.scenarios || [])
120
+ .map((c, i) => ({
121
+ id: `ac-${i + 1}`,
122
+ text: typeof c === 'string' ? c : (c.description || c.title || `Criterion ${i + 1}`),
123
+ done: false
124
+ }));
125
+ await saveCheckpoint({
126
+ taskId,
127
+ taskTitle,
128
+ currentPhase: 'coding',
129
+ criteria: criteriaList,
130
+ changedFiles: []
131
+ });
132
+ } catch (err) {
133
+ if (process.env.DEBUG) console.error(`[post-tool-use] Checkpoint init: ${err.message}`);
134
+ }
135
+
116
136
  if (process.env.DEBUG) {
117
137
  console.error(`[post-tool-use] Initialized durable session for ${taskId} (prompt-path bridge)`);
118
138
  }
@@ -126,6 +146,31 @@ async function main() {
126
146
  }
127
147
  }
128
148
 
149
+ // Auto registry scan after successful git commit (fire-and-forget)
150
+ // v7.0: Mechanical enforcement — AI no longer needs to remember to run registry scan.
151
+ // Runs after every commit, regardless of task level (L3 included).
152
+ if (toolName === 'Bash' && toolInput.command && !toolFailed) {
153
+ const { isGitCommit } = require('../../core/commit-log-gate');
154
+ if (isGitCommit(toolInput.command)) {
155
+ try {
156
+ const { RegistryManager } = require('../../../flow-registry-manager');
157
+ const manager = new RegistryManager();
158
+ manager.loadPlugins();
159
+ manager.activatePlugins();
160
+ manager.scanAll().catch((err) => {
161
+ if (process.env.DEBUG) {
162
+ console.error(`[post-tool-use] Auto registry scan failed: ${err.message}`);
163
+ }
164
+ });
165
+ } catch (err) {
166
+ // Non-blocking — registry manager may not be available
167
+ if (process.env.DEBUG) {
168
+ console.error(`[post-tool-use] Registry manager load error: ${err.message}`);
169
+ }
170
+ }
171
+ }
172
+ }
173
+
129
174
  // Only run validation for Edit/Write
130
175
  if (toolName !== 'Edit' && toolName !== 'Write') {
131
176
  console.log(JSON.stringify({ continue: true }));
@@ -140,6 +185,21 @@ async function main() {
140
185
  return;
141
186
  }
142
187
 
188
+ // v7.0: Track changed files in task checkpoint (continuous state persistence)
189
+ // This ensures the PostCompact hook can restore the changed files list
190
+ // after auto-compaction, making /wogi-pre-compact redundant for file tracking.
191
+ if (filePath && !filePath.includes('.workflow/') && !filePath.includes('.claude/')) {
192
+ try {
193
+ const { trackChangedFile } = require('../../../flow-task-checkpoint');
194
+ trackChangedFile(filePath);
195
+ } catch (err) {
196
+ // Non-blocking — checkpoint may not exist yet (no active task)
197
+ if (process.env.DEBUG) {
198
+ console.error(`[post-tool-use] File tracking: ${err.message}`);
199
+ }
200
+ }
201
+ }
202
+
143
203
  // Run validation
144
204
  const coreResult = await runValidation({
145
205
  filePath,
@@ -17,6 +17,7 @@ const { checkComponentReuse } = require('../../core/component-check');
17
17
  const { checkTodoWriteGate } = require('../../core/todowrite-gate');
18
18
  const { checkRoutingGate, clearRoutingPending, hasActiveTask } = require('../../core/routing-gate');
19
19
  const { checkPhaseGate } = require('../../core/phase-gate');
20
+ const { checkCommitLogGate } = require('../../core/commit-log-gate');
20
21
  const { claudeCodeAdapter } = require('../../adapters/claude-code');
21
22
  const { markSkillPending } = require('../../../flow-durable-session');
22
23
  const { getConfig } = require('../../../flow-utils');
@@ -242,6 +243,32 @@ async function main() {
242
243
  }
243
244
  }
244
245
 
246
+ // Commit log gate check (for Bash git commit commands)
247
+ // v9.0: Block git commit when active task has no request-log entry staged.
248
+ // Same mechanical enforcement pattern as routing gate.
249
+ if (toolName === 'Bash' && toolInput.command) {
250
+ try {
251
+ const commitLogResult = checkCommitLogGate(toolInput.command, config);
252
+ if (commitLogResult.blocked) {
253
+ coreResult = {
254
+ allowed: false,
255
+ blocked: true,
256
+ reason: `Commit log gate: ${commitLogResult.reason}`,
257
+ message: commitLogResult.message
258
+ };
259
+ const output = claudeCodeAdapter.transformResult('PreToolUse', coreResult);
260
+ console.log(JSON.stringify(output));
261
+ process.exit(0);
262
+ return;
263
+ }
264
+ } catch (err) {
265
+ // Fail-open for commit log gate — don't block work if gate has issues
266
+ if (process.env.DEBUG) {
267
+ console.error(`[Hook] Commit log gate error (fail-open): ${err.message}`);
268
+ }
269
+ }
270
+ }
271
+
245
272
  // Strict adherence check (for Bash commands)
246
273
  // v5.0: Block AI from using wrong package manager or port
247
274
  if (toolName === 'Bash') {
@@ -290,10 +290,147 @@ class ComponentScanner extends BaseScanner {
290
290
  }
291
291
 
292
292
  generateMap() {
293
- // Component registry generates component-index.json only.
294
- // It does NOT overwrite app-map.md — that remains human-curated.
295
- // The index is complementary: auto-generated for machine use.
296
- success(`Component index available at ${path.relative(PROJECT_ROOT, INDEX_PATH)}`);
293
+ const MAP_PATH = path.join(STATE_DIR, 'app-map.md');
294
+
295
+ // Check if app-map.md exists and has content
296
+ let existing = '';
297
+ try {
298
+ existing = fs.readFileSync(MAP_PATH, 'utf-8');
299
+ } catch (_err) {
300
+ // File doesn't exist — will create
301
+ }
302
+
303
+ // Use marker to distinguish auto-generated from human-curated
304
+ const AUTO_MARKER = '<!-- AUTO-GENERATED BY COMPONENT SCANNER -->';
305
+ const isAutoGenerated = existing.includes(AUTO_MARKER);
306
+ const hasContent = existing.split('\n').filter(l => l.startsWith('|')).length > 5;
307
+
308
+ if (hasContent && !isAutoGenerated) {
309
+ // Human-curated content — merge without overwriting
310
+ this._mergeIntoAppMap(MAP_PATH, existing);
311
+ return;
312
+ }
313
+
314
+ // Generate fresh app-map.md from scan results
315
+ const lines = [
316
+ '# App Map',
317
+ '',
318
+ AUTO_MARKER,
319
+ '',
320
+ 'Component and page registry. **Check before creating anything new.**',
321
+ '',
322
+ '> Auto-generated by component scanner. Edit to add context.',
323
+ '',
324
+ '---',
325
+ ''
326
+ ];
327
+
328
+ // Group components by category
329
+ const categories = {};
330
+ for (const comp of this.registry.components) {
331
+ const cat = comp.category || 'uncategorized';
332
+ if (!categories[cat]) categories[cat] = [];
333
+ categories[cat].push(comp);
334
+ }
335
+
336
+ // Components section
337
+ if (this.registry.components.length > 0) {
338
+ lines.push('## Components', '');
339
+
340
+ for (const cat of Object.keys(categories).sort()) {
341
+ const catName = cat.charAt(0).toUpperCase() + cat.slice(1);
342
+ lines.push(`### ${catName}`, '');
343
+ lines.push('| Component | File | Description |');
344
+ lines.push('|-----------|------|-------------|');
345
+
346
+ for (const comp of categories[cat].sort((a, b) => a.name.localeCompare(b.name))) {
347
+ const desc = comp.description || '-';
348
+ lines.push(`| \`${comp.name}\` | \`${comp.file}\` | ${desc} |`);
349
+ }
350
+ lines.push('');
351
+ }
352
+ }
353
+
354
+ // Hooks section
355
+ if (this.registry.hooks.length > 0) {
356
+ lines.push('## Hooks', '');
357
+ lines.push('| Hook | File | Description |');
358
+ lines.push('|------|------|-------------|');
359
+
360
+ for (const hook of this.registry.hooks.sort((a, b) => a.name.localeCompare(b.name))) {
361
+ const desc = hook.description || '-';
362
+ lines.push(`| \`${hook.name}\` | \`${hook.file}\` | ${desc} |`);
363
+ }
364
+ lines.push('');
365
+ }
366
+
367
+ lines.push('---', '');
368
+ lines.push('## Rules', '');
369
+ lines.push('1. **Before creating** → Search this file');
370
+ lines.push('2. **If similar exists** → Add variant, don\'t create new');
371
+ lines.push('3. **After creating** → Run `flow registry-manager scan` to update');
372
+ lines.push('');
373
+
374
+ fs.writeFileSync(MAP_PATH, lines.join('\n'));
375
+ success(`Generated ${path.relative(PROJECT_ROOT, MAP_PATH)} (${this.registry.components.length} components, ${this.registry.hooks.length} hooks)`);
376
+ }
377
+
378
+ /**
379
+ * Merge scanner results into existing app-map.md without overwriting curated content.
380
+ * Adds only components not already present.
381
+ */
382
+ _mergeIntoAppMap(mapPath, existing) {
383
+ // Path containment check (defense-in-depth)
384
+ if (!mapPath.startsWith(STATE_DIR)) {
385
+ warn('Write target outside state directory — skipping merge');
386
+ return;
387
+ }
388
+
389
+ // Extract names already in app-map (with or without backticks)
390
+ const existingNames = new Set();
391
+ const nameRegex = /\|\s*`?(\w+)`?\s*\|/g;
392
+ let m;
393
+ while ((m = nameRegex.exec(existing)) !== null) {
394
+ existingNames.add(m[1]);
395
+ }
396
+
397
+ // Find new components not in app-map
398
+ const newComponents = this.registry.components.filter(c => !existingNames.has(c.name));
399
+ const newHooks = this.registry.hooks.filter(h => !existingNames.has(h.name));
400
+
401
+ if (newComponents.length === 0 && newHooks.length === 0) {
402
+ info(`app-map.md is up to date (${existingNames.size} entries)`);
403
+ return;
404
+ }
405
+
406
+ // Remove existing "## Auto-Discovered" section if present (prevent duplicates)
407
+ const autoDiscoveredIdx = existing.indexOf('\n## Auto-Discovered');
408
+ const base = autoDiscoveredIdx !== -1 ? existing.substring(0, autoDiscoveredIdx) : existing;
409
+
410
+ const additions = ['\n## Auto-Discovered (new entries)', ''];
411
+
412
+ if (newComponents.length > 0) {
413
+ additions.push('### Components', '');
414
+ additions.push('| Component | File | Description |');
415
+ additions.push('|-----------|------|-------------|');
416
+ for (const comp of newComponents.sort((a, b) => a.name.localeCompare(b.name))) {
417
+ additions.push(`| \`${comp.name}\` | \`${comp.file}\` | ${comp.description || '-'} |`);
418
+ }
419
+ additions.push('');
420
+ }
421
+
422
+ if (newHooks.length > 0) {
423
+ additions.push('### Hooks', '');
424
+ additions.push('| Hook | File | Description |');
425
+ additions.push('|------|------|-------------|');
426
+ for (const hook of newHooks.sort((a, b) => a.name.localeCompare(b.name))) {
427
+ additions.push(`| \`${hook.name}\` | \`${hook.file}\` | ${hook.description || '-'} |`);
428
+ }
429
+ additions.push('');
430
+ }
431
+
432
+ fs.writeFileSync(mapPath, base + additions.join('\n'));
433
+ success(`Merged ${newComponents.length} components + ${newHooks.length} hooks into app-map.md`);
297
434
  }
298
435
  }
299
436