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 +28 -0
- package/bin/install.js +32 -0
- package/commands/rrr/check-todos.md +5 -0
- package/commands/rrr/complete-milestone.md +5 -0
- package/commands/rrr/debug.md +5 -0
- package/commands/rrr/execute-phase.md +3 -2
- package/commands/rrr/execute-plan.md +3 -1
- package/commands/rrr/new-milestone.md +5 -0
- package/commands/rrr/pause-work.md +5 -0
- package/commands/rrr/plan-phase.md +7 -0
- package/commands/rrr/progress.md +5 -4
- package/commands/rrr/resume-work.md +3 -2
- package/commands/rrr/verify-work.md +11 -0
- package/hooks/rrr-check-update.sh +15 -0
- package/package.json +1 -1
- package/scripts/build-project-context.js +477 -0
- package/scripts/rrr-hud.js +181 -41
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:**
|
package/commands/rrr/debug.md
CHANGED
|
@@ -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. **
|
|
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.
|
|
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**
|
package/commands/rrr/progress.md
CHANGED
|
@@ -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
|
|
36
|
+
Same commands work in PowerShell. If Node unavailable, skip silently.
|
|
36
37
|
|
|
37
|
-
**Note:**
|
|
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.
|
|
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.
|
|
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
|
@@ -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
|
+
};
|
package/scripts/rrr-hud.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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(
|
|
76
|
-
console.log(
|
|
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
|
-
|
|
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
|
|
204
|
+
console.log(`${COLORS.bright}║ Run ${COLORS.cyan}/rrr:discuss-milestone${COLORS.reset} to plan next milestone`);
|
|
114
205
|
}
|
|
115
206
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
// Drift status from filesystem
|
|
207
|
+
// === DRIFT SECTION - PROMINENT ===
|
|
119
208
|
const driftStatus = getDriftStatus();
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
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
|
|
134
|
-
|
|
135
|
-
console.log(`${COLORS.bright}║
|
|
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
|
-
|
|
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
|
|
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}
|
|
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}║
|
|
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}║
|
|
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
|
|