@ijfw/memory-server 1.5.0 → 1.5.3

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.
Files changed (71) hide show
  1. package/bin/ijfw-memorize +14 -7
  2. package/fixtures/team/book.json +6 -6
  3. package/fixtures/team/business.json +146 -20
  4. package/fixtures/team/content.json +6 -6
  5. package/fixtures/team/design.json +148 -20
  6. package/fixtures/team/mixed.json +206 -27
  7. package/fixtures/team/research.json +146 -20
  8. package/fixtures/team/software.json +148 -20
  9. package/package.json +8 -4
  10. package/src/brain/budget-guard.js +86 -0
  11. package/src/brain/citation-resolver.js +41 -0
  12. package/src/brain/context-injection.js +69 -0
  13. package/src/brain/discovery.js +83 -0
  14. package/src/brain/dream-pipeline.js +324 -0
  15. package/src/brain/dump-ingest.js +88 -0
  16. package/src/brain/entity-collapse.js +28 -0
  17. package/src/brain/export.js +112 -0
  18. package/src/brain/extractors/index.js +24 -0
  19. package/src/brain/extractors/markdown.js +27 -0
  20. package/src/brain/extractors/pdf.js +31 -0
  21. package/src/brain/extractors/transcript.js +38 -0
  22. package/src/brain/first-run-scan.js +61 -0
  23. package/src/brain/index.js +1 -0
  24. package/src/brain/layout-sentinel.js +29 -0
  25. package/src/brain/migrate-facts-internal-once.js +87 -0
  26. package/src/brain/path-guard.js +103 -0
  27. package/src/brain/paths.js +26 -0
  28. package/src/brain/promotion-suggester.js +41 -0
  29. package/src/brain/stub-detector.js +33 -0
  30. package/src/brain/tiered-llm.js +83 -0
  31. package/src/brain/wiki-compiler.js +144 -0
  32. package/src/brain/wiki-sentinels.js +45 -0
  33. package/src/brain/wiki-templates.js +94 -0
  34. package/src/cross-orchestrator-cli.js +336 -150
  35. package/src/cross-orchestrator.js +52 -3
  36. package/src/dashboard-server.js +1 -1
  37. package/src/dispatch/extension.js +1 -1
  38. package/src/dream/runner.mjs +21 -0
  39. package/src/extension-registry.js +2 -2
  40. package/src/handlers/brain-handler.js +319 -0
  41. package/src/hardware-signer.js +4 -2
  42. package/src/lib/ui-review-runner.js +48 -7
  43. package/src/memory/auto-linker.js +121 -2
  44. package/src/memory/benchmark.js +4 -3
  45. package/src/memory/layout-migrations/001-visible-layer.js +131 -0
  46. package/src/memory/layout-migrations/index.js +50 -0
  47. package/src/memory/migration-runner.js +37 -3
  48. package/src/memory/migrations/009-obsidian-backfill.js +50 -0
  49. package/src/memory/obsidian-parser.js +65 -2
  50. package/src/memory/reader.js +2 -1
  51. package/src/memory/search.js +190 -41
  52. package/src/memory/temporal.js +40 -1
  53. package/src/orchestrator/agents-md-blackboard.js +114 -1
  54. package/src/orchestrator/debug-trident-trigger.js +374 -0
  55. package/src/orchestrator/discipline-selector.js +276 -0
  56. package/src/orchestrator/merge-block-aware.js +15 -5
  57. package/src/orchestrator/post-done-runner.js +36 -8
  58. package/src/orchestrator/state-sdk.js +216 -10
  59. package/src/orchestrator/subagent-telemetry.js +19 -0
  60. package/src/orchestrator/wave-state.js +38 -0
  61. package/src/override-resolver.js +5 -3
  62. package/src/recovery/code-fixer.js +311 -6
  63. package/src/runtime-mediator.js +0 -1
  64. package/src/server.js +486 -132
  65. package/src/swarm-config.js +30 -22
  66. package/src/team/domain-templates/business.json +4 -1
  67. package/src/team/domain-templates/research.json +4 -1
  68. package/src/team/generator.js +162 -0
  69. package/src/update-apply.js +1 -1
  70. package/src/dashboard-charts.js +0 -239
  71. package/src/orchestrator/runtime-loop.js +0 -430
@@ -62,6 +62,8 @@ import {
62
62
  } from './swarm/worktree.js';
63
63
  import { renderSwarmDispatchPrompt } from './swarm/dispatch-prompt.js';
64
64
  import { syncCodexAgents } from './codex-agents.js';
