30 KiB
Persistent Search Runtime Hosting Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Build a persistent workspace search index for verstak.search and host runtime searchProviders through the existing plugin command bridge.
Architecture: Keep search as an official plugin. Add a generic plugin data storage surface to the SDK and desktop frontend bridge, then make verstak.search persist its own JSON index and fan out to registered provider command handlers with api.commands.executeFor.
Tech Stack: TypeScript SDK types/tests with Vitest, Wails desktop frontend bridge JavaScript smoke tests, official plugin browserless Node smoke tests, Markdown docs.
Global Constraints
- Do not copy code or architecture from
~/git/verstak; it is feature reference only. - Search remains plugin functionality; do not move search semantics into desktop core.
- User files remain the source of truth; the index is an optimization and discovery layer.
- Use JSON plugin data named
search-index; do not use SQLite FTS or a sidecar indexer. searchProviders[].handleris a command id declared by the same plugin.- A provider is executable only when its command handler is registered in the current frontend runtime.
- Missing storage, corrupt index data, provider failures, and unreadable files must degrade Search without failing the whole view.
- Use TDD for production code changes: write the failing test, run it red, implement the minimal code, run it green.
- After each meaningful change, commit and push the affected repository.
File Structure
verstak-sdk
- Modify
src/plugin-api.ts- Add
PluginDataJSONtype. - Add
storage.data.read/writetoVerstakPluginAPI.
- Add
- Modify
src/test-utils.ts- Store mock plugin data separately from mock settings.
- Expose
api.storage.data.read/write.
- Modify
src/plugin-api.test.ts- Add a focused mock API test for plugin data storage isolation.
- Generated by build:
dist/*.
verstak-desktop
- Modify
frontend/src/lib/plugin-host/VerstakPluginAPI.js- Expose
api.storage.data.read/write. - Route to existing Wails methods
ReadPluginDataJSONandWritePluginDataJSON.
- Expose
- Modify
frontend/tests/plugin-api-contributions-test.mjs- Extend the smoke mock and assertions to cover plugin data round-trip.
- Modify
frontend/src/lib/test/wails-mock.js- Add in-memory plugin data behavior for browser/e2e mocks.
verstak-official-plugins
- Modify
plugins/search/plugin.json- Add
storage.namespace,events.subscribe, andcommands.register. - Add a command contribution whose id is used by
searchProviders[].handler.
- Add
- Modify
plugins/search/frontend/src/index.js- Add small index helpers.
- Register the local vault-text search provider command.
- Load/build/write
search-index. - Subscribe to
file.changed. - Fan out to other
searchProviders.
- Modify
scripts/smoke-search-plugin.js- Cover persisted index reads/writes, provider fan-out, provider failures, and file event updates.
verstak-docs
- Modify
05_Official_Plugins.md- Replace the future-work sentence with current behavior after implementation.
- Modify
07_Full_Implementation_Roadmap.md- Mark persistent search index and runtime hosting complete after code is verified.
Task 1: SDK Plugin Data Storage Contract
Files:
- Modify:
/home/mirivlad/git/verstak2/verstak-sdk/src/plugin-api.ts - Modify:
/home/mirivlad/git/verstak2/verstak-sdk/src/test-utils.ts - Modify:
/home/mirivlad/git/verstak2/verstak-sdk/src/plugin-api.test.ts - Generated by build:
/home/mirivlad/git/verstak2/verstak-sdk/dist/*
Interfaces:
-
Produces:
export type PluginDataJSON = Record<string, unknown>;VerstakPluginAPI.storage.data.read(name: string): Promise<PluginDataJSON>VerstakPluginAPI.storage.data.write(name: string, data: PluginDataJSON): Promise<void>
-
Consumes:
- Existing
VerstakPluginAPIandcreateMockPluginAPI.
- Existing
-
Step 1: Write the failing SDK test
Add this test after settings persist in the mock API namespace in src/plugin-api.test.ts:
test('plugin data persists separately from settings in the mock API namespace', async () => {
const api = createMockPluginAPI('storage.plugin');
await api.settings.write('search-index', { source: 'settings' });
await api.storage.data.write('search-index', {
version: 1,
workspaceRootPath: 'Project',
entries: [{ path: 'Project/Docs/case.md' }],
});
await expect(api.storage.data.read('search-index')).resolves.toEqual({
version: 1,
workspaceRootPath: 'Project',
entries: [{ path: 'Project/Docs/case.md' }],
});
await expect(api.settings.read('search-index')).resolves.toEqual({ source: 'settings' });
await expect(api.storage.data.read('missing')).resolves.toEqual({});
});
Also extend mock API exposes the bundled runtime shape with:
expect(typeof api.storage.data.read).toBe('function');
expect(typeof api.storage.data.write).toBe('function');
- Step 2: Run RED
Run:
cd /home/mirivlad/git/verstak2/verstak-sdk
npx vitest run src/plugin-api.test.ts -t "plugin data persists separately from settings in the mock API namespace"
Expected: TypeScript/Vitest fails because storage does not exist on VerstakPluginAPI.
- Step 3: Add the SDK type
In src/plugin-api.ts, add this type near PluginCommandArgs:
export type PluginDataJSON = Record<string, unknown>;
Then add this block to VerstakPluginAPI immediately after settings:
storage: {
data: {
read(name: string): Promise<PluginDataJSON>;
write(name: string, data: PluginDataJSON): Promise<void>;
};
};
- Step 4: Add mock storage data
In src/test-utils.ts, add an in-memory data map near the existing settings map:
const pluginData = new Map<string, Record<string, unknown>>();
Then add this storage object immediately after the returned settings object:
storage: {
data: {
read: vi.fn(async (name: string) => ({ ...(pluginData.get(name) || {}) })),
write: vi.fn(async (name: string, data: Record<string, unknown>) => {
pluginData.set(name, { ...(data || {}) });
}),
},
},
- Step 5: Run GREEN
Run:
cd /home/mirivlad/git/verstak2/verstak-sdk
npx vitest run src/plugin-api.test.ts -t "plugin data persists separately from settings in the mock API namespace"
Expected: one matching test passes.
- Step 6: Run SDK verification
Run:
cd /home/mirivlad/git/verstak2/verstak-sdk
npm test
npm run build
Expected: Vitest exits 0; tsc exits 0 and updates dist/.
- Step 7: Commit and push SDK
Run:
cd /home/mirivlad/git/verstak2/verstak-sdk
git status --short
git add src/plugin-api.ts src/test-utils.ts src/plugin-api.test.ts dist
git commit -m "feat: expose plugin data storage API"
git push
Expected: commit is created and pushed; git status --short --branch reports ## main...origin/main.
Task 2: Desktop Frontend Storage Bridge
Files:
- Modify:
/home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/plugin-host/VerstakPluginAPI.js - Modify:
/home/mirivlad/git/verstak2/verstak-desktop/frontend/tests/plugin-api-contributions-test.mjs - Modify:
/home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/test/wails-mock.js
Interfaces:
-
Consumes:
- Existing Wails methods
App.ReadPluginDataJSON(pluginId, name)andApp.WritePluginDataJSON(pluginId, name, data).
- Existing Wails methods
-
Produces:
- Runtime
api.storage.data.read(name)andapi.storage.data.write(name, data).
- Runtime
-
Step 1: Write the failing bridge smoke test
In frontend/tests/plugin-api-contributions-test.mjs, extend window.go.api.App.App with plugin data storage:
const pluginData = {};
Place it before globalThis.window = { ... }.
Inside App, add:
ReadPluginDataJSON: (pluginId, name) => Promise.resolve([
Object.assign({}, (pluginData[pluginId] && pluginData[pluginId][name]) || {}),
'',
]),
WritePluginDataJSON: (pluginId, name, data) => {
pluginData[pluginId] = pluginData[pluginId] || {};
pluginData[pluginId][name] = Object.assign({}, data || {});
return Promise.resolve('');
},
After the existing executeFor assertion, add:
if (!api.storage || !api.storage.data || typeof api.storage.data.read !== 'function' || typeof api.storage.data.write !== 'function') {
throw new Error('api.storage.data read/write is missing');
}
await api.storage.data.write('search-index', { version: 1, workspaceRootPath: 'Project' });
const stored = await api.storage.data.read('search-index');
if (stored.version !== 1 || stored.workspaceRootPath !== 'Project') {
throw new Error(`unexpected storage data: ${JSON.stringify(stored)}`);
}
- Step 2: Run RED
Run:
cd /home/mirivlad/git/verstak2/verstak-desktop
node frontend/tests/plugin-api-contributions-test.mjs
Expected: fails with api.storage.data read/write is missing.
- Step 3: Implement bridge storage
In frontend/src/lib/plugin-host/VerstakPluginAPI.js, add this block immediately after settings and before files:
storage: {
data: {
read: function(name) {
assertActive('storage.data.read(' + name + ')');
if (!name) {
throw new Error('storage.data.read requires a name');
}
return callBackend(pluginId, 'storage.data.read(' + name + ')', function() {
return App.ReadPluginDataJSON(pluginId, name);
}).then(function(data) {
return data || {};
});
},
write: function(name, data) {
assertActive('storage.data.write(' + name + ')');
if (!name) {
throw new Error('storage.data.write requires a name');
}
return callBackendErrorString(pluginId, 'storage.data.write(' + name + ')', function() {
return App.WritePluginDataJSON(pluginId, name, data || {});
});
}
}
},
- Step 4: Implement browser mock storage
In frontend/src/lib/test/wails-mock.js, add a module-scope object near existing plugin mock state:
var pluginData = {};
Replace the current ReadPluginDataJSON and WritePluginDataJSON stubs with:
ReadPluginDataJSON: function (pluginId, name) {
var data = (pluginData[pluginId] && pluginData[pluginId][name]) || {};
return Promise.resolve([Object.assign({}, data), '']);
},
WritePluginDataJSON: function (pluginId, name, data) {
pluginData[pluginId] = pluginData[pluginId] || {};
pluginData[pluginId][name] = Object.assign({}, data || {});
return Promise.resolve('');
},
If pluginData already exists in another scope, reuse that exact object and do not create a duplicate.
- Step 5: Run GREEN
Run:
cd /home/mirivlad/git/verstak2/verstak-desktop
node frontend/tests/plugin-api-contributions-test.mjs
Expected: plugin api contributions smoke passed.
- Step 6: Run desktop frontend verification
Run:
cd /home/mirivlad/git/verstak2/verstak-desktop/frontend
npm run build
Expected: Vite build exits 0.
- Step 7: Commit and push desktop
Run:
cd /home/mirivlad/git/verstak2/verstak-desktop
git status --short
git add frontend/src/lib/plugin-host/VerstakPluginAPI.js frontend/tests/plugin-api-contributions-test.mjs frontend/src/lib/test/wails-mock.js
git commit -m "feat: expose plugin data storage bridge"
git push
Expected: commit is created and pushed; git status --short --branch reports ## main...origin/main.
Task 3: Search Plugin Persistent Index And Provider Hosting
Files:
- Modify:
/home/mirivlad/git/verstak2/verstak-official-plugins/plugins/search/plugin.json - Modify:
/home/mirivlad/git/verstak2/verstak-official-plugins/plugins/search/frontend/src/index.js - Modify:
/home/mirivlad/git/verstak2/verstak-official-plugins/scripts/smoke-search-plugin.js
Interfaces:
-
Consumes:
api.storage.data.read('search-index')api.storage.data.write('search-index', data)api.events.subscribe('file.changed', handler)api.contributions.list('searchProviders')api.commands.register(commandId, handler)api.commands.executeFor(pluginId, commandId, args)
-
Produces:
- Command
verstak.search.searchVaultText. - Search provider host behavior that merges local indexed results with external provider results.
- Command
-
Step 1: Write failing smoke coverage for storage and providers
In scripts/smoke-search-plugin.js, extend the fake API with:
const eventHandlers = {};
const storageWrites = [];
const providerCalls = [];
let caseText = '# Case\nTarget phrase is here.\n';
Add to api:
storage: {
data: {
read: async (name) => {
if (name !== 'search-index') throw new Error('unexpected storage read ' + name);
return {};
},
write: async (name, data) => {
if (name !== 'search-index') throw new Error('unexpected storage write ' + name);
storageWrites.push(data);
},
},
},
events: {
subscribe: async (name, handler) => {
eventHandlers[name] = eventHandlers[name] || [];
eventHandlers[name].push(handler);
return () => {
eventHandlers[name] = (eventHandlers[name] || []).filter((item) => item !== handler);
};
},
},
contributions: {
list: async (point) => point === 'searchProviders' ? [{
pluginId: 'provider.search',
id: 'provider.search.activity',
label: 'Activity Provider',
handler: 'provider.search.activity',
}, {
pluginId: 'provider.search',
id: 'provider.search.fail',
label: 'Failing Provider',
handler: 'provider.search.fail',
}] : [],
},
commands: {
register: async () => () => {},
executeFor: async (pluginId, commandId, args) => {
providerCalls.push({ pluginId, commandId, args });
if (commandId === 'provider.search.fail') throw new Error('provider unavailable');
return {
status: 'handled',
pluginId,
commandId,
result: {
results: [{
title: 'Activity hit',
snippet: 'Target activity result',
matchType: 'Activity',
providerId: 'provider.search.activity',
providerLabel: 'Activity Provider',
type: 'activity',
openable: false,
score: 75,
}],
},
};
},
},
Change the readText branch for Project/Docs/case.md to:
if (relativePath === 'Project/Docs/case.md') return caseText;
After the first target search assertions, add:
if (storageWrites.length === 0) throw new Error('search index should be persisted after initial build');
if (!container.textContent.includes('Activity hit')) throw new Error('provider result should be rendered');
if (!container.textContent.includes('Activity Provider')) throw new Error('provider label should be rendered');
if (!container.textContent.includes('provider unavailable')) throw new Error('provider failure should be reported without hiding results');
if (providerCalls.length === 0 || providerCalls[0].args.query !== 'target') {
throw new Error(`provider was not called with search query: ${JSON.stringify(providerCalls)}`);
}
Then add this event refresh check before the image search:
caseText = '# Case\nFresh event phrase is here.\n';
(eventHandlers['file.changed'] || []).forEach((handler) => handler({
name: 'file.changed',
payload: { path: 'Project/Docs/case.md', operation: 'update' },
timestamp: new Date().toISOString(),
}));
await flush();
input = queryInput();
input.value = 'fresh';
input.dispatchEvent('input');
await wait(360);
if (!container.textContent.includes('Fresh event phrase is here')) {
throw new Error('file.changed should refresh persisted search index content');
}
- Step 2: Run RED
Run:
cd /home/mirivlad/git/verstak2/verstak-official-plugins
node scripts/smoke-search-plugin.js
Expected: fails because verstak.search does not call storage or provider APIs.
- Step 3: Update the Search manifest
Change plugins/search/plugin.json:
"permissions": [
"files.read",
"workbench.open",
"storage.namespace",
"events.subscribe",
"commands.register",
"ui.register"
],
Add commands before workspaceItems:
"commands": [
{
"id": "verstak.search.searchVaultText",
"title": "Search Vault Text",
"handler": "searchVaultText"
}
],
Change the local search provider handler:
"handler": "verstak.search.searchVaultText"
- Step 4: Add index helpers in Search frontend
In plugins/search/frontend/src/index.js, add constants near existing limits:
var INDEX_NAME = 'search-index';
var INDEX_VERSION = 1;
Add helper functions after scanPath:
function entryToIndexEntry(entry, text) {
return {
path: entry.relativePath,
name: entry.name || entry.relativePath,
type: entry.type,
extension: extension(entry),
size: Number(entry.size || 0),
modifiedAt: String(entry.modifiedAt || ''),
text: String(text || '').replace(/\s+/g, ' ').slice(0, 4000)
};
}
function validIndex(index, rootPath) {
return index && index.version === INDEX_VERSION &&
cleanPath(index.workspaceRootPath) === cleanPath(rootPath) &&
Array.isArray(index.entries);
}
async function buildIndex(api, rootPath) {
var entries = await collectEntries(api, rootPath);
var indexed = [];
for (var i = 0; i < entries.length; i++) {
var entry = entries[i];
var text = '';
if (isTextFile(entry)) {
try {
text = await api.files.readText(entry.relativePath);
} catch (err) {
text = '';
}
}
indexed.push(entryToIndexEntry(entry, text));
}
return {
version: INDEX_VERSION,
workspaceRootPath: cleanPath(rootPath),
builtAt: new Date().toISOString(),
entries: indexed
};
}
function searchIndex(index, query) {
var results = [];
var needle = String(query || '').toLowerCase();
if (!validIndex(index, index && index.workspaceRootPath) || needle.length < 2) return results;
for (var i = 0; i < index.entries.length && results.length < MAX_RESULTS; i++) {
var entry = index.entries[i];
var path = String(entry.path || '');
var name = String(entry.name || '');
var text = String(entry.text || '');
var pathHit = path.toLowerCase().indexOf(needle) !== -1 || name.toLowerCase().indexOf(needle) !== -1;
var textIndex = text.toLowerCase().indexOf(needle);
if (pathHit) {
results.push({
path: path,
type: entry.type,
matchType: entry.type === 'folder' ? 'Folder name' : 'File name',
openable: entry.type !== 'folder',
line: 0,
snippet: entry.type === 'folder' ? 'Folder name/path match' : 'File name/path match',
providerLabel: 'Vault Text Search',
score: 100
});
} else if (textIndex !== -1) {
results.push({
path: path,
type: entry.type,
matchType: 'Content match',
openable: true,
line: 0,
snippet: snippet(text, textIndex, needle.length),
providerLabel: 'Vault Text Search',
score: 90
});
}
}
return results;
}
- Step 5: Add provider fan-out helper
Add this helper after searchIndex:
async function runExternalProviders(api, rootPath, query) {
if (!api.contributions || typeof api.contributions.list !== 'function' ||
!api.commands || typeof api.commands.executeFor !== 'function') {
return { results: [], failed: 0 };
}
var providers = await api.contributions.list('searchProviders');
providers = Array.isArray(providers) ? providers : [];
var results = [];
var failed = 0;
for (var i = 0; i < providers.length; i++) {
var provider = providers[i];
if (!provider || !provider.pluginId || !provider.handler) continue;
if (provider.pluginId === 'verstak.search' && provider.handler === 'verstak.search.searchVaultText') continue;
try {
var response = await api.commands.executeFor(provider.pluginId, provider.handler, {
source: 'search',
providerId: provider.id,
query: query,
workspaceRootPath: cleanPath(rootPath),
limit: MAX_RESULTS
});
var payload = response && response.result;
var providerResults = payload && Array.isArray(payload.results) ? payload.results : [];
providerResults.forEach(function (result) {
results.push(Object.assign({}, result, {
providerId: result.providerId || provider.id,
providerLabel: result.providerLabel || provider.label || provider.id
}));
});
} catch (err) {
failed += 1;
}
}
return { results: results, failed: failed };
}
- Step 6: Wire index lifecycle in SearchView
Inside SearchView.mount, extend state:
var state = { query: '', searching: false, results: [], status: 'Enter at least 2 characters.', error: '', providerFailures: 0 };
var index = null;
var cleanupFns = [];
Add these functions inside mount before render:
async function loadOrBuildIndex() {
if (api.storage && api.storage.data && typeof api.storage.data.read === 'function') {
try {
var stored = await api.storage.data.read(INDEX_NAME);
if (validIndex(stored, rootPath)) {
index = stored;
return;
}
} catch (err) {
state.status = 'Search index unavailable; using live scan.';
}
}
index = await buildIndex(api, rootPath);
if (api.storage && api.storage.data && typeof api.storage.data.write === 'function') {
try {
await api.storage.data.write(INDEX_NAME, index);
} catch (err) {
state.status = 'Search index could not be saved.';
}
}
}
async function refreshIndexPath(path) {
var clean = cleanPath(path);
if (!clean || (rootPath && clean !== rootPath && clean.indexOf(rootPath + '/') !== 0)) return;
index = await buildIndex(api, rootPath);
if (api.storage && api.storage.data && typeof api.storage.data.write === 'function') {
await api.storage.data.write(INDEX_NAME, index);
}
}
function subscribeIndexEvents() {
if (!api.events || typeof api.events.subscribe !== 'function') return;
api.events.subscribe('file.changed', function (event) {
var payload = (event && event.payload) || {};
refreshIndexPath(payload.path || payload.toPath || '').catch(function (err) {
console.error('[search] index refresh:', err);
});
}).then(function (unsubscribe) {
if (typeof unsubscribe === 'function') cleanupFns.push(unsubscribe);
}).catch(function (err) {
console.error('[search] file.changed subscription:', err);
});
}
At the end of mount, before render();, add:
if (api.commands && typeof api.commands.register === 'function') {
api.commands.register('verstak.search.searchVaultText', function (args) {
var q = String((args && args.query) || '');
return { results: searchIndex(index, q) };
}).then(function (unsubscribe) {
if (typeof unsubscribe === 'function') cleanupFns.push(unsubscribe);
}).catch(function (err) {
console.error('[search] command register:', err);
});
}
loadOrBuildIndex().then(function () {
subscribeIndexEvents();
}).catch(function (err) {
console.error('[search] initial index:', err);
});
In cleanup, add:
while (cleanupFns.length) {
try { cleanupFns.pop()(); } catch (err) { console.error('[search] cleanup:', err); }
}
- Step 7: Replace search execution with index plus provider results
In search(), replace:
var results = await runSearch(api, rootPath, state.query);
with:
if (!index) await loadOrBuildIndex();
var localResults = searchIndex(index, state.query);
var providerResponse = await runExternalProviders(api, rootPath, state.query);
var results = localResults.concat(providerResponse.results)
.sort(function (a, b) {
return Number(b.score || 0) - Number(a.score || 0) ||
String(a.path || a.title || '').localeCompare(String(b.path || b.title || ''));
})
.slice(0, MAX_RESULTS);
state.providerFailures = providerResponse.failed;
Change status assignment to:
state.status = state.results.length + ' result' + (state.results.length === 1 ? '' : 's') +
(state.providerFailures ? ' - ' + state.providerFailures + ' provider unavailable' + (state.providerFailures === 1 ? '' : 's') : '');
In render, change result title rendering to support provider results:
el('div', { className: 'search-path' }, [result.path || result.title || 'Untitled result']),
el('div', { className: 'search-snippet' }, [result.snippet || '']),
el('div', { className: 'search-meta' }, [
(result.providerLabel ? result.providerLabel + ' - ' : '') +
(result.matchType || result.type || 'Search result') +
(result.line ? ' - Line ' + result.line : '')
])
Change openResource payload to use result.resource when present:
var resource = result.resource || {
kind: 'vault-file',
path: result.path,
mode: 'view'
};
api.workbench.openResource(resource).catch(function (err) { console.error('[search] openResource:', err); });
- Step 8: Run GREEN for focused Search smoke
Run:
cd /home/mirivlad/git/verstak2/verstak-official-plugins
node scripts/smoke-search-plugin.js
Expected: search plugin smoke passed.
- Step 9: Run official plugin verification
Run:
cd /home/mirivlad/git/verstak2/verstak-official-plugins
./scripts/check.sh
Expected: check exits 0 and search frontend smoke passes.
- Step 10: Commit and push official plugins
Run:
cd /home/mirivlad/git/verstak2/verstak-official-plugins
git status --short
git add plugins/search/plugin.json plugins/search/frontend/src/index.js scripts/smoke-search-plugin.js
git commit -m "feat: persist search index and host providers"
git push
Expected: commit is created and pushed; git status --short --branch reports ## main...origin/main.
Task 4: Documentation And Roadmap Update
Files:
- Modify:
/home/mirivlad/git/verstak2/verstak-docs/05_Official_Plugins.md - Modify:
/home/mirivlad/git/verstak2/verstak-docs/07_Full_Implementation_Roadmap.md
Interfaces:
-
Consumes:
- Verified commits from SDK, desktop, and official plugins.
-
Produces:
- Docs that match implemented Search behavior.
-
Step 1: Update official plugin status text
In 05_Official_Plugins.md, replace the final Search status sentence:
Persistent indexing and cross-provider runtime hosting are still future work.
with:
It persists a workspace-scoped JSON search index in the plugin data namespace,
refreshes it from public file events, registers its own vault-text provider as
a command-backed `searchProviders` handler, and fans out to other registered
provider commands at runtime. Full-text ranking, typo/layout tolerant search,
and sidecar indexing remain later work.
- Step 2: Update roadmap checkbox
In 07_Full_Implementation_Roadmap.md, change:
- implement persistent search index and cross-provider runtime hosting;
to:
- [x] implement persistent search index and cross-provider runtime hosting;
- Step 3: Verify docs diff
Run:
cd /home/mirivlad/git/verstak2/verstak-docs
git diff --check
rg -n "Persistent indexing and cross-provider runtime hosting are still future work|implement persistent search index and cross-provider runtime hosting" 05_Official_Plugins.md 07_Full_Implementation_Roadmap.md
Expected: git diff --check exits 0. The rg command shows the checked roadmap line and does not show the old future-work sentence.
- Step 4: Commit and push docs
Run:
cd /home/mirivlad/git/verstak2/verstak-docs
git status --short
git add 05_Official_Plugins.md 07_Full_Implementation_Roadmap.md
git commit -m "docs: mark persistent search hosting complete"
git push
Expected: commit is created and pushed; git status --short --branch reports ## main...origin/main.
Final Verification Checklist
Run after all tasks are implemented:
cd /home/mirivlad/git/verstak2/verstak-sdk
npm test
npm run build
cd /home/mirivlad/git/verstak2/verstak-desktop
node frontend/tests/plugin-api-contributions-test.mjs
cd frontend
npm run build
cd /home/mirivlad/git/verstak2/verstak-official-plugins
node scripts/smoke-search-plugin.js
./scripts/check.sh
cd /home/mirivlad/git/verstak2/verstak-docs
git diff --check
Then inspect:
cd /home/mirivlad/git/verstak2
git -C verstak-sdk status --short --branch
git -C verstak-desktop status --short --branch
git -C verstak-official-plugins status --short --branch
git -C verstak-docs status --short --branch
Expected: each changed repository reports ## main...origin/main.