@ijfw/install 1.3.1 → 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,23 @@
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
+
3
21
  ## [1.3.1] -- 2026-05-12
4
22
 
5
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.
@@ -659,7 +677,7 @@ Platform count: **8 install targets -> 13 MCP-integrated + 1 rules-only**. Same
659
677
 
660
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.
661
679
  - Blocking vs advisory distinction: exit 0 when all blocking gates pass even if advisory warnings exist. Exit 1 on any blocking failure.
662
- - 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.
663
681
  - Warm-cache SLO: <=90s. Cold-cache: <=240s. Both printed in the summary line.
664
682
  - `prepublishOnly` in `installer/package.json` now runs preflight before every publish so no tag can ship with a blocking gate open.
665
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,25 +696,47 @@ __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
722
  const packageDirs = ["installer", "mcp-server"];
586
723
  const runs = packageDirs.map((dir) => {
587
724
  const res = spawnSync7(
588
- "npx",
589
- ["--yes", `audit-ci@${ver}`, "--config", configPath],
725
+ "npm",
726
+ ["audit", "--audit-level=high", "--json"],
590
727
  {
591
728
  encoding: "utf8",
592
729
  cwd: join4(ctx.repoRoot, dir),
593
730
  timeout: 6e4
594
731
  }
595
732
  );
596
- return { dir, status: res.status, output: (res.stdout || "") + (res.stderr || "") };
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 };
597
737
  });
598
738
  const durationMs = Date.now() - t0;
599
- const failed = runs.filter((r) => r.status !== 0);
739
+ const failed = runs.filter((r) => !r.report || r.highCritical > 0);
600
740
  if (failed.length === 0) {
601
741
  return {
602
742
  name: "audit-ci",
@@ -608,8 +748,13 @@ async function run7(ctx) {
608
748
  }
609
749
  const lines = [];
610
750
  for (const r of failed) {
611
- lines.push(`${r.dir}: audit failed`);
612
- lines.push(...r.output.split("\n").filter(Boolean).slice(0, 10));
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));
613
758
  }
