@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzlaa/rcode",
3
- "version": "3.6.7",
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: Refresh HANDOFF.json before context compaction (#743).
324
+ * pre-compact: Capture rihal session state before context compaction.
325
325
  *
326
- * Triggered by the PreCompact hook. Reads .rihal/state.json from the current
327
- * working directory and, if a phase is active, writes a HANDOFF.json pointer
328
- * so a post-compaction agent can resume cleanly. No-op when no phase is
329
- * active. Never blocks compaction.
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
- if (!fs.existsSync(statePath)) {
339
- process.exit(0);
346
+ let state = null;
347
+ if (fs.existsSync(statePath)) {
348
+ try { state = JSON.parse(fs.readFileSync(statePath, 'utf8')); } catch {}
340
349
  }
341
350
 
342
- let state;
343
- try {
344
- state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
345
- } catch {
346
- process.exit(0);
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
- const phases = Array.isArray(state.phases) ? state.phases : [];
350
- const hasActivePhase =
351
- !!state.current_phase &&
352
- phases.length > 0 &&
353
- phases.some(
354
- (p) =>
355
- p &&
356
- (p.status === 'executing' ||
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
- if (!hasActivePhase) {
362
- process.exit(0);
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
- const executing = phases.find((p) => p && p.status === 'executing');
366
- const matched = phases.find(
367
- (p) => p && (p.name === state.current_phase || p.number === state.current_phase)
368
- );
369
- const activePhase = executing || matched;
370
- const phaseLabel = activePhase
371
- ? activePhase.number || activePhase.name || state.current_phase
372
- : state.current_phase;
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
- current_plan: state.current_plan ?? null,
379
- current_sprint: state.current_sprint ?? null,
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 handoffPath = path.join(cwd, 'HANDOFF.json');
383
- const tmpPath = handoffPath + '.tmp';
384
- fs.writeFileSync(tmpPath, JSON.stringify(handoff, null, 2) + '\n');
385
- fs.renameSync(tmpPath, handoffPath);
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}`);