santifer/career-ops · error · Error

portals.yml not found

Error message

portals.yml not found

What it means

Thrown by parsePortals() when readFile('portals.yml') returns empty/falsy (file missing, empty, or unreadable as text). parsePortals is the runner's no-CLI scanner config source, mirroring scan.mjs's schema; without it the runner cannot know which companies to scan.

Source

Thrown at openrouter-runner.mjs:454

  }
}

// ---------------------------------------------------------------------------
// portals.yml parser — reads the canonical schema with js-yaml (same library and
// field names as scan.mjs: `title_filter.positive/negative` + `tracked_companies`),
// so it never drifts from the main scanner. The runner's no-CLI scan path covers
// companies that expose a direct JSON `api:`; careers_url-only / Playwright /
// search-query companies are handled by the full /career-ops scan pipeline.
// `rawOverride` lets tests feed YAML text directly (see test-all.mjs drift guard).
// ---------------------------------------------------------------------------
function normKeywords(v) {
  if (!Array.isArray(v)) return [];
  return v.map(x => String(x ?? '').toLowerCase().trim()).filter(Boolean);
}

export function parsePortals(rawOverride) {
  const raw = rawOverride ?? readFile('portals.yml');
  if (!raw) throw new Error('portals.yml not found');
  const config = yaml.load(raw) || {};

  const tf = config.title_filter || {};
  const positive = normKeywords(tf.positive);
  const negative = normKeywords(tf.negative);
  function titleMatches(title) {
    const t = String(title ?? '').toLowerCase();
    return positive.some(k => t.includes(k)) && !negative.some(k => t.includes(k));
  }

  // Companies with a direct JSON `api:` endpoint (the no-CLI scan path).
  const tracked = Array.isArray(config.tracked_companies) ? config.tracked_companies : [];
  const companies = tracked
    .filter(c => c && c.api && c.enabled !== false)
    .map(c => ({ name: String(c.name ?? c.company ?? 'Unknown'), api: String(c.api).trim() }));

  return { companies, titleMatches };
}

View on GitHub (pinned to 9b17a8ac97)

Solutions

  1. Run onboarding (node doctor.mjs) to scaffold portals.yml from templates/portals.example.yml.
  2. If the file exists but is empty, copy templates/portals.example.yml over it and edit.
  3. Run the runner from the repo root so the relative 'portals.yml' path resolves.
  4. Pass a rawOverride string to parsePortals() for tests instead of relying on the file.

Example fix

// before: portals.yml missing -> throws
const cfg = parsePortals();
// after: copy the template first, then parse
fs.copyFileSync('templates/portals.example.yml', 'portals.yml');
const cfg = parsePortals();
Defensive patterns

Strategy: validation

Validate before calling

import { existsSync, statSync } from 'fs';
function portalsReadable() {
  if (!existsSync('portals.yml')) return false;
  return statSync('portals.yml').size > 0;
}
if (!portalsReadable()) {
  // scaffold from template before calling parsePortals()
}

Type guard

null

Try / catch

try {
  const cfg = parsePortals();
} catch (e) {
  if (/portals\.yml not found/.test(e.message)) {
    // run onboarding / copy template
  } else throw e;
}

Prevention

When it happens

Trigger: First run before onboarding completed; portals.yml deleted or never created; the file exists but is empty; running the runner from a working directory that isn't the repo root so the relative path resolves to nothing.

Common situations: New clone without running onboarding; portals.yml was gitignored locally and not regenerated; user ran `node openrouter-runner.mjs` from a subdirectory; the file was truncated to 0 bytes by a bad editor save.

Related errors


AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13). Data as JSON: /api/errors/70b90d632be0c9f9. Report an issue: GitHub.