@ijfw/install 1.3.0 → 1.3.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,42 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.3.2] -- 2026-05-15
4
+
5
+ **Project-agnostic swarm orchestration + live visual workflow + richer Codex native surface.** Adds first-class Team Assembly, blackboard coordination, swarm task lifecycle, conservative git worktrees, recovery checkpoints, Superpowers-style live design previews, and Claude-parity Codex command aliases.
6
+
7
+ - `ijfw design start/open/status/stop/push/clear` now runs a localhost visual companion with live reload, placeholder preview, and multi-file HTML support under `/design`.
8
+ - `ijfw team init/status` creates project-specific agent charters and workflow manifests across software, design, content, book, research, business, and mixed projects.
9
+ - `.ijfw/blackboard/` now provides runtime coordination: artifact claims, notes/findings/decisions/blockers, handoffs, prepared tasks, and append-only events.
10
+ - `ijfw swarm plan/prepare/tasks/start/complete/block/ready/status` turns team workflows into artifact-aware parallel waves with explicit dependencies and review tasks.
11
+ - `ijfw swarm prompt <task-id>` renders a scoped worker prompt for subagents, including artifact scope, dependencies, blockers, checks, blackboard commands, and non-revert rules.
12
+ - Codex-native dispatch now covers `ijfw codex doctor`, `ijfw codex sync-agents`, `.codex/agents/*.toml` generation from Team Assembly, and `ijfw swarm prompt <task-id> --codex` for pasteable Codex worker/explorer dispatch.
13
+ - `ijfw swarm worktree create/list/integrate/cleanup` adds conservative isolated git worktrees for in-progress code-heavy tasks, with no automatic conflict resolution.
14
+ - `ijfw memory checkpoint <label>` and `ijfw recover status/latest` create durable workflow snapshots so project state can be recovered after context loss.
15
+ - Workflow and team skills now include SWARM/PREPARE lifecycle guidance, review gates, worktree safety rules, and checkpoint expectations at stage transitions.
16
+ - Codex now ships the same 22 IJFW command aliases as Claude, installs them to `~/.codex/commands` and project `.codex/commands` when applicable, and advertises `commands_dir` in the Codex plugin manifest.
17
+ - The terminal CLI exposes the same command vocabulary as the agent surfaces: `ijfw cross audit <target>`, `ijfw cross-audit`, `ijfw workflow`, `ijfw memory-audit`, `ijfw ijfw-verify`, and related aliases all route through the shared IJFW command dispatcher.
18
+
19
+ Files: `mcp-server/src/blackboard.js`, `mcp-server/src/team/`, `mcp-server/src/swarm/`, `mcp-server/src/recovery/`, `mcp-server/src/dashboard-server.js`, `mcp-server/src/cross-orchestrator-cli.js`, `installer/src/ijfw.js`, `shared/skills/ijfw-workflow/SKILL.md`, platform workflow/team/design skill mirrors, and related fixtures/tests.
20
+
21
+ ## [1.3.1] -- 2026-05-12
22
+
23
+ **Codex hook cleanup + release cadence hardening.** Tightens IJFW's Codex integration for Codex 0.130+, reduces the default dependency footprint, and moves more release drift into automated gates before it reaches users.
24
+
25
+ - Codex `SessionStart` now emits `additionalContext` inside `hookSpecificOutput` with `hookEventName: "SessionStart"`, matching Codex's strict hook response shape. This closes the `SessionStart hook (failed): hook returned invalid session start JSON output` error.
26
+ - Codex `PostToolUse` is now stdout-silent. IJFW still records local observations and failure signals, but it no longer re-injects cleaned tool output as `systemMessage`, which Codex renders as visible hook warning spam.
27
+ - Codex hook Node shims now pass user/tool text after `--`, so tool output beginning with `---` is treated as data instead of a Node CLI option. This closes the `node: bad option: ---` failure.
28
+ - Codex `UserPromptSubmit` and `PreToolUse` advisory output now use the same `hookSpecificOutput.additionalContext` shape when they do emit context.
29
+ - Codex `Stop` now stays stdout-silent on routine session saves. Normal receipts are written locally; only actionable compression notices can surface by default. This closes the `Stop hook (completed) warning: [ijfw] Session #... saved` noise.
30
+ - Codex bundle tests now cover strict `SessionStart` JSON and silent `PostToolUse` behavior for both routine output and failure-looking output.
31
+ - The shipped MCP server no longer installs `@xenova/transformers` by default. Cold semantic vectors are explicit opt-in and `IJFW_VECTORS` now defaults to `off`, keeping the production install smaller and quieter under package audits.
32
+ - The preflight audit gate now runs against both `installer` and `mcp-server`, so server-side dependency drift is covered by the same release gate as the installer.
33
+ - `ijfw preflight` now routes through the canonical 11-gate preflight entrypoint from both installer and MCP CLI paths instead of falling back to `scripts/check-all.sh`.
34
+ - Platform capability drift is now gated by `platform-capabilities.json` plus `scripts/check-platform-drift.js`, covering skill counts, hook-event counts, and marketplace/plugin manifest claims.
35
+ - Update-check changelog URLs now point at GitLab releases, matching the canonical repository.
36
+ - D2 graph-write lock timeout handling now throws the intended `EBUSY_GRAPH_WRITE` error instead of tripping an undeclared variable path under collision.
37
+
38
+ Files: `codex/.codex/hooks/session-start.sh`, `codex/.codex/hooks/post-tool-use.sh`, `codex/.codex/hooks/pre-prompt.sh`, `codex/.codex/hooks/pre-tool-use.sh`, `codex/.codex/hooks/session-end.sh`, `scripts/observation/test-envelope-invariant-codex.sh`, `mcp-server/test-codex-bundle.js`, `mcp-server/src/vectors.js`, `mcp-server/package.json`, `installer/src/preflight/gates/audit-ci.js`, `installer/src/preflight.js`, `mcp-server/src/cross-orchestrator-cli.js`, `mcp-server/src/compute/graph-lock.js`, `platform-capabilities.json`, `scripts/check-platform-drift.js`.
39
+
3
40
  ## [1.2.6] -- 2026-05-01
4
41
 
5
42
  **Token sandbox + parallel workflow dispatch + DeepSeek frontier upgrade.** A new `ijfw_run` MCP tool keeps large command output out of your context window entirely -- builds, test suites, grep runs, and log tails are sandboxed to disk and summarized in a few lines instead of flooding thousands of tokens. The `ijfw-workflow` execution engine gains a formal Wave Table that makes parallel agent dispatch deterministic rather than inferred. DeepSeek moves to `deepseek-v4-pro` -- the actual frontier model -- so the Trident gets Frontier AI checking Frontier AI.
@@ -640,7 +677,7 @@ Platform count: **8 install targets -> 13 MCP-integrated + 1 rules-only**. Same
640
677
 
641
678
  - `ijfw preflight` -- 11-gate quality pipeline covering shell lint, JS lint, security scan, secret detection, npm audit, dead-code detection, license check, pack-smoke, and upgrade-smoke.
