blun-king-cli 9.1.398 → 9.1.399

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/LIESMICH.txt CHANGED
@@ -392,6 +392,21 @@ eingestuft ist. Fehlt dieser Beleg, bleibt das Ziel aktiv und King erhält eine
392
392
  konkrete Nachbesserung statt einer falschen Fertigmeldung. Ziele ohne ausdrücklich
393
393
  gesetzte Abschlussbedingung behalten ihr bisheriges Verhalten.
394
394
 
395
+ Projektbezogenes Lernen ohne Übersprechen
396
+ ------------------------------------------
397
+
398
+ Ab BLUN King 9.1.399 bleiben bestätigte Rot-zu-Grün-Lernkandidaten innerhalb
399
+ des Git-Projekts, in dem sie entstanden sind. Zwei Arbeitskopien desselben
400
+ Remote-Repositories teilen denselben anonymisierten Projekt-Schlüssel; andere
401
+ Repositories erhalten getrennte Lernspeicher. Lokale Git-Projekte ohne Remote
402
+ werden anhand ihres Repository-Wurzelpfads getrennt.
403
+
404
+ Weder Remote-Adresse noch lokaler Projektpfad werden im Lernkandidaten
405
+ gespeichert. Außerhalb eines Git-Projekts bleibt der bisherige globale
406
+ Profilspeicher erhalten. Damit kann eine gemessene Projektregel später wieder
407
+ aufgerufen werden, ohne als vermeintlich allgemeine Regel in fachfremde Projekte
408
+ zu gelangen.
409
+
395
410
  Keine doppelte Telegram-Antwort ohne neue Nachricht
396
411
  ---------------------------------------------------
397
412
 
package/README.md CHANGED
@@ -401,6 +401,20 @@ eingestuft ist. Fehlt dieser Beleg, bleibt das Ziel aktiv und King erhält eine
401
401
  konkrete Nachbesserung statt einer falschen Fertigmeldung. Ziele ohne ausdrücklich
402
402
  gesetzte Abschlussbedingung behalten ihr bisheriges Verhalten.
403
403
 
404
+ ## Projektbezogenes Lernen ohne Übersprechen
405
+
406
+ Ab BLUN King 9.1.399 bleiben bestätigte Rot-zu-Grün-Lernkandidaten innerhalb
407
+ des Git-Projekts, in dem sie entstanden sind. Zwei Arbeitskopien desselben
408
+ Remote-Repositories teilen denselben anonymisierten Projekt-Schlüssel; andere
409
+ Repositories erhalten getrennte Lernspeicher. Lokale Git-Projekte ohne Remote
410
+ werden anhand ihres Repository-Wurzelpfads getrennt.
411
+
412
+ Weder Remote-Adresse noch lokaler Projektpfad werden im Lernkandidaten
413
+ gespeichert. Außerhalb eines Git-Projekts bleibt der bisherige globale
414
+ Profilspeicher erhalten. Damit kann eine gemessene Projektregel später wieder
415
+ aufgerufen werden, ohne als vermeintlich allgemeine Regel in fachfremde Projekte
416
+ zu gelangen.
417
+
404
418
  ## Keine doppelte Telegram-Antwort ohne neue Nachricht
405
419
 
406
420
  Ab BLUN King 9.1.398 prüft jeder Text-Ausgangspfad vor dem Senden die jüngste
@@ -115,13 +115,96 @@ function profileKey(value) {
115
115
  return (normalized || 'default').replace(/[^a-z0-9._-]/giu, '_').slice(0, 64);
116
116
  }
117
117
 
