@routinecrew/routinecode 2.31.44 → 2.31.46

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 (81) hide show
  1. package/bin/prompt-vault.js +1 -1
  2. package/bin/setup.js +1 -1
  3. package/hooks/hooks-config.json +13 -0
  4. package/hooks/lib/known_solution.py +148 -0
  5. package/hooks/lib/receipt.py +31 -0
  6. package/hooks/lib/time_savings.py +240 -0
  7. package/hooks/post-bash-knowledge.sh +45 -0
  8. package/hooks/pre-agent-spawn.mjs +1 -1
  9. package/hooks/pre-askuser-guard.sh +32 -0
  10. package/hooks/stop-usage-metrics.sh +9 -0
  11. package/hooks/user-prompt-submit.sh +16 -2
  12. package/mcp-db-manager/dist/analyzers/duplicate-detector.js +1 -1
  13. package/mcp-db-manager/dist/core/model-parser.js +1 -1
  14. package/mcp-db-manager/dist/core/schema-reader.js +1 -1
  15. package/mcp-db-manager/dist/executors/migration-executor.js +1 -1
  16. package/mcp-db-manager/dist/generators/migration-generator.js +1 -1
  17. package/mcp-db-manager/dist/index.js +1 -1
  18. package/mcp-db-manager/dist/validators/schema-validator.js +1 -1
  19. package/mcp-dev-tester/dist/analyzers/auth-diagnoser.js +1 -1
  20. package/mcp-dev-tester/dist/analyzers/backend-analyzer.js +1 -1
  21. package/mcp-dev-tester/dist/analyzers/endpoint-diagnoser.js +1 -1
  22. package/mcp-dev-tester/dist/analyzers/frontend-analyzer.js +1 -1
  23. package/mcp-dev-tester/dist/analyzers/runtime-diagnoser.js +1 -1
  24. package/mcp-dev-tester/dist/analyzers/type-comparator.js +1 -1
  25. package/mcp-dev-tester/dist/analyzers/validation-diagnoser.js +1 -1
  26. package/mcp-dev-tester/dist/browser/playwright-manager.js +1 -1
  27. package/mcp-dev-tester/dist/browser/ui-tester.js +1 -1
  28. package/mcp-dev-tester/dist/config/project-config.js +1 -1
  29. package/mcp-dev-tester/dist/index.js +1 -1
  30. package/mcp-dev-tester/dist/testers/api-tester.js +1 -1
  31. package/mcp-doc-fetcher/dist/index.js +1 -1
  32. package/mcp-html-renderer/dist/index.js +1 -1
  33. package/mcp-html-renderer/dist/preview.js +1 -1
  34. package/mcp-html-renderer/dist/template-engine.js +1 -1
  35. package/mcp-intelligence/dist/concept-catalog.js +1 -1
  36. package/mcp-intelligence/dist/concept-server-proxy.js +1 -1
  37. package/mcp-intelligence/dist/config-auth.js +1 -1
  38. package/mcp-intelligence/dist/contracts.generated.js +1 -1
  39. package/mcp-intelligence/dist/domain-templates.js +1 -1
  40. package/mcp-intelligence/dist/grounding.js +1 -1
  41. package/mcp-intelligence/dist/handlers.js +1 -1
  42. package/mcp-intelligence/dist/index.js +1 -1
  43. package/mcp-intelligence/dist/intelligence-file.js +1 -1
  44. package/mcp-intelligence/dist/knowledge-critic.js +1 -1
  45. package/mcp-intelligence/dist/knowledge-handlers.js +1 -1
  46. package/mcp-intelligence/dist/mention-extractor.js +1 -1
  47. package/mcp-intelligence/dist/provenance.js +1 -1
  48. package/mcp-intelligence/dist/semantic-embedder.js +1 -1
  49. package/mcp-log-watcher/dist/config-auth.js +1 -1
  50. package/mcp-log-watcher/dist/file-watcher.js +1 -1
  51. package/mcp-log-watcher/dist/index.js +1 -1
  52. package/mcp-log-watcher/dist/known-solutions.js +1 -1
  53. package/mcp-log-watcher/dist/log-store.js +1 -1
  54. package/mcp-log-watcher/dist/websocket-server.js +1 -1
  55. package/mcp-plan-manager/dist/__tests__/plan-parser.test.js +1 -1
  56. package/mcp-plan-manager/dist/file-ops.js +1 -1
  57. package/mcp-plan-manager/dist/index.js +1 -1
  58. package/mcp-plan-manager/dist/plan-parser.js +1 -1
  59. package/mcp-project-context/dist/git-ops.js +1 -1
  60. package/mcp-project-context/dist/index.js +1 -1
  61. package/mcp-project-context/dist/project-analysis.js +1 -1
  62. package/mcp-routinecode/dist/agent-tracker.js +1 -1
  63. package/mcp-routinecode/dist/blast-radius.js +1 -1
  64. package/mcp-routinecode/dist/config-auth.js +1 -1
  65. package/mcp-routinecode/dist/contracts.generated.js +1 -1
  66. package/mcp-routinecode/dist/explorer-advisor.js +1 -1
  67. package/mcp-routinecode/dist/failure-tracker.js +1 -1
  68. package/mcp-routinecode/dist/file-lock-registry.js +1 -1
  69. package/mcp-routinecode/dist/file-risk-scorer.js +1 -1
  70. package/mcp-routinecode/dist/index.js +1 -1
  71. package/mcp-routinecode/dist/known-solutions.js +1 -1
  72. package/mcp-routinecode/dist/ledger-miner.js +1 -1
  73. package/mcp-routinecode/dist/observed-draft.js +1 -1
  74. package/mcp-routinecode/dist/pattern-matcher.js +1 -1
  75. package/mcp-routinecode/dist/routines.js +1 -1
  76. package/mcp-routinecode/dist/rule-engine.js +1 -1
  77. package/mcp-routinecode/dist/rule-promoter.js +1 -1
  78. package/mcp-routinecode/dist/telemetry-analyzer.js +1 -1
  79. package/mcp-routinecode/dist/verification.js +0 -0
  80. package/mcp-routinecode/dist/viral-loop.js +1 -1
  81. 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)
