unify agent items into a flat file-based store, drop the marketplace

Replace the marketplace service dependency and the native/global/user
scope tiers with a single external directory ($OFFICER_ITEMS_DIR) holding
skills, tools, tasks, processes and extensions as plain files.

- tasks move from Postgres to TASK.md files (new file-backed task layer);
  task editing now works, which the DB path never supported
- skills/tools/processes collapse into one shared file router (single dir)
- remove the marketplace client (sync-marketplace/sync-version) and the
  boot-time sync; pi-bridge/pi-manager/sandbox point at the flat store
- drop the dead tasks + vestigial skills/tools/processes/extensions +
  item_chats tables (migration 0004)
- one-time migration script exports DB tasks and consolidates disk items

Migration verified: all 6 tasks round-trip through the runtime parser
identically to their DB rows (pipeline steps, triggers, script impls and
agentic bodies all intact).

NOTE: not yet functionally tested end-to-end — every item (each task mode,
tool, skill, extension) still needs to be run/exercised in the app before
this is trusted. To be done manually.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 00:39:17 +00:00
co-authored by Claude Opus 4.8
parent 5fd3d4faac
commit f3492512ba
29 changed files with 2366 additions and 1357 deletions
@@ -8,14 +8,12 @@ import { ArrowLeft, Pencil, Plus, Check, X, Trash2, Search, ChevronRight } from
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
import { useClient } from 'hooks/useClient';
import { useAuth } from 'hooks/useAuth';
import { Card } from '@/components/Card';
import { usePiChat, EmbeddableChat } from 'officerdev';
type CapabilitySummary = {
dirName: string;
name: string;
description: string;
scope: 'native' | 'global' | 'user';
};
export type CapabilityDetail = CapabilitySummary & {
@@ -276,9 +274,9 @@ export const CapabilityChat = ({
}: CapabilityChatProps) => {
const seedFile = `${kind.toUpperCase()}.md`;
const genericPrefix = `<frontmatter>\ninput file: ${filePath}\n${seedFile}: ${filePath}\ndir: ${resourceDir}\n\nBe aware of any extra files alongside the same dir as the ${kind} file we're handling, for possible extra context. You can also, if pertinent, create scripts or other files that will help you in the future.\n</frontmatter>`;
const promptFrontmatter = isNew ? buildCreationPrefix(kind, filePath, resourceDir) ?? genericPrefix : genericPrefix;
const promptFrontmatter = isNew ? (buildCreationPrefix(kind, filePath, resourceDir) ?? genericPrefix) : genericPrefix;
const defaultInput = isNew
? description ?? `Help me create the content for this new ${kind} file`
? (description ?? `Help me create the content for this new ${kind} file`)
: `Help me understand and improve this ${kind} file`;
const pi = usePiChat(undefined, undefined, { replaceUrl: false });
@@ -294,14 +292,7 @@ export const CapabilityChat = ({
wasGenerating.current = pi.isGenerating;
}, [pi.isGenerating]);
return (
<EmbeddableChat
chat={pi}
defaultInput={defaultInput}
promptPrefix={promptFrontmatter}
className="h-full"
/>
);
return <EmbeddableChat chat={pi} defaultInput={defaultInput} promptPrefix={promptFrontmatter} className="h-full" />;
};
export const FrontmatterBlock = ({ yaml }: { yaml: string }) => {
@@ -323,7 +314,17 @@ export const FrontmatterBlock = ({ yaml }: { yaml: string }) => {
);
};
export const CapabilityList = ({ kind, endpoint, queryKey, selected, onSelect, onCreate, search: externalSearch, showCreate, onShowCreateChange }: CapabilityListProps) => {
export const CapabilityList = ({
kind,
endpoint,
queryKey,
selected,
onSelect,
onCreate,
search: externalSearch,
showCreate,
onShowCreateChange,
}: CapabilityListProps) => {
const client = useClient();
const qc = useQueryClient();
const [internalCreating, setInternalCreating] = useState(false);
@@ -437,13 +438,6 @@ export const CapabilityList = ({ kind, endpoint, queryKey, selected, onSelect, o
>
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-duck-dark truncate">{item.name}</span>
<span
className={`shrink-0 text-[10px] px-1.5 py-0.5 rounded-full font-medium ${
item.scope === 'user' ? 'bg-duck-teal/20 text-duck-teal' : 'bg-duck-dark/10 text-duck-dark/60'
}`}
>
{item.scope}
</span>
</div>
{item.description && <p className="text-xs text-duck-dark/50 mt-1 line-clamp-2">{item.description}</p>}
</button>
@@ -462,7 +456,6 @@ export const CapabilityList = ({ kind, endpoint, queryKey, selected, onSelect, o
export const CapabilityPage = ({ kind, endpoint, queryKey }: CapabilityPageProps) => {
const client = useClient();
const qc = useQueryClient();
const { user } = useAuth();
const [selected, setSelected] = useState<string | null>(null);
const [editing, setEditing] = useState(false);
const [isNew, setIsNew] = useState(false);
@@ -544,7 +537,7 @@ export const CapabilityPage = ({ kind, endpoint, queryKey }: CapabilityPageProps
<span className="text-sm font-medium text-duck-dark/70 flex-1">
{detail?.name ?? `Select a ${kind.toLowerCase()}`}
</span>
{detail && (detail.scope === 'user' || user?.role === 'Super Admin') && (
{detail && (
<>
<button
onClick={() => setEditing((e) => !e)}