@morsa/guidance-bank 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +135 -0
- package/bin/gbank.js +3 -0
- package/dist/cli/commands/init.js +27 -0
- package/dist/cli/commands/mcpServe.js +8 -0
- package/dist/cli/commands/stats.js +92 -0
- package/dist/cli/index.js +67 -0
- package/dist/cli/postinstall.js +24 -0
- package/dist/cli/prompts/initPrompts.js +89 -0
- package/dist/cli/prompts/providerAvailability.js +31 -0
- package/dist/core/audit/summarizeEntryContent.js +43 -0
- package/dist/core/audit/types.js +1 -0
- package/dist/core/bank/canonicalEntry.js +106 -0
- package/dist/core/bank/integration.js +16 -0
- package/dist/core/bank/layout.js +159 -0
- package/dist/core/bank/lifecycle.js +24 -0
- package/dist/core/bank/manifest.js +37 -0
- package/dist/core/bank/project.js +105 -0
- package/dist/core/bank/types.js +6 -0
- package/dist/core/context/contextEntryResolver.js +140 -0
- package/dist/core/context/contextTextRenderer.js +116 -0
- package/dist/core/context/detectProjectContext.js +118 -0
- package/dist/core/context/resolveContextService.js +138 -0
- package/dist/core/context/types.js +1 -0
- package/dist/core/init/initService.js +112 -0
- package/dist/core/init/initTypes.js +1 -0
- package/dist/core/projects/createBankDeriveGuidance/index.js +47 -0
- package/dist/core/projects/createBankDeriveGuidance/shared/general.js +49 -0
- package/dist/core/projects/createBankDeriveGuidance/shared/typescript.js +18 -0
- package/dist/core/projects/createBankDeriveGuidance/stacks/angular.js +252 -0
- package/dist/core/projects/createBankDeriveGuidance/stacks/ios.js +254 -0
- package/dist/core/projects/createBankDeriveGuidance/stacks/nextjs.js +220 -0
- package/dist/core/projects/createBankDeriveGuidance/stacks/nodejs.js +221 -0
- package/dist/core/projects/createBankDeriveGuidance/stacks/other.js +34 -0
- package/dist/core/projects/createBankDeriveGuidance/stacks/react.js +214 -0
- package/dist/core/projects/createBankFlow.js +252 -0
- package/dist/core/projects/createBankIterationPrompt.js +294 -0
- package/dist/core/projects/createBankPrompt.js +95 -0
- package/dist/core/projects/createFlowPhases.js +28 -0
- package/dist/core/projects/discoverCurrentProjectBank.js +43 -0
- package/dist/core/projects/discoverExistingGuidance.js +99 -0
- package/dist/core/projects/discoverProjectEvidence.js +87 -0
- package/dist/core/projects/discoverRecentCommits.js +28 -0
- package/dist/core/projects/findReferenceProjects.js +42 -0
- package/dist/core/projects/guidanceStrategies.js +29 -0
- package/dist/core/projects/identity.js +16 -0
- package/dist/core/projects/providerProjectGuidance.js +82 -0
- package/dist/core/providers/providerRegistry.js +51 -0
- package/dist/core/providers/types.js +1 -0
- package/dist/core/stats/statsService.js +117 -0
- package/dist/core/sync/syncService.js +145 -0
- package/dist/core/sync/syncTypes.js +1 -0
- package/dist/core/upgrade/upgradeService.js +134 -0
- package/dist/integrations/claudeCode/install.js +78 -0
- package/dist/integrations/codex/install.js +80 -0
- package/dist/integrations/commandRunner.js +32 -0
- package/dist/integrations/cursor/install.js +118 -0
- package/dist/integrations/shared.js +20 -0
- package/dist/mcp/config.js +19 -0
- package/dist/mcp/createMcpServer.js +31 -0
- package/dist/mcp/launcher.js +49 -0
- package/dist/mcp/registerTools.js +33 -0
- package/dist/mcp/serverMetadata.js +7 -0
- package/dist/mcp/tools/auditUtils.js +49 -0
- package/dist/mcp/tools/createBankApply.js +106 -0
- package/dist/mcp/tools/createBankToolRuntime.js +115 -0
- package/dist/mcp/tools/createBankToolSchemas.js +234 -0
- package/dist/mcp/tools/entryMutationHelpers.js +44 -0
- package/dist/mcp/tools/registerBankManifestTool.js +47 -0
- package/dist/mcp/tools/registerClearProjectBankTool.js +73 -0
- package/dist/mcp/tools/registerCreateBankTool.js +240 -0
- package/dist/mcp/tools/registerDeleteEntryTool.js +98 -0
- package/dist/mcp/tools/registerDeleteGuidanceSourceTool.js +120 -0
- package/dist/mcp/tools/registerListEntriesTool.js +94 -0
- package/dist/mcp/tools/registerReadEntryTool.js +99 -0
- package/dist/mcp/tools/registerResolveContextTool.js +128 -0
- package/dist/mcp/tools/registerSetProjectStateTool.js +121 -0
- package/dist/mcp/tools/registerSyncBankTool.js +113 -0
- package/dist/mcp/tools/registerUpgradeBankTool.js +89 -0
- package/dist/mcp/tools/registerUpsertRuleTool.js +100 -0
- package/dist/mcp/tools/registerUpsertSkillTool.js +102 -0
- package/dist/mcp/tools/sharedSchemas.js +13 -0
- package/dist/shared/errors.js +18 -0
- package/dist/shared/paths.js +11 -0
- package/dist/storage/atomicWrite.js +15 -0
- package/dist/storage/auditLogger.js +20 -0
- package/dist/storage/auditStore.js +22 -0
- package/dist/storage/bankRepository.js +168 -0
- package/dist/storage/entryStore.js +142 -0
- package/dist/storage/manifestStore.js +30 -0
- package/dist/storage/projectBankStore.js +55 -0
- package/dist/storage/providerIntegrationStore.js +22 -0
- package/dist/storage/safeFs.js +202 -0
- package/package.json +64 -0
- package/scripts/postinstall.js +20 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { CREATE_FLOW_COMPLETED_ITERATION, requiresCreateFlowStepOutcome } from "./createFlowPhases.js";
|
|
2
|
+
import { renderCreateDeriveGuidance } from "./createBankDeriveGuidance/index.js";
|
|
3
|
+
import { formatGuidanceSourceStrategy, } from "./guidanceStrategies.js";
|
|
4
|
+
const STABLE_CONTRACT_NOTE = `Use \`phase\` as the main guide for the current create step and treat \`iteration\` as diagnostic only. If \`creationPrompt\` is present, use it as the stable create-flow contract; this step prompt contains only the incremental instruction for the current phase.`;
|
|
5
|
+
const renderExistingBankBaselineSection = (hasExistingProjectBank, currentBankSnapshot) => hasExistingProjectBank
|
|
6
|
+
? `## Current Bank Baseline
|
|
7
|
+
|
|
8
|
+
A project AI Guidance Bank already exists for this repository. Treat the current project bank as the canonical baseline and improve it instead of recreating it blindly.
|
|
9
|
+
|
|
10
|
+
- Current project bank inventory: ${currentBankSnapshot.entries.length} entr${currentBankSnapshot.entries.length === 1 ? "y" : "ies"}.
|
|
11
|
+
- Reuse strong existing entries
|
|
12
|
+
- Prefer updating or replacing weak entries over duplicating them
|
|
13
|
+
- Remove stale or overlapping entries only when there is clear evidence and the user approves destructive changes
|
|
14
|
+
- Use \`list_entries\` and \`read_entry\` with \`scope: "project"\` and the current \`projectPath\` when you need the full text of an existing project-bank entry
|
|
15
|
+
`
|
|
16
|
+
: "";
|
|
17
|
+
const buildContinuationOutcomeInstruction = (iteration) => {
|
|
18
|
+
if (!requiresCreateFlowStepOutcome(iteration)) {
|
|
19
|
+
return "";
|
|
20
|
+
}
|
|
21
|
+
const baseInstruction = " Also provide an explicit result for this content phase: use `create_bank.apply` for changes or set `stepOutcome` to `applied` or `no_changes`.";
|
|
22
|
+
if (iteration === 3) {
|
|
23
|
+
return `${baseInstruction} If you use \`no_changes\`, use \`stepOutcomeNote\` to name the strongest remaining candidates you reviewed and why they were skipped.`;
|
|
24
|
+
}
|
|
25
|
+
if (iteration === 4) {
|
|
26
|
+
return `${baseInstruction} If you use \`no_changes\`, use \`stepOutcomeNote\` to summarize the strongest skipped or already-covered candidates and why the bank is complete enough.`;
|
|
27
|
+
}
|
|
28
|
+
return `${baseInstruction} If you use \`no_changes\`, include \`stepOutcomeNote\`.`;
|
|
29
|
+
};
|
|
30
|
+
const appendContinuationInstruction = (prompt, iteration) => {
|
|
31
|
+
const continuationSuffix = buildContinuationOutcomeInstruction(iteration);
|
|
32
|
+
return `${prompt}
|
|
33
|
+
|
|
34
|
+
## Continuation
|
|
35
|
+
|
|
36
|
+
After completing this step, call \`create_bank\` again with \`iteration: ${iteration + 1}\` and \`stepCompleted: true\`.${continuationSuffix}`;
|
|
37
|
+
};
|
|
38
|
+
const renderDiscoveredSourcesSection = (discoveredSources) => {
|
|
39
|
+
if (discoveredSources.length === 0) {
|
|
40
|
+
return `## Discovered Guidance Sources
|
|
41
|
+
|
|
42
|
+
No repository-local or provider-project guidance sources were discovered for this project.`;
|
|
43
|
+
}
|
|
44
|
+
return `## Discovered Guidance Sources
|
|
45
|
+
|
|
46
|
+
${discoveredSources
|
|
47
|
+
.map((source) => `- [${source.kind}${source.scope === "provider-project" && source.provider ? `/${source.provider}` : ""}] ${source.relativePath} (${source.entryType}, ${source.scope})`)
|
|
48
|
+
.join("\n")}`;
|
|
49
|
+
};
|
|
50
|
+
const renderConfirmedSourceStrategiesSection = (confirmedSourceStrategies) => {
|
|
51
|
+
if (confirmedSourceStrategies.length === 0) {
|
|
52
|
+
return `## Confirmed Source Decisions
|
|
53
|
+
|
|
54
|
+
No confirmed source decisions are stored yet for this flow.`;
|
|
55
|
+
}
|
|
56
|
+
return `## Confirmed Source Decisions
|
|
57
|
+
|
|
58
|
+
${confirmedSourceStrategies
|
|
59
|
+
.map((strategy) => `- ${strategy.sourceRef} -> ${formatGuidanceSourceStrategy(strategy.strategy)}${strategy.note ? ` (${strategy.note})` : ""}`)
|
|
60
|
+
.join("\n")}`;
|
|
61
|
+
};
|
|
62
|
+
const renderDetectedStacksSection = (detectedStacks) => detectedStacks.length === 0
|
|
63
|
+
? `## Detected Stacks
|
|
64
|
+
|
|
65
|
+
- other`
|
|
66
|
+
: `## Detected Stacks
|
|
67
|
+
|
|
68
|
+
${detectedStacks.map((stack) => `- ${stack}`).join("\n")}`;
|
|
69
|
+
const renderReferenceProjectsSection = (selectedReferenceProjects) => {
|
|
70
|
+
if (selectedReferenceProjects.length === 0) {
|
|
71
|
+
return `## Reference Projects
|
|
72
|
+
|
|
73
|
+
No reference project banks were selected for this run.`;
|
|
74
|
+
}
|
|
75
|
+
return `## Reference Projects
|
|
76
|
+
|
|
77
|
+
${selectedReferenceProjects
|
|
78
|
+
.map((project) => `- ${project.projectName}
|
|
79
|
+
- Project path: \`${project.projectPath}\`
|
|
80
|
+
- Shared stacks: ${project.sharedStacks.join(", ")}`)
|
|
81
|
+
.join("\n")}`;
|
|
82
|
+
};
|
|
83
|
+
const buildKickoffPrompt = ({ projectName, projectPath, projectBankPath, rulesDirectory, skillsDirectory, detectedStacks, selectedReferenceProjects, }) => `# Create Flow Kickoff
|
|
84
|
+
|
|
85
|
+
${STABLE_CONTRACT_NOTE}
|
|
86
|
+
|
|
87
|
+
Project:
|
|
88
|
+
- \`${projectName}\`
|
|
89
|
+
- \`${projectPath}\`
|
|
90
|
+
|
|
91
|
+
Target AI Guidance Bank:
|
|
92
|
+
- \`${projectBankPath}\`
|
|
93
|
+
- Rules: \`${rulesDirectory}\`
|
|
94
|
+
- Skills: \`${skillsDirectory}\`
|
|
95
|
+
|
|
96
|
+
${renderDetectedStacksSection(detectedStacks)}
|
|
97
|
+
|
|
98
|
+
${renderReferenceProjectsSection(selectedReferenceProjects)}
|
|
99
|
+
|
|
100
|
+
What to do in this step:
|
|
101
|
+
- inspect the repository and selected reference projects
|
|
102
|
+
- form a candidate list for the first high-value rules and skills
|
|
103
|
+
- start writing only when the evidence is already strong
|
|
104
|
+
- delay external guidance import or deletion until the dedicated review step
|
|
105
|
+
- treat AI Guidance Bank as durable, reusable rules-and-skills guidance across sessions
|
|
106
|
+
- do not stop at a thin summary if the repository clearly supports a richer bank
|
|
107
|
+
|
|
108
|
+
Step output:
|
|
109
|
+
- short list of created, updated, or planned files
|
|
110
|
+
- short purpose for each item
|
|
111
|
+
- strongest remaining candidates or uncertainties to handle next`;
|
|
112
|
+
const buildReviewExistingPrompt = (projectPath, discoveredSources) => `# Existing Guidance Review
|
|
113
|
+
|
|
114
|
+
${STABLE_CONTRACT_NOTE}
|
|
115
|
+
|
|
116
|
+
Review available external guidance before importing anything into AI Guidance Bank.
|
|
117
|
+
|
|
118
|
+
Project path:
|
|
119
|
+
- \`${projectPath}\`
|
|
120
|
+
|
|
121
|
+
${renderDiscoveredSourcesSection(discoveredSources)}
|
|
122
|
+
|
|
123
|
+
What to do:
|
|
124
|
+
- Treat the listed repository-local and provider-project sources as the guaranteed inputs for this review
|
|
125
|
+
- Skip purely empty, obsolete, or trivial sources without bothering the user
|
|
126
|
+
- By default, do not expose internal strategy labels to the user
|
|
127
|
+
- Treat AI Guidance Bank as the durable canonical rules-and-skills layer for the project
|
|
128
|
+
- Ask for one simple confirmation:
|
|
129
|
+
- \`ok\`: make AI Guidance Bank the canonical source for this project and migrate useful guidance by the default policy
|
|
130
|
+
- \`not ok\`: leave legacy guidance untouched and do not treat it as canonical AI Guidance Bank coverage
|
|
131
|
+
- When the simple confirmation is enough, advance with \`sourceReviewDecision: "ok"\` or \`sourceReviewDecision: "not_ok"\`
|
|
132
|
+
- Ask a more detailed follow-up only when deletion is risky enough that the simple confirmation is not safe
|
|
133
|
+
- Keep the user-facing review short and action-oriented:
|
|
134
|
+
- start with a 1-2 sentence summary of what sources were found
|
|
135
|
+
- recommend one default action
|
|
136
|
+
- end with one explicit CTA question telling the user to answer \`ok\` or \`not ok\`
|
|
137
|
+
- avoid long protocol dumps, source-strategy labels, or repeating the same source list multiple times
|
|
138
|
+
|
|
139
|
+
Decision rules:
|
|
140
|
+
- Treat provider-project guidance as legacy project-specific input that usually needs review or migration
|
|
141
|
+
- Never delete or rewrite any original source during this review step`;
|
|
142
|
+
const buildImportSelectedPrompt = (discoveredSources, confirmedSourceStrategies) => `# Import Selected Guidance
|
|
143
|
+
|
|
144
|
+
${STABLE_CONTRACT_NOTE}
|
|
145
|
+
|
|
146
|
+
Apply the source-level strategies the user approved for external guidance.
|
|
147
|
+
|
|
148
|
+
${renderDiscoveredSourcesSection(discoveredSources)}
|
|
149
|
+
|
|
150
|
+
${renderConfirmedSourceStrategiesSection(confirmedSourceStrategies)}
|
|
151
|
+
|
|
152
|
+
What to do:
|
|
153
|
+
- Treat the confirmed source decisions below as the internal execution plan for this import step
|
|
154
|
+
- Convert approved guidance into canonical AI Guidance Bank rules and skills
|
|
155
|
+
- Keep imported content durable, operational, and reusable across future sessions
|
|
156
|
+
- Split entries between project scope and shared scope when appropriate
|
|
157
|
+
- Assign stable ids, titles, topics, and stacks
|
|
158
|
+
- Deduplicate against existing AI Guidance Bank content before writing
|
|
159
|
+
- Use \`create_bank\` with an \`apply\` payload for batched canonical writes and deletions during this flow
|
|
160
|
+
- In \`create_bank.apply\`, paths must be relative to the rules/skills root only; use \`topics/example.md\` or \`adding-feature\`, not \`rules/topics/example.md\` or \`skills/adding-feature\`
|
|
161
|
+
- If the user simply confirmed \`ok\`, follow the default policy: make AI Guidance Bank canonical, migrate useful file-level guidance, ignore empty or container-only sources automatically, and clean up migrated legacy files when it is safe to do so after successful writes and verification
|
|
162
|
+
- If the user confirmed \`not ok\`, still migrate useful guidance into the canonical AI Guidance Bank but leave the legacy sources untouched even if that creates temporary duplication
|
|
163
|
+
- When replacing or deleting an existing AI Guidance Bank entry, read it first and pass its \`sha256\` back as \`baseSha256\`
|
|
164
|
+
- If \`create_bank.apply\` reports a \`conflict\`, re-read the affected entry, rebuild the full final document, and retry with the fresh \`baseSha256\`
|
|
165
|
+
|
|
166
|
+
Write rules:
|
|
167
|
+
- Create a \`rule\` when the source describes a stable constraint, convention, or preference
|
|
168
|
+
- Create a \`skill\` when the source describes a reusable workflow or task sequence
|
|
169
|
+
- Prefer a small number of high-value entries over fragmented boilerplate
|
|
170
|
+
- If a source duplicates existing canonical content, update or skip instead of cloning it
|
|
171
|
+
|
|
172
|
+
Safety rules:
|
|
173
|
+
- Do not delete, rewrite, or trim any original source unless the confirmed review decision allows cleanup and the migration was already written and verified successfully
|
|
174
|
+
- If the user did not clearly approve an action for a source, leave that source untouched
|
|
175
|
+
- If one source mixes project-specific and shared material, split it across scopes instead of forcing one destination
|
|
176
|
+
- Do not count provider-local or provider-global guidance as existing AI Guidance Bank coverage when deciding what still needs to be written`;
|
|
177
|
+
const buildDeriveFromProjectPrompt = (projectPath, detectedStacks) => `# Derive From Project
|
|
178
|
+
|
|
179
|
+
${STABLE_CONTRACT_NOTE}
|
|
180
|
+
|
|
181
|
+
Derive additional AI Guidance Bank entries from the real repository.
|
|
182
|
+
|
|
183
|
+
Project path:
|
|
184
|
+
- \`${projectPath}\`
|
|
185
|
+
|
|
186
|
+
What to do:
|
|
187
|
+
- Inspect the real repository directly: project structure, entrypoints, configuration, source files, and recurring implementation patterns
|
|
188
|
+
- Create a focused set of high-value project rules and skills
|
|
189
|
+
- Prefer stable patterns over one-off details
|
|
190
|
+
- Keep AI Guidance Bank focused on durable guidance that remains useful across future sessions
|
|
191
|
+
- Put reusable cross-project guidance into shared scope only when the evidence is strong
|
|
192
|
+
- Review the strongest remaining candidates before a major batch
|
|
193
|
+
- Treat the bank as incomplete if obvious entries are still missing without a clear skip reason
|
|
194
|
+
|
|
195
|
+
Quality rules:
|
|
196
|
+
- Do not rely on a server-provided file checklist; gather your own evidence from the real repository
|
|
197
|
+
- Prefer patterns confirmed by multiple files, configuration, or stable architecture boundaries
|
|
198
|
+
- Skip temporary, noisy, or accidental implementation details
|
|
199
|
+
- If a candidate rule is high-impact and your confidence is low, ask the user before writing it
|
|
200
|
+
- Apply derived changes through \`create_bank.apply\` in batches instead of a long series of one-entry write calls
|
|
201
|
+
- In \`create_bank.apply\`, keep each path relative to the rules/skills root instead of prefixing it with \`rules/\` or \`skills/\`
|
|
202
|
+
- If \`create_bank.apply\` reports a \`conflict\`, re-read the affected entry, rebuild the full final document, and retry with the fresh \`baseSha256\`
|
|
203
|
+
- For each obvious candidate you skip, keep a short reason: already covered, weak evidence, intentionally deferred, or better suited to shared scope
|
|
204
|
+
|
|
205
|
+
${renderCreateDeriveGuidance(detectedStacks)}`;
|
|
206
|
+
const buildFinalizePrompt = () => `# Finalize AI Guidance Bank
|
|
207
|
+
|
|
208
|
+
${STABLE_CONTRACT_NOTE}
|
|
209
|
+
|
|
210
|
+
Finish the project AI Guidance Bank creation flow.
|
|
211
|
+
|
|
212
|
+
What to do:
|
|
213
|
+
- Deduplicate overlapping rules and skills
|
|
214
|
+
- Verify scope split between shared and project entries
|
|
215
|
+
- Check ids, titles, topics, and stacks for consistency
|
|
216
|
+
- Keep only durable guidance that should survive across sessions; leave conversational context out
|
|
217
|
+
- If confidence is low for any high-impact rule, ask the user before keeping it
|
|
218
|
+
- Use \`create_bank.apply\` for the final cleanup batch when you need to replace or delete multiple entries
|
|
219
|
+
- If \`create_bank.apply\` reports a \`conflict\`, re-read the affected entry, rebuild the final canonical document, and retry the cleanup batch with fresh \`baseSha256\`
|
|
220
|
+
- Return a concise completion report when the bank is in a good canonical state
|
|
221
|
+
- Run an explicit gap-and-coverage review before declaring the bank done
|
|
222
|
+
|
|
223
|
+
Final pass checklist:
|
|
224
|
+
- Remove near-duplicate entries and merge them into the clearest canonical version
|
|
225
|
+
- Ensure each entry is either clearly a \`rule\` or clearly a \`skill\`
|
|
226
|
+
- Ensure project overrides do not duplicate shared guidance without adding real specificity
|
|
227
|
+
- Leave unresolved or low-confidence items out unless the user explicitly approves them
|
|
228
|
+
- Confirm the bank is not materially poorer than the strongest project evidence from this run
|
|
229
|
+
- Check the strongest applicable topic and skill candidates, then create them, merge them, or record a skip reason
|
|
230
|
+
- In the final report, mention imported sources, newly derived entries, and any important skipped uncertainties or intentionally omitted candidates
|
|
231
|
+
- If you finish with \`stepOutcome: "no_changes"\`, use \`stepOutcomeNote\` to summarize the strongest skipped or already-covered high-value candidates and why no further mutation was needed`;
|
|
232
|
+
const buildCompletedPrompt = () => `# Create Flow Completed
|
|
233
|
+
|
|
234
|
+
The iterative project AI Guidance Bank creation flow is complete.
|
|
235
|
+
|
|
236
|
+
What to do:
|
|
237
|
+
- Do not continue the create flow automatically
|
|
238
|
+
- Re-enter the flow only if the user explicitly asks for another create pass or wants to restart parts of the review
|
|
239
|
+
- Continue normal AI Guidance Bank work through the standard mutation tools when the user asks for targeted updates`;
|
|
240
|
+
const CREATE_FLOW_PROMPT_BUILDERS = [
|
|
241
|
+
({ projectName, projectPath, projectBankPath, rulesDirectory, skillsDirectory, detectedStacks, selectedReferenceProjects }) => buildKickoffPrompt({
|
|
242
|
+
projectName,
|
|
243
|
+
projectPath,
|
|
244
|
+
projectBankPath,
|
|
245
|
+
rulesDirectory,
|
|
246
|
+
skillsDirectory,
|
|
247
|
+
detectedStacks,
|
|
248
|
+
selectedReferenceProjects,
|
|
249
|
+
}),
|
|
250
|
+
({ projectPath, discoveredSources }) => buildReviewExistingPrompt(projectPath, discoveredSources),
|
|
251
|
+
({ discoveredSources, confirmedSourceStrategies }) => buildImportSelectedPrompt(discoveredSources, confirmedSourceStrategies),
|
|
252
|
+
({ projectPath, detectedStacks }) => buildDeriveFromProjectPrompt(projectPath, detectedStacks),
|
|
253
|
+
() => buildFinalizePrompt(),
|
|
254
|
+
() => buildCompletedPrompt(),
|
|
255
|
+
];
|
|
256
|
+
export const buildReadyProjectBankPrompt = ({ updatedAt, updatedDaysAgo, }) => {
|
|
257
|
+
const updatedLine = updatedAt === null || updatedDaysAgo === null
|
|
258
|
+
? "A project AI Guidance Bank already exists for this repository."
|
|
259
|
+
: `A project AI Guidance Bank already exists for this repository and was last updated ${updatedDaysAgo} day${updatedDaysAgo === 1 ? "" : "s"} ago (${updatedAt}).`;
|
|
260
|
+
return `# Existing Project AI Guidance Bank
|
|
261
|
+
|
|
262
|
+
${updatedLine}
|
|
263
|
+
|
|
264
|
+
What to do:
|
|
265
|
+
- Tell the user that a project AI Guidance Bank already exists for this repository
|
|
266
|
+
- Ask whether they want to improve it now instead of keeping it as-is
|
|
267
|
+
- If the user wants to improve it, call \`create_bank\` again with \`iteration: 1\`
|
|
268
|
+
- If the user does not want to improve it, continue normal work with the current ready bank through \`resolve_context\`
|
|
269
|
+
- If you continue into later create iterations, treat the existing project bank as the canonical baseline and improve gaps, stale entries, duplicates, and weak coverage instead of recreating the bank from scratch`;
|
|
270
|
+
};
|
|
271
|
+
export const buildCreateBankIterationPrompt = ({ iteration, projectName, projectPath, projectBankPath, rulesDirectory, skillsDirectory, detectedStacks, selectedReferenceProjects, discoveredSources, confirmedSourceStrategies, currentBankSnapshot, hasExistingProjectBank = false, }) => {
|
|
272
|
+
const normalizedIteration = Math.min(Math.max(iteration, 0), CREATE_FLOW_COMPLETED_ITERATION);
|
|
273
|
+
const buildPrompt = CREATE_FLOW_PROMPT_BUILDERS[normalizedIteration];
|
|
274
|
+
const prompt = buildPrompt({
|
|
275
|
+
iteration,
|
|
276
|
+
projectName,
|
|
277
|
+
projectPath,
|
|
278
|
+
projectBankPath,
|
|
279
|
+
rulesDirectory,
|
|
280
|
+
skillsDirectory,
|
|
281
|
+
detectedStacks,
|
|
282
|
+
selectedReferenceProjects,
|
|
283
|
+
discoveredSources,
|
|
284
|
+
confirmedSourceStrategies,
|
|
285
|
+
currentBankSnapshot,
|
|
286
|
+
hasExistingProjectBank,
|
|
287
|
+
});
|
|
288
|
+
const promptWithBaseline = hasExistingProjectBank && normalizedIteration > 0 && normalizedIteration < CREATE_FLOW_COMPLETED_ITERATION
|
|
289
|
+
? `${renderExistingBankBaselineSection(true, currentBankSnapshot)}\n${prompt}`
|
|
290
|
+
: prompt;
|
|
291
|
+
return normalizedIteration < CREATE_FLOW_COMPLETED_ITERATION
|
|
292
|
+
? appendContinuationInstruction(promptWithBaseline, normalizedIteration)
|
|
293
|
+
: promptWithBaseline;
|
|
294
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { DETECTABLE_STACKS } from "../context/types.js";
|
|
2
|
+
const renderDetectedStackSection = (detectedStacks) => {
|
|
3
|
+
if (detectedStacks.length === 1 && detectedStacks[0] === "other") {
|
|
4
|
+
return `## Detected Stack
|
|
5
|
+
|
|
6
|
+
No specific stack signals were detected confidently. Use \`other\` as the fallback stack and infer only project-supported patterns from the codebase.`;
|
|
7
|
+
}
|
|
8
|
+
return `## Detected Stack
|
|
9
|
+
|
|
10
|
+
${detectedStacks.map((stack) => `- ${stack}`).join("\n")}`;
|
|
11
|
+
};
|
|
12
|
+
const renderSupportedStackIdsSection = () => `## Supported Stack Ids
|
|
13
|
+
|
|
14
|
+
Use only these canonical stack ids in AI Guidance Bank metadata:
|
|
15
|
+
${DETECTABLE_STACKS.map((stack) => `- ${stack}`).join("\n")}
|
|
16
|
+
|
|
17
|
+
If no specific stack fits confidently, use \`other\`.`;
|
|
18
|
+
const renderReferenceProjectsSection = (selectedReferenceProjects) => {
|
|
19
|
+
if (selectedReferenceProjects.length === 0) {
|
|
20
|
+
return `## Reference Projects
|
|
21
|
+
|
|
22
|
+
No reference project banks were selected for this run.`;
|
|
23
|
+
}
|
|
24
|
+
return `## Reference Projects
|
|
25
|
+
|
|
26
|
+
${selectedReferenceProjects
|
|
27
|
+
.map((project) => `- ${project.projectName}
|
|
28
|
+
- Project path: \`${project.projectPath}\`
|
|
29
|
+
- Shared stacks: ${project.sharedStacks.join(", ")}`)
|
|
30
|
+
.join("\n")}`;
|
|
31
|
+
};
|
|
32
|
+
export const buildCreateBankPrompt = ({ projectName, projectPath, projectBankPath, rulesDirectory, skillsDirectory, detectedStacks, selectedReferenceProjects, }) => `# Project AI Guidance Bank Creation
|
|
33
|
+
|
|
34
|
+
You are creating the canonical AI Guidance Bank for \`${projectName}\`.
|
|
35
|
+
|
|
36
|
+
Project path:
|
|
37
|
+
- \`${projectPath}\`
|
|
38
|
+
|
|
39
|
+
Target AI Guidance Bank:
|
|
40
|
+
- \`${projectBankPath}\`
|
|
41
|
+
- Rules root: \`${rulesDirectory}\`
|
|
42
|
+
- Skills root: \`${skillsDirectory}\`
|
|
43
|
+
|
|
44
|
+
${renderDetectedStackSection(detectedStacks)}
|
|
45
|
+
|
|
46
|
+
${renderSupportedStackIdsSection()}
|
|
47
|
+
|
|
48
|
+
${renderReferenceProjectsSection(selectedReferenceProjects)}
|
|
49
|
+
|
|
50
|
+
## Stable Contract
|
|
51
|
+
|
|
52
|
+
- AI Guidance Bank is the canonical user-managed guidance layer for this project
|
|
53
|
+
- AI Guidance Bank stores durable rules, skills, and reusable project guidance across sessions
|
|
54
|
+
- Use \`phase\` as the primary guide during the create/improve flow; treat \`iteration\` as diagnostic only
|
|
55
|
+
- Use real project code, shared AI Guidance Bank context, selected reference projects, and explicit user instructions as the main inputs
|
|
56
|
+
- External repository-local or provider-project guidance must be reviewed explicitly in later steps before import
|
|
57
|
+
- Provider-local skills, provider-global skills, and model-native instructions may help analysis, but they never count as canonical AI Guidance Bank coverage
|
|
58
|
+
|
|
59
|
+
## Writing Contract
|
|
60
|
+
|
|
61
|
+
- During the guided flow, prefer batched writes through \`create_bank.apply\`
|
|
62
|
+
- Pass complete final documents, not partial markdown patches
|
|
63
|
+
- \`create_bank.apply.path\` must be relative to the rules/skills root:
|
|
64
|
+
- rules: \`core/general.md\`, \`topics/architecture.md\`
|
|
65
|
+
- skills: \`adding-feature\`, \`task-based-reading\`
|
|
66
|
+
- Do not prefix apply paths with \`rules/\` or \`skills/\`
|
|
67
|
+
- When replacing or deleting an existing entry, read it first and pass \`baseSha256\`
|
|
68
|
+
- If \`create_bank.apply\` reports a conflict, re-read the affected entry and retry with a fresh \`baseSha256\`
|
|
69
|
+
- Reserve \`upsert_rule\`, \`upsert_skill\`, and \`delete_entry\` for targeted edits outside the full create/improve flow
|
|
70
|
+
|
|
71
|
+
## Scope Rules
|
|
72
|
+
|
|
73
|
+
- Put guidance in the project bank only when it reflects stable patterns from this repository or meaningfully refines shared guidance
|
|
74
|
+
- Put clearly reusable cross-project guidance into the shared layer instead of the project layer
|
|
75
|
+
- Only shared/project AI Guidance Bank entries and explicitly reviewed external sources count as canonical coverage
|
|
76
|
+
|
|
77
|
+
## Coverage Expectations
|
|
78
|
+
|
|
79
|
+
- Create a right-sized bank, not a thin summary
|
|
80
|
+
- Consider both rules and skills
|
|
81
|
+
- Build a candidate list before the first substantial write batch
|
|
82
|
+
- If obvious candidates are skipped, keep a clear reason and reflect it later in \`stepOutcomeNote\`
|
|
83
|
+
|
|
84
|
+
Expected Bank Density:
|
|
85
|
+
- 2-6 focused rule files when project evidence supports them
|
|
86
|
+
- 2-5 focused skills when reusable workflows are clearly present
|
|
87
|
+
|
|
88
|
+
## Kickoff Expectations
|
|
89
|
+
|
|
90
|
+
During the initial step:
|
|
91
|
+
- inspect the repository and selected reference projects
|
|
92
|
+
- build a broad candidate inventory before the first major write batch
|
|
93
|
+
- do not import or delete external guidance yet; that happens in later review/import steps
|
|
94
|
+
- do not stop after the first acceptable batch if the project clearly supports stronger canonical coverage
|
|
95
|
+
`;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const CREATE_FLOW_PHASES = [
|
|
2
|
+
"sync_required",
|
|
3
|
+
"ready_to_improve",
|
|
4
|
+
"kickoff",
|
|
5
|
+
"review_existing_guidance",
|
|
6
|
+
"import_selected_guidance",
|
|
7
|
+
"derive_from_project",
|
|
8
|
+
"finalize",
|
|
9
|
+
"completed",
|
|
10
|
+
];
|
|
11
|
+
export const CREATE_ITERATION_PHASES = [
|
|
12
|
+
"kickoff",
|
|
13
|
+
"review_existing_guidance",
|
|
14
|
+
"import_selected_guidance",
|
|
15
|
+
"derive_from_project",
|
|
16
|
+
"finalize",
|
|
17
|
+
"completed",
|
|
18
|
+
];
|
|
19
|
+
export const CREATE_FLOW_COMPLETED_ITERATION = CREATE_ITERATION_PHASES.length - 1;
|
|
20
|
+
const CREATE_FLOW_OUTCOME_REQUIRED_PHASES = [
|
|
21
|
+
"import_selected_guidance",
|
|
22
|
+
"derive_from_project",
|
|
23
|
+
"finalize",
|
|
24
|
+
];
|
|
25
|
+
export const getCreateFlowPhase = (iteration) => CREATE_ITERATION_PHASES[Math.min(Math.max(iteration, 0), CREATE_FLOW_COMPLETED_ITERATION)];
|
|
26
|
+
export const getNextCreateFlowIteration = (iteration) => iteration < CREATE_FLOW_COMPLETED_ITERATION ? iteration + 1 : null;
|
|
27
|
+
export const isCreateFlowComplete = (iteration) => iteration >= CREATE_FLOW_COMPLETED_ITERATION;
|
|
28
|
+
export const requiresCreateFlowStepOutcome = (iteration) => CREATE_FLOW_OUTCOME_REQUIRED_PHASES.includes(getCreateFlowPhase(iteration));
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { parseCanonicalRuleDocument, parseCanonicalSkillDocument } from "../bank/canonicalEntry.js";
|
|
3
|
+
const sha256 = (content) => createHash("sha256").update(content).digest("hex");
|
|
4
|
+
export const discoverCurrentProjectBank = async (repository, projectId, exists) => {
|
|
5
|
+
if (!exists) {
|
|
6
|
+
return {
|
|
7
|
+
exists: false,
|
|
8
|
+
entries: [],
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
const [ruleEntries, skillEntries] = await Promise.all([
|
|
12
|
+
repository.listLayerEntries("project", "rules", projectId),
|
|
13
|
+
repository.listLayerEntries("project", "skills", projectId),
|
|
14
|
+
]);
|
|
15
|
+
const [ruleSnapshots, skillSnapshots] = await Promise.all([
|
|
16
|
+
Promise.all(ruleEntries.map(async ({ path }) => {
|
|
17
|
+
const content = await repository.readLayerEntry("project", "rules", path, projectId);
|
|
18
|
+
const document = parseCanonicalRuleDocument(content);
|
|
19
|
+
return {
|
|
20
|
+
kind: "rules",
|
|
21
|
+
scope: "project",
|
|
22
|
+
path,
|
|
23
|
+
id: document.frontmatter.id,
|
|
24
|
+
sha256: sha256(content),
|
|
25
|
+
};
|
|
26
|
+
})),
|
|
27
|
+
Promise.all(skillEntries.map(async ({ path }) => {
|
|
28
|
+
const content = await repository.readLayerEntry("project", "skills", path, projectId);
|
|
29
|
+
const document = parseCanonicalSkillDocument(content);
|
|
30
|
+
return {
|
|
31
|
+
kind: "skills",
|
|
32
|
+
scope: "project",
|
|
33
|
+
path,
|
|
34
|
+
id: document.frontmatter.id,
|
|
35
|
+
sha256: sha256(content),
|
|
36
|
+
};
|
|
37
|
+
})),
|
|
38
|
+
]);
|
|
39
|
+
return {
|
|
40
|
+
exists: true,
|
|
41
|
+
entries: [...ruleSnapshots, ...skillSnapshots].sort((left, right) => left.path.localeCompare(right.path)),
|
|
42
|
+
};
|
|
43
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { promises as fs } from "node:fs";
|
|
3
|
+
import { discoverProviderProjectGuidance } from "./providerProjectGuidance.js";
|
|
4
|
+
const fileCandidates = [
|
|
5
|
+
{ kind: "agents", relativePath: "AGENTS.md" },
|
|
6
|
+
{ kind: "claude-md", relativePath: "CLAUDE.md" },
|
|
7
|
+
{ kind: "claude-md", relativePath: "claude.md" },
|
|
8
|
+
{ kind: "copilot", relativePath: "copilot-instructions.md" },
|
|
9
|
+
{ kind: "copilot", relativePath: ".github/copilot-instructions.md" },
|
|
10
|
+
];
|
|
11
|
+
const directoryCandidates = [
|
|
12
|
+
{ kind: "cursor", relativePath: ".cursor" },
|
|
13
|
+
{ kind: "claude", relativePath: ".claude" },
|
|
14
|
+
{ kind: "codex", relativePath: ".codex" },
|
|
15
|
+
];
|
|
16
|
+
const pathExists = async (targetPath) => {
|
|
17
|
+
try {
|
|
18
|
+
await fs.access(targetPath);
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
const listFilesRecursively = async (directoryPath) => {
|
|
26
|
+
const directoryEntries = await fs.readdir(directoryPath, { withFileTypes: true });
|
|
27
|
+
const filePaths = [];
|
|
28
|
+
for (const directoryEntry of directoryEntries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
29
|
+
const entryPath = path.join(directoryPath, directoryEntry.name);
|
|
30
|
+
if (directoryEntry.isDirectory()) {
|
|
31
|
+
filePaths.push(...(await listFilesRecursively(entryPath)));
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (directoryEntry.isFile()) {
|
|
35
|
+
filePaths.push(entryPath);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return filePaths;
|
|
39
|
+
};
|
|
40
|
+
export const discoverExistingGuidance = async (projectPath) => {
|
|
41
|
+
const resolvedProjectPath = path.resolve(projectPath);
|
|
42
|
+
const discoveredSources = [];
|
|
43
|
+
for (const candidate of fileCandidates) {
|
|
44
|
+
const candidatePath = path.join(resolvedProjectPath, candidate.relativePath);
|
|
45
|
+
if (!(await pathExists(candidatePath))) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
discoveredSources.push({
|
|
49
|
+
kind: candidate.kind,
|
|
50
|
+
entryType: "file",
|
|
51
|
+
scope: "repository-local",
|
|
52
|
+
provider: null,
|
|
53
|
+
path: candidatePath,
|
|
54
|
+
relativePath: candidate.relativePath,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
for (const candidate of directoryCandidates) {
|
|
58
|
+
const candidatePath = path.join(resolvedProjectPath, candidate.relativePath);
|
|
59
|
+
if (!(await pathExists(candidatePath))) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
discoveredSources.push({
|
|
63
|
+
kind: candidate.kind,
|
|
64
|
+
entryType: "directory",
|
|
65
|
+
scope: "repository-local",
|
|
66
|
+
provider: null,
|
|
67
|
+
path: candidatePath,
|
|
68
|
+
relativePath: candidate.relativePath,
|
|
69
|
+
});
|
|
70
|
+
const nestedFilePaths = await listFilesRecursively(candidatePath);
|
|
71
|
+
for (const nestedFilePath of nestedFilePaths) {
|
|
72
|
+
discoveredSources.push({
|
|
73
|
+
kind: candidate.kind,
|
|
74
|
+
entryType: "file",
|
|
75
|
+
scope: "repository-local",
|
|
76
|
+
provider: null,
|
|
77
|
+
path: nestedFilePath,
|
|
78
|
+
relativePath: path.relative(resolvedProjectPath, nestedFilePath),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const providerProjectSources = await discoverProviderProjectGuidance(resolvedProjectPath);
|
|
83
|
+
for (const source of providerProjectSources) {
|
|
84
|
+
const kind = source.provider === "codex"
|
|
85
|
+
? "codex-project"
|
|
86
|
+
: source.provider === "cursor"
|
|
87
|
+
? "cursor-project"
|
|
88
|
+
: "claude-project";
|
|
89
|
+
discoveredSources.push({
|
|
90
|
+
kind,
|
|
91
|
+
entryType: source.entryType,
|
|
92
|
+
scope: "provider-project",
|
|
93
|
+
provider: source.provider,
|
|
94
|
+
path: source.path,
|
|
95
|
+
relativePath: source.relativePath,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return discoveredSources.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
|
|
99
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { promises as fs } from "node:fs";
|
|
3
|
+
const topLevelDirectoryCandidates = ["src", "app", "pages", "lib", "server", "packages", "scripts", "test", "tests", "docs"];
|
|
4
|
+
const configFileCandidates = [
|
|
5
|
+
"package.json",
|
|
6
|
+
"tsconfig.json",
|
|
7
|
+
"tsconfig.app.json",
|
|
8
|
+
"tsconfig.base.json",
|
|
9
|
+
"angular.json",
|
|
10
|
+
"eslint.config.js",
|
|
11
|
+
"eslint.config.mjs",
|
|
12
|
+
"eslint.config.cjs",
|
|
13
|
+
"vite.config.ts",
|
|
14
|
+
"vite.config.js",
|
|
15
|
+
"vite.config.mjs",
|
|
16
|
+
"next.config.ts",
|
|
17
|
+
"next.config.js",
|
|
18
|
+
"next.config.mjs",
|
|
19
|
+
];
|
|
20
|
+
const rootDocCandidates = ["README.md", "README.mdx", "docs.md"];
|
|
21
|
+
const pathExists = async (targetPath) => {
|
|
22
|
+
try {
|
|
23
|
+
await fs.access(targetPath);
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
const listMarkdownFilesRecursively = async (directoryPath) => {
|
|
31
|
+
const directoryEntries = await fs.readdir(directoryPath, { withFileTypes: true });
|
|
32
|
+
const filePaths = [];
|
|
33
|
+
for (const directoryEntry of directoryEntries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
34
|
+
const entryPath = path.join(directoryPath, directoryEntry.name);
|
|
35
|
+
if (directoryEntry.isDirectory()) {
|
|
36
|
+
filePaths.push(...(await listMarkdownFilesRecursively(entryPath)));
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (directoryEntry.isFile() && /\.(md|mdx)$/iu.test(directoryEntry.name)) {
|
|
40
|
+
filePaths.push(entryPath);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return filePaths;
|
|
44
|
+
};
|
|
45
|
+
export const discoverProjectEvidence = async (projectPath) => {
|
|
46
|
+
const resolvedProjectPath = path.resolve(projectPath);
|
|
47
|
+
const topLevelDirectories = [];
|
|
48
|
+
const evidenceFiles = [];
|
|
49
|
+
for (const directoryName of topLevelDirectoryCandidates) {
|
|
50
|
+
const candidatePath = path.join(resolvedProjectPath, directoryName);
|
|
51
|
+
if (await pathExists(candidatePath)) {
|
|
52
|
+
topLevelDirectories.push(directoryName);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
for (const fileName of configFileCandidates) {
|
|
56
|
+
const candidatePath = path.join(resolvedProjectPath, fileName);
|
|
57
|
+
if (await pathExists(candidatePath)) {
|
|
58
|
+
evidenceFiles.push({
|
|
59
|
+
kind: "config",
|
|
60
|
+
relativePath: fileName,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
for (const fileName of rootDocCandidates) {
|
|
65
|
+
const candidatePath = path.join(resolvedProjectPath, fileName);
|
|
66
|
+
if (await pathExists(candidatePath)) {
|
|
67
|
+
evidenceFiles.push({
|
|
68
|
+
kind: "doc",
|
|
69
|
+
relativePath: fileName,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const docsDirectoryPath = path.join(resolvedProjectPath, "docs");
|
|
74
|
+
if (await pathExists(docsDirectoryPath)) {
|
|
75
|
+
const docsFilePaths = await listMarkdownFilesRecursively(docsDirectoryPath);
|
|
76
|
+
for (const docsFilePath of docsFilePaths.slice(0, 12)) {
|
|
77
|
+
evidenceFiles.push({
|
|
78
|
+
kind: "doc",
|
|
79
|
+
relativePath: path.relative(resolvedProjectPath, docsFilePath),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
topLevelDirectories: topLevelDirectories.sort((left, right) => left.localeCompare(right)),
|
|
85
|
+
evidenceFiles: evidenceFiles.sort((left, right) => left.relativePath.localeCompare(right.relativePath)),
|
|
86
|
+
};
|
|
87
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
const execFileAsync = promisify(execFile);
|
|
4
|
+
export const discoverRecentCommits = async (projectPath) => {
|
|
5
|
+
try {
|
|
6
|
+
const { stdout } = await execFileAsync("git", ["-C", projectPath, "log", "-n", "5", "--pretty=format:%h%x09%s"], {
|
|
7
|
+
cwd: projectPath,
|
|
8
|
+
});
|
|
9
|
+
return stdout
|
|
10
|
+
.split("\n")
|
|
11
|
+
.map((line) => line.trim())
|
|
12
|
+
.filter(Boolean)
|
|
13
|
+
.map((line) => {
|
|
14
|
+
const [shortHash, ...subjectParts] = line.split("\t");
|
|
15
|
+
if (!shortHash) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
shortHash,
|
|
20
|
+
subject: subjectParts.join("\t"),
|
|
21
|
+
};
|
|
22
|
+
})
|
|
23
|
+
.filter((commit) => commit !== null && commit.subject.length > 0);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
};
|