@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.
- package/bin/prompt-vault.js +1 -1
- package/bin/setup.js +1 -1
- package/hooks/hooks-config.json +13 -0
- package/hooks/lib/known_solution.py +148 -0
- package/hooks/lib/receipt.py +31 -0
- package/hooks/lib/time_savings.py +240 -0
- package/hooks/post-bash-knowledge.sh +45 -0
- package/hooks/pre-agent-spawn.mjs +1 -1
- package/hooks/pre-askuser-guard.sh +32 -0
- package/hooks/stop-usage-metrics.sh +9 -0
- package/hooks/user-prompt-submit.sh +16 -2
- package/mcp-db-manager/dist/analyzers/duplicate-detector.js +1 -1
- package/mcp-db-manager/dist/core/model-parser.js +1 -1
- package/mcp-db-manager/dist/core/schema-reader.js +1 -1
- package/mcp-db-manager/dist/executors/migration-executor.js +1 -1
- package/mcp-db-manager/dist/generators/migration-generator.js +1 -1
- package/mcp-db-manager/dist/index.js +1 -1
- package/mcp-db-manager/dist/validators/schema-validator.js +1 -1
- package/mcp-dev-tester/dist/analyzers/auth-diagnoser.js +1 -1
- package/mcp-dev-tester/dist/analyzers/backend-analyzer.js +1 -1
- package/mcp-dev-tester/dist/analyzers/endpoint-diagnoser.js +1 -1
- package/mcp-dev-tester/dist/analyzers/frontend-analyzer.js +1 -1
- package/mcp-dev-tester/dist/analyzers/runtime-diagnoser.js +1 -1
- package/mcp-dev-tester/dist/analyzers/type-comparator.js +1 -1
- package/mcp-dev-tester/dist/analyzers/validation-diagnoser.js +1 -1
- package/mcp-dev-tester/dist/browser/playwright-manager.js +1 -1
- package/mcp-dev-tester/dist/browser/ui-tester.js +1 -1
- package/mcp-dev-tester/dist/config/project-config.js +1 -1
- package/mcp-dev-tester/dist/index.js +1 -1
- package/mcp-dev-tester/dist/testers/api-tester.js +1 -1
- package/mcp-doc-fetcher/dist/index.js +1 -1
- package/mcp-html-renderer/dist/index.js +1 -1
- package/mcp-html-renderer/dist/preview.js +1 -1
- package/mcp-html-renderer/dist/template-engine.js +1 -1
- package/mcp-intelligence/dist/concept-catalog.js +1 -1
- package/mcp-intelligence/dist/concept-server-proxy.js +1 -1
- package/mcp-intelligence/dist/config-auth.js +1 -1
- package/mcp-intelligence/dist/contracts.generated.js +1 -1
- package/mcp-intelligence/dist/domain-templates.js +1 -1
- package/mcp-intelligence/dist/grounding.js +1 -1
- package/mcp-intelligence/dist/handlers.js +1 -1
- package/mcp-intelligence/dist/index.js +1 -1
- package/mcp-intelligence/dist/intelligence-file.js +1 -1
- package/mcp-intelligence/dist/knowledge-critic.js +1 -1
- package/mcp-intelligence/dist/knowledge-handlers.js +1 -1
- package/mcp-intelligence/dist/mention-extractor.js +1 -1
- package/mcp-intelligence/dist/provenance.js +1 -1
- package/mcp-intelligence/dist/semantic-embedder.js +1 -1
- package/mcp-log-watcher/dist/config-auth.js +1 -1
- package/mcp-log-watcher/dist/file-watcher.js +1 -1
- package/mcp-log-watcher/dist/index.js +1 -1
- package/mcp-log-watcher/dist/known-solutions.js +1 -1
- package/mcp-log-watcher/dist/log-store.js +1 -1
- package/mcp-log-watcher/dist/websocket-server.js +1 -1
- package/mcp-plan-manager/dist/__tests__/plan-parser.test.js +1 -1
- package/mcp-plan-manager/dist/file-ops.js +1 -1
- package/mcp-plan-manager/dist/index.js +1 -1
- package/mcp-plan-manager/dist/plan-parser.js +1 -1
- package/mcp-project-context/dist/git-ops.js +1 -1
- package/mcp-project-context/dist/index.js +1 -1
- package/mcp-project-context/dist/project-analysis.js +1 -1
- package/mcp-routinecode/dist/agent-tracker.js +1 -1
- package/mcp-routinecode/dist/blast-radius.js +1 -1
- package/mcp-routinecode/dist/config-auth.js +1 -1
- package/mcp-routinecode/dist/contracts.generated.js +1 -1
- package/mcp-routinecode/dist/explorer-advisor.js +1 -1
- package/mcp-routinecode/dist/failure-tracker.js +1 -1
- package/mcp-routinecode/dist/file-lock-registry.js +1 -1
- package/mcp-routinecode/dist/file-risk-scorer.js +1 -1
- package/mcp-routinecode/dist/index.js +1 -1
- package/mcp-routinecode/dist/known-solutions.js +1 -1
- package/mcp-routinecode/dist/ledger-miner.js +1 -1
- package/mcp-routinecode/dist/observed-draft.js +1 -1
- package/mcp-routinecode/dist/pattern-matcher.js +1 -1
- package/mcp-routinecode/dist/routines.js +1 -1
- package/mcp-routinecode/dist/rule-engine.js +1 -1
- package/mcp-routinecode/dist/rule-promoter.js +1 -1
- package/mcp-routinecode/dist/telemetry-analyzer.js +1 -1
- package/mcp-routinecode/dist/verification.js +0 -0
- package/mcp-routinecode/dist/viral-loop.js +1 -1
- 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)
|
package/hooks/lib/receipt.py
CHANGED
|
@@ -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
|