import type { Dirent } from "node:fs"; import { readdir } from "node:fs/promises"; import { join } from "node:path"; import type { StartStep, WorkflowMessage } from "@uncaged/nerve-core"; import type { ExplorerMeta } from "../roles/explorer.js"; import type { QuestionerMeta } from "../roles/questioner.js"; async function walkMarkdownFiles(rootDir: string, base: string): Promise { const out: string[] = []; let entries: Dirent[]; try { entries = (await readdir(rootDir, { withFileTypes: true })) as Dirent[]; } catch { return out; } for (const e of entries) { const name = e.name; const rel = base ? `${base}/${name}` : name; const full = join(rootDir, name); if (e.isDirectory()) { out.push(...(await walkMarkdownFiles(full, rel))); } else if (e.isFile() && name.endsWith(".md")) { out.push(rel.replace(/\\/g, "/")); } } return out; } /** Enumerate all markdown files under `.knowledge/` as repo-relative paths; seed line first if present. */ export async function bootstrapKnowledgeQueue(cwd: string, startContent: string): Promise { const knowledgeDir = join(cwd, ".knowledge"); const relFiles = await walkMarkdownFiles(knowledgeDir, ""); const paths = relFiles.map((f) => `.knowledge/${f}`); const seed = startContent.trim().split(/\r?\n/u)[0]?.trim() ?? ""; if (paths.length === 0 && seed.length > 0) { return [seed]; } if (seed.length > 0 && paths.includes(seed)) { return [seed, ...paths.filter((p) => p !== seed)]; } if (seed.length > 0 && !paths.includes(seed)) { return [seed, ...paths]; } return [...paths].sort(); } function lastIndexOfRole(messages: WorkflowMessage[], role: string): number { for (let i = messages.length - 1; i >= 0; i--) { if (messages[i].role === role) return i; } return -1; } /** Next queue for questioner: bootstrap, or continue after answerer / explorer. */ export async function resolveQueueForQuestioner( start: StartStep, messages: WorkflowMessage[], cwd: string, ): Promise { const lastQi = lastIndexOfRole(messages, "questioner"); if (lastQi === -1) { return bootstrapKnowledgeQueue(cwd, start.content); } const qMeta = messages[lastQi].meta as QuestionerMeta; const tail = messages.slice(lastQi + 1); const explorerMsg = tail.find((m) => m.role === "explorer"); if (explorerMsg) { const eMeta = explorerMsg.meta as ExplorerMeta; return [...qMeta.remaining_queue, ...eMeta.new_cards]; } return qMeta.remaining_queue; }