helloloop 0.2.1 → 0.6.1
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/plugin.json +1 -1
- package/.codex-plugin/plugin.json +3 -3
- package/README.md +297 -272
- package/hosts/claude/marketplace/plugins/helloloop/.claude-plugin/plugin.json +1 -1
- package/hosts/claude/marketplace/plugins/helloloop/commands/helloloop.md +19 -9
- package/hosts/claude/marketplace/plugins/helloloop/skills/helloloop/SKILL.md +12 -4
- package/hosts/gemini/extension/GEMINI.md +13 -4
- package/hosts/gemini/extension/commands/helloloop.toml +19 -8
- package/hosts/gemini/extension/gemini-extension.json +1 -1
- package/package.json +2 -2
- package/scripts/uninstall-home-plugin.ps1 +25 -0
- package/skills/helloloop/SKILL.md +42 -7
- package/src/analyze_confirmation.mjs +108 -8
- package/src/analyze_prompt.mjs +17 -1
- package/src/analyze_user_input.mjs +321 -0
- package/src/analyzer.mjs +167 -42
- package/src/cli.mjs +34 -308
- package/src/cli_analyze_command.mjs +248 -0
- package/src/cli_args.mjs +106 -0
- package/src/cli_command_handlers.mjs +120 -0
- package/src/cli_context.mjs +31 -0
- package/src/cli_render.mjs +70 -0
- package/src/cli_support.mjs +95 -31
- package/src/completion_review.mjs +243 -0
- package/src/config.mjs +50 -0
- package/src/discovery.mjs +243 -9
- package/src/discovery_inference.mjs +62 -18
- package/src/discovery_paths.mjs +143 -8
- package/src/discovery_prompt.mjs +273 -0
- package/src/engine_metadata.mjs +79 -0
- package/src/engine_selection.mjs +335 -0
- package/src/engine_selection_failure.mjs +51 -0
- package/src/engine_selection_messages.mjs +119 -0
- package/src/engine_selection_probe.mjs +78 -0
- package/src/engine_selection_prompt.mjs +48 -0
- package/src/engine_selection_settings.mjs +38 -0
- package/src/guardrails.mjs +15 -4
- package/src/install.mjs +20 -266
- package/src/install_claude.mjs +189 -0
- package/src/install_codex.mjs +114 -0
- package/src/install_gemini.mjs +43 -0
- package/src/install_shared.mjs +90 -0
- package/src/process.mjs +482 -39
- package/src/prompt.mjs +9 -5
- package/src/prompt_session.mjs +40 -0
- package/src/rebuild.mjs +116 -0
- package/src/runner.mjs +3 -341
- package/src/runner_execute_task.mjs +301 -0
- package/src/runner_execution_support.mjs +155 -0
- package/src/runner_loop.mjs +106 -0
- package/src/runner_once.mjs +29 -0
- package/src/runner_status.mjs +104 -0
- package/src/runtime_recovery.mjs +301 -0
- package/src/shell_invocation.mjs +16 -0
- package/templates/analysis-output.schema.json +58 -1
- package/templates/policy.template.json +27 -0
- package/templates/project.template.json +2 -0
- package/templates/task-review-output.schema.json +70 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import { classifyExplicitPath, pathExists, resolveAbsolute } from "./discovery_paths.mjs";
|
|
4
|
+
import { normalizeEngineName } from "./engine_metadata.mjs";
|
|
5
|
+
|
|
6
|
+
const DOC_SUFFIX = new Set([
|
|
7
|
+
".md",
|
|
8
|
+
".markdown",
|
|
9
|
+
".mdx",
|
|
10
|
+
".txt",
|
|
11
|
+
".rst",
|
|
12
|
+
".adoc",
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
const ROLE_LABELS = {
|
|
16
|
+
docs: "开发文档",
|
|
17
|
+
repo: "项目路径",
|
|
18
|
+
input: "补充路径",
|
|
19
|
+
new_repo: "项目路径",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const SOURCE_LABELS = {
|
|
23
|
+
flag: "命令参数",
|
|
24
|
+
positional: "命令附带路径",
|
|
25
|
+
interactive: "交互确认",
|
|
26
|
+
interactive_new_repo: "交互确认",
|
|
27
|
+
workspace_single_doc: "工作区唯一文档候选",
|
|
28
|
+
workspace_single_repo: "工作区唯一项目候选",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function isProbablyPathToken(token) {
|
|
32
|
+
const value = String(token || "").trim();
|
|
33
|
+
if (!value) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (/^[A-Za-z]:[\\/]/.test(value) || /^\\\\/.test(value)) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (/^\.\.?([\\/]|$)/.test(value) || /^[~][\\/]/.test(value)) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (value.includes("/") || value.includes("\\")) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return DOC_SUFFIX.has(path.extname(value).toLowerCase());
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function normalizePathKey(targetPath) {
|
|
53
|
+
const resolved = path.resolve(String(targetPath || ""));
|
|
54
|
+
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isSamePath(left, right) {
|
|
58
|
+
return normalizePathKey(left) === normalizePathKey(right);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function createIssue(severity, message) {
|
|
62
|
+
return { severity, message: String(message || "").trim() };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function roleLabel(role) {
|
|
66
|
+
return ROLE_LABELS[role] || "路径";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function sourceLabel(source) {
|
|
70
|
+
return SOURCE_LABELS[source] || "输入";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function formatPathRef(ref) {
|
|
74
|
+
const label = roleLabel(ref.role);
|
|
75
|
+
const statusSuffix = ref.role === "new_repo" ? ",当前不存在,将按新项目创建" : "";
|
|
76
|
+
const suffix = ref.source ? `(来源:${sourceLabel(ref.source)}${statusSuffix})` : "";
|
|
77
|
+
return `${label}:${String(ref.absolutePath || "").replaceAll("\\", "/")}${suffix}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function ensureSelectionSource(selectionSources, role, source) {
|
|
81
|
+
if (!selectionSources[role] && source) {
|
|
82
|
+
selectionSources[role] = source;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function pushDistinctRef(refs, role, absolutePath, source) {
|
|
87
|
+
const existing = refs.find((item) => item.role === role && isSamePath(item.absolutePath, absolutePath));
|
|
88
|
+
if (!existing) {
|
|
89
|
+
refs.push({ role, absolutePath, source });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function pushRoleConflict(issues, role, acceptedPath, incomingPath, acceptedSource, incomingSource) {
|
|
94
|
+
if (isSamePath(acceptedPath, incomingPath)) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
issues.blocking.push(createIssue(
|
|
99
|
+
"blocking",
|
|
100
|
+
`同时给出了多个${roleLabel(role)}:${String(acceptedPath).replaceAll("\\", "/")}(${sourceLabel(acceptedSource)}) 与 ${String(incomingPath).replaceAll("\\", "/")}(${sourceLabel(incomingSource)})。请只保留一个,或改用 ${role === "repo" ? "--repo" : "--docs"} 明确指定。`,
|
|
101
|
+
));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function assignDocsPath({
|
|
105
|
+
absolutePath,
|
|
106
|
+
source,
|
|
107
|
+
explicitRefs,
|
|
108
|
+
issues,
|
|
109
|
+
selectionSources,
|
|
110
|
+
currentDocsPath,
|
|
111
|
+
}) {
|
|
112
|
+
if (!currentDocsPath) {
|
|
113
|
+
pushDistinctRef(explicitRefs, "docs", absolutePath, source);
|
|
114
|
+
ensureSelectionSource(selectionSources, "docs", source);
|
|
115
|
+
return absolutePath;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
pushRoleConflict(issues, "docs", currentDocsPath, absolutePath, selectionSources.docs || source, source);
|
|
119
|
+
pushDistinctRef(explicitRefs, "docs", absolutePath, source);
|
|
120
|
+
return currentDocsPath;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function assignRepoRoot({
|
|
124
|
+
absolutePath,
|
|
125
|
+
source,
|
|
126
|
+
explicitRefs,
|
|
127
|
+
issues,
|
|
128
|
+
selectionSources,
|
|
129
|
+
currentRepoRoot,
|
|
130
|
+
allowNewRepoRoot = false,
|
|
131
|
+
}) {
|
|
132
|
+
if (!currentRepoRoot) {
|
|
133
|
+
pushDistinctRef(explicitRefs, allowNewRepoRoot ? "new_repo" : "repo", absolutePath, source);
|
|
134
|
+
ensureSelectionSource(selectionSources, "repo", source);
|
|
135
|
+
return absolutePath;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
pushRoleConflict(issues, "repo", currentRepoRoot, absolutePath, selectionSources.repo || source, source);
|
|
139
|
+
pushDistinctRef(explicitRefs, allowNewRepoRoot ? "new_repo" : "repo", absolutePath, source);
|
|
140
|
+
return currentRepoRoot;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function normalizeAnalyzeOptions(rawOptions = {}, cwd = process.cwd()) {
|
|
144
|
+
const options = {
|
|
145
|
+
...rawOptions,
|
|
146
|
+
requiredDocs: Array.isArray(rawOptions.requiredDocs) ? [...rawOptions.requiredDocs] : [],
|
|
147
|
+
constraints: Array.isArray(rawOptions.constraints) ? [...rawOptions.constraints] : [],
|
|
148
|
+
};
|
|
149
|
+
const rawPositionals = Array.isArray(rawOptions.positionalArgs) ? rawOptions.positionalArgs : [];
|
|
150
|
+
const positionals = [...rawPositionals];
|
|
151
|
+
const explicitRefs = [];
|
|
152
|
+
const requestTokens = [];
|
|
153
|
+
const issues = {
|
|
154
|
+
blocking: [],
|
|
155
|
+
warnings: [],
|
|
156
|
+
};
|
|
157
|
+
const selectionSources = {
|
|
158
|
+
...(rawOptions.selectionSources || {}),
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
let docsPath = options.docsPath ? resolveAbsolute(options.docsPath, cwd) : "";
|
|
162
|
+
let repoRoot = options.repoRoot ? resolveAbsolute(options.repoRoot, cwd) : "";
|
|
163
|
+
let inputPath = options.inputPath ? resolveAbsolute(options.inputPath, cwd) : "";
|
|
164
|
+
let allowNewRepoRoot = Boolean(options.allowNewRepoRoot);
|
|
165
|
+
let engine = normalizeEngineName(options.engine);
|
|
166
|
+
let engineSource = options.engineSource || (engine ? "flag" : "");
|
|
167
|
+
|
|
168
|
+
if (!engine && positionals.length) {
|
|
169
|
+
const firstToken = String(positionals[0] || "").trim();
|
|
170
|
+
const leadingEngine = normalizeEngineName(firstToken);
|
|
171
|
+
if (leadingEngine && firstToken.toLowerCase() === leadingEngine) {
|
|
172
|
+
engine = leadingEngine;
|
|
173
|
+
engineSource = "leading_positional";
|
|
174
|
+
positionals.shift();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (docsPath) {
|
|
179
|
+
ensureSelectionSource(selectionSources, "docs", rawOptions.selectionSources?.docs || "flag");
|
|
180
|
+
}
|
|
181
|
+
if (repoRoot) {
|
|
182
|
+
ensureSelectionSource(selectionSources, "repo", rawOptions.selectionSources?.repo || "flag");
|
|
183
|
+
if (allowNewRepoRoot && !pathExists(repoRoot)) {
|
|
184
|
+
pushDistinctRef(explicitRefs, "new_repo", repoRoot, selectionSources.repo);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
for (const token of positionals) {
|
|
189
|
+
if (!isProbablyPathToken(token)) {
|
|
190
|
+
requestTokens.push(token);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const absolutePath = resolveAbsolute(token, cwd);
|
|
195
|
+
if (pathExists(absolutePath)) {
|
|
196
|
+
const classified = classifyExplicitPath(absolutePath);
|
|
197
|
+
if (classified.kind === "docs") {
|
|
198
|
+
docsPath = assignDocsPath({
|
|
199
|
+
absolutePath: classified.absolutePath,
|
|
200
|
+
source: "positional",
|
|
201
|
+
explicitRefs,
|
|
202
|
+
issues,
|
|
203
|
+
selectionSources,
|
|
204
|
+
currentDocsPath: docsPath,
|
|
205
|
+
});
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (classified.kind === "repo") {
|
|
209
|
+
repoRoot = assignRepoRoot({
|
|
210
|
+
absolutePath: classified.absolutePath,
|
|
211
|
+
source: "positional",
|
|
212
|
+
explicitRefs,
|
|
213
|
+
issues,
|
|
214
|
+
selectionSources,
|
|
215
|
+
currentRepoRoot: repoRoot,
|
|
216
|
+
});
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if ((classified.kind === "workspace" || classified.kind === "directory") && docsPath) {
|
|
220
|
+
repoRoot = assignRepoRoot({
|
|
221
|
+
absolutePath: classified.absolutePath,
|
|
222
|
+
source: "positional",
|
|
223
|
+
explicitRefs,
|
|
224
|
+
issues,
|
|
225
|
+
selectionSources,
|
|
226
|
+
currentRepoRoot: repoRoot,
|
|
227
|
+
});
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (!inputPath) {
|
|
231
|
+
inputPath = classified.absolutePath;
|
|
232
|
+
pushDistinctRef(explicitRefs, "input", classified.absolutePath, "positional");
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
} else if (!repoRoot) {
|
|
236
|
+
repoRoot = absolutePath;
|
|
237
|
+
allowNewRepoRoot = true;
|
|
238
|
+
ensureSelectionSource(selectionSources, "repo", "positional");
|
|
239
|
+
pushDistinctRef(explicitRefs, "new_repo", absolutePath, "positional");
|
|
240
|
+
continue;
|
|
241
|
+
} else {
|
|
242
|
+
pushRoleConflict(issues, "repo", repoRoot, absolutePath, selectionSources.repo || "positional", "positional");
|
|
243
|
+
pushDistinctRef(explicitRefs, "new_repo", absolutePath, "positional");
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
requestTokens.push(token);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (options.dryRun && options.yes) {
|
|
251
|
+
issues.warnings.push(createIssue(
|
|
252
|
+
"warning",
|
|
253
|
+
"同时传入了 --dry-run 和 -y;本次仍按 --dry-run 只分析,不自动执行。",
|
|
254
|
+
));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const userRequestText = requestTokens.join(" ").trim();
|
|
258
|
+
|
|
259
|
+
options.docsPath = docsPath;
|
|
260
|
+
options.repoRoot = repoRoot;
|
|
261
|
+
options.inputPath = inputPath;
|
|
262
|
+
options.allowNewRepoRoot = allowNewRepoRoot;
|
|
263
|
+
options.engine = engine;
|
|
264
|
+
options.engineSource = engineSource;
|
|
265
|
+
options.selectionSources = selectionSources;
|
|
266
|
+
options.userRequestText = userRequestText;
|
|
267
|
+
options.inputIssues = issues;
|
|
268
|
+
options.userIntent = {
|
|
269
|
+
rawPositionals,
|
|
270
|
+
explicitRefs,
|
|
271
|
+
requestText: userRequestText,
|
|
272
|
+
issues,
|
|
273
|
+
selectionSources,
|
|
274
|
+
explicitEngine: engine,
|
|
275
|
+
explicitEngineSource: engineSource,
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
return options;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function hasBlockingInputIssues(inputIssues = {}) {
|
|
282
|
+
return Array.isArray(inputIssues.blocking) && inputIssues.blocking.length > 0;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function renderInputIssueLines(inputIssues = {}) {
|
|
286
|
+
return [
|
|
287
|
+
...(Array.isArray(inputIssues.blocking) ? inputIssues.blocking : []),
|
|
288
|
+
...(Array.isArray(inputIssues.warnings) ? inputIssues.warnings : []),
|
|
289
|
+
].map((item) => `- ${item.message}`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function renderBlockingInputIssueMessage(inputIssues = {}) {
|
|
293
|
+
const lines = renderInputIssueLines({
|
|
294
|
+
blocking: Array.isArray(inputIssues.blocking) ? inputIssues.blocking : [],
|
|
295
|
+
});
|
|
296
|
+
if (!lines.length) {
|
|
297
|
+
return "";
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return [
|
|
301
|
+
"检测到命令输入存在冲突:",
|
|
302
|
+
...lines,
|
|
303
|
+
"",
|
|
304
|
+
"请整理后重试;建议只保留一个开发文档路径和一个项目路径,必要时改用 `--docs` / `--repo` 明确指定。",
|
|
305
|
+
].join("\n");
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export function renderUserIntentLines(userIntent = {}) {
|
|
309
|
+
const lines = [];
|
|
310
|
+
const explicitRefs = Array.isArray(userIntent.explicitRefs) ? userIntent.explicitRefs : [];
|
|
311
|
+
const requestText = String(userIntent.requestText || "").trim();
|
|
312
|
+
|
|
313
|
+
if (explicitRefs.length) {
|
|
314
|
+
lines.push(...explicitRefs.map((ref) => formatPathRef(ref)));
|
|
315
|
+
}
|
|
316
|
+
if (requestText) {
|
|
317
|
+
lines.push(`附加要求:${requestText}`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return lines;
|
|
321
|
+
}
|
package/src/analyzer.mjs
CHANGED
|
@@ -6,7 +6,11 @@ import { loadPolicy, loadProjectConfig, scaffoldIfMissing, writeStateMarkdown, w
|
|
|
6
6
|
import { createContext } from "./context.mjs";
|
|
7
7
|
import { discoverWorkspace } from "./discovery.mjs";
|
|
8
8
|
import { readDocumentPackets } from "./doc_loader.mjs";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
rememberEngineSelection,
|
|
11
|
+
resolveEngineSelection,
|
|
12
|
+
} from "./engine_selection.mjs";
|
|
13
|
+
import { runEngineTask } from "./process.mjs";
|
|
10
14
|
import { buildAnalysisPrompt } from "./analyze_prompt.mjs";
|
|
11
15
|
|
|
12
16
|
function renderAnalysisState(context, backlog, analysis) {
|
|
@@ -45,33 +49,67 @@ function sanitizeTask(task) {
|
|
|
45
49
|
}
|
|
46
50
|
|
|
47
51
|
function normalizeAnalysisPayload(payload, docsEntries) {
|
|
52
|
+
const summary = {
|
|
53
|
+
currentState: String(payload?.summary?.currentState || "").trim() || "已完成接续分析。",
|
|
54
|
+
implemented: Array.isArray(payload?.summary?.implemented) ? payload.summary.implemented.map((item) => String(item || "").trim()).filter(Boolean) : [],
|
|
55
|
+
remaining: Array.isArray(payload?.summary?.remaining) ? payload.summary.remaining.map((item) => String(item || "").trim()).filter(Boolean) : [],
|
|
56
|
+
nextAction: String(payload?.summary?.nextAction || "").trim() || "查看下一任务。",
|
|
57
|
+
};
|
|
48
58
|
const tasks = Array.isArray(payload.tasks)
|
|
49
59
|
? payload.tasks.map((task) => sanitizeTask(task)).filter((task) => (
|
|
50
60
|
task.id && task.title && task.goal && task.acceptance.length
|
|
51
61
|
))
|
|
52
62
|
: [];
|
|
53
63
|
|
|
54
|
-
if (!tasks.length) {
|
|
55
|
-
throw new Error("
|
|
64
|
+
if (!tasks.length && summary.remaining.length) {
|
|
65
|
+
throw new Error("分析结果无效:仍存在剩余工作,但未生成可用任务。");
|
|
56
66
|
}
|
|
57
67
|
|
|
68
|
+
const requestInterpretation = payload?.requestInterpretation && typeof payload.requestInterpretation === "object"
|
|
69
|
+
? {
|
|
70
|
+
summary: String(payload.requestInterpretation.summary || "").trim(),
|
|
71
|
+
priorities: Array.isArray(payload.requestInterpretation.priorities)
|
|
72
|
+
? payload.requestInterpretation.priorities.map((item) => String(item || "").trim()).filter(Boolean)
|
|
73
|
+
: [],
|
|
74
|
+
cautions: Array.isArray(payload.requestInterpretation.cautions)
|
|
75
|
+
? payload.requestInterpretation.cautions.map((item) => String(item || "").trim()).filter(Boolean)
|
|
76
|
+
: [],
|
|
77
|
+
}
|
|
78
|
+
: null;
|
|
79
|
+
const repoDecision = payload?.repoDecision && typeof payload.repoDecision === "object"
|
|
80
|
+
? {
|
|
81
|
+
compatibility: ["compatible", "conflict", "uncertain"].includes(String(payload.repoDecision.compatibility || ""))
|
|
82
|
+
? String(payload.repoDecision.compatibility)
|
|
83
|
+
: "compatible",
|
|
84
|
+
action: ["continue_existing", "confirm_rebuild", "start_new"].includes(String(payload.repoDecision.action || ""))
|
|
85
|
+
? String(payload.repoDecision.action)
|
|
86
|
+
: "continue_existing",
|
|
87
|
+
reason: String(payload.repoDecision.reason || "").trim(),
|
|
88
|
+
}
|
|
89
|
+
: null;
|
|
90
|
+
|
|
58
91
|
return {
|
|
59
92
|
project: String(payload.project || "").trim() || "helloloop-project",
|
|
60
|
-
summary
|
|
61
|
-
currentState: String(payload?.summary?.currentState || "").trim() || "已完成接续分析。",
|
|
62
|
-
implemented: Array.isArray(payload?.summary?.implemented) ? payload.summary.implemented.map((item) => String(item || "").trim()).filter(Boolean) : [],
|
|
63
|
-
remaining: Array.isArray(payload?.summary?.remaining) ? payload.summary.remaining.map((item) => String(item || "").trim()).filter(Boolean) : [],
|
|
64
|
-
nextAction: String(payload?.summary?.nextAction || "").trim() || "查看下一任务。",
|
|
65
|
-
},
|
|
93
|
+
summary,
|
|
66
94
|
constraints: Array.isArray(payload.constraints)
|
|
67
95
|
? payload.constraints.map((item) => String(item || "").trim()).filter(Boolean)
|
|
68
96
|
: [],
|
|
97
|
+
requestInterpretation: requestInterpretation && (
|
|
98
|
+
requestInterpretation.summary
|
|
99
|
+
|| requestInterpretation.priorities.length
|
|
100
|
+
|| requestInterpretation.cautions.length
|
|
101
|
+
)
|
|
102
|
+
? requestInterpretation
|
|
103
|
+
: null,
|
|
104
|
+
repoDecision: repoDecision && repoDecision.reason
|
|
105
|
+
? repoDecision
|
|
106
|
+
: null,
|
|
69
107
|
tasks,
|
|
70
108
|
requiredDocs: docsEntries,
|
|
71
109
|
};
|
|
72
110
|
}
|
|
73
111
|
|
|
74
|
-
function buildAnalysisSummaryText(context, analysis, backlog) {
|
|
112
|
+
function buildAnalysisSummaryText(context, analysis, backlog, engineResolution) {
|
|
75
113
|
const summary = summarizeBacklog(backlog);
|
|
76
114
|
const nextTask = selectNextTask(backlog);
|
|
77
115
|
|
|
@@ -79,6 +117,7 @@ function buildAnalysisSummaryText(context, analysis, backlog) {
|
|
|
79
117
|
"HelloLoop 已完成接续分析。",
|
|
80
118
|
`项目仓库:${context.repoRoot}`,
|
|
81
119
|
`开发文档:${analysis.requiredDocs.join(", ")}`,
|
|
120
|
+
`执行引擎:${engineResolution?.displayName || "未记录"}`,
|
|
82
121
|
"",
|
|
83
122
|
"当前进度:",
|
|
84
123
|
analysis.summary.currentState,
|
|
@@ -92,29 +131,27 @@ function buildAnalysisSummaryText(context, analysis, backlog) {
|
|
|
92
131
|
].join("\n");
|
|
93
132
|
}
|
|
94
133
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
134
|
+
async function analyzeResolvedWorkspace(context, discovery, options = {}) {
|
|
135
|
+
scaffoldIfMissing(context);
|
|
136
|
+
const policy = loadPolicy(context);
|
|
137
|
+
let engineResolution = options.engineResolution?.ok
|
|
138
|
+
? options.engineResolution
|
|
139
|
+
: await resolveEngineSelection({
|
|
140
|
+
context,
|
|
141
|
+
policy,
|
|
142
|
+
options,
|
|
143
|
+
interactive: !options.yes,
|
|
144
|
+
});
|
|
145
|
+
if (!engineResolution.ok) {
|
|
105
146
|
return {
|
|
106
147
|
ok: false,
|
|
107
|
-
code:
|
|
108
|
-
summary:
|
|
148
|
+
code: engineResolution.code,
|
|
149
|
+
summary: engineResolution.message,
|
|
150
|
+
discovery,
|
|
151
|
+
engineResolution,
|
|
109
152
|
};
|
|
110
153
|
}
|
|
111
154
|
|
|
112
|
-
const context = createContext({
|
|
113
|
-
repoRoot: discovery.repoRoot,
|
|
114
|
-
configDirName: options.configDirName,
|
|
115
|
-
});
|
|
116
|
-
scaffoldIfMissing(context);
|
|
117
|
-
|
|
118
155
|
const existingProjectConfig = loadProjectConfig(context);
|
|
119
156
|
const existingStateText = readTextIfExists(context.stateFile, "");
|
|
120
157
|
const existingBacklogText = readTextIfExists(context.backlogFile, "");
|
|
@@ -125,46 +162,49 @@ export async function analyzeWorkspace(options = {}) {
|
|
|
125
162
|
|
|
126
163
|
const prompt = buildAnalysisPrompt({
|
|
127
164
|
repoRoot: context.repoRoot,
|
|
165
|
+
repoOriginallyExisted: discovery?.resolution?.repo?.exists !== false,
|
|
128
166
|
docsEntries: discovery.docsEntries,
|
|
129
167
|
docPackets,
|
|
130
168
|
existingStateText,
|
|
131
169
|
existingBacklogText,
|
|
132
170
|
existingProjectConstraints: existingProjectConfig.constraints,
|
|
171
|
+
userIntent: options.userIntent,
|
|
133
172
|
});
|
|
134
173
|
|
|
135
174
|
const runDir = path.join(context.runsDir, `${nowIso().replaceAll(":", "-").replaceAll(".", "-")}-analysis`);
|
|
136
|
-
const policy = loadPolicy(context);
|
|
137
175
|
const schemaFile = path.join(context.templatesDir, "analysis-output.schema.json");
|
|
138
|
-
|
|
176
|
+
let analysisResult = await runEngineTask({
|
|
177
|
+
engine: engineResolution.engine,
|
|
139
178
|
context,
|
|
140
179
|
prompt,
|
|
141
180
|
runDir,
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
sandbox: policy.codex.sandbox,
|
|
145
|
-
dangerouslyBypassSandbox: policy.codex.dangerouslyBypassSandbox,
|
|
181
|
+
policy,
|
|
182
|
+
executionMode: "analyze",
|
|
146
183
|
outputSchemaFile: schemaFile,
|
|
147
|
-
outputPrefix:
|
|
148
|
-
jsonOutput: false,
|
|
184
|
+
outputPrefix: `${engineResolution.engine}-analysis`,
|
|
149
185
|
skipGitRepoCheck: true,
|
|
150
186
|
});
|
|
151
187
|
|
|
152
|
-
if (!
|
|
188
|
+
if (!analysisResult.ok) {
|
|
153
189
|
return {
|
|
154
190
|
ok: false,
|
|
155
191
|
code: "analysis_failed",
|
|
156
|
-
summary:
|
|
192
|
+
summary: analysisResult.stderr || analysisResult.stdout || `${engineResolution.displayName} 接续分析失败。`,
|
|
193
|
+
engineResolution,
|
|
194
|
+
discovery,
|
|
157
195
|
};
|
|
158
196
|
}
|
|
159
197
|
|
|
160
198
|
let payload;
|
|
161
199
|
try {
|
|
162
|
-
payload = JSON.parse(
|
|
200
|
+
payload = JSON.parse(analysisResult.finalMessage);
|
|
163
201
|
} catch (error) {
|
|
164
202
|
return {
|
|
165
203
|
ok: false,
|
|
166
204
|
code: "invalid_analysis_json",
|
|
167
|
-
summary:
|
|
205
|
+
summary: `${engineResolution.displayName} 分析结果无法解析为 JSON:${String(error?.message || error || "")}`,
|
|
206
|
+
engineResolution,
|
|
207
|
+
discovery,
|
|
168
208
|
};
|
|
169
209
|
}
|
|
170
210
|
|
|
@@ -179,11 +219,14 @@ export async function analyzeWorkspace(options = {}) {
|
|
|
179
219
|
const projectConfig = {
|
|
180
220
|
requiredDocs: analysis.requiredDocs,
|
|
181
221
|
constraints: analysis.constraints.length ? analysis.constraints : existingProjectConfig.constraints,
|
|
222
|
+
defaultEngine: existingProjectConfig.defaultEngine,
|
|
223
|
+
lastSelectedEngine: engineResolution.engine,
|
|
182
224
|
planner: existingProjectConfig.planner,
|
|
183
225
|
};
|
|
184
226
|
|
|
185
227
|
writeJson(context.backlogFile, backlog);
|
|
186
228
|
writeJson(context.projectFile, projectConfig);
|
|
229
|
+
rememberEngineSelection(context, engineResolution, options);
|
|
187
230
|
writeStateMarkdown(context, renderAnalysisState(context, backlog, analysis));
|
|
188
231
|
writeStatus(context, {
|
|
189
232
|
ok: true,
|
|
@@ -194,15 +237,97 @@ export async function analyzeWorkspace(options = {}) {
|
|
|
194
237
|
summary: summarizeBacklog(backlog),
|
|
195
238
|
message: analysis.summary.currentState,
|
|
196
239
|
});
|
|
197
|
-
writeText(path.join(runDir, "analysis-summary.txt"), buildAnalysisSummaryText(context, analysis, backlog));
|
|
240
|
+
writeText(path.join(runDir, "analysis-summary.txt"), buildAnalysisSummaryText(context, analysis, backlog, engineResolution));
|
|
198
241
|
|
|
199
242
|
return {
|
|
200
243
|
ok: true,
|
|
201
244
|
code: "analyzed",
|
|
202
245
|
context,
|
|
203
246
|
runDir,
|
|
247
|
+
engineResolution,
|
|
204
248
|
analysis,
|
|
205
249
|
backlog,
|
|
206
|
-
summary: buildAnalysisSummaryText(context, analysis, backlog),
|
|
250
|
+
summary: buildAnalysisSummaryText(context, analysis, backlog, engineResolution),
|
|
251
|
+
discovery,
|
|
207
252
|
};
|
|
208
253
|
}
|
|
254
|
+
|
|
255
|
+
function buildCurrentWorkspaceDiscovery(context, docsEntries) {
|
|
256
|
+
return {
|
|
257
|
+
ok: true,
|
|
258
|
+
repoRoot: context.repoRoot,
|
|
259
|
+
docsEntries,
|
|
260
|
+
resolvedDocs: docsEntries,
|
|
261
|
+
resolution: {
|
|
262
|
+
repo: {
|
|
263
|
+
source: "current_repo",
|
|
264
|
+
sourceLabel: "当前项目",
|
|
265
|
+
confidence: "high",
|
|
266
|
+
confidenceLabel: "高",
|
|
267
|
+
path: context.repoRoot,
|
|
268
|
+
exists: true,
|
|
269
|
+
basis: [
|
|
270
|
+
"已在当前项目基础上执行主线终态复核。",
|
|
271
|
+
],
|
|
272
|
+
},
|
|
273
|
+
docs: {
|
|
274
|
+
source: "existing_state",
|
|
275
|
+
sourceLabel: "已有 .helloloop 配置",
|
|
276
|
+
confidence: "high",
|
|
277
|
+
confidenceLabel: "高",
|
|
278
|
+
entries: docsEntries,
|
|
279
|
+
basis: [
|
|
280
|
+
"已复用 `.helloloop/project.json` 中记录的 requiredDocs。",
|
|
281
|
+
],
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export async function reanalyzeCurrentWorkspace(context, options = {}) {
|
|
288
|
+
const existingProjectConfig = loadProjectConfig(context);
|
|
289
|
+
const docsEntries = Array.isArray(options.requiredDocs) && options.requiredDocs.length
|
|
290
|
+
? options.requiredDocs
|
|
291
|
+
: existingProjectConfig.requiredDocs;
|
|
292
|
+
|
|
293
|
+
if (!docsEntries.length) {
|
|
294
|
+
return {
|
|
295
|
+
ok: false,
|
|
296
|
+
code: "missing_docs",
|
|
297
|
+
summary: "当前 `.helloloop/project.json` 未记录 requiredDocs,无法执行主线终态复核。",
|
|
298
|
+
discovery: null,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return analyzeResolvedWorkspace(
|
|
303
|
+
context,
|
|
304
|
+
buildCurrentWorkspaceDiscovery(context, docsEntries),
|
|
305
|
+
options,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export async function analyzeWorkspace(options = {}) {
|
|
310
|
+
const discovery = discoverWorkspace({
|
|
311
|
+
cwd: options.cwd,
|
|
312
|
+
inputPath: options.inputPath,
|
|
313
|
+
repoRoot: options.repoRoot,
|
|
314
|
+
docsPath: options.docsPath,
|
|
315
|
+
configDirName: options.configDirName,
|
|
316
|
+
allowNewRepoRoot: options.allowNewRepoRoot,
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
if (!discovery.ok) {
|
|
320
|
+
return {
|
|
321
|
+
ok: false,
|
|
322
|
+
code: discovery.code,
|
|
323
|
+
summary: discovery.message,
|
|
324
|
+
discovery,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const context = createContext({
|
|
329
|
+
repoRoot: discovery.repoRoot,
|
|
330
|
+
configDirName: options.configDirName,
|
|
331
|
+
});
|
|
332
|
+
return analyzeResolvedWorkspace(context, discovery, options);
|
|
333
|
+
}
|