@@ -131,6 +131,24 @@ def _fmt_tok(n):
131
131
  return str(n)
132
132
 
133
133
 
134
+ def _fmt_dur(sec):
135
+ sec = int(sec)
136
+ if sec >= 3600:
137
+ return "%dh %dm" % (sec // 3600, (sec % 3600) // 60)
138
+ if sec >= 60:
139
+ return "%dm %ds" % (sec // 60, sec % 60)
140
+ return "%ds" % sec
141
+
142
+
143
+ def _time_totals():
144
+ """Prevented Time 누계 (lib/time_savings.py 원장) — 실패 시 침묵."""
145
+ try:
146
+ import time_savings
147
+ return time_savings.totals()
148
+ except Exception:
149
+ return None
150
+
151
+
134
152
  def build_receipt():
135
153
  """전체 영수증 dict — /receipt 렌더 원천."""
136
154
  r1 = r1_pooled()
@@ -144,6 +162,7 @@ def build_receipt():
144
162
  "r3": {"usd_potential": round(usd_potential, 2),
145
163
  "usd_measured": round(usd_measured, 2),
146
164
  "price_basis": "cache_read $%.2f/M (%s)" % (cr, pr.get('_note', 'prices.json'))},
165
+ "time": _time_totals(),
147
166
  "snapshot": snapshot()}
148
167
 
149
168
 
@@ -177,6 +196,18 @@ def render(r):
177
196
  % (b["adopted_count"], b["total_uses"]))
178
197
  else:
179
198
  L.append(" 반복 루틴 아직 감지된 반복 없음 (세션 데이터 축적 중)")
199
+ t = r.get("time") or {}
200
+ if t.get("sessions"):
201
+ parts = []
202
+ if t.get("measured_s"):
203
+ parts.append("실측 %s [병렬 서브에이전트]" % _fmt_dur(t["measured_s"]))
204
+ if t.get("anchored_s"):
205
+ parts.append("앵커 %s [지식·루틴 — 실측앵커 하한]" % _fmt_dur(t["anchored_s"]))
206
+ if parts:
207
+ L.append(" 시간 절감 " + " + ".join(parts))
208
+ if t.get("potential_gate_blocks"):
209
+ L.append(" 게이트 선차단 %d건 [잠재 — 실측분포 축적 전 미적립]"
210
+ % t["potential_gate_blocks"])
180
211
  c = r["r3"]
