projecta-rrr 1.16.7 → 1.16.8

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/CHANGELOG.md CHANGED
@@ -4,6 +4,34 @@ All notable changes to RRR will be documented in this file.
4
4
 
5
5
  Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
+ ## [1.16.8] - 2026-01-28
8
+
9
+ ### Added
10
+
11
+ - **Install-Time Project Context** (`scripts/build-project-context.js`) - Builds comprehensive project memory at install/upgrade:
12
+ - Enumerates all milestones and phases with status
13
+ - Analyzes git history for project evolution narrative
14
+ - Stores context in `.planning/rrr-project-context.json`
15
+ - Auto-rebuilds on RRR upgrade
16
+
17
+ - **Enhanced HUD with Drift Detection** (`scripts/rrr-hud.js`) - Bulletproof drift detection:
18
+ - Prominent red "DRIFT DETECTED" warning with stale plan counts
19
+ - Context signals (files changed without active plan)
20
+ - Changed file tracking with code file detection
21
+ - Actionable numbered guidance: verify-work, complete plans, or pause-work
22
+ - Robust error handling for all git/file operations
23
+
24
+ - **HUD Integration** - Added visual HUD display to 9 commands:
25
+ - Tier 1: progress, execute-plan, verify-work, execute-phase, plan-phase
26
+ - Tier 2: complete-milestone, resume-work, check-todos
27
+ - Tier 3: debug, new-milestone, pause-work
28
+
29
+ ### Changed
30
+
31
+ - **bin/install.js** - Runs context builder after install for any project with `.planning/`
32
+
33
+ - **hooks/rrr-check-update.sh** - Rebuilds project context when RRR upgrade detected
34
+
7
35
  ## [1.16.7] - 2026-01-28
8
36
 
9
37
  ### Added
package/bin/install.js CHANGED
@@ -942,6 +942,17 @@ function install(isGlobal) {
942
942
  console.log(` ${green}✓${reset} Installed rrr/scripts/rrr-memory/`);
943
943
  }
944
944
 
945
+ // PATCH-01: Copy build-project-context.js to ~/.claude/rrr/scripts/
946
+ // Builds project memory (milestones, phases, git history) at install/upgrade
947
+ const projectContextSrc = path.join(src, 'scripts', 'build-project-context.js');
948
+ if (fs.existsSync(projectContextSrc)) {
949
+ const scriptsDestDir = path.join(claudeDir, 'rrr', 'scripts');
950
+ fs.mkdirSync(scriptsDestDir, { recursive: true });
951
+ const projectContextDest = path.join(scriptsDestDir, 'build-project-context.js');
952
+ fs.copyFileSync(projectContextSrc, projectContextDest);
953
+ console.log(` ${green}✓${reset} Installed rrr/scripts/build-project-context.js`);
954
+ }
955
+
945
956
  // Copy skills to ~/.claude/skills (skills system)
946
957
  const skillsSrc = path.join(src, 'rrr', 'skills');
947
958
  if (fs.existsSync(skillsSrc)) {
@@ -1247,6 +1258,27 @@ function install(isGlobal) {
1247
1258
  // Silent fail - cleanup is optional, don't block install
1248
1259
  }
1249
1260
  }
1261
+
1262
+ // PATCH-01: Build project context (milestones, phases, git history)
1263
+ // This creates a persistent memory of project structure for HUD and decisions
1264
+ // Runs once, AFTER all project-specific setup, for any project with .planning
1265
+ const projectContextScript = path.join(src, 'scripts', 'build-project-context.js');
1266
+ if (fs.existsSync(projectContextScript)) {
1267
+ try {
1268
+ const { buildProjectContext } = require(projectContextScript);
1269
+ const planningDir = path.join(projectDir, '.planning');
1270
+ if (fs.existsSync(planningDir)) {
1271
+ const context = buildProjectContext();
1272
+ if (context) {
1273
+ console.log(` ${green}✓${reset} Built project context (${context.stats.totalMilestones} milestones, ${context.stats.totalPlans} plans)`);
1274
+ } else {
1275
+ console.log(` ${green}✓${reset} Project context up to date`);
1276
+ }
1277
+ }
1278
+ } catch (e) {
1279
+ // Silent fail - context building is optional
1280
+ }
1281
+ }
1250
1282
  }
1251
1283
 
1252
1284
  return { settingsPath, settings, statuslineCommand, notifyCommand, claudeDir, localDirName, isGlobal, bashAvailable: bashStatus.available };
@@ -23,6 +23,11 @@ Enables reviewing captured ideas and deciding what to work on next.
23
23
 
24
24
  <process>
25
25
 
26
+ **Show HUD (first)**
27
+ ```bash
28
+ node ~/.claude/rrr/scripts/rrr-hud.js 2>/dev/null || echo "HUD skipped"
29
+ ```
30
+
26
31
  <step name="check_exist">
27
32
  ```bash
28
33
  TODO_COUNT=$(ls .planning/todos/pending/*.md 2>/dev/null | wc -l | tr -d ' ')
@@ -37,6 +37,11 @@ Output: Milestone archived (roadmap + requirements), PROJECT.md evolved, git tag
37
37
 
38
38
  <process>
39
39
 
40
+ **Show HUD (first)**
41
+ ```bash
42
+ node ~/.claude/rrr/scripts/rrr-hud.js 2>/dev/null || echo "HUD skipped"
43
+ ```
44
+
40
45
  **Follow complete-milestone.md workflow:**
41
46
 
42
47
  0. **Check for audit:**
@@ -28,6 +28,11 @@ ls .planning/debug/*.md 2>/dev/null | grep -v resolved | head -5
28
28
 
29
29
  <process>
30
30
 
31
+ **Show HUD (first)**
32
+ ```bash
33
+ node ~/.claude/rrr/scripts/rrr-hud.js 2>/dev/null || echo "HUD skipped"
34
+ ```
35
+
31
36
  ## 1. Check Active Sessions
32
37
 
33
38
  If active sessions exist AND no $ARGUMENTS:
@@ -38,11 +38,12 @@ Phase: $ARGUMENTS
38
38
 
39
39
  <process>
40
40
 
41
- 0. **Refresh Scope Cache (first)**
41
+ 0. **Show HUD and refresh cache (first)**
42
42
  ```bash
43
+ node ~/.claude/rrr/scripts/rrr-hud.js 2>/dev/null || echo "HUD skipped"
43
44
  node ~/.claude/rrr/scripts/refresh-scope-cache.js 2>/dev/null || echo "Cache refresh skipped"
