wogiflow 2.1.2 → 2.2.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.
Files changed (49) hide show
  1. package/.claude/commands/wogi-audit.md +189 -3
  2. package/.claude/commands/wogi-bulk.md +1 -1
  3. package/.claude/commands/wogi-help.md +1 -1
  4. package/.claude/commands/{wogi-compact.md → wogi-pre-compact.md} +6 -2
  5. package/.claude/commands/wogi-review.md +86 -13
  6. package/.claude/commands/wogi-setup-stack.md +1 -1
  7. package/.claude/commands/wogi-skill-learn.md +1 -1
  8. package/.claude/commands/wogi-start.md +65 -20
  9. package/.claude/docs/claude-code-compatibility.md +28 -0
  10. package/.claude/docs/commands.md +1 -1
  11. package/.claude/docs/knowledge-base/02-task-execution/04-completion.md +1 -1
  12. package/.claude/docs/knowledge-base/04-memory-context/README.md +2 -2
  13. package/.claude/docs/knowledge-base/04-memory-context/context-management.md +1 -1
  14. package/.claude/rules/_internal/README.md +64 -0
  15. package/.claude/rules/_internal/document-structure.md +77 -0
  16. package/.claude/rules/_internal/dual-repo-management.md +174 -0
  17. package/.claude/rules/_internal/feature-refactoring-cleanup.md +87 -0
  18. package/.claude/rules/_internal/github-releases.md +71 -0
  19. package/.claude/rules/_internal/model-management.md +35 -0
  20. package/.claude/rules/_internal/self-maintenance.md +87 -0
  21. package/.claude/rules/architecture/component-reuse.md +38 -0
  22. package/.claude/rules/code-style/naming-conventions.md +52 -0
  23. package/.claude/rules/operations/git-workflows.md +92 -0
  24. package/.claude/rules/operations/scratch-directory.md +54 -0
  25. package/.claude/rules/security/security-patterns.md +176 -0
  26. package/.claude/skills/figma-analyzer/knowledge/learnings.md +11 -0
  27. package/.workflow/bridges/claude-bridge.js +1 -1
  28. package/.workflow/models/registry.json +1 -1
  29. package/.workflow/specs/architecture.md.template +24 -0
  30. package/.workflow/specs/stack.md.template +33 -0
  31. package/.workflow/specs/testing.md.template +36 -0
  32. package/.workflow/templates/claude-md.hbs +33 -3
  33. package/README.md +1 -1
  34. package/package.json +1 -1
  35. package/scripts/flow-audit.js +158 -1
  36. package/scripts/flow-context-compact/index.js +1 -1
  37. package/scripts/flow-context-monitor.js +2 -2
  38. package/scripts/flow-loop-retry-learning.js +1 -1
  39. package/scripts/flow-proactive-compact.js +3 -3
  40. package/scripts/flow-progress-tracker.js +289 -0
  41. package/scripts/flow-prompt-capture.js +263 -170
  42. package/scripts/flow-standards-learner.js +167 -3
  43. package/scripts/flow-task-checkpoint.js +2 -0
  44. package/scripts/flow-version-check.js +1 -0
  45. package/scripts/hooks/core/commit-log-gate.js +146 -0
  46. package/scripts/hooks/core/post-compact.js +109 -4
  47. package/scripts/hooks/core/task-completed.js +19 -0
  48. package/scripts/hooks/entry/claude-code/post-tool-use.js +60 -0
  49. package/scripts/hooks/entry/claude-code/pre-tool-use.js +27 -0
@@ -18,8 +18,10 @@ const {
18
18
  readFile,
19
19
  writeFile,
20
20
  getConfig,
21
- color
21
+ color,
22
+ escapeRegex
22
23
  } = require('./flow-utils');
24
+ const { getTodayDate } = require('./flow-output');
23
25
 
24
26
  // ============================================================================
25
27
  // Constants
@@ -101,6 +103,158 @@ const VIOLATION_LEARNING_MAP = {
101
103
  }
102
104
  };
103
105
 
