@hanzlaa/rcode 3.6.7 → 3.6.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/package.json +1 -1
- package/rihal/bin/rihal-hooks.cjs +154 -39
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzlaa/rcode",
|
|
3
|
-
"version": "3.6.
|
|
3
|
+
"version": "3.6.8",
|
|
4
4
|
"description": "rcode — the AI team that never forgets. Persistent memory, specialist agents, and slash commands for AI IDEs. Works in Claude Code, Cursor, Gemini, VS Code, and Antigravity.",
|
|
5
5
|
"main": "cli/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -321,69 +321,184 @@ async function bashGuard() {
|
|
|
321
321
|
}
|
|
322
322
|
|
|
323
323
|
/**
|
|
324
|
-
* pre-compact:
|
|
324
|
+
* pre-compact: Capture rihal session state before context compaction.
|
|
325
325
|
*
|
|
326
|
-
* Triggered by the PreCompact hook.
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
326
|
+
* Triggered by the PreCompact hook. Enriched version (#743 + enhancement):
|
|
327
|
+
* 1. Reads .rihal/state.json + .planning/STATE.md + active SPRINT.md
|
|
328
|
+
* 2. Collects recent git commits and in-progress task checkboxes
|
|
329
|
+
* 3. Writes enriched .rihal/HANDOFF.json (machine-readable resume file)
|
|
330
|
+
* 4. Writes .rihal/.continue-here.md (paste-ready resume prompt)
|
|
331
|
+
* 5. Outputs { systemMessage } so Claude sees context immediately after
|
|
332
|
+
* compaction — enabling /rihal-resume-work to restore full context.
|
|
333
|
+
*
|
|
334
|
+
* Never blocks compaction — any error exits 1 (non-blocking per spec).
|
|
330
335
|
*/
|
|
331
336
|
async function preCompact() {
|
|
332
337
|
try {
|
|
333
338
|
const path = require('path');
|
|
339
|
+
const { execSync } = require('child_process');
|
|
334
340
|
await readInputJson(); // drain the PreCompact event payload
|
|
335
341
|
|
|
336
342
|
const cwd = process.cwd();
|
|
343
|
+
|
|
344
|
+
// ── 1. Load state.json ──────────────────────────────────────────────
|
|
337
345
|
const statePath = path.join(cwd, '.rihal', 'state.json');
|
|
338
|
-
|
|
339
|
-
|
|
346
|
+
let state = null;
|
|
347
|
+
if (fs.existsSync(statePath)) {
|
|
348
|
+
try { state = JSON.parse(fs.readFileSync(statePath, 'utf8')); } catch {}
|
|
340
349
|
}
|
|
341
350
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
351
|
+
// ── 2. Determine active phase ────────────────────────────────────────
|
|
352
|
+
const phases = Array.isArray(state?.phases) ? state.phases : [];
|
|
353
|
+
const executing = phases.find((p) => p && p.status === 'executing');
|
|
354
|
+
const matched = phases.find(
|
|
355
|
+
(p) => p && (p.name === state?.current_phase || p.number === state?.current_phase)
|
|
356
|
+
);
|
|
357
|
+
const activePhase = executing || matched || null;
|
|
358
|
+
const phaseLabel = activePhase
|
|
359
|
+
? (activePhase.number || activePhase.name || state?.current_phase)
|
|
360
|
+
: (state?.current_phase || null);
|
|
361
|
+
|
|
362
|
+
// ── 3. Read active SPRINT.md (incomplete tasks) ──────────────────────
|
|
363
|
+
const incompleteTasks = [];
|
|
364
|
+
const completedCount = { done: 0, total: 0 };
|
|
365
|
+
const planningBase = path.join(cwd, '.planning', 'phases');
|
|
366
|
+
if (phaseLabel && fs.existsSync(planningBase)) {
|
|
367
|
+
try {
|
|
368
|
+
const phaseDirs = fs.readdirSync(planningBase)
|
|
369
|
+
.filter(d => d.startsWith(String(phaseLabel)));
|
|
370
|
+
for (const pd of phaseDirs) {
|
|
371
|
+
const pdPath = path.join(planningBase, pd);
|
|
372
|
+
if (!fs.statSync(pdPath).isDirectory()) continue;
|
|
373
|
+
const sprintFiles = fs.readdirSync(pdPath)
|
|
374
|
+
.filter(f => f.endsWith('-SPRINT.md'))
|
|
375
|
+
.sort()
|
|
376
|
+
.reverse(); // most recent first
|
|
377
|
+
if (sprintFiles.length === 0) continue;
|
|
378
|
+
const sprintText = fs.readFileSync(path.join(pdPath, sprintFiles[0]), 'utf8');
|
|
379
|
+
for (const line of sprintText.split('\n')) {
|
|
380
|
+
const done = /^\s*-\s*\[x\]/i.test(line);
|
|
381
|
+
const pending = /^\s*-\s*\[ \]/.test(line);
|
|
382
|
+
if (done || pending) completedCount.total++;
|
|
383
|
+
if (done) completedCount.done++;
|
|
384
|
+
if (pending) {
|
|
385
|
+
const task = line.replace(/^\s*-\s*\[ \]\s*/, '').trim();
|
|
386
|
+
if (task) incompleteTasks.push(task);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
break; // use first matching phase dir only
|
|
390
|
+
}
|
|
391
|
+
} catch {}
|
|
347
392
|
}
|
|
348
393
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
p.name === state.current_phase ||
|
|
358
|
-
p.number === state.current_phase)
|
|
359
|
-
);
|
|
394
|
+
// ── 4. Recent git commits ────────────────────────────────────────────
|
|
395
|
+
let recentCommits = [];
|
|
396
|
+
try {
|
|
397
|
+
const log = execSync('git log --oneline -5 --no-decorate 2>/dev/null', {
|
|
398
|
+
cwd, encoding: 'utf8', timeout: 3000,
|
|
399
|
+
}).trim();
|
|
400
|
+
recentCommits = log ? log.split('\n').filter(Boolean) : [];
|
|
401
|
+
} catch {}
|
|
360
402
|
|
|
361
|
-
|
|
362
|
-
|
|
403
|
+
// ── 5. Read milestone / roadmap headline ────────────────────────────
|
|
404
|
+
let milestoneHint = state?.milestone || null;
|
|
405
|
+
if (!milestoneHint) {
|
|
406
|
+
for (const rp of ['.planning/ROADMAP.md', '.planning/milestones/ROADMAP.md']) {
|
|
407
|
+
const full = path.join(cwd, rp);
|
|
408
|
+
if (!fs.existsSync(full)) continue;
|
|
409
|
+
const m = fs.readFileSync(full, 'utf8').match(/^##\s+Milestone\s+(M\d+[^\n]*)/m);
|
|
410
|
+
if (m) { milestoneHint = m[1].trim(); break; }
|
|
411
|
+
}
|
|
363
412
|
}
|
|
364
413
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
)
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
414
|
+
// ── 6. Read last 3 decisions from STATE.md ───────────────────────────
|
|
415
|
+
let recentDecisions = [];
|
|
416
|
+
const stateFile = path.join(cwd, '.planning', 'STATE.md');
|
|
417
|
+
if (fs.existsSync(stateFile)) {
|
|
418
|
+
try {
|
|
419
|
+
const stateText = fs.readFileSync(stateFile, 'utf8');
|
|
420
|
+
const decSection = stateText.match(/##\s+(?:Recent\s+)?Decisions[\s\S]*?(?=\n##|\Z)/i);
|
|
421
|
+
if (decSection) {
|
|
422
|
+
recentDecisions = decSection[0]
|
|
423
|
+
.split('\n')
|
|
424
|
+
.filter(l => /^\s*[-*]/.test(l))
|
|
425
|
+
.slice(0, 3)
|
|
426
|
+
.map(l => l.replace(/^\s*[-*]\s*/, '').trim())
|
|
427
|
+
.filter(Boolean);
|
|
428
|
+
}
|
|
429
|
+
} catch {}
|
|
430
|
+
}
|
|
373
431
|
|
|
432
|
+
// ── 7. Build enriched HANDOFF.json ───────────────────────────────────
|
|
374
433
|
const handoff = {
|
|
375
434
|
generated_at: new Date().toISOString(),
|
|
376
435
|
reason: 'pre-compact',
|
|
377
436
|
phase: phaseLabel,
|
|
378
|
-
|
|
379
|
-
|
|
437
|
+
milestone: milestoneHint,
|
|
438
|
+
current_plan: state?.current_plan ?? null,
|
|
439
|
+
current_sprint: state?.current_sprint ?? null,
|
|
440
|
+
progress: completedCount.total > 0
|
|
441
|
+
? `${completedCount.done}/${completedCount.total} tasks done`
|
|
442
|
+
: null,
|
|
443
|
+
incomplete_tasks: incompleteTasks.slice(0, 10),
|
|
444
|
+
recent_commits: recentCommits,
|
|
445
|
+
recent_decisions: recentDecisions,
|
|
380
446
|
};
|
|
381
447
|
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
448
|
+
const rihalDir = path.join(cwd, '.rihal');
|
|
449
|
+
if (!fs.existsSync(rihalDir)) {
|
|
450
|
+
process.exit(0); // not a rihal project
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const handoffPath = path.join(rihalDir, 'HANDOFF.json');
|
|
454
|
+
fs.writeFileSync(handoffPath + '.tmp', JSON.stringify(handoff, null, 2) + '\n');
|
|
455
|
+
fs.renameSync(handoffPath + '.tmp', handoffPath);
|
|
456
|
+
|
|
457
|
+
// ── 8. Write .continue-here.md (paste-ready resume prompt) ───────────
|
|
458
|
+
const resumeLines = [
|
|
459
|
+
'# Rihal Session Resume',
|
|
460
|
+
'',
|
|
461
|
+
`**Compacted:** ${handoff.generated_at}`,
|
|
462
|
+
phaseLabel ? `**Phase:** ${phaseLabel}` : null,
|
|
463
|
+
milestoneHint ? `**Milestone:** ${milestoneHint}` : null,
|
|
464
|
+
handoff.progress ? `**Progress:** ${handoff.progress}` : null,
|
|
465
|
+
'',
|
|
466
|
+
].filter(l => l !== null);
|
|
467
|
+
|
|
468
|
+
if (incompleteTasks.length > 0) {
|
|
469
|
+
resumeLines.push('**Next tasks:**');
|
|
470
|
+
incompleteTasks.slice(0, 5).forEach(t => resumeLines.push(`- [ ] ${t}`));
|
|
471
|
+
resumeLines.push('');
|
|
472
|
+
}
|
|
473
|
+
if (recentCommits.length > 0) {
|
|
474
|
+
resumeLines.push('**Recent commits:**');
|
|
475
|
+
recentCommits.forEach(c => resumeLines.push(`- ${c}`));
|
|
476
|
+
resumeLines.push('');
|
|
477
|
+
}
|
|
478
|
+
if (recentDecisions.length > 0) {
|
|
479
|
+
resumeLines.push('**Recent decisions:**');
|
|
480
|
+
recentDecisions.forEach(d => resumeLines.push(`- ${d}`));
|
|
481
|
+
resumeLines.push('');
|
|
482
|
+
}
|
|
483
|
+
resumeLines.push('---');
|
|
484
|
+
resumeLines.push('Run `/rihal-resume-work` to restore full project context.');
|
|
485
|
+
|
|
486
|
+
fs.writeFileSync(
|
|
487
|
+
path.join(rihalDir, '.continue-here.md'),
|
|
488
|
+
resumeLines.join('\n') + '\n'
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
// ── 9. Emit systemMessage for Claude post-compaction ─────────────────
|
|
492
|
+
const msgParts = ['**Rihal context compacted.**'];
|
|
493
|
+
if (phaseLabel) msgParts.push(`Active phase: **${phaseLabel}**`);
|
|
494
|
+
if (milestoneHint) msgParts.push(`Milestone: ${milestoneHint}`);
|
|
495
|
+
if (handoff.progress) msgParts.push(`Progress: ${handoff.progress}`);
|
|
496
|
+
if (incompleteTasks.length > 0) {
|
|
497
|
+
msgParts.push(`Next task: ${incompleteTasks[0]}`);
|
|
498
|
+
}
|
|
499
|
+
msgParts.push('Run `/rihal-resume-work` to restore full context, or `/clear` then paste `.rihal/.continue-here.md`.');
|
|
386
500
|
|
|
501
|
+
process.stdout.write(JSON.stringify({ systemMessage: msgParts.join(' | ') }) + '\n');
|
|
387
502
|
process.exit(0);
|
|
388
503
|
} catch (err) {
|
|
389
504
|
console.error(`Hook error: ${err.message}`);
|