@triflux/core 10.1.0 → 10.26.0
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/hooks/agent-route-guard.mjs +14 -10
- package/hooks/cross-review-tracker.mjs +21 -8
- package/hooks/error-context.mjs +20 -8
- package/hooks/hook-adaptive-collector.mjs +18 -16
- package/hooks/hook-manager.mjs +93 -32
- package/hooks/hook-orchestrator.mjs +267 -35
- package/hooks/hook-registry.json +343 -256
- package/hooks/hooks.json +49 -1
- package/hooks/keyword-rules.json +191 -31
- package/hooks/lib/resolve-root.mjs +1 -1
- package/hooks/mcp-config-watcher.mjs +6 -2
- package/hooks/permission-safe-allow.mjs +233 -0
- package/hooks/pipeline-stop.mjs +369 -22
- package/hooks/post-tool-tips.mjs +306 -0
- package/hooks/pre-compact-snapshot.mjs +181 -0
- package/hooks/safety-guard.mjs +262 -34
- package/hooks/session-end-cleanup.mjs +345 -0
- package/hooks/session-start-fast.mjs +214 -0
- package/hooks/subagent-tracker.mjs +267 -0
- package/hooks/subagent-verifier.mjs +9 -4
- package/hub/account-broker.mjs +1036 -66
- package/hub/adaptive-diagnostic.mjs +75 -48
- package/hub/adaptive-inject.mjs +96 -58
- package/hub/adaptive-memory.mjs +156 -42
- package/hub/adaptive.mjs +60 -31
- package/hub/assign-callbacks.mjs +163 -126
- package/hub/bridge.mjs +1051 -290
- package/hub/cli-adapter-base.mjs +321 -97
- package/hub/codex-adapter.mjs +179 -93
- package/hub/codex-compat.mjs +4 -4
- package/hub/codex-preflight.mjs +113 -37
- package/hub/delegator/contracts.mjs +22 -21
- package/hub/delegator/index.mjs +14 -14
- package/hub/delegator/schema/delegator-tools.schema.json +1 -1
- package/hub/delegator/service.mjs +93 -64
- package/hub/delegator/tool-definitions.mjs +35 -35
- package/hub/dynamic-routing-engine.mjs +511 -0
- package/hub/fullcycle.mjs +34 -18
- package/hub/gemini-adapter.mjs +82 -108
- package/hub/hitl.mjs +202 -143
- package/hub/intent.mjs +167 -38
- package/hub/lib/bash-path.mjs +73 -0
- package/hub/lib/cache-guard.mjs +43 -17
- package/hub/lib/env-detect.mjs +145 -0
- package/hub/lib/hosts-compat.mjs +319 -0
- package/hub/lib/mcp-response-cache.mjs +239 -0
- package/hub/lib/memory-store.mjs +285 -111
- package/hub/lib/path-utils.mjs +165 -0
- package/hub/lib/phase-manager.mjs +493 -0
- package/hub/lib/process-utils.mjs +797 -104
- package/hub/lib/prompt-tmp.mjs +33 -0
- package/hub/lib/risk-tier.mjs +53 -0
- package/hub/lib/spawn-trace.mjs +561 -0
- package/hub/lib/ssh-command.mjs +44 -24
- package/hub/lib/ssh-retry.mjs +5 -1
- package/hub/lib/staged-file-detect.mjs +43 -0
- package/hub/lib/state-snapshot.mjs +293 -0
- package/hub/lib/tfx-route-args.mjs +309 -0
- package/hub/lib/timeline-adapter.mjs +190 -0
- package/hub/lib/trace-recorder.mjs +153 -0
- package/hub/lib/uuidv7.mjs +4 -3
- package/hub/memory-doctor.mjs +266 -106
- package/hub/middleware/quota-middleware.mjs +70 -0
- package/hub/middleware/request-logger.mjs +61 -34
- package/hub/paths.mjs +9 -9
- package/hub/pipeline/gates/confidence.mjs +34 -15
- package/hub/pipeline/gates/consensus.mjs +27 -15
- package/hub/pipeline/gates/index.mjs +7 -3
- package/hub/pipeline/gates/selfcheck.mjs +57 -19
- package/hub/pipeline/index.mjs +77 -42
- package/hub/pipeline/state.mjs +10 -10
- package/hub/pipeline/transitions.mjs +40 -23
- package/hub/platform.mjs +65 -48
- package/hub/quality/deslop.mjs +70 -49
- package/hub/reflexion.mjs +87 -13
- package/hub/research.mjs +33 -25
- package/hub/router.mjs +941 -791
- package/hub/routing/complexity.mjs +132 -29
- package/hub/routing/index.mjs +17 -12
- package/hub/routing/q-learning.mjs +78 -30
- package/hub/routing-snapshot.mjs +261 -0
- package/hub/session-fingerprint.mjs +127 -61
- package/hub/state.mjs +86 -44
- package/hub/team/claude-daemon-control.mjs +1130 -0
- package/hub/team/retry-state-machine.mjs +336 -0
- package/hub/token-mode.mjs +72 -71
- package/hub/workers/worker-utils.mjs +23 -17
- package/hud/colors.mjs +27 -9
- package/hud/constants.mjs +190 -27
- package/hud/context-monitor.mjs +133 -44
- package/hud/hud-qos-status.mjs +157 -50
- package/hud/mission-board.mjs +56 -0
- package/hud/providers/claude.mjs +398 -114
- package/hud/providers/codex.mjs +96 -21
- package/hud/providers/gemini.mjs +210 -52
- package/hud/renderers.mjs +440 -127
- package/hud/terminal.mjs +101 -31
- package/hud/utils.mjs +81 -39
- package/mesh/index.mjs +69 -0
- package/mesh/mesh-budget.mjs +137 -0
- package/mesh/mesh-heartbeat.mjs +100 -0
- package/mesh/mesh-protocol.mjs +96 -0
- package/mesh/mesh-queue.mjs +163 -0
- package/mesh/mesh-registry.mjs +78 -0
- package/mesh/mesh-router.mjs +77 -0
- package/package.json +5 -1
- package/scripts/lib/agent-json.mjs +27 -0
- package/scripts/lib/async.mjs +174 -0
- package/scripts/lib/claudemd-scanner.mjs +6 -1
- package/scripts/lib/cli-agy.mjs +62 -0
- package/scripts/lib/cli-claude.mjs +78 -0
- package/scripts/lib/cli-codex.mjs +199 -0
- package/scripts/lib/cli-gemini.mjs +37 -0
- package/scripts/lib/codex-recovery.sh +50 -0
- package/scripts/lib/context.mjs +3 -3
- package/scripts/lib/cross-review-utils.mjs +13 -5
- package/scripts/lib/doctor-env-checks.mjs +121 -0
- package/scripts/lib/dynamic-route-cli.mjs +107 -0
- package/scripts/lib/env-probe.mjs +58 -30
- package/scripts/lib/env.mjs +14 -0
- package/scripts/lib/gemini-profiles.mjs +44 -10
- package/scripts/lib/handoff.mjs +33 -17
- package/scripts/lib/hook-utils.mjs +8 -6
- package/scripts/lib/hub.mjs +48 -0
- package/scripts/lib/keyword-rules.mjs +49 -20
- package/scripts/lib/logger.mjs +24 -24
- package/scripts/lib/mcp-filter.mjs +391 -247
- package/scripts/lib/mcp-gateway-health-check.mjs +159 -0
- package/scripts/lib/mcp-gateway-servers.mjs +70 -0
- package/scripts/lib/mcp-gateway-wrapper-check.mjs +35 -0
- package/scripts/lib/mcp-guard-engine.mjs +1281 -115
- package/scripts/lib/mcp-health.mjs +773 -0
- package/scripts/lib/mcp-manifest.mjs +24 -14
- package/scripts/lib/mcp-server-catalog.mjs +355 -118
- package/scripts/lib/pid.mjs +63 -0
- package/scripts/lib/process-utils.mjs +26 -0
- package/scripts/lib/psmux-info.mjs +117 -12
- package/scripts/lib/quota.mjs +47 -0
- package/scripts/lib/remote-spawn-transfer.mjs +44 -14
- package/scripts/lib/skill-state.mjs +220 -0
- package/scripts/lib/skill-template.mjs +32 -7
- package/scripts/lib/team.mjs +75 -0
- package/scripts/lib/timeout.mjs +66 -0
- package/scripts/lib/tmp.mjs +26 -0
- package/scripts/lib/toml.mjs +113 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
// hub/team/retry-state-machine.mjs
|
|
2
|
+
// Phase 3 Step A — true ralph / auto-escalate retry state machine.
|
|
3
|
+
// 설계 문서: .triflux/plans/phase3-lead-codex-ralph-escalate.md
|
|
4
|
+
//
|
|
5
|
+
// Modes:
|
|
6
|
+
// bounded — --retry 1 (기본): maxIterations 도달 시 BUDGET_EXCEEDED.
|
|
7
|
+
// ralph — --retry ralph: maxIterations=0 이면 unlimited. stuck detector 만 종료.
|
|
8
|
+
// auto-escalate — --retry auto-escalate: BUDGET_EXCEEDED 시 다음 CLI 로 전이.
|
|
9
|
+
//
|
|
10
|
+
// 상태 전이:
|
|
11
|
+
// PLANNING → EXECUTING → VERIFYING.success → DONE
|
|
12
|
+
// → VERIFYING.fail → DIAGNOSING → EXECUTING …
|
|
13
|
+
// → stuckCounter≥3 → STUCK
|
|
14
|
+
// → iter≥max → BUDGET_EXCEEDED (escalate 모드는 다음 CLI)
|
|
15
|
+
|
|
16
|
+
import { EventEmitter } from "node:events";
|
|
17
|
+
import {
|
|
18
|
+
appendFileSync,
|
|
19
|
+
existsSync,
|
|
20
|
+
mkdirSync,
|
|
21
|
+
readFileSync,
|
|
22
|
+
writeFileSync,
|
|
23
|
+
} from "node:fs";
|
|
24
|
+
import { dirname, join } from "node:path";
|
|
25
|
+
|
|
26
|
+
export const STATES = Object.freeze({
|
|
27
|
+
PLANNING: "PLANNING",
|
|
28
|
+
EXECUTING: "EXECUTING",
|
|
29
|
+
DIAGNOSING: "DIAGNOSING",
|
|
30
|
+
STUCK: "STUCK",
|
|
31
|
+
BUDGET_EXCEEDED: "BUDGET_EXCEEDED",
|
|
32
|
+
DONE: "DONE",
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export const MODES = Object.freeze({
|
|
36
|
+
BOUNDED: "bounded",
|
|
37
|
+
RALPH: "ralph",
|
|
38
|
+
ESCALATE: "auto-escalate",
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const ESCALATION_CHAIN_CONFIG_PATH = ".triflux/config/escalation-chain.json";
|
|
42
|
+
|
|
43
|
+
// Escalation chain (2026-05-27 정책):
|
|
44
|
+
// 1. codex gpt-5.5 — 코드/추론/비용에서 mini/5.3-codex 중간 단계를 대체
|
|
45
|
+
// 2. claude opus-4-7 — 최종 수단
|
|
46
|
+
const DEFAULT_ESCALATION_CHAIN = Object.freeze([
|
|
47
|
+
Object.freeze({ cli: "codex", model: "gpt-5.5" }),
|
|
48
|
+
Object.freeze({ cli: "claude", model: "opus-4-7" }),
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
const STUCK_THRESHOLD = 3;
|
|
52
|
+
|
|
53
|
+
export function createRetryStateMachine(options = {}) {
|
|
54
|
+
const mode = options.mode || MODES.BOUNDED;
|
|
55
|
+
const cliChain = resolveEscalationChain(options);
|
|
56
|
+
const maxIterationsInput =
|
|
57
|
+
typeof options.maxIterations === "number" ? options.maxIterations : null;
|
|
58
|
+
const maxIterations =
|
|
59
|
+
maxIterationsInput !== null
|
|
60
|
+
? maxIterationsInput
|
|
61
|
+
: mode === MODES.BOUNDED
|
|
62
|
+
? 3
|
|
63
|
+
: 0;
|
|
64
|
+
const stateFile = options.stateFile || null;
|
|
65
|
+
const sessionId = options.sessionId || null;
|
|
66
|
+
|
|
67
|
+
const emitter = new EventEmitter();
|
|
68
|
+
const state = {
|
|
69
|
+
current: STATES.PLANNING,
|
|
70
|
+
iterations: 0,
|
|
71
|
+
maxIterations,
|
|
72
|
+
stuckCounter: 0,
|
|
73
|
+
lastFailureReason: null,
|
|
74
|
+
cliIndex: 0,
|
|
75
|
+
cliChain,
|
|
76
|
+
mode,
|
|
77
|
+
sessionId,
|
|
78
|
+
stateFile,
|
|
79
|
+
history: [],
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
function transition(next, meta = {}) {
|
|
83
|
+
const prev = state.current;
|
|
84
|
+
state.current = next;
|
|
85
|
+
const entry = {
|
|
86
|
+
t: Date.now(),
|
|
87
|
+
from: prev,
|
|
88
|
+
to: next,
|
|
89
|
+
iteration: state.iterations,
|
|
90
|
+
cliIndex: state.cliIndex,
|
|
91
|
+
...meta,
|
|
92
|
+
};
|
|
93
|
+
state.history.push(entry);
|
|
94
|
+
if (stateFile) persistTransition(stateFile, entry);
|
|
95
|
+
emitter.emit("transition", entry);
|
|
96
|
+
return entry;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function startIteration() {
|
|
100
|
+
state.iterations += 1;
|
|
101
|
+
return transition(STATES.EXECUTING, { iteration: state.iterations });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function reportVerifySuccess() {
|
|
105
|
+
return transition(STATES.DONE);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function reportVerifyFail(failureReason) {
|
|
109
|
+
const reason = String(failureReason || "unknown");
|
|
110
|
+
if (reason === state.lastFailureReason) {
|
|
111
|
+
state.stuckCounter += 1;
|
|
112
|
+
} else {
|
|
113
|
+
state.stuckCounter = 1;
|
|
114
|
+
state.lastFailureReason = reason;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (state.stuckCounter >= STUCK_THRESHOLD) {
|
|
118
|
+
return transition(STATES.STUCK, {
|
|
119
|
+
reason,
|
|
120
|
+
stuckCounter: state.stuckCounter,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (state.maxIterations > 0 && state.iterations >= state.maxIterations) {
|
|
125
|
+
if (state.mode === MODES.ESCALATE) {
|
|
126
|
+
return escalate();
|
|
127
|
+
}
|
|
128
|
+
return transition(STATES.BUDGET_EXCEEDED, {
|
|
129
|
+
reason,
|
|
130
|
+
iterations: state.iterations,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return transition(STATES.DIAGNOSING, { reason });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function escalate() {
|
|
138
|
+
if (state.cliIndex + 1 >= state.cliChain.length) {
|
|
139
|
+
return transition(STATES.BUDGET_EXCEEDED, {
|
|
140
|
+
reason: "escalation-chain-exhausted",
|
|
141
|
+
chain: state.cliChain,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
state.cliIndex += 1;
|
|
145
|
+
state.iterations = 0;
|
|
146
|
+
state.stuckCounter = 0;
|
|
147
|
+
state.lastFailureReason = null;
|
|
148
|
+
return transition(STATES.EXECUTING, {
|
|
149
|
+
cli: state.cliChain[state.cliIndex],
|
|
150
|
+
escalated: true,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function getCurrent() {
|
|
155
|
+
return {
|
|
156
|
+
current: state.current,
|
|
157
|
+
iterations: state.iterations,
|
|
158
|
+
maxIterations: state.maxIterations,
|
|
159
|
+
stuckCounter: state.stuckCounter,
|
|
160
|
+
lastFailureReason: state.lastFailureReason,
|
|
161
|
+
cliIndex: state.cliIndex,
|
|
162
|
+
cliChain: state.cliChain.slice(),
|
|
163
|
+
mode: state.mode,
|
|
164
|
+
sessionId: state.sessionId,
|
|
165
|
+
history: state.history.slice(),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function on(event, listener) {
|
|
170
|
+
emitter.on(event, listener);
|
|
171
|
+
return () => emitter.off(event, listener);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function serialize() {
|
|
175
|
+
return {
|
|
176
|
+
version: 1,
|
|
177
|
+
current: state.current,
|
|
178
|
+
iterations: state.iterations,
|
|
179
|
+
maxIterations: state.maxIterations,
|
|
180
|
+
stuckCounter: state.stuckCounter,
|
|
181
|
+
lastFailureReason: state.lastFailureReason,
|
|
182
|
+
cliIndex: state.cliIndex,
|
|
183
|
+
cliChain: state.cliChain.slice(),
|
|
184
|
+
mode: state.mode,
|
|
185
|
+
sessionId: state.sessionId,
|
|
186
|
+
history: state.history.slice(),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function applySnapshot(snapshot) {
|
|
191
|
+
if (!snapshot || typeof snapshot !== "object") return;
|
|
192
|
+
state.current = snapshot.current || STATES.PLANNING;
|
|
193
|
+
state.iterations = Number(snapshot.iterations) || 0;
|
|
194
|
+
state.maxIterations =
|
|
195
|
+
snapshot.maxIterations !== undefined
|
|
196
|
+
? Number(snapshot.maxIterations)
|
|
197
|
+
: state.maxIterations;
|
|
198
|
+
state.stuckCounter = Number(snapshot.stuckCounter) || 0;
|
|
199
|
+
state.lastFailureReason = snapshot.lastFailureReason || null;
|
|
200
|
+
state.cliIndex = Number(snapshot.cliIndex) || 0;
|
|
201
|
+
if (Array.isArray(snapshot.cliChain) && snapshot.cliChain.length > 0) {
|
|
202
|
+
state.cliChain = normalizeEscalationChain(
|
|
203
|
+
snapshot.cliChain,
|
|
204
|
+
"snapshot.cliChain",
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
if (snapshot.mode) state.mode = snapshot.mode;
|
|
208
|
+
if (snapshot.sessionId !== undefined) state.sessionId = snapshot.sessionId;
|
|
209
|
+
if (Array.isArray(snapshot.history))
|
|
210
|
+
state.history = snapshot.history.slice();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
STATES,
|
|
215
|
+
MODES,
|
|
216
|
+
getCurrent,
|
|
217
|
+
startIteration,
|
|
218
|
+
reportVerifySuccess,
|
|
219
|
+
reportVerifyFail,
|
|
220
|
+
escalate,
|
|
221
|
+
on,
|
|
222
|
+
serialize,
|
|
223
|
+
applySnapshot,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function resolveEscalationChain(options) {
|
|
228
|
+
if (Array.isArray(options.cliChain)) {
|
|
229
|
+
return normalizeEscalationChain(options.cliChain, "options.cliChain");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const projectRoot = options.projectRoot || process.cwd();
|
|
233
|
+
return (
|
|
234
|
+
loadEscalationChainOverride(projectRoot) ||
|
|
235
|
+
normalizeEscalationChain(
|
|
236
|
+
DEFAULT_ESCALATION_CHAIN,
|
|
237
|
+
"DEFAULT_ESCALATION_CHAIN",
|
|
238
|
+
)
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function loadEscalationChainOverride(projectRoot = process.cwd()) {
|
|
243
|
+
const configPath = join(projectRoot, ESCALATION_CHAIN_CONFIG_PATH);
|
|
244
|
+
if (!existsSync(configPath)) return null;
|
|
245
|
+
|
|
246
|
+
const raw = readFileSync(configPath, "utf8");
|
|
247
|
+
let parsed;
|
|
248
|
+
try {
|
|
249
|
+
parsed = JSON.parse(raw);
|
|
250
|
+
} catch (err) {
|
|
251
|
+
throw new Error(
|
|
252
|
+
`escalation-chain override 파싱 실패: ${configPath} — 원인: ${err.message} — 수정: JSON 문법 확인 또는 파일 삭제로 DEFAULT_ESCALATION_CHAIN 사용`,
|
|
253
|
+
{ cause: err },
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
257
|
+
throw new Error(`${configPath}: expected object`);
|
|
258
|
+
}
|
|
259
|
+
if (parsed.version !== undefined && parsed.version !== 1) {
|
|
260
|
+
throw new Error(`${configPath}: unsupported version ${parsed.version}`);
|
|
261
|
+
}
|
|
262
|
+
return normalizeEscalationChain(parsed.chain, configPath);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function normalizeEscalationChain(chain, source) {
|
|
266
|
+
if (!Array.isArray(chain) || chain.length === 0) {
|
|
267
|
+
throw new Error(`${source}: chain must be a non-empty array`);
|
|
268
|
+
}
|
|
269
|
+
return chain.map((entry, index) =>
|
|
270
|
+
normalizeEscalationChainEntry(entry, index, source),
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function normalizeEscalationChainEntry(entry, index, source) {
|
|
275
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
276
|
+
throw new Error(`${source}: chain[${index}] must be an object`);
|
|
277
|
+
}
|
|
278
|
+
if (typeof entry.cli !== "string" || entry.cli.trim() === "") {
|
|
279
|
+
throw new Error(
|
|
280
|
+
`${source}: chain[${index}].cli must be a non-empty string`,
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
if (typeof entry.model !== "string" || entry.model.trim() === "") {
|
|
284
|
+
throw new Error(
|
|
285
|
+
`${source}: chain[${index}].model must be a non-empty string`,
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const normalized = {
|
|
290
|
+
cli: entry.cli,
|
|
291
|
+
model: entry.model,
|
|
292
|
+
};
|
|
293
|
+
if (typeof entry.profile === "string" && entry.profile.trim() !== "") {
|
|
294
|
+
normalized.profile = entry.profile;
|
|
295
|
+
}
|
|
296
|
+
return normalized;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function persistTransition(stateFile, entry) {
|
|
300
|
+
const dir = dirname(stateFile);
|
|
301
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
302
|
+
appendFileSync(stateFile, `${JSON.stringify(entry)}\n`, "utf8");
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function resumeFromStateFile(stateFile) {
|
|
306
|
+
if (!existsSync(stateFile)) return null;
|
|
307
|
+
const raw = readFileSync(stateFile, "utf8").trim();
|
|
308
|
+
if (!raw) return null;
|
|
309
|
+
const lines = raw.split("\n").filter(Boolean);
|
|
310
|
+
if (lines.length === 0) return null;
|
|
311
|
+
return JSON.parse(lines[lines.length - 1]);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Full-snapshot 기반 stateFile I/O — bridge retry-run 에서 multi-process state 복원용.
|
|
315
|
+
// transition event log (resumeFromStateFile) 와 별도 파일로 관리하는 것을 권장.
|
|
316
|
+
export function loadSnapshot(snapshotFile) {
|
|
317
|
+
if (!existsSync(snapshotFile)) return null;
|
|
318
|
+
const raw = readFileSync(snapshotFile, "utf8").trim();
|
|
319
|
+
if (!raw) return null;
|
|
320
|
+
const parsed = JSON.parse(raw);
|
|
321
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
322
|
+
if (parsed.version !== 1) {
|
|
323
|
+
throw new Error(
|
|
324
|
+
`unsupported snapshot version: ${parsed.version} (expected 1)`,
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
return parsed;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function saveSnapshot(snapshotFile, snapshot) {
|
|
331
|
+
const dir = dirname(snapshotFile);
|
|
332
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
333
|
+
writeFileSync(snapshotFile, JSON.stringify(snapshot), "utf8");
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export { DEFAULT_ESCALATION_CHAIN, STUCK_THRESHOLD };
|
package/hub/token-mode.mjs
CHANGED
|
@@ -2,83 +2,84 @@
|
|
|
2
2
|
// GAP 분석 P2 #7: 심볼 통신 + 약어로 30-50% 토큰 절감
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
+
* @experimental 런타임 미연결 — 향후 통합 예정
|
|
5
6
|
* @experimental
|
|
6
7
|
* @type {Array<{ from: string[], to: string, type: 'symbol'|'abbrev' }>}
|
|
7
8
|
*/
|
|
8
9
|
export const COMPACT_RULES = [
|
|
9
10
|
// ── 심볼 치환 (긴 매칭 우선 정렬) ──
|
|
10
|
-
{ from: [
|
|
11
|
-
{ from: [
|
|
12
|
-
{ from: [
|
|
13
|
-
{ from: [
|
|
14
|
-
{ from: [
|
|
15
|
-
{ from: [
|
|
16
|
-
{ from: [
|
|
17
|
-
{ from: [
|
|
18
|
-
{ from: [
|
|
19
|
-
{ from: [
|
|
20
|
-
{ from: [
|
|
21
|
-
{ from: [
|
|
22
|
-
{ from: [
|
|
23
|
-
{ from: [
|
|
11
|
+
{ from: ["greater than or equal"], to: "≥", type: "symbol" },
|
|
12
|
+
{ from: ["less than or equal"], to: "≤", type: "symbol" },
|
|
13
|
+
{ from: ["results in", "결과적으로"], to: "→", type: "symbol" },
|
|
14
|
+
{ from: ["therefore", "따라서"], to: "∴", type: "symbol" },
|
|
15
|
+
{ from: ["because", "왜냐하면"], to: "∵", type: "symbol" },
|
|
16
|
+
{ from: ["approximately", "대략"], to: "≈", type: "symbol" },
|
|
17
|
+
{ from: ["not equal", "같지 않"], to: "≠", type: "symbol" },
|
|
18
|
+
{ from: ["in progress", "진행 중"], to: "⏳", type: "symbol" },
|
|
19
|
+
{ from: ["completed", "완료"], to: "✓", type: "symbol" },
|
|
20
|
+
{ from: ["success", "성공"], to: "✓", type: "symbol" },
|
|
21
|
+
{ from: ["failure", "실패"], to: "✗", type: "symbol" },
|
|
22
|
+
{ from: ["warning", "경고"], to: "⚠", type: "symbol" },
|
|
23
|
+
{ from: ["error", "에러"], to: "✗", type: "symbol" },
|
|
24
|
+
{ from: ["pending", "대기"], to: "⏸", type: "symbol" },
|
|
24
25
|
|
|
25
26
|
// ── 약어 (긴 매칭 우선 정렬) ──
|
|
26
|
-
{ from: [
|
|
27
|
-
{ from: [
|
|
28
|
-
{ from: [
|
|
29
|
-
{ from: [
|
|
30
|
-
{ from: [
|
|
31
|
-
{ from: [
|
|
32
|
-
{ from: [
|
|
33
|
-
{ from: [
|
|
34
|
-
{ from: [
|
|
35
|
-
{ from: [
|
|
27
|
+
{ from: ["configuration", "설정"], to: "cfg", type: "abbrev" },
|
|
28
|
+
{ from: ["implementation", "구현"], to: "impl", type: "abbrev" },
|
|
29
|
+
{ from: ["architecture", "아키텍처"], to: "arch", type: "abbrev" },
|
|
30
|
+
{ from: ["dependency", "의존성"], to: "dep", type: "abbrev" },
|
|
31
|
+
{ from: ["function", "함수"], to: "fn", type: "abbrev" },
|
|
32
|
+
{ from: ["parameter", "파라미터"], to: "param", type: "abbrev" },
|
|
33
|
+
{ from: ["repository", "저장소"], to: "repo", type: "abbrev" },
|
|
34
|
+
{ from: ["environment", "환경"], to: "env", type: "abbrev" },
|
|
35
|
+
{ from: ["variable", "변수"], to: "var", type: "abbrev" },
|
|
36
|
+
{ from: ["directory", "디렉토리"], to: "dir", type: "abbrev" },
|
|
36
37
|
|
|
37
38
|
// ── 한국어 동사/명령형 약어 ──
|
|
38
|
-
{ from: [
|
|
39
|
-
{ from: [
|
|
40
|
-
{ from: [
|
|
41
|
-
{ from: [
|
|
42
|
-
{ from: [
|
|
43
|
-
{ from: [
|
|
44
|
-
{ from: [
|
|
45
|
-
{ from: [
|
|
39
|
+
{ from: ["구현해"], to: "impl", type: "abbrev" },
|
|
40
|
+
{ from: ["확인해"], to: "check", type: "abbrev" },
|
|
41
|
+
{ from: ["수정해"], to: "fix", type: "abbrev" },
|
|
42
|
+
{ from: ["테스트"], to: "test", type: "abbrev" },
|
|
43
|
+
{ from: ["리뷰"], to: "review", type: "abbrev" },
|
|
44
|
+
{ from: ["분석"], to: "analyze", type: "abbrev" },
|
|
45
|
+
{ from: ["설계"], to: "design", type: "abbrev" },
|
|
46
|
+
{ from: ["문서화"], to: "docs", type: "abbrev" },
|
|
46
47
|
];
|
|
47
48
|
|
|
48
49
|
/** @type {Array<{ from: string[], to: string, type: 'symbol'|'abbrev' }>} */
|
|
49
50
|
export const REVIEW_RULES = [
|
|
50
|
-
{ from: [
|
|
51
|
-
{ from: [
|
|
52
|
-
{ from: [
|
|
53
|
-
{ from: [
|
|
54
|
-
{ from: [
|
|
55
|
-
{ from: [
|
|
56
|
-
{ from: [
|
|
57
|
-
{ from: [
|
|
51
|
+
{ from: ["looks good to me", "lgtm"], to: "✓lgtm", type: "abbrev" },
|
|
52
|
+
{ from: ["needs changes", "수정 필요"], to: "✗chg", type: "abbrev" },
|
|
53
|
+
{ from: ["nitpick", "사소한"], to: "nit", type: "abbrev" },
|
|
54
|
+
{ from: ["blocking", "블로킹"], to: "blk", type: "abbrev" },
|
|
55
|
+
{ from: ["suggestion", "제안"], to: "sug", type: "abbrev" },
|
|
56
|
+
{ from: ["question", "질문"], to: "q", type: "abbrev" },
|
|
57
|
+
{ from: ["approved", "승인"], to: "✓apv", type: "abbrev" },
|
|
58
|
+
{ from: ["request changes", "변경 요청"], to: "✗req", type: "abbrev" },
|
|
58
59
|
];
|
|
59
60
|
|
|
60
61
|
/** @type {Array<{ from: string[], to: string, type: 'symbol'|'abbrev' }>} */
|
|
61
62
|
export const DESIGN_RULES = [
|
|
62
|
-
{ from: [
|
|
63
|
-
{ from: [
|
|
64
|
-
{ from: [
|
|
65
|
-
{ from: [
|
|
66
|
-
{ from: [
|
|
67
|
-
{ from: [
|
|
68
|
-
{ from: [
|
|
69
|
-
{ from: [
|
|
63
|
+
{ from: ["component", "컴포넌트"], to: "cmp", type: "abbrev" },
|
|
64
|
+
{ from: ["interface", "인터페이스"], to: "iface", type: "abbrev" },
|
|
65
|
+
{ from: ["abstraction", "추상화"], to: "abs", type: "abbrev" },
|
|
66
|
+
{ from: ["pattern", "패턴"], to: "ptn", type: "abbrev" },
|
|
67
|
+
{ from: ["dependency injection", "의존성 주입"], to: "di", type: "abbrev" },
|
|
68
|
+
{ from: ["single responsibility", "단일 책임"], to: "srp", type: "abbrev" },
|
|
69
|
+
{ from: ["open closed", "개방 폐쇄"], to: "ocp", type: "abbrev" },
|
|
70
|
+
{ from: ["inheritance", "상속"], to: "inh", type: "abbrev" },
|
|
70
71
|
];
|
|
71
72
|
|
|
72
73
|
/** @type {Array<{ from: string[], to: string, type: 'symbol'|'abbrev' }>} */
|
|
73
74
|
export const DOCS_RULES = [
|
|
74
|
-
{ from: [
|
|
75
|
-
{ from: [
|
|
76
|
-
{ from: [
|
|
77
|
-
{ from: [
|
|
78
|
-
{ from: [
|
|
79
|
-
{ from: [
|
|
80
|
-
{ from: [
|
|
81
|
-
{ from: [
|
|
75
|
+
{ from: ["description", "설명"], to: "desc", type: "abbrev" },
|
|
76
|
+
{ from: ["example", "예시"], to: "ex", type: "abbrev" },
|
|
77
|
+
{ from: ["reference", "참조"], to: "ref", type: "abbrev" },
|
|
78
|
+
{ from: ["introduction", "소개"], to: "intro", type: "abbrev" },
|
|
79
|
+
{ from: ["deprecated", "사용 중단"], to: "dep", type: "abbrev" },
|
|
80
|
+
{ from: ["optional", "선택적"], to: "opt", type: "abbrev" },
|
|
81
|
+
{ from: ["required", "필수"], to: "req", type: "abbrev" },
|
|
82
|
+
{ from: ["returns", "반환"], to: "ret", type: "abbrev" },
|
|
82
83
|
];
|
|
83
84
|
|
|
84
85
|
// ── 프로필 맵 ──
|
|
@@ -86,9 +87,9 @@ export const DOCS_RULES = [
|
|
|
86
87
|
/** @type {Record<string, Array<{ from: string[], to: string, type: 'symbol'|'abbrev' }>>} */
|
|
87
88
|
const PROFILE_MAP = {
|
|
88
89
|
default: COMPACT_RULES,
|
|
89
|
-
review:
|
|
90
|
-
design:
|
|
91
|
-
docs:
|
|
90
|
+
review: [...COMPACT_RULES, ...REVIEW_RULES],
|
|
91
|
+
design: [...COMPACT_RULES, ...DESIGN_RULES],
|
|
92
|
+
docs: [...COMPACT_RULES, ...DOCS_RULES],
|
|
92
93
|
};
|
|
93
94
|
|
|
94
95
|
// ── 내부: 정렬된 치환 쌍 빌드 헬퍼 ──
|
|
@@ -134,7 +135,7 @@ const CODE_BLOCK_RE = /```[\s\S]*?```/g;
|
|
|
134
135
|
*/
|
|
135
136
|
function withCodeProtection(text, transform) {
|
|
136
137
|
const blocks = [];
|
|
137
|
-
const placeholder =
|
|
138
|
+
const placeholder = "\x00CB";
|
|
138
139
|
let idx = 0;
|
|
139
140
|
const masked = text.replace(CODE_BLOCK_RE, (match) => {
|
|
140
141
|
blocks.push(match);
|
|
@@ -145,7 +146,7 @@ function withCodeProtection(text, transform) {
|
|
|
145
146
|
|
|
146
147
|
// 플레이스홀더 복원
|
|
147
148
|
return transformed.replace(
|
|
148
|
-
new RegExp(`${placeholder}(\\d+)${placeholder}`,
|
|
149
|
+
new RegExp(`${placeholder}(\\d+)${placeholder}`, "g"),
|
|
149
150
|
(_, i) => blocks[Number(i)],
|
|
150
151
|
);
|
|
151
152
|
}
|
|
@@ -169,14 +170,14 @@ export function isCompactMode() {
|
|
|
169
170
|
* @returns {string}
|
|
170
171
|
*/
|
|
171
172
|
export function compactify(text) {
|
|
172
|
-
if (!text || typeof text !==
|
|
173
|
+
if (!text || typeof text !== "string") return text ?? "";
|
|
173
174
|
_compactMode = true;
|
|
174
175
|
|
|
175
176
|
return withCodeProtection(text, (segment) => {
|
|
176
177
|
let result = segment;
|
|
177
178
|
for (const { pattern, to } of _compactPairs) {
|
|
178
|
-
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g,
|
|
179
|
-
const re = new RegExp(escaped,
|
|
179
|
+
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
180
|
+
const re = new RegExp(escaped, "gi");
|
|
180
181
|
result = result.replace(re, to);
|
|
181
182
|
}
|
|
182
183
|
return result;
|
|
@@ -189,8 +190,8 @@ export function compactify(text) {
|
|
|
189
190
|
* @param {'default'|'review'|'design'|'docs'} [profile='default'] — 도메인 프로필
|
|
190
191
|
* @returns {string}
|
|
191
192
|
*/
|
|
192
|
-
export function applyCompactRules(text, profile =
|
|
193
|
-
if (!text || typeof text !==
|
|
193
|
+
export function applyCompactRules(text, profile = "default") {
|
|
194
|
+
if (!text || typeof text !== "string") return text ?? "";
|
|
194
195
|
|
|
195
196
|
const rules = PROFILE_MAP[profile] ?? COMPACT_RULES;
|
|
196
197
|
const pairs = buildCompactPairs(rules);
|
|
@@ -198,8 +199,8 @@ export function applyCompactRules(text, profile = 'default') {
|
|
|
198
199
|
return withCodeProtection(text, (segment) => {
|
|
199
200
|
let result = segment;
|
|
200
201
|
for (const { pattern, to } of pairs) {
|
|
201
|
-
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g,
|
|
202
|
-
const re = new RegExp(escaped,
|
|
202
|
+
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
203
|
+
const re = new RegExp(escaped, "gi");
|
|
203
204
|
result = result.replace(re, to);
|
|
204
205
|
}
|
|
205
206
|
return result;
|
|
@@ -212,14 +213,14 @@ export function applyCompactRules(text, profile = 'default') {
|
|
|
212
213
|
* @returns {string}
|
|
213
214
|
*/
|
|
214
215
|
export function expand(text) {
|
|
215
|
-
if (!text || typeof text !==
|
|
216
|
+
if (!text || typeof text !== "string") return text ?? "";
|
|
216
217
|
_compactMode = false;
|
|
217
218
|
|
|
218
219
|
return withCodeProtection(text, (segment) => {
|
|
219
220
|
let result = segment;
|
|
220
221
|
for (const { symbol, restore } of _expandPairs) {
|
|
221
|
-
const escaped = symbol.replace(/[.*+?^${}()|[\]\\]/g,
|
|
222
|
-
const re = new RegExp(escaped,
|
|
222
|
+
const escaped = symbol.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
223
|
+
const re = new RegExp(escaped, "g");
|
|
223
224
|
result = result.replace(re, restore);
|
|
224
225
|
}
|
|
225
226
|
return result;
|