65
+ // v1.5.1 W2.H — memory benchmark harness (T22). Surfaced via `ijfw metrics --benchmark`.
66
+ import { runBenchmark } from './memory/benchmark.js';
65
67
  import {
66
68
  DESIGN_ACTIONS,
67
69
  auditDesignText,
@@ -69,6 +71,12 @@ import {
69
71
  initialDesignMarkdown,
70
72
  loadDesignContext,
71
73
  } from './design-intelligence.js';
74
+ // v1.5.1 W3.A.4 — registry-driven usage + alias help.
75
+ // SINGLE SOURCE OF TRUTH lives at installer/src/command-registry.js;
76
+ // import sibling-package style, matching extension-installer.js precedent.
77
+ import {
78
+ COMMAND_REGISTRY,
79
+ } from '../../installer/src/command-registry.js';
72
80
 
73
81
  // ---------------------------------------------------------------------------
74
82
  // Auditor error translator (1.2.5)
@@ -219,68 +227,19 @@ function parseArgs(argv) {
219
227
  return out;
220
228
  }
221
229
 
222
- const COMMAND_ALIAS_HELP = {
223
- workflow: {
224
- title: 'IJFW workflow',
225
- usage: 'Use the ijfw-workflow skill in agents. Terminal helpers: ijfw team init, ijfw swarm plan, ijfw swarm prepare.',
226
- },
227
- handoff: {
228
- title: 'IJFW handoff',
229
- usage: 'Use the ijfw-handoff skill in agents, or record swarm handoff text with: ijfw blackboard handoff --message "<summary>".',
230
- },
231
- compress: {
232
- title: 'IJFW compress',
233
- usage: 'Use the ijfw-compress skill in agents. Terminal context compression is host-specific and should preserve exact paths, commands, versions, and decisions.',
234
- },
235
- consolidate: {
236
- title: 'IJFW consolidate',
237
- usage: 'Use the ijfw-handoff or ijfw-memory-audit skill to consolidate decisions into memory. For swarm state, run: ijfw memory checkpoint <label>.',
238
- },
239
- 'ijfw-audit': {
240
- title: 'IJFW audit',
241
- usage: 'Run verification with: ijfw preflight. For multi-model review, run: ijfw cross audit <target>.',
242
- },
243
- 'ijfw-execute': {
244
- title: 'IJFW execute',
245
- usage: 'Use ijfw-workflow in agents, then terminal helpers: ijfw team init, ijfw swarm plan, ijfw swarm prepare, ijfw swarm start <task-id>.',
246
- },
247
- 'ijfw-help': {
248
- title: 'IJFW help',
249
- usage: 'Run: ijfw help. Add --browser for the rendered local guide.',
250
- },
251
- 'ijfw-plan': {
252
- title: 'IJFW plan',
253
- usage: 'Use ijfw-workflow for planning. Terminal helpers: ijfw team init, ijfw swarm plan, ijfw swarm prepare --reviews.',
254
- },
255
- 'ijfw-ship': {
256
- title: 'IJFW ship',
257
- usage: 'Run: ijfw preflight. Do not publish or tag until your release gate is explicitly cleared.',
258
- },
259
- 'ijfw-verify': {
260
- title: 'IJFW verify',
261
- usage: 'Run: ijfw preflight. For focused review, run: ijfw cross audit <target>.',
262
- },
263
- 'memory-audit': {
264
- title: 'IJFW memory audit',
265
- usage: 'Use the ijfw-memory-audit skill in agents. Terminal safety net: ijfw recover status and ijfw memory checkpoint <label>.',
266
- },
267
- 'memory-consent': {
268
- title: 'IJFW memory consent',
269
- usage: 'Use IJFW memory tools only for explicit project memory. Terminal checkpoint: ijfw memory checkpoint <label>.',
270
- },
271
- 'memory-why': {
272
- title: 'IJFW memory why',
273
- usage: 'Use ijfw-recall or ijfw-memory-audit in agents to inspect why memory exists. Terminal recovery state: ijfw recover latest.',
274
- },
275
- metrics: {
276
- title: 'IJFW metrics',
277
- usage: 'Open the dashboard with: ijfw dashboard start. Agent-side metrics are available through ijfw_metrics.',
278
- },
279
- mode: {
280
- title: 'IJFW mode',
281
- usage: 'Inspect configuration with: ijfw config --audit. Statusline mode helpers: ijfw statusline --status, --compose, or --disable.',
282
- },
283
- };
230
+ // v1.5.1 W3.A.4 — COMMAND_ALIAS_HELP is now derived from the command-registry
231
+ // (entries where tier === 'pointer-stub'). Each entry's deprecatedReason
232
+ // becomes the usage line; title is auto-derived from the canonical name.
233
+ // To change the help text or add a new pointer-stub: edit
234
+ // installer/src/command-registry.js.
235
+ const COMMAND_ALIAS_HELP = Object.freeze(Object.fromEntries(
236
+ COMMAND_REGISTRY
237
+ .filter(e => e.tier === 'pointer-stub')
238
+ .map(e => [e.name, {
239
+ title: `IJFW ${e.name}`,
240
+ usage: e.deprecatedReason,
241
+ }])
242
+ ));
284
243
 
285
244
  function parseCrossAlias(mode, args) {
286
245
  let only = null;
@@ -313,6 +272,16 @@ function parseCommandAlias(args) {
313
272
  return parseCrossAlias('research', args);
314
273
  }
315
274
  if (Object.prototype.hasOwnProperty.call(COMMAND_ALIAS_HELP, name)) {
275
+ // v1.5.1 W2.H — `ijfw metrics` is a deprecated pointer-stub, but
276
+ // `ijfw metrics --benchmark` surfaces the memory benchmark harness (T22).
277
+ // Bare `ijfw metrics` still falls through to the deprecation redirect.
278
+ if (name === 'metrics' && args.includes('--benchmark')) {
279
+ const opts = { cmd: 'metrics-benchmark', json: args.includes('--json'), write: true };
280
+ for (let i = 1; i < args.length; i++) {
281
+ if (args[i] === '--no-write') opts.write = false;
282
+ }
283
+ return opts;
284
+ }
316
285
  return { cmd: 'command-alias', alias: name };
317
286
  }
318
287
  return null;
@@ -441,6 +410,13 @@ function parseArgsInner(args) {
441
410
  return { cmd: 'extension', sub: args[1] || 'list', rest: args.slice(2) };
442
411
  }
443
412
 
413
+ if (args[0] === 'env') {
414
+ // v1.5.2 F6: `ijfw env` lists every IJFW_* env var with current value,
415
+ // default, and one-line description. Closes the configuration-sprawl
416
+ // discoverability gap surfaced in the v1.5.2 cross-audit.
417
+ return { cmd: 'env' };
418
+ }
419
+
444
420
  if (args[0] === 'swarm') {
445
421
  return { cmd: 'swarm', sub: args[1] || 'status' };
446
422
  }
@@ -449,8 +425,25 @@ function parseArgsInner(args) {
449
425
  return { cmd: 'codex', sub: args[1] || 'doctor' };
450
426
  }
451
427
 
452
- if (args[0] === 'memory' && args[1] === 'checkpoint') {
453
- return { cmd: 'memory-checkpoint', label: args[2] || 'manual' };
428
+ if (args[0] === 'memory') {
429
+ // `ijfw memory` / `ijfw memory --help` / `ijfw memory -h` → namespace help
430
+ if (args.length === 1 || args[1] === '--help' || args[1] === '-h') {
431
+ return { cmd: 'memory-help' };
432
+ }
433
+ if (args[1] === 'checkpoint') {
434
+ return { cmd: 'memory-checkpoint', label: args[2] || 'manual' };
435
+ }
436
+ if (args[1] === 'reindex') {
437
+ // `ijfw memory reindex` -> M1 backfill (free, obsidian indexing)
438
+ // `ijfw memory reindex --m2` -> also run M2 A-Mem auto-link backfill
439
+ // (budget-gated; needs IJFW_AUTOLINK_*)
440
+ let m2 = false;
441
+ for (let i = 2; i < args.length; i++) {
442
+ if (args[i] === '--m2' || args[i] === '--autolink') m2 = true;
443
+ }
444
+ return { cmd: 'memory-reindex', m2 };
445
+ }
446
+ return { cmd: 'memory-unknown', sub: args[1] };
454
447
  }
455
448
 
456
449
  if (args[0] === 'recover') {
@@ -514,86 +507,67 @@ function parseArgsInner(args) {
514
507
  // Commands
515
508
  // ---------------------------------------------------------------------------
516
509
 
517
- function printUsage() {
510
+ // v1.5.2 F6: every IJFW_* env var the brain + memory subsystems read at runtime.
511
+ // Order: most-likely-to-set first. `default` is the documented fallback when
512
+ // the var is unset; `description` is one short line for `ijfw env` output.
513
+ const IJFW_ENV_VARS = [
514
+ // Brain (v1.5.2)
515
+ { name: 'IJFW_DREAM_BUDGET_USD', default: '0.50', description: 'Per-cycle USD cap for dream-cycle LLM extraction.' },
516
+ { name: 'IJFW_DREAM_BUDGET_DAY_USD', default: '5.00', description: 'Per-day USD cap; cycle stops when reached.' },
517
+ { name: 'IJFW_BRAIN_LOCAL_URL', default: '(unset)', description: 'Ollama-compatible local LLM endpoint to try first.' },
518
+ { name: 'IJFW_BRAIN_EXTRACT_MODEL', default: 'claude-haiku-4-5-20251001', description: 'Cheap-tier model id for fact extraction.' },
519
+ { name: 'IJFW_BRAIN_SYNTH_MODEL', default: 'claude-sonnet-4-6', description: 'Mid-tier model id for reconciliation + page synthesis.' },
520
+ { name: 'IJFW_BRAIN_API_KEY', default: '(falls back to ANTHROPIC_API_KEY)', description: 'API key for the synth-tier Anthropic call.' },
521
+ { name: 'IJFW_BRAIN_INJECT', default: 'never', description: '"auto"|"always" appends top-N wiki pages to handlePrelude.' },
522
+ // Memory-moat (v1.5.0 — A-Mem auto-linker)
523
+ { name: 'IJFW_AUTOLINK_OFF', default: '(unset)', description: 'Set to disable the A-Mem auto-linker entirely.' },
524
+ { name: 'IJFW_AUTOLINK_BUDGET_USD', default: '(unbounded if unset)', description: 'Per-write USD cap for auto-linker LLM calls.' },
525
+ { name: 'IJFW_AUTOLINK_BACKFILL', default: '(unset)', description: 'Set to "1" to opt into M2 backfill during `memory reindex --m2`.' },
526
+ ];
527
+
528
+ export function cmdEnv() {
529
+ const widthName = Math.max(...IJFW_ENV_VARS.map((v) => v.name.length)) + 2;
530
+ console.log('IJFW environment variables\n');
531
+ for (const v of IJFW_ENV_VARS) {
532
+ const current = process.env[v.name];
533
+ const shownValue = current !== undefined && current !== '' ? current : '(unset)';
534
+ const isOverridden = current !== undefined && current !== '';
535
+ const tag = isOverridden ? ' [SET]' : '';
536
+ console.log(` ${v.name.padEnd(widthName)} = ${shownValue}${tag}`);
537
+ console.log(` ${' '.repeat(widthName)} default: ${v.default}`);
538
+ console.log(` ${' '.repeat(widthName)} ${v.description}`);
539
+ console.log('');
540
+ }
541
+ console.log('Tip: variables tagged [SET] override their defaults. Unset values show the documented default in effect.');
542
+ }
543
+
544
+ function printMemoryHelp() {
518
545
  console.log(`
519
- ijfw -- It Just Fucking Works CLI
520
- Fire 2-4 AIs at any target. Receipts logged. Cache hits tracked. Memory follows you.
546
+ ijfw memory -- project memory namespace
521
547
 
522
548
  Usage:
523
- ijfw install
524
- ijfw uninstall
525
- ijfw preflight
526
- ijfw dashboard [start|stop|status]
527
- ijfw design [start|open|status|stop|push|clear|init|plan|audit|critique|polish|normalize|bolder|quieter|handoff]
528
- ijfw blackboard [init|status|claim|release|note|handoff]
529
- ijfw team [init|status]
530
- ijfw swarm [plan|prepare|tasks|prompt|start|complete|block|ready|status]
531
- ijfw swarm worktree [create|list|integrate|cleanup]
532
- ijfw codex [doctor|sync-agents]
533
- ijfw memory checkpoint <label>
534
- ijfw recover [status|latest]
535
- ijfw cross <mode> <target> [options]
536
- ijfw cross project-audit <rule-file> [--dry-run]
537
- ijfw import <tool> [--all] [--dry-run] [--force] [--path <p>]
538
- ijfw status
539
- ijfw doctor
540
- ijfw update
541
- ijfw receipt last
542
- ijfw --purge-receipts
543
- ijfw --help
544
-
545
- Commands:
546
- install Install IJFW into your AI coding agents.
547
- uninstall Remove IJFW and revert AI-agent configs. Same as: ijfw off
548
- preflight Run the 11-gate quality pipeline (blocking + advisory).
549
- dashboard Control the dashboard server (start, stop, status).
550
- design Control the live visual design companion.
551
- blackboard Coordinate project-local swarm state and artifact claims.
552
- team Assemble project agents, charter, and workflow manifest.
553
- swarm Plan artifact-aware parallel work from the team manifest.
554
- recover Show the latest checkpoint and next recovery step.
555
- demo 30-second live tour of the Trident (fires real auditors).
556
- cross Fire external auditors at a target. Try: ijfw cross audit README.md
557
- import Pull memory in from another tool. Try: ijfw import claude-mem --all
558
- status Show recent cross-audit activity. Try: ijfw status
559
- doctor Probe which CLIs and API keys are reachable. Try: ijfw doctor
560
- update Pull latest IJFW + reinstall merge-safely. Try: ijfw update
561
- update --check Non-invasive check. Exits 0 always; prints "update-available: <ver>" when an update exists (grep-safe).
562
- receipt last Print a redacted, shareable block from the last Trident run.
563
- --purge-receipts Clear the cross-runs receipt log. Try: ijfw --purge-receipts
564
-
565
- Modes (for ijfw cross):
566
- audit Adversarial review of a file, module, or path
567
- research Multi-source research on a topic
568
- critique Structured counter-argument generation
569
- project-audit Run the same audit across every registered IJFW project
570
- Usage: ijfw cross project-audit <rule-file> [--dry-run]
571
-
572
- Options for ijfw cross:
573
- --with <id> Force a specific auditor (comma-separated for multiple)
574
- --confirm Prompt for confirmation before firing
575
- --expand Include extended swarm when available
576
-
577
- Global flags:
578
- --json Emit JSON instead of human output. status and doctor auto-JSON
579
- on non-TTY (gh-CLI convention); version stays one-line on pipe
580
- and only JSON-ifies with explicit --json. Other commands ignore.
581
-
582
- Environment:
583
- IJFW_AUDIT_BUDGET_USD Session spend cap (default $2.00). First call is always
584
- allowed (no cap). Cap enforced from the 2nd call on.
585
-
586
- Examples:
587
- ijfw demo
588
- ijfw cross audit README.md
589
- ijfw cross research "vector search approaches"
590
- ijfw cross critique HEAD~3..HEAD
591
- ijfw cross audit CLAUDE.md --with codex,gemini
592
- ijfw status
593
- ijfw doctor
549
+ ijfw memory checkpoint <label> Snapshot current swarm/memory state under <label>.
550
+ <label> defaults to "manual" if omitted.
551
+ ijfw memory reindex [--m2] Backfill M1 obsidian indexing (wikilinks,
552
+ #tags, [k:: v] metadata) over the whole
553
+ memory db. Free + idempotent. Add --m2 to
554
+ also run the A-Mem auto-link backfill --
555
+ budget-gated (set IJFW_AUTOLINK_BUDGET_USD
556
+ and IJFW_AUTOLINK_BACKFILL=1).
557
+
558
+ Related:
559
+ ijfw recover [status|latest] Inspect checkpoints and recovery state.
560
+ ijfw env List every IJFW_* env var, current value, default, description.
561
+ ijfw --help Top-level user-facing commands.
562
+ ijfw commands Full command surface (all verbs).
594
563
  `.trim());
595
564
  }
596
565
 
566
+ function printUnknownCommand(raw) {
567
+ console.error(`Unknown command: ${raw}`);
568
+ console.error('Run `ijfw --help` for the user-facing command list, or `ijfw commands` for the full surface.');
569
+ }
570
+
597
571
  function cmdCommandAlias(alias) {
598
572
  const info = COMMAND_ALIAS_HELP[alias];
599
573
  if (!info) {
@@ -606,6 +580,41 @@ function cmdCommandAlias(alias) {
606
580
  process.exit(0);
607
581
  }
608
582
 
583
+ // v1.5.1 W2.H — `ijfw metrics --benchmark`: run the memory benchmark harness
584
+ // (T22) against IJFW's own 3-tier store and report recall@k, MRR/NDCG-style
585
+ // retrieval quality, throughput, and p50/p95/p99 latency. See
586
+ // docs/MEMORY-BENCHMARK.md for the axes and how to interpret the numbers.
587
+ async function cmdMetricsBenchmark(opts = {}) {
588
+ const results = await runBenchmark({
589
+ root: process.cwd(),
590
+ write: opts.write !== false,
591
+ });
592
+
593
+ if (wantsJson(opts)) {
594
+ emitJson(results);
595
+ return;
596
+ }
597
+
598
+ const q = results.axes.query_warm_fts5;
599
+ const ing = results.axes.ingest;
600
+ const recallPairs = Object.entries(q.recall || {});
601
+ console.log('IJFW memory benchmark (T22)');
602
+ console.log('');
603
+ console.log(` corpus ${results.corpus.docs} docs / ${results.corpus.queries} queries / ${results.corpus.total_query_samples} timed samples`);
604
+ console.log(` ingest throughput ${ing.throughput_rps} rows/s`);
605
+ console.log(` ingest latency p50 ${ing.latency_ms.p50}ms p95 ${ing.latency_ms.p95}ms p99 ${ing.latency_ms.p99}ms`);
606
+ console.log(` query latency p50 ${q.latency_ms.p50}ms p95 ${q.latency_ms.p95}ms p99 ${q.latency_ms.p99}ms`);
607
+ console.log(` recall ${recallPairs.map(([k, v]) => `${k}=${v.toFixed(3)}`).join(' ')}`);
608
+ console.log(` storage ${results.axes.storage.bytes_per_memory} bytes/memory (${results.axes.storage.rows_indexed} rows)`);
609
+ console.log(` cold tier ${results.axes.query_cold_vector.available ? 'available' : 'reserved (no embedding model)'}`);
610
+ if (results.artifact_path) {
611
+ console.log('');
612
+ console.log(` artifact ${results.artifact_path}`);
613
+ }
614
+ console.log('');
615
+ console.log('Axes explained: docs/MEMORY-BENCHMARK.md');
616
+ }
617
+
609
618
  async function cmdStatus(projectDir, opts = {}) {
610
619
  const receipts = readReceipts(projectDir);
611
620
  const last = receipts[receipts.length - 1];
@@ -696,8 +705,6 @@ async function cmdDemo() {
696
705
 
697
706
  let result;
698
707
  try {
699
- // TODO post-merge: perAuditorTimeoutSec, minResponses, quiet are added by Item 2 agent.
700
- // Passed through here; current orchestrator silently ignores unknown params.
701
708
  result = await runCrossOp({
702
709
  mode: 'audit',
703
710
  target,
@@ -1072,7 +1079,7 @@ async function cmdCross({ mode, target, only, confirm, expand, chunk }) {
1072
1079
  const chunks = buildChunkedTargets(absPath, rawTarget);
1073
1080
  console.log('');
1074
1081
  console.log(`--chunk: splitting ${rawTarget} into ${chunks.length} chunks (≈${(CHUNKER_DEFAULTS.chunkSize / 1024).toFixed(0)} KB each, ${(CHUNKER_DEFAULTS.overlap / 1024).toFixed(0)} KB overlap).`);
1075
- console.log(`Trident dispatches: ${chunks.length} × per-chunk audit. Cost scales linearly.`);
1082
+ console.log(`Trident dispatches: ${chunks.length} x per-chunk audit. Cost scales linearly.`);
1076
1083
 
1077
1084
  const perChunkResults = [];
1078
1085
  const auditorIds = new Set();
@@ -1107,7 +1114,7 @@ async function cmdCross({ mode, target, only, confirm, expand, chunk }) {
1107
1114
  }
1108
1115
  for (const f of merged) {
1109
1116
  const sev = (f.severity || 'note').toUpperCase();
1110
- const cluster = f.clusterSize > 1 ? ` [×${f.clusterSize}]` : '';
1117
+ const cluster = f.clusterSize > 1 ? ` [x${f.clusterSize}]` : '';
1111
1118
  const tgt = f.target ? ` ${f.target} —` : '';
1112
1119
  // v1.5.0 wire-W4: widen field fallback to cover description/issue/
1113
1120
  // detail/note/summary keys auditors emit. Closes the r19 "(no detail)"
@@ -2353,10 +2360,25 @@ if (isMainModule) {
2353
2360
  }
2354
2361
 
2355
2362
  if (parsed.cmd === 'help') {
2356
- printUsage();
2363
+ // v1.5.1 W1.D+E: orchestrator-side help is handled by the installer
2364
+ // (`ijfw --help` for the primary surface, `ijfw commands` for full).
2365
+ // Print a pointer instead of the old stale Usage block.
2366
+ console.log('Run `ijfw --help` for the user-facing command list, or `ijfw commands` for the full surface.');
2357
2367
  process.exit(0);
2358
2368
  }
2359
2369
 
2370
+ if (parsed.cmd === 'memory-help') {
2371
+ printMemoryHelp();
2372
+ process.exit(0);
2373
+ }
2374
+
2375
+ if (parsed.cmd === 'memory-unknown') {
2376
+ console.error(`Unknown memory subcommand: ${parsed.sub}`);
2377
+ console.error('');
2378
+ printMemoryHelp();
2379
+ process.exit(1);
2380
+ }
2381
+
2360
2382
  if (parsed.cmd === 'status') {
2361
2383
  cmdStatus(process.cwd(), parsed).catch(err => { console.error(err.message); process.exit(1); });
2362
2384
  } else if (parsed.cmd === 'demo') {
@@ -2367,6 +2389,8 @@ if (isMainModule) {
2367
2389
  cmdCrossProjectAudit(parsed).catch(err => { console.error(err.message); process.exit(1); });
2368
2390
  } else if (parsed.cmd === 'command-alias') {
2369
2391
  cmdCommandAlias(parsed.alias);
2392
+ } else if (parsed.cmd === 'metrics-benchmark') {
2393
+ cmdMetricsBenchmark(parsed).catch(err => { console.error(err.message); process.exit(1); });
2370
2394
  } else if (parsed.cmd === 'import') {
2371
2395
  cmdImport(parsed).catch(err => { console.error(err.message); process.exit(1); });
2372
2396
  } else if (parsed.cmd === 'doctor') {
@@ -2411,11 +2435,15 @@ if (isMainModule) {
2411
2435
  cmdCodex(parsed.sub);
2412
2436
  } else if (parsed.cmd === 'memory-checkpoint') {
2413
2437
  cmdMemoryCheckpoint(parsed.label);
2438
+ } else if (parsed.cmd === 'memory-reindex') {
2439
+ cmdMemoryReindex(parsed).catch(err => { console.error(err.message); process.exit(1); });
2440
+ } else if (parsed.cmd === 'env') {
2441
+ cmdEnv();
2414
2442
  } else if (parsed.cmd === 'recover') {
2415
2443
  cmdRecover(parsed.sub);
2416
2444
  } else {
2417
- console.error(`Unknown command: ${parsed.raw}`);
2418
- printUsage();
2445
+ // v1.5.1 W1.D+E: clean unknown-command message; no stale usage dump.
2446
+ printUnknownCommand(parsed.raw);
2419
2447
  process.exit(1);
2420
2448
  }
2421
2449
  }
@@ -2433,10 +2461,19 @@ function repoRootFromCli() {
2433
2461
  return join(here, '..', '..');
2434
2462
  }
2435
2463
  function findCliAsset(...rel) {
2464
+ // F-C-4 (Lens 3): probe XDG_DATA_HOME and XDG_CONFIG_HOME in addition to
2465
+ // ~/.ijfw and IJFW_HOME. Users who installed via a distro-aware packager
2466
+ // (e.g. dotfiles repo, nix, distro RPM, or any wrapper that honours XDG
2467
+ // base-dir spec) land their ijfw tree under $XDG_DATA_HOME/ijfw rather
2468
+ // than ~/.ijfw, and were silently invisible to the doctor fallback.
2469
+ const xdgData = process.env.XDG_DATA_HOME;
2470
+ const xdgConfig = process.env.XDG_CONFIG_HOME;
2436
2471
  const candidates = [
2437
2472
  join(repoRootFromCli(), ...rel),
2438
2473
  process.env.IJFW_HOME ? join(process.env.IJFW_HOME, ...rel) : null,
2439
2474
  join(homedir(), '.ijfw', ...rel),
2475
+ xdgData ? join(xdgData, 'ijfw', ...rel) : null,
2476
+ xdgConfig ? join(xdgConfig, 'ijfw', ...rel) : null,
2440
2477
  ].filter(Boolean);
2441
2478
  return candidates.find(p => existsSync(p)) || null;
2442
2479
  }
@@ -2935,6 +2972,42 @@ function cmdCodex(sub) {
2935
2972
  process.exit(1);
2936
2973
  }
2937
2974
 
2975
+ // F4.1/F4.2: resolve the canonical IJFW version with explicit source labelling
2976
+ // and a corrupt-vs-missing distinction. Exported so the codex-doctor tests can
2977
+ // exercise the fallback matrix without spawning the CLI against synthetic
2978
+ // repo trees. Inputs are absolute paths (or null) so callers can stub them.
2979
+ //
2980
+ // Returns: { canonicalVersion, canonicalSource, canonicalParseError } where
2981
+ // canonicalVersion : string | null (first source whose JSON parses)
2982
+ // canonicalSource : 'installer' | 'mcp-server' | null
2983
+ // canonicalParseError : string | null (only set if installer pkg corrupt)
2984
+ export function resolveCanonicalVersion({ installerPkg, selfPkg } = {}) {
2985
+ let canonicalVersion = null;
2986
+ let canonicalSource = null;
2987
+ let canonicalParseError = null;
2988
+ for (const [src, p] of [['installer', installerPkg], ['mcp-server', selfPkg]]) {
2989
+ if (!p || !existsSync(p)) continue;
2990
+ let raw;
2991
+ try {
2992
+ raw = readFileSync(p, 'utf8');
2993
+ } catch { continue; }
2994
+ try {
2995
+ canonicalVersion = JSON.parse(raw).version;
2996
+ canonicalSource = src;
2997
+ break;
2998
+ } catch (e) {
2999
+ // F-C-2 (Lens 3): capture parse error from whichever source was attempted
3000
+ // and label the source. Previously only installer parse errors surfaced,
3001
+ // so a corrupt mcp-server/package.json (e.g. partial pnpm install) caused
3002
+ // the doctor to render misleading "install @ijfw/install" fix text.
3003
+ if (canonicalParseError == null) {
3004
+ canonicalParseError = `${src}: ${e.message}`;
3005
+ }
3006
+ }
3007
+ }
3008
+ return { canonicalVersion, canonicalSource, canonicalParseError };
3009
+ }
3010
+
2938
3011
  function codexDoctor(projectRoot) {
2939
3012
  const root = resolve(projectRoot);
2940
3013
  const checks = [];
@@ -2962,12 +3035,50 @@ function codexDoctor(projectRoot) {
2962
3035
  const agentsMd = join(root, 'AGENTS.md');
2963
3036
 
2964
3037
  const plugin = readJsonFile(pluginPath);
3038
+ // v1.5.2.1: read the canonical version dynamically from
3039
+ // installer/package.json instead of comparing against a hardcoded literal.
3040
+ // The previous hardcoded '1.3.2' check drifted out of sync on every
3041
+ // release (latest observed: project at 1.5.1, doctor still expecting
3042
+ // 1.3.2 → false-positive failures on every fresh install). Reading from
3043
+ // installer/package.json keeps the doctor honest as long as that file
3044
+ // and the codex plugin.json are bumped together at ship-gate.
3045
+ //
3046
+ // F4.1: standalone @ijfw/memory-server installs do not ship installer/.
3047
+ // F4.3: regressing against the established findCliAsset() convention. Reuse it.
3048
+ // F4.2: differentiate ENOENT (missing) from SyntaxError (corrupt).
3049
+ const { canonicalVersion, canonicalSource, canonicalParseError } =
3050
+ resolveCanonicalVersion({
3051
+ installerPkg: findCliAsset('installer', 'package.json'),
3052
+ // F-C-1 (Lens 3): IJFW_HOME fallback was previously installer-only; make
3053
+ // mcp-server resolution symmetric so a custom-checkout user with IJFW_HOME
3054
+ // pointed at a partial tree also gets a working fallback.
3055
+ selfPkg: findCliAsset('mcp-server', 'package.json')
3056
+ ?? join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'),
3057
+ });
2965
3058
  checks.push({
2966
3059
  name: 'plugin metadata',
2967
- ok: plugin?.version === '1.3.2',
3060
+ ok: !!plugin && !!canonicalVersion && plugin.version === canonicalVersion,
2968
3061
  required: true,
2969
- message: plugin ? `version ${plugin.version}` : 'missing plugin.json',
2970
- fix: 'update codex/.codex-plugin/plugin.json',
3062
+ message: plugin
3063
+ ? (canonicalVersion
3064
+ ? `version ${plugin.version}${plugin.version === canonicalVersion ? '' : ` (expected ${canonicalVersion} per ${canonicalSource}/package.json)`}`
3065
+ : canonicalParseError
3066
+ ? `version ${plugin.version} (canonical source corrupt: ${canonicalParseError})`
3067
+ : `version ${plugin.version} (canonical version unreadable -- install @ijfw/install or set IJFW_HOME)`)
3068
+ : 'missing plugin.json',
3069
+ // F-C-9 (Lens 3): if plugin.json itself is missing, the fix text needs
3070
+ // to point at restoring plugin.json, NOT at the canonical-version dance.
3071
+ // canonicalParseError is now source-labelled by F-C-2 ("installer: ..."
3072
+ // or "mcp-server: ..."), so derive the specific package.json to restore.
3073
+ fix: !plugin
3074
+ ? `restore codex/.codex-plugin/plugin.json (try \`git checkout codex/.codex-plugin/plugin.json\`)`
3075
+ : canonicalVersion
3076
+ ? (plugin.version !== canonicalVersion
3077
+ ? `update codex/.codex-plugin/plugin.json version to ${canonicalVersion}`
3078
+ : null)
3079
+ : canonicalParseError
3080
+ ? `restore canonical source (try \`git checkout ${canonicalParseError.split(':')[0]}/package.json\`)`
3081
+ : `install @ijfw/install (npm i -g @ijfw/install), or set IJFW_HOME=<ijfw-repo-checkout>`,
2971
3082
  });
2972
3083
 
2973
3084
  const hooks = readJsonFile(hooksPath);
@@ -2981,11 +3092,23 @@ function codexDoctor(projectRoot) {
2981
3092
  fix: 'restore codex/.codex/hooks.json and hook scripts',
2982
3093
  });
2983
3094
 
3095
+ // C11 — the message MUST track the same condition `ok` does. Previously the
3096
+ // message said "ijfw-memory configured" whenever config.toml merely existed,
3097
+ // so a config.toml present-but-missing-the-ijfw-memory-block printed the
3098
+ // [ !! ] failure glyph (ok=false) next to success text. Branch all three
3099
+ // states: file absent, file present-but-unconfigured, file configured.
3100
+ const _codexConfigExists = existsSync(configPath);
3101
+ const _codexMemoryConfigured =
3102
+ _codexConfigExists && readFileSync(configPath, 'utf8').includes('ijfw-memory');
2984
3103
  checks.push({
2985
3104
  name: 'MCP config',
2986
- ok: existsSync(configPath) && readFileSync(configPath, 'utf8').includes('ijfw-memory'),
3105
+ ok: _codexMemoryConfigured,
2987
3106
  required: true,
2988
- message: existsSync(configPath) ? 'ijfw-memory configured' : 'missing config.toml',
3107
+ message: _codexMemoryConfigured
3108
+ ? 'ijfw-memory configured'
3109
+ : _codexConfigExists
3110
+ ? 'config.toml present but ijfw-memory not configured'
3111
+ : 'missing config.toml',
2989
3112
  fix: 'run ijfw install or restore codex/.codex/config.toml',
2990
3113
  });
2991
3114
 
@@ -3355,6 +3478,69 @@ function cmdMemoryCheckpoint(label) {
3355
3478
  process.exit(0);
3356
3479
  }
3357
3480
 
3481
+ // v1.5.1 R5-1.2 -- `ijfw memory reindex [--m2]`. Closes Trident r5 finding
3482
+ // 1.2: memory written during v1.5.0 (before Round-4 Fix-1 wired M1/M2 into
3483
+ // the production write path) has empty memory_links / memory_tags /
3484
+ // memory_meta. Migration 009 already backfills M1 once on upgrade; this verb
3485
+ // is the manual re-run path AND the only way to opt into the M2 (A-Mem
3486
+ // auto-link) backfill, which is budget-gated because it makes one LLM call
3487
+ // per row.
3488
+ async function cmdMemoryReindex(parsed) {
3489
+ const projectRoot = process.cwd();
3490
+ // Lazy import: better-sqlite3 is heavy; only pay for it on this verb.
3491
+ const { openDb, closeDb, dbPathFor } = await import('./memory/fts5.js');
3492
+ const { backfillObsidianIndex } = await import('./memory/obsidian-parser.js');
3493
+ const { backfillAutoLink } = await import('./memory/auto-linker.js');
3494
+
3495
+ let db;
3496
+ try {
3497
+ db = await openDb(projectRoot);
3498
+ } catch (e) {
3499
+ console.error(`Memory db unavailable: ${e.message}`);
3500
+ process.exit(1);
3501
+ }
3502
+
3503
+ try {
3504
+ console.log(`Reindexing memory at ${dbPathFor(projectRoot)}`);
3505
+ // M1 -- always. Free + idempotent obsidian indexing.
3506
+ const m1 = backfillObsidianIndex(db);
3507
+ console.log(
3508
+ `M1 obsidian-index backfill: ${m1.rows} entries re-indexed ` +
3509
+ `(${m1.links} links, ${m1.tags} tags, ${m1.meta} meta` +
3510
+ `${m1.errors ? `, ${m1.errors} errors` : ''}).`,
3511
+ );
3512
+
3513
+ // M2 -- opt-in via --m2. Budget-gated; backfillAutoLink internally
3514
+ // forces past the IJFW_AUTOLINK_BACKFILL opt-in (the --m2 flag IS the
3515
+ // explicit opt-in) but still honours IJFW_AUTOLINK_OFF, the budget cap,
3516
+ // and the API-key requirement.
3517
+ if (parsed.m2) {
3518
+ const m2 = await backfillAutoLink(db, { force: true });
3519
+ if (m2.skipped) {
3520
+ console.log(
3521
+ `M2 auto-link backfill skipped (${m2.reason}). ` +
3522
+ `M2 backfill needs a positive IJFW_AUTOLINK_BUDGET_USD cap and an ` +
3523
+ `API key (IJFW_AUTOLINK_API_KEY or ANTHROPIC_API_KEY).`,
3524
+ );
3525
+ } else {
3526
+ console.log(
3527
+ `M2 auto-link backfill: ${m2.linked}/${m2.rows} entries linked ` +
3528
+ `(${m2.links_added} links, ${m2.neighbor_tags_added} neighbor tags)` +
3529
+ `${m2.stopped_early ? ' -- stopped early (budget / kill switch)' : ''}.`,
3530
+ );
3531
+ }
3532
+ } else {
3533
+ console.log(
3534
+ 'M2 auto-link backfill not run. Re-run with --m2 (budget-gated) to ' +
3535
+ 'auto-link old entries via the A-Mem LLM pass.',
3536
+ );
3537
+ }
3538
+ } finally {
3539
+ closeDb(db);
3540
+ }
3541
+ process.exit(0);
3542
+ }
3543
+
3358
3544
  function cmdRecover(sub) {
3359
3545
  if (sub === 'latest') {
3360
3546
  const latest = latestCheckpoint(process.cwd());