#!/usr/bin/env node /** * Stop 合并调度器 * 合并 6 个 Stop hooks 为单进程,减少 5 次 Node.js 进程启动 (~200-250ms) * * 合并清单: * 1. route-auditor.js (hooks/) — 路由审计 + 反馈闭环 * 1b. session-pin.js (scripts/) — 钉住命名会话防清理 * 2. auto-cleanup.js (scripts/) — 磁盘清理 * 3. daily-health-snapshot.js (scripts/) — 健康快照 * 4. implicit-feedback.js (scripts/) — 隐式反馈推断 * 5. constitution-session-report.js (hooks/) — 宪法违规摘要 * 6. log-rotator.js (hooks/) — 日志轮转 * * 所有子模块 fail-open: 任何异常不影响其他模块 * 退出码: 始终 0 * * stdin: { session_id, transcript_path, hook_event_name: "Stop" } */ const path = require('path'); const fs = require('fs'); const readStdin = require('./lib/read-stdin.js'); readStdin({ maxSize: 128 * 1024 }).then(input => { runAll(input); }).catch(() => { // stdin 解析失败也尝试运行 (Stop hooks 通常不依赖 stdin 内容) runAll({}); }); // GH-1: hook-errors.log JSONL 去重聚合 function deduplicateHookErrors() { const errLog = require('path').join(require('./lib/root.js'), 'debug', 'hook-errors.log'); if (!require('fs').existsSync(errLog)) return; try { const content = require('fs').readFileSync(errLog, 'utf8'); if (content.length < 100) return; const entries = {}; const blocks = content.split(/\n(?=[A-Z\[])/); for (const block of blocks) { if (!block.trim()) continue; const firstLine = block.split('\n')[0].trim(); const key = firstLine.slice(0, 100); if (!entries[key]) entries[key] = { firstLine: key, count: 0, lastSeen: new Date().toISOString() }; entries[key].count++; } const jsonl = Object.values(entries) .map(e => JSON.stringify(e)) .join('\n') + '\n'; const tmp = errLog + '.tmp.' + process.pid; require('fs').writeFileSync(tmp, jsonl); require('fs').renameSync(tmp, errLog); } catch {} } async function runAll(input) { // P0V2_PARALLEL_BATCHES (2026-04-19 performance-expert) // 14 子模块串行 (P99=12.1s) → 三批编排 (Batch1 轻量并行 / Batch2 学习管线 / Batch3 尾部串行) // fail-open: 任一阶段 reject/timeout 不阻断后续 batch; 记录 debug/hook-timeout.log const { runStageWithTimeout: race } = require('./lib/run-stage.js'); const _perf_start = Date.now(); const _stageRecords = []; const _record = (rs) => { for (const r of rs) if (r && r.status === 'fulfilled') _stageRecords.push(r.value); }; // ─── Batch 1 · 轻量 I/O 并行 (500-800ms) ─── // 无互相依赖: 各自读写不同文件 (route-feedback.jsonl / pinned-sessions.json / session-* / hook-errors.log / constitution-report) _record(await Promise.allSettled([ race('route-auditor', () => { const ra = require('./route-auditor.js'); if (ra.runAudit) ra.runAudit(); }, 800), race('session-pin', () => { const sp = require('../scripts/session-pin.js'); if (sp.pinSession) sp.pinSession(input); }, 500), race('session-memory', () => { const sm = require('../scripts/session-memory.js'); if (sm.cleanExpiredSessions) sm.cleanExpiredSessions(); }, 800), race('dedup-errors', () => deduplicateHookErrors(), 500), race('constitution', () => { const csr = require('./constitution-session-report.js'); if (csr.runReport) csr.runReport(); }, 800), ])); // ─── Batch 2 · 学习管线 + 巡检 并行 (2-15s) ─── // implicit-feedback → fusion-weight-learner 链式 (读后写依赖) // daily-health / skill-effectiveness 各自独立冷却, 与学习管线并发无冲突 _record(await Promise.allSettled([ race('implicit→fwl', async () => { try { const imf = require('../scripts/implicit-feedback.js'); const fn = imf.generateImplicitFeedback || imf.inferAndWrite; if (fn) fn({ days: 1 }); } catch {} try { const fwl = require('../scripts/fusion-weight-learner.js'); if (fwl.atomicWeightUpdate) { fwl.atomicWeightUpdate(); } else { if (fwl.bootstrapWeights) fwl.bootstrapWeights(); if (fwl.learnFusionWeights) fwl.learnFusionWeights(); if (fwl.applyImplicitWeights) fwl.applyImplicitWeights(); } } catch {} }, 15000), race('daily-health', () => { const dhs = require('../scripts/daily-health-snapshot.js'); if (dhs.main) dhs.main(); }, 2000), race('skill-effectiveness', () => { const se = require('../scripts/skill-effectiveness.js'); if (!se.analyze) return; const reportFile = path.join(require('./lib/root.js'), 'debug', 'skill-effectiveness-report.json'); const lastRun = fs.existsSync(reportFile) ? fs.statSync(reportFile).mtimeMs : 0; if (Date.now() - lastRun > 23 * 60 * 60 * 1000) se.analyze({ save: true }); }, 1500), ])); // ─── Batch 3 · 尾部串行 (明确依赖顺序) ─── // sentinel append evolution-log → cleanup 才能 truncate // auto-backup → auto-git-push (快照先于远端推送) _stageRecords.push(await race('consistency-sentinel', () => runConsistencySentinel(), 1000)); _stageRecords.push(await race('auto-cleanup', () => { const ac = require('../scripts/auto-cleanup.js'); if (ac.main) ac.main({ execute: true, ifStale: 86400 }); }, 5000)); _stageRecords.push(await race('log-rotator', () => { const lr = require('./log-rotator.js'); if (lr.runRotation) lr.runRotation(); }, 800)); _stageRecords.push(await race('auto-backup', () => { const backup = require('../scripts/auto-backup.js'); backup(); }, 3000)); _stageRecords.push(await race('auto-git-push', () => { const sync = require('../scripts/auto-git-sync.js'); sync.pushChanges(); }, 5000)); // ─── 慢 Stop 总耗时监控 + W3 告警 (保持原语义) ─── try { const _total = Date.now() - _perf_start; if (_total > 2000) { const debugDir = path.join(require('./lib/root.js'), 'debug'); const errLog = path.join(debugDir, 'hook-slow.log'); fs.appendFileSync(errLog, JSON.stringify({ ts: new Date().toISOString(), hook: 'stop-dispatcher', totalMs: _total, stages: _stageRecords.map(r => ({ n: r.name, ok: r.ok, ms: r.ms })), }) + '\n'); try { const alertSentinel = path.join(debugDir, '.slow-alert.sentinel'); const now = Date.now(); let lastAlert = 0; try { if (fs.existsSync(alertSentinel)) lastAlert = fs.statSync(alertSentinel).mtimeMs || 0; } catch {} if (now - lastAlert > 60000) { process.stderr.write('[stop-dispatcher] 慢 Stop 事件 (' + _total + 'ms) — 详见 debug/hook-slow.log\n'); try { const _tmp = alertSentinel + '.tmp.' + process.pid; fs.writeFileSync(_tmp, String(now)); fs.renameSync(_tmp, alertSentinel); } catch {} } } catch {} } } catch {} process.exit(0); } // consistency-sentinel 拆出为独立函数 (原 L171-232, P0v2 重构) function runConsistencySentinel() { try { const crypto = require('crypto'); const sRoot = require('./lib/root.js'); const evoLog = path.join(sRoot, 'evolution-log.jsonl'); const findings = []; try { const ffPath = path.join(sRoot, 'feature-flags.json'); const sigPath = path.join(sRoot, 'feature-flags.json.sig'); if (fs.existsSync(ffPath) && fs.existsSync(sigPath)) { const actual = crypto.createHash('sha256').update(fs.readFileSync(ffPath)).digest('hex'); const stored = fs.readFileSync(sigPath, 'utf8').trim(); if (actual !== stored) { const tmp = sigPath + '.tmp.' + process.pid; fs.writeFileSync(tmp, actual); fs.renameSync(tmp, sigPath); findings.push({ id: 'sig-drift', fix: 'auto', detail: 'feature-flags.sig 重签 ' + actual.slice(0, 8) }); } } } catch {} try { const statsPath = path.join(sRoot, 'stats-compiled.json'); const skillsDir = path.join(sRoot, 'skills'); if (fs.existsSync(statsPath) && fs.existsSync(skillsDir)) { const stats = JSON.parse(fs.readFileSync(statsPath, 'utf8')); const actual = fs.readdirSync(skillsDir, { withFileTypes: true }) .filter(d => d.isDirectory() && !d.name.startsWith('_') && !d.name.startsWith('.')) .filter(d => fs.existsSync(path.join(skillsDir, d.name, 'SKILL.md'))) .length; const declared = stats && stats.summary && stats.summary.skills; if (declared && declared !== actual) { findings.push({ id: 'skills-drift', fix: 'todo', detail: 'stats=' + declared + ' actual=' + actual + ', 需跑 generate-stats.js' }); } } } catch {} if (findings.length > 0 && fs.existsSync(evoLog)) { const evoContent = fs.readFileSync(evoLog, 'utf8'); const lines = evoContent.trim().split('\n'); let maxSeq = 0; for (const line of lines) { try { const e = JSON.parse(line); if (e.seq > maxSeq) maxSeq = e.seq; } catch {} } const fixed = findings.filter(f => f.fix === 'auto').length; const entry = { seq: maxSeq + 1, ts: new Date().toISOString().slice(0, 10), version: 'v6.5.1', scope: 'consistency-sentinel', trigger: 'stop-dispatcher', summary: 'T1 自动巡检: ' + findings.map(f => f.id + '(' + f.fix + ')').join(', ') + ' | ' + findings.map(f => f.detail).join(' | '), fix_count: fixed, tags: ['learning-loop', 'auto-sentinel'] }; const needsNewline = evoContent.length > 0 && !evoContent.endsWith('\n'); fs.appendFileSync(evoLog, (needsNewline ? '\n' : '') + JSON.stringify(entry) + '\n'); } } catch {} }