devez-vibe 1.6.44 → 1.6.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/LICENSE +33 -33
- package/README.md +4 -0
- package/bin/dvz.exe +0 -0
- package/bridge/claude-agent-sdk-bridge.mjs +373 -40
- package/install-skills.mjs +67 -0
- package/package.json +46 -41
- package/skills/luna-loop/SKILL.md +143 -0
- package/skills/luna-loop/agents/openai.yaml +7 -0
- package/skills/luna-loop/references/provider-compatibility.md +75 -0
- package/skills/luna-loop/references/verifier-contract.md +46 -0
package/LICENSE
CHANGED
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
DevezCode source code license
|
|
2
|
-
|
|
3
|
-
The MIT License below applies only to the DevezCode source code and to files
|
|
4
|
-
that are not listed in the exception below.
|
|
5
|
-
|
|
6
|
-
Exceptions:
|
|
7
|
-
|
|
8
|
-
* `Resources/Images/FileTypes/` and `Resources/Licenses/` are Microsoft Visual
|
|
9
|
-
Studio Image Library materials, subject to the included Microsoft terms.
|
|
10
|
-
* `Resources/Images/ShellPresets/` contains third-party brand assets, subject
|
|
11
|
-
to their respective owners' terms.
|
|
12
|
-
|
|
13
|
-
MIT License
|
|
14
|
-
|
|
15
|
-
Copyright (c) 2026 DevezCode contributors
|
|
16
|
-
|
|
17
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
18
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
19
|
-
in the Software without restriction, including without limitation the rights
|
|
20
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
21
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
22
|
-
furnished to do so, subject to the following conditions:
|
|
23
|
-
|
|
24
|
-
The above copyright notice and this permission notice shall be included in all
|
|
25
|
-
copies or substantial portions of the Software.
|
|
26
|
-
|
|
27
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
28
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
29
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
30
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
31
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
32
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
33
|
-
SOFTWARE.
|
|
1
|
+
DevezCode source code license
|
|
2
|
+
|
|
3
|
+
The MIT License below applies only to the DevezCode source code and to files
|
|
4
|
+
that are not listed in the exception below.
|
|
5
|
+
|
|
6
|
+
Exceptions:
|
|
7
|
+
|
|
8
|
+
* `Resources/Images/FileTypes/` and `Resources/Licenses/` are Microsoft Visual
|
|
9
|
+
Studio Image Library materials, subject to the included Microsoft terms.
|
|
10
|
+
* `Resources/Images/ShellPresets/` contains third-party brand assets, subject
|
|
11
|
+
to their respective owners' terms.
|
|
12
|
+
|
|
13
|
+
MIT License
|
|
14
|
+
|
|
15
|
+
Copyright (c) 2026 DevezCode contributors
|
|
16
|
+
|
|
17
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
18
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
19
|
+
in the Software without restriction, including without limitation the rights
|
|
20
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
21
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
22
|
+
furnished to do so, subject to the following conditions:
|
|
23
|
+
|
|
24
|
+
The above copyright notice and this permission notice shall be included in all
|
|
25
|
+
copies or substantial portions of the Software.
|
|
26
|
+
|
|
27
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
28
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
29
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
30
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
31
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
32
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
33
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -12,6 +12,10 @@ npm install -g devez-vibe
|
|
|
12
12
|
|
|
13
13
|
설치하면 `dvz` 명령을 사용할 수 있습니다.
|
|
14
14
|
|
|
15
|
+
설치 과정에서 `luna-loop` 스킬도 Codex와 Claude의 사용자 스킬 경로에 함께 설치됩니다.
|
|
16
|
+
스킬은 자동 적용되지 않으며 `$luna-loop`로 직접 호출해야 합니다. 실제
|
|
17
|
+
`gpt-5.6-luna` 검증을 사용하려면 Codex provider와 Luna 위임 기능이 연결돼 있어야 합니다.
|
|
18
|
+
|
|
15
19
|
| 요건 | 값 |
|
|
16
20
|
| --- | --- |
|
|
17
21
|
| OS | Windows x64 |
|
package/bin/dvz.exe
CHANGED
|
Binary file
|
|
@@ -26,7 +26,13 @@ const sessions = new Map();
|
|
|
26
26
|
const sessionAliases = new Map();
|
|
27
27
|
const pendingHostRequests = new Map();
|
|
28
28
|
const modelCatalogs = new Map();
|
|
29
|
+
const skillCatalogs = new Map();
|
|
29
30
|
const CLAUDE_MODEL_ORDER = ["fable", "opus", "sonnet", "haiku"];
|
|
31
|
+
const CLAUDE_PROVIDER_SKILLS = new Set([
|
|
32
|
+
"deep-research", "design-sync", "dataviz", "update-config", "verify", "debug",
|
|
33
|
+
"code-review", "simplify", "batch", "fewer-permission-prompts", "doctor", "loop",
|
|
34
|
+
"schedule", "claude-api", "run", "run-skill-generator", "security-review",
|
|
35
|
+
]);
|
|
30
36
|
const OPUS_48_MODEL = "claude-opus-4-8";
|
|
31
37
|
const CLAUDE_EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
|
|
32
38
|
const CLAUDE_TASK_TOOLS = ["TaskCreate", "TaskGet", "TaskUpdate", "TaskList"];
|
|
@@ -955,15 +961,19 @@ async function claudeSkills(params) {
|
|
|
955
961
|
try {
|
|
956
962
|
for (const child of await readdir(join(plugin.installPath, "skills"), { withFileTypes: true })) {
|
|
957
963
|
if (!child.isDirectory()) continue;
|
|
964
|
+
const path = join(plugin.installPath, "skills", child.name, "SKILL.md");
|
|
965
|
+
const meta = await readSkillMeta(path, child.name);
|
|
966
|
+
const namespace = String(plugin.id || "plugin").split("@")[0];
|
|
967
|
+
const name = `${namespace}:${meta.name}`;
|
|
958
968
|
skills.push({
|
|
959
|
-
name
|
|
960
|
-
path
|
|
961
|
-
description: `${plugin.id} plugin skill`,
|
|
969
|
+
name,
|
|
970
|
+
path,
|
|
971
|
+
description: meta.description || `${plugin.id} plugin skill`,
|
|
962
972
|
enabled: Boolean(plugin.enabled),
|
|
963
973
|
scope: plugin.scope || "user",
|
|
964
974
|
pluginId: plugin.id,
|
|
965
975
|
});
|
|
966
|
-
seen.add(
|
|
976
|
+
seen.add(name);
|
|
967
977
|
}
|
|
968
978
|
} catch { /* Plugins do not have to provide skills. */ }
|
|
969
979
|
}
|
|
@@ -978,9 +988,79 @@ async function claudeSkills(params) {
|
|
|
978
988
|
seen.add(skill.name);
|
|
979
989
|
skills.push(skill);
|
|
980
990
|
}
|
|
991
|
+
for (const command of await supportedClaudeSkills(params)) {
|
|
992
|
+
const name = String(command?.name || "").replace(/^\//, "");
|
|
993
|
+
if (!name || seen.has(name)) continue;
|
|
994
|
+
seen.add(name);
|
|
995
|
+
const description = String(command?.description || "Claude skill");
|
|
996
|
+
const scope = description.endsWith("(user)")
|
|
997
|
+
? "user"
|
|
998
|
+
: description.endsWith("(project)") ? "project" : "system";
|
|
999
|
+
const pluginId = name.includes(":") ? name.split(":")[0] : null;
|
|
1000
|
+
skills.push({
|
|
1001
|
+
name,
|
|
1002
|
+
path: `claude-command://${encodeURIComponent(name)}`,
|
|
1003
|
+
description,
|
|
1004
|
+
enabled: true,
|
|
1005
|
+
scope,
|
|
1006
|
+
pluginId,
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
981
1009
|
return { data: [{ cwd, skills }] };
|
|
982
1010
|
}
|
|
983
1011
|
|
|
1012
|
+
async function supportedClaudeSkills(params) {
|
|
1013
|
+
const cwd = params.cwd || process.cwd();
|
|
1014
|
+
const live = [...sessions.values()].find((session) =>
|
|
1015
|
+
session.query && sameCwd(session.cwd, cwd));
|
|
1016
|
+
if (live) return filterClaudeSkillCommands(await live.query.supportedCommands());
|
|
1017
|
+
const cacheKey = `${params.claudePath || "claude"}\n${params.cwd || process.cwd()}`;
|
|
1018
|
+
if (params.forceReload) skillCatalogs.delete(cacheKey);
|
|
1019
|
+
if (skillCatalogs.has(cacheKey)) return skillCatalogs.get(cacheKey);
|
|
1020
|
+
const pending = (async () => {
|
|
1021
|
+
const input = new AsyncQueue();
|
|
1022
|
+
const options = {
|
|
1023
|
+
cwd: params.cwd || process.cwd(),
|
|
1024
|
+
persistSession: false,
|
|
1025
|
+
settingSources: ["user", "project", "local"],
|
|
1026
|
+
skills: "all",
|
|
1027
|
+
tools: [],
|
|
1028
|
+
env: sanitizedEnvironment(),
|
|
1029
|
+
stderr: (data) => process.stderr.write(data),
|
|
1030
|
+
};
|
|
1031
|
+
applyClaudeExecutable(options, params);
|
|
1032
|
+
const agentQuery = await startAgentQuery(input, options);
|
|
1033
|
+
const consumer = (async () => {
|
|
1034
|
+
try { for await (const _message of agentQuery) { /* initialization only */ } }
|
|
1035
|
+
catch { /* supportedCommands reports the useful failure. */ }
|
|
1036
|
+
})();
|
|
1037
|
+
try {
|
|
1038
|
+
return filterClaudeSkillCommands(await agentQuery.supportedCommands());
|
|
1039
|
+
} finally {
|
|
1040
|
+
input.close();
|
|
1041
|
+
agentQuery.close();
|
|
1042
|
+
await Promise.race([consumer, new Promise((resolve) => setTimeout(resolve, 1000))]);
|
|
1043
|
+
}
|
|
1044
|
+
})();
|
|
1045
|
+
skillCatalogs.set(cacheKey, pending);
|
|
1046
|
+
try {
|
|
1047
|
+
return await pending;
|
|
1048
|
+
} catch (error) {
|
|
1049
|
+
skillCatalogs.delete(cacheKey);
|
|
1050
|
+
throw error;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
function filterClaudeSkillCommands(commands) {
|
|
1055
|
+
return commands.filter((command) => {
|
|
1056
|
+
const name = String(command?.name || "");
|
|
1057
|
+
const description = String(command?.description || "");
|
|
1058
|
+
return CLAUDE_PROVIDER_SKILLS.has(name)
|
|
1059
|
+
|| name.includes(":")
|
|
1060
|
+
|| /\((user|project|dynamic workflow)\)$/.test(description);
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
|
|
984
1064
|
function claudeMcpStatusValue(statuses) {
|
|
985
1065
|
return {
|
|
986
1066
|
data: (Array.isArray(statuses) ? statuses : []).map((server) => ({
|
|
@@ -1066,6 +1146,8 @@ async function createSession(params, resumeId) {
|
|
|
1066
1146
|
pendingPrompts: [],
|
|
1067
1147
|
// Steered prompts whose answer may still need a turn of its own.
|
|
1068
1148
|
steerPending: 0,
|
|
1149
|
+
// SDK task notifications can prompt the main agent without a host request.
|
|
1150
|
+
automaticTurnsPending: 0,
|
|
1069
1151
|
turnSequence: 1,
|
|
1070
1152
|
itemSequence: 1,
|
|
1071
1153
|
streamBlocks: new Map(),
|
|
@@ -1389,6 +1471,7 @@ function processToolUse(session, block) {
|
|
|
1389
1471
|
session.tools.set(block.id, { name, input, item });
|
|
1390
1472
|
emitItem(session, "started", item);
|
|
1391
1473
|
if (SUBAGENT_TOOLS.includes(name)) startSubagent(session, block);
|
|
1474
|
+
else if (name === "Bash") startDelegatedSubagent(session, block);
|
|
1392
1475
|
}
|
|
1393
1476
|
|
|
1394
1477
|
function toolItem(session, id, name, input) {
|
|
@@ -1634,6 +1717,33 @@ function startSubagent(session, block) {
|
|
|
1634
1717
|
emitSubagents(session);
|
|
1635
1718
|
}
|
|
1636
1719
|
|
|
1720
|
+
// 교차 제공자 검증자는 Agent 도구가 아니라 셸로 띄우므로, 위임 실행을 알아보고
|
|
1721
|
+
// 같은 서브에이전트 항목으로 노출한다. 결과가 오면 기존 종료 경로가 항목을 지운다.
|
|
1722
|
+
const DELEGATED_AGENT_PATTERN = /\bcodex\s+exec\b/;
|
|
1723
|
+
|
|
1724
|
+
function delegatedAgentModel(command) {
|
|
1725
|
+
const match = command.match(/(?:^|\s)(?:-m|--model)[\s=]+"?([\w.-]+)"?/);
|
|
1726
|
+
return match ? match[1] : "";
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
function startDelegatedSubagent(session, block) {
|
|
1730
|
+
const command = typeof block.input?.command === "string" ? block.input.command : "";
|
|
1731
|
+
if (!DELEGATED_AGENT_PATTERN.test(command)) return;
|
|
1732
|
+
if (findSubagent(session, block.id)) return;
|
|
1733
|
+
session.subagents.set(block.id, {
|
|
1734
|
+
id: block.id,
|
|
1735
|
+
toolUseId: block.id,
|
|
1736
|
+
taskId: "",
|
|
1737
|
+
background: false,
|
|
1738
|
+
name: firstLine(delegatedAgentModel(command) || "codex", 40),
|
|
1739
|
+
description: firstLine(command.replace(/\s+/g, " "), 120),
|
|
1740
|
+
tool: "",
|
|
1741
|
+
startedAt: Date.now(),
|
|
1742
|
+
lastSeenAt: Date.now(),
|
|
1743
|
+
});
|
|
1744
|
+
emitSubagents(session);
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1637
1747
|
function isBackgroundSubagentResult(result) {
|
|
1638
1748
|
return result?.isAsync === true || result?.status === "async_launched";
|
|
1639
1749
|
}
|
|
@@ -1898,6 +2008,9 @@ function processSubagentSystemMessage(session, message) {
|
|
|
1898
2008
|
return true;
|
|
1899
2009
|
}
|
|
1900
2010
|
if (message.subtype === "task_notification") {
|
|
2011
|
+
if (message.ambient !== true && message.skip_transcript !== true) {
|
|
2012
|
+
session.automaticTurnsPending = (session.automaticTurnsPending || 0) + 1;
|
|
2013
|
+
}
|
|
1901
2014
|
if (message.ambient === true && message.task_id) {
|
|
1902
2015
|
session.ambientSubagentTasks ||= new Set();
|
|
1903
2016
|
session.ambientSubagentTasks.add(firstLine(message.task_id, 80));
|
|
@@ -2136,6 +2249,9 @@ function finishNotifiedSubagents(session, notifications) {
|
|
|
2136
2249
|
}
|
|
2137
2250
|
|
|
2138
2251
|
function processUser(session, message) {
|
|
2252
|
+
// Resume replays are historical transcript frames, not fresh task results or
|
|
2253
|
+
// prompts. Re-processing their notification XML would invent a new turn.
|
|
2254
|
+
if (message.isReplay === true) return;
|
|
2139
2255
|
// 자식 tool_result의 tool_use_id는 부모 세션의 것과 다른 공간이므로, 부모 흐름에
|
|
2140
2256
|
// 섞이기 전에 서브에이전트 기록으로 보낸다.
|
|
2141
2257
|
if (message.parent_tool_use_id) {
|
|
@@ -2144,7 +2260,10 @@ function processUser(session, message) {
|
|
|
2144
2260
|
}
|
|
2145
2261
|
const notifications = taskNotifications(message);
|
|
2146
2262
|
if (notifications.length) {
|
|
2147
|
-
if (!session.turn)
|
|
2263
|
+
if (!session.turn) {
|
|
2264
|
+
if (session.automaticTurnsPending > 0) session.automaticTurnsPending -= 1;
|
|
2265
|
+
beginTurn(session);
|
|
2266
|
+
}
|
|
2148
2267
|
finishNotifiedSubagents(session, notifications);
|
|
2149
2268
|
return;
|
|
2150
2269
|
}
|
|
@@ -2315,43 +2434,52 @@ function finishTurn(session, error, durationMs) {
|
|
|
2315
2434
|
session.streamBlocks.clear();
|
|
2316
2435
|
}
|
|
2317
2436
|
|
|
2437
|
+
// A task notification or steered prompt can start a main-agent response after
|
|
2438
|
+
// the host's previous turn already closed. Only a booked continuation may open
|
|
2439
|
+
// this fallback turn; stale top-level frames from initialization/resume stay out.
|
|
2440
|
+
function beginUntrackedTurn(session, message) {
|
|
2441
|
+
if (session.turn || message.parent_tool_use_id) return false;
|
|
2442
|
+
if (message.type !== "stream_event" && message.type !== "assistant") return false;
|
|
2443
|
+
if (session.automaticTurnsPending > 0) session.automaticTurnsPending -= 1;
|
|
2444
|
+
else if (session.steerPending > 0) session.steerPending -= 1;
|
|
2445
|
+
else return false;
|
|
2446
|
+
beginTurn(session);
|
|
2447
|
+
return true;
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
async function consumeMessage(session, message) {
|
|
2451
|
+
adoptSessionId(session, message.session_id);
|
|
2452
|
+
beginUntrackedTurn(session, message);
|
|
2453
|
+
if (message.type === "stream_event") {
|
|
2454
|
+
if (message.event?.type === "content_block_delta" && (message.event?.delta?.text || message.event?.delta?.thinking)) {
|
|
2455
|
+
if (session.turn) session.turn.sawStreamText = true;
|
|
2456
|
+
}
|
|
2457
|
+
await processStreamEvent(session, message);
|
|
2458
|
+
} else if (message.type === "assistant") processAssistant(session, message);
|
|
2459
|
+
else if (message.type === "user") processUser(session, message);
|
|
2460
|
+
else if (message.type === "result") await processResult(session, message);
|
|
2461
|
+
else if (message.type === "system" && processSubagentSystemMessage(session, message)) {
|
|
2462
|
+
// Structured SDK task lifecycle handled above.
|
|
2463
|
+
}
|
|
2464
|
+
else if (message.type === "system" && message.subtype === "compact_boundary") {
|
|
2465
|
+
noteCompactBoundary(session, message.compact_metadata);
|
|
2466
|
+
notify("thread/compacted", { threadId: session.id });
|
|
2467
|
+
} else if (message.type === "system" && message.subtype === "permission_denied") {
|
|
2468
|
+
rememberPermissionDenial(session, {
|
|
2469
|
+
tool: message.tool_name,
|
|
2470
|
+
toolUseId: message.tool_use_id,
|
|
2471
|
+
reason: message.decision_reason || message.decision_reason_type,
|
|
2472
|
+
});
|
|
2473
|
+
} else if (message.type === "rate_limit_event") {
|
|
2474
|
+
notify("claude/account/updated", { threadId: session.id, rateLimitInfo: message.rate_limit_info });
|
|
2475
|
+
} else if (message.type === "system" && message.subtype === "api_retry") {
|
|
2476
|
+
notify("warning", { threadId: session.id, provider: "Claude", message: `Claude API 재시도 ${message.attempt}/${message.max_retries}` });
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2318
2480
|
async function consume(session) {
|
|
2319
2481
|
for await (const message of session.query) {
|
|
2320
|
-
|
|
2321
|
-
// A steered prompt answered after its turn already ended still deserves a
|
|
2322
|
-
// turn of its own, or the host would drop every event that follows.
|
|
2323
|
-
if (!session.turn
|
|
2324
|
-
&& session.steerPending > 0
|
|
2325
|
-
&& !message.parent_tool_use_id
|
|
2326
|
-
&& (message.type === "stream_event" || message.type === "assistant")) {
|
|
2327
|
-
session.steerPending -= 1;
|
|
2328
|
-
beginTurn(session);
|
|
2329
|
-
}
|
|
2330
|
-
if (message.type === "stream_event") {
|
|
2331
|
-
if (message.event?.type === "content_block_delta" && (message.event?.delta?.text || message.event?.delta?.thinking)) {
|
|
2332
|
-
if (session.turn) session.turn.sawStreamText = true;
|
|
2333
|
-
}
|
|
2334
|
-
await processStreamEvent(session, message);
|
|
2335
|
-
} else if (message.type === "assistant") processAssistant(session, message);
|
|
2336
|
-
else if (message.type === "user") processUser(session, message);
|
|
2337
|
-
else if (message.type === "result") await processResult(session, message);
|
|
2338
|
-
else if (message.type === "system" && processSubagentSystemMessage(session, message)) {
|
|
2339
|
-
// Structured SDK task lifecycle handled above.
|
|
2340
|
-
}
|
|
2341
|
-
else if (message.type === "system" && message.subtype === "compact_boundary") {
|
|
2342
|
-
noteCompactBoundary(session, message.compact_metadata);
|
|
2343
|
-
notify("thread/compacted", { threadId: session.id });
|
|
2344
|
-
} else if (message.type === "system" && message.subtype === "permission_denied") {
|
|
2345
|
-
rememberPermissionDenial(session, {
|
|
2346
|
-
tool: message.tool_name,
|
|
2347
|
-
toolUseId: message.tool_use_id,
|
|
2348
|
-
reason: message.decision_reason || message.decision_reason_type,
|
|
2349
|
-
});
|
|
2350
|
-
} else if (message.type === "rate_limit_event") {
|
|
2351
|
-
notify("claude/account/updated", { threadId: session.id, rateLimitInfo: message.rate_limit_info });
|
|
2352
|
-
} else if (message.type === "system" && message.subtype === "api_retry") {
|
|
2353
|
-
notify("warning", { threadId: session.id, provider: "Claude", message: `Claude API 재시도 ${message.attempt}/${message.max_retries}` });
|
|
2354
|
-
}
|
|
2482
|
+
await consumeMessage(session, message);
|
|
2355
2483
|
}
|
|
2356
2484
|
}
|
|
2357
2485
|
|
|
@@ -2383,8 +2511,10 @@ function stripHandoff(text) {
|
|
|
2383
2511
|
|
|
2384
2512
|
async function inputContent(input, handoffContext) {
|
|
2385
2513
|
const content = [];
|
|
2514
|
+
const explicitSkills = [];
|
|
2386
2515
|
for (const item of Array.isArray(input) ? input : []) {
|
|
2387
2516
|
if (item.type === "text") content.push({ type: "text", text: item.text || "" });
|
|
2517
|
+
else if (item.type === "skill" && item.name) explicitSkills.push(String(item.name));
|
|
2388
2518
|
else if (item.type === "localImage" && item.path) {
|
|
2389
2519
|
const bytes = await readFile(item.path);
|
|
2390
2520
|
const extension = item.path.split(".").pop()?.toLowerCase();
|
|
@@ -2395,6 +2525,17 @@ async function inputContent(input, handoffContext) {
|
|
|
2395
2525
|
content.push({ type: "image", source: { type: "base64", media_type: mediaType, data: bytes.toString("base64") } });
|
|
2396
2526
|
}
|
|
2397
2527
|
}
|
|
2528
|
+
if (explicitSkills.length) {
|
|
2529
|
+
content.unshift({
|
|
2530
|
+
type: "text",
|
|
2531
|
+
text: [
|
|
2532
|
+
"<devez-vibe-explicit-skills>",
|
|
2533
|
+
"The user explicitly selected these Claude Code skills. Invoke each skill before handling the request:",
|
|
2534
|
+
...explicitSkills.map((name) => `- /${name}`),
|
|
2535
|
+
"</devez-vibe-explicit-skills>",
|
|
2536
|
+
].join("\n"),
|
|
2537
|
+
});
|
|
2538
|
+
}
|
|
2398
2539
|
return prependHandoff(content.length ? content : [{ type: "text", text: "" }], handoffContext);
|
|
2399
2540
|
}
|
|
2400
2541
|
|
|
@@ -3486,6 +3627,177 @@ async function runSelfTest() {
|
|
|
3486
3627
|
return true;
|
|
3487
3628
|
};
|
|
3488
3629
|
try {
|
|
3630
|
+
const automaticTurnSession = {
|
|
3631
|
+
id: "automatic-turn-self-test",
|
|
3632
|
+
turn: null,
|
|
3633
|
+
turnSequence: 1,
|
|
3634
|
+
itemSequence: 1,
|
|
3635
|
+
steerPending: 0,
|
|
3636
|
+
automaticTurnsPending: 0,
|
|
3637
|
+
streamBlocks: new Map(),
|
|
3638
|
+
tools: new Map(),
|
|
3639
|
+
tasks: new Map(),
|
|
3640
|
+
planCreatePending: false,
|
|
3641
|
+
subagents: new Map([
|
|
3642
|
+
["toolu_automatic", {
|
|
3643
|
+
id: "toolu_automatic",
|
|
3644
|
+
toolUseId: "toolu_automatic",
|
|
3645
|
+
taskId: "automatic-agent",
|
|
3646
|
+
background: true,
|
|
3647
|
+
name: "Explore",
|
|
3648
|
+
description: "Inspect automatic response",
|
|
3649
|
+
tool: "",
|
|
3650
|
+
startedAt: Date.now(),
|
|
3651
|
+
lastSeenAt: Date.now(),
|
|
3652
|
+
}],
|
|
3653
|
+
["toolu_automatic_2", {
|
|
3654
|
+
id: "toolu_automatic_2",
|
|
3655
|
+
toolUseId: "toolu_automatic_2",
|
|
3656
|
+
taskId: "automatic-agent-2",
|
|
3657
|
+
background: true,
|
|
3658
|
+
name: "Explore",
|
|
3659
|
+
description: "Inspect a second automatic response",
|
|
3660
|
+
tool: "",
|
|
3661
|
+
startedAt: Date.now(),
|
|
3662
|
+
lastSeenAt: Date.now(),
|
|
3663
|
+
}],
|
|
3664
|
+
]),
|
|
3665
|
+
knownSubagents: new Map(),
|
|
3666
|
+
hiddenSubagentTasks: new Set(),
|
|
3667
|
+
ambientSubagentTasks: new Set(),
|
|
3668
|
+
subagentPulse: null,
|
|
3669
|
+
models: [],
|
|
3670
|
+
model: "claude:default",
|
|
3671
|
+
pendingPrompts: [],
|
|
3672
|
+
permissionDenials: [],
|
|
3673
|
+
lastContextUsage: null,
|
|
3674
|
+
lastContextWindow: 0,
|
|
3675
|
+
query: {
|
|
3676
|
+
async accountInfo() { return null; },
|
|
3677
|
+
async usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET() { return null; },
|
|
3678
|
+
},
|
|
3679
|
+
};
|
|
3680
|
+
await consumeMessage(automaticTurnSession, {
|
|
3681
|
+
type: "assistant",
|
|
3682
|
+
parent_tool_use_id: null,
|
|
3683
|
+
message: { content: [{ type: "text", text: "stale replay" }] },
|
|
3684
|
+
});
|
|
3685
|
+
if (automaticTurnSession.turn !== null) {
|
|
3686
|
+
throw new Error("Claude stale top-level response incorrectly opened a host turn");
|
|
3687
|
+
}
|
|
3688
|
+
await consumeMessage(automaticTurnSession, {
|
|
3689
|
+
type: "user",
|
|
3690
|
+
isReplay: true,
|
|
3691
|
+
origin: { kind: "task-notification" },
|
|
3692
|
+
parent_tool_use_id: null,
|
|
3693
|
+
message: { content: `<task-notification>
|
|
3694
|
+
<task-id>automatic-agent</task-id>
|
|
3695
|
+
<tool-use-id>toolu_automatic</tool-use-id>
|
|
3696
|
+
<status>completed</status><summary>Historical completion</summary>
|
|
3697
|
+
</task-notification>` },
|
|
3698
|
+
});
|
|
3699
|
+
if (automaticTurnSession.turn !== null
|
|
3700
|
+
|| automaticTurnSession.automaticTurnsPending !== 0
|
|
3701
|
+
|| automaticTurnSession.subagents.size !== 2) {
|
|
3702
|
+
throw new Error("Claude replayed task notification changed live turn state");
|
|
3703
|
+
}
|
|
3704
|
+
|
|
3705
|
+
await consumeMessage(automaticTurnSession, {
|
|
3706
|
+
type: "system",
|
|
3707
|
+
subtype: "task_notification",
|
|
3708
|
+
task_id: "automatic-agent",
|
|
3709
|
+
tool_use_id: "toolu_automatic",
|
|
3710
|
+
status: "completed",
|
|
3711
|
+
summary: "Agent finished",
|
|
3712
|
+
});
|
|
3713
|
+
await consumeMessage(automaticTurnSession, {
|
|
3714
|
+
type: "system",
|
|
3715
|
+
subtype: "task_notification",
|
|
3716
|
+
task_id: "automatic-agent-2",
|
|
3717
|
+
tool_use_id: "toolu_automatic_2",
|
|
3718
|
+
status: "completed",
|
|
3719
|
+
summary: "Second agent finished",
|
|
3720
|
+
});
|
|
3721
|
+
if (automaticTurnSession.subagents.size !== 0
|
|
3722
|
+
|| automaticTurnSession.automaticTurnsPending !== 2
|
|
3723
|
+
|| automaticTurnSession.turn !== null) {
|
|
3724
|
+
throw new Error("Claude task notifications did not book both automatic turns");
|
|
3725
|
+
}
|
|
3726
|
+
await consumeMessage(automaticTurnSession, {
|
|
3727
|
+
type: "assistant",
|
|
3728
|
+
parent_tool_use_id: "toolu_child",
|
|
3729
|
+
message: { content: [{ type: "text", text: "child" }] },
|
|
3730
|
+
});
|
|
3731
|
+
if (automaticTurnSession.turn !== null
|
|
3732
|
+
|| automaticTurnSession.automaticTurnsPending !== 2) {
|
|
3733
|
+
throw new Error("Claude child response consumed the pending main-agent turn");
|
|
3734
|
+
}
|
|
3735
|
+
|
|
3736
|
+
await consumeMessage(automaticTurnSession, {
|
|
3737
|
+
type: "stream_event",
|
|
3738
|
+
parent_tool_use_id: null,
|
|
3739
|
+
event: { type: "message_start" },
|
|
3740
|
+
});
|
|
3741
|
+
await consumeMessage(automaticTurnSession, {
|
|
3742
|
+
type: "stream_event",
|
|
3743
|
+
parent_tool_use_id: null,
|
|
3744
|
+
event: {
|
|
3745
|
+
type: "content_block_start",
|
|
3746
|
+
index: 0,
|
|
3747
|
+
content_block: { type: "text" },
|
|
3748
|
+
},
|
|
3749
|
+
});
|
|
3750
|
+
await consumeMessage(automaticTurnSession, {
|
|
3751
|
+
type: "stream_event",
|
|
3752
|
+
parent_tool_use_id: null,
|
|
3753
|
+
event: {
|
|
3754
|
+
type: "content_block_delta",
|
|
3755
|
+
index: 0,
|
|
3756
|
+
delta: { text: "automatic summary one" },
|
|
3757
|
+
},
|
|
3758
|
+
});
|
|
3759
|
+
await consumeMessage(automaticTurnSession, {
|
|
3760
|
+
type: "stream_event",
|
|
3761
|
+
parent_tool_use_id: null,
|
|
3762
|
+
event: { type: "content_block_stop", index: 0 },
|
|
3763
|
+
});
|
|
3764
|
+
await consumeMessage(automaticTurnSession, {
|
|
3765
|
+
type: "assistant",
|
|
3766
|
+
parent_tool_use_id: null,
|
|
3767
|
+
message: { content: [{ type: "text", text: "automatic summary one" }] },
|
|
3768
|
+
});
|
|
3769
|
+
if (automaticTurnSession.turn === null
|
|
3770
|
+
|| automaticTurnSession.automaticTurnsPending !== 1) {
|
|
3771
|
+
throw new Error("Claude streamed automatic response did not open one host turn");
|
|
3772
|
+
}
|
|
3773
|
+
await consumeMessage(automaticTurnSession, {
|
|
3774
|
+
type: "result",
|
|
3775
|
+
is_error: false,
|
|
3776
|
+
modelUsage: {},
|
|
3777
|
+
permission_denials: [],
|
|
3778
|
+
duration_ms: 1,
|
|
3779
|
+
});
|
|
3780
|
+
if (automaticTurnSession.turn !== null) {
|
|
3781
|
+
throw new Error("Claude automatic response did not complete its host turn");
|
|
3782
|
+
}
|
|
3783
|
+
|
|
3784
|
+
await consumeMessage(automaticTurnSession, {
|
|
3785
|
+
type: "assistant",
|
|
3786
|
+
parent_tool_use_id: null,
|
|
3787
|
+
message: { content: [{ type: "text", text: "automatic summary two" }] },
|
|
3788
|
+
});
|
|
3789
|
+
await consumeMessage(automaticTurnSession, {
|
|
3790
|
+
type: "result",
|
|
3791
|
+
is_error: false,
|
|
3792
|
+
modelUsage: {},
|
|
3793
|
+
permission_denials: [],
|
|
3794
|
+
duration_ms: 1,
|
|
3795
|
+
});
|
|
3796
|
+
if (automaticTurnSession.turn !== null
|
|
3797
|
+
|| automaticTurnSession.automaticTurnsPending !== 0) {
|
|
3798
|
+
throw new Error("Claude consecutive automatic responses left a pending turn");
|
|
3799
|
+
}
|
|
3800
|
+
|
|
3489
3801
|
processUser(lifecycleSession, {
|
|
3490
3802
|
message: { content: [{ type: "tool_result", tool_use_id: "toolu_1", content: "launched" }] },
|
|
3491
3803
|
tool_use_result: { isAsync: true, status: "async_launched", agentId: "agent-1" },
|
|
@@ -3786,6 +4098,12 @@ async function runSelfTest() {
|
|
|
3786
4098
|
const lifecycleMethods = lifecycleEvents.map((event) => event.method);
|
|
3787
4099
|
if (!lifecycleMethods.includes("turn/subagents/updated")
|
|
3788
4100
|
|| lifecycleMethods.filter((method) => method === "turn/started").length < 3
|
|
4101
|
+
|| !lifecycleEvents.some((event) => event.method === "item/completed"
|
|
4102
|
+
&& event.params?.item?.type === "agentMessage"
|
|
4103
|
+
&& event.params.item.text === "automatic summary one")
|
|
4104
|
+
|| !lifecycleEvents.some((event) => event.method === "item/completed"
|
|
4105
|
+
&& event.params?.item?.type === "agentMessage"
|
|
4106
|
+
&& event.params.item.text === "automatic summary two")
|
|
3789
4107
|
|| !lifecycleEvents.some((event) => event.method === "turn/subagent/line"
|
|
3790
4108
|
&& event.params?.line?.kind === "error")) {
|
|
3791
4109
|
throw new Error(`Claude subagent lifecycle events self-test failed: ${lifecycleMethods}`);
|
|
@@ -3901,6 +4219,21 @@ async function runSelfTest() {
|
|
|
3901
4219
|
if (keptStarted !== 0 || keptCompleted?.params?.item?.text !== "타일 보기 로직을 고쳤습니다.") {
|
|
3902
4220
|
throw new Error(`Claude held Korean text self-test failed: ${JSON.stringify(keptEvents)}`);
|
|
3903
4221
|
}
|
|
4222
|
+
const explicitSkillContent = await inputContent([
|
|
4223
|
+
{ type: "text", text: "$debug investigate" },
|
|
4224
|
+
{ type: "skill", name: "debug", path: "claude-command://debug" },
|
|
4225
|
+
]);
|
|
4226
|
+
if (!explicitSkillContent[0]?.text?.includes("/debug")) {
|
|
4227
|
+
throw new Error(`Claude explicit skill self-test failed: ${JSON.stringify(explicitSkillContent)}`);
|
|
4228
|
+
}
|
|
4229
|
+
const filteredSkills = filterClaudeSkillCommands([
|
|
4230
|
+
{ name: "compact", description: "Compact context" },
|
|
4231
|
+
{ name: "debug", description: "Debug a problem" },
|
|
4232
|
+
{ name: "personal", description: "Personal skill (user)" },
|
|
4233
|
+
]).map((skill) => skill.name);
|
|
4234
|
+
if (filteredSkills.join(",") !== "debug,personal") {
|
|
4235
|
+
throw new Error(`Claude skill filtering self-test failed: ${filteredSkills.join(",")}`);
|
|
4236
|
+
}
|
|
3904
4237
|
process.stdout.write("Claude bridge self-test passed\n");
|
|
3905
4238
|
}
|
|
3906
4239
|
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import {
|
|
2
|
+
copyFileSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readdirSync,
|
|
6
|
+
rmSync,
|
|
7
|
+
} from "node:fs";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
|
|
12
|
+
const packageRoot = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const sourceRoot = join(packageRoot, "skills", "luna-loop");
|
|
14
|
+
const userHome = homedir();
|
|
15
|
+
const codexHome = process.env.CODEX_HOME?.trim() || join(userHome, ".codex");
|
|
16
|
+
const claudeHome = process.env.CLAUDE_CONFIG_DIR?.trim() || join(userHome, ".claude");
|
|
17
|
+
|
|
18
|
+
const targets = [
|
|
19
|
+
{ name: "Codex", path: join(codexHome, "skills", "luna-loop") },
|
|
20
|
+
{ name: "Claude", path: join(claudeHome, "skills", "luna-loop") },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
function copyTree(source, destination) {
|
|
24
|
+
mkdirSync(destination, { recursive: true });
|
|
25
|
+
for (const entry of readdirSync(source, { withFileTypes: true })) {
|
|
26
|
+
const sourcePath = join(source, entry.name);
|
|
27
|
+
const destinationPath = join(destination, entry.name);
|
|
28
|
+
if (entry.isDirectory()) {
|
|
29
|
+
copyTree(sourcePath, destinationPath);
|
|
30
|
+
} else if (entry.isFile()) {
|
|
31
|
+
copyFileSync(sourcePath, destinationPath);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 덮어쓰기만 하면 원본에서 없앤 참고 문서가 설치본에 남아 계속 읽힌다.
|
|
37
|
+
// 복사한 뒤 원본에 없는 항목만 지운다. 복사가 먼저이므로 실패해도 설치본이 비지 않는다.
|
|
38
|
+
function pruneTree(source, destination) {
|
|
39
|
+
for (const entry of readdirSync(destination, { withFileTypes: true })) {
|
|
40
|
+
const sourcePath = join(source, entry.name);
|
|
41
|
+
const destinationPath = join(destination, entry.name);
|
|
42
|
+
if (!existsSync(sourcePath)) {
|
|
43
|
+
rmSync(destinationPath, { recursive: true, force: true });
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (entry.isDirectory()) pruneTree(sourcePath, destinationPath);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!existsSync(join(sourceRoot, "SKILL.md"))) {
|
|
51
|
+
console.error(`스킬 원본(luna-loop)을 찾지 못했습니다: ${sourceRoot}`);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let failed = false;
|
|
56
|
+
for (const target of targets) {
|
|
57
|
+
try {
|
|
58
|
+
copyTree(sourceRoot, target.path);
|
|
59
|
+
pruneTree(sourceRoot, target.path);
|
|
60
|
+
console.log(`스킬 설치 완료 (${target.name}): ${target.path}`);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
failed = true;
|
|
63
|
+
console.error(`스킬 설치 실패 (${target.name}): ${error instanceof Error ? error.message : error}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (failed) process.exitCode = 1;
|
package/package.json
CHANGED
|
@@ -1,41 +1,46 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "devez-vibe",
|
|
3
|
-
"version": "1.6.
|
|
4
|
-
"description": "Stable terminal UI for Codex and Claude Agent SDK",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"codex",
|
|
7
|
-
"cli",
|
|
8
|
-
"tui",
|
|
9
|
-
"terminal",
|
|
10
|
-
"app-server",
|
|
11
|
-
"claude-agent-sdk"
|
|
12
|
-
],
|
|
13
|
-
"homepage": "https://github.com/MrHoje/Devez-vibe#readme",
|
|
14
|
-
"bugs": "https://github.com/MrHoje/Devez-vibe/issues",
|
|
15
|
-
"repository": {
|
|
16
|
-
"type": "git",
|
|
17
|
-
"url": "git+https://github.com/MrHoje/Devez-vibe.git"
|
|
18
|
-
},
|
|
19
|
-
"license": "MIT",
|
|
20
|
-
"bin": {
|
|
21
|
-
"dvz": "bin/dvz.exe"
|
|
22
|
-
},
|
|
23
|
-
"files": [
|
|
24
|
-
"bin/dvz.exe",
|
|
25
|
-
"bridge/claude-agent-sdk-bridge.mjs",
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "devez-vibe",
|
|
3
|
+
"version": "1.6.46",
|
|
4
|
+
"description": "Stable terminal UI for Codex and Claude Agent SDK",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"codex",
|
|
7
|
+
"cli",
|
|
8
|
+
"tui",
|
|
9
|
+
"terminal",
|
|
10
|
+
"app-server",
|
|
11
|
+
"claude-agent-sdk"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/MrHoje/Devez-vibe#readme",
|
|
14
|
+
"bugs": "https://github.com/MrHoje/Devez-vibe/issues",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/MrHoje/Devez-vibe.git"
|
|
18
|
+
},
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"bin": {
|
|
21
|
+
"dvz": "bin/dvz.exe"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"bin/dvz.exe",
|
|
25
|
+
"bridge/claude-agent-sdk-bridge.mjs",
|
|
26
|
+
"install-skills.mjs",
|
|
27
|
+
"skills/luna-loop/",
|
|
28
|
+
"README.md",
|
|
29
|
+
"LICENSE"
|
|
30
|
+
],
|
|
31
|
+
"os": [
|
|
32
|
+
"win32"
|
|
33
|
+
],
|
|
34
|
+
"cpu": [
|
|
35
|
+
"x64"
|
|
36
|
+
],
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"postinstall": "node install-skills.mjs"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@anthropic-ai/claude-agent-sdk": "0.3.247"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: luna-loop
|
|
3
|
+
description: 호출 시 Luna 검증자로 수정의 검증 및 재수정 루프를 돌린다.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 수정-검증 루프
|
|
7
|
+
|
|
8
|
+
코드·설정·문서를 실제로 바꾸는 작업에서 완료 기준을 `수정함`으로 끝내지 않는다. 요청의 수용 조건을 정리하고, 변경 후 독립 검증을 거쳐 유효한 문제가 발견되면 원래 작업 맥락에서 재수정한 뒤 검증을 반복한다.
|
|
9
|
+
|
|
10
|
+
이 스킬은 사용자가 `$luna-loop`로 직접 호출한 경우에만 사용한다. 직접 호출된 경우에는 새 수정 작업뿐 아니라 현재 변경의 검증만 요청된 경우도 수행하며, 호출되지 않은 단순 설명·조사에는 적용하지 않는다.
|
|
11
|
+
|
|
12
|
+
## 적용 경계
|
|
13
|
+
|
|
14
|
+
- 구현·수정·버그 해결·리팩터링·UI 변경 또는 현재 변경 검증 요청에 적용한다.
|
|
15
|
+
- 검증만 요청된 경우에는 기준선 확인과 직접 검증·Luna 검증을 수행하고, 확정 문제가 있을 때만 수정을 진행한다.
|
|
16
|
+
- 커밋·푸시·배포·외부 시스템 변경은 사용자가 별도로 요청한 경우에만 수행한다.
|
|
17
|
+
- 기존 미커밋 변경은 기준선으로 보존하고, 이번 작업과 무관한 파일을 되돌리거나 정리하지 않는다.
|
|
18
|
+
|
|
19
|
+
## 핵심 운영 원칙
|
|
20
|
+
|
|
21
|
+
1. **주 편집자는 하나로 유지한다.** 검증 에이전트는 기본적으로 읽기 전용이며 파일을 수정하지 않는다. 여러 에이전트의 의견을 주 편집자가 원본 코드와 수용 조건으로 재확인한 뒤에만 수정한다.
|
|
22
|
+
2. **검증 대상과 기준선을 분리한다.** 시작 전에 저장소 지침, 현재 변경, 실행 중인 프로세스, 기존 실패를 확인하고 이번 작업에서 생긴 변경과 이미 존재하던 상태를 구분한다.
|
|
23
|
+
3. **추측을 결함으로 올리지 않는다.** 지적에는 기대 조건, 실제 코드 경로, 재현 방법 또는 사용자 영향이 모두 있어야 한다. 근거가 부족하면 `미확인`으로 남기고 무리하게 수정하지 않는다.
|
|
24
|
+
4. **검증 결과를 꾸며내지 않는다.** Luna를 호출하지 못했거나 테스트·빌드가 실행되지 않았으면 성공으로 표시하지 말고 그 한계를 보고한다.
|
|
25
|
+
|
|
26
|
+
## 실행 절차
|
|
27
|
+
|
|
28
|
+
### 1. 기준선과 수용 조건 확정
|
|
29
|
+
|
|
30
|
+
- 저장소의 `AGENTS.md`, `CLAUDE.md`, 기여 문서와 관련 지침을 먼저 읽는다.
|
|
31
|
+
- 현재 작업 디렉터리, 변경 파일, 실행 중인 프로세스, 기존 테스트·빌드 실패를 확인한다.
|
|
32
|
+
- 사용자의 요구를 관찰 가능한 수용 조건으로 바꾼다. 표시·상태·입력·저장·오류·성능 중 영향을 받는 축을 빠뜨리지 않는다.
|
|
33
|
+
- 변경 파일과 직접 영향을 받는 호출자·피호출자·테스트를 짧은 목록으로 고정한다.
|
|
34
|
+
- 수용 조건마다 가장 값싼 직접 검증 명령이나 실행 경로를 정한다.
|
|
35
|
+
|
|
36
|
+
### 1-1. Luna 실행 가능성 게이트
|
|
37
|
+
|
|
38
|
+
- 파일을 수정하기 전에 현재 호스트가 실제 `gpt-5.6-luna`를 위임 실행할 수 있는지 확인한다.
|
|
39
|
+
- 호출 수단은 제공자별로 다르지만 모델 확인 규칙은 하나다. 위임 도구의 반환값도, 검증자에게 모델을 물어 받은 답도 근거로 쓰지 않는다. 검증자는 자기 모델을 틀리게 답한다.
|
|
40
|
+
- Codex에서는 `collaboration.spawn_agent`에 `model`을 `gpt-5.6-luna`, `reasoning_effort`를 `high`로 넘긴다. 두 값을 덮어쓰려면 `fork_turns`를 `"none"` 또는 양의 정수 문자열로 함께 준다.
|
|
41
|
+
- Claude에서는 Codex CLI를 자식 프로세스로 실행해 위임한다. 파일을 수정하기 전에 아래 한 줄로 게이트를 확인한다.
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
echo "OK만 출력하라." | codex exec -m gpt-5.6-luna \
|
|
45
|
+
-c model_reasoning_effort="high" -s read-only --skip-git-repo-check --json - \
|
|
46
|
+
> <게이트 로그 파일> 2>&1
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
- 성공과 실패는 출력 JSONL의 최상위 이벤트 유형으로만 판정한다. `turn.completed`가 있고 `error`·`turn.failed`가 없어야 통과다. 실패해도 종료 코드는 0이 나오므로 종료 코드를 판정 근거로 쓰지 않는다.
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
grep -o '^{"type":"[a-z._]*"' <로그 파일> | sort -u
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
- 로그 전체를 문자열로 검색하지 않는다. 검증자가 이 문서를 읽으면 본문에 적힌 실패 신호가 로그에 그대로 실려 오탐이 된다. 줄 시작 기준으로만 판정한다.
|
|
56
|
+
- 실제 실행 모델은 로컬 세션 기록으로만 확정한다. 확인 절차는 [제공자 호환 안내](references/provider-compatibility.md)의 `실행 모델 확인`을 따른다.
|
|
57
|
+
- `--ephemeral`은 세션 기록을 남기지 않아 모델 확인을 막으므로 검증 실행에 쓰지 않는다.
|
|
58
|
+
- Codex CLI가 없거나 인증되지 않았거나 모델이 거부되면 Claude 계열 모델이나 직접 검증으로 대체하지 않는다.
|
|
59
|
+
- 게이트를 통과하지 못하면 새 수정은 시작하지 않고 `Luna 검증 불가` 사유와 필요한 연결 상태만 보고한다. 이미 존재하는 변경은 읽기 전용으로 확인할 수 있지만 완료로 판정하지 않는다.
|
|
60
|
+
|
|
61
|
+
### 2. 최소 범위로 수정
|
|
62
|
+
|
|
63
|
+
- 기준선과 수용 조건을 벗어나지 않는 가장 작은 일관된 변경을 적용한다.
|
|
64
|
+
- 검증을 쉽게 하려고 무관한 리팩터링, 형식 정리, 테스트 삭제를 끼워 넣지 않는다.
|
|
65
|
+
- 수정 직후 변경 목록과 수용 조건의 대응을 다시 대조한다.
|
|
66
|
+
|
|
67
|
+
### 3. 주 편집자의 직접 검증
|
|
68
|
+
|
|
69
|
+
- 가능한 경우 변경 지점에 가장 가까운 단위 테스트·정적 검사·빌드·실행 확인을 먼저 한다.
|
|
70
|
+
- 실패는 `이번 변경의 실패`, `기준선도 실패`, `환경상 실행 불가`로 나눠 기록한다.
|
|
71
|
+
- 직접 검증이 통과해도 독립 검증을 생략하지 않는다. 직접 검증은 실행 가능성, Luna 검증은 놓친 계약·회귀·경계 조건을 확인하는 역할이다.
|
|
72
|
+
|
|
73
|
+
### 4. Luna 독립 검증
|
|
74
|
+
|
|
75
|
+
- 실행 가능성 게이트를 통과한 뒤에만 검증 에이전트를 생성한다. 모든 검증자는 실제 `gpt-5.6-luna`로 실행돼야 하며, Luna를 사용할 수 없으면 대체 검증 없이 중단한다.
|
|
76
|
+
- 검증자에게는 요청 요약, 수용 조건, 변경 파일·관련 코드 범위, 기준선 정보, 이미 실행한 검사만 전달한다. 주 편집자의 의심 결론이나 원하는 판정을 전달하지 않는다.
|
|
77
|
+
- 각 검증자는 읽기 전용으로 동작하게 하고, 근거 없는 광범위 탐색이나 파일 수정을 금지한다.
|
|
78
|
+
- Claude에서는 검증자 하나가 Codex CLI 실행 한 번이다. 요청문을 파일로 쓰고 아래 형태로 실행한다.
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
codex exec -m gpt-5.6-luna -c model_reasoning_effort="high" \
|
|
82
|
+
-s read-only --skip-git-repo-check \
|
|
83
|
+
-C <저장소 경로> -o <결과 파일> --json - < <요청문 파일> > <로그 파일> 2>&1
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
- 검증자마다 결과 파일과 로그 파일을 서로 다른 경로로 지정한다. 이벤트 판정은 로그 파일로, 검토 내용 확인은 결과 파일로 한다.
|
|
87
|
+
- 로그 파일에서 `thread_id`를 뽑아 두고, 검증이 끝난 뒤 그 값으로 세션 기록을 찾아 실행 모델과 추론 강도를 확인한다.
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
grep -o '"thread_id":"[^"]*"' <로그 파일> | head -1
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
- 병렬 검증은 검증자마다 위 명령을 별도 프로세스로 동시에 띄워 얻는다. Claude 계열 서브에이전트로 검증자를 대신하지 않는다.
|
|
94
|
+
- 검증자 하나를 셸 호출 하나로 띄운다. 한 호출 안에서 여러 개를 묶어 띄우면 진행 표시가 하나로 합쳐져 검증자별 상태를 볼 수 없다.
|
|
95
|
+
- `-s read-only`가 검증자의 쓰기를 호스트 차원에서 막으므로 이 옵션을 빼지 않는다.
|
|
96
|
+
- 각 검증자의 종료 판정은 게이트와 같은 명령과 같은 신호 집합을 쓴다. 최상위 이벤트에 `turn.completed`가 있고 `turn.failed`·`error`가 없어야 완료이며, 그렇지 않으면 해당 범위를 미완료로 기록한다.
|
|
97
|
+
- 이벤트가 정상이어도 결과 파일이 없거나 비어 있거나 검토 내용이 담기지 않았으면 완료로 보지 않는다. 결과 파일을 읽어 실제 지적이나 문제 없음 판정이 있는지 확인한다.
|
|
98
|
+
- 검증자별로 실행 모델과 추론 강도를 확인한 뒤에만 그 결과를 종합 대상에 넣는다. `gpt-5.6-luna`와 `high`를 확인하지 못한 범위는 미완료로 다룬다.
|
|
99
|
+
- 수정과 직접 검증이 끝난 직후, 완료 직전의 최종 검증 패널로 최소 2개의 Luna 검증자를 서로 다른 관점에서 동시에 생성한다.
|
|
100
|
+
- 변경 범위가 크면 독립 범위를 3~4개로 나눈다. 한 번에 띄우는 수는 제공자의 가용 슬롯을 넘기지 않으며, 슬롯이 부족하면 범위를 유지한 채 나눠 실행한다. 제공자별 슬롯은 [제공자 호환 안내](references/provider-compatibility.md)를 따른다. 다음 중 하나라도 해당하면 큰 변경으로 본다.
|
|
101
|
+
- 변경 파일이 5개 이상이다.
|
|
102
|
+
- 서로 다른 하위 시스템이 2개 이상이다.
|
|
103
|
+
- UI와 상태·저장·통신 로직이 함께 바뀐다.
|
|
104
|
+
- 수용 조건이 서로 다른 동작 축 3개 이상으로 나뉜다.
|
|
105
|
+
- 병렬 검증의 범위는 겹치지 않게 나눈다. 적합한 축은 `정상 사용 흐름·요구사항`, `회귀·상태·오류 경로`, `테스트·빌드·호환성`, `UI·입력·접근성`이다. UI가 없으면 마지막 축을 만들지 않는다.
|
|
106
|
+
- 모든 검증자의 결과를 받은 뒤 한 번에 종합한다. 같은 문제를 반복 보고한 수는 확신도의 근거가 아니라 한 건의 근거 묶음으로 취급한다.
|
|
107
|
+
- 생성한 각 검증자의 종료 상태를 확인한다. `완료`가 아닌 `오류`·`중단`·`시간 초과`·`상태 미확인`은 해당 범위의 검증 미완료로 기록한다.
|
|
108
|
+
- 이벤트는 정상이지만 결과 파일이 없거나 비어 있거나 검토 내용이 없으면 `검증 결과 미확인`으로 기록하고 아래 재시도 규칙을 그대로 적용한다.
|
|
109
|
+
- `오류`·`중단`·`시간 초과`·`상태 미확인`·`Luna 모델 미확인`·`검증 결과 미확인` 범위는 같은 범위의 새 Luna 검증자로 한 번만 재시도한다. 재시도도 완료되지 않으면 추가 재시도·직접 검증·다른 모델 대체 없이 즉시 중단하고 실행 불가 사유와 영향 범위를 보고한다.
|
|
110
|
+
- 검증자가 일부만 끝났다면 끝난 결과는 잠정 결과로만 사용한다. 누락 범위가 있으면 같은 범위의 Luna 검증자를 새로 실행하며, 직접 검증이나 다른 모델로 대체하지 않는다.
|
|
111
|
+
- 병렬 결과가 하나라도 미완료인 채로 남으면 전체를 통과로 판정하지 않는다. 새 Luna 검증자도 실패하면 해당 범위의 실행 불가 사유와 영향 범위를 보고하고 중단한다.
|
|
112
|
+
|
|
113
|
+
검증자에게 전달할 기본 요청은 [검증자 계약](references/verifier-contract.md)을 따른다.
|
|
114
|
+
|
|
115
|
+
### 5. 문제 판정과 재수정
|
|
116
|
+
|
|
117
|
+
- 각 지적을 원본 코드, 수용 조건, 실행 결과로 다시 확인한다. 검증자의 말만으로 수정하지 않는다.
|
|
118
|
+
- 유효한 지적만 `확정 문제`로 채택하고, 의도 확인이 필요한 내용은 `확인 요청`, 근거가 약한 내용은 `미확인`으로 분리한다.
|
|
119
|
+
- 요청·기획·기존 계약을 확인해야만 결정할 수 있는 `확인 요청`은 결함으로 단정하지 않는다. 사용자의 답이 없으면 해당 항목을 완료 차단 사유로 남긴다.
|
|
120
|
+
- 확정 문제를 주 편집자가 수정한 뒤, 영향을 받은 직접 검증을 다시 실행한다.
|
|
121
|
+
- 확정 문제가 하나라도 있으면 수정과 직접 검증을 끝낸 뒤 최종 Luna 검증 패널 전체를 같은 관점으로 다시 병렬 실행한다. 고립된 수정이어도 일부 검증자만 생략하지 않는다.
|
|
122
|
+
- 재검증에서 새로운 확정 문제가 나오면 기본 최대 3회의 수정-검증 회차 안에서 같은 루프를 반복한다. 3회 뒤에도 확정 문제가 남으면 아래 종료 규칙에 따라 중단하며 완료하지 않는다.
|
|
123
|
+
- 반복마다 회차, 채택·기각한 지적과 근거, 새로 바뀐 범위, 실행한 검사를 기록한다. 이전에 통과한 검사를 결과 없이 재사용하지 않는다.
|
|
124
|
+
|
|
125
|
+
### 6. 종료 판정
|
|
126
|
+
|
|
127
|
+
다음 조건을 모두 만족할 때만 완료로 판정한다.
|
|
128
|
+
|
|
129
|
+
- 수용 조건을 위반하는 확정 문제가 남아 있지 않다.
|
|
130
|
+
- 사용자 판단이 필요한 `확인 요청`이 남아 있지 않다. 남아 있다면 완료가 아니라 명시적 중단·질문 상태로 보고한다.
|
|
131
|
+
- 필수 직접 검증이 통과했거나, 실행 불가 사유와 영향 범위가 명확히 기록됐다.
|
|
132
|
+
- 마지막 변경 범위에 대해 실제 실행 모델이 `gpt-5.6-luna`, 추론 강도가 `high`로 확인된 Luna 검증이 끝났다.
|
|
133
|
+
- 기존 실패·미확인 런타임·사용자 확인 필요 항목이 성공으로 숨겨지지 않았다.
|
|
134
|
+
|
|
135
|
+
기본 수정-검증 회차는 3회까지다. 3회 뒤에도 확정 문제가 남거나 같은 검증이 반복해서 모순되면 무한 재시도하지 말고, 남은 문제·시도한 수정·차단된 검증·사용자에게 필요한 결정을 보고한다. 사용자가 더 반복하라고 명시한 경우에만 회차를 늘린다.
|
|
136
|
+
|
|
137
|
+
## 제공자 차이
|
|
138
|
+
|
|
139
|
+
공통 본문은 Codex와 Claude에서 읽을 수 있게 작성한다. 위임 도구와 모델 이름은 [제공자 호환 안내](references/provider-compatibility.md)를 먼저 확인하고, 현재 호스트에 실제로 노출된 기능만 사용한다.
|
|
140
|
+
|
|
141
|
+
## 결과 보고
|
|
142
|
+
|
|
143
|
+
최종 보고에는 최소한 `확인된 원인`, `사용자 영향`, `실제 조치`, `직접 검증`, `Luna 검증`, `남은 한계`를 포함한다. 파일 경로·기술 식별자는 원인과 검증 근거를 판단하는 데 필요한 범위만 쓴다.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# 제공자 호환 안내
|
|
2
|
+
|
|
3
|
+
## Codex
|
|
4
|
+
|
|
5
|
+
- 위임 도구의 함수 이름은 `spawn_agent`이고, 호출 수신자 표기는 `functions.collaboration.spawn_agent`다. 검증자별로 분리된 읽기 전용 작업을 이 도구로 생성한다.
|
|
6
|
+
- 실제 입력 필드는 `task_name`, `message`, `model`, `fork_turns`, `reasoning_effort`다. 다른 이름의 위임 도구나 필드가 기억에 떠올라도 쓰지 않는다. 호출 기록에서 확인된 이름만 쓴다.
|
|
7
|
+
- `model`과 `reasoning_effort`는 선택 필드다. 검증자는 `gpt-5.6-luna`와 `high`를 함께 넘긴다.
|
|
8
|
+
- 전체 이력 fork는 부모의 모델과 추론 강도를 상속하고 오버라이드를 받지 않는다. 두 값을 지정하려면 `fork_turns`를 `"none"` 또는 양의 정수 문자열로 함께 줘야 한다.
|
|
9
|
+
- 이 도구의 반환값에는 실행 모델이 없다. 반환값만으로 Luna 실행을 확정하지 않는다.
|
|
10
|
+
- 모델 지정이 거부되거나 위임 기능이 없거나 실제 모델 식별이 불가능하면 직접 검증·다른 모델·Claude 검증으로 폴백하지 않는다. 재시도 한 번까지 허용하고, 그래도 확인되지 않으면 `Luna 검증 불가` 또는 `Luna 모델 미확인`으로 기록하고 중단한다.
|
|
11
|
+
- 수정과 직접 검증이 끝나면 서로 다른 관점의 Luna 검증자 2개 이상을 병렬로 생성한다. 큰 변경은 3~4개로 늘리고, 확정 문제를 수정한 뒤에는 전체 검증자 묶음을 다시 병렬 실행한다.
|
|
12
|
+
- 동시 실행 슬롯은 자신을 포함해 4개다. 이 경로에서 한 번에 띄울 검증자는 3개까지이며, 범위가 4개면 3개를 먼저 띄우고 남은 하나를 이어서 실행한다.
|
|
13
|
+
- 오류·시간 초과·상태 미확인·모델 미확인은 같은 범위의 Luna 검증자로 한 번만 재시도한다. 재시도도 실패하면 대체 없이 중단한다.
|
|
14
|
+
|
|
15
|
+
## Claude
|
|
16
|
+
|
|
17
|
+
- Claude provider에는 Luna를 직접 실행할 모델 선택지가 없다. Claude 계열 서브에이전트로 검증자를 만들지 않고, Codex CLI를 자식 프로세스로 실행해 교차 제공자로 위임한다.
|
|
18
|
+
- 위임 수단은 셸에서 실행하는 `codex exec`다. 검증자 하나가 명령 한 번이고, 병렬 검증은 같은 명령을 검증자 수만큼 동시에 띄워 얻는다.
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
codex exec -m gpt-5.6-luna -c model_reasoning_effort="high" \
|
|
22
|
+
-s read-only --skip-git-repo-check \
|
|
23
|
+
-C <저장소 경로> -o <결과 파일> --json - < <요청문 파일>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
- 파일을 수정하기 전에 같은 명령의 짧은 형태로 게이트를 확인한다.
|
|
27
|
+
- 종료 코드는 모델 호출이 400으로 실패해도 0이 나온다. 성공 판정은 항상 최상위 이벤트 유형으로 한다.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
grep -o '^{"type":"[a-z._]*"' <로그 파일> | sort -u
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
- `turn.completed`가 있고 `error`·`turn.failed`가 없어야 통과다. 로그 전체를 문자열로 검색하면 검증자가 읽은 문서 본문이 실패 신호로 오탐되므로 줄 시작 기준으로만 본다.
|
|
34
|
+
- `--ephemeral`은 세션 기록을 남기지 않아 실행 모델 확인을 막으므로 검증 실행에 쓰지 않는다.
|
|
35
|
+
- 출력의 `thread_id`를 기록해 두고 아래 `실행 모델 확인` 절차로 실제 모델을 확정한다.
|
|
36
|
+
- `-s read-only`가 검증자의 파일 수정을 호스트 차원에서 막는다. 이 옵션을 빼거나 승인 우회 옵션으로 바꾸지 않으며, 요청문에도 파일 수정 금지를 함께 명시한다.
|
|
37
|
+
- Codex CLI가 설치돼 있지 않거나 인증되지 않았거나 셸 실행이 허용되지 않으면 `Luna 검증 불가`로 기록하고 파일을 수정하지 않는다.
|
|
38
|
+
|
|
39
|
+
## 추론 강도
|
|
40
|
+
|
|
41
|
+
검증 깊이가 제공자에 따라 달라지지 않도록 검증자는 항상 `high`로 실행한다.
|
|
42
|
+
|
|
43
|
+
- 새 프로세스로 띄우면 추론 강도가 설정 기본값인 `low`로 잡히므로, 실행마다 `-c model_reasoning_effort="high"`를 함께 넘긴다.
|
|
44
|
+
- 위임 도구로 띄울 때는 `reasoning_effort`를 `"high"`로 넘기고, 오버라이드가 가능하도록 `fork_turns`를 함께 지정한다.
|
|
45
|
+
- 실제 적용값은 아래 `실행 모델 확인`의 같은 명령으로 확인한다. 추출된 값이 `high`가 아니면 해당 범위를 미완료로 기록한다.
|
|
46
|
+
- 셸로 띄운 검증자는 강도가 기록으로 확인된다. 위임 도구로 띄운 검증자의 기록에는 강도가 남지 않을 수 있으며, 확인하지 못하면 `미확인`으로 남기고 통과로 판정하지 않는다.
|
|
47
|
+
|
|
48
|
+
## 실행 모델 확인
|
|
49
|
+
|
|
50
|
+
호출 수단은 제공자별로 다르지만 확인 규칙은 하나다. 실제 실행 모델은 로컬 세션 기록에서만 확정한다.
|
|
51
|
+
|
|
52
|
+
- 검증자에게 모델 이름을 물어 받은 답은 근거가 아니다. 실제로 Luna로 띄운 검증자가 자신을 `GPT-5 (Codex)`로 답한다.
|
|
53
|
+
- 세션 기록은 `$CODEX_HOME/sessions/<연>/<월>/<일>/rollout-*.jsonl`에 남는다. 기본 위치는 사용자 홈의 `.codex`다.
|
|
54
|
+
- 근거가 되는 레코드는 `turn_context`와 `world_state`뿐이다. 파일 전체를 검색하면 첫 줄 `session_meta`의 부모 모델과 요청문에 들어간 모델 문자열까지 섞여 오판한다.
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
grep -o '"type":"turn_context".*\|"type":"world_state".*' <rollout 파일> \
|
|
58
|
+
| grep -o '"model":"[^"]*"\|"reasoning_effort":"[^"]*"' | sort -u
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
- 추출된 모델 값이 모두 `gpt-5.6-luna`여야 통과다. 다른 값이 하나라도 섞이면 미완료로 기록한다.
|
|
62
|
+
- Claude에서는 실행 출력의 `thread_id`가 그대로 파일 이름에 들어가므로 해당 파일을 바로 찾을 수 있고, 모델과 강도가 `turn_context`에 남는다.
|
|
63
|
+
- Codex에서는 서브에이전트가 별도 세션 파일로 기록된다. 부모의 `thread_id`로 자식 파일을 찾고, 모델은 `world_state`에서 확인한다.
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
grep -l '"parent_thread_id":"<부모 thread_id>"' <세션 디렉터리>/*.jsonl
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
- 기록에서 `gpt-5.6-luna`를 확인하지 못하면 해당 범위를 `Luna 모델 미확인`으로 남기고 통과로 판정하지 않는다.
|
|
70
|
+
|
|
71
|
+
## 공통 제한
|
|
72
|
+
|
|
73
|
+
- 제공자가 무엇이든 검증자는 파일을 수정하지 않는다. 수정은 주 편집자 하나가 수행한다.
|
|
74
|
+
- 서로 다른 제공자의 결과가 충돌하면 양쪽 결과를 사실로 합치지 말고 원본 코드와 실행 결과로 재판정한다.
|
|
75
|
+
- 모델을 호출하지 못한 상태와 접근 불가한 실행 환경은 Luna 검증 불가로 기록하고 완료하지 않는다. 기준선 실패는 Luna 실행 결과와 별도 한계로 기록한다.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# 검증자 계약
|
|
2
|
+
|
|
3
|
+
검증자는 변경을 고치거나 파일을 쓰지 않고, 독립적인 결함 탐색 결과만 반환한다. 변경의 옳고 그름을 미리 암시하는 문장이나 주 편집자의 의심 목록을 전달하지 않는다.
|
|
4
|
+
|
|
5
|
+
## 전달할 정보
|
|
6
|
+
|
|
7
|
+
- 사용자의 요청을 한 문장으로 요약한 내용
|
|
8
|
+
- 관찰 가능한 수용 조건
|
|
9
|
+
- 기준선에서 이미 존재하던 실패와 이번 변경 파일
|
|
10
|
+
- 검토할 코드·설정·문서 범위와 필요한 테스트·빌드·실행 명령
|
|
11
|
+
- `읽기 전용`, `파일 수정 금지`, `근거 없는 추측 금지`라는 제약
|
|
12
|
+
- 도구 이름과 입력 필드를 판단할 때 기억이나 관례에 의존하지 말고, 실행 기록이나 노출된 정의에서 확인한 이름만 쓰라는 제약
|
|
13
|
+
|
|
14
|
+
## 검증 요청문
|
|
15
|
+
|
|
16
|
+
```text
|
|
17
|
+
현재 변경을 읽기 전용으로 검토하라. 아래 수용 조건을 기준으로 요구사항 누락,
|
|
18
|
+
기존 동작 회귀, 상태·입력·오류 경로, 경계값, 테스트·빌드·호환성 문제를 찾아라.
|
|
19
|
+
검증자는 파일을 수정하지 말고, 문제가 없으면 없다고 명시하라.
|
|
20
|
+
|
|
21
|
+
각 지적은 다음을 모두 포함해야 한다.
|
|
22
|
+
- 심각도: 치명적 / 주요 / 경미 / 미확인
|
|
23
|
+
- 위치: 파일과 줄 또는 식별 가능한 코드·문서 범위
|
|
24
|
+
- 기대 조건: 어떤 수용 조건을 위반하는가
|
|
25
|
+
- 근거: 실제 코드 경로나 문서 구절과 관찰된 동작
|
|
26
|
+
- 재현: 실행 명령, 입력 또는 재현 불가 사유
|
|
27
|
+
- 영향: 사용자·데이터·호환성에 미치는 결과
|
|
28
|
+
- 제안: 수정 방향만 제시하고 직접 수정하지 말 것
|
|
29
|
+
|
|
30
|
+
수용 조건:
|
|
31
|
+
{acceptance_criteria}
|
|
32
|
+
|
|
33
|
+
변경 범위:
|
|
34
|
+
{changed_scope}
|
|
35
|
+
|
|
36
|
+
기준선 및 이미 실행한 검사:
|
|
37
|
+
{baseline_and_checks}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## 결과 종합 규칙
|
|
41
|
+
|
|
42
|
+
- 검증자가 찾은 내용은 데이터이며, 주 편집자가 원본과 수용 조건으로 채택 여부를 결정한다.
|
|
43
|
+
- 수용 조건·코드 경로·재현 또는 영향 중 하나라도 빠진 지적은 `미확인`으로 남긴다.
|
|
44
|
+
- 같은 지적을 여러 검증자가 보고해도 한 건으로 합치고, 서로 다른 근거만 병합한다.
|
|
45
|
+
- 검증자만 발견한 새 지적은 원본을 다시 추적해 실제 문제일 때만 채택한다.
|
|
46
|
+
- 주 편집자만 발견한 지적은 독립 검증 결과와 모순될 수 있으므로 직접 재현한 뒤 등급을 정한다.
|