642
679
  - Blocking vs advisory distinction: exit 0 when all blocking gates pass even if advisory warnings exist. Exit 1 on any blocking failure.
643
- - Each gate uses `npx --yes <tool>@<pinned-version>`. Pinned versions in `preflight-versions.json`. Missing tools report "skipped" with a positive install hint, not a failure.
680
+ - Tool-backed gates use `npx --yes <tool>@<pinned-version>`. The dependency-audit gate uses `npm audit --json` directly against the package lockfiles. Pinned versions live in `preflight-versions.json`; missing tools report "skipped" with a positive install hint, not a failure.
644
681
  - Warm-cache SLO: <=90s. Cold-cache: <=240s. Both printed in the summary line.
645
682
  - `prepublishOnly` in `installer/package.json` now runs preflight before every publish so no tag can ship with a blocking gate open.
646
683
 
package/dist/ijfw.js CHANGED
@@ -267,6 +267,17 @@ import { spawnSync as spawnSync3 } from "node:child_process";
267
267
  import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
268
268
  import { join as join2 } from "node:path";
269
269
  import { tmpdir } from "node:os";
270
+ function formatMessages(results, severity12) {
271
+ const out = [];
272
+ for (const file of results) {
273
+ for (const msg of file.messages || []) {
274
+ if (severity12 === "error" && msg.severity !== 2) continue;
275
+ if (severity12 === "warning" && msg.severity !== 1) continue;
276
+ out.push(`${file.filePath}:${msg.line}:${msg.column} ${msg.severity === 2 ? "error" : "warning"} ${msg.ruleId} -- ${msg.message}`);
277
+ }
278
+ }
279
+ return out;
280
+ }
270
281
  async function run3(ctx) {
271
282
  const t0 = Date.now();
272
283
  const eslintVer = ctx.versions["eslint"] || "latest";
@@ -292,6 +303,7 @@ async function run3(ctx) {
292
303
  import security from '${join2(tmpDir, "node_modules", "eslint-plugin-security", "index.js").replace(/\\/g, "/")}';
293
304
  export default [
294
305
  {
306
+ linterOptions: { reportUnusedDisableDirectives: 'off' },
295
307
  files: ['installer/src/**/*.js', 'mcp-server/src/**/*.js'],
296
308
  plugins: { security },
297
309
  rules: ${JSON.stringify(RULES)},
@@ -303,13 +315,20 @@ export default [
303
315
  const eslintBin = join2(tmpDir, "node_modules", ".bin", "eslint");
304
316
  const res = spawnSync3(
305
317
  eslintBin,
306
- ["--config", configPath, "installer/src/**/*.js", "mcp-server/src/**/*.js"],
318
+ ["--no-config-lookup", "--config", configPath, "--format", "json", "installer/src/**/*.js", "mcp-server/src/**/*.js"],
307
319
  { encoding: "utf8", cwd: ctx.repoRoot, timeout: 6e4 }
308
320
  );
309
321
  const durationMs = Date.now() - t0;
310
322
  const output = (res.stdout || "") + (res.stderr || "");
311
- const lines = output.split("\n").filter(Boolean);
312
- if (res.status === 0) {
323
+ let results = [];
324
+ try {
325
+ results = JSON.parse(res.stdout || "[]");
326
+ } catch {
327
+ results = [];
328
+ }
329
+ const errorCount = results.reduce((sum, file) => sum + (file.errorCount || 0), 0);
330
+ const warningCount = results.reduce((sum, file) => sum + (file.warningCount || 0), 0);
331
+ if (res.status === 0 && errorCount === 0 && warningCount === 0) {
313
332
  return {
314
333
  name: "eslint-security",
315
334
  status: "PASS",
@@ -318,20 +337,20 @@ export default [
318
337
  durationMs
319
338
  };
320
339
  }
321
- if (res.status === 1) {
340
+ if (errorCount > 0 || res.status === 2) {
322
341
  return {
323
342
  name: "eslint-security",
324
- status: "WARN",
325
- message: "eslint-security: advisory warnings (review above)",
326
- details: lines.slice(0, 30),
343
+ status: "FAIL",
344
+ message: `eslint-security: ${errorCount || "unknown"} security error(s) found`,
345
+ details: (formatMessages(results, "error").length ? formatMessages(results, "error") : output.split("\n").filter(Boolean)).slice(0, 30),
327
346
  durationMs
328
347
  };
329
348
  }
330
349
  return {
331
350
  name: "eslint-security",
332
- status: "FAIL",
333
- message: "eslint-security: security errors found (exit code 2)",
334
- details: lines.slice(0, 30),
351
+ status: "WARN",
352
+ message: `eslint-security: ${warningCount} advisory warning(s)`,
353
+ details: (formatMessages(results, "warning").length ? formatMessages(results, "warning") : output.split("\n").filter(Boolean)).slice(0, 30),
335
354
  durationMs
336
355
  };
337
356
  } finally {
@@ -350,11 +369,7 @@ var init_eslint_security = __esm({
350
369
  "src/preflight/gates/eslint-security.js"() {
351
370
  RULES = {
352
371
  "security/detect-eval-with-expression": "error",
353
- "security/detect-non-literal-fs-filename": "warn",
354
- "security/detect-non-literal-regexp": "warn",
355
372
  "security/detect-non-literal-require": "warn",
356
- "security/detect-object-injection": "warn",
357
- "security/detect-possible-timing-attacks": "warn",
358
373
  "security/detect-pseudoRandomBytes": "error",
359
374
  "security/detect-unsafe-regex": "error"
360
375
  };
@@ -367,15 +382,15 @@ var init_eslint_security = __esm({
367
382
  // src/preflight/gates/psscriptanalyzer.js
368
383
  var psscriptanalyzer_exports = {};
369
384
  __export(psscriptanalyzer_exports, {
385
+ fallbackAnalyzePowerShellText: () => fallbackAnalyzePowerShellText,
370
386
  name: () => name4,
371
387
  parallel: () => parallel4,
372
388
  run: () => run4,
373
389
  severity: () => severity4
374
390
  });
375
391
  import { spawnSync as spawnSync4 } from "node:child_process";
376
- import { readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
392
+ import { readFileSync, readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
377
393
  import { join as join3 } from "node:path";
378
- import { platform } from "node:os";
379
394
  function findPs1Files(dir, acc = []) {
380
395
  let entries;
381
396
  try {
@@ -397,6 +412,109 @@ function findPs1Files(dir, acc = []) {
397
412
  }
398
413
  return acc;
399
414
  }
415
+ function stripPowerShellComments(source) {
416
+ let out = "";
417
+ let i = 0;
418
+ let inSingle = false;
419
+ let inDouble = false;
420
+ let inBlockComment = false;
421
+ while (i < source.length) {
422
+ const ch = source[i];
423
+ const next = source[i + 1];
424
+ if (inBlockComment) {
425
+ if (ch === "#" && next === ">") {
426
+ inBlockComment = false;
427
+ i += 2;
428
+ } else {
429
+ if (ch === "\n") out += "\n";
430
+ i += 1;
431
+ }
432
+ continue;
433
+ }
434
+ if (!inSingle && !inDouble && ch === "<" && next === "#") {
435
+ inBlockComment = true;
436
+ i += 2;
437
+ continue;
438
+ }
439
+ if (!inSingle && !inDouble && ch === "#") {
440
+ while (i < source.length && source[i] !== "\n") i += 1;
441
+ out += "\n";
442
+ continue;
443
+ }
444
+ out += ch;
445
+ if (!inDouble && ch === "'") inSingle = !inSingle;
446
+ else if (!inSingle && ch === '"' && source[i - 1] !== "`") inDouble = !inDouble;
447
+ i += 1;
448
+ }
449
+ return out;
450
+ }
451
+ function bracketIssues(source, file) {
452
+ const pairs = { "(": ")", "[": "]", "{": "}" };
453
+ const closers = new Set(Object.values(pairs));
454
+ const stack = [];
455
+ let inSingle = false;
456
+ let inDouble = false;
457
+ for (let i = 0; i < source.length; i++) {
458
+ const ch = source[i];
459
+ if (!inDouble && ch === "'") {
460
+ inSingle = !inSingle;
461
+ continue;
462
+ }
463
+ if (!inSingle && ch === '"' && source[i - 1] !== "`") {
464
+ inDouble = !inDouble;
465
+ continue;
466
+ }
467
+ if (inSingle || inDouble) continue;
468
+ if (pairs[ch]) stack.push({ ch, index: i });
469
+ else if (closers.has(ch)) {
470
+ const open = stack.pop();
471
+ if (!open || pairs[open.ch] !== ch) return [`${file}: unbalanced bracket near offset ${i}`];
472
+ }
473
+ }
474
+ if (inSingle || inDouble) return [`${file}: unterminated string literal`];
475
+ if (stack.length > 0) return [`${file}: unclosed bracket near offset ${stack[stack.length - 1].index}`];
476
+ return [];
477
+ }
478
+ function fallbackAnalyzePowerShellText(source, file = "<inline>") {
479
+ const stripped = stripPowerShellComments(source);
480
+ const issues = bracketIssues(stripped, file);
481
+ const banned = [
482
+ [/\bInvoke-Expression\b|\biex\b/i, "Invoke-Expression/iex dynamic execution"],
483
+ [/\bSet-ExecutionPolicy\b[\s\S]{0,80}\bBypass\b/i, "Set-ExecutionPolicy Bypass"],
484
+ [/\bStart-Process\b[\s\S]{0,160}\b-Verb\s+RunAs\b/i, "Start-Process -Verb RunAs elevation"],
485
+ [/\bNew-Object\s+Net\.WebClient\b|\bDownloadString\s*\(/i, "legacy WebClient/DownloadString network execution"]
486
+ ];
487
+ for (const [re2, label] of banned) {
488
+ if (re2.test(stripped)) issues.push(`${file}: banned PowerShell pattern: ${label}`);
489
+ }
490
+ return issues;
491
+ }
492
+ function runFallback(files, t0, reason) {
493
+ const issues = [];
494
+ for (const file of files) {
495
+ try {
496
+ issues.push(...fallbackAnalyzePowerShellText(readFileSync(file, "utf8"), file));
497
+ } catch (e) {
498
+ issues.push(`${file}: could not read script (${e.message || e})`);
499
+ }
500
+ }
501
+ if (issues.length > 0) {
502
+ return {
503
+ name: "psscriptanalyzer",
504
+ status: "FAIL",
505
+ message: `PowerShell fallback found issues in ${files.length} script(s)`,
506
+ details: [reason, ...issues].slice(0, 20),
507
+ durationMs: Date.now() - t0
508
+ };
509
+ }
510
+ return {
511
+ name: "psscriptanalyzer",
512
+ status: "PASS",
513
+ message: `${files.length} PowerShell script(s) clean (static fallback; ${reason})`,
514
+ details: [],
515
+ durationMs: Date.now() - t0
516
+ };
517
+ }
400
518
  async function run4(ctx) {
401
519
  const t0 = Date.now();
402
520
  const files = findPs1Files(ctx.repoRoot);
@@ -411,14 +529,15 @@ async function run4(ctx) {
411
529
  }
412
530
  const which = spawnSync4("pwsh", ["--version"], { encoding: "utf8" });
413
531
  if (which.status === null || which.error) {
414
- const isWin2 = platform() === "win32";
415
- return {
416
- name: "psscriptanalyzer",
417
- status: isWin2 ? "FAIL" : "WARN",
418
- message: isWin2 ? `pwsh not found -- ${files.length} .ps1 file(s) unchecked (install PowerShell)` : `pwsh not installed -- PSScriptAnalyzer skipped on non-Windows (runs in CI on windows-latest)`,
419
- details: [`Files that would be checked: ${files.join(", ")}`],
420
- durationMs: Date.now() - t0
421
- };
532
+ return runFallback(files, t0, "pwsh unavailable");
533
+ }
534
+ const moduleCheck = spawnSync4(
535
+ "pwsh",
536
+ ["-NoProfile", "-NonInteractive", "-Command", "if (Get-Module -ListAvailable -Name PSScriptAnalyzer) { exit 0 } else { exit 3 }"],
537
+ { encoding: "utf8", cwd: ctx.repoRoot, timeout: 1e4 }
538
+ );
539
+ if (moduleCheck.status !== 0) {
540
+ return runFallback(files, t0, "PSScriptAnalyzer module unavailable");
422
541
  }
423
542
  const script = `
424
543
  $files = @(${files.map((f) => `'${f.replace(/'/g, "''")}'`).join(",")})
@@ -450,10 +569,9 @@ if ($found) { exit 1 } else { exit 0 }
450
569
  };
451
570
  }
452
571
  const lines = ((res.stdout || "") + (res.stderr || "")).split("\n").filter(Boolean);
453
- const isWin = platform() === "win32";
454
572
  return {
455
573
  name: "psscriptanalyzer",
456
- status: isWin ? "FAIL" : "WARN",
574
+ status: "FAIL",
457
575
  message: `PSScriptAnalyzer found issues in ${files.length} script(s)`,
458
576
  details: lines.slice(0, 20),
459
577
  durationMs
@@ -578,31 +696,66 @@ __export(audit_ci_exports, {
578
696
  });
579
697
  import { spawnSync as spawnSync7 } from "node:child_process";
580
698
  import { join as join4 } from "node:path";
699
+ function parseAuditReport(output) {
700
+ const start = output.indexOf("{");
701
+ if (start === -1) return null;
702
+ try {
703
+ return JSON.parse(output.slice(start));
704
+ } catch {
705
+ return null;
706
+ }
707
+ }
708
+ function highCriticalCount(report) {
709
+ const vulns = report?.metadata?.vulnerabilities || {};
710
+ return Number(vulns.high || 0) + Number(vulns.critical || 0);
711
+ }
712
+ function vulnerableNames(report) {
713
+ const out = [];
714
+ for (const [name12, vuln] of Object.entries(report?.vulnerabilities || {})) {
715
+ const severity12 = String(vuln?.severity || "").toLowerCase();
716
+ if (severity12 === "high" || severity12 === "critical") out.push(`${name12}: ${severity12}`);
717
+ }
718
+ return out;
719
+ }
581
720
  async function run7(ctx) {
582
721
  const t0 = Date.now();
583
- const ver = ctx.versions["audit-ci"] || "latest";
584
- const configPath = join4(ctx.repoRoot, ".audit-ci.jsonc");
585
- const res = spawnSync7(
586
- "npx",
587
- ["--yes", `audit-ci@${ver}`, "--config", configPath],
588
- {
589
- encoding: "utf8",
590
- cwd: join4(ctx.repoRoot, "installer"),
591
- timeout: 6e4
592
- }
593
- );
722
+ const packageDirs = ["installer", "mcp-server"];
723
+ const runs = packageDirs.map((dir) => {
724
+ const res = spawnSync7(
725
+ "npm",
726
+ ["audit", "--audit-level=high", "--json"],
727
+ {
728
+ encoding: "utf8",
729
+ cwd: join4(ctx.repoRoot, dir),
730
+ timeout: 6e4
731
+ }
732
+ );
733
+ const output = (res.stdout || "") + (res.stderr || "");
734
+ const report = parseAuditReport(output);
735
+ const highCritical = highCriticalCount(report);
736
+ return { dir, status: res.status, output, report, highCritical };
737
+ });
594
738
  const durationMs = Date.now() - t0;
595
- const output = (res.stdout || "") + (res.stderr || "");
596
- const lines = output.split("\n").filter(Boolean);
597
- if (res.status === 0) {
739
+ const failed = runs.filter((r) => !r.report || r.highCritical > 0);
740
+ if (failed.length === 0) {
598
741
  return {
599
742
  name: "audit-ci",
600
743
  status: "PASS",
601
- message: "audit-ci: no high/critical vulnerabilities",
602
- details: [],
744
+ message: "audit-ci: no high/critical vulnerabilities in installer or mcp-server",
745
+ details: runs.map((r) => `${r.dir}: pass`),
603
746
  durationMs
604
747
  };
605
748
  }
749
+ const lines = [];
750
+ for (const r of failed) {
751
+ if (!r.report) {
752
+ lines.push(`${r.dir}: audit report unavailable`);
753
+ lines.push(...r.output.split("\n").filter(Boolean).slice(0, 10));
754
+ continue;
755
+ }
756
+ lines.push(`${r.dir}: ${r.highCritical} high/critical advisory item(s)`);
757
+ lines.push(...vulnerableNames(r.report).slice(0, 10));
758
+ }
606
759
  return {
607
760
  name: "audit-ci",
608
761
  status: "FAIL",
@@ -879,7 +1032,7 @@ __export(upgrade_smoke_exports, {
879
1032
  severity: () => severity11
880
1033
  });
881
1034
  import { spawnSync as spawnSync11 } from "node:child_process";
882
- import { mkdtempSync as mkdtempSync3, rmSync as rmSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3, readFileSync, existsSync as existsSync2 } from "node:fs";
1035
+ import { mkdtempSync as mkdtempSync3, rmSync as rmSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3, readFileSync as readFileSync2, existsSync as existsSync2 } from "node:fs";
883
1036
  import { join as join7, resolve as resolve2 } from "node:path";
884
1037
  import { tmpdir as tmpdir3 } from "node:os";
885
1038
  async function run11(ctx) {
@@ -966,7 +1119,7 @@ async function run11(ctx) {
966
1119
  if (existsSync2(settingsPath)) {
967
1120
  let settings;
968
1121
  try {
969
- settings = JSON.parse(readFileSync(settingsPath, "utf8"));
1122
+ settings = JSON.parse(readFileSync2(settingsPath, "utf8"));
970
1123
  } catch (e) {
971
1124
  return {
972
1125
  name: "upgrade-smoke",
@@ -989,7 +1142,7 @@ async function run11(ctx) {
989
1142
  }
990
1143
  const marketplaceSrc = join7(installerDir, "src", "marketplace.js");
991
1144
  if (existsSync2(marketplaceSrc)) {
992
- const src = readFileSync(marketplaceSrc, "utf8");
1145
+ const src = readFileSync2(marketplaceSrc, "utf8");
993
1146
  const registersCorrectKey = src.includes("'ijfw@ijfw'") || src.includes('"ijfw@ijfw"');
994
1147
  const registersWrongKey = /enabledPlugins\[['"]ijfw-core@ijfw['"]\]\s*=\s*true/.test(src);
995
1148
  if (!registersCorrectKey) {
@@ -1044,8 +1197,8 @@ var preflight_exports = {};
1044
1197
  __export(preflight_exports, {
1045
1198
  runPreflightCommand: () => runPreflightCommand
1046
1199
  });
1047
- import { readFileSync as readFileSync2, existsSync as existsSync3 } from "node:fs";
1048
- import { join as join8, dirname } from "node:path";
1200
+ import { readFileSync as readFileSync3, existsSync as existsSync3 } from "node:fs";
1201
+ import { join as join8, dirname, resolve as resolve3 } from "node:path";
1049
1202
  import { fileURLToPath } from "node:url";
1050
1203
  function printHelp() {
1051
1204
  console.log(`
@@ -1088,7 +1241,7 @@ function loadVersions(repoRoot2) {
1088
1241
  for (const f of candidates) {
1089
1242
  if (existsSync3(f)) {
1090
1243
  try {
1091
- return JSON.parse(readFileSync2(f, "utf8"));
1244
+ return JSON.parse(readFileSync3(f, "utf8"));
1092
1245
  } catch {
1093
1246
  }
1094
1247
  }
@@ -1144,11 +1297,24 @@ async function runPreflightCommand(argv, repoRoot2) {
1144
1297
  const report = await runPreflight(gates, ctx);
1145
1298
  process.exit(report.outcome === "pass" ? 0 : 1);
1146
1299
  }
1300
+ function defaultRepoRoot() {
1301
+ let dir = __dirname;
1302
+ for (let i = 0; i < 8; i++) {
1303
+ if (existsSync3(join8(dir, "package.json")) && existsSync3(join8(dir, "mcp-server"))) return dir;
1304
+ const next = resolve3(dir, "..");
1305
+ if (next === dir) break;
1306
+ dir = next;
1307
+ }
1308
+ return process.cwd();
1309
+ }
1147
1310
  var __dirname;
1148
1311
  var init_preflight = __esm({
1149
- "src/preflight.js"() {
1312
+ async "src/preflight.js"() {
1150
1313
  init_runner();
1151
1314
  __dirname = dirname(fileURLToPath(import.meta.url));
1315
+ if (process.argv[1] && resolve3(process.argv[1]) === fileURLToPath(import.meta.url)) {
1316
+ await runPreflightCommand(process.argv, defaultRepoRoot());
1317
+ }
1152
1318
  }
1153
1319
  });
1154
1320
 
@@ -2412,20 +2578,76 @@ Please report this to https://github.com/markedjs/marked.`, e) {
2412
2578
  });
2413
2579
 
2414
2580
  // src/ijfw.js
2415
- import { dirname as dirname2, join as join9, resolve as resolve3, basename } from "node:path";
2581
+ import { dirname as dirname2, join as join9, resolve as resolve4, basename } from "node:path";
2416
2582
  import { fileURLToPath as fileURLToPath2 } from "node:url";
2417
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, copyFileSync, readdirSync as readdirSync4, rmSync as rmSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "node:fs";
2418
- import { homedir, platform as platform2 } from "node:os";
2583
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, copyFileSync, readdirSync as readdirSync4, rmSync as rmSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
2584
+ import { homedir, platform } from "node:os";
2419
2585
  import { spawnSync as spawnSync12 } from "node:child_process";
2420
2586
  var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
2421
2587
  function repoRoot() {
2422
2588
  let dir = __dirname2;
2423
2589
  for (let i = 0; i < 6; i++) {
2424
2590
  if (existsSync4(join9(dir, "package.json")) && existsSync4(join9(dir, ".git"))) return dir;
2425
- dir = resolve3(dir, "..");
2591
+ dir = resolve4(dir, "..");
2426
2592
  }
2427
2593
  return process.cwd();
2428
2594
  }
2595
+ function findInternalAsset(...rel) {
2596
+ const root = repoRoot();
2597
+ const ijfwHome = join9(homedir(), ".ijfw");
2598
+ const candidates = [join9(root, ...rel), join9(ijfwHome, ...rel)];
2599
+ return candidates.find((p) => existsSync4(p)) || null;
2600
+ }
2601
+ function readDashboardPort() {
2602
+ const portFile = join9(homedir(), ".ijfw", "dashboard.port");
2603
+ try {
2604
+ const port = Number.parseInt(readFileSync4(portFile, "utf8").trim(), 10);
2605
+ return Number.isFinite(port) ? port : 37891;
2606
+ } catch {
2607
+ return 37891;
2608
+ }
2609
+ }
2610
+ function openBrowser(url) {
2611
+ if (process.env.CI || process.env.NO_OPEN) return;
2612
+ const r = platform() === "darwin" ? spawnSync12("open", [url], { stdio: "ignore" }) : platform() === "win32" ? spawnSync12("cmd", ["/c", "start", "", url], { stdio: "ignore", shell: false }) : spawnSync12("xdg-open", [url], { stdio: "ignore" });
2613
+ return r.status ?? 0;
2614
+ }
2615
+ var ORCHESTRATOR_COMMANDS = /* @__PURE__ */ new Set([
2616
+ "update",
2617
+ "statusline",
2618
+ "config",
2619
+ "insight",
2620
+ "blackboard",
2621
+ "team",
2622
+ "swarm",
2623
+ "codex",
2624
+ "recover",
2625
+ "memory",
2626
+ "cross",
2627
+ "status",
2628
+ "demo",
2629
+ "import",
2630
+ "receipt",
2631
+ "--purge-receipts",
2632
+ "workflow",
2633
+ "handoff",
2634
+ "compress",
2635
+ "consolidate",
2636
+ "cross-audit",
2637
+ "cross-critique",
2638
+ "cross-research",
2639
+ "ijfw-audit",
2640
+ "ijfw-execute",
2641
+ "ijfw-help",
2642
+ "ijfw-plan",
2643
+ "ijfw-ship",
2644
+ "ijfw-verify",
2645
+ "memory-audit",
2646
+ "memory-consent",
2647
+ "memory-why",
2648
+ "metrics",
2649
+ "mode"
2650
+ ]);
2429
2651
  function printHelp2() {
2430
2652
  console.log(`
2431
2653
  ijfw -- the AI efficiency layer
@@ -2437,9 +2659,15 @@ COMMANDS
2437
2659
  install Install IJFW into your AI coding agents
2438
2660
  uninstall Remove IJFW from your AI coding agents
2439
2661
  help Open the full IJFW guide (terminal, or --browser for rendered)
2440
- preflight Run 12-gate quality pipeline before publishing
2662
+ preflight Run 11-gate quality pipeline before publishing
2441
2663
  dashboard Start / stop / check the local observability dashboard
2442
- design Manage the visual design companion
2664
+ design Manage live previews and durable design intelligence
2665
+ blackboard Coordinate project-local swarm state and artifact claims
2666
+ codex Check and sync Codex-native IJFW surfaces
2667
+ team Assemble project agents, charter, and workflow manifest
2668
+ swarm Plan, prepare, and track artifact-aware parallel work
2669
+ recover Show latest checkpoint and next recovery step
2670
+ cross Run Trident audit/research/critique, e.g. ijfw cross audit README.md
2443
2671
  doctor Diagnose IJFW installation health
2444
2672
 
2445
2673
  --help, -h Show this help
@@ -2477,7 +2705,7 @@ async function main() {
2477
2705
  if (delegateToCli(argv.slice(2))) return;
2478
2706
  try {
2479
2707
  const pkgPath = join9(__dirname2, "..", "package.json");
2480
- const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
2708
+ const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
2481
2709
  console.log(`@ijfw/install@${pkg.version || "unknown"}`);
2482
2710
  if (verbose) {
2483
2711
  console.log(" (full --verbose details require a completed install: run ijfw install)");
@@ -2487,44 +2715,41 @@ async function main() {
2487
2715
  }
2488
2716
  process.exit(0);
2489
2717
  }
2490
- if (sub === "update" || sub === "statusline" || sub === "config" || sub === "insight") {
2718
+ if (ORCHESTRATOR_COMMANDS.has(sub)) {
2491
2719
  if (delegateToCli(argv.slice(2))) return;
2492
2720
  console.error(`'ijfw ${sub}' requires a completed IJFW install. Run: ijfw install`);
2493
2721
  process.exit(1);
2494
2722
  }
2723
+ if (sub === "doctor" && findCli()) {
2724
+ if (delegateToCli(argv.slice(2))) return;
2725
+ }
2495
2726
  switch (sub) {
2496
2727
  case "install": {
2497
- const installBin = resolve3(__dirname2, "..", "dist", "install.js");
2728
+ const installBin = resolve4(__dirname2, "..", "dist", "install.js");
2498
2729
  const r = spawnSync12("node", [installBin, ...argv.slice(3)], { stdio: "inherit" });
2499
2730
  process.exit(r.status ?? 1);
2500
2731
  break;
2501
2732
  }
2502
2733
  case "uninstall": {
2503
- const uninstallBin = resolve3(__dirname2, "..", "dist", "uninstall.js");
2734
+ const uninstallBin = resolve4(__dirname2, "..", "dist", "uninstall.js");
2504
2735
  const r = spawnSync12("node", [uninstallBin, ...argv.slice(3)], { stdio: "inherit" });
2505
2736
  process.exit(r.status ?? 1);
2506
2737
  break;
2507
2738
  }
2508
2739
  case "preflight": {
2509
- const { runPreflightCommand: runPreflightCommand2 } = await Promise.resolve().then(() => (init_preflight(), preflight_exports));
2740
+ const { runPreflightCommand: runPreflightCommand2 } = await init_preflight().then(() => preflight_exports);
2510
2741
  await runPreflightCommand2([argv[0], argv[1], ...argv.slice(3)], repoRoot());
2511
2742
  break;
2512
2743
  }
2513
2744
  case "dashboard": {
2514
2745
  const dashSub = argv[3];
2515
- const root = repoRoot();
2516
- const ijfwHome = join9(homedir(), ".ijfw");
2517
- const findInTree = (...rel) => {
2518
- const candidates = [join9(root, ...rel), join9(ijfwHome, ...rel)];
2519
- return candidates.find((p) => existsSync4(p)) || null;
2520
- };
2521
2746
  if (dashSub === "start" || dashSub === "stop" || dashSub === "status") {
2522
- const dashBin = findInTree("mcp-server", "bin", "ijfw-dashboard");
2747
+ const dashBin = findInternalAsset("mcp-server", "bin", "ijfw-dashboard");
2523
2748
  if (dashBin) {
2524
2749
  const r = spawnSync12("node", [dashBin, dashSub, ...argv.slice(4)], { stdio: "inherit" });
2525
2750
  process.exit(r.status ?? 0);
2526
2751
  } else {
2527
- const serverJs = findInTree("mcp-server", "src", "dashboard-server.js");
2752
+ const serverJs = findInternalAsset("mcp-server", "src", "dashboard-server.js");
2528
2753
  if (dashSub === "start" && serverJs) {
2529
2754
  const { spawn } = await import("node:child_process");
2530
2755
  const child = spawn(process.execPath, [serverJs, "start", "--daemon"], {
@@ -2539,7 +2764,7 @@ async function main() {
2539
2764
  process.exit(1);
2540
2765
  }
2541
2766
  } else if (dashSub === "render" || !dashSub) {
2542
- const binJs = findInTree("scripts", "dashboard", "bin.js");
2767
+ const binJs = findInternalAsset("scripts", "dashboard", "bin.js");
2543
2768
  if (binJs) {
2544
2769
  const r = spawnSync12("node", [binJs, ...argv.slice(dashSub ? 4 : 3)], { stdio: "inherit" });
2545
2770
  process.exit(r.status ?? 0);
@@ -2555,30 +2780,72 @@ async function main() {
2555
2780
  }
2556
2781
  case "design": {
2557
2782
  const designSub = argv[3];
2783
+ const durableDesign = ["init", "plan", "audit", "critique", "polish", "normalize", "bolder", "quieter", "handoff"];
2784
+ if (durableDesign.includes(designSub)) {
2785
+ if (delegateToCli(argv.slice(2))) return;
2786
+ console.error(`'ijfw design ${designSub}' requires a completed IJFW install. Run: ijfw install`);
2787
+ process.exit(1);
2788
+ }
2558
2789
  const contentDir = join9(homedir(), ".ijfw", "design-companion", "content");
2559
2790
  mkdirSync3(contentDir, { recursive: true });
2560
- if (designSub === "push") {
2561
- const filePath = argv[4];
2562
- if (!filePath) {
2563
- console.error("Usage: ijfw design push <file.html>");
2791
+ if (designSub === "start" || designSub === "open") {
2792
+ const dashBin = findInternalAsset("mcp-server", "bin", "ijfw-dashboard");
2793
+ if (!dashBin) {
2794
+ console.error("[ijfw] Design companion server not found. Run `ijfw-install` to deploy ~/.ijfw/, or run from the IJFW repo root.");
2795
+ process.exit(1);
2796
+ }
2797
+ const noOpen = argv.slice(4).includes("--no-open");
2798
+ const r = spawnSync12("node", [dashBin, "start", "--no-open"], { stdio: designSub === "start" ? "inherit" : "ignore" });
2799
+ if ((r.status ?? 1) !== 0) process.exit(r.status ?? 1);
2800
+ const url = `http://localhost:${readDashboardPort()}/design`;
2801
+ if (!noOpen) openBrowser(url);
2802
+ console.log(`Design companion running at ${url}`);
2803
+ } else if (designSub === "status") {
2804
+ const dashBin = findInternalAsset("mcp-server", "bin", "ijfw-dashboard");
2805
+ if (!dashBin) {
2806
+ console.error("[ijfw] Design companion server not found. Run `ijfw-install` to deploy ~/.ijfw/, or run from the IJFW repo root.");
2807
+ process.exit(1);
2808
+ }
2809
+ const r = spawnSync12("node", [dashBin, "status"], { stdio: "inherit" });
2810
+ if ((r.status ?? 1) === 0) console.log(`Design companion URL: http://localhost:${readDashboardPort()}/design`);
2811
+ process.exit(r.status ?? 0);
2812
+ } else if (designSub === "stop") {
2813
+ const dashBin = findInternalAsset("mcp-server", "bin", "ijfw-dashboard");
2814
+ if (!dashBin) {
2815
+ console.error("[ijfw] Design companion server not found. Run `ijfw-install` to deploy ~/.ijfw/, or run from the IJFW repo root.");
2564
2816
  process.exit(1);
2565
2817
  }
2566
- const abs = resolve3(filePath);
2567
- if (!existsSync4(abs)) {
2568
- console.error(`File not found: ${abs}`);
2818
+ const r = spawnSync12("node", [dashBin, "stop"], { stdio: "inherit" });
2819
+ process.exit(r.status ?? 0);
2820
+ } else if (designSub === "push") {
2821
+ const filePaths = argv.slice(4);
2822
+ if (filePaths.length === 0) {
2823
+ console.error("Usage: ijfw design push <file.html> [more.html ...]");
2569
2824
  process.exit(1);
2570
2825
  }
2571
- const dest = join9(contentDir, basename(abs));
2572
- copyFileSync(abs, dest);
2573
- console.log(`Design pushed: ${dest}`);
2826
+ for (const filePath of filePaths) {
2827
+ const abs = resolve4(filePath);
2828
+ if (!abs.toLowerCase().endsWith(".html")) {
2829
+ console.error("Design companion accepts standalone .html files.");
2830
+ process.exit(1);
2831
+ }
2832
+ if (!existsSync4(abs)) {
2833
+ console.error(`File not found: ${abs}`);
2834
+ process.exit(1);
2835
+ }
2836
+ const dest = join9(contentDir, basename(abs));
2837
+ copyFileSync(abs, dest);
2838
+ console.log(`Design pushed: ${dest}`);
2839
+ }
2840
+ console.log(`Preview: http://localhost:${readDashboardPort()}/design`);
2574
2841
  } else if (designSub === "clear") {
2575
2842
  const files = readdirSync4(contentDir);
2576
2843
  for (const f of files) rmSync4(join9(contentDir, f), { force: true });
2577
2844
  console.log("Design companion content cleared.");
2578
2845
  } else {
2579
- console.log("ijfw design -- Manage the visual design companion. Push HTML mockups for live preview.");
2846
+ console.log("ijfw design -- Manage live preview and durable design intelligence.");
2580
2847
  console.log("");
2581
- console.log("Usage: ijfw design push <file.html> | ijfw design clear");
2848
+ console.log("Usage: ijfw design start [--no-open] | open | status | stop | push <file.html> [more.html ...] | clear | init|plan|audit|critique|polish|normalize|bolder|quieter|handoff");
2582
2849
  process.exit(1);
2583
2850
  }
2584
2851
  break;
@@ -2587,7 +2854,7 @@ async function main() {
2587
2854
  const wantsBrowser = argv.slice(3).includes("--browser");
2588
2855
  const candidates = [
2589
2856
  join9(repoRoot(), "docs", "GUIDE.md"),
2590
- resolve3(__dirname2, "..", "docs", "GUIDE.md"),
2857
+ resolve4(__dirname2, "..", "docs", "GUIDE.md"),
2591
2858
  join9(homedir(), ".ijfw", "docs", "GUIDE.md")
2592
2859
  ];
2593
2860
  const guidePath = candidates.find((p) => existsSync4(p));
@@ -2605,7 +2872,7 @@ async function main() {
2605
2872
  copyFileSync(join9(assetsSrc, f), join9(outDir, "assets", f));
2606
2873
  }
2607
2874
  }
2608
- const md = readFileSync3(guidePath, "utf8").replace(/\(guide\/assets\//g, "(assets/");
2875
+ const md = readFileSync4(guidePath, "utf8").replace(/\(guide\/assets\//g, "(assets/");
2609
2876
  const rendered = marked.parse(md, { gfm: true, breaks: false });
2610
2877
  const html = `<!doctype html>
2611
2878
  <html lang="en"><head>
@@ -2624,7 +2891,7 @@ async function main() {
2624
2891
  </head><body><div class="wrap markdown-body">${rendered}</div></body></html>`;
2625
2892
  const outHtml = join9(outDir, "index.html");
2626
2893
  writeFileSync4(outHtml, html);
2627
- const opener = platform2() === "darwin" ? "open" : platform2() === "win32" ? "start" : "xdg-open";
2894
+ const opener = platform() === "darwin" ? "open" : platform() === "win32" ? "start" : "xdg-open";
2628
2895
  spawnSync12(opener, [outHtml], { stdio: "ignore", detached: true });
2629
2896
  console.log(`[ijfw] Guide opened in your browser.`);
2630
2897
  console.log(` Local copy: ${outHtml}`);
@@ -2634,10 +2901,10 @@ async function main() {
2634
2901
  if (hasLess) {
2635
2902
  const lessRes = spawnSync12("less", ["-R", guidePath], { stdio: "inherit" });
2636
2903
  if (lessRes.status !== 0 && lessRes.status !== null) {
2637
- process.stdout.write(readFileSync3(guidePath, "utf8"));
2904
+ process.stdout.write(readFileSync4(guidePath, "utf8"));
2638
2905
  }
2639
2906
  } else {
2640
- process.stdout.write(readFileSync3(guidePath, "utf8"));
2907
+ process.stdout.write(readFileSync4(guidePath, "utf8"));
2641
2908
  }
2642
2909
  process.exit(0);
2643
2910
  break;
package/dist/install.js CHANGED
@@ -385,7 +385,7 @@ function mergeYamlPluginsEnabled(dst, pluginName, ts) {
385
385
  if (inPluginsBlock) {
386
386
  if (/^\S/.test(line) && line.trim() !== "") {
387
387
  inPluginsBlock = false;
388
- } else if (/^\s+enabled:\s*(\[\s*\])?\s*$/.test(line) || /^\s+enabled:\s*\[.*\]\s*$/.test(line)) {
388
+ } else if (isIndentedEnabledLine(line)) {
389
389
  enabledLineIdx = i;
390
390
  } else if (enabledLineIdx >= 0 && itemRe.test(line)) {
391
391
  alreadyListed = true;
@@ -423,6 +423,11 @@ function mergeYamlPluginsEnabled(dst, pluginName, ts) {
423
423
  outText += "# IJFW-PLUGINS-END\n";
424
424
  writeAtomic(dst, outText, { mode: 384 });
425
425
  }
426
+ function isIndentedEnabledLine(line) {
427
+ if (!line || !/\s/.test(line[0])) return false;
428
+ const trimmed = line.trim();
429
+ return trimmed === "enabled:" || trimmed === "enabled: []" || trimmed.startsWith("enabled: [") && trimmed.endsWith("]");
430
+ }
426
431
  function opencodeMerge(dst, serverJs, ts) {
427
432
  mkdirSync2(dirname3(dst), { recursive: true });
428
433
  if (ts) backup(dst, ts);
@@ -759,6 +764,12 @@ async function installCodex(ctx) {
759
764
  for (const sd of listSubdirs(repoSkills)) {
760
765
  copyDirIfAbsent(sd.path, join4(userSkills, sd.name));
761
766
  }
767
+ const userCommands = join4(ctx.home, ".codex", "commands");
768
+ ensureDir(userCommands);
769
+ const repoCommands = join4(ctx.repoRoot, "codex", "commands");
770
+ for (const f of listFiles(repoCommands, ".md")) {
771
+ copyIfAbsent(f.path, join4(userCommands, f.name));
772
+ }
762
773
  const cwd = ctx.cwd || process.cwd();
763
774
  if (existsSync4(join4(cwd, ".codex", "config.toml")) || existsSync4(join4(cwd, ".ijfw"))) {
764
775
  const projSkills = join4(cwd, ".codex", "skills");
@@ -766,8 +777,13 @@ async function installCodex(ctx) {
766
777
  for (const sd of listSubdirs(repoSkills)) {
767
778
  copyDirIfAbsent(sd.path, join4(projSkills, sd.name));
768
779
  }
780
+ const projCommands = join4(cwd, ".codex", "commands");
781
+ ensureDir(projCommands);
782
+ for (const f of listFiles(repoCommands, ".md")) {
783
+ copyIfAbsent(f.path, join4(projCommands, f.name));
784
+ }
769
785
  }
770
- ctx.log.ok("Installed Codex bundle: MCP + hooks + 15 skills + context");
786
+ ctx.log.ok("Installed Codex bundle: MCP + hooks + 19 skills + 22 command aliases + context");
771
787
  return { status: "ok" };
772
788
  }
773
789
  async function installGemini(ctx) {
@@ -832,7 +848,7 @@ async function installGemini(ctx) {
832
848
  for (const f of listFiles(agentSrc, ".md")) {
833
849
  copyIfAbsent(f.path, join4(extDst, "agents", f.name));
834
850
  }
835
- ctx.log.ok("Installed Gemini bundle: MCP + extension + 15 skills + 11 hooks + policy");
851
+ ctx.log.ok("Installed Gemini bundle: MCP + extension + 19 skills + 11 hooks + policy");
836
852
  return { status: "ok" };
837
853
  }
838
854
  async function installWayland(ctx) {
@@ -2048,14 +2064,12 @@ function cloneOrPull(dir, branch) {
2048
2064
  else if (remoteSignal) console.warn(` git exited on signal ${remoteSignal}`);
2049
2065
  else if (remoteStatus !== 0 && remoteStderr) console.warn(` git remote get-url: ${remoteStderr.slice(0, 120).trim()}`);
2050
2066
  if (remoteStatus === 0) {
2051
- const STALE_ORIGINS = [
2052
- "https://github.com/seandonahoe/ijfw.git",
2053
- "https://github.com/seandonahoe/ijfw",
2054
- "https://github.com/seandonahoe/ijfw/",
2055
- "https://github.com/seandonahoe/ijfw.git/"
2067
+ const STALE_PATTERNS = [
2068
+ /^https:\/\/github\.com\/seandonahoe\/ijfw(\.git)?\/?$/i,
2069
+ /^https:\/\/github\.com\/therealseandonahoe\/ijfw(\.git)?\/?$/i
2056
2070
  ];
2057
2071
  const currentOrigin = (stdout || "").trim();
2058
- if (STALE_ORIGINS.includes(currentOrigin)) {
2072
+ if (STALE_PATTERNS.some((re) => re.test(currentOrigin))) {
2059
2073
  const setUrl = spawnSync2("git", ["-C", dir, "remote", "set-url", "origin", DEFAULT_REPO], { stdio: "inherit" });
2060
2074
  if (setUrl.status !== 0) {
2061
2075
  console.warn(` [!] origin migration failed -- could not repoint ${currentOrigin} to ${DEFAULT_REPO}`);
package/dist/uninstall.js CHANGED
@@ -241,9 +241,11 @@ import os; os.replace(p + ".tmp", p)
241
241
  return true;
242
242
  }
243
243
  const stripped = raw.replace(
244
+ // eslint-disable-next-line security/detect-unsafe-regex -- raw is a small local YAML config file; pattern is line-anchored to the IJFW-owned block.
244
245
  /^ ijfw-memory:\n(?: .*\n)*(?:# IJFW-MCP-END ijfw-memory\n)?/m,
245
246
  ""
246
247
  ).replace(
248
+ // eslint-disable-next-line security/detect-unsafe-regex -- raw is a small local YAML config file; pattern is bounded by exact IJFW sentinel markers.
247
249
  /# IJFW-MCP-BEGIN ijfw-memory\n(?:.*\n)*?# IJFW-MCP-END ijfw-memory\n/,
248
250
  ""
249
251
  );
@@ -263,6 +265,42 @@ function removeIjfwSkills(dir) {
263
265
  }
264
266
  return count;
265
267
  }
268
+ var CODEX_COMMAND_FILES = [
269
+ "compress.md",
270
+ "consolidate.md",
271
+ "cross-audit.md",
272
+ "cross-critique.md",
273
+ "cross-research.md",
274
+ "doctor.md",
275
+ "handoff.md",
276
+ "ijfw-audit.md",
277
+ "ijfw-execute.md",
278
+ "ijfw-help.md",
279
+ "ijfw-plan.md",
280
+ "ijfw-ship.md",
281
+ "ijfw-verify.md",
282
+ "ijfw.md",
283
+ "memory-audit.md",
284
+ "memory-consent.md",
285
+ "memory-why.md",
286
+ "metrics.md",
287
+ "mode.md",
288
+ "status.md",
289
+ "team.md",
290
+ "workflow.md"
291
+ ];
292
+ function removeCodexCommands(dir) {
293
+ if (!existsSync2(dir)) return 0;
294
+ let count = 0;
295
+ for (const name of CODEX_COMMAND_FILES) {
296
+ const path = join2(dir, name);
297
+ if (existsSync2(path)) {
298
+ rmSync(path, { force: true });
299
+ count++;
300
+ }
301
+ }
302
+ return count;
303
+ }
266
304
  function cleanPlatforms() {
267
305
  const removed = [];
268
306
  if (removeTomlSection(join2(HOME, ".codex", "config.toml"))) {
@@ -273,6 +311,8 @@ function cleanPlatforms() {
273
311
  }
274
312
  const codexSkills = removeIjfwSkills(join2(HOME, ".codex", "skills"));
275
313
  if (codexSkills > 0) removed.push(`~/.codex/skills/ijfw-* (removed ${codexSkills} skill dirs)`);
314
+ const codexCommands = removeCodexCommands(join2(HOME, ".codex", "commands"));
315
+ if (codexCommands > 0) removed.push(`~/.codex/commands (removed ${codexCommands} IJFW command aliases)`);
276
316
  const codexMd = join2(HOME, ".codex", "IJFW.md");
277
317
  if (existsSync2(codexMd)) {
278
318
  rmSync(codexMd, { force: true });
package/docs/GUIDE.md CHANGED
@@ -172,7 +172,7 @@ Run `/team setup` in Claude Code, or `ijfw team` from the shell, to see your cur
172
172
  |------|-------|--------------|
173
173
  | Hot | Plain markdown in `.ijfw/memory/` | Always on. Instant reads. Git friendly. |
174
174
  | Warm | BM25 ranked search | Always on. Scales to around 10,000 entries per project. |
175
- | Cold | Optional semantic vectors | Only if you install `@xenova/transformers`. Off by default. |
175
+ | Cold | Optional semantic vectors | Off by default. Requires a user-installed embedding provider. |
176
176
 
177
177
  Every session also ends with an optional "dream cycle". Run `/consolidate` or "run a dream cycle" to have IJFW sweep the day's memory: promote observed patterns into your knowledge base, prune stale entries, reconcile contradictions, optionally lift winners into global memory so every future project benefits. Memory that grows sharper over time instead of heavier.
178
178
 
@@ -359,9 +359,9 @@ IJFW configures six AI coding agents with native affordances on each, plus a uni
359
359
 
360
360
  | Platform | What ships |
361
361
  |----------|------------|
362
- | Claude Code | Native plugin via marketplace, MCP auto-registered, 9 hooks, 20 on-demand skills, 21 slash commands |
363
- | Codex CLI | Native plugin (`.codex-plugin/plugin.json`), 20 skills, 9 hooks, MCP registered, marketplace-ready |
364
- | Gemini CLI | Native extension (`gemini-extension.json`), 20 skills, 11 hook events, 21 TOML slash commands, policy engine, BeforeModel injection, checkpointing |
362
+ | Claude Code | Native plugin via marketplace, MCP auto-registered, 6 hook events / 12 scripts, 22 on-demand skills, 22 slash commands |
363
+ | Codex CLI | Native plugin (`.codex-plugin/plugin.json`), 19 skills, 5 hook events, MCP registered, marketplace-ready |
364
+ | Gemini CLI | Native extension (`gemini-extension.json`), 19 skills, 11 hook events, 19 TOML slash commands, policy engine, BeforeModel injection, checkpointing |
365
365
  | Cursor | `.cursor/mcp.json` plus `.cursor/rules/ijfw.mdc`. Dashboard view-only. |
366
366
  | Windsurf | `~/.codeium/windsurf/mcp_config.json` plus `.windsurfrules`. Dashboard view-only. |
367
367
  | Copilot (VS Code) | `.vscode/mcp.json` plus `.github/copilot-instructions.md`. Dashboard view-only. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ijfw/install",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "description": "One-command installer for IJFW -- the AI efficiency layer. One install, every AI coding agent, zero config.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -55,7 +55,6 @@
55
55
  "url": "git+https://gitlab.com/therealseandonahoe/ijfw.git"
56
56
  },
57
57
  "publishConfig": {
58
- "access": "public",
59
- "provenance": true
58
+ "access": "public"
60
59
  }
61
60
  }