@dev-guard/cli 0.1.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/LICENSE +21 -0
- package/README.md +79 -0
- package/dist/agent-strategies.d.ts +23 -0
- package/dist/agent-strategies.js +130 -0
- package/dist/agent-strategies.js.map +1 -0
- package/dist/ai-context.d.ts +10 -0
- package/dist/ai-context.js +143 -0
- package/dist/ai-context.js.map +1 -0
- package/dist/check.d.ts +6 -0
- package/dist/check.js +89 -0
- package/dist/check.js.map +1 -0
- package/dist/clipboard.d.ts +6 -0
- package/dist/clipboard.js +43 -0
- package/dist/clipboard.js.map +1 -0
- package/dist/codex-notify.d.ts +23 -0
- package/dist/codex-notify.js +146 -0
- package/dist/codex-notify.js.map +1 -0
- package/dist/command-targets.d.ts +10 -0
- package/dist/command-targets.js +124 -0
- package/dist/command-targets.js.map +1 -0
- package/dist/config.d.ts +22 -0
- package/dist/config.js +180 -0
- package/dist/config.js.map +1 -0
- package/dist/configure.d.ts +1 -0
- package/dist/configure.js +79 -0
- package/dist/configure.js.map +1 -0
- package/dist/doctor.d.ts +1 -0
- package/dist/doctor.js +326 -0
- package/dist/doctor.js.map +1 -0
- package/dist/drift-telemetry.d.ts +13 -0
- package/dist/drift-telemetry.js +64 -0
- package/dist/drift-telemetry.js.map +1 -0
- package/dist/effective-task.d.ts +44 -0
- package/dist/effective-task.js +355 -0
- package/dist/effective-task.js.map +1 -0
- package/dist/fs.d.ts +10 -0
- package/dist/fs.js +58 -0
- package/dist/fs.js.map +1 -0
- package/dist/git.d.ts +24 -0
- package/dist/git.js +235 -0
- package/dist/git.js.map +1 -0
- package/dist/hooks.d.ts +39 -0
- package/dist/hooks.js +513 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +555 -0
- package/dist/index.js.map +1 -0
- package/dist/infer-task.d.ts +1 -0
- package/dist/infer-task.js +120 -0
- package/dist/infer-task.js.map +1 -0
- package/dist/init.d.ts +1 -0
- package/dist/init.js +50 -0
- package/dist/init.js.map +1 -0
- package/dist/install-agent-instructions.d.ts +1 -0
- package/dist/install-agent-instructions.js +113 -0
- package/dist/install-agent-instructions.js.map +1 -0
- package/dist/migration.d.ts +8 -0
- package/dist/migration.js +43 -0
- package/dist/migration.js.map +1 -0
- package/dist/paths.d.ts +38 -0
- package/dist/paths.js +41 -0
- package/dist/paths.js.map +1 -0
- package/dist/project-detection.d.ts +10 -0
- package/dist/project-detection.js +144 -0
- package/dist/project-detection.js.map +1 -0
- package/dist/project-identity.d.ts +7 -0
- package/dist/project-identity.js +93 -0
- package/dist/project-identity.js.map +1 -0
- package/dist/project-memory.d.ts +4 -0
- package/dist/project-memory.js +32 -0
- package/dist/project-memory.js.map +1 -0
- package/dist/prompt.d.ts +13 -0
- package/dist/prompt.js +125 -0
- package/dist/prompt.js.map +1 -0
- package/dist/refresh.d.ts +15 -0
- package/dist/refresh.js +146 -0
- package/dist/refresh.js.map +1 -0
- package/dist/report.d.ts +1 -0
- package/dist/report.js +109 -0
- package/dist/report.js.map +1 -0
- package/dist/review.d.ts +2 -0
- package/dist/review.js +653 -0
- package/dist/review.js.map +1 -0
- package/dist/rule-filter.d.ts +8 -0
- package/dist/rule-filter.js +79 -0
- package/dist/rule-filter.js.map +1 -0
- package/dist/runs.d.ts +21 -0
- package/dist/runs.js +142 -0
- package/dist/runs.js.map +1 -0
- package/dist/runtime-state.d.ts +69 -0
- package/dist/runtime-state.js +1383 -0
- package/dist/runtime-state.js.map +1 -0
- package/dist/scan.d.ts +1 -0
- package/dist/scan.js +55 -0
- package/dist/scan.js.map +1 -0
- package/dist/self.d.ts +3 -0
- package/dist/self.js +235 -0
- package/dist/self.js.map +1 -0
- package/dist/task-ai.d.ts +1 -0
- package/dist/task-ai.js +643 -0
- package/dist/task-ai.js.map +1 -0
- package/dist/telemetry.d.ts +1 -0
- package/dist/telemetry.js +11 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/update.d.ts +6 -0
- package/dist/update.js +154 -0
- package/dist/update.js.map +1 -0
- package/dist/watch.d.ts +1 -0
- package/dist/watch.js +303 -0
- package/dist/watch.js.map +1 -0
- package/package.json +31 -0
package/dist/task-ai.js
ADDED
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
import { defaultConfig, analyzeFileRelevance, analyzeSemanticDrift, buildImpactHints, buildTaskCompletionCriteria, classifyTaskType, detectRequirementMismatch, defaultContextPriority, extractTaskAIKeywords, filterDevGuardContextFiles, generateCodexPrompt, generateTaskMarkdownResult, inferRelatedFileCandidates, isAlwaysIgnoredContextPath, NoneAIProvider, OpenAIProvider, selectRelatedFilesFromScan } from "@dev-guard/core";
|
|
2
|
+
import { buildAIContextPreamble, writeAIContext } from "./ai-context.js";
|
|
3
|
+
import { copyTextToClipboard } from "./clipboard.js";
|
|
4
|
+
import { filterCommandTargetCandidates, inferCommandTargetFiles, mergeCommandTargetCandidates } from "./command-targets.js";
|
|
5
|
+
import { loadConfig, readOpenAIApiKey } from "./config.js";
|
|
6
|
+
import { fromRoot, readJsonFile, readTextFile, writeTextFile } from "./fs.js";
|
|
7
|
+
import { getGitChanges, getProjectFiles } from "./git.js";
|
|
8
|
+
import { formatProjectIdentityWarning, loadCurrentProjectIdentity, readStoredProjectIdentity, sameProjectIdentity } from "./project-identity.js";
|
|
9
|
+
import { filterRelevantMarkdown } from "./rule-filter.js";
|
|
10
|
+
import { createRunLog, listRunLogs, logRunSaved } from "./runs.js";
|
|
11
|
+
const defaultContextFileLimit = 5;
|
|
12
|
+
const perFileCharacterLimit = 4000;
|
|
13
|
+
const totalCodeContextCharacterLimit = 12000;
|
|
14
|
+
export async function runTaskAI(root, args) {
|
|
15
|
+
const options = parseTaskAIOptions(args);
|
|
16
|
+
const resolvedConfig = await loadConfig(root);
|
|
17
|
+
const config = resolvedConfig.config;
|
|
18
|
+
const providerName = config.ai?.provider ?? defaultConfig.ai.provider ?? "none";
|
|
19
|
+
const model = config.ai?.model ?? defaultConfig.ai.model ?? "gpt-4o-mini";
|
|
20
|
+
const openAIApiKey = readOpenAIApiKey();
|
|
21
|
+
const currentIdentity = await loadCurrentProjectIdentity(root).catch(() => undefined);
|
|
22
|
+
const [gitChanges, projectMemory, rulesMarkdown, mistakesMarkdown, projectStateMarkdown, decisionsMarkdown] = await Promise.all([
|
|
23
|
+
getGitChanges(root),
|
|
24
|
+
loadProjectMemory(root, options.noCache || options.fresh, currentIdentity),
|
|
25
|
+
readTextFile(fromRoot(root, ".devguard/rules.md")),
|
|
26
|
+
readTextFile(fromRoot(root, ".devguard/mistakes.md")),
|
|
27
|
+
readTextFile(fromRoot(root, "docs/PROJECT_STATE.md")),
|
|
28
|
+
readTextFile(fromRoot(root, "docs/DECISIONS.md"))
|
|
29
|
+
]);
|
|
30
|
+
if (projectMemory.fromCache && gitChanges.changedFiles.length > 0) {
|
|
31
|
+
console.error("dev-guard task-ai: warning: scan cache may be stale because git changes exist. Run `dev-guard refresh` to update project memory.");
|
|
32
|
+
}
|
|
33
|
+
const taskChangeFiles = filterDevGuardContextFiles(gitChanges.changeFiles, false);
|
|
34
|
+
const taskChangedFiles = [...new Set(taskChangeFiles.map((file) => file.path))].sort();
|
|
35
|
+
const taskType = classifyTaskType(options.requirement);
|
|
36
|
+
const completionCriteria = buildTaskCompletionCriteria(taskType);
|
|
37
|
+
const scopedMemory = options.fresh
|
|
38
|
+
? { targetFiles: [], summary: ["fresh mode suppresses run memory"] }
|
|
39
|
+
: await loadScopedTaskMemory(root, options.requirement, taskType, currentIdentity);
|
|
40
|
+
const relevanceText = [options.requirement, taskChangedFiles.join("\n"), projectMemory.projectMapMarkdown].join("\n");
|
|
41
|
+
const filteredRules = filterRelevantMarkdown(rulesMarkdown, relevanceText, currentIdentity, projectMemory.projectFiles.slice(0, 20));
|
|
42
|
+
const filteredMistakes = filterRelevantMarkdown(mistakesMarkdown, relevanceText, currentIdentity, projectMemory.projectFiles.slice(0, 20));
|
|
43
|
+
const filteredProjectState = options.fresh
|
|
44
|
+
? emptyMarkdownFilter(projectStateMarkdown, "fresh mode suppresses project docs")
|
|
45
|
+
: filterRelevantMarkdown(projectStateMarkdown, relevanceText, currentIdentity, projectMemory.projectFiles.slice(0, 20));
|
|
46
|
+
const filteredDecisions = options.fresh
|
|
47
|
+
? emptyMarkdownFilter(decisionsMarkdown, "fresh mode suppresses decisions")
|
|
48
|
+
: filterRelevantMarkdown(decisionsMarkdown, relevanceText, currentIdentity, projectMemory.projectFiles.slice(0, 20));
|
|
49
|
+
const relevanceCandidates = analyzeFileRelevance(options.requirement, projectMemory.projectFiles, {
|
|
50
|
+
index: projectMemory.index,
|
|
51
|
+
summaries: projectMemory.summaries,
|
|
52
|
+
runTargetFiles: scopedMemory.targetFiles,
|
|
53
|
+
codeGraph: projectMemory.codeGraph
|
|
54
|
+
}).filter((candidate) => !isAlwaysIgnoredContextPath(candidate.path));
|
|
55
|
+
const routedCandidates = routeCandidateFiles(taskType, options.requirement, projectMemory.projectFiles, relevanceCandidates, projectMemory);
|
|
56
|
+
const relatedFileCandidates = routedCandidates.relatedFileCandidates;
|
|
57
|
+
const scoredCandidates = routedCandidates.scoredCandidates;
|
|
58
|
+
const impactHints = buildImpactHints([...taskChangedFiles, ...relatedFileCandidates], projectMemory.codeGraph);
|
|
59
|
+
if (taskType.requiresPhasing) {
|
|
60
|
+
console.error(`dev-guard task-ai: ${taskType.type}/large-task decomposition enabled. Strategy: ${taskType.strategy}.`);
|
|
61
|
+
}
|
|
62
|
+
const codeContexts = options.codeContext
|
|
63
|
+
? await collectCodeContexts(root, prioritizeChangedCandidates(scoredCandidates.filter((candidate) => candidate.role !== "ignored").map((candidate) => candidate.path), projectMemory.index), extractTaskAIKeywords(options.requirement), {
|
|
64
|
+
maxFiles: options.contextFiles,
|
|
65
|
+
perFileLimit: perFileCharacterLimit,
|
|
66
|
+
totalLimit: totalCodeContextCharacterLimit
|
|
67
|
+
})
|
|
68
|
+
: [];
|
|
69
|
+
if (options.debugContext) {
|
|
70
|
+
printDebugContext({
|
|
71
|
+
fromCache: projectMemory.fromCache,
|
|
72
|
+
keywords: extractTaskAIKeywords(options.requirement),
|
|
73
|
+
relatedFileCandidates,
|
|
74
|
+
codeContextFiles: codeContexts.map((context) => context.path),
|
|
75
|
+
scoredCandidates,
|
|
76
|
+
impactHints,
|
|
77
|
+
taskType,
|
|
78
|
+
completionCriteria,
|
|
79
|
+
requirementAnchor: options.requirement,
|
|
80
|
+
taskSubtype: taskType.subtype,
|
|
81
|
+
ignoredStaleContextSummary: summarizeSuppressedContext(filteredRules, filteredMistakes, filteredProjectState, filteredDecisions),
|
|
82
|
+
scopedMemorySummary: scopedMemory.summary,
|
|
83
|
+
mismatchResult: detectRequirementMismatch([filteredProjectState.filteredMarkdown, filteredDecisions.filteredMarkdown].join("\n"), options.requirement, taskType),
|
|
84
|
+
driftResult: analyzeSemanticDrift(options.requirement, [filteredProjectState.filteredMarkdown, filteredDecisions.filteredMarkdown].join("\n"), taskType),
|
|
85
|
+
contextPriority: defaultContextPriority(),
|
|
86
|
+
fresh: options.fresh,
|
|
87
|
+
provider: providerName,
|
|
88
|
+
model,
|
|
89
|
+
configSource: resolvedConfig.source,
|
|
90
|
+
envResolution: resolvedConfig.env,
|
|
91
|
+
rulesFilter: filteredRules,
|
|
92
|
+
mistakesFilter: filteredMistakes
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
if (providerName === "none") {
|
|
96
|
+
throw new Error("AI provider가 none입니다. `dev-guard configure ai --provider openai --model gpt-4o-mini`로 먼저 설정하세요.");
|
|
97
|
+
}
|
|
98
|
+
if (providerName === "openai" && !openAIApiKey) {
|
|
99
|
+
throw new Error("OpenAI API key 환경변수가 없습니다. API key는 config에 저장하지 말고 `DEV_GUARD_OPENAI_API_KEY` 또는 `OPENAI_API_KEY`로 설정해 주세요.");
|
|
100
|
+
}
|
|
101
|
+
const provider = providerName === "openai"
|
|
102
|
+
? new OpenAIProvider({
|
|
103
|
+
apiKey: openAIApiKey ?? "",
|
|
104
|
+
model,
|
|
105
|
+
temperature: config.ai?.temperature,
|
|
106
|
+
maxTokens: config.ai?.maxTokens,
|
|
107
|
+
reasoningEffort: config.ai?.reasoningEffort,
|
|
108
|
+
baseURL: config.ai?.baseURL
|
|
109
|
+
})
|
|
110
|
+
: new NoneAIProvider();
|
|
111
|
+
const taskResult = await generateTaskMarkdownResult(provider, {
|
|
112
|
+
requirement: options.requirement,
|
|
113
|
+
rulesMarkdown: filteredRules.filteredMarkdown,
|
|
114
|
+
mistakesMarkdown: filteredMistakes.filteredMarkdown,
|
|
115
|
+
projectStateMarkdown: [filteredProjectState.filteredMarkdown, projectMemory.projectMapMarkdown].filter(Boolean).join("\n\n"),
|
|
116
|
+
decisionsMarkdown: filteredDecisions.filteredMarkdown,
|
|
117
|
+
changedFiles: taskChangedFiles,
|
|
118
|
+
changeFiles: taskChangeFiles,
|
|
119
|
+
diffText: buildFilteredDiffSummary(gitChanges.diffText, taskChangedFiles),
|
|
120
|
+
projectFiles: projectMemory.projectFiles,
|
|
121
|
+
relatedFileCandidates,
|
|
122
|
+
fileCandidates: scoredCandidates,
|
|
123
|
+
codeGraph: projectMemory.codeGraph,
|
|
124
|
+
impactHints,
|
|
125
|
+
codeContexts,
|
|
126
|
+
taskType,
|
|
127
|
+
completionCriteria
|
|
128
|
+
}, model);
|
|
129
|
+
const taskMarkdown = taskResult.markdown;
|
|
130
|
+
if (taskResult.scopeFilledFromCandidates) {
|
|
131
|
+
console.error("dev-guard task-ai: scope filled from related file candidates");
|
|
132
|
+
}
|
|
133
|
+
if (options.write) {
|
|
134
|
+
await writeTextFile(fromRoot(root, ".devguard/task.md"), taskMarkdown);
|
|
135
|
+
await writeAIContext(root).catch(() => undefined);
|
|
136
|
+
console.error("dev-guard task-ai: .devguard/task.md updated");
|
|
137
|
+
console.error("dev-guard task-ai: .devguard/AI_CONTEXT.md updated");
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
console.error("dev-guard task-ai: preview only. 저장하려면 --write를 사용하세요.");
|
|
141
|
+
console.error("dev-guard task-ai: Codex 프롬프트도 보려면 --prompt, 클립보드 복사는 --copy를 사용하세요.");
|
|
142
|
+
}
|
|
143
|
+
const rawCodexPrompt = options.prompt || options.copy || options.saveRun
|
|
144
|
+
? generateCodexPrompt({
|
|
145
|
+
taskMarkdown,
|
|
146
|
+
rulesMarkdown: filteredRules.filteredMarkdown,
|
|
147
|
+
mistakesMarkdown: filteredMistakes.filteredMarkdown,
|
|
148
|
+
projectStateMarkdown: filteredProjectState.filteredMarkdown,
|
|
149
|
+
decisionsMarkdown: filteredDecisions.filteredMarkdown,
|
|
150
|
+
changedFiles: taskChangedFiles,
|
|
151
|
+
changeFiles: taskChangeFiles,
|
|
152
|
+
diffText: buildFilteredDiffSummary(gitChanges.diffText, taskChangedFiles),
|
|
153
|
+
compact: true,
|
|
154
|
+
density: "ultra",
|
|
155
|
+
maxPromptTokens: 2500,
|
|
156
|
+
impactHints
|
|
157
|
+
}).promptText
|
|
158
|
+
: undefined;
|
|
159
|
+
// Prepend AI_CONTEXT.md reference so Codex/Claude reads it first
|
|
160
|
+
const codexPrompt = rawCodexPrompt ? buildAIContextPreamble() + rawCodexPrompt : undefined;
|
|
161
|
+
if (options.copy && codexPrompt) {
|
|
162
|
+
const result = await copyTextToClipboard(codexPrompt);
|
|
163
|
+
if (result.ok) {
|
|
164
|
+
console.error("dev-guard task-ai: copied Codex prompt to clipboard.");
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
console.error(`dev-guard task-ai: clipboard copy failed (${result.reason}).`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
console.log(taskMarkdown);
|
|
171
|
+
if (options.prompt && codexPrompt) {
|
|
172
|
+
console.log("\n---\n");
|
|
173
|
+
console.log(codexPrompt);
|
|
174
|
+
}
|
|
175
|
+
if ((options.write && options.prompt && options.copy) || options.saveRun) {
|
|
176
|
+
const run = await createRunLog(root, {
|
|
177
|
+
command: "task-ai",
|
|
178
|
+
userRequest: options.requirement,
|
|
179
|
+
generatedTaskMarkdown: taskMarkdown,
|
|
180
|
+
generatedCodexPrompt: rawCodexPrompt,
|
|
181
|
+
relatedFiles: relatedFileCandidates,
|
|
182
|
+
model,
|
|
183
|
+
provider: providerName,
|
|
184
|
+
changedFilesAtCreation: taskChangedFiles,
|
|
185
|
+
projectIdentity: currentIdentity,
|
|
186
|
+
status: "created"
|
|
187
|
+
});
|
|
188
|
+
logRunSaved(run);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function parseTaskAIOptions(args) {
|
|
192
|
+
const write = args.includes("--write");
|
|
193
|
+
const prompt = args.includes("--prompt");
|
|
194
|
+
const copy = args.includes("--copy");
|
|
195
|
+
const codeContext = !args.includes("--no-code-context");
|
|
196
|
+
const noCache = args.includes("--no-cache");
|
|
197
|
+
const fresh = args.includes("--fresh");
|
|
198
|
+
const debugContext = args.includes("--debug-context");
|
|
199
|
+
const saveRun = args.includes("--save-run");
|
|
200
|
+
const contextFiles = parseNumberOption(args, "--context-files", defaultContextFileLimit);
|
|
201
|
+
const requirement = collectRequirementArgs(args).join(" ").trim();
|
|
202
|
+
if (!requirement) {
|
|
203
|
+
throw new Error('요구사항을 입력해 주세요. 예: dev-guard task-ai "README 사용법을 보강해줘"');
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
requirement,
|
|
207
|
+
write,
|
|
208
|
+
prompt,
|
|
209
|
+
copy,
|
|
210
|
+
contextFiles,
|
|
211
|
+
codeContext,
|
|
212
|
+
noCache,
|
|
213
|
+
fresh,
|
|
214
|
+
debugContext,
|
|
215
|
+
saveRun
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function summarizeSuppressedContext(...filters) {
|
|
219
|
+
return filters
|
|
220
|
+
.flatMap((filter) => filter.suppressed ?? [])
|
|
221
|
+
.map((block) => block.replace(/\s+/g, " ").trim())
|
|
222
|
+
.filter(Boolean)
|
|
223
|
+
.slice(0, 8);
|
|
224
|
+
}
|
|
225
|
+
function emptyMarkdownFilter(markdown, reason) {
|
|
226
|
+
const suppressed = markdown
|
|
227
|
+
.split("\n")
|
|
228
|
+
.map((line) => line.trim())
|
|
229
|
+
.filter(Boolean)
|
|
230
|
+
.slice(0, 8)
|
|
231
|
+
.map((line) => `${reason}: ${line.slice(0, 80)}`);
|
|
232
|
+
return {
|
|
233
|
+
filteredMarkdown: "",
|
|
234
|
+
loaded: suppressed.length,
|
|
235
|
+
relevant: 0,
|
|
236
|
+
suppressed
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
async function loadScopedTaskMemory(root, requirement, taskType, currentIdentity) {
|
|
240
|
+
const runs = await listRunLogs(root);
|
|
241
|
+
const requirementDrift = (text) => analyzeSemanticDrift(requirement, text, taskType);
|
|
242
|
+
const related = runs
|
|
243
|
+
.filter((run) => !currentIdentity || !run.projectIdentity || sameProjectIdentity(currentIdentity, run.projectIdentity))
|
|
244
|
+
.map((run) => {
|
|
245
|
+
const text = [run.title, run.userRequest, run.generatedTaskMarkdown].filter(Boolean).join("\n");
|
|
246
|
+
const drift = requirementDrift(text);
|
|
247
|
+
const decay = memoryDecay(run.createdAt);
|
|
248
|
+
const subtypeBoost = text.includes(taskType.subtype ?? taskType.type) ? 1.5 : 0;
|
|
249
|
+
const rawScore = (drift.severity === "low" ? 4 : drift.severity === "medium" ? 1 : -4) + (run.relatedFiles?.length ? 1 : 0) + subtypeBoost;
|
|
250
|
+
const score = Number((rawScore * decay).toFixed(2));
|
|
251
|
+
return { run, drift, score, decay };
|
|
252
|
+
})
|
|
253
|
+
.filter((entry) => entry.score > 0)
|
|
254
|
+
.sort((a, b) => b.score - a.score || Date.parse(b.run.createdAt) - Date.parse(a.run.createdAt))
|
|
255
|
+
.slice(0, 5);
|
|
256
|
+
return {
|
|
257
|
+
targetFiles: [...new Set(related.flatMap((entry) => entry.run.relatedFiles ?? []))].slice(0, 12),
|
|
258
|
+
summary: related.map((entry) => `${entry.run.id} score=${entry.score} decay=${entry.decay.toFixed(2)} drift=${entry.drift.severity}`)
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
function memoryDecay(createdAt) {
|
|
262
|
+
const ageDays = (Date.now() - Date.parse(createdAt)) / 86_400_000;
|
|
263
|
+
if (!Number.isFinite(ageDays) || ageDays <= 7) {
|
|
264
|
+
return 1;
|
|
265
|
+
}
|
|
266
|
+
if (ageDays <= 30) {
|
|
267
|
+
return 0.8;
|
|
268
|
+
}
|
|
269
|
+
if (ageDays <= 90) {
|
|
270
|
+
return 0.5;
|
|
271
|
+
}
|
|
272
|
+
if (ageDays <= 180) {
|
|
273
|
+
return 0.25;
|
|
274
|
+
}
|
|
275
|
+
return 0.1;
|
|
276
|
+
}
|
|
277
|
+
function printDebugContext(debug) {
|
|
278
|
+
console.error("dev-guard task-ai debug context");
|
|
279
|
+
console.error(`- scan cache: ${debug.fromCache ? "yes" : "no"}`);
|
|
280
|
+
console.error(`- fresh mode: ${debug.fresh ? "yes" : "no"}`);
|
|
281
|
+
console.error(`- requirement anchor: ${debug.requirementAnchor}`);
|
|
282
|
+
console.error("- context priority:");
|
|
283
|
+
console.error(` requirement=${debug.contextPriority.requirement}, relatedCode=${debug.contextPriority.relatedCode}, subtype=${debug.contextPriority.taskSubtypeContext}, run=${debug.contextPriority.recentRun}, memory=${debug.contextPriority.projectMemory}, staleDocs=${debug.contextPriority.staleDocs}`);
|
|
284
|
+
console.error(`- provider: ${debug.provider}`);
|
|
285
|
+
console.error(`- model: ${debug.model}`);
|
|
286
|
+
console.error(`- config source: ${debug.configSource}`);
|
|
287
|
+
console.error("- env resolution:");
|
|
288
|
+
console.error(` DEV_GUARD_OPENAI_API_KEY: ${debug.envResolution.apiKey.checked[0]?.found ? "found" : "missing"}`);
|
|
289
|
+
console.error(` OPENAI_API_KEY: ${debug.envResolution.apiKey.checked[1]?.found ? "found" : "missing"}`);
|
|
290
|
+
console.error(` selected API key source: ${debug.envResolution.apiKey.selectedKey ?? "none"}`);
|
|
291
|
+
console.error("- task type:");
|
|
292
|
+
console.error(` type: ${debug.taskType.type}`);
|
|
293
|
+
if (debug.taskSubtype) {
|
|
294
|
+
console.error(` subtype: ${debug.taskSubtype}`);
|
|
295
|
+
}
|
|
296
|
+
console.error(` confidence: ${debug.taskType.confidence}`);
|
|
297
|
+
console.error(` strategy: ${debug.taskType.strategy}`);
|
|
298
|
+
console.error(` risk: ${debug.taskType.riskLevel}`);
|
|
299
|
+
console.error(` requires phasing: ${debug.taskType.requiresPhasing ? "true" : "false"}`);
|
|
300
|
+
console.error(` reasons: ${debug.taskType.reasons.length > 0 ? debug.taskType.reasons.join("; ") : "none"}`);
|
|
301
|
+
console.error(` domain: ${debug.taskType.domainKeywords?.length ? debug.taskType.domainKeywords.join(", ") : "none"}`);
|
|
302
|
+
console.error("- stale context guard:");
|
|
303
|
+
console.error(` ignored stale contexts: ${debug.ignoredStaleContextSummary.length}`);
|
|
304
|
+
for (const item of debug.ignoredStaleContextSummary.slice(0, 5)) {
|
|
305
|
+
console.error(` - ${item}`);
|
|
306
|
+
}
|
|
307
|
+
console.error(` mismatch check: ${debug.mismatchResult.length > 0 ? debug.mismatchResult.join("; ") : "pass"}`);
|
|
308
|
+
console.error(` drift: ${debug.driftResult.severity} score=${debug.driftResult.driftScore} similarity=${debug.driftResult.similarity.toFixed(2)}`);
|
|
309
|
+
console.error(` drift domains: requirement=${debug.driftResult.requirementDomains.join(", ") || "none"} generated=${debug.driftResult.generatedDomains.join(", ") || "none"}`);
|
|
310
|
+
console.error("- scoped task memory:");
|
|
311
|
+
console.error(` related memories: ${debug.scopedMemorySummary.length}`);
|
|
312
|
+
for (const item of debug.scopedMemorySummary.slice(0, 5)) {
|
|
313
|
+
console.error(` - ${item}`);
|
|
314
|
+
}
|
|
315
|
+
console.error("- completion criteria:");
|
|
316
|
+
for (const check of debug.completionCriteria.requiredChecks.slice(0, 8)) {
|
|
317
|
+
console.error(` required: ${check}`);
|
|
318
|
+
}
|
|
319
|
+
for (const failure of (debug.completionCriteria.blockingFailures ?? []).slice(0, 6)) {
|
|
320
|
+
console.error(` blocking: ${failure}`);
|
|
321
|
+
}
|
|
322
|
+
console.error(`- extracted keywords: ${debug.keywords.length > 0 ? debug.keywords.join(", ") : "none"}`);
|
|
323
|
+
console.error(`- scored candidates (${debug.scoredCandidates.length}):`);
|
|
324
|
+
for (const candidate of debug.scoredCandidates.slice(0, 20)) {
|
|
325
|
+
console.error(` - ${candidate.path}`);
|
|
326
|
+
console.error(` score: ${candidate.score}`);
|
|
327
|
+
console.error(` role: ${candidate.role}`);
|
|
328
|
+
console.error(` reasons: ${candidate.reasons.length > 0 ? candidate.reasons.join("; ") : "none"}`);
|
|
329
|
+
console.error(` negative reasons: ${candidate.negativeReasons.length > 0 ? candidate.negativeReasons.join("; ") : "none"}`);
|
|
330
|
+
}
|
|
331
|
+
console.error(`- impact hints (${debug.impactHints.length}):`);
|
|
332
|
+
for (const hint of debug.impactHints.slice(0, 8)) {
|
|
333
|
+
console.error(` - ${hint.file}`);
|
|
334
|
+
console.error(` imported by: ${hint.importedByCount}`);
|
|
335
|
+
console.error(` affected areas: ${hint.affectedAreas.join(", ") || "unknown"}`);
|
|
336
|
+
console.error(` examples: ${hint.importedBy.slice(0, 4).join(", ") || "none"}`);
|
|
337
|
+
}
|
|
338
|
+
console.error(`- code context files (${debug.codeContextFiles.length}):`);
|
|
339
|
+
for (const file of debug.codeContextFiles) {
|
|
340
|
+
console.error(` - ${file}`);
|
|
341
|
+
}
|
|
342
|
+
console.error(`- candidate files sent to AI: ${debug.relatedFileCandidates.length}`);
|
|
343
|
+
printMarkdownFilterDebug("rules", debug.rulesFilter);
|
|
344
|
+
printMarkdownFilterDebug("mistakes", debug.mistakesFilter);
|
|
345
|
+
}
|
|
346
|
+
function printMarkdownFilterDebug(label, filter) {
|
|
347
|
+
console.error(`- loaded ${label}: ${filter.loaded}`);
|
|
348
|
+
console.error(`- relevant ${label}: ${filter.relevant}`);
|
|
349
|
+
console.error(`- suppressed ${label}: ${filter.suppressed.length}`);
|
|
350
|
+
for (const item of filter.suppressed.slice(0, 10)) {
|
|
351
|
+
console.error(` - ${item}`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
async function loadProjectMemory(root, noCache, currentIdentity) {
|
|
355
|
+
if (!noCache) {
|
|
356
|
+
const [index, summaries, projectMapMarkdown, codeGraph, storedIdentity] = await Promise.all([
|
|
357
|
+
readJsonFile(fromRoot(root, ".devguard/project-index.json"), []),
|
|
358
|
+
readJsonFile(fromRoot(root, ".devguard/file-summaries.json"), []),
|
|
359
|
+
readTextFile(fromRoot(root, ".devguard/project-map.md")),
|
|
360
|
+
readJsonFile(fromRoot(root, ".devguard/code-graph.json"), []),
|
|
361
|
+
readStoredProjectIdentity(root)
|
|
362
|
+
]);
|
|
363
|
+
if (index.length > 0) {
|
|
364
|
+
if (!currentIdentity || !sameProjectIdentity(currentIdentity, storedIdentity)) {
|
|
365
|
+
console.error(`dev-guard task-ai: warning: ${formatProjectIdentityWarning("scan cache", currentIdentity ?? { fingerprint: "unknown", root, frameworkKeywords: [] }, storedIdentity)}`);
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
const projectFiles = index.map((entry) => entry.path).filter((path) => !isAlwaysIgnoredContextPath(path));
|
|
369
|
+
console.error("dev-guard task-ai: using scan cache from .devguard/project-index.json");
|
|
370
|
+
return {
|
|
371
|
+
fromCache: true,
|
|
372
|
+
projectFiles,
|
|
373
|
+
index: index.filter((entry) => !isAlwaysIgnoredContextPath(entry.path)),
|
|
374
|
+
summaries: summaries.filter((summary) => !isAlwaysIgnoredContextPath(summary.path)),
|
|
375
|
+
projectMapMarkdown,
|
|
376
|
+
codeGraph: codeGraph.filter((entry) => !isAlwaysIgnoredContextPath(entry.file))
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
const projectFiles = await getProjectFiles(root);
|
|
382
|
+
return {
|
|
383
|
+
fromCache: false,
|
|
384
|
+
projectFiles,
|
|
385
|
+
index: [],
|
|
386
|
+
summaries: [],
|
|
387
|
+
projectMapMarkdown: "",
|
|
388
|
+
codeGraph: []
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
function mergeCandidates(...candidateGroups) {
|
|
392
|
+
return [...new Set(candidateGroups.flat())].slice(0, 30);
|
|
393
|
+
}
|
|
394
|
+
function mergeScoredCandidates(scored, paths) {
|
|
395
|
+
const byPath = new Map(scored.map((candidate) => [candidate.path, candidate]));
|
|
396
|
+
for (const path of paths) {
|
|
397
|
+
if (!byPath.has(path)) {
|
|
398
|
+
byPath.set(path, {
|
|
399
|
+
path,
|
|
400
|
+
score: 5,
|
|
401
|
+
reasons: ["fallback candidate"],
|
|
402
|
+
negativeReasons: [],
|
|
403
|
+
role: "reference"
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return [...byPath.values()].sort((a, b) => b.score - a.score || a.path.localeCompare(b.path)).slice(0, 30);
|
|
408
|
+
}
|
|
409
|
+
function filterCandidateFiles(candidates) {
|
|
410
|
+
return candidates.filter((candidate) => !isAlwaysIgnoredContextPath(candidate));
|
|
411
|
+
}
|
|
412
|
+
function routeCandidateFiles(taskType, requirement, projectFiles, relevanceCandidates, projectMemory) {
|
|
413
|
+
const commandTarget = inferCommandTargetFiles(requirement, projectFiles);
|
|
414
|
+
const genericCandidates = filterCandidateFiles(mergeCandidates(relevanceCandidates.filter((candidate) => candidate.role !== "ignored").map((candidate) => candidate.path), projectMemory.fromCache ? selectRelatedFilesFromScan(requirement, projectMemory.index, projectMemory.summaries) : [], inferRelatedFileCandidates(requirement, projectFiles)));
|
|
415
|
+
if (taskType.type === "i18n") {
|
|
416
|
+
const relatedFileCandidates = inferI18nCandidateFiles(projectFiles);
|
|
417
|
+
return {
|
|
418
|
+
relatedFileCandidates,
|
|
419
|
+
scoredCandidates: scoreTaskTypeCandidates(taskType, relatedFileCandidates, projectFiles)
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
if (taskType.type === "docs" || taskType.type === "infra_config" || taskType.type === "architecture" || taskType.type === "migration" || taskType.type === "product_strategy") {
|
|
423
|
+
const relatedFileCandidates = inferTaskTypeCandidateFiles(taskType, projectFiles, genericCandidates);
|
|
424
|
+
return {
|
|
425
|
+
relatedFileCandidates,
|
|
426
|
+
scoredCandidates: scoreTaskTypeCandidates(taskType, relatedFileCandidates, projectFiles)
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
if (commandTarget && (taskType.subtype === "cli_output_polish" || /명령|command|출력|output|요약|summary|preview/i.test(requirement))) {
|
|
430
|
+
const commandRelated = [...commandTarget.edit, ...commandTarget.reference];
|
|
431
|
+
const nonCommandGeneric = genericCandidates.filter((file) => !/(^|\/)update\.ts$/.test(file) || commandRelated.includes(file));
|
|
432
|
+
const scopedRelevanceCandidates = filterCommandTargetCandidates(relevanceCandidates, commandTarget);
|
|
433
|
+
const scoredCandidates = mergeCommandTargetCandidates(mergeScoredCandidates(scopedRelevanceCandidates, nonCommandGeneric), commandTarget);
|
|
434
|
+
return {
|
|
435
|
+
relatedFileCandidates: filterCandidateFiles(scoredCandidates.filter((candidate) => candidate.role !== "ignored").map((candidate) => candidate.path)),
|
|
436
|
+
scoredCandidates
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
return {
|
|
440
|
+
relatedFileCandidates: genericCandidates,
|
|
441
|
+
scoredCandidates: mergeScoredCandidates(relevanceCandidates, genericCandidates)
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
function inferI18nCandidateFiles(projectFiles) {
|
|
445
|
+
const fileSet = new Set(projectFiles);
|
|
446
|
+
const existingI18n = projectFiles.filter((file) => /(^|\/)(i18n|locale|locales|messages|translations|dictionaries)(\/|\.|-|_)/i.test(file));
|
|
447
|
+
const candidates = existingI18n.length > 0 ? existingI18n.slice(0, 5) : ["lib/i18n/config.ts", "messages/ko.json", "messages/en.json"];
|
|
448
|
+
const layout = ["app/layout.tsx", "src/app/layout.tsx", "pages/_app.tsx", "src/pages/_app.tsx"].find((file) => fileSet.has(file));
|
|
449
|
+
if (layout) {
|
|
450
|
+
candidates.push(layout);
|
|
451
|
+
}
|
|
452
|
+
const samplePage = [
|
|
453
|
+
"app/about/page.tsx",
|
|
454
|
+
"src/app/about/page.tsx",
|
|
455
|
+
"app/about/service/page.tsx",
|
|
456
|
+
"src/app/about/service/page.tsx",
|
|
457
|
+
"app/page.tsx",
|
|
458
|
+
"src/app/page.tsx",
|
|
459
|
+
"pages/index.tsx",
|
|
460
|
+
"src/pages/index.tsx"
|
|
461
|
+
].find((file) => fileSet.has(file)) ??
|
|
462
|
+
projectFiles.find((file) => /(^|\/)(about|landing|public|home)(\/|[-_.])/.test(file) && /\.(tsx|jsx|ts|js)$/.test(file));
|
|
463
|
+
if (samplePage) {
|
|
464
|
+
candidates.push(samplePage);
|
|
465
|
+
}
|
|
466
|
+
return filterCandidateFiles([...new Set(candidates)]).slice(0, 8);
|
|
467
|
+
}
|
|
468
|
+
function inferTaskTypeCandidateFiles(taskType, projectFiles, fallback) {
|
|
469
|
+
const docsCandidates = projectFiles.filter((file) => /(^|\/)(README|CHANGELOG|CONTRIBUTING|docs\/|\.md$)/i.test(file));
|
|
470
|
+
const infraCandidates = projectFiles.filter((file) => /(^|\/)(package\.json|pnpm-workspace\.yaml|tsconfig|next\.config|vite\.config|tailwind\.config|postcss\.config|eslint\.config|biome\.json|\.github\/|vercel\.json|netlify\.toml|Dockerfile|docker-compose|supabase\/config)/i.test(file));
|
|
471
|
+
const architectureCandidates = projectFiles.filter((file) => /(^|\/)(lib|src|packages|app\/layout|providers?|context|config|middleware)\//i.test(file) || /(^|\/)(middleware|next\.config|vite\.config|package\.json)/i.test(file));
|
|
472
|
+
if (taskType.type === "docs") {
|
|
473
|
+
return filterCandidateFiles(docsCandidates.length > 0 ? docsCandidates : ["README.md", "docs/"]);
|
|
474
|
+
}
|
|
475
|
+
if (taskType.type === "infra_config") {
|
|
476
|
+
return filterCandidateFiles(infraCandidates.length > 0 ? infraCandidates : ["package.json", ".env.example", "next.config.ts"]);
|
|
477
|
+
}
|
|
478
|
+
if (taskType.type === "architecture" || taskType.type === "migration") {
|
|
479
|
+
return filterCandidateFiles((architectureCandidates.length > 0 ? architectureCandidates : fallback).slice(0, 8));
|
|
480
|
+
}
|
|
481
|
+
if (taskType.type === "product_strategy") {
|
|
482
|
+
return filterCandidateFiles(fallback.slice(0, 8));
|
|
483
|
+
}
|
|
484
|
+
return filterCandidateFiles(fallback);
|
|
485
|
+
}
|
|
486
|
+
function scoreTaskTypeCandidates(taskType, candidates, projectFiles) {
|
|
487
|
+
const fileSet = new Set(projectFiles);
|
|
488
|
+
return candidates.map((path, index) => {
|
|
489
|
+
const isStructure = /(^|\/)(i18n|locale|locales|messages|translations|dictionaries)(\/|\.|-|_)/i.test(path);
|
|
490
|
+
const isLayout = /(^|\/)(layout|_app)\.(tsx|jsx|ts|js)$/i.test(path);
|
|
491
|
+
const isDocs = taskType.type === "docs";
|
|
492
|
+
const isInfra = taskType.type === "infra_config";
|
|
493
|
+
const role = taskType.type === "i18n" ? (isStructure || isLayout ? "edit" : "reference") : taskType.type === "product_strategy" ? "reference" : index < 5 ? "edit" : "reference";
|
|
494
|
+
const reasons = [
|
|
495
|
+
taskType.type === "i18n" && isStructure ? "i18n structure candidate" : "",
|
|
496
|
+
taskType.type === "i18n" && isLayout ? "provider/wiring candidate" : "",
|
|
497
|
+
taskType.type === "product_strategy" ? "product discovery reference only" : "",
|
|
498
|
+
isDocs ? "docs strategy candidate" : "",
|
|
499
|
+
isInfra ? "infra/config strategy candidate" : "",
|
|
500
|
+
taskType.type === "architecture" || taskType.type === "migration" ? "phased structure candidate" : "",
|
|
501
|
+
!fileSet.has(path) ? "new file candidate" : "existing project file",
|
|
502
|
+
role === "reference" ? "single representative sample screen" : ""
|
|
503
|
+
].filter(Boolean);
|
|
504
|
+
return {
|
|
505
|
+
path,
|
|
506
|
+
score: 100 - index,
|
|
507
|
+
reasons,
|
|
508
|
+
negativeReasons: [],
|
|
509
|
+
role
|
|
510
|
+
};
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
function buildFilteredDiffSummary(diffText, changedFiles) {
|
|
514
|
+
if (!diffText.trim() || changedFiles.length === 0) {
|
|
515
|
+
return "";
|
|
516
|
+
}
|
|
517
|
+
const allowed = new Set(changedFiles);
|
|
518
|
+
const blocks = diffText.split(/(?=^diff --git\s+)/m);
|
|
519
|
+
const keptBlocks = blocks.filter((block) => {
|
|
520
|
+
if (!block.startsWith("diff --git ")) {
|
|
521
|
+
return block
|
|
522
|
+
.split("\n")
|
|
523
|
+
.some((line) => {
|
|
524
|
+
const match = line.match(/^Untracked file:\s+(.+?)\s+-/);
|
|
525
|
+
return match?.[1] ? allowed.has(match[1]) : false;
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
const match = block.match(/^diff --git\s+a\/(.+?)\s+b\/(.+)$/m);
|
|
529
|
+
return Boolean(match && (allowed.has(match[1]) || allowed.has(match[2])));
|
|
530
|
+
});
|
|
531
|
+
return keptBlocks.join("").trim();
|
|
532
|
+
}
|
|
533
|
+
function prioritizeChangedCandidates(candidates, index) {
|
|
534
|
+
if (index.length === 0) {
|
|
535
|
+
return candidates;
|
|
536
|
+
}
|
|
537
|
+
const lastModifiedByPath = new Map(index.map((entry) => [entry.path, entry.lastModified]));
|
|
538
|
+
return [...candidates].sort((a, b) => {
|
|
539
|
+
const aTime = Date.parse(lastModifiedByPath.get(a) ?? "1970-01-01T00:00:00.000Z");
|
|
540
|
+
const bTime = Date.parse(lastModifiedByPath.get(b) ?? "1970-01-01T00:00:00.000Z");
|
|
541
|
+
return bTime - aTime;
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
async function collectCodeContexts(root, candidateFiles, keywords, limits) {
|
|
545
|
+
const contexts = [];
|
|
546
|
+
let totalLength = 0;
|
|
547
|
+
for (const file of candidateFiles.slice(0, limits.maxFiles)) {
|
|
548
|
+
if (totalLength >= limits.totalLimit) {
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
const remaining = limits.totalLimit - totalLength;
|
|
552
|
+
const perFileLimit = Math.min(limits.perFileLimit, remaining);
|
|
553
|
+
const content = await readTextFile(fromRoot(root, file));
|
|
554
|
+
if (!content.trim()) {
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
const context = buildCodeContext(file, content, keywords, perFileLimit);
|
|
558
|
+
contexts.push(context);
|
|
559
|
+
totalLength += context.excerpt.length;
|
|
560
|
+
}
|
|
561
|
+
return contexts;
|
|
562
|
+
}
|
|
563
|
+
function buildCodeContext(path, content, keywords, maxCharacters) {
|
|
564
|
+
const lines = content.split("\n");
|
|
565
|
+
const matchedKeywords = keywords.filter((keyword) => content.toLowerCase().includes(keyword.toLowerCase()));
|
|
566
|
+
const excerpt = matchedKeywords.length > 0 ? keywordExcerpt(lines, matchedKeywords, maxCharacters) : headExcerpt(content, maxCharacters);
|
|
567
|
+
return {
|
|
568
|
+
path,
|
|
569
|
+
matchedKeywords,
|
|
570
|
+
excerpt,
|
|
571
|
+
truncated: content.length > excerpt.length
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
function keywordExcerpt(lines, keywords, maxCharacters) {
|
|
575
|
+
const selected = new Set();
|
|
576
|
+
const headLineCount = Math.min(lines.length, 40);
|
|
577
|
+
for (let index = 0; index < headLineCount; index += 1) {
|
|
578
|
+
selected.add(index);
|
|
579
|
+
}
|
|
580
|
+
for (const keyword of keywords) {
|
|
581
|
+
const lowerKeyword = keyword.toLowerCase();
|
|
582
|
+
lines.forEach((line, index) => {
|
|
583
|
+
if (!line.toLowerCase().includes(lowerKeyword)) {
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
for (let cursor = Math.max(0, index - 8); cursor <= Math.min(lines.length - 1, index + 12); cursor += 1) {
|
|
587
|
+
selected.add(cursor);
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
if (selected.size === 0) {
|
|
592
|
+
return headExcerpt(lines.join("\n"), maxCharacters);
|
|
593
|
+
}
|
|
594
|
+
const chunks = [];
|
|
595
|
+
let previous = -2;
|
|
596
|
+
for (const lineIndex of [...selected].sort((a, b) => a - b)) {
|
|
597
|
+
if (lineIndex > previous + 1 && chunks.length > 0) {
|
|
598
|
+
chunks.push("...");
|
|
599
|
+
}
|
|
600
|
+
chunks.push(`${lineIndex + 1}: ${lines[lineIndex]}`);
|
|
601
|
+
previous = lineIndex;
|
|
602
|
+
if (chunks.join("\n").length >= maxCharacters) {
|
|
603
|
+
break;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
return trimToLimit(chunks.join("\n"), maxCharacters);
|
|
607
|
+
}
|
|
608
|
+
function headExcerpt(content, maxCharacters) {
|
|
609
|
+
return trimToLimit(content, maxCharacters);
|
|
610
|
+
}
|
|
611
|
+
function trimToLimit(content, maxCharacters) {
|
|
612
|
+
if (content.length <= maxCharacters) {
|
|
613
|
+
return content;
|
|
614
|
+
}
|
|
615
|
+
return `${content.slice(0, Math.max(0, maxCharacters - 80)).trimEnd()}\n... truncated by dev-guard code context limit ...`;
|
|
616
|
+
}
|
|
617
|
+
function parseNumberOption(args, name, fallback) {
|
|
618
|
+
const index = args.indexOf(name);
|
|
619
|
+
if (index < 0) {
|
|
620
|
+
return fallback;
|
|
621
|
+
}
|
|
622
|
+
const value = Number(args[index + 1]);
|
|
623
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
624
|
+
throw new Error(`${name} 옵션에는 1 이상의 숫자가 필요합니다.`);
|
|
625
|
+
}
|
|
626
|
+
return value;
|
|
627
|
+
}
|
|
628
|
+
function collectRequirementArgs(args) {
|
|
629
|
+
const requirement = [];
|
|
630
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
631
|
+
const arg = args[index];
|
|
632
|
+
if (arg === "--context-files") {
|
|
633
|
+
index += 1;
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
if (arg.startsWith("--")) {
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
requirement.push(arg);
|
|
640
|
+
}
|
|
641
|
+
return requirement;
|
|
642
|
+
}
|
|
643
|
+
//# sourceMappingURL=task-ai.js.map
|