106
+ // ============================================================================
107
+ // Enforcement Gap Detection
108
+ // ============================================================================
109
+
110
+ /**
111
+ * Check if a pattern has an existing rule in decisions.md
112
+ * @param {string} patternId - kebab-case pattern identifier
113
+ * @param {string} [description] - description to also search for
114
+ * @returns {{ exists: boolean, ruleText: string|null, section: string|null }}
115
+ */
116
+ function checkEnforcementGap(patternId, description) {
117
+ if (!fileExists(DECISIONS_PATH)) {
118
+ return { exists: false, ruleText: null, section: null };
119
+ }
120
+
121
+ const content = readFile(DECISIONS_PATH, '');
122
+
123
+ // Extract description words for fuzzy matching (separate from patternId)
124
+ const STOPWORDS = new Set(['code', 'file', 'data', 'type', 'that', 'this', 'with', 'from', 'have', 'been', 'each', 'when', 'should', 'must', 'function', 'class', 'module']);
125
+ const descriptionWords = description
126
+ ? description.split(/\s+/)
127
+ .filter(w => w.length >= 4 && !STOPWORDS.has(w.toLowerCase()))
128
+ .map(w => w.toLowerCase())
129
+ .slice(0, 5)
130
+ : [];
131
+
132
+ // Find which section contains the match
133
+ const sections = content.split(/^## /m).slice(1);
134
+ for (const section of sections) {
135
+ const sectionTitle = section.split('\n')[0].trim();
136
+ const sectionLower = section.toLowerCase();
137
+
138
+ // Check if pattern ID appears in this section (exact match — most reliable)
139
+ if (sectionLower.includes(patternId.toLowerCase())) {
140
+ // Extract the rule block (from ### to next ### or end of section)
141
+ // escapeRegex prevents ReDoS from patternId with regex metacharacters
142
+ const escapedId = escapeRegex(patternId).replace(/-/g, '[\\s-]');
143
+ const ruleMatch = section.match(new RegExp(
144
+ `### [^\\n]*${escapedId}[^\\n]*\\n([\\s\\S]*?)(?=###|$)`,
145
+ 'i'
146
+ ));
147
+ return {
148
+ exists: true,
149
+ ruleText: ruleMatch ? ruleMatch[0].trim().slice(0, 2000) : section.trim().slice(0, 2000),
150
+ section: `## ${sectionTitle}`
151
+ };
152
+ }
153
+
154
+ // Fuzzy fallback: check description words ONLY (patternId excluded to avoid weak matches)
155
+ // Proportional threshold: at least 50% of words must match, minimum 3
156
+ if (descriptionWords.length >= 3) {
157
+ const matchCount = descriptionWords.filter(word => sectionLower.includes(word)).length;
158
+ const threshold = Math.max(3, Math.ceil(descriptionWords.length * 0.5));
159
+ if (matchCount >= threshold) {
160
+ return {
161
+ exists: true,
162
+ ruleText: section.trim().slice(0, 2000),
163
+ section: `## ${sectionTitle}`
164
+ };
165
+ }
166
+ }
167
+ }
168
+
169
+ return { exists: false, ruleText: null, section: null };
170
+ }
171
+
172
+ /**
173
+ * Record a pattern from an audit finding (wraps recordViolationPattern with audit-specific logic).
174
+ * Creates a synthetic learning object from audit cluster data.
175
+ *
176
+ * @param {Object} cluster - Clustered audit pattern
177
+ * @param {string} cluster.patternId - kebab-case canonical name
178
+ * @param {string} cluster.category - architecture, code-style, security, etc.
179
+ * @param {string} cluster.description - one-sentence description
180
+ * @param {number} cluster.instanceCount - number of files affected
181
+ * @param {Object[]} [cluster.instances] - array of { file, detail }
182
+ * @returns {Object} Result with status, count, shouldPromote
183
+ */
184
+ function recordAuditPattern(cluster) {
185
+ const safeDesc = sanitizeForMarkdown(cluster.description, 100);
186
+ // Build a synthetic learning object compatible with recordViolationPattern
187
+ const learning = {
188
+ canLearn: true,
189
+ violationType: cluster.category,
190
+ category: mapAuditCategoryToLearnerCategory(cluster.category),
191
+ subcategory: cluster.patternId,
192
+ patternName: safeDesc,
193
+ preventionPrompt: `Avoid: ${safeDesc}`,
194
+ ruleTemplate: buildAuditRuleTemplate(cluster),
195
+ message: `${cluster.instanceCount} instances found across project (audit source)`,
196
+ source: 'audit'
197
+ };
198
+
199
+ return recordViolationPattern(learning);
200
+ }
201
+
202
+ /**
203
+ * Map audit category names to learner category names
204
+ */
205
+ function mapAuditCategoryToLearnerCategory(auditCategory) {
206
+ const map = {
207
+ 'architecture': 'architecture',
208
+ 'code-style': 'code-style',
209
+ 'security': 'security',
210
+ 'performance': 'code-style',
211
+ 'consistency': 'code-style',
212
+ 'dependencies': 'architecture',
213
+ 'tech-debt': 'architecture',
214
+ 'modernization': 'code-style'
215
+ };
216
+ return map[auditCategory] || 'code-style';
217
+ }
218
+
219
+ /**
220
+ * Sanitize a value for safe interpolation into markdown state files.
221
+ * Strips heading markers, pipe chars (break tables), and newlines.
222
+ * Prevents prompt injection via decisions.md or feedback-patterns.md.
223
+ *
224
+ * @param {string} value - Raw string
225
+ * @param {number} [maxLen=200] - Maximum length
226
+ * @returns {string} Sanitized string
227
+ */
228
+ function sanitizeForMarkdown(value, maxLen = 200) {
229
+ return String(value)
230
+ .replace(/^#+\s/gm, '') // strip heading markers
231
+ .replace(/\|/g, '-') // replace pipes (break table formatting)
232
+ .replace(/`{3,}/g, '') // strip code fence markers (prevent breakout)
233
+ .replace(/[<>]/g, '') // strip angle brackets (prevent HTML injection)
234
+ .replace(/[\r\n]+/g, ' ') // collapse newlines
235
+ .slice(0, maxLen)
236
+ .trim();
237
+ }
238
+
239
+ /**
240
+ * Build a rule template from an audit cluster
241
+ */
242
+ function buildAuditRuleTemplate(cluster) {
243
+ const safeDesc = sanitizeForMarkdown(cluster.description, 200);
244
+ const files = (cluster.instances || [])
245
+ .slice(0, 5)
246
+ .map(i => `\`${sanitizeForMarkdown(i.file, 100)}\``)
247
+ .join(', ');
248
+
249
+ const moreCount = (cluster.instanceCount || 0) - 5;
250
+ const filesList = moreCount > 0 ? `${files}, and ${moreCount} more` : files;
251
+
252
+ return `### ${safeDesc}
253
+ **Source**: Audit pattern promotion (${cluster.instanceCount} instances)
254
+ **Files**: ${filesList || 'Multiple files'}
255
+ **Rule**: ${safeDesc}`;
256
+ }
257
+
104
258
  // ============================================================================
105
259
  // Pattern Analysis
106
260
  // ============================================================================
@@ -223,7 +377,10 @@ function recordViolationPattern(learning) {
223
377
 
224
378
  // Check if pattern already exists in table
225
379
  const dateStr = getTodayDate();
226
- const patternRegex = new RegExp(`\\|\\s*[\\d-]+\\s*\\|\\s*${patternKey.replace(/[-]/g, '[-]?')}\\s*\\|\\s*(\\d+)\\s*\\|`, 'i');
380
+ // escapeRegex prevents ReDoS from patternKey with regex metacharacters
381
+ // Hyphens are required (not optional) — patternKey is kebab-case and must match exactly
382
+ const escapedKey = escapeRegex(patternKey);
383
+ const patternRegex = new RegExp(`\\|\\s*[\\d-]+\\s*\\|\\s*${escapedKey}\\s*\\|\\s*(\\d+)\\s*\\|`, 'i');
227
384
 
228
385
  if (patternRegex.test(content)) {
229
386
  // Update existing count
@@ -362,8 +519,10 @@ ${learning.ruleTemplate}
362
519
  try {
363
520
  let patternsContent = readFile(FEEDBACK_PATTERNS_PATH, '');
364
521
  const patternKey = `${learning.violationType}-${learning.patternName}`.replace(/\s+/g, '-').toLowerCase();
522
+ // escapeRegex prevents ReDoS from patternKey with regex metacharacters
523
+ const escapedPromoteKey = escapeRegex(patternKey);
365
524
  patternsContent = patternsContent.replace(
366
- new RegExp(`(\\|\\s*[\\d-]+\\s*\\|\\s*${patternKey}\\s*\\|\\s*\\d+\\s*\\|)\\s*-\\s*\\|\\s*Monitor\\s*\\|`, 'i'),
525
+ new RegExp(`(\\|\\s*[\\d-]+\\s*\\|\\s*${escapedPromoteKey}\\s*\\|\\s*\\d+\\s*\\|)\\s*-\\s*\\|\\s*Monitor\\s*\\|`, 'i'),
367
526
  `$1 decisions.md | **PROMOTED** |`
368
527
  );
369
528
  fs.writeFileSync(FEEDBACK_PATTERNS_PATH, patternsContent, 'utf-8');
@@ -680,8 +839,13 @@ Examples:
680
839
  module.exports = {
681
840
  analyzeViolationForLearning,
682
841
  recordViolationPattern,
842
+ recordAuditPattern,
683
843
  promoteToDecisions,
684
844
  syncToRulesDir,
845
+ checkEnforcementGap,
846
+ mapAuditCategoryToLearnerCategory,
847
+ buildAuditRuleTemplate,
848
+ sanitizeForMarkdown,
685
849
  getPreventionPrompts,
686
850
  formatPreventionPrompts,
687
851
  learnFromViolations,
@@ -72,6 +72,7 @@ function getDefaultCheckpoint() {
72
72
  currentIndex: -1
73
73
  },
74
74
  changedFiles: [],
75
+ criteria: [],
75
76
  verificationResults: [],
76
77
  explorationSummary: null,
77
78
  completedPhases: [],
@@ -129,6 +130,7 @@ async function saveCheckpoint(params) {
129
130
  specPath: params.specPath || existing.specPath,
130
131
  scenarios: params.scenarios || existing.scenarios,
131
132
  changedFiles: params.changedFiles || existing.changedFiles,
133
+ criteria: params.criteria || existing.criteria,
132
134
  verificationResults: params.verificationResults || existing.verificationResults,
133
135
  explorationSummary: params.explorationSummary || existing.explorationSummary,
134
136
  lastUpdated: new Date().toISOString(),
@@ -124,6 +124,7 @@ function checkClaudeCodeVersionOnce() {
124
124
  { version: [2, 1, 50], features: 'worktree hooks, agent isolation' },
125
125
  { version: [2, 1, 72], features: 'ConfigChange/InstructionsLoaded hooks, effort levels' },
126
126
  { version: [2, 1, 76], features: 'PostCompact hook (state recovery after compaction)' },
127
+ { version: [2, 1, 77], features: 'Elicitation hooks, worktree sparse checkout, 128k output tokens, compaction circuit breaker' },
127
128
  ];
128
129
 
129
130
  const disabledFeatures = SOFT_GATES
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow - Commit Log Gate (Core Module)
5
+ *
6
+ * Blocks git commit when there's an active task but request-log.md
7
+ * hasn't been staged. Mechanical enforcement — same pattern as routing gate.
8
+ *
9
+ * Whitelist (commit allowed without log entry):
10
+ * - No active task in ready.json
11
+ * - Merge commits
12
+ * - State-only commits (all files under .workflow/ or .claude/)
13
+ * - Gate disabled via config.enforcement.commitLogGate.enabled: false
14
+ *
15
+ * v1.0: Initial implementation — pre-commit blocking gate
16
+ */
17
+
18
+ const { execFileSync } = require('node:child_process');
19
+ const { getConfig, getReadyData } = require('../../flow-utils');
20
+
21
+ /**
22
+ * Check if a Bash command contains a git commit
23
+ * @param {string} command - The Bash command string
24
+ * @returns {boolean}
25
+ */
26
+ function isGitCommit(command) {
27
+ if (!command || typeof command !== 'string') return false;
28
+ // Match git commit at start or after chain operators (&&, ;, ||)
29
+ return /(?:^|&&\s*|;\s*|\|\|\s*)git\s+commit\b/.test(command.trim());
30
+ }
31
+
32
+ /**
33
+ * Check if the command is a merge-related commit (whitelisted)
34
+ * @param {string} command - The Bash command string
35
+ * @returns {boolean}
36
+ */
37
+ function isMergeCommit(command) {
38
+ // git merge --continue or similar
39
+ if (/git\s+merge/.test(command)) return true;
40
+ // Commit message starts with "Merge" (from -m flag)
41
+ const msgMatch = command.match(/-m\s+["']([^"']*)/);
42
+ if (msgMatch && /^Merge\b/i.test(msgMatch[1])) return true;
43
+ return false;
44
+ }
45
+
46
+ /**
47
+ * Get list of staged file paths (relative to repo root)
48
+ * @returns {string[]}
49
+ */
50
+ function getStagedFiles() {
51
+ try {
52
+ const output = execFileSync('git', ['diff', '--cached', '--name-only'], {
53
+ encoding: 'utf-8',
54
+ timeout: 3000,
55
+ stdio: ['pipe', 'pipe', 'pipe']
56
+ });
57
+ return output.trim().split('\n').filter(Boolean);
58
+ } catch (err) {
59
+ if (process.env.DEBUG) {
60
+ console.error(`[commit-log-gate] getStagedFiles error: ${err.message}`);
61
+ }
62
+ return [];
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Check if a git commit should be blocked due to missing request-log entry.
68
+ *
69
+ * @param {string} command - The Bash command being executed
70
+ * @param {Object} [config] - Pre-loaded config (optional)
71
+ * @returns {{ allowed: boolean, blocked: boolean, reason?: string, message?: string }}
72
+ */
73
+ function checkCommitLogGate(command, config) {
74
+ // Only check git commit commands
75
+ if (!isGitCommit(command)) {
76
+ return { allowed: true, blocked: false };
77
+ }
78
+
79
+ // Load config if not provided
80
+ if (!config) {
81
+ try { config = getConfig(); } catch (err) { config = {}; }
82
+ }
83
+
84
+ // Check if gate is enabled (default: enabled when enforcement section exists)
85
+ if (config.enforcement?.commitLogGate?.enabled === false) {
86
+ return { allowed: true, blocked: false };
87
+ }
88
+
89
+ // Check for active task in ready.json
90
+ let readyData;
91
+ try {
92
+ readyData = getReadyData();
93
+ } catch (err) {
94
+ // Can't read ready.json → fail-open (don't block work)
95
+ return { allowed: true, blocked: false };
96
+ }
97
+
98
+ if (!readyData.inProgress || readyData.inProgress.length === 0) {
99
+ // No active task → allow (non-task commit)
100
+ return { allowed: true, blocked: false };
101
+ }
102
+
103
+ // Whitelist merge commits
104
+ if (isMergeCommit(command)) {
105
+ return { allowed: true, blocked: false };
106
+ }
107
+
108
+ // Get staged files to check
109
+ const stagedFiles = getStagedFiles();
110
+ if (stagedFiles.length === 0) {
111
+ // No staged files → commit will fail on its own, don't add noise
112
+ return { allowed: true, blocked: false };
113
+ }
114
+
115
+ // Whitelist state-only commits (e.g., pre-compact, wogi-pre-compact state saves)
116
+ const allStateOrConfig = stagedFiles.every(f =>
117
+ f.startsWith('.workflow/') || f.startsWith('.claude/')
118
+ );
119
+ if (allStateOrConfig) {
120
+ return { allowed: true, blocked: false };
121
+ }
122
+
123
+ // Check if request-log.md is in staged changes
124
+ const hasLogEntry = stagedFiles.some(f => f.endsWith('request-log.md'));
125
+ if (hasLogEntry) {
126
+ return { allowed: true, blocked: false };
127
+ }
128
+
129
+ // Block: active task + code changes but no log entry
130
+ const task = readyData.inProgress[0];
131
+ const taskId = (typeof task === 'string' ? task : task.id) || 'unknown';
132
+
133
+ return {
134
+ allowed: false,
135
+ blocked: true,
136
+ reason: 'commit_without_log_entry',
137
+ message: [
138
+ `BLOCKED: Active task ${taskId} but request-log.md is not staged.`,
139
+ 'Add a request-log entry before committing.',
140
+ 'Append a ### R-[N] entry to .workflow/state/request-log.md following the existing format,',
141
+ 'then stage it: git add .workflow/state/request-log.md'
142
+ ].join(' ')
143
+ };
144
+ }
145
+
146
+ module.exports = { checkCommitLogGate, isGitCommit, isMergeCommit };
@@ -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') {
@@ -112,6 +188,35 @@ function handlePostCompact() {
112
188
  }
113
189
  }
114
190
 
191
+ // 5. Check for auto-compaction circuit breaker state (Claude Code 2.1.76+)
192
+ // Claude Code stops auto-compaction after 3 consecutive failures.
193
+ // If we detect repeated compactions in quick succession, warn about potential issues.
194
+ try {
195
+ const compactStatePath = path.join(PATHS.state, '.compact-tracker.json');
196
+ const tracker = safeJsonParse(compactStatePath, { count: 0, lastAt: null });
197
+ const now = Date.now();
198
+ const lastAt = tracker.lastAt ? new Date(tracker.lastAt).getTime() : 0;
199
+ const timeSinceLast = now - lastAt;
200
+
201
+ // If compaction happened less than 2 minutes ago, increment counter
202
+ if (timeSinceLast < 2 * 60 * 1000 && lastAt > 0) {
203
+ tracker.count = (tracker.count || 0) + 1;
204
+ } else {
205
+ tracker.count = 1;
206
+ }
207
+ tracker.lastAt = new Date().toISOString();
208
+
209
+ fs.writeFileSync(compactStatePath, JSON.stringify(tracker, null, 2));
210
+
211
+ if (tracker.count >= 3) {
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.');
213
+ }
214
+ } catch (err) {
215
+ if (process.env.DEBUG) {
216
+ console.error(`[post-compact] Compact tracker failed: ${err.message}`);
217
+ }
218
+ }
219
+
115
220
  // Build the result
116
221
  if (contextParts.length === 0) {
117
222
  return {
@@ -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') {