@ijfw/install 1.3.1 → 1.4.0

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/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
@@ -568,6 +686,996 @@ var init_gitleaks = __esm({
568
686
  }
569
687
  });
570
688
 
689
+ // ../mcp-server/src/gate-result-schema.js
690
+ function makeGateId(gate) {
691
+ if (typeof gate !== "string" || !GATE_NAME_PATTERN.test(gate)) {
692
+ throw new TypeError(
693
+ `makeGateId: invalid gate name "${gate}" \u2014 must match ${GATE_NAME_PATTERN}`
694
+ );
695
+ }
696
+ const safe = gate.replace(/:/g, "-");
697
+ const ts = Date.now();
698
+ const rand4 = Math.floor(Math.random() * 65536).toString(16).padStart(4, "0");
699
+ return `${safe}-${ts}-${rand4}`;
700
+ }
701
+ function isString(v2) {
702
+ return typeof v2 === "string";
703
+ }
704
+ function isNonNullObject(v2) {
705
+ return v2 !== null && typeof v2 === "object" && !Array.isArray(v2);
706
+ }
707
+ function validateGateResult(obj) {
708
+ const errors = [];
709
+ if (!isNonNullObject(obj)) {
710
+ return { valid: false, errors: ["root: must be an object"] };
711
+ }
712
+ if (obj.schema_version !== SCHEMA_VERSION) {
713
+ errors.push(
714
+ `schema_version: must equal "${SCHEMA_VERSION}", got ${JSON.stringify(obj.schema_version)}`
715
+ );
716
+ }
717
+ if (!isString(obj.gate)) {
718
+ errors.push("gate: must be a string");
719
+ } else if (!GATE_NAME_PATTERN.test(obj.gate)) {
720
+ errors.push(
721
+ `gate: "${obj.gate}" does not match ${GATE_NAME_PATTERN}`
722
+ );
723
+ }
724
+ if (!VALID_STATUSES.includes(obj.status)) {
725
+ errors.push(
726
+ `status: must be one of ${VALID_STATUSES.join("|")}, got ${JSON.stringify(obj.status)}`
727
+ );
728
+ }
729
+ if (!VALID_PROJECT_TYPES.includes(obj.project_type)) {
730
+ errors.push(
731
+ `project_type: must be one of ${VALID_PROJECT_TYPES.join("|")}, got ${JSON.stringify(obj.project_type)}`
732
+ );
733
+ }
734
+ if (!Array.isArray(obj.lenses)) {
735
+ errors.push("lenses: must be an array (empty for single-model gates)");
736
+ } else {
737
+ obj.lenses.forEach((lens, i) => {
738
+ if (!isNonNullObject(lens)) {
739
+ errors.push(`lenses[${i}]: must be an object`);
740
+ return;
741
+ }
742
+ if (!isString(lens.model)) errors.push(`lenses[${i}].model: must be a string`);
743
+ if (!VALID_STATUSES.includes(lens.verdict)) {
744
+ errors.push(
745
+ `lenses[${i}].verdict: must be one of ${VALID_STATUSES.join("|")}`
746
+ );
747
+ }
748
+ if (typeof lens.confidence !== "number" || lens.confidence < 0 || lens.confidence > 1) {
749
+ errors.push(`lenses[${i}].confidence: must be number in [0,1]`);
750
+ }
751
+ if (!isString(lens.summary)) errors.push(`lenses[${i}].summary: must be a string`);
752
+ });
753
+ }
754
+ if (!Array.isArray(obj.affected_artifacts)) {
755
+ errors.push("affected_artifacts: must be an array");
756
+ } else {
757
+ obj.affected_artifacts.forEach((a, i) => {
758
+ if (!isNonNullObject(a)) {
759
+ errors.push(`affected_artifacts[${i}]: must be an object`);
760
+ return;
761
+ }
762
+ if (!VALID_ARTIFACT_TYPES.includes(a.type)) {
763
+ errors.push(
764
+ `affected_artifacts[${i}].type: must be one of ${VALID_ARTIFACT_TYPES.join("|")}`
765
+ );
766
+ }
767
+ if (!isString(a.ref)) errors.push(`affected_artifacts[${i}].ref: must be a string`);
768
+ if (!isString(a.role)) errors.push(`affected_artifacts[${i}].role: must be a string`);
769
+ });
770
+ }
771
+ if (!isNonNullObject(obj.accounting)) {
772
+ errors.push("accounting: must be an object");
773
+ } else {
774
+ const a = obj.accounting;
775
+ if (typeof a.duration_ms !== "number" || a.duration_ms < 0) {
776
+ errors.push("accounting.duration_ms: must be a non-negative number");
777
+ }
778
+ if (typeof a.lenses_invoked !== "number" || a.lenses_invoked < 0 || !Number.isInteger(a.lenses_invoked)) {
779
+ errors.push("accounting.lenses_invoked: must be a non-negative integer");
780
+ }
781
+ if (a.cost_usd !== null && (typeof a.cost_usd !== "number" || a.cost_usd < 0)) {
782
+ errors.push("accounting.cost_usd: must be null or non-negative number");
783
+ }
784
+ }
785
+ if (!Array.isArray(obj.remediation)) {
786
+ errors.push("remediation: must be an array (may be empty)");
787
+ } else {
788
+ obj.remediation.forEach((r, i) => {
789
+ if (!isNonNullObject(r)) {
790
+ errors.push(`remediation[${i}]: must be an object`);
791
+ return;
792
+ }
793
+ if (!isString(r.action)) errors.push(`remediation[${i}].action: must be a string`);
794
+ if (!isString(r.target)) errors.push(`remediation[${i}].target: must be a string`);
795
+ if (!isString(r.agent_recommended)) {
796
+ errors.push(`remediation[${i}].agent_recommended: must be a string`);
797
+ }
798
+ if (typeof r.confidence !== "number" || r.confidence < 0 || r.confidence > 1) {
799
+ errors.push(`remediation[${i}].confidence: must be number in [0,1]`);
800
+ }
801
+ });
802
+ }
803
+ if (obj.receipts_ref !== null && !isString(obj.receipts_ref)) {
804
+ errors.push("receipts_ref: must be a string or null");
805
+ }
806
+ if (obj.supersedes !== null && !isString(obj.supersedes)) {
807
+ errors.push("supersedes: must be a string or null");
808
+ }
809
+ if (!isString(obj.gate_id) || obj.gate_id.length === 0) {
810
+ errors.push("gate_id: must be a non-empty string");
811
+ } else if (isString(obj.gate)) {
812
+ const safeGate = obj.gate.replace(/:/g, "-");
813
+ if (!obj.gate_id.startsWith(safeGate + "-")) {
814
+ errors.push(
815
+ `gate_id: expected to start with "${safeGate}-" (colon-collapsed gate name)`
816
+ );
817
+ }
818
+ }
819
+ if (!isString(obj.emitted_at) || !ISO8601_PATTERN.test(obj.emitted_at)) {
820
+ errors.push("emitted_at: must be ISO-8601 string");
821
+ }
822
+ return { valid: errors.length === 0, errors };
823
+ }
824
+ function formatGateResult(obj) {
825
+ const json = JSON.stringify(obj, null, 2);
826
+ return "```gate-result\n" + json + "\n```";
827
+ }
828
+ var SCHEMA_VERSION, GATE_NAME_PATTERN, VALID_STATUSES, VALID_PROJECT_TYPES, VALID_ARTIFACT_TYPES, ISO8601_PATTERN;
829
+ var init_gate_result_schema = __esm({
830
+ "../mcp-server/src/gate-result-schema.js"() {
831
+ SCHEMA_VERSION = "1.0";
832
+ GATE_NAME_PATTERN = /^[a-z][a-z0-9-]*(:[a-z][a-z0-9-]*)?$/;
833
+ VALID_STATUSES = Object.freeze([
834
+ "PASS",
835
+ "CONDITIONAL",
836
+ "WARN",
837
+ "FLAG",
838
+ "FAIL"
839
+ ]);
840
+ VALID_PROJECT_TYPES = Object.freeze([
841
+ "software",
842
+ "book",
843
+ "content",
844
+ "business",
845
+ "design",
846
+ "mixed",
847
+ "unknown"
848
+ ]);
849
+ VALID_ARTIFACT_TYPES = Object.freeze([
850
+ "file",
851
+ "chapter",
852
+ "section",
853
+ "asset",
854
+ "persona",
855
+ "decision",
856
+ "component"
857
+ ]);
858
+ ISO8601_PATTERN = // eslint-disable-next-line security/detect-unsafe-regex -- fixed-length anchored ISO 8601 shape; optional fractional + tz are non-overlapping
859
+ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})$/;
860
+ }
861
+ });
862
+
863
+ // ../mcp-server/src/scan-resume.js
864
+ import { existsSync, mkdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2, renameSync, unlinkSync, copyFileSync } from "fs";
865
+ import { join as join4 } from "path";
866
+ function statePath(projectRoot) {
867
+ return join4(String(projectRoot), ".ijfw", STATE_FILE);
868
+ }
869
+ function loadScanState(projectRoot) {
870
+ const path = statePath(projectRoot);
871
+ if (!existsSync(path)) return null;
872
+ try {
873
+ const raw = readFileSync2(path, "utf8");
874
+ const parsed = JSON.parse(raw);
875
+ if (parsed && typeof parsed === "object") return parsed;
876
+ } catch {
877
+ }
878
+ return null;
879
+ }
880
+ function writeScanState(projectRoot, state) {
881
+ const dir = join4(String(projectRoot), ".ijfw");
882
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
883
+ const finalPath = statePath(projectRoot);
884
+ const tmpPath = `${finalPath}.tmp.${process.pid}.${Date.now()}`;
885
+ const safe = {
886
+ scan_id: String(state.scan_id || ""),
887
+ started_at: String(state.started_at || (/* @__PURE__ */ new Date()).toISOString()),
888
+ last_path_walked: String(state.last_path_walked || ""),
889
+ files_scanned: Number.isFinite(state.files_scanned) ? state.files_scanned : 0,
890
+ total_estimate: Number.isFinite(state.total_estimate) ? state.total_estimate : 0,
891
+ attempts: Number.isFinite(state.attempts) ? state.attempts : 1,
892
+ incomplete: state.incomplete !== false,
893
+ session_id: state.session_id || null
894
+ };
895
+ if (state.partial && typeof state.partial === "object") {
896
+ safe.partial = state.partial;
897
+ }
898
+ writeFileSync2(tmpPath, JSON.stringify(safe, null, 2) + "\n", "utf8");
899
+ try {
900
+ renameSync(tmpPath, finalPath);
901
+ } catch (err) {
902
+ if (!err || err.code !== "EXDEV") throw err;
903
+ try {
904
+ copyFileSync(tmpPath, finalPath);
905
+ } finally {
906
+ try {
907
+ unlinkSync(tmpPath);
908
+ } catch {
909
+ }
910
+ }
911
+ }
912
+ return finalPath;
913
+ }
914
+ function lockPath(projectRoot) {
915
+ return join4(String(projectRoot), ".ijfw", LOCK_FILE);
916
+ }
917
+ function isPidAlive(pid) {
918
+ if (!Number.isFinite(pid) || pid <= 0) return false;
919
+ try {
920
+ process.kill(pid, 0);
921
+ return true;
922
+ } catch (err) {
923
+ if (err && err.code === "EPERM") return true;
924
+ return false;
925
+ }
926
+ }
927
+ function reclaimIfStale(lp) {
928
+ if (!existsSync(lp)) return;
929
+ let raw;
930
+ try {
931
+ raw = readFileSync2(lp, "utf8");
932
+ } catch {
933
+ return;
934
+ }
935
+ const lines = String(raw).split(/\r?\n/);
936
+ const pid = Number(lines[0]);
937
+ const ts = Number(lines[1]);
938
+ const ageOk = Number.isFinite(ts) && Date.now() - ts <= LOCK_STALE_MS;
939
+ if (isPidAlive(pid) && ageOk) return;
940
+ try {
941
+ unlinkSync(lp);
942
+ } catch {
943
+ }
944
+ }
945
+ function acquireScanLock(projectRoot) {
946
+ const dir = join4(String(projectRoot), ".ijfw");
947
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
948
+ const lp = lockPath(projectRoot);
949
+ reclaimIfStale(lp);
950
+ const payload = String(process.pid) + "\n" + String(Date.now()) + "\n";
951
+ try {
952
+ writeFileSync2(lp, payload, { encoding: "utf8", flag: "wx" });
953
+ } catch (err) {
954
+ if (err && err.code === "EEXIST") return null;
955
+ throw err;
956
+ }
957
+ let released = false;
958
+ return {
959
+ released: () => {
960
+ if (released) return;
961
+ released = true;
962
+ try {
963
+ unlinkSync(lp);
964
+ } catch {
965
+ }
966
+ }
967
+ };
968
+ }
969
+ function shouldResume(state) {
970
+ if (!state || typeof state !== "object") return false;
971
+ if (state.incomplete !== true) return false;
972
+ if (!state.started_at || typeof state.started_at !== "string") return false;
973
+ const startedMs = Date.parse(state.started_at);
974
+ if (!Number.isFinite(startedMs)) return false;
975
+ const ageMs = Date.now() - startedMs;
976
+ if (ageMs > STALENESS_MS) return false;
977
+ const attempts = Number.isFinite(state.attempts) ? state.attempts : 0;
978
+ if (attempts >= ATTEMPT_CAP) return false;
979
+ return true;
980
+ }
981
+ function clearScanState(projectRoot) {
982
+ const path = statePath(projectRoot);
983
+ if (existsSync(path)) {
984
+ try {
985
+ unlinkSync(path);
986
+ } catch {
987
+ }
988
+ }
989
+ }
990
+ var STATE_FILE, LOCK_FILE, STALENESS_MS, ATTEMPT_CAP, LOCK_STALE_MS;
991
+ var init_scan_resume = __esm({
992
+ "../mcp-server/src/scan-resume.js"() {
993
+ STATE_FILE = "scan-state.json";
994
+ LOCK_FILE = "scan-state.json.lock";
995
+ STALENESS_MS = 24 * 60 * 60 * 1e3;
996
+ ATTEMPT_CAP = 3;
997
+ LOCK_STALE_MS = 60 * 1e3;
998
+ }
999
+ });
1000
+
1001
+ // ../mcp-server/src/project-type-detector.js
1002
+ import {
1003
+ readFileSync as readFileSync3,
1004
+ writeFileSync as writeFileSync3,
1005
+ existsSync as existsSync2,
1006
+ readdirSync as readdirSync3,
1007
+ statSync as statSync3,
1008
+ renameSync as renameSync2,
1009
+ mkdirSync as mkdirSync2,
1010
+ unlinkSync as unlinkSync2,
1011
+ realpathSync,
1012
+ copyFileSync as copyFileSync2
1013
+ } from "fs";
1014
+ import { join as join5, extname, isAbsolute, resolve as pathResolve, dirname } from "path";
1015
+ import { fileURLToPath } from "url";
1016
+ import { createHash } from "crypto";
1017
+ function detect(projectRoot, options = {}) {
1018
+ const root = String(projectRoot || process.cwd());
1019
+ const c9Available = options.c9Available === false ? false : options.c9Available === true ? true : isC9AvailableSync();
1020
+ const maxFiles = Number.isFinite(options.maxFiles) && options.maxFiles > 0 ? options.maxFiles : MAX_FILES;
1021
+ const signals = [];
1022
+ const fallbackReason = c9Available ? null : "c9_unavailable";
1023
+ if (options.explicitType && DOMAINS.includes(String(options.explicitType))) {
1024
+ signals.push({ kind: "user_declaration", weight: 1, value: options.explicitType });
1025
+ return finalize({
1026
+ primary: options.explicitType,
1027
+ secondary: [],
1028
+ score: 1,
1029
+ signals,
1030
+ scanIncomplete: false,
1031
+ fallbackReason,
1032
+ treeHash: "",
1033
+ branchHash: branchHash(root)
1034
+ });
1035
+ }
1036
+ const fmAgents = readFrontmatterType(join5(root, "AGENTS.md"));
1037
+ if (fmAgents && DOMAINS.includes(fmAgents)) {
1038
+ signals.push({ kind: "agents_md_frontmatter", weight: 0.9, value: fmAgents });
1039
+ }
1040
+ const fmBrief = readFrontmatterType(join5(root, ".ijfw", "memory", "brief.md"));
1041
+ if (fmBrief && DOMAINS.includes(fmBrief)) {
1042
+ signals.push({ kind: "brief_md_frontmatter", weight: 0.8, value: fmBrief });
1043
+ }
1044
+ const timeBudgetMs = resolveTimeBudgetMs(options);
1045
+ const walk = walkProject(root, { maxFiles, maxDepth: MAX_DEPTH, options, timeBudgetMs });
1046
+ const treeHash = fileTreeHash(walk.fingerprint);
1047
+ if (walk.manifestsFound.length > 0) {
1048
+ signals.push({
1049
+ kind: "manifest",
1050
+ weight: 0.9,
1051
+ manifests: walk.manifestsFound.slice(0, 6)
1052
+ });
1053
+ }
1054
+ for (const d of walk.dirHits.book) signals.push({ kind: "dir_book", weight: 0.4, name: d });
1055
+ for (const d of walk.dirHits.content) signals.push({ kind: "dir_content", weight: 0.4, name: d });
1056
+ for (const d of walk.dirHits.business) signals.push({ kind: "dir_business", weight: 0.4, name: d });
1057
+ for (const d of walk.dirHits.design) signals.push({ kind: "dir_design", weight: 0.4, name: d });
1058
+ const totals = walk.extTotals;
1059
+ const totalClassified = Object.values(totals).reduce((a, b2) => a + b2, 0);
1060
+ if (totalClassified > 0) {
1061
+ for (const [domain, count] of Object.entries(totals)) {
1062
+ const ratio = count / totalClassified;
1063
+ if (ratio >= 0.05) {
1064
+ signals.push({
1065
+ kind: "file_extension_ratio",
1066
+ weight: 0.7,
1067
+ domain,
1068
+ ratio: Number(ratio.toFixed(3)),
1069
+ count
1070
+ });
1071
+ }
1072
+ }
1073
+ }
1074
+ for (const hit of walk.patternHits) {
1075
+ signals.push({ kind: "filename_pattern", weight: hit.weight, domain: hit.domain, name: hit.name });
1076
+ }
1077
+ const scoreboard = scoreSignals(signals);
1078
+ const ranked = rankDomains(scoreboard);
1079
+ let primary;
1080
+ let secondary = [];
1081
+ let confidence;
1082
+ if (ranked.length === 0) {
1083
+ primary = "unknown";
1084
+ confidence = 0;
1085
+ } else {
1086
+ primary = ranked[0].domain;
1087
+ confidence = ranked[0].score;
1088
+ secondary = ranked.slice(1).filter((r) => r.score >= 0.4 && r.domain !== primary).map((r) => r.domain);
1089
+ if (ranked.length >= 2 && ranked[0].score >= 0.55 && ranked[1].score >= 0.5 && ranked[1].score / ranked[0].score >= 0.75) {
1090
+ const topTwo = [ranked[0].domain, ranked[1].domain];
1091
+ secondary = topTwo;
1092
+ primary = "mixed";
1093
+ confidence = Math.min(0.85, (ranked[0].score + ranked[1].score) / 2);
1094
+ }
1095
+ }
1096
+ const highTrust = signals.some(
1097
+ (s) => s.kind === "user_declaration" || s.kind === "agents_md_frontmatter" || s.kind === "brief_md_frontmatter"
1098
+ );
1099
+ if (!c9Available && !highTrust && confidence > 0.7) confidence = 0.7;
1100
+ const scanIncomplete = walk.incomplete;
1101
+ if (scanIncomplete) {
1102
+ const lock = acquireScanLock(root);
1103
+ if (lock) {
1104
+ try {
1105
+ const prior = loadScanState(root) || {};
1106
+ writeScanState(root, {
1107
+ scan_id: prior.scan_id || newScanId(),
1108
+ started_at: prior.started_at || (/* @__PURE__ */ new Date()).toISOString(),
1109
+ last_path_walked: walk.lastPathWalked,
1110
+ files_scanned: walk.filesScanned,
1111
+ total_estimate: walk.totalEstimate,
1112
+ attempts: (prior.attempts || 0) + 1,
1113
+ incomplete: true,
1114
+ session_id: options.sessionId || null,
1115
+ partial: snapshotPartial(walk)
1116
+ });
1117
+ } catch {
1118
+ } finally {
1119
+ lock.released();
1120
+ }
1121
+ }
1122
+ } else {
1123
+ try {
1124
+ clearScanState(root);
1125
+ } catch {
1126
+ }
1127
+ }
1128
+ return finalize({
1129
+ primary,
1130
+ secondary,
1131
+ score: confidence,
1132
+ signals,
1133
+ scanIncomplete,
1134
+ fallbackReason,
1135
+ treeHash,
1136
+ branchHash: branchHash(root)
1137
+ });
1138
+ }
1139
+ function finalize({ primary, secondary, score, signals, scanIncomplete, fallbackReason, treeHash, branchHash: bh }) {
1140
+ const confidence = Number(Math.max(0, Math.min(1, score)).toFixed(3));
1141
+ const out = {
1142
+ type: primary,
1143
+ // single-label alias for hoist
1144
+ primary_type: primary,
1145
+ secondary_types: Array.isArray(secondary) ? secondary : [],
1146
+ confidence,
1147
+ scan_incomplete: !!scanIncomplete,
1148
+ detected_at: (/* @__PURE__ */ new Date()).toISOString(),
1149
+ signals,
1150
+ fallback_reason: fallbackReason,
1151
+ file_tree_hash: treeHash || "",
1152
+ branch_hash: bh || ""
1153
+ };
1154
+ return out;
1155
+ }
1156
+ function readFrontmatterType(path) {
1157
+ if (!existsSync2(path)) return null;
1158
+ let src;
1159
+ try {
1160
+ src = readFileSync3(path, "utf8");
1161
+ } catch {
1162
+ return null;
1163
+ }
1164
+ if (!src.startsWith("---\n")) return null;
1165
+ const after = src.slice(4);
1166
+ const closeIdx = after.search(/\n---\s*(?:\r?\n|$)/);
1167
+ if (closeIdx < 0) return null;
1168
+ const fm = after.slice(0, closeIdx);
1169
+ for (const ln of fm.split(/\r?\n/)) {
1170
+ const m2 = ln.match(/^type\s*:\s*(\S+)\s*$/);
1171
+ if (m2) {
1172
+ const v2 = m2[1].replace(/^["']|["']$/g, "");
1173
+ return v2;
1174
+ }
1175
+ }
1176
+ return null;
1177
+ }
1178
+ function walkProject(root, { maxFiles, maxDepth, options, timeBudgetMs }) {
1179
+ const out = {
1180
+ filesScanned: 0,
1181
+ totalEstimate: 0,
1182
+ incomplete: false,
1183
+ lastPathWalked: "",
1184
+ fingerprint: [],
1185
+ manifestsFound: [],
1186
+ dirHits: { book: [], content: [], business: [], design: [] },
1187
+ extTotals: {},
1188
+ patternHits: []
1189
+ };
1190
+ let resumeFrom = null;
1191
+ let priorState = null;
1192
+ if (options.resume !== false) {
1193
+ const state = loadScanState(root);
1194
+ if (state && shouldResume(state)) {
1195
+ resumeFrom = state.last_path_walked || null;
1196
+ priorState = state;
1197
+ }
1198
+ }
1199
+ if (priorState && priorState.partial && typeof priorState.partial === "object") {
1200
+ const p = priorState.partial;
1201
+ out.filesScanned = Number.isFinite(p.files_scanned) ? p.files_scanned : 0;
1202
+ out.totalEstimate = Number.isFinite(p.total_estimate) ? p.total_estimate : out.filesScanned;
1203
+ if (Array.isArray(p.fingerprint)) out.fingerprint = p.fingerprint.slice(0, 4096);
1204
+ if (Array.isArray(p.manifestsFound)) out.manifestsFound = p.manifestsFound.slice();
1205
+ if (p.dirHits && typeof p.dirHits === "object") {
1206
+ for (const k2 of ["book", "content", "business", "design"]) {
1207
+ if (Array.isArray(p.dirHits[k2])) out.dirHits[k2] = p.dirHits[k2].slice();
1208
+ }
1209
+ }
1210
+ if (p.extTotals && typeof p.extTotals === "object") out.extTotals = { ...p.extTotals };
1211
+ if (Array.isArray(p.patternHits)) out.patternHits = p.patternHits.slice();
1212
+ }
1213
+ let resumed = !resumeFrom;
1214
+ const visitedDirs = /* @__PURE__ */ new Set();
1215
+ try {
1216
+ visitedDirs.add(realpathSync.native(root));
1217
+ } catch {
1218
+ }
1219
+ const startedAt = Date.now();
1220
+ const budget = Number.isFinite(timeBudgetMs) && timeBudgetMs > 0 ? timeBudgetMs : DEFAULT_TIME_BUDGET_MS;
1221
+ let entriesSinceTimeCheck = 0;
1222
+ const stack = [{ path: root, depth: 0 }];
1223
+ while (stack.length > 0) {
1224
+ const { path, depth } = stack.pop();
1225
+ if (depth > maxDepth) continue;
1226
+ let entries;
1227
+ try {
1228
+ entries = readdirSync3(path, { withFileTypes: true });
1229
+ } catch {
1230
+ continue;
1231
+ }
1232
+ entries.sort((a, b2) => a.name < b2.name ? -1 : a.name > b2.name ? 1 : 0);
1233
+ for (const entry of entries) {
1234
+ const childPath = join5(path, entry.name);
1235
+ if (!resumed) {
1236
+ if (childPath === resumeFrom) resumed = true;
1237
+ continue;
1238
+ }
1239
+ out.lastPathWalked = childPath;
1240
+ entriesSinceTimeCheck += 1;
1241
+ if (entriesSinceTimeCheck >= TIME_BUDGET_CHECK_EVERY) {
1242
+ entriesSinceTimeCheck = 0;
1243
+ if (Date.now() - startedAt > budget) {
1244
+ out.incomplete = true;
1245
+ return out;
1246
+ }
1247
+ }
1248
+ if (entry.isDirectory()) {
1249
+ if (SKIP_DIRS.has(entry.name)) continue;
1250
+ try {
1251
+ const real = realpathSync.native(childPath);
1252
+ if (visitedDirs.has(real)) continue;
1253
+ visitedDirs.add(real);
1254
+ } catch {
1255
+ }
1256
+ recordDirHit(out, entry.name, depth);
1257
+ stack.push({ path: childPath, depth: depth + 1 });
1258
+ continue;
1259
+ }
1260
+ if (!entry.isFile()) continue;
1261
+ out.filesScanned += 1;
1262
+ out.totalEstimate = Math.max(out.totalEstimate, out.filesScanned);
1263
+ if (out.fingerprint.length < 4096) {
1264
+ out.fingerprint.push(childPath.slice(root.length + 1));
1265
+ }
1266
+ if (depth <= 2 && SOFTWARE_MANIFESTS.includes(entry.name)) {
1267
+ out.manifestsFound.push(entry.name);
1268
+ }
1269
+ for (const p of FILENAME_PATTERNS) {
1270
+ if (p.re.test(entry.name)) {
1271
+ out.patternHits.push({ name: entry.name, domain: p.domain, weight: p.weight });
1272
+ break;
1273
+ }
1274
+ }
1275
+ const ext = extname(entry.name).toLowerCase();
1276
+ const dom = EXT_DOMAIN[ext];
1277
+ if (dom) {
1278
+ out.extTotals[dom] = (out.extTotals[dom] || 0) + 1;
1279
+ }
1280
+ if (out.filesScanned % CHECKPOINT_EVERY === 0) {
1281
+ try {
1282
+ writeScanState(root, {
1283
+ scan_id: newScanId(),
1284
+ started_at: (/* @__PURE__ */ new Date()).toISOString(),
1285
+ last_path_walked: childPath,
1286
+ files_scanned: out.filesScanned,
1287
+ total_estimate: out.totalEstimate,
1288
+ attempts: 1,
1289
+ incomplete: true,
1290
+ partial: snapshotPartial(out)
1291
+ });
1292
+ } catch {
1293
+ }
1294
+ }
1295
+ if (out.filesScanned >= maxFiles) {
1296
+ out.incomplete = true;
1297
+ return out;
1298
+ }
1299
+ }
1300
+ }
1301
+ return out;
1302
+ }
1303
+ function snapshotPartial(out) {
1304
+ return {
1305
+ files_scanned: out.filesScanned,
1306
+ total_estimate: out.totalEstimate,
1307
+ fingerprint: out.fingerprint.slice(0, 4096),
1308
+ manifestsFound: out.manifestsFound.slice(0, 32),
1309
+ dirHits: {
1310
+ book: out.dirHits.book.slice(0, 32),
1311
+ content: out.dirHits.content.slice(0, 32),
1312
+ business: out.dirHits.business.slice(0, 32),
1313
+ design: out.dirHits.design.slice(0, 32)
1314
+ },
1315
+ extTotals: { ...out.extTotals },
1316
+ patternHits: out.patternHits.slice(0, 64)
1317
+ };
1318
+ }
1319
+ function resolveTimeBudgetMs(options) {
1320
+ if (Number.isFinite(options.timeBudgetMs) && options.timeBudgetMs > 0) {
1321
+ return options.timeBudgetMs;
1322
+ }
1323
+ const env = process.env.IJFW_DETECT_TIME_BUDGET_MS;
1324
+ if (env) {
1325
+ const n = Number(env);
1326
+ if (Number.isFinite(n) && n > 0) return n;
1327
+ }
1328
+ return DEFAULT_TIME_BUDGET_MS;
1329
+ }
1330
+ function recordDirHit(out, name12, depth) {
1331
+ if (depth > 2) return;
1332
+ const lower = name12.toLowerCase();
1333
+ if (BOOK_DIRS.includes(lower)) out.dirHits.book.push(lower);
1334
+ if (CONTENT_DIRS.includes(lower)) out.dirHits.content.push(lower);
1335
+ if (BUSINESS_DIRS.includes(lower)) out.dirHits.business.push(lower);
1336
+ if (DESIGN_DIRS.includes(lower)) out.dirHits.design.push(lower);
1337
+ }
1338
+ function scoreSignals(signals) {
1339
+ const board = {
1340
+ software: 0,
1341
+ book: 0,
1342
+ content: 0,
1343
+ business: 0,
1344
+ design: 0,
1345
+ mixed: 0,
1346
+ unknown: 0
1347
+ };
1348
+ const patternBudget = { software: 0.8, book: 0.8, content: 0.8, business: 0.8, design: 0.8, mixed: 0.8, unknown: 0.8 };
1349
+ const dirBudget = { software: 0.6, book: 0.6, content: 0.6, business: 0.6, design: 0.6, mixed: 0.6, unknown: 0.6 };
1350
+ for (const s of signals) {
1351
+ if (s.kind === "user_declaration" && s.value) board[s.value] += 1;
1352
+ else if (s.kind === "agents_md_frontmatter" && s.value) board[s.value] += 0.9;
1353
+ else if (s.kind === "brief_md_frontmatter" && s.value) board[s.value] += 0.8;
1354
+ else if (s.kind === "manifest") board.software += 0.9;
1355
+ else if (s.kind === "dir_book") {
1356
+ const add = Math.min(0.4, dirBudget.book);
1357
+ board.book += add;
1358
+ dirBudget.book -= add;
1359
+ } else if (s.kind === "dir_content") {
1360
+ const add = Math.min(0.4, dirBudget.content);
1361
+ board.content += add;
1362
+ dirBudget.content -= add;
1363
+ } else if (s.kind === "dir_business") {
1364
+ const add = Math.min(0.4, dirBudget.business);
1365
+ board.business += add;
1366
+ dirBudget.business -= add;
1367
+ } else if (s.kind === "dir_design") {
1368
+ const add = Math.min(0.4, dirBudget.design);
1369
+ board.design += add;
1370
+ dirBudget.design -= add;
1371
+ } else if (s.kind === "file_extension_ratio") {
1372
+ const m2 = s.ratio || 0;
1373
+ board[s.domain] = (board[s.domain] || 0) + 0.7 * m2;
1374
+ } else if (s.kind === "filename_pattern") {
1375
+ const add = Math.min(s.weight, patternBudget[s.domain] || 0);
1376
+ if (add > 0) {
1377
+ board[s.domain] = (board[s.domain] || 0) + add;
1378
+ patternBudget[s.domain] -= add;
1379
+ }
1380
+ }
1381
+ }
1382
+ return board;
1383
+ }
1384
+ function rankDomains(board) {
1385
+ const arr = Object.entries(board).filter(([d]) => d !== "mixed" && d !== "unknown").map(([domain, raw]) => ({ domain, raw }));
1386
+ if (arr.length === 0) return [];
1387
+ const maxRaw = arr.reduce((m2, e) => Math.max(m2, e.raw), 0);
1388
+ if (maxRaw <= 0) return [];
1389
+ for (const e of arr) {
1390
+ e.score = anchor(e.raw, maxRaw);
1391
+ }
1392
+ arr.sort((a, b2) => b2.score - a.score);
1393
+ return arr;
1394
+ }
1395
+ function anchor(raw, maxRaw) {
1396
+ if (raw <= 0) return 0;
1397
+ const top = Math.min(0.95, 0.4 + 0.55 * Math.tanh(raw));
1398
+ return Number((top * (raw / maxRaw)).toFixed(3));
1399
+ }
1400
+ function fileTreeHash(paths) {
1401
+ if (!paths || paths.length === 0) return "";
1402
+ const h = createHash("sha256");
1403
+ for (const p of paths) h.update(p + "\n");
1404
+ return h.digest("hex").slice(0, 16);
1405
+ }
1406
+ function branchHash(root) {
1407
+ try {
1408
+ const dotGit = join5(root, ".git");
1409
+ if (!existsSync2(dotGit)) return "";
1410
+ let headPath = null;
1411
+ let st;
1412
+ try {
1413
+ st = statSync3(dotGit);
1414
+ } catch {
1415
+ return "";
1416
+ }
1417
+ if (st.isDirectory()) {
1418
+ headPath = join5(dotGit, "HEAD");
1419
+ } else if (st.isFile()) {
1420
+ const ptr = readFileSync3(dotGit, "utf8");
1421
+ const m3 = ptr.match(/^gitdir:\s*(.+?)\s*$/m);
1422
+ if (!m3) return "";
1423
+ const target = m3[1];
1424
+ const gitDir = isAbsolute(target) ? target : pathResolve(root, target);
1425
+ headPath = join5(gitDir, "HEAD");
1426
+ } else {
1427
+ return "";
1428
+ }
1429
+ if (!headPath || !existsSync2(headPath)) return "";
1430
+ const head = readFileSync3(headPath, "utf8").trim();
1431
+ const m2 = head.match(/^ref:\s*(.+)$/);
1432
+ const branch = m2 ? m2[1] : head;
1433
+ return createHash("sha256").update(branch).digest("hex").slice(0, 16);
1434
+ } catch {
1435
+ }
1436
+ return "";
1437
+ }
1438
+ function isC9AvailableSync() {
1439
+ if (_c9AvailableCache !== null) return _c9AvailableCache;
1440
+ try {
1441
+ const here = fileURLToPath(import.meta.url);
1442
+ const fts5Path = join5(dirname(here), "compute", "fts5.js");
1443
+ _c9AvailableCache = existsSync2(fts5Path);
1444
+ } catch {
1445
+ _c9AvailableCache = false;
1446
+ }
1447
+ return _c9AvailableCache;
1448
+ }
1449
+ function newScanId() {
1450
+ return createHash("sha256").update(String(process.pid) + ":" + String(Date.now()) + ":" + Math.random()).digest("hex").slice(0, 12);
1451
+ }
1452
+ var DOMAINS, MAX_FILES, MAX_DEPTH, CHECKPOINT_EVERY, DEFAULT_TIME_BUDGET_MS, TIME_BUDGET_CHECK_EVERY, SKIP_DIRS, EXT_DOMAIN, SOFTWARE_MANIFESTS, BOOK_DIRS, CONTENT_DIRS, BUSINESS_DIRS, DESIGN_DIRS, FILENAME_PATTERNS, _c9AvailableCache;
1453
+ var init_project_type_detector = __esm({
1454
+ "../mcp-server/src/project-type-detector.js"() {
1455
+ init_scan_resume();
1456
+ DOMAINS = ["software", "book", "content", "business", "design", "mixed", "unknown"];
1457
+ MAX_FILES = 2e5;
1458
+ MAX_DEPTH = 12;
1459
+ CHECKPOINT_EVERY = 500;
1460
+ DEFAULT_TIME_BUDGET_MS = 5e3;
1461
+ TIME_BUDGET_CHECK_EVERY = 1e3;
1462
+ SKIP_DIRS = /* @__PURE__ */ new Set([
1463
+ ".git",
1464
+ ".hg",
1465
+ ".svn",
1466
+ ".ijfw",
1467
+ ".planning",
1468
+ ".cache",
1469
+ "node_modules",
1470
+ "dist",
1471
+ "build",
1472
+ "out",
1473
+ "target",
1474
+ ".next",
1475
+ "__pycache__",
1476
+ ".venv",
1477
+ "venv",
1478
+ "env",
1479
+ ".pytest_cache",
1480
+ ".mypy_cache",
1481
+ ".tox",
1482
+ ".gradle",
1483
+ ".idea",
1484
+ ".vscode",
1485
+ "vendor",
1486
+ "bower_components"
1487
+ ]);
1488
+ EXT_DOMAIN = {
1489
+ // software (heavy)
1490
+ ".js": "software",
1491
+ ".jsx": "software",
1492
+ ".ts": "software",
1493
+ ".tsx": "software",
1494
+ ".mjs": "software",
1495
+ ".cjs": "software",
1496
+ ".py": "software",
1497
+ ".rs": "software",
1498
+ ".go": "software",
1499
+ ".java": "software",
1500
+ ".kt": "software",
1501
+ ".scala": "software",
1502
+ ".rb": "software",
1503
+ ".php": "software",
1504
+ ".c": "software",
1505
+ ".cc": "software",
1506
+ ".cpp": "software",
1507
+ ".h": "software",
1508
+ ".hpp": "software",
1509
+ ".hh": "software",
1510
+ ".swift": "software",
1511
+ ".m": "software",
1512
+ ".mm": "software",
1513
+ ".cs": "software",
1514
+ ".fs": "software",
1515
+ ".lua": "software",
1516
+ ".dart": "software",
1517
+ ".zig": "software",
1518
+ // book / long-form prose
1519
+ ".tex": "book",
1520
+ ".bib": "book",
1521
+ ".latex": "book",
1522
+ ".epub": "book",
1523
+ ".mobi": "book",
1524
+ // content / blog / docs / marketing
1525
+ ".mdx": "content",
1526
+ ".markdown": "content",
1527
+ ".rst": "content",
1528
+ // design / assets
1529
+ ".fig": "design",
1530
+ ".sketch": "design",
1531
+ ".xd": "design",
1532
+ ".ai": "design",
1533
+ ".psd": "design",
1534
+ ".indd": "design",
1535
+ ".svg": "design",
1536
+ ".afdesign": "design",
1537
+ ".afphoto": "design",
1538
+ // business / ops
1539
+ ".xlsx": "business",
1540
+ ".xls": "business",
1541
+ ".csv": "business",
1542
+ ".numbers": "business",
1543
+ ".ods": "business",
1544
+ ".pptx": "business",
1545
+ ".ppt": "business",
1546
+ ".key": "business",
1547
+ ".docx": "business",
1548
+ ".doc": "business"
1549
+ };
1550
+ SOFTWARE_MANIFESTS = [
1551
+ "package.json",
1552
+ "Cargo.toml",
1553
+ "pyproject.toml",
1554
+ "setup.py",
1555
+ "Gemfile",
1556
+ "go.mod",
1557
+ "pom.xml",
1558
+ "build.gradle",
1559
+ "build.gradle.kts",
1560
+ "composer.json",
1561
+ "Package.swift",
1562
+ "mix.exs",
1563
+ "rebar.config",
1564
+ "pubspec.yaml",
1565
+ "CMakeLists.txt",
1566
+ "Makefile"
1567
+ ];
1568
+ BOOK_DIRS = ["manuscripts", "manuscript", "drafts", "draft", "chapters", "book"];
1569
+ CONTENT_DIRS = ["content", "posts", "articles", "blog", "newsletter", "social"];
1570
+ BUSINESS_DIRS = ["strategy", "financials", "finance", "ops", "runbooks", "sop", "sops", "ops-runbooks"];
1571
+ DESIGN_DIRS = ["designs", "design", "assets", "mockups", "wireframes", "figma"];
1572
+ FILENAME_PATTERNS = [
1573
+ { re: /^chapter[-_]?\d+/i, domain: "book", weight: 0.4 },
1574
+ { re: /^ch\d+/i, domain: "book", weight: 0.3 },
1575
+ { re: /^brand[-_]voice/i, domain: "content", weight: 0.4 },
1576
+ { re: /^seo[-_]/i, domain: "content", weight: 0.2 },
1577
+ { re: /^post[-_]/i, domain: "content", weight: 0.2 },
1578
+ { re: /^figma[-_]export/i, domain: "design", weight: 0.4 },
1579
+ { re: /^wireframe/i, domain: "design", weight: 0.3 }
1580
+ ];
1581
+ _c9AvailableCache = null;
1582
+ }
1583
+ });
1584
+
1585
+ // ../mcp-server/src/gate-result.js
1586
+ import { mkdir, writeFile } from "node:fs/promises";
1587
+ import { basename, dirname as dirname2, join as join6 } from "node:path";
1588
+ async function emitGateResult(gateOpts, context = {}) {
1589
+ if (gateOpts === null || typeof gateOpts !== "object") {
1590
+ throw new TypeError("emitGateResult: gateOpts must be an object");
1591
+ }
1592
+ const projectType = typeof context.project_type === "string" && context.project_type.length > 0 ? context.project_type : await resolveProjectType(context.projectRoot);
1593
+ const result = {
1594
+ schema_version: SCHEMA_VERSION,
1595
+ gate_id: makeGateId(gateOpts.gate),
1596
+ gate: gateOpts.gate,
1597
+ status: gateOpts.status,
1598
+ project_type: projectType,
1599
+ lenses: Array.isArray(gateOpts.lenses) ? gateOpts.lenses : [],
1600
+ affected_artifacts: Array.isArray(gateOpts.affected_artifacts) ? gateOpts.affected_artifacts : [],
1601
+ accounting: gateOpts.accounting,
1602
+ remediation: Array.isArray(gateOpts.remediation) ? gateOpts.remediation : [],
1603
+ receipts_ref: gateOpts.receipts_ref === void 0 ? null : gateOpts.receipts_ref,
1604
+ supersedes: gateOpts.supersedes === void 0 ? null : gateOpts.supersedes,
1605
+ emitted_at: (/* @__PURE__ */ new Date()).toISOString()
1606
+ };
1607
+ const { valid, errors } = validateGateResult(result);
1608
+ if (!valid) {
1609
+ throw new Error(
1610
+ `emitGateResult: invalid gate-result \u2014 ${errors.join("; ")}`
1611
+ );
1612
+ }
1613
+ makeReceipt(result, {
1614
+ projectRoot: typeof context.projectRoot === "string" && context.projectRoot.length > 0 ? context.projectRoot : process.cwd()
1615
+ }).catch(() => {
1616
+ });
1617
+ return formatGateResult(result);
1618
+ }
1619
+ async function makeReceipt(gateResult, opts = {}) {
1620
+ try {
1621
+ if (!gateResult || typeof gateResult !== "object") return;
1622
+ const gateId = typeof gateResult.gate_id === "string" ? gateResult.gate_id : null;
1623
+ if (!gateId) return;
1624
+ if (!RECEIPT_GATE_ID_PATTERN.test(gateId)) {
1625
+ try {
1626
+ process.stderr.write(
1627
+ `ijfw: makeReceipt rejected unsafe gate_id "${gateId}"
1628
+ `
1629
+ );
1630
+ } catch {
1631
+ }
1632
+ return;
1633
+ }
1634
+ const safeId = basename(gateId);
1635
+ const root = typeof opts.projectRoot === "string" && opts.projectRoot.length > 0 ? opts.projectRoot : process.cwd();
1636
+ const receiptPath = join6(
1637
+ root,
1638
+ ".ijfw",
1639
+ "memory",
1640
+ "gate-receipts",
1641
+ `${safeId}.json`
1642
+ );
1643
+ await mkdir(dirname2(receiptPath), { recursive: true });
1644
+ const body = JSON.stringify(gateResult, null, 2) + "\n";
1645
+ await writeFile(receiptPath, body, "utf8");
1646
+ } catch (err) {
1647
+ const msg = err && err.message ? err.message : String(err);
1648
+ try {
1649
+ process.stderr.write(`ijfw: gate-result receipt write failed: ${msg}
1650
+ `);
1651
+ } catch {
1652
+ }
1653
+ }
1654
+ }
1655
+ async function resolveProjectType(projectRoot) {
1656
+ try {
1657
+ const root = typeof projectRoot === "string" && projectRoot.length > 0 ? projectRoot : process.cwd();
1658
+ const detected = detect(root);
1659
+ if (detected && typeof detected.primary_type === "string" && detected.primary_type.length > 0) {
1660
+ return detected.primary_type;
1661
+ }
1662
+ if (detected && typeof detected.type === "string" && detected.type.length > 0) {
1663
+ return detected.type;
1664
+ }
1665
+ return "unknown";
1666
+ } catch {
1667
+ return "unknown";
1668
+ }
1669
+ }
1670
+ var RECEIPT_GATE_ID_PATTERN;
1671
+ var init_gate_result = __esm({
1672
+ "../mcp-server/src/gate-result.js"() {
1673
+ init_gate_result_schema();
1674
+ init_project_type_detector();
1675
+ RECEIPT_GATE_ID_PATTERN = /^[a-z][a-z0-9-]+$/;
1676
+ }
1677
+ });
1678
+
571
1679
  // src/preflight/gates/audit-ci.js
572
1680
  var audit_ci_exports = {};
573
1681
  __export(audit_ci_exports, {
@@ -577,51 +1685,105 @@ __export(audit_ci_exports, {
577
1685
  severity: () => severity7
578
1686
  });
579
1687
  import { spawnSync as spawnSync7 } from "node:child_process";
580
- import { join as join4 } from "node:path";
1688
+ import { join as join7 } from "node:path";
1689
+ function parseAuditReport(output) {
1690
+ const start = output.indexOf("{");
1691
+ if (start === -1) return null;
1692
+ try {
1693
+ return JSON.parse(output.slice(start));
1694
+ } catch {
1695
+ return null;
1696
+ }
1697
+ }
1698
+ function highCriticalCount(report) {
1699
+ const vulns = report?.metadata?.vulnerabilities || {};
1700
+ return Number(vulns.high || 0) + Number(vulns.critical || 0);
1701
+ }
1702
+ function vulnerableNames(report) {
1703
+ const out = [];
1704
+ for (const [name12, vuln] of Object.entries(report?.vulnerabilities || {})) {
1705
+ const severity12 = String(vuln?.severity || "").toLowerCase();
1706
+ if (severity12 === "high" || severity12 === "critical") out.push(`${name12}: ${severity12}`);
1707
+ }
1708
+ return out;
1709
+ }
581
1710
  async function run7(ctx) {
582
1711
  const t0 = Date.now();
583
- const ver = ctx.versions["audit-ci"] || "latest";
584
- const configPath = join4(ctx.repoRoot, ".audit-ci.jsonc");
585
1712
  const packageDirs = ["installer", "mcp-server"];
586
1713
  const runs = packageDirs.map((dir) => {
587
1714
  const res = spawnSync7(
588
- "npx",
589
- ["--yes", `audit-ci@${ver}`, "--config", configPath],
1715
+ "npm",
1716
+ ["audit", "--audit-level=high", "--json"],
590
1717
  {
591
1718
  encoding: "utf8",
592
- cwd: join4(ctx.repoRoot, dir),
1719
+ cwd: join7(ctx.repoRoot, dir),
593
1720
  timeout: 6e4
594
1721
  }
595
1722
  );
596
- return { dir, status: res.status, output: (res.stdout || "") + (res.stderr || "") };
1723
+ const output = (res.stdout || "") + (res.stderr || "");
1724
+ const report = parseAuditReport(output);
1725
+ const highCritical = highCriticalCount(report);
1726
+ return { dir, status: res.status, output, report, highCritical };
597
1727
  });
598
1728
  const durationMs = Date.now() - t0;
599
- const failed = runs.filter((r) => r.status !== 0);
600
- if (failed.length === 0) {
601
- return {
602
- name: "audit-ci",
603
- status: "PASS",
604
- message: "audit-ci: no high/critical vulnerabilities in installer or mcp-server",
605
- details: runs.map((r) => `${r.dir}: pass`),
606
- durationMs
607
- };
1729
+ const failed = runs.filter((r) => !r.report || r.highCritical > 0);
1730
+ const status = failed.length === 0 ? "PASS" : "FAIL";
1731
+ const message = status === "PASS" ? "audit-ci: no high/critical vulnerabilities in installer or mcp-server" : "audit-ci: high or critical vulnerabilities found";
1732
+ let details;
1733
+ if (status === "PASS") {
1734
+ details = runs.map((r) => `${r.dir}: pass`);
1735
+ } else {
1736
+ const lines = [];
1737
+ for (const r of failed) {
1738
+ if (!r.report) {
1739
+ lines.push(`${r.dir}: audit report unavailable`);
1740
+ lines.push(...r.output.split("\n").filter(Boolean).slice(0, 10));
1741
+ continue;
1742
+ }
1743
+ lines.push(`${r.dir}: ${r.highCritical} high/critical advisory item(s)`);
1744
+ lines.push(...vulnerableNames(r.report).slice(0, 10));
1745
+ }
1746
+ details = lines.slice(0, 20);
608
1747
  }
609
- const lines = [];
610
- for (const r of failed) {
611
- lines.push(`${r.dir}: audit failed`);
612
- lines.push(...r.output.split("\n").filter(Boolean).slice(0, 10));
1748
+ try {
1749
+ const block = await emitGateResult(
1750
+ {
1751
+ gate: "preflight:audit-ci",
1752
+ status,
1753
+ lenses: [],
1754
+ affected_artifacts: [],
1755
+ accounting: {
1756
+ duration_ms: durationMs,
1757
+ lenses_invoked: 0,
1758
+ cost_usd: null
1759
+ },
1760
+ remediation: []
1761
+ },
1762
+ ctx && ctx.repoRoot ? { projectRoot: ctx.repoRoot } : {}
1763
+ );
1764
+ if (typeof block === "string" && block.length > 0) {
1765
+ details = [...details, block];
1766
+ }
1767
+ } catch (err) {
1768
+ const msg = err && err.message ? err.message : String(err);
1769
+ try {
1770
+ process.stderr.write(`ijfw: preflight:audit-ci gate-result emit failed: ${msg}
1771
+ `);
1772
+ } catch {
1773
+ }
613
1774
  }
614
1775
  return {
615
1776
  name: "audit-ci",
616
- status: "FAIL",
617
- message: "audit-ci: high or critical vulnerabilities found",
618
- details: lines.slice(0, 20),
1777
+ status,
1778
+ message,
1779
+ details,
619
1780
  durationMs
620
1781
  };
621
1782
  }
622
1783
  var name7, severity7, parallel7;
623
1784
  var init_audit_ci = __esm({
624
1785
  "src/preflight/gates/audit-ci.js"() {
1786
+ init_gate_result();
625
1787
  name7 = "audit-ci";
626
1788
  severity7 = "blocking";
627
1789
  parallel7 = true;
@@ -683,7 +1845,7 @@ __export(license_check_exports, {
683
1845
  severity: () => severity9
684
1846
  });
685
1847
  import { spawnSync as spawnSync9 } from "node:child_process";
686
- import { join as join5 } from "node:path";
1848
+ import { join as join8 } from "node:path";
687
1849
  async function run9(ctx) {
688
1850
  const t0 = Date.now();
689
1851
  const ver = ctx.versions["license-checker"] || "latest";
@@ -692,7 +1854,7 @@ async function run9(ctx) {
692
1854
  ["--yes", `license-checker@${ver}`, "--onlyAllow", ALLOWED, "--production"],
693
1855
  {
694
1856
  encoding: "utf8",
695
- cwd: join5(ctx.repoRoot, "installer"),
1857
+ cwd: join8(ctx.repoRoot, "installer"),
696
1858
  timeout: 3e4
697
1859
  }
698
1860
  );
@@ -735,12 +1897,12 @@ __export(pack_smoke_exports, {
735
1897
  severity: () => severity10
736
1898
  });
737
1899
  import { spawnSync as spawnSync10 } from "node:child_process";
738
- import { mkdtempSync as mkdtempSync2, rmSync as rmSync2, mkdirSync, writeFileSync as writeFileSync2, readdirSync as readdirSync3, existsSync } from "node:fs";
739
- import { join as join6, resolve } from "node:path";
1900
+ import { mkdtempSync as mkdtempSync2, rmSync as rmSync2, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4, readdirSync as readdirSync4, existsSync as existsSync3 } from "node:fs";
1901
+ import { join as join9, resolve } from "node:path";
740
1902
  import { tmpdir as tmpdir2 } from "node:os";
741
1903
  async function run10(ctx) {
742
1904
  const t0 = Date.now();
743
- const installerDir = join6(ctx.repoRoot, "installer");
1905
+ const installerDir = join9(ctx.repoRoot, "installer");
744
1906
  const build = spawnSync10("npm", ["run", "build"], {
745
1907
  encoding: "utf8",
746
1908
  cwd: installerDir,
@@ -782,13 +1944,13 @@ async function run10(ctx) {
782
1944
  };
783
1945
  }
784
1946
  const tarballPath = resolve(installerDir, tarball);
785
- const tmpRoot = mkdtempSync2(join6(tmpdir2(), "ijfw-pack-smoke-"));
786
- const fakeHome = join6(tmpRoot, "home");
787
- const installDir = join6(tmpRoot, "install");
788
- mkdirSync(fakeHome, { recursive: true });
789
- mkdirSync(installDir, { recursive: true });
1947
+ const tmpRoot = mkdtempSync2(join9(tmpdir2(), "ijfw-pack-smoke-"));
1948
+ const fakeHome = join9(tmpRoot, "home");
1949
+ const installDir = join9(tmpRoot, "install");
1950
+ mkdirSync3(fakeHome, { recursive: true });
1951
+ mkdirSync3(installDir, { recursive: true });
790
1952
  try {
791
- writeFileSync2(join6(installDir, "package.json"), JSON.stringify({ name: "smoke-test", version: "1.0.0", type: "module" }));
1953
+ writeFileSync4(join9(installDir, "package.json"), JSON.stringify({ name: "smoke-test", version: "1.0.0", type: "module" }));
792
1954
  const install = spawnSync10("npm", ["install", "--no-save", tarballPath], {
793
1955
  encoding: "utf8",
794
1956
  cwd: installDir,
@@ -806,25 +1968,25 @@ async function run10(ctx) {
806
1968
  };
807
1969
  }
808
1970
  const binCandidates = [
809
- join6(installDir, "node_modules", ".bin", "ijfw"),
810
- join6(installDir, "node_modules", ".bin", "ijfw-install")
1971
+ join9(installDir, "node_modules", ".bin", "ijfw"),
1972
+ join9(installDir, "node_modules", ".bin", "ijfw-install")
811
1973
  ];
812
1974
  let binPath = null;
813
1975
  for (const c2 of binCandidates) {
814
- if (existsSync(c2)) {
1976
+ if (existsSync3(c2)) {
815
1977
  binPath = c2;
816
1978
  break;
817
1979
  }
818
1980
  }
819
1981
  if (!binPath) {
820
- const binDir = join6(installDir, "node_modules", ".bin");
1982
+ const binDir = join9(installDir, "node_modules", ".bin");
821
1983
  let entries = [];
822
1984
  try {
823
- entries = readdirSync3(binDir);
1985
+ entries = readdirSync4(binDir);
824
1986
  } catch {
825
1987
  }
826
1988
  const found = entries.find((e) => e.startsWith("ijfw"));
827
- if (found) binPath = join6(binDir, found);
1989
+ if (found) binPath = join9(binDir, found);
828
1990
  }
829
1991
  if (!binPath) {
830
1992
  return {
@@ -887,12 +2049,12 @@ __export(upgrade_smoke_exports, {
887
2049
  severity: () => severity11
888
2050
  });
889
2051
  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";
891
- import { join as join7, resolve as resolve2 } from "node:path";
2052
+ import { mkdtempSync as mkdtempSync3, rmSync as rmSync3, mkdirSync as mkdirSync4, writeFileSync as writeFileSync5, readFileSync as readFileSync4, existsSync as existsSync4 } from "node:fs";
2053
+ import { join as join10, resolve as resolve2 } from "node:path";
892
2054
  import { tmpdir as tmpdir3 } from "node:os";
893
2055
  async function run11(ctx) {
894
2056
  const t0 = Date.now();
895
- const installerDir = join7(ctx.repoRoot, "installer");
2057
+ const installerDir = join10(ctx.repoRoot, "installer");
896
2058
  const build = spawnSync11("npm", ["run", "build"], {
897
2059
  encoding: "utf8",
898
2060
  cwd: installerDir,
@@ -925,15 +2087,15 @@ async function run11(ctx) {
925
2087
  }
926
2088
  const tarball = pack.stdout.trim();
927
2089
  const tarballPath = resolve2(installerDir, tarball);
928
- const tmpRoot = mkdtempSync3(join7(tmpdir3(), "ijfw-upgrade-smoke-"));
929
- const fakeHome = join7(tmpRoot, "home");
930
- const installDir = join7(tmpRoot, "install");
931
- mkdirSync2(fakeHome, { recursive: true });
932
- mkdirSync2(installDir, { recursive: true });
933
- const claudeDir = join7(fakeHome, ".claude");
934
- mkdirSync2(claudeDir, { recursive: true });
2090
+ const tmpRoot = mkdtempSync3(join10(tmpdir3(), "ijfw-upgrade-smoke-"));
2091
+ const fakeHome = join10(tmpRoot, "home");
2092
+ const installDir = join10(tmpRoot, "install");
2093
+ mkdirSync4(fakeHome, { recursive: true });
2094
+ mkdirSync4(installDir, { recursive: true });
2095
+ const claudeDir = join10(fakeHome, ".claude");
2096
+ mkdirSync4(claudeDir, { recursive: true });
935
2097
  try {
936
- writeFileSync3(join7(installDir, "package.json"), JSON.stringify({ name: "upgrade-smoke", version: "1.0.0", type: "module" }));
2098
+ writeFileSync5(join10(installDir, "package.json"), JSON.stringify({ name: "upgrade-smoke", version: "1.0.0", type: "module" }));
937
2099
  const install = spawnSync11("npm", ["install", "--no-save", tarballPath], {
938
2100
  encoding: "utf8",
939
2101
  cwd: installDir,
@@ -951,12 +2113,12 @@ async function run11(ctx) {
951
2113
  };
952
2114
  }
953
2115
  const binCandidates = [
954
- join7(installDir, "node_modules", ".bin", "ijfw-install"),
955
- join7(installDir, "node_modules", ".bin", "ijfw")
2116
+ join10(installDir, "node_modules", ".bin", "ijfw-install"),
2117
+ join10(installDir, "node_modules", ".bin", "ijfw")
956
2118
  ];
957
2119
  let installerBin = null;
958
2120
  for (const c2 of binCandidates) {
959
- if (existsSync2(c2)) {
2121
+ if (existsSync4(c2)) {
960
2122
  installerBin = c2;
961
2123
  break;
962
2124
  }
@@ -970,11 +2132,11 @@ async function run11(ctx) {
970
2132
  durationMs: Date.now() - t0
971
2133
  };
972
2134
  }
973
- const settingsPath = join7(claudeDir, "settings.json");
974
- if (existsSync2(settingsPath)) {
2135
+ const settingsPath = join10(claudeDir, "settings.json");
2136
+ if (existsSync4(settingsPath)) {
975
2137
  let settings;
976
2138
  try {
977
- settings = JSON.parse(readFileSync(settingsPath, "utf8"));
2139
+ settings = JSON.parse(readFileSync4(settingsPath, "utf8"));
978
2140
  } catch (e) {
979
2141
  return {
980
2142
  name: "upgrade-smoke",
@@ -995,9 +2157,9 @@ async function run11(ctx) {
995
2157
  };
996
2158
  }
997
2159
  }
998
- const marketplaceSrc = join7(installerDir, "src", "marketplace.js");
999
- if (existsSync2(marketplaceSrc)) {
1000
- const src = readFileSync(marketplaceSrc, "utf8");
2160
+ const marketplaceSrc = join10(installerDir, "src", "marketplace.js");
2161
+ if (existsSync4(marketplaceSrc)) {
2162
+ const src = readFileSync4(marketplaceSrc, "utf8");
1001
2163
  const registersCorrectKey = src.includes("'ijfw@ijfw'") || src.includes('"ijfw@ijfw"');
1002
2164
  const registersWrongKey = /enabledPlugins\[['"]ijfw-core@ijfw['"]\]\s*=\s*true/.test(src);
1003
2165
  if (!registersCorrectKey) {
@@ -1052,9 +2214,9 @@ var preflight_exports = {};
1052
2214
  __export(preflight_exports, {
1053
2215
  runPreflightCommand: () => runPreflightCommand
1054
2216
  });
1055
- import { readFileSync as readFileSync2, existsSync as existsSync3 } from "node:fs";
1056
- import { join as join8, dirname, resolve as resolve3 } from "node:path";
1057
- import { fileURLToPath } from "node:url";
2217
+ import { readFileSync as readFileSync5, existsSync as existsSync5 } from "node:fs";
2218
+ import { join as join11, dirname as dirname3, resolve as resolve3 } from "node:path";
2219
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
1058
2220
  function printHelp() {
1059
2221
  console.log(`
1060
2222
  ijfw preflight -- 11-gate quality pipeline
@@ -1090,13 +2252,13 @@ SLO
1090
2252
  }
1091
2253
  function loadVersions(repoRoot2) {
1092
2254
  const candidates = [
1093
- join8(repoRoot2, "preflight-versions.json"),
1094
- join8(repoRoot2, ".ijfw", "preflight-versions.json")
2255
+ join11(repoRoot2, "preflight-versions.json"),
2256
+ join11(repoRoot2, ".ijfw", "preflight-versions.json")
1095
2257
  ];
1096
2258
  for (const f of candidates) {
1097
- if (existsSync3(f)) {
2259
+ if (existsSync5(f)) {
1098
2260
  try {
1099
- return JSON.parse(readFileSync2(f, "utf8"));
2261
+ return JSON.parse(readFileSync5(f, "utf8"));
1100
2262
  } catch {
1101
2263
  }
1102
2264
  }
@@ -1155,7 +2317,7 @@ async function runPreflightCommand(argv, repoRoot2) {
1155
2317
  function defaultRepoRoot() {
1156
2318
  let dir = __dirname;
1157
2319
  for (let i = 0; i < 8; i++) {
1158
- if (existsSync3(join8(dir, "package.json")) && existsSync3(join8(dir, "mcp-server"))) return dir;
2320
+ if (existsSync5(join11(dir, "package.json")) && existsSync5(join11(dir, "mcp-server"))) return dir;
1159
2321
  const next = resolve3(dir, "..");
1160
2322
  if (next === dir) break;
1161
2323
  dir = next;
@@ -1166,8 +2328,8 @@ var __dirname;
1166
2328
  var init_preflight = __esm({
1167
2329
  async "src/preflight.js"() {
1168
2330
  init_runner();
1169
- __dirname = dirname(fileURLToPath(import.meta.url));
1170
- if (process.argv[1] && resolve3(process.argv[1]) === fileURLToPath(import.meta.url)) {
2331
+ __dirname = dirname3(fileURLToPath2(import.meta.url));
2332
+ if (process.argv[1] && resolve3(process.argv[1]) === fileURLToPath2(import.meta.url)) {
1171
2333
  await runPreflightCommand(process.argv, defaultRepoRoot());
1172
2334
  }
1173
2335
  }
@@ -2433,20 +3595,78 @@ Please report this to https://github.com/markedjs/marked.`, e) {
2433
3595
  });
2434
3596
 
2435
3597
  // src/ijfw.js
2436
- import { dirname as dirname2, join as join9, resolve as resolve4, basename } from "node:path";
2437
- 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";
3598
+ import { dirname as dirname4, join as join12, resolve as resolve4, basename as basename2 } from "node:path";
3599
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
3600
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, copyFileSync as copyFileSync3, readdirSync as readdirSync5, rmSync as rmSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "node:fs";
3601
+ import { homedir, platform } from "node:os";
2440
3602
  import { spawnSync as spawnSync12 } from "node:child_process";
2441
- var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
3603
+ var __dirname2 = dirname4(fileURLToPath3(import.meta.url));
2442
3604
  function repoRoot() {
2443
3605
  let dir = __dirname2;
2444
3606
  for (let i = 0; i < 6; i++) {
2445
- if (existsSync4(join9(dir, "package.json")) && existsSync4(join9(dir, ".git"))) return dir;
3607
+ if (existsSync6(join12(dir, "package.json")) && existsSync6(join12(dir, ".git"))) return dir;
2446
3608
  dir = resolve4(dir, "..");
2447
3609
  }
2448
3610
  return process.cwd();
2449
3611
  }
3612
+ function findInternalAsset(...rel) {
3613
+ const root = repoRoot();
3614
+ const ijfwHome = join12(homedir(), ".ijfw");
3615
+ const candidates = [join12(root, ...rel), join12(ijfwHome, ...rel)];
3616
+ return candidates.find((p) => existsSync6(p)) || null;
3617
+ }
3618
+ function readDashboardPort() {
3619
+ const portFile = join12(homedir(), ".ijfw", "dashboard.port");
3620
+ try {
3621
+ const port = Number.parseInt(readFileSync6(portFile, "utf8").trim(), 10);
3622
+ return Number.isFinite(port) ? port : 37891;
3623
+ } catch {
3624
+ return 37891;
3625
+ }
3626
+ }
3627
+ function openBrowser(url) {
3628
+ if (process.env.CI || process.env.NO_OPEN) return;
3629
+ 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" });
3630
+ return r.status ?? 0;
3631
+ }
3632
+ var ORCHESTRATOR_COMMANDS = /* @__PURE__ */ new Set([
3633
+ "update",
3634
+ "statusline",
3635
+ "config",
3636
+ "insight",
3637
+ "blackboard",
3638
+ "team",
3639
+ "swarm",
3640
+ "codex",
3641
+ "recover",
3642
+ "memory",
3643
+ "cross",
3644
+ "status",
3645
+ "demo",
3646
+ "import",
3647
+ "receipt",
3648
+ "--purge-receipts",
3649
+ "workflow",
3650
+ "handoff",
3651
+ "compress",
3652
+ "consolidate",
3653
+ "cross-audit",
3654
+ "cross-critique",
3655
+ "cross-research",
3656
+ "ijfw-audit",
3657
+ "ijfw-execute",
3658
+ "ijfw-help",
3659
+ "ijfw-plan",
3660
+ "ijfw-ship",
3661
+ "ijfw-verify",
3662
+ "memory-audit",
3663
+ "memory-consent",
3664
+ "memory-why",
3665
+ "metrics",
3666
+ "mode",
3667
+ "override",
3668
+ "extension"
3669
+ ]);
2450
3670
  function printHelp2() {
2451
3671
  console.log(`
2452
3672
  ijfw -- the AI efficiency layer
@@ -2460,7 +3680,13 @@ COMMANDS
2460
3680
  help Open the full IJFW guide (terminal, or --browser for rendered)
2461
3681
  preflight Run 11-gate quality pipeline before publishing
2462
3682
  dashboard Start / stop / check the local observability dashboard
2463
- design Manage the visual design companion
3683
+ design Manage live previews and durable design intelligence
3684
+ blackboard Coordinate project-local swarm state and artifact claims
3685
+ codex Check and sync Codex-native IJFW surfaces
3686
+ team Assemble project agents, charter, and workflow manifest
3687
+ swarm Plan, prepare, and track artifact-aware parallel work
3688
+ recover Show latest checkpoint and next recovery step
3689
+ cross Run Trident audit/research/critique, e.g. ijfw cross audit README.md
2464
3690
  doctor Diagnose IJFW installation health
2465
3691
 
2466
3692
  --help, -h Show this help
@@ -2475,10 +3701,10 @@ function doctorCheck(cmd, args) {
2475
3701
  }
2476
3702
  function findCli() {
2477
3703
  const candidates = [
2478
- join9(repoRoot(), "mcp-server", "src", "cross-orchestrator-cli.js"),
2479
- join9(homedir(), ".ijfw", "mcp-server", "src", "cross-orchestrator-cli.js")
3704
+ join12(repoRoot(), "mcp-server", "src", "cross-orchestrator-cli.js"),
3705
+ join12(homedir(), ".ijfw", "mcp-server", "src", "cross-orchestrator-cli.js")
2480
3706
  ];
2481
- return candidates.find((p) => existsSync4(p)) || null;
3707
+ return candidates.find((p) => existsSync6(p)) || null;
2482
3708
  }
2483
3709
  function delegateToCli(argTail) {
2484
3710
  const cli = findCli();
@@ -2497,8 +3723,8 @@ async function main() {
2497
3723
  const verbose = argv.slice(3).includes("--verbose");
2498
3724
  if (delegateToCli(argv.slice(2))) return;
2499
3725
  try {
2500
- const pkgPath = join9(__dirname2, "..", "package.json");
2501
- const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
3726
+ const pkgPath = join12(__dirname2, "..", "package.json");
3727
+ const pkg = JSON.parse(readFileSync6(pkgPath, "utf8"));
2502
3728
  console.log(`@ijfw/install@${pkg.version || "unknown"}`);
2503
3729
  if (verbose) {
2504
3730
  console.log(" (full --verbose details require a completed install: run ijfw install)");
@@ -2508,11 +3734,14 @@ async function main() {
2508
3734
  }
2509
3735
  process.exit(0);
2510
3736
  }
2511
- if (sub === "update" || sub === "statusline" || sub === "config" || sub === "insight") {
3737
+ if (ORCHESTRATOR_COMMANDS.has(sub)) {
2512
3738
  if (delegateToCli(argv.slice(2))) return;
2513
3739
  console.error(`'ijfw ${sub}' requires a completed IJFW install. Run: ijfw install`);
2514
3740
  process.exit(1);
2515
3741
  }
3742
+ if (sub === "doctor" && findCli()) {
3743
+ if (delegateToCli(argv.slice(2))) return;
3744
+ }
2516
3745
  switch (sub) {
2517
3746
  case "install": {
2518
3747
  const installBin = resolve4(__dirname2, "..", "dist", "install.js");
@@ -2533,19 +3762,13 @@ async function main() {
2533
3762
  }
2534
3763
  case "dashboard": {
2535
3764
  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
3765
  if (dashSub === "start" || dashSub === "stop" || dashSub === "status") {
2543
- const dashBin = findInTree("mcp-server", "bin", "ijfw-dashboard");
3766
+ const dashBin = findInternalAsset("mcp-server", "bin", "ijfw-dashboard");
2544
3767
  if (dashBin) {
2545
3768
  const r = spawnSync12("node", [dashBin, dashSub, ...argv.slice(4)], { stdio: "inherit" });
2546
3769
  process.exit(r.status ?? 0);
2547
3770
  } else {
2548
- const serverJs = findInTree("mcp-server", "src", "dashboard-server.js");
3771
+ const serverJs = findInternalAsset("mcp-server", "src", "dashboard-server.js");
2549
3772
  if (dashSub === "start" && serverJs) {
2550
3773
  const { spawn } = await import("node:child_process");
2551
3774
  const child = spawn(process.execPath, [serverJs, "start", "--daemon"], {
@@ -2560,7 +3783,7 @@ async function main() {
2560
3783
  process.exit(1);
2561
3784
  }
2562
3785
  } else if (dashSub === "render" || !dashSub) {
2563
- const binJs = findInTree("scripts", "dashboard", "bin.js");
3786
+ const binJs = findInternalAsset("scripts", "dashboard", "bin.js");
2564
3787
  if (binJs) {
2565
3788
  const r = spawnSync12("node", [binJs, ...argv.slice(dashSub ? 4 : 3)], { stdio: "inherit" });
2566
3789
  process.exit(r.status ?? 0);
@@ -2576,30 +3799,72 @@ async function main() {
2576
3799
  }
2577
3800
  case "design": {
2578
3801
  const designSub = argv[3];
2579
- const contentDir = join9(homedir(), ".ijfw", "design-companion", "content");
2580
- 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>");
3802
+ const durableDesign = ["init", "plan", "audit", "critique", "polish", "normalize", "bolder", "quieter", "handoff"];
3803
+ if (durableDesign.includes(designSub)) {
3804
+ if (delegateToCli(argv.slice(2))) return;
3805
+ console.error(`'ijfw design ${designSub}' requires a completed IJFW install. Run: ijfw install`);
3806
+ process.exit(1);
3807
+ }
3808
+ const contentDir = join12(homedir(), ".ijfw", "design-companion", "content");
3809
+ mkdirSync5(contentDir, { recursive: true });
3810
+ if (designSub === "start" || designSub === "open") {
3811
+ const dashBin = findInternalAsset("mcp-server", "bin", "ijfw-dashboard");
3812
+ if (!dashBin) {
3813
+ console.error("[ijfw] Design companion server not found. Run `ijfw-install` to deploy ~/.ijfw/, or run from the IJFW repo root.");
2585
3814
  process.exit(1);
2586
3815
  }
2587
- const abs = resolve4(filePath);
2588
- if (!existsSync4(abs)) {
2589
- console.error(`File not found: ${abs}`);
3816
+ const noOpen = argv.slice(4).includes("--no-open");
3817
+ const r = spawnSync12("node", [dashBin, "start", "--no-open"], { stdio: designSub === "start" ? "inherit" : "ignore" });
3818
+ if ((r.status ?? 1) !== 0) process.exit(r.status ?? 1);
3819
+ const url = `http://localhost:${readDashboardPort()}/design`;
3820
+ if (!noOpen) openBrowser(url);
3821
+ console.log(`Design companion running at ${url}`);
3822
+ } else if (designSub === "status") {
3823
+ const dashBin = findInternalAsset("mcp-server", "bin", "ijfw-dashboard");
3824
+ if (!dashBin) {
3825
+ console.error("[ijfw] Design companion server not found. Run `ijfw-install` to deploy ~/.ijfw/, or run from the IJFW repo root.");
2590
3826
  process.exit(1);
2591
3827
  }
2592
- const dest = join9(contentDir, basename(abs));
2593
- copyFileSync(abs, dest);
2594
- console.log(`Design pushed: ${dest}`);
3828
+ const r = spawnSync12("node", [dashBin, "status"], { stdio: "inherit" });
3829
+ if ((r.status ?? 1) === 0) console.log(`Design companion URL: http://localhost:${readDashboardPort()}/design`);
3830
+ process.exit(r.status ?? 0);
3831
+ } else if (designSub === "stop") {
3832
+ const dashBin = findInternalAsset("mcp-server", "bin", "ijfw-dashboard");
3833
+ if (!dashBin) {
3834
+ console.error("[ijfw] Design companion server not found. Run `ijfw-install` to deploy ~/.ijfw/, or run from the IJFW repo root.");
3835
+ process.exit(1);
3836
+ }
3837
+ const r = spawnSync12("node", [dashBin, "stop"], { stdio: "inherit" });
3838
+ process.exit(r.status ?? 0);
3839
+ } else if (designSub === "push") {
3840
+ const filePaths = argv.slice(4);
3841
+ if (filePaths.length === 0) {
3842
+ console.error("Usage: ijfw design push <file.html> [more.html ...]");
3843
+ process.exit(1);
3844
+ }
3845
+ for (const filePath of filePaths) {
3846
+ const abs = resolve4(filePath);
3847
+ if (!abs.toLowerCase().endsWith(".html")) {
3848
+ console.error("Design companion accepts standalone .html files.");
3849
+ process.exit(1);
3850
+ }
3851
+ if (!existsSync6(abs)) {
3852
+ console.error(`File not found: ${abs}`);
3853
+ process.exit(1);
3854
+ }
3855
+ const dest = join12(contentDir, basename2(abs));
3856
+ copyFileSync3(abs, dest);
3857
+ console.log(`Design pushed: ${dest}`);
3858
+ }
3859
+ console.log(`Preview: http://localhost:${readDashboardPort()}/design`);
2595
3860
  } else if (designSub === "clear") {
2596
- const files = readdirSync4(contentDir);
2597
- for (const f of files) rmSync4(join9(contentDir, f), { force: true });
3861
+ const files = readdirSync5(contentDir);
3862
+ for (const f of files) rmSync4(join12(contentDir, f), { force: true });
2598
3863
  console.log("Design companion content cleared.");
2599
3864
  } else {
2600
- console.log("ijfw design -- Manage the visual design companion. Push HTML mockups for live preview.");
3865
+ console.log("ijfw design -- Manage live preview and durable design intelligence.");
2601
3866
  console.log("");
2602
- console.log("Usage: ijfw design push <file.html> | ijfw design clear");
3867
+ 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
3868
  process.exit(1);
2604
3869
  }
2605
3870
  break;
@@ -2607,26 +3872,26 @@ async function main() {
2607
3872
  case "help": {
2608
3873
  const wantsBrowser = argv.slice(3).includes("--browser");
2609
3874
  const candidates = [
2610
- join9(repoRoot(), "docs", "GUIDE.md"),
3875
+ join12(repoRoot(), "docs", "GUIDE.md"),
2611
3876
  resolve4(__dirname2, "..", "docs", "GUIDE.md"),
2612
- join9(homedir(), ".ijfw", "docs", "GUIDE.md")
3877
+ join12(homedir(), ".ijfw", "docs", "GUIDE.md")
2613
3878
  ];
2614
- const guidePath = candidates.find((p) => existsSync4(p));
3879
+ const guidePath = candidates.find((p) => existsSync6(p));
2615
3880
  if (!guidePath) {
2616
3881
  console.error("[ijfw] Guide not found. Run `ijfw install` to fetch the full guide, or visit https://gitlab.com/therealseandonahoe/ijfw/-/blob/main/docs/GUIDE.md");
2617
3882
  process.exit(1);
2618
3883
  }
2619
3884
  if (wantsBrowser) {
2620
3885
  const { marked } = await Promise.resolve().then(() => (init_marked_esm(), marked_esm_exports));
2621
- const assetsSrc = join9(dirname2(guidePath), "guide", "assets");
2622
- const outDir = join9(homedir(), ".ijfw", "guide");
2623
- mkdirSync3(join9(outDir, "assets"), { recursive: true });
2624
- if (existsSync4(assetsSrc)) {
2625
- for (const f of readdirSync4(assetsSrc)) {
2626
- copyFileSync(join9(assetsSrc, f), join9(outDir, "assets", f));
3886
+ const assetsSrc = join12(dirname4(guidePath), "guide", "assets");
3887
+ const outDir = join12(homedir(), ".ijfw", "guide");
3888
+ mkdirSync5(join12(outDir, "assets"), { recursive: true });
3889
+ if (existsSync6(assetsSrc)) {
3890
+ for (const f of readdirSync5(assetsSrc)) {
3891
+ copyFileSync3(join12(assetsSrc, f), join12(outDir, "assets", f));
2627
3892
  }
2628
3893
  }
2629
- const md = readFileSync3(guidePath, "utf8").replace(/\(guide\/assets\//g, "(assets/");
3894
+ const md = readFileSync6(guidePath, "utf8").replace(/\(guide\/assets\//g, "(assets/");
2630
3895
  const rendered = marked.parse(md, { gfm: true, breaks: false });
2631
3896
  const html = `<!doctype html>
2632
3897
  <html lang="en"><head>
@@ -2643,9 +3908,9 @@ async function main() {
2643
3908
  table{display:table;width:100%}
2644
3909
  </style>
2645
3910
  </head><body><div class="wrap markdown-body">${rendered}</div></body></html>`;
2646
- const outHtml = join9(outDir, "index.html");
2647
- writeFileSync4(outHtml, html);
2648
- const opener = platform2() === "darwin" ? "open" : platform2() === "win32" ? "start" : "xdg-open";
3911
+ const outHtml = join12(outDir, "index.html");
3912
+ writeFileSync6(outHtml, html);
3913
+ const opener = platform() === "darwin" ? "open" : platform() === "win32" ? "start" : "xdg-open";
2649
3914
  spawnSync12(opener, [outHtml], { stdio: "ignore", detached: true });
2650
3915
  console.log(`[ijfw] Guide opened in your browser.`);
2651
3916
  console.log(` Local copy: ${outHtml}`);
@@ -2655,10 +3920,10 @@ async function main() {
2655
3920
  if (hasLess) {
2656
3921
  const lessRes = spawnSync12("less", ["-R", guidePath], { stdio: "inherit" });
2657
3922
  if (lessRes.status !== 0 && lessRes.status !== null) {
2658
- process.stdout.write(readFileSync3(guidePath, "utf8"));
3923
+ process.stdout.write(readFileSync6(guidePath, "utf8"));
2659
3924
  }
2660
3925
  } else {
2661
- process.stdout.write(readFileSync3(guidePath, "utf8"));
3926
+ process.stdout.write(readFileSync6(guidePath, "utf8"));
2662
3927
  }
2663
3928
  process.exit(0);
2664
3929
  break;