@routinecrew/routinecode 2.31.45 → 2.31.47

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.
Files changed (80) hide show
  1. package/bin/prompt-vault.js +1 -1
  2. package/bin/setup.js +1 -1
  3. package/hooks/hooks-config.json +5 -1
  4. package/hooks/lib/known_solution.py +148 -0
  5. package/hooks/lib/receipt.py +2 -0
  6. package/hooks/lib/time_savings.py +137 -3
  7. package/hooks/post-bash-knowledge.sh +45 -0
  8. package/hooks/pre-agent-spawn.mjs +1 -1
  9. package/hooks/stop-usage-metrics.sh +9 -8
  10. package/hooks/user-prompt-submit.sh +16 -2
  11. package/mcp-db-manager/dist/analyzers/duplicate-detector.js +1 -1
  12. package/mcp-db-manager/dist/core/model-parser.js +1 -1
  13. package/mcp-db-manager/dist/core/schema-reader.js +1 -1
  14. package/mcp-db-manager/dist/executors/migration-executor.js +1 -1
  15. package/mcp-db-manager/dist/generators/migration-generator.js +1 -1
  16. package/mcp-db-manager/dist/index.js +1 -1
  17. package/mcp-db-manager/dist/validators/schema-validator.js +1 -1
  18. package/mcp-dev-tester/dist/analyzers/auth-diagnoser.js +1 -1
  19. package/mcp-dev-tester/dist/analyzers/backend-analyzer.js +1 -1
  20. package/mcp-dev-tester/dist/analyzers/endpoint-diagnoser.js +1 -1
  21. package/mcp-dev-tester/dist/analyzers/frontend-analyzer.js +1 -1
  22. package/mcp-dev-tester/dist/analyzers/runtime-diagnoser.js +1 -1
  23. package/mcp-dev-tester/dist/analyzers/type-comparator.js +1 -1
  24. package/mcp-dev-tester/dist/analyzers/validation-diagnoser.js +1 -1
  25. package/mcp-dev-tester/dist/browser/playwright-manager.js +1 -1
  26. package/mcp-dev-tester/dist/browser/ui-tester.js +1 -1
  27. package/mcp-dev-tester/dist/config/project-config.js +1 -1
  28. package/mcp-dev-tester/dist/index.js +1 -1
  29. package/mcp-dev-tester/dist/testers/api-tester.js +1 -1
  30. package/mcp-doc-fetcher/dist/index.js +1 -1
  31. package/mcp-html-renderer/dist/index.js +1 -1
  32. package/mcp-html-renderer/dist/preview.js +1 -1
  33. package/mcp-html-renderer/dist/template-engine.js +1 -1
  34. package/mcp-intelligence/dist/concept-catalog.js +1 -1
  35. package/mcp-intelligence/dist/concept-server-proxy.js +1 -1
  36. package/mcp-intelligence/dist/config-auth.js +1 -1
  37. package/mcp-intelligence/dist/contracts.generated.js +1 -1
  38. package/mcp-intelligence/dist/domain-templates.js +1 -1
  39. package/mcp-intelligence/dist/grounding.js +1 -1
  40. package/mcp-intelligence/dist/handlers.js +1 -1
  41. package/mcp-intelligence/dist/index.js +1 -1
  42. package/mcp-intelligence/dist/intelligence-file.js +1 -1
  43. package/mcp-intelligence/dist/knowledge-critic.js +1 -1
  44. package/mcp-intelligence/dist/knowledge-handlers.js +1 -1
  45. package/mcp-intelligence/dist/mention-extractor.js +1 -1
  46. package/mcp-intelligence/dist/provenance.js +1 -1
  47. package/mcp-intelligence/dist/semantic-embedder.js +1 -1
  48. package/mcp-log-watcher/dist/config-auth.js +1 -1
  49. package/mcp-log-watcher/dist/file-watcher.js +1 -1
  50. package/mcp-log-watcher/dist/index.js +1 -1
  51. package/mcp-log-watcher/dist/known-solutions.js +1 -1
  52. package/mcp-log-watcher/dist/log-store.js +1 -1
  53. package/mcp-log-watcher/dist/websocket-server.js +1 -1
  54. package/mcp-plan-manager/dist/__tests__/plan-parser.test.js +1 -1
  55. package/mcp-plan-manager/dist/file-ops.js +1 -1
  56. package/mcp-plan-manager/dist/index.js +1 -1
  57. package/mcp-plan-manager/dist/plan-parser.js +1 -1
  58. package/mcp-project-context/dist/git-ops.js +1 -1
  59. package/mcp-project-context/dist/index.js +1 -1
  60. package/mcp-project-context/dist/project-analysis.js +1 -1
  61. package/mcp-routinecode/dist/agent-tracker.js +1 -1
  62. package/mcp-routinecode/dist/blast-radius.js +1 -1
  63. package/mcp-routinecode/dist/config-auth.js +1 -1
  64. package/mcp-routinecode/dist/contracts.generated.js +1 -1
  65. package/mcp-routinecode/dist/explorer-advisor.js +1 -1
  66. package/mcp-routinecode/dist/failure-tracker.js +1 -1
  67. package/mcp-routinecode/dist/file-lock-registry.js +1 -1
  68. package/mcp-routinecode/dist/file-risk-scorer.js +1 -1
  69. package/mcp-routinecode/dist/index.js +1 -1
  70. package/mcp-routinecode/dist/known-solutions.js +1 -1
  71. package/mcp-routinecode/dist/ledger-miner.js +1 -1
  72. package/mcp-routinecode/dist/observed-draft.js +1 -1
  73. package/mcp-routinecode/dist/pattern-matcher.js +1 -1
  74. package/mcp-routinecode/dist/routines.js +1 -1
  75. package/mcp-routinecode/dist/rule-engine.js +1 -1
  76. package/mcp-routinecode/dist/rule-promoter.js +1 -1
  77. package/mcp-routinecode/dist/telemetry-analyzer.js +1 -1
  78. package/mcp-routinecode/dist/verification.js +0 -0
  79. package/mcp-routinecode/dist/viral-loop.js +1 -1
  80. package/package.json +1 -1
