@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,159 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
export const resolveBankPaths = (root) => ({
|
|
3
|
+
root,
|
|
4
|
+
manifestFile: path.join(root, "manifest.json"),
|
|
5
|
+
auditDirectory: path.join(root, "audit"),
|
|
6
|
+
auditEventsFile: path.join(root, "audit", "events.ndjson"),
|
|
7
|
+
sharedDirectory: path.join(root, "shared"),
|
|
8
|
+
sharedRulesDirectory: path.join(root, "shared", "rules"),
|
|
9
|
+
sharedSkillsDirectory: path.join(root, "shared", "skills"),
|
|
10
|
+
projectsDirectory: path.join(root, "projects"),
|
|
11
|
+
mcpDirectory: path.join(root, "mcp"),
|
|
12
|
+
integrationsDirectory: path.join(root, "integrations"),
|
|
13
|
+
mcpServerConfigFile: path.join(root, "mcp", "server.json"),
|
|
14
|
+
integrationFile: (providerId) => path.join(root, "integrations", `${providerId}.json`),
|
|
15
|
+
projectDirectory: (projectId) => path.join(root, "projects", projectId),
|
|
16
|
+
projectManifestFile: (projectId) => path.join(root, "projects", projectId, "manifest.json"),
|
|
17
|
+
projectStateFile: (projectId) => path.join(root, "projects", projectId, "state.json"),
|
|
18
|
+
projectRulesDirectory: (projectId) => path.join(root, "projects", projectId, "rules"),
|
|
19
|
+
projectSkillsDirectory: (projectId) => path.join(root, "projects", projectId, "skills"),
|
|
20
|
+
});
|
|
21
|
+
export const createStarterFiles = (paths) => [
|
|
22
|
+
{
|
|
23
|
+
filePath: path.join(paths.sharedRulesDirectory, "core", "README.md"),
|
|
24
|
+
content: `# Core Rules
|
|
25
|
+
|
|
26
|
+
Store shared, provider-agnostic rules here.
|
|
27
|
+
|
|
28
|
+
- Use subdirectories to cluster rules by topic.
|
|
29
|
+
- Keep each rule as a separate markdown file.
|
|
30
|
+
`,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
filePath: path.join(paths.sharedRulesDirectory, "core", "general.md"),
|
|
34
|
+
content: `---
|
|
35
|
+
id: shared-general-behavior
|
|
36
|
+
kind: rule
|
|
37
|
+
title: General Behavior
|
|
38
|
+
stacks: []
|
|
39
|
+
topics: [general]
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
# General Behavior
|
|
43
|
+
|
|
44
|
+
- Apply these rules as user-level guidance across repositories unless the local project clearly conflicts.
|
|
45
|
+
- Keep changes tightly scoped and prefer existing project patterns over generic rewrites.
|
|
46
|
+
- Before non-trivial work, form a short plan and validate assumptions from the real codebase.
|
|
47
|
+
- Run the most relevant checks for touched areas, or state clearly why they could not be run.
|
|
48
|
+
`,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
filePath: path.join(paths.sharedRulesDirectory, "stacks", "README.md"),
|
|
52
|
+
content: `# Stack Rules
|
|
53
|
+
|
|
54
|
+
Store stack-specific rules here.
|
|
55
|
+
|
|
56
|
+
Examples:
|
|
57
|
+
- nodejs/
|
|
58
|
+
- typescript/
|
|
59
|
+
- react/
|
|
60
|
+
`,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
filePath: path.join(paths.sharedRulesDirectory, "stacks", "nodejs", "runtime.md"),
|
|
64
|
+
content: `---
|
|
65
|
+
id: shared-nodejs-runtime
|
|
66
|
+
kind: rule
|
|
67
|
+
title: Node.js Runtime
|
|
68
|
+
stacks: [nodejs]
|
|
69
|
+
topics: [runtime]
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
# Node.js Runtime
|
|
73
|
+
|
|
74
|
+
- Respect the existing package manager and lockfile already present in the repository.
|
|
75
|
+
- Prefer stable CLI scripts from package.json over ad-hoc commands when available.
|
|
76
|
+
- Keep runtime and tooling changes backwards compatible unless the task explicitly requires a breaking change.
|
|
77
|
+
`,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
filePath: path.join(paths.sharedRulesDirectory, "stacks", "typescript", "strict-mode.md"),
|
|
81
|
+
content: `---
|
|
82
|
+
id: shared-typescript-strict-mode
|
|
83
|
+
kind: rule
|
|
84
|
+
title: TypeScript Strict Mode
|
|
85
|
+
stacks: [typescript]
|
|
86
|
+
topics: [typing]
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
# TypeScript Strict Mode
|
|
90
|
+
|
|
91
|
+
- Preserve strict typing and avoid weakening types with \`any\`, broad casts, or unchecked fallbacks.
|
|
92
|
+
- Prefer narrowing and explicit domain types when changing shared interfaces or data contracts.
|
|
93
|
+
- Run a typecheck after TypeScript changes when the project has a supported typecheck command.
|
|
94
|
+
`,
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
filePath: path.join(paths.sharedRulesDirectory, "topics", "README.md"),
|
|
98
|
+
content: `# Topic Rules
|
|
99
|
+
|
|
100
|
+
Store thematic shared rule groups here.
|
|
101
|
+
|
|
102
|
+
Examples:
|
|
103
|
+
- architecture/
|
|
104
|
+
- styling/
|
|
105
|
+
- routing/
|
|
106
|
+
`,
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
filePath: path.join(paths.sharedRulesDirectory, "providers", "README.md"),
|
|
110
|
+
content: `# Provider Rules
|
|
111
|
+
|
|
112
|
+
Store provider-specific rule variations here.
|
|
113
|
+
|
|
114
|
+
Examples:
|
|
115
|
+
- codex/
|
|
116
|
+
- cursor/
|
|
117
|
+
- claude-code/
|
|
118
|
+
`,
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
filePath: path.join(paths.sharedSkillsDirectory, "README.md"),
|
|
122
|
+
content: `# Skills
|
|
123
|
+
|
|
124
|
+
Store each skill in its own folder with a single \`SKILL.md\` file.
|
|
125
|
+
|
|
126
|
+
Examples:
|
|
127
|
+
- task-based-reading/SKILL.md
|
|
128
|
+
- adding-feature/SKILL.md
|
|
129
|
+
`,
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
filePath: path.join(paths.sharedSkillsDirectory, "shared", "task-based-reading", "SKILL.md"),
|
|
133
|
+
content: `---
|
|
134
|
+
id: shared-task-based-reading
|
|
135
|
+
kind: skill
|
|
136
|
+
title: Task-Based Reading
|
|
137
|
+
name: task-based-reading
|
|
138
|
+
description: Use when starting work in an unfamiliar repository and you need to identify the minimum relevant files quickly.
|
|
139
|
+
stacks: []
|
|
140
|
+
topics: [reading, discovery]
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
# Task-Based Reading
|
|
144
|
+
|
|
145
|
+
## When to use
|
|
146
|
+
- Starting work in a new repository or feature area.
|
|
147
|
+
- The task is broad and you need to reduce file-reading noise.
|
|
148
|
+
|
|
149
|
+
## Workflow
|
|
150
|
+
1. Read the repository README and the nearest package/config files first.
|
|
151
|
+
2. Identify the feature entrypoint, then follow imports and route registration files.
|
|
152
|
+
3. Read only the minimum rules and skills relevant to the current task before editing.
|
|
153
|
+
|
|
154
|
+
## Do not
|
|
155
|
+
- Read the entire repository before narrowing scope.
|
|
156
|
+
- Infer architecture from one file when surrounding routing or config files are available.
|
|
157
|
+
`,
|
|
158
|
+
},
|
|
159
|
+
];
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const isProjectBankPostponedUntilActive = (projectState, now = new Date()) => {
|
|
2
|
+
if (!projectState?.postponedUntil) {
|
|
3
|
+
return false;
|
|
4
|
+
}
|
|
5
|
+
return new Date(projectState.postponedUntil).getTime() > now.getTime();
|
|
6
|
+
};
|
|
7
|
+
export const isProjectBankSyncPostponed = (projectState, now = new Date()) => isProjectBankPostponedUntilActive(projectState, now);
|
|
8
|
+
export const requiresProjectBankSync = (projectState, expectedStorageVersion) => projectState?.lastSyncedStorageVersion !== expectedStorageVersion;
|
|
9
|
+
export const getProjectBankContinuationIteration = (projectState) => (projectState?.createIteration ?? 0) + 1;
|
|
10
|
+
export const resolveProjectBankLifecycleStatus = ({ projectManifest, projectState, expectedStorageVersion, now = new Date(), }) => {
|
|
11
|
+
if (projectState?.creationState === "declined") {
|
|
12
|
+
return "creation_declined";
|
|
13
|
+
}
|
|
14
|
+
if (projectManifest === null) {
|
|
15
|
+
return "missing";
|
|
16
|
+
}
|
|
17
|
+
if (projectState?.creationState === "creating") {
|
|
18
|
+
return "creation_in_progress";
|
|
19
|
+
}
|
|
20
|
+
if (requiresProjectBankSync(projectState, expectedStorageVersion) && !isProjectBankPostponedUntilActive(projectState, now)) {
|
|
21
|
+
return "sync_required";
|
|
22
|
+
}
|
|
23
|
+
return "ready";
|
|
24
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { CURRENT_STORAGE_VERSION, PROVIDER_IDS } from "./types.js";
|
|
4
|
+
const ProviderIdSchema = z.enum(PROVIDER_IDS);
|
|
5
|
+
export const MemoryBankManifestSchema = z
|
|
6
|
+
.object({
|
|
7
|
+
schemaVersion: z.literal(1),
|
|
8
|
+
storageVersion: z.union([z.literal(1), z.literal(2)]),
|
|
9
|
+
bankId: z.uuid(),
|
|
10
|
+
createdAt: z.iso.datetime(),
|
|
11
|
+
updatedAt: z.iso.datetime(),
|
|
12
|
+
enabledProviders: z.array(ProviderIdSchema),
|
|
13
|
+
defaultMcpTransport: z.literal("stdio"),
|
|
14
|
+
})
|
|
15
|
+
.strict();
|
|
16
|
+
const providerOrder = new Map(PROVIDER_IDS.map((providerId, index) => [providerId, index]));
|
|
17
|
+
export const sortProviders = (providerIds) => [...new Set(providerIds)].sort((left, right) => (providerOrder.get(left) ?? 0) - (providerOrder.get(right) ?? 0));
|
|
18
|
+
export const createManifest = (enabledProviders, now = new Date()) => {
|
|
19
|
+
const timestamp = now.toISOString();
|
|
20
|
+
return {
|
|
21
|
+
schemaVersion: 1,
|
|
22
|
+
storageVersion: CURRENT_STORAGE_VERSION,
|
|
23
|
+
bankId: randomUUID(),
|
|
24
|
+
createdAt: timestamp,
|
|
25
|
+
updatedAt: timestamp,
|
|
26
|
+
enabledProviders: sortProviders(enabledProviders),
|
|
27
|
+
defaultMcpTransport: "stdio",
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
export const updateManifest = (manifest, enabledProviders, now = new Date(), options) => ({
|
|
31
|
+
...manifest,
|
|
32
|
+
...(options?.storageVersion ? { storageVersion: options.storageVersion } : {}),
|
|
33
|
+
updatedAt: now.toISOString(),
|
|
34
|
+
enabledProviders: sortProviders(enabledProviders),
|
|
35
|
+
});
|
|
36
|
+
export const parseManifest = (value) => MemoryBankManifestSchema.parse(value);
|
|
37
|
+
export const isCurrentStorageVersion = (storageVersion) => storageVersion === CURRENT_STORAGE_VERSION;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { PROJECT_CREATION_STATES, } from "./types.js";
|
|
3
|
+
import { DETECTABLE_STACKS } from "../context/types.js";
|
|
4
|
+
import { GUIDANCE_SOURCE_STRATEGIES } from "../projects/guidanceStrategies.js";
|
|
5
|
+
const ProjectCreationStateSchema = z.enum(PROJECT_CREATION_STATES);
|
|
6
|
+
const DetectableStackSchema = z.enum(DETECTABLE_STACKS);
|
|
7
|
+
const GuidanceSourceStrategySchema = z.enum(GUIDANCE_SOURCE_STRATEGIES);
|
|
8
|
+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
9
|
+
const ConfirmedGuidanceSourceStrategySchema = z
|
|
10
|
+
.object({
|
|
11
|
+
sourceRef: z.string().trim().min(1),
|
|
12
|
+
strategy: GuidanceSourceStrategySchema,
|
|
13
|
+
note: z.string().trim().min(1).nullable(),
|
|
14
|
+
})
|
|
15
|
+
.strict();
|
|
16
|
+
export const DEFAULT_PROJECT_BANK_POSTPONE_DAYS = 1;
|
|
17
|
+
export const computeProjectBankPostponedUntil = (now, postponeDays = DEFAULT_PROJECT_BANK_POSTPONE_DAYS) => new Date(now.getTime() + postponeDays * MS_PER_DAY).toISOString();
|
|
18
|
+
export const ProjectBankManifestSchema = z
|
|
19
|
+
.object({
|
|
20
|
+
schemaVersion: z.literal(1),
|
|
21
|
+
projectId: z.string().min(1),
|
|
22
|
+
projectName: z.string().min(1),
|
|
23
|
+
projectPath: z.string().min(1),
|
|
24
|
+
detectedStacks: z.array(DetectableStackSchema).default([]),
|
|
25
|
+
createdAt: z.iso.datetime(),
|
|
26
|
+
updatedAt: z.iso.datetime(),
|
|
27
|
+
})
|
|
28
|
+
.strict();
|
|
29
|
+
export const ProjectBankStateSchema = z
|
|
30
|
+
.object({
|
|
31
|
+
schemaVersion: z.literal(1),
|
|
32
|
+
creationState: ProjectCreationStateSchema,
|
|
33
|
+
createIteration: z.number().int().nonnegative().nullable().default(null),
|
|
34
|
+
sourceStrategies: z.array(ConfirmedGuidanceSourceStrategySchema).default([]),
|
|
35
|
+
postponedUntil: z.iso.datetime().nullable(),
|
|
36
|
+
lastSyncedAt: z.iso.datetime().nullable(),
|
|
37
|
+
lastSyncedStorageVersion: z.number().int().positive().nullable(),
|
|
38
|
+
updatedAt: z.iso.datetime(),
|
|
39
|
+
})
|
|
40
|
+
.strict();
|
|
41
|
+
export const createProjectBankManifest = (projectId, projectName, projectPath, detectedStacks, now = new Date()) => {
|
|
42
|
+
const timestamp = now.toISOString();
|
|
43
|
+
return {
|
|
44
|
+
schemaVersion: 1,
|
|
45
|
+
projectId,
|
|
46
|
+
projectName,
|
|
47
|
+
projectPath,
|
|
48
|
+
detectedStacks: [...detectedStacks],
|
|
49
|
+
createdAt: timestamp,
|
|
50
|
+
updatedAt: timestamp,
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
export const createProjectBankState = (creationState, options, now = new Date()) => ({
|
|
54
|
+
schemaVersion: 1,
|
|
55
|
+
creationState,
|
|
56
|
+
createIteration: options?.createIteration ?? null,
|
|
57
|
+
sourceStrategies: options?.sourceStrategies ?? [],
|
|
58
|
+
postponedUntil: creationState === "postponed"
|
|
59
|
+
? (options?.postponedUntil ?? computeProjectBankPostponedUntil(now))
|
|
60
|
+
: (options?.postponedUntil ?? null),
|
|
61
|
+
lastSyncedAt: options?.lastSyncedAt ?? null,
|
|
62
|
+
lastSyncedStorageVersion: options?.lastSyncedStorageVersion ?? null,
|
|
63
|
+
updatedAt: now.toISOString(),
|
|
64
|
+
});
|
|
65
|
+
export const updateProjectBankState = (state, creationState, options, now = new Date()) => ({
|
|
66
|
+
...state,
|
|
67
|
+
creationState,
|
|
68
|
+
postponedUntil: creationState === "postponed"
|
|
69
|
+
? (options?.postponedUntil ?? computeProjectBankPostponedUntil(now))
|
|
70
|
+
: null,
|
|
71
|
+
updatedAt: now.toISOString(),
|
|
72
|
+
});
|
|
73
|
+
export const setProjectBankCreateIteration = (state, createIteration, now = new Date()) => ({
|
|
74
|
+
...state,
|
|
75
|
+
createIteration,
|
|
76
|
+
updatedAt: now.toISOString(),
|
|
77
|
+
});
|
|
78
|
+
export const setProjectBankSourceStrategies = (state, sourceStrategies, now = new Date()) => ({
|
|
79
|
+
...state,
|
|
80
|
+
sourceStrategies,
|
|
81
|
+
updatedAt: now.toISOString(),
|
|
82
|
+
});
|
|
83
|
+
export const markProjectBankSynced = (state, storageVersion, now = new Date()) => ({
|
|
84
|
+
...state,
|
|
85
|
+
postponedUntil: null,
|
|
86
|
+
lastSyncedAt: now.toISOString(),
|
|
87
|
+
lastSyncedStorageVersion: storageVersion,
|
|
88
|
+
updatedAt: now.toISOString(),
|
|
89
|
+
});
|
|
90
|
+
export const postponeProjectBankSync = (state, postponeDays, now = new Date()) => {
|
|
91
|
+
const postponedUntil = new Date(now);
|
|
92
|
+
postponedUntil.setDate(postponedUntil.getDate() + postponeDays);
|
|
93
|
+
return {
|
|
94
|
+
...state,
|
|
95
|
+
postponedUntil: postponedUntil.toISOString(),
|
|
96
|
+
updatedAt: now.toISOString(),
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
export const updateProjectBankManifest = (manifest, detectedStacks, now = new Date()) => ({
|
|
100
|
+
...manifest,
|
|
101
|
+
detectedStacks: [...detectedStacks],
|
|
102
|
+
updatedAt: now.toISOString(),
|
|
103
|
+
});
|
|
104
|
+
export const parseProjectBankManifest = (value) => ProjectBankManifestSchema.parse(value);
|
|
105
|
+
export const parseProjectBankState = (value) => ProjectBankStateSchema.parse(value);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export const PROVIDER_IDS = ["codex", "cursor", "claude-code"];
|
|
2
|
+
export const STORAGE_VERSIONS = [1, 2];
|
|
3
|
+
export const CURRENT_STORAGE_VERSION = 2;
|
|
4
|
+
export const ENTRY_KINDS = ["rules", "skills"];
|
|
5
|
+
export const ENTRY_SCOPES = ["shared", "project"];
|
|
6
|
+
export const PROJECT_CREATION_STATES = ["unknown", "postponed", "declined", "creating", "ready"];
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { parseCanonicalRuleDocument, parseCanonicalSkillDocument } from "../bank/canonicalEntry.js";
|
|
2
|
+
import { ValidationError } from "../../shared/errors.js";
|
|
3
|
+
const normalizeCatalogEntryPath = (kind, layer, entryPath) => {
|
|
4
|
+
if (kind !== "skills") {
|
|
5
|
+
return entryPath;
|
|
6
|
+
}
|
|
7
|
+
const normalizedEntryPath = entryPath.replaceAll("\\", "/");
|
|
8
|
+
const scopePrefix = `${layer}/`;
|
|
9
|
+
return normalizedEntryPath.startsWith(scopePrefix)
|
|
10
|
+
? normalizedEntryPath.slice(scopePrefix.length)
|
|
11
|
+
: normalizedEntryPath;
|
|
12
|
+
};
|
|
13
|
+
const isDocumentationFile = (entryPath) => {
|
|
14
|
+
const normalizedEntryPath = entryPath.replaceAll("\\", "/").toLowerCase();
|
|
15
|
+
return normalizedEntryPath.endsWith("/readme.md") || normalizedEntryPath === "readme.md";
|
|
16
|
+
};
|
|
17
|
+
const matchesStacks = (entryStacks, detectedStacks) => {
|
|
18
|
+
if (entryStacks.length === 0) {
|
|
19
|
+
return {
|
|
20
|
+
selected: true,
|
|
21
|
+
reason: "Always-on canonical entry.",
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
for (const stack of entryStacks) {
|
|
25
|
+
if (detectedStacks.includes(stack)) {
|
|
26
|
+
return {
|
|
27
|
+
selected: true,
|
|
28
|
+
reason: `Matches canonical stack metadata: ${stack}.`,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
selected: false,
|
|
34
|
+
reason: "",
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
const parseRuleEntry = (layer, entryPath, content) => {
|
|
38
|
+
try {
|
|
39
|
+
return parseCanonicalRuleDocument(content);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
const message = error instanceof Error ? error.message : "Unknown canonical rule parsing error.";
|
|
43
|
+
throw new ValidationError(`Invalid canonical rule at ${layer}/${entryPath}: ${message}`);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const parseSkillEntry = (layer, entryPath, content) => {
|
|
47
|
+
try {
|
|
48
|
+
return parseCanonicalSkillDocument(content);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
const message = error instanceof Error ? error.message : "Unknown canonical skill parsing error.";
|
|
52
|
+
throw new ValidationError(`Invalid canonical skill at ${layer}/${entryPath}: ${message}`);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
export const loadResolvedContextEntries = async (repository, layer, kind, detectedStacks, projectId) => {
|
|
56
|
+
const entries = await repository.listLayerEntries(layer, kind, projectId);
|
|
57
|
+
const selectedEntries = [];
|
|
58
|
+
for (const entry of entries) {
|
|
59
|
+
if (isDocumentationFile(entry.path)) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const content = await repository.readLayerEntry(layer, kind, entry.path, projectId);
|
|
63
|
+
const metadata = kind === "rules"
|
|
64
|
+
? parseRuleEntry(layer, entry.path, content).frontmatter
|
|
65
|
+
: parseSkillEntry(layer, entry.path, content).frontmatter;
|
|
66
|
+
const selection = matchesStacks(metadata.stacks, detectedStacks);
|
|
67
|
+
if (!selection.selected) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
selectedEntries.push({
|
|
71
|
+
layer,
|
|
72
|
+
path: entry.path,
|
|
73
|
+
reason: selection.reason,
|
|
74
|
+
content,
|
|
75
|
+
metadata,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return selectedEntries;
|
|
79
|
+
};
|
|
80
|
+
export const assertUniqueResolvedEntryIds = (entries, layer, kind) => {
|
|
81
|
+
const pathsById = new Map();
|
|
82
|
+
for (const entry of entries) {
|
|
83
|
+
const existingPaths = pathsById.get(entry.metadata.id) ?? [];
|
|
84
|
+
existingPaths.push(entry.path);
|
|
85
|
+
pathsById.set(entry.metadata.id, existingPaths);
|
|
86
|
+
}
|
|
87
|
+
for (const [entryId, entryPaths] of pathsById) {
|
|
88
|
+
if (entryPaths.length > 1) {
|
|
89
|
+
throw new ValidationError(`Duplicate canonical ${kind.slice(0, -1)} id "${entryId}" in ${layer} layer: ${entryPaths.join(", ")}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
export const mergeResolvedLayerEntries = (sharedEntries, projectEntries) => {
|
|
94
|
+
const mergedEntries = new Map();
|
|
95
|
+
for (const entry of sharedEntries) {
|
|
96
|
+
mergedEntries.set(entry.metadata.id, entry);
|
|
97
|
+
}
|
|
98
|
+
for (const entry of projectEntries) {
|
|
99
|
+
mergedEntries.set(entry.metadata.id, entry);
|
|
100
|
+
}
|
|
101
|
+
return [...mergedEntries.values()].sort((left, right) => {
|
|
102
|
+
const titleComparison = left.metadata.title.localeCompare(right.metadata.title);
|
|
103
|
+
if (titleComparison !== 0) {
|
|
104
|
+
return titleComparison;
|
|
105
|
+
}
|
|
106
|
+
return left.path.localeCompare(right.path);
|
|
107
|
+
});
|
|
108
|
+
};
|
|
109
|
+
const stripFrontmatter = (content) => content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/u, "");
|
|
110
|
+
const toPreview = (content) => {
|
|
111
|
+
const body = stripFrontmatter(content);
|
|
112
|
+
const lines = body
|
|
113
|
+
.split(/\r?\n/u)
|
|
114
|
+
.map((line) => line.trim())
|
|
115
|
+
.filter((line) => line.length > 0 && !line.startsWith("#"));
|
|
116
|
+
const previewSource = lines[0] ?? null;
|
|
117
|
+
if (previewSource === null) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
return previewSource.length > 180 ? `${previewSource.slice(0, 177)}...` : previewSource;
|
|
121
|
+
};
|
|
122
|
+
export const selectAlwaysOnRules = (entries) => entries
|
|
123
|
+
.filter((entry) => entry.metadata.kind === "rule" && entry.metadata.stacks.length === 0)
|
|
124
|
+
.map((entry) => ({
|
|
125
|
+
scope: entry.layer,
|
|
126
|
+
path: entry.path,
|
|
127
|
+
id: entry.metadata.id,
|
|
128
|
+
title: entry.metadata.title,
|
|
129
|
+
topics: [...entry.metadata.topics],
|
|
130
|
+
content: entry.content,
|
|
131
|
+
}));
|
|
132
|
+
export const excludeAlwaysOnRules = (entries) => entries.filter((entry) => !(entry.metadata.kind === "rule" && entry.metadata.stacks.length === 0));
|
|
133
|
+
export const buildResolvedContextCatalog = (kind, entries) => entries.map((entry) => ({
|
|
134
|
+
scope: entry.layer,
|
|
135
|
+
kind,
|
|
136
|
+
path: normalizeCatalogEntryPath(kind, entry.layer, entry.path),
|
|
137
|
+
title: entry.metadata.title,
|
|
138
|
+
topics: [...entry.metadata.topics],
|
|
139
|
+
description: entry.metadata.kind === "skill" ? entry.metadata.description : toPreview(entry.content),
|
|
140
|
+
}));
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { DEFAULT_PROJECT_BANK_POSTPONE_DAYS } from "../bank/project.js";
|
|
2
|
+
const renderPostponeDays = (days) => `${days} day${days === 1 ? "" : "s"}`;
|
|
3
|
+
const renderScopedEntryPath = (scope, entryPath) => entryPath.startsWith(`${scope}/`) ? entryPath : `${scope}/${entryPath}`;
|
|
4
|
+
const renderAlwaysOnRules = (rules) => {
|
|
5
|
+
if (rules.length === 0) {
|
|
6
|
+
return `## Always-On Rules
|
|
7
|
+
|
|
8
|
+
No always-on rules matched for this repository.`;
|
|
9
|
+
}
|
|
10
|
+
const blocks = rules.map((rule) => `### ${renderScopedEntryPath(rule.scope, rule.path)}
|
|
11
|
+
|
|
12
|
+
${rule.content.trim()}`);
|
|
13
|
+
return `## Always-On Rules
|
|
14
|
+
|
|
15
|
+
${blocks.join("\n\n")}`;
|
|
16
|
+
};
|
|
17
|
+
const renderCatalogSummary = (title, entries) => {
|
|
18
|
+
if (entries.length === 0) {
|
|
19
|
+
return `- ${title}: none matched.`;
|
|
20
|
+
}
|
|
21
|
+
const previewPaths = entries.slice(0, 3).map((entry) => renderScopedEntryPath(entry.scope, entry.path));
|
|
22
|
+
const previewSuffix = entries.length > 3 ? `, +${entries.length - 3} more` : "";
|
|
23
|
+
return `- ${title}: ${entries.length} entries. Examples: ${previewPaths.join(", ")}${previewSuffix}.`;
|
|
24
|
+
};
|
|
25
|
+
const renderReferenceProjects = (projectPaths) => projectPaths.map((projectPath, index) => `${index + 1}. ${projectPath}`).join("\n");
|
|
26
|
+
export const buildReadyContextText = ({ projectPath, detectedStacks, alwaysOnRules, rulesCatalog, skillsCatalog, }) => {
|
|
27
|
+
const detectedStacksLine = detectedStacks.length > 0
|
|
28
|
+
? `Detected stack signals: ${detectedStacks.join(", ")}.`
|
|
29
|
+
: "No stable stack signals were detected automatically.";
|
|
30
|
+
return `Use the following AI Guidance Bank context catalog as the primary user-managed context and guidance layer for this repository.
|
|
31
|
+
|
|
32
|
+
Repository: ${projectPath}
|
|
33
|
+
${detectedStacksLine}
|
|
34
|
+
|
|
35
|
+
AI Guidance Bank stores durable rules, skills, and reusable project guidance for this repository.
|
|
36
|
+
|
|
37
|
+
Always-on rules are expanded inline below. Other canonical entries are listed in the structured rule and skill catalogs; call \`read_entry\` when you need the full canonical document.
|
|
38
|
+
|
|
39
|
+
${renderAlwaysOnRules(alwaysOnRules)}
|
|
40
|
+
|
|
41
|
+
## Catalog Summary
|
|
42
|
+
|
|
43
|
+
${renderCatalogSummary("Rules", rulesCatalog)}
|
|
44
|
+
${renderCatalogSummary("Skills", skillsCatalog)}`;
|
|
45
|
+
};
|
|
46
|
+
export const buildUpgradeRequiredContextText = ({ bankRoot, sourceRoot, storageVersion, expectedStorageVersion, }) => `AI Guidance Bank update is required before resolving repository context.
|
|
47
|
+
|
|
48
|
+
Current AI Guidance Bank source:
|
|
49
|
+
- \`${sourceRoot}\`
|
|
50
|
+
|
|
51
|
+
Target AI Guidance Bank root:
|
|
52
|
+
- \`${bankRoot}\`
|
|
53
|
+
|
|
54
|
+
Storage version:
|
|
55
|
+
- current: ${storageVersion}
|
|
56
|
+
- expected: ${expectedStorageVersion}
|
|
57
|
+
|
|
58
|
+
What to do:
|
|
59
|
+
- Ask the user whether to update AI Guidance Bank now
|
|
60
|
+
- If the user agrees, call \`upgrade_bank\`
|
|
61
|
+
- After the upgrade finishes, call \`resolve_context\` again
|
|
62
|
+
- Do not start project-bank creation, sync, or normal repository-context work until the bank-level update is complete`;
|
|
63
|
+
export const buildSharedFallbackContextText = ({ projectPath, detectedStacks, alwaysOnRules, rulesCatalog, skillsCatalog, }) => {
|
|
64
|
+
const detectedStacksLine = detectedStacks.length > 0
|
|
65
|
+
? `Detected stack signals: ${detectedStacks.join(", ")}.`
|
|
66
|
+
: "No stable stack signals were detected automatically.";
|
|
67
|
+
return `Shared AI Guidance Bank context is available even though this repository does not have a project-specific bank yet.
|
|
68
|
+
|
|
69
|
+
Repository: ${projectPath}
|
|
70
|
+
${detectedStacksLine}
|
|
71
|
+
|
|
72
|
+
These entries come from the shared layer only. Project-specific entries will appear after a project bank is created.
|
|
73
|
+
|
|
74
|
+
AI Guidance Bank stores durable rules, skills, and reusable shared guidance.
|
|
75
|
+
|
|
76
|
+
${renderAlwaysOnRules(alwaysOnRules)}
|
|
77
|
+
|
|
78
|
+
## Catalog Summary
|
|
79
|
+
|
|
80
|
+
${renderCatalogSummary("Rules", rulesCatalog)}
|
|
81
|
+
${renderCatalogSummary("Skills", skillsCatalog)}`;
|
|
82
|
+
};
|
|
83
|
+
export const buildMissingContextText = ({ referenceProjectPaths, creationState, postponedUntil, sharedContextText, }) => {
|
|
84
|
+
const referenceSection = referenceProjectPaths.length > 0
|
|
85
|
+
? `\n\nBefore creating a new project AI Guidance Bank, offer these existing project banks as optional reference bases:\n${renderReferenceProjects(referenceProjectPaths)}`
|
|
86
|
+
: "";
|
|
87
|
+
const userInteractionSection = creationState === "postponed"
|
|
88
|
+
? `No project AI Guidance Bank exists for this repository yet, and AI Guidance Bank creation was previously postponed until ${postponedUntil}. Continue the current task normally and do not ask again about project-bank creation until that time has passed. If the user explicitly asks for AI Guidance Bank setup or project-bank editing before then, call \`create_bank\`.`
|
|
89
|
+
: `No project AI Guidance Bank exists for this repository yet. Continue the current task normally, but in every useful final response append one short explicit closing question about project-bank creation until the user chooses one of these outcomes: create now, postpone, or decline. Use this question shape: "Create the project AI Guidance Bank now, or postpone the question for ${renderPostponeDays(DEFAULT_PROJECT_BANK_POSTPONE_DAYS)} or longer?" If the user wants to postpone but does not give a duration, store a ${renderPostponeDays(DEFAULT_PROJECT_BANK_POSTPONE_DAYS)} postponement by calling \`set_project_state\` with \`creationState: "postponed"\`.`;
|
|
90
|
+
const sharedSection = sharedContextText ? `\n\n${sharedContextText}` : "";
|
|
91
|
+
return `${userInteractionSection}${referenceSection}${sharedSection}
|
|
92
|
+
|
|
93
|
+
Internal next steps after the user answers:
|
|
94
|
+
- If the user wants to create it, call \`create_bank\`.
|
|
95
|
+
- If the user wants to postpone the question for later, record that choice with \`set_project_state\` using \`creationState: "postponed"\` and include either \`postponeDays\` or \`postponedUntil\`. If the user gives no duration, default to ${renderPostponeDays(DEFAULT_PROJECT_BANK_POSTPONE_DAYS)}.
|
|
96
|
+
- If the user does not want to be asked again, record that choice with \`set_project_state\` using \`creationState: "declined"\`.
|
|
97
|
+
- After the user decision is recorded, call \`resolve_context\` again.`;
|
|
98
|
+
};
|
|
99
|
+
export const buildCreatingContextText = ({ phase, nextIteration, }) => `Continue the create flow at phase \`${phase}\`. Use \`phase\` as the primary guide, treat \`iteration\` as diagnostic only, and prefer \`create_bank.apply\` for batched writes inside the guided flow. Call \`create_bank\` with \`iteration: ${nextIteration}\` and \`stepCompleted: true\` after the current step is actually complete.`;
|
|
100
|
+
export const buildDeclinedContextText = () => "Project AI Guidance Bank creation was previously declined for this repository. Do not ask again unless the user explicitly requests AI Guidance Bank creation. If the user later wants to create it, call `create_bank` and then call `resolve_context` again.";
|
|
101
|
+
export const buildSyncRequiredContextText = ({ postponedUntil, }) => {
|
|
102
|
+
const postponeLine = postponedUntil
|
|
103
|
+
? `A previous sync was postponed until ${postponedUntil}, but that deferral has now expired.`
|
|
104
|
+
: "This project AI Guidance Bank has not been synced to the current AI Guidance Bank storage version yet.";
|
|
105
|
+
return `Project AI Guidance Bank synchronization is required before using the project-specific bank.
|
|
106
|
+
|
|
107
|
+
${postponeLine}
|
|
108
|
+
|
|
109
|
+
Sync only reconciles the existing project bank with the current AI Guidance Bank storage version. It does not create a new bank and does not replace the normal create or improve flow.
|
|
110
|
+
|
|
111
|
+
Ask the user whether to synchronize the project AI Guidance Bank now or postpone it.
|
|
112
|
+
- If the user wants to sync now, call \`sync_bank\` with \`action: "run"\`.
|
|
113
|
+
- If the user wants to postpone, call \`sync_bank\` with \`action: "postpone"\`.
|
|
114
|
+
- After that, call \`resolve_context\` again.
|
|
115
|
+
- If the user later wants to create or improve project-specific content, continue with \`create_bank\` after synchronization is no longer required.`;
|
|
116
|
+
};
|