@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.
- package/bin/prompt-vault.js +1 -1
- package/bin/setup.js +1 -1
- package/hooks/hooks-config.json +5 -1
- package/hooks/lib/known_solution.py +148 -0
- package/hooks/lib/receipt.py +2 -0
- package/hooks/lib/time_savings.py +137 -3
- package/hooks/post-bash-knowledge.sh +45 -0
- package/hooks/pre-agent-spawn.mjs +1 -1
- package/hooks/stop-usage-metrics.sh +9 -8
- 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
|
@@ -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
|
|
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
|