reasonix 0.4.26 → 0.4.28
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/cli/{chunk-2BYEKJHX.js → chunk-ANMDY236.js} +19 -1
- package/dist/cli/chunk-ANMDY236.js.map +1 -0
- package/dist/cli/index.js +157 -39
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/{prompt-6DMLWG2H.js → prompt-75XLIUTO.js} +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/cli/chunk-2BYEKJHX.js.map +0 -1
- /package/dist/cli/{prompt-6DMLWG2H.js.map → prompt-75XLIUTO.js.map} +0 -0
|
@@ -573,6 +573,24 @@ function applyMemoryStack(basePrompt, rootDir) {
|
|
|
573
573
|
// src/code/prompt.ts
|
|
574
574
|
var CODE_SYSTEM_PROMPT = `You are Reasonix Code, a coding assistant. You have filesystem tools (read_file, write_file, list_directory, search_files, etc.) rooted at the user's working directory.
|
|
575
575
|
|
|
576
|
+
# Cite or shut up \u2014 non-negotiable
|
|
577
|
+
|
|
578
|
+
Every factual claim you make about THIS codebase must be backed by evidence. Reasonix VALIDATES the citations you write \u2014 broken paths or out-of-range lines render in **red strikethrough with \u274C** in front of the user.
|
|
579
|
+
|
|
580
|
+
**Positive claims** (a file exists, a function does X, a feature IS implemented) \u2014 append a markdown link to the source:
|
|
581
|
+
|
|
582
|
+
- \u2705 Correct: \`The MCP client supports listResources [listResources](src/mcp/client.ts:142).\`
|
|
583
|
+
- \u274C Wrong: \`The MCP client supports listResources.\` \u2190 no citation, looks authoritative but unverifiable.
|
|
584
|
+
|
|
585
|
+
**Negative claims** (X is missing, Y is not implemented, lacks Z, doesn't have W) are the **most common hallucination shape**. They feel safe to write because no citation seems possible \u2014 but that's exactly why you must NOT write them on instinct.
|
|
586
|
+
|
|
587
|
+
If you are about to write "X is missing" or "Y is not implemented" \u2014 **STOP**. Call \`search_content\` for the relevant symbol or term FIRST. Only then:
|
|
588
|
+
|
|
589
|
+
- If the search returns matches \u2192 you were wrong; correct yourself and cite the matches.
|
|
590
|
+
- If the search returns nothing \u2192 state the absence with the search query as your evidence: \`No callers of \\\`foo()\\\` found (search_content "foo").\`
|
|
591
|
+
|
|
592
|
+
Asserting absence without a search is the #1 way evaluative answers go wrong. Treat the urge to write "missing" as a red flag in your own reasoning.
|
|
593
|
+
|
|
576
594
|
# When to propose a plan (submit_plan)
|
|
577
595
|
|
|
578
596
|
You have a \`submit_plan\` tool that shows the user a markdown plan and lets them Approve / Refine / Cancel before you execute. Use it proactively when the task is large enough to deserve a review gate:
|
|
@@ -707,4 +725,4 @@ export {
|
|
|
707
725
|
CODE_SYSTEM_PROMPT,
|
|
708
726
|
codeSystemPrompt
|
|
709
727
|
};
|
|
710
|
-
//# sourceMappingURL=chunk-
|
|
728
|
+
//# sourceMappingURL=chunk-ANMDY236.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/code/prompt.ts","../../src/user-memory.ts","../../src/project-memory.ts","../../src/skills.ts"],"sourcesContent":["/**\n * System prompt used by `reasonix code`. Teaches the model:\n *\n * 1. It has a filesystem MCP bridge rooted at the user's CWD.\n * 2. To modify files it emits SEARCH/REPLACE blocks (not\n * `write_file` — that would whole-file rewrite and kill diff\n * reviewability).\n * 3. Read first, edit second — SEARCH must match byte-for-byte.\n * 4. Be concise. The user can read a diff faster than prose.\n *\n * Kept short on purpose. Long system prompts eat context budget that\n * the Cache-First Loop is trying to conserve. The SEARCH/REPLACE spec\n * is the one unavoidable bloat; we trim everything else.\n */\n\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { applyMemoryStack } from \"../user-memory.js\";\n\nexport const CODE_SYSTEM_PROMPT = `You are Reasonix Code, a coding assistant. You have filesystem tools (read_file, write_file, list_directory, search_files, etc.) rooted at the user's working directory.\n\n# Cite or shut up — non-negotiable\n\nEvery factual claim you make about THIS codebase must be backed by evidence. Reasonix VALIDATES the citations you write — broken paths or out-of-range lines render in **red strikethrough with ❌** in front of the user.\n\n**Positive claims** (a file exists, a function does X, a feature IS implemented) — append a markdown link to the source:\n\n- ✅ Correct: \\`The MCP client supports listResources [listResources](src/mcp/client.ts:142).\\`\n- ❌ Wrong: \\`The MCP client supports listResources.\\` ← no citation, looks authoritative but unverifiable.\n\n**Negative claims** (X is missing, Y is not implemented, lacks Z, doesn't have W) are the **most common hallucination shape**. They feel safe to write because no citation seems possible — but that's exactly why you must NOT write them on instinct.\n\nIf you are about to write \"X is missing\" or \"Y is not implemented\" — **STOP**. Call \\`search_content\\` for the relevant symbol or term FIRST. Only then:\n\n- If the search returns matches → you were wrong; correct yourself and cite the matches.\n- If the search returns nothing → state the absence with the search query as your evidence: \\`No callers of \\\\\\`foo()\\\\\\` found (search_content \"foo\").\\`\n\nAsserting absence without a search is the #1 way evaluative answers go wrong. Treat the urge to write \"missing\" as a red flag in your own reasoning.\n\n# When to propose a plan (submit_plan)\n\nYou have a \\`submit_plan\\` tool that shows the user a markdown plan and lets them Approve / Refine / Cancel before you execute. Use it proactively when the task is large enough to deserve a review gate:\n\n- Multi-file refactors or renames.\n- Architecture changes (moving modules, splitting / merging files, new abstractions).\n- Anything where \"undo\" after the fact would be expensive — migrations, destructive cleanups, API shape changes.\n- When the user's request is ambiguous and multiple reasonable interpretations exist — propose your reading as a plan and let them confirm.\n\nSkip submit_plan for small, obvious changes: one-line typo, clear bug with a clear fix, adding a missing import, renaming a local variable. Just do those.\n\nPlan body: one-sentence summary, then a file-by-file breakdown of what you'll change and why, and any risks or open questions. If some decisions are genuinely up to the user (naming, tradeoffs, out-of-scope possibilities), list them in an \"Open questions\" section — the user sees the plan in a picker and has a text input to answer your questions before approving. Don't pretend certainty you don't have; flagged questions are how the user tells you what they care about. After calling submit_plan, STOP — don't call any more tools, wait for the user's verdict.\n\n# Plan mode (/plan)\n\nThe user can ALSO enter \"plan mode\" via /plan, which is a stronger, explicit constraint:\n- Write tools (edit_file, write_file, create_directory, move_file) and non-allowlisted run_command calls are BOUNCED at dispatch — you'll get a tool result like \"unavailable in plan mode\". Don't retry them.\n- Read tools (read_file, list_directory, search_files, directory_tree, get_file_info) and allowlisted read-only / test shell commands still work — use them to investigate.\n- You MUST call submit_plan before anything will execute. Approve exits plan mode; Refine stays in; Cancel exits without implementing.\n\n\n# Delegating to subagents via Skills (🧬)\n\nThe pinned Skills index below lists playbooks you can invoke with \\`run_skill\\`. Skills marked with **🧬** spawn an **isolated subagent** — a fresh child loop that runs the playbook in its own context and returns only the final answer. The subagent's tool calls and reasoning never enter your context, so 🧬 skills are how you keep the main session lean.\n\nTwo built-ins ship by default:\n- **🧬 explore** — read-only investigation across the codebase. Use when the user says things like \"find all places that...\", \"how does X work across the project\", \"survey the code for Y\". Pass \\`arguments\\` describing the concrete question.\n- **🧬 research** — combines web search + code reading. Use for \"is X supported by lib Y\", \"what's the canonical way to Z\", \"compare our impl to the spec\".\n\nWhen to delegate (call \\`run_skill\\` with a 🧬 skill):\n- The task would otherwise need >5 file reads or searches.\n- You only need the conclusion, not the exploration trail.\n- The work is self-contained (you can describe it in one paragraph).\n\nWhen NOT to delegate:\n- Direct, narrow questions answerable in 1-2 tool calls — just do them.\n- Anything where you need to track intermediate results yourself (planning, multi-step edits).\n- Anything that requires user interaction (subagents can't submit plans or ask you for clarification).\n\nAlways pass a clear, self-contained \\`arguments\\` — that text is the **only** context the subagent gets.\n\n# When to edit vs. when to explore\n\nOnly propose edits when the user explicitly asks you to change, fix, add, remove, refactor, or write something. Do NOT propose edits when the user asks you to:\n- analyze, read, explore, describe, or summarize a project\n- explain how something works\n- answer a question about the code\n\nIn those cases, use tools to gather what you need, then reply in prose. No SEARCH/REPLACE blocks, no file changes. If you're unsure what the user wants, ask.\n\nWhen you do propose edits, the user will review them and decide whether to \\`/apply\\` or \\`/discard\\`. Don't assume they'll accept — write as if each edit will be audited, because it will.\n\n# Editing files\n\nWhen you've been asked to change a file, output one or more SEARCH/REPLACE blocks in this exact format:\n\npath/to/file.ext\n<<<<<<< SEARCH\nexact existing lines from the file, including whitespace\n=======\nthe new lines\n>>>>>>> REPLACE\n\nRules:\n- Always read_file first so your SEARCH matches byte-for-byte. If it doesn't match, the edit is rejected and you'll have to retry with the exact current content.\n- One edit per block. Multiple blocks in one response are fine.\n- To create a new file, leave SEARCH empty:\n path/to/new.ts\n <<<<<<< SEARCH\n =======\n (whole file content here)\n >>>>>>> REPLACE\n- Do NOT use write_file to change existing files — the user reviews your edits as SEARCH/REPLACE. write_file is only for files you explicitly want to overwrite wholesale (rare).\n- Paths are relative to the working directory. Don't use absolute paths.\n\n# Trust what you already know\n\nBefore exploring the filesystem to answer a factual question, check whether the answer is already in context: the user's current message, earlier turns in this conversation (including prior tool results from \\`remember\\`), and the pinned memory blocks at the top of this prompt. When the user has stated a fact or you have remembered one, it outranks what the files say — don't re-derive from code what the user already told you. Explore when you genuinely don't know.\n\n# Exploration\n\n- Skip dependency, build, and VCS directories unless the user explicitly asks. The pinned .gitignore block (if any, below) is your authoritative denylist.\n- Prefer \\`search_files\\` over \\`list_directory\\` when you know roughly what you're looking for — it saves context and avoids enumerating huge trees. Note: \\`search_files\\` matches file NAMES; for searching file CONTENTS use \\`search_content\\`.\n- Available exploration tools: \\`read_file\\`, \\`list_directory\\`, \\`directory_tree\\`, \\`search_files\\` (filename match), \\`search_content\\` (content grep — use for \"where is X called\", \"find all references to Y\"), \\`get_file_info\\`. Don't call \\`grep\\` or other tools that aren't in this list — they don't exist as functions.\n\n# Path conventions\n\nTwo different rules depending on which tool:\n\n- **Filesystem tools** (\\`read_file\\`, \\`list_directory\\`, \\`search_files\\`, \\`edit_file\\`, etc.): paths are sandbox-relative. \\`/\\` means the project root, \\`/src/foo.ts\\` means \\`<project>/src/foo.ts\\`. Both relative (\\`src/foo.ts\\`) and POSIX-absolute (\\`/src/foo.ts\\`) forms work.\n- **\\`run_command\\`**: the command runs in a real OS shell with cwd pinned to the project root. Paths inside the shell command are interpreted by THAT shell, not by us. **Never use leading \\`/\\` in run_command arguments** — Windows treats \\`/tests\\` as drive-root \\`F:\\\\tests\\` (non-existent), POSIX shells treat it as filesystem root. Use plain relative paths (\\`tests\\`, \\`./tests\\`, \\`src/loop.ts\\`) instead.\n\n# Style\n\n- Show edits; don't narrate them in prose. \"Here's the fix:\" is enough.\n- One short paragraph explaining *why*, then the blocks.\n- If you need to explore first (list / read / search), do it with tool calls before writing any prose — silence while exploring is fine.\n`;\n\n/**\n * Inject the project's `.gitignore` content into the system prompt as a\n * \"respect this on top of the built-in denylist\" hint. We don't parse\n * the file — we hand it to the model as-is. Truncate long ones so we\n * don't eat context budget on huge generated ignore lists.\n *\n * Stacking order (stable for cache prefix):\n * base prompt → REASONIX.md → global MEMORY.md → project MEMORY.md → .gitignore\n */\nexport function codeSystemPrompt(rootDir: string): string {\n const withMemory = applyMemoryStack(CODE_SYSTEM_PROMPT, rootDir);\n const gitignorePath = join(rootDir, \".gitignore\");\n if (!existsSync(gitignorePath)) return withMemory;\n let content: string;\n try {\n content = readFileSync(gitignorePath, \"utf8\");\n } catch {\n return withMemory;\n }\n const MAX = 2000;\n const truncated =\n content.length > MAX\n ? `${content.slice(0, MAX)}\\n… (truncated ${content.length - MAX} chars)`\n : content;\n return `${withMemory}\n\n# Project .gitignore\n\nThe user's repo ships this .gitignore — treat every pattern as \"don't traverse or edit inside these paths unless explicitly asked\":\n\n\\`\\`\\`\n${truncated}\n\\`\\`\\`\n`;\n}\n","/**\n * User memory — `~/.reasonix/memory/` markdown notes pinned into the\n * immutable-prefix system prompt across sessions.\n *\n * Two scopes:\n * - `global` → `~/.reasonix/memory/global/` (cross-project)\n * - `project` → `~/.reasonix/memory/<hash>/` (per sandbox root)\n *\n * Each scope has an always-loaded `MEMORY.md` index plus zero-or-more\n * `<name>.md` detail files loaded on demand via `recall_memory`.\n *\n * Distinct from `src/project-memory.ts` (REASONIX.md) in purpose:\n * REASONIX.md is committable, team-shared project memory.\n * ~/.reasonix/memory is user-private memory, never committed.\n */\n\nimport { createHash } from \"node:crypto\";\nimport {\n existsSync,\n mkdirSync,\n readFileSync,\n readdirSync,\n unlinkSync,\n writeFileSync,\n} from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join, resolve } from \"node:path\";\nimport { applyProjectMemory, memoryEnabled } from \"./project-memory.js\";\nimport { applySkillsIndex } from \"./skills.js\";\n\nexport const USER_MEMORY_DIR = \"memory\";\nexport const MEMORY_INDEX_FILE = \"MEMORY.md\";\n/** Cap on the index file content loaded into the prefix, per scope. */\nexport const MEMORY_INDEX_MAX_CHARS = 4000;\n\nexport type MemoryType = \"user\" | \"feedback\" | \"project\" | \"reference\";\nexport type MemoryScope = \"global\" | \"project\";\n\nexport interface MemoryEntry {\n name: string;\n type: MemoryType;\n scope: MemoryScope;\n description: string;\n body: string;\n /** ISO date string (YYYY-MM-DD). */\n createdAt: string;\n}\n\nexport interface MemoryStoreOptions {\n /** Override `~/.reasonix` — tests set this to a tmpdir. */\n homeDir?: string;\n /** Absolute sandbox root. Required to use `scope: \"project\"`. */\n projectRoot?: string;\n}\n\nexport interface WriteInput {\n name: string;\n type: MemoryType;\n scope: MemoryScope;\n description: string;\n body: string;\n}\n\nconst VALID_NAME = /^[a-zA-Z0-9_-][a-zA-Z0-9_.-]{1,38}[a-zA-Z0-9]$/;\n\n/**\n * Throws on filename injection attempts (`../foo`, `foo/bar`, leading\n * dots, etc.). Allowed: 3-40 chars, alnum + `_` + `-` + interior `.`.\n */\nexport function sanitizeMemoryName(raw: string): string {\n const trimmed = String(raw ?? \"\").trim();\n if (!VALID_NAME.test(trimmed)) {\n throw new Error(\n `invalid memory name: ${JSON.stringify(raw)} — must be 3-40 chars, alnum/_/-, no path separators`,\n );\n }\n return trimmed;\n}\n\n/** Stable 16-hex-char hash of an absolute sandbox root path. */\nexport function projectHash(rootDir: string): string {\n const abs = resolve(rootDir);\n return createHash(\"sha1\").update(abs).digest(\"hex\").slice(0, 16);\n}\n\nfunction scopeDir(opts: { homeDir: string; scope: MemoryScope; projectRoot?: string }): string {\n if (opts.scope === \"global\") {\n return join(opts.homeDir, USER_MEMORY_DIR, \"global\");\n }\n if (!opts.projectRoot) {\n throw new Error(\"scope=project requires a projectRoot on MemoryStore\");\n }\n return join(opts.homeDir, USER_MEMORY_DIR, projectHash(opts.projectRoot));\n}\n\nfunction ensureDir(p: string): void {\n if (!existsSync(p)) mkdirSync(p, { recursive: true });\n}\n\n/**\n * Parse a `---` frontmatter block off the top of a markdown string.\n * Tolerates missing frontmatter, returning `{}` for data and the full\n * string as body. Only recognizes the simple `key: value` shape — no\n * quoting, no multi-line, no YAML features. Matches what we emit.\n */\nfunction parseFrontmatter(raw: string): { data: Record<string, string>; body: string } {\n const lines = raw.split(/\\r?\\n/);\n if (lines[0] !== \"---\") return { data: {}, body: raw };\n const end = lines.indexOf(\"---\", 1);\n if (end < 0) return { data: {}, body: raw };\n const data: Record<string, string> = {};\n for (let i = 1; i < end; i++) {\n const line = lines[i];\n if (!line) continue;\n const m = line.match(/^([a-zA-Z_][a-zA-Z0-9_-]*):\\s*(.*)$/);\n if (m?.[1]) data[m[1]] = (m[2] ?? \"\").trim();\n }\n return {\n data,\n body: lines\n .slice(end + 1)\n .join(\"\\n\")\n .replace(/^\\n+/, \"\"),\n };\n}\n\nfunction formatFrontmatter(e: WriteInput & { createdAt: string }): string {\n return [\n \"---\",\n `name: ${e.name}`,\n `description: ${e.description.replace(/\\n/g, \" \")}`,\n `type: ${e.type}`,\n `scope: ${e.scope}`,\n `created: ${e.createdAt}`,\n \"---\",\n \"\",\n ].join(\"\\n\");\n}\n\nfunction todayIso(): string {\n const d = new Date();\n return d.toISOString().slice(0, 10);\n}\n\n/**\n * A `MEMORY.md` index line for one entry. One-liner, under ~150 chars.\n * `description` is truncated if it would push past the soft limit.\n */\nfunction indexLine(e: Pick<MemoryEntry, \"name\" | \"description\">): string {\n const safeDesc = e.description.replace(/\\n/g, \" \").trim();\n const max = 130 - e.name.length;\n const clipped = safeDesc.length > max ? `${safeDesc.slice(0, Math.max(1, max - 1))}…` : safeDesc;\n return `- [${e.name}](${e.name}.md) — ${clipped}`;\n}\n\nexport class MemoryStore {\n private readonly homeDir: string;\n private readonly projectRoot: string | undefined;\n\n constructor(opts: MemoryStoreOptions = {}) {\n this.homeDir = opts.homeDir ?? join(homedir(), \".reasonix\");\n this.projectRoot = opts.projectRoot ? resolve(opts.projectRoot) : undefined;\n }\n\n /** Directory this store writes `scope` files into, creating it if needed. */\n dir(scope: MemoryScope): string {\n const d = scopeDir({ homeDir: this.homeDir, scope, projectRoot: this.projectRoot });\n ensureDir(d);\n return d;\n }\n\n /** Absolute path to a memory file (no existence check). */\n pathFor(scope: MemoryScope, name: string): string {\n return join(this.dir(scope), `${sanitizeMemoryName(name)}.md`);\n }\n\n /** True iff this store is configured with a project scope available. */\n hasProjectScope(): boolean {\n return this.projectRoot !== undefined;\n }\n\n /**\n * Read the `MEMORY.md` index for a scope. Returns post-cap content\n * (with a truncation marker if clipped), or `null` when absent / empty.\n */\n loadIndex(\n scope: MemoryScope,\n ): { content: string; originalChars: number; truncated: boolean } | null {\n if (scope === \"project\" && !this.projectRoot) return null;\n const file = join(\n scopeDir({ homeDir: this.homeDir, scope, projectRoot: this.projectRoot }),\n MEMORY_INDEX_FILE,\n );\n if (!existsSync(file)) return null;\n let raw: string;\n try {\n raw = readFileSync(file, \"utf8\");\n } catch {\n return null;\n }\n const trimmed = raw.trim();\n if (!trimmed) return null;\n const originalChars = trimmed.length;\n const truncated = originalChars > MEMORY_INDEX_MAX_CHARS;\n const content = truncated\n ? `${trimmed.slice(0, MEMORY_INDEX_MAX_CHARS)}\\n… (truncated ${originalChars - MEMORY_INDEX_MAX_CHARS} chars)`\n : trimmed;\n return { content, originalChars, truncated };\n }\n\n /** Read one memory file's body (frontmatter stripped). Throws if missing. */\n read(scope: MemoryScope, name: string): MemoryEntry {\n const file = this.pathFor(scope, name);\n if (!existsSync(file)) {\n throw new Error(`memory not found: scope=${scope} name=${name}`);\n }\n const raw = readFileSync(file, \"utf8\");\n const { data, body } = parseFrontmatter(raw);\n return {\n name: data.name ?? name,\n type: (data.type as MemoryType) ?? \"project\",\n scope: (data.scope as MemoryScope) ?? scope,\n description: data.description ?? \"\",\n body: body.trim(),\n createdAt: data.created ?? \"\",\n };\n }\n\n /**\n * List every memory in this store. Scans both scopes (skips project\n * scope if unconfigured). Silently skips malformed files; the index\n * must stay queryable even if one file is hand-edited into nonsense.\n */\n list(): MemoryEntry[] {\n const out: MemoryEntry[] = [];\n const scopes: MemoryScope[] = this.projectRoot ? [\"global\", \"project\"] : [\"global\"];\n for (const scope of scopes) {\n const dir = scopeDir({ homeDir: this.homeDir, scope, projectRoot: this.projectRoot });\n if (!existsSync(dir)) continue;\n let entries: string[];\n try {\n entries = readdirSync(dir);\n } catch {\n continue;\n }\n for (const entry of entries) {\n if (entry === MEMORY_INDEX_FILE) continue;\n if (!entry.endsWith(\".md\")) continue;\n const name = entry.slice(0, -3);\n try {\n out.push(this.read(scope, name));\n } catch {\n // malformed file — skip rather than fail the whole list\n }\n }\n }\n return out;\n }\n\n /**\n * Write a new memory (or overwrite existing). Creates the scope dir,\n * writes the `.md` file, and regenerates `MEMORY.md`. Returns the\n * absolute path written to.\n */\n write(input: WriteInput): string {\n if (input.scope === \"project\" && !this.projectRoot) {\n throw new Error(\"cannot write project-scoped memory: no projectRoot configured\");\n }\n const name = sanitizeMemoryName(input.name);\n const desc = String(input.description ?? \"\").trim();\n if (!desc) throw new Error(\"memory description cannot be empty\");\n const body = String(input.body ?? \"\").trim();\n if (!body) throw new Error(\"memory body cannot be empty\");\n const entry: WriteInput & { createdAt: string } = {\n ...input,\n name,\n description: desc,\n body,\n createdAt: todayIso(),\n };\n const dir = this.dir(input.scope);\n const file = join(dir, `${name}.md`);\n const content = `${formatFrontmatter(entry)}${body}\\n`;\n writeFileSync(file, content, \"utf8\");\n this.regenerateIndex(input.scope);\n return file;\n }\n\n /** Delete one memory + its index line. No-op if the file is already gone. */\n delete(scope: MemoryScope, rawName: string): boolean {\n if (scope === \"project\" && !this.projectRoot) {\n throw new Error(\"cannot delete project-scoped memory: no projectRoot configured\");\n }\n const file = this.pathFor(scope, rawName);\n if (!existsSync(file)) return false;\n unlinkSync(file);\n this.regenerateIndex(scope);\n return true;\n }\n\n /**\n * Rebuild `MEMORY.md` from the `.md` files currently in the scope dir.\n * Called after every write/delete. Sorted by name for stable prefix\n * hashing — two stores with the same set of files produce byte-identical\n * MEMORY.md content, keeping the cache prefix reproducible.\n */\n private regenerateIndex(scope: MemoryScope): void {\n const dir = scopeDir({ homeDir: this.homeDir, scope, projectRoot: this.projectRoot });\n if (!existsSync(dir)) return;\n let files: string[];\n try {\n files = readdirSync(dir);\n } catch {\n return;\n }\n const mdFiles = files\n .filter((f) => f !== MEMORY_INDEX_FILE && f.endsWith(\".md\"))\n .sort((a, b) => a.localeCompare(b));\n const indexPath = join(dir, MEMORY_INDEX_FILE);\n if (mdFiles.length === 0) {\n if (existsSync(indexPath)) unlinkSync(indexPath);\n return;\n }\n const lines: string[] = [];\n for (const f of mdFiles) {\n const name = f.slice(0, -3);\n try {\n const entry = this.read(scope, name);\n lines.push(indexLine({ name: entry.name || name, description: entry.description }));\n } catch {\n // Malformed: still surface it in the index so the user notices.\n lines.push(`- [${name}](${name}.md) — (malformed, check frontmatter)`);\n }\n }\n writeFileSync(indexPath, `${lines.join(\"\\n\")}\\n`, \"utf8\");\n }\n}\n\n/**\n * Append `MEMORY_GLOBAL` and (optionally) `MEMORY_PROJECT` blocks to\n * `basePrompt`. Omits a block entirely when its index is absent — an\n * empty tag would add bytes to the prefix hash without content.\n * Respects `REASONIX_MEMORY=off` via `memoryEnabled()` from\n * `project-memory.ts`.\n */\nexport function applyUserMemory(\n basePrompt: string,\n opts: { homeDir?: string; projectRoot?: string } = {},\n): string {\n if (!memoryEnabled()) return basePrompt;\n const store = new MemoryStore(opts);\n const global = store.loadIndex(\"global\");\n const project = store.hasProjectScope() ? store.loadIndex(\"project\") : null;\n if (!global && !project) return basePrompt;\n const parts: string[] = [basePrompt];\n if (global) {\n parts.push(\n \"\",\n \"# User memory — global (~/.reasonix/memory/global/MEMORY.md)\",\n \"\",\n \"Cross-project facts and preferences the user has told you in prior sessions. TREAT AS AUTHORITATIVE — don't re-verify via filesystem or web. One-liners index detail files; call `recall_memory` for full bodies only when the one-liner isn't enough.\",\n \"\",\n \"```\",\n global.content,\n \"```\",\n );\n }\n if (project) {\n parts.push(\n \"\",\n \"# User memory — this project\",\n \"\",\n \"Per-project facts the user established in prior sessions (not committed to the repo). TREAT AS AUTHORITATIVE. Same recall pattern as global memory.\",\n \"\",\n \"```\",\n project.content,\n \"```\",\n );\n }\n return parts.join(\"\\n\");\n}\n\n/**\n * Compose every lazy-loaded prefix block in one call: REASONIX.md,\n * user memory (global + project), and the skills index. Drop-in\n * replacement for `applyProjectMemory` at CLI entry points. Stacking\n * order is stable — the prefix hash only changes when block *content*\n * changes, not when this helper is called a second time with the same\n * filesystem state.\n */\nexport function applyMemoryStack(basePrompt: string, rootDir: string): string {\n const withProject = applyProjectMemory(basePrompt, rootDir);\n const withMemory = applyUserMemory(withProject, { projectRoot: rootDir });\n return applySkillsIndex(withMemory, { projectRoot: rootDir });\n}\n","/**\n * Project memory — a user-authored `REASONIX.md` in the project root\n * that gets pinned into the immutable-prefix system prompt.\n *\n * Design notes:\n *\n * - The file lands in `ImmutablePrefix.system`, so the whole memory\n * block is hashed into the cache prefix fingerprint. Editing the\n * file invalidates the prefix; unchanged memory across sessions\n * keeps the DeepSeek prefix cache warm. That matches Pillar 1 —\n * memory is a deliberate, stable prefix, not per-turn drift.\n * - Only one source: the working-root `REASONIX.md`. No parent walk,\n * no `~/.reasonix/REASONIX.md`, no CLAUDE.md fallback. User-global\n * memory can come later; for v1 one file == one mental model.\n * - Truncated at 8 000 chars (≈ 2k tokens). `.gitignore` gets 2 000\n * because it's a constraint dump; memory gets more headroom because\n * it's deliberate instructions.\n * - Opt-out via `REASONIX_MEMORY=off|false|0`. No CLI flag — memory\n * is a file, `rm REASONIX.md` is the other opt-out.\n */\n\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\nexport const PROJECT_MEMORY_FILE = \"REASONIX.md\";\nexport const PROJECT_MEMORY_MAX_CHARS = 8000;\n\nexport interface ProjectMemory {\n /** Absolute path the memory was read from. */\n path: string;\n /** Post-truncation content (may include a \"… (truncated N chars)\" marker). */\n content: string;\n /** Original byte length before truncation. */\n originalChars: number;\n /** True iff `originalChars > PROJECT_MEMORY_MAX_CHARS`. */\n truncated: boolean;\n}\n\n/**\n * Read `REASONIX.md` from `rootDir`. Returns `null` when the file is\n * missing, unreadable, or empty (whitespace-only counts as empty — an\n * empty memory file shouldn't perturb the cache prefix).\n */\nexport function readProjectMemory(rootDir: string): ProjectMemory | null {\n const path = join(rootDir, PROJECT_MEMORY_FILE);\n if (!existsSync(path)) return null;\n let raw: string;\n try {\n raw = readFileSync(path, \"utf8\");\n } catch {\n return null;\n }\n const trimmed = raw.trim();\n if (!trimmed) return null;\n const originalChars = trimmed.length;\n const truncated = originalChars > PROJECT_MEMORY_MAX_CHARS;\n const content = truncated\n ? `${trimmed.slice(0, PROJECT_MEMORY_MAX_CHARS)}\\n… (truncated ${\n originalChars - PROJECT_MEMORY_MAX_CHARS\n } chars)`\n : trimmed;\n return { path, content, originalChars, truncated };\n}\n\n/**\n * Resolve whether project memory should be read. Default: on.\n * `REASONIX_MEMORY=off|false|0` turns it off (CI, reproducing issues,\n * intentional offline runs).\n */\nexport function memoryEnabled(): boolean {\n const env = process.env.REASONIX_MEMORY;\n if (env === \"off\" || env === \"false\" || env === \"0\") return false;\n return true;\n}\n\n/**\n * Return `basePrompt` with the project's `REASONIX.md` appended as a\n * \"Project memory\" section. No-op when the file is absent, empty, or\n * memory is disabled via env.\n *\n * The appended block is deterministic — identical input ⇒ identical\n * output — so every session that opens against the same memory file\n * gets the same prefix hash.\n */\nexport function applyProjectMemory(basePrompt: string, rootDir: string): string {\n if (!memoryEnabled()) return basePrompt;\n const mem = readProjectMemory(rootDir);\n if (!mem) return basePrompt;\n return `${basePrompt}\n\n# Project memory (REASONIX.md)\n\nThe user pinned these notes about this project — treat them as authoritative context for every turn:\n\n\\`\\`\\`\n${mem.content}\n\\`\\`\\`\n`;\n}\n","/**\n * Skills — user-defined prompt packs pinned (by name) into the\n * immutable prefix and loaded (by body) on demand.\n *\n * Two scopes mirror the user-memory layout:\n * - `project` → `<projectRoot>/.reasonix/skills/` (this repo only)\n * - `global` → `~/.reasonix/skills/` (every session)\n *\n * Project scope wins on a name collision. Deliberately NOT tied to\n * any specific client's directory convention (`.claude/`, `.glm/`,\n * etc.) — Reasonix is model-agnostic at the conversation layer, so\n * coupling the skill filesystem to one vendor would break any user\n * running a different backend.\n *\n * Accepted file layouts (both emit the same `Skill`):\n * - `{dir}/<name>/SKILL.md` (preferred — lets a skill bundle\n * additional assets alongside)\n * - `{dir}/<name>.md` (flat, one-file shorthand)\n *\n * Frontmatter keys we read:\n * - `name` — optional, defaults to the file / dir name\n * - `description` — one-line index description (REQUIRED for listing)\n * - `allowed-tools` — parsed but UNUSED in v1 (see tools/skills.ts)\n *\n * Cache-First contract (Pillar 1):\n * - The PREFIX sees only names + descriptions (one line each).\n * - Bodies enter the APPEND-ONLY LOG lazily, via `run_skill` or\n * `/skill <name>` — never the prefix. That keeps the prefix hash\n * stable across skill additions to the body store.\n */\n\nimport { existsSync, readFileSync, readdirSync, statSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join, resolve } from \"node:path\";\n\nexport const SKILLS_DIRNAME = \"skills\";\nexport const SKILL_FILE = \"SKILL.md\";\n/** Cap on the pinned skills-index block, mirrors memory-index cap. */\nexport const SKILLS_INDEX_MAX_CHARS = 4000;\n/** Skill identifier shape — alnum + `_` + `-` + interior `.`, 1-64 chars. */\nconst VALID_SKILL_NAME = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/;\n\nexport type SkillScope = \"project\" | \"global\" | \"builtin\";\n\n/**\n * Execution mode for a skill. `inline` (default) returns the body as a\n * tool result so the body enters the parent's append-only log — the\n * model continues the loop using the loaded instructions. `subagent`\n * spawns an isolated child loop with the body as the system prompt and\n * the user-supplied `arguments` as the task; only the child's final\n * answer comes back. Use `subagent` for big-context exploration / research\n * playbooks where the parent doesn't need to see the trail.\n */\nexport type SkillRunAs = \"inline\" | \"subagent\";\n\nexport interface Skill {\n /** Canonical name — sanitized, matches the directory / filename stem. */\n name: string;\n /** One-line description shown in the pinned index. */\n description: string;\n /** Full markdown body (post-frontmatter). Loaded on demand. */\n body: string;\n /** Which scope this skill was loaded from. */\n scope: SkillScope;\n /** Absolute path to the SKILL.md (or {name}.md) file, or \"(builtin)\" for shipped defaults. */\n path: string;\n /** Raw `allowed-tools` field from frontmatter, if any. Unused in v1. */\n allowedTools?: string;\n /**\n * Execution mode (frontmatter `runAs`). Defaults to `inline` for\n * backwards compatibility with skills written before this field\n * existed.\n */\n runAs: SkillRunAs;\n /**\n * Frontmatter `model` — when set, overrides the default model the\n * subagent runs on. Only meaningful when `runAs === \"subagent\"`.\n * Accept any DeepSeek model id; the subagent layer falls back to its\n * own default if this is missing or invalid.\n */\n model?: string;\n}\n\nexport interface SkillStoreOptions {\n /** Override `$HOME` — tests point this at a tmpdir. */\n homeDir?: string;\n /**\n * Absolute project root. Required to surface project-scope skills;\n * omit (e.g. in `reasonix chat` without `code`) and the store only\n * reads the global scope.\n */\n projectRoot?: string;\n /**\n * Suppress the bundled built-in skills (`explore`, `research`).\n * Used by unit tests that want to assert exact list contents\n * without the +2 builtins distorting counts. Production callers\n * leave this off so users always get the bundled defaults.\n */\n disableBuiltins?: boolean;\n}\n\n/**\n * Parse a `---` frontmatter block. Same minimal shape as user-memory:\n * `key: value` lines, no quoting, no nesting. Returns `{}` data and the\n * full input as body when no frontmatter fence is present — so hand-\n * written files without frontmatter still surface (with empty desc).\n */\nfunction parseFrontmatter(raw: string): { data: Record<string, string>; body: string } {\n const lines = raw.split(/\\r?\\n/);\n if (lines[0] !== \"---\") return { data: {}, body: raw };\n const end = lines.indexOf(\"---\", 1);\n if (end < 0) return { data: {}, body: raw };\n const data: Record<string, string> = {};\n for (let i = 1; i < end; i++) {\n const line = lines[i];\n if (!line) continue;\n const m = line.match(/^([a-zA-Z_][a-zA-Z0-9_-]*):\\s*(.*)$/);\n if (m?.[1]) data[m[1]] = (m[2] ?? \"\").trim();\n }\n return {\n data,\n body: lines\n .slice(end + 1)\n .join(\"\\n\")\n .replace(/^\\n+/, \"\"),\n };\n}\n\nfunction isValidSkillName(name: string): boolean {\n return VALID_SKILL_NAME.test(name);\n}\n\nexport class SkillStore {\n private readonly homeDir: string;\n private readonly projectRoot: string | undefined;\n private readonly disableBuiltins: boolean;\n\n constructor(opts: SkillStoreOptions = {}) {\n this.homeDir = opts.homeDir ?? homedir();\n this.projectRoot = opts.projectRoot ? resolve(opts.projectRoot) : undefined;\n this.disableBuiltins = opts.disableBuiltins === true;\n }\n\n /** True iff this store was configured with a project root. */\n hasProjectScope(): boolean {\n return this.projectRoot !== undefined;\n }\n\n /**\n * Root directories scanned, in priority order. Project scope first\n * so a per-repo skill overrides a global one with the same name —\n * users expect the local copy to win when both exist.\n */\n roots(): Array<{ dir: string; scope: SkillScope }> {\n const out: Array<{ dir: string; scope: SkillScope }> = [];\n if (this.projectRoot) {\n out.push({\n dir: join(this.projectRoot, \".reasonix\", SKILLS_DIRNAME),\n scope: \"project\",\n });\n }\n out.push({ dir: join(this.homeDir, \".reasonix\", SKILLS_DIRNAME), scope: \"global\" });\n return out;\n }\n\n /**\n * List every skill visible to this store. On name collisions the\n * higher-priority root (project over global over builtin) wins.\n * Sorted by name for stable prefix hashing.\n */\n list(): Skill[] {\n const byName = new Map<string, Skill>();\n for (const { dir, scope } of this.roots()) {\n if (!existsSync(dir)) continue;\n let entries: import(\"node:fs\").Dirent[];\n try {\n entries = readdirSync(dir, { withFileTypes: true });\n } catch {\n continue;\n }\n for (const entry of entries) {\n const skill = this.readEntry(dir, scope, entry);\n if (!skill) continue;\n if (!byName.has(skill.name)) byName.set(skill.name, skill);\n }\n }\n // Builtins are appended last so user/project files take precedence\n // when names collide. The same priority you'd expect: my-project's\n // \"explore\" overrides the shipped one without forcing a different\n // name.\n if (!this.disableBuiltins) {\n for (const skill of BUILTIN_SKILLS) {\n if (!byName.has(skill.name)) byName.set(skill.name, skill);\n }\n }\n return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));\n }\n\n /** Resolve one skill by name. Returns `null` if not found or malformed. */\n read(name: string): Skill | null {\n if (!isValidSkillName(name)) return null;\n for (const { dir, scope } of this.roots()) {\n if (!existsSync(dir)) continue;\n const dirCandidate = join(dir, name, SKILL_FILE);\n if (existsSync(dirCandidate) && statSync(dirCandidate).isFile()) {\n return this.parse(dirCandidate, name, scope);\n }\n const flatCandidate = join(dir, `${name}.md`);\n if (existsSync(flatCandidate) && statSync(flatCandidate).isFile()) {\n return this.parse(flatCandidate, name, scope);\n }\n }\n // Fall back to builtins. Same precedence as `list()` — user-authored\n // wins, builtins are the floor.\n if (!this.disableBuiltins) {\n for (const skill of BUILTIN_SKILLS) {\n if (skill.name === name) return skill;\n }\n }\n return null;\n }\n\n private readEntry(dir: string, scope: SkillScope, entry: import(\"node:fs\").Dirent): Skill | null {\n if (entry.isDirectory()) {\n if (!isValidSkillName(entry.name)) return null;\n const file = join(dir, entry.name, SKILL_FILE);\n if (!existsSync(file)) return null;\n return this.parse(file, entry.name, scope);\n }\n if (entry.isFile() && entry.name.endsWith(\".md\")) {\n const stem = entry.name.slice(0, -3);\n if (!isValidSkillName(stem)) return null;\n return this.parse(join(dir, entry.name), stem, scope);\n }\n return null;\n }\n\n private parse(path: string, stem: string, scope: SkillScope): Skill | null {\n let raw: string;\n try {\n raw = readFileSync(path, \"utf8\");\n } catch {\n return null;\n }\n const { data, body } = parseFrontmatter(raw);\n const name = data.name && isValidSkillName(data.name) ? data.name : stem;\n return {\n name,\n description: (data.description ?? \"\").trim(),\n body: body.trim(),\n scope,\n path,\n allowedTools: data[\"allowed-tools\"],\n runAs: parseRunAs(data.runAs),\n model: data.model?.startsWith(\"deepseek-\") ? data.model : undefined,\n };\n }\n}\n\n/**\n * Coerce a frontmatter `runAs` string to the discriminated union. Any\n * value other than the literal \"subagent\" is treated as inline — typos\n * and unknown values default to the safe (non-spawning) mode rather\n * than failing the load.\n */\nfunction parseRunAs(raw: string | undefined): SkillRunAs {\n return raw?.trim() === \"subagent\" ? \"subagent\" : \"inline\";\n}\n\n/**\n * Build a single index line for one skill. Shape mirrors memory's\n * `indexLine` — a bullet suitable for a markdown fenced block in the\n * system prompt. Description is truncated to keep the full line under\n * ~150 chars. Subagent-runAs skills carry a 🧬 marker so the model\n * sees at a glance which skills will spawn an isolated child loop.\n */\nfunction skillIndexLine(s: Pick<Skill, \"name\" | \"description\" | \"runAs\">): string {\n const safeDesc = s.description.replace(/\\n/g, \" \").trim();\n const marker = s.runAs === \"subagent\" ? \"🧬 \" : \"\";\n const max = 130 - s.name.length - marker.length;\n const clipped = safeDesc.length > max ? `${safeDesc.slice(0, Math.max(1, max - 1))}…` : safeDesc;\n return clipped ? `- ${marker}${s.name} — ${clipped}` : `- ${marker}${s.name}`;\n}\n\n/**\n * Append a `# Skills` block to `basePrompt` listing every discovered\n * skill (name + description only). Bodies are NOT inlined — that's the\n * whole point: the prefix stays short and cacheable; full content loads\n * on demand via `run_skill` or `/skill <name>`.\n *\n * Emits nothing when no skills are discovered — keeps the prefix hash\n * stable for users who don't use skills at all.\n */\nexport function applySkillsIndex(basePrompt: string, opts: SkillStoreOptions = {}): string {\n const store = new SkillStore(opts);\n const skills = store.list().filter((s) => s.description);\n if (skills.length === 0) return basePrompt;\n const lines = skills.map(skillIndexLine);\n const joined = lines.join(\"\\n\");\n const truncated =\n joined.length > SKILLS_INDEX_MAX_CHARS\n ? `${joined.slice(0, SKILLS_INDEX_MAX_CHARS)}\\n… (truncated ${\n joined.length - SKILLS_INDEX_MAX_CHARS\n } chars)`\n : joined;\n return [\n basePrompt,\n \"\",\n \"# Skills — playbooks you can invoke\",\n \"\",\n 'One-liner index. Each entry is either a built-in or a user-authored playbook. Call `run_skill({ name: \"<skill-name>\", arguments: \"<task>\" })` to invoke one. Skills marked with 🧬 spawn an **isolated subagent** — its tool calls and reasoning never enter your context, only its final answer does. Use 🧬 skills for tasks that would otherwise flood your context (deep exploration, multi-step research, anything where you only need the conclusion). Plain skills are inlined: their body becomes a tool result you read and act on directly. The user can also invoke a skill via `/skill <name>`.',\n \"\",\n \"```\",\n truncated,\n \"```\",\n ].join(\"\\n\");\n}\n\n/**\n * Built-in skills shipped with Reasonix. These are always available\n * (no install step) and live as constants rather than files because:\n * - Zero filesystem coupling — no copy-on-first-run dance, nothing\n * to migrate when we update them.\n * - They participate in the same `byName` priority as user/project\n * skills: write `~/.reasonix/skills/explore.md` to override.\n *\n * Keep this list small and high-leverage. The bar for adding one: it\n * demonstrates a pattern users would otherwise have to invent\n * themselves, and the body fits in a screen.\n */\nconst BUILTIN_EXPLORE_BODY = `You are running as an exploration subagent. Your job is to investigate the codebase the parent agent pointed you at, then return one focused, distilled answer.\n\nHow to operate:\n- Use read_file, search_files, search_content, directory_tree, list_directory, get_file_info as your primary tools. Stay read-only.\n- For \"find all places that call / reference / use X\" questions, use \\`search_content\\` (content grep) — NOT \\`search_files\\` (which only matches file names). This is the most common subagent mistake; using the wrong tool gives empty results and you waste your iter budget chasing a phantom.\n- Cast a wide net first (search_content for symbol references, directory_tree for structure) to map the territory; then read the 3-10 most relevant files in full.\n- Don't read every file — be selective. Aim for breadth on the first pass, depth only where the question demands it.\n- Stop exploring as soon as you can answer the question. The parent doesn't see your tool calls, so over-exploration is pure waste.\n\nYour final answer:\n- One paragraph (or a few short bullets). Lead with the conclusion.\n- Cite specific file paths + line ranges when they support the answer.\n- If the question can't be answered from what you found, say so plainly and suggest where to look next.\n- No follow-up offers, no \"let me know if you need more.\" The parent will ask again if they need more.\n\nFormatting (rendered in a TUI):\n- Tabular data → GitHub-Flavored Markdown tables with ASCII pipes (\\`| col | col |\\` + \\`| --- | --- |\\`). Never use Unicode box-drawing characters (│ ─ ┼) — they break word-wrap.\n- Keep table cells short; if a cell needs a paragraph, use bullets below the table instead.\n- Code, file paths with line ranges, and shell commands → fenced code blocks (\\`\\`\\`).\n- NEVER draw decorative frames around code or text with \\`┌──┐ │ └──┘\\` box-drawing characters. Use plain code blocks; the renderer adds its own border.\n- For flow charts: use a bullet list with \\`→\\` or \\`↓\\` between steps, not ASCII boxes-and-arrows.\n\nThe 'task' the parent gave you is the question you must answer. Treat any other reading of it as scope creep.`;\n\nconst BUILTIN_RESEARCH_BODY = `You are running as a research subagent. Your job is to gather information from code AND the web, synthesize it, and return one focused conclusion.\n\nHow to operate:\n- Combine code reading (read_file, search_files) with web tools (web_search, web_fetch) as appropriate to the question.\n- For \"how does X work\" / \"is Y supported\" questions: web first to find the canonical reference, then verify against the local code.\n- For \"what's our policy on Z\" / \"where do we use Q\": local code first, web only if you need to compare against external standards.\n- Cap yourself at ~10 tool calls. If you can't converge in 10, return what you have plus a note about what's missing.\n\nYour final answer:\n- One paragraph (or short bullets). Lead with the conclusion.\n- Cite both code (file:line) AND web sources (URL) when they back the answer.\n- Distinguish \"I verified this in code\" from \"I read this on a docs page\" — the parent will trust the former more.\n- If the answer is uncertain, say so. Don't invent confidence.\n\nFormatting (rendered in a TUI):\n- Tabular data → GitHub-Flavored Markdown tables with ASCII pipes (\\`| col | col |\\` + \\`| --- | --- |\\`). Never use Unicode box-drawing characters (│ ─ ┼) — they break word-wrap.\n- Keep table cells short; if a cell needs a paragraph, use bullets below the table instead.\n- Code, file paths with line ranges, and shell commands → fenced code blocks (\\`\\`\\`).\n- NEVER draw decorative frames around code or text with \\`┌──┐ │ └──┘\\` box-drawing characters. Use plain code blocks; the renderer adds its own border.\n- For flow charts: use a bullet list with \\`→\\` or \\`↓\\` between steps, not ASCII boxes-and-arrows.\n\nThe 'task' the parent gave you is the research question. Stay on it.`;\n\nconst BUILTIN_SKILLS: readonly Skill[] = Object.freeze([\n Object.freeze<Skill>({\n name: \"explore\",\n description:\n \"Explore the codebase in an isolated subagent — wide-net read-only investigation that returns one distilled answer. Best for: 'find all places that...', 'how does X work across the project', 'survey the code for Y'.\",\n body: BUILTIN_EXPLORE_BODY,\n scope: \"builtin\",\n path: \"(builtin)\",\n runAs: \"subagent\",\n }),\n Object.freeze<Skill>({\n name: \"research\",\n description:\n \"Research a question by combining web search + code reading in an isolated subagent. Best for: 'is X feature supported by lib Y', 'what's the canonical way to do Z', 'compare our impl against the spec'.\",\n body: BUILTIN_RESEARCH_BODY,\n scope: \"builtin\",\n path: \"(builtin)\",\n runAs: \"subagent\",\n }),\n]);\n"],"mappings":"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","names":["existsSync","readFileSync","join","existsSync","readFileSync","readdirSync","homedir","join","resolve","existsSync","readFileSync","join","resolve","join","existsSync","parseFrontmatter","homedir","readFileSync","readdirSync","join","existsSync","readFileSync"]}
|
package/dist/cli/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
memoryEnabled,
|
|
8
8
|
readProjectMemory,
|
|
9
9
|
sanitizeMemoryName
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-ANMDY236.js";
|
|
11
11
|
|
|
12
12
|
// src/cli/index.ts
|
|
13
13
|
import { Command } from "commander";
|
|
@@ -5039,7 +5039,7 @@ function formatLogSize(path = defaultUsageLogPath()) {
|
|
|
5039
5039
|
}
|
|
5040
5040
|
|
|
5041
5041
|
// src/cli/commands/chat.tsx
|
|
5042
|
-
import { existsSync as existsSync8, statSync as
|
|
5042
|
+
import { existsSync as existsSync8, statSync as statSync5 } from "fs";
|
|
5043
5043
|
import { render } from "ink";
|
|
5044
5044
|
import React15, { useState as useState7 } from "react";
|
|
5045
5045
|
|
|
@@ -5137,6 +5137,8 @@ function PlanStateBlock({ planState }) {
|
|
|
5137
5137
|
}
|
|
5138
5138
|
|
|
5139
5139
|
// src/cli/ui/markdown.tsx
|
|
5140
|
+
import { readFileSync as readFileSync9, statSync as statSync4 } from "fs";
|
|
5141
|
+
import { isAbsolute as isAbsolute3, join as join7 } from "path";
|
|
5140
5142
|
import { Box as Box2, Text as Text2 } from "ink";
|
|
5141
5143
|
import React2 from "react";
|
|
5142
5144
|
var SUPERSCRIPT = {
|
|
@@ -5187,8 +5189,75 @@ function stripMath(s) {
|
|
|
5187
5189
|
(_m, n, k) => `C(${n.trim()},${k.trim()})`
|
|
5188
5190
|
).replace(/\\sqrt\s*\{([^{}]+)\}/g, (_m, g) => `\u221A(${g.trim()})`).replace(/\\boxed\s*\{([^{}]+)\}/g, (_m, g) => `\u3010${g.trim()}\u3011`).replace(/\\text\s*\{([^{}]+)\}/g, (_m, g) => g.trim()).replace(/\\overline\s*\{([^{}]+)\}/g, (_m, g) => `${g.trim()}\u0304`).replace(/\\hat\s*\{([^{}]+)\}/g, (_m, g) => `${g.trim()}\u0302`).replace(/\\vec\s*\{([^{}]+)\}/g, (_m, g) => `\u2192${g.trim()}`).replace(/\\cdot/g, "\xB7").replace(/\\times/g, "\xD7").replace(/\\div/g, "\xF7").replace(/\\pm/g, "\xB1").replace(/\\mp/g, "\u2213").replace(/\\leq/g, "\u2264").replace(/\\geq/g, "\u2265").replace(/\\neq/g, "\u2260").replace(/\\approx/g, "\u2248").replace(/\\in\b/g, "\u2208").replace(/\\notin\b/g, "\u2209").replace(/\\infty/g, "\u221E").replace(/\\sum\b/g, "\u03A3").replace(/\\prod\b/g, "\u03A0").replace(/\\int\b/g, "\u222B").replace(/\\alpha/g, "\u03B1").replace(/\\beta/g, "\u03B2").replace(/\\gamma/g, "\u03B3").replace(/\\delta/g, "\u03B4").replace(/\\theta/g, "\u03B8").replace(/\\lambda/g, "\u03BB").replace(/\\mu/g, "\u03BC").replace(/\\pi/g, "\u03C0").replace(/\\sigma/g, "\u03C3").replace(/\\phi/g, "\u03C6").replace(/\\omega/g, "\u03C9").replace(/\\implies\b/g, "\u21D2").replace(/\\iff\b/g, "\u21D4").replace(/\\to\b/g, "\u2192").replace(/\\rightarrow/g, "\u2192").replace(/\\Rightarrow/g, "\u21D2").replace(/\\leftarrow/g, "\u2190").replace(/\\Leftarrow/g, "\u21D0").replace(/\\ldots/g, "\u2026").replace(/\\cdots/g, "\u22EF").replace(/\\quad/g, " ").replace(/\\qquad/g, " ").replace(/\\,/g, " ").replace(/\\;/g, " ").replace(/\\!/g, "").replace(/\\\\/g, "\n").replace(/\^\{([\w+-]+)\}/g, (_m, g) => toSuperscript(g)).replace(/\^([0-9+\-n])/g, (_m, g) => toSuperscript(g)).replace(/_\{([\w+-]+)\}/g, (_m, g) => toSubscript(g)).replace(/_([0-9+\-])/g, (_m, g) => toSubscript(g)).replace(/\\[a-zA-Z]+\s*\{([^{}]+)\}\s*\{([^{}]+)\}/g, "($1)/($2)").replace(/\\[a-zA-Z]+\s*\{([^{}]+)\}/g, "$1").replace(/\\[a-zA-Z]+/g, "").replace(/[ \t]{2,}/g, " ");
|
|
5189
5191
|
}
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
+
function isExternalUrl(url) {
|
|
5193
|
+
return /^[a-z][a-z0-9+.-]*:\/\//i.test(url) || url.startsWith("mailto:") || url.startsWith("//");
|
|
5194
|
+
}
|
|
5195
|
+
function parseCitationUrl(url) {
|
|
5196
|
+
const trimmed = url.trim();
|
|
5197
|
+
if (!trimmed) return null;
|
|
5198
|
+
let m = trimmed.match(/^(.+?)#L(\d+)(?:-L?(\d+))?$/);
|
|
5199
|
+
if (m) {
|
|
5200
|
+
return {
|
|
5201
|
+
path: m[1] ?? "",
|
|
5202
|
+
startLine: Number(m[2]),
|
|
5203
|
+
endLine: m[3] ? Number(m[3]) : void 0
|
|
5204
|
+
};
|
|
5205
|
+
}
|
|
5206
|
+
m = trimmed.match(/^(.+?):(\d+)(?:-(\d+))?$/);
|
|
5207
|
+
if (m) {
|
|
5208
|
+
return {
|
|
5209
|
+
path: m[1] ?? "",
|
|
5210
|
+
startLine: Number(m[2]),
|
|
5211
|
+
endLine: m[3] ? Number(m[3]) : void 0
|
|
5212
|
+
};
|
|
5213
|
+
}
|
|
5214
|
+
return { path: trimmed };
|
|
5215
|
+
}
|
|
5216
|
+
function validateCitation(url, projectRoot) {
|
|
5217
|
+
const parts = parseCitationUrl(url);
|
|
5218
|
+
if (!parts || !parts.path) return { ok: false, reason: "empty path" };
|
|
5219
|
+
const fullPath = isAbsolute3(parts.path) ? parts.path : join7(projectRoot, parts.path);
|
|
5220
|
+
let stat;
|
|
5221
|
+
try {
|
|
5222
|
+
stat = statSync4(fullPath);
|
|
5223
|
+
} catch {
|
|
5224
|
+
return { ok: false, reason: "file not found" };
|
|
5225
|
+
}
|
|
5226
|
+
if (!stat.isFile()) return { ok: false, reason: "not a file" };
|
|
5227
|
+
if (parts.startLine === void 0) return { ok: true };
|
|
5228
|
+
let lineCount;
|
|
5229
|
+
try {
|
|
5230
|
+
lineCount = readFileSync9(fullPath, "utf8").split("\n").length;
|
|
5231
|
+
} catch {
|
|
5232
|
+
return { ok: false, reason: "unreadable" };
|
|
5233
|
+
}
|
|
5234
|
+
if (parts.startLine < 1 || parts.startLine > lineCount) {
|
|
5235
|
+
return { ok: false, reason: `line ${parts.startLine} > ${lineCount}` };
|
|
5236
|
+
}
|
|
5237
|
+
if (parts.endLine !== void 0) {
|
|
5238
|
+
if (parts.endLine < parts.startLine || parts.endLine > lineCount) {
|
|
5239
|
+
return { ok: false, reason: `range end ${parts.endLine} invalid` };
|
|
5240
|
+
}
|
|
5241
|
+
}
|
|
5242
|
+
return { ok: true };
|
|
5243
|
+
}
|
|
5244
|
+
function collectCitations(text, projectRoot) {
|
|
5245
|
+
const map = /* @__PURE__ */ new Map();
|
|
5246
|
+
const re = /\[([^\]\n]+)\]\(([^)\n]+)\)/g;
|
|
5247
|
+
for (const m of text.matchAll(re)) {
|
|
5248
|
+
const url = m[2] ?? "";
|
|
5249
|
+
if (!url || isExternalUrl(url)) continue;
|
|
5250
|
+
if (map.has(url)) continue;
|
|
5251
|
+
map.set(url, validateCitation(url, projectRoot));
|
|
5252
|
+
}
|
|
5253
|
+
return map;
|
|
5254
|
+
}
|
|
5255
|
+
var INLINE_RE = /(\[([^\]\n]+)\]\(([^)\n]+)\)|\*\*([^*\n]+?)\*\*|```([^\n]+?)```|`([^`\n]+?)`|(?<![*\w])\*([^*\n]+?)\*(?!\w))/g;
|
|
5256
|
+
function InlineMd({
|
|
5257
|
+
text,
|
|
5258
|
+
padTo,
|
|
5259
|
+
citations
|
|
5260
|
+
}) {
|
|
5192
5261
|
const parts = [];
|
|
5193
5262
|
let last = 0;
|
|
5194
5263
|
let idx = 0;
|
|
@@ -5197,22 +5266,41 @@ function InlineMd({ text, padTo }) {
|
|
|
5197
5266
|
if (start > last) {
|
|
5198
5267
|
parts.push(/* @__PURE__ */ React2.createElement(Text2, { key: `t${idx++}` }, text.slice(last, start)));
|
|
5199
5268
|
}
|
|
5200
|
-
if (m[2] !== void 0) {
|
|
5269
|
+
if (m[2] !== void 0 && m[3] !== void 0) {
|
|
5270
|
+
const linkText = m[2];
|
|
5271
|
+
const url = m[3];
|
|
5272
|
+
if (isExternalUrl(url)) {
|
|
5273
|
+
parts.push(
|
|
5274
|
+
/* @__PURE__ */ React2.createElement(Text2, { key: `l${idx++}`, color: "blue", underline: true }, linkText)
|
|
5275
|
+
);
|
|
5276
|
+
} else {
|
|
5277
|
+
const status = citations?.get(url);
|
|
5278
|
+
if (status && !status.ok) {
|
|
5279
|
+
parts.push(
|
|
5280
|
+
/* @__PURE__ */ React2.createElement(Text2, { key: `l${idx++}`, color: "red", strikethrough: true }, `${linkText} \u274C`)
|
|
5281
|
+
);
|
|
5282
|
+
} else {
|
|
5283
|
+
parts.push(
|
|
5284
|
+
/* @__PURE__ */ React2.createElement(Text2, { key: `l${idx++}`, color: "cyan", underline: true }, linkText)
|
|
5285
|
+
);
|
|
5286
|
+
}
|
|
5287
|
+
}
|
|
5288
|
+
} else if (m[4] !== void 0) {
|
|
5201
5289
|
parts.push(
|
|
5202
|
-
/* @__PURE__ */ React2.createElement(Text2, { key: `b${idx++}`, bold: true }, m[
|
|
5290
|
+
/* @__PURE__ */ React2.createElement(Text2, { key: `b${idx++}`, bold: true }, m[4])
|
|
5203
5291
|
);
|
|
5204
|
-
} else if (m[
|
|
5205
|
-
const stripped = m[
|
|
5292
|
+
} else if (m[5] !== void 0) {
|
|
5293
|
+
const stripped = m[5].replace(/^(\w+)\s+/, "");
|
|
5206
5294
|
parts.push(
|
|
5207
5295
|
/* @__PURE__ */ React2.createElement(Text2, { key: `c${idx++}`, color: "yellow" }, stripped)
|
|
5208
5296
|
);
|
|
5209
|
-
} else if (m[
|
|
5297
|
+
} else if (m[6] !== void 0) {
|
|
5210
5298
|
parts.push(
|
|
5211
|
-
/* @__PURE__ */ React2.createElement(Text2, { key: `c${idx++}`, color: "yellow" }, m[
|
|
5299
|
+
/* @__PURE__ */ React2.createElement(Text2, { key: `c${idx++}`, color: "yellow" }, m[6])
|
|
5212
5300
|
);
|
|
5213
|
-
} else if (m[
|
|
5301
|
+
} else if (m[7] !== void 0) {
|
|
5214
5302
|
parts.push(
|
|
5215
|
-
/* @__PURE__ */ React2.createElement(Text2, { key: `i${idx++}`, italic: true }, m[
|
|
5303
|
+
/* @__PURE__ */ React2.createElement(Text2, { key: `i${idx++}`, italic: true }, m[7])
|
|
5216
5304
|
);
|
|
5217
5305
|
}
|
|
5218
5306
|
last = start + m[0].length;
|
|
@@ -5229,7 +5317,7 @@ function InlineMd({ text, padTo }) {
|
|
|
5229
5317
|
return /* @__PURE__ */ React2.createElement(Text2, null, parts);
|
|
5230
5318
|
}
|
|
5231
5319
|
function stripInlineMarkup(s) {
|
|
5232
|
-
return s.replace(/\*\*([^*\n]+?)\*\*/g, "$1").replace(/```([^\n]+?)```/g, (_m, c) => c.replace(/^(\w+)\s+/, "")).replace(/`([^`\n]+?)`/g, "$1").replace(/(?<![*\w])\*([^*\n]+?)\*(?!\w)/g, "$1");
|
|
5320
|
+
return s.replace(/\[([^\]\n]+)\]\(([^)\n]+)\)/g, "$1").replace(/\*\*([^*\n]+?)\*\*/g, "$1").replace(/```([^\n]+?)```/g, (_m, c) => c.replace(/^(\w+)\s+/, "")).replace(/`([^`\n]+?)`/g, "$1").replace(/(?<![*\w])\*([^*\n]+?)\*(?!\w)/g, "$1");
|
|
5233
5321
|
}
|
|
5234
5322
|
function visibleWidth(s) {
|
|
5235
5323
|
return displayWidth(stripInlineMarkup(s));
|
|
@@ -5419,20 +5507,20 @@ function parseBlocks(raw) {
|
|
|
5419
5507
|
flushList();
|
|
5420
5508
|
return out;
|
|
5421
5509
|
}
|
|
5422
|
-
function BlockView({ block }) {
|
|
5510
|
+
function BlockView({ block, citations }) {
|
|
5423
5511
|
switch (block.kind) {
|
|
5424
5512
|
case "heading":
|
|
5425
|
-
return /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "cyan" }, /* @__PURE__ */ React2.createElement(InlineMd, { text: block.text }));
|
|
5513
|
+
return /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "cyan" }, /* @__PURE__ */ React2.createElement(InlineMd, { text: block.text, citations }));
|
|
5426
5514
|
case "paragraph":
|
|
5427
|
-
return /* @__PURE__ */ React2.createElement(InlineMd, { text: block.text });
|
|
5515
|
+
return /* @__PURE__ */ React2.createElement(InlineMd, { text: block.text, citations });
|
|
5428
5516
|
case "bullet":
|
|
5429
|
-
return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column" }, block.items.map((item, i) => /* @__PURE__ */ React2.createElement(Box2, { key: `${i}-${item.slice(0, 24)}` }, /* @__PURE__ */ React2.createElement(Text2, { color: "cyan" }, block.ordered ? ` ${block.start + i}. ` : " \u2022 "), /* @__PURE__ */ React2.createElement(InlineMd, { text: item }))));
|
|
5517
|
+
return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column" }, block.items.map((item, i) => /* @__PURE__ */ React2.createElement(Box2, { key: `${i}-${item.slice(0, 24)}` }, /* @__PURE__ */ React2.createElement(Text2, { color: "cyan" }, block.ordered ? ` ${block.start + i}. ` : " \u2022 "), /* @__PURE__ */ React2.createElement(InlineMd, { text: item, citations }))));
|
|
5430
5518
|
case "code":
|
|
5431
5519
|
return /* @__PURE__ */ React2.createElement(Box2, { borderStyle: "single", borderColor: "gray", paddingX: 1 }, /* @__PURE__ */ React2.createElement(Text2, { color: "yellow" }, block.text));
|
|
5432
5520
|
case "edit-block":
|
|
5433
5521
|
return /* @__PURE__ */ React2.createElement(EditBlockRow, { block });
|
|
5434
5522
|
case "table":
|
|
5435
|
-
return /* @__PURE__ */ React2.createElement(TableBlockRow, { block });
|
|
5523
|
+
return /* @__PURE__ */ React2.createElement(TableBlockRow, { block, citations });
|
|
5436
5524
|
case "hr":
|
|
5437
5525
|
return /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
5438
5526
|
}
|
|
@@ -5443,7 +5531,7 @@ function splitTableRow(line) {
|
|
|
5443
5531
|
const trimmed = masked.trim().replace(/^\||\|$/g, "");
|
|
5444
5532
|
return trimmed.split("|").map((c) => c.trim().replace(new RegExp(SENTINEL, "g"), "|"));
|
|
5445
5533
|
}
|
|
5446
|
-
function TableBlockRow({ block }) {
|
|
5534
|
+
function TableBlockRow({ block, citations }) {
|
|
5447
5535
|
const colCount = Math.max(block.header.length, ...block.rows.map((r) => r.length));
|
|
5448
5536
|
const widths = [];
|
|
5449
5537
|
for (let c = 0; c < colCount; c++) {
|
|
@@ -5454,12 +5542,12 @@ function TableBlockRow({ block }) {
|
|
|
5454
5542
|
const separator = widths.map((w) => "\u2500".repeat(w)).join("\u2500\u253C\u2500");
|
|
5455
5543
|
return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column" }, /* @__PURE__ */ React2.createElement(Box2, null, block.header.map((cell, ci) => (
|
|
5456
5544
|
// biome-ignore lint/suspicious/noArrayIndexKey: table columns never reorder — derived from a static header array
|
|
5457
|
-
/* @__PURE__ */ React2.createElement(Text2, { key: `h-${ci}`, bold: true, color: "cyan" }, /* @__PURE__ */ React2.createElement(InlineMd, { text: cell, padTo: widths[ci] ?? 3 }), ci < colCount - 1 ? " \u2502 " : "")
|
|
5545
|
+
/* @__PURE__ */ React2.createElement(Text2, { key: `h-${ci}`, bold: true, color: "cyan" }, /* @__PURE__ */ React2.createElement(InlineMd, { text: cell, padTo: widths[ci] ?? 3, citations }), ci < colCount - 1 ? " \u2502 " : "")
|
|
5458
5546
|
))), /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, separator), block.rows.map((row2, ri) => (
|
|
5459
5547
|
// biome-ignore lint/suspicious/noArrayIndexKey: table rows render in source order and don't reorder
|
|
5460
5548
|
/* @__PURE__ */ React2.createElement(Box2, { key: `r-${ri}` }, Array.from({ length: colCount }).map((_, ci) => (
|
|
5461
5549
|
// biome-ignore lint/suspicious/noArrayIndexKey: same — column axis is fixed by the table shape
|
|
5462
|
-
/* @__PURE__ */ React2.createElement(Text2, { key: `c-${ri}-${ci}` }, /* @__PURE__ */ React2.createElement(InlineMd, { text: row2[ci] ?? "", padTo: widths[ci] ?? 3 }), ci < colCount - 1 ? " \u2502 " : "")
|
|
5550
|
+
/* @__PURE__ */ React2.createElement(Text2, { key: `c-${ri}-${ci}` }, /* @__PURE__ */ React2.createElement(InlineMd, { text: row2[ci] ?? "", padTo: widths[ci] ?? 3, citations }), ci < colCount - 1 ? " \u2502 " : "")
|
|
5463
5551
|
)))
|
|
5464
5552
|
)));
|
|
5465
5553
|
}
|
|
@@ -5481,10 +5569,22 @@ function EditBlockRow({ block }) {
|
|
|
5481
5569
|
const replaceLines = block.replace.split("\n");
|
|
5482
5570
|
return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "cyan" }, block.filename), isNewFile ? /* @__PURE__ */ React2.createElement(Text2, { color: "green", bold: true }, " (new file)") : null), isNewFile ? null : /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", marginTop: 1 }, searchLines.map((line, i) => /* @__PURE__ */ React2.createElement(Text2, { key: `s-${i}-${line.length}`, color: "red" }, `- ${line}`))), /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", marginTop: isNewFile ? 1 : 0 }, replaceLines.map((line, i) => /* @__PURE__ */ React2.createElement(Text2, { key: `r-${i}-${line.length}`, color: "green" }, `+ ${line}`))));
|
|
5483
5571
|
}
|
|
5484
|
-
function Markdown({ text }) {
|
|
5572
|
+
function Markdown({ text, projectRoot }) {
|
|
5485
5573
|
const cleaned = stripMath(text);
|
|
5574
|
+
const root = projectRoot ?? process.cwd();
|
|
5575
|
+
const citations = React2.useMemo(() => collectCitations(cleaned, root), [cleaned, root]);
|
|
5486
5576
|
const blocks = React2.useMemo(() => parseBlocks(cleaned), [cleaned]);
|
|
5487
|
-
|
|
5577
|
+
const broken = [];
|
|
5578
|
+
for (const [url, status] of citations) {
|
|
5579
|
+
if (!status.ok) broken.push({ url, reason: status.reason });
|
|
5580
|
+
}
|
|
5581
|
+
return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", gap: 1 }, blocks.map((b, i) => /* @__PURE__ */ React2.createElement(BlockView, { key: `${i}-${b.kind}`, block: b, citations })), broken.length > 0 ? /* @__PURE__ */ React2.createElement(BrokenCitationsBlock, { items: broken }) : null);
|
|
5582
|
+
}
|
|
5583
|
+
function BrokenCitationsBlock({ items }) {
|
|
5584
|
+
return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", borderStyle: "round", borderColor: "red", paddingX: 1 }, /* @__PURE__ */ React2.createElement(Text2, { color: "red", bold: true }, `\u26A0 ${items.length} broken citation${items.length > 1 ? "s" : ""} \u2014 the model referenced paths or lines that don't exist`), items.map((b, i) => (
|
|
5585
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: list is derived from a Map iteration order, stable per render
|
|
5586
|
+
/* @__PURE__ */ React2.createElement(Text2, { key: `bc-${i}`, color: "red" }, ` \u274C ${b.url} \u2192 ${b.reason}`)
|
|
5587
|
+
)));
|
|
5488
5588
|
}
|
|
5489
5589
|
|
|
5490
5590
|
// src/cli/ui/ticker.tsx
|
|
@@ -5510,13 +5610,16 @@ function useElapsedSeconds() {
|
|
|
5510
5610
|
}
|
|
5511
5611
|
|
|
5512
5612
|
// src/cli/ui/EventLog.tsx
|
|
5513
|
-
var EventRow = React4.memo(function EventRow2({
|
|
5613
|
+
var EventRow = React4.memo(function EventRow2({
|
|
5614
|
+
event,
|
|
5615
|
+
projectRoot
|
|
5616
|
+
}) {
|
|
5514
5617
|
if (event.role === "user") {
|
|
5515
5618
|
return /* @__PURE__ */ React4.createElement(Box3, null, /* @__PURE__ */ React4.createElement(Text3, { bold: true, color: "cyan" }, "you \u203A", " "), /* @__PURE__ */ React4.createElement(Text3, null, event.text));
|
|
5516
5619
|
}
|
|
5517
5620
|
if (event.role === "assistant") {
|
|
5518
5621
|
if (event.streaming) return /* @__PURE__ */ React4.createElement(StreamingAssistant, { event });
|
|
5519
|
-
return /* @__PURE__ */ React4.createElement(Box3, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React4.createElement(Box3, null, /* @__PURE__ */ React4.createElement(Text3, { bold: true, color: "green" }, "assistant")), event.branch ? /* @__PURE__ */ React4.createElement(BranchBlock, { branch: event.branch }) : null, event.reasoning ? /* @__PURE__ */ React4.createElement(ReasoningBlock, { reasoning: event.reasoning }) : null, !isPlanStateEmpty(event.planState) ? /* @__PURE__ */ React4.createElement(PlanStateBlock, { planState: event.planState }) : null, event.text ? /* @__PURE__ */ React4.createElement(Markdown, { text: event.text }) : /* @__PURE__ */ React4.createElement(Text3, { dimColor: true }, "(no content)"), event.stats ? /* @__PURE__ */ React4.createElement(StatsLine, { stats: event.stats }) : null, event.repair ? /* @__PURE__ */ React4.createElement(Text3, { color: "magenta" }, event.repair) : null);
|
|
5622
|
+
return /* @__PURE__ */ React4.createElement(Box3, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React4.createElement(Box3, null, /* @__PURE__ */ React4.createElement(Text3, { bold: true, color: "green" }, "assistant")), event.branch ? /* @__PURE__ */ React4.createElement(BranchBlock, { branch: event.branch }) : null, event.reasoning ? /* @__PURE__ */ React4.createElement(ReasoningBlock, { reasoning: event.reasoning }) : null, !isPlanStateEmpty(event.planState) ? /* @__PURE__ */ React4.createElement(PlanStateBlock, { planState: event.planState }) : null, event.text ? /* @__PURE__ */ React4.createElement(Markdown, { text: event.text, projectRoot }) : /* @__PURE__ */ React4.createElement(Text3, { dimColor: true }, "(no content)"), event.stats ? /* @__PURE__ */ React4.createElement(StatsLine, { stats: event.stats }) : null, event.repair ? /* @__PURE__ */ React4.createElement(Text3, { color: "magenta" }, event.repair) : null);
|
|
5520
5623
|
}
|
|
5521
5624
|
if (event.role === "tool") {
|
|
5522
5625
|
const isError = event.text.startsWith("ERROR:");
|
|
@@ -5732,14 +5835,14 @@ function findNextEnabled(items, from, step) {
|
|
|
5732
5835
|
|
|
5733
5836
|
// src/cli/ui/PlanConfirm.tsx
|
|
5734
5837
|
var DEFAULT_MAX_RENDERED = 2400;
|
|
5735
|
-
function PlanConfirm({ plan, onChoose, maxRenderedChars }) {
|
|
5838
|
+
function PlanConfirm({ plan, onChoose, maxRenderedChars, projectRoot }) {
|
|
5736
5839
|
const cap = maxRenderedChars ?? DEFAULT_MAX_RENDERED;
|
|
5737
5840
|
const tooLong = plan.length > cap;
|
|
5738
5841
|
const visible = tooLong ? `${plan.slice(0, cap)}
|
|
5739
5842
|
|
|
5740
5843
|
\u2026 (${plan.length - cap} chars truncated \u2014 use /tool to view the full proposal)` : plan;
|
|
5741
5844
|
const hasOpenQuestions = /^#{1,6}\s*(open[-\s]?questions?|risks?|unknowns?|assumptions?|unclear)/im.test(plan) || /^#{1,6}\s*(待确认|开放问题|风险|未知|假设|不确定)/im.test(plan);
|
|
5742
|
-
return /* @__PURE__ */ React6.createElement(Box5, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, marginY: 1 }, /* @__PURE__ */ React6.createElement(Box5, null, /* @__PURE__ */ React6.createElement(Text5, { bold: true, color: "cyan" }, "\u25B8 plan submitted \u2014 awaiting your review")), /* @__PURE__ */ React6.createElement(Box5, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React6.createElement(Markdown, { text: visible })), hasOpenQuestions ? /* @__PURE__ */ React6.createElement(Box5, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(Text5, { color: "yellow" }, "\u25B2 the plan has open questions or flagged risks \u2014 pick", " ", /* @__PURE__ */ React6.createElement(Text5, { bold: true }, "Refine / answer questions"), " to write concrete answers before the model moves on.")) : null, /* @__PURE__ */ React6.createElement(Box5, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(
|
|
5845
|
+
return /* @__PURE__ */ React6.createElement(Box5, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, marginY: 1 }, /* @__PURE__ */ React6.createElement(Box5, null, /* @__PURE__ */ React6.createElement(Text5, { bold: true, color: "cyan" }, "\u25B8 plan submitted \u2014 awaiting your review")), /* @__PURE__ */ React6.createElement(Box5, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React6.createElement(Markdown, { text: visible, projectRoot })), hasOpenQuestions ? /* @__PURE__ */ React6.createElement(Box5, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(Text5, { color: "yellow" }, "\u25B2 the plan has open questions or flagged risks \u2014 pick", " ", /* @__PURE__ */ React6.createElement(Text5, { bold: true }, "Refine / answer questions"), " to write concrete answers before the model moves on.")) : null, /* @__PURE__ */ React6.createElement(Box5, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(
|
|
5743
5846
|
SingleSelect,
|
|
5744
5847
|
{
|
|
5745
5848
|
initialValue: hasOpenQuestions ? "refine" : "approve",
|
|
@@ -6106,7 +6209,7 @@ function formatTokens(n) {
|
|
|
6106
6209
|
import { spawnSync } from "child_process";
|
|
6107
6210
|
|
|
6108
6211
|
// src/cli/commands/stats.ts
|
|
6109
|
-
import { existsSync as existsSync7, readFileSync as
|
|
6212
|
+
import { existsSync as existsSync7, readFileSync as readFileSync10 } from "fs";
|
|
6110
6213
|
function statsCommand(opts) {
|
|
6111
6214
|
if (opts.transcript) {
|
|
6112
6215
|
transcriptSummary(opts.transcript);
|
|
@@ -6119,7 +6222,7 @@ function transcriptSummary(path) {
|
|
|
6119
6222
|
console.error(`no such transcript: ${path}`);
|
|
6120
6223
|
process.exit(1);
|
|
6121
6224
|
}
|
|
6122
|
-
const lines =
|
|
6225
|
+
const lines = readFileSync10(path, "utf8").split(/\r?\n/).filter(Boolean);
|
|
6123
6226
|
let assistantTurns = 0;
|
|
6124
6227
|
let toolCalls = 0;
|
|
6125
6228
|
let lastTurn = 0;
|
|
@@ -7877,14 +7980,14 @@ Stay in plan mode \u2014 address the feedback (explore more if needed), then sub
|
|
|
7877
7980
|
balance,
|
|
7878
7981
|
updateAvailable
|
|
7879
7982
|
}
|
|
7880
|
-
), /* @__PURE__ */ React12.createElement(Static, { items: historical }, (item) => /* @__PURE__ */ React12.createElement(EventRow, { key: item.id, event: item })), !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && streaming ? /* @__PURE__ */ React12.createElement(Box11, { marginY: 1 }, /* @__PURE__ */ React12.createElement(EventRow, { event: streaming })) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && ongoingTool ? /* @__PURE__ */ React12.createElement(OngoingToolRow, { tool: ongoingTool, progress: toolProgress }) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && subagentActivity ? /* @__PURE__ */ React12.createElement(SubagentRow, { activity: subagentActivity }) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && !ongoingTool && statusLine ? /* @__PURE__ */ React12.createElement(StatusRow, { text: statusLine }) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && busy && !streaming && !ongoingTool && !statusLine ? /* @__PURE__ */ React12.createElement(StatusRow, { text: "processing\u2026" }) : null, stagedInput ? /* @__PURE__ */ React12.createElement(
|
|
7983
|
+
), /* @__PURE__ */ React12.createElement(Static, { items: historical }, (item) => /* @__PURE__ */ React12.createElement(EventRow, { key: item.id, event: item, projectRoot: hookCwd })), !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && streaming ? /* @__PURE__ */ React12.createElement(Box11, { marginY: 1 }, /* @__PURE__ */ React12.createElement(EventRow, { event: streaming, projectRoot: hookCwd })) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && ongoingTool ? /* @__PURE__ */ React12.createElement(OngoingToolRow, { tool: ongoingTool, progress: toolProgress }) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && subagentActivity ? /* @__PURE__ */ React12.createElement(SubagentRow, { activity: subagentActivity }) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && !ongoingTool && statusLine ? /* @__PURE__ */ React12.createElement(StatusRow, { text: statusLine }) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && busy && !streaming && !ongoingTool && !statusLine ? /* @__PURE__ */ React12.createElement(StatusRow, { text: "processing\u2026" }) : null, stagedInput ? /* @__PURE__ */ React12.createElement(
|
|
7881
7984
|
PlanRefineInput,
|
|
7882
7985
|
{
|
|
7883
7986
|
mode: stagedInput.mode,
|
|
7884
7987
|
onSubmit: handleStagedInputSubmit,
|
|
7885
7988
|
onCancel: handleStagedInputCancel
|
|
7886
7989
|
}
|
|
7887
|
-
) : pendingPlan ? /* @__PURE__ */ React12.createElement(PlanConfirm, { plan: pendingPlan, onChoose: handlePlanConfirm }) : pendingShell ? /* @__PURE__ */ React12.createElement(
|
|
7990
|
+
) : pendingPlan ? /* @__PURE__ */ React12.createElement(PlanConfirm, { plan: pendingPlan, onChoose: handlePlanConfirm, projectRoot: hookCwd }) : pendingShell ? /* @__PURE__ */ React12.createElement(
|
|
7888
7991
|
ShellConfirm,
|
|
7889
7992
|
{
|
|
7890
7993
|
command: pendingShell,
|
|
@@ -8238,7 +8341,7 @@ async function chatCommand(opts) {
|
|
|
8238
8341
|
const prior = loadSessionMessages(opts.session);
|
|
8239
8342
|
if (prior.length > 0) {
|
|
8240
8343
|
const p = sessionPath(opts.session);
|
|
8241
|
-
const mtime = existsSync8(p) ?
|
|
8344
|
+
const mtime = existsSync8(p) ? statSync5(p).mtime : /* @__PURE__ */ new Date();
|
|
8242
8345
|
sessionPreview = { messageCount: prior.length, lastActive: mtime };
|
|
8243
8346
|
}
|
|
8244
8347
|
} else if (opts.session && opts.forceNew) {
|
|
@@ -8271,7 +8374,7 @@ async function chatCommand(opts) {
|
|
|
8271
8374
|
// src/cli/commands/code.tsx
|
|
8272
8375
|
import { basename, resolve as resolve5 } from "path";
|
|
8273
8376
|
async function codeCommand(opts = {}) {
|
|
8274
|
-
const { codeSystemPrompt: codeSystemPrompt2 } = await import("./prompt-
|
|
8377
|
+
const { codeSystemPrompt: codeSystemPrompt2 } = await import("./prompt-75XLIUTO.js");
|
|
8275
8378
|
const rootDir = resolve5(opts.dir ?? process.cwd());
|
|
8276
8379
|
const session = opts.noSession ? void 0 : `code-${sanitizeName(basename(rootDir))}`;
|
|
8277
8380
|
const tools = new ToolRegistry();
|
|
@@ -8293,8 +8396,7 @@ async function codeCommand(opts = {}) {
|
|
|
8293
8396
|
);
|
|
8294
8397
|
await chatCommand({
|
|
8295
8398
|
model: opts.model ?? "deepseek-reasoner",
|
|
8296
|
-
harvest:
|
|
8297
|
-
// smart preset's harvest setting, always on for code
|
|
8399
|
+
harvest: opts.harvest ?? false,
|
|
8298
8400
|
system: codeSystemPrompt2(rootDir),
|
|
8299
8401
|
transcript: opts.transcript,
|
|
8300
8402
|
session,
|
|
@@ -9379,7 +9481,19 @@ function resolveSession(flag, configSession) {
|
|
|
9379
9481
|
}
|
|
9380
9482
|
|
|
9381
9483
|
// src/cli/index.ts
|
|
9382
|
-
var DEFAULT_SYSTEM =
|
|
9484
|
+
var DEFAULT_SYSTEM = `You are Reasonix, a helpful DeepSeek-powered assistant. Be concise and accurate. Use tools when available.
|
|
9485
|
+
|
|
9486
|
+
# Cite or shut up \u2014 non-negotiable
|
|
9487
|
+
|
|
9488
|
+
Every factual claim about a codebase must be backed by evidence. Reasonix VALIDATES your citations \u2014 broken paths render in **red strikethrough with \u274C** in front of the user.
|
|
9489
|
+
|
|
9490
|
+
**Positive claims** \u2014 append a markdown link:
|
|
9491
|
+
- \u2705 \`The MCP client supports listResources [listResources](src/mcp/client.ts:142).\`
|
|
9492
|
+
- \u274C \`The MCP client supports listResources.\` \u2190 unverifiable, do not write.
|
|
9493
|
+
|
|
9494
|
+
**Negative claims** ("X is missing", "Y isn't implemented", "lacks Z") are the #1 hallucination shape. STOP before writing them. If you have a search tool, call it first; if the search returns nothing, cite the search itself as evidence (\`No matches for "foo" in src/\`). If you have no tool, qualify hard: "I haven't verified \u2014 this is a guess."
|
|
9495
|
+
|
|
9496
|
+
Asserting absence without checking is how evaluative answers go wrong. Treat the urge to write "missing" as a red flag in your own reasoning.`;
|
|
9383
9497
|
var program = new Command();
|
|
9384
9498
|
program.name("reasonix").description("DeepSeek-native agent framework \u2014 built for cache hits and cheap tokens.").version(VERSION);
|
|
9385
9499
|
program.action(async () => {
|
|
@@ -9402,15 +9516,19 @@ program.command("setup").description("Interactive wizard \u2014 API key, preset,
|
|
|
9402
9516
|
await setupCommand({});
|
|
9403
9517
|
});
|
|
9404
9518
|
program.command("code [dir]").description(
|
|
9405
|
-
"Code-editing chat \u2014 filesystem
|
|
9406
|
-
).option("-m, --model <id>", "Override default reasoner model").option("--no-session", "Disable session persistence for this run").option("-r, --resume", "Skip the session picker \u2014 always continue prior messages").option("-n, --new", "Skip the session picker \u2014 always wipe prior messages and start fresh").option("--transcript <path>", "Write a JSONL transcript to this path").
|
|
9519
|
+
"Code-editing chat \u2014 filesystem tools rooted at <dir> (default: cwd), coding system prompt, deepseek-reasoner. Model proposes SEARCH/REPLACE blocks; Reasonix applies them to disk."
|
|
9520
|
+
).option("-m, --model <id>", "Override default reasoner model").option("--no-session", "Disable session persistence for this run").option("-r, --resume", "Skip the session picker \u2014 always continue prior messages").option("-n, --new", "Skip the session picker \u2014 always wipe prior messages and start fresh").option("--transcript <path>", "Write a JSONL transcript to this path").option(
|
|
9521
|
+
"--harvest",
|
|
9522
|
+
"Extract typed plan state from R1 reasoning (Pillar 2). Adds ~10-15% cost per turn. Off by default in code mode."
|
|
9523
|
+
).action(async (dir, opts) => {
|
|
9407
9524
|
await codeCommand({
|
|
9408
9525
|
dir,
|
|
9409
9526
|
model: opts.model,
|
|
9410
9527
|
noSession: opts.session === false,
|
|
9411
9528
|
transcript: opts.transcript,
|
|
9412
9529
|
forceResume: !!opts.resume,
|
|
9413
|
-
forceNew: !!opts.new
|
|
9530
|
+
forceNew: !!opts.new,
|
|
9531
|
+
harvest: !!opts.harvest
|
|
9414
9532
|
});
|
|
9415
9533
|
});
|
|
9416
9534
|
program.command("chat").description("Interactive Ink TUI with live cache/cost panel.").option("-m, --model <id>", "DeepSeek model id (overrides preset)").option("-s, --system <prompt>", "System prompt (pinned in the immutable prefix)", DEFAULT_SYSTEM).option("--transcript <path>", "Write a JSONL transcript to this path").option(
|