cuelo 0.10.21 → 0.10.22
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/.next/BUILD_ID +1 -1
- package/.next/app-path-routes-manifest.json +4 -4
- package/.next/build-manifest.json +2 -2
- package/.next/prerender-manifest.json +3 -3
- package/.next/required-server-files.js +1 -1
- package/.next/required-server-files.json +1 -1
- package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +2 -2
- package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/api/updates/route.js +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +4 -4
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +3 -3
- package/.next/server/app/index.segments/_full.segment.rsc +4 -4
- package/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/.next/server/app/index.segments/_index.segment.rsc +2 -2
- package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/office/page_client-reference-manifest.js +1 -1
- package/.next/server/app/office-preview/page_client-reference-manifest.js +1 -1
- package/.next/server/app/office-preview.html +1 -1
- package/.next/server/app/office-preview.rsc +2 -2
- package/.next/server/app/office-preview.segments/_full.segment.rsc +2 -2
- package/.next/server/app/office-preview.segments/_head.segment.rsc +1 -1
- package/.next/server/app/office-preview.segments/_index.segment.rsc +2 -2
- package/.next/server/app/office-preview.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/office-preview.segments/office-preview/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/office-preview.segments/office-preview.segment.rsc +1 -1
- package/.next/server/app/office.html +1 -1
- package/.next/server/app/office.rsc +4 -4
- package/.next/server/app/office.segments/_full.segment.rsc +4 -4
- package/.next/server/app/office.segments/_head.segment.rsc +1 -1
- package/.next/server/app/office.segments/_index.segment.rsc +2 -2
- package/.next/server/app/office.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/office.segments/office/__PAGE__.segment.rsc +3 -3
- package/.next/server/app/office.segments/office.segment.rsc +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/recover/page_client-reference-manifest.js +1 -1
- package/.next/server/app/recover.html +1 -1
- package/.next/server/app/recover.rsc +2 -2
- package/.next/server/app/recover.segments/_full.segment.rsc +2 -2
- package/.next/server/app/recover.segments/_head.segment.rsc +1 -1
- package/.next/server/app/recover.segments/_index.segment.rsc +2 -2
- package/.next/server/app/recover.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/recover.segments/recover/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/recover.segments/recover.segment.rsc +1 -1
- package/.next/server/app-paths-manifest.json +4 -4
- package/.next/server/chunks/5489.js +2 -2
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/{9575-032510d25828764f.js → 9575-9b108bc443510ebc.js} +2 -2
- package/.next/static/chunks/app/{layout-a7fca69a4fa9b2c2.js → layout-cdb057e5a5cc0f50.js} +1 -1
- package/.next/trace +108 -108
- package/.next/trace-build +1 -1
- package/Tools/CUELO_Setup/files/native-runtime-patch.js +1 -1
- package/Tools/OMP_Global_Config/agent/extensions/docmap-route.ts +124 -26
- package/Tools/OMP_Global_Config/agent/extensions/lib/docmap-search.ts +38 -2
- package/Tools/OMP_Global_Config/agent/extensions/lib/native-judge.ts +82 -0
- package/Tools/OMP_Global_Config/agent/extensions/mcp-selection.ts +11 -78
- package/Tools/OMP_Global_Config/agent/rules/docs-handoff.md +1 -1
- package/Tools/OMP_Global_Config/patches/apply-core-patch.mjs +117 -198
- package/package.json +1 -1
- /package/.next/static/{IKi2VjUzAnD5AUcDn7QNi → WFIfYmiOdtHjqFYh2crxO}/_buildManifest.js +0 -0
- /package/.next/static/{IKi2VjUzAnD5AUcDn7QNi → WFIfYmiOdtHjqFYh2crxO}/_ssgManifest.js +0 -0
package/.next/trace-build
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
[{"name":"run-webpack","duration":
|
|
1
|
+
[{"name":"run-webpack","duration":117755282,"timestamp":674678,"id":14,"parentId":1,"tags":{},"startTime":1792201420670,"traceId":"8dce0c281bb3683b"},{"name":"run-typescript","duration":21030544,"timestamp":118832470,"id":11087,"parentId":1,"tags":{},"startTime":1792201538828,"traceId":"8dce0c281bb3683b"},{"name":"static-check","duration":4358731,"timestamp":139978787,"id":11090,"parentId":1,"tags":{},"startTime":1792201559975,"traceId":"8dce0c281bb3683b"},{"name":"static-generation","duration":13952586,"timestamp":144451205,"id":11262,"parentId":1,"tags":{},"startTime":1792201564447,"traceId":"8dce0c281bb3683b"},{"name":"collect-build-traces","duration":24903119,"timestamp":144411264,"id":11259,"parentId":1,"tags":{},"startTime":1792201564407,"traceId":"8dce0c281bb3683b"},{"name":"telemetry-flush","duration":95,"timestamp":169321566,"id":11271,"parentId":1,"tags":{},"startTime":1792201589318,"traceId":"8dce0c281bb3683b"},{"name":"next-build","duration":168751116,"timestamp":570574,"id":1,"tags":{"buildMode":"default","version":"16.2.12","bundler":"webpack","has-custom-webpack-config":"true","use-build-worker":"false"},"startTime":1792201420566,"traceId":"8dce0c281bb3683b"}]
|
|
@@ -9,7 +9,7 @@ const MEMORY_RE = /(const phase1Model = await resolveMemoryModel\(\{[^}]*?fallba
|
|
|
9
9
|
const MEMORY_DONE = /const phase1Model = await resolveMemoryModel\(\{[^}]*?fallbackRole: "smol"/;
|
|
10
10
|
const EXPECTED = {
|
|
11
11
|
name: 'cuelo',
|
|
12
|
-
version: '0.10.
|
|
12
|
+
version: '0.10.22',
|
|
13
13
|
distribution: 'cuelo',
|
|
14
14
|
coreVersion: '18.11.3',
|
|
15
15
|
};
|
|
@@ -1,34 +1,132 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
|
|
2
|
-
import { routeDocmap } from "./lib/docmap-search";
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@oh-my-pi/pi-coding-agent";
|
|
2
|
+
import { type DocmapCandidate, type DocmapRoute, routeDocmap } from "./lib/docmap-search";
|
|
3
|
+
import { type CoreJudgmentModules, defaultLoadCore, JUDGE_STOPPED, type NativeJudge, resolveNativeJudge, stoppedBy } from "./lib/native-judge";
|
|
3
4
|
|
|
4
5
|
// 2026-10-16 재조사(doc/history/2026/10/16-rag-reinvestigation): 격리 표본 12건에서 문서 지도를 연 경우가 0건이었다.
|
|
5
6
|
// 도구나 규칙만 두면 같은 문제가 되풀이되므로, 사용자 요청마다 하네스가 지도·RECORD 헤더 검색을 직접 돌려
|
|
6
7
|
// 후보 기록과 맞는 구간을 그 prompt의 숨은 메시지로 넘긴다. 하한 미만·지도 없음·오류면 아무것도 붙이지 않는다.
|
|
7
8
|
// 안내는 external-advice-check와 같은 이유로 aside가 아니라 그 prompt의 `before_agent_start` 메시지로만 붙인다.
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
//
|
|
10
|
+
// 2026-10-17 사용자 결정(doc/history/2026/10/17-docmap-judge): 낱말로 고른 후보의 절반은 주제만 겹치는 지시였다
|
|
11
|
+
// (실제 프롬프트 표본 60건 중 맞는 첨부 29·엉뚱한 첨부 31). 낱말·길이·의도 표지로는 둘이 갈리지 않아, 붙이기 전에
|
|
12
|
+
// native JEV에 후보마다 「요청이 가리키는 그 과거 작업의 기록인가」를 한 요청으로 묻고 P>=0.6인 후보만 붙인다(사용자 선택).
|
|
13
|
+
// 같은 표본에서 맞는 첨부 19/29를 지키고 엉뚱한 첨부는 5/31만 남았다(0.5면 23/29·10/31). 판정 실패·제한 시간·native 판정기 없음은
|
|
14
|
+
// 낱말 후보를 그대로 붙이고(재현율 보존), 실행 중 steering·queued 재요청은 판정하지 않는다(스티어링 지연 방지).
|
|
15
|
+
|
|
16
|
+
export interface DocmapRouteDeps {
|
|
17
|
+
/** 결정적 판정기 seam(테스트). 없으면 {@link loadCore}의 native JEV를 쓴다. */
|
|
18
|
+
resolveJudge?: (ctx: ExtensionContext) => Promise<NativeJudge | undefined>;
|
|
19
|
+
loadCore?: () => Promise<CoreJudgmentModules>;
|
|
20
|
+
/** 판정 대기 상한. 테스트만 짧게 준다. 기본은 DOCMAP_JUDGE_TIMEOUT_MS다. */
|
|
21
|
+
judgeTimeoutMs?: number;
|
|
22
|
+
/** 날짜 의도 판정의 기준 시각(테스트). */
|
|
23
|
+
now?: () => Date;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 판정 대기 상한. 넘으면 낱말 후보를 그대로 붙인다. */
|
|
27
|
+
export const DOCMAP_JUDGE_TIMEOUT_MS = 1_500;
|
|
28
|
+
/** 후보를 남기는 P(관련) 하한. */
|
|
29
|
+
export const DOCMAP_JUDGE_KEEP = 0.6;
|
|
30
|
+
const PROMPT_HEAD_CHARS = 1000;
|
|
31
|
+
const PROMPT_TAIL_CHARS = 1000;
|
|
32
|
+
const CUSTOM_TYPE = "docmap-route";
|
|
33
|
+
|
|
34
|
+
function judgePrompt(prompt: string): string {
|
|
35
|
+
return prompt.length <= PROMPT_HEAD_CHARS + PROMPT_TAIL_CHARS ? prompt : `${prompt.slice(0, PROMPT_HEAD_CHARS)}\n…\n${prompt.slice(-PROMPT_TAIL_CHARS)}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 후보마다 bool 질문 하나. 같은 state를 보므로 한 요청에 묶는다. */
|
|
39
|
+
export function buildDocmapJudgeRequest(prompt: string, route: DocmapRoute): { state: unknown; questions: Record<string, unknown> } {
|
|
40
|
+
const questions: Record<string, unknown> = {};
|
|
41
|
+
route.judgeCandidates.forEach((_, i) => {
|
|
42
|
+
questions[`c${i + 1}`] = {
|
|
43
|
+
type: "bool",
|
|
44
|
+
instructions: `Is candidates[${i}] a record of the earlier work the prompt refers to (the same event, decision, procedure or state), not merely sharing words with it? The prompt is one message a user sent to a coding agent mid-conversation; a new instruction, a reaction or small talk that only shares vocabulary is "no".`,
|
|
45
|
+
criteria: { true: "the prompt asks about or continues the work this record covers", false: "new instruction, reaction, small talk, or only topical word overlap" },
|
|
46
|
+
};
|
|
17
47
|
});
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
48
|
+
return { state: { prompt: judgePrompt(prompt), candidates: route.judgeCandidates }, questions };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 판정 답에서 남길 후보. 답이 없는 후보는 버린다. */
|
|
52
|
+
export function keptByJudge(route: DocmapRoute, answers: Record<string, { bool?: number } | undefined>): DocmapCandidate[] {
|
|
53
|
+
return route.candidates.filter((_, i) => (answers[`c${i + 1}`]?.bool ?? 0) >= DOCMAP_JUDGE_KEEP);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function createDocmapRoute(deps: DocmapRouteDeps = {}) {
|
|
57
|
+
const loadCore = deps.loadCore ?? defaultLoadCore;
|
|
58
|
+
const resolveJudge = deps.resolveJudge ?? ((ctx: ExtensionContext) => resolveNativeJudge(ctx, loadCore, "docmap_route"));
|
|
59
|
+
const judgeTimeoutMs = deps.judgeTimeoutMs ?? DOCMAP_JUDGE_TIMEOUT_MS;
|
|
60
|
+
|
|
61
|
+
return function docmapRoute(pi: ExtensionAPI): void {
|
|
62
|
+
let pendingText: string | undefined;
|
|
63
|
+
let judgePromise: Promise<NativeJudge | undefined> | undefined;
|
|
64
|
+
/** agent_start~agent_end 사이. 실행 중 steering·queued 재요청은 판정 없이 붙인다(mcp-selection과 같은 추적). */
|
|
65
|
+
let inRun = false;
|
|
66
|
+
const judgeFor = (ctx: ExtensionContext) => (judgePromise ??= resolveJudge(ctx).catch(() => undefined));
|
|
67
|
+
|
|
68
|
+
pi.on("session_start", () => {
|
|
69
|
+
pendingText = undefined;
|
|
70
|
+
judgePromise = undefined;
|
|
71
|
+
inRun = false;
|
|
72
|
+
});
|
|
73
|
+
pi.on("session_shutdown", () => { pendingText = undefined; });
|
|
74
|
+
pi.on("agent_start", () => { inRun = true; });
|
|
75
|
+
pi.on("agent_end", () => { inRun = false; });
|
|
76
|
+
pi.on("input", (event) => {
|
|
77
|
+
pendingText = undefined;
|
|
78
|
+
if (event.source !== "interactive" && event.source !== "rpc") return;
|
|
79
|
+
const text = event.text.trim();
|
|
80
|
+
if (text) pendingText = text;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
/** 판정으로 거른 안내문. 판정 실패·제한 시간이면 낱말 후보 그대로. */
|
|
84
|
+
const filtered = async (route: DocmapRoute, prompt: string, ctx: ExtensionContext): Promise<string> => {
|
|
85
|
+
const controller = new AbortController();
|
|
86
|
+
const stopped = stoppedBy(controller.signal);
|
|
87
|
+
let timedOut = false;
|
|
88
|
+
const timer = setTimeout(() => {
|
|
89
|
+
timedOut = true;
|
|
90
|
+
controller.abort();
|
|
91
|
+
}, judgeTimeoutMs);
|
|
92
|
+
try {
|
|
93
|
+
const judge = await Promise.race([judgeFor(ctx), stopped]);
|
|
94
|
+
if (judge === JUDGE_STOPPED) throw new Error("판정 취소");
|
|
95
|
+
if (!judge) throw new Error("native JEV 판정기 없음");
|
|
96
|
+
const result = await Promise.race([judge.judge(buildDocmapJudgeRequest(prompt, route), { signal: controller.signal }), stopped]);
|
|
97
|
+
if (result === JUDGE_STOPPED) throw new Error("판정 취소");
|
|
98
|
+
return route.render(keptByJudge(route, result.answers));
|
|
99
|
+
} catch (error) {
|
|
100
|
+
pi.logger.debug("docmap-route: JEV 후보 판정 없이 낱말 후보를 붙임", {
|
|
101
|
+
error: timedOut ? "판정이 제한 시간 안에 끝나지 않음" : error instanceof Error ? error.message : String(error),
|
|
102
|
+
});
|
|
103
|
+
return route.text;
|
|
104
|
+
} finally {
|
|
105
|
+
clearTimeout(timer);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
110
|
+
// 대화형·RPC는 사용자 입력과 같은 본문의 prompt에만 붙인다. CUELO의 재개·업데이트 통지와 goal 계속 같은
|
|
111
|
+
// 내부 prompt는 `input`을 내지 않아(lib/rpc-manager.ts isGenuineUserRequest) 여기서 빠진다.
|
|
112
|
+
// `omp -p`는 input 이벤트가 없어 최상위 세션의 CLI prompt 자체를 사용자 요청으로 본다. CUELO도 확장을
|
|
113
|
+
// mode 없이 초기화해 ctx.mode가 "print"이므로, UI 문맥이 없다는 것까지 봐야 CLI로 가를 수 있다.
|
|
114
|
+
const fromInput = pendingText !== undefined && event.prompt.includes(pendingText);
|
|
115
|
+
const cliPrompt = (ctx.mode === "print" || ctx.mode === "json") && !ctx.hasUI && ctx.agent?.kind === "main";
|
|
116
|
+
if (!fromInput && !cliPrompt) return;
|
|
117
|
+
const prompt = fromInput ? pendingText! : event.prompt;
|
|
118
|
+
let route: DocmapRoute | undefined;
|
|
119
|
+
try {
|
|
120
|
+
route = routeDocmap(ctx.cwd, prompt, deps.now?.());
|
|
121
|
+
} catch {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
28
124
|
if (!route) return;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
125
|
+
const content = inRun ? route.text : await filtered(route, prompt, ctx);
|
|
126
|
+
if (!content) return;
|
|
127
|
+
return { message: { customType: CUSTOM_TYPE, content, display: false, attribution: "agent" } };
|
|
128
|
+
});
|
|
129
|
+
};
|
|
34
130
|
}
|
|
131
|
+
|
|
132
|
+
export default createDocmapRoute();
|
|
@@ -100,11 +100,24 @@ export interface DocmapSection {
|
|
|
100
100
|
readonly excerpt: string;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
/** 후보 하나를 판정기에 보여 주는 모양. 경로·지도 줄·SCOPE·DATE·가장 맞는 절의 발췌만 싣는다. */
|
|
104
|
+
export interface DocmapJudgeCandidate {
|
|
105
|
+
readonly path: string;
|
|
106
|
+
readonly map?: string;
|
|
107
|
+
readonly scope?: string;
|
|
108
|
+
readonly date?: string;
|
|
109
|
+
readonly excerpt?: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
103
112
|
export interface DocmapRoute {
|
|
104
113
|
readonly topScore: number;
|
|
105
114
|
readonly signals: DocmapSignals;
|
|
106
115
|
readonly candidates: readonly DocmapCandidate[];
|
|
107
116
|
readonly text: string;
|
|
117
|
+
/** 판정기에 넘길 후보 요약. `candidates`와 같은 순서다. */
|
|
118
|
+
readonly judgeCandidates: readonly DocmapJudgeCandidate[];
|
|
119
|
+
/** 후보 일부만 남겼을 때의 안내문. 빈 배열이면 빈 문자열. */
|
|
120
|
+
render(kept: readonly DocmapCandidate[]): string;
|
|
108
121
|
}
|
|
109
122
|
|
|
110
123
|
/** 소문자 ASCII 낱말(2자 이상)과 한글 연속 구간의 2글자 조각. */
|
|
@@ -529,12 +542,17 @@ function clip(text: string, max: number): string {
|
|
|
529
542
|
return one.length > max ? `${one.slice(0, max - 1)}…` : one;
|
|
530
543
|
}
|
|
531
544
|
|
|
545
|
+
/** 지도 줄에서 링크 대상·머리 기호·표 구분을 빼고 한 줄로 줄인다. */
|
|
546
|
+
function mapLineText(line: string): string {
|
|
547
|
+
return clip(line.replace(/\]\([^)]*\)/g, "]").replace(/^[-*]\s*/, "").replace(/\s*\|\s*/g, " "), 200);
|
|
548
|
+
}
|
|
549
|
+
|
|
532
550
|
export function renderDocmapRoute(corpus: DocmapCorpus, candidates: readonly DocmapCandidate[], sections: ReadonlyMap<string, DocmapSection>, maxChars = DOCMAP_MAX_CHARS): string {
|
|
533
551
|
const blocks = candidates.map((c, i) => {
|
|
534
552
|
const u = c.unit;
|
|
535
553
|
const lines = [`${i + 1}. ${rel(corpus.root, u.file)}${u.fields.STATUS ? ` (STATUS: ${clip(u.fields.STATUS, 60)})` : ""}`];
|
|
536
554
|
if (c.supersededFrom) lines.push(` 대체 전: ${rel(corpus.root, c.supersededFrom)}`);
|
|
537
|
-
if (u.mapLines[0]) lines.push(` 지도: ${
|
|
555
|
+
if (u.mapLines[0]) lines.push(` 지도: ${mapLineText(u.mapLines[0])}`);
|
|
538
556
|
else if (u.title) lines.push(` 제목: ${clip(u.title, 160)}`);
|
|
539
557
|
if (u.fields.SCOPE) lines.push(` SCOPE: ${clip(u.fields.SCOPE, 160)}`);
|
|
540
558
|
const sec = sections.get(u.file);
|
|
@@ -572,5 +590,23 @@ export function routeDocmap(cwd: string, prompt: string, now = new Date()): Docm
|
|
|
572
590
|
if (!docmapPasses(signals)) return undefined;
|
|
573
591
|
const candidates = pickDocmapCandidates(corpus, ranked, DOCMAP_MAX_CANDIDATES, DOCMAP_CANDIDATE_RATIO, docmapDateIntent(prompt, now));
|
|
574
592
|
const sections = bestDocmapSections(candidates.map((c) => c.unit.file), prompt);
|
|
575
|
-
|
|
593
|
+
const judgeCandidates = candidates.map((c) => {
|
|
594
|
+
const u = c.unit;
|
|
595
|
+
const sec = sections.get(u.file);
|
|
596
|
+
return {
|
|
597
|
+
path: rel(corpus.root, u.file),
|
|
598
|
+
...(u.mapLines[0] ? { map: mapLineText(u.mapLines[0]) } : u.title ? { map: clip(u.title, 160) } : {}),
|
|
599
|
+
...(u.fields.SCOPE ? { scope: clip(u.fields.SCOPE, 160) } : {}),
|
|
600
|
+
...(u.fields.DATE ? { date: u.fields.DATE } : {}),
|
|
601
|
+
...(sec ? { excerpt: `${clip(sec.heading, 60)}${sec.excerpt ? ` — ${sec.excerpt}` : ""}` } : {}),
|
|
602
|
+
};
|
|
603
|
+
});
|
|
604
|
+
return {
|
|
605
|
+
topScore: top.score,
|
|
606
|
+
signals,
|
|
607
|
+
candidates,
|
|
608
|
+
text: renderDocmapRoute(corpus, candidates, sections),
|
|
609
|
+
judgeCandidates,
|
|
610
|
+
render: (kept) => (kept.length === 0 ? "" : renderDocmapRoute(corpus, kept, sections)),
|
|
611
|
+
};
|
|
576
612
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@oh-my-pi/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* core의 native JEV를 확장에서 쓰는 공통 틀. mcp-selection과 docmap-route가 같이 쓴다.
|
|
5
|
+
* judge 역할의 첫 후보가 native가 아니면 판정하지 않고, 세션 모델 fallback을 붙이지 않으며(sessionModel 미전달),
|
|
6
|
+
* 체인이 native가 아닌 후보를 내면 그 후보에서는 실행하지 않는다.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface JudgeAnswers {
|
|
10
|
+
answers: Record<string, { type: string; choice?: string; bool?: number } | undefined>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** 판정기 seam. 기본 구현은 native 후보에서만 판정을 실행한다. */
|
|
14
|
+
export interface NativeJudge {
|
|
15
|
+
judge(request: { state: unknown; questions: Record<string, unknown> }, options?: { signal?: AbortSignal }): Promise<JudgeAnswers>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type JudgeKind = "native" | "local" | "online";
|
|
19
|
+
|
|
20
|
+
interface ChainJudgeLike {
|
|
21
|
+
withCandidate<T>(run: (judge: NativeJudge, kind: JudgeKind) => Promise<T>, options?: { signal?: AbortSignal }): Promise<T>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 기존 core 판정 모듈. 런타임 값은 host가 해석하고 타입은 여기 필요한 만큼만 둔다. */
|
|
25
|
+
export interface CoreJudgmentModules {
|
|
26
|
+
judgment: {
|
|
27
|
+
resolveJudge(deps: {
|
|
28
|
+
settings: unknown;
|
|
29
|
+
registry: unknown;
|
|
30
|
+
sessionId?: string;
|
|
31
|
+
purpose: string;
|
|
32
|
+
onUsage?: unknown;
|
|
33
|
+
}): ChainJudgeLike;
|
|
34
|
+
hasNativeJudge(settings: unknown, registry: unknown): boolean;
|
|
35
|
+
journalJudgmentUsage?(manager: unknown): unknown;
|
|
36
|
+
};
|
|
37
|
+
settings: { findScopedSettings(cwd?: string): unknown };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function defaultLoadCore(): Promise<CoreJudgmentModules> {
|
|
41
|
+
// 지연 import: 테스트 미러에는 node_modules가 없고, 런타임은 host 번들로 해석한다(jev-runtime과 같다).
|
|
42
|
+
const [judgment, settings] = await Promise.all([
|
|
43
|
+
import("@oh-my-pi/pi-coding-agent/judgment"),
|
|
44
|
+
import("@oh-my-pi/pi-coding-agent/config/settings"),
|
|
45
|
+
]);
|
|
46
|
+
return { judgment, settings } as unknown as CoreJudgmentModules;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function resolveNativeJudge(ctx: ExtensionContext, loadCore: () => Promise<CoreJudgmentModules>, purpose: string): Promise<NativeJudge> {
|
|
50
|
+
const { judgment, settings: settingsModule } = await loadCore();
|
|
51
|
+
const settings = settingsModule.findScopedSettings(ctx.cwd);
|
|
52
|
+
if (!settings) throw new Error("세션 설정을 찾지 못함");
|
|
53
|
+
if (!judgment.hasNativeJudge(settings, ctx.modelRegistry)) throw new Error("judge 역할 첫 후보가 native JEV가 아님");
|
|
54
|
+
const chain = judgment.resolveJudge({
|
|
55
|
+
settings,
|
|
56
|
+
registry: ctx.modelRegistry,
|
|
57
|
+
sessionId: ctx.sessionManager?.getSessionId?.(),
|
|
58
|
+
purpose,
|
|
59
|
+
onUsage: judgment.journalJudgmentUsage?.(ctx.sessionManager),
|
|
60
|
+
});
|
|
61
|
+
return {
|
|
62
|
+
judge: (request, options) =>
|
|
63
|
+
chain.withCandidate(
|
|
64
|
+
(candidate, kind) =>
|
|
65
|
+
kind === "native" ? candidate.judge(request, options) : Promise.reject(new Error(`native가 아닌 judge 후보(${kind})`)),
|
|
66
|
+
options,
|
|
67
|
+
),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 취소·제한 시간이 판정보다 먼저 끝났음을 나타내는 race 표지. */
|
|
72
|
+
export const JUDGE_STOPPED: unique symbol = Symbol("judge-stopped");
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* `signal`이 끊기면 바로 돌아오는 promise. provider가 signal을 무시해도 호출부가 `Promise.race`로 제때 빠져나온다.
|
|
76
|
+
*/
|
|
77
|
+
export function stoppedBy(signal: AbortSignal): Promise<typeof JUDGE_STOPPED> {
|
|
78
|
+
const stopped = Promise.withResolvers<typeof JUDGE_STOPPED>();
|
|
79
|
+
if (signal.aborted) stopped.resolve(JUDGE_STOPPED);
|
|
80
|
+
else signal.addEventListener("abort", () => stopped.resolve(JUDGE_STOPPED), { once: true });
|
|
81
|
+
return stopped.promise;
|
|
82
|
+
}
|
|
@@ -16,6 +16,9 @@ import {
|
|
|
16
16
|
summarizeProjectStack,
|
|
17
17
|
VERIFIED_CATALOG,
|
|
18
18
|
} from "./lib/mcp-selection";
|
|
19
|
+
import { type CoreJudgmentModules, defaultLoadCore, JUDGE_STOPPED, type NativeJudge, resolveNativeJudge, stoppedBy } from "./lib/native-judge";
|
|
20
|
+
|
|
21
|
+
export type { CoreJudgmentModules, NativeJudge } from "./lib/native-judge";
|
|
19
22
|
|
|
20
23
|
/**
|
|
21
24
|
* 요청별 MCP 선택(`mcp.selection: per-request`). 패치된 core가 매 사용자 요청의 system prompt를 만들기
|
|
@@ -26,37 +29,6 @@ import {
|
|
|
26
29
|
* `mcp_select`를 모르는 core(standalone omp 등)에서는 핸들러가 호출되지 않아 아무 일도 하지 않는다.
|
|
27
30
|
*/
|
|
28
31
|
|
|
29
|
-
interface JudgeAnswers {
|
|
30
|
-
answers: Record<string, { type: string; choice?: string } | undefined>;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** 판정기 seam. 기본 구현은 native 후보에서만 판정을 실행한다. */
|
|
34
|
-
export interface NativeJudge {
|
|
35
|
-
judge(request: { state: unknown; questions: Record<string, unknown> }, options?: { signal?: AbortSignal }): Promise<JudgeAnswers>;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
type JudgeKind = "native" | "local" | "online";
|
|
39
|
-
|
|
40
|
-
interface ChainJudgeLike {
|
|
41
|
-
withCandidate<T>(run: (judge: NativeJudge, kind: JudgeKind) => Promise<T>, options?: { signal?: AbortSignal }): Promise<T>;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** 기존 core 판정 모듈. 런타임 값은 host가 해석하고 타입은 여기 필요한 만큼만 둔다. */
|
|
45
|
-
export interface CoreJudgmentModules {
|
|
46
|
-
judgment: {
|
|
47
|
-
resolveJudge(deps: {
|
|
48
|
-
settings: unknown;
|
|
49
|
-
registry: unknown;
|
|
50
|
-
sessionId?: string;
|
|
51
|
-
purpose: string;
|
|
52
|
-
onUsage?: unknown;
|
|
53
|
-
}): ChainJudgeLike;
|
|
54
|
-
hasNativeJudge(settings: unknown, registry: unknown): boolean;
|
|
55
|
-
journalJudgmentUsage?(manager: unknown): unknown;
|
|
56
|
-
};
|
|
57
|
-
settings: { findScopedSettings(cwd?: string): unknown };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
32
|
export interface McpSelectionDeps {
|
|
61
33
|
/** 결정적 판정기 seam(테스트). 없으면 {@link loadCore}의 native JEV를 쓴다. */
|
|
62
34
|
resolveJudge?: (ctx: ExtensionContext) => Promise<NativeJudge | undefined>;
|
|
@@ -89,8 +61,6 @@ const STACK_DIR_LIMIT = 40;
|
|
|
89
61
|
/** 새 요청의 판정 대기 상한. 넘으면 판정을 취소하고 판정 실패와 같게 새 연결 없이 진행한다(runner 30초 제한에 기대지 않는다). */
|
|
90
62
|
const MCP_SELECTION_TIMEOUT_MS = 3_000;
|
|
91
63
|
const JUDGE_TIMEOUT_REASON = "판정이 제한 시간 안에 끝나지 않음";
|
|
92
|
-
/** 취소·제한 시간이 판정보다 먼저 끝났음을 나타내는 race 표지. */
|
|
93
|
-
const STOPPED: unique symbol = Symbol("mcp-selection-stopped");
|
|
94
64
|
|
|
95
65
|
async function readJson(path: string): Promise<Record<string, unknown> | undefined> {
|
|
96
66
|
try {
|
|
@@ -167,46 +137,11 @@ async function readProjectStack(cwd: string): Promise<string> {
|
|
|
167
137
|
return summarizeProjectStack({ fileNames, dependencyNames });
|
|
168
138
|
}
|
|
169
139
|
|
|
170
|
-
async function defaultLoadCore(): Promise<CoreJudgmentModules> {
|
|
171
|
-
// 지연 import: 테스트 미러에는 node_modules가 없고, 런타임은 host 번들로 해석한다(jev-runtime과 같다).
|
|
172
|
-
const [judgment, settings] = await Promise.all([
|
|
173
|
-
import("@oh-my-pi/pi-coding-agent/judgment"),
|
|
174
|
-
import("@oh-my-pi/pi-coding-agent/config/settings"),
|
|
175
|
-
]);
|
|
176
|
-
return { judgment, settings } as unknown as CoreJudgmentModules;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* 기존 native JEV만 쓴다. judge 역할의 첫 후보가 native가 아니면 판정하지 않고, 세션 모델 fallback을
|
|
181
|
-
* 붙이지 않으며(sessionModel 미전달), 체인이 native가 아닌 후보를 내면 그 후보에서는 실행하지 않는다.
|
|
182
|
-
*/
|
|
183
|
-
async function resolveNativeJudge(ctx: ExtensionContext, loadCore: () => Promise<CoreJudgmentModules>): Promise<NativeJudge> {
|
|
184
|
-
const { judgment, settings: settingsModule } = await loadCore();
|
|
185
|
-
const settings = settingsModule.findScopedSettings(ctx.cwd);
|
|
186
|
-
if (!settings) throw new Error("세션 설정을 찾지 못함");
|
|
187
|
-
if (!judgment.hasNativeJudge(settings, ctx.modelRegistry)) throw new Error("judge 역할 첫 후보가 native JEV가 아님");
|
|
188
|
-
const chain = judgment.resolveJudge({
|
|
189
|
-
settings,
|
|
190
|
-
registry: ctx.modelRegistry,
|
|
191
|
-
sessionId: ctx.sessionManager?.getSessionId?.(),
|
|
192
|
-
purpose: "mcp_selection",
|
|
193
|
-
onUsage: judgment.journalJudgmentUsage?.(ctx.sessionManager),
|
|
194
|
-
});
|
|
195
|
-
return {
|
|
196
|
-
judge: (request, options) =>
|
|
197
|
-
chain.withCandidate(
|
|
198
|
-
(candidate, kind) =>
|
|
199
|
-
kind === "native" ? candidate.judge(request, options) : Promise.reject(new Error(`native가 아닌 judge 후보(${kind})`)),
|
|
200
|
-
options,
|
|
201
|
-
),
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
|
|
205
140
|
export function createMcpSelection(deps: McpSelectionDeps = {}) {
|
|
206
141
|
const agentDir = deps.agentDir ?? join(import.meta.dir, "..");
|
|
207
142
|
const catalog = deps.catalog ?? VERIFIED_CATALOG;
|
|
208
143
|
const loadCore = deps.loadCore ?? defaultLoadCore;
|
|
209
|
-
const resolveJudge = deps.resolveJudge ?? ((ctx: ExtensionContext) => resolveNativeJudge(ctx, loadCore));
|
|
144
|
+
const resolveJudge = deps.resolveJudge ?? ((ctx: ExtensionContext) => resolveNativeJudge(ctx, loadCore, "mcp_selection"));
|
|
210
145
|
const judgeTimeoutMs = deps.judgeTimeoutMs ?? MCP_SELECTION_TIMEOUT_MS;
|
|
211
146
|
|
|
212
147
|
return function mcpSelection(pi: ExtensionAPI): void {
|
|
@@ -280,22 +215,20 @@ export function createMcpSelection(deps: McpSelectionDeps = {}) {
|
|
|
280
215
|
const answers: Record<string, string | undefined> = {};
|
|
281
216
|
const signal = event.signal ? AbortSignal.any([event.signal, token.controller.signal]) : token.controller.signal;
|
|
282
217
|
// provider가 signal을 무시해도 취소·제한 시간에 바로 돌아온다.
|
|
283
|
-
const stopped =
|
|
284
|
-
if (signal.aborted) stopped.resolve(STOPPED);
|
|
285
|
-
else signal.addEventListener("abort", () => stopped.resolve(STOPPED), { once: true });
|
|
218
|
+
const stopped = stoppedBy(signal);
|
|
286
219
|
let timedOut = false;
|
|
287
220
|
const timer = setTimeout(() => {
|
|
288
221
|
timedOut = true;
|
|
289
222
|
token.controller.abort();
|
|
290
223
|
}, judgeTimeoutMs);
|
|
291
224
|
try {
|
|
292
|
-
const judge = await Promise.race([judgeFor(ctx), stopped
|
|
293
|
-
if (judge ===
|
|
225
|
+
const judge = await Promise.race([judgeFor(ctx), stopped]);
|
|
226
|
+
if (judge === JUDGE_STOPPED) throw new Error("판정 취소");
|
|
294
227
|
if (!judge) throw new Error("JEV 판정기를 해석하지 못함");
|
|
295
|
-
const stack = await Promise.race([projectStack, stopped
|
|
296
|
-
if (stack ===
|
|
297
|
-
const result = await Promise.race([judge.judge(buildJudgeRequest(cue, stack, candidates), { signal }), stopped
|
|
298
|
-
if (result ===
|
|
228
|
+
const stack = await Promise.race([projectStack, stopped]);
|
|
229
|
+
if (stack === JUDGE_STOPPED) throw new Error("판정 취소");
|
|
230
|
+
const result = await Promise.race([judge.judge(buildJudgeRequest(cue, stack, candidates), { signal }), stopped]);
|
|
231
|
+
if (result === JUDGE_STOPPED) throw new Error("판정 취소");
|
|
299
232
|
for (const [key, answer] of Object.entries(result.answers)) {
|
|
300
233
|
answers[key] = answer?.type === "choice" ? answer.choice : undefined;
|
|
301
234
|
}
|
|
@@ -22,7 +22,7 @@ description: HANDOFF·README·doc 지도의 역할, 작업 기록 소유권과
|
|
|
22
22
|
- 미해결 실패도 같은 규칙으로 다룬다. 관련 작업에서 만난 미해결 실패는 새 절차를 만들지 않고 기존 실패 환류(`verdict.md` 「실패를 다음 작업의 실행 방식으로 환류」)와 `RULES.md`의 동일 재시도 금지를 따른다: 오류 원문 등 증거로 원인을 좁히고, 이전과 다른 조치를 하고, 원래 실패했던 경로를 다시 실행해 닫는다. 가드·검증을 약화하거나 바뀐 것 없이 되풀이하지 않는다. 승인·권한·범위 때문에 막히면 Main이 `HANDOFF.md` 행에 실패 증거, 시도한 조치, 정확한 차단 사유·필요한 승인, 다음 행동, 재검증·종료 기준을 적어 다음 관련 세션이 이어받게 한다. 원인 해결 증거 없이 영구 결함이 고쳐졌다고 단정하지 않는다.
|
|
23
23
|
- 다음 신호를 보면 해당 절을 바로 고친다: ① Maker가 끝낸 범위가 HANDOFF에서 미적용으로 남음 ② 작업 행의 다음 행동·미해결 경계를 설명할 수 없음 ③ 같은 상태를 둘 이상 독립 서술함 ④ 열린 일이 늘지 않는데 HANDOFF만 늘어남. Maker는 종료 보고에 HANDOFF 영향 주제와 남은 행동 유무를 적는다. Main은 관련 절과 참조처만 확인하며 상시 감사·별도 hook·문서 DB를 만들지 않는다.
|
|
24
24
|
- 기록은 역할별이다: `DD-주제/main.md`는 Main 판단·수용·공유 인계, `maker-<name>.md`는 그 Maker의 변경·검증·남은 제한이다. Maker는 자기 기록만 쓰며 Main 단독 작업에는 Maker 기록을 만들지 않는다. 발주 시 Main은 Maker 기록과 `evidence/` 경로를 `OWNED_PATHS`에 넣고, 공유 지도는 Main 또는 지정된 한 owner가 갱신한다. 긴 diff·로그는 작업 폴더 `evidence/`에 두고 상대 링크로 가리킨다. secret·대화 원문·인증 DB는 기록하지 않는다.
|
|
25
|
-
- 검색 순서는 ① `HANDOFF.md` ② `doc/README.md` 지도·월별 목록 ③ 경로·주제 키워드로 `doc/history/**/*.md`의 `RECORD:` 헤더(`SCOPE`·`PATHS`)를 최신순 검색 ④ 일치 기록의 헤더와 필요한 줄 범위만 읽는다. ②③은 하네스가 먼저 돈다: 사용자 요청마다 `docmap-route` 확장이 루트·하위 지도 줄과 기록 제목·헤더만으로 요청 중에 BM25를 만들어(긴 요청은 앞·끝 1,000자), 기록 수로 정규화한 점수·흔하지 않은 낱말의 포괄도 기준을
|
|
25
|
+
- 검색 순서는 ① `HANDOFF.md` ② `doc/README.md` 지도·월별 목록 ③ 경로·주제 키워드로 `doc/history/**/*.md`의 `RECORD:` 헤더(`SCOPE`·`PATHS`)를 최신순 검색 ④ 일치 기록의 헤더와 필요한 줄 범위만 읽는다. ②③은 하네스가 먼저 돈다: 사용자 요청마다 `docmap-route` 확장이 루트·하위 지도 줄과 기록 제목·헤더만으로 요청 중에 BM25를 만들어(긴 요청은 앞·끝 1,000자), 기록 수로 정규화한 점수·흔하지 않은 낱말의 포괄도 기준을 넘는 후보 최대 3개를 고른 뒤, native Jev 한 요청으로 후보마다 「요청이 가리키는 그 과거 작업의 기록인가」를 물어 남은 후보만(경로·지도 줄·SCOPE·STATUS·가장 맞는 절의 줄 범위와 발췌) `DATE` 최신순 동점 처리로 숨은 메시지로 붙인다(2026-10-17 사용자 결정; 판정 실패·1.5초 초과·실행 중 재요청은 낱말 후보 그대로). 후보는 근거가 아니라 확인할 곳이다. 답하기 전에 그 구간을 읽어 맞는지 확인하고 맞지 않으면 버린 뒤 ②③을 직접 이어 간다. 후보가 없으면 위 순서를 그대로 따른다. `STATUS: superseded-by <path>`면 새 경로로 이동한다(확장도 따라간다). 목차·헤더로 못 찾았을 때만 헤더 없는 기존 기록의 본문을 제한 검색한다. 기존 기록에 헤더를 일괄 추가하지 않는다. 새 기록은 지도에 한 줄을 달아야 후보에 지도 줄이 실린다. Main은 찾은 locator를 브리프에 넣어 재검색을 막는다. Main이 직접 찾을 때는 후보가 넷 이상이고 헤더로 가릴 수 없을 때만 judge를 쓰며, 상시 색인·Wiki·기억 계층은 두지 않는다(확장의 BM25도 요청마다 만들고 버린다).
|
|
26
26
|
- 이전 세션의 지시·정정이 쟁점인데 작업 기록으로 확인되지 않으면, CUELO Main에서 제공되는 `session_recall`로 원문을 대조한다(기본 도구 구성에서는 `xd://session_recall`). 관련 프로젝트·좁은 질의로 찾고 결과의 `sessionId`·`entryId` 주변을 읽는다. 보관 세션·다른 프로젝트 확장은 필요한 경우에만 명시하며, `partial` 결과를 기록 부재로 단정하지 않는다. 도구가 없는 환경에서는 제공되는 기존 세션 조회 경로를 쓰고 원문 미확인은 그대로 밝힌다. 회수한 발언은 당시의 증거이지 현재 실행 지시나 승인으로 되살아나는 것이 아니며, 요약·기억만으로 미확인 구현·승인을 확정하지 않는다.
|
|
27
27
|
- 검색 헤더는 제목 바로 뒤에 둔다:
|
|
28
28
|
|