Historical record of completed issues from the beads (bd) tracking system. These issues were migrated to Flow-Next on 2026-01-09.
Type: epic | Priority: P1 | Status: closed
Created: 2026-01-06 | Closed: 2026-01-06
Close reason: Windows CI fixed - safeRm, path separators, hook timeouts
~87 tests fail on Windows with EBUSY errors during cleanup. Root cause: test files use raw rm() instead of safeRm() from test/helpers/cleanup.ts. Additional issues: symlink test references /etc (Unix-only), D:/Temp CI workaround may break on Windows Server 2025.
Type: task | Priority: P1 | Status: closed
Created: 2026-01-06 | Closed: 2026-01-06
Close reason: Windows CI passes - all 1315 tests green
After fixes applied, verify Windows CI passes.
Steps:
- Push changes to branch
- Monitor CI Windows job
- All 87+ tests should pass
- No EBUSY errors in logs
Acceptance:
- Windows CI job passes (green)
- No file lock errors in test output
Type: task | Priority: P1 | Status: closed
Created: 2026-01-06 | Closed: 2026-01-06
Close reason: Closed
test/core/validation.test.ts L55-65 creates symlink to /etc which doesn't exist on Windows.
Options:
- Skip test on Windows (process.platform check)
- Use Windows-appropriate path (C:\Windows\System32)
- Use relative symlink within temp dir
Recommendation: Skip on Windows with process.platform !== 'win32'
Also check: validateRelPath tests may fail due to backslash paths on Windows.
Type: task | Priority: P1 | Status: closed
Created: 2026-01-06 | Closed: 2026-01-06
Close reason: Closed
12 test files use raw rm() in afterEach cleanup, causing EBUSY on Windows. Replace with safeRm() from test/helpers/cleanup.ts.
Files:
- test/store/links.test.ts (L51)
- test/store/tags.test.ts (L51)
- test/serve/api-collections.test.ts (L152, L217)
- test/serve/api-links.test.ts (L48)
- test/serve/api-docs-update.test.ts (L64)
- test/serve/api-tags.test.ts (L52)
- test/ingestion/sync-links.test.ts (L48, L467)
- test/ingestion/sync-tags.test.ts (L48, L216)
- test/core/validation.test.ts (L21)
- test/core/file-ops.test.ts (L19)
- test/llm/lockfile.test.ts (L25)
Acceptance:
- All rm() calls in afterEach/afterAll replaced with safeRm()
- Import { safeRm } from '../helpers/cleanup' added
- Tests pass locally on macOS/Linux
Type: task | Priority: P1 | Status: closed
Created: 2026-01-05 | Closed: 2026-01-05
Close reason: done
Address review issues: resolveLinks optional .md + basename; getGraph basename matches; fix SQL error; add tests; ensure docs align. See review list.
Type: task | Priority: P1 | Status: closed
Created: 2026-01-05 | Closed: 2026-01-05
Close reason: done
Add store + sync tests for Obsidian-style wiki paths (absolute/relative, basename, .md). refs: test/store/links.test.ts, test/ingestion/sync-links.test.ts
Type: task | Priority: P1 | Status: closed
Created: 2026-01-05 | Closed: 2026-01-05
Close reason: done
Add extractWikiBasename helper; extend resolveLinks/getBacklinksForDoc/getGraph wiki matching w/ pathlike fallbacks, deterministic order, no reindex. refs: src/core/links.ts, src/store/sqlite/adapter.ts
Type: bug | Priority: P1 | Status: closed
Created: 2026-01-05 | Closed: 2026-01-05
Close reason: fixed: vec0 auto-sync, vec CLI commands, transaction wrap
The similar command returns no results even when documents have high cosine similarity (0.79 verified manually).
upsertVectors in sqlite-vec.ts silently catches vec0 insert errors (lines 176-179). syncVecIndex/rebuildVecIndex exist but are never called.
- Add collection with short docs
- gno sync && gno index
- gno similar test/note-a.md --threshold 0.0
- Returns empty even though docs have 0.79 cosine similarity
In sqlite-vec.ts upsertVectors:
- Log warning on vec0 failure (rate-limited, once per run)
- Set
vecDirty = trueflag on VectorIndexPort
In embedBacklog() (src/embed/index.ts):
- After all batches complete, check vecDirty
- If dirty, call syncVecIndex() once
- Don't sync per-batch (too expensive)
New commands under gno vec:
gno vec sync- incremental sync (add missing, remove orphans)gno vec rebuild- drop + recreate + repopulate- Infer dimensions from content_vectors.embedding.byteLength / 4
Add targeted syncVecIndexForMirror(mirrorHash) for efficient incremental updates using known chunk IDs instead of global NOT IN queries.
- Dimension mismatch: vec table created with wrong FLOAT[n] - rebuild fixes
- Schema invalidation: DROP/recreate invalidates prepared statements - rebuild fixes
- Logical divergence: content_vectors has data, vec0 doesn't - sync fixes
- src/store/vector/sqlite-vec.ts - error handling, vecDirty flag, export flag
- src/embed/index.ts - call sync after embedBacklog if dirty
- src/cli/program.ts - add
gno vecsubcommand group
RelatedNotesSidebar already uses /api/doc/:id/similar endpoint.
Type: task | Priority: P1 | Status: closed
Created: 2026-01-04 | Closed: 2026-01-05
Close reason: All documentation updated: docs, spec, website bento card, pSEO page
Documentation and specification updates for the note linking feature.
Additions:
- New section "## Link Commands" after Document Commands
gno links <doc>- List outgoing links from a documentgno backlinks <doc>- List documents that link TO this documentgno similar <doc>- Find semantically similar documents- Update Quick Reference table with 3 new commands
- Examples with all output formats (--json, --md)
Additions:
- New section "## Link Endpoints"
GET /api/doc/:id/links- Get outgoing linksGET /api/doc/:id/backlinks- Get backlinksGET /api/doc/:id/similar- Get similar documentsGET /api/docs/suggest- Autocomplete endpoint for wiki links- Update Quick Reference table
- Request/response examples with curl
- Python and JavaScript usage examples
Additions:
- New section "## Link Tools"
gno_links- Get outgoing links from documentgno_backlinks- Get documents linking to a documentgno_similar- Get similar/related documents- Update Available Tools table
- Example prompts for Claude Desktop
- Use cases in AI workflows
Additions:
- New section "## Document Sidebar"
- Backlinks panel description
- Outgoing links panel description (with broken link indicator)
- Related notes panel description
- New section "## Wiki Link Autocomplete"
- Trigger behavior (
[[) - Cross-collection suggestions with prefix
- Keyboard navigation (arrows, Enter, Escape)
- Trigger behavior (
- Update Keyboard Shortcuts section
- Add to Features overview
Additions:
- Add "## Exploring Links" section with examples:
gno backlinks my-note.md gno links my-note.md gno similar my-note.md
- Mention wiki link syntax
[[link]]in indexing section
Additions:
- New section "## Link System"
- Schema explanation (doc_links table)
- Link types: wiki vs markdown
- Resolution at query time (not stored target_doc_id)
- Position tracking (1-based line/col)
- Update Storage section with doc_links table
- Update Pipeline diagram if needed
New terms to add:
- Wiki Link -
[[document-name]]syntax for internal links - Backlink - Documents that link TO a given document
- Outgoing Link - Links FROM a document to other documents
- Similar Documents - Semantically related docs via vector search
- Link Resolution - Process of matching link targets to documents
- Cross-collection Link -
[[collection:Note]]syntax
Minor additions:
- Note about link-based ranking (if applicable)
- Link to ARCHITECTURE.md for link system details
New section:
- "## Networked Notes (Zettelkasten/Obsidian-style)"
- Example of using backlinks for knowledge graph navigation
- Example AI queries: "Show all notes linking to my auth architecture decision"
Additions:
- Full command specifications for:
gno links <doc>with all options (--limit, --json, --md, --context)gno backlinks <doc>with all optionsgno similar <doc>with all options
- Exit codes for link commands
- Update Output Format Support Matrix table
- Add to Global Flags if any link-specific flags
Additions:
- Full tool specifications for:
gno_linkswith input/output schemasgno_backlinkswith input/output schemasgno_similarwith input/output schemas
- Update Tools section
- Add security considerations (read-only tools)
Create:
links-response.schema.json- Response for gno links command/toolbacklinks-response.schema.json- Response for gno backlinks command/toolsimilar-response.schema.json- Response for gno similar command/tooldoc-suggest-response.schema.json- Response for autocomplete endpoint
Additions:
- Document the doc_links table schema
- Include indexes and constraints
- Add comments explaining design decisions
Create:
links.schema.test.ts- Validate links responses match schemabacklinks.schema.test.ts- Validate backlinks responses match schemasimilar.schema.test.ts- Validate similar responses match schema
Add feature card to homepage bento grid: The homepage features grid (lines ~100-243) contains hardcoded feature cards. Add new card for Note Linking:
<a href="{{ '/features/note-linking/' | relative_url }}" class="feature-card">
<div class="feature-card-icon">
{% include icons.html icon="link" size="24" %}
</div>
<h3 class="feature-card-title">Note Linking</h3>
<p class="feature-card-description">
Wiki links, backlinks, and semantic similarity. Navigate your knowledge
graph.
</p>
</a>Position: After "Tag System" card, before "Web UI" card
Add feature definition for pSEO page:
- title: Note Linking
description: Wiki links, backlinks, and semantic similarity
icon: link
details:
- "[[Wiki links]] and [Markdown](links)"
- Cross-collection linking
- Backlink discovery
- Semantic similar notesCreate pSEO feature page:
- Overview of note linking feature
- Wiki link syntax examples
- Backlinks use cases (Zettelkasten, PKM)
- Similar docs for discovery
- Cross-collection linking
- Integration examples (CLI, API, MCP)
- Run
bun run website:sync-docsafter docs/ updates - CHANGELOG.md copied automatically
After all updates, verify:
-
bun run docs-verifypasses (docs match implementation) -
bun run website:sync-docssuccessful - All new schemas validate with JSON Schema Draft 2020-12
- Contract tests in test/spec/schemas/ pass
- No broken internal links in documentation
- Examples are accurate and runnable
- CLI help text matches docs
- Homepage bento includes Note Linking card
- pSEO feature page /features/note-linking/ renders correctly
- CLI.md documents all link commands
- API.md documents all link endpoints
- MCP.md documents all link tools
- WEB-UI.md documents sidebar and autocomplete
- ARCHITECTURE.md explains link system
- QUICKSTART.md has link usage examples
- links-response.schema.json is valid JSON Schema
- CLI links --json output matches schema
- API /api/doc/:id/links response matches schema
- backlinks-response.schema.json is valid JSON Schema
- CLI backlinks --json output matches schema
- API /api/doc/:id/backlinks response matches schema
- similar-response.schema.json is valid JSON Schema
- CLI similar --json output matches schema
- API /api/doc/:id/similar response matches schema
- All 9 docs files updated with link feature info
- spec/cli.md has full link command specs
- spec/mcp.md has full link tool specs
- 4 new output schemas created
- spec/db/schema.sql documents doc_links table
- Contract tests for all new schemas
- Homepage bento (home.html) includes Note Linking card
- website/_data/features.yml includes link feature
- pSEO page website/features/note-linking.md created
-
bun run website:sync-docssuccessful - No broken internal links in docs
- All examples accurate and runnable
Type: task | Priority: P1 | Status: closed
Created: 2026-01-04 | Closed: 2026-01-05
Close reason: Duplicate of gno-4ms
WebUI components for link management (React + TypeScript).
For ALL UI component work in this phase, use the frontend-design plugin:
/frontend-design:frontend-design <description of component>
This ensures distinctive, high-quality designs matching the "Scholarly Dusk" aesthetic.
Collapsible sidebar on document view with multiple panels.
Location: src/serve/public/components/DocSidebar.tsx
interface DocSidebarProps {
docId: number;
docUri: string;
}
export function DocSidebar({ docId, docUri }: DocSidebarProps) {
return (
<aside className="doc-sidebar">
<BacklinksPanel docId={docId} />
<OutgoingLinksPanel docId={docId} />
<SimilarDocsPanel docId={docId} docUri={docUri} />
</aside>
);
}Shows documents that link TO this document.
Location: src/serve/public/components/BacklinksPanel.tsx
Use /frontend-design:frontend-design for:
- Collapsible panel header with count badge
- Backlink item with source doc title + link text
- Empty state design
- Loading skeleton
Features:
- Collapsible with "Backlinks (N)" header
- Show source doc title + link text
- Click navigates to source doc
- Empty state: "No backlinks"
Shows links FROM this document.
Location: src/serve/public/components/OutgoingLinksPanel.tsx
Use /frontend-design:frontend-design for:
- Link item with type icon (wiki/markdown)
- Broken link indicator (subtle warning)
- Panel layout matching BacklinksPanel
Features:
- Collapsible with "Links (N)" header
- Show target + display text + type icon (wiki/md)
- Broken links: subtle indicator (muted text, ⚠ icon)
- Click navigates to target (if resolved)
Shows semantically similar documents.
Location: src/serve/public/components/SimilarDocsPanel.tsx
Use /frontend-design:frontend-design for:
- Similarity score badge design
- Related notes item layout
- Loading/refresh indicator
Features:
- Collapsible with "Related Notes (N)" header
- Show doc title + similarity score badge
- Background refresh: show cached, refresh on mount
- Empty state: "No related notes"
Autocomplete for [[wiki links in editor.
Location: src/serve/public/components/WikiLinkAutocomplete.tsx
Use /frontend-design:frontend-design for:
- Floating dropdown positioning
- Suggestion item with collection prefix
- Keyboard focus states
- Match highlighting
Features:
- Trigger: after typing
[[ - Position: floating below cursor
- Show all collections with prefix for cross-collection
- Same collection docs first, then others prefixed with
collection: - Keyboard navigation: up/down/enter/escape
- Insert:
[[DocTitle]]or[[collection:DocTitle]]
// src/serve/public/hooks/useBacklinks.ts
export function useBacklinks(docId: number) {
return useQuery({
queryKey: ["backlinks", docId],
queryFn: () => fetch(`/api/doc/${docId}/backlinks`).then((r) => r.json()),
});
}// src/serve/public/hooks/useLinks.ts
export function useLinks(docId: number) {
return useQuery({
queryKey: ["links", docId],
queryFn: () => fetch(`/api/doc/${docId}/links`).then((r) => r.json()),
});
}// src/serve/public/hooks/useSimilar.ts
export function useSimilar(docId: number, docUri: string) {
return useQuery({
queryKey: ["similar", docId],
queryFn: () => fetch(`/api/doc/${docId}/similar`).then((r) => r.json()),
staleTime: 60_000, // Cache for 1 min
refetchOnMount: "always", // Background refresh
});
}// src/serve/public/hooks/useDocSuggestions.ts
export function useDocSuggestions(query: string, currentCollection?: string) {
return useQuery({
queryKey: ["doc-suggestions", query],
queryFn: () => fetch(`/api/docs/suggest?q=${query}`).then((r) => r.json()),
enabled: query.length >= 0, // Trigger on [[
});
}Autocomplete endpoint for wiki links.
Query Parameters:
q- Search query (partial title match)collection- Current collection (for prioritization)limit- Max results (default: 10)
Response:
{
"suggestions": [
{
"uri": "gno://coll/note.md",
"title": "Note Title",
"collection": "coll"
},
{ "uri": "gno://work/other.md", "title": "Other Doc", "collection": "work" }
]
}Use existing Tailwind classes. For custom component styles, use /frontend-design:frontend-design to get proper design tokens.
- Renders loading state
- Renders backlinks list
- Renders empty state
- Click navigates to source doc
- Collapsible toggle works
- Renders links list
- Shows broken link indicator
- Click navigates to resolved target
- Broken links not clickable
- Renders similar docs
- Shows similarity score
- Background refresh triggers
- Empty state when no similar
- Shows suggestions after [[
- Filters by query
- Shows collection prefix for cross-collection
- Keyboard navigation works
- Escape closes dropdown
- Enter selects suggestion
- All components designed via frontend-design plugin
- DocSidebar with 3 collapsible panels
- BacklinksPanel shows incoming links
- OutgoingLinksPanel shows links with broken indicators
- SimilarDocsPanel with background refresh
- WikiLinkAutocomplete triggered by [[
- Cross-collection suggestions with prefix
- All component tests pass
- Responsive design (sidebar collapsible on mobile)
- Keyboard accessible
Type: task | Priority: P1 | Status: closed
Created: 2026-01-04 | Closed: 2026-01-05
Close reason: Implemented: CLI/REST/MCP link tools complete, all tests pass
MCP tools and resources for link management (mirrors existing tool patterns).
Get outgoing links from a document.
Input Schema:
{
"type": "object",
"properties": {
"uri": {
"type": "string",
"description": "Document URI (gno://collection/path)"
},
"limit": {
"type": "number",
"description": "Max results (default: 20)"
}
},
"required": ["uri"]
}Output:
{
"doc": { "uri": "gno://coll/note.md", "title": "My Note" },
"links": [
{
"type": "wiki",
"target": "Related",
"displayText": null,
"resolved": true,
"targetUri": "gno://coll/related.md"
},
{
"type": "markdown",
"target": "./other.md",
"displayText": "Link",
"resolved": false,
"targetUri": null
}
],
"meta": { "total": 2, "broken": 1 }
}Get documents that link TO a document.
Input Schema:
{
"type": "object",
"properties": {
"uri": {
"type": "string",
"description": "Document URI"
},
"limit": {
"type": "number",
"description": "Max results (default: 20)"
},
"context": {
"type": "boolean",
"description": "Include surrounding text (default: false)"
}
},
"required": ["uri"]
}Output:
{
"doc": { "uri": "gno://coll/note.md", "title": "My Note" },
"backlinks": [
{
"sourceUri": "gno://coll/other.md",
"sourceTitle": "Other",
"linkText": "see My Note",
"context": "...context..."
}
],
"meta": { "total": 1 }
}Get semantically similar documents.
Input Schema:
{
"type": "object",
"properties": {
"uri": {
"type": "string",
"description": "Document URI"
},
"limit": {
"type": "number",
"description": "Max results (default: 5)"
}
},
"required": ["uri"]
}Output:
{
"doc": { "uri": "gno://coll/note.md", "title": "My Note" },
"similar": [
{ "uri": "gno://coll/related.md", "title": "Related Topic", "score": 0.92 }
],
"meta": { "total": 1 }
}Read-only resource exposing document links.
URI Template: gno://doc/{docId}/links
Response: Same as gno_links output
Usage:
// Client reads resource
const links = await client.readResource("gno://doc/123/links");export const gnoLinksToolDef: ToolDefinition = { ... };
export async function handleGnoLinks(args: GnoLinksArgs): Promise<ToolResponse>;export const gnoBacklinksToolDef: ToolDefinition = { ... };
export async function handleGnoBacklinks(args: GnoBacklinksArgs): Promise<ToolResponse>;export const gnoSimilarToolDef: ToolDefinition = { ... };
export async function handleGnoSimilar(args: GnoSimilarArgs): Promise<ToolResponse>;export const docLinksResourceDef: ResourceDefinition = { ... };
export async function handleDocLinksResource(uri: string): Promise<ResourceResponse>;Register new tools:
export const allTools = [
// existing...
gnoLinksToolDef,
gnoBacklinksToolDef,
gnoSimilarToolDef,
];Register new resource:
export const allResources = [
// existing...
docLinksResourceDef,
];- gno_links returns outgoing links
- gno_links respects limit parameter
- gno_links returns error for invalid URI
- gno_links returns error for missing doc
- gno_links shows resolved status correctly
- gno_backlinks returns incoming links
- gno_backlinks with context=true includes text
- gno_backlinks respects limit
- gno_backlinks returns error for invalid URI
- gno_similar returns similar docs
- gno_similar excludes self and linked
- gno_similar respects limit
- gno_similar returns error for invalid URI
- gno://doc/:id/links resource returns links
- Resource returns error for invalid docId
- Resource returns error for missing doc
- All three tools implemented and registered
- Tool input/output schemas match spec
- Resource implemented and registered
- Proper error handling for invalid URIs
- All tests pass
- Tools documented in MCP.md
Type: feature | Priority: P1 | Status: closed
Created: 2026-01-04 | Closed: 2026-01-04
Close reason: Implemented server-side debounced embed scheduler with getter-based context survival and correct rerun latch
Web UI editor autosaves every 2s. Current implementation embeds on every sync, which:
- Wastes compute on intermediate states
- Could embed unrelated docs (global backlog)
- Fails silently without logging
Edit → Autosave (2s) → Sync (FTS only)
↓
Scheduler.notifySyncComplete({docIds})
↓
Debounce timer reset (30s)
OR max-wait reached (5 min)
↓
embedBacklog(docIds) runs once
- Single-instance only - In-memory scheduler (gno serve is single-process)
- Global scope - No workspace/tenant concept in gno
- Track dirty docIds - Only embed docs that changed, not global backlog
- Max-wait throttle - 5 min max to prevent starvation during long edits
- Concurrency guard - running flag + needsRerun latch
interface EmbedScheduler {
// Called after sync with list of changed doc IDs
notifySyncComplete(docIds: string[]): void;
// Force immediate embed (for Cmd+S)
triggerNow(): Promise<{ embedded: number; errors: number } | null>;
// Get current state (for debugging/status)
getState(): {
pendingDocCount: number;
running: boolean;
nextRunAt?: number;
};
// Cleanup on server shutdown
dispose(): void;
}State management:
- pendingDocIds: Set - accumulated dirty docs
- timer: NodeJS.Timeout - debounce timer
- running: boolean - embed in progress
- needsRerun: boolean - more docs added while running
- firstPendingAt: number - for max-wait calculation
Constants:
- DEBOUNCE_MS = 30_000 (30s)
- MAX_WAIT_MS = 300_000 (5 min)
Current: embeds global backlog New: accepts optional docIds filter
async function embedBacklog(
store: SqliteAdapter,
embedPort: EmbeddingPort | null,
vectorIndex: VectorIndexPort | null,
modelUri: string,
docIds?: string[] // NEW: filter to specific docs
): Promise<{ embedded: number; errors: number } | null>;Changes:
- Add try/catch wrapper (don't throw)
- Log errors with console.error
- Filter backlog query by docIds if provided
- Return null if no embedPort (graceful degradation)
PUT /api/docs/:id (handleUpdateDoc)
- Remove inline embedBacklog call from sync job
- After sync completes, call scheduler.notifySyncComplete([docId])
POST /api/docs (handleCreateDoc)
- Same pattern as PUT
POST /api/embed (NEW endpoint)
- Calls scheduler.triggerNow()
- Returns { embedded, errors } or { running: true, pendingCount }
- Used by Cmd+S handler in frontend
GET /api/embed/status (optional, for debugging)
- Returns scheduler.getState()
src/serve/server.ts
- Create scheduler instance with ServerContext
- Pass to route handlers
- Call scheduler.dispose() on shutdown
src/serve/context.ts
- Add scheduler to ServerContext (or keep separate)
Autosave (existing):
- No change - just saves, backend handles scheduling
Explicit save (Cmd+S):
- After PUT succeeds, call POST /api/embed
- Show "Saved & indexed" vs "Saved"
Idle timeout (optional enhancement):
- Could add 30s idle timer that calls POST /api/embed
- But server-side scheduler already handles this
- embedBacklog wrapped in try/catch
- Errors logged server-side
- Sync job always succeeds
- Frontend shows save status, not embed status
src/serve/embed-scheduler.ts- NEW (~100 lines)src/serve/routes/api.ts- Modify handlers, add POST /api/embedsrc/serve/server.ts- Initialize schedulersrc/serve/public/pages/DocumentEditor.tsx- Cmd+S calls POST /api/embedsrc/store/vector/stats.ts- Add docId filter to getBacklog (optional)
- Unit test embed-scheduler: debounce, max-wait, concurrency
- Integration: rapid saves accumulate, single embed runs
- Manual: edit doc, wait, verify vector search works
Remove inline embedBacklog calls from handleUpdateDoc and handleCreateDoc sync jobs. Keep the embedBacklog helper function but will modify it.
Type: task | Priority: P0 | Status: closed
Created: 2026-01-01 | Closed: 2026-01-01
Close reason: Full document content (32K chars) to answer generation
Pass full document content to answer LLM instead of 1500-char snippets.
Current limits cause answer generation to miss critical content:
- MAX_SNIPPET_CHARS = 1500
- MAX_CONTEXT_SOURCES = 5
- Summary tables never reach LLM
const MAX_SNIPPET_CHARS = 1500;
const MAX_CONTEXT_SOURCES = 5;const MAX_DOC_CHARS = 32000; // ~8K tokens per doc
const MAX_CONTEXT_SOURCES = 3; // Fewer docs but full content
const MAX_TOTAL_CONTEXT = 96000; // ~24K tokens total
for (const r of results.slice(0, MAX_CONTEXT_SOURCES)) {
const contentResult = await store.getContent(r.conversion?.mirrorHash);
let content = contentResult.ok ? contentResult.value : r.snippet;
if (content.length > MAX_DOC_CHARS) {
content = content.slice(0, MAX_DOC_CHARS) + '\\n\\n[... truncated ...]';
}
contextParts.push(\`[\${citationIndex}] \${content}\`);
}- Pass store to generateGroundedAnswer()
- Fetch full content by mirrorHash
Competitor doesn't have RAG answers at all. We provide full-doc grounded answers.
Blocks: gno-5hk
Type: task | Priority: P0 | Status: closed
Created: 2026-01-01 | Closed: 2026-01-01
Close reason: Full document reranking with 32K context
Send full document content to reranker instead of truncated chunks.
- Competitor truncates to 4K chars
- We can use Qwen3's 32K context for FULL document reranking
- This EXCEEDS competitor capability
const texts: string[] = toRerank.map((c) => {
const chunk = getChunk(c.mirrorHash, c.seq);
return chunk?.text ?? "";
});const texts: string[] = await Promise.all(
toRerank.map(async (c) => {
const contentResult = await store.getContent(c.mirrorHash);
if (contentResult.ok && contentResult.value) {
const content = contentResult.value;
return content.length > 128000
? content.slice(0, 128000) + "..."
: content;
}
const chunk = getChunk(c.mirrorHash, c.seq);
return chunk?.text ?? "";
})
);// Multiple chunks from same doc -> one rerank call with full doc
const uniqueDocs = new Map<string, ...>();- Over-fetch 30+ chunks, rerank together, keep top 10
- Long-context reranking: +13.9% over standard RAG
- Complex financial questions: +16.3% improvement
Requires: gno-??? (Qwen3-Reranker switch)
Blocks: gno-5hk
Type: task | Priority: P0 | Status: closed
Created: 2026-01-01 | Closed: 2026-01-01
Close reason: Implemented 2x weight for original query in RRF
Give original query results 2x weight vs expansion variants in RRF fusion.
Expansion queries sometimes dilute exact matches. Original query is most valuable signal.
// First 2 lists (original FTS + original vector) get 2x weight
const weights = rankedLists.map((_, i) => (i < 2 ? 2.0 : 1.0));
const fused = reciprocalRankFusion(rankedLists, weights);for (const input of bm25Inputs) {
const weight =
input.source === "bm25"
? config.bm25Weight * 2.0 // 2x for original
: config.bm25Weight * 0.5; // 0.5x for variants
}
for (const input of vectorInputs) {
if (input.source === "vector") {
weight = config.vecWeight * 2.0; // 2x for original
} else if (input.source === "vector_variant") {
weight = config.vecWeight * 0.5;
} else if (input.source === "hyde") {
weight = config.vecWeight * 0.7;
}
}- RRF k=60 remains robust default
- Equal weights usually preferred, but 2x original helps prevent dilution
- Only add weights when empirical evidence shows benefit
Blocks: gno-5hk
Type: task | Priority: P0 | Status: closed
Created: 2026-01-01 | Closed: 2026-01-01
Close reason: Implemented document-level BM25 with Snowball stemmer. Migration 002, fts5-snowball loader, syncDocumentFts. All 625 tests pass.
Switch BM25/FTS from chunk-level to document-level indexing with Snowball stemmer.
No users yet - just change schema directly, delete old code.
- Current chunk-level FTS fails when query terms span chunks
- No stemming means "scored" doesn't match "score"
Test during implementation. If works (expected), use it. If not, fall back to porter unicode61.
-- Delete old content_fts, create new documents_fts
CREATE VIRTUAL TABLE IF NOT EXISTS documents_fts USING fts5(
filepath, title, body,
tokenize='snowball english'
);- Single FTS table, one tokenizer for all collections
- English Snowball still better than
unicode61for all languages - Proper multilingual = future work (gno-9jl)
See notes/epic-search-quality-improvements.md for full context. Depends on: gno-ad6 (TDD tests first)
Blocks: gno-5hk
Type: task | Priority: P0 | Status: closed
Created: 2026-01-01 | Closed: 2026-01-01
Close reason: Added 9 failing TDD tests for search quality
Add tests that FAIL with current implementation, proving the problem exists. After epic implementation, these tests should pass.
test("finds document when query terms span multiple chunks", async () => {
// "gmickel-bench" in chunk 1, "score table" in chunk 18
const results = await query("which model scored best on gmickel-bench?");
expect(results).toContainDocument("AI Coding Assistant Eval Results.md");
});test('stemming: "scored" matches "score"', async () => {
const results = await search("scored best");
expect(results.some((r) => r.content.includes("score"))).toBe(true);
});
test('stemming: "running" matches "run"', async () => {
const results = await search("running tests");
expect(results.some((r) => r.content.includes("run test"))).toBe(true);
});test("answer includes data from document tables", async () => {
const answer = await ask("which model scored best on gmickel-bench?", {
answer: true,
});
expect(answer.text).toContain("494.6"); // or GPT-5.2-xhigh
expect(answer.citations).toHaveLength(1);
});test("synthesizes answer from multiple documents", async () => {
const answer = await ask("compare async programming in go and python", {
answer: true,
});
expect(answer.citations.length).toBeGreaterThan(1);
});- Add to test/pipeline/search-quality.test.ts (new file)
- Use existing fixtures or add new ones
- Mark as
.skipinitially if needed, but document expected behavior - Run before/after epic to verify improvement
- Proves the problem exists
- Prevents regressions
- Clear success criteria for the epic
See notes/epic-search-quality-improvements.md for full context.
Blocks: gno-5hk Should be done FIRST before any implementation.
Type: task | Priority: P0 | Status: closed
Created: 2025-12-23 | Closed: 2025-12-30
Close reason: Closed
Implement all 6 MCP tools per spec/mcp.md with exception firewall, mutex, and input validation.
import type { SqliteAdapter } from "../store/sqlite/adapter.js";
import type { Config, Collection } from "../config/types.js";
import type { Mutex } from "async-mutex";
import type { LlmAdapter } from "../llm/adapter.js";
export interface ToolContext {
store: SqliteAdapter;
config: Config;
collections: Collection[];
actualConfigPath: string;
toolMutex: Mutex;
modelManager?: ModelManager;
isShuttingDown: () => boolean;
}
// Model lifecycle with retry-safe states
export interface ModelManager {
state: "idle" | "loading" | "ready" | "error";
adapter?: LlmAdapter;
getAdapter(): Promise<LlmAdapter>; // returns cached or initializes
dispose(): Promise<void>; // best-effort, non-throwing
}
// Retry-safe implementation
class ModelManagerImpl implements ModelManager {
state: "idle" | "loading" | "ready" | "error" = "idle";
adapter?: LlmAdapter;
private loadPromise?: Promise<LlmAdapter>;
async getAdapter(): Promise<LlmAdapter> {
if (this.state === "ready" && this.adapter) return this.adapter;
if (this.state === "loading" && this.loadPromise) return this.loadPromise;
// Reset error state to allow retry
this.state = "loading";
this.loadPromise = this.doLoad();
try {
this.adapter = await this.loadPromise;
this.state = "ready";
return this.adapter;
} catch (e) {
this.state = "idle"; // Allow retry on next call
this.loadPromise = undefined;
throw e;
}
}
private async doLoad(): Promise<LlmAdapter> {
// Initialize LlmAdapter with config...
}
async dispose(): Promise<void> {
try {
await this.adapter?.dispose?.();
} catch {
/* ignore */
}
this.adapter = undefined;
this.state = "idle";
}
}import { z } from "zod";
import type {
McpServer,
CallToolResult,
} from "@modelcontextprotocol/sdk/server/mcp.js";
// DRY helper: exception firewall + response shaping + mutex
export async function runTool<T>(
ctx: ToolContext,
name: string,
fn: () => Promise<T>,
formatText: (data: T) => string
): Promise<CallToolResult> {
// Check shutdown
if (ctx.isShuttingDown()) {
return {
isError: true,
content: [{ type: "text", text: "Error: Server is shutting down" }],
};
}
// Sequential execution via mutex
const release = await ctx.toolMutex.acquire();
try {
const data = await fn();
return {
content: [{ type: "text", text: formatText(data) }],
structuredContent: data,
};
} catch (e) {
// Exception firewall: never throw, always return isError
return {
isError: true,
content: [
{
type: "text",
text: `Error: ${e instanceof Error ? e.message : String(e)}`,
},
],
};
} finally {
release();
}
}
export function registerTools(server: McpServer, ctx: ToolContext): void {
// Tool IDs exactly per spec
server.tool("gno.search", searchInputSchema, (args) =>
handleSearch(args, ctx)
);
server.tool("gno.vsearch", vsearchInputSchema, (args) =>
handleVsearch(args, ctx)
);
server.tool("gno.query", queryInputSchema, (args) => handleQuery(args, ctx));
server.tool("gno.get", getInputSchema, (args) => handleGet(args, ctx));
server.tool("gno.multi_get", multiGetInputSchema, (args) =>
handleMultiGet(args, ctx)
); // underscore!
server.tool("gno.status", statusInputSchema, (args) =>
handleStatus(args, ctx)
);
}import { z } from "zod";
const searchInputSchema = {
query: z.string().min(1, "Query cannot be empty"),
collection: z.string().optional(),
limit: z.number().int().min(1).max(100).default(5),
minScore: z.number().min(0).max(1).optional(),
lang: z.string().optional(),
};
function validateSearchInput(args: unknown, ctx: ToolContext) {
const parsed = searchInputSchema.parse(args);
if (
parsed.collection &&
!ctx.collections.find((c) => c.name === parsed.collection)
) {
throw new Error(`Collection not found: ${parsed.collection}`);
}
return parsed;
}import path from "node:path";
import { parseUri } from "../../app/constants.js";
// CORRECT: Derive collection from result's URI, not tool input
function enrichWithAbsPath(
results: SearchResults,
collections: Collection[]
): SearchResults {
return {
...results,
results: results.results.map((r) => {
// Parse collection from result's URI (handles multi-collection searches)
const { collection: collName } = parseUri(r.uri);
const collection = collections.find((c) => c.name === collName);
const absPath = collection
? path.join(collection.root, r.source.relPath)
: r.source.relPath;
return {
...r,
source: { ...r.source, absPath },
};
}),
};
}- Validate input (query required, limit 1-100, collection exists)
- Wrap
searchBm25()from pipeline/search.ts - Enrich with absPath (derived from result's collection)
- Return content[] + structuredContent
- Validate input
- Get adapter from ModelManager (retry-safe, allows retry on failure)
- Wrap
searchVectorWithEmbedding()from pipeline/vsearch.ts - Graceful error if no vector index: isError:true + suggest
gno index - Enrich with absPath
- Validate input
- Get adapter from ModelManager
- Wrap
searchHybrid()from pipeline/hybrid.ts - Graceful degradation per spec (vectorsUsed, expanded, reranked flags)
- Enrich with absPath
- Validate ref format (URI, collection/path, #docid)
- Normalize gno:// URI using parseUri+buildUri from constants.ts
- Include source.absPath in response
- Tool ID is
gno.multi_get(underscore per spec) - Support refs[] OR pattern (not both)
- Implement maxBytes truncation
- Return skipped[] for docs exceeding limit
- Include source.absPath in all documents
- No input params
- Return collections, doc counts, health status
- Include truthful actualConfigPath in response
- All 6 tools registered with exact spec names
- Input validation via Zod (already in deps v4.2.1)
- Output matches spec/output-schemas/*.json
- Errors return isError:true via exception firewall (never throw)
- absPath derived from result's URI (not tool input collection)
- Graceful degradation for missing vectors/models
- All tools execute sequentially via mutex
- ModelManager allows retry after failure (no cached broken state)
- Shutdown check in runTool prevents new work
- spec/mcp.md - Full tool specs
- spec/output-schemas/ - Response schemas
- src/app/constants.ts - parseUri/buildUri for URI handling
- src/pipeline/search.ts:124 - searchBm25
- src/pipeline/vsearch.ts - searchVectorWithEmbedding
- src/pipeline/hybrid.ts - searchHybrid
Type: task | Priority: P0 | Status: closed
Created: 2025-12-23 | Closed: 2025-12-30
Close reason: Closed
gno mcp starts stdio MCP server with persistent DB connection, honoring --index and --config.
import type { GlobalOptions } from "../options.js";
export async function mcpCommand(options: GlobalOptions): Promise<void> {
const { startMcpServer } = await import("../../mcp/server.js");
await startMcpServer({
indexName: options.index,
configPath: options.config,
verbose: options.verbose,
});
}import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from "../app/constants.js";
import { initStore } from "../cli/commands/shared.js";
import { Mutex } from "async-mutex";
interface McpServerOptions {
indexName?: string;
configPath?: string;
verbose?: boolean;
}
export async function startMcpServer(options: McpServerOptions): Promise<void> {
// ========================================
// STDOUT PURITY GUARD (CRITICAL)
// ========================================
// Wrap stdout to catch accidental writes
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
let protocolMode = false;
process.stdout.write = (chunk: any, ...args: any[]) => {
if (!protocolMode) {
// During init, redirect to stderr
return process.stderr.write(chunk, ...args);
}
// After transport connected, allow JSON-RPC only
return originalStdoutWrite(chunk, ...args);
};
// Open DB once with index/config threading
const init = await initStore({
indexName: options.indexName,
configPath: options.configPath,
verbose: options.verbose ?? false,
});
if (!init.ok) {
console.error("Failed to initialize:", init.error);
process.exit(1);
}
const { store, config, collections, actualConfigPath } = init;
// Create MCP server
const server = new McpServer(
{
name: MCP_SERVER_NAME,
version: MCP_SERVER_VERSION,
},
{
capabilities: {
tools: { listChanged: false },
resources: { subscribe: false, listChanged: false },
},
}
);
// Sequential execution mutex
const toolMutex = new Mutex();
// Shutdown state
let shuttingDown = false;
// Tool context (passed to all handlers)
const ctx = {
store,
config,
collections,
actualConfigPath,
toolMutex,
isShuttingDown: () => shuttingDown,
};
// Register tools (T10.2) - pass ctx
// Register resources (T10.3) - pass ctx
// ========================================
// GRACEFUL SHUTDOWN (ordered)
// ========================================
const shutdown = async () => {
if (shuttingDown) return;
shuttingDown = true;
// 1. Stop accepting new messages (SDK handles this on close)
// 2. Wait for current handler (bounded timeout via mutex tryAcquire)
const release = await Promise.race([
toolMutex.acquire(),
new Promise<null>((r) => setTimeout(() => r(null), 5000)),
]);
if (release && typeof release === "function") release();
// 3. Dispose model ports (best-effort, non-throwing)
try {
if (ctx.modelManager?.adapter) {
await ctx.modelManager.adapter.dispose?.();
}
} catch {
/* ignore */
}
// 4. Close DB
await store.close();
// 5. Exit
process.exit(0);
};
process.on("SIGTERM", shutdown);
process.on("SIGINT", shutdown);
// Connect transport
const transport = new StdioServerTransport();
protocolMode = true; // Enable stdout for JSON-RPC
await server.connect(transport);
console.error("GNO MCP server running on stdio");
}Replace stub at line 710-718. CRITICAL: Ensure Commander doesn't print help to stdout:
function wireMcpCommand(program: Command): void {
program
.command("mcp")
.description("Start MCP server (stdio transport)")
.helpOption(false) // Disable --help to prevent stdout pollution
.action(async () => {
const { mcpCommand } = await import("./commands/mcp.js");
const globalOpts = program.opts();
await mcpCommand(globalOpts);
});
}Move WAL + busy_timeout to lowest level (open time):
// In SqliteAdapter.open() or constructor, immediately after db open:
db.exec("PRAGMA journal_mode=WAL");
db.exec("PRAGMA busy_timeout=5000");This ensures ALL connections (CLI, MCP, tests) get proper concurrency handling.
Add indexName and configPath to InitStoreOptions:
interface InitStoreOptions {
verbose?: boolean;
indexName?: string; // honor --index flag
configPath?: string; // honor --config flag
}
interface InitStoreResult {
ok: true;
store: SqliteAdapter;
config: Config;
collections: Collection[];
actualConfigPath: string; // truthful path for status
}Decision: Load-once, no hot reload
- Config loaded at server startup
- Collections/contexts synced once
- If user edits config, must restart
gno mcp - Document this behavior in docs/MCP.md
-
gno mcpstarts without error - Honors
--index <name>flag (opens correct DB) - Honors
--config <path>flag (loads correct config) - Server responds to MCP initialize request
- NO stdout except JSON-RPC (Commander help disabled, stdout guard active)
- DB stays open during session (WAL mode in SqliteAdapter.open)
- SIGTERM/SIGINT: ordered shutdown (wait handler → dispose models → close DB → exit)
- stderr shows startup message
- shuttingDown flag prevents new work during shutdown
- @modelcontextprotocol/sdk (already in package.json)
- zod (already in package.json v4.2.1)
- async-mutex (add) OR implement simple promise mutex
- src/cli/program.ts:710-718 (stub to replace)
- src/cli/commands/shared.ts:42-109 (initStore pattern)
- src/store/sqlite/adapter.ts (WAL pragma location)
- src/app/constants.ts:33-40 (MCP constants)
Type: epic | Priority: P0 | Status: closed
Created: 2025-12-23 | Closed: 2025-12-30
Close reason: Search pipelines complete - all tests passing
Type: task | Priority: P0 | Status: closed
Created: 2025-12-29 | Closed: 2025-12-29
Close reason: Closed
SCSS with CSS variables. [USES frontend-design PLUGIN - CRITICAL]
Brand Brief for GNO:
- Name origin: Greek 'gnosis' = knowledge, wisdom, discovery
- Mood: Clean, intelligent, trustworthy - like a personal librarian
- NOT: Cold/corporate, overly playful, generic tech startup, Terminal Noir clone
- Color direction: Deep blues or teals (knowledge/depth), warm accents (approachable), neutral backgrounds
- Typography: Mono for code/headings (tool feel), clean sans for body (readable)
- Icon concept: Search + knowledge (magnifying glass + book/brain/index)
- Dark mode default, light mode available
- Feel: Local-first, privacy-respecting, powerful but not intimidating
Technical scope:
- Design tokens (colors, spacing, typography, shadows, radii)
- Dark/light theme CSS variables
- Component styles: header, sidebar, nav, cards, code blocks, tables, buttons
- Syntax highlighting (Rouge)
- Animations (subtle, purposeful)
- Print styles
- Mobile responsive breakpoints
Type: task | Priority: P0 | Status: closed
Created: 2025-12-23 | Closed: 2025-12-27
Close reason: Implemented in commit a7152f8
gno get retrieves single doc by gno:// URI, collection/path, or #docid. Supports :line suffix, --from, -l, --line-numbers, --source. gno multi-get for multiple docs with --max-bytes limit. Reference: docs/prd.md §13.
Type: task | Priority: P0 | Status: closed
Created: 2025-12-23 | Closed: 2025-12-25
Close reason: Implemented in PR #9
Vector similarity search. Same output schema as search. Graceful error if vectors unavailable (suggest gno index/embed). Reference: docs/prd.md §12.1 and §11.3.
Type: task | Priority: P0 | Status: closed
Created: 2025-12-23 | Closed: 2025-12-25
Close reason: Implemented in PR #9
FTS-only search. Options: -n limit, --min-score, -c collection, --full, --line-numbers, --lang. Output: docid, score, uri, title, snippet, snippetRange, source metadata. Reference: docs/prd.md §12.1 and §15.1.
Type: epic | Priority: P0 | Status: closed
Created: 2025-12-23 | Closed: 2025-12-25
Close reason: EPIC 7 complete: vector index, stats, embed command merged
Type: task | Priority: P0 | Status: closed
Created: 2025-12-23 | Closed: 2025-12-25
Close reason: Completed in EPIC 7 vector embeddings PR
'gno embed [--force] [--model] [--batch-size]' embeds pending chunks. Store vectors keyed by (mirror_hash, seq, model). Batching for efficiency. Reference: docs/prd.md §14.2.
Type: task | Priority: P0 | Status: closed
Created: 2025-12-23 | Closed: 2025-12-25
Close reason: Completed in EPIC 7 vector embeddings PR
Integrate sqlite-vec for vector storage. Handle as optional dep - graceful degradation if unavailable. content_vectors table: (mirror_hash, seq, model) PK, embedding blob, embedded_at. Reference: docs/prd.md §9.2.
Type: task | Priority: P0 | Status: closed
Created: 2025-12-23 | Closed: 2025-12-24
Close reason: Implemented in PR #4
node-llama-cpp adapter for embeddings, generation, reranking. Keep models loaded for repeated calls. Dispose contexts/sequences promptly. Safe memory management for long MCP sessions. Reference: docs/prd.md §11.1.
Type: epic | Priority: P0 | Status: closed
Created: 2025-12-23 | Closed: 2025-12-24
Close reason: Implemented in PR #4
Type: task | Priority: P0 | Status: closed
Created: 2025-12-23 | Closed: 2025-12-24
Close reason: Merged to main
For each file: stat, read bytes, compute source_hash (sha256), detect MIME, convert to markdown mirror, compute mirror_hash, upsert doc keyed by (collection, relativePath). Mark missing files inactive. Store converter_id/version. Reference: docs/prd.md §7.2.
Type: task | Priority: P0 | Status: closed
Created: 2025-12-23 | Closed: 2025-12-24
Close reason: Merged to main
Walk collection directory matching glob pattern. Support include (extensions allowlist) and exclude (patterns like .git, node_modules). Deterministic path normalization. Use Bun.file for reading. Reference: docs/prd.md §7.1.
Type: epic | Priority: P0 | Status: closed
Created: 2025-12-23 | Closed: 2025-12-24
Close reason: Merged to main
Type: task | Priority: P0 | Status: closed
Created: 2025-12-23 | Closed: 2025-12-24
Close reason: Merged to main
Implement native/markdown (passthrough + canonicalization) and native/plaintext (wrap as markdown + canonicalization) converters. Reference: docs/prd.md §8.6.
Type: task | Priority: P0 | Status: closed
Created: 2025-12-23 | Closed: 2025-12-24
Close reason: Merged to main
Types in src/converters/types.ts: ConverterId, ConvertInput, ConvertWarning, ConvertOutput, ConvertResult, Converter interface. Error codes: UNSUPPORTED, TOO_LARGE, TIMEOUT, CORRUPT, etc. Registry selects first canHandle() match. Reference: docs/prd.md §8.2-§8.3.
Type: task | Priority: P0 | Status: closed
Created: 2025-12-23 | Closed: 2025-12-24
Close reason: Merged to main
Deterministic normalization rules: \n newlines, strip \u0000 and non-printables except \n\t, trim trailing whitespace per line, collapse 3+ blank lines to 2, ensure single final newline. NO timestamps or paths in output. Reference: docs/prd.md §8.4.
Type: task | Priority: P0 | Status: closed
Created: 2025-12-23 | Closed: 2025-12-24
Close reason: Merged to main
Implement MimeDetector interface. Layered detection: 1) extension map (.md→text/markdown, .pdf→application/pdf, etc.), 2) byte sniffing (%PDF-, PK zip header). Return {mime, ext, confidence, via}. Reference: docs/prd.md §8.5.
Type: epic | Priority: P0 | Status: closed
Created: 2025-12-23 | Closed: 2025-12-24
Close reason: Merged to main
Type: epic | Priority: P0 | Status: closed
Created: 2025-12-23 | Closed: 2025-12-23
Close reason: Implemented in previous session - migrations, core tables, adapter, status queries all complete. 191 tests passing.
Type: task | Priority: P0 | Status: closed
Created: 2025-12-23 | Closed: 2025-12-23
Close reason: Implemented in previous session - migrations, core tables, adapter, status queries all complete. 191 tests passing.
Tables: collections, contexts, documents, content, content_chunks, content_fts (FTS5), content_vectors, llm_cache, ingest_errors. Key fields in docs/prd.md §9.2. Use bun:sqlite.
Type: task | Priority: P0 | Status: closed
Created: 2025-12-23 | Closed: 2025-12-23
Close reason: Implemented in previous session - migrations, core tables, adapter, status queries all complete. 191 tests passing.
SQLite migrations system. DB path: /index-.sqlite. Run migrations on init/first access. Use bun:sqlite. Reference: docs/prd.md §9.
Type: epic | Priority: P0 | Status: closed
Created: 2025-12-23 | Closed: 2025-12-23
Close reason: EPIC 2 complete: config schema, loader/saver, collection/context/init commands, multilingual support
Type: task | Priority: P0 | Status: closed
Created: 2025-12-23 | Closed: 2025-12-23
Close reason: Implemented: all commands working, 43 tests passing
gno init [] [--name] [--pattern] [--yes] creates config if missing, runs DB migrations, optionally adds collection. Must be idempotent and safe to run repeatedly. Print resolved paths. Reference: docs/prd.md §7.1.
Type: task | Priority: P0 | Status: closed
Created: 2025-12-23 | Closed: 2025-12-23
Close reason: Config schema, loader, saver complete with tests
Implement config loading from YAML file at /index.yml. Support XDG paths (Linux), Library/Application Support (macOS), AppData (Windows). Environment overrides: GNO_CONFIG_DIR, GNO_DATA_DIR, GNO_CACHE_DIR. Reference: docs/prd.md §2.1-§2.3.
Type: epic | Priority: P0 | Status: closed
Created: 2025-12-23 | Closed: 2025-12-23
Close reason: All child tasks complete
Type: task | Priority: P0 | Status: closed
Created: 2025-12-23 | Closed: 2025-12-23
Close reason: Closed
Create JSON schemas for: search result item, status payload, get payload, multi-get payload, MCP tool outputs, ask payload. Reference: docs/prd.md §15.
Type: task | Priority: P0 | Status: closed
Created: 2025-12-23 | Closed: 2025-12-23
Close reason: Closed
Document MCP server spec: stdio transport, tools (gno.search/vsearch/query/get/multi_get/status), resources (gno:// URIs), schema versioning rules. Reference: docs/prd.md §16.
Type: task | Priority: P0 | Status: closed
Created: 2025-12-23 | Closed: 2025-12-23
Close reason: Closed
Document all CLI commands from §14, flags, exit codes (0/1/2), output format flags (--json/--files/--csv/--md/--xml). Reference: docs/prd.md §14.
Type: epic | Priority: P0 | Status: closed
Created: 2025-12-23 | Closed: 2025-12-23
Close reason: All tasks complete: scaffold, constants module, CI
Type: task | Priority: P0 | Status: closed
Created: 2025-12-23 | Closed: 2025-12-23
Close reason: Implemented: folder structure per PRD, constants module with OS paths/env overrides, 33 passing tests
Create src/app/constants.ts with all configurable names: CLI name ("gno"), URI scheme ("gno://"), config/data/cache dir names, MCP server name, MCP tool namespace prefix. Must be single-module rename. Reference: docs/prd.md §2.2.
Type: task | Priority: P0 | Status: closed
Created: 2025-12-23 | Closed: 2025-12-23
Close reason: Implemented: folder structure per PRD, constants module with OS paths/env overrides, 33 passing tests
Set up Bun + TypeScript ESM project. Configure biome for linting/formatting, tsgo for typecheck, bun test for testing. Verify with bun test passing. Reference: docs/prd.md §22 EPIC 0.