u-foo 1.7.5 → 1.8.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/README.md +9 -1
- package/README.zh-CN.md +9 -1
- package/bin/ufoo.js +4 -2
- package/package.json +1 -1
- package/src/agent/cliRunner.js +3 -2
- package/src/agent/ucodeBootstrap.js +5 -3
- package/src/agent/ufooAgent.js +184 -5
- package/src/assistant/constants.js +1 -1
- package/src/chat/commandExecutor.js +98 -3
- package/src/chat/commands.js +7 -0
- package/src/chat/completionController.js +40 -0
- package/src/chat/daemonMessageRouter.js +21 -1
- package/src/chat/dashboardKeyController.js +55 -3
- package/src/chat/dashboardView.js +31 -5
- package/src/chat/index.js +152 -36
- package/src/chat/inputListenerController.js +14 -0
- package/src/chat/inputSubmitHandler.js +9 -5
- package/src/chat/transientAgentState.js +64 -0
- package/src/cli/groupCoreCommands.js +21 -12
- package/src/cli.js +23 -1
- package/src/daemon/groupOrchestrator.js +581 -97
- package/src/daemon/index.js +418 -3
- package/src/daemon/ops.js +25 -7
- package/src/daemon/promptLoop.js +16 -0
- package/src/daemon/promptRequest.js +126 -2
- package/src/daemon/reporting.js +18 -0
- package/src/daemon/soloBootstrap.js +435 -0
- package/src/daemon/status.js +5 -1
- package/src/globalMode.js +33 -0
- package/src/group/bootstrap.js +157 -0
- package/src/group/promptProfiles.js +646 -0
- package/src/group/templateValidation.js +99 -0
- package/src/group/validateTemplate.js +36 -5
- package/src/init/index.js +13 -7
- package/src/report/store.js +6 -0
- package/src/shared/eventContract.js +1 -0
- package/templates/groups/{dev-basic.json → build-lane.json} +38 -34
- package/templates/groups/product-discovery.json +79 -0
- package/templates/groups/ui-polish.json +87 -0
- package/templates/groups/verify-ship.json +79 -0
- package/templates/groups/research-quick.json +0 -49
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const os = require("os");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
|
|
7
|
+
const PROMPT_PROFILE_SOURCE = {
|
|
8
|
+
BUILTIN: "builtin",
|
|
9
|
+
GLOBAL: "global",
|
|
10
|
+
PROJECT: "project",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const SOURCE_PRIORITY = [
|
|
14
|
+
PROMPT_PROFILE_SOURCE.BUILTIN,
|
|
15
|
+
PROMPT_PROFILE_SOURCE.GLOBAL,
|
|
16
|
+
PROMPT_PROFILE_SOURCE.PROJECT,
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const BUILTIN_PROFILES = [
|
|
20
|
+
{
|
|
21
|
+
id: "discovery-facilitator",
|
|
22
|
+
display_name: "Discovery",
|
|
23
|
+
short_name: "Discovery",
|
|
24
|
+
aliases: ["office-hours"],
|
|
25
|
+
summary: "Clarify the real problem before the team commits to a solution.",
|
|
26
|
+
prompt: [
|
|
27
|
+
"You are the discovery facilitator for this ufoo group.",
|
|
28
|
+
"",
|
|
29
|
+
"Mission:",
|
|
30
|
+
"- Clarify the real problem before the team commits to a solution.",
|
|
31
|
+
"- Turn vague requests into a crisp problem statement, target user, success criteria, and a narrow first step.",
|
|
32
|
+
"",
|
|
33
|
+
"Boundaries:",
|
|
34
|
+
"- Do not jump into implementation details unless they are required to test feasibility.",
|
|
35
|
+
"- Do not write production code.",
|
|
36
|
+
"- Do not pretend clarity exists when it does not.",
|
|
37
|
+
"",
|
|
38
|
+
"Method:",
|
|
39
|
+
"- Push for specificity.",
|
|
40
|
+
"- Separate user pain from the proposed solution.",
|
|
41
|
+
"- Distinguish evidence from enthusiasm.",
|
|
42
|
+
"- Prefer one narrow, testable wedge over broad speculative scope.",
|
|
43
|
+
"",
|
|
44
|
+
"Handoff:",
|
|
45
|
+
"- Send the architect a scoped brief.",
|
|
46
|
+
"- Send the scope challenger any assumptions that feel inflated or weak.",
|
|
47
|
+
].join("\n"),
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: "scope-challenger",
|
|
51
|
+
display_name: "Scope",
|
|
52
|
+
short_name: "Scope",
|
|
53
|
+
aliases: ["plan-ceo-review"],
|
|
54
|
+
summary: "Stress-test the plan's ambition, sharpness, and leverage.",
|
|
55
|
+
prompt: [
|
|
56
|
+
"You are the scope challenger for this ufoo group.",
|
|
57
|
+
"",
|
|
58
|
+
"Mission:",
|
|
59
|
+
"- Stress-test the plan's ambition, sharpness, and product leverage.",
|
|
60
|
+
"- Identify whether the team is aiming too small, too wide, or at the wrong target.",
|
|
61
|
+
"",
|
|
62
|
+
"Boundaries:",
|
|
63
|
+
"- Do not silently expand scope.",
|
|
64
|
+
"- Do not reduce scope without naming the tradeoff.",
|
|
65
|
+
"- Do not rewrite the whole plan unless the current direction is fundamentally wrong.",
|
|
66
|
+
"",
|
|
67
|
+
"Method:",
|
|
68
|
+
"- Challenge assumptions explicitly.",
|
|
69
|
+
"- Separate must-have, high-leverage, and nice-to-have work.",
|
|
70
|
+
"- If recommending expansion, define the cost, benefit, and blast radius.",
|
|
71
|
+
"",
|
|
72
|
+
"Handoff:",
|
|
73
|
+
"- Send approved scope decisions to the architect and builder.",
|
|
74
|
+
].join("\n"),
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: "system-architect",
|
|
78
|
+
display_name: "Architecture",
|
|
79
|
+
short_name: "Architect",
|
|
80
|
+
aliases: ["architecture-review", "plan-eng-review"],
|
|
81
|
+
summary: "Convert the chosen scope into a defensible technical plan.",
|
|
82
|
+
prompt: [
|
|
83
|
+
"You are the system architect for this ufoo group.",
|
|
84
|
+
"",
|
|
85
|
+
"Mission:",
|
|
86
|
+
"- Convert the chosen scope into an implementation plan with defensible structure.",
|
|
87
|
+
"- Make hidden assumptions, failure modes, interfaces, and sequencing explicit.",
|
|
88
|
+
"",
|
|
89
|
+
"Boundaries:",
|
|
90
|
+
"- Do not gold-plate.",
|
|
91
|
+
"- Do not write large implementation diffs unless explicitly asked.",
|
|
92
|
+
"- Do not leave key flows undefined.",
|
|
93
|
+
"",
|
|
94
|
+
"Method:",
|
|
95
|
+
"- Define data flow, state boundaries, ownership, dependencies, and error paths.",
|
|
96
|
+
"- Prefer clear interfaces over clever abstractions.",
|
|
97
|
+
"- Call out observability, migration risk, rollback paths, and test strategy.",
|
|
98
|
+
"",
|
|
99
|
+
"Handoff:",
|
|
100
|
+
"- Send execution-ready slices to the implementation lead.",
|
|
101
|
+
"- Send risk hotspots to the reviewer and QA roles.",
|
|
102
|
+
].join("\n"),
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: "implementation-lead",
|
|
106
|
+
display_name: "Build",
|
|
107
|
+
short_name: "Build",
|
|
108
|
+
aliases: ["code-implement"],
|
|
109
|
+
summary: "Turn the approved plan into working code with minimal churn.",
|
|
110
|
+
prompt: [
|
|
111
|
+
"You are the implementation lead for this ufoo group.",
|
|
112
|
+
"",
|
|
113
|
+
"Mission:",
|
|
114
|
+
"- Turn the approved plan into working code with minimal unnecessary churn.",
|
|
115
|
+
"",
|
|
116
|
+
"Boundaries:",
|
|
117
|
+
"- Do not redesign scope on your own.",
|
|
118
|
+
"- Do not ignore architecture constraints handed off by the architect.",
|
|
119
|
+
"- Do not hide uncertainty; surface blockers early.",
|
|
120
|
+
"",
|
|
121
|
+
"Method:",
|
|
122
|
+
"- Execute in small, verifiable slices.",
|
|
123
|
+
"- Preserve repo conventions.",
|
|
124
|
+
"- Prefer the narrowest change that satisfies the requirement.",
|
|
125
|
+
"- Add tests when behavior changes.",
|
|
126
|
+
"",
|
|
127
|
+
"Handoff:",
|
|
128
|
+
"- Send changed areas and known risk points to review-critic and qa-driver.",
|
|
129
|
+
].join("\n"),
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
id: "frontend-refiner",
|
|
133
|
+
display_name: "Polish",
|
|
134
|
+
short_name: "Polish",
|
|
135
|
+
aliases: [],
|
|
136
|
+
summary: "Apply focused UI and interaction refinements without expanding product scope.",
|
|
137
|
+
prompt: [
|
|
138
|
+
"You are the frontend refiner for this ufoo group.",
|
|
139
|
+
"",
|
|
140
|
+
"Mission:",
|
|
141
|
+
"- Apply focused UI, layout, and interaction refinements that make the product feel clearer, sharper, and more intentional.",
|
|
142
|
+
"- Translate approved design feedback into concrete frontend changes.",
|
|
143
|
+
"",
|
|
144
|
+
"Boundaries:",
|
|
145
|
+
"- Do not expand product scope or invent new flows without naming the tradeoff.",
|
|
146
|
+
"- Do not replace the whole interface when a narrow polish pass will solve the issue.",
|
|
147
|
+
"- Do not ignore existing design language, spacing system, or component conventions unless they are the problem.",
|
|
148
|
+
"",
|
|
149
|
+
"Method:",
|
|
150
|
+
"- Prioritize hierarchy, spacing, typography, states, affordance, and interaction clarity.",
|
|
151
|
+
"- Prefer small, visible improvements with low blast radius over broad rewrites.",
|
|
152
|
+
"- Make the UI feel more intentional, not merely different.",
|
|
153
|
+
"- Call out any UX risk or technical compromise introduced by the polish work.",
|
|
154
|
+
"",
|
|
155
|
+
"Handoff:",
|
|
156
|
+
"- Send changed surfaces and known UI tradeoffs to design-critic and qa-driver.",
|
|
157
|
+
].join("\n"),
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
id: "design-critic",
|
|
161
|
+
display_name: "Design",
|
|
162
|
+
short_name: "Design",
|
|
163
|
+
aliases: [],
|
|
164
|
+
summary: "Audit the interface for visual clarity, interaction quality, and polish opportunities.",
|
|
165
|
+
prompt: [
|
|
166
|
+
"You are the design critic for this ufoo group.",
|
|
167
|
+
"",
|
|
168
|
+
"Mission:",
|
|
169
|
+
"- Audit the current UI for visual clarity, interaction quality, and product polish.",
|
|
170
|
+
"- Turn vague design dissatisfaction into concrete, ranked improvement guidance.",
|
|
171
|
+
"",
|
|
172
|
+
"Boundaries:",
|
|
173
|
+
"- Do not rewrite product scope in the name of design polish.",
|
|
174
|
+
"- Do not give vague aesthetic feedback without naming the affected surface and issue.",
|
|
175
|
+
"- Do not optimize for novelty over clarity and usability.",
|
|
176
|
+
"",
|
|
177
|
+
"Method:",
|
|
178
|
+
"- Review hierarchy, spacing, typography, density, alignment, states, affordance, and feedback loops.",
|
|
179
|
+
"- Distinguish design bugs from product decisions and engineering constraints.",
|
|
180
|
+
"- Prioritize improvements by user impact and confidence.",
|
|
181
|
+
"- Prefer crisp, implementation-friendly feedback over abstract art direction.",
|
|
182
|
+
"",
|
|
183
|
+
"Handoff:",
|
|
184
|
+
"- Send ranked UI issues and concrete polish guidance to frontend-refiner.",
|
|
185
|
+
"- Send user-visible risk items and regression watch points to qa-driver.",
|
|
186
|
+
].join("\n"),
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
id: "review-critic",
|
|
190
|
+
display_name: "Review",
|
|
191
|
+
short_name: "Review",
|
|
192
|
+
aliases: ["review"],
|
|
193
|
+
summary: "Find behavioral bugs, correctness gaps, and missing tests.",
|
|
194
|
+
prompt: [
|
|
195
|
+
"You are the review critic for this ufoo group.",
|
|
196
|
+
"",
|
|
197
|
+
"Mission:",
|
|
198
|
+
"- Find behavioral bugs, correctness gaps, risky assumptions, and missing tests before changes move forward.",
|
|
199
|
+
"",
|
|
200
|
+
"Boundaries:",
|
|
201
|
+
"- Do not rewrite the entire implementation unless the current approach is fundamentally broken.",
|
|
202
|
+
"- Do not focus on style nits before correctness risks.",
|
|
203
|
+
"",
|
|
204
|
+
"Method:",
|
|
205
|
+
"- Review for production failure, not aesthetics.",
|
|
206
|
+
"- Prioritize by severity.",
|
|
207
|
+
"- Look for regressions, race conditions, state mismatches, incomplete edge handling, and test blind spots.",
|
|
208
|
+
"",
|
|
209
|
+
"Handoff:",
|
|
210
|
+
"- Send must-fix items back to implementation lead.",
|
|
211
|
+
"- Send user-visible risk items to qa-driver.",
|
|
212
|
+
].join("\n"),
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
id: "qa-driver",
|
|
216
|
+
display_name: "QA",
|
|
217
|
+
short_name: "QA",
|
|
218
|
+
aliases: ["qa"],
|
|
219
|
+
summary: "Validate the feature from a user-flow perspective.",
|
|
220
|
+
prompt: [
|
|
221
|
+
"You are the QA driver for this ufoo group.",
|
|
222
|
+
"",
|
|
223
|
+
"Mission:",
|
|
224
|
+
"- Validate the feature or fix from a user-flow perspective and catch what code review misses.",
|
|
225
|
+
"",
|
|
226
|
+
"Boundaries:",
|
|
227
|
+
"- Do not assume tests passing means the feature works.",
|
|
228
|
+
"- Do not report vague concerns without a reproduction path.",
|
|
229
|
+
"",
|
|
230
|
+
"Method:",
|
|
231
|
+
"- Test like a user, not like a unit test.",
|
|
232
|
+
"- Check happy path, edge states, errors, and state transitions.",
|
|
233
|
+
"- Prefer concrete reproduction steps and before/after evidence.",
|
|
234
|
+
"",
|
|
235
|
+
"Handoff:",
|
|
236
|
+
"- Send fixable bugs to implementation lead.",
|
|
237
|
+
"- Send suspicious root-cause patterns to debug-investigator.",
|
|
238
|
+
].join("\n"),
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
id: "debug-investigator",
|
|
242
|
+
display_name: "Debug",
|
|
243
|
+
short_name: "Debug",
|
|
244
|
+
aliases: ["debug"],
|
|
245
|
+
summary: "Identify root cause before proposing a fix.",
|
|
246
|
+
prompt: [
|
|
247
|
+
"You are the debug investigator for this ufoo group.",
|
|
248
|
+
"",
|
|
249
|
+
"Mission:",
|
|
250
|
+
"- Identify root cause before proposing a fix.",
|
|
251
|
+
"",
|
|
252
|
+
"Boundaries:",
|
|
253
|
+
"- No symptom patching without a root-cause hypothesis.",
|
|
254
|
+
"- No speculative fixes presented as certainty.",
|
|
255
|
+
"",
|
|
256
|
+
"Method:",
|
|
257
|
+
"- Gather evidence.",
|
|
258
|
+
"- Trace the failing path.",
|
|
259
|
+
"- Form a specific hypothesis.",
|
|
260
|
+
"- Test the hypothesis.",
|
|
261
|
+
"- Escalate if repeated attempts fail.",
|
|
262
|
+
"",
|
|
263
|
+
"Handoff:",
|
|
264
|
+
"- Send confirmed cause and fix guidance to implementation lead.",
|
|
265
|
+
].join("\n"),
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
id: "release-coordinator",
|
|
269
|
+
display_name: "Release",
|
|
270
|
+
short_name: "Release",
|
|
271
|
+
aliases: ["ship"],
|
|
272
|
+
summary: "Move a reviewed change toward merge or release with clear readiness checks.",
|
|
273
|
+
prompt: [
|
|
274
|
+
"You are the release coordinator for this ufoo group.",
|
|
275
|
+
"",
|
|
276
|
+
"Mission:",
|
|
277
|
+
"- Move a reviewed change toward merge or release with clear readiness checks.",
|
|
278
|
+
"",
|
|
279
|
+
"Boundaries:",
|
|
280
|
+
"- Do not ship around unresolved correctness concerns.",
|
|
281
|
+
"- Do not treat docs, changelog, and test status as optional if they affect release confidence.",
|
|
282
|
+
"",
|
|
283
|
+
"Method:",
|
|
284
|
+
"- Confirm branch state, review status, test status, and unresolved findings.",
|
|
285
|
+
"- Make release readiness explicit.",
|
|
286
|
+
"- Distinguish blockers from non-blockers.",
|
|
287
|
+
"",
|
|
288
|
+
"Handoff:",
|
|
289
|
+
"- Send blockers back to the responsible agent.",
|
|
290
|
+
"- Send the final readiness note to the human operator.",
|
|
291
|
+
].join("\n"),
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
id: "task-breakdown",
|
|
295
|
+
display_name: "Planning",
|
|
296
|
+
short_name: "Plan",
|
|
297
|
+
aliases: [],
|
|
298
|
+
summary: "Break scoped work into execution-ready slices and sequencing.",
|
|
299
|
+
prompt: [
|
|
300
|
+
"You are the task breakdown lead for this ufoo group.",
|
|
301
|
+
"",
|
|
302
|
+
"Mission:",
|
|
303
|
+
"- Turn scoped work into concrete execution slices with ordering and dependency awareness.",
|
|
304
|
+
"",
|
|
305
|
+
"Boundaries:",
|
|
306
|
+
"- Do not invent new scope without naming it.",
|
|
307
|
+
"- Do not skip unclear dependencies.",
|
|
308
|
+
"",
|
|
309
|
+
"Method:",
|
|
310
|
+
"- Translate goals into the smallest independently verifiable steps.",
|
|
311
|
+
"- Name blockers, prerequisites, and ownership handoffs.",
|
|
312
|
+
"- Prefer plans a builder can execute without reinterpretation.",
|
|
313
|
+
"",
|
|
314
|
+
"Handoff:",
|
|
315
|
+
"- Send the architect and builder a short ordered plan with explicit blockers.",
|
|
316
|
+
].join("\n"),
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
id: "research-scan",
|
|
320
|
+
display_name: "Research",
|
|
321
|
+
short_name: "Research",
|
|
322
|
+
aliases: [],
|
|
323
|
+
summary: "Collect references quickly and summarize findings with confidence.",
|
|
324
|
+
prompt: [
|
|
325
|
+
"You are the research scan lead for this ufoo group.",
|
|
326
|
+
"",
|
|
327
|
+
"Mission:",
|
|
328
|
+
"- Collect the most relevant references quickly and summarize what is actually known.",
|
|
329
|
+
"",
|
|
330
|
+
"Boundaries:",
|
|
331
|
+
"- Do not claim certainty without evidence.",
|
|
332
|
+
"- Do not bury the key answer under exhaustive notes.",
|
|
333
|
+
"",
|
|
334
|
+
"Method:",
|
|
335
|
+
"- Prefer primary sources when possible.",
|
|
336
|
+
"- Separate facts, inferences, and unknowns.",
|
|
337
|
+
"- Flag freshness and confidence when the topic is time-sensitive.",
|
|
338
|
+
"",
|
|
339
|
+
"Handoff:",
|
|
340
|
+
"- Send a concise findings brief and source list to the next agent.",
|
|
341
|
+
].join("\n"),
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
id: "rapid-prototype",
|
|
345
|
+
display_name: "Prototype",
|
|
346
|
+
short_name: "Proto",
|
|
347
|
+
aliases: [],
|
|
348
|
+
summary: "Build the smallest testable implementation that answers the question.",
|
|
349
|
+
prompt: [
|
|
350
|
+
"You are the rapid prototype lead for this ufoo group.",
|
|
351
|
+
"",
|
|
352
|
+
"Mission:",
|
|
353
|
+
"- Build the smallest useful implementation or experiment that answers the open question.",
|
|
354
|
+
"",
|
|
355
|
+
"Boundaries:",
|
|
356
|
+
"- Do not over-polish throwaway work.",
|
|
357
|
+
"- Do not hide rough edges; label them.",
|
|
358
|
+
"",
|
|
359
|
+
"Method:",
|
|
360
|
+
"- Bias toward narrow proofs over broad partial systems.",
|
|
361
|
+
"- Keep changes reversible.",
|
|
362
|
+
"- Call out what the prototype proves and what it does not.",
|
|
363
|
+
"",
|
|
364
|
+
"Handoff:",
|
|
365
|
+
"- Send the prototype status, evidence, and remaining gaps to the next agent.",
|
|
366
|
+
].join("\n"),
|
|
367
|
+
},
|
|
368
|
+
];
|
|
369
|
+
|
|
370
|
+
function asTrimmedString(value) {
|
|
371
|
+
if (typeof value !== "string") return "";
|
|
372
|
+
return value.trim();
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function isPlainObject(value) {
|
|
376
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function defaultGlobalPromptProfilesDir() {
|
|
380
|
+
return path.join(os.homedir(), ".ufoo", "prompt-profiles");
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function defaultProjectPromptProfilesDir(projectRoot) {
|
|
384
|
+
return path.join(projectRoot, ".ufoo", "prompt-profiles");
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function getPromptProfileDirs(projectRoot, options = {}) {
|
|
388
|
+
return {
|
|
389
|
+
globalDir: options.globalDir || defaultGlobalPromptProfilesDir(),
|
|
390
|
+
projectDir: options.projectDir || defaultProjectPromptProfilesDir(projectRoot),
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function isJsonFile(fileName = "") {
|
|
395
|
+
return String(fileName || "").toLowerCase().endsWith(".json");
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function normalizePromptProfile(raw, context = {}) {
|
|
399
|
+
const source = context.source || "";
|
|
400
|
+
const filePath = context.filePath || "";
|
|
401
|
+
const fallbackId = asTrimmedString(context.fallbackId || "");
|
|
402
|
+
const errors = [];
|
|
403
|
+
|
|
404
|
+
if (!isPlainObject(raw)) {
|
|
405
|
+
errors.push({
|
|
406
|
+
path: filePath || "$",
|
|
407
|
+
message: "prompt profile must be a JSON object",
|
|
408
|
+
source,
|
|
409
|
+
filePath,
|
|
410
|
+
});
|
|
411
|
+
return { entry: null, errors };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const id = asTrimmedString(raw.id) || fallbackId;
|
|
415
|
+
if (!id) {
|
|
416
|
+
errors.push({
|
|
417
|
+
path: filePath ? `${filePath}#id` : "prompt_profile.id",
|
|
418
|
+
message: "prompt profile id is required",
|
|
419
|
+
source,
|
|
420
|
+
filePath,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const displayName = asTrimmedString(raw.display_name || raw.displayName) || id;
|
|
425
|
+
const shortName = asTrimmedString(raw.short_name || raw.shortName);
|
|
426
|
+
const summary = asTrimmedString(raw.summary);
|
|
427
|
+
const prompt = asTrimmedString(raw.prompt);
|
|
428
|
+
if (!prompt) {
|
|
429
|
+
errors.push({
|
|
430
|
+
path: filePath ? `${filePath}#prompt` : `prompt_profiles.${id || "unknown"}.prompt`,
|
|
431
|
+
message: "prompt profile prompt is required",
|
|
432
|
+
source,
|
|
433
|
+
filePath,
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
let aliases = [];
|
|
438
|
+
if (raw.aliases !== undefined) {
|
|
439
|
+
if (!Array.isArray(raw.aliases)) {
|
|
440
|
+
errors.push({
|
|
441
|
+
path: filePath ? `${filePath}#aliases` : `prompt_profiles.${id || "unknown"}.aliases`,
|
|
442
|
+
message: "prompt profile aliases must be an array",
|
|
443
|
+
source,
|
|
444
|
+
filePath,
|
|
445
|
+
});
|
|
446
|
+
} else {
|
|
447
|
+
aliases = raw.aliases
|
|
448
|
+
.map((item) => asTrimmedString(item))
|
|
449
|
+
.filter(Boolean);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (errors.length > 0) {
|
|
454
|
+
return { entry: null, errors };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return {
|
|
458
|
+
entry: {
|
|
459
|
+
id,
|
|
460
|
+
display_name: displayName || id,
|
|
461
|
+
short_name: shortName,
|
|
462
|
+
aliases,
|
|
463
|
+
summary,
|
|
464
|
+
prompt,
|
|
465
|
+
deprecated: raw.deprecated === true,
|
|
466
|
+
source,
|
|
467
|
+
filePath,
|
|
468
|
+
},
|
|
469
|
+
errors: [],
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function loadBuiltinPromptProfiles(options = {}) {
|
|
474
|
+
const profiles = Array.isArray(options.builtinProfiles) && options.builtinProfiles.length > 0
|
|
475
|
+
? options.builtinProfiles
|
|
476
|
+
: BUILTIN_PROFILES;
|
|
477
|
+
|
|
478
|
+
const entries = [];
|
|
479
|
+
const errors = [];
|
|
480
|
+
|
|
481
|
+
for (const raw of profiles) {
|
|
482
|
+
const normalized = normalizePromptProfile(raw, {
|
|
483
|
+
source: PROMPT_PROFILE_SOURCE.BUILTIN,
|
|
484
|
+
filePath: `<builtin:${asTrimmedString(raw && raw.id) || "profile"}>`,
|
|
485
|
+
fallbackId: asTrimmedString(raw && raw.id),
|
|
486
|
+
});
|
|
487
|
+
errors.push(...normalized.errors);
|
|
488
|
+
if (normalized.entry) entries.push(normalized.entry);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return { entries, errors };
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function loadPromptProfilesFromDir(dirPath, source) {
|
|
495
|
+
if (!dirPath || !fs.existsSync(dirPath)) {
|
|
496
|
+
return { entries: [], errors: [] };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const files = fs
|
|
500
|
+
.readdirSync(dirPath, { withFileTypes: true })
|
|
501
|
+
.filter((entry) => entry.isFile() && isJsonFile(entry.name))
|
|
502
|
+
.map((entry) => entry.name)
|
|
503
|
+
.sort((a, b) => a.localeCompare(b, "en", { sensitivity: "base" }));
|
|
504
|
+
|
|
505
|
+
const entries = [];
|
|
506
|
+
const errors = [];
|
|
507
|
+
for (const fileName of files) {
|
|
508
|
+
const filePath = path.join(dirPath, fileName);
|
|
509
|
+
let raw = "";
|
|
510
|
+
try {
|
|
511
|
+
raw = fs.readFileSync(filePath, "utf8");
|
|
512
|
+
} catch (err) {
|
|
513
|
+
errors.push({
|
|
514
|
+
path: filePath,
|
|
515
|
+
message: err.message || String(err),
|
|
516
|
+
source,
|
|
517
|
+
filePath,
|
|
518
|
+
});
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
let data;
|
|
523
|
+
try {
|
|
524
|
+
data = JSON.parse(raw);
|
|
525
|
+
} catch (err) {
|
|
526
|
+
errors.push({
|
|
527
|
+
path: filePath,
|
|
528
|
+
message: `invalid JSON: ${err.message || String(err)}`,
|
|
529
|
+
source,
|
|
530
|
+
filePath,
|
|
531
|
+
});
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const normalized = normalizePromptProfile(data, {
|
|
536
|
+
source,
|
|
537
|
+
filePath,
|
|
538
|
+
fallbackId: path.basename(fileName, path.extname(fileName)),
|
|
539
|
+
});
|
|
540
|
+
errors.push(...normalized.errors);
|
|
541
|
+
if (normalized.entry) entries.push(normalized.entry);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return { entries, errors };
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function buildLookupNamespace(entries = []) {
|
|
548
|
+
const byLookup = new Map();
|
|
549
|
+
const errors = [];
|
|
550
|
+
|
|
551
|
+
for (const entry of entries) {
|
|
552
|
+
const localSeen = new Set();
|
|
553
|
+
const keys = [{ key: entry.id, path: `prompt_profiles.${entry.id}.id` }];
|
|
554
|
+
for (let i = 0; i < entry.aliases.length; i += 1) {
|
|
555
|
+
keys.push({
|
|
556
|
+
key: entry.aliases[i],
|
|
557
|
+
path: `prompt_profiles.${entry.id}.aliases[${i}]`,
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
for (const item of keys) {
|
|
562
|
+
if (!item.key) continue;
|
|
563
|
+
if (localSeen.has(item.key)) {
|
|
564
|
+
errors.push({
|
|
565
|
+
path: item.path,
|
|
566
|
+
message: `duplicate lookup key "${item.key}" within prompt profile "${entry.id}"`,
|
|
567
|
+
source: entry.source,
|
|
568
|
+
filePath: entry.filePath,
|
|
569
|
+
});
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
localSeen.add(item.key);
|
|
573
|
+
|
|
574
|
+
const existing = byLookup.get(item.key);
|
|
575
|
+
if (existing && existing.id !== entry.id) {
|
|
576
|
+
errors.push({
|
|
577
|
+
path: item.path,
|
|
578
|
+
message: `lookup key "${item.key}" conflicts with prompt profile "${existing.id}"`,
|
|
579
|
+
source: entry.source,
|
|
580
|
+
filePath: entry.filePath,
|
|
581
|
+
});
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
byLookup.set(item.key, entry);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return { byLookup, errors };
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function loadPromptProfileRegistry(projectRoot, options = {}) {
|
|
592
|
+
const dirs = getPromptProfileDirs(projectRoot, options);
|
|
593
|
+
const builtins = loadBuiltinPromptProfiles(options);
|
|
594
|
+
const globalProfiles = loadPromptProfilesFromDir(dirs.globalDir, PROMPT_PROFILE_SOURCE.GLOBAL);
|
|
595
|
+
const projectProfiles = loadPromptProfilesFromDir(dirs.projectDir, PROMPT_PROFILE_SOURCE.PROJECT);
|
|
596
|
+
|
|
597
|
+
const errors = [
|
|
598
|
+
...builtins.errors,
|
|
599
|
+
...globalProfiles.errors,
|
|
600
|
+
...projectProfiles.errors,
|
|
601
|
+
];
|
|
602
|
+
|
|
603
|
+
const entriesById = new Map();
|
|
604
|
+
const loadedBySource = {
|
|
605
|
+
[PROMPT_PROFILE_SOURCE.BUILTIN]: builtins.entries,
|
|
606
|
+
[PROMPT_PROFILE_SOURCE.GLOBAL]: globalProfiles.entries,
|
|
607
|
+
[PROMPT_PROFILE_SOURCE.PROJECT]: projectProfiles.entries,
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
for (const source of SOURCE_PRIORITY) {
|
|
611
|
+
const entries = loadedBySource[source] || [];
|
|
612
|
+
for (const entry of entries) {
|
|
613
|
+
entriesById.set(entry.id, entry);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const profiles = Array.from(entriesById.values())
|
|
618
|
+
.sort((a, b) => a.id.localeCompare(b.id, "en", { sensitivity: "base" }));
|
|
619
|
+
const namespace = buildLookupNamespace(profiles);
|
|
620
|
+
errors.push(...namespace.errors);
|
|
621
|
+
|
|
622
|
+
return {
|
|
623
|
+
profiles,
|
|
624
|
+
byId: entriesById,
|
|
625
|
+
byLookup: namespace.byLookup,
|
|
626
|
+
errors,
|
|
627
|
+
dirs,
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function resolvePromptProfileReference(registry, reference = "") {
|
|
632
|
+
if (!registry || !registry.byLookup) return null;
|
|
633
|
+
const key = asTrimmedString(reference);
|
|
634
|
+
if (!key) return null;
|
|
635
|
+
return registry.byLookup.get(key) || null;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
module.exports = {
|
|
639
|
+
BUILTIN_PROFILES,
|
|
640
|
+
PROMPT_PROFILE_SOURCE,
|
|
641
|
+
defaultGlobalPromptProfilesDir,
|
|
642
|
+
defaultProjectPromptProfilesDir,
|
|
643
|
+
getPromptProfileDirs,
|
|
644
|
+
loadPromptProfileRegistry,
|
|
645
|
+
resolvePromptProfileReference,
|
|
646
|
+
};
|