118
- function candidateDirectory(homeDir, profile) {
119
- return path.join(
118
+ function normalizedRemote(value) {
119
+ let remote = typeof value === 'string' ? value.trim() : '';
120
+ if (!remote) return '';
121
+ remote = remote.replace(/^git@([^:]+):/iu, 'ssh://git@$1/');
122
+ try {
123
+ const parsed = new URL(remote);
124
+ const host = parsed.hostname.toLowerCase();
125
+ const pathname = parsed.pathname.replace(/^\/+|\/+$/gu, '').replace(/\.git$/iu, '').toLowerCase();
126
+ return host && pathname ? `${host}/${pathname}` : '';
127
+ } catch {
128
+ return remote.replace(/\\/gu, '/').replace(/\.git$/iu, '').toLowerCase();
129
+ }
130
+ }
131
+
132
+ function gitConfigPath(dotGitPath) {
133
+ try {
134
+ const stat = fs.lstatSync(dotGitPath);
135
+ if (stat.isDirectory()) return path.join(dotGitPath, 'config');
136
+ if (!stat.isFile()) return '';
137
+ const pointer = fs.readFileSync(dotGitPath, 'utf8').match(/^gitdir:\s*(.+)$/imu)?.[1]?.trim();
138
+ if (!pointer) return '';
139
+ const gitDir = path.resolve(path.dirname(dotGitPath), pointer);
140
+ const commonPointerPath = path.join(gitDir, 'commondir');
141
+ if (fs.existsSync(commonPointerPath)) {
142
+ const commonPointer = fs.readFileSync(commonPointerPath, 'utf8').trim();
143
+ if (commonPointer) return path.join(path.resolve(gitDir, commonPointer), 'config');
144
+ }
145
+ return path.join(gitDir, 'config');
146
+ } catch {
147
+ return '';
148
+ }
149
+ }
150
+
151
+ function originRemote(configPath) {
152
+ if (!configPath) return '';
153
+ try {
154
+ const stat = fs.statSync(configPath);
155
+ if (!stat.isFile() || stat.size > 1024 * 1024) return '';
156
+ const lines = fs.readFileSync(configPath, 'utf8').split(/\r?\n/gu);
157
+ let inOrigin = false;
158
+ for (const line of lines) {
159
+ const section = line.match(/^\s*\[([^\]]+)\]\s*$/u)?.[1]?.trim();
160
+ if (section !== undefined) {
161
+ inOrigin = /^remote\s+"origin"$/iu.test(section);
162
+ continue;
163
+ }
164
+ if (!inOrigin) continue;
165
+ const url = line.match(/^\s*url\s*=\s*(.+?)\s*$/iu)?.[1];
166
+ if (url) return normalizedRemote(url);
167
+ }
168
+ } catch {
169
+ return '';
170
+ }
171
+ return '';
172
+ }
173
+
174
+ function resolveValidatedLearningProject(projectDir) {
175
+ const requested = typeof projectDir === 'string' ? projectDir.trim() : '';
176
+ if (!requested) return { scope: 'global', projectId: '' };
177
+ let current;
178
+ try {
179
+ current = fs.realpathSync(path.resolve(requested));
180
+ } catch {
181
+ return { scope: 'global', projectId: '' };
182
+ }
183
+ for (let depth = 0; depth < 64; depth += 1) {
184
+ const dotGitPath = path.join(current, '.git');
185
+ if (fs.existsSync(dotGitPath)) {
186
+ const remote = originRemote(gitConfigPath(dotGitPath));
187
+ const identity = remote ? `remote:${remote}` : `path:${current.toLowerCase()}`;
188
+ return {
189
+ scope: 'project',
190
+ projectId: crypto.createHash('sha256').update(identity).digest('hex').slice(0, 16),
191
+ };
192
+ }
193
+ const parent = path.dirname(current);
194
+ if (parent === current) break;
195
+ current = parent;
196
+ }
197
+ return { scope: 'global', projectId: '' };
198
+ }
199
+
200
+ function candidateDirectory(homeDir, profile, projectId = '') {
201
+ const base = path.join(
120
202
  path.resolve(homeDir),
121
203
  'self-improvement',
122
204
  'validated-learning',
123
205
  profileKey(profile),
124
206
  );
207
+ return projectId ? path.join(base, 'projects', projectId) : base;
125
208
  }
126
209
 
127
210
  function atomicCreate(filePath, value) {
@@ -161,7 +244,8 @@ function readCandidate(filePath) {
161
244
  function readValidatedLearningCandidates(options = {}) {
162
245
  const homeDir = typeof options.homeDir === 'string' ? options.homeDir.trim() : '';
163
246
  if (!homeDir) return [];
164
- const directory = candidateDirectory(homeDir, options.profile);
247
+ const project = resolveValidatedLearningProject(options.projectDir);
248
+ const directory = candidateDirectory(homeDir, options.profile, project.projectId);
165
249
  try {
166
250
  return fs.readdirSync(directory, { withFileTypes: true })
167
251
  .filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
@@ -255,21 +339,25 @@ function pendingCandidateForSignal(candidates, signal) {
255
339
 
256
340
  function createCandidate(signal, options, nowIso) {
257
341
  const profile = profileKey(options.profile);
342
+ const projectId = typeof options.projectId === 'string' ? options.projectId : '';
258
343
  const commandHash = verificationCommandHash(signal);
259
344
  const digest = crypto.createHash('sha256').update([
260
345
  profile,
346
+ projectId,
261
347
  commandHash,
262
348
  signal.failedToolCallId,
263
349
  signal.successfulToolCallId,
264
350
  ].join('\0')).digest('hex').slice(0, 32);
265
351
  const id = `vl-${digest}`;
266
- const filePath = path.join(candidateDirectory(options.homeDir, profile), `${id}.json`);
352
+ const filePath = path.join(candidateDirectory(options.homeDir, profile, projectId), `${id}.json`);
267
353
  atomicCreate(filePath, {
268
354
  schemaVersion: SCHEMA_VERSION,
269
355
  id,
270
356
  type: 'validated_red_green',
271
357
  status: 'pending',
272
358
  profile,
359
+ scope: projectId ? 'project' : 'global',
360
+ ...(projectId ? { projectId } : {}),
273
361
  verification: {
274
362
  toolName: signal.toolName,
275
363
  commandHash,
@@ -286,6 +374,7 @@ function createCandidate(signal, options, nowIso) {
286
374
  function syncValidatedLearningCandidate(history, options = {}) {
287
375
  const signal = detectValidatedLearningSignal(history);
288
376
  const homeDir = typeof options.homeDir === 'string' ? options.homeDir.trim() : '';
377
+ const project = resolveValidatedLearningProject(options.projectDir);
289
378
  if (!homeDir) {
290
379
  return signal === null ? null : {
291
380
  ...signal,
@@ -300,6 +389,7 @@ function syncValidatedLearningCandidate(history, options = {}) {
300
389
  let candidates = readValidatedLearningCandidates({
301
390
  homeDir,
302
391
  profile: options.profile,
392
+ projectDir: options.projectDir,
303
393
  });
304
394
  const recordedCandidateIds = successfulMistakeRecordCandidateIds(history);
305
395
  for (const candidateId of recordedCandidateIds) {
@@ -309,7 +399,11 @@ function syncValidatedLearningCandidate(history, options = {}) {
309
399
  markCandidate(candidate, 'recorded', nowIso);
310
400
  }
311
401
  if (recordedCandidateIds.size > 0) {
312
- candidates = readValidatedLearningCandidates({ homeDir, profile: options.profile });
402
+ candidates = readValidatedLearningCandidates({
403
+ homeDir,
404
+ profile: options.profile,
405
+ projectDir: options.projectDir,
406
+ });
313
407
  }
314
408
  let expiredCandidate = false;
315
409
  for (const candidate of candidates) {
@@ -320,11 +414,19 @@ function syncValidatedLearningCandidate(history, options = {}) {
320
414
  }
321
415
  }
322
416
  if (expiredCandidate) {
323
- candidates = readValidatedLearningCandidates({ homeDir, profile: options.profile });
417
+ candidates = readValidatedLearningCandidates({
418
+ homeDir,
419
+ profile: options.profile,
420
+ projectDir: options.projectDir,
421
+ });
324
422
  }
325
423
  const retention = pruneTerminalValidatedLearningCandidates({ candidates });
326
424
  if (retention.deleted > 0) {
327
- candidates = readValidatedLearningCandidates({ homeDir, profile: options.profile });
425
+ candidates = readValidatedLearningCandidates({
426
+ homeDir,
427
+ profile: options.profile,
428
+ projectDir: options.projectDir,
429
+ });
328
430
  }
329
431
  if (signal !== null) {
330
432
  const duplicate = pendingCandidateForSignal(candidates, signal);
@@ -332,7 +434,11 @@ function syncValidatedLearningCandidate(history, options = {}) {
332
434
  return { ...duplicate, source: 'current_turn' };
333
435
  }
334
436
  return {
335
- ...createCandidate(signal, { homeDir, profile: options.profile }, nowIso),
437
+ ...createCandidate(signal, {
438
+ homeDir,
439
+ profile: options.profile,
440
+ projectId: project.projectId,
441
+ }, nowIso),
336
442
  source: 'current_turn',
337
443
  };
338
444
  }
@@ -352,5 +458,6 @@ module.exports = {
352
458
  normalizeCommand,
353
459
  pruneTerminalValidatedLearningCandidates,
354
460
  readValidatedLearningCandidates,
461
+ resolveValidatedLearningProject,
355
462
  syncValidatedLearningCandidate,
356
463
  };
package/blun.mjs CHANGED
@@ -232224,7 +232224,8 @@ var init_validated_learning_signal = __esmMin((() => {
232224
232224
  if (!shouldSyncValidatedLearningCandidate(this.agent.context.history)) return;
232225
232225
  const signal = syncValidatedLearningCandidate(this.agent.context.history, {
232226
232226
  homeDir: process.env["BLUN_SHARED_HOME"] ?? process.env["BLUN_HOME"] ?? "",
232227
- profile: process.env["BLUN_PROFILE"] ?? "default"
232227
+ profile: process.env["BLUN_PROFILE"] ?? "default",
232228
+ projectDir: process.cwd()
232228
232229
  });
232229
232230
  if (signal === null) {
232230
232231
  if (existing.length > 0) this.agent.context.removeSystemRemindersMatching(isValidatedLearningSignalReminder);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "9.1.398",
3
+ "version": "9.1.399",
4
4
  "description": "BLUN CLI - your own AI agent with a Telegram channel. Get it done. With BLUN.",
5
5
  "license": "MIT",
6
6
  "bin": {