@hanzlaa/rcode 4.4.4 → 4.5.0
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/AGENTS.md +11 -4
- package/CLAUDE.md +11 -4
- package/CONTRIBUTING.md +6 -1
- package/cli/doctor.js +122 -0
- package/cli/index.js +4 -0
- package/cli/install.js +8 -0
- package/cli/lib/namespace-migrate.cjs +216 -0
- package/cli/migrate-namespace.js +61 -0
- package/cli/update.js +19 -0
- package/dist/rcode.js +215 -208
- package/package.json +14 -12
- package/rcode/bin/lib/brain.cjs +353 -0
- package/rcode/bin/lib/gitignore.cjs +126 -0
- package/rcode/bin/lib/memory-drift.cjs +237 -0
- package/rcode/bin/lib/memory-select.cjs +263 -0
- package/rcode/bin/lib/progress.cjs +440 -0
- package/rcode/bin/lib/state-reader.cjs +127 -0
- package/rcode/bin/lib/summary.cjs +82 -0
- package/rcode/bin/rcode-hooks.cjs +194 -64
- package/rcode/bin/rcode-tools.cjs +106 -985
- package/rcode/data/intent-table.json +19 -19
- package/rcode/skills/actions/4-implementation/rcode-herdr-orchestration/templates/heartbeat.sh +0 -0
- package/rcode/templates/memory/INDEX.md +6 -1
- package/rcode/templates/settings-hooks.json +12 -1
- package/rcode/workflows/do.md +2 -2
- package/rcode/workflows/enable-hooks.md +3 -2
- package/rcode/workflows/new-project-roadmap.md +2 -2
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
* compact-nudge — advise /rcode-trim or /clear after N Edit/Write calls (#749)
|
|
15
15
|
* pre-tool-use — stderr warning before large file reads to avoid context bloat (#749)
|
|
16
16
|
* prompt-router — nudge toward rcode commands for memory consistency (#892)
|
|
17
|
+
* session-start — emit one-line project status primer at session open (#947)
|
|
18
|
+
* drift — print the full memory-drift report (#958)
|
|
17
19
|
*
|
|
18
20
|
* All subcommands read stdin JSON from the hook execution context.
|
|
19
21
|
* Pure Node stdlib. No external dependencies.
|
|
@@ -23,6 +25,18 @@ const fs = require('fs');
|
|
|
23
25
|
const os = require('os');
|
|
24
26
|
const path = require('path');
|
|
25
27
|
const { execSync, spawnSync } = require('child_process');
|
|
28
|
+
const { resolveActivePhase, readSprintProgress, readRecentCommits, readMilestoneHint } = require('./lib/state-reader.cjs');
|
|
29
|
+
const { selectMemoryChunks, formatMemoryContext, hasMemory } = require('./lib/memory-select.cjs');
|
|
30
|
+
|
|
31
|
+
// lib/memory-drift.cjs is optional at the module-load level: some hook-copy
|
|
32
|
+
// test fixtures deliberately stage a minimal bin/lib/ (only state-reader.cjs)
|
|
33
|
+
// to exercise other subcommands' missing-file handling. A hard top-level
|
|
34
|
+
// require would crash every subcommand, not just `drift`/`post-commit`, so
|
|
35
|
+
// this loads lazily and fails open — same pattern as INTENT_TABLE below.
|
|
36
|
+
let checkDrift = null;
|
|
37
|
+
try {
|
|
38
|
+
({ checkDrift } = require('./lib/memory-drift.cjs'));
|
|
39
|
+
} catch { /* optional — see comment above */ }
|
|
26
40
|
|
|
27
41
|
/**
|
|
28
42
|
* Read and parse stdin JSON.
|
|
@@ -116,6 +130,56 @@ async function preWorkflow() {
|
|
|
116
130
|
}
|
|
117
131
|
}
|
|
118
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Emit a one-line systemMessage nudge toward /rcode-memory-update when
|
|
135
|
+
* memory-drift.cjs finds drifts, at most once per session (#958).
|
|
136
|
+
*
|
|
137
|
+
* "Session" here has no reliable id on a post-commit hook payload, so we
|
|
138
|
+
* fall back to parent-pid + hourly bucket — same fallback prompt-router
|
|
139
|
+
* already uses (see promptRouter() below) — scoped naturally to the
|
|
140
|
+
* current shell without requiring a session_id in the payload.
|
|
141
|
+
*/
|
|
142
|
+
function maybeEmitDriftNudge(cwd, input) {
|
|
143
|
+
try {
|
|
144
|
+
if (!checkDrift) return;
|
|
145
|
+
const { drifts } = checkDrift(cwd);
|
|
146
|
+
if (drifts.length === 0) return;
|
|
147
|
+
|
|
148
|
+
const sessionFallback = String(process.ppid) + '-' + String(Math.floor(Date.now() / 3600000));
|
|
149
|
+
const sessionId = input?.session_id || input?.tool_input?.session_id || sessionFallback;
|
|
150
|
+
const safeId = String(sessionId).replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
151
|
+
|
|
152
|
+
const cacheDir = path.join(cwd, '.rcode', '.cache');
|
|
153
|
+
const markerFile = path.join(cacheDir, `drift-nudge-${safeId}.json`);
|
|
154
|
+
if (fs.existsSync(markerFile)) return;
|
|
155
|
+
|
|
156
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
157
|
+
fs.writeFileSync(markerFile, JSON.stringify({ ts: Date.now(), count: drifts.length }));
|
|
158
|
+
|
|
159
|
+
const kinds = [...new Set(drifts.map((d) => d.kind))].join(', ');
|
|
160
|
+
const nudge =
|
|
161
|
+
`⚠ Memory drift detected (${drifts.length} finding${drifts.length === 1 ? '' : 's'}: ${kinds}) — ` +
|
|
162
|
+
`run /rcode-memory-update, or \`rcode-hooks drift\` for the full report.`;
|
|
163
|
+
process.stdout.write(JSON.stringify({ systemMessage: nudge }) + '\n');
|
|
164
|
+
} catch {
|
|
165
|
+
// Advisory only — never break the commit flow.
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* drift: Print the full memory-drift report (#958). Standalone CLI use —
|
|
171
|
+
* not gated by the once-per-session nudge limiter in maybeEmitDriftNudge().
|
|
172
|
+
*/
|
|
173
|
+
function driftCommand() {
|
|
174
|
+
if (!checkDrift) {
|
|
175
|
+
console.error('rcode/bin/lib/memory-drift.cjs failed to load — cannot run drift check.');
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
const report = checkDrift(process.cwd());
|
|
179
|
+
console.log(JSON.stringify(report, null, 2));
|
|
180
|
+
process.exit(0);
|
|
181
|
+
}
|
|
182
|
+
|
|
119
183
|
/**
|
|
120
184
|
* post-commit: Verify commit format and banned patterns.
|
|
121
185
|
* Warns (not blocking) if violations found.
|
|
@@ -197,6 +261,8 @@ async function postCommit() {
|
|
|
197
261
|
violations.forEach((v) => console.warn(` • ${v}`));
|
|
198
262
|
}
|
|
199
263
|
|
|
264
|
+
maybeEmitDriftNudge(process.cwd(), input);
|
|
265
|
+
|
|
200
266
|
process.exit(0);
|
|
201
267
|
} catch (err) {
|
|
202
268
|
console.error(`Hook error: ${err.message}`);
|
|
@@ -353,67 +419,16 @@ async function preCompact() {
|
|
|
353
419
|
}
|
|
354
420
|
|
|
355
421
|
// ── 2. Determine active phase ────────────────────────────────────────
|
|
356
|
-
const
|
|
357
|
-
const executing = phases.find((p) => p && p.status === 'executing');
|
|
358
|
-
const matched = phases.find(
|
|
359
|
-
(p) => p && (p.name === state?.current_phase || p.number === state?.current_phase)
|
|
360
|
-
);
|
|
361
|
-
const activePhase = executing || matched || null;
|
|
362
|
-
const phaseLabel = activePhase
|
|
363
|
-
? (activePhase.number || activePhase.name || state?.current_phase)
|
|
364
|
-
: (state?.current_phase || null);
|
|
422
|
+
const { activePhase, phaseLabel } = resolveActivePhase(state);
|
|
365
423
|
|
|
366
424
|
// ── 3. Read active SPRINT.md (incomplete tasks) ──────────────────────
|
|
367
|
-
const incompleteTasks =
|
|
368
|
-
const completedCount = { done: 0, total: 0 };
|
|
369
|
-
const planningBase = path.join(cwd, '.planning', 'phases');
|
|
370
|
-
if (phaseLabel && fs.existsSync(planningBase)) {
|
|
371
|
-
try {
|
|
372
|
-
const phaseDirs = fs.readdirSync(planningBase)
|
|
373
|
-
.filter(d => d.startsWith(String(phaseLabel)));
|
|
374
|
-
for (const pd of phaseDirs) {
|
|
375
|
-
const pdPath = path.join(planningBase, pd);
|
|
376
|
-
if (!fs.statSync(pdPath).isDirectory()) continue;
|
|
377
|
-
const sprintFiles = fs.readdirSync(pdPath)
|
|
378
|
-
.filter(f => f.endsWith('-SPRINT.md'))
|
|
379
|
-
.sort()
|
|
380
|
-
.reverse(); // most recent first
|
|
381
|
-
if (sprintFiles.length === 0) continue;
|
|
382
|
-
const sprintText = fs.readFileSync(path.join(pdPath, sprintFiles[0]), 'utf8');
|
|
383
|
-
for (const line of sprintText.split('\n')) {
|
|
384
|
-
const done = /^\s*-\s*\[x\]/i.test(line);
|
|
385
|
-
const pending = /^\s*-\s*\[ \]/.test(line);
|
|
386
|
-
if (done || pending) completedCount.total++;
|
|
387
|
-
if (done) completedCount.done++;
|
|
388
|
-
if (pending) {
|
|
389
|
-
const task = line.replace(/^\s*-\s*\[ \]\s*/, '').trim();
|
|
390
|
-
if (task) incompleteTasks.push(task);
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
break; // use first matching phase dir only
|
|
394
|
-
}
|
|
395
|
-
} catch {}
|
|
396
|
-
}
|
|
425
|
+
const { completedCount, incompleteTasks } = readSprintProgress(phaseLabel, cwd);
|
|
397
426
|
|
|
398
427
|
// ── 4. Recent git commits ────────────────────────────────────────────
|
|
399
|
-
|
|
400
|
-
try {
|
|
401
|
-
const log = execSync('git log --oneline -5 --no-decorate 2>/dev/null', {
|
|
402
|
-
cwd, encoding: 'utf8', timeout: 3000,
|
|
403
|
-
}).trim();
|
|
404
|
-
recentCommits = log ? log.split('\n').filter(Boolean) : [];
|
|
405
|
-
} catch {}
|
|
428
|
+
const recentCommits = readRecentCommits(cwd);
|
|
406
429
|
|
|
407
430
|
// ── 5. Read milestone / roadmap headline ────────────────────────────
|
|
408
|
-
|
|
409
|
-
if (!milestoneHint) {
|
|
410
|
-
for (const rp of ['.planning/ROADMAP.md', '.planning/milestones/ROADMAP.md']) {
|
|
411
|
-
const full = path.join(cwd, rp);
|
|
412
|
-
if (!fs.existsSync(full)) continue;
|
|
413
|
-
const m = fs.readFileSync(full, 'utf8').match(/^##\s+Milestone\s+(M\d+[^\n]*)/m);
|
|
414
|
-
if (m) { milestoneHint = m[1].trim(); break; }
|
|
415
|
-
}
|
|
416
|
-
}
|
|
431
|
+
const milestoneHint = readMilestoneHint(state, cwd);
|
|
417
432
|
|
|
418
433
|
// ── 6. Read last 3 decisions from STATE.md ───────────────────────────
|
|
419
434
|
let recentDecisions = [];
|
|
@@ -502,7 +517,24 @@ async function preCompact() {
|
|
|
502
517
|
}
|
|
503
518
|
msgParts.push('Run `/rcode-resume-work` to restore full context, or `/clear` then paste `.rcode/.continue-here.md`.');
|
|
504
519
|
|
|
505
|
-
|
|
520
|
+
// ── 10. Relevance-ranked memory survival context (#958) ──────────────
|
|
521
|
+
// Smaller budget than session-start — this rides alongside HANDOFF.json
|
|
522
|
+
// as compaction survival context, not a full primer. Degrades silently
|
|
523
|
+
// when .rcode/memory/ is missing or empty.
|
|
524
|
+
const payload = { systemMessage: msgParts.join(' | ') };
|
|
525
|
+
if (hasMemory(cwd)) {
|
|
526
|
+
try {
|
|
527
|
+
const selection = selectMemoryChunks(cwd, { defaultBudget: 600 });
|
|
528
|
+
const additionalContext = formatMemoryContext(selection);
|
|
529
|
+
if (additionalContext) {
|
|
530
|
+
payload.hookSpecificOutput = {
|
|
531
|
+
hookEventName: 'PreCompact',
|
|
532
|
+
additionalContext,
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
} catch { /* memory injection is advisory — never block compaction */ }
|
|
536
|
+
}
|
|
537
|
+
process.stdout.write(JSON.stringify(payload) + '\n');
|
|
506
538
|
process.exit(0);
|
|
507
539
|
} catch (err) {
|
|
508
540
|
console.error(`Hook error: ${err.message}`);
|
|
@@ -626,12 +658,30 @@ async function costTrack() {
|
|
|
626
658
|
// INTENT_TABLE — keyword map for prompt-router (#892).
|
|
627
659
|
// Source of truth: rcode/workflows/do.md routing table (~285-320). First-match-wins.
|
|
628
660
|
// Loaded from data file to keep this file under 1000 lines (#896).
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
661
|
+
//
|
|
662
|
+
// Fail-open (#952 review H1): the load is wrapped so a MISSING data file degrades
|
|
663
|
+
// the prompt-router to a no-op instead of throwing at module-require time, which
|
|
664
|
+
// would crash EVERY hook subcommand (bash-guard, pre-edit, session-start, …) —
|
|
665
|
+
// not just the router.
|
|
666
|
+
//
|
|
667
|
+
// #952 follow-up: a missing data file used to degrade silently (empty table,
|
|
668
|
+
// zero output, no way for the user to know auto-detection is broken). It now
|
|
669
|
+
// still fails open — prompt-router still exits 0 and never blocks — but
|
|
670
|
+
// promptRouter() emits a one-time additionalContext warning pointing at the
|
|
671
|
+
// fix (`npx @hanzlaa/rcode update`) instead of no-op-ing forever.
|
|
672
|
+
let INTENT_TABLE = [];
|
|
673
|
+
let INTENT_TABLE_LOAD_ERROR = null;
|
|
674
|
+
try {
|
|
675
|
+
INTENT_TABLE = JSON.parse(
|
|
676
|
+
require('fs').readFileSync(
|
|
677
|
+
require('path').join(__dirname, '..', 'data', 'intent-table.json'),
|
|
678
|
+
'utf8'
|
|
679
|
+
)
|
|
680
|
+
);
|
|
681
|
+
} catch (err) {
|
|
682
|
+
INTENT_TABLE = [];
|
|
683
|
+
INTENT_TABLE_LOAD_ERROR = err;
|
|
684
|
+
}
|
|
635
685
|
|
|
636
686
|
/**
|
|
637
687
|
* Inline flat-YAML parser — mirrors parseSimpleYaml in rcode-tools.cjs:91.
|
|
@@ -758,6 +808,30 @@ function promptRouter() {
|
|
|
758
808
|
process.exit(0);
|
|
759
809
|
}
|
|
760
810
|
|
|
811
|
+
// ── Missing data file: warn once instead of silently no-op-ing (#952) ──
|
|
812
|
+
// A consumer install missing rcode/data/intent-table.json used to degrade
|
|
813
|
+
// to a permanent, invisible no-op — no output, no hint anything was wrong.
|
|
814
|
+
// Warn once per machine (tmpdir marker keyed by project path) and point
|
|
815
|
+
// at the fix, then continue with the (empty) table like before.
|
|
816
|
+
if (INTENT_TABLE_LOAD_ERROR) {
|
|
817
|
+
const warnKey = cwd.replace(/[^a-zA-Z0-9]/g, '_');
|
|
818
|
+
const warnFile = path.join(os.tmpdir(), 'rcode-intent-table-missing-warned-' + warnKey);
|
|
819
|
+
if (!fs.existsSync(warnFile)) {
|
|
820
|
+
try { fs.writeFileSync(warnFile, String(process.pid)); } catch { /* fail open */ }
|
|
821
|
+
const payload = {
|
|
822
|
+
hookSpecificOutput: {
|
|
823
|
+
hookEventName,
|
|
824
|
+
additionalContext:
|
|
825
|
+
'rcode/data/intent-table.json is missing — skill auto-detection from prompts is disabled. ' +
|
|
826
|
+
'Run `npx @hanzlaa/rcode update` to reinstall the missing data files.',
|
|
827
|
+
},
|
|
828
|
+
};
|
|
829
|
+
process.stdout.write(JSON.stringify(payload));
|
|
830
|
+
process.exit(0);
|
|
831
|
+
}
|
|
832
|
+
process.exit(0);
|
|
833
|
+
}
|
|
834
|
+
|
|
761
835
|
// ── Keyword match (first-match-wins, case-insensitive) ───────────────
|
|
762
836
|
const lower = prompt.toLowerCase();
|
|
763
837
|
let matched = null;
|
|
@@ -808,8 +882,11 @@ function promptRouter() {
|
|
|
808
882
|
// gentle memory-framed tip loses the skill-selection race to imperative
|
|
809
883
|
// SessionStart primers (e.g. superpowers' "you MUST invoke"). The memory
|
|
810
884
|
// rationale stays, but as the fallback note rather than the headline.
|
|
885
|
+
// #956: dropped the "records the outcome in .rcode/state.json" claim —
|
|
886
|
+
// most routed workflows (e.g. karpathy-audit) only write a report file,
|
|
887
|
+
// they don't touch state.json, so the blanket claim was false.
|
|
811
888
|
const advisory =
|
|
812
|
-
`Use ${matched.command} for this ${matched.intent} task — it's the rcode workflow built for it
|
|
889
|
+
`Use ${matched.command} for this ${matched.intent} task — it's the rcode workflow built for it. ` +
|
|
813
890
|
`Prefer it over handling this ad-hoc; if you do proceed manually, run /rcode-memory-update afterward so long-term memory stays consistent.`;
|
|
814
891
|
|
|
815
892
|
const payload = {
|
|
@@ -943,6 +1020,53 @@ async function stopHandler() {
|
|
|
943
1020
|
}
|
|
944
1021
|
}
|
|
945
1022
|
|
|
1023
|
+
/**
|
|
1024
|
+
* session-start: Emit a one-line project status primer at session open. (#947)
|
|
1025
|
+
* Uses resolveActivePhase from state-reader.cjs. Advisory only — exits 0 on any error.
|
|
1026
|
+
*/
|
|
1027
|
+
function sessionStart() {
|
|
1028
|
+
try {
|
|
1029
|
+
try { fs.readFileSync(0, 'utf8'); } catch { /* drain stdin */ }
|
|
1030
|
+
const cwd = process.cwd();
|
|
1031
|
+
const statePath = path.join(cwd, '.rcode', 'state.json');
|
|
1032
|
+
if (!fs.existsSync(statePath)) process.exit(0);
|
|
1033
|
+
let state;
|
|
1034
|
+
try { state = JSON.parse(fs.readFileSync(statePath, 'utf8')); } catch { process.exit(0); }
|
|
1035
|
+
const { activePhase, phaseLabel } = resolveActivePhase(state);
|
|
1036
|
+
if (!phaseLabel) process.exit(0);
|
|
1037
|
+
const phaseKey = String(activePhase?.number ?? phaseLabel);
|
|
1038
|
+
const phaseSprints = (Array.isArray(state.sprints) ? state.sprints : []).filter(s => String(s.phase) === phaseKey);
|
|
1039
|
+
const doneCount = phaseSprints.filter(s => s.status === 'completed' || s.status === 'complete').length;
|
|
1040
|
+
const sprintSummary = phaseSprints.length > 0 ? `${doneCount}/${phaseSprints.length} sprints done` : 'no sprints yet';
|
|
1041
|
+
const phaseStatus = activePhase?.status || 'planned';
|
|
1042
|
+
const nextCmd = phaseStatus === 'executing' ? '/rcode-execute'
|
|
1043
|
+
: phaseStatus === 'complete' ? '/rcode-add-phase'
|
|
1044
|
+
: phaseSprints.length === 0 ? `/rcode-plan ${phaseLabel}`
|
|
1045
|
+
: '/rcode-execute';
|
|
1046
|
+
const primer = `\u{1F4CD} Phase ${phaseLabel} ${phaseStatus} · ${sprintSummary} · next: ${nextCmd}`;
|
|
1047
|
+
|
|
1048
|
+
// ── Relevance-ranked memory injection (#958) ─────────────────────────
|
|
1049
|
+
// Only attempted when .rcode/memory/ exists and has content — a missing
|
|
1050
|
+
// or empty memory bank degrades silently to the primer-only behavior
|
|
1051
|
+
// that predates this feature.
|
|
1052
|
+
const payload = { systemMessage: primer };
|
|
1053
|
+
if (hasMemory(cwd)) {
|
|
1054
|
+
try {
|
|
1055
|
+
const selection = selectMemoryChunks(cwd);
|
|
1056
|
+
const additionalContext = formatMemoryContext(selection);
|
|
1057
|
+
if (additionalContext) {
|
|
1058
|
+
payload.hookSpecificOutput = {
|
|
1059
|
+
hookEventName: 'SessionStart',
|
|
1060
|
+
additionalContext,
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
} catch { /* memory injection is advisory — never block session start */ }
|
|
1064
|
+
}
|
|
1065
|
+
process.stdout.write(JSON.stringify(payload) + '\n');
|
|
1066
|
+
} catch { /* fail open — never block session start */ }
|
|
1067
|
+
process.exit(0);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
946
1070
|
/**
|
|
947
1071
|
* Main entry point.
|
|
948
1072
|
*/
|
|
@@ -983,9 +1107,15 @@ async function main() {
|
|
|
983
1107
|
case 'prompt-router':
|
|
984
1108
|
promptRouter(); // synchronous — exits inside; never falls through to async path
|
|
985
1109
|
break;
|
|
1110
|
+
case 'session-start':
|
|
1111
|
+
sessionStart();
|
|
1112
|
+
break;
|
|
1113
|
+
case 'drift':
|
|
1114
|
+
driftCommand();
|
|
1115
|
+
break;
|
|
986
1116
|
default:
|
|
987
1117
|
console.error(`Unknown subcommand: ${subcommand}`);
|
|
988
|
-
console.error('Usage: rcode-hooks.cjs pre-edit|pre-workflow|post-commit|bash-guard|pre-compact|stop-verify|cost-track|stop|compact-nudge|pre-tool-use|prompt-router');
|
|
1118
|
+
console.error('Usage: rcode-hooks.cjs pre-edit|pre-workflow|post-commit|bash-guard|pre-compact|stop-verify|cost-track|stop|compact-nudge|pre-tool-use|prompt-router|session-start|drift');
|
|
989
1119
|
process.exit(1);
|
|
990
1120
|
}
|
|
991
1121
|
}
|