44
45
  ```
45
- Cross-platform: works in PowerShell on Windows. Path resolved to installed location.
46
+ Cross-platform: works in PowerShell on Windows. Shows visual project state before execution.
46
47
 
47
48
  1. **Validate phase exists**
48
49
  - Use `find_phase_dir()` from phase-paths library to locate phase directory:
@@ -41,11 +41,13 @@ Plan path: $ARGUMENTS
41
41
 
42
42
  <process>
43
43
 
44
- 0. **Refresh Scope Cache (first)**
44
+ 0. **Refresh Scope Cache and HUD (first)**
45
45
  ```bash
46
46
  node ~/.claude/rrr/scripts/refresh-scope-cache.js 2>/dev/null || echo "Cache refresh skipped"
47
+ node ~/.claude/rrr/scripts/rrr-hud.js 2>/dev/null || echo "HUD skipped"
47
48
  ```
48
49
  Cross-platform: works in PowerShell on Windows. Path resolved to installed location.
50
+ Shows visual project state before execution.
49
51
 
50
52
  0. **Track command in memory (before execution)**
51
53
  Parse the plan to extract intent from frontmatter and context, then track this execution:
@@ -37,6 +37,11 @@ Milestone name: $ARGUMENTS (optional - will prompt if not provided)
37
37
 
38
38
  <process>
39
39
 
40
+ **Show HUD (first)**
41
+ ```bash
42
+ node ~/.claude/rrr/scripts/rrr-hud.js 2>/dev/null || echo "HUD skipped"
43
+ ```
44
+
40
45
  1. **Load context:**
41
46
  - Read PROJECT.md (existing project, Validated requirements, decisions)
42
47
  - Read MILESTONES.md (what shipped previously)
@@ -19,6 +19,11 @@ Enables seamless resumption in fresh session with full context restoration.
19
19
 
20
20
  <process>
21
21
 
22
+ **Show HUD (first)**
23
+ ```bash
24
+ node ~/.claude/rrr/scripts/rrr-hud.js 2>/dev/null || echo "HUD skipped"
25
+ ```
26
+
22
27
  <step name="detect">
23
28
  Find current phase directory from most recently modified files.
24
29
  </step>
@@ -71,6 +71,13 @@ Use Glob tool with both structures:
71
71
 
72
72
  <process>
73
73
 
74
+ 0. **Show HUD and refresh cache (first)**
75
+ ```bash
76
+ node ~/.claude/rrr/scripts/rrr-hud.js 2>/dev/null || echo "HUD skipped"
77
+ node ~/.claude/rrr/scripts/refresh-scope-cache.js 2>/dev/null || echo "Cache refresh skipped"
78
+ ```
79
+ Cross-platform: works in PowerShell on Windows. Shows visual context before planning.
80
+
74
81
  ## 1. Validate Environment (Cross-Platform)
75
82
 
76
83
  **Step 1: Check .planning/ exists via STATE.md**
@@ -24,19 +24,20 @@ Provides situational awareness before continuing work.
24
24
  <process>
25
25
 
26
26
  <step name="refresh_cache">
27
- **Refresh Scope Cache (first, before any reads):**
27
+ **Refresh Scope Cache and HUD (first, before any reads):**
28
28
 
29
29
  **On macOS/Linux:**
30
30
  ```bash
31
31
  node ~/.claude/rrr/scripts/refresh-scope-cache.js 2>/dev/null || echo "Cache refresh skipped"
32
+ node ~/.claude/rrr/scripts/rrr-hud.js 2>/dev/null || echo "HUD skipped"
32
33
  ```
33
34
 
34
35
  **On Windows (Platform: win32):**
35
- Same command works in PowerShell. If Node unavailable, skip silently.
36
+ Same commands work in PowerShell. If Node unavailable, skip silently.
36
37
 
37
- **Note:** Path is resolved to installed location (e.g., `$HOME/.claude/` or `./.claude/`).
38
+ **Note:** Paths resolve to installed location (e.g., `$HOME/.claude/` or `./.claude/`).
38
39
 
39
- This ensures SCOPE_CACHE.md is current before displaying status.
40
+ This ensures SCOPE_CACHE.md is current and shows visual HUD before displaying status.
40
41
  </step>
41
42
 
42
43
  <step name="verify">
@@ -27,13 +27,14 @@ Routes to the resume-project workflow which handles:
27
27
 
28
28
  <process>
29
29
 
30
- **0. Refresh Scope Cache (first)**
30
+ **0. Show HUD and refresh cache (first)**
31
31
 
32
32
  ```bash
33
+ node ~/.claude/rrr/scripts/rrr-hud.js 2>/dev/null || echo "HUD skipped"
33
34
  node ~/.claude/rrr/scripts/refresh-scope-cache.js 2>/dev/null || echo "Cache refresh skipped"
