projecta-rrr 1.16.5 → 1.16.7
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 +99 -2
- package/bin/install.js +43 -1
- package/commands/rrr/add-phase.md +45 -0
- package/commands/rrr/execute-plan.md +37 -0
- package/commands/rrr/insert-phase.md +45 -0
- package/commands/rrr/progress.md +161 -30
- package/commands/rrr/verify-work.md +36 -0
- package/package.json +5 -2
- package/rrr/lib/install-roots.js +54 -1
- package/rrr/lib/memory-store.js +412 -0
- package/rrr/lib/phase-paths.md +40 -16
- package/rrr/references/project-state-flow.md +208 -0
- package/scripts/doctor-rrr.js +48 -2
- package/scripts/handoff-preflight.js +52 -7
- package/scripts/prepublish-check.js +19 -0
- package/scripts/publish-rrr.sh +185 -0
- package/scripts/rrr-hud.js +166 -0
- package/scripts/rrr-memory/state-detector.js +413 -0
- package/scripts/test-install-smoke.js +23 -1
- package/scripts/pushpa-jarvis.sh +0 -703
- package/scripts/pushpa-mode.sh +0 -1560
package/rrr/lib/install-roots.js
CHANGED
|
@@ -535,6 +535,58 @@ if (require.main === module) {
|
|
|
535
535
|
process.exit(summary.verification.valid ? 0 : 1);
|
|
536
536
|
}
|
|
537
537
|
|
|
538
|
+
/**
|
|
539
|
+
* Clean up old quarantine directories
|
|
540
|
+
* Removes quarantine folders older than maxAgeMs to prevent duplicates from being scanned
|
|
541
|
+
* @param {object} options
|
|
542
|
+
* @param {string} [options.configDir] - Explicit config directory
|
|
543
|
+
* @param {number} [options.maxAgeMs=7*24*60*60*1000] - Max age in ms (default 7 days)
|
|
544
|
+
* @returns {{cleaned: number, errors: string[]}}
|
|
545
|
+
*/
|
|
546
|
+
function cleanOldQuarantines(options = {}, maxAgeMs = 7 * 24 * 60 * 60 * 1000) {
|
|
547
|
+
const roots = getCanonicalRoots(options);
|
|
548
|
+
const quarantineRoot = roots.quarantineRoot;
|
|
549
|
+
const cleaned = [];
|
|
550
|
+
const errors = [];
|
|
551
|
+
|
|
552
|
+
if (!fs.existsSync(quarantineRoot)) {
|
|
553
|
+
return { cleaned: 0, errors: [] };
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const now = Date.now();
|
|
557
|
+
const entries = fs.readdirSync(quarantineRoot, { withFileTypes: true });
|
|
558
|
+
|
|
559
|
+
for (const entry of entries) {
|
|
560
|
+
if (!entry.isDirectory()) continue;
|
|
561
|
+
if (entry.name === 'QUARANTINE_LOG.json') continue;
|
|
562
|
+
|
|
563
|
+
const dirPath = path.join(quarantineRoot, entry.name);
|
|
564
|
+
const stats = fs.statSync(dirPath);
|
|
565
|
+
const age = now - stats.mtimeMs;
|
|
566
|
+
|
|
567
|
+
if (age > maxAgeMs) {
|
|
568
|
+
try {
|
|
569
|
+
// Delete directory contents and directory itself
|
|
570
|
+
const files = fs.readdirSync(dirPath);
|
|
571
|
+
for (const file of files) {
|
|
572
|
+
const filePath = path.join(dirPath, file);
|
|
573
|
+
if (fs.statSync(filePath).isDirectory()) {
|
|
574
|
+
fs.rmSync(filePath, { recursive: true });
|
|
575
|
+
} else {
|
|
576
|
+
fs.unlinkSync(filePath);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
fs.rmdirSync(dirPath);
|
|
580
|
+
cleaned.push(entry.name);
|
|
581
|
+
} catch (e) {
|
|
582
|
+
errors.push(`Failed to clean ${entry.name}: ${e.message}`);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
return { cleaned, errors };
|
|
588
|
+
}
|
|
589
|
+
|
|
538
590
|
module.exports = {
|
|
539
591
|
getCanonicalRoots,
|
|
540
592
|
detectAllCandidateRoots,
|
|
@@ -545,5 +597,6 @@ module.exports = {
|
|
|
545
597
|
getInstallInfo,
|
|
546
598
|
detectAndQuarantineDuplicates,
|
|
547
599
|
verifyInstallation,
|
|
548
|
-
getInstallationSummary
|
|
600
|
+
getInstallationSummary,
|
|
601
|
+
cleanOldQuarantines
|
|
549
602
|
};
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RRR Memory Store
|
|
3
|
+
*
|
|
4
|
+
* Central memory library for tracking:
|
|
5
|
+
* - Command history (what commands user ran)
|
|
6
|
+
* - Intent tracking (what user is trying to accomplish)
|
|
7
|
+
* - Decision log (key decisions made)
|
|
8
|
+
* - Drift detection (is user on track?)
|
|
9
|
+
*
|
|
10
|
+
* Uses existing LanceDB for semantic memory and JSON for structured data.
|
|
11
|
+
* NO new commands - this is a library integrated into existing workflows.
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* const memory = require('./rrr/lib/memory-store');
|
|
15
|
+
* await memory.init();
|
|
16
|
+
* await memory.trackCommand('/rrr:execute-plan', '52-01', { intent: 'TTS integration' });
|
|
17
|
+
* const drift = await memory.detectDrift(['src/db/schema.ts']);
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
|
|
23
|
+
// Configuration
|
|
24
|
+
const MEMORY_DIR = '.rrr/memory';
|
|
25
|
+
const COMMAND_FILE = '.planning/rrr-command-memory.json';
|
|
26
|
+
const DECISIONS_FILE = '.planning/rrr-decisions.json';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get active milestone from ROADMAP.md
|
|
30
|
+
*/
|
|
31
|
+
function getActiveMilestone() {
|
|
32
|
+
const roadmap = path.join(process.cwd(), '.planning', 'ROADMAP.md');
|
|
33
|
+
if (!fs.existsSync(roadmap)) return null;
|
|
34
|
+
const content = fs.readFileSync(roadmap, 'utf8');
|
|
35
|
+
const match = content.match(/^## Current Milestone:\s*(v[0-9]+\.[0-9]+)/m);
|
|
36
|
+
return match ? match[1] : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Initialize memory store
|
|
41
|
+
*/
|
|
42
|
+
async function initMemory() {
|
|
43
|
+
const projectPath = process.cwd();
|
|
44
|
+
const memoryPath = path.join(projectPath, MEMORY_DIR);
|
|
45
|
+
const dbPath = path.join(memoryPath, 'memory.lance');
|
|
46
|
+
|
|
47
|
+
// Create memory directory
|
|
48
|
+
if (!fs.existsSync(memoryPath)) {
|
|
49
|
+
fs.mkdirSync(memoryPath, { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Connect to LanceDB
|
|
53
|
+
let semanticDb = null;
|
|
54
|
+
let semanticTable = null;
|
|
55
|
+
try {
|
|
56
|
+
const lancedb = require('@lancedb/lancedb');
|
|
57
|
+
semanticDb = await lancedb.connect(dbPath);
|
|
58
|
+
const tableNames = await semanticDb.tableNames();
|
|
59
|
+
if (!tableNames.includes('memories')) {
|
|
60
|
+
await semanticDb.createEmptyTable('memories', [
|
|
61
|
+
{ id: 'string', vector: 'float32[1024]', type: 'string', content: 'string', milestone: 'string', created_at: 'int64' }
|
|
62
|
+
]);
|
|
63
|
+
}
|
|
64
|
+
semanticTable = await semanticDb.openTable('memories');
|
|
65
|
+
} catch (e) {
|
|
66
|
+
// LanceDB not available, continue with JSON-only mode
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
projectPath,
|
|
71
|
+
memoryPath,
|
|
72
|
+
semanticDb,
|
|
73
|
+
semanticTable,
|
|
74
|
+
commands: loadJson(COMMAND_FILE, { version: 1, commands: [] }),
|
|
75
|
+
decisions: loadJson(DECISIONS_FILE, { version: 1, decisions: [] }),
|
|
76
|
+
sessionStart: Date.now()
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Load JSON or return default
|
|
82
|
+
*/
|
|
83
|
+
function loadJson(file, defaultValue) {
|
|
84
|
+
if (fs.existsSync(file)) {
|
|
85
|
+
try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return defaultValue; }
|
|
86
|
+
}
|
|
87
|
+
return defaultValue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Save memory to disk
|
|
92
|
+
*/
|
|
93
|
+
function saveMemory(memory) {
|
|
94
|
+
fs.writeFileSync(COMMAND_FILE, JSON.stringify(memory.commands, null, 2));
|
|
95
|
+
fs.writeFileSync(DECISIONS_FILE, JSON.stringify(memory.decisions, null, 2));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Track a command execution
|
|
100
|
+
*/
|
|
101
|
+
async function trackCommand(memory, { command, target, intent, reason, files }) {
|
|
102
|
+
const entry = {
|
|
103
|
+
timestamp: Date.now(),
|
|
104
|
+
command,
|
|
105
|
+
target,
|
|
106
|
+
intent,
|
|
107
|
+
reason,
|
|
108
|
+
files: files || [],
|
|
109
|
+
milestone: getActiveMilestone()
|
|
110
|
+
};
|
|
111
|
+
memory.commands.commands.push(entry);
|
|
112
|
+
|
|
113
|
+
// Keep only last 100 commands
|
|
114
|
+
if (memory.commands.commands.length > 100) {
|
|
115
|
+
memory.commands.commands = memory.commands.commands.slice(-100);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Add semantic memory if intent provided
|
|
119
|
+
if (intent && memory.semanticTable) {
|
|
120
|
+
try {
|
|
121
|
+
const embedding = await generateEmbedding(intent);
|
|
122
|
+
await memory.semanticTable.add([{
|
|
123
|
+
id: `cmd:${entry.timestamp}`,
|
|
124
|
+
type: 'intent',
|
|
125
|
+
content: `${command}: ${intent}`,
|
|
126
|
+
vector: embedding,
|
|
127
|
+
milestone: entry.milestone,
|
|
128
|
+
created_at: entry.timestamp
|
|
129
|
+
}]);
|
|
130
|
+
} catch (e) {
|
|
131
|
+
// Silently fail semantic indexing
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
saveMemory(memory);
|
|
136
|
+
return entry;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Record a decision
|
|
141
|
+
*/
|
|
142
|
+
async function recordDecision(memory, { decision, rationale, tradeoffs, alternatives, context, relatedPlans }) {
|
|
143
|
+
const id = `DEC-${String(memory.decisions.decisions.length + 1).padStart(3, '0')}`;
|
|
144
|
+
const entry = {
|
|
145
|
+
id,
|
|
146
|
+
timestamp: Date.now(),
|
|
147
|
+
decision,
|
|
148
|
+
rationale,
|
|
149
|
+
tradeoffs,
|
|
150
|
+
alternatives: alternatives || [],
|
|
151
|
+
context,
|
|
152
|
+
relatedPlans: relatedPlans || [],
|
|
153
|
+
outcome: 'pending',
|
|
154
|
+
milestone: getActiveMilestone()
|
|
155
|
+
};
|
|
156
|
+
memory.decisions.decisions.push(entry);
|
|
157
|
+
saveMemory(memory);
|
|
158
|
+
return entry;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Get current session intent (last intent mentioned)
|
|
163
|
+
*/
|
|
164
|
+
function getCurrentIntent(memory) {
|
|
165
|
+
// Find most recent command with intent
|
|
166
|
+
for (let i = memory.commands.commands.length - 1; i >= 0; i--) {
|
|
167
|
+
if (memory.commands.commands[i].intent) {
|
|
168
|
+
return memory.commands.commands[i];
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get recent commands
|
|
176
|
+
*/
|
|
177
|
+
function getRecentCommands(memory, limit = 10) {
|
|
178
|
+
return memory.commands.commands.slice(-limit).reverse();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Get recent decisions
|
|
183
|
+
*/
|
|
184
|
+
function getRecentDecisions(memory, limit = 5) {
|
|
185
|
+
return memory.decisions.decisions.slice(-limit).reverse();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Detect drift - is user working on things related to their intent?
|
|
190
|
+
*/
|
|
191
|
+
async function detectDrift(memory, currentFiles = []) {
|
|
192
|
+
const signals = [];
|
|
193
|
+
const intent = getCurrentIntent(memory);
|
|
194
|
+
|
|
195
|
+
if (!intent) {
|
|
196
|
+
return { drifting: false, signals, message: 'No intent tracked yet' };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 1. Check if current files relate to intent
|
|
200
|
+
if (currentFiles.length > 0 && intent.intent) {
|
|
201
|
+
let relatedCount = 0;
|
|
202
|
+
|
|
203
|
+
// Simple keyword matching as fallback
|
|
204
|
+
const intentWords = intent.intent.toLowerCase().split(/\s+/);
|
|
205
|
+
for (const file of currentFiles) {
|
|
206
|
+
const fileContent = file.toLowerCase();
|
|
207
|
+
for (const word of intentWords) {
|
|
208
|
+
if (word.length > 3 && fileContent.includes(word)) {
|
|
209
|
+
relatedCount++;
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const relatedRatio = currentFiles.length > 0 ? relatedCount / currentFiles.length : 1;
|
|
216
|
+
if (relatedRatio < 0.3 && currentFiles.length >= 3) {
|
|
217
|
+
signals.push({
|
|
218
|
+
type: 'context_drift',
|
|
219
|
+
severity: 'high',
|
|
220
|
+
message: `Working on ${currentFiles.length} files but only ${relatedCount} relate to intent`,
|
|
221
|
+
intent: intent.intent,
|
|
222
|
+
suggestion: 'Are you switching contexts? Consider /rrr:pause-work'
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 2. Check for stale incomplete plans (>48 hours)
|
|
228
|
+
const staleThreshold = 48 * 60 * 60 * 1000;
|
|
229
|
+
const stalePlans = memory.commands.commands.filter(c =>
|
|
230
|
+
c.command.includes('execute-plan') &&
|
|
231
|
+
c.outcome !== 'completed' &&
|
|
232
|
+
Date.now() - c.timestamp > staleThreshold
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
if (stalePlans.length > 0) {
|
|
236
|
+
signals.push({
|
|
237
|
+
type: 'stale_plan',
|
|
238
|
+
severity: 'low',
|
|
239
|
+
message: `${stalePlans.length} plans incomplete for >48 hours`,
|
|
240
|
+
plans: stalePlans.map(p => p.target),
|
|
241
|
+
suggestion: 'Run /rrr:verify-work or /rrr:pause-work'
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// 3. Check for conflicting decisions
|
|
246
|
+
const recentDecisions = getRecentDecisions(memory, 5);
|
|
247
|
+
const conflictSignals = recentDecisions.filter(d => {
|
|
248
|
+
if (d.outcome === 'rejected') return false;
|
|
249
|
+
return currentFiles.some(f => d.relatedPlans?.some(p => f.includes(p)));
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
if (conflictSignals.length > 0) {
|
|
253
|
+
signals.push({
|
|
254
|
+
type: 'decision_conflict',
|
|
255
|
+
severity: 'medium',
|
|
256
|
+
message: 'Current work may conflict with past decisions',
|
|
257
|
+
decisions: conflictSignals.map(d => d.id),
|
|
258
|
+
suggestion: 'Review past decisions before proceeding'
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
drifting: signals.length > 0,
|
|
264
|
+
signals,
|
|
265
|
+
currentIntent: intent.intent,
|
|
266
|
+
severity: signals.length > 2 ? 'high' : signals.length > 0 ? 'medium' : 'none',
|
|
267
|
+
message: signals.length === 0 ? 'On track - working on intent-aligned files' : 'Drift detected'
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Generate embedding using existing Ollama setup
|
|
273
|
+
*/
|
|
274
|
+
async function generateEmbedding(text) {
|
|
275
|
+
try {
|
|
276
|
+
const embeddings = require('./search/embeddings');
|
|
277
|
+
const result = await embeddings.generateQwenEmbedding(text);
|
|
278
|
+
return result.embedding;
|
|
279
|
+
} catch (e) {
|
|
280
|
+
// Return zero vector if embedding fails
|
|
281
|
+
return new Array(1024).fill(0);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Get memory summary for HUD
|
|
287
|
+
*/
|
|
288
|
+
async function getSummary(memory) {
|
|
289
|
+
const recentCommands = getRecentCommands(memory, 5);
|
|
290
|
+
const recentDecisions = getRecentDecisions(memory, 3);
|
|
291
|
+
const intent = getCurrentIntent(memory);
|
|
292
|
+
const drift = await detectDrift(memory, []);
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
sessionStart: new Date(memory.sessionStart).toLocaleString(),
|
|
296
|
+
lastCommand: recentCommands[0]?.command || 'none',
|
|
297
|
+
lastTarget: recentCommands[0]?.target || '',
|
|
298
|
+
currentIntent: intent?.intent || 'tracking...',
|
|
299
|
+
commandCount: memory.commands.commands.length,
|
|
300
|
+
decisionCount: memory.decisions.decisions.length,
|
|
301
|
+
recentCommands: recentCommands.map(c => ({
|
|
302
|
+
cmd: c.command.replace('/rrr:', ''),
|
|
303
|
+
target: c.target,
|
|
304
|
+
time: new Date(c.timestamp).toLocaleTimeString(),
|
|
305
|
+
intent: c.intent
|
|
306
|
+
})),
|
|
307
|
+
recentDecisions: recentDecisions.map(d => ({
|
|
308
|
+
id: d.id,
|
|
309
|
+
decision: d.decision.substring(0, 50),
|
|
310
|
+
outcome: d.outcome
|
|
311
|
+
})),
|
|
312
|
+
drift
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Get project state for guidance
|
|
318
|
+
*/
|
|
319
|
+
async function getProjectState() {
|
|
320
|
+
const planningDir = path.join(process.cwd(), '.planning');
|
|
321
|
+
const state = {
|
|
322
|
+
projectType: 'NEW_PROJECT',
|
|
323
|
+
milestoneState: 'NO_MILESTONE',
|
|
324
|
+
phaseState: 'NO_PHASES',
|
|
325
|
+
phases: []
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
if (!fs.existsSync(planningDir)) {
|
|
329
|
+
return state;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Detect project type
|
|
333
|
+
const hasProject = fs.existsSync(path.join(planningDir, 'PROJECT.md'));
|
|
334
|
+
const hasMilestones = fs.existsSync(path.join(planningDir, 'milestones'));
|
|
335
|
+
|
|
336
|
+
if (hasProject && hasMilestones) {
|
|
337
|
+
state.projectType = 'EXISTING_PROJECT';
|
|
338
|
+
} else if (hasProject) {
|
|
339
|
+
state.projectType = 'PROJECT_INITIALIZED';
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Detect milestone state
|
|
343
|
+
const roadmap = path.join(planningDir, 'ROADMAP.md');
|
|
344
|
+
if (fs.existsSync(roadmap)) {
|
|
345
|
+
const content = fs.readFileSync(roadmap, 'utf8');
|
|
346
|
+
if (content.includes('## Current Milestone:')) {
|
|
347
|
+
if (content.includes(':construction:')) {
|
|
348
|
+
state.milestoneState = 'MILESTONE_STARTING';
|
|
349
|
+
} else {
|
|
350
|
+
state.milestoneState = 'ACTIVE_MILESTONE';
|
|
351
|
+
}
|
|
352
|
+
} else if (content.includes('SHIPPED') || content.includes('ARCHIVED')) {
|
|
353
|
+
state.milestoneState = 'COMPLETING_MILESTONE';
|
|
354
|
+
} else {
|
|
355
|
+
state.milestoneState = 'NO_MILESTONE';
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Detect phase state
|
|
360
|
+
if (state.milestoneState === 'ACTIVE_MILESTONE') {
|
|
361
|
+
const match = content.match(/## Current Milestone:\s*(v[0-9]+\.[0-9]+)/);
|
|
362
|
+
if (match) {
|
|
363
|
+
const phasesDir = path.join(planningDir, 'milestones', match[1], 'phases');
|
|
364
|
+
if (fs.existsSync(phasesDir)) {
|
|
365
|
+
const phases = [];
|
|
366
|
+
for (const dir of fs.readdirSync(phasesDir)) {
|
|
367
|
+
if (!dir.match(/^\d/)) continue;
|
|
368
|
+
const phaseDir = path.join(phasesDir, dir);
|
|
369
|
+
const planCount = fs.readdirSync(phaseDir).filter(f => f.endsWith('-PLAN.md')).length;
|
|
370
|
+
const summaryCount = fs.readdirSync(phaseDir).filter(f => f.endsWith('-SUMMARY.md')).length;
|
|
371
|
+
phases.push({
|
|
372
|
+
name: dir,
|
|
373
|
+
plans: planCount,
|
|
374
|
+
completed: summaryCount,
|
|
375
|
+
pct: planCount > 0 ? Math.round((summaryCount / planCount) * 100) : 0
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
state.phases = phases.sort((a, b) =>
|
|
379
|
+
parseInt(a.name.split('-')[0]) - parseInt(b.name.split('-')[0])
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
const totalPlans = state.phases.reduce((sum, p) => sum + p.plans, 0);
|
|
383
|
+
const donePlans = state.phases.reduce((sum, p) => sum + p.completed, 0);
|
|
384
|
+
|
|
385
|
+
if (totalPlans === 0) {
|
|
386
|
+
state.phaseState = 'NO_PHASES';
|
|
387
|
+
} else if (donePlans === 0) {
|
|
388
|
+
state.phaseState = 'PLANNED';
|
|
389
|
+
} else if (donePlans < totalPlans) {
|
|
390
|
+
state.phaseState = 'EXECUTING';
|
|
391
|
+
} else if (donePlans === totalPlans) {
|
|
392
|
+
state.phaseState = 'COMPLETE';
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return state;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Export functions
|
|
402
|
+
module.exports = {
|
|
403
|
+
initMemory,
|
|
404
|
+
trackCommand,
|
|
405
|
+
recordDecision,
|
|
406
|
+
getCurrentIntent,
|
|
407
|
+
getRecentCommands,
|
|
408
|
+
getRecentDecisions,
|
|
409
|
+
detectDrift,
|
|
410
|
+
getSummary,
|
|
411
|
+
getProjectState
|
|
412
|
+
};
|
package/rrr/lib/phase-paths.md
CHANGED
|
@@ -28,25 +28,45 @@ get_phase_base_dir() {
|
|
|
28
28
|
local planning_dir="${1:-.planning}"
|
|
29
29
|
local roadmap_file="$planning_dir/ROADMAP.md"
|
|
30
30
|
|
|
31
|
-
#
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
active_milestone=$(grep -E "Current Milestone:|:construction:" "$roadmap_file" 2>/dev/null | head -1 | grep -oE "v[0-9]+\.[0-9]+" | head -1)
|
|
38
|
-
fi
|
|
31
|
+
# Strategy 1: Look for ## Current Milestone: header
|
|
32
|
+
local active_milestone=""
|
|
33
|
+
if [ -f "$roadmap_file" ]; then
|
|
34
|
+
active_milestone=$(grep -E "^## Current Milestone:" "$roadmap_file" 2>/dev/null | \
|
|
35
|
+
grep -oE "v[0-9]+\.[0-9]+" | head -1)
|
|
36
|
+
fi
|
|
39
37
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
echo "$phases_dir"
|
|
45
|
-
return 0
|
|
46
|
-
fi
|
|
38
|
+
# Strategy 2: Look for :construction: marker
|
|
39
|
+
if [ -z "$active_milestone" ]; then
|
|
40
|
+
active_milestone=$(grep -E ":construction:" "$roadmap_file" 2>/dev/null | \
|
|
41
|
+
grep -oE "v[0-9]+\.[0-9]+" | head -1)
|
|
47
42
|
fi
|
|
48
43
|
|
|
49
|
-
#
|
|
44
|
+
# Strategy 3: Find milestone directory with phases (even without ROADMAP entry)
|
|
45
|
+
if [ -z "$active_milestone" ] && [ -d "$planning_dir/milestones" ]; then
|
|
46
|
+
for milestone_dir in "$planning_dir/milestones"/v*/; do
|
|
47
|
+
if [ -d "${milestone_dir}phases" ] && [ "$(ls -A "${milestone_dir}phases" 2>/dev/null)" ]; then
|
|
48
|
+
active_milestone=$(basename "$milestone_dir")
|
|
49
|
+
break
|
|
50
|
+
fi
|
|
51
|
+
done
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# ERROR if milestones dir exists but no active milestone
|
|
55
|
+
if [ -d "$planning_dir/milestones" ] && [ -z "$active_milestone" ]; then
|
|
56
|
+
echo "ERROR: .planning/milestones/ exists but no active milestone detected" >&2
|
|
57
|
+
echo "Add '## Current Milestone: vX.Y Name' to ROADMAP.md" >&2
|
|
58
|
+
return 1
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
# Use milestone phases dir if found
|
|
62
|
+
if [ -n "$active_milestone" ]; then
|
|
63
|
+
local phases_dir="$planning_dir/milestones/$active_milestone/phases"
|
|
64
|
+
mkdir -p "$phases_dir" 2>/dev/null
|
|
65
|
+
echo "$phases_dir"
|
|
66
|
+
return 0
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
# Legacy fallback ONLY if no milestones directory at all
|
|
50
70
|
echo "$planning_dir/phases"
|
|
51
71
|
return 0
|
|
52
72
|
}
|
|
@@ -57,12 +77,16 @@ get_phase_base_dir() {
|
|
|
57
77
|
|
|
58
78
|
**Returns:**
|
|
59
79
|
- Path to the phase base directory (e.g., `.planning/phases` or `.planning/milestones/v1.11/phases`)
|
|
80
|
+
- Exits with error if `.planning/milestones/` exists but no active milestone is detected
|
|
60
81
|
|
|
61
82
|
**Example:**
|
|
62
83
|
```bash
|
|
63
84
|
base_dir=$(get_phase_base_dir)
|
|
64
85
|
echo "Phases stored in: $base_dir"
|
|
65
86
|
# Output: "Phases stored in: .planning/milestones/v1.11/phases"
|
|
87
|
+
|
|
88
|
+
# Error case (milestones dir exists but no active milestone):
|
|
89
|
+
# ERROR: .planning/milestones/ exists but no active milestone detected
|
|
66
90
|
```
|
|
67
91
|
|
|
68
92
|
---
|