diff --git a/src/apps/officer-web/Screens/Dashboard/Layout/Header/Header.tsx b/src/apps/officer-web/Screens/Dashboard/Layout/Header/Header.tsx
index 3d38434b..9c7dc408 100644
--- a/src/apps/officer-web/Screens/Dashboard/Layout/Header/Header.tsx
+++ b/src/apps/officer-web/Screens/Dashboard/Layout/Header/Header.tsx
@@ -6,6 +6,7 @@ import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sh
import type { DockItem } from '../Dock';
import { useIsTouch } from '../useIsTouch';
import { UserMenu } from './UserMenu';
+import { RescanButton } from '../Rescan/RescanButton';
// import { BugReportButton } from '../BugReport/BugReportButton';
type HeaderProps = {
@@ -52,6 +53,7 @@ export function Header({ dockItems }: HeaderProps) {
*/}
{/* Bug Report — hidden */}
{/* */}
+
diff --git a/src/apps/officer-web/Screens/Dashboard/Layout/Rescan/RescanButton.tsx b/src/apps/officer-web/Screens/Dashboard/Layout/Rescan/RescanButton.tsx
new file mode 100644
index 00000000..ff4e79eb
--- /dev/null
+++ b/src/apps/officer-web/Screens/Dashboard/Layout/Rescan/RescanButton.tsx
@@ -0,0 +1,44 @@
+import { useState } from 'react';
+import { RotateCw } from 'lucide-react';
+import { useQueryClient } from '@tanstack/react-query';
+import { toast } from 'sonner';
+import { useClient } from 'hooks/useClient';
+
+type RescanResponse = { ok: boolean; counts: Record };
+
+// Item query caches the header refreshes after a rescan (matches the queryKeys used by the
+// Automation and Capability pages).
+const ITEM_QUERY_KEYS = ['tasks', 'skills', 'tools', 'processes'];
+
+export function RescanButton() {
+ const client = useClient();
+ const qc = useQueryClient();
+ const [loading, setLoading] = useState(false);
+
+ const rescan = async () => {
+ if (loading) return;
+ setLoading(true);
+ try {
+ const res = await client.post('/rescan', {});
+ await Promise.all(ITEM_QUERY_KEYS.map((key) => qc.invalidateQueries({ queryKey: [key] })));
+ const c = res.counts ?? {};
+ toast.success(`Rescanned items — ${c.tasks ?? 0} tasks, ${c.tools ?? 0} tools, ${c.skills ?? 0} skills, ${c.processes ?? 0} processes`);
+ } catch {
+ toast.error('Failed to rescan items');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/src/servers/api/items/rescan.ts b/src/servers/api/items/rescan.ts
new file mode 100644
index 00000000..1dbfddcd
--- /dev/null
+++ b/src/servers/api/items/rescan.ts
@@ -0,0 +1,39 @@
+import { readdirSync, existsSync } from 'node:fs';
+import { join } from 'node:path';
+import { createRouter } from '../../create-router';
+import { ITEM_TYPES, itemsDir, ensureItemDirs, type ItemType } from '../../data-path';
+import { ensureToolLoader } from '../../ensure-tool-loader';
+
+// Re-applies the boot-time item setup so changes under OFFICER_ITEMS_DIR take effect without a
+// server restart. Item lists are already read fresh from disk on every request; this ensures the
+// type dirs exist and the tool-loader extension is current, and reports the live item counts so the
+// UI can refetch.
+
+const CANONICAL: Record = {
+ skills: 'SKILL.md',
+ tools: 'TOOL.md',
+ tasks: 'TASK.md',
+ processes: 'PROCESS.md',
+ extensions: 'index.ts',
+};
+
+function countItems(type: ItemType): number {
+ const dir = itemsDir(type);
+ if (!existsSync(dir)) return 0;
+ let count = 0;
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
+ if (entry.isDirectory() && existsSync(join(dir, entry.name, CANONICAL[type]))) count++;
+ }
+ return count;
+}
+
+export const rescanRouter = createRouter();
+
+rescanRouter.post('/', async (ctx) => {
+ ensureItemDirs();
+ ensureToolLoader();
+
+ const counts = Object.fromEntries(ITEM_TYPES.map((type) => [type, countItems(type)])) as Record;
+
+ return ctx.json({ ok: true, counts });
+});
diff --git a/src/servers/ensure-tool-loader.ts b/src/servers/ensure-tool-loader.ts
index ea07c74b..cb223eb6 100644
--- a/src/servers/ensure-tool-loader.ts
+++ b/src/servers/ensure-tool-loader.ts
@@ -1,11 +1,11 @@
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { join, resolve } from 'node:path';
-import { DATA_PATH } from './data-path';
+import { itemsDir } from './data-path';
const SOURCE_PATH = resolve(import.meta.dir, 'tool-loader-source.ts');
export function ensureToolLoader(): void {
- const targetDir = join(DATA_PATH, 'extensions', 'tool-loader');
+ const targetDir = join(itemsDir('extensions'), 'tool-loader');
const targetFile = join(targetDir, 'index.ts');
mkdirSync(targetDir, { recursive: true });
diff --git a/src/servers/hono.ts b/src/servers/hono.ts
index dfc065f6..cfee0dba 100644
--- a/src/servers/hono.ts
+++ b/src/servers/hono.ts
@@ -12,6 +12,7 @@ import { skillsRouter } from './api/skills/skills';
import { tasksRouter } from './api/tasks/tasks';
import { toolsRouter } from './api/tools/tools';
import { processesRouter } from './api/processes/processes';
+import { rescanRouter } from './api/items/rescan';
import { scrapeRouter } from './api/scrape/scrape';
import { uploadRouter } from './api/upload/upload';
import { settingsRouter } from './api/settings/settings';
@@ -85,6 +86,7 @@ protectedRouter.route('/skills', skillsRouter);
protectedRouter.route('/tasks', tasksRouter);
protectedRouter.route('/tools', toolsRouter);
protectedRouter.route('/processes', processesRouter);
+protectedRouter.route('/rescan', rescanRouter);
protectedRouter.route('/scrape', scrapeRouter);
protectedRouter.route('/upload', uploadRouter);
protectedRouter.route('/user', settingsRouter);