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.
@@ -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)