helloloop 0.3.1 → 0.7.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/plugin.json +1 -1
- package/.codex-plugin/plugin.json +4 -4
- package/README.md +194 -81
- package/hosts/claude/marketplace/plugins/helloloop/.claude-plugin/plugin.json +1 -1
- package/hosts/claude/marketplace/plugins/helloloop/commands/helloloop.md +17 -13
- package/hosts/claude/marketplace/plugins/helloloop/skills/helloloop/SKILL.md +12 -5
- package/hosts/gemini/extension/GEMINI.md +14 -7
- package/hosts/gemini/extension/commands/helloloop.toml +17 -12
- package/hosts/gemini/extension/gemini-extension.json +1 -1
- package/package.json +2 -2
- package/skills/helloloop/SKILL.md +18 -7
- package/src/analyze_confirmation.mjs +29 -5
- package/src/analyze_prompt.mjs +5 -1
- package/src/analyze_user_input.mjs +20 -2
- package/src/analyzer.mjs +130 -43
- package/src/cli.mjs +32 -492
- 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 +11 -14
- package/src/completion_review.mjs +243 -0
- package/src/config.mjs +51 -0
- package/src/discovery.mjs +21 -2
- package/src/discovery_prompt.mjs +2 -27
- package/src/email_notification.mjs +343 -0
- package/src/engine_metadata.mjs +79 -0
- package/src/engine_process_support.mjs +294 -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 +104 -0
- package/src/global_config.mjs +21 -0
- package/src/guardrails.mjs +15 -4
- package/src/install.mjs +6 -405
- 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 +138 -0
- package/src/process.mjs +567 -100
- package/src/prompt.mjs +9 -5
- package/src/prompt_session.mjs +40 -0
- package/src/runner.mjs +3 -341
- package/src/runner_execute_task.mjs +255 -0
- package/src/runner_execution_support.mjs +146 -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 +302 -0
- package/src/shell_invocation.mjs +16 -0
- package/templates/analysis-output.schema.json +0 -1
- package/templates/policy.template.json +25 -0
- package/templates/project.template.json +2 -0
- package/templates/task-review-output.schema.json +70 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import { formatList, tailText } from "./common.mjs";
|
|
4
|
+
import { runEngineTask } from "./process.mjs";
|
|
5
|
+
|
|
6
|
+
function section(title, content) {
|
|
7
|
+
if (!content || !String(content).trim()) {
|
|
8
|
+
return "";
|
|
9
|
+
}
|
|
10
|
+
return `## ${title}\n${String(content).trim()}\n`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function listSection(title, items) {
|
|
14
|
+
if (!Array.isArray(items) || !items.length) {
|
|
15
|
+
return "";
|
|
16
|
+
}
|
|
17
|
+
return section(title, formatList(items));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function normalizeDocEntry(doc) {
|
|
21
|
+
return String(doc || "").trim().replaceAll("\\", "/");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function normalizeDocList(items) {
|
|
25
|
+
const result = [];
|
|
26
|
+
const seen = new Set();
|
|
27
|
+
|
|
28
|
+
for (const item of items || []) {
|
|
29
|
+
const normalized = normalizeDocEntry(item);
|
|
30
|
+
if (!normalized) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const key = normalized.toLowerCase();
|
|
35
|
+
if (seen.has(key)) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
seen.add(key);
|
|
39
|
+
result.push(normalized);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function renderVerifyResultLines(verifyResult) {
|
|
46
|
+
if (!verifyResult?.results?.length) {
|
|
47
|
+
return [
|
|
48
|
+
"- 未执行额外验证命令",
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return verifyResult.results.map((item) => {
|
|
53
|
+
const stdoutTail = tailText(item.stdout, 6) || "无";
|
|
54
|
+
const stderrTail = tailText(item.stderr, 6) || "无";
|
|
55
|
+
return [
|
|
56
|
+
`- ${item.ok ? "通过" : "失败"}:${item.command}`,
|
|
57
|
+
` stdout: ${stdoutTail}`,
|
|
58
|
+
` stderr: ${stderrTail}`,
|
|
59
|
+
].join("\n");
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function buildTaskReviewPrompt({
|
|
64
|
+
task,
|
|
65
|
+
requiredDocs = [],
|
|
66
|
+
constraints = [],
|
|
67
|
+
repoStateText = "",
|
|
68
|
+
engineFinalMessage = "",
|
|
69
|
+
verifyResult = null,
|
|
70
|
+
}) {
|
|
71
|
+
const allDocs = normalizeDocList([
|
|
72
|
+
...requiredDocs,
|
|
73
|
+
...(task.docs || []),
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
return [
|
|
77
|
+
"你要做的是“任务完成复核”,目标是判断当前任务是否真的已经完成。",
|
|
78
|
+
"不要相信执行代理口头说“已完成”;必须直接检查仓库当前代码、测试和产物。",
|
|
79
|
+
"如果只是做到一半、只改了部分文件、或者验收条件仍缺项,都不能判定为完成。",
|
|
80
|
+
"",
|
|
81
|
+
section("当前任务", [
|
|
82
|
+
`- 标题:${task.title}`,
|
|
83
|
+
`- 编号:${task.id}`,
|
|
84
|
+
`- 目标:${task.goal || "按文档完成当前工作包。"}`,
|
|
85
|
+
`- 风险:${task.risk || "low"}`,
|
|
86
|
+
].join("\n")),
|
|
87
|
+
listSection("开发文档", allDocs),
|
|
88
|
+
listSection("涉及路径", task.paths || []),
|
|
89
|
+
listSection("验收条件", task.acceptance || []),
|
|
90
|
+
constraints.length ? listSection("项目约束", constraints) : "",
|
|
91
|
+
repoStateText ? section("当前仓库状态摘要", repoStateText) : "",
|
|
92
|
+
engineFinalMessage
|
|
93
|
+
? section("执行代理最后输出", tailText(engineFinalMessage, 20))
|
|
94
|
+
: "",
|
|
95
|
+
section("验证结果", renderVerifyResultLines(verifyResult).join("\n")),
|
|
96
|
+
section("判定规则", [
|
|
97
|
+
"1. 只有当所有验收条件都有明确仓库证据支撑时,才能输出 `complete`。",
|
|
98
|
+
"2. 只要有任一验收条件未满足、证据不足、或只能部分成立,就输出 `incomplete`。",
|
|
99
|
+
"3. 只有外部权限、环境损坏、文档缺口、不可获得依赖等真正硬阻塞,才允许输出 `blocked`。",
|
|
100
|
+
"4. “代理提前停止”“只完成一部分”“建议下次继续”都属于 `incomplete`,不是 `blocked`。",
|
|
101
|
+
"5. 不要给泛泛建议,`missing` 和 `nextAction` 必须直接指出还差什么。",
|
|
102
|
+
].join("\n")),
|
|
103
|
+
section("输出要求", [
|
|
104
|
+
"1. 严格输出 JSON,不要带 Markdown 代码块。",
|
|
105
|
+
"2. `verdict` 只能是 `complete`、`incomplete`、`blocked`。",
|
|
106
|
+
"3. `acceptanceChecks` 必须逐条覆盖本任务所有验收条件。",
|
|
107
|
+
"4. `acceptanceChecks[].status` 只能是 `met`、`not_met`、`uncertain`。",
|
|
108
|
+
"5. `acceptanceChecks[].evidence` 必须写具体证据或缺口,不能只写“已完成”。",
|
|
109
|
+
"6. 若 `verdict=complete`,则所有 `acceptanceChecks[].status` 必须都是 `met`,且 `missing` 为空。",
|
|
110
|
+
"7. 若 `verdict=blocked`,`blockerReason` 必须明确写出硬阻塞原因。",
|
|
111
|
+
].join("\n")),
|
|
112
|
+
].filter(Boolean).join("\n");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function normalizeAcceptanceCheck(check) {
|
|
116
|
+
return {
|
|
117
|
+
item: String(check?.item || "").trim(),
|
|
118
|
+
status: ["met", "not_met", "uncertain"].includes(String(check?.status || ""))
|
|
119
|
+
? String(check.status)
|
|
120
|
+
: "uncertain",
|
|
121
|
+
evidence: String(check?.evidence || "").trim() || "未提供可用证据。",
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function normalizeTaskReviewPayload(payload, task) {
|
|
126
|
+
const requestedAcceptance = Array.isArray(task?.acceptance)
|
|
127
|
+
? task.acceptance.map((item) => String(item || "").trim()).filter(Boolean)
|
|
128
|
+
: [];
|
|
129
|
+
const receivedChecks = Array.isArray(payload?.acceptanceChecks)
|
|
130
|
+
? payload.acceptanceChecks.map((item) => normalizeAcceptanceCheck(item)).filter((item) => item.item)
|
|
131
|
+
: [];
|
|
132
|
+
|
|
133
|
+
const normalizedChecks = requestedAcceptance.map((item) => {
|
|
134
|
+
const matched = receivedChecks.find((check) => check.item === item);
|
|
135
|
+
return matched || {
|
|
136
|
+
item,
|
|
137
|
+
status: "uncertain",
|
|
138
|
+
evidence: "复核结果未覆盖该验收条件。",
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const missing = Array.isArray(payload?.missing)
|
|
143
|
+
? payload.missing.map((item) => String(item || "").trim()).filter(Boolean)
|
|
144
|
+
: [];
|
|
145
|
+
const blockerReason = String(payload?.blockerReason || "").trim();
|
|
146
|
+
const requestedVerdict = ["complete", "incomplete", "blocked"].includes(String(payload?.verdict || ""))
|
|
147
|
+
? String(payload.verdict)
|
|
148
|
+
: "incomplete";
|
|
149
|
+
const hasUnmetAcceptance = normalizedChecks.some((item) => item.status !== "met");
|
|
150
|
+
const verdict = requestedVerdict === "complete" && (hasUnmetAcceptance || missing.length)
|
|
151
|
+
? "incomplete"
|
|
152
|
+
: requestedVerdict;
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
verdict,
|
|
156
|
+
summary: String(payload?.summary || "").trim() || "任务完成复核已结束。",
|
|
157
|
+
acceptanceChecks: normalizedChecks,
|
|
158
|
+
missing,
|
|
159
|
+
blockerReason,
|
|
160
|
+
nextAction: String(payload?.nextAction || "").trim() || "请根据缺口继续完成当前任务。",
|
|
161
|
+
isComplete: verdict === "complete" && !hasUnmetAcceptance && missing.length === 0,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function renderTaskReviewSummary(review) {
|
|
166
|
+
const acceptanceLines = review.acceptanceChecks.map((item) => (
|
|
167
|
+
`- ${item.status === "met" ? "已满足" : (item.status === "not_met" ? "未满足" : "待确认")}:${item.item};${item.evidence}`
|
|
168
|
+
));
|
|
169
|
+
|
|
170
|
+
return [
|
|
171
|
+
`任务复核结论:${review.summary}`,
|
|
172
|
+
...acceptanceLines,
|
|
173
|
+
...(review.missing.length
|
|
174
|
+
? review.missing.map((item) => `- 剩余缺口:${item}`)
|
|
175
|
+
: []),
|
|
176
|
+
review.blockerReason ? `- 硬阻塞:${review.blockerReason}` : "",
|
|
177
|
+
`- 下一动作:${review.nextAction}`,
|
|
178
|
+
].filter(Boolean).join("\n");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export async function reviewTaskCompletion({
|
|
182
|
+
engine,
|
|
183
|
+
context,
|
|
184
|
+
task,
|
|
185
|
+
requiredDocs = [],
|
|
186
|
+
constraints = [],
|
|
187
|
+
repoStateText = "",
|
|
188
|
+
engineFinalMessage = "",
|
|
189
|
+
verifyResult = null,
|
|
190
|
+
runDir,
|
|
191
|
+
policy = {},
|
|
192
|
+
}) {
|
|
193
|
+
const prompt = buildTaskReviewPrompt({
|
|
194
|
+
task,
|
|
195
|
+
requiredDocs,
|
|
196
|
+
constraints,
|
|
197
|
+
repoStateText,
|
|
198
|
+
engineFinalMessage,
|
|
199
|
+
verifyResult,
|
|
200
|
+
});
|
|
201
|
+
const schemaFile = path.join(context.templatesDir, "task-review-output.schema.json");
|
|
202
|
+
const reviewResult = await runEngineTask({
|
|
203
|
+
engine,
|
|
204
|
+
context,
|
|
205
|
+
prompt,
|
|
206
|
+
runDir,
|
|
207
|
+
policy,
|
|
208
|
+
executionMode: "analyze",
|
|
209
|
+
outputSchemaFile: schemaFile,
|
|
210
|
+
outputPrefix: `${engine}-task-review`,
|
|
211
|
+
skipGitRepoCheck: true,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
if (!reviewResult.ok) {
|
|
215
|
+
return {
|
|
216
|
+
ok: false,
|
|
217
|
+
code: "task_review_failed",
|
|
218
|
+
summary: reviewResult.stderr || reviewResult.stdout || "任务完成复核失败。",
|
|
219
|
+
raw: reviewResult,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
let payload;
|
|
224
|
+
try {
|
|
225
|
+
payload = JSON.parse(reviewResult.finalMessage);
|
|
226
|
+
} catch (error) {
|
|
227
|
+
return {
|
|
228
|
+
ok: false,
|
|
229
|
+
code: "invalid_task_review_json",
|
|
230
|
+
summary: `任务完成复核结果无法解析为 JSON:${String(error?.message || error || "")}`,
|
|
231
|
+
raw: reviewResult,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const review = normalizeTaskReviewPayload(payload, task);
|
|
236
|
+
return {
|
|
237
|
+
ok: true,
|
|
238
|
+
code: "task_reviewed",
|
|
239
|
+
review,
|
|
240
|
+
summary: renderTaskReviewSummary(review),
|
|
241
|
+
raw: reviewResult,
|
|
242
|
+
};
|
|
243
|
+
}
|
package/src/config.mjs
CHANGED
|
@@ -15,8 +15,19 @@ const defaultPolicy = {
|
|
|
15
15
|
maxLoopTasks: 4,
|
|
16
16
|
maxTaskAttempts: 2,
|
|
17
17
|
maxTaskStrategies: 4,
|
|
18
|
+
maxReanalysisPasses: 3,
|
|
18
19
|
stopOnFailure: false,
|
|
19
20
|
stopOnHighRisk: true,
|
|
21
|
+
runtimeRecovery: {
|
|
22
|
+
enabled: true,
|
|
23
|
+
heartbeatIntervalSeconds: 60,
|
|
24
|
+
stallWarningSeconds: 900,
|
|
25
|
+
maxIdleSeconds: 2700,
|
|
26
|
+
killGraceSeconds: 10,
|
|
27
|
+
healthProbeTimeoutSeconds: 120,
|
|
28
|
+
hardRetryDelaysSeconds: [900, 900, 900, 900, 900],
|
|
29
|
+
softRetryDelaysSeconds: [900, 900, 900, 900, 900, 1800, 1800, 3600, 5400, 7200, 9000, 10800],
|
|
30
|
+
},
|
|
20
31
|
codex: {
|
|
21
32
|
model: "",
|
|
22
33
|
executable: "",
|
|
@@ -24,6 +35,20 @@ const defaultPolicy = {
|
|
|
24
35
|
dangerouslyBypassSandbox: false,
|
|
25
36
|
jsonOutput: true,
|
|
26
37
|
},
|
|
38
|
+
claude: {
|
|
39
|
+
model: "",
|
|
40
|
+
executable: "",
|
|
41
|
+
permissionMode: "bypassPermissions",
|
|
42
|
+
analysisPermissionMode: "plan",
|
|
43
|
+
outputFormat: "text",
|
|
44
|
+
},
|
|
45
|
+
gemini: {
|
|
46
|
+
model: "",
|
|
47
|
+
executable: "",
|
|
48
|
+
approvalMode: "yolo",
|
|
49
|
+
analysisApprovalMode: "plan",
|
|
50
|
+
outputFormat: "text",
|
|
51
|
+
},
|
|
27
52
|
};
|
|
28
53
|
|
|
29
54
|
const defaultPlanner = {
|
|
@@ -46,6 +71,24 @@ export function loadPolicy(context) {
|
|
|
46
71
|
...defaultPolicy.codex,
|
|
47
72
|
...(policy.codex || {}),
|
|
48
73
|
};
|
|
74
|
+
policy.claude = {
|
|
75
|
+
...defaultPolicy.claude,
|
|
76
|
+
...(policy.claude || {}),
|
|
77
|
+
};
|
|
78
|
+
policy.gemini = {
|
|
79
|
+
...defaultPolicy.gemini,
|
|
80
|
+
...(policy.gemini || {}),
|
|
81
|
+
};
|
|
82
|
+
policy.runtimeRecovery = {
|
|
83
|
+
...defaultPolicy.runtimeRecovery,
|
|
84
|
+
...(policy.runtimeRecovery || {}),
|
|
85
|
+
hardRetryDelaysSeconds: Array.isArray(policy?.runtimeRecovery?.hardRetryDelaysSeconds)
|
|
86
|
+
? policy.runtimeRecovery.hardRetryDelaysSeconds
|
|
87
|
+
: defaultPolicy.runtimeRecovery.hardRetryDelaysSeconds,
|
|
88
|
+
softRetryDelaysSeconds: Array.isArray(policy?.runtimeRecovery?.softRetryDelaysSeconds)
|
|
89
|
+
? policy.runtimeRecovery.softRetryDelaysSeconds
|
|
90
|
+
: defaultPolicy.runtimeRecovery.softRetryDelaysSeconds,
|
|
91
|
+
};
|
|
49
92
|
return policy;
|
|
50
93
|
}
|
|
51
94
|
|
|
@@ -54,6 +97,8 @@ export function loadProjectConfig(context) {
|
|
|
54
97
|
return {
|
|
55
98
|
requiredDocs: [],
|
|
56
99
|
constraints: [],
|
|
100
|
+
defaultEngine: "",
|
|
101
|
+
lastSelectedEngine: "",
|
|
57
102
|
planner: defaultPlanner,
|
|
58
103
|
};
|
|
59
104
|
}
|
|
@@ -62,6 +107,8 @@ export function loadProjectConfig(context) {
|
|
|
62
107
|
return {
|
|
63
108
|
requiredDocs: Array.isArray(config.requiredDocs) ? config.requiredDocs : [],
|
|
64
109
|
constraints: Array.isArray(config.constraints) ? config.constraints : [],
|
|
110
|
+
defaultEngine: typeof config.defaultEngine === "string" ? config.defaultEngine : "",
|
|
111
|
+
lastSelectedEngine: typeof config.lastSelectedEngine === "string" ? config.lastSelectedEngine : "",
|
|
65
112
|
planner: {
|
|
66
113
|
...defaultPlanner,
|
|
67
114
|
...(config.planner || {}),
|
|
@@ -87,6 +134,10 @@ export function saveBacklog(context, backlog) {
|
|
|
87
134
|
});
|
|
88
135
|
}
|
|
89
136
|
|
|
137
|
+
export function saveProjectConfig(context, config) {
|
|
138
|
+
writeJson(context.projectFile, config);
|
|
139
|
+
}
|
|
140
|
+
|
|
90
141
|
export function loadVerifyCommands(context) {
|
|
91
142
|
const raw = readTextIfExists(context.repoVerifyFile, "");
|
|
92
143
|
return raw
|
package/src/discovery.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import os from "node:os";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
|
|
3
4
|
import { expandDocumentEntries } from "./doc_loader.mjs";
|
|
@@ -49,6 +50,11 @@ const DOCS_SOURCE_META = {
|
|
|
49
50
|
repo_docs: { label: "仓库 docs 目录", confidence: "medium" },
|
|
50
51
|
};
|
|
51
52
|
|
|
53
|
+
function isImplicitHomeDirectory(targetPath) {
|
|
54
|
+
return Boolean(targetPath)
|
|
55
|
+
&& path.resolve(targetPath) === path.resolve(os.homedir());
|
|
56
|
+
}
|
|
57
|
+
|
|
52
58
|
function pushBasis(basis, message) {
|
|
53
59
|
if (message && !basis.includes(message)) {
|
|
54
60
|
basis.push(message);
|
|
@@ -127,6 +133,10 @@ export function discoverWorkspace(options = {}) {
|
|
|
127
133
|
const explicitRepoRoot = options.repoRoot ? resolveAbsolute(options.repoRoot, cwd) : "";
|
|
128
134
|
const explicitDocsPath = options.docsPath ? resolveAbsolute(options.docsPath, cwd) : "";
|
|
129
135
|
const explicitInputPath = options.inputPath ? resolveAbsolute(options.inputPath, cwd) : "";
|
|
136
|
+
const treatCwdAsHomeWorkspace = !explicitRepoRoot
|
|
137
|
+
&& !explicitDocsPath
|
|
138
|
+
&& !explicitInputPath
|
|
139
|
+
&& isImplicitHomeDirectory(cwd);
|
|
130
140
|
|
|
131
141
|
if (explicitRepoRoot && !pathExists(explicitRepoRoot)) {
|
|
132
142
|
if (!allowNewRepoRoot) {
|
|
@@ -196,13 +206,15 @@ export function discoverWorkspace(options = {}) {
|
|
|
196
206
|
}
|
|
197
207
|
|
|
198
208
|
if (!repoRoot && !docsEntries.length) {
|
|
199
|
-
const cwdRepoRoot = findRepoRootFromPath(cwd);
|
|
209
|
+
const cwdRepoRoot = treatCwdAsHomeWorkspace ? "" : findRepoRootFromPath(cwd);
|
|
200
210
|
if (cwdRepoRoot) {
|
|
201
211
|
repoRoot = cwdRepoRoot;
|
|
202
212
|
repoSource = "cwd_repo";
|
|
203
213
|
pushBasis(repoBasis, "当前终端目录已经位于一个项目仓库内。");
|
|
204
214
|
} else {
|
|
205
|
-
const cwdClassified =
|
|
215
|
+
const cwdClassified = treatCwdAsHomeWorkspace
|
|
216
|
+
? { kind: "workspace", absolutePath: cwd }
|
|
217
|
+
: classifyExplicitPath(cwd);
|
|
206
218
|
if (cwdClassified.kind === "docs") {
|
|
207
219
|
docsEntries = [cwdClassified.absolutePath];
|
|
208
220
|
docsSource = "cwd_docs";
|
|
@@ -373,6 +385,13 @@ export function resolveRepoRoot(options = {}) {
|
|
|
373
385
|
return { ok: false, message: renderMissingRepoMessage([classified.absolutePath], inferred.candidates) };
|
|
374
386
|
}
|
|
375
387
|
|
|
388
|
+
if (!explicitRepoRoot && !explicitInputPath && isImplicitHomeDirectory(cwd)) {
|
|
389
|
+
return {
|
|
390
|
+
ok: false,
|
|
391
|
+
message: "当前目录看起来是用户主目录;HelloLoop 不会把主目录自动当作项目仓库。请切到项目目录,或传入一个项目路径/开发文档路径。",
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
376
395
|
const repoRoot = findRepoRootFromPath(cwd);
|
|
377
396
|
if (repoRoot) {
|
|
378
397
|
return { ok: true, repoRoot };
|
package/src/discovery_prompt.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { createInterface } from "node:readline/promises";
|
|
4
3
|
|
|
5
4
|
import {
|
|
6
5
|
listDocFilesInDirectory,
|
|
@@ -8,38 +7,14 @@ import {
|
|
|
8
7
|
pathExists,
|
|
9
8
|
resolveAbsolute,
|
|
10
9
|
} from "./discovery_paths.mjs";
|
|
10
|
+
import { createPromptSession } from "./prompt_session.mjs";
|
|
11
11
|
|
|
12
12
|
function toDisplayPath(targetPath) {
|
|
13
13
|
return String(targetPath || "").replaceAll("\\", "/");
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export function createDiscoveryPromptSession() {
|
|
17
|
-
|
|
18
|
-
const readline = createInterface({
|
|
19
|
-
input: process.stdin,
|
|
20
|
-
output: process.stdout,
|
|
21
|
-
});
|
|
22
|
-
return {
|
|
23
|
-
async question(promptText) {
|
|
24
|
-
return readline.question(promptText);
|
|
25
|
-
},
|
|
26
|
-
close() {
|
|
27
|
-
readline.close();
|
|
28
|
-
},
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const bufferedAnswers = fs.readFileSync(0, "utf8").split(/\r?\n/);
|
|
33
|
-
let answerIndex = 0;
|
|
34
|
-
return {
|
|
35
|
-
async question(promptText) {
|
|
36
|
-
process.stdout.write(promptText);
|
|
37
|
-
const answer = bufferedAnswers[answerIndex] ?? "";
|
|
38
|
-
answerIndex += 1;
|
|
39
|
-
return answer;
|
|
40
|
-
},
|
|
41
|
-
close() {},
|
|
42
|
-
};
|
|
17
|
+
return createPromptSession();
|
|
43
18
|
}
|
|
44
19
|
|
|
45
20
|
function summarizeList(items, options = {}) {
|