wogiflow 2.10.0 → 2.12.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/docs/explore-agents.md +35 -0
- package/.workflow/state/bugfix-scope.json.template +7 -0
- package/.workflow/state/deploy-history.json.template +3 -0
- package/.workflow/state/deploy-routes.json.template +4 -0
- package/.workflow/state/strike-tracker.json.template +3 -0
- package/lib/workspace.js +42 -11
- package/package.json +1 -1
- package/scripts/flow-config-defaults.js +56 -0
- package/scripts/flow-deploy-gate.js +335 -0
- package/scripts/flow-deploy-history.js +270 -0
- package/scripts/flow-hook-status.js +7 -1
- package/scripts/flow-mcp-capabilities.js +617 -0
- package/scripts/hooks/core/bugfix-scope-gate.js +427 -0
- package/scripts/hooks/core/deploy-gate.js +655 -0
- package/scripts/hooks/core/git-safety-gate.js +358 -0
- package/scripts/hooks/core/index.js +62 -0
- package/scripts/hooks/core/scope-mutation-gate.js +257 -0
- package/scripts/hooks/core/strike-gate.js +380 -0
- package/scripts/hooks/core/task-completed.js +101 -2
- package/scripts/hooks/entry/claude-code/post-tool-use.js +33 -0
- package/scripts/hooks/entry/claude-code/pre-tool-use.js +137 -0
- package/scripts/postinstall.js +15 -4
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow - Deploy History & Revert-First Protocol
|
|
5
|
+
*
|
|
6
|
+
* Tracks deploy history and implements the revert-first protocol
|
|
7
|
+
* for production crashes. Part of Mechanical Enforcement Gates v3.0.
|
|
8
|
+
*
|
|
9
|
+
* Commands:
|
|
10
|
+
* flow deploy-history add <commit> [env] — Record a deploy
|
|
11
|
+
* flow deploy-history show — Show deploy history
|
|
12
|
+
* flow deploy-history last-good — Show last known-good deploy
|
|
13
|
+
* flow deploy-history detect <text> — Detect production crash keywords
|
|
14
|
+
*
|
|
15
|
+
* The revert-first protocol is a WORKFLOW MODIFICATION, not a blocking hook.
|
|
16
|
+
* When a production crash is detected:
|
|
17
|
+
* 1. Present the revert option with last-good commit
|
|
18
|
+
* 2. If forward-fix chosen, reduce strike threshold
|
|
19
|
+
* 3. Track the decision in state
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
const path = require('node:path');
|
|
25
|
+
const { getConfig, PATHS, safeJsonParse, writeJson } = require('./flow-utils');
|
|
26
|
+
const { recordDeploy, getLastGoodDeploy, DEPLOY_HISTORY_PATH } = require('./hooks/core/deploy-gate');
|
|
27
|
+
|
|
28
|
+
// ============================================================
|
|
29
|
+
// Production Crash Detection
|
|
30
|
+
// ============================================================
|
|
31
|
+
|
|
32
|
+
/** Default keywords that suggest a production crash */
|
|
33
|
+
const DEFAULT_CRASH_KEYWORDS = [
|
|
34
|
+
'production', 'crash', 'down', 'outage',
|
|
35
|
+
'500 errors', "users can't", 'site is broken',
|
|
36
|
+
'live issue', 'prod is broken', 'prod down',
|
|
37
|
+
'users are seeing', 'in production', 'affecting users',
|
|
38
|
+
'critical bug', 'service down', 'api down',
|
|
39
|
+
'white screen in prod', 'deployment broke'
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Check if revert-first protocol is enabled
|
|
44
|
+
* @param {Object} [config]
|
|
45
|
+
* @returns {boolean}
|
|
46
|
+
*/
|
|
47
|
+
function isRevertFirstEnabled(config) {
|
|
48
|
+
if (!config) config = getConfig();
|
|
49
|
+
return config.enforcement?.revertFirst?.enabled === true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get revert-first configuration
|
|
54
|
+
* @param {Object} [config]
|
|
55
|
+
* @returns {Object}
|
|
56
|
+
*/
|
|
57
|
+
function getRevertFirstConfig(config) {
|
|
58
|
+
if (!config) config = getConfig();
|
|
59
|
+
const gate = config.enforcement?.revertFirst ?? {};
|
|
60
|
+
return {
|
|
61
|
+
enabled: gate.enabled === true,
|
|
62
|
+
keywords: gate.keywords ?? DEFAULT_CRASH_KEYWORDS,
|
|
63
|
+
deployHistoryRetention: gate.deployHistoryRetention ?? 50,
|
|
64
|
+
oldDeployWarningDays: gate.oldDeployWarningDays ?? 7
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Detect if a bug description suggests a production crash.
|
|
70
|
+
* @param {string} description - Bug report text
|
|
71
|
+
* @param {Object} [config]
|
|
72
|
+
* @returns {{ isProductionCrash: boolean, matchedKeywords: string[], confidence: 'high'|'medium'|'low' }}
|
|
73
|
+
*/
|
|
74
|
+
function detectProductionCrash(description, config) {
|
|
75
|
+
const revertConfig = getRevertFirstConfig(config);
|
|
76
|
+
if (!description) {
|
|
77
|
+
return { isProductionCrash: false, matchedKeywords: [], confidence: 'low' };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const lower = description.toLowerCase();
|
|
81
|
+
const matched = revertConfig.keywords.filter(kw => lower.includes(kw.toLowerCase()));
|
|
82
|
+
|
|
83
|
+
let confidence = 'low';
|
|
84
|
+
if (matched.length >= 3) confidence = 'high';
|
|
85
|
+
else if (matched.length >= 1) confidence = 'medium';
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
isProductionCrash: matched.length >= 1,
|
|
89
|
+
matchedKeywords: matched,
|
|
90
|
+
confidence
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Generate the revert-first recommendation message.
|
|
96
|
+
* Called by /wogi-bug when production crash is confirmed.
|
|
97
|
+
* @param {Object} [options]
|
|
98
|
+
* @param {boolean} [options.hasDeployHistory] - Whether deploy history exists
|
|
99
|
+
* @returns {string} Formatted recommendation message
|
|
100
|
+
*/
|
|
101
|
+
function generateRevertRecommendation(options) {
|
|
102
|
+
const lastDeploy = getLastGoodDeploy();
|
|
103
|
+
const revertConfig = getRevertFirstConfig();
|
|
104
|
+
|
|
105
|
+
if (!lastDeploy.found) {
|
|
106
|
+
return `━━━ REVERT-FIRST PROTOCOL ━━━
|
|
107
|
+
|
|
108
|
+
Production crash detected. No deploy history is tracked.
|
|
109
|
+
|
|
110
|
+
If you know the last good commit, provide it and I'll create a revert.
|
|
111
|
+
Otherwise, check your deployment platform for the last successful deploy hash.
|
|
112
|
+
|
|
113
|
+
Options:
|
|
114
|
+
[1] Provide a commit hash to revert to
|
|
115
|
+
[2] Forward-fix (reduced strike threshold — escalation after ${revertConfig.deployHistoryRetention} failures)
|
|
116
|
+
|
|
117
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const deploy = lastDeploy.deploy;
|
|
121
|
+
const deployAge = Math.floor((Date.now() - new Date(deploy.timestamp).getTime()) / (1000 * 60 * 60 * 24));
|
|
122
|
+
const isOld = deployAge > revertConfig.oldDeployWarningDays;
|
|
123
|
+
|
|
124
|
+
let warnings = '';
|
|
125
|
+
if (isOld) {
|
|
126
|
+
warnings += `\n⚠️ Last deploy was ${deployAge} days ago. Reverting may remove recent features.`;
|
|
127
|
+
}
|
|
128
|
+
warnings += '\n⚠️ If the issue involves database migrations or data changes, revert may not help.';
|
|
129
|
+
|
|
130
|
+
return `━━━ REVERT-FIRST PROTOCOL ━━━
|
|
131
|
+
|
|
132
|
+
Production crash detected.
|
|
133
|
+
|
|
134
|
+
Last successful deploy:
|
|
135
|
+
Commit: ${deploy.commitHash}
|
|
136
|
+
Date: ${deploy.timestamp}
|
|
137
|
+
Env: ${deploy.environment}
|
|
138
|
+
|
|
139
|
+
RECOMMENDED: Revert to ${deploy.commitHash.slice(0, 8)} to restore service immediately,
|
|
140
|
+
then forward-fix on a branch.
|
|
141
|
+
${warnings}
|
|
142
|
+
|
|
143
|
+
Options:
|
|
144
|
+
[1] Revert — \`git revert ${deploy.commitHash.slice(0, 8)}..HEAD\` (restores service now)
|
|
145
|
+
[2] Forward-fix (strike threshold reduced — escalation after 2 failures)
|
|
146
|
+
|
|
147
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Record the production crash decision (revert or forward-fix).
|
|
152
|
+
* @param {string} taskId
|
|
153
|
+
* @param {'revert'|'forward-fix'} decision
|
|
154
|
+
* @param {string} [commitHash] - Revert target commit (if reverting)
|
|
155
|
+
*/
|
|
156
|
+
function recordCrashDecision(taskId, decision, commitHash) {
|
|
157
|
+
const statePath = path.join(PATHS.state, 'crash-decisions.json');
|
|
158
|
+
const decisions = safeJsonParse(statePath, { decisions: [] });
|
|
159
|
+
|
|
160
|
+
decisions.decisions.unshift({
|
|
161
|
+
taskId,
|
|
162
|
+
decision,
|
|
163
|
+
commitHash: commitHash ?? null,
|
|
164
|
+
timestamp: new Date().toISOString()
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Keep last 20
|
|
168
|
+
if (decisions.decisions.length > 20) {
|
|
169
|
+
decisions.decisions = decisions.decisions.slice(0, 20);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
writeJson(statePath, decisions);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ============================================================
|
|
176
|
+
// CLI Commands
|
|
177
|
+
// ============================================================
|
|
178
|
+
|
|
179
|
+
function cmdAdd(commitHash, environment) {
|
|
180
|
+
if (!commitHash) {
|
|
181
|
+
console.error('Usage: flow deploy-history add <commit-hash> [environment]');
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
recordDeploy({
|
|
185
|
+
commitHash,
|
|
186
|
+
environment: environment || 'production'
|
|
187
|
+
});
|
|
188
|
+
console.log(`✓ Recorded deploy: ${commitHash.slice(0, 8)} (${environment || 'production'})`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function cmdShow() {
|
|
192
|
+
const history = safeJsonParse(DEPLOY_HISTORY_PATH, { deploys: [] });
|
|
193
|
+
console.log('━━━ Deploy History ━━━\n');
|
|
194
|
+
if (history.deploys.length === 0) {
|
|
195
|
+
console.log('No deploy history recorded.');
|
|
196
|
+
console.log('Record deploys with: flow deploy-history add <commit-hash> [environment]');
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
for (const d of history.deploys.slice(0, 15)) {
|
|
200
|
+
const age = Math.floor((Date.now() - new Date(d.timestamp).getTime()) / (1000 * 60 * 60));
|
|
201
|
+
const ageStr = age < 24 ? `${age}h ago` : `${Math.floor(age / 24)}d ago`;
|
|
202
|
+
console.log(` ${d.commitHash.slice(0, 8)} | ${d.environment.padEnd(12)} | ${d.timestamp} (${ageStr})`);
|
|
203
|
+
}
|
|
204
|
+
console.log(`\nTotal: ${history.deploys.length} deploys`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function cmdLastGood() {
|
|
208
|
+
const last = getLastGoodDeploy();
|
|
209
|
+
if (!last.found) {
|
|
210
|
+
console.log('No deploy history. Record with: flow deploy-history add <hash>');
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
console.log(`Last known-good deploy:`);
|
|
214
|
+
console.log(` Commit: ${last.deploy.commitHash}`);
|
|
215
|
+
console.log(` Date: ${last.deploy.timestamp}`);
|
|
216
|
+
console.log(` Env: ${last.deploy.environment}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function cmdDetect(text) {
|
|
220
|
+
if (!text) {
|
|
221
|
+
console.error('Usage: flow deploy-history detect "<bug description>"');
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
const result = detectProductionCrash(text);
|
|
225
|
+
console.log(JSON.stringify(result, null, 2));
|
|
226
|
+
if (result.isProductionCrash) {
|
|
227
|
+
console.log('\n' + generateRevertRecommendation());
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ============================================================
|
|
232
|
+
// CLI Entrypoint
|
|
233
|
+
// ============================================================
|
|
234
|
+
|
|
235
|
+
// CLI entrypoint (only when run directly)
|
|
236
|
+
if (require.main === module) {
|
|
237
|
+
const args = process.argv.slice(2);
|
|
238
|
+
const command = args[0];
|
|
239
|
+
|
|
240
|
+
switch (command) {
|
|
241
|
+
case 'add':
|
|
242
|
+
cmdAdd(args[1], args[2]);
|
|
243
|
+
break;
|
|
244
|
+
case 'show':
|
|
245
|
+
cmdShow();
|
|
246
|
+
break;
|
|
247
|
+
case 'last-good':
|
|
248
|
+
cmdLastGood();
|
|
249
|
+
break;
|
|
250
|
+
case 'detect':
|
|
251
|
+
cmdDetect(args.slice(1).join(' '));
|
|
252
|
+
break;
|
|
253
|
+
default:
|
|
254
|
+
console.log('Usage: flow deploy-history <add|show|last-good|detect>');
|
|
255
|
+
if (!command) process.exit(1);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ============================================================
|
|
260
|
+
// Exports (for programmatic use)
|
|
261
|
+
// ============================================================
|
|
262
|
+
|
|
263
|
+
module.exports = {
|
|
264
|
+
isRevertFirstEnabled,
|
|
265
|
+
getRevertFirstConfig,
|
|
266
|
+
detectProductionCrash,
|
|
267
|
+
generateRevertRecommendation,
|
|
268
|
+
recordCrashDecision,
|
|
269
|
+
recordDeploy
|
|
270
|
+
};
|
|
@@ -99,7 +99,13 @@ function buildEnforcementFromConfig(config) {
|
|
|
99
99
|
routingGate: config.enforcement?.routingGate?.enabled === true,
|
|
100
100
|
commitLogGate: config.enforcement?.commitLogGate?.enabled === true,
|
|
101
101
|
todoWriteGate: config.enforcement?.todoWriteGate?.enabled === true,
|
|
102
|
-
loopEnforcement: config.enforcement?.loopEnforcement?.enabled === true
|
|
102
|
+
loopEnforcement: config.enforcement?.loopEnforcement?.enabled === true,
|
|
103
|
+
// F5: Include v3.0 enforcement gates in hook status
|
|
104
|
+
deployGate: config.enforcement?.deployGate?.enabled === true,
|
|
105
|
+
strikeEscalation: config.enforcement?.strikeEscalation?.enabled !== false,
|
|
106
|
+
bugfixScope: config.enforcement?.bugfixScope?.enabled !== false,
|
|
107
|
+
scopeMutation: config.enforcement?.scopeMutation?.enabled !== false,
|
|
108
|
+
gitSafety: config.enforcement?.gitSafety?.enabled !== false
|
|
103
109
|
},
|
|
104
110
|
componentReuse: config.componentReuse?.enabled === true,
|
|
105
111
|
// Correct path: hooks.rules.phaseGate.enabled (matches phase-gate.js:84)
|