@@ -0,0 +1,148 @@
1
+ # -*- coding: utf-8 -*-
2
+ """[KNOWN-SOLUTION] 주입 공유 모듈 — error-fix 지식(events.jsonl)의 결정적 소비 경로.
3
+
4
+ 설계: .routinecode/plans/design-knowledge-consumption-loop.md (design score 96)
5
+ 사용처: user-prompt-submit.sh(L1, additionalContext) / post-bash-knowledge.sh(L2, systemMessage)
6
+
7
+ 원칙: 보수 이중 문턱(12자+ 공통 부분열 OR 토큰 자카드>=0.5), 1건만, 저장된 지식 필드만 인용
8
+ (사용자 프롬프트 원문 비포함), 세션x시그니처 1회(dedup 마커), 실패 시 조용히 None(fail-open).
9
+ """
10
+ import hashlib
11
+ import json
12
+ import os
13
+ import re
14
+ import time
15
+
16
+ KN_DIR = os.path.expanduser('~/.routinecode/knowledge')
17
+ EVENTS = os.path.join(KN_DIR, 'events.jsonl')
18
+ MAX_ROWS = 5000
19
+
20
+ ERROR_HINT = re.compile(
21
+ r'(Error|에러|실패|Exception|Cannot |undefined|TypeError|Traceback|ERR_|error:)', re.I)
22
+
23
+
24
+ def _normalize(s):
25
+ s = str(s or '').lower()
26
+ s = re.sub(r'/[\w./\-]{8,}', '*', s) # 절대경로 → *
27
+ s = re.sub(r'\d+', '#', s) # 숫자 → #
28
+ s = re.sub(r'\s+', ' ', s).strip()
29
+ return s
30
+
31
+
32
+ def _tokens(s):
33
+ return set(re.findall(r'[a-z가-힣_]{3,}', s))
34
+
35
+
36
+ def _jaccard(a, b):
37
+ if not a or not b:
38
+ return 0.0
39
+ return len(a & b) / len(a | b)
40
+
41
+
42
+ def _common_substr_len(a, b):
43
+ """최장 공통 부분열 길이 (O(n*m) — 시그니처 400자 캡이라 충분히 빠름)."""
44
+ a, b = a[:400], b[:400]
45
+ if not a or not b:
46
+ return 0
47
+ prev = [0] * (len(b) + 1)
48
+ best = 0
49
+ for i in range(1, len(a) + 1):
50
+ cur = [0] * (len(b) + 1)
51
+ for j in range(1, len(b) + 1):
52
+ if a[i - 1] == b[j - 1]:
53
+ cur[j] = prev[j - 1] + 1
54
+ if cur[j] > best:
55
+ best = cur[j]
56
+ prev = cur
57
+ return best
58
+
59
+
60
+ def _tenant_ok(entry_tid):
61
+ """pre-read-inject _tenant_ok와 동일 의미(무태그=통과). 현 events.jsonl 전행 무태그 — 미래 대비."""
62
+ legacy = entry_tid in (None, 0, '0', '')
63
+ if legacy:
64
+ return True
65
+ try:
66
+ cfg = json.load(open(os.path.expanduser('~/.routinecode/config.json')))
67
+ my = cfg.get('tenantId')
68
+ except Exception:
69
+ my = None
70
+ if my in (None, 0, '0', ''):
71
+ return False
72
+ return str(entry_tid) == str(my)
73
+
74
+
75
+ def find_known_solution(error_text):
76
+ """에러 텍스트 → 최고 매칭 error-fix 1건 또는 None."""
77
+ try:
78
+ if not error_text or not ERROR_HINT.search(error_text):
79
+ return None
80
+ if not os.path.exists(EVENTS):
81
+ return None
82
+ q_norm = _normalize(error_text)
83
+ q_tok = _tokens(q_norm)
84
+ best, best_score = None, 0.0
85
+ n = 0
86
+ with open(EVENTS, errors='ignore') as f:
87
+ for line in f:
88
+ n += 1
89
+ if n > MAX_ROWS:
90
+ break
91
+ try:
92
+ e = json.loads(line)
93
+ except Exception:
94
+ continue
95
+ sig = e.get('errorSignature')
96
+ if not sig or not e.get('rootCause') or not _tenant_ok(e.get('tenantId')):
97
+ continue
98
+ s_norm = _normalize(sig)
99
+ jac = _jaccard(q_tok, _tokens(s_norm))
100
+ sub = _common_substr_len(q_norm, s_norm)
101
+ if sub >= 12 or jac >= 0.5:
102
+ score = jac + min(sub, 40) / 40.0
103
+ if score > best_score:
104
+ best, best_score = e, score
105
+ return best
106
+ except Exception:
107
+ return None
108
+
109
+
110
+ def build_injection(e):
111
+ root = str(e.get('rootCause', ''))[:120]
112
+ fix = str(e.get('fixApproach', ''))[:120]
113
+ txt = '[KNOWN-SOLUTION] 과거 동일 에러 해결 이력 — 원인: %s / 해결: %s' % (root, fix)
114
+ sid = e.get('serverId')
115
+ if isinstance(sid, int) or (isinstance(sid, str) and sid.isdigit()):
116
+ # resolvedFromId 는 number 타입 (mcp-intelligence index.ts:164)
117
+ txt += ' (검증 시 record_verification resolvedFromId=%d)' % int(sid)
118
+ return txt
119
+
120
+
121
+ def _sig8(e):
122
+ return hashlib.sha1(_normalize(e.get('errorSignature', '')).encode()).hexdigest()[:8]
123
+
124
+
125
+ def consume_once(session_id, error_text):
126
+ """조회+dedup — 세션x시그니처당 1회만 주입문 반환, 그 외 None."""
127
+ e = find_known_solution(error_text)
128
+ if not e:
129
+ return None
130
+ marker = os.path.join(KN_DIR, '.ks-%s-%s' % (str(session_id)[:12], _sig8(e)))
131
+ if os.path.exists(marker):
132
+ return None
133
+ try:
134
+ os.makedirs(KN_DIR, exist_ok=True)
135
+ open(marker, 'w').write(str(int(time.time())))
136
+ # GC: 24h 초과 .ks-* 자가 정리 (별도 cron 불요 — 설계 v2-2)
137
+ cutoff = time.time() - 86400
138
+ for fn in os.listdir(KN_DIR):
139
+ if fn.startswith('.ks-'):
140
+ fp = os.path.join(KN_DIR, fn)
141
+ try:
142
+ if os.path.getmtime(fp) < cutoff:
143
+ os.remove(fp)
144
+ except Exception:
145
+ pass
146
+ except Exception:
147
+ pass
148
+ return build_injection(e)
@@ -201,6 +201,8 @@ def render(r):
201
201
  parts = []
