@ijfw/memory-server 1.5.0 → 1.5.1

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.
@@ -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;
@@ -449,8 +418,25 @@ function parseArgsInner(args) {
449
418
  return { cmd: 'codex', sub: args[1] || 'doctor' };
450
419
  }
451
420
 
452
- if (args[0] === 'memory' && args[1] === 'checkpoint') {
453
- return { cmd: 'memory-checkpoint', label: args[2] || 'manual' };
421
+ if (args[0] === 'memory') {
422
+ // `ijfw memory` / `ijfw memory --help` / `ijfw memory -h` → namespace help
423
+ if (args.length === 1 || args[1] === '--help' || args[1] === '-h') {
424
+ return { cmd: 'memory-help' };
425
+ }
426
+ if (args[1] === 'checkpoint') {
427
+ return { cmd: 'memory-checkpoint', label: args[2] || 'manual' };
428
+ }
429
+ if (args[1] === 'reindex') {
430
+ // `ijfw memory reindex` -> M1 backfill (free, obsidian indexing)
431
+ // `ijfw memory reindex --m2` -> also run M2 A-Mem auto-link backfill
432
+ // (budget-gated; needs IJFW_AUTOLINK_*)
433
+ let m2 = false;
434
+ for (let i = 2; i < args.length; i++) {
435
+ if (args[i] === '--m2' || args[i] === '--autolink') m2 = true;
436
+ }
437
+ return { cmd: 'memory-reindex', m2 };
438
+ }
439
+ return { cmd: 'memory-unknown', sub: args[1] };
454
440
  }
455
441
 
456
442
  if (args[0] === 'recover') {
@@ -514,86 +500,32 @@ function parseArgsInner(args) {
514
500
  // Commands
515
501
  // ---------------------------------------------------------------------------
516
502
 
517
- function printUsage() {
503
+ function printMemoryHelp() {
518
504
  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.
505
+ ijfw memory -- project memory namespace
521
506
 
522
507
  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
508
+ ijfw memory checkpoint <label> Snapshot current swarm/memory state under <label>.
509
+ <label> defaults to "manual" if omitted.
510
+ ijfw memory reindex [--m2] Backfill M1 obsidian indexing (wikilinks,
511
+ #tags, [k:: v] metadata) over the whole
512
+ memory db. Free + idempotent. Add --m2 to
513
+ also run the A-Mem auto-link backfill --
514
+ budget-gated (set IJFW_AUTOLINK_BUDGET_USD
515
+ and IJFW_AUTOLINK_BACKFILL=1).
516
+
517
+ Related:
518
+ ijfw recover [status|latest] Inspect checkpoints and recovery state.
519
+ ijfw --help Top-level user-facing commands.
520
+ ijfw commands Full command surface (all verbs).
594
521
  `.trim());
595
522
  }
596
523
 
524
+ function printUnknownCommand(raw) {
525
+ console.error(`Unknown command: ${raw}`);
526
+ console.error('Run `ijfw --help` for the user-facing command list, or `ijfw commands` for the full surface.');
527
+ }
528
+
597
529
  function cmdCommandAlias(alias) {
598
530
  const info = COMMAND_ALIAS_HELP[alias];
599
531
  if (!info) {
@@ -606,6 +538,41 @@ function cmdCommandAlias(alias) {
606
538
  process.exit(0);
607
539
  }
608
540
 
541
+ // v1.5.1 W2.H — `ijfw metrics --benchmark`: run the memory benchmark harness
542
+ // (T22) against IJFW's own 3-tier store and report recall@k, MRR/NDCG-style
543
+ // retrieval quality, throughput, and p50/p95/p99 latency. See
544
+ // docs/MEMORY-BENCHMARK.md for the axes and how to interpret the numbers.
545
+ async function cmdMetricsBenchmark(opts = {}) {
546
+ const results = await runBenchmark({
547
+ root: process.cwd(),
548
+ write: opts.write !== false,
549
+ });
550
+
551
+ if (wantsJson(opts)) {
552
+ emitJson(results);
553
+ return;
554
+ }
555
+
556
+ const q = results.axes.query_warm_fts5;
557
+ const ing = results.axes.ingest;
558
+ const recallPairs = Object.entries(q.recall || {});
559
+ console.log('IJFW memory benchmark (T22)');
560
+ console.log('');
561
+ console.log(` corpus ${results.corpus.docs} docs / ${results.corpus.queries} queries / ${results.corpus.total_query_samples} timed samples`);
562
+ console.log(` ingest throughput ${ing.throughput_rps} rows/s`);
563
+ console.log(` ingest latency p50 ${ing.latency_ms.p50}ms p95 ${ing.latency_ms.p95}ms p99 ${ing.latency_ms.p99}ms`);
564
+ console.log(` query latency p50 ${q.latency_ms.p50}ms p95 ${q.latency_ms.p95}ms p99 ${q.latency_ms.p99}ms`);
565
+ console.log(` recall ${recallPairs.map(([k, v]) => `${k}=${v.toFixed(3)}`).join(' ')}`);
566
+ console.log(` storage ${results.axes.storage.bytes_per_memory} bytes/memory (${results.axes.storage.rows_indexed} rows)`);
567
+ console.log(` cold tier ${results.axes.query_cold_vector.available ? 'available' : 'reserved (no embedding model)'}`);
568
+ if (results.artifact_path) {
569
+ console.log('');
570
+ console.log(` artifact ${results.artifact_path}`);
571
+ }
572
+ console.log('');
573
+ console.log('Axes explained: docs/MEMORY-BENCHMARK.md');
574
+ }
575
+
609
576
  async function cmdStatus(projectDir, opts = {}) {
610
577
  const receipts = readReceipts(projectDir);
611
578
  const last = receipts[receipts.length - 1];
@@ -696,8 +663,6 @@ async function cmdDemo() {
696
663
 
697
664
  let result;
698
665
  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
666
  result = await runCrossOp({
702
667
  mode: 'audit',
703
668
  target,
@@ -2353,10 +2318,25 @@ if (isMainModule) {
2353
2318
  }
2354
2319
 
2355
2320
  if (parsed.cmd === 'help') {
2356
- printUsage();
2321
+ // v1.5.1 W1.D+E: orchestrator-side help is handled by the installer
2322
+ // (`ijfw --help` for the primary surface, `ijfw commands` for full).
2323
+ // Print a pointer instead of the old stale Usage block.
2324
+ console.log('Run `ijfw --help` for the user-facing command list, or `ijfw commands` for the full surface.');
2357
2325
  process.exit(0);
2358
2326
  }
2359
2327
 
2328
+ if (parsed.cmd === 'memory-help') {
2329
+ printMemoryHelp();
2330
+ process.exit(0);
2331
+ }
2332
+
2333
+ if (parsed.cmd === 'memory-unknown') {
2334
+ console.error(`Unknown memory subcommand: ${parsed.sub}`);
2335
+ console.error('');
2336
+ printMemoryHelp();
2337
+ process.exit(1);
2338
+ }
2339
+
2360
2340
  if (parsed.cmd === 'status') {
2361
2341
  cmdStatus(process.cwd(), parsed).catch(err => { console.error(err.message); process.exit(1); });
2362
2342
  } else if (parsed.cmd === 'demo') {
@@ -2367,6 +2347,8 @@ if (isMainModule) {
2367
2347
  cmdCrossProjectAudit(parsed).catch(err => { console.error(err.message); process.exit(1); });
2368
2348
  } else if (parsed.cmd === 'command-alias') {
2369
2349
  cmdCommandAlias(parsed.alias);
2350
+ } else if (parsed.cmd === 'metrics-benchmark') {
2351
+ cmdMetricsBenchmark(parsed).catch(err => { console.error(err.message); process.exit(1); });
2370
2352
  } else if (parsed.cmd === 'import') {
2371
2353
  cmdImport(parsed).catch(err => { console.error(err.message); process.exit(1); });
2372
2354
  } else if (parsed.cmd === 'doctor') {
@@ -2411,11 +2393,13 @@ if (isMainModule) {
2411
2393
  cmdCodex(parsed.sub);
2412
2394
  } else if (parsed.cmd === 'memory-checkpoint') {
2413
2395
  cmdMemoryCheckpoint(parsed.label);
2396
+ } else if (parsed.cmd === 'memory-reindex') {
2397
+ cmdMemoryReindex(parsed).catch(err => { console.error(err.message); process.exit(1); });
2414
2398
  } else if (parsed.cmd === 'recover') {
2415
2399
  cmdRecover(parsed.sub);
2416
2400
  } else {
2417
- console.error(`Unknown command: ${parsed.raw}`);
2418
- printUsage();
2401
+ // v1.5.1 W1.D+E: clean unknown-command message; no stale usage dump.
2402
+ printUnknownCommand(parsed.raw);
2419
2403
  process.exit(1);
2420
2404
  }
2421
2405
  }
@@ -2981,11 +2965,23 @@ function codexDoctor(projectRoot) {
2981
2965
  fix: 'restore codex/.codex/hooks.json and hook scripts',
2982
2966
  });
2983
2967
 
2968
+ // C11 — the message MUST track the same condition `ok` does. Previously the
2969
+ // message said "ijfw-memory configured" whenever config.toml merely existed,
2970
+ // so a config.toml present-but-missing-the-ijfw-memory-block printed the
2971
+ // [ !! ] failure glyph (ok=false) next to success text. Branch all three
2972
+ // states: file absent, file present-but-unconfigured, file configured.
2973
+ const _codexConfigExists = existsSync(configPath);
2974
+ const _codexMemoryConfigured =
2975
+ _codexConfigExists && readFileSync(configPath, 'utf8').includes('ijfw-memory');
2984
2976
  checks.push({
2985
2977
  name: 'MCP config',
2986
- ok: existsSync(configPath) && readFileSync(configPath, 'utf8').includes('ijfw-memory'),
2978
+ ok: _codexMemoryConfigured,
2987
2979
  required: true,
2988
- message: existsSync(configPath) ? 'ijfw-memory configured' : 'missing config.toml',
2980
+ message: _codexMemoryConfigured
2981
+ ? 'ijfw-memory configured'
2982
+ : _codexConfigExists
2983
+ ? 'config.toml present but ijfw-memory not configured'
2984
+ : 'missing config.toml',
2989
2985
  fix: 'run ijfw install or restore codex/.codex/config.toml',
2990
2986
  });
2991
2987
 
@@ -3355,6 +3351,69 @@ function cmdMemoryCheckpoint(label) {
3355
3351
  process.exit(0);
3356
3352
  }
3357
3353
 
3354
+ // v1.5.1 R5-1.2 -- `ijfw memory reindex [--m2]`. Closes Trident r5 finding
3355
+ // 1.2: memory written during v1.5.0 (before Round-4 Fix-1 wired M1/M2 into
3356
+ // the production write path) has empty memory_links / memory_tags /
3357
+ // memory_meta. Migration 009 already backfills M1 once on upgrade; this verb
3358
+ // is the manual re-run path AND the only way to opt into the M2 (A-Mem
3359
+ // auto-link) backfill, which is budget-gated because it makes one LLM call
3360
+ // per row.
3361
+ async function cmdMemoryReindex(parsed) {
3362
+ const projectRoot = process.cwd();
3363
+ // Lazy import: better-sqlite3 is heavy; only pay for it on this verb.
3364
+ const { openDb, closeDb, dbPathFor } = await import('./memory/fts5.js');
3365
+ const { backfillObsidianIndex } = await import('./memory/obsidian-parser.js');
3366
+ const { backfillAutoLink } = await import('./memory/auto-linker.js');
3367
+
3368
+ let db;
3369
+ try {
3370
+ db = await openDb(projectRoot);
3371
+ } catch (e) {
3372
+ console.error(`Memory db unavailable: ${e.message}`);
3373
+ process.exit(1);
3374
+ }
3375
+
3376
+ try {
3377
+ console.log(`Reindexing memory at ${dbPathFor(projectRoot)}`);
3378
+ // M1 -- always. Free + idempotent obsidian indexing.
3379
+ const m1 = backfillObsidianIndex(db);
3380
+ console.log(
3381
+ `M1 obsidian-index backfill: ${m1.rows} entries re-indexed ` +
3382
+ `(${m1.links} links, ${m1.tags} tags, ${m1.meta} meta` +
3383
+ `${m1.errors ? `, ${m1.errors} errors` : ''}).`,
3384
+ );
3385
+
3386
+ // M2 -- opt-in via --m2. Budget-gated; backfillAutoLink internally
3387
+ // forces past the IJFW_AUTOLINK_BACKFILL opt-in (the --m2 flag IS the
3388
+ // explicit opt-in) but still honours IJFW_AUTOLINK_OFF, the budget cap,
3389
+ // and the API-key requirement.
3390
+ if (parsed.m2) {
3391
+ const m2 = await backfillAutoLink(db, { force: true });
3392
+ if (m2.skipped) {
3393
+ console.log(
3394
+ `M2 auto-link backfill skipped (${m2.reason}). ` +
3395
+ `M2 backfill needs a positive IJFW_AUTOLINK_BUDGET_USD cap and an ` +
3396
+ `API key (IJFW_AUTOLINK_API_KEY or ANTHROPIC_API_KEY).`,
3397
+ );
3398
+ } else {
3399
+ console.log(
3400
+ `M2 auto-link backfill: ${m2.linked}/${m2.rows} entries linked ` +
3401
+ `(${m2.links_added} links, ${m2.neighbor_tags_added} neighbor tags)` +
3402
+ `${m2.stopped_early ? ' -- stopped early (budget / kill switch)' : ''}.`,
3403
+ );
3404
+ }
3405
+ } else {
3406
+ console.log(
3407
+ 'M2 auto-link backfill not run. Re-run with --m2 (budget-gated) to ' +
3408
+ 'auto-link old entries via the A-Mem LLM pass.',
3409
+ );
3410
+ }
3411
+ } finally {
3412
+ closeDb(db);
3413
+ }
3414
+ process.exit(0);
3415
+ }
3416
+
3358
3417
  function cmdRecover(sub) {
3359
3418
  if (sub === 'latest') {
3360
3419
  const latest = latestCheckpoint(process.cwd());
@@ -1184,9 +1184,22 @@ function buildCycleSummary(iteration, prior) {
1184
1184
  // this convergence cycle. Aborts a lens once its
1185
1185
  // cumulative cost in this run exceeds the cap. Defaults
1186
1186
  // to env IJFW_AUDIT_BUDGET_USD_PER_LENS.
1187
+ // autoFix v1.5.1 C2 (T27) — opt-in. When truthy, after a non-PASS
1188
+ // convergence the consensus code-fixer (recovery/code-fixer.js)
1189
+ // fires on HIGH findings that 2+ lenses agreed on. `true`
1190
+ // uses defaults; an object is forwarded to runConsensusFix
1191
+ // (minLenses, dryRun, verifyCmd, maxAutoFixFiles, ...).
1192
+ // Mutates the working tree + writes per-finding atomic
1193
+ // commits — off by default.
1194
+ // SAFETY BOUNDARY (R5-1.10): the fixer can only modify files
1195
+ // inside `projectRoot` (path containment — out-of-root
1196
+ // findings are refused) and `maxAutoFixFiles` (default 10,
1197
+ // ceiling 50) caps the distinct files one run may touch;
1198
+ // beyond the cap it stops + reports rather than mass-rewrite.
1199
+ // `dryRun: true` reports what it WOULD fix without writing.
1187
1200
  // Returns:
1188
1201
  // { verdict, iterations, findings, divergence?, stalled?, perIteration,
1189
- // timedOutTotal?, lensesOverBudget?, lensCosts }
1202
+ // timedOutTotal?, lensesOverBudget?, lensCosts, autoFix? }
1190
1203
  export async function runPhaseEConverge({
1191
1204
  commitRange,
1192
1205
  lenses = DEFAULT_LENSES,
@@ -1198,6 +1211,11 @@ export async function runPhaseEConverge({
1198
1211
  totalTimeoutMs, // v1.5.0 audit-MED-trident-M6 — cumulative timeout
1199
1212
  perLensBudgetUsd, // v1.5.0 audit-MED-trident-M5 — per-lens USD cap
1200
1213
  keepaliveOnTick, // v1.5.0 wire-W1.B — caller-supplied keepalive heartbeat
1214
+ autoFix = false, // v1.5.1 C2 (T27) — when truthy, fire the consensus
1215
+ // code-fixer on 2+-lens-agreed HIGH findings after a
1216
+ // non-PASS convergence. Accepts `true` (defaults) or an
1217
+ // options object { minLenses, dryRun, verifyCmd, ... }
1218
+ // forwarded to recovery/code-fixer.js#runConsensusFix.
1201
1219
  env = process.env,
1202
1220
  } = {}) {
1203
1221
  if (typeof dispatch !== 'function') {
@@ -1488,6 +1506,37 @@ export async function runPhaseEConverge({
1488
1506
  // break the orchestrator return value.
1489
1507
  }
1490
1508
 
1509
+ // v1.5.1 C2 (T27) — consensus code-fixer wire-up. This is the call site
1510
+ // T27 was designed for: "when 2+ lenses agree on the same HIGH, the fixer
1511
+ // fires automatically." The convergence loop is the canonical Trident
1512
+ // path; once it settles on a non-PASS verdict, extract the consensus HIGH
1513
+ // findings from `perIteration` and run recovery/code-fixer.js's atomic
1514
+ // per-finding fix loop over them. Opt-in (`autoFix`) because it mutates
1515
+ // the working tree + writes commits — never the default for a read-only
1516
+ // audit. PASS verdicts are skipped (nothing to fix). Failure here is
1517
+ // surfaced on `enriched.autoFix` but NEVER changes the convergence
1518
+ // verdict — the fixer is a downstream remediation, not a gate.
1519
+ if (autoFix && enriched.verdict !== VERDICT_PASS) {
1520
+ try {
1521
+ const { runConsensusFix } = await import('./recovery/code-fixer.js');
1522
+ const fixOpts = (autoFix && typeof autoFix === 'object') ? autoFix : {};
1523
+ const fixResult = await runConsensusFix({
1524
+ perIteration,
1525
+ projectRoot: _resolvedProjectDir,
1526
+ dispatch,
1527
+ commitRange,
1528
+ lenses,
1529
+ ...fixOpts,
1530
+ });
1531
+ enriched.autoFix = fixResult;
1532
+ } catch (err) {
1533
+ enriched.autoFix = {
1534
+ triggered: false,
1535
+ reason: `code-fixer error: ${err && err.message ? err.message : String(err)}`,
1536
+ };
1537
+ }
1538
+ }
1539
+
1491
1540
  return enriched;
1492
1541
  }
1493
1542
 
@@ -18,7 +18,7 @@
18
18
  * session-start hook so org/user-scoped extensions
19
19
  * become available in every project session.
20
20
  *
21
- * TODO(v1.5.0-major S01 — IJFW_PARENT_PROJECT_ROOT env passthrough):
21
+ * DEFERRED (harness-dependency — IJFW_PARENT_PROJECT_ROOT env passthrough):
22
22
  * The Agent({ isolation: 'worktree' }) spawn path lives in the Claude Code
23
23
  * harness (Task tool / SDK), NOT in this MCP server's dispatch flow. When the
24
24
  * harness eventually exposes a hook for env passthrough on worktree dispatch,
@@ -14,8 +14,10 @@
14
14
  *
15
15
  * Backend resolution is FAIL-CLOSED (SEC-L-02): unknown backend names throw
16
16
  * rather than silently fall through to software. This means a manifest with
17
- * `publisher_key_backend: 'libfido2'` (not yet implemented in v1.4.3) is a
18
- * hard error at sign-time, not a quiet downgrade to a weaker backend.
17
+ * `publisher_key_backend: 'libfido2'` (a direct-FIDO2 backend deferred to a
18
+ * future release the ssh-agent backend already covers FIDO2 tokens via the
19
+ * agent socket) is a hard error at sign-time, not a quiet downgrade to a
20
+ * weaker backend.
19
21
  *
20
22
  * Identity selection (SEC-H-03): when the ssh-agent backend signs, the
21
23
  * agent is asked to enumerate identities. The expected public-key blob is
@@ -1,8 +1,10 @@
1
- // ui-review-runner.js -- v1.5.0 wire-W1.D + W1.E.
1
+ // ui-review-runner.js -- v1.5.0 wire-W1.D + W1.E (v1.5.1 W2.A: intake wired).
2
2
  //
3
- // Production wire-up for the 7 design libs (uispec-intake, uispec-drift,
3
+ // Production wire-up for the 6 design libs (uispec-intake, uispec-drift,
4
4
  // a11y-contract, lighthouse-pillar, playwright-baseline, sketches-gc) plus
5
5
  // the 7-pillar visual audit declared in `claude/agents/ijfw-ui-auditor.md`.
6
+ // All 6 are imported below; the import list IS the canonical wiring count —
7
+ // docstring and imports must move together (v1.5.1 W2.A audit finding).
6
8
  //
7
9
  // Before W1.D these libraries shipped with isolated tests but ZERO callers.
8
10
  // The auditor agent's "wave dispatch one subagent per pillar" was declared
@@ -38,6 +40,7 @@ import {
38
40
  import { evaluateLighthouse, LIGHTHOUSE_THRESHOLDS } from './lighthouse-pillar.js';
39
41
  import { compareToBaseline } from './playwright-baseline.js';
40
42
  import { runSketchesGc } from './sketches-gc.js';
43
+ import { fromImage, fromFigma } from './uispec-intake.js';
41
44
 
42
45
  // Pillar order is canonical -- the auditor agent spec enumerates them in
43
46
  // this exact sequence. The runner emits per-pillar sections in the same
@@ -374,13 +377,16 @@ const GRADERS = Object.freeze({
374
377
  * @param {object} [args.peerInputs] { axe, lighthouse, playwright } -- optional pre-computed peer-tool outputs
375
378
  * @param {boolean} [args.write] when true, write UI-REVIEW.md (default true)
376
379
  * @param {boolean} [args.gcSketches] when true, run sketches-gc as the finalizer (default false)
380
+ * @param {string} [args.fromImage] when set, run uispec-intake.fromImage and attach the stub to the result + UI-REVIEW.md "Intake" section.
381
+ * @param {string} [args.fromFigma] when set, run uispec-intake.fromFigma (same surfacing as fromImage).
377
382
  * @returns {Promise<{
378
383
  * topVerdict: 'PASS'|'FLAG'|'BLOCK',
379
384
  * pillarVerdicts: Record<string, string>,
380
385
  * verdicts: Array<{pillar, verdict, findings, startedAt, finishedAt}>,
381
386
  * reviewPath: string|null,
382
387
  * reviewMarkdown: string,
383
- * parallel: { minStart: number, maxStart: number, minFinish: number, maxFinish: number, parallelism: number }
388
+ * parallel: { minStart: number, maxStart: number, minFinish: number, maxFinish: number, parallelism: number },
389
+ * intake: { kind: 'image'|'figma', ok: boolean, stub: object|null, error: string|null }|null
384
390
  * }>}
385
391
  */
386
392
  export async function runUiReview({
@@ -390,6 +396,8 @@ export async function runUiReview({
390
396
  peerInputs = {},
391
397
  write = true,
392
398
  gcSketches = false,
399
+ fromImage: fromImagePath = null,
400
+ fromFigma: fromFigmaUrl = null,
393
401
  } = {}) {
394
402
  if (typeof uiSpecPath !== 'string' || uiSpecPath.length === 0) {
395
403
  throw new TypeError('runUiReview: uiSpecPath is required');
@@ -410,6 +418,20 @@ export async function runUiReview({
410
418
  const spec = parseUISpec(rawSpec);
411
419
  spec.__rawText = rawSpec;
412
420
 
421
+ // v1.5.1 W2.A: optional uispec-intake pre-fill. When the caller supplies
422
+ // --from-image or --from-figma we run the intake helper and surface the
423
+ // resulting stub on the review (rendered into UI-REVIEW.md and returned
424
+ // structurally). This does NOT mutate the parsed spec used for grading —
425
+ // intake is purely a pre-fill hint for the user's next UI-SPEC edit.
426
+ let intake = null;
427
+ if (fromImagePath) {
428
+ const res = fromImage(fromImagePath, { projectRoot });
429
+ intake = { kind: 'image', ok: res.ok, stub: res.stub, error: res.error };
430
+ } else if (fromFigmaUrl) {
431
+ const res = await fromFigma(fromFigmaUrl);
432
+ intake = { kind: 'figma', ok: res.ok, stub: res.stub, error: res.error };
433
+ }
434
+
413
435
  const files = walkSourceFiles(scopes, projectRoot);
414
436
 
415
437
  // W1.E: 7 graders in parallel via Promise.all. Concurrency witness is a
@@ -475,6 +497,7 @@ export async function runUiReview({
475
497
  sourceScope: scopes,
476
498
  verdicts,
477
499
  topVerdict,
500
+ intake,
478
501
  });
479
502
 
480
503
  let reviewPath = null;
@@ -488,7 +511,7 @@ export async function runUiReview({
488
511
  try { runSketchesGc({ root: join(projectRoot, '.planning', 'sketches') }); } catch {}
489
512
  }
490
513
 
491
- return { topVerdict, pillarVerdicts, verdicts, reviewPath, reviewMarkdown, parallel };
514
+ return { topVerdict, pillarVerdicts, verdicts, reviewPath, reviewMarkdown, parallel, intake };
492
515
  }
493
516
 
494
517
  function computeTopVerdict(verdicts) {
@@ -503,7 +526,7 @@ function computeTopVerdict(verdicts) {
503
526
  return top;
504
527
  }
505
528
 
506
- function renderReview({ uiSpecPath, sourceScope, verdicts, topVerdict }) {
529
+ function renderReview({ uiSpecPath, sourceScope, verdicts, topVerdict, intake = null }) {
507
530
  const date = new Date().toISOString().slice(0, 10);
508
531
  const scopeStr = Array.isArray(sourceScope) ? sourceScope.join(',') : String(sourceScope);
509
532
  const lines = [
@@ -512,9 +535,27 @@ function renderReview({ uiSpecPath, sourceScope, verdicts, topVerdict }) {
512
535
  `**Spec:** ${uiSpecPath} **Source scope:** ${scopeStr}`,
513
536
  `**Top-level verdict:** ${topVerdict}`,
514
537
  '',
515
- '## Per-pillar verdicts',
516
- '',
517
538
  ];
539
+ if (intake) {
540
+ lines.push('## Intake (uispec-intake)');
541
+ lines.push('');
542
+ lines.push(`- **Source kind:** ${intake.kind}`);
543
+ lines.push(`- **Status:** ${intake.ok ? 'ok' : 'error'}`);
544
+ if (intake.error) lines.push(`- **Error:** ${intake.error}`);
545
+ if (intake.stub && intake.stub.advisory) lines.push(`- **Advisory:** ${intake.stub.advisory}`);
546
+ if (intake.stub && intake.stub.source) {
547
+ const src = intake.stub.source;
548
+ if (src.path) lines.push(`- **Path:** ${src.path}`);
549
+ if (src.url) lines.push(`- **URL:** ${src.url}`);
550
+ if (src.bytes != null) lines.push(`- **Bytes:** ${src.bytes}`);
551
+ if (src.dimensions) lines.push(`- **Dimensions:** ${src.dimensions.width}x${src.dimensions.height}`);
552
+ if (src.fileKey) lines.push(`- **Figma file key:** ${src.fileKey}`);
553
+ if (src.name) lines.push(`- **Figma file name:** ${src.name}`);
554
+ }
555
+ lines.push('');
556
+ }
557
+ lines.push('## Per-pillar verdicts');
558
+ lines.push('');
518
559
  for (const v of verdicts) {
519
560
  const title = PILLAR_TITLES[v.pillar] || v.pillar;
520
561
  lines.push(`### ${title} — ${v.verdict}`);