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.
- package/.claude/commands/wogi-audit.md +189 -3
- package/.claude/commands/wogi-bulk.md +1 -1
- package/.claude/commands/wogi-help.md +1 -1
- package/.claude/commands/{wogi-compact.md → wogi-pre-compact.md} +6 -2
- package/.claude/commands/wogi-review.md +86 -13
- package/.claude/commands/wogi-setup-stack.md +1 -1
- package/.claude/commands/wogi-skill-learn.md +1 -1
- package/.claude/commands/wogi-start.md +65 -20
- package/.claude/docs/claude-code-compatibility.md +28 -0
- package/.claude/docs/commands.md +1 -1
- package/.claude/docs/knowledge-base/02-task-execution/04-completion.md +1 -1
- package/.claude/docs/knowledge-base/04-memory-context/README.md +2 -2
- package/.claude/docs/knowledge-base/04-memory-context/context-management.md +1 -1
- package/.claude/rules/_internal/README.md +64 -0
- package/.claude/rules/_internal/document-structure.md +77 -0
- package/.claude/rules/_internal/dual-repo-management.md +174 -0
- package/.claude/rules/_internal/feature-refactoring-cleanup.md +87 -0
- package/.claude/rules/_internal/github-releases.md +71 -0
- package/.claude/rules/_internal/model-management.md +35 -0
- package/.claude/rules/_internal/self-maintenance.md +87 -0
- package/.claude/rules/architecture/component-reuse.md +38 -0
- package/.claude/rules/code-style/naming-conventions.md +52 -0
- package/.claude/rules/operations/git-workflows.md +92 -0
- package/.claude/rules/operations/scratch-directory.md +54 -0
- package/.claude/rules/security/security-patterns.md +176 -0
- package/.claude/skills/figma-analyzer/knowledge/learnings.md +11 -0
- package/.workflow/bridges/claude-bridge.js +1 -1
- package/.workflow/models/registry.json +1 -1
- package/.workflow/specs/architecture.md.template +24 -0
- package/.workflow/specs/stack.md.template +33 -0
- package/.workflow/specs/testing.md.template +36 -0
- package/.workflow/templates/claude-md.hbs +33 -3
- package/README.md +1 -1
- package/package.json +1 -1
- package/scripts/flow-audit.js +158 -1
- package/scripts/flow-context-compact/index.js +1 -1
- package/scripts/flow-context-monitor.js +2 -2
- package/scripts/flow-loop-retry-learning.js +1 -1
- package/scripts/flow-proactive-compact.js +3 -3
- package/scripts/flow-progress-tracker.js +289 -0
- package/scripts/flow-prompt-capture.js +263 -170
- package/scripts/flow-standards-learner.js +167 -3
- package/scripts/flow-task-checkpoint.js +2 -0
- package/scripts/flow-version-check.js +1 -0
- package/scripts/hooks/core/commit-log-gate.js +146 -0
- package/scripts/hooks/core/post-compact.js +109 -4
- package/scripts/hooks/core/task-completed.js +19 -0
- package/scripts/hooks/entry/claude-code/post-tool-use.js +60 -0
- 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
|
-
|
|
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*${
|
|
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
|
|
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') {
|