@boshu2/vibe-check 1.6.1 → 1.7.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/.agents/bundles/automatic-learning-cadence-plan-2025-12-02.md +1297 -0
- package/.agents/bundles/automatic-learning-cadence-research-2025-12-02.md +481 -0
- package/.agents/bundles/dashboard-data-quality-plan.md +458 -0
- package/.agents/bundles/rating-scoring-alignment-plan.md +427 -0
- package/.agents/bundles/rpi-session-capture-plan-2025-12-02.md +693 -0
- package/.agents/bundles/rpi-session-capture-research-2025-12-02.md +433 -0
- package/.agents/bundles/session-integration-plan-2025-12-02.md +144 -0
- package/CHANGELOG.md +17 -0
- package/CLAUDE.md +74 -2
- package/Makefile +173 -0
- package/README.md +35 -2
- package/claude-progress.json +34 -5
- package/claude-progress.txt +66 -0
- package/dashboard/app.js +699 -66
- package/dashboard/chart.min.js +20 -0
- package/dashboard/dashboard-data.js +764 -0
- package/dashboard/dashboard-data.json +182 -71
- package/dashboard/index.html +139 -14
- package/dashboard/styles.css +579 -4
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/analyze.d.ts.map +1 -1
- package/dist/commands/analyze.js +38 -2
- package/dist/commands/analyze.js.map +1 -1
- package/dist/commands/dashboard.js +4 -1
- package/dist/commands/dashboard.js.map +1 -1
- package/dist/commands/index.d.ts +1 -1
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +3 -3
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/learn.d.ts +3 -0
- package/dist/commands/learn.d.ts.map +1 -0
- package/dist/commands/learn.js +161 -0
- package/dist/commands/learn.js.map +1 -0
- package/dist/commands/lesson.d.ts +8 -0
- package/dist/commands/lesson.d.ts.map +1 -0
- package/dist/commands/lesson.js +206 -0
- package/dist/commands/lesson.js.map +1 -0
- package/dist/commands/profile.d.ts.map +1 -1
- package/dist/commands/profile.js +3 -202
- package/dist/commands/profile.js.map +1 -1
- package/dist/commands/session.d.ts +51 -0
- package/dist/commands/session.d.ts.map +1 -0
- package/dist/commands/session.js +561 -0
- package/dist/commands/session.js.map +1 -0
- package/dist/gamification/index.d.ts +1 -3
- package/dist/gamification/index.d.ts.map +1 -1
- package/dist/gamification/index.js +2 -5
- package/dist/gamification/index.js.map +1 -1
- package/dist/gamification/pattern-memory.d.ts +1 -1
- package/dist/gamification/pattern-memory.d.ts.map +1 -1
- package/dist/gamification/pattern-memory.js.map +1 -1
- package/dist/gamification/profile.d.ts +2 -2
- package/dist/gamification/profile.d.ts.map +1 -1
- package/dist/gamification/profile.js +2 -15
- package/dist/gamification/profile.js.map +1 -1
- package/dist/gamification/types.d.ts +8 -2
- package/dist/gamification/types.d.ts.map +1 -1
- package/dist/gamification/types.js.map +1 -1
- package/dist/insights/index.d.ts.map +1 -1
- package/dist/insights/index.js +16 -4
- package/dist/insights/index.js.map +1 -1
- package/dist/insights/types.d.ts +14 -0
- package/dist/insights/types.d.ts.map +1 -1
- package/dist/learning/cadence.d.ts +15 -0
- package/dist/learning/cadence.d.ts.map +1 -0
- package/dist/learning/cadence.js +130 -0
- package/dist/learning/cadence.js.map +1 -0
- package/dist/learning/index.d.ts +19 -0
- package/dist/learning/index.d.ts.map +1 -0
- package/dist/learning/index.js +35 -0
- package/dist/learning/index.js.map +1 -0
- package/dist/learning/lessons-storage.d.ts +48 -0
- package/dist/learning/lessons-storage.d.ts.map +1 -0
- package/dist/learning/lessons-storage.js +266 -0
- package/dist/learning/lessons-storage.js.map +1 -0
- package/dist/learning/lessons-types.d.ts +83 -0
- package/dist/learning/lessons-types.d.ts.map +1 -0
- package/dist/learning/lessons-types.js +15 -0
- package/dist/learning/lessons-types.js.map +1 -0
- package/dist/learning/nudges.d.ts +20 -0
- package/dist/learning/nudges.d.ts.map +1 -0
- package/dist/learning/nudges.js +68 -0
- package/dist/learning/nudges.js.map +1 -0
- package/dist/learning/retrospective.d.ts +27 -0
- package/dist/learning/retrospective.d.ts.map +1 -0
- package/dist/learning/retrospective.js +184 -0
- package/dist/learning/retrospective.js.map +1 -0
- package/dist/learning/storage.d.ts +44 -0
- package/dist/learning/storage.d.ts.map +1 -0
- package/dist/learning/storage.js +194 -0
- package/dist/learning/storage.js.map +1 -0
- package/dist/learning/surfacing.d.ts +36 -0
- package/dist/learning/surfacing.d.ts.map +1 -0
- package/dist/learning/surfacing.js +255 -0
- package/dist/learning/surfacing.js.map +1 -0
- package/dist/learning/synthesis.d.ts +17 -0
- package/dist/learning/synthesis.d.ts.map +1 -0
- package/dist/learning/synthesis.js +293 -0
- package/dist/learning/synthesis.js.map +1 -0
- package/dist/learning/types.d.ts +60 -0
- package/dist/learning/types.d.ts.map +1 -0
- package/dist/learning/types.js +17 -0
- package/dist/learning/types.js.map +1 -0
- package/docs/METRICS.md +528 -0
- package/feature-list.json +21 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1297 @@
|
|
|
1
|
+
# Automatic Learning & Retrospective Cadence - Implementation Plan
|
|
2
|
+
|
|
3
|
+
**Type:** Plan
|
|
4
|
+
**Created:** 2025-12-02
|
|
5
|
+
**Depends On:** `automatic-learning-cadence-research-2025-12-02.md`
|
|
6
|
+
**Loop:** Middle (bridges research to implementation)
|
|
7
|
+
**Tags:** learning-loop, retrospective, cadence, nudges, automation
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
Implement automatic learning cadence for vibe-check, transforming passive data collection into active system improvement. This plan follows the **Hybrid A+B approach** from research:
|
|
14
|
+
- **Automatic triggers** in `recordSession()` for lightweight operations
|
|
15
|
+
- **Explicit `learn` command** for retrospectives and heavy operations
|
|
16
|
+
- **Nudge display** in CLI after analyze
|
|
17
|
+
|
|
18
|
+
**Scope:** 6 new files, 4 modified files, ~800 lines of new code
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Approach Selected
|
|
23
|
+
|
|
24
|
+
**From research:** Hybrid A+B - Hook lightweight cadence checks into `recordSession()`, provide explicit `learn` command for retrospectives.
|
|
25
|
+
|
|
26
|
+
**Rationale:**
|
|
27
|
+
- Automatic triggers catch users at natural breakpoints (post-session)
|
|
28
|
+
- Manual command respects user autonomy for retrospectives
|
|
29
|
+
- Low latency impact (cadence checks are O(1))
|
|
30
|
+
- Nudges displayed only when actionable
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## PDC Strategy
|
|
35
|
+
|
|
36
|
+
### Prevent
|
|
37
|
+
- [x] Read all existing code (completed in research)
|
|
38
|
+
- [ ] Run `npm test` before starting
|
|
39
|
+
- [ ] Commit after each file creation
|
|
40
|
+
|
|
41
|
+
### Detect
|
|
42
|
+
- [ ] `npm run build` after each TypeScript file
|
|
43
|
+
- [ ] Test nudge display manually after integration
|
|
44
|
+
- [ ] Verify learning state persistence
|
|
45
|
+
|
|
46
|
+
### Correct
|
|
47
|
+
- [ ] Each module is independent - can revert selectively
|
|
48
|
+
- [ ] Learning state can be deleted to reset
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Files to Create
|
|
53
|
+
|
|
54
|
+
### 1. `src/learning/types.ts`
|
|
55
|
+
|
|
56
|
+
**Purpose:** Type definitions for learning system
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
/**
|
|
60
|
+
* Learning System Types
|
|
61
|
+
*
|
|
62
|
+
* Types for the automatic learning cadence system including:
|
|
63
|
+
* - Learning state persistence
|
|
64
|
+
* - Nudge queue management
|
|
65
|
+
* - Retrospective summaries
|
|
66
|
+
*/
|
|
67
|
+
|
|
68
|
+
export type NudgeType = 'pattern' | 'intervention' | 'retro' | 'achievement' | 'learning';
|
|
69
|
+
|
|
70
|
+
export interface Nudge {
|
|
71
|
+
id: string;
|
|
72
|
+
type: NudgeType;
|
|
73
|
+
icon: string;
|
|
74
|
+
title: string;
|
|
75
|
+
message: string;
|
|
76
|
+
action?: string;
|
|
77
|
+
priority: number; // 1-10, higher = more important
|
|
78
|
+
createdAt: string; // ISO datetime
|
|
79
|
+
expiresAt?: string; // ISO datetime, null = never expires
|
|
80
|
+
dismissed?: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface RetroSummary {
|
|
84
|
+
date: string; // ISO date
|
|
85
|
+
periodStart: string; // ISO date
|
|
86
|
+
periodEnd: string; // ISO date
|
|
87
|
+
sessionsCount: number;
|
|
88
|
+
commitsCount: number;
|
|
89
|
+
activeMinutes: number;
|
|
90
|
+
topPattern?: string;
|
|
91
|
+
topIntervention?: string;
|
|
92
|
+
keyInsight: string;
|
|
93
|
+
trustPassRateChange?: number;
|
|
94
|
+
spiralRateChange?: number;
|
|
95
|
+
actionTaken?: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface LearningState {
|
|
99
|
+
version: string;
|
|
100
|
+
|
|
101
|
+
// Cadence tracking
|
|
102
|
+
lastDailyCheck: string; // ISO date (YYYY-MM-DD)
|
|
103
|
+
lastWeeklyRetro: string; // ISO date
|
|
104
|
+
lastMonthlyReview: string; // ISO date
|
|
105
|
+
|
|
106
|
+
// Nudge queue (FIFO, max 5)
|
|
107
|
+
pendingNudges: Nudge[];
|
|
108
|
+
|
|
109
|
+
// Retrospective state
|
|
110
|
+
retroDue: boolean;
|
|
111
|
+
retroDueReason: string;
|
|
112
|
+
lastRetroSummary?: RetroSummary;
|
|
113
|
+
|
|
114
|
+
// Statistics
|
|
115
|
+
totalRetrosCompleted: number;
|
|
116
|
+
nudgesDisplayed: number;
|
|
117
|
+
nudgesDismissed: number;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface CadenceResult {
|
|
121
|
+
nudges: Nudge[];
|
|
122
|
+
retroDue: boolean;
|
|
123
|
+
retroDueReason?: string;
|
|
124
|
+
learningState: LearningState;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export const NUDGE_TTL_DAYS = 7;
|
|
128
|
+
export const MAX_PENDING_NUDGES = 5;
|
|
129
|
+
export const RETRO_CADENCE_DAYS = 7;
|
|
130
|
+
export const PATTERN_REPEAT_THRESHOLD = 3;
|
|
131
|
+
export const PATTERN_WINDOW_DAYS = 7;
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**Validation:** `npm run build`
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
### 2. `src/learning/storage.ts`
|
|
139
|
+
|
|
140
|
+
**Purpose:** Persist and load learning state
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
/**
|
|
144
|
+
* Learning State Storage
|
|
145
|
+
*
|
|
146
|
+
* Manages persistence of learning state to ~/.vibe-check/learning-state.json
|
|
147
|
+
* This is global (not per-repo) to track cross-repo patterns.
|
|
148
|
+
*/
|
|
149
|
+
|
|
150
|
+
import * as fs from 'fs';
|
|
151
|
+
import * as path from 'path';
|
|
152
|
+
import * as os from 'os';
|
|
153
|
+
import {
|
|
154
|
+
LearningState,
|
|
155
|
+
Nudge,
|
|
156
|
+
RetroSummary,
|
|
157
|
+
NUDGE_TTL_DAYS,
|
|
158
|
+
MAX_PENDING_NUDGES,
|
|
159
|
+
} from './types';
|
|
160
|
+
|
|
161
|
+
const LEARNING_DIR = '.vibe-check';
|
|
162
|
+
const LEARNING_FILE = 'learning-state.json';
|
|
163
|
+
const LEARNING_STATE_VERSION = '1.0.0';
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Get learning state file path (global)
|
|
167
|
+
*/
|
|
168
|
+
export function getLearningStatePath(): string {
|
|
169
|
+
return path.join(os.homedir(), LEARNING_DIR, LEARNING_FILE);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Create initial learning state
|
|
174
|
+
*/
|
|
175
|
+
export function createInitialLearningState(): LearningState {
|
|
176
|
+
const today = new Date().toISOString().split('T')[0];
|
|
177
|
+
return {
|
|
178
|
+
version: LEARNING_STATE_VERSION,
|
|
179
|
+
lastDailyCheck: '',
|
|
180
|
+
lastWeeklyRetro: today,
|
|
181
|
+
lastMonthlyReview: today,
|
|
182
|
+
pendingNudges: [],
|
|
183
|
+
retroDue: false,
|
|
184
|
+
retroDueReason: '',
|
|
185
|
+
totalRetrosCompleted: 0,
|
|
186
|
+
nudgesDisplayed: 0,
|
|
187
|
+
nudgesDismissed: 0,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Load learning state from disk
|
|
193
|
+
*/
|
|
194
|
+
export function loadLearningState(): LearningState {
|
|
195
|
+
const filePath = getLearningStatePath();
|
|
196
|
+
|
|
197
|
+
if (fs.existsSync(filePath)) {
|
|
198
|
+
try {
|
|
199
|
+
const data = fs.readFileSync(filePath, 'utf-8');
|
|
200
|
+
const state = JSON.parse(data) as LearningState;
|
|
201
|
+
return migrateLearningState(state);
|
|
202
|
+
} catch {
|
|
203
|
+
return createInitialLearningState();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return createInitialLearningState();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Save learning state to disk
|
|
212
|
+
*/
|
|
213
|
+
export function saveLearningState(state: LearningState): void {
|
|
214
|
+
const dirPath = path.join(os.homedir(), LEARNING_DIR);
|
|
215
|
+
const filePath = getLearningStatePath();
|
|
216
|
+
|
|
217
|
+
if (!fs.existsSync(dirPath)) {
|
|
218
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
fs.writeFileSync(filePath, JSON.stringify(state, null, 2));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Add a nudge to the queue
|
|
226
|
+
*/
|
|
227
|
+
export function addNudge(state: LearningState, nudge: Omit<Nudge, 'id' | 'createdAt'>): LearningState {
|
|
228
|
+
const now = new Date();
|
|
229
|
+
const newNudge: Nudge = {
|
|
230
|
+
...nudge,
|
|
231
|
+
id: `nudge-${now.getTime()}`,
|
|
232
|
+
createdAt: now.toISOString(),
|
|
233
|
+
expiresAt: new Date(now.getTime() + NUDGE_TTL_DAYS * 24 * 60 * 60 * 1000).toISOString(),
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
// Add to queue, keeping max size
|
|
237
|
+
const updatedNudges = [...state.pendingNudges, newNudge]
|
|
238
|
+
.filter(n => !n.dismissed)
|
|
239
|
+
.filter(n => !n.expiresAt || new Date(n.expiresAt) > now)
|
|
240
|
+
.sort((a, b) => b.priority - a.priority)
|
|
241
|
+
.slice(0, MAX_PENDING_NUDGES);
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
...state,
|
|
245
|
+
pendingNudges: updatedNudges,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Get pending nudges (not dismissed, not expired)
|
|
251
|
+
*/
|
|
252
|
+
export function getPendingNudges(state: LearningState): Nudge[] {
|
|
253
|
+
const now = new Date();
|
|
254
|
+
return state.pendingNudges
|
|
255
|
+
.filter(n => !n.dismissed)
|
|
256
|
+
.filter(n => !n.expiresAt || new Date(n.expiresAt) > now)
|
|
257
|
+
.sort((a, b) => b.priority - a.priority);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Dismiss a nudge by ID
|
|
262
|
+
*/
|
|
263
|
+
export function dismissNudge(state: LearningState, nudgeId: string): LearningState {
|
|
264
|
+
return {
|
|
265
|
+
...state,
|
|
266
|
+
pendingNudges: state.pendingNudges.map(n =>
|
|
267
|
+
n.id === nudgeId ? { ...n, dismissed: true } : n
|
|
268
|
+
),
|
|
269
|
+
nudgesDismissed: state.nudgesDismissed + 1,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Mark nudges as displayed
|
|
275
|
+
*/
|
|
276
|
+
export function markNudgesDisplayed(state: LearningState, count: number): LearningState {
|
|
277
|
+
return {
|
|
278
|
+
...state,
|
|
279
|
+
nudgesDisplayed: state.nudgesDisplayed + count,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Record retrospective completion
|
|
285
|
+
*/
|
|
286
|
+
export function recordRetroCompletion(
|
|
287
|
+
state: LearningState,
|
|
288
|
+
summary: RetroSummary
|
|
289
|
+
): LearningState {
|
|
290
|
+
const today = new Date().toISOString().split('T')[0];
|
|
291
|
+
return {
|
|
292
|
+
...state,
|
|
293
|
+
lastWeeklyRetro: today,
|
|
294
|
+
retroDue: false,
|
|
295
|
+
retroDueReason: '',
|
|
296
|
+
lastRetroSummary: summary,
|
|
297
|
+
totalRetrosCompleted: state.totalRetrosCompleted + 1,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Migrate old learning state versions
|
|
303
|
+
*/
|
|
304
|
+
function migrateLearningState(state: LearningState): LearningState {
|
|
305
|
+
if (!state.version) {
|
|
306
|
+
state.version = LEARNING_STATE_VERSION;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Add any missing fields
|
|
310
|
+
if (state.totalRetrosCompleted === undefined) {
|
|
311
|
+
state.totalRetrosCompleted = 0;
|
|
312
|
+
}
|
|
313
|
+
if (state.nudgesDisplayed === undefined) {
|
|
314
|
+
state.nudgesDisplayed = 0;
|
|
315
|
+
}
|
|
316
|
+
if (state.nudgesDismissed === undefined) {
|
|
317
|
+
state.nudgesDismissed = 0;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return state;
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
**Validation:** `npm run build`
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
### 3. `src/learning/cadence.ts`
|
|
329
|
+
|
|
330
|
+
**Purpose:** Core cadence scheduler - checks triggers and generates nudges
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
/**
|
|
334
|
+
* Learning Cadence Scheduler
|
|
335
|
+
*
|
|
336
|
+
* Checks time-based and event-based triggers to generate nudges
|
|
337
|
+
* and determine when retrospectives are due.
|
|
338
|
+
*/
|
|
339
|
+
|
|
340
|
+
import {
|
|
341
|
+
LearningState,
|
|
342
|
+
CadenceResult,
|
|
343
|
+
Nudge,
|
|
344
|
+
RETRO_CADENCE_DAYS,
|
|
345
|
+
PATTERN_REPEAT_THRESHOLD,
|
|
346
|
+
PATTERN_WINDOW_DAYS,
|
|
347
|
+
} from './types';
|
|
348
|
+
import { loadLearningState, saveLearningState, addNudge } from './storage';
|
|
349
|
+
import { PatternMemory, InterventionMemory } from '../gamification/types';
|
|
350
|
+
import { getPatternDisplayName, getPatternAdvice } from '../gamification/pattern-memory';
|
|
351
|
+
import { getRecommendedIntervention, getInterventionDisplayName, getInterventionIcon } from '../gamification/intervention-memory';
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Run learning cadence check after a session
|
|
355
|
+
*
|
|
356
|
+
* Called from recordSession() to check all triggers and generate nudges.
|
|
357
|
+
*/
|
|
358
|
+
export function runLearningCadence(
|
|
359
|
+
patternMemory: PatternMemory | undefined,
|
|
360
|
+
interventionMemory: InterventionMemory | undefined,
|
|
361
|
+
streakCurrent: number,
|
|
362
|
+
xpToNextLevel: number,
|
|
363
|
+
totalXp: number
|
|
364
|
+
): CadenceResult {
|
|
365
|
+
const state = loadLearningState();
|
|
366
|
+
const today = new Date().toISOString().split('T')[0];
|
|
367
|
+
let updatedState = { ...state };
|
|
368
|
+
|
|
369
|
+
const nudges: Nudge[] = [];
|
|
370
|
+
|
|
371
|
+
// 1. Check daily trigger (first session of day)
|
|
372
|
+
if (state.lastDailyCheck !== today) {
|
|
373
|
+
updatedState.lastDailyCheck = today;
|
|
374
|
+
// Could add daily summary nudge here if desired
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// 2. Check weekly retro trigger
|
|
378
|
+
const daysSinceRetro = getDaysSince(state.lastWeeklyRetro);
|
|
379
|
+
if (daysSinceRetro >= RETRO_CADENCE_DAYS) {
|
|
380
|
+
updatedState.retroDue = true;
|
|
381
|
+
updatedState.retroDueReason = `${daysSinceRetro} days since last retrospective`;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// 3. Check pattern repeat threshold
|
|
385
|
+
const repeatedPattern = getRepeatedPattern(patternMemory);
|
|
386
|
+
if (repeatedPattern) {
|
|
387
|
+
const displayName = getPatternDisplayName(repeatedPattern.pattern);
|
|
388
|
+
const advice = getPatternAdvice(repeatedPattern.pattern);
|
|
389
|
+
const intervention = getRecommendedIntervention(interventionMemory, repeatedPattern.pattern);
|
|
390
|
+
const interventionText = intervention
|
|
391
|
+
? `Your top intervention for this: ${getInterventionIcon(intervention)} ${getInterventionDisplayName(intervention)}`
|
|
392
|
+
: 'Try a tracer test to validate assumptions';
|
|
393
|
+
|
|
394
|
+
updatedState = addNudge(updatedState, {
|
|
395
|
+
type: 'pattern',
|
|
396
|
+
icon: '⚠️',
|
|
397
|
+
title: `${displayName} Pattern Detected`,
|
|
398
|
+
message: `${displayName} caused ${repeatedPattern.count} spirals this week (${repeatedPattern.totalMinutes} min)`,
|
|
399
|
+
action: interventionText,
|
|
400
|
+
priority: 8,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// 4. Check achievement proximity (within 20% of next level)
|
|
405
|
+
const xpProgress = xpToNextLevel > 0 ? (totalXp % xpToNextLevel) / xpToNextLevel : 0;
|
|
406
|
+
if (xpProgress >= 0.8) {
|
|
407
|
+
const xpNeeded = Math.round(xpToNextLevel * (1 - xpProgress));
|
|
408
|
+
updatedState = addNudge(updatedState, {
|
|
409
|
+
type: 'achievement',
|
|
410
|
+
icon: '📈',
|
|
411
|
+
title: 'Level Up Soon!',
|
|
412
|
+
message: `Only ${xpNeeded} XP to your next level`,
|
|
413
|
+
priority: 5,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// 5. Check streak milestone proximity
|
|
418
|
+
if (streakCurrent > 0 && streakCurrent % 7 === 6) {
|
|
419
|
+
updatedState = addNudge(updatedState, {
|
|
420
|
+
type: 'achievement',
|
|
421
|
+
icon: '🔥',
|
|
422
|
+
title: 'Streak Milestone Tomorrow!',
|
|
423
|
+
message: `One more day for a ${streakCurrent + 1}-day streak`,
|
|
424
|
+
priority: 6,
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// 6. Add retro nudge if due
|
|
429
|
+
if (updatedState.retroDue) {
|
|
430
|
+
updatedState = addNudge(updatedState, {
|
|
431
|
+
type: 'retro',
|
|
432
|
+
icon: '📅',
|
|
433
|
+
title: 'Weekly Retro Due',
|
|
434
|
+
message: updatedState.retroDueReason,
|
|
435
|
+
action: 'Run `vibe-check learn --retro` to review your week',
|
|
436
|
+
priority: 7,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Save updated state
|
|
441
|
+
saveLearningState(updatedState);
|
|
442
|
+
|
|
443
|
+
return {
|
|
444
|
+
nudges: updatedState.pendingNudges.filter(n => !n.dismissed),
|
|
445
|
+
retroDue: updatedState.retroDue,
|
|
446
|
+
retroDueReason: updatedState.retroDueReason,
|
|
447
|
+
learningState: updatedState,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Get days since a date string
|
|
453
|
+
*/
|
|
454
|
+
function getDaysSince(dateStr: string): number {
|
|
455
|
+
if (!dateStr) return RETRO_CADENCE_DAYS + 1; // Force retro if no date
|
|
456
|
+
const date = new Date(dateStr);
|
|
457
|
+
const now = new Date();
|
|
458
|
+
return Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24));
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Check if any pattern has repeated >= threshold times in the window
|
|
463
|
+
*/
|
|
464
|
+
function getRepeatedPattern(
|
|
465
|
+
patternMemory: PatternMemory | undefined
|
|
466
|
+
): { pattern: string; count: number; totalMinutes: number } | null {
|
|
467
|
+
if (!patternMemory || patternMemory.records.length === 0) {
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const cutoff = new Date();
|
|
472
|
+
cutoff.setDate(cutoff.getDate() - PATTERN_WINDOW_DAYS);
|
|
473
|
+
const cutoffStr = cutoff.toISOString().split('T')[0];
|
|
474
|
+
|
|
475
|
+
// Count patterns in the window
|
|
476
|
+
const recentRecords = patternMemory.records.filter(r => r.date >= cutoffStr);
|
|
477
|
+
const patternCounts = new Map<string, { count: number; minutes: number }>();
|
|
478
|
+
|
|
479
|
+
for (const record of recentRecords) {
|
|
480
|
+
const current = patternCounts.get(record.pattern) || { count: 0, minutes: 0 };
|
|
481
|
+
patternCounts.set(record.pattern, {
|
|
482
|
+
count: current.count + 1,
|
|
483
|
+
minutes: current.minutes + record.duration,
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Find first pattern exceeding threshold
|
|
488
|
+
for (const [pattern, data] of patternCounts) {
|
|
489
|
+
if (data.count >= PATTERN_REPEAT_THRESHOLD) {
|
|
490
|
+
return {
|
|
491
|
+
pattern,
|
|
492
|
+
count: data.count,
|
|
493
|
+
totalMinutes: data.minutes,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
**Validation:** `npm run build`
|
|
503
|
+
|
|
504
|
+
---
|
|
505
|
+
|
|
506
|
+
### 4. `src/learning/nudges.ts`
|
|
507
|
+
|
|
508
|
+
**Purpose:** Nudge display formatting for CLI output
|
|
509
|
+
|
|
510
|
+
```typescript
|
|
511
|
+
/**
|
|
512
|
+
* Nudge Display System
|
|
513
|
+
*
|
|
514
|
+
* Formats and displays nudges in CLI output.
|
|
515
|
+
*/
|
|
516
|
+
|
|
517
|
+
import chalk from 'chalk';
|
|
518
|
+
import { Nudge } from './types';
|
|
519
|
+
import { loadLearningState, saveLearningState, markNudgesDisplayed, getPendingNudges } from './storage';
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Format nudges for CLI display (after gamification section)
|
|
523
|
+
*/
|
|
524
|
+
export function formatNudgesForCli(maxDisplay: number = 2): string[] {
|
|
525
|
+
const state = loadLearningState();
|
|
526
|
+
const nudges = getPendingNudges(state);
|
|
527
|
+
|
|
528
|
+
if (nudges.length === 0) {
|
|
529
|
+
return [];
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const toDisplay = nudges.slice(0, maxDisplay);
|
|
533
|
+
const lines: string[] = [];
|
|
534
|
+
|
|
535
|
+
lines.push('');
|
|
536
|
+
|
|
537
|
+
for (const nudge of toDisplay) {
|
|
538
|
+
lines.push(formatSingleNudge(nudge));
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (nudges.length > maxDisplay) {
|
|
542
|
+
lines.push(chalk.gray(` ... and ${nudges.length - maxDisplay} more. Run \`vibe-check profile\` to see all.`));
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Mark as displayed
|
|
546
|
+
const updatedState = markNudgesDisplayed(state, toDisplay.length);
|
|
547
|
+
saveLearningState(updatedState);
|
|
548
|
+
|
|
549
|
+
return lines;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Format a single nudge for display
|
|
554
|
+
*/
|
|
555
|
+
function formatSingleNudge(nudge: Nudge): string {
|
|
556
|
+
const lines: string[] = [];
|
|
557
|
+
|
|
558
|
+
// Color based on type
|
|
559
|
+
const colorFn = nudge.type === 'pattern' ? chalk.yellow :
|
|
560
|
+
nudge.type === 'retro' ? chalk.cyan :
|
|
561
|
+
nudge.type === 'achievement' ? chalk.green :
|
|
562
|
+
chalk.white;
|
|
563
|
+
|
|
564
|
+
lines.push(colorFn(` ${nudge.icon} ${nudge.title}`));
|
|
565
|
+
lines.push(chalk.gray(` ${nudge.message}`));
|
|
566
|
+
|
|
567
|
+
if (nudge.action) {
|
|
568
|
+
lines.push(chalk.gray(` ${nudge.action}`));
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
return lines.join('\n');
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Get nudge summary for profile command
|
|
576
|
+
*/
|
|
577
|
+
export function getNudgeSummary(): {
|
|
578
|
+
pending: number;
|
|
579
|
+
displayed: number;
|
|
580
|
+
dismissed: number;
|
|
581
|
+
nudges: Nudge[];
|
|
582
|
+
} {
|
|
583
|
+
const state = loadLearningState();
|
|
584
|
+
const pending = getPendingNudges(state);
|
|
585
|
+
|
|
586
|
+
return {
|
|
587
|
+
pending: pending.length,
|
|
588
|
+
displayed: state.nudgesDisplayed,
|
|
589
|
+
dismissed: state.nudgesDismissed,
|
|
590
|
+
nudges: pending,
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
**Validation:** `npm run build`
|
|
596
|
+
|
|
597
|
+
---
|
|
598
|
+
|
|
599
|
+
### 5. `src/learning/retrospective.ts`
|
|
600
|
+
|
|
601
|
+
**Purpose:** Generate and display weekly retrospectives
|
|
602
|
+
|
|
603
|
+
```typescript
|
|
604
|
+
/**
|
|
605
|
+
* Retrospective System
|
|
606
|
+
*
|
|
607
|
+
* Generates weekly retrospective summaries from accumulated data.
|
|
608
|
+
*/
|
|
609
|
+
|
|
610
|
+
import chalk from 'chalk';
|
|
611
|
+
import { RetroSummary } from './types';
|
|
612
|
+
import { loadLearningState, saveLearningState, recordRetroCompletion } from './storage';
|
|
613
|
+
import { loadProfile, getRecentSessions } from '../gamification/profile';
|
|
614
|
+
import { formatPatternMemory } from '../gamification/pattern-memory';
|
|
615
|
+
import { formatInterventionMemory } from '../gamification/intervention-memory';
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Generate a weekly retrospective summary
|
|
619
|
+
*/
|
|
620
|
+
export function generateWeeklyRetro(): RetroSummary {
|
|
621
|
+
const profile = loadProfile();
|
|
622
|
+
const sessions = getRecentSessions(profile, 7);
|
|
623
|
+
|
|
624
|
+
const now = new Date();
|
|
625
|
+
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
626
|
+
|
|
627
|
+
// Calculate metrics
|
|
628
|
+
const totalCommits = sessions.reduce((sum, s) => sum + s.commits, 0);
|
|
629
|
+
const totalSpirals = sessions.reduce((sum, s) => sum + s.spirals, 0);
|
|
630
|
+
const avgScore = sessions.length > 0
|
|
631
|
+
? Math.round(sessions.reduce((sum, s) => sum + s.vibeScore, 0) / sessions.length)
|
|
632
|
+
: 0;
|
|
633
|
+
|
|
634
|
+
// Get pattern analysis
|
|
635
|
+
const patternData = formatPatternMemory(profile.patternMemory);
|
|
636
|
+
const topPattern = patternData.topPatterns[0]?.pattern;
|
|
637
|
+
|
|
638
|
+
// Get intervention analysis
|
|
639
|
+
const interventionData = formatInterventionMemory(profile.interventionMemory);
|
|
640
|
+
const topIntervention = interventionData.topInterventions[0]?.name;
|
|
641
|
+
|
|
642
|
+
// Calculate changes from previous week
|
|
643
|
+
const previousSessions = profile.sessions.slice(-14, -7);
|
|
644
|
+
let trustPassRateChange: number | undefined;
|
|
645
|
+
let spiralRateChange: number | undefined;
|
|
646
|
+
|
|
647
|
+
if (previousSessions.length > 0 && sessions.length > 0) {
|
|
648
|
+
const currentTrust = sessions.reduce((sum, s) =>
|
|
649
|
+
sum + (s.metrics?.trustPassRate || 0), 0) / sessions.length;
|
|
650
|
+
const prevTrust = previousSessions.reduce((sum, s) =>
|
|
651
|
+
sum + (s.metrics?.trustPassRate || 0), 0) / previousSessions.length;
|
|
652
|
+
trustPassRateChange = Math.round(currentTrust - prevTrust);
|
|
653
|
+
|
|
654
|
+
const currentSpiralRate = totalSpirals / sessions.length;
|
|
655
|
+
const prevSpiralRate = previousSessions.reduce((sum, s) => sum + s.spirals, 0) / previousSessions.length;
|
|
656
|
+
spiralRateChange = Math.round((prevSpiralRate - currentSpiralRate) / (prevSpiralRate || 1) * 100);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Generate key insight
|
|
660
|
+
let keyInsight = '';
|
|
661
|
+
if (totalSpirals === 0) {
|
|
662
|
+
keyInsight = 'Zero spirals this week - excellent flow state!';
|
|
663
|
+
} else if (topPattern && patternData.topPatterns[0]) {
|
|
664
|
+
const topPatternData = patternData.topPatterns[0];
|
|
665
|
+
keyInsight = `${topPatternData.displayName} is your main spiral trigger (${topPatternData.count} occurrences)`;
|
|
666
|
+
} else {
|
|
667
|
+
keyInsight = `${sessions.length} sessions completed with ${avgScore}% average score`;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
return {
|
|
671
|
+
date: now.toISOString().split('T')[0],
|
|
672
|
+
periodStart: weekAgo.toISOString().split('T')[0],
|
|
673
|
+
periodEnd: now.toISOString().split('T')[0],
|
|
674
|
+
sessionsCount: sessions.length,
|
|
675
|
+
commitsCount: totalCommits,
|
|
676
|
+
activeMinutes: sessions.length * 30, // Estimate
|
|
677
|
+
topPattern,
|
|
678
|
+
topIntervention,
|
|
679
|
+
keyInsight,
|
|
680
|
+
trustPassRateChange,
|
|
681
|
+
spiralRateChange,
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Display retrospective in terminal
|
|
687
|
+
*/
|
|
688
|
+
export function displayRetro(summary: RetroSummary): void {
|
|
689
|
+
const profile = loadProfile();
|
|
690
|
+
const patternData = formatPatternMemory(profile.patternMemory);
|
|
691
|
+
const interventionData = formatInterventionMemory(profile.interventionMemory);
|
|
692
|
+
|
|
693
|
+
console.log('');
|
|
694
|
+
console.log(chalk.bold.cyan('═'.repeat(64)));
|
|
695
|
+
console.log(chalk.bold.cyan(' WEEKLY RETROSPECTIVE'));
|
|
696
|
+
console.log(chalk.bold.cyan(` ${summary.periodStart} - ${summary.periodEnd}`));
|
|
697
|
+
console.log(chalk.bold.cyan('═'.repeat(64)));
|
|
698
|
+
console.log('');
|
|
699
|
+
|
|
700
|
+
// Sessions summary
|
|
701
|
+
console.log(chalk.bold.white(' SESSIONS'));
|
|
702
|
+
console.log(` ${summary.sessionsCount} sessions | ${summary.commitsCount} commits`);
|
|
703
|
+
console.log('');
|
|
704
|
+
|
|
705
|
+
// Top patterns
|
|
706
|
+
if (patternData.hasData && patternData.topPatterns.length > 0) {
|
|
707
|
+
console.log(chalk.bold.white(' TOP SPIRAL TRIGGERS'));
|
|
708
|
+
for (const pattern of patternData.topPatterns.slice(0, 3)) {
|
|
709
|
+
console.log(` ${pattern.displayName}: ${pattern.count} spirals (${pattern.totalMinutes} min)`);
|
|
710
|
+
console.log(chalk.gray(` ${pattern.advice}`));
|
|
711
|
+
}
|
|
712
|
+
console.log('');
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// What worked
|
|
716
|
+
if (interventionData.hasData && interventionData.topInterventions.length > 0) {
|
|
717
|
+
console.log(chalk.bold.white(' WHAT WORKED'));
|
|
718
|
+
for (const intervention of interventionData.topInterventions.slice(0, 3)) {
|
|
719
|
+
console.log(` ${intervention.icon} ${intervention.name}: ${intervention.count} times`);
|
|
720
|
+
}
|
|
721
|
+
console.log('');
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// Progress
|
|
725
|
+
console.log(chalk.bold.white(' PROGRESS'));
|
|
726
|
+
if (summary.trustPassRateChange !== undefined) {
|
|
727
|
+
const trustColor = summary.trustPassRateChange >= 0 ? chalk.green : chalk.yellow;
|
|
728
|
+
const trustSign = summary.trustPassRateChange >= 0 ? '+' : '';
|
|
729
|
+
console.log(` Trust Pass Rate: ${trustColor(`${trustSign}${summary.trustPassRateChange}%`)}`);
|
|
730
|
+
}
|
|
731
|
+
if (summary.spiralRateChange !== undefined) {
|
|
732
|
+
const spiralColor = summary.spiralRateChange >= 0 ? chalk.green : chalk.yellow;
|
|
733
|
+
const spiralSign = summary.spiralRateChange >= 0 ? '+' : '';
|
|
734
|
+
console.log(` Spiral Reduction: ${spiralColor(`${spiralSign}${summary.spiralRateChange}%`)}`);
|
|
735
|
+
}
|
|
736
|
+
console.log('');
|
|
737
|
+
|
|
738
|
+
// Key insight
|
|
739
|
+
console.log(chalk.bold.cyan(` KEY INSIGHT: ${summary.keyInsight}`));
|
|
740
|
+
console.log('');
|
|
741
|
+
|
|
742
|
+
console.log(chalk.bold.cyan('═'.repeat(64)));
|
|
743
|
+
console.log('');
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* Run and save retrospective
|
|
748
|
+
*/
|
|
749
|
+
export function runAndSaveRetro(): RetroSummary {
|
|
750
|
+
const summary = generateWeeklyRetro();
|
|
751
|
+
displayRetro(summary);
|
|
752
|
+
|
|
753
|
+
const state = loadLearningState();
|
|
754
|
+
const updatedState = recordRetroCompletion(state, summary);
|
|
755
|
+
saveLearningState(updatedState);
|
|
756
|
+
|
|
757
|
+
return summary;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Check if retrospective is due
|
|
762
|
+
*/
|
|
763
|
+
export function isRetroDue(): { due: boolean; reason: string; daysSince: number } {
|
|
764
|
+
const state = loadLearningState();
|
|
765
|
+
|
|
766
|
+
if (!state.lastWeeklyRetro) {
|
|
767
|
+
return { due: true, reason: 'No retrospective recorded yet', daysSince: 999 };
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
const lastRetro = new Date(state.lastWeeklyRetro);
|
|
771
|
+
const now = new Date();
|
|
772
|
+
const daysSince = Math.floor((now.getTime() - lastRetro.getTime()) / (1000 * 60 * 60 * 24));
|
|
773
|
+
|
|
774
|
+
if (daysSince >= 7) {
|
|
775
|
+
return { due: true, reason: `${daysSince} days since last retrospective`, daysSince };
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
return { due: false, reason: '', daysSince };
|
|
779
|
+
}
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
**Validation:** `npm run build`
|
|
783
|
+
|
|
784
|
+
---
|
|
785
|
+
|
|
786
|
+
### 6. `src/learning/index.ts`
|
|
787
|
+
|
|
788
|
+
**Purpose:** Export all learning module functions
|
|
789
|
+
|
|
790
|
+
```typescript
|
|
791
|
+
/**
|
|
792
|
+
* Learning System - Automatic learning cadence for vibe-check
|
|
793
|
+
*
|
|
794
|
+
* This module provides:
|
|
795
|
+
* - Cadence-based triggers for learning
|
|
796
|
+
* - Nudge generation and display
|
|
797
|
+
* - Weekly retrospectives
|
|
798
|
+
*/
|
|
799
|
+
|
|
800
|
+
export * from './types';
|
|
801
|
+
export * from './storage';
|
|
802
|
+
export * from './cadence';
|
|
803
|
+
export * from './nudges';
|
|
804
|
+
export * from './retrospective';
|
|
805
|
+
```
|
|
806
|
+
|
|
807
|
+
**Validation:** `npm run build`
|
|
808
|
+
|
|
809
|
+
---
|
|
810
|
+
|
|
811
|
+
### 7. `src/commands/learn.ts`
|
|
812
|
+
|
|
813
|
+
**Purpose:** Explicit learn command for retrospectives and learning operations
|
|
814
|
+
|
|
815
|
+
```typescript
|
|
816
|
+
import { Command } from 'commander';
|
|
817
|
+
import chalk from 'chalk';
|
|
818
|
+
import { runAndSaveRetro, isRetroDue, generateWeeklyRetro, displayRetro } from '../learning/retrospective';
|
|
819
|
+
import { loadLearningState, saveLearningState, getPendingNudges, dismissNudge } from '../learning/storage';
|
|
820
|
+
import { formatPatternMemory, getPatternDisplayName, getPatternAdvice } from '../gamification/pattern-memory';
|
|
821
|
+
import { formatInterventionMemory } from '../gamification/intervention-memory';
|
|
822
|
+
import { loadProfile } from '../gamification/profile';
|
|
823
|
+
|
|
824
|
+
export function createLearnCommand(): Command {
|
|
825
|
+
const cmd = new Command('learn')
|
|
826
|
+
.description('Run learning operations - retrospectives, pattern analysis, nudge management')
|
|
827
|
+
.option('--retro', 'Run weekly retrospective')
|
|
828
|
+
.option('--status', 'Show learning status and pending nudges')
|
|
829
|
+
.option('--pattern <name>', 'Show details for a specific pattern (e.g., SSL_TLS)')
|
|
830
|
+
.option('--dismiss <id>', 'Dismiss a nudge by ID')
|
|
831
|
+
.option('--dismiss-all', 'Dismiss all pending nudges')
|
|
832
|
+
.action(async (options) => {
|
|
833
|
+
await runLearn(options);
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
return cmd;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
interface LearnOptions {
|
|
840
|
+
retro?: boolean;
|
|
841
|
+
status?: boolean;
|
|
842
|
+
pattern?: string;
|
|
843
|
+
dismiss?: string;
|
|
844
|
+
dismissAll?: boolean;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
async function runLearn(options: LearnOptions): Promise<void> {
|
|
848
|
+
// Default to status if no options
|
|
849
|
+
if (!options.retro && !options.pattern && !options.dismiss && !options.dismissAll) {
|
|
850
|
+
options.status = true;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
if (options.retro) {
|
|
854
|
+
await runRetro();
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
if (options.status) {
|
|
859
|
+
showStatus();
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
if (options.pattern) {
|
|
864
|
+
showPattern(options.pattern);
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
if (options.dismissAll) {
|
|
869
|
+
dismissAllNudges();
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
if (options.dismiss) {
|
|
874
|
+
dismissSingleNudge(options.dismiss);
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
async function runRetro(): Promise<void> {
|
|
880
|
+
const retroCheck = isRetroDue();
|
|
881
|
+
|
|
882
|
+
if (!retroCheck.due) {
|
|
883
|
+
console.log(chalk.yellow(`\nRetrospective not due yet (${retroCheck.daysSince} days since last).`));
|
|
884
|
+
console.log(chalk.gray('Run with --force to run anyway.\n'));
|
|
885
|
+
|
|
886
|
+
// Ask if they want to run anyway
|
|
887
|
+
const profile = loadProfile();
|
|
888
|
+
if (profile.sessions.length >= 3) {
|
|
889
|
+
console.log(chalk.gray('Running anyway since you have recent sessions...\n'));
|
|
890
|
+
runAndSaveRetro();
|
|
891
|
+
}
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
runAndSaveRetro();
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function showStatus(): void {
|
|
899
|
+
const state = loadLearningState();
|
|
900
|
+
const profile = loadProfile();
|
|
901
|
+
const nudges = getPendingNudges(state);
|
|
902
|
+
const retroCheck = isRetroDue();
|
|
903
|
+
|
|
904
|
+
console.log('');
|
|
905
|
+
console.log(chalk.bold.cyan('═'.repeat(64)));
|
|
906
|
+
console.log(chalk.bold.cyan(' LEARNING STATUS'));
|
|
907
|
+
console.log(chalk.bold.cyan('═'.repeat(64)));
|
|
908
|
+
console.log('');
|
|
909
|
+
|
|
910
|
+
// Retro status
|
|
911
|
+
if (retroCheck.due) {
|
|
912
|
+
console.log(chalk.yellow(` 📅 Retrospective due: ${retroCheck.reason}`));
|
|
913
|
+
console.log(chalk.gray(' Run `vibe-check learn --retro` to complete'));
|
|
914
|
+
} else {
|
|
915
|
+
console.log(chalk.green(` 📅 Retrospective: ${retroCheck.daysSince} days ago (due in ${7 - retroCheck.daysSince} days)`));
|
|
916
|
+
}
|
|
917
|
+
console.log('');
|
|
918
|
+
|
|
919
|
+
// Pending nudges
|
|
920
|
+
if (nudges.length > 0) {
|
|
921
|
+
console.log(chalk.bold.white(` PENDING NUDGES (${nudges.length})`));
|
|
922
|
+
for (const nudge of nudges) {
|
|
923
|
+
console.log(` ${nudge.icon} ${nudge.title}`);
|
|
924
|
+
console.log(chalk.gray(` ${nudge.message}`));
|
|
925
|
+
console.log(chalk.gray(` ID: ${nudge.id}`));
|
|
926
|
+
}
|
|
927
|
+
} else {
|
|
928
|
+
console.log(chalk.gray(' No pending nudges'));
|
|
929
|
+
}
|
|
930
|
+
console.log('');
|
|
931
|
+
|
|
932
|
+
// Pattern summary
|
|
933
|
+
const patternData = formatPatternMemory(profile.patternMemory);
|
|
934
|
+
if (patternData.hasData) {
|
|
935
|
+
console.log(chalk.bold.white(' PATTERN SUMMARY'));
|
|
936
|
+
console.log(` ${patternData.summary}`);
|
|
937
|
+
console.log(chalk.gray(` Avg recovery time: ${patternData.avgRecoveryTime} min`));
|
|
938
|
+
}
|
|
939
|
+
console.log('');
|
|
940
|
+
|
|
941
|
+
// Stats
|
|
942
|
+
console.log(chalk.bold.white(' LEARNING STATS'));
|
|
943
|
+
console.log(` Retrospectives completed: ${state.totalRetrosCompleted}`);
|
|
944
|
+
console.log(` Nudges displayed: ${state.nudgesDisplayed}`);
|
|
945
|
+
console.log(` Nudges dismissed: ${state.nudgesDismissed}`);
|
|
946
|
+
console.log('');
|
|
947
|
+
|
|
948
|
+
console.log(chalk.bold.cyan('═'.repeat(64)));
|
|
949
|
+
console.log('');
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
function showPattern(patternName: string): void {
|
|
953
|
+
const profile = loadProfile();
|
|
954
|
+
const displayName = getPatternDisplayName(patternName);
|
|
955
|
+
const advice = getPatternAdvice(patternName);
|
|
956
|
+
|
|
957
|
+
console.log('');
|
|
958
|
+
console.log(chalk.bold.cyan(` PATTERN: ${displayName}`));
|
|
959
|
+
console.log('');
|
|
960
|
+
|
|
961
|
+
const patternMemory = profile.patternMemory;
|
|
962
|
+
if (!patternMemory) {
|
|
963
|
+
console.log(chalk.gray(' No pattern data recorded yet.'));
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
const records = patternMemory.records.filter(r => r.pattern === patternName);
|
|
968
|
+
const count = patternMemory.patternCounts[patternName] || 0;
|
|
969
|
+
const totalMinutes = patternMemory.patternDurations[patternName] || 0;
|
|
970
|
+
|
|
971
|
+
console.log(` Occurrences: ${count}`);
|
|
972
|
+
console.log(` Total time lost: ${totalMinutes} min`);
|
|
973
|
+
console.log(` Avg recovery: ${count > 0 ? Math.round(totalMinutes / count) : 0} min`);
|
|
974
|
+
console.log('');
|
|
975
|
+
console.log(chalk.bold.yellow(` ADVICE: ${advice}`));
|
|
976
|
+
console.log('');
|
|
977
|
+
|
|
978
|
+
// Recent occurrences
|
|
979
|
+
if (records.length > 0) {
|
|
980
|
+
console.log(chalk.bold.white(' RECENT OCCURRENCES'));
|
|
981
|
+
for (const record of records.slice(-5).reverse()) {
|
|
982
|
+
console.log(` ${record.date}: ${record.component} (${record.duration} min, ${record.commits} commits)`);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
console.log('');
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function dismissAllNudges(): void {
|
|
989
|
+
let state = loadLearningState();
|
|
990
|
+
const nudges = getPendingNudges(state);
|
|
991
|
+
|
|
992
|
+
for (const nudge of nudges) {
|
|
993
|
+
state = dismissNudge(state, nudge.id);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
saveLearningState(state);
|
|
997
|
+
console.log(chalk.green(`\n Dismissed ${nudges.length} nudges.\n`));
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function dismissSingleNudge(nudgeId: string): void {
|
|
1001
|
+
const state = loadLearningState();
|
|
1002
|
+
const updatedState = dismissNudge(state, nudgeId);
|
|
1003
|
+
saveLearningState(updatedState);
|
|
1004
|
+
console.log(chalk.green(`\n Nudge dismissed.\n`));
|
|
1005
|
+
}
|
|
1006
|
+
```
|
|
1007
|
+
|
|
1008
|
+
**Validation:** `npm run build`
|
|
1009
|
+
|
|
1010
|
+
---
|
|
1011
|
+
|
|
1012
|
+
## Files to Modify
|
|
1013
|
+
|
|
1014
|
+
### 1. `src/gamification/profile.ts:255-258`
|
|
1015
|
+
|
|
1016
|
+
**Purpose:** Add learning cadence check after saving profile
|
|
1017
|
+
|
|
1018
|
+
**Before (line 255-258):**
|
|
1019
|
+
```typescript
|
|
1020
|
+
// Save profile
|
|
1021
|
+
saveProfile(profile);
|
|
1022
|
+
|
|
1023
|
+
return {
|
|
1024
|
+
```
|
|
1025
|
+
|
|
1026
|
+
**After:**
|
|
1027
|
+
```typescript
|
|
1028
|
+
// Save profile
|
|
1029
|
+
saveProfile(profile);
|
|
1030
|
+
|
|
1031
|
+
// Run learning cadence check (generates nudges)
|
|
1032
|
+
const { runLearningCadence } = require('../learning/cadence');
|
|
1033
|
+
runLearningCadence(
|
|
1034
|
+
profile.patternMemory,
|
|
1035
|
+
profile.interventionMemory,
|
|
1036
|
+
profile.streak.current,
|
|
1037
|
+
profile.xp.nextLevelXP - profile.xp.currentLevelXP,
|
|
1038
|
+
profile.xp.total
|
|
1039
|
+
);
|
|
1040
|
+
|
|
1041
|
+
return {
|
|
1042
|
+
```
|
|
1043
|
+
|
|
1044
|
+
**Validation:** `npm run build && npm test`
|
|
1045
|
+
|
|
1046
|
+
---
|
|
1047
|
+
|
|
1048
|
+
### 2. `src/commands/analyze.ts:388-390`
|
|
1049
|
+
|
|
1050
|
+
**Purpose:** Display pending nudges after gamification section
|
|
1051
|
+
|
|
1052
|
+
**Before (line 388-390):**
|
|
1053
|
+
```typescript
|
|
1054
|
+
console.log(chalk.cyan('─'.repeat(64)));
|
|
1055
|
+
console.log(chalk.gray(` Run ${chalk.white('vibe-check profile')} to see your full stats`));
|
|
1056
|
+
console.log();
|
|
1057
|
+
```
|
|
1058
|
+
|
|
1059
|
+
**After:**
|
|
1060
|
+
```typescript
|
|
1061
|
+
console.log(chalk.cyan('─'.repeat(64)));
|
|
1062
|
+
|
|
1063
|
+
// Display pending nudges from learning system
|
|
1064
|
+
const { formatNudgesForCli } = require('../learning/nudges');
|
|
1065
|
+
const nudgeLines = formatNudgesForCli(2);
|
|
1066
|
+
if (nudgeLines.length > 0) {
|
|
1067
|
+
for (const line of nudgeLines) {
|
|
1068
|
+
console.log(line);
|
|
1069
|
+
}
|
|
1070
|
+
console.log(chalk.cyan('─'.repeat(64)));
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
console.log(chalk.gray(` Run ${chalk.white('vibe-check profile')} to see your full stats`));
|
|
1074
|
+
console.log();
|
|
1075
|
+
```
|
|
1076
|
+
|
|
1077
|
+
**Validation:** `npm run build && npm run dev --score`
|
|
1078
|
+
|
|
1079
|
+
---
|
|
1080
|
+
|
|
1081
|
+
### 3. `src/cli.ts:4`
|
|
1082
|
+
|
|
1083
|
+
**Purpose:** Import learn command
|
|
1084
|
+
|
|
1085
|
+
**Before (line 4):**
|
|
1086
|
+
```typescript
|
|
1087
|
+
import { createAnalyzeCommand, createStartCommand, createProfileCommand, createInitHookCommand, createWatchCommand, createInterveneCommand, createTimelineCommand, createCacheCommand, createDashboardCommand, runAnalyze } from './commands';
|
|
1088
|
+
```
|
|
1089
|
+
|
|
1090
|
+
**After:**
|
|
1091
|
+
```typescript
|
|
1092
|
+
import { createAnalyzeCommand, createStartCommand, createProfileCommand, createInitHookCommand, createWatchCommand, createInterveneCommand, createTimelineCommand, createCacheCommand, createDashboardCommand, createLearnCommand, runAnalyze } from './commands';
|
|
1093
|
+
```
|
|
1094
|
+
|
|
1095
|
+
**Validation:** `npm run build`
|
|
1096
|
+
|
|
1097
|
+
---
|
|
1098
|
+
|
|
1099
|
+
### 4. `src/cli.ts:27` (add after line 27)
|
|
1100
|
+
|
|
1101
|
+
**Purpose:** Register learn command
|
|
1102
|
+
|
|
1103
|
+
**Before (line 27):**
|
|
1104
|
+
```typescript
|
|
1105
|
+
program.addCommand(createDashboardCommand());
|
|
1106
|
+
```
|
|
1107
|
+
|
|
1108
|
+
**After:**
|
|
1109
|
+
```typescript
|
|
1110
|
+
program.addCommand(createDashboardCommand());
|
|
1111
|
+
program.addCommand(createLearnCommand());
|
|
1112
|
+
```
|
|
1113
|
+
|
|
1114
|
+
**Validation:** `npm run build && npm run dev learn --help`
|
|
1115
|
+
|
|
1116
|
+
---
|
|
1117
|
+
|
|
1118
|
+
### 5. `src/commands/index.ts:9` (add after line 9)
|
|
1119
|
+
|
|
1120
|
+
**Purpose:** Export learn command
|
|
1121
|
+
|
|
1122
|
+
**Before (line 9):**
|
|
1123
|
+
```typescript
|
|
1124
|
+
export { createDashboardCommand } from './dashboard';
|
|
1125
|
+
```
|
|
1126
|
+
|
|
1127
|
+
**After:**
|
|
1128
|
+
```typescript
|
|
1129
|
+
export { createDashboardCommand } from './dashboard';
|
|
1130
|
+
export { createLearnCommand } from './learn';
|
|
1131
|
+
```
|
|
1132
|
+
|
|
1133
|
+
**Validation:** `npm run build`
|
|
1134
|
+
|
|
1135
|
+
---
|
|
1136
|
+
|
|
1137
|
+
## Implementation Order
|
|
1138
|
+
|
|
1139
|
+
**CRITICAL: Sequence matters. Do not reorder.**
|
|
1140
|
+
|
|
1141
|
+
| Step | Action | Validation | Rollback |
|
|
1142
|
+
|------|--------|------------|----------|
|
|
1143
|
+
| 0 | Run baseline tests | `npm test` passes | N/A |
|
|
1144
|
+
| 1 | Create `src/learning/types.ts` | `npm run build` | Delete file |
|
|
1145
|
+
| 2 | Create `src/learning/storage.ts` | `npm run build` | Delete file |
|
|
1146
|
+
| 3 | Create `src/learning/cadence.ts` | `npm run build` | Delete file |
|
|
1147
|
+
| 4 | Create `src/learning/nudges.ts` | `npm run build` | Delete file |
|
|
1148
|
+
| 5 | Create `src/learning/retrospective.ts` | `npm run build` | Delete file |
|
|
1149
|
+
| 6 | Create `src/learning/index.ts` | `npm run build` | Delete file |
|
|
1150
|
+
| 7 | Create `src/commands/learn.ts` | `npm run build` | Delete file |
|
|
1151
|
+
| 8 | Modify `src/commands/index.ts` | `npm run build` | Revert file |
|
|
1152
|
+
| 9 | Modify `src/cli.ts` | `npm run build` | Revert file |
|
|
1153
|
+
| 10 | Modify `src/gamification/profile.ts` | `npm run build` | Revert file |
|
|
1154
|
+
| 11 | Modify `src/commands/analyze.ts` | `npm run build` | Revert file |
|
|
1155
|
+
| 12 | Full test | `npm test && npm run dev --score` | Revert all |
|
|
1156
|
+
| 13 | Commit | `git commit` | N/A |
|
|
1157
|
+
|
|
1158
|
+
---
|
|
1159
|
+
|
|
1160
|
+
## Validation Strategy
|
|
1161
|
+
|
|
1162
|
+
### Syntax Validation
|
|
1163
|
+
```bash
|
|
1164
|
+
npm run build
|
|
1165
|
+
# Expected: No TypeScript errors
|
|
1166
|
+
```
|
|
1167
|
+
|
|
1168
|
+
### Unit Test Validation
|
|
1169
|
+
```bash
|
|
1170
|
+
npm test
|
|
1171
|
+
# Expected: All existing tests pass
|
|
1172
|
+
```
|
|
1173
|
+
|
|
1174
|
+
### Integration Validation
|
|
1175
|
+
|
|
1176
|
+
**Test learn command:**
|
|
1177
|
+
```bash
|
|
1178
|
+
npm run dev learn --status
|
|
1179
|
+
# Expected: Shows learning status, no errors
|
|
1180
|
+
|
|
1181
|
+
npm run dev learn --retro
|
|
1182
|
+
# Expected: Shows retrospective summary
|
|
1183
|
+
|
|
1184
|
+
npm run dev learn --help
|
|
1185
|
+
# Expected: Shows command options
|
|
1186
|
+
```
|
|
1187
|
+
|
|
1188
|
+
**Test nudge display:**
|
|
1189
|
+
```bash
|
|
1190
|
+
npm run dev --score --since "1 week ago"
|
|
1191
|
+
# Expected: Shows gamification + any pending nudges
|
|
1192
|
+
```
|
|
1193
|
+
|
|
1194
|
+
**Test learning state persistence:**
|
|
1195
|
+
```bash
|
|
1196
|
+
cat ~/.vibe-check/learning-state.json
|
|
1197
|
+
# Expected: JSON with version, cadence dates, nudges array
|
|
1198
|
+
```
|
|
1199
|
+
|
|
1200
|
+
---
|
|
1201
|
+
|
|
1202
|
+
## Rollback Procedure
|
|
1203
|
+
|
|
1204
|
+
**Time to rollback:** ~3 minutes
|
|
1205
|
+
|
|
1206
|
+
### Full Rollback
|
|
1207
|
+
```bash
|
|
1208
|
+
# Step 1: Remove new files
|
|
1209
|
+
rm -rf src/learning/
|
|
1210
|
+
rm src/commands/learn.ts
|
|
1211
|
+
|
|
1212
|
+
# Step 2: Revert modified files
|
|
1213
|
+
git checkout \
|
|
1214
|
+
src/gamification/profile.ts \
|
|
1215
|
+
src/commands/analyze.ts \
|
|
1216
|
+
src/commands/index.ts \
|
|
1217
|
+
src/cli.ts
|
|
1218
|
+
|
|
1219
|
+
# Step 3: Rebuild
|
|
1220
|
+
npm run build
|
|
1221
|
+
|
|
1222
|
+
# Step 4: Verify
|
|
1223
|
+
npm test
|
|
1224
|
+
```
|
|
1225
|
+
|
|
1226
|
+
### Partial Rollback (keep learn command, remove auto-triggers)
|
|
1227
|
+
```bash
|
|
1228
|
+
git checkout src/gamification/profile.ts src/commands/analyze.ts
|
|
1229
|
+
npm run build
|
|
1230
|
+
```
|
|
1231
|
+
|
|
1232
|
+
---
|
|
1233
|
+
|
|
1234
|
+
## Risk Assessment
|
|
1235
|
+
|
|
1236
|
+
### Low Risk: Additional Latency
|
|
1237
|
+
- **What:** Learning cadence check adds latency to analyze
|
|
1238
|
+
- **Mitigation:** All checks are O(1) - just date comparisons and array filters
|
|
1239
|
+
- **Detection:** `time npm run dev --score`
|
|
1240
|
+
- **Recovery:** Revert profile.ts modification
|
|
1241
|
+
|
|
1242
|
+
### Low Risk: Learning State Corruption
|
|
1243
|
+
- **What:** Invalid JSON in learning-state.json
|
|
1244
|
+
- **Mitigation:** Try-catch with default state fallback
|
|
1245
|
+
- **Detection:** Errors during `learn --status`
|
|
1246
|
+
- **Recovery:** Delete ~/.vibe-check/learning-state.json
|
|
1247
|
+
|
|
1248
|
+
### Very Low Risk: Existing Tests
|
|
1249
|
+
- **What:** New code could break existing functionality
|
|
1250
|
+
- **Mitigation:** No existing files have logic changes, only additions
|
|
1251
|
+
- **Detection:** `npm test`
|
|
1252
|
+
- **Recovery:** Revert all changes
|
|
1253
|
+
|
|
1254
|
+
---
|
|
1255
|
+
|
|
1256
|
+
## Approval Checklist
|
|
1257
|
+
|
|
1258
|
+
**Human must verify before /implement:**
|
|
1259
|
+
|
|
1260
|
+
- [ ] Every file specified precisely (file:line)
|
|
1261
|
+
- [ ] All templates complete (no placeholders)
|
|
1262
|
+
- [ ] Validation commands provided
|
|
1263
|
+
- [ ] Rollback procedure complete
|
|
1264
|
+
- [ ] Implementation order is correct
|
|
1265
|
+
- [ ] Risks identified and mitigated
|
|
1266
|
+
- [ ] No breaking changes to existing functionality
|
|
1267
|
+
|
|
1268
|
+
---
|
|
1269
|
+
|
|
1270
|
+
## Summary: What Changes
|
|
1271
|
+
|
|
1272
|
+
### New Capabilities
|
|
1273
|
+
1. **`vibe-check learn --status`** - View learning state, pending nudges, pattern summary
|
|
1274
|
+
2. **`vibe-check learn --retro`** - Run weekly retrospective with summary
|
|
1275
|
+
3. **`vibe-check learn --pattern <name>`** - Deep dive into specific spiral pattern
|
|
1276
|
+
4. **`vibe-check learn --dismiss-all`** - Clear pending nudges
|
|
1277
|
+
5. **Automatic nudges** - Displayed after gamification in analyze output
|
|
1278
|
+
6. **Cadence triggers** - Weekly retro reminders, pattern warnings, level-up hints
|
|
1279
|
+
|
|
1280
|
+
### Data Flow
|
|
1281
|
+
```
|
|
1282
|
+
analyze --score
|
|
1283
|
+
↓
|
|
1284
|
+
recordSession()
|
|
1285
|
+
↓ (new)
|
|
1286
|
+
runLearningCadence() → generates nudges if conditions met
|
|
1287
|
+
↓
|
|
1288
|
+
display gamification
|
|
1289
|
+
↓ (new)
|
|
1290
|
+
formatNudgesForCli() → shows pending nudges
|
|
1291
|
+
```
|
|
1292
|
+
|
|
1293
|
+
---
|
|
1294
|
+
|
|
1295
|
+
## Next Step
|
|
1296
|
+
|
|
1297
|
+
Once approved: `/implement automatic-learning-cadence-plan-2025-12-02.md`
|