@ijfw/memory-server 1.5.1 → 1.5.4
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/package.json +6 -5
- package/src/brain/budget-guard.js +86 -0
- package/src/brain/citation-resolver.js +41 -0
- package/src/brain/context-injection.js +69 -0
- package/src/brain/discovery.js +83 -0
- package/src/brain/dream-pipeline.js +324 -0
- package/src/brain/dump-ingest.js +88 -0
- package/src/brain/entity-collapse.js +28 -0
- package/src/brain/export.js +121 -0
- package/src/brain/extractors/index.js +24 -0
- package/src/brain/extractors/markdown.js +27 -0
- package/src/brain/extractors/pdf.js +31 -0
- package/src/brain/extractors/transcript.js +38 -0
- package/src/brain/first-run-scan.js +61 -0
- package/src/brain/index.js +1 -0
- package/src/brain/layout-sentinel.js +29 -0
- package/src/brain/migrate-facts-internal-once.js +87 -0
- package/src/brain/path-guard.js +103 -0
- package/src/brain/paths.js +26 -0
- package/src/brain/promotion-suggester.js +41 -0
- package/src/brain/stub-detector.js +33 -0
- package/src/brain/tiered-llm.js +83 -0
- package/src/brain/wiki-compiler.js +144 -0
- package/src/brain/wiki-sentinels.js +45 -0
- package/src/brain/wiki-templates.js +94 -0
- package/src/cross-orchestrator-cli.js +132 -5
- package/src/cross-orchestrator.js +2 -2
- package/src/dashboard-server.js +1 -1
- package/src/dream/runner.mjs +21 -0
- package/src/extension-registry.js +2 -2
- package/src/handlers/brain-handler.js +319 -0
- package/src/memory/auto-linker.js +5 -1
- package/src/memory/benchmark.js +4 -3
- package/src/memory/layout-migrations/001-visible-layer.js +131 -0
- package/src/memory/layout-migrations/index.js +50 -0
- package/src/memory/migration-runner.js +31 -2
- package/src/memory/obsidian-parser.js +3 -1
- package/src/memory/reader.js +2 -1
- package/src/memory/search.js +144 -16
- package/src/memory/temporal.js +40 -1
- package/src/orchestrator/agents-md-blackboard.js +114 -1
- package/src/orchestrator/discipline-selector.js +259 -0
- package/src/orchestrator/merge-block-aware.js +15 -5
- package/src/orchestrator/state-sdk.js +42 -4
- package/src/orchestrator/wave-state.js +38 -0
- package/src/recovery/code-fixer.js +1 -1
- package/src/server.js +290 -75
- package/src/update-apply.js +1 -1
package/src/server.js
CHANGED
|
@@ -16,9 +16,17 @@ import { createInterface } from 'readline';
|
|
|
16
16
|
import {
|
|
17
17
|
existsSync, mkdirSync, readFileSync, writeFileSync,
|
|
18
18
|
appendFileSync, readdirSync, statSync, renameSync, unlinkSync,
|
|
19
|
-
openSync, closeSync, fsyncSync, realpathSync
|
|
19
|
+
openSync, closeSync, fsyncSync, realpathSync,
|
|
20
|
+
accessSync, constants as fsConstants
|
|
20
21
|
} from 'fs';
|
|
21
22
|
import { join, resolve, isAbsolute, normalize, basename, dirname } from 'path';
|
|
23
|
+
import { resolveBrainPaths } from './brain/paths.js';
|
|
24
|
+
import { migrateFactsInternalOnce } from './brain/migrate-facts-internal-once.js';
|
|
25
|
+
// v1.5.2.1 F3: fs-layout migrations live in their own directory with their
|
|
26
|
+
// own registry (not auto-discovered by the SQL migration-runner). Invoked
|
|
27
|
+
// inside __mainEntryPoint() only — never as a top-level side effect of
|
|
28
|
+
// importing server.js.
|
|
29
|
+
import { runLayoutMigrations } from './memory/layout-migrations/index.js';
|
|
22
30
|
import { homedir } from 'os';
|
|
23
31
|
import { fileURLToPath } from 'url';
|
|
24
32
|
import { createHash, randomBytes } from 'crypto';
|
|
@@ -30,6 +38,7 @@ const PKG_VERSION = (() => {
|
|
|
30
38
|
catch { return 'unknown'; }
|
|
31
39
|
})();
|
|
32
40
|
import { checkPrompt } from './prompt-check.js';
|
|
41
|
+
import { handleIjfwBrain, IJFW_BRAIN_VERBS } from './handlers/brain-handler.js';
|
|
33
42
|
import { applyCaps, CAP_CONTENT } from './caps.js';
|
|
34
43
|
import { ensureSchemaHeader, SCHEMA_HEADER } from './schema.js';
|
|
35
44
|
import { searchCorpus } from './search-bm25.js';
|
|
@@ -276,14 +285,16 @@ function validatePath(raw) {
|
|
|
276
285
|
function isWritable(dir) {
|
|
277
286
|
try {
|
|
278
287
|
if (!existsSync(dir)) {
|
|
279
|
-
// Try to create it; if
|
|
288
|
+
// Try to create it; if mkdir fails, treat as non-writable.
|
|
280
289
|
mkdirSync(dir, { recursive: true });
|
|
281
290
|
return true;
|
|
282
291
|
}
|
|
283
|
-
//
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
292
|
+
// v1.5.2.1 H-1 (Lens 1): previously wrote+unlinked a probe file
|
|
293
|
+
// (`.ijfw-probe-<pid>-<ts>`). That broke the "importing server.js
|
|
294
|
+
// produces ZERO filesystem artifacts" contract: the probe leaked
|
|
295
|
+
// under inotify/fswatch even when self-tests in tmpdir saw nothing.
|
|
296
|
+
// accessSync(W_OK) gives the same writability signal with no I/O.
|
|
297
|
+
accessSync(dir, fsConstants.W_OK);
|
|
287
298
|
return true;
|
|
288
299
|
} catch {
|
|
289
300
|
return false;
|
|
@@ -312,8 +323,35 @@ function safeProjectDir() {
|
|
|
312
323
|
const PROJECT_DIR = safeProjectDir();
|
|
313
324
|
const PROJECT_HASH = createHash('sha256').update(PROJECT_DIR).digest('hex').slice(0, 12);
|
|
314
325
|
const IJFW_DIR = join(PROJECT_DIR, '.ijfw');
|
|
326
|
+
// REPO_ROOT is the parent of .ijfw/ — required by resolveBrainPaths.
|
|
327
|
+
const REPO_ROOT = dirname(IJFW_DIR);
|
|
328
|
+
// paths() re-reads the layout sentinel on every call so a long-running server
|
|
329
|
+
// process picks up the migration-010 sentinel flip without restart.
|
|
330
|
+
function paths() { return resolveBrainPaths(REPO_ROOT); }
|
|
331
|
+
// v1.5.2.1 F1: __isServerEntryPoint gates ALL top-level imperative effects
|
|
332
|
+
// (facts relocation, fs-layout migration, dir bootstrap, WS client, stdio
|
|
333
|
+
// transport, signal handlers). Without this gate, importing server.js from
|
|
334
|
+
// a test or helper would fire every side-effect against the importer's cwd:
|
|
335
|
+
// stray `ijfw/`, `.ijfw/`, .ijfw-probe-* files in repos that aren't IJFW
|
|
336
|
+
// projects. The gate uses realpathSync on BOTH sides because process.argv[1]
|
|
337
|
+
// can be a symlinked bin shim (npm global installs) while import.meta.url
|
|
338
|
+
// resolves to the real path inside node_modules.
|
|
339
|
+
const __isServerEntryPoint = (() => {
|
|
340
|
+
if (!process.argv[1]) return false;
|
|
341
|
+
try {
|
|
342
|
+
const entryReal = realpathSync(process.argv[1]);
|
|
343
|
+
const selfReal = realpathSync(fileURLToPath(import.meta.url));
|
|
344
|
+
return entryReal === selfReal;
|
|
345
|
+
} catch (e) {
|
|
346
|
+
if (process.env.IJFW_DEBUG) {
|
|
347
|
+
try { process.stderr.write(`[ijfw entry-gate] check failed: ${e.message}\n`); } catch {}
|
|
348
|
+
}
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
})();
|
|
352
|
+
// Back-compat values for the line-2349 re-export. New code MUST use paths().memoryDir / paths().sessionsDir.
|
|
315
353
|
const MEMORY_DIR = join(IJFW_DIR, 'memory');
|
|
316
|
-
|
|
354
|
+
// SESSIONS_DIR removed -- replaced by paths().sessionsDir via brainPaths() helper.
|
|
317
355
|
const GLOBAL_DIR = join(homedir(), '.ijfw', 'memory');
|
|
318
356
|
// Legacy single-file location (pre-Phase 2). Still read for backward compat
|
|
319
357
|
// but new writes go to the faceted structure.
|
|
@@ -347,14 +385,18 @@ const NATIVE_CLAUDE_DIR = join(
|
|
|
347
385
|
// Failures here do NOT write to stderr during startup -- any stderr byte during
|
|
348
386
|
// MCP handshake can make strict clients (incl. Claude Code) mark the server
|
|
349
387
|
// as failed. Subsequent store/read calls surface structured errors instead.
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
} catch { /* handleStore
|
|
388
|
+
// v1.5.2.1 F1: called from __mainEntryPoint() so importing server.js no
|
|
389
|
+
// longer creates directories in the importer's cwd.
|
|
390
|
+
function __bootstrapDirs() {
|
|
391
|
+
try {
|
|
392
|
+
[paths().memoryDir, paths().sessionsDir].forEach(dir => {
|
|
393
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
394
|
+
});
|
|
395
|
+
} catch { /* handleStore/recall surface structured errors on first use */ }
|
|
396
|
+
try {
|
|
397
|
+
if (!existsSync(GLOBAL_DIR)) mkdirSync(GLOBAL_DIR, { recursive: true });
|
|
398
|
+
} catch { /* handleStore reports on attempted write */ }
|
|
399
|
+
}
|
|
358
400
|
|
|
359
401
|
// R2-E -- sanitizeContent moved to mcp-server/src/sanitizer.js so MCP stores
|
|
360
402
|
// and auto-memorize stores share a single implementation. Imported above.
|
|
@@ -467,7 +509,7 @@ function emitRecallObservation({ context_hint, from_project } = {}) {
|
|
|
467
509
|
|
|
468
510
|
// --- Storage helpers ---
|
|
469
511
|
function appendToJournal(entry) {
|
|
470
|
-
const journalPath = join(
|
|
512
|
+
const journalPath = join(paths().memoryDir, 'project-journal.md');
|
|
471
513
|
const ts = new Date().toISOString();
|
|
472
514
|
const line = `- [${ts}] ${entry}`;
|
|
473
515
|
return appendLine(journalPath, line);
|
|
@@ -477,7 +519,7 @@ function appendToJournal(entry) {
|
|
|
477
519
|
// similar to Claude's native auto-memory format: YAML frontmatter plus a body with
|
|
478
520
|
// Why / How-to-apply sections. This is the format users retrieve well from.
|
|
479
521
|
function appendStructuredToKnowledge({ type, summary, content, why, howToApply, tags }) {
|
|
480
|
-
const filepath = join(
|
|
522
|
+
const filepath = join(paths().memoryDir, 'knowledge.md');
|
|
481
523
|
const ts = new Date().toISOString();
|
|
482
524
|
const tagLine = tags && tags.length ? tags.join(', ') : '';
|
|
483
525
|
const block = [
|
|
@@ -524,10 +566,10 @@ function appendToGlobalPrefs(entry, tags = []) {
|
|
|
524
566
|
}
|
|
525
567
|
|
|
526
568
|
function readKnowledgeBase() {
|
|
527
|
-
return readOr(join(
|
|
569
|
+
return readOr(join(paths().memoryDir, 'knowledge.md'));
|
|
528
570
|
}
|
|
529
571
|
function readHandoff() {
|
|
530
|
-
return readOr(join(
|
|
572
|
+
return readOr(join(paths().memoryDir, 'handoff.md'));
|
|
531
573
|
}
|
|
532
574
|
// Read Claude Code native auto-memory for this project. Returns concatenated
|
|
533
575
|
// sanitized content of all project_*.md files (skipping MEMORY.md index).
|
|
@@ -597,7 +639,7 @@ function readGlobalKnowledge() {
|
|
|
597
639
|
}
|
|
598
640
|
|
|
599
641
|
function getRecentJournalEntries(count = 5) {
|
|
600
|
-
const journal = readOr(join(
|
|
642
|
+
const journal = readOr(join(paths().memoryDir, 'project-journal.md'));
|
|
601
643
|
if (!journal) return '';
|
|
602
644
|
const entries = journal.split('\n').filter(l => /^- \[\d{4}-/.test(l));
|
|
603
645
|
return entries.slice(-count).join('\n');
|
|
@@ -608,7 +650,7 @@ function getRecentJournalEntries(count = 5) {
|
|
|
608
650
|
// down to ms; collisions would only happen on near-simultaneous writes which
|
|
609
651
|
// our atomic-append + fs flush ordering already serialise).
|
|
610
652
|
function getRecentMemoriesForDedup(limit = 50) {
|
|
611
|
-
const journal = readOr(join(
|
|
653
|
+
const journal = readOr(join(paths().memoryDir, 'project-journal.md'));
|
|
612
654
|
if (!journal) return [];
|
|
613
655
|
const lines = journal.split('\n').filter(l => /^- \[\d{4}-/.test(l));
|
|
614
656
|
// Most-recent-last in the file → reverse so findNearDuplicate sees newest first.
|
|
@@ -623,7 +665,18 @@ function getRecentMemoriesForDedup(limit = 50) {
|
|
|
623
665
|
|
|
624
666
|
// H5.5 — sidecar file for structured facts (one JSON object per line).
|
|
625
667
|
// Append-only; consumed by handleRecall({context_hint:'facts'}).
|
|
626
|
-
|
|
668
|
+
// v1.5.2 F5: now resolves via paths().factsJsonl (.ijfw/facts.jsonl) — internal
|
|
669
|
+
// hidden location per Task 3 design. The one-shot migrateFactsInternalOnce()
|
|
670
|
+
// call above relocated existing data from the legacy .ijfw/memory/ location.
|
|
671
|
+
//
|
|
672
|
+
// v1.5.2.1 H-2 (Lens 1): use accessor functions for internal callers so that
|
|
673
|
+
// a layout-migration sentinel flip mid-process (rare but possible) is picked
|
|
674
|
+
// up without a server restart. The named export `FACTS_FILE` (consumed by
|
|
675
|
+
// test-server-ingest.js as a one-shot import-time snapshot) is preserved
|
|
676
|
+
// below for back-compat.
|
|
677
|
+
function factsFile() { return paths().factsJsonl; }
|
|
678
|
+
function factsDbFile() { return paths().indexDb; }
|
|
679
|
+
const FACTS_FILE = factsFile();
|
|
627
680
|
|
|
628
681
|
// Stable short id for joining a fact back to its journal entry. We don't have
|
|
629
682
|
// a uuid; the journal-line text itself + ts is unique enough for cross-ref.
|
|
@@ -638,7 +691,8 @@ function appendFactsToSidecar(facts, meta) {
|
|
|
638
691
|
if (!Array.isArray(facts) || facts.length === 0) return { ok: true, written: 0 };
|
|
639
692
|
try {
|
|
640
693
|
const lines = facts.map(f => factToJsonl(f, meta)).join('\n') + '\n';
|
|
641
|
-
|
|
694
|
+
// v1.5.2.1 H-2: factsFile() re-reads layout sentinel each call.
|
|
695
|
+
appendFileSync(factsFile(), lines);
|
|
642
696
|
return { ok: true, written: facts.length };
|
|
643
697
|
} catch (err) {
|
|
644
698
|
// Non-fatal: facts are augmentation, not source-of-truth. Journal already
|
|
@@ -652,12 +706,16 @@ function appendFactsToSidecar(facts, meta) {
|
|
|
652
706
|
// table (queryable point-in-time view) stay co-located. Lazy-opened on first
|
|
653
707
|
// use so a project that never stores a memory never pays the better-sqlite3
|
|
654
708
|
// load cost.
|
|
655
|
-
|
|
709
|
+
// v1.5.2 F5: now resolves via paths().indexDb (.ijfw/index/memory.db) — internal
|
|
710
|
+
// hidden location per Task 3 design. Data migrated by migrateFactsInternalOnce().
|
|
711
|
+
// v1.5.2.1 H-2: FACTS_DB_FILE is the export-time snapshot; internal use goes
|
|
712
|
+
// through factsDbFile() above so mid-process sentinel flips are honored.
|
|
713
|
+
const FACTS_DB_FILE = factsDbFile();
|
|
656
714
|
let _factsDbHandle = null;
|
|
657
715
|
function getFactsDb() {
|
|
658
716
|
if (_factsDbHandle) return _factsDbHandle;
|
|
659
717
|
try {
|
|
660
|
-
_factsDbHandle = openTemporalDbSync(
|
|
718
|
+
_factsDbHandle = openTemporalDbSync(factsDbFile());
|
|
661
719
|
return _factsDbHandle;
|
|
662
720
|
} catch (err) {
|
|
663
721
|
// Non-fatal. JSONL sidecar still gets written; we just lose the bi-temporal
|
|
@@ -792,10 +850,10 @@ async function searchMemory(query, limit = 10, scope = 'project', opts = {}) {
|
|
|
792
850
|
}
|
|
793
851
|
|
|
794
852
|
const sources = [
|
|
795
|
-
{ name: 'team', content: readTeamKnowledge(), boost: 1.25, path: join(
|
|
796
|
-
{ name: 'knowledge', content: readKnowledgeBase(), boost: 1.15, path: join(
|
|
797
|
-
{ name: 'journal', content: readOr(join(
|
|
798
|
-
{ name: 'handoff', content: readHandoff(), boost: 1.1, path: join(
|
|
853
|
+
{ name: 'team', content: readTeamKnowledge(), boost: 1.25, path: join(paths().memoryDir, 'team-knowledge.md') },
|
|
854
|
+
{ name: 'knowledge', content: readKnowledgeBase(), boost: 1.15, path: join(paths().memoryDir, 'knowledge.md') },
|
|
855
|
+
{ name: 'journal', content: readOr(join(paths().memoryDir, 'project-journal.md')), boost: 1.0, path: join(paths().memoryDir, 'project-journal.md') },
|
|
856
|
+
{ name: 'handoff', content: readHandoff(), boost: 1.1, path: join(paths().memoryDir, 'handoff.md') },
|
|
799
857
|
{ name: 'global', content: readGlobalKnowledge(), boost: 0.95, path: null },
|
|
800
858
|
{ name: 'claude-native', content: readNativeClaudeMemory(), boost: 0.95, path: null },
|
|
801
859
|
];
|
|
@@ -1071,6 +1129,21 @@ const TOOLS = [
|
|
|
1071
1129
|
required: ['command'],
|
|
1072
1130
|
},
|
|
1073
1131
|
},
|
|
1132
|
+
{
|
|
1133
|
+
// v1.5.2 T24-27+T29: ijfw_brain — combined brain-query tool (slot 14/14).
|
|
1134
|
+
// Replaces what would have been 4 standalone tools; combined-tool pattern
|
|
1135
|
+
// keeps the cap raise to +1 instead of +4.
|
|
1136
|
+
name: 'ijfw_brain',
|
|
1137
|
+
description: 'IJFW Brain — query, links, wiki, conflict-resolve in one combined tool. verb=' + IJFW_BRAIN_VERBS.join('|'),
|
|
1138
|
+
inputSchema: {
|
|
1139
|
+
type: 'object',
|
|
1140
|
+
properties: {
|
|
1141
|
+
verb: { type: 'string', enum: IJFW_BRAIN_VERBS, description: 'Brain verb: think|links|wiki.get|wiki.compile|wiki.promote|wiki.export|wiki.shareReadme|conflict.resolve' },
|
|
1142
|
+
args: { type: 'object', description: 'Verb-specific arguments (see verb docs).' },
|
|
1143
|
+
},
|
|
1144
|
+
required: ['verb'],
|
|
1145
|
+
},
|
|
1146
|
+
},
|
|
1074
1147
|
{
|
|
1075
1148
|
// v1.5.0 T13: ijfw_state — single MCP face for the state-SDK verb facade.
|
|
1076
1149
|
// Absorbs the retired ijfw_subagent_post_done tool (post-done IS a state
|
|
@@ -1098,7 +1171,7 @@ const TOOLS = [
|
|
|
1098
1171
|
// (codex/gemini/claude by default) in parallel; if verdicts diverge,
|
|
1099
1172
|
// re-runs with a CYCLE_SUMMARY of the disagreement until consensus or
|
|
1100
1173
|
// maxIterations (default 3). Stall breaker halts on byte-identical
|
|
1101
|
-
// iterations. Slot
|
|
1174
|
+
// iterations. Slot 14 of the 14/14 tool cap.
|
|
1102
1175
|
name: 'ijfw_cross_audit_converge',
|
|
1103
1176
|
description: 'Multi-lens Trident audit with consensus convergence loop. Dispatches codex/gemini/claude in parallel against a commit range, detects verdict divergence, and re-runs with a cycle summary until consensus or maxIterations. Returns {verdict, iterations, findings, divergence?, stalled?}. Verdict: PASS / CONDITIONAL / FAIL / consensus_failed / UNREACHABLE.',
|
|
1104
1177
|
inputSchema: {
|
|
@@ -1239,10 +1312,12 @@ function handleRecall({ context_hint, detail_level = 'standard', from_project })
|
|
|
1239
1312
|
}
|
|
1240
1313
|
// SQL table empty -- fall through to JSONL back-compat path below.
|
|
1241
1314
|
}
|
|
1242
|
-
|
|
1315
|
+
// v1.5.2.1 H-2: factsFile() re-resolves via paths() each call.
|
|
1316
|
+
const _ff = factsFile();
|
|
1317
|
+
if (!existsSync(_ff)) {
|
|
1243
1318
|
return { text: 'No structured facts extracted yet. Store memories with key:value lines, "X uses Y", or "decided to ..." phrases to populate.' };
|
|
1244
1319
|
}
|
|
1245
|
-
const raw = readFileSync(
|
|
1320
|
+
const raw = readFileSync(_ff, 'utf8');
|
|
1246
1321
|
// detail_level === 'summary' → just count + a sample tail. Keeps token
|
|
1247
1322
|
// usage bounded for session-start hydration.
|
|
1248
1323
|
if (detail_level === 'summary') {
|
|
@@ -1467,7 +1542,7 @@ function handleStore({ content, type, tags = [], summary, why, how_to_apply }) {
|
|
|
1467
1542
|
}
|
|
1468
1543
|
|
|
1469
1544
|
if (type === 'handoff') {
|
|
1470
|
-
const handoffPath = join(
|
|
1545
|
+
const handoffPath = join(paths().memoryDir, 'handoff.md');
|
|
1471
1546
|
const prior = readMarkdownFile(handoffPath);
|
|
1472
1547
|
if (prior.ok && prior.content.trim()) {
|
|
1473
1548
|
appendToJournal(`prior-handoff-archived: ${sanitizeContent(prior.content).substring(0, 500)}`);
|
|
@@ -1566,7 +1641,7 @@ async function handlePrelude({ detail_level = 'summary' } = {}) {
|
|
|
1566
1641
|
try {
|
|
1567
1642
|
const top = topKSuccessfulSkills(db, { k: 5 });
|
|
1568
1643
|
if (top.length > 0) {
|
|
1569
|
-
const names = top.map((r) => `${r.skill_id} (${r.success_count}
|
|
1644
|
+
const names = top.map((r) => `${r.skill_id} (${r.success_count}x)`).join(', ');
|
|
1570
1645
|
parts.push(
|
|
1571
1646
|
'<ijfw-recommended-skills>',
|
|
1572
1647
|
`Observed success this project: ${names}`,
|
|
@@ -1741,7 +1816,21 @@ async function handlePrelude({ detail_level = 'summary' } = {}) {
|
|
|
1741
1816
|
return { text: abstain.join('\n') };
|
|
1742
1817
|
}
|
|
1743
1818
|
|
|
1744
|
-
|
|
1819
|
+
// v1.5.2 brain context injection — env-gated (IJFW_BRAIN_INJECT=auto|always|never).
|
|
1820
|
+
// Default 'never' keeps existing behavior unchanged; opt-in surfaces the
|
|
1821
|
+
// top-N most-recently-touched wiki pages to the prelude. Best-effort —
|
|
1822
|
+
// any failure is swallowed so prelude never breaks.
|
|
1823
|
+
let injectedText = text;
|
|
1824
|
+
try {
|
|
1825
|
+
const mode = process.env.IJFW_BRAIN_INJECT || 'never';
|
|
1826
|
+
if (mode === 'auto' || mode === 'always') {
|
|
1827
|
+
const { buildContextInjection } = await import('./brain/context-injection.js');
|
|
1828
|
+
const injection = buildContextInjection(REPO_ROOT, { mode });
|
|
1829
|
+
if (injection) injectedText = text + injection;
|
|
1830
|
+
}
|
|
1831
|
+
} catch { /* swallow — injection is best-effort */ }
|
|
1832
|
+
|
|
1833
|
+
return { text: injectedText };
|
|
1745
1834
|
}
|
|
1746
1835
|
|
|
1747
1836
|
async function handleSearch({ query, limit = 10, scope = 'project', label }) {
|
|
@@ -2001,6 +2090,28 @@ function handleMessage(msg) {
|
|
|
2001
2090
|
result = { text: JSON.stringify(r, null, 2), isError: !!(r && r.error) };
|
|
2002
2091
|
break;
|
|
2003
2092
|
}
|
|
2093
|
+
case 'ijfw_brain': {
|
|
2094
|
+
// v1.5.2 T24-27+T29: combined brain verb facade.
|
|
2095
|
+
const a = args || {};
|
|
2096
|
+
if (typeof a.verb !== 'string' || a.verb.length === 0) {
|
|
2097
|
+
result = { text: JSON.stringify({ ok: false, error: 'verb (string) is required' }), isError: true };
|
|
2098
|
+
break;
|
|
2099
|
+
}
|
|
2100
|
+
try {
|
|
2101
|
+
const r = await handleIjfwBrain({
|
|
2102
|
+
verb: a.verb,
|
|
2103
|
+
args: (a.args && typeof a.args === 'object') ? a.args : {},
|
|
2104
|
+
db: getFactsDb(),
|
|
2105
|
+
repoRoot: REPO_ROOT,
|
|
2106
|
+
env: process.env,
|
|
2107
|
+
});
|
|
2108
|
+
result = { text: JSON.stringify(r, null, 2), isError: r.ok === false };
|
|
2109
|
+
} catch (err) {
|
|
2110
|
+
const msg = err && err.message ? err.message : String(err);
|
|
2111
|
+
result = { text: JSON.stringify({ ok: false, error: msg }), isError: true };
|
|
2112
|
+
}
|
|
2113
|
+
break;
|
|
2114
|
+
}
|
|
2004
2115
|
case 'ijfw_state': {
|
|
2005
2116
|
// v1.5.0 T13: single MCP face for the state-SDK. Routes every call
|
|
2006
2117
|
// into the same `query(verb, payload, ctx)` core that the JS module
|
|
@@ -2278,7 +2389,11 @@ function handleMessage(msg) {
|
|
|
2278
2389
|
// of the import graph entirely unless the operator opts in via the env var, so
|
|
2279
2390
|
// MCP startup never opens a socket for the common (unset) case. Best-effort:
|
|
2280
2391
|
// a failed WS bind must never block the stdio transport.
|
|
2281
|
-
|
|
2392
|
+
// v1.5.2.1 F1: called from __mainEntryPoint() so importing server.js no
|
|
2393
|
+
// longer fires the WS client even when the env var is set in the importer's
|
|
2394
|
+
// process (relevant in tests / dashboards that share env with the server).
|
|
2395
|
+
function __maybeStartWsClient() {
|
|
2396
|
+
if (!process.env.IJFW_REGISTRY_WS_URL && !process.env.IJFW_REGISTRY_WS_SOURCE) return;
|
|
2282
2397
|
(async () => {
|
|
2283
2398
|
try {
|
|
2284
2399
|
const { initWsClient } = await import('./extension-registry-ws.js');
|
|
@@ -2293,50 +2408,149 @@ if (process.env.IJFW_REGISTRY_WS_URL || process.env.IJFW_REGISTRY_WS_SOURCE) {
|
|
|
2293
2408
|
}
|
|
2294
2409
|
|
|
2295
2410
|
// --- stdio Transport ---
|
|
2296
|
-
|
|
2411
|
+
// v1.5.2.1 F1: called from __mainEntryPoint(). Importing server.js used to
|
|
2412
|
+
// install a global stdin listener that swallowed every byte the importer's
|
|
2413
|
+
// process received — making the import poisonous to any host with its own
|
|
2414
|
+
// stdin loop.
|
|
2415
|
+
// v1.5.2.1 M-3 (Lens 1): idempotency flag so a re-entrant invocation (test
|
|
2416
|
+
// harness, repeated __mainEntryPoint call, etc.) does NOT install a second
|
|
2417
|
+
// stdin listener — duplicate listeners cause double-handling of each line.
|
|
2418
|
+
let __stdioAttached = false;
|
|
2419
|
+
function __attachStdioTransport() {
|
|
2420
|
+
if (__stdioAttached) return;
|
|
2421
|
+
__stdioAttached = true;
|
|
2422
|
+
const rl = createInterface({ input: process.stdin, terminal: false });
|
|
2423
|
+
rl.on('line', (line) => {
|
|
2424
|
+
if (!line.trim()) return;
|
|
2425
|
+
let msg;
|
|
2426
|
+
try {
|
|
2427
|
+
msg = JSON.parse(line);
|
|
2428
|
+
} catch {
|
|
2429
|
+
process.stdout.write(JSON.stringify({
|
|
2430
|
+
jsonrpc: '2.0', id: null,
|
|
2431
|
+
error: { code: -32700, message: 'Parse error' }
|
|
2432
|
+
}) + '\n');
|
|
2433
|
+
return;
|
|
2434
|
+
}
|
|
2435
|
+
try {
|
|
2436
|
+
const response = handleMessage(msg);
|
|
2437
|
+
if (response && typeof response.then === 'function') {
|
|
2438
|
+
response.then(r => { if (r) process.stdout.write(r + '\n'); }).catch(err => {
|
|
2439
|
+
process.stdout.write(JSON.stringify({
|
|
2440
|
+
jsonrpc: '2.0',
|
|
2441
|
+
id: msg && msg.id ? msg.id : null,
|
|
2442
|
+
error: { code: -32603, message: `Internal error: ${err.message}` }
|
|
2443
|
+
}) + '\n');
|
|
2444
|
+
});
|
|
2445
|
+
} else if (response) {
|
|
2446
|
+
process.stdout.write(response + '\n');
|
|
2447
|
+
}
|
|
2448
|
+
} catch (err) {
|
|
2449
|
+
process.stdout.write(JSON.stringify({
|
|
2450
|
+
jsonrpc: '2.0',
|
|
2451
|
+
id: msg && msg.id ? msg.id : null,
|
|
2452
|
+
error: { code: -32603, message: `Internal error: ${err.message}` }
|
|
2453
|
+
}) + '\n');
|
|
2454
|
+
}
|
|
2455
|
+
});
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
// v1.5.2.1 F1: signal handlers belong to the server process — installing
|
|
2459
|
+
// them on import would steal SIGINT/SIGTERM from whatever host imported us.
|
|
2460
|
+
// v1.5.2.1 M-2 (Lens 1): idempotency flag — a re-entrant call would stack
|
|
2461
|
+
// duplicate SIGINT/SIGTERM handlers (each calling process.exit(0)) and
|
|
2462
|
+
// stack uncaughtException loggers (duplicating stderr noise per crash).
|
|
2463
|
+
let __signalsAttached = false;
|
|
2464
|
+
function __attachSignalHandlers() {
|
|
2465
|
+
if (__signalsAttached) return;
|
|
2466
|
+
__signalsAttached = true;
|
|
2467
|
+
process.on('SIGINT', () => process.exit(0));
|
|
2468
|
+
process.on('SIGTERM', () => process.exit(0));
|
|
2469
|
+
process.on('uncaughtException', (err) => {
|
|
2470
|
+
process.stderr.write(`IJFW: uncaught: ${err.stack || err.message}\n`);
|
|
2471
|
+
});
|
|
2472
|
+
process.on('unhandledRejection', (err) => {
|
|
2473
|
+
process.stderr.write(`IJFW: unhandled rejection: ${err}\n`);
|
|
2474
|
+
});
|
|
2475
|
+
}
|
|
2297
2476
|
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2477
|
+
// v1.5.2.1 F6.META: when migrateFactsInternalOnce returns a list of orphan
|
|
2478
|
+
// files (stray facts at visible-layer paths after relocation), surface them
|
|
2479
|
+
// to the operator ONCE — keyed by a sentinel containing the fingerprint of
|
|
2480
|
+
// the orphan list, so repeated server starts don't spam stderr unless the
|
|
2481
|
+
// orphan set changes. Best-effort: any write failure is silent (stderr may
|
|
2482
|
+
// be detached during smoke tests; the sentinel may be in a read-only fs).
|
|
2483
|
+
async function __maybeWarnFactsOrphans(orphans) {
|
|
2484
|
+
if (!orphans || orphans.length === 0) return;
|
|
2485
|
+
const sentinelPath = join(REPO_ROOT, '.ijfw', '.facts-orphan-warned');
|
|
2486
|
+
// v1.5.2.1 M-5 (Lens 1): switched from '\n'.join to JSON.stringify so a
|
|
2487
|
+
// path containing an embedded newline cannot collide-by-fingerprint with
|
|
2488
|
+
// a different orphan set. JSON encodes the separator and each element
|
|
2489
|
+
// unambiguously.
|
|
2490
|
+
const orphanFingerprint = JSON.stringify(orphans.slice().sort());
|
|
2301
2491
|
try {
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2492
|
+
const prev = readFileSync(sentinelPath, 'utf8');
|
|
2493
|
+
if (prev === orphanFingerprint) return;
|
|
2494
|
+
} catch { /* sentinel missing; fall through to warn */ }
|
|
2495
|
+
try {
|
|
2496
|
+
process.stderr.write(
|
|
2497
|
+
`[ijfw facts-migrate] stray facts files at visible-layer paths ` +
|
|
2498
|
+
`(NOT read at runtime; safe to delete after backup):\n` +
|
|
2499
|
+
orphans.map(p => ` rm "${p}"`).join('\n') + '\n'
|
|
2500
|
+
);
|
|
2501
|
+
// v1.5.2.1 M-4 (Lens 1): ensure parent dir exists before writing the
|
|
2502
|
+
// sentinel. The orphan-warn helper can fire before any code has created
|
|
2503
|
+
// .ijfw/ (e.g. on a brand-new repo where the only IJFW artifact is the
|
|
2504
|
+
// stray legacy file we're warning about).
|
|
2505
|
+
try {
|
|
2506
|
+
mkdirSync(dirname(sentinelPath), { recursive: true });
|
|
2507
|
+
writeFileSync(sentinelPath, orphanFingerprint, 'utf8');
|
|
2508
|
+
} catch {}
|
|
2509
|
+
} catch { /* stderr detached */ }
|
|
2510
|
+
}
|
|
2511
|
+
|
|
2512
|
+
// v1.5.2.1 F1: the single entry point for ALL server-startup side effects.
|
|
2513
|
+
// Anything that touches the filesystem, opens sockets, or installs process
|
|
2514
|
+
// listeners belongs here. The __isServerEntryPoint gate below ensures this
|
|
2515
|
+
// only fires when server.js is the Node entry point — never on import.
|
|
2516
|
+
async function __mainEntryPoint() {
|
|
2517
|
+
// F5: relocate FACTS_FILE / FACTS_DB_FILE from legacy .ijfw/memory/ to
|
|
2518
|
+
// .ijfw/. Sync + idempotent. F6.META: surface any orphan files left
|
|
2519
|
+
// behind so the operator can clean them up.
|
|
2520
|
+
try {
|
|
2521
|
+
const factsResult = migrateFactsInternalOnce(REPO_ROOT);
|
|
2522
|
+
if (factsResult && Array.isArray(factsResult.orphans) && factsResult.orphans.length > 0) {
|
|
2523
|
+
await __maybeWarnFactsOrphans(factsResult.orphans);
|
|
2524
|
+
}
|
|
2525
|
+
} catch (e) {
|
|
2526
|
+
try { process.stderr.write(`[ijfw facts-migrate] failed: ${e && e.message ? e.message : e}\n`); } catch {}
|
|
2309
2527
|
}
|
|
2528
|
+
// F3.4: fs-layout migrations via static registry (NOT the SQL runner).
|
|
2529
|
+
// L-2 (Lens 1): runLayoutMigrations returns per-migration results so one
|
|
2530
|
+
// failure cannot abort siblings. Surface each failure to stderr.
|
|
2310
2531
|
try {
|
|
2311
|
-
const
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
}
|
|
2319
|
-
}
|
|
2320
|
-
} else if (response) {
|
|
2321
|
-
process.stdout.write(response + '\n');
|
|
2532
|
+
const layoutResults = await runLayoutMigrations(REPO_ROOT);
|
|
2533
|
+
for (const r of layoutResults) {
|
|
2534
|
+
if (!r.ok) {
|
|
2535
|
+
try {
|
|
2536
|
+
process.stderr.write(
|
|
2537
|
+
`[ijfw layout-migrate] ${r.description}: ${r.error}\n`
|
|
2538
|
+
);
|
|
2539
|
+
} catch {}
|
|
2540
|
+
}
|
|
2322
2541
|
}
|
|
2323
|
-
} catch (
|
|
2324
|
-
process.
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
}
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
process.stderr.write(`IJFW: uncaught: ${err.stack || err.message}\n`);
|
|
2336
|
-
});
|
|
2337
|
-
process.on('unhandledRejection', (err) => {
|
|
2338
|
-
process.stderr.write(`IJFW: unhandled rejection: ${err}\n`);
|
|
2339
|
-
});
|
|
2542
|
+
} catch (e) {
|
|
2543
|
+
try { process.stderr.write(`[ijfw layout-migrate] failed: ${e && e.message ? e.message : e}\n`); } catch {}
|
|
2544
|
+
}
|
|
2545
|
+
__bootstrapDirs();
|
|
2546
|
+
__maybeStartWsClient();
|
|
2547
|
+
__attachStdioTransport();
|
|
2548
|
+
__attachSignalHandlers();
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2551
|
+
if (__isServerEntryPoint) {
|
|
2552
|
+
await __mainEntryPoint();
|
|
2553
|
+
}
|
|
2340
2554
|
|
|
2341
2555
|
// Export for tests (Node ESM allows this -- only consumed when imported, not on stdio run)
|
|
2342
2556
|
// gatePermissionAndQuota is exported inline at its declaration above (B16/SEC-M-03)
|
|
@@ -2348,4 +2562,5 @@ export {
|
|
|
2348
2562
|
handleStore, handleRecall, handleSearch, handlePrelude,
|
|
2349
2563
|
MEMORY_DIR, FACTS_FILE, FACTS_DB_FILE,
|
|
2350
2564
|
getFactsDb,
|
|
2565
|
+
paths,
|
|
2351
2566
|
};
|
package/src/update-apply.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// terminal CLI does not require the sentinel to confirm — the token itself is
|
|
8
8
|
// authoritative. The tool is retained for v1.5.0 back-compat (older skills that
|
|
9
9
|
// still call it work unchanged) and slated for retirement in v1.6.0 to free the
|
|
10
|
-
// MCP-tool slot (see CLAUDE.md "MCP server: ≤
|
|
10
|
+
// MCP-tool slot (see CLAUDE.md "MCP server: ≤14 tools" cap).
|
|
11
11
|
//
|
|
12
12
|
// Does NOT execute the update. Validates the token, writes (or overwrites)
|
|
13
13
|
// the pending sentinel, returns instruction telling the user to run the
|