181
212
  if c["usd_measured"] > 0:
182
213
  L.append(" 환산 가치 실측분 ≈ $%.2f [실측]" % c["usd_measured"])
@@ -0,0 +1,240 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Prevented Time — 시간절감 적립기 (돈 계산(receipt.py PRICES)과 동형 구조).
3
+
4
+ 대표 케이스(기준값은 time-rates.json 으로 교체 가능, 전부 '보수 하한' 원칙):
5
+ C1 knowledge_error_reuse : 재발 에러를 지식 조회로 해결. 폴백 60s/건
6
+ (2026-10-13 A/B 벤치: 극소형 레포 T1=66s 가 하한 근거.
7
+ 실측쌍(T1-T2)이 쌓이면 rolling 중앙값으로 자가보정)
8
+ C2 domain_knowledge_use : 도메인지식 소비 후 구현. 40s/건
9
+ (동일 벤치 실측: opus 탐색태스크 api 131→90s, -40s)
10
+ C3 parallel_subagents : 병렬 서브에이전트. 상수 없음 — Σ개별duration - max(duration) 전액 실측
11
+ C4 mcp_routine : 루틴을 MCP 1콜로 대체. 수동 대체 하한 상수
12
+ C5 gate_pre_block : 게이트 선차단. 실측 분포 축적 전 적립 0 — '잠재 건수'로만 표기
13
+ (C6 community 는 서버 매칭 — knowledge-service 측 과제)
14
+
15
+ 원칙: 소비 이벤트가 없으면 적립 0. 실측(C3)과 앵커상수(C1/C2/C4)는 분리 집계·표기.
16
+ """
17
+ import json
18
+ import os
19
+ import sys
20
+ from datetime import datetime, timezone
21
+
22
+ _RC = os.path.expanduser('~/.routinecode')
23
+ RATES_PATH = os.path.join(_RC, 'rdpe', 'time-rates.json')
24
+ LEDGER = os.path.join(_RC, 'knowledge', 'time-savings.jsonl')
25
+ TELEMETRY = os.path.join(_RC, 'knowledge', 'agent-telemetry.jsonl')
26
+
27
+ DEFAULT_RATES = {
28
+ "knowledge_error_reuse_s": 60, # C1 폴백 (보수 하한)
29
+ "domain_knowledge_use_s": 40, # C2 실측 앵커 (2026-10-13 벤치)
30
+ "mcp_build_check_s": 10, # C4: 수동 빌드+에러해석 하한
31
+ "mcp_code_search_s": 15, # C4: find_files/search_code/dependency_graph
32
+ "mcp_blast_radius_s": 60, # C4: 수동 호출체인 추적 하한
33
+ "mcp_routine_other_s": 5,
34
+ "session_cap_anchored_s": 600, # 앵커상수 적립의 세션 상한 (과대적립 방지)
35
+ }
36
+
37
+ C2_TOOLS = ('get_domain_knowledge', 'get_patterns', 'get_known_quirks')
38
+ C1_TOOLS = ('get_similar_errors',)
39
+ C4_MAP = {
40
+ 'build_check': 'mcp_build_check_s',
41
+ 'find_files': 'mcp_code_search_s',
42
+ 'search_code': 'mcp_code_search_s',
43
+ 'get_dependency_graph': 'mcp_code_search_s',
44
+ 'trace_blast_radius': 'mcp_blast_radius_s',
45
+ 'project_state': 'mcp_routine_other_s',
46
+ 'git_diff_summary': 'mcp_routine_other_s',
47
+ }
48
+
49
+
50
+ def _rates():
51
+ try:
52
+ with open(RATES_PATH) as f:
53
+ r = json.load(f)
54
+ return {**DEFAULT_RATES, **r}
55
+ except Exception:
56
+ return dict(DEFAULT_RATES)
57
+
58
+
59
+ def _iter_transcript(path):
60
+ with open(path, errors='ignore') as f:
61
+ for line in f:
62
+ try:
63
+ yield json.loads(line)
64
+ except Exception:
65
+ continue
66
+
67
+
68
+ def collect(transcript_path):
69
+ """transcript 1개에서 이벤트 집계 → 시간절감 스냅샷."""
70
+ rates = _rates()
71
+ tool_uses = [] # (name, tool_use_id)
72
+ results = {} # tool_use_id -> result text
73
+ had_edit = False
74
+ gate_blocks = 0
75
+ session_start = None
76
+ session_end = None
77
+ # 훅 주입 소비(C1/C2) — attachment(hook_success) stdout 마커. 주입문 sha1로 dedup
78
+ # (tool_use 경로와 단일 집합 공유 → 이중계수 차단. 설계 v2-6)
79
+ injected_c1 = set()
80
+ injected_c2 = set()
81
+
82
+ for o in _iter_transcript(transcript_path):
83
+ ts = o.get('timestamp')
84
+ if ts:
85
+ session_start = session_start or ts
86
+ session_end = ts
87
+ att = o.get('attachment') or {}
88
+ if att.get('type') in ('hook_success', 'hook_blocking_error'):
89
+ out = str(att.get('stdout', ''))[:2000]
90
+ if '[KNOWN-SOLUTION]' in out:
91
+ import hashlib as _h
92
+ injected_c1.add(_h.sha1(out.split('[KNOWN-SOLUTION]')[1][:200].encode()).hexdigest())
93
+ if '[DOMAIN KNOWLEDGE]' in out:
94
+ import hashlib as _h
95
+ injected_c2.add(_h.sha1(out.split('[DOMAIN KNOWLEDGE]')[1][:200].encode()).hexdigest())
96
+ raw = None
97
+ for c in (o.get('message', {}).get('content') or []):
98
+ if not isinstance(c, dict):
99
+ continue
100
+ if c.get('type') == 'tool_use':
101
+ tool_uses.append((c.get('name', ''), c.get('id', '')))
102
+ if c.get('name') in ('Edit', 'Write', 'MultiEdit', 'NotebookEdit'):
103
+ had_edit = True
104
+ elif c.get('name') == 'Bash':
105
+ # 모델이 Edit 대신 Bash로 파일을 고치는 경우(sed -i/리다이렉션/tee)도 구현 신호
106
+ _cmd = str((c.get('input') or {}).get('command', ''))[:500]
107
+ if ('sed -i' in _cmd or ' tee ' in _cmd or '>>' in _cmd
108
+ or ('>' in _cmd and ('.ts' in _cmd or '.js' in _cmd or '.py' in _cmd or '.json' in _cmd))):
109
+ had_edit = True
110
+ elif c.get('type') == 'tool_result':
111
+ txt = c.get('content')
112
+ results[c.get('tool_use_id', '')] = json.dumps(txt, ensure_ascii=False)[:400] if txt else ''
113
+ if raw is None:
114
+ raw = json.dumps(o, ensure_ascii=False)[:200]
115
+ if 'HOOK:design-gate' in raw or 'HOOK:pre-mark-complete' in raw:
116
+ gate_blocks += 1
117
+
118
+ def consumed(tool_id):
119
+ """빈 결과(미발견)는 소비로 치지 않는다 — '소비 없으면 적립 0' 원칙."""
120
+ t = results.get(tool_id, '')
121
+ if len(t) < 80:
122
+ return False
123
+ for empty_sig in ('"total": 0', '"count": 0', '"quirks": []', '"local": []', '"patterns": []'):
124
+ if empty_sig in t:
125
+ return False
126
+ return True
127
+
128
+ c1 = c2 = 0
129
+ c4_s = 0
130
+ for name, tid in tool_uses:
131
+ short = name.split('__')[-1]
132
+ if any(k in short for k in C1_TOOLS) and consumed(tid):
133
+ c1 += 1
134
+ elif any(k in short for k in C2_TOOLS) and consumed(tid):
135
+ c2 += 1
136
+ elif short in C4_MAP:
137
+ c4_s += rates[C4_MAP[short]]
138
+
139
+ # 훅 주입 소비 합산 (동일 게이트·상한 적용 — 설계 v2-6)
140
+ c1 += len(injected_c1)
141
+ c2 += len(injected_c2)
142
+
143
+ # 지식 소비(C1/C2)는 이후 실제 구현(Edit/Write)이 있어야 적립 — 조회만으론 0
144
+ if not had_edit:
145
+ c1 = c2 = 0
146
+ c1 = min(c1, 2) # 세션당 보수 상한
147
+ c2 = min(c2, 3)
148
+
149
+ anchored_s = min(
150
+ c1 * rates['knowledge_error_reuse_s'] + c2 * rates['domain_knowledge_use_s'] + c4_s,
151
+ rates['session_cap_anchored_s'],
152
+ )
153
+
154
+ # C3 병렬 — agent-telemetry에서 이 세션 시간창 내 배치 실측
155
+ measured_s = _parallel_savings(session_start, session_end)
156
+
157
+ return {
158
+ 'at': datetime.now(timezone.utc).isoformat(),
159
+ 'transcript': os.path.basename(transcript_path),
160
+ 'measured_s': round(measured_s), # 전액 실측 (병렬)
161
+ 'anchored_s': round(anchored_s), # 실측앵커 상수 기반
162
+ 'potential_gate_blocks': gate_blocks, # C5: 적립 0, 잠재 건수만
163
+ 'detail': {'c1_error_reuse': c1, 'c2_domain_use': c2, 'c4_mcp_routine_s': c4_s},
164
+ }
165
+
166
+
167
+ def _parse_iso(s):
168
+ try:
169
+ return datetime.fromisoformat(s.replace('Z', '+00:00')).timestamp()
170
+ except Exception:
171
+ return None
172
+
173
+
174
+ def _parallel_savings(session_start, session_end):
175
+ """같은 10초 창에 발사된 완료 스폰 배치: Σduration - max(duration)."""
176
+ if not (session_start and session_end and os.path.exists(TELEMETRY)):
177
+ return 0.0
178
+ lo, hi = _parse_iso(session_start), _parse_iso(session_end)
179
+ if lo is None or hi is None:
180
+ return 0.0
181
+ rows = []
182
+ for line in open(TELEMETRY, errors='ignore'):
183
+ try:
184
+ r = json.loads(line)
185
+ except Exception:
186
+ continue
187
+ s, e = _parse_iso(r.get('startedAt', '')), _parse_iso(r.get('completedAt', ''))
188
+ if s and e and lo - 5 <= s <= hi + 5 and r.get('result') == 'success':
189
+ rows.append((s, e - s))
190
+ rows.sort()
191
+ saved, batch = 0.0, []
192
+ for s, dur in rows:
193
+ if batch and s - batch[0][0] > 10:
194
+ if len(batch) >= 2:
195
+ ds = [d for _, d in batch]
196
+ saved += sum(ds) - max(ds)
197
+ batch = []
198
+ batch.append((s, dur))
199
+ if len(batch) >= 2:
200
+ ds = [d for _, d in batch]
201
+ saved += sum(ds) - max(ds)
202
+ return saved
203
+
204
+
205
+ def append_ledger(snap):
206
+ if snap['measured_s'] <= 0 and snap['anchored_s'] <= 0 and snap['potential_gate_blocks'] <= 0:
207
+ return False
208
+ os.makedirs(os.path.dirname(LEDGER), exist_ok=True)
209
+ with open(LEDGER, 'a') as f:
210
+ f.write(json.dumps(snap, ensure_ascii=False) + '\n')
211
+ return True
212
+
213
+
214
+ def totals():
215
+ """영수증(receipt.py)용 누계."""
216
+ t = {'measured_s': 0, 'anchored_s': 0, 'potential_gate_blocks': 0, 'sessions': 0}
217
+ if not os.path.exists(LEDGER):
218
+ return t
219
+ for line in open(LEDGER, errors='ignore'):
220
+ try:
221
+ r = json.loads(line)
222
+ except Exception:
223
+ continue
224
+ t['measured_s'] += r.get('measured_s', 0)
225
+ t['anchored_s'] += r.get('anchored_s', 0)
226
+ t['potential_gate_blocks'] += r.get('potential_gate_blocks', 0)
227
+ t['sessions'] += 1
228
+ return t
229
+
230
+
231
+ if __name__ == '__main__':
232
+ inp = os.environ.get('ROUTINECODE_HOOK_INPUT', '')
233
+ try:
234
+ tp = json.loads(inp).get('transcript_path', '') if inp else (sys.argv[1] if len(sys.argv) > 1 else '')
235
+ except Exception:
236
+ tp = sys.argv[1] if len(sys.argv) > 1 else ''
237
+ if tp and os.path.exists(tp):
238
+ snap = collect(tp)
239
+ append_ledger(snap)
240
+ print(json.dumps(snap, ensure_ascii=False))
@@ -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