202
202
  if t.get("measured_s"):
203
203
  parts.append("실측 %s [병렬 서브에이전트]" % _fmt_dur(t["measured_s"]))
204
+ if t.get("paired_s"):
205
+ parts.append("실측쌍 %s [탐색 cold→warm 동일모듈]" % _fmt_dur(t["paired_s"]))
204
206
  if t.get("anchored_s"):
205
207
  parts.append("앵커 %s [지식·루틴 — 실측앵커 하한]" % _fmt_dur(t["anchored_s"]))
206
208
  if parts:
@@ -23,6 +23,11 @@ _RC = os.path.expanduser('~/.routinecode')
23
23
  RATES_PATH = os.path.join(_RC, 'rdpe', 'time-rates.json')
24
24
  LEDGER = os.path.join(_RC, 'knowledge', 'time-savings.jsonl')
25
25
  TELEMETRY = os.path.join(_RC, 'knowledge', 'agent-telemetry.jsonl')
26
+ # 탐색 시간 cold/warm 장부 — 모듈 단위 실측쌍(같은 모듈의 cold 중앙값 vs warm 실측).
27
+ # "코드 전체 스캔 시간" 절감의 정직한 계산: 양변 전부 실측, 상수 없음.
28
+ EXPLORE_LEDGER = os.path.join(_RC, 'knowledge', 'explore-ledger.jsonl')
29
+ IDLE_GAP_S = 600 # 이 이상 간격은 사람 부재로 보고 탐색 시간에서 제외
30
+ PAIRED_SESSION_CAP_S = 900 # 실측쌍 적립의 세션 상한(보수)
26
31
 
