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:
@@ -0,0 +1,6 @@
|
||||
DROP TABLE "extensions" CASCADE;--> statement-breakpoint
|
||||
DROP TABLE "item_chats" CASCADE;--> statement-breakpoint
|
||||
DROP TABLE "processes" CASCADE;--> statement-breakpoint
|
||||
DROP TABLE "skills" CASCADE;--> statement-breakpoint
|
||||
DROP TABLE "tasks" CASCADE;--> statement-breakpoint
|
||||
DROP TABLE "tools" CASCADE;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -29,6 +29,13 @@
|
||||
"when": 1773040399200,
|
||||
"tag": "0003_perpetual_james_howlett",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 4,
|
||||
"version": "7",
|
||||
"when": 1784593790333,
|
||||
"tag": "0004_true_annihilus",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -72,17 +72,6 @@ export {
|
||||
deleteSavedSession,
|
||||
} from './queries/saved-sessions';
|
||||
|
||||
export {
|
||||
getTasksForUser,
|
||||
getTaskById,
|
||||
getTaskByDirName,
|
||||
createTask,
|
||||
updateTask,
|
||||
deleteTask,
|
||||
upsertNativeTask,
|
||||
deleteNativeTasksNotIn,
|
||||
} from './queries/tasks';
|
||||
|
||||
export {
|
||||
createPipelineJob,
|
||||
getPipelineJob,
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
import { eq, or, and, isNull, notInArray, sql } from 'drizzle-orm';
|
||||
import { db } from '../db';
|
||||
import { tasks } from '../schema/agent-items';
|
||||
|
||||
export async function getTasksForUser(userId: number) {
|
||||
return db
|
||||
.select({
|
||||
id: tasks.id,
|
||||
dirName: tasks.dirName,
|
||||
name: tasks.name,
|
||||
description: tasks.description,
|
||||
mode: tasks.mode,
|
||||
language: tasks.language,
|
||||
version: tasks.version,
|
||||
scope: tasks.scope,
|
||||
trigger: tasks.trigger,
|
||||
userId: tasks.userId,
|
||||
})
|
||||
.from(tasks)
|
||||
.where(
|
||||
or(
|
||||
eq(tasks.scope, 'native'),
|
||||
eq(tasks.scope, 'global'),
|
||||
and(eq(tasks.scope, 'user'), eq(tasks.userId, userId)),
|
||||
),
|
||||
)
|
||||
.orderBy(tasks.name);
|
||||
}
|
||||
|
||||
export async function getTaskById(id: number) {
|
||||
const rows = await db.select().from(tasks).where(eq(tasks.id, id)).limit(1);
|
||||
return rows[0] ?? null;
|
||||
}
|
||||
|
||||
export async function getTaskByDirName(dirName: string, userId: number) {
|
||||
// User scope takes priority over global, which takes priority over native
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(tasks)
|
||||
.where(
|
||||
and(
|
||||
eq(tasks.dirName, dirName),
|
||||
or(
|
||||
eq(tasks.scope, 'native'),
|
||||
eq(tasks.scope, 'global'),
|
||||
and(eq(tasks.scope, 'user'), eq(tasks.userId, userId)),
|
||||
),
|
||||
),
|
||||
)
|
||||
.orderBy(sql`CASE scope WHEN 'user' THEN 0 WHEN 'global' THEN 1 ELSE 2 END`)
|
||||
.limit(1);
|
||||
|
||||
return rows[0] ?? null;
|
||||
}
|
||||
|
||||
type TaskInsert = typeof tasks.$inferInsert;
|
||||
|
||||
export async function createTask(data: TaskInsert) {
|
||||
const rows = await db.insert(tasks).values(data).returning();
|
||||
return rows[0]!;
|
||||
}
|
||||
|
||||
export async function updateTask(id: number, data: Partial<TaskInsert>) {
|
||||
const rows = await db
|
||||
.update(tasks)
|
||||
.set({ ...data, updatedAt: new Date() })
|
||||
.where(eq(tasks.id, id))
|
||||
.returning();
|
||||
return rows[0] ?? null;
|
||||
}
|
||||
|
||||
export async function deleteTask(id: number) {
|
||||
await db.delete(tasks).where(eq(tasks.id, id));
|
||||
}
|
||||
|
||||
export async function deleteNativeTasksNotIn(dirNames: string[]) {
|
||||
if (dirNames.length === 0) return [];
|
||||
const deleted = await db
|
||||
.delete(tasks)
|
||||
.where(and(eq(tasks.scope, 'native'), notInArray(tasks.dirName, dirNames)))
|
||||
.returning({ dirName: tasks.dirName });
|
||||
return deleted;
|
||||
}
|
||||
|
||||
export async function upsertNativeTask(data: Omit<TaskInsert, 'scope' | 'userId'>) {
|
||||
const existing = await db
|
||||
.select({ id: tasks.id })
|
||||
.from(tasks)
|
||||
.where(and(eq(tasks.dirName, data.dirName), eq(tasks.scope, 'native')))
|
||||
.limit(1);
|
||||
|
||||
if (existing.length > 0) {
|
||||
await db
|
||||
.update(tasks)
|
||||
.set({ ...data, updatedAt: new Date() })
|
||||
.where(eq(tasks.id, existing[0]!.id));
|
||||
} else {
|
||||
await db.insert(tasks).values({ ...data, scope: 'native', userId: null });
|
||||
}
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
import { pgTable, serial, text, integer, timestamp, jsonb, index, unique } from 'drizzle-orm/pg-core';
|
||||
import { users } from './auth';
|
||||
|
||||
// ── Shared columns pattern ──
|
||||
// Each table has: id, scope, userId, dirName, name, description, body, version, timestamps
|
||||
// Type-specific columns are added per table
|
||||
|
||||
// ── Tasks ──
|
||||
// Complex frontmatter: inputs, outputs, dependencies, triggers, config, tags, tools, skills
|
||||
|
||||
export const tasks = pgTable(
|
||||
'tasks',
|
||||
{
|
||||
id: serial('id').primaryKey(),
|
||||
scope: text('scope').notNull(),
|
||||
userId: integer('user_id').references(() => users.id, { onDelete: 'cascade' }),
|
||||
dirName: text('dir_name').notNull(),
|
||||
name: text('name').notNull(),
|
||||
description: text('description'),
|
||||
body: text('body'),
|
||||
version: integer('version').notNull().default(1),
|
||||
mode: text('mode').notNull().default('agentic'),
|
||||
language: text('language'),
|
||||
implementation: text('implementation'),
|
||||
args: jsonb('args').$type<string[]>(),
|
||||
tags: jsonb('tags').$type<string[]>(),
|
||||
tools: jsonb('tools').$type<string[]>(),
|
||||
skills: jsonb('skills').$type<string[]>(),
|
||||
inputs: jsonb('inputs'),
|
||||
outputs: jsonb('outputs'),
|
||||
dependencies: jsonb('dependencies'),
|
||||
config: jsonb('config'),
|
||||
trigger: jsonb('trigger'),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => [
|
||||
unique('uq_tasks_scope_user_dir').on(table.scope, table.userId, table.dirName),
|
||||
index('idx_tasks_scope').on(table.scope),
|
||||
index('idx_tasks_user').on(table.userId),
|
||||
],
|
||||
);
|
||||
|
||||
// ── Skills ──
|
||||
// Minimal frontmatter: name, description only. Rich markdown body.
|
||||
|
||||
export const skills = pgTable(
|
||||
'skills',
|
||||
{
|
||||
id: serial('id').primaryKey(),
|
||||
scope: text('scope').notNull(),
|
||||
userId: integer('user_id').references(() => users.id, { onDelete: 'cascade' }),
|
||||
dirName: text('dir_name').notNull(),
|
||||
name: text('name').notNull(),
|
||||
description: text('description'),
|
||||
body: text('body'),
|
||||
version: integer('version').notNull().default(1),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => [
|
||||
unique('uq_skills_scope_user_dir').on(table.scope, table.userId, table.dirName),
|
||||
index('idx_skills_scope').on(table.scope),
|
||||
index('idx_skills_user').on(table.userId),
|
||||
],
|
||||
);
|
||||
|
||||
// ── Processes ──
|
||||
// Same shape as skills. Represents documented workflows.
|
||||
|
||||
export const processes = pgTable(
|
||||
'processes',
|
||||
{
|
||||
id: serial('id').primaryKey(),
|
||||
scope: text('scope').notNull(),
|
||||
userId: integer('user_id').references(() => users.id, { onDelete: 'cascade' }),
|
||||
dirName: text('dir_name').notNull(),
|
||||
name: text('name').notNull(),
|
||||
description: text('description'),
|
||||
body: text('body'),
|
||||
version: integer('version').notNull().default(1),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => [
|
||||
unique('uq_processes_scope_user_dir').on(table.scope, table.userId, table.dirName),
|
||||
index('idx_processes_scope').on(table.scope),
|
||||
index('idx_processes_user').on(table.userId),
|
||||
],
|
||||
);
|
||||
|
||||
// ── Tools ──
|
||||
// Has implementation code, language, structured input params, label.
|
||||
|
||||
export const tools = pgTable(
|
||||
'tools',
|
||||
{
|
||||
id: serial('id').primaryKey(),
|
||||
scope: text('scope').notNull(),
|
||||
userId: integer('user_id').references(() => users.id, { onDelete: 'cascade' }),
|
||||
dirName: text('dir_name').notNull(),
|
||||
name: text('name').notNull(),
|
||||
label: text('label'),
|
||||
description: text('description'),
|
||||
body: text('body'),
|
||||
version: integer('version').notNull().default(1),
|
||||
language: text('language'),
|
||||
inputs: jsonb('inputs'),
|
||||
implementation: text('implementation'),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => [
|
||||
unique('uq_tools_scope_user_dir').on(table.scope, table.userId, table.dirName),
|
||||
index('idx_tools_scope').on(table.scope),
|
||||
index('idx_tools_user').on(table.userId),
|
||||
],
|
||||
);
|
||||
|
||||
// ── Extensions ──
|
||||
// Code-only, no markdown, no chat. Just implementation.
|
||||
|
||||
export const extensions = pgTable(
|
||||
'extensions',
|
||||
{
|
||||
id: serial('id').primaryKey(),
|
||||
scope: text('scope').notNull(),
|
||||
userId: integer('user_id').references(() => users.id, { onDelete: 'cascade' }),
|
||||
dirName: text('dir_name').notNull(),
|
||||
name: text('name').notNull(),
|
||||
implementation: text('implementation'),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => [
|
||||
unique('uq_extensions_scope_user_dir').on(table.scope, table.userId, table.dirName),
|
||||
index('idx_extensions_scope').on(table.scope),
|
||||
index('idx_extensions_user').on(table.userId),
|
||||
],
|
||||
);
|
||||
|
||||
// ── Item Chats ──
|
||||
// Chat history for tasks, skills, processes, resources, tools.
|
||||
// Uses polymorphic reference (item_type + item_id) instead of per-table FKs.
|
||||
|
||||
export const itemChats = pgTable(
|
||||
'item_chats',
|
||||
{
|
||||
id: text('id').primaryKey(),
|
||||
itemType: text('item_type').notNull(),
|
||||
itemId: integer('item_id').notNull(),
|
||||
messages: jsonb('messages').notNull().default([]),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => [
|
||||
unique('uq_item_chats_type_item').on(table.itemType, table.itemId),
|
||||
index('idx_item_chats_type_item').on(table.itemType, table.itemId),
|
||||
],
|
||||
);
|
||||
@@ -2,7 +2,6 @@ export * from './auth';
|
||||
export * from './user-data';
|
||||
export * from './chat';
|
||||
export * from './dashboards';
|
||||
export * from './agent-items';
|
||||
export * from './operations';
|
||||
export * from './server';
|
||||
export * from './email';
|
||||
|
||||
@@ -50,36 +50,6 @@ export type ScreenInsert = typeof Schema.screens.$inferInsert;
|
||||
export type ProjectSelect = typeof Schema.projects.$inferSelect;
|
||||
export type ProjectInsert = typeof Schema.projects.$inferInsert;
|
||||
|
||||
// ── Tasks ──
|
||||
|
||||
export type TaskSelect = typeof Schema.tasks.$inferSelect;
|
||||
export type TaskInsert = typeof Schema.tasks.$inferInsert;
|
||||
|
||||
// ── Skills ──
|
||||
|
||||
export type SkillSelect = typeof Schema.skills.$inferSelect;
|
||||
export type SkillInsert = typeof Schema.skills.$inferInsert;
|
||||
|
||||
// ── Processes ──
|
||||
|
||||
export type ProcessSelect = typeof Schema.processes.$inferSelect;
|
||||
export type ProcessInsert = typeof Schema.processes.$inferInsert;
|
||||
|
||||
// ── Tools ──
|
||||
|
||||
export type ToolSelect = typeof Schema.tools.$inferSelect;
|
||||
export type ToolInsert = typeof Schema.tools.$inferInsert;
|
||||
|
||||
// ── Extensions ──
|
||||
|
||||
export type ExtensionSelect = typeof Schema.extensions.$inferSelect;
|
||||
export type ExtensionInsert = typeof Schema.extensions.$inferInsert;
|
||||
|
||||
// ── Item Chats ──
|
||||
|
||||
export type ItemChatSelect = typeof Schema.itemChats.$inferSelect;
|
||||
export type ItemChatInsert = typeof Schema.itemChats.$inferInsert;
|
||||
|
||||
// ── Operations ──
|
||||
|
||||
export type TaskLogSelect = typeof Schema.taskLogs.$inferSelect;
|
||||
|
||||
Reference in New Issue
Block a user