614
759
  return {
615
760
  name: "audit-ci",
@@ -887,7 +1032,7 @@ __export(upgrade_smoke_exports, {
887
1032
  severity: () => severity11
888
1033
  });
889
1034
  import { spawnSync as spawnSync11 } from "node:child_process";
890
- 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";
891
1036
  import { join as join7, resolve as resolve2 } from "node:path";
892
1037
  import { tmpdir as tmpdir3 } from "node:os";
893
1038
  async function run11(ctx) {
@@ -974,7 +1119,7 @@ async function run11(ctx) {
974
1119
  if (existsSync2(settingsPath)) {
975
1120
  let settings;
976
1121
  try {
977
- settings = JSON.parse(readFileSync(settingsPath, "utf8"));
1122
+ settings = JSON.parse(readFileSync2(settingsPath, "utf8"));
978
1123
  } catch (e) {
979
1124
  return {
980
1125
  name: "upgrade-smoke",
@@ -997,7 +1142,7 @@ async function run11(ctx) {
997
1142
  }
998
1143
  const marketplaceSrc = join7(installerDir, "src", "marketplace.js");
999
1144
  if (existsSync2(marketplaceSrc)) {
1000
- const src = readFileSync(marketplaceSrc, "utf8");
1145
+ const src = readFileSync2(marketplaceSrc, "utf8");
1001
1146
  const registersCorrectKey = src.includes("'ijfw@ijfw'") || src.includes('"ijfw@ijfw"');
1002
1147
  const registersWrongKey = /enabledPlugins\[['"]ijfw-core@ijfw['"]\]\s*=\s*true/.test(src);
1003
1148
  if (!registersCorrectKey) {
@@ -1052,7 +1197,7 @@ var preflight_exports = {};
1052
1197
  __export(preflight_exports, {
1053
1198
  runPreflightCommand: () => runPreflightCommand
1054
1199
  });
1055
- import { readFileSync as readFileSync2, existsSync as existsSync3 } from "node:fs";
1200
+ import { readFileSync as readFileSync3, existsSync as existsSync3 } from "node:fs";
1056
1201
  import { join as join8, dirname, resolve as resolve3 } from "node:path";
1057
1202
  import { fileURLToPath } from "node:url";
1058
1203
  function printHelp() {
@@ -1096,7 +1241,7 @@ function loadVersions(repoRoot2) {
1096
1241
  for (const f of candidates) {
1097
1242
  if (existsSync3(f)) {
1098
1243
  try {
1099
- return JSON.parse(readFileSync2(f, "utf8"));
1244
+ return JSON.parse(readFileSync3(f, "utf8"));
1100
1245
  } catch {
1101
1246
  }
1102
1247
  }
@@ -2435,8 +2580,8 @@ Please report this to https://github.com/markedjs/marked.`, e) {
2435
2580
  // src/ijfw.js
2436
2581
  import { dirname as dirname2, join as join9, resolve as resolve4, basename } from "node:path";
2437
2582
  import { fileURLToPath as fileURLToPath2 } from "node:url";
2438
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, copyFileSync, readdirSync as readdirSync4, rmSync as rmSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "node:fs";
2439
- 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";
2440
2585
  import { spawnSync as spawnSync12 } from "node:child_process";
2441
2586
  var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
2442
2587
  function repoRoot() {
@@ -2447,6 +2592,62 @@ function repoRoot() {
2447
2592
  }
2448
2593
  return process.cwd();
2449
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
+ ]);
2450
2651
  function printHelp2() {
2451
2652
  console.log(`
2452
2653
  ijfw -- the AI efficiency layer
@@ -2460,7 +2661,13 @@ COMMANDS
2460
2661
  help Open the full IJFW guide (terminal, or --browser for rendered)
2461
2662
  preflight Run 11-gate quality pipeline before publishing
2462
2663
  dashboard Start / stop / check the local observability dashboard
2463
- 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
2464
2671
  doctor Diagnose IJFW installation health
2465
2672
 
2466
2673
  --help, -h Show this help
@@ -2498,7 +2705,7 @@ async function main() {
2498
2705
  if (delegateToCli(argv.slice(2))) return;
2499
2706
  try {
2500
2707
  const pkgPath = join9(__dirname2, "..", "package.json");
2501
- const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
2708
+ const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
2502
2709
  console.log(`@ijfw/install@${pkg.version || "unknown"}`);
2503
2710
  if (verbose) {
2504
2711
  console.log(" (full --verbose details require a completed install: run ijfw install)");
@@ -2508,11 +2715,14 @@ async function main() {
2508
2715
  }
2509
2716
  process.exit(0);
2510
2717
  }
2511
- if (sub === "update" || sub === "statusline" || sub === "config" || sub === "insight") {
2718
+ if (ORCHESTRATOR_COMMANDS.has(sub)) {
2512
2719
  if (delegateToCli(argv.slice(2))) return;
2513
2720
  console.error(`'ijfw ${sub}' requires a completed IJFW install. Run: ijfw install`);
2514
2721
  process.exit(1);
2515
2722
  }
2723
+ if (sub === "doctor" && findCli()) {
2724
+ if (delegateToCli(argv.slice(2))) return;
2725
+ }
2516
2726
  switch (sub) {
2517
2727
  case "install": {
2518
2728
  const installBin = resolve4(__dirname2, "..", "dist", "install.js");
@@ -2533,19 +2743,13 @@ async function main() {
2533
2743
  }
2534
2744
  case "dashboard": {
2535
2745
  const dashSub = argv[3];
2536
- const root = repoRoot();
2537
- const ijfwHome = join9(homedir(), ".ijfw");
2538
- const findInTree = (...rel) => {
2539
- const candidates = [join9(root, ...rel), join9(ijfwHome, ...rel)];
2540
- return candidates.find((p) => existsSync4(p)) || null;
2541
- };
2542
2746
  if (dashSub === "start" || dashSub === "stop" || dashSub === "status") {
2543
- const dashBin = findInTree("mcp-server", "bin", "ijfw-dashboard");
2747
+ const dashBin = findInternalAsset("mcp-server", "bin", "ijfw-dashboard");
2544
2748
  if (dashBin) {
2545
2749
  const r = spawnSync12("node", [dashBin, dashSub, ...argv.slice(4)], { stdio: "inherit" });
2546
2750
  process.exit(r.status ?? 0);
2547
2751
  } else {
2548
- const serverJs = findInTree("mcp-server", "src", "dashboard-server.js");
2752
+ const serverJs = findInternalAsset("mcp-server", "src", "dashboard-server.js");
2549
2753
  if (dashSub === "start" && serverJs) {
2550
2754
  const { spawn } = await import("node:child_process");
2551
2755
  const child = spawn(process.execPath, [serverJs, "start", "--daemon"], {
@@ -2560,7 +2764,7 @@ async function main() {
2560
2764
  process.exit(1);
2561
2765
  }
2562
2766
  } else if (dashSub === "render" || !dashSub) {
2563
- const binJs = findInTree("scripts", "dashboard", "bin.js");
2767
+ const binJs = findInternalAsset("scripts", "dashboard", "bin.js");
2564
2768
  if (binJs) {
2565
2769
  const r = spawnSync12("node", [binJs, ...argv.slice(dashSub ? 4 : 3)], { stdio: "inherit" });
2566
2770
  process.exit(r.status ?? 0);
@@ -2576,30 +2780,72 @@ async function main() {
2576
2780
  }
2577
2781
  case "design": {
2578
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
+ }
2579
2789
  const contentDir = join9(homedir(), ".ijfw", "design-companion", "content");
2580
2790
  mkdirSync3(contentDir, { recursive: true });
2581
- if (designSub === "push") {
2582
- const filePath = argv[4];
2583
- if (!filePath) {
2584
- 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.");
2585
2807
  process.exit(1);
2586
2808
  }
2587
- const abs = resolve4(filePath);
2588
- if (!existsSync4(abs)) {
2589
- console.error(`File not found: ${abs}`);
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.");
2590
2816
  process.exit(1);
2591
2817
  }
2592
- const dest = join9(contentDir, basename(abs));
2593
- copyFileSync(abs, dest);
2594
- console.log(`Design pushed: ${dest}`);
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 ...]");
2824
+ process.exit(1);
2825
+ }
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`);
2595
2841
  } else if (designSub === "clear") {
2596
2842
  const files = readdirSync4(contentDir);
2597
2843
  for (const f of files) rmSync4(join9(contentDir, f), { force: true });
2598
2844
  console.log("Design companion content cleared.");
2599
2845
  } else {
2600
- 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.");
2601
2847
  console.log("");
2602
- 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");
2603
2849
  process.exit(1);
2604
2850
  }
2605
2851
  break;
@@ -2626,7 +2872,7 @@ async function main() {
2626
2872
  copyFileSync(join9(assetsSrc, f), join9(outDir, "assets", f));
2627
2873
  }
2628
2874
  }
2629
- const md = readFileSync3(guidePath, "utf8").replace(/\(guide\/assets\//g, "(assets/");
2875
+ const md = readFileSync4(guidePath, "utf8").replace(/\(guide\/assets\//g, "(assets/");
2630
2876
  const rendered = marked.parse(md, { gfm: true, breaks: false });
2631
2877
  const html = `<!doctype html>
2632
2878
  <html lang="en"><head>
@@ -2645,7 +2891,7 @@ async function main() {
2645
2891
  </head><body><div class="wrap markdown-body">${rendered}</div></body></html>`;
2646
2892
  const outHtml = join9(outDir, "index.html");
2647
2893
  writeFileSync4(outHtml, html);
2648
- const opener = platform2() === "darwin" ? "open" : platform2() === "win32" ? "start" : "xdg-open";
2894
+ const opener = platform() === "darwin" ? "open" : platform() === "win32" ? "start" : "xdg-open";
2649
2895
  spawnSync12(opener, [outHtml], { stdio: "ignore", detached: true });
2650
2896
  console.log(`[ijfw] Guide opened in your browser.`);
2651
2897
  console.log(` Local copy: ${outHtml}`);
@@ -2655,10 +2901,10 @@ async function main() {
2655
2901
  if (hasLess) {
2656
2902
  const lessRes = spawnSync12("less", ["-R", guidePath], { stdio: "inherit" });
2657
2903
  if (lessRes.status !== 0 && lessRes.status !== null) {
2658
- process.stdout.write(readFileSync3(guidePath, "utf8"));
2904
+ process.stdout.write(readFileSync4(guidePath, "utf8"));
2659
2905
  }
2660
2906
  } else {
2661
- process.stdout.write(readFileSync3(guidePath, "utf8"));
2907
+ process.stdout.write(readFileSync4(guidePath, "utf8"));
2662
2908
  }
2663
2909
  process.exit(0);
2664
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 + 19 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) {
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ijfw/install",
3
- "version": "1.3.1",
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": {