27
32
  DEFAULT_RATES = {
28
33
  "knowledge_error_reuse_s": 60, # C1 폴백 (보수 하한)
@@ -74,20 +79,39 @@ def collect(transcript_path):
74
79
  gate_blocks = 0
75
80
  session_start = None
76
81
  session_end = None
82
+ # 훅 주입 소비(C1/C2) — attachment(hook_success) stdout 마커. 주입문 sha1로 dedup
83
+ # (tool_use 경로와 단일 집합 공유 → 이중계수 차단. 설계 v2-6)
84
+ injected_c1 = set()
85
+ injected_c2 = set()
77
86
 
78
87
  for o in _iter_transcript(transcript_path):
79
88
  ts = o.get('timestamp')
80
89
  if ts:
81
90
  session_start = session_start or ts
82
91
  session_end = ts
92
+ att = o.get('attachment') or {}
93
+ if att.get('type') in ('hook_success', 'hook_blocking_error'):
94
+ out = str(att.get('stdout', ''))[:2000]
95
+ if '[KNOWN-SOLUTION]' in out:
96
+ import hashlib as _h
97
+ injected_c1.add(_h.sha1(out.split('[KNOWN-SOLUTION]')[1][:200].encode()).hexdigest())
98
+ if '[DOMAIN KNOWLEDGE]' in out:
99
+ import hashlib as _h
100
+ injected_c2.add(_h.sha1(out.split('[DOMAIN KNOWLEDGE]')[1][:200].encode()).hexdigest())
83
101
  raw = None
84
102
  for c in (o.get('message', {}).get('content') or []):
85
103
  if not isinstance(c, dict):
86
104
  continue
87
105
  if c.get('type') == 'tool_use':
88
106
  tool_uses.append((c.get('name', ''), c.get('id', '')))
89
- if c.get('name') in ('Edit', 'Write', 'MultiEdit'):
107
+ if c.get('name') in ('Edit', 'Write', 'MultiEdit', 'NotebookEdit'):
90
108
  had_edit = True
109
+ elif c.get('name') == 'Bash':
110
+ # 모델이 Edit 대신 Bash로 파일을 고치는 경우(sed -i/리다이렉션/tee)도 구현 신호
111
+ _cmd = str((c.get('input') or {}).get('command', ''))[:500]
112
+ if ('sed -i' in _cmd or ' tee ' in _cmd or '>>' in _cmd
113
+ or ('>' in _cmd and ('.ts' in _cmd or '.js' in _cmd or '.py' in _cmd or '.json' in _cmd))):
114
+ had_edit = True
91
115
  elif c.get('type') == 'tool_result':
92
116
  txt = c.get('content')
93
117
  results[c.get('tool_use_id', '')] = json.dumps(txt, ensure_ascii=False)[:400] if txt else ''
@@ -117,6 +141,10 @@ def collect(transcript_path):
117
141
  elif short in C4_MAP:
118
142
  c4_s += rates[C4_MAP[short]]
119
143
 
144
+ # 훅 주입 소비 합산 (동일 게이트·상한 적용 — 설계 v2-6)
145
+ c1 += len(injected_c1)
146
+ c2 += len(injected_c2)
147
+
120
148
  # 지식 소비(C1/C2)는 이후 실제 구현(Edit/Write)이 있어야 적립 — 조회만으론 0
121
149
  if not had_edit:
122
150
  c1 = c2 = 0
@@ -131,16 +159,120 @@ def collect(transcript_path):
131
159
  # C3 병렬 — agent-telemetry에서 이 세션 시간창 내 배치 실측
132
160
  measured_s = _parallel_savings(session_start, session_end)
133
161
 
162
+ # C-explore 실측쌍 — 같은 모듈 cold 중앙값 vs 이번 warm 탐색 (양변 실측, 상수 없음)
163
+ paired_s = record_explore(transcript_path)
164
+
134
165
  return {
135
166
  'at': datetime.now(timezone.utc).isoformat(),
136
167
  'transcript': os.path.basename(transcript_path),
137
168
  'measured_s': round(measured_s), # 전액 실측 (병렬)
169
+ 'paired_s': round(paired_s), # 실측쌍 (cold/warm 동일모듈 탐색)
138
170
  'anchored_s': round(anchored_s), # 실측앵커 상수 기반
139
171
  'potential_gate_blocks': gate_blocks, # C5: 적립 0, 잠재 건수만
140
172
  'detail': {'c1_error_reuse': c1, 'c2_domain_use': c2, 'c4_mcp_routine_s': c4_s},
141
173
  }
142
174
 
143
175
 
176
+ def _module_key(path):
177
+ """Read 파일경로 → 모듈 키 (마지막 2세그먼트 디렉토리). 프로젝트 이동에도 안정."""
178
+ try:
179
+ d = os.path.dirname(str(path))
180
+ segs = [s for s in d.split(os.sep) if s]
181
+ return '/'.join(segs[-2:]) if segs else ''
182
+ except Exception:
183
+ return ''
184
+
185
+
186
+ def measure_explore(transcript_path):
187
+ """탐색 구간 실측: 세션 시작 → 첫 구현 행위까지, idle(>IDLE_GAP_S) 제외.
188
+ 반환 {module, warm, explore_s} 또는 None(구현행위/Read 부재 시 — 장부 미기록)."""
189
+ first_ts = None
190
+ prev_ts = None
191
+ explore_s = 0.0
192
+ impl_seen = False
193
+ warm = False
194
+ reads = {}
195
+ for o in _iter_transcript(transcript_path):
196
+ ts = _parse_iso(o.get('timestamp') or '')
197
+ att = o.get('attachment') or {}
198
+ if not impl_seen and att.get('type') in ('hook_success', 'hook_system_message'):
199
+ blob = str(att.get('stdout', '')) + str(att.get('content', ''))
200
+ if '[DOMAIN KNOWLEDGE]' in blob or '[KNOWN-SOLUTION]' in blob:
201
+ warm = True
202
+ for c in (o.get('message', {}).get('content') or []):
203
+ if not isinstance(c, dict) or c.get('type') != 'tool_use':
204
+ continue
205
+ name = c.get('name', '')
206
+ if name in ('Edit', 'Write', 'MultiEdit', 'NotebookEdit'):
207
+ impl_seen = True
208
+ elif name == 'Bash':
209
+ _cmd = str((c.get('input') or {}).get('command', ''))[:500]
210
+ if ('sed -i' in _cmd or ' tee ' in _cmd or '>>' in _cmd
211
+ or ('>' in _cmd and ('.ts' in _cmd or '.js' in _cmd or '.py' in _cmd or '.json' in _cmd))):
212
+ impl_seen = True
213
+ elif name == 'Read' and not impl_seen:
214
+ mk = _module_key((c.get('input') or {}).get('file_path', ''))
215
+ if mk:
216
+ reads[mk] = reads.get(mk, 0) + 1
217
+ elif not impl_seen and ('get_domain_knowledge' in name or 'get_similar_errors' in name
218
+ or 'get_patterns' in name or 'get_known_quirks' in name):
219
+ warm = True
220
+ if ts:
221
+ if first_ts is None:
222
+ first_ts = ts
223
+ if not impl_seen and prev_ts is not None:
224
+ gap = ts - prev_ts
225
+ if 0 < gap <= IDLE_GAP_S:
226
+ explore_s += gap
227
+ prev_ts = ts
228
+ if not impl_seen or not reads or explore_s <= 0:
229
+ return None
230
+ module = max(reads.items(), key=lambda kv: kv[1])[0]
231
+ return {'module': module, 'warm': warm, 'explore_s': round(explore_s, 1)}
232
+
233
+
234
+ def _explore_cold_median(module):
235
+ """같은 모듈의 cold 탐색 실측 중앙값 (장부 이력에서)."""
236
+ vals = []
237
+ if not os.path.exists(EXPLORE_LEDGER):
238
+ return None
239
+ for line in open(EXPLORE_LEDGER, errors='ignore'):
240
+ try:
241
+ r = json.loads(line)
242
+ except Exception:
243
+ continue
244
+ if r.get('module') == module and not r.get('warm') and r.get('explore_s', 0) > 0:
245
+ vals.append(float(r['explore_s']))
246
+ if not vals:
247
+ return None
248
+ vals.sort()
249
+ return vals[len(vals) // 2]
250
+
251
+
252
+ def record_explore(transcript_path):
253
+ """탐색 장부 기록 + warm이면 cold 중앙값과의 실측쌍 절감 반환(초)."""
254
+ m = measure_explore(transcript_path)
255
+ if not m:
256
+ return 0.0
257
+ paired = 0.0
258
+ if m['warm']:
259
+ cold = _explore_cold_median(m['module'])
260
+ if cold is not None and cold > m['explore_s']:
261
+ paired = min(cold - m['explore_s'], PAIRED_SESSION_CAP_S)
262
+ try:
263
+ os.makedirs(os.path.dirname(EXPLORE_LEDGER), exist_ok=True)
264
+ with open(EXPLORE_LEDGER, 'a') as f:
265
+ f.write(json.dumps({
266
+ 'at': datetime.now(timezone.utc).isoformat(),
267
+ 'transcript': os.path.basename(transcript_path),
268
+ 'module': m['module'], 'warm': m['warm'],
269
+ 'explore_s': m['explore_s'], 'paired_saved_s': round(paired),
270
+ }, ensure_ascii=False) + '\n')
271
+ except Exception:
272
+ pass
273
+ return paired
274
+
275
+
144
276
  def _parse_iso(s):
145
277
  try:
146
278
  return datetime.fromisoformat(s.replace('Z', '+00:00')).timestamp()
@@ -180,7 +312,8 @@ def _parallel_savings(session_start, session_end):
180
312
 
181
313
 
182
314
  def append_ledger(snap):
183
- if snap['measured_s'] <= 0 and snap['anchored_s'] <= 0 and snap['potential_gate_blocks'] <= 0:
315
+ if (snap['measured_s'] <= 0 and snap['anchored_s'] <= 0
316
+ and snap.get('paired_s', 0) <= 0 and snap['potential_gate_blocks'] <= 0):
184
317
  return False
185
318
  os.makedirs(os.path.dirname(LEDGER), exist_ok=True)
186
319
  with open(LEDGER, 'a') as f:
@@ -190,7 +323,7 @@ def append_ledger(snap):
190
323
 
191
324
  def totals():
192
325
  """영수증(receipt.py)용 누계."""
193
- t = {'measured_s': 0, 'anchored_s': 0, 'potential_gate_blocks': 0, 'sessions': 0}
326
+ t = {'measured_s': 0, 'paired_s': 0, 'anchored_s': 0, 'potential_gate_blocks': 0, 'sessions': 0}
194
327
  if not os.path.exists(LEDGER):
195
328
  return t
196
329
  for line in open(LEDGER, errors='ignore'):
@@ -199,6 +332,7 @@ def totals():
199
332
  except Exception:
200
333
  continue
201
334
  t['measured_s'] += r.get('measured_s', 0)
335
+ t['paired_s'] += r.get('paired_s', 0)
202
336
  t['anchored_s'] += r.get('anchored_s', 0)
203
337
  t['potential_gate_blocks'] += r.get('potential_gate_blocks', 0)
204
338
  t['sessions'] += 1
@@ -0,0 +1,45 @@
1
+ #!/bin/bash
2
+ # KS(L2): Bash 도구 출력에 에러가 보이면 과거 해결 이력([KNOWN-SOLUTION])을 systemMessage로 주입.
3
+ # 설계: .routinecode/plans/design-knowledge-consumption-loop.md (score 96)
4
+ # - 조회/매칭/dedup 전부 lib/known_solution.py 공유 (L1 user-prompt-submit와 동일 모듈)
5
+ # - PostToolUse에서 systemMessage는 유효 (post-build-check.sh와 동일 포맷)
6
+ # - fail-open: 어떤 실패도 {"continue":true} 무주입
7
+
8
+ INPUT=$(cat)
9
+ HOOKS_DIR="$(cd "$(dirname "$0")" && pwd)"
10
+
11
+ ROUTINECODE_HOOK_INPUT="$INPUT" _HOOK_LIB_DIR="$HOOKS_DIR/lib" python3 - <<'PYEOF' 2>/dev/null || echo '{"continue":true}'
12
+ import json, os, re, sys
13
+
14
+ def emit(obj):
15
+ print(json.dumps(obj, ensure_ascii=False))
16
+ sys.exit(0)
17
+
18
+ try:
19
+ inp = json.loads(os.environ.get('ROUTINECODE_HOOK_INPUT', '') or '{}')
20
+ except Exception:
21
+ emit({"continue": True})
22
+
23
+ session_id = inp.get('session_id') or 'unknown'
24
+ resp = inp.get('tool_response')
25
+ text = json.dumps(resp, ensure_ascii=False) if not isinstance(resp, str) else resp
26
+ if not text:
27
+ emit({"continue": True})
28
+
29
+ # 에러 라인만 추출 (마지막 3줄) — 전체 출력 전달 금지(노이즈·성능)
30
+ err_lines = [l for l in text.split('\\n') if re.search(r'(Error|error:|에러|Traceback|Cannot |ERR_|Exception)', l)]
31
+ if not err_lines:
32
+ emit({"continue": True})
33
+ probe = ' '.join(err_lines[-3:])[:400]
34
+
35
+ try:
36
+ sys.path.insert(0, os.environ.get('_HOOK_LIB_DIR', ''))
37
+ import known_solution
38
+ msg = known_solution.consume_once(session_id, probe)
39
+ except Exception:
40
+ msg = None
41
+
42
+ if msg:
43
+ emit({"continue": True, "systemMessage": msg})
44
+ emit({"continue": True})
45
+ PYEOF