@hanzlaa/rcode 3.4.33 → 3.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 +6 -6
- package/CONTRIBUTING.md +2 -0
- package/LICENSE +21 -0
- package/README.md +66 -403
- package/cli/doctor.js +87 -1
- package/cli/install.js +122 -31
- package/cli/lib/schemas.cjs +318 -0
- package/cli/postinstall.js +19 -3
- package/dist/rcode.js +316 -23
- package/package.json +8 -4
- package/rihal/agents/rihal-cross-platform-auditor.md +1 -1
- package/rihal/agents/rihal-dep-auditor.md +1 -1
- package/rihal/agents/rihal-docs-auditor.md +3 -145
- package/rihal/agents/rihal-i18n-auditor.md +1 -1
- package/rihal/agents/rihal-nyquist-auditor.md +4 -156
- package/rihal/agents/rihal-observability-auditor.md +1 -1
- package/rihal/bin/rihal-hooks.cjs +394 -4
- package/rihal/bin/rihal-tools.cjs +891 -24
- package/rihal/commands/create-prd.md +18 -0
- package/rihal/commands/execute-milestone.md +18 -0
- package/rihal/commands/plan-milestone.md +18 -0
- package/rihal/commands/scaffold-milestone.md +18 -0
- package/rihal/commands/scaffold-skill.md +18 -0
- package/rihal/references/REFERENCES_INDEX.md +49 -7
- package/rihal/references/agent-contracts.md +10 -0
- package/rihal/references/design-tokens.md +98 -0
- package/rihal/references/docs-auditor-playbook.md +148 -0
- package/rihal/references/git-preflight.md +117 -0
- package/rihal/references/iterative-retrieval.md +85 -0
- package/rihal/references/nyquist-auditor-playbook.md +157 -0
- package/rihal/references/workstream-flag.md +2 -2
- package/rihal/skills/actions/1-analysis/rihal-prfaq/SKILL.md +9 -0
- package/rihal/skills/actions/4-implementation/rihal-checkpoint-preview/SKILL.md +9 -0
- package/rihal/skills/actions/4-implementation/rihal-ci/SKILL.md +4 -0
- package/rihal/skills/actions/4-implementation/rihal-code-review/steps/step-02-review.md +2 -2
- package/rihal/skills/actions/4-implementation/rihal-harden/SKILL.md +4 -0
- package/rihal/skills/actions/4-implementation/rihal-migrate/SKILL.md +4 -0
- package/rihal/skills/agents/haitham-frontend/SKILL.md +2 -0
- package/rihal/templates/settings-hooks.json +39 -0
- package/rihal/workflows/check-todos.md +4 -0
- package/rihal/workflows/code-review-fix.md +4 -3
- package/rihal/workflows/code-review.md +1 -1
- package/rihal/workflows/debug.md +1 -1
- package/rihal/workflows/dev-story.md +4 -0
- package/rihal/workflows/diff.md +2 -2
- package/rihal/workflows/do.md +16 -8
- package/rihal/workflows/docs-update.md +2 -2
- package/rihal/workflows/enable-hooks.md +6 -1
- package/rihal/workflows/execute-milestone.md +139 -0
- package/rihal/workflows/execute-regression-gates.md +1 -1
- package/rihal/workflows/execute-sprint.md +54 -2
- package/rihal/workflows/execute-verify-phase-goal.md +31 -4
- package/rihal/workflows/execute-waves.md +33 -5
- package/rihal/workflows/execute.md +40 -6
- package/rihal/workflows/help.md +1 -1
- package/rihal/workflows/import.md +1 -1
- package/rihal/workflows/lens-audit.md +39 -23
- package/rihal/workflows/list-workspaces.md +1 -1
- package/rihal/workflows/map-codebase.md +4 -4
- package/rihal/workflows/new-milestone.md +18 -1
- package/rihal/workflows/new-project-research.md +53 -1
- package/rihal/workflows/new-workspace.md +1 -1
- package/rihal/workflows/plan-milestone.md +105 -0
- package/rihal/workflows/plan-research-validation.md +1 -1
- package/rihal/workflows/plan-spawn-planner.md +1 -1
- package/rihal/workflows/plan.md +31 -3
- package/rihal/workflows/plant-seed.md +6 -0
- package/rihal/workflows/quick.md +11 -5
- package/rihal/workflows/research-phase.md +24 -0
- package/rihal/workflows/scaffold-milestone.md +60 -0
- package/rihal/workflows/scaffold-skill.md +137 -0
- package/rihal/workflows/scan.md +1 -1
- package/rihal/workflows/session-report.md +43 -3
- package/rihal/workflows/verify-work.md +3 -3
- package/server/dashboard.js +52 -5
- package/server/lib/html/client.js +723 -11
- package/server/lib/html/css.js +2046 -466
- package/server/lib/html/shell.js +227 -134
- package/server/lib/scanner.js +33 -0
- package/server/orchestrator.js +438 -0
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
* pre-edit — verify file was Read before Edit/Write (exit 2 if not)
|
|
7
7
|
* pre-workflow — soft warning for rihal-* commands with suspicious args
|
|
8
8
|
* post-commit — verify commit format and no forbidden patterns
|
|
9
|
+
* bash-guard — block dangerous Bash commands before they run (exit 2)
|
|
10
|
+
* pre-compact — refresh HANDOFF.json before context compaction (#743)
|
|
11
|
+
* stop-verify — syntax-check files changed during the response (#744)
|
|
12
|
+
* cost-track — append per-response token usage to cost.jsonl (#745)
|
|
13
|
+
* compact-nudge — advise /rihal-trim or /clear after N Edit/Write calls (#749)
|
|
9
14
|
*
|
|
10
15
|
* All subcommands read stdin JSON from the hook execution context.
|
|
11
16
|
* Pure Node stdlib. No external dependencies.
|
|
@@ -111,6 +116,8 @@ async function preWorkflow() {
|
|
|
111
116
|
*/
|
|
112
117
|
async function postCommit() {
|
|
113
118
|
try {
|
|
119
|
+
const path = require('path');
|
|
120
|
+
const os = require('os');
|
|
114
121
|
const input = await readInputJson();
|
|
115
122
|
const command = input.tool_input?.command || input.command || '';
|
|
116
123
|
const output = input.tool_input?.output || input.output || '';
|
|
@@ -130,11 +137,27 @@ async function postCommit() {
|
|
|
130
137
|
|
|
131
138
|
let commitMsg = output;
|
|
132
139
|
|
|
133
|
-
// If -F flag used, try to read the message file
|
|
140
|
+
// If -F flag used, try to read the message file — but only if it resolves
|
|
141
|
+
// inside the repo working tree. An attacker-controlled commit command could
|
|
142
|
+
// otherwise point -F at e.g. ~/.ssh/id_rsa. Mirror the resolve + realpathSync
|
|
143
|
+
// + startsWith guard from server/lib/api.js:131-141 (#754).
|
|
134
144
|
const fMatch = command.match(/-F\s+(\S+)/);
|
|
135
|
-
if (fMatch
|
|
145
|
+
if (fMatch) {
|
|
136
146
|
try {
|
|
137
|
-
|
|
147
|
+
const repoRoot = process.cwd();
|
|
148
|
+
const resolved = path.resolve(repoRoot, fMatch[1]);
|
|
149
|
+
// Dereference symlinks so a symlink outside the repo cannot bypass the guard.
|
|
150
|
+
const realPath = fs.realpathSync(resolved);
|
|
151
|
+
const insideRepo = realPath.startsWith(repoRoot + path.sep);
|
|
152
|
+
// Exception: rihal-tools.cjs writes its commit-message tmp file to
|
|
153
|
+
// os.tmpdir() (outside the repo) — see rihal-tools.cjs:3668. That path
|
|
154
|
+
// is rihal-controlled (not attacker input), so allow it explicitly.
|
|
155
|
+
const isRihalCommitMsgTmp =
|
|
156
|
+
realPath.startsWith(fs.realpathSync(os.tmpdir()) + path.sep) &&
|
|
157
|
+
/^rihal-commit-msg-\d+\.txt$/.test(path.basename(realPath));
|
|
158
|
+
if (insideRepo || isRihalCommitMsgTmp) {
|
|
159
|
+
commitMsg += '\n' + fs.readFileSync(resolved, 'utf8');
|
|
160
|
+
}
|
|
138
161
|
} catch {}
|
|
139
162
|
}
|
|
140
163
|
|
|
@@ -175,6 +198,358 @@ async function postCommit() {
|
|
|
175
198
|
}
|
|
176
199
|
}
|
|
177
200
|
|
|
201
|
+
// rm -rf is permitted only against these relative build/cache paths.
|
|
202
|
+
const RM_SAFE_TARGET = /^(?:\.\/)?(?:node_modules|dist|build|coverage|\.next|out|temp|tmp|\.rihal\/cache)(?:\/.*)?$/;
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* bash-guard: Block dangerous Bash commands before they execute.
|
|
206
|
+
* Exit 2 blocks the tool call; exit 0 allows it.
|
|
207
|
+
*
|
|
208
|
+
* Enforces the repo's non-negotiable rules (AGENTS.md): no unapproved
|
|
209
|
+
* `git push`, never `--force`, no `--no-verify`, no unscoped destructive
|
|
210
|
+
* git/rm. An authorized push must be prefixed with `RIHAL_PUSH_OK=1`.
|
|
211
|
+
*
|
|
212
|
+
* This guard is best-effort, NOT a security boundary: a determined caller
|
|
213
|
+
* can still craft a bypass (e.g. obscure git aliases). It enforces AGENTS.md
|
|
214
|
+
* conventions, not a sandbox.
|
|
215
|
+
*/
|
|
216
|
+
async function bashGuard() {
|
|
217
|
+
try {
|
|
218
|
+
const input = await readInputJson();
|
|
219
|
+
const command = (input.tool_input?.command || input.command || '').trim();
|
|
220
|
+
|
|
221
|
+
if (!command) {
|
|
222
|
+
process.exit(0);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const block = (reason, guidance) => {
|
|
226
|
+
console.error(`⛔ BLOCKED by rihal bash-guard: ${reason}`);
|
|
227
|
+
if (guidance) console.error(` ${guidance}`);
|
|
228
|
+
process.exit(2);
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const isPush = /\bgit\s+push\b/.test(command);
|
|
232
|
+
|
|
233
|
+
// A `+`-prefixed refspec (`git push origin +main`) is a force-push that
|
|
234
|
+
// matches neither `--force` nor `-f`. Detect it by scanning the tokens
|
|
235
|
+
// after `push` for a non-flag token starting with `+` (`+` is not a glob
|
|
236
|
+
// or option char, so a leading-`+` token is unambiguously a refspec).
|
|
237
|
+
const isPlusRefspecForce =
|
|
238
|
+
isPush &&
|
|
239
|
+
(() => {
|
|
240
|
+
const tokens = command.split(/\s+/);
|
|
241
|
+
const pushIdx = tokens.findIndex((t) => t === 'push');
|
|
242
|
+
if (pushIdx === -1) return false;
|
|
243
|
+
return tokens
|
|
244
|
+
.slice(pushIdx + 1)
|
|
245
|
+
.some((t) => t.startsWith('+'));
|
|
246
|
+
})();
|
|
247
|
+
|
|
248
|
+
// Force-push is never permitted through an agent.
|
|
249
|
+
if (
|
|
250
|
+
isPush &&
|
|
251
|
+
(/(--force\b|--force-with-lease\b|(?:^|\s)-f\b)/.test(command) ||
|
|
252
|
+
isPlusRefspecForce)
|
|
253
|
+
) {
|
|
254
|
+
block(
|
|
255
|
+
'git push --force is never permitted.',
|
|
256
|
+
'A human must run a force-push manually. See AGENTS.md.'
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Plain git push requires an explicit per-push authorization token.
|
|
261
|
+
// Token must be a real leading env-var assignment — substring match is
|
|
262
|
+
// bypassable via 'echo RIHAL_PUSH_OK; git push'.
|
|
263
|
+
if (isPush && !/^\s*RIHAL_PUSH_OK=1(\s|$)/.test(command)) {
|
|
264
|
+
block(
|
|
265
|
+
'git push requires explicit human approval.',
|
|
266
|
+
'If the user authorized THIS push, prefix the command with RIHAL_PUSH_OK=1. See AGENTS.md.'
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Bypassing git hooks is banned.
|
|
271
|
+
if (/--no-verify\b/.test(command)) {
|
|
272
|
+
block(
|
|
273
|
+
'--no-verify bypasses git hooks.',
|
|
274
|
+
'Fix the underlying hook failure instead of skipping it.'
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Destructive git operations.
|
|
279
|
+
if (/\bgit\s+reset\s+--hard\b/.test(command)) {
|
|
280
|
+
block(
|
|
281
|
+
'git reset --hard discards uncommitted work.',
|
|
282
|
+
'Confirm with the user; they should run it manually if intended.'
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
if (/\bgit\s+clean\s+-[a-zA-Z]*f/.test(command)) {
|
|
286
|
+
block(
|
|
287
|
+
'git clean -f permanently deletes untracked files.',
|
|
288
|
+
'Confirm with the user; they should run it manually if intended.'
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// rm -rf outside the safe build/cache allowlist.
|
|
293
|
+
for (const segment of command.split(/(?:&&|\|\||;|\|)/)) {
|
|
294
|
+
const m = segment.trim().match(/^(?:\S+=\S+\s+)*rm\s+(.+)$/);
|
|
295
|
+
if (!m) continue;
|
|
296
|
+
const tokens = m[1].split(/\s+/).filter(Boolean);
|
|
297
|
+
const flags = tokens.filter((t) => /^-[a-zA-Z]+$/.test(t)).join('');
|
|
298
|
+
if (!(/r/.test(flags) && /f/.test(flags))) continue;
|
|
299
|
+
const targets = tokens.filter((t) => !t.startsWith('-'));
|
|
300
|
+
const unsafe =
|
|
301
|
+
targets.length === 0 ||
|
|
302
|
+
targets.some((t) => {
|
|
303
|
+
if (t.startsWith('/tmp/')) return false;
|
|
304
|
+
if (t.includes('..') || t.includes('*')) return true;
|
|
305
|
+
if (t.startsWith('/') || t.startsWith('~') || t === '.') return true;
|
|
306
|
+
return !RM_SAFE_TARGET.test(t);
|
|
307
|
+
});
|
|
308
|
+
if (unsafe) {
|
|
309
|
+
block(
|
|
310
|
+
`rm -rf targets a path outside the safe allowlist: ${targets.join(', ') || '(none)'}`,
|
|
311
|
+
'Safe targets: node_modules, dist, build, temp, /tmp/*. Confirm anything else with the user.'
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
process.exit(0);
|
|
317
|
+
} catch (err) {
|
|
318
|
+
console.error(`Hook error: ${err.message}`);
|
|
319
|
+
process.exit(1);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* pre-compact: Refresh HANDOFF.json before context compaction (#743).
|
|
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.
|
|
330
|
+
*/
|
|
331
|
+
async function preCompact() {
|
|
332
|
+
try {
|
|
333
|
+
const path = require('path');
|
|
334
|
+
await readInputJson(); // drain the PreCompact event payload
|
|
335
|
+
|
|
336
|
+
const cwd = process.cwd();
|
|
337
|
+
const statePath = path.join(cwd, '.rihal', 'state.json');
|
|
338
|
+
if (!fs.existsSync(statePath)) {
|
|
339
|
+
process.exit(0);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
let state;
|
|
343
|
+
try {
|
|
344
|
+
state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
345
|
+
} catch {
|
|
346
|
+
process.exit(0);
|
|
347
|
+
}
|
|
348
|
+
|
|
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
|
+
);
|
|
360
|
+
|
|
361
|
+
if (!hasActivePhase) {
|
|
362
|
+
process.exit(0);
|
|
363
|
+
}
|
|
364
|
+
|
|
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;
|
|
373
|
+
|
|
374
|
+
const handoff = {
|
|
375
|
+
generated_at: new Date().toISOString(),
|
|
376
|
+
reason: 'pre-compact',
|
|
377
|
+
phase: phaseLabel,
|
|
378
|
+
current_plan: state.current_plan ?? null,
|
|
379
|
+
current_sprint: state.current_sprint ?? null,
|
|
380
|
+
};
|
|
381
|
+
|
|
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);
|
|
386
|
+
|
|
387
|
+
process.exit(0);
|
|
388
|
+
} catch (err) {
|
|
389
|
+
console.error(`Hook error: ${err.message}`);
|
|
390
|
+
process.exit(1);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* stop-verify: Syntax-check files changed during the response (#744).
|
|
396
|
+
*
|
|
397
|
+
* Triggered by the Stop hook. Collects the files changed during the response
|
|
398
|
+
* (from the payload, falling back to `git diff --name-only`) and syntax-checks
|
|
399
|
+
* each .js/.cjs (node --check) and .json (JSON.parse). Surfaces failures to
|
|
400
|
+
* stderr with a non-zero exit. Advisory only — never auto-fixes, never blocks.
|
|
401
|
+
*/
|
|
402
|
+
async function stopVerify() {
|
|
403
|
+
try {
|
|
404
|
+
const path = require('path');
|
|
405
|
+
const { spawnSync } = require('child_process');
|
|
406
|
+
const input = await readInputJson();
|
|
407
|
+
|
|
408
|
+
let changed =
|
|
409
|
+
input.changed_files ||
|
|
410
|
+
input.tool_input?.changed_files ||
|
|
411
|
+
input.files_changed ||
|
|
412
|
+
null;
|
|
413
|
+
|
|
414
|
+
if (!Array.isArray(changed)) {
|
|
415
|
+
const diff = spawnSync('git', ['diff', '--name-only'], {
|
|
416
|
+
encoding: 'utf8',
|
|
417
|
+
cwd: process.cwd(),
|
|
418
|
+
});
|
|
419
|
+
changed =
|
|
420
|
+
diff.status === 0
|
|
421
|
+
? diff.stdout.split('\n').map((l) => l.trim()).filter(Boolean)
|
|
422
|
+
: [];
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (changed.length === 0) {
|
|
426
|
+
process.exit(0);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const failures = [];
|
|
430
|
+
for (const file of changed) {
|
|
431
|
+
const abs = path.isAbsolute(file)
|
|
432
|
+
? file
|
|
433
|
+
: path.resolve(process.cwd(), file);
|
|
434
|
+
if (!fs.existsSync(abs)) continue;
|
|
435
|
+
const ext = path.extname(abs).toLowerCase();
|
|
436
|
+
if (ext === '.js' || ext === '.cjs' || ext === '.mjs') {
|
|
437
|
+
const check = spawnSync(process.execPath, ['--check', abs], {
|
|
438
|
+
encoding: 'utf8',
|
|
439
|
+
});
|
|
440
|
+
if (check.status !== 0) {
|
|
441
|
+
failures.push(`${file}: ${(check.stderr || '').trim().split('\n')[0]}`);
|
|
442
|
+
}
|
|
443
|
+
} else if (ext === '.json') {
|
|
444
|
+
try {
|
|
445
|
+
JSON.parse(fs.readFileSync(abs, 'utf8'));
|
|
446
|
+
} catch (e) {
|
|
447
|
+
failures.push(`${file}: ${e.message}`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (failures.length > 0) {
|
|
453
|
+
console.error('⚠ stop-verify: changed files failed syntax check:');
|
|
454
|
+
failures.forEach((f) => console.error(` • ${f}`));
|
|
455
|
+
process.exit(1);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
process.exit(0);
|
|
459
|
+
} catch (err) {
|
|
460
|
+
console.error(`Hook error: ${err.message}`);
|
|
461
|
+
process.exit(1);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* cost-track: Append per-response token usage to cost.jsonl (#745).
|
|
467
|
+
*
|
|
468
|
+
* Triggered by the Stop hook. Extracts the token usage block from the Stop
|
|
469
|
+
* event payload and appends one JSON line to .rihal/telemetry/cost.jsonl so
|
|
470
|
+
* session-report can report measured totals. No-op when no usage block is
|
|
471
|
+
* present. Never blocks.
|
|
472
|
+
*/
|
|
473
|
+
async function costTrack() {
|
|
474
|
+
try {
|
|
475
|
+
const path = require('path');
|
|
476
|
+
const input = await readInputJson();
|
|
477
|
+
|
|
478
|
+
const usage = input.usage || input.tool_input?.usage || null;
|
|
479
|
+
if (!usage || typeof usage !== 'object') {
|
|
480
|
+
process.exit(0);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const record = {
|
|
484
|
+
ts: new Date().toISOString(),
|
|
485
|
+
input_tokens: usage.input_tokens ?? 0,
|
|
486
|
+
output_tokens: usage.output_tokens ?? 0,
|
|
487
|
+
};
|
|
488
|
+
if (usage.cache_creation_input_tokens != null) {
|
|
489
|
+
record.cache_creation_input_tokens = usage.cache_creation_input_tokens;
|
|
490
|
+
}
|
|
491
|
+
if (usage.cache_read_input_tokens != null) {
|
|
492
|
+
record.cache_read_input_tokens = usage.cache_read_input_tokens;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const telemetryDir = path.join(process.cwd(), '.rihal', 'telemetry');
|
|
496
|
+
fs.mkdirSync(telemetryDir, { recursive: true });
|
|
497
|
+
fs.appendFileSync(
|
|
498
|
+
path.join(telemetryDir, 'cost.jsonl'),
|
|
499
|
+
JSON.stringify(record) + '\n'
|
|
500
|
+
);
|
|
501
|
+
|
|
502
|
+
process.exit(0);
|
|
503
|
+
} catch (err) {
|
|
504
|
+
console.error(`Hook error: ${err.message}`);
|
|
505
|
+
process.exit(1);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* compact-nudge: Advise /rihal-trim or /clear after N Edit/Write calls (#749).
|
|
511
|
+
*
|
|
512
|
+
* Triggered by the PreToolUse:Edit|Write hook. Maintains a per-session call
|
|
513
|
+
* counter in a temp file and, once the count crosses RIHAL_NUDGE_THRESHOLD
|
|
514
|
+
* (default 50), prints an advisory to reclaim context budget. Purely
|
|
515
|
+
* advisory — always exits 0, never blocks a tool call.
|
|
516
|
+
*/
|
|
517
|
+
async function compactNudge() {
|
|
518
|
+
try {
|
|
519
|
+
const path = require('path');
|
|
520
|
+
const os = require('os');
|
|
521
|
+
const input = await readInputJson();
|
|
522
|
+
|
|
523
|
+
const sessionId =
|
|
524
|
+
input.session_id || input.tool_input?.session_id || 'default';
|
|
525
|
+
const counterPath = path.join(
|
|
526
|
+
os.tmpdir(),
|
|
527
|
+
'rihal-nudge-' + sessionId + '.count'
|
|
528
|
+
);
|
|
529
|
+
|
|
530
|
+
let count = 0;
|
|
531
|
+
try {
|
|
532
|
+
count = parseInt(fs.readFileSync(counterPath, 'utf8').trim(), 10) || 0;
|
|
533
|
+
} catch {}
|
|
534
|
+
count += 1;
|
|
535
|
+
try {
|
|
536
|
+
fs.writeFileSync(counterPath, String(count));
|
|
537
|
+
} catch {}
|
|
538
|
+
|
|
539
|
+
const threshold = parseInt(process.env.RIHAL_NUDGE_THRESHOLD, 10) || 50;
|
|
540
|
+
if (count >= threshold) {
|
|
541
|
+
console.error(
|
|
542
|
+
`⚠ rihal compact-nudge: ${count} edits this session. Consider /rihal-trim or /clear to reclaim context budget.`
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
process.exit(0);
|
|
547
|
+
} catch {
|
|
548
|
+
// Advisory hook must never break the session.
|
|
549
|
+
process.exit(0);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
178
553
|
/**
|
|
179
554
|
* Main entry point.
|
|
180
555
|
*/
|
|
@@ -191,9 +566,24 @@ async function main() {
|
|
|
191
566
|
case 'post-commit':
|
|
192
567
|
await postCommit();
|
|
193
568
|
break;
|
|
569
|
+
case 'bash-guard':
|
|
570
|
+
await bashGuard();
|
|
571
|
+
break;
|
|
572
|
+
case 'pre-compact':
|
|
573
|
+
await preCompact();
|
|
574
|
+
break;
|
|
575
|
+
case 'stop-verify':
|
|
576
|
+
await stopVerify();
|
|
577
|
+
break;
|
|
578
|
+
case 'cost-track':
|
|
579
|
+
await costTrack();
|
|
580
|
+
break;
|
|
581
|
+
case 'compact-nudge':
|
|
582
|
+
await compactNudge();
|
|
583
|
+
break;
|
|
194
584
|
default:
|
|
195
585
|
console.error(`Unknown subcommand: ${subcommand}`);
|
|
196
|
-
console.error('Usage: rihal-hooks.cjs pre-edit|pre-workflow|post-commit');
|
|
586
|
+
console.error('Usage: rihal-hooks.cjs pre-edit|pre-workflow|post-commit|bash-guard|pre-compact|stop-verify|cost-track|compact-nudge');
|
|
197
587
|
process.exit(1);
|
|
198
588
|
}
|
|
199
589
|
}
|