claude-nexus 0.2.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/.claude-plugin/marketplace.json +23 -0
- package/.claude-plugin/plugin.json +17 -0
- package/.mcp.json +8 -0
- package/LICENSE +21 -0
- package/README.md +106 -0
- package/agents/analyst.md +43 -0
- package/agents/architect.md +43 -0
- package/agents/builder.md +36 -0
- package/agents/debugger.md +38 -0
- package/agents/finder.md +35 -0
- package/agents/guard.md +42 -0
- package/agents/reviewer.md +42 -0
- package/agents/strategist.md +37 -0
- package/agents/tester.md +43 -0
- package/agents/writer.md +42 -0
- package/bridge/mcp-server.cjs +22147 -0
- package/bridge/mcp-server.cjs.map +7 -0
- package/hooks/hooks.json +89 -0
- package/package.json +45 -0
- package/scripts/gate.cjs +294 -0
- package/scripts/gate.cjs.map +7 -0
- package/scripts/pulse.cjs +295 -0
- package/scripts/pulse.cjs.map +7 -0
- package/scripts/statusline.cjs +329 -0
- package/scripts/statusline.cjs.map +7 -0
- package/scripts/tracker.cjs +325 -0
- package/scripts/tracker.cjs.map +7 -0
- package/skills/consult/SKILL.md +165 -0
- package/skills/init/SKILL.md +196 -0
- package/skills/plan/SKILL.md +176 -0
- package/skills/setup/SKILL.md +275 -0
- package/skills/sync/SKILL.md +118 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// src/shared/hook-io.ts
|
|
4
|
+
function readStdin() {
|
|
5
|
+
return new Promise((resolve2) => {
|
|
6
|
+
let data = "";
|
|
7
|
+
process.stdin.on("data", (chunk) => data += chunk);
|
|
8
|
+
process.stdin.on("end", () => resolve2(data));
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
function respond(obj) {
|
|
12
|
+
process.stdout.write(JSON.stringify(obj));
|
|
13
|
+
}
|
|
14
|
+
function pass() {
|
|
15
|
+
respond({ continue: true });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// src/hooks/tracker.ts
|
|
19
|
+
var import_fs3 = require("fs");
|
|
20
|
+
|
|
21
|
+
// src/shared/paths.ts
|
|
22
|
+
var import_path = require("path");
|
|
23
|
+
var import_fs = require("fs");
|
|
24
|
+
function findProjectRoot() {
|
|
25
|
+
let dir = process.cwd();
|
|
26
|
+
while (dir !== "/") {
|
|
27
|
+
if ((0, import_fs.existsSync)((0, import_path.join)(dir, ".git"))) return dir;
|
|
28
|
+
dir = (0, import_path.resolve)(dir, "..");
|
|
29
|
+
}
|
|
30
|
+
return process.cwd();
|
|
31
|
+
}
|
|
32
|
+
var PROJECT_ROOT = findProjectRoot();
|
|
33
|
+
var RUNTIME_ROOT = (0, import_path.join)(PROJECT_ROOT, ".nexus");
|
|
34
|
+
var KNOWLEDGE_ROOT = (0, import_path.join)(PROJECT_ROOT, ".claude", "nexus");
|
|
35
|
+
function sessionDir(sessionId) {
|
|
36
|
+
return (0, import_path.join)(RUNTIME_ROOT, "state", "sessions", sessionId);
|
|
37
|
+
}
|
|
38
|
+
function ensureDir(dir) {
|
|
39
|
+
if (!(0, import_fs.existsSync)(dir)) {
|
|
40
|
+
(0, import_fs.mkdirSync)(dir, { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function updateWorkflowPhase(sid, phase) {
|
|
44
|
+
const workflowPath = (0, import_path.join)(sessionDir(sid), "workflow.json");
|
|
45
|
+
if (!(0, import_fs.existsSync)(workflowPath)) return;
|
|
46
|
+
try {
|
|
47
|
+
const state = JSON.parse((0, import_fs.readFileSync)(workflowPath, "utf-8"));
|
|
48
|
+
if ((state.mode === "consult" || state.mode === "plan") && state.phase !== phase) {
|
|
49
|
+
state.phase = phase;
|
|
50
|
+
(0, import_fs.writeFileSync)(workflowPath, JSON.stringify(state, null, 2));
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function getBasePhase(sid) {
|
|
56
|
+
const workflowPath = (0, import_path.join)(sessionDir(sid), "workflow.json");
|
|
57
|
+
if (!(0, import_fs.existsSync)(workflowPath)) return null;
|
|
58
|
+
try {
|
|
59
|
+
const state = JSON.parse((0, import_fs.readFileSync)(workflowPath, "utf-8"));
|
|
60
|
+
if (state.mode === "consult") return "exploring";
|
|
61
|
+
if (state.mode === "plan") return "analyzing";
|
|
62
|
+
} catch {
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/shared/session.ts
|
|
68
|
+
var import_crypto = require("crypto");
|
|
69
|
+
var import_fs2 = require("fs");
|
|
70
|
+
var import_path2 = require("path");
|
|
71
|
+
var SESSION_FILE = (0, import_path2.join)(RUNTIME_ROOT, "state", "current-session.json");
|
|
72
|
+
function getSessionId() {
|
|
73
|
+
if ((0, import_fs2.existsSync)(SESSION_FILE)) {
|
|
74
|
+
try {
|
|
75
|
+
const data = JSON.parse((0, import_fs2.readFileSync)(SESSION_FILE, "utf-8"));
|
|
76
|
+
if (data.sessionId && typeof data.sessionId === "string") {
|
|
77
|
+
return data.sessionId;
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return createSession();
|
|
83
|
+
}
|
|
84
|
+
function createSession() {
|
|
85
|
+
const sessionId = (0, import_crypto.randomUUID)().slice(0, 8);
|
|
86
|
+
ensureDir((0, import_path2.join)(RUNTIME_ROOT, "state"));
|
|
87
|
+
(0, import_fs2.writeFileSync)(SESSION_FILE, JSON.stringify({ sessionId, createdAt: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
88
|
+
return sessionId;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/hooks/tracker.ts
|
|
92
|
+
var import_path3 = require("path");
|
|
93
|
+
var import_child_process = require("child_process");
|
|
94
|
+
function normalizeAgentName(name) {
|
|
95
|
+
return name.replace(/^(nexus|claude-nexus):/, "");
|
|
96
|
+
}
|
|
97
|
+
function loadAgents(sid) {
|
|
98
|
+
const path = (0, import_path3.join)(sessionDir(sid), "agents.json");
|
|
99
|
+
if ((0, import_fs3.existsSync)(path)) {
|
|
100
|
+
try {
|
|
101
|
+
return JSON.parse((0, import_fs3.readFileSync)(path, "utf-8"));
|
|
102
|
+
} catch {
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return { active: [], history: [] };
|
|
106
|
+
}
|
|
107
|
+
function saveAgents(sid, record) {
|
|
108
|
+
const dir = sessionDir(sid);
|
|
109
|
+
ensureDir(dir);
|
|
110
|
+
(0, import_fs3.writeFileSync)((0, import_path3.join)(dir, "agents.json"), JSON.stringify(record, null, 2));
|
|
111
|
+
}
|
|
112
|
+
function analyzeCodebase(cwd) {
|
|
113
|
+
let fileCount = 0;
|
|
114
|
+
try {
|
|
115
|
+
const entries = (0, import_fs3.readdirSync)(cwd);
|
|
116
|
+
fileCount = entries.length;
|
|
117
|
+
} catch {
|
|
118
|
+
}
|
|
119
|
+
const has = (names) => names.some((n) => (0, import_fs3.existsSync)((0, import_path3.join)(cwd, n)));
|
|
120
|
+
const hasLinter = has([".eslintrc", ".eslintrc.js", ".eslintrc.json", ".eslintrc.yml", ".eslintrc.yaml", "eslint.config.js", "eslint.config.ts", "eslint.config.mjs", ".prettierrc", ".prettierrc.js", ".prettierrc.json", ".prettierrc.yml"]);
|
|
121
|
+
const hasTests = has(["test", "tests", "__tests__", "spec"]);
|
|
122
|
+
const hasCI = has([".github", ".circleci"]);
|
|
123
|
+
const hasSrc = has(["src"]);
|
|
124
|
+
let type;
|
|
125
|
+
let description;
|
|
126
|
+
if (fileCount < 20 && !hasLinter && !hasTests) {
|
|
127
|
+
type = "greenfield";
|
|
128
|
+
description = "Few files, no established patterns yet";
|
|
129
|
+
} else if (hasLinter && hasTests && hasCI) {
|
|
130
|
+
type = "disciplined";
|
|
131
|
+
description = "Has linter, tests, and CI \u2014 follow existing conventions strictly";
|
|
132
|
+
} else if (hasSrc) {
|
|
133
|
+
type = "transitional";
|
|
134
|
+
description = "Has src/ but missing some tooling \u2014 introduce patterns incrementally";
|
|
135
|
+
} else {
|
|
136
|
+
type = "legacy";
|
|
137
|
+
description = "Large codebase without modern tooling \u2014 be conservative with changes";
|
|
138
|
+
}
|
|
139
|
+
return { type, description, hasLinter, hasTests, hasCI, hasSrc, fileCount };
|
|
140
|
+
}
|
|
141
|
+
function handleSessionStart() {
|
|
142
|
+
cleanupAllSessionStates();
|
|
143
|
+
const sid = createSession();
|
|
144
|
+
const dir = sessionDir(sid);
|
|
145
|
+
ensureDir(dir);
|
|
146
|
+
let branch = "unknown";
|
|
147
|
+
let cwd = process.cwd();
|
|
148
|
+
try {
|
|
149
|
+
branch = (0, import_child_process.execSync)("git rev-parse --abbrev-ref HEAD", { encoding: "utf-8" }).trim();
|
|
150
|
+
cwd = (0, import_child_process.execSync)("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
|
|
151
|
+
} catch {
|
|
152
|
+
}
|
|
153
|
+
const branchDir = branch.replace(/\//g, "--");
|
|
154
|
+
const planFile = (0, import_path3.join)(KNOWLEDGE_ROOT, "plans", `${branchDir}.md`);
|
|
155
|
+
const hasPlan = (0, import_fs3.existsSync)(planFile);
|
|
156
|
+
const planDirPath = (0, import_path3.join)(KNOWLEDGE_ROOT, "plans", branchDir);
|
|
157
|
+
const hasPlanDir = (0, import_fs3.existsSync)(planDirPath);
|
|
158
|
+
const workflowPath = (0, import_path3.join)(sessionDir(sid), "workflow.json");
|
|
159
|
+
const hasWorkflow = (0, import_fs3.existsSync)(workflowPath);
|
|
160
|
+
const profile = analyzeCodebase(cwd);
|
|
161
|
+
try {
|
|
162
|
+
(0, import_fs3.writeFileSync)((0, import_path3.join)(dir, "codebase-profile.json"), JSON.stringify(profile, null, 2));
|
|
163
|
+
} catch {
|
|
164
|
+
}
|
|
165
|
+
const codebaseCtx = `Codebase: ${profile.type}. ${profile.description}`;
|
|
166
|
+
const isMainBranch = branch === "main" || branch === "master";
|
|
167
|
+
if (hasPlanDir && !hasWorkflow && !isMainBranch) {
|
|
168
|
+
respond({
|
|
169
|
+
continue: true,
|
|
170
|
+
additionalContext: `[NEXUS] Session ${sid} started. Branch: ${branch}. Mode: planning. Plan directory found. ${codebaseCtx}
|
|
171
|
+
DECISION CAPTURE: You are in multi-turn planning mode. When the user makes decisions (confirmatory expressions like "\uC774\uAC78\uB85C \uD558\uC790", "\uC0AD\uC81C\uD558\uC790", "\uC774\uB807\uAC8C \uBC14\uAFB8\uC790", or [d] tag), record them in .claude/nexus/plans/${branchDir}/plan.md under the decisions section.
|
|
172
|
+
When the user says "\uAD6C\uD604\uD558\uC790" or requests implementation, generate tasks.json from the accumulated decisions.`
|
|
173
|
+
});
|
|
174
|
+
} else {
|
|
175
|
+
respond({
|
|
176
|
+
continue: true,
|
|
177
|
+
additionalContext: `[NEXUS] Session ${sid} started. Branch: ${branch}. Plan: ${hasPlan ? "found" : "none"}. ${codebaseCtx}`
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function handleSessionEnd() {
|
|
182
|
+
const sid = getSessionId();
|
|
183
|
+
const summary = generateSessionSummary(sid);
|
|
184
|
+
cleanupSessionState(sid);
|
|
185
|
+
if (summary) {
|
|
186
|
+
respond({ continue: true, additionalContext: summary });
|
|
187
|
+
} else {
|
|
188
|
+
pass();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function generateSessionSummary(sid) {
|
|
192
|
+
const dir = sessionDir(sid);
|
|
193
|
+
if (!(0, import_fs3.existsSync)(dir)) return null;
|
|
194
|
+
try {
|
|
195
|
+
const parts = [`Session ${sid} summary:`];
|
|
196
|
+
let hasActivity = false;
|
|
197
|
+
const agentsPath = (0, import_path3.join)(dir, "agents.json");
|
|
198
|
+
if ((0, import_fs3.existsSync)(agentsPath)) {
|
|
199
|
+
const record = JSON.parse((0, import_fs3.readFileSync)(agentsPath, "utf-8"));
|
|
200
|
+
if (record.history.length > 0) {
|
|
201
|
+
hasActivity = true;
|
|
202
|
+
const agentCounts = {};
|
|
203
|
+
for (const h of record.history) agentCounts[h.name] = (agentCounts[h.name] ?? 0) + 1;
|
|
204
|
+
const agentStr = Object.entries(agentCounts).map(([n, c]) => `${n}\xD7${c}`).join(", ");
|
|
205
|
+
parts.push(`Agents: ${record.history.length} total (${agentStr})`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const trackerPath = (0, import_path3.join)(dir, "whisper-tracker.json");
|
|
209
|
+
if ((0, import_fs3.existsSync)(trackerPath)) {
|
|
210
|
+
const t = JSON.parse((0, import_fs3.readFileSync)(trackerPath, "utf-8"));
|
|
211
|
+
if (t.toolCallCount > 0) {
|
|
212
|
+
hasActivity = true;
|
|
213
|
+
parts.push(`Tools: ${t.toolCallCount} calls`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const sessionFile = (0, import_path3.join)(RUNTIME_ROOT, "state", "current-session.json");
|
|
217
|
+
if ((0, import_fs3.existsSync)(sessionFile)) {
|
|
218
|
+
const sessionData = JSON.parse((0, import_fs3.readFileSync)(sessionFile, "utf-8"));
|
|
219
|
+
if (sessionData.createdAt) {
|
|
220
|
+
const elapsed = Math.floor((Date.now() - new Date(sessionData.createdAt).getTime()) / 1e3);
|
|
221
|
+
const hh = Math.floor(elapsed / 3600);
|
|
222
|
+
const mm = Math.floor(elapsed % 3600 / 60);
|
|
223
|
+
parts.push(`Duration: ${hh > 0 ? `${hh}h${mm}m` : `${mm}m`}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (!hasActivity) return null;
|
|
227
|
+
return parts.join("\n");
|
|
228
|
+
} catch {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
function cleanupAllSessionStates() {
|
|
233
|
+
const sessionsDir = (0, import_path3.join)(RUNTIME_ROOT, "state", "sessions");
|
|
234
|
+
if (!(0, import_fs3.existsSync)(sessionsDir)) return;
|
|
235
|
+
try {
|
|
236
|
+
const dirs = (0, import_fs3.readdirSync)(sessionsDir);
|
|
237
|
+
for (const dir of dirs) {
|
|
238
|
+
cleanupSessionState(dir);
|
|
239
|
+
}
|
|
240
|
+
if (dirs.length > 10) {
|
|
241
|
+
const sorted = dirs.filter((d) => !d.startsWith("e2e")).map((d) => ({ name: d, mtime: (0, import_fs3.statSync)((0, import_path3.join)(sessionsDir, d)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
|
|
242
|
+
for (const s of sorted.slice(10)) {
|
|
243
|
+
const sdir = (0, import_path3.join)(sessionsDir, s.name);
|
|
244
|
+
try {
|
|
245
|
+
const files = (0, import_fs3.readdirSync)(sdir);
|
|
246
|
+
if (files.length === 0) {
|
|
247
|
+
(0, import_fs3.rmdirSync)(sdir);
|
|
248
|
+
}
|
|
249
|
+
} catch {
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
} catch {
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function cleanupSessionState(sid) {
|
|
257
|
+
const dir = sessionDir(sid);
|
|
258
|
+
if (!(0, import_fs3.existsSync)(dir)) return;
|
|
259
|
+
try {
|
|
260
|
+
(0, import_fs3.rmSync)(dir, { recursive: true, force: true });
|
|
261
|
+
} catch {
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function handleSubagentStart(event) {
|
|
265
|
+
const sid = getSessionId();
|
|
266
|
+
if (!sid) {
|
|
267
|
+
pass();
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const record = loadAgents(sid);
|
|
271
|
+
const name = normalizeAgentName(event.agent_type ?? event.agent_name ?? "unknown");
|
|
272
|
+
record.active.push(name);
|
|
273
|
+
record.history.push({ name, startedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
274
|
+
saveAgents(sid, record);
|
|
275
|
+
updateWorkflowPhase(sid, "delegating");
|
|
276
|
+
pass();
|
|
277
|
+
}
|
|
278
|
+
function handleSubagentStop(event) {
|
|
279
|
+
const sid = getSessionId();
|
|
280
|
+
if (!sid) {
|
|
281
|
+
pass();
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const record = loadAgents(sid);
|
|
285
|
+
const name = normalizeAgentName(event.agent_type ?? event.agent_name ?? "unknown");
|
|
286
|
+
const idx = record.active.indexOf(name);
|
|
287
|
+
if (idx >= 0) record.active.splice(idx, 1);
|
|
288
|
+
for (let i = record.history.length - 1; i >= 0; i--) {
|
|
289
|
+
if (record.history[i].name === name && !record.history[i].stoppedAt) {
|
|
290
|
+
record.history[i].stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
saveAgents(sid, record);
|
|
295
|
+
if (record.active.length === 0) {
|
|
296
|
+
const base = getBasePhase(sid);
|
|
297
|
+
if (base) updateWorkflowPhase(sid, base);
|
|
298
|
+
}
|
|
299
|
+
pass();
|
|
300
|
+
}
|
|
301
|
+
async function main() {
|
|
302
|
+
const input = await readStdin();
|
|
303
|
+
const event = JSON.parse(input);
|
|
304
|
+
const hookEvent = event.hook_event_name ?? event.type ?? "";
|
|
305
|
+
switch (hookEvent) {
|
|
306
|
+
case "SessionStart":
|
|
307
|
+
handleSessionStart();
|
|
308
|
+
break;
|
|
309
|
+
case "SessionEnd":
|
|
310
|
+
handleSessionEnd();
|
|
311
|
+
break;
|
|
312
|
+
case "SubagentStart":
|
|
313
|
+
handleSubagentStart(event);
|
|
314
|
+
break;
|
|
315
|
+
case "SubagentStop":
|
|
316
|
+
handleSubagentStop(event);
|
|
317
|
+
break;
|
|
318
|
+
default:
|
|
319
|
+
pass();
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
main().catch(() => {
|
|
323
|
+
respond({ continue: true });
|
|
324
|
+
});
|
|
325
|
+
//# sourceMappingURL=tracker.cjs.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/shared/hook-io.ts", "../src/hooks/tracker.ts", "../src/shared/paths.ts", "../src/shared/session.ts"],
|
|
4
|
+
"sourcesContent": ["/** \uD6C5 \uC2A4\uD06C\uB9BD\uD2B8 \uACF5\uD1B5 I/O: stdin JSON \uC77D\uAE30 + stdout JSON \uC751\uB2F5 */\n\nexport function readStdin(): Promise<string> {\n return new Promise((resolve) => {\n let data = '';\n process.stdin.on('data', (chunk: Buffer) => (data += chunk));\n process.stdin.on('end', () => resolve(data));\n });\n}\n\nexport function respond(obj: Record<string, unknown>): void {\n process.stdout.write(JSON.stringify(obj));\n}\n\nexport function pass(): void {\n respond({ continue: true });\n}\n", "// Tracker \uD6C5: SubagentStart/Stop, SessionStart/End \u2014 \uC5D0\uC774\uC804\uD2B8/\uC138\uC158 \uCD94\uC801\nimport { readStdin, respond, pass } from '../shared/hook-io.js';\nimport { existsSync, readFileSync, writeFileSync, readdirSync, rmdirSync, rmSync, statSync } from 'fs';\nimport { sessionDir, ensureDir, RUNTIME_ROOT, KNOWLEDGE_ROOT, updateWorkflowPhase, getBasePhase } from '../shared/paths.js';\nimport { getSessionId, createSession } from '../shared/session.js';\nimport { join } from 'path';\nimport { execSync } from 'child_process';\n\n// --- Agent Tracking ---\n\ninterface AgentRecord {\n active: string[];\n history: Array<{ name: string; startedAt: string; stoppedAt?: string }>;\n}\n\nfunction normalizeAgentName(name: string): string {\n return name.replace(/^(nexus|claude-nexus):/, '');\n}\n\nfunction loadAgents(sid: string): AgentRecord {\n const path = join(sessionDir(sid), 'agents.json');\n if (existsSync(path)) {\n try { return JSON.parse(readFileSync(path, 'utf-8')); } catch { /* fallthrough */ }\n }\n return { active: [], history: [] };\n}\n\nfunction saveAgents(sid: string, record: AgentRecord): void {\n const dir = sessionDir(sid);\n ensureDir(dir);\n writeFileSync(join(dir, 'agents.json'), JSON.stringify(record, null, 2));\n}\n\n// --- Session Start ---\n\ntype CodebaseType = 'disciplined' | 'transitional' | 'legacy' | 'greenfield';\n\ninterface CodebaseProfile {\n type: CodebaseType;\n description: string;\n hasLinter: boolean;\n hasTests: boolean;\n hasCI: boolean;\n hasSrc: boolean;\n fileCount: number;\n}\n\nfunction analyzeCodebase(cwd: string): CodebaseProfile {\n let fileCount = 0;\n try {\n const entries = readdirSync(cwd);\n fileCount = entries.length;\n } catch { /* skip */ }\n\n const has = (names: string[]): boolean => names.some(n => existsSync(join(cwd, n)));\n\n const hasLinter = has(['.eslintrc', '.eslintrc.js', '.eslintrc.json', '.eslintrc.yml', '.eslintrc.yaml', 'eslint.config.js', 'eslint.config.ts', 'eslint.config.mjs', '.prettierrc', '.prettierrc.js', '.prettierrc.json', '.prettierrc.yml']);\n const hasTests = has(['test', 'tests', '__tests__', 'spec']);\n const hasCI = has(['.github', '.circleci']);\n const hasSrc = has(['src']);\n\n let type: CodebaseType;\n let description: string;\n\n if (fileCount < 20 && !hasLinter && !hasTests) {\n type = 'greenfield';\n description = 'Few files, no established patterns yet';\n } else if (hasLinter && hasTests && hasCI) {\n type = 'disciplined';\n description = 'Has linter, tests, and CI \u2014 follow existing conventions strictly';\n } else if (hasSrc) {\n type = 'transitional';\n description = 'Has src/ but missing some tooling \u2014 introduce patterns incrementally';\n } else {\n type = 'legacy';\n description = 'Large codebase without modern tooling \u2014 be conservative with changes';\n }\n\n return { type, description, hasLinter, hasTests, hasCI, hasSrc, fileCount };\n}\n\nfunction handleSessionStart(): void {\n // \uBAA8\uB4E0 \uC138\uC158\uC758 \uC794\uC874 \uC6CC\uD06C\uD50C\uB85C\uC6B0 \uC0C1\uD0DC \uC815\uB9AC (resume, \uBE44\uC815\uC0C1 \uC885\uB8CC, \uBCA4\uCE58\uB9C8\uD06C \uC794\uC874 \uB4F1 \uBC29\uC5B4)\n cleanupAllSessionStates();\n\n const sid = createSession();\n const dir = sessionDir(sid);\n ensureDir(dir);\n\n // \uD604\uC7AC \uBE0C\uB79C\uCE58\uC758 plan \uC874\uC7AC \uD655\uC778\n let branch = 'unknown';\n let cwd = process.cwd();\n try {\n branch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf-8' }).trim();\n cwd = execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim();\n } catch { /* skip */ }\n\n const branchDir = branch.replace(/\\//g, '--');\n const planFile = join(KNOWLEDGE_ROOT, 'plans', `${branchDir}.md`);\n const hasPlan = existsSync(planFile);\n const planDirPath = join(KNOWLEDGE_ROOT, 'plans', branchDir);\n const hasPlanDir = existsSync(planDirPath);\n\n const workflowPath = join(sessionDir(sid), 'workflow.json');\n const hasWorkflow = existsSync(workflowPath);\n\n // Codebase analysis\n const profile = analyzeCodebase(cwd);\n try {\n writeFileSync(join(dir, 'codebase-profile.json'), JSON.stringify(profile, null, 2));\n } catch { /* skip */ }\n\n const codebaseCtx = `Codebase: ${profile.type}. ${profile.description}`;\n\n const isMainBranch = branch === 'main' || branch === 'master';\n\n if (hasPlanDir && !hasWorkflow && !isMainBranch) {\n respond({\n continue: true,\n additionalContext: `[NEXUS] Session ${sid} started. Branch: ${branch}. Mode: planning. Plan directory found. ${codebaseCtx}\nDECISION CAPTURE: You are in multi-turn planning mode. When the user makes decisions (confirmatory expressions like \"\uC774\uAC78\uB85C \uD558\uC790\", \"\uC0AD\uC81C\uD558\uC790\", \"\uC774\uB807\uAC8C \uBC14\uAFB8\uC790\", or [d] tag), record them in .claude/nexus/plans/${branchDir}/plan.md under the decisions section.\nWhen the user says \"\uAD6C\uD604\uD558\uC790\" or requests implementation, generate tasks.json from the accumulated decisions.`,\n });\n } else {\n respond({\n continue: true,\n additionalContext: `[NEXUS] Session ${sid} started. Branch: ${branch}. Plan: ${hasPlan ? 'found' : 'none'}. ${codebaseCtx}`,\n });\n }\n}\n\n// --- Session End ---\n\nfunction handleSessionEnd(): void {\n const sid = getSessionId();\n\n // \uC138\uC158 \uC694\uC57D \uB9AC\uD3EC\uD2B8 \uC0DD\uC131\n const summary = generateSessionSummary(sid);\n\n cleanupSessionState(sid);\n\n if (summary) {\n respond({ continue: true, additionalContext: summary });\n } else {\n pass();\n }\n}\n\n/** \uC138\uC158 \uC885\uB8CC \uC2DC \uD65C\uB3D9 \uC694\uC57D \uD14D\uC2A4\uD2B8\uB97C \uBC18\uD658 */\nfunction generateSessionSummary(sid: string): string | null {\n const dir = sessionDir(sid);\n if (!existsSync(dir)) return null;\n\n try {\n const parts: string[] = [`Session ${sid} summary:`];\n let hasActivity = false;\n\n // \uC5D0\uC774\uC804\uD2B8 \uC774\uB825\n const agentsPath = join(dir, 'agents.json');\n if (existsSync(agentsPath)) {\n const record: AgentRecord = JSON.parse(readFileSync(agentsPath, 'utf-8'));\n if (record.history.length > 0) {\n hasActivity = true;\n const agentCounts: Record<string, number> = {};\n for (const h of record.history) agentCounts[h.name] = (agentCounts[h.name] ?? 0) + 1;\n const agentStr = Object.entries(agentCounts).map(([n, c]) => `${n}\u00D7${c}`).join(', ');\n parts.push(`Agents: ${record.history.length} total (${agentStr})`);\n }\n }\n\n // \uB3C4\uAD6C \uD638\uCD9C \uC218\n const trackerPath = join(dir, 'whisper-tracker.json');\n if (existsSync(trackerPath)) {\n const t = JSON.parse(readFileSync(trackerPath, 'utf-8'));\n if (t.toolCallCount > 0) { hasActivity = true; parts.push(`Tools: ${t.toolCallCount} calls`); }\n }\n\n // \uC138\uC158 \uC2DC\uAC04\n const sessionFile = join(RUNTIME_ROOT, 'state', 'current-session.json');\n if (existsSync(sessionFile)) {\n const sessionData = JSON.parse(readFileSync(sessionFile, 'utf-8'));\n if (sessionData.createdAt) {\n const elapsed = Math.floor((Date.now() - new Date(sessionData.createdAt).getTime()) / 1000);\n const hh = Math.floor(elapsed / 3600);\n const mm = Math.floor((elapsed % 3600) / 60);\n parts.push(`Duration: ${hh > 0 ? `${hh}h${mm}m` : `${mm}m`}`);\n }\n }\n\n if (!hasActivity) return null;\n\n return parts.join('\\n');\n } catch { return null; }\n}\n\n/** \uBAA8\uB4E0 \uC138\uC158\uC758 \uC6CC\uD06C\uD50C\uB85C\uC6B0 \uC0C1\uD0DC \uC815\uB9AC + \uC624\uB798\uB41C \uBE48 \uC138\uC158 \uC0AD\uC81C (SessionStart \uC2DC \uD638\uCD9C) */\nfunction cleanupAllSessionStates(): void {\n const sessionsDir = join(RUNTIME_ROOT, 'state', 'sessions');\n if (!existsSync(sessionsDir)) return;\n try {\n const dirs = readdirSync(sessionsDir);\n for (const dir of dirs) {\n cleanupSessionState(dir);\n }\n // \uBE48 \uC138\uC158 \uB514\uB809\uD1A0\uB9AC \uC815\uB9AC (\uCD5C\uADFC 10\uAC1C \uC720\uC9C0)\n if (dirs.length > 10) {\n const sorted = dirs\n .filter(d => !d.startsWith('e2e'))\n .map(d => ({ name: d, mtime: statSync(join(sessionsDir, d)).mtimeMs }))\n .sort((a, b) => b.mtime - a.mtime);\n for (const s of sorted.slice(10)) {\n const sdir = join(sessionsDir, s.name);\n try {\n const files = readdirSync(sdir);\n if (files.length === 0) {\n rmdirSync(sdir);\n }\n } catch { /* skip */ }\n }\n }\n } catch { /* skip */ }\n}\n\n/** \uC138\uC158 \uB514\uB809\uD1A0\uB9AC \uC804\uCCB4 \uC0AD\uC81C */\nfunction cleanupSessionState(sid: string): void {\n const dir = sessionDir(sid);\n if (!existsSync(dir)) return;\n\n try { rmSync(dir, { recursive: true, force: true }); } catch { /* skip */ }\n}\n\n// --- Subagent Start ---\n\nfunction handleSubagentStart(event: { agent_name?: string; agent_type?: string }): void {\n const sid = getSessionId();\n if (!sid) { pass(); return; }\n const record = loadAgents(sid);\n const name = normalizeAgentName(event.agent_type ?? event.agent_name ?? 'unknown');\n\n record.active.push(name);\n record.history.push({ name, startedAt: new Date().toISOString() });\n saveAgents(sid, record);\n\n // Phase \uC790\uB3D9 \uC804\uD658: \uC5D0\uC774\uC804\uD2B8 spawn \u2192 delegating\n updateWorkflowPhase(sid, 'delegating');\n\n pass();\n}\n\n// --- Subagent Stop ---\n\nfunction handleSubagentStop(event: { agent_name?: string; agent_type?: string }): void {\n const sid = getSessionId();\n if (!sid) { pass(); return; }\n const record = loadAgents(sid);\n const name = normalizeAgentName(event.agent_type ?? event.agent_name ?? 'unknown');\n\n const idx = record.active.indexOf(name);\n if (idx >= 0) record.active.splice(idx, 1);\n\n // \uB9C8\uC9C0\uB9C9 history \uD56D\uBAA9\uC5D0 \uC885\uB8CC \uC2DC\uAC04 \uAE30\uB85D\n for (let i = record.history.length - 1; i >= 0; i--) {\n if (record.history[i].name === name && !record.history[i].stoppedAt) {\n record.history[i].stoppedAt = new Date().toISOString();\n break;\n }\n }\n\n saveAgents(sid, record);\n\n // Phase \uC790\uB3D9 \uC804\uD658: \uB9C8\uC9C0\uB9C9 \uC5D0\uC774\uC804\uD2B8 \uC885\uB8CC \u2192 base phase\uB85C \uBCF5\uADC0\n if (record.active.length === 0) {\n const base = getBasePhase(sid);\n if (base) updateWorkflowPhase(sid, base);\n }\n\n pass();\n}\n\n// --- Main ---\n\nasync function main() {\n const input = await readStdin();\n const event = JSON.parse(input);\n const hookEvent = event.hook_event_name ?? event.type ?? '';\n\n switch (hookEvent) {\n case 'SessionStart': handleSessionStart(); break;\n case 'SessionEnd': handleSessionEnd(); break;\n case 'SubagentStart': handleSubagentStart(event); break;\n case 'SubagentStop': handleSubagentStop(event); break;\n default: pass();\n }\n}\n\nmain().catch(() => {\n respond({ continue: true });\n});\n", "import { resolve, join } from 'path';\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';\n\n/** \uD504\uB85C\uC81D\uD2B8 \uB8E8\uD2B8 (.git\uC774 \uC788\uB294 \uB514\uB809\uD1A0\uB9AC) */\nfunction findProjectRoot(): string {\n let dir = process.cwd();\n while (dir !== '/') {\n if (existsSync(join(dir, '.git'))) return dir;\n dir = resolve(dir, '..');\n }\n return process.cwd();\n}\n\nconst PROJECT_ROOT = findProjectRoot();\n\n/** .nexus/ \u2014 gitignore, \uB7F0\uD0C0\uC784 \uC0C1\uD0DC */\nexport const RUNTIME_ROOT = join(PROJECT_ROOT, '.nexus');\n\n/** .claude/nexus/ \u2014 git \uCD94\uC801, \uACF5\uC720 \uC9C0\uC2DD */\nexport const KNOWLEDGE_ROOT = join(PROJECT_ROOT, '.claude', 'nexus');\n\n/** \uC138\uC158\uBCC4 \uC0C1\uD0DC \uB514\uB809\uD1A0\uB9AC */\nexport function sessionDir(sessionId: string): string {\n return join(RUNTIME_ROOT, 'state', 'sessions', sessionId);\n}\n\n/** \uC0C1\uD0DC \uD30C\uC77C \uACBD\uB85C */\nexport function statePath(sessionId: string, key: string): string {\n return join(sessionDir(sessionId), `${key}.json`);\n}\n\n/** \uC9C0\uC2DD \uD30C\uC77C \uACBD\uB85C */\nexport function knowledgePath(topic: string): string {\n return join(KNOWLEDGE_ROOT, 'knowledge', `${topic}.md`);\n}\n\n/** \uD50C\uB79C \uB514\uB809\uD1A0\uB9AC */\nexport function plansDir(): string {\n return join(KNOWLEDGE_ROOT, 'plans');\n}\n\n/** \uB514\uB809\uD1A0\uB9AC \uC0DD\uC131 (\uC7AC\uADC0) */\nexport function ensureDir(dir: string): void {\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n}\n\n/** workflow.json\uC758 phase\uB97C \uAC31\uC2E0 (consult/plan \uBAA8\uB4DC\uC77C \uB54C\uB9CC) */\nexport function updateWorkflowPhase(sid: string, phase: string): void {\n const workflowPath = join(sessionDir(sid), 'workflow.json');\n if (!existsSync(workflowPath)) return;\n try {\n const state = JSON.parse(readFileSync(workflowPath, 'utf-8'));\n if ((state.mode === 'consult' || state.mode === 'plan') && state.phase !== phase) {\n state.phase = phase;\n writeFileSync(workflowPath, JSON.stringify(state, null, 2));\n }\n } catch { /* skip */ }\n}\n\n/** \uD604\uC7AC \uC6CC\uD06C\uD50C\uB85C\uC6B0\uC758 base phase \uBC18\uD658 (consult\u2192exploring, plan\u2192analyzing) */\nexport function getBasePhase(sid: string): string | null {\n const workflowPath = join(sessionDir(sid), 'workflow.json');\n if (!existsSync(workflowPath)) return null;\n try {\n const state = JSON.parse(readFileSync(workflowPath, 'utf-8'));\n if (state.mode === 'consult') return 'exploring';\n if (state.mode === 'plan') return 'analyzing';\n } catch { /* skip */ }\n return null;\n}\n", "import { randomUUID } from 'crypto';\nimport { existsSync, readFileSync, writeFileSync } from 'fs';\nimport { join } from 'path';\nimport { RUNTIME_ROOT, ensureDir } from './paths.js';\n\nconst SESSION_FILE = join(RUNTIME_ROOT, 'state', 'current-session.json');\n\n/** \uD604\uC7AC \uC138\uC158 ID\uB97C \uAC00\uC838\uC624\uAC70\uB098 \uC0C8\uB85C \uC0DD\uC131 */\nexport function getSessionId(): string {\n if (existsSync(SESSION_FILE)) {\n try {\n const data = JSON.parse(readFileSync(SESSION_FILE, 'utf-8'));\n if (data.sessionId && typeof data.sessionId === 'string') {\n return data.sessionId;\n }\n } catch {\n // \uD30C\uC2F1 \uC2E4\uD328 \uC2DC \uC0C8\uB85C \uC0DD\uC131\n }\n }\n return createSession();\n}\n\n/** \uD604\uC7AC \uC800\uC7A5\uB41C \uC138\uC158 ID\uB97C \uC77D\uAE30 (\uB36E\uC5B4\uC4F0\uAE30 \uC804 \uD638\uCD9C\uC6A9) */\nexport function getPreviousSessionId(): string | null {\n if (existsSync(SESSION_FILE)) {\n try {\n const data = JSON.parse(readFileSync(SESSION_FILE, 'utf-8'));\n if (data.sessionId && typeof data.sessionId === 'string') {\n return data.sessionId;\n }\n } catch { /* skip */ }\n }\n return null;\n}\n\n/** \uC0C8 \uC138\uC158 \uC0DD\uC131 */\nexport function createSession(): string {\n const sessionId = randomUUID().slice(0, 8);\n ensureDir(join(RUNTIME_ROOT, 'state'));\n writeFileSync(SESSION_FILE, JSON.stringify({ sessionId, createdAt: new Date().toISOString() }));\n return sessionId;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["resolve", "import_fs", "import_fs", "import_path", "import_path"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: consult
|
|
3
|
+
description: Interactive discovery workflow with adaptive depth and dimension tracking.
|
|
4
|
+
triggers: ["consult", "상담", "어떻게 하면 좋을까", "뭐가 좋을까", "방법을 찾아줘"]
|
|
5
|
+
---
|
|
6
|
+
# Consult
|
|
7
|
+
|
|
8
|
+
Interactive discovery workflow — understand the user's real intent, explore options, and converge on the best approach before execution.
|
|
9
|
+
|
|
10
|
+
## Trigger
|
|
11
|
+
- User says: "consult", "상담", "어떻게 하면 좋을까", "뭐가 좋을까", "방법을 찾아줘"
|
|
12
|
+
- Explicit tag: `[consult]`
|
|
13
|
+
- Direct invocation: `/nexus:consult`
|
|
14
|
+
|
|
15
|
+
## What It Does
|
|
16
|
+
|
|
17
|
+
A structured conversation loop that **discovers** the best approach rather than immediately executing.
|
|
18
|
+
Consult keeps the user in the loop at every decision point.
|
|
19
|
+
|
|
20
|
+
## Adaptive Depth
|
|
21
|
+
|
|
22
|
+
모든 상담을 12라운드 인터뷰로 만들지 않는다. 첫 탐색에서 복잡도를 판단:
|
|
23
|
+
|
|
24
|
+
- **Lightweight** (기본): 불명확 차원 0-1개. 기존 consult 수준 — 2라운드 수렴, 선택지 제시 중심.
|
|
25
|
+
- **Deep** (자동 전환): 불명확 차원 2개 이상. 차원 추적 활성화, 관점 전환 포함, 라운드 연장 가능.
|
|
26
|
+
|
|
27
|
+
## Workflow
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
explore → assess → (clarify) → diverge → propose → converge → crystallize → execute bridge
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Phase 1: Explore
|
|
34
|
+
|
|
35
|
+
**Brownfield/Greenfield 자동 감지** 후 행동 분기:
|
|
36
|
+
- **Brownfield** (관련 파일/디렉토리가 이미 존재): 코드베이스 먼저 탐색 → 기존 패턴/제약 파악 → 그 위에 질문
|
|
37
|
+
- **Greenfield** (새로 만드는 것): 사용자 의도 중심 질문 → 기술 선택지 제시
|
|
38
|
+
|
|
39
|
+
탐색 수단:
|
|
40
|
+
- `nx_knowledge_read`, `nx_context`로 프로젝트 컨텍스트 파악
|
|
41
|
+
- Brownfield일 경우 `nx_lsp_document_symbols`, `nx_ast_search`로 기존 코드 구조 파악
|
|
42
|
+
- "코드를 봤더니 X인데 맞나요?" 형태의 근거 있는 질문
|
|
43
|
+
|
|
44
|
+
탐색 후 **차원별 이해도** 초기 평가:
|
|
45
|
+
```
|
|
46
|
+
[Goal: ?] [Constraints: ?] [Criteria: ?] [Context: ?]
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Phase 2: Clarify (대화)
|
|
50
|
+
|
|
51
|
+
**한 번에 하나의 질문** 원칙. 가장 약한 차원을 다음 질문 대상으로 선택.
|
|
52
|
+
**반드시 `AskUserQuestion`을 사용**하여 선택지 형태로 질문. 자유 텍스트 질문 금지.
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
❌ 일반 텍스트로 질문: "성능이란 어떤 의미인가요?"
|
|
56
|
+
✅ AskUserQuestion으로 선택지 제시:
|
|
57
|
+
AskUserQuestion({
|
|
58
|
+
questions: [{
|
|
59
|
+
question: "어떤 종류의 성능을 의미하나요?",
|
|
60
|
+
header: "Clarify",
|
|
61
|
+
multiSelect: false,
|
|
62
|
+
options: [
|
|
63
|
+
{ label: "응답 속도", description: "훅/MCP 프로세스 스폰 오버헤드" },
|
|
64
|
+
{ label: "토큰 효율성", description: "컨텍스트 소비, 에이전트 호출 비용" },
|
|
65
|
+
{ label: "둘 다", description: "속도와 토큰 효율 모두" }
|
|
66
|
+
]
|
|
67
|
+
}]
|
|
68
|
+
})
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
차원별 정성 추적 (숫자 점수 없음):
|
|
72
|
+
```
|
|
73
|
+
[Goal: ✅ 명확] [Constraints: ⚠️ 불명확] [Criteria: ❌ 미정의] [Context: ✅ 파악됨]
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Lightweight: 모든 차원 ✅이면 바로 Phase 3으로. 대부분 1-2 질문이면 충분.
|
|
77
|
+
Deep: 불명확 차원이 남아있으면 계속. 단, 대화 흐름에 따라 자연스럽게 **관점 전환**:
|
|
78
|
+
- 사용자가 한 방향에만 집중할 때: "반대 입장에서 보면..."
|
|
79
|
+
- 요구사항이 과도하게 복잡할 때: "가장 단순하게 줄이면..."
|
|
80
|
+
- 핵심 개념이 불명확할 때: "이 시스템의 본질적인 문제는..."
|
|
81
|
+
|
|
82
|
+
### Phase 3: Diverge (자동)
|
|
83
|
+
- 2-4개의 genuinely different 접근 방식 생성
|
|
84
|
+
- 각 접근에 pros, cons, effort level 정리
|
|
85
|
+
- 기존 패턴, 팀 컨벤션, 확장성 고려
|
|
86
|
+
|
|
87
|
+
### Phase 4: Propose (사용자 상호작용)
|
|
88
|
+
|
|
89
|
+
`AskUserQuestion`으로 선택지 제시:
|
|
90
|
+
```
|
|
91
|
+
AskUserQuestion({
|
|
92
|
+
questions: [{
|
|
93
|
+
question: "어떤 접근 방식이 좋을까요?",
|
|
94
|
+
header: "Approach",
|
|
95
|
+
multiSelect: false,
|
|
96
|
+
options: [
|
|
97
|
+
{
|
|
98
|
+
label: "Option A (Recommended)",
|
|
99
|
+
description: "간단한 설명...",
|
|
100
|
+
preview: "## Option A\n\n구체적인 구현 방향...\n\n**Pros:** ...\n**Cons:** ...\n**Effort:** ..."
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
}]
|
|
104
|
+
})
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
- `preview`에 구체적 내용 (코드 스니펫, 파일 구조 등)
|
|
108
|
+
- 추천 옵션에 "(Recommended)" 표기
|
|
109
|
+
- 라벨은 짧게 (1-5 단어), 상세는 description과 preview에
|
|
110
|
+
|
|
111
|
+
### Phase 5: Converge (자동 + 상호작용)
|
|
112
|
+
- 선택된 접근 방식 구체화
|
|
113
|
+
- 필요시 후속 질문 (한 번에 하나씩)
|
|
114
|
+
- 구현 계획 작성 (파일, 단계, 테스트 전략)
|
|
115
|
+
|
|
116
|
+
### Phase 6: Crystallize
|
|
117
|
+
|
|
118
|
+
계획을 최종 정리하며 **불명확 차원의 리스크를 투명하게 공개**:
|
|
119
|
+
```
|
|
120
|
+
⚠️ Constraints가 아직 불명확합니다. 진행하면 X 리스크가 있을 수 있어요.
|
|
121
|
+
```
|
|
122
|
+
사용자가 "됐어, 시작하자"라고 하면 차단하지 않음. 리스크만 알리고 존중.
|
|
123
|
+
|
|
124
|
+
### Phase 7: Execute Bridge
|
|
125
|
+
|
|
126
|
+
수렴 후 실행 전환 시 `AskUserQuestion`으로 선택지:
|
|
127
|
+
```
|
|
128
|
+
options:
|
|
129
|
+
- "Execute with delegation (Recommended)" — Nexus가 에이전트에 위임하여 실행
|
|
130
|
+
- "Plan only" — 계획 문서 생성 후 종료
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Key Principles
|
|
134
|
+
|
|
135
|
+
1. **한 번에 하나의 질문** — 여러 질문을 한꺼번에 던지지 않음
|
|
136
|
+
2. **질문은 구체적으로** — "어떻게 할까요?"가 아니라 명확한 선택지 제시
|
|
137
|
+
3. **선택지는 진짜 다른 것** — A와 B가 사실상 같으면 의미 없음
|
|
138
|
+
4. **컨텍스트를 먼저 파악** — 질문하기 전에 코드와 knowledge를 충분히 탐색
|
|
139
|
+
5. **적응형 깊이** — 단순한 건 빠르게, 복잡한 건 깊게
|
|
140
|
+
6. **리스크 투명 공개** — 불명확한 부분이 있으면 숨기지 않고 알림
|
|
141
|
+
7. **실행 강요 금지** — 사용자가 "아직"이라고 하면 계획만 정리하고 종료
|
|
142
|
+
|
|
143
|
+
## Dimension Tracking
|
|
144
|
+
|
|
145
|
+
네 가지 차원을 정성적으로 추적. 숫자 점수 없음 — LLM이 자기 이해도를 0.65 vs 0.70으로 평가하는 건 가짜 정밀도.
|
|
146
|
+
|
|
147
|
+
| 차원 | 의미 | 예시 질문 |
|
|
148
|
+
|------|------|-----------|
|
|
149
|
+
| Goal | 무엇을 달성하려는가 | "최종 사용자에게 어떤 변화를 주려는 건가요?" |
|
|
150
|
+
| Constraints | 제약조건, 불가능한 것 | "기존 API 호환성을 유지해야 하나요?" |
|
|
151
|
+
| Criteria | 성공/실패 판단 기준 | "어떤 상태가 되면 완료라고 볼 수 있나요?" |
|
|
152
|
+
| Context | 배경, 기존 시스템, 팀 상황 | "이 모듈을 다른 팀도 사용하나요?" |
|
|
153
|
+
|
|
154
|
+
상태 표기: ✅ 명확 / ⚠️ 불명확 / ❌ 미정의
|
|
155
|
+
|
|
156
|
+
## State Management
|
|
157
|
+
|
|
158
|
+
Consult는 상태 파일 없이 동작합니다.
|
|
159
|
+
|
|
160
|
+
## Deactivation
|
|
161
|
+
|
|
162
|
+
Consult는 자연스럽게 종료됩니다:
|
|
163
|
+
- 실행으로 전환 시 → 에이전트 위임으로 인계
|
|
164
|
+
- 계획만 정리 시 → 메모에 기록하고 종료
|
|
165
|
+
- 별도 `nx_state_clear`는 불필요
|