34
35
  ```
35
36
 
36
- Cross-platform: works in PowerShell on Windows. Path resolved to installed location.
37
+ Cross-platform: works in PowerShell on Windows. Shows visual project state on resume.
37
38
 
38
39
  **Follow the resume-project workflow** from `@~/.claude/rrr/workflows/resume-project.md`.
39
40
 
@@ -74,6 +74,17 @@ Validate built features through **audit mode by default**, or interactive UAT wi
74
74
  @rrr/lib/memory-store.js
75
75
  </execution_context>
76
76
 
77
+ <process>
78
+
79
+ 0. **Show HUD and refresh cache (first)**
80
+ ```bash
81
+ node ~/.claude/rrr/scripts/rrr-hud.js 2>/dev/null || echo "HUD skipped"
82
+ node ~/.claude/rrr/scripts/refresh-scope-cache.js 2>/dev/null || echo "Cache refresh skipped"
83
+ ```
84
+ Cross-platform: works in PowerShell on Windows. Shows visual project state and drift before verification.
85
+
86
+ </process>
87
+
77
88
  <!-- ═══════════════════════════════════════════════════════════════════════════
78
89
  MODE SELECTION - MUST EXECUTE FIRST BEFORE ANY OTHER SECTION
79
90
  ═══════════════════════════════════════════════════════════════════════════ -->
@@ -7,10 +7,14 @@
7
7
  # Graceful degradation: if npm is not available, exit silently
8
8
  command -v npm >/dev/null 2>&1 || exit 0
9
9
 
10
+ # Graceful degradation: if jq is not available, exit silently
11
+ command -v jq >/dev/null 2>&1 || exit 0
12
+
10
13
  # Catch any errors and exit gracefully (non-blocking hook)
11
14
  set +e
12
15
 
13
16
  CACHE_FILE="$HOME/.claude/cache/rrr-update-check.json"
17
+ CONTEXT_FILE=".planning/rrr-project-context.json"
14
18
  mkdir -p "$HOME/.claude/cache"
15
19
 
16
20
  # Run check in background (non-blocking)
@@ -18,11 +22,22 @@ mkdir -p "$HOME/.claude/cache"
18
22
  installed=$(cat "$HOME/.claude/rrr/VERSION" 2>/dev/null || echo "0.0.0")
19
23
  latest=$(npm view projecta-rrr version 2>/dev/null)
20
24
 
25
+ update_available=false
21
26
  if [[ -n "$latest" && "$installed" != "$latest" ]]; then
27
+ update_available=true
28
+ fi
29
+
30
+ if [[ "$update_available" == "true" ]]; then
22
31
  echo "{\"update_available\":true,\"installed\":\"$installed\",\"latest\":\"$latest\",\"checked\":$(date +%s)}" > "$CACHE_FILE"
23
32
  else
24
33
  echo "{\"update_available\":false,\"installed\":\"$installed\",\"latest\":\"${latest:-unknown}\",\"checked\":$(date +%s)}" > "$CACHE_FILE"
25
34
  fi
35
+
36
+ # If upgrade available, rebuild project context in background
37
+ # This ensures new RRR capabilities get fresh context data
38
+ if [[ "$update_available" == "true" ]] && [[ -f "scripts/build-project-context.js" ]]; then
39
+ node scripts/build-project-context.js 2>/dev/null &
40
+ fi
26
41
  ) &
27
42
 
28
43
  exit 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "projecta-rrr",
3
- "version": "1.16.7",
3
+ "version": "1.16.8",
4
4
  "description": "A meta-prompting, context engineering and spec-driven development system for Claude Code by Projecta.ai",
5
5
  "bin": {
6
6
  "projecta-rrr": "bin/install.js"
@@ -0,0 +1,477 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * RRR Project Context Builder
5
+ *
6
+ * Builds comprehensive project context at install/upgrade time:
7
+ * - Enumerates all milestones and their phases
8
+ * - Analyzes git history for project evolution narrative
9
+ * - Stores context for HUD, decisions, and drift detection
10
+ *
11
+ * This creates a "memory" of the project structure that persists
12
+ * across sessions, avoiding repeated disk scanning.
13
+ */
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const { execSync } = require('child_process');
18
+
19
+ // Paths
20
+ const PLANNING_DIR = '.planning';
21
+ const MILESTONES_DIR = path.join(PLANNING_DIR, 'milestones');
22
+ const CONTEXT_FILE = path.join(PLANNING_DIR, 'rrr-project-context.json');
23
+
24
+ /**
25
+ * Safely read file content
26
+ */
27
+ function safeReadFile(filePath) {
28
+ try {
29
+ return fs.readFileSync(filePath, 'utf8');
30
+ } catch (e) {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Parse milestone version from directory name (e.g., v1.16 -> 1.16)
37
+ */
38
+ function parseMilestoneVersion(dirName) {
39
+ const match = dirName.match(/^v(\d+)\.(\d+)$/);
40
+ if (match) {
41
+ return { major: parseInt(match[1]), minor: parseInt(match[2]), full: match[0] };
42
+ }
43
+ return null;
44
+ }
45
+
46
+ /**
47
+ * Parse frontmatter from markdown
48
+ */
49
+ function parseFrontmatter(content) {
50
+ if (!content) return {};
51
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
52
+ if (!match) return {};
53
+
54
+ const yaml = match[1];
55
+ const result = {};
56
+
57
+ for (const line of yaml.split('\n')) {
58
+ const kvMatch = line.match(/^(\w+):\s*(.*)/);
59
+ if (kvMatch) {
60
+ const [, key, value] = kvMatch;
61
+ result[key] = value.trim().replace(/^["']|["']$/g, '');
62
+ }
63
+ }
64
+
65
+ return result;
66
+ }
67
+
68
+ /**
69
+ * Extract objective from PLAN.md
70
+ */
71
+ function extractObjective(content) {
72
+ if (!content) return null;
73
+ const match = content.match(/<objective>([\s\S]*?)<\/objective>/);
74
+ if (!match) return null;
75
+ return match[1].trim().split('\n')[0];
76
+ }
77
+
78
+ /**
79
+ * Parse ROADMAP.md to get phase list and status
80
+ */
81
+ function parseMilestoneRoadmap(content) {
82
+ if (!content) return { phases: [], completedMilestones: [] };
83
+
84
+ const result = {
85
+ phases: [],
86
+ completedMilestones: [],
87
+ status: 'active'
88
+ };
89
+
90
+ // Extract phases
91
+ const phaseMatches = content.matchAll(/###?\s*Phase\s+(\d+)(?:\.\d+)?:?\s*(.+)/gi);
92
+ for (const match of phaseMatches) {
93
+ result.phases.push({
94
+ number: match[1],
95
+ name: match[2].trim()
96
+ });
97
+ }
98
+
99
+ // Extract completed milestones
100
+ const completedMatch = content.match(/## Completed Milestones([\s\S]*)/i);
101
+ if (completedMatch) {
102
+ const milestoneMatches = completedMatch[1].matchAll(/\*\*v([0-9]+\.[0-9]+)\*\*.*?Phases\s*(\d+)[-–](\d+)/g);
103
+ for (const m of milestoneMatches) {
104
+ result.completedMilestones.push({
105
+ version: m[1],
106
+ phasesRange: `${m[2]}-${m[3]}`
107
+ });
108
+ }
109
+ }
110
+
111
+ return result;
112
+ }
113
+
114
+ /**
115
+ * Get git commit history with filtering
116
+ */
117
+ function getGitHistory(options = {}) {
118
+ const {
119
+ maxCommits = 100,
120
+ since = null,
121
+ includeMessages = true,
122
+ includeFiles = false
123
+ } = options;
124
+
125
+ try {
126
+ let cmd = `git log --oneline -n ${maxCommits}`;
127
+ if (since) {
128
+ cmd += ` --since="${since}"`;
129
+ }
130
+
131
+ const output = execSync(cmd, { encoding: 'utf8', cwd: process.cwd() });
132
+ const lines = output.trim().split('\n').filter(Boolean);
133
+
134
+ return lines.map(line => {
135
+ const match = line.match(/^([a-f0-9]+)\s+(.*)$/);
136
+ if (!match) return { hash: line.slice(0, 7), message: line };
137
+
138
+ const hash = match[1];
139
+ const message = match[2];
140
+
141
+ // Extract plan ID from commit message if present
142
+ const planMatch = message.match(/(\d+-\d+)/);
143
+ const planId = planMatch ? planMatch[0] : null;
144
+
145
+ // Try to categorize the commit type
146
+ let type = 'other';
147
+ if (message.includes('feat:')) type = 'feature';
148
+ else if (message.includes('fix:')) type = 'fix';
149
+ else if (message.includes('chore:')) type = 'chore';
150
+ else if (message.includes('docs:')) type = 'docs';
151
+ else if (message.includes('refactor:')) type = 'refactor';
152
+ else if (planId) {
153
+ // Check if it's a planning commit
154
+ if (message.includes('PLAN') || message.includes('plan')) type = 'planning';
155
+ else if (message.includes('SUMMARY') || message.includes('summary')) type = 'summary';
156
+ else if (message.includes('VERIFICATION') || message.includes('verification')) type = 'verification';
157
+ }
158
+
159
+ return {
160
+ hash: hash.slice(0, 7),
161
+ fullHash: hash,
162
+ message,
163
+ planId,
164
+ type,
165
+ timestamp: null // Would need additional git command to get timestamp
166
+ };
167
+ });
168
+ } catch (e) {
169
+ return [];
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Analyze git history to build project evolution narrative
175
+ */
176
+ function analyzeGitHistory(commits) {
177
+ const narrative = {
178
+ totalCommits: commits.length,
179
+ byType: {},
180
+ planActivity: [],
181
+ recentMilestones: []
182
+ };
183
+
184
+ // Count by type
185
+ for (const commit of commits) {
186
+ narrative.byType[commit.type] = (narrative.byType[commit.type] || 0) + 1;
187
+ }
188
+
189
+ // Extract plan-related activity
190
+ for (const commit of commits) {
191
+ if (commit.planId) {
192
+ narrative.planActivity.push({
193
+ planId: commit.planId,
194
+ message: commit.message.slice(0, 80),
195
+ type: commit.type,
196
+ hash: commit.hash
197
+ });
198
+ }
199
+ }
200
+
201
+ return narrative;
202
+ }
203
+
204
+ /**
205
+ * Enumerate all milestones in .planning/milestones/
206
+ */
207
+ function enumerateMilestones() {
208
+ if (!fs.existsSync(MILESTONES_DIR)) {
209
+ return { milestones: [], activeMilestone: null };
210
+ }
211
+
212
+ const dirs = fs.readdirSync(MILESTONES_DIR, { withFileTypes: true })
213
+ .filter(d => d.isDirectory())
214
+ .map(d => d.name)
215
+ .filter(name => name.startsWith('v'))
216
+ .sort((a, b) => {
217
+ const va = parseMilestoneVersion(a);
218
+ const vb = parseMilestoneVersion(b);
219
+ if (!va || !vb) return 0;
220
+ return va.major - vb.major || va.minor - vb.minor;
221
+ });
222
+
223
+ // Determine active milestone from root ROADMAP.md
224
+ const rootRoadmap = safeReadFile(path.join(PLANNING_DIR, 'ROADMAP.md'));
225
+ let activeMilestone = null;
226
+ if (rootRoadmap) {
227
+ const match = rootRoadmap.match(/^## Current Milestone:\s*(v[0-9]+\.[0-9]+)/m);
228
+ if (match) activeMilestone = match[1];
229
+ }
230
+
231
+ const milestones = [];
232
+
233
+ for (const dir of dirs) {
234
+ const version = parseMilestoneVersion(dir);
235
+ if (!version) continue;
236
+
237
+ const milestonePath = path.join(MILESTONES_DIR, dir);
238
+ const roadmapPath = path.join(milestonePath, 'ROADMAP.md');
239
+ const requirementsPath = path.join(milestonePath, 'REQUIREMENTS.md');
240
+ const phasesDir = path.join(milestonePath, 'phases');
241
+
242
+ const roadmapContent = safeReadFile(roadmapPath);
243
+ const requirementsContent = safeReadFile(requirementsPath);
244
+
245
+ const roadmapInfo = parseMilestoneRoadmap(roadmapContent);
246
+
247
+ // Count phases
248
+ let phaseCount = 0;
249
+ let planCount = 0;
250
+ let completedPlans = 0;
251
+
252
+ if (fs.existsSync(phasesDir)) {
253
+ const phaseDirs = fs.readdirSync(phasesDir, { withFileTypes: true })
254
+ .filter(d => d.isDirectory())
255
+ .map(d => d.name);
256
+
257
+ phaseCount = phaseDirs.length;
258
+
259
+ for (const phaseDir of phaseDirs) {
260
+ const phasePath = path.join(phasesDir, phaseDir);
261
+ const planFiles = fs.readdirSync(phasePath).filter(f => f.endsWith('-PLAN.md'));
262
+ planCount += planFiles.length;
263
+
264
+ for (const planFile of planFiles) {
265
+ const summaryFile = planFile.replace('-PLAN.md', '-SUMMARY.md');
266
+ const summaryPath = path.join(phasePath, summaryFile);
267
+ if (fs.existsSync(summaryPath)) {
268
+ completedPlans++;
269
+ }
270
+ }
271
+ }
272
+ }
273
+
274
+ milestones.push({
275
+ version: version.full,
276
+ major: version.major,
277
+ minor: version.minor,
278
+ isActive: dir === activeMilestone,
279
+ phases: roadmapInfo.phases,
280
+ phaseCount,
281
+ planCount,
282
+ completedPlans,
283
+ progress: planCount > 0 ? Math.round((completedPlans / planCount) * 100) : 0,
284
+ requirements: parseFrontmatter(requirementsContent),
285
+ completedMilestonesInHistory: roadmapInfo.completedMilestones
286
+ });
287
+ }
288
+
289
+ return { milestones, activeMilestone };
290
+ }
291
+
292
+ /**
293
+ * Build comprehensive project context
294
+ */
295
+ function buildProjectContext() {
296
+ console.log('Building project context...\n');
297
+
298
+ const startTime = Date.now();
299
+
300
+ // Enumerate milestones
301
+ const { milestones, activeMilestone } = enumerateMilestones();
302
+ console.log(` Found ${milestones.length} milestone(s)`);
303
+ if (activeMilestone) {
304
+ console.log(` Active milestone: ${activeMilestone}`);
305
+ }
306
+
307
+ // Get git history
308
+ const commits = getGitHistory({ maxCommits: 200 });
309
+ console.log(` Analyzed ${commits.length} recent commits`);
310
+
311
+ // Analyze git history
312
+ const historyAnalysis = analyzeGitHistory(commits);
313
+
314
+ // Build project evolution narrative
315
+ const narrative = [];
316
+ let currentPhase = null;
317
+ let lastDate = null;
318
+
319
+ for (const commit of commits.slice(0, 50)) {
320
+ const dateMatch = execSync(`git log -1 --format=%ci ${commit.fullHash}`, { encoding: 'utf8' }).trim();
321
+ const date = dateMatch ? dateMatch.split('T')[0] : null;
322
+
323
+ if (commit.planId && commit.planId !== currentPhase) {
324
+ if (currentPhase && narrative.length > 0) {
325
+ // Close previous phase
326
+ const last = narrative[narrative.length - 1];
327
+ if (last.endDate && date) {
328
+ last.duration = Math.ceil((new Date(last.endDate) - new Date(last.startDate)) / (1000 * 60 * 60 * 24));
329
+ }
330
+ }
331
+ currentPhase = commit.planId;
332
+ narrative.push({
333
+ planId: commit.planId,
334
+ startDate: date,
335
+ endDate: null,
336
+ type: commit.type,
337
+ commits: 1,
338
+ keyChange: commit.message.slice(0, 60)
339
+ });
340
+ } else if (commit.planId) {
341
+ const existing = narrative.find(n => n.planId === commit.planId);
342
+ if (existing) {
343
+ existing.commits++;
344
+ existing.endDate = date;
345
+ }
346
+ }
347
+ }
348
+
349
+ // Compute project statistics
350
+ const stats = {
351
+ totalMilestones: milestones.length,
352
+ activeMilestone,
353
+ totalPhases: milestones.reduce((sum, m) => sum + m.phaseCount, 0),
354
+ totalPlans: milestones.reduce((sum, m) => sum + m.planCount, 0),
355
+ completedPlans: milestones.reduce((sum, m) => sum + m.completedPlans, 0),
356
+ overallProgress: 0
357
+ };
358
+
359
+ if (stats.totalPlans > 0) {
360
+ stats.overallProgress = Math.round((stats.completedPlans / stats.totalPlans) * 100);
361
+ }
362
+
363
+ // Build the context object
364
+ const context = {
365
+ version: 1,
366
+ builtAt: new Date().toISOString(),
367
+ rrrVersion: safeReadFile(path.join(__dirname, '..', 'VERSION'))?.trim() || null,
368
+
369
+ milestones,
370
+ activeMilestone,
371
+
372
+ // Git-derived context
373
+ gitHistory: {
374
+ recentCommits: commits.slice(0, 50),
375
+ analysis: historyAnalysis,
376
+ projectNarrative: narrative
377
+ },
378
+
379
+ // Project statistics
380
+ stats,
381
+
382
+ // For caching purposes
383
+ cacheMetadata: {
384
+ file: CONTEXT_FILE,
385
+ validUntil: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), // 24 hours
386
+ rebuildTriggers: [
387
+ 'milestone added',
388
+ 'phase added',
389
+ 'plan created',
390
+ 'summary completed'
391
+ ]
392
+ }
393
+ };
394
+
395
+ // Write context file
396
+ fs.writeFileSync(CONTEXT_FILE, JSON.stringify(context, null, 2));
397
+ console.log(`\n Wrote ${CONTEXT_FILE}`);
398
+
399
+ const elapsed = Date.now() - startTime;
400
+ console.log(` Built in ${elapsed}ms\n`);
401
+
402
+ // Print summary
403
+ console.log('Project Context Summary:');
404
+ console.log(` Milestones: ${stats.totalMilestones}`);
405
+ console.log(` Phases: ${stats.totalPhases}`);
406
+ console.log(` Plans: ${stats.completedPlans}/${stats.totalPlans} (${stats.overallProgress}%)`);
407
+ if (activeMilestone) {
408
+ console.log(` Active: ${activeMilestone}`);
409
+ }
410
+
411
+ return context;
412
+ }
413
+
414
+ /**
415
+ * Check if context needs rebuilding
416
+ */
417
+ function needsRebuild() {
418
+ if (!fs.existsSync(CONTEXT_FILE)) return true;
419
+
420
+ try {
421
+ const existing = JSON.parse(fs.readFileSync(CONTEXT_FILE, 'utf8'));
422
+
423
+ // Check if cache is expired
424
+ if (existing.cacheMetadata?.validUntil) {
425
+ const validUntil = new Date(existing.cacheMetadata.validUntil);
426
+ if (new Date() > validUntil) return true;
427
+ }
428
+
429
+ // Check if RRR version changed
430
+ const currentVersion = safeReadFile(path.join(__dirname, '..', 'VERSION'))?.trim();
431
+ if (existing.rrrVersion !== currentVersion) {
432
+ console.log(` Version change detected: ${existing.rrrVersion} -> ${currentVersion}`);
433
+ return true;
434
+ }
435
+
436
+ return false;
437
+ } catch (e) {
438
+ return true;
439
+ }
440
+ }
441
+
442
+ /**
443
+ * Main entry point
444
+ */
445
+ function main() {
446
+ const projectDir = process.cwd();
447
+ const planningDir = path.join(projectDir, PLANNING_DIR);
448
+
449
+ // Check if .planning exists
450
+ if (!fs.existsSync(planningDir)) {
451
+ console.log('No .planning directory found - skipping context build');
452
+ console.log(' Run /rrr:new-project or /rrr:new-milestone to initialize\n');
453
+ return null;
454
+ }
455
+
456
+ // Check if rebuild is needed
457
+ if (!needsRebuild()) {
458
+ console.log('Project context up to date - skipping rebuild\n');
459
+ return null;
460
+ }
461
+
462
+ return buildProjectContext();
463
+ }
464
+
465
+ // Run if called directly
466
+ if (require.main === module) {
467
+ main();
468
+ }
469
+
470
+ // Export for use as module
471
+ module.exports = {
472
+ buildProjectContext,
473
+ needsRebuild,
474
+ enumerateMilestones,
475
+ getGitHistory,
476
+ analyzeGitHistory
477
+ };
@@ -3,16 +3,22 @@
3
3
  *
4
4
  * Run via: node scripts/rrr-hud.js
5
5
  * Or integrate into existing commands for auto-show
6
+ *
7
+ * Enhanced with prominent drift detection and actionable guidance.
6
8
  */
7
9
 
8
10
  const fs = require('fs');
9
11
  const path = require('path');
10
- const { initMemory, getSummary, detectDrift, getProjectState } = require('../lib/memory-store');
12
+ const { execSync } = require('child_process');
13
+
14
+ // Resolve memory-store from rrr/lib/ regardless of cwd
15
+ const memoryStorePath = path.join(__dirname, '..', 'rrr', 'lib', 'memory-store');
16
+ const { initMemory, getSummary, detectDrift, getProjectState } = require(memoryStorePath);
11
17
 
12
18
  const COLORS = {
13
19
  reset: '\x1b[0m', bright: '\x1b[1m', dim: '\x1b[2m',
14
20
  green: '\x1b[32m', yellow: '\x1b[33m', red: '\x1b[31m',
15
- cyan: '\x1b[36m', magenta: '\x1b[35m'
21
+ cyan: '\x1b[36m', magenta: '\x1b[35m', orange: '\x1b[91m'
16
22
  };
17
23
 
18
24
  function progressBar(current, total, width = 20) {
@@ -47,33 +53,115 @@ function getPhaseStatus(milestone) {
47
53
 
48
54
  function getDriftStatus() {
49
55
  let drifting = 0, pending = 0;
56
+ const stalePlans = [];
50
57
  const phasesDir = path.join(process.cwd(), '.planning', 'milestones');
51
- if (!fs.existsSync(phasesDir)) return { drifting: 0, pending: 0 };
58
+ if (!fs.existsSync(phasesDir)) return { drifting: 0, pending: 0, stalePlans: [] };
52
59
 
53
60
  for (const milestone of fs.readdirSync(phasesDir)) {
54
61
  const msPhases = path.join(phasesDir, milestone, 'phases');
55
62
  if (!fs.existsSync(msPhases)) continue;
56
63
  for (const phase of fs.readdirSync(msPhases)) {
57
64
  const phaseDir = path.join(msPhases, phase);
58
- const plans = fs.readdirSync(phaseDir).filter(f => f.endsWith('-PLAN.md'));
59
- const summaries = fs.readdirSync(phaseDir).filter(f => f.endsWith('-SUMMARY.md'));
60
- for (const plan of plans) {
61
- if (!summaries.some(s => s.replace('SUMMARY', 'PLAN') === plan)) {
62
- const planPath = path.join(phaseDir, plan);
63
- const age = (Date.now() - fs.statSync(planPath).mtimeMs) / (1000 * 60 * 60);
64
- if (age > 48) drifting++; else pending++;
65
+ if (!fs.existsSync(phaseDir)) continue;
66
+ try {
67
+ const plans = fs.readdirSync(phaseDir).filter(f => f.endsWith('-PLAN.md'));
68
+ const summaries = fs.readdirSync(phaseDir).filter(f => f.endsWith('-SUMMARY.md'));
69
+ for (const plan of plans) {
70
+ if (!summaries.some(s => s.replace('SUMMARY', 'PLAN') === plan)) {
71
+ const planPath = path.join(phaseDir, plan);
72
+ try {
73
+ const stats = fs.statSync(planPath);
74
+ const age = (Date.now() - stats.mtimeMs) / (1000 * 60 * 60);
75
+ const planId = plan.replace('-PLAN.md', '');
76
+ if (age > 48) {
77
+ drifting++;
78
+ stalePlans.push({ planId, age: Math.round(age), phase });
79
+ } else {
80
+ pending++;
81
+ }
82
+ } catch (e) {
83
+ // Skip plans we can't stat
84
+ }
85
+ }
65
86
  }
87
+ } catch (e) {
88
+ // Skip phases we can't read
66
89
  }
67
90
  }
68
91
  }
69
- return { drifting, pending };
92
+ return { drifting, pending, stalePlans };
93
+ }
94
+
95
+ function getChangedFiles() {
96
+ try {
97
+ const unstaged = execSync('git diff --name-only 2>/dev/null', { encoding: 'utf8', timeout: 5000 }).trim().split('\n').filter(Boolean);
98
+ const staged = execSync('git diff --cached --name-only 2>/dev/null', { encoding: 'utf8', timeout: 5000 }).trim().split('\n').filter(Boolean);
99
+ const untracked = execSync('git ls-files --others --exclude-standard 2>/dev/null', { encoding: 'utf8', timeout: 5000 }).trim().split('\n').filter(Boolean);
100
+ return { unstaged, staged, untracked, total: unstaged.length + staged.length + untracked.length };
101
+ } catch (e) {
102
+ // Git not available or error - no changed files
103
+ return { unstaged: [], staged: [], untracked: [], total: 0 };
104
+ }
105
+ }
106
+
107
+ function checkContextDrift() {
108
+ const signals = [];
109
+
110
+ // Check 1: Are there changed files but no recent intent?
111
+ const changedFiles = getChangedFiles();
112
+ if (changedFiles.total > 0) {
113
+ const statePath = path.join(process.cwd(), '.planning', 'STATE.md');
114
+ try {
115
+ const state = fs.readFileSync(statePath, 'utf8');
116
+ const statusMatch = state.match(/Status:\s*(.+)/);
117
+ if (statusMatch && !statusMatch[1].includes('executing') && !statusMatch[1].includes('Planning')) {
118
+ signals.push({
119
+ type: 'context_drift',
120
+ severity: 'medium',
121
+ message: `${changedFiles.total} file(s) changed but no active plan`
122
+ });
123
+ }
124
+ } catch (e) {
125
+ // STATE.md missing
126
+ }
127
+ }
128
+
129
+ // Check 2: Are there incomplete plans in multiple phases?
130
+ const phasesDir = path.join(process.cwd(), '.planning', 'milestones');
131
+ let phasesWithIncomplete = 0;
132
+ try {
133
+ for (const ms of fs.readdirSync(phasesDir)) {
134
+ const msPhases = path.join(phasesDir, ms, 'phases');
135
+ if (!fs.existsSync(msPhases)) continue;
136
+ for (const phase of fs.readdirSync(msPhases)) {
137
+ const phaseDir = path.join(msPhases, phase);
138
+ const plans = fs.readdirSync(phaseDir).filter(f => f.endsWith('-PLAN.md'));
139
+ const summaries = fs.readdirSync(phaseDir).filter(f => f.endsWith('-SUMMARY.md'));
140
+ if (plans.length > summaries.length) phasesWithIncomplete++;
141
+ }
142
+ }
143
+ } catch (e) {
144
+ // Ignore
145
+ }
146
+
147
+ if (phasesWithIncomplete > 1) {
148
+ signals.push({
149
+ type: 'scattered_work',
150
+ severity: 'low',
151
+ message: `Work spread across ${phasesWithIncomplete} phases`
152
+ });
153
+ }
154
+
155
+ return signals;
70
156
  }
71
157
 
72
158
  async function renderHUD() {
73
159
  const planningDir = path.join(process.cwd(), '.planning');
74
160
  if (!fs.existsSync(planningDir)) {
75
- console.log('No .planning directory found');
76
- console.log('Run /rrr:mvp to get started');
161
+ console.log(`${COLORS.orange}╔════════════════════════════════════════════════════════════════════╗${COLORS.reset}`);
162
+ console.log(`${COLORS.orange}║${COLORS.reset} ${COLORS.red}No .planning directory found${COLORS.reset}`);
163
+ console.log(`${COLORS.orange}║${COLORS.reset} Run ${COLORS.cyan}/rrr:new-project${COLORS.reset} or ${COLORS.cyan}/rrr:mvp${COLORS.reset} to initialize`);
164
+ console.log(`${COLORS.orange}╚════════════════════════════════════════════════════════════════════╝${COLORS.reset}`);
77
165
  return;
78
166
  }
79
167
 
@@ -81,11 +169,13 @@ async function renderHUD() {
81
169
  let memory = null;
82
170
  let memorySummary = null;
83
171
  let projectState = null;
172
+ let memoryDrift = null;
84
173
 
85
174
  try {
86
175
  memory = await initMemory();
87
176
  memorySummary = await getSummary(memory);
88
177
  projectState = await getProjectState();
178
+ memoryDrift = await detectDrift(memory, []);
89
179
  } catch (e) {
90
180
  // Memory not initialized
91
181
  }
@@ -100,62 +190,112 @@ async function renderHUD() {
100
190
  const donePlans = phases.reduce((sum, p) => sum + p.completed, 0);
101
191
  const overallPct = totalPlans > 0 ? Math.round((donePlans / totalPlans) * 100) : 0;
102
192
 
103
- console.log(`${COLORS.bright}║ Milestone ${milestone.version}${COLORS.reset} ${milestone.description}`);
193
+ console.log(`${COLORS.bright}║ ${COLORS.cyan}Milestone ${milestone.version}${COLORS.reset} ${milestone.description}`);
104
194
  console.log(`${COLORS.bright}║ ${progressBar(donePlans, totalPlans, 60)} ${overallPct}%${COLORS.reset}`);
105
195
 
106
196
  for (const phase of phases) {
107
197
  const icon = phase.completed === phase.plans ? '✓' : phase.completed > 0 ? '◐' : '○';
108
- console.log(`${COLORS.bright}║ ${icon} ${phase.name}${COLORS.reset} ${progressBar(phase.completed, phase.plans, 30)} ${phase.completed}/${phase.plans} plans`);
198
+ const color = phase.completed === phase.plans ? COLORS.green : phase.completed > 0 ? COLORS.yellow : COLORS.dim;
199
+ console.log(`${COLORS.bright}║ ${icon} ${phase.name}${COLORS.reset} ${progressBar(phase.completed, phase.plans, 30)} ${phase.completed}/${phase.plans}`);
109
200
  }
110
201
  } else {
111
- console.log(`${COLORS.bright}║ 📋 Between Milestones${COLORS.reset}`);
202
+ console.log(`${COLORS.bright}║ ${COLORS.cyan}📋 Between Milestones${COLORS.reset}`);
112
203
  console.log(`${COLORS.bright}║${COLORS.reset}`);
113
- console.log(`${COLORS.bright}║ Run /rrr:discuss-milestone to plan next milestone${COLORS.reset}`);
204
+ console.log(`${COLORS.bright}║ Run ${COLORS.cyan}/rrr:discuss-milestone${COLORS.reset} to plan next milestone`);
114
205
  }
115
206
 
116
- console.log(`${COLORS.bright}║${COLORS.reset}`);
117
-
118
- // Drift status from filesystem
207
+ // === DRIFT SECTION - PROMINENT ===
119
208
  const driftStatus = getDriftStatus();
120
- let driftIcon = '🟢', driftText = 'On track';
121
- if (driftStatus.drifting > 0) {
122
- driftIcon = '🔴';
123
- driftText = `${driftStatus.drifting} stale (>48h)`;
124
- } else if (driftStatus.pending > 0) {
125
- driftIcon = '🟡';
126
- driftText = `${driftStatus.pending} in progress`;
127
- }
209
+ const changedFiles = getChangedFiles();
210
+ const contextSignals = checkContextDrift();
211
+ const isDrifting = driftStatus.drifting > 0 || (memoryDrift?.drifting) || contextSignals.length > 0;
212
+
213
+ if (isDrifting) {
214
+ console.log(`${COLORS.bright}║${COLORS.reset}`);
215
+ console.log(`${COLORS.bright} ${COLORS.red}⚠ DRIFT DETECTED ⚠${COLORS.reset}`);
216
+
217
+ // Show stale plans
218
+ if (driftStatus.stalePlans.length > 0) {
219
+ console.log(`${COLORS.bright}║ Stale plans (>48h):${COLORS.reset}`);
220
+ for (const sp of driftStatus.stalePlans.slice(0, 3)) {
221
+ console.log(`${COLORS.bright}║ - ${sp.planId}${COLORS.reset} in ${sp.phase} (${sp.age}h old)`);
222
+ }
223
+ if (driftStatus.stalePlans.length > 3) {
224
+ console.log(`${COLORS.bright}║ ... and ${driftStatus.stalePlans.length - 3} more${COLORS.reset}`);
225
+ }
226
+ }
227
+
228
+ // Show memory drift signals
229
+ if (memoryDrift?.signals) {
230
+ for (const signal of memoryDrift.signals.slice(0, 2)) {
231
+ const severityColor = signal.severity === 'high' ? COLORS.red : signal.severity === 'medium' ? COLORS.orange : COLORS.yellow;
232
+ console.log(`${COLORS.bright}║ ${severityColor}${signal.type}${COLORS.reset}: ${signal.message.slice(0, 50)}`);
233
+ }
234
+ }
235
+
236
+ // Show context drift signals
237
+ if (contextSignals.length > 0) {
238
+ for (const signal of contextSignals) {
239
+ const severityColor = signal.severity === 'high' ? COLORS.red : signal.severity === 'medium' ? COLORS.orange : COLORS.yellow;
240
+ console.log(`${COLORS.bright}║ ${severityColor}${signal.type}${COLORS.reset}: ${signal.message.slice(0, 50)}`);
241
+ }
242
+ }
243
+
244
+ // Show changed files
245
+ if (changedFiles.total > 0) {
246
+ console.log(`${COLORS.bright}║ Changed files: ${changedFiles.total}${COLORS.reset}`);
247
+ const codeFiles = changedFiles.untracked.filter(f => f.match(/\.(js|ts|tsx|jsx|py|go|rs|java)$/));
248
+ if (codeFiles.length > 0) {
249
+ console.log(`${COLORS.bright}║ Code: ${codeFiles.slice(0, 3).join(', ')}${COLORS.reset}`);
250
+ }
251
+ }
128
252
 
129
- console.log(`${COLORS.bright}║ DRIFT: ${driftIcon} ${driftText}${COLORS.reset}`);
253
+ // Show actionable guidance
254
+ console.log(`${COLORS.bright}║${COLORS.reset}`);
255
+ console.log(`${COLORS.bright}║ ${COLORS.yellow}Action needed:${COLORS.reset}`);
256
+ console.log(`${COLORS.bright}║ ${COLORS.cyan}1.${COLORS.reset} Run ${COLORS.cyan}/rrr:verify-work${COLORS.reset} to audit current state`);
257
+ console.log(`${COLORS.bright}║ ${COLORS.cyan}2.${COLORS.reset} Complete stale plans or create SUMMARY.md`);
258
+ console.log(`${COLORS.bright}║ ${COLORS.cyan}3.${COLORS.reset} Or run ${COLORS.cyan}/rrr:pause-work${COLORS.reset} to reset intent`);
259
+ } else {
260
+ // Clean state
261
+ console.log(`${COLORS.bright}║${COLORS.reset}`);
262
+ console.log(`${COLORS.bright}║ ${COLORS.green}✓ On Track${COLORS.reset}`);
263
+ if (changedFiles.total > 0) {
264
+ console.log(`${COLORS.bright}║ ${changedFiles.total} file(s) changed${COLORS.reset}`);
265
+ }
266
+ }
267
+ // === END DRIFT SECTION ===
130
268
 
131
269
  // Memory status
270
+ console.log(`${COLORS.bright}║${COLORS.reset}`);
132
271
  if (memorySummary) {
133
- const memDriftIcon = memorySummary.drift?.drifting ? '🔴' : '🟢';
134
- console.log(`${COLORS.bright}║ MEMORY ${memDriftIcon} ${memorySummary.drift?.message || 'Initializing...'}${COLORS.reset}`);
135
- console.log(`${COLORS.bright}║ Intent: ${memorySummary.currentIntent}${COLORS.reset}`);
272
+ const memStatus = memorySummary.drift?.drifting ? 'Drifting' : memorySummary.drift?.severity === 'medium' ? 'Caution' : 'Clean';
273
+ const memColor = memorySummary.drift?.drifting ? COLORS.red : memorySummary.drift?.severity === 'medium' ? COLORS.orange : COLORS.green;
274
+ console.log(`${COLORS.bright}║ ${COLORS.cyan}MEMORY${COLORS.reset} ${memColor}${memStatus}${COLORS.reset} | Intent: ${memorySummary.currentIntent?.slice(0, 30) || 'none'}`);
136
275
 
137
276
  if (memorySummary.recentCommands.length > 0) {
138
- console.log(`${COLORS.bright}║ Recent: ${COLORS.reset}${memorySummary.recentCommands.slice(0, 2).map(c =>
139
- `${c.cmd}${c.target ? '(' + c.target + ')' : ''}`
140
- ).join(' → ')}`);
277
+ const recent = memorySummary.recentCommands.slice(0, 2).map(c =>
278
+ `${c.cmd}${c.target ? '(' + c.target.split('-').slice(0, 2).join('-') + ')' : ''}`
279
+ ).join(' → ');
280
+ console.log(`${COLORS.bright}║ Recent: ${COLORS.reset}${recent}`);
141
281
  }
142
282
  }
143
283
 
144
- // Project state info
284
+ // Project state
145
285
  if (projectState) {
146
286
  console.log(`${COLORS.bright}║${COLORS.reset}`);
147
- console.log(`${COLORS.bright}║ STATE: ${projectState.projectType} / ${projectState.milestoneState}${COLORS.reset}`);
287
+ console.log(`${COLORS.bright}║ ${COLORS.cyan}STATE${COLORS.reset}: ${projectState.projectType} / ${projectState.milestoneState}`);
148
288
  }
149
289
 
150
290
  // Guidance based on state
151
- if (projectState && projectState.milestoneState === 'ACTIVE_MILESTONE') {
291
+ if (projectState && projectState.milestoneState === 'ACTIVE_MILESTONE' && !isDrifting) {
152
292
  const incompletePhase = projectState.phases.find(p => p.completed < p.plans && p.completed > 0);
153
293
  if (incompletePhase) {
154
294
  console.log(`${COLORS.bright}║${COLORS.reset}`);
155
- console.log(`${COLORS.bright}║ GUIDANCE:${COLORS.reset}`);
156
- console.log(`${COLORS.bright}║ ${incompletePhase.name} is ${incompletePhase.pct}% complete${COLORS.reset}`);
295
+ console.log(`${COLORS.bright}║ ${COLORS.cyan}SUGGESTED:${COLORS.reset}`);
157
296
  const nextPlan = incompletePhase.completed + 1;
158
- console.log(`${COLORS.bright}║ Try: /rrr:execute-plan ${incompletePhase.name.split('-')[0]}-${nextPlan}${COLORS.reset}`);
297
+ console.log(`${COLORS.bright}║ ${incompletePhase.name} is ${incompletePhase.pct}% complete${COLORS.reset}`);
298
+ console.log(`${COLORS.bright}║ Try: ${COLORS.cyan}/rrr:execute-plan ${incompletePhase.name.split('-')[0]}-${nextPlan}${COLORS.reset}`);
159
299
  }
160
300
  }
161
301