engineering-memory 1.10.3 → 1.10.5

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.
@@ -1,8 +1,8 @@
1
1
  import { acceptedContent, CLIENT_CAPABILITIES_META_KEY, inputRequired, inputResponse, } from '@modelcontextprotocol/server';
2
2
  import { questionnaireAnswerSchema, } from '../runtime/questionnaire-store.js';
3
- export async function askQuestionnaire(server, service, input, context) {
3
+ export async function askQuestionnaire(server, service, input, context, previousDefinitions = []) {
4
4
  try {
5
- return await present(server, service, await service.questionnaireAsk(input), input.repoRoot, context);
5
+ return await present(server, service, await service.questionnaireAsk(input, previousDefinitions), input.repoRoot, context, input.preparation);
6
6
  }
7
7
  catch (error) {
8
8
  return failure(error);
@@ -10,13 +10,20 @@ export async function askQuestionnaire(server, service, input, context) {
10
10
  }
11
11
  export async function resumeQuestionnaire(server, service, input, context) {
12
12
  try {
13
- return await present(server, service, await service.questionnaireResume(input), input.repoRoot, context);
13
+ return await present(server, service, await service.questionnaireResume(input), input.repoRoot, context, input.preparation);
14
14
  }
15
15
  catch (error) {
16
16
  return failure(error);
17
17
  }
18
18
  }
19
- async function present(server, service, record, repoRoot, context) {
19
+ async function present(server, service, record, repoRoot, context, preparation) {
20
+ if (record.status === 'withdrawn')
21
+ return result({
22
+ questionnaireId: record.questionnaireId,
23
+ status: 'withdrawn',
24
+ answerAvailable: false,
25
+ nextAction: 'This decision was explicitly withdrawn. Do not continue dependent work. Use a new questionnaireId or onboarding requestKey only if the user resumes that work.',
26
+ });
20
27
  if (record.status === 'answered')
21
28
  return answered(record, record.answer, true);
22
29
  if (context.mcpReq.signal.aborted)
@@ -29,6 +36,7 @@ async function present(server, service, record, repoRoot, context) {
29
36
  return pending(record, 'invalid_response');
30
37
  const resolved = await service.questionnaireAccept({
31
38
  repoRoot,
39
+ preparation,
32
40
  questionnaireId: record.questionnaireId,
33
41
  requestKey: record.requestKey,
34
42
  scope: record.scope,
@@ -45,36 +53,47 @@ async function present(server, service, record, repoRoot, context) {
45
53
  const elicitation = capabilities?.elicitation;
46
54
  if (!elicitation || (!elicitation.form && elicitation.url !== undefined))
47
55
  return pending(record, 'native_form_unavailable');
56
+ const copy = questionnaireCopy[record.language ?? 'en'];
48
57
  return inputRequired({
49
58
  inputRequests: {
50
59
  [record.requestKey]: inputRequired.elicit({
51
60
  mode: 'form',
52
- message: `${record.message}\n\nChoose an answer to resolve this decision. Closing or cancelling the form leaves it pending.${record.allowFreeText ? ' For another answer choose Other and fill in text. Free text is not stored and cannot be replayed.' : ''}`,
61
+ message: `${record.message}\n\n${copy.pending}${record.allowFreeText ? copy.freeText : ''}${record.textField ? copy.storedText : ''}`,
53
62
  requestedSchema: {
54
63
  type: 'object',
55
64
  properties: {
56
65
  choice: {
57
66
  type: 'string',
58
- title: 'Answer',
67
+ title: copy.answer,
59
68
  enum: [
60
69
  ...record.options.map((option) => option.id),
61
70
  ...(record.allowFreeText ? ['__other__'] : []),
62
71
  ],
63
72
  enumNames: [
64
73
  ...record.options.map((option) => option.label),
65
- ...(record.allowFreeText ? ['Other'] : []),
74
+ ...(record.allowFreeText ? [copy.other] : []),
66
75
  ],
67
76
  },
68
77
  ...(record.allowFreeText
69
78
  ? {
70
79
  text: {
71
80
  type: 'string',
72
- title: 'Other answer',
81
+ title: copy.otherAnswer,
73
82
  minLength: 1,
74
83
  maxLength: 2000,
75
84
  },
76
85
  }
77
86
  : {}),
87
+ ...(record.textField
88
+ ? {
89
+ text: {
90
+ type: 'string',
91
+ title: record.textField.title,
92
+ minLength: 1,
93
+ maxLength: record.textField.maxLength,
94
+ },
95
+ }
96
+ : {}),
78
97
  },
79
98
  required: ['choice'],
80
99
  },
@@ -82,6 +101,24 @@ async function present(server, service, record, repoRoot, context) {
82
101
  },
83
102
  });
84
103
  }
104
+ const questionnaireCopy = {
105
+ en: {
106
+ pending: 'Choose an answer to resolve this decision. Closing or cancelling the form leaves it pending.',
107
+ freeText: ' For another answer choose Other and fill in text. Free text is not stored and cannot be replayed.',
108
+ storedText: ' The bounded text field is stored as part of this exact decision and is replayed after restart.',
109
+ answer: 'Answer',
110
+ other: 'Other',
111
+ otherAnswer: 'Other answer',
112
+ },
113
+ tr: {
114
+ pending: 'Kararını kaydetmek için bir seçenek seç. Formu kapatmak veya iptal etmek onay sayılmaz; soru beklemede kalır.',
115
+ freeText: ' Farklı bir yanıt için Diğer seçeneğini seçip metin alanını doldur. Serbest yanıt saklanmaz ve daha sonra geri getirilemez.',
116
+ storedText: ' Bu metin alanındaki yanıt kararınla birlikte saklanır; yeniden başlattığında korunur.',
117
+ answer: 'Yanıt',
118
+ other: 'Diğer',
119
+ otherAnswer: 'Diğer yanıt',
120
+ },
121
+ };
85
122
  function pending(record, reason) {
86
123
  return result({
87
124
  questionnaireId: record.questionnaireId,
@@ -1,4 +1,5 @@
1
1
  import { McpServer } from '@modelcontextprotocol/server';
2
+ import { registerOnboardingTools } from './onboarding-tools.js';
2
3
  import { registerEngineeringMemoryTools, registerQuestionnaireTools } from './tool-definitions.js';
3
4
  export function createEngineeringMemoryServer(service) {
4
5
  const server = new McpServer({
@@ -7,6 +8,7 @@ export function createEngineeringMemoryServer(service) {
7
8
  });
8
9
  registerEngineeringMemoryTools(server, service);
9
10
  registerQuestionnaireTools(server, service);
11
+ registerOnboardingTools(server, service);
10
12
  return server;
11
13
  }
12
14
  //# sourceMappingURL=server.js.map
@@ -49,6 +49,7 @@ export const engineeringMemoryToolNames = [
49
49
  'audit.list',
50
50
  'questionnaire.ask',
51
51
  'questionnaire.resume',
52
+ 'questionnaire.withdraw',
52
53
  'session.entry',
53
54
  'session.set_decision',
54
55
  'session.decline_update',
@@ -57,6 +58,24 @@ export const engineeringMemoryToolNames = [
57
58
  'session.resume',
58
59
  'context.prepare_change',
59
60
  'context.refresh',
61
+ 'project.inspect',
62
+ 'project.onboard',
63
+ 'project.onboarding_status',
64
+ 'project.rework_plan',
65
+ 'project.initialize',
66
+ 'memory.source',
67
+ 'memory.sync_start',
68
+ 'memory.sync_status',
69
+ 'memory.sync_inventory',
70
+ 'memory.sync_plan',
71
+ 'memory.sync_next',
72
+ 'memory.sync_submit',
73
+ 'memory.sync_reopen',
74
+ 'memory.sync_delta',
75
+ 'memory.sync_verify',
76
+ 'memory.catalog',
77
+ 'memory.read_revisions',
78
+ 'memory.review_batch',
60
79
  'memory.query',
61
80
  'memory.history',
62
81
  'memory.propose_revision',
@@ -121,15 +140,49 @@ const reconciliationEntry = z.object({
121
140
  reason: z.string().optional(),
122
141
  });
123
142
  export function registerQuestionnaireTools(server, service) {
143
+ server.registerTool('questionnaire.withdraw', {
144
+ description: 'Withdraw a pending decision only when the user explicitly cancels that decision or its owning workflow. This is not approval. An answered decision is immutable. Interruption, timeout and dismissal alone never authorize withdrawal.',
145
+ inputSchema: z.strictObject({
146
+ repoRoot: optionalRepoRoot,
147
+ questionnaireId: z.string().regex(/^[A-Za-z0-9_-]{1,100}$/),
148
+ preparation: z.boolean().optional(),
149
+ }),
150
+ }, async (input) => {
151
+ try {
152
+ const record = await service.questionnaireWithdraw(input);
153
+ return toolResult({
154
+ ok: true,
155
+ data: {
156
+ questionnaireId: record.questionnaireId,
157
+ status: record.status,
158
+ answerAvailable: record.answerAvailable ?? false,
159
+ },
160
+ });
161
+ }
162
+ catch {
163
+ return toolResult({
164
+ ok: false,
165
+ error: {
166
+ kind: 'bridge_error',
167
+ message: 'Cannot withdraw this questionnaire. Check the account and repository with session.entry and the exact questionnaireId with questionnaire.resume.',
168
+ retryable: false,
169
+ },
170
+ });
171
+ }
172
+ });
124
173
  server.registerTool('questionnaire.ask', {
125
- description: 'Persist a required decision before displaying a native questionnaire. Use a questionnaireId unique to this decision occurrence or task, and reuse it only for identical retries. A later decision or changed wording/options requires a new id. Only a schema-validated native acceptance answers it. Dismissal, timeout and missing replies remain pending without expiry. Never supply answers in tool arguments. Do not put personal information or secrets in the question or options. Free text is returned once and never stored; use explicit options for replayable decisions.',
126
- inputSchema: questionnaireDefinitionSchema.extend({ repoRoot: optionalRepoRoot }),
174
+ description: 'Persist a required decision before displaying a native questionnaire. Write the message and labels in the conversation language and set language (tr or en) for the native form help. Use a questionnaireId unique to this decision occurrence or task, and reuse it only for identical retries. A later decision or changed wording/options requires a new id. Only a schema-validated native acceptance answers it. Dismissal, timeout and missing replies remain pending without expiry. Never supply answers in tool arguments. Do not put personal information or secrets in the question or options. Free text is returned once and never stored; use explicit options for replayable decisions.',
175
+ inputSchema: questionnaireDefinitionSchema.extend({
176
+ repoRoot: optionalRepoRoot,
177
+ preparation: z.boolean().optional(),
178
+ }),
127
179
  }, async (input, context) => await askQuestionnaire(server, service, input, context));
128
180
  server.registerTool('questionnaire.resume', {
129
181
  description: 'Display the original durable questionnaire after interruption or return its saved accepted choice. No timeout or dismissal resolves a pending question. A saved free-text receipt cannot replay the text and must never be treated as a recovered answer.',
130
182
  inputSchema: z.strictObject({
131
183
  repoRoot: optionalRepoRoot,
132
184
  questionnaireId: z.string().regex(/^[A-Za-z0-9_-]{1,100}$/),
185
+ preparation: z.boolean().optional(),
133
186
  }),
134
187
  }, async (input, context) => await resumeQuestionnaire(server, service, input, context));
135
188
  }
@@ -286,6 +339,7 @@ export function registerEngineeringMemoryTools(server, service) {
286
339
  tags: stringList.optional(),
287
340
  metadata: jsonObject.optional(),
288
341
  provenance: jsonObject.optional(),
342
+ status: z.enum(['active', 'archived']).optional(),
289
343
  }),
290
344
  }, async (input) => toolResult(await service.memoryProposeRevision(input)));
291
345
  server.registerTool('memory.list_proposals', {
@@ -0,0 +1,406 @@
1
+ import { lstat, readFile, readdir } from 'node:fs/promises';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { canonicalPath, ensureManagedDirectory } from '../utilities/files.js';
4
+ import { NativeCommandRunner } from '../utilities/process.js';
5
+ export class ProjectIntake {
6
+ runner;
7
+ constructor(runner = new NativeCommandRunner()) {
8
+ this.runner = runner;
9
+ }
10
+ async inspect(repoPath) {
11
+ const requested = resolve(repoPath);
12
+ let root;
13
+ try {
14
+ const requestedInfo = await lstat(requested);
15
+ if (requestedInfo.isSymbolicLink())
16
+ throw new Error('Project path must not be a symbolic link');
17
+ root = await canonicalPath(requested);
18
+ const info = await lstat(root);
19
+ if (!info.isDirectory() || info.isSymbolicLink())
20
+ throw new Error('Project path must be a real directory');
21
+ }
22
+ catch (error) {
23
+ if (error instanceof Error &&
24
+ 'code' in error &&
25
+ error.code === 'ENOENT') {
26
+ return emptyReport(requested, 'missing', ['directory does not exist']);
27
+ }
28
+ throw error;
29
+ }
30
+ const entries = await readdir(root, { withFileTypes: true });
31
+ if (entries.length === 0)
32
+ return emptyReport(root, 'empty', ['directory exists and is empty']);
33
+ const inventory = await collectInventory(root);
34
+ const evidence = [];
35
+ const stacks = [];
36
+ for (const detected of await detectStacks(root, inventory.manifestCandidates)) {
37
+ if (stacks.includes(detected.stack))
38
+ continue;
39
+ stacks.push(detected.stack);
40
+ evidence.push(detected.evidence);
41
+ }
42
+ const hasGit = await this.runner.run('git', ['rev-parse', '--is-inside-work-tree'], {
43
+ cwd: root,
44
+ });
45
+ const applicationAreas = [
46
+ 'state',
47
+ 'services',
48
+ 'network',
49
+ 'persistence',
50
+ 'architecture',
51
+ 'screens',
52
+ 'components',
53
+ 'navigation',
54
+ ].filter((area) => inventory.areaCounts[area] > 0);
55
+ const hasApplicationAreas = applicationAreas.length > 0;
56
+ if (hasApplicationAreas)
57
+ evidence.push(`application structural sources found in ${applicationAreas.join(', ')}`);
58
+ const recommendation = hasApplicationAreas
59
+ ? 'established'
60
+ : meaningfulEntries(entries).length === 0
61
+ ? 'empty'
62
+ : 'scaffold';
63
+ const reasons = stacks.length > 0
64
+ ? hasApplicationAreas
65
+ ? [`detected ${stacks.join(', ')} with application structural evidence`]
66
+ : [`detected ${stacks.join(', ')} without application structural evidence`]
67
+ : hasApplicationAreas
68
+ ? [`application structural sources found in ${applicationAreas.join(', ')}`]
69
+ : meaningfulEntries(entries).length === 0
70
+ ? ['directory contains only Git metadata']
71
+ : hasGit.exitCode === 0
72
+ ? [
73
+ 'Git repository has no recognized stack manifest or application architectural signals',
74
+ ]
75
+ : [
76
+ 'directory has content but no recognized stack manifest or application architectural signals',
77
+ ];
78
+ return { repoPath: root, recommendation, reasons, stacks, evidence, inventory };
79
+ }
80
+ async initialize(repoPath) {
81
+ const requested = resolve(repoPath);
82
+ await assertNoRepositoryAncestor(requested, this.runner);
83
+ let exists = true;
84
+ try {
85
+ const requestedInfo = await lstat(requested);
86
+ if (requestedInfo.isSymbolicLink())
87
+ throw new Error('Project path must not be a symbolic link');
88
+ }
89
+ catch (error) {
90
+ if (error instanceof Error &&
91
+ 'code' in error &&
92
+ error.code === 'ENOENT')
93
+ exists = false;
94
+ else
95
+ throw error;
96
+ }
97
+ if (!exists) {
98
+ await ensureManagedDirectory(dirname(requested), requested);
99
+ }
100
+ const root = await canonicalPath(requested);
101
+ const info = await lstat(root);
102
+ if (!info.isDirectory() || info.isSymbolicLink())
103
+ throw new Error('Project path must be a real directory');
104
+ const parentGit = await this.runner.run('git', ['rev-parse', '--show-toplevel'], { cwd: root });
105
+ if (parentGit.exitCode === 0 && resolve(parentGit.stdout.trim()) !== root)
106
+ throw new Error('Refusing to initialize inside an existing parent repository');
107
+ const gitPath = join(root, '.git');
108
+ const existingGit = await lstat(gitPath).then((entry) => {
109
+ if (entry.isSymbolicLink())
110
+ throw new Error('Refusing to use a symbolic-link .git path');
111
+ return true;
112
+ }, (error) => (error.code === 'ENOENT' ? false : Promise.reject(error)));
113
+ if (existingGit)
114
+ return { repoPath: root, initialized: false };
115
+ const result = await this.runner.run('git', ['init', '--quiet', '--'], { cwd: root });
116
+ if (result.exitCode !== 0)
117
+ throw new Error(`Git initialization failed: ${result.stderr.trim()}`);
118
+ return { repoPath: root, initialized: true };
119
+ }
120
+ }
121
+ function emptyReport(repoPath, recommendation, reasons) {
122
+ return {
123
+ repoPath,
124
+ recommendation,
125
+ reasons,
126
+ stacks: [],
127
+ evidence: [],
128
+ inventory: {
129
+ all: [],
130
+ state: [],
131
+ services: [],
132
+ network: [],
133
+ persistence: [],
134
+ architecture: [],
135
+ screens: [],
136
+ components: [],
137
+ navigation: [],
138
+ symlinks: [],
139
+ totalFiles: 0,
140
+ omittedFiles: 0,
141
+ prunedDirectories: [],
142
+ prunedDirectoryCount: 0,
143
+ omittedPrunedDirectories: 0,
144
+ sampleLimit: INVENTORY_SAMPLE_LIMIT,
145
+ areaCounts: emptyAreaCounts(),
146
+ manifestCandidates: [],
147
+ manifestCandidatesOmitted: 0,
148
+ },
149
+ };
150
+ }
151
+ async function collectInventory(root) {
152
+ const all = [];
153
+ const state = [];
154
+ const services = [];
155
+ const network = [];
156
+ const persistence = [];
157
+ const architecture = [];
158
+ const screens = [];
159
+ const components = [];
160
+ const navigation = [];
161
+ const symlinks = [];
162
+ const prunedDirectories = [];
163
+ let prunedDirectoryCount = 0;
164
+ const areaCounts = emptyAreaCounts();
165
+ const manifestCandidates = [];
166
+ let manifestCandidatesOmitted = 0;
167
+ let totalFiles = 0;
168
+ const walk = async (directory) => {
169
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
170
+ if (entry.name === '.git')
171
+ continue;
172
+ const path = join(directory, entry.name);
173
+ const relativePath = path.slice(root.length + 1).replace(/\\/g, '/');
174
+ if (entry.isSymbolicLink()) {
175
+ if (symlinks.length < INVENTORY_SAMPLE_LIMIT) {
176
+ all.push(relativePath);
177
+ symlinks.push(relativePath);
178
+ }
179
+ continue;
180
+ }
181
+ if (entry.isDirectory()) {
182
+ if (await shouldPruneDirectory(root, relativePath)) {
183
+ prunedDirectoryCount += 1;
184
+ if (prunedDirectories.length < INVENTORY_SAMPLE_LIMIT)
185
+ prunedDirectories.push(relativePath);
186
+ continue;
187
+ }
188
+ await walk(path);
189
+ continue;
190
+ }
191
+ totalFiles += 1;
192
+ const sampled = totalFiles <= INVENTORY_SAMPLE_LIMIT;
193
+ if (sampled)
194
+ all.push(relativePath);
195
+ const lower = relativePath.toLowerCase();
196
+ if (['package.json', 'pubspec.yaml', 'pom.xml', 'build.gradle', 'build.gradle.kts'].includes(entry.name.toLowerCase())) {
197
+ if (manifestCandidates.length < MANIFEST_CANDIDATE_LIMIT)
198
+ manifestCandidates.push(relativePath);
199
+ else
200
+ manifestCandidatesOmitted += 1;
201
+ }
202
+ const addArea = (area, target) => {
203
+ areaCounts[area] += 1;
204
+ if (target.length < INVENTORY_SAMPLE_LIMIT)
205
+ target.push(relativePath);
206
+ };
207
+ if (/(^|\/)(architecture|adr|adrs|design|docs?)(\/|\.|$)|(^|\/)(architecture|adr)[^/]*\.(md|ya?ml|json)$/.test(lower))
208
+ addArea('architecture', architecture);
209
+ if (!entry.isFile() ||
210
+ !/\.(?:[cm]?[jt]sx?|dart|java|kt|kts|swift|cs|go|rs|py|rb|vue|svelte|openapi|graphql)$/.test(lower) ||
211
+ /(^|\/)(node_modules|vendor|pods|\.dart_tool|\.next|\.nuxt)(\/|$)|\.(g|freezed)\.dart$|(^|\/)(__tests__|tests?|spec)(\/|$)|\.(test|spec)\.[^/]+$|_test\.[^/]+$/.test(lower) ||
212
+ isCredentialDataPath(lower))
213
+ continue;
214
+ if (/(^|\/)(state|store|stores|reducers?|providers?|bloc|cubits?)(\/|\.|$)|(?:state|store|reducer|provider|bloc|cubit)\.[^/]+$/.test(lower))
215
+ addArea('state', state);
216
+ if (/(^|\/)(service|services|use-cases?|usecases|domain)(\/|\.|$)|service\.[^/]+$/.test(lower))
217
+ addArea('services', services);
218
+ if (/(^|\/)(api|apis|network|http|graphql|routes?|controllers?|clients?)(\/|\.|$)|(?:controller|client)\.[^/]+$|\.(openapi|graphql)$/.test(lower))
219
+ addArea('network', network);
220
+ if (/(^|\/)(persistence|repositories?|storage|database|db|migrations?|entities|models?)(\/|\.|$)|(?:repository|entity|model)\.[^/]+$/.test(lower))
221
+ addArea('persistence', persistence);
222
+ if (/(^|\/)(screens?|pages?|views?)(\/|\.|$)|(^|\/)[^/]*(?:screen|page|view)\.[^/]+$/.test(lower))
223
+ addArea('screens', screens);
224
+ if (/(^|\/)(components?|widgets?)(\/|\.|$)|(^|\/)[^/]*component\.[^/]+$/.test(lower))
225
+ addArea('components', components);
226
+ if (/(^|\/)(navigation|routing|routes?|router)(\/|\.|$)|(^|\/)[^/]*(?:navigation|router|routes?)\.[^/]+$/.test(lower))
227
+ addArea('navigation', navigation);
228
+ }
229
+ };
230
+ await walk(root);
231
+ const sort = (values) => values.sort((left, right) => (left < right ? -1 : left > right ? 1 : 0));
232
+ return {
233
+ all: sort(all),
234
+ state: sort(state),
235
+ services: sort(services),
236
+ network: sort(network),
237
+ persistence: sort(persistence),
238
+ architecture: sort(architecture),
239
+ screens: sort(screens),
240
+ components: sort(components),
241
+ navigation: sort(navigation),
242
+ symlinks: sort(symlinks),
243
+ totalFiles,
244
+ omittedFiles: Math.max(0, totalFiles - INVENTORY_SAMPLE_LIMIT),
245
+ prunedDirectories: sort(prunedDirectories),
246
+ prunedDirectoryCount,
247
+ omittedPrunedDirectories: Math.max(0, prunedDirectoryCount - INVENTORY_SAMPLE_LIMIT),
248
+ sampleLimit: INVENTORY_SAMPLE_LIMIT,
249
+ areaCounts,
250
+ manifestCandidates: manifestCandidates.sort(),
251
+ manifestCandidatesOmitted,
252
+ };
253
+ }
254
+ const INVENTORY_SAMPLE_LIMIT = 1000;
255
+ const MANIFEST_CANDIDATE_LIMIT = 512;
256
+ function emptyAreaCounts() {
257
+ return {
258
+ state: 0,
259
+ services: 0,
260
+ network: 0,
261
+ persistence: 0,
262
+ architecture: 0,
263
+ screens: 0,
264
+ components: 0,
265
+ navigation: 0,
266
+ };
267
+ }
268
+ async function shouldPruneDirectory(root, relativePath) {
269
+ const segments = relativePath.toLowerCase().split('/');
270
+ const name = segments.at(-1) ?? '';
271
+ if (/^(node_modules|vendor|pods|\.venv|\.gradle|\.dart_tool|\.next|\.nuxt|coverage)$/.test(name))
272
+ return true;
273
+ if (!/^(dist|build|out|target|bin|obj)$/.test(name))
274
+ return false;
275
+ const parent = dirname(join(root, relativePath));
276
+ const names = (await readdir(parent)).map((entry) => entry.toLowerCase());
277
+ if (names.some((entry) => /^(package\.json|pubspec\.yaml|pom\.xml|build\.gradle(?:\.kts)?)$|\.(csproj|fsproj|vbproj)$/.test(entry)))
278
+ return true;
279
+ if (segments.slice(0, -1).some((segment) => /^(src|lib|app|packages)$/.test(segment)))
280
+ return false;
281
+ return !['bin', 'obj'].includes(name);
282
+ }
283
+ function isCredentialDataPath(path) {
284
+ const segments = path.split('/');
285
+ const basename = segments.at(-1) ?? '';
286
+ if (/(^|\/)(\.env(?:\.[^/]*)?)(\/|$)/.test(path) ||
287
+ /\.(pem|key|p12|pfx|jks|keystore)$/.test(path) ||
288
+ /(^|\/)id_(rsa|dsa|ecdsa|ed25519)$/.test(path))
289
+ return true;
290
+ const parentIsSensitive = segments
291
+ .slice(0, -1)
292
+ .some((segment) => /^(?:secret|secrets|credential|credentials)$/i.test(segment));
293
+ return parentIsSensitive && /\.(json|ya?ml|toml|ini|conf|cfg|txt)$/i.test(basename);
294
+ }
295
+ async function detectStacks(root, manifestCandidates) {
296
+ const detected = [];
297
+ for (const candidate of manifestCandidates) {
298
+ const directory = dirname(join(root, candidate));
299
+ const packageManifest = candidate.toLowerCase().endsWith('package.json')
300
+ ? await readJsonManifest(join(root, candidate))
301
+ : null;
302
+ const dependencies = new Set([
303
+ ...Object.keys(packageManifest?.dependencies ?? {}),
304
+ ...Object.keys(packageManifest?.devDependencies ?? {}),
305
+ ]);
306
+ const relative = directory.slice(root.length + 1).replace(/\\/g, '/') || '.';
307
+ const prefix = relative === '.' ? '' : `${relative}/`;
308
+ if (candidate.toLowerCase().endsWith('pubspec.yaml')) {
309
+ const pubspec = await readBoundedText(join(root, candidate));
310
+ if (pubspec !== null && isFlutterManifest(pubspec))
311
+ detected.push({ stack: 'Flutter', evidence: `${prefix}pubspec.yaml declares Flutter` });
312
+ }
313
+ if (candidate.toLowerCase().endsWith('package.json') && dependencies.has('@nestjs/core'))
314
+ detected.push({ stack: 'Nest', evidence: `${prefix}package.json declares @nestjs/core` });
315
+ if (candidate.toLowerCase().endsWith('package.json') &&
316
+ (dependencies.has('react') || dependencies.has('react-dom')))
317
+ detected.push({ stack: 'React', evidence: `${prefix}package.json declares React` });
318
+ if (/build\.gradle(?:\.kts)?$/i.test(candidate)) {
319
+ const gradle = await readBoundedText(join(root, candidate));
320
+ if (gradle !== null && isSpringGradleManifest(gradle))
321
+ detected.push({ stack: 'Spring', evidence: `${candidate} declares Spring` });
322
+ }
323
+ if (candidate.toLowerCase().endsWith('pom.xml')) {
324
+ const pom = await readBoundedText(join(root, candidate));
325
+ if (pom !== null && isSpringManifest(pom))
326
+ detected.push({ stack: 'Spring', evidence: `${prefix}pom.xml declares Spring` });
327
+ }
328
+ }
329
+ return detected;
330
+ }
331
+ function meaningfulEntries(entries) {
332
+ return entries.filter((entry) => entry.name !== '.git');
333
+ }
334
+ async function readBoundedText(path, maxBytes = 1024 * 1024) {
335
+ try {
336
+ const info = await lstat(path);
337
+ if (!info.isFile() || info.isSymbolicLink() || info.size > maxBytes)
338
+ return null;
339
+ return await readFile(path, 'utf8');
340
+ }
341
+ catch (error) {
342
+ if (error instanceof Error &&
343
+ 'code' in error &&
344
+ error.code === 'ENOENT')
345
+ return null;
346
+ throw error;
347
+ }
348
+ }
349
+ async function readJsonManifest(path) {
350
+ const text = await readBoundedText(path);
351
+ if (text === null)
352
+ return null;
353
+ try {
354
+ const value = JSON.parse(text);
355
+ return value !== null && typeof value === 'object'
356
+ ? value
357
+ : null;
358
+ }
359
+ catch {
360
+ return null;
361
+ }
362
+ }
363
+ function isFlutterManifest(text) {
364
+ return (/(^|\n)\s*(?:flutter|flutter_test|flutter_lints)\s*:/m.test(text) ||
365
+ /(^|\n)\s*sdk\s*:\s*flutter\s*$/m.test(text));
366
+ }
367
+ function isSpringGradleManifest(text) {
368
+ const declarations = text.replace(/\/\*[\s\S]*?\*\/|^\s*\/\/.*$/gm, '');
369
+ return /\bid\s*(?:\(\s*)?["']org\.springframework\.boot["']|\bapply\s+plugin\s*:\s*["']org\.springframework\.boot["']|["']org\.springframework(?:\.boot)?:[^"'\r\n]+["']/.test(declarations);
370
+ }
371
+ function isSpringManifest(text) {
372
+ return /org\.springframework|spring-boot-(?:starter|maven-plugin)|spring-boot-starter-parent/i.test(text);
373
+ }
374
+ async function assertNoRepositoryAncestor(path, runner) {
375
+ const target = resolve(path);
376
+ let current = target;
377
+ let checkedRepository = false;
378
+ while (true) {
379
+ try {
380
+ const info = await lstat(current);
381
+ if (info.isSymbolicLink())
382
+ throw new Error('Project path ancestry must not contain symbolic links');
383
+ if (!info.isDirectory())
384
+ throw new Error('Project path must be a real directory');
385
+ if (!checkedRepository) {
386
+ const result = await runner.run('git', ['rev-parse', '--show-toplevel'], { cwd: current });
387
+ if (result.exitCode === 0 &&
388
+ resolve(result.stdout.trim()) !== (await canonicalPath(target))) {
389
+ throw new Error('Refusing to initialize inside an existing parent repository');
390
+ }
391
+ checkedRepository = true;
392
+ }
393
+ }
394
+ catch (error) {
395
+ if (!(error instanceof Error &&
396
+ 'code' in error &&
397
+ error.code === 'ENOENT'))
398
+ throw error;
399
+ }
400
+ const parent = dirname(current);
401
+ if (parent === current)
402
+ return;
403
+ current = parent;
404
+ }
405
+ }
406
+ //# sourceMappingURL=project-intake.js.map
@@ -20,7 +20,10 @@ export const backendRecoveryOperationNames = [
20
20
  'session.resume',
21
21
  'context.refresh',
22
22
  'context.prepare_change',
23
+ 'memory.sync_status',
24
+ 'project.onboarding_status',
23
25
  'memory.query',
26
+ 'memory.catalog',
24
27
  'memory.history',
25
28
  'memory.propose_revision',
26
29
  'memory.list_proposals',