orionfold-relay 0.26.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1186,7 +1186,7 @@ var CURRENT_PLUGIN_API_VERSION, CAPABILITY_VALUES, ORIGIN_VALUES, PrimitivesBund
1186
1186
  var init_types = __esm({
1187
1187
  "src/lib/plugins/sdk/types.ts"() {
1188
1188
  "use strict";
1189
- CURRENT_PLUGIN_API_VERSION = "0.26";
1189
+ CURRENT_PLUGIN_API_VERSION = "0.27";
1190
1190
  CAPABILITY_VALUES = ["fs", "net", "child_process", "env"];
1191
1191
  ORIGIN_VALUES = ["ainative-internal", "third-party"];
1192
1192
  PrimitivesBundleManifestSchema = z.object({
@@ -12982,7 +12982,7 @@ var init_registry6 = __esm({
12982
12982
  init_registry5();
12983
12983
  init_installer();
12984
12984
  init_schedule_spec();
12985
- SUPPORTED_API_VERSIONS = /* @__PURE__ */ new Set([CURRENT_PLUGIN_API_VERSION, "0.25"]);
12985
+ SUPPORTED_API_VERSIONS = /* @__PURE__ */ new Set([CURRENT_PLUGIN_API_VERSION, "0.26"]);
12986
12986
  pluginCache = null;
12987
12987
  lastLoadedPluginIds = /* @__PURE__ */ new Set();
12988
12988
  PluginTableSchema = z16.object({
@@ -25913,8 +25913,8 @@ import { execFileSync as execFileSync3 } from "child_process";
25913
25913
  import yaml12 from "js-yaml";
25914
25914
  import semver from "semver";
25915
25915
  function relayCoreVersion() {
25916
- if (semver.valid("0.26.0")) {
25917
- return "0.26.0";
25916
+ if (semver.valid("0.27.0")) {
25917
+ return "0.27.0";
25918
25918
  }
25919
25919
  try {
25920
25920
  const root = getAppRoot(import.meta.dirname, 3);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orionfold-relay",
3
- "version": "0.26.0",
3
+ "version": "0.27.0",
4
4
  "description": "Orionfold Relay — a local-first, multi-agent orchestration runtime and builder scaffold for AI-native work.",
5
5
  "keywords": [
6
6
  "ai",
@@ -1,5 +1,6 @@
1
1
  import { listProfiles, isBuiltin } from "@/lib/agents/profiles/registry";
2
2
  import { sortProfilesByName } from "@/lib/agents/profiles/sort";
3
+ import { listApps } from "@/lib/apps/registry";
3
4
  import { ProfileBrowser } from "@/components/profiles/profile-browser";
4
5
  import { PageShell } from "@/components/shared/page-shell";
5
6
 
@@ -13,12 +14,17 @@ export default async function ProfilesPage() {
13
14
  }))
14
15
  );
15
16
 
17
+ // Installed packs — the source-of-truth set the client resolves each
18
+ // profile's pack provenance against (FEAT-8 pill / FEAT-7 filter via
19
+ // packOf). Just {id, name}; the pill renders the name, packOf gates on the id.
20
+ const installedPacks = listApps().map((a) => ({ id: a.id, name: a.name }));
21
+
16
22
  return (
17
23
  <PageShell
18
24
  title="Profiles"
19
25
  description="Browse and inspect agent profiles without blur-heavy detail surfaces."
20
26
  >
21
- <ProfileBrowser initialProfiles={profiles} />
27
+ <ProfileBrowser initialProfiles={profiles} installedPacks={installedPacks} />
22
28
  </PageShell>
23
29
  );
24
30
  }
@@ -1,6 +1,7 @@
1
1
  import { listTables } from "@/lib/data/tables";
2
2
  import { db } from "@/lib/db";
3
3
  import { projects } from "@/lib/db/schema";
4
+ import { listApps } from "@/lib/apps/registry";
4
5
  import { TableBrowser } from "@/components/tables/table-browser";
5
6
  import { PageShell } from "@/components/shared/page-shell";
6
7
 
@@ -13,9 +14,17 @@ export default async function TablesPage() {
13
14
  .select({ id: projects.id, name: projects.name })
14
15
  .from(projects);
15
16
 
17
+ // Installed packs — mark tables whose projectId is a pack with a pack pill
18
+ // (FEAT-8) instead of the plain project name.
19
+ const installedPacks = listApps().map((a) => ({ id: a.id, name: a.name }));
20
+
16
21
  return (
17
22
  <PageShell title="Tables">
18
- <TableBrowser initialTables={tables} projects={projectList} />
23
+ <TableBrowser
24
+ initialTables={tables}
25
+ projects={projectList}
26
+ installedPacks={installedPacks}
27
+ />
19
28
  </PageShell>
20
29
  );
21
30
  }
@@ -17,18 +17,52 @@ import { ProfileCard } from "@/components/profiles/profile-card";
17
17
  import { ProfileImportDialog } from "@/components/profiles/profile-import-dialog";
18
18
  import { RepoImportWizard } from "@/components/profiles/repo-import-wizard";
19
19
  import type { AgentProfile } from "@/lib/agents/profiles/types";
20
+ import { packOf } from "@/lib/apps/pack-of";
20
21
 
21
22
  interface ProfileWithBuiltin extends AgentProfile {
22
23
  isBuiltin?: boolean;
23
24
  }
24
25
 
26
+ /** The installed-pack identity a profile carries (FEAT-8). {id, name} only. */
27
+ export interface InstalledPackRef {
28
+ id: string;
29
+ name: string;
30
+ }
31
+
25
32
  interface ProfileBrowserProps {
26
33
  initialProfiles: AgentProfile[];
34
+ /** Installed packs — resolves each profile's pack provenance (FEAT-8). */
35
+ installedPacks?: InstalledPackRef[];
27
36
  }
28
37
 
29
- export function ProfileBrowser({ initialProfiles }: ProfileBrowserProps) {
38
+ export function ProfileBrowser({
39
+ initialProfiles,
40
+ installedPacks = [],
41
+ }: ProfileBrowserProps) {
30
42
  const router = useRouter();
31
43
  const [profiles, setProfiles] = useState<ProfileWithBuiltin[]>(initialProfiles);
44
+
45
+ // Stable {id → display name} lookup + the gated id-set for packOf. Rebuilt
46
+ // only when the installed packs change (never on a profile refresh), so the
47
+ // pill survives refreshProfiles — which recomputes from the refreshed id.
48
+ const packNameById = useMemo(
49
+ () => new Map(installedPacks.map((p) => [p.id, p.name])),
50
+ [installedPacks]
51
+ );
52
+ const installedPackIds = useMemo(
53
+ () => new Set(installedPacks.map((p) => p.id)),
54
+ [installedPacks]
55
+ );
56
+ const packNameFor = useCallback(
57
+ (profile: AgentProfile): string | null => {
58
+ const packId = packOf(
59
+ { kind: "profile", id: profile.id },
60
+ installedPackIds
61
+ );
62
+ return packId ? packNameById.get(packId) ?? null : null;
63
+ },
64
+ [installedPackIds, packNameById]
65
+ );
32
66
  const [search, setSearch] = useState("");
33
67
  const [domainFilter, setDomainFilter] = useState<
34
68
  "all" | "work" | "personal"
@@ -201,6 +235,7 @@ export function ProfileBrowser({ initialProfiles }: ProfileBrowserProps) {
201
235
  key={profile.id}
202
236
  profile={profile}
203
237
  isBuiltin={profile.isBuiltin}
238
+ packName={packNameFor(profile)}
204
239
  onClick={() => router.push(`/profiles/${profile.id}`)}
205
240
  />
206
241
  ))}
@@ -6,11 +6,14 @@ import { Download } from "lucide-react";
6
6
  import type { AgentRuntimeId } from "@/lib/agents/runtime/catalog";
7
7
  import { getSupportedRuntimes } from "@/lib/agents/profiles/compatibility";
8
8
  import { IconCircle, getProfileIcon, getDomainColors } from "@/lib/constants/card-icons";
9
+ import { PackPill } from "@/components/shared/pack-pill";
9
10
  import type { AgentProfile } from "@/lib/agents/profiles/types";
10
11
 
11
12
  interface ProfileCardProps {
12
13
  profile: AgentProfile;
13
14
  isBuiltin?: boolean;
15
+ /** Display name of the pack that installed this profile, or null (FEAT-8). */
16
+ packName?: string | null;
14
17
  onClick: () => void;
15
18
  }
16
19
 
@@ -30,7 +33,7 @@ const RUNTIME_SHORT_LABEL: Record<AgentRuntimeId, string> = {
30
33
  ollama: "Ollama (Local)",
31
34
  };
32
35
 
33
- export function ProfileCard({ profile, isBuiltin = false, onClick }: ProfileCardProps) {
36
+ export function ProfileCard({ profile, isBuiltin = false, packName = null, onClick }: ProfileCardProps) {
34
37
 
35
38
  return (
36
39
  <Card
@@ -83,7 +86,11 @@ export function ProfileCard({ profile, isBuiltin = false, onClick }: ProfileCard
83
86
  </div>
84
87
 
85
88
  <div className="flex items-center gap-3 text-xs text-muted-foreground">
86
- {profile.importMeta ? (
89
+ {/* Pack provenance outranks every other origin: a pack-installed
90
+ profile is never "Custom"/"Discovered" — it belongs to its pack. */}
91
+ {packName ? (
92
+ <PackPill packName={packName} />
93
+ ) : profile.importMeta ? (
87
94
  <span className="flex items-center gap-1.5">
88
95
  <Badge variant="outline" className="border-purple-200 text-purple-600 dark:border-purple-800 dark:text-purple-400">
89
96
  <Download className="mr-1 h-3 w-3" />
@@ -11,6 +11,8 @@ import { ScheduleStatusBadge } from "./schedule-status-badge";
11
11
  import { ConfirmDialog } from "@/components/shared/confirm-dialog";
12
12
  import { EmptyState } from "@/components/shared/empty-state";
13
13
  import { describeCron } from "@/lib/schedules/interval-parser";
14
+ import { PackPill } from "@/components/shared/pack-pill";
15
+ import { packOf } from "@/lib/apps/pack-of";
14
16
  import { Clock, Heart, Pause, Play, Trash2 } from "lucide-react";
15
17
  import { toast } from "sonner";
16
18
 
@@ -39,6 +41,9 @@ interface ScheduleListProps {
39
41
 
40
42
  export function ScheduleList({ projects, initialSelectedId }: ScheduleListProps) {
41
43
  const [schedules, setSchedules] = useState<Schedule[]>([]);
44
+ const [installedPacks, setInstalledPacks] = useState<
45
+ { id: string; name: string }[]
46
+ >([]);
42
47
  const [loaded, setLoaded] = useState(false);
43
48
  const [confirmDeleteId, setConfirmDeleteId] = useState<string | null>(null);
44
49
  const [selectedScheduleId, setSelectedScheduleId] = useState<string | null>(
@@ -57,6 +62,24 @@ export function ScheduleList({ projects, initialSelectedId }: ScheduleListProps)
57
62
  refresh();
58
63
  }, [refresh]);
59
64
 
65
+ // Installed packs — a pack schedule's id is `app:<packId>:<sid>`, so packOf
66
+ // resolves provenance from the id alone (FEAT-8).
67
+ useEffect(() => {
68
+ fetch("/api/apps")
69
+ .then((r) => (r.ok ? r.json() : []))
70
+ .then((apps: Array<{ id: string; name: string }>) =>
71
+ setInstalledPacks(apps.map((a) => ({ id: a.id, name: a.name })))
72
+ )
73
+ .catch(() => {});
74
+ }, []);
75
+
76
+ const installedPackIds = new Set(installedPacks.map((p) => p.id));
77
+ const packNameById = new Map(installedPacks.map((p) => [p.id, p.name]));
78
+ const packNameForSchedule = (id: string): string | null => {
79
+ const packId = packOf({ kind: "schedule", id }, installedPackIds);
80
+ return packId ? packNameById.get(packId) ?? null : null;
81
+ };
82
+
60
83
  async function handlePauseResume(id: string, currentStatus: string) {
61
84
  const newStatus = currentStatus === "active" ? "paused" : "active";
62
85
  const res = await fetch(`/api/schedules/${id}`, {
@@ -163,7 +186,13 @@ export function ScheduleList({ projects, initialSelectedId }: ScheduleListProps)
163
186
  )}
164
187
  {sched.name}
165
188
  </CardTitle>
166
- <ScheduleStatusBadge status={sched.status} />
189
+ <div className="flex shrink-0 items-center gap-1.5">
190
+ {(() => {
191
+ const packName = packNameForSchedule(sched.id);
192
+ return packName ? <PackPill packName={packName} /> : null;
193
+ })()}
194
+ <ScheduleStatusBadge status={sched.status} />
195
+ </div>
167
196
  </div>
168
197
  </CardHeader>
169
198
  <CardContent>
@@ -0,0 +1,39 @@
1
+ import { Package } from "lucide-react";
2
+ import { Badge } from "@/components/ui/badge";
3
+ import { cn } from "@/lib/utils";
4
+
5
+ interface PackPillProps {
6
+ /** The installed pack's display name (e.g. "Relay Agency"), from its manifest. */
7
+ packName: string;
8
+ className?: string;
9
+ }
10
+
11
+ /**
12
+ * PackPill — provenance label marking a primitive (profile, blueprint, table,
13
+ * schedule) as installed by a pack (FEAT-8, spec:
14
+ * features/fix-app-shell-activation-redesign.md).
15
+ *
16
+ * Deliberately NOT a StatusChip / status-family: pack provenance is an open-set
17
+ * identity ("which pack"), not one of the 5 fixed status dimensions. It shares
18
+ * the visual system by building on the same `Badge` primitive StatusChip uses,
19
+ * with its own icon (package) and a distinct amber color family so it reads as
20
+ * a provenance marker — the sibling of the card's existing Imported/Discovered/
21
+ * Built-in badges, and outranks the "Custom" fallback (a pack-installed
22
+ * primitive is never "Custom").
23
+ */
24
+ export function PackPill({ packName, className }: PackPillProps) {
25
+ return (
26
+ <Badge
27
+ data-testid="pack-pill"
28
+ variant="outline"
29
+ title={`Installed by the ${packName} pack`}
30
+ className={cn(
31
+ "border-amber-200 text-amber-700 dark:border-amber-800 dark:text-amber-400",
32
+ className
33
+ )}
34
+ >
35
+ <Package className="mr-1 h-3 w-3" />
36
+ {packName}
37
+ </Badge>
38
+ );
39
+ }
@@ -19,15 +19,37 @@ import { TableCreateSheet } from "./table-create-sheet";
19
19
  import { FilterBar } from "@/components/shared/filter-bar";
20
20
  import { EmptyState } from "@/components/shared/empty-state";
21
21
  import { Table2 } from "lucide-react";
22
+ import { packOf } from "@/lib/apps/pack-of";
22
23
  import type { TableWithRelations } from "./types";
23
24
 
24
25
  interface TableBrowserProps {
25
26
  initialTables: TableWithRelations[];
26
27
  projects: { id: string; name: string }[];
28
+ /** Installed packs — marks tables whose project is a pack (FEAT-8). */
29
+ installedPacks?: { id: string; name: string }[];
27
30
  }
28
31
 
29
- export function TableBrowser({ initialTables, projects }: TableBrowserProps) {
32
+ export function TableBrowser({
33
+ initialTables,
34
+ projects,
35
+ installedPacks = [],
36
+ }: TableBrowserProps) {
30
37
  const [tables, setTables] = useState(initialTables);
38
+ // {projectId → pack name} for pack-installed projects, via the shared
39
+ // resolver (tables associate to a pack by projectId === packId).
40
+ const installedPackIds = new Set(installedPacks.map((p) => p.id));
41
+ const packNameById = new Map(installedPacks.map((p) => [p.id, p.name]));
42
+ const packNameForProject = useCallback(
43
+ (projectId: string | null | undefined): string | null => {
44
+ const packId = packOf(
45
+ { kind: "table", id: "", projectId: projectId ?? undefined },
46
+ installedPackIds
47
+ );
48
+ return packId ? packNameById.get(packId) ?? null : null;
49
+ },
50
+ // eslint-disable-next-line react-hooks/exhaustive-deps
51
+ [installedPacks]
52
+ );
31
53
  const [view, setView] = useState<"table" | "grid">("table");
32
54
  const [search, setSearch] = useState("");
33
55
  const [sourceFilter, setSourceFilter] = useState<string>("all");
@@ -214,12 +236,14 @@ export function TableBrowser({ initialTables, projects }: TableBrowserProps) {
214
236
  onToggleSelectAll={toggleSelectAll}
215
237
  onSelect={navigate}
216
238
  onOpen={navigate}
239
+ packNameForProject={packNameForProject}
217
240
  />
218
241
  ) : (
219
242
  <TableGrid
220
243
  tables={filtered}
221
244
  onSelect={navigate}
222
245
  onOpen={navigate}
246
+ packNameForProject={packNameForProject}
223
247
  />
224
248
  )}
225
249
 
@@ -4,6 +4,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
4
4
  import { Badge } from "@/components/ui/badge";
5
5
  import { Table2 } from "lucide-react";
6
6
  import { tableSourceVariant } from "@/lib/constants/table-status";
7
+ import { PackPill } from "@/components/shared/pack-pill";
7
8
  import { formatRowCount, formatColumnCount } from "./utils";
8
9
  import type { TableWithRelations } from "./types";
9
10
 
@@ -11,9 +12,16 @@ interface TableGridProps {
11
12
  tables: TableWithRelations[];
12
13
  onSelect: (id: string) => void;
13
14
  onOpen: (id: string) => void;
15
+ /** Resolves a projectId to its pack display name, or null (FEAT-8). */
16
+ packNameForProject?: (projectId: string | null | undefined) => string | null;
14
17
  }
15
18
 
16
- export function TableGrid({ tables, onSelect, onOpen }: TableGridProps) {
19
+ export function TableGrid({
20
+ tables,
21
+ onSelect,
22
+ onOpen,
23
+ packNameForProject,
24
+ }: TableGridProps) {
17
25
  return (
18
26
  <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
19
27
  {tables.map((t) => (
@@ -49,9 +57,13 @@ export function TableGrid({ tables, onSelect, onOpen }: TableGridProps) {
49
57
  <div className="flex items-center gap-3 text-xs text-muted-foreground">
50
58
  <span>{formatColumnCount(t.columnCount)}</span>
51
59
  <span>{formatRowCount(t.rowCount)}</span>
52
- {t.projectName && (
53
- <span className="truncate">{t.projectName}</span>
54
- )}
60
+ {(() => {
61
+ const packName = packNameForProject?.(t.projectId);
62
+ if (packName) return <PackPill packName={packName} />;
63
+ return t.projectName ? (
64
+ <span className="truncate">{t.projectName}</span>
65
+ ) : null;
66
+ })()}
55
67
  </div>
56
68
  </CardContent>
57
69
  </Card>
@@ -11,6 +11,7 @@ import {
11
11
  TableRow,
12
12
  } from "@/components/ui/table";
13
13
  import { tableSourceVariant } from "@/lib/constants/table-status";
14
+ import { PackPill } from "@/components/shared/pack-pill";
14
15
  import { formatRowCount } from "./utils";
15
16
  import type { TableWithRelations } from "./types";
16
17
 
@@ -21,6 +22,8 @@ interface TableListTableProps {
21
22
  onToggleSelectAll: () => void;
22
23
  onSelect: (id: string) => void;
23
24
  onOpen: (id: string) => void;
25
+ /** Resolves a projectId to its pack display name, or null (FEAT-8). */
26
+ packNameForProject?: (projectId: string | null | undefined) => string | null;
24
27
  }
25
28
 
26
29
  export function TableListTable({
@@ -30,6 +33,7 @@ export function TableListTable({
30
33
  onToggleSelectAll,
31
34
  onSelect,
32
35
  onOpen,
36
+ packNameForProject,
33
37
  }: TableListTableProps) {
34
38
  return (
35
39
  <div className="rounded-lg border">
@@ -68,7 +72,14 @@ export function TableListTable({
68
72
  </TableCell>
69
73
  <TableCell className="font-medium">{t.name}</TableCell>
70
74
  <TableCell className="text-muted-foreground">
71
- {t.projectName ?? "—"}
75
+ {(() => {
76
+ const packName = packNameForProject?.(t.projectId);
77
+ return packName ? (
78
+ <PackPill packName={packName} />
79
+ ) : (
80
+ (t.projectName ?? "—")
81
+ );
82
+ })()}
72
83
  </TableCell>
73
84
  <TableCell className="text-right text-muted-foreground">
74
85
  {t.columnCount}
@@ -12,8 +12,16 @@ import { Button } from "@/components/ui/button";
12
12
  import { Search, Layers, Plus } from "lucide-react";
13
13
  import { patternLabels } from "@/lib/constants/status-colors";
14
14
  import { IconCircle, getWorkflowIconFromName } from "@/lib/constants/card-icons";
15
+ import { PackPill } from "@/components/shared/pack-pill";
16
+ import { packOf } from "@/lib/apps/pack-of";
15
17
  import type { WorkflowBlueprint } from "@/lib/workflows/blueprints/types";
16
18
 
19
+ /** {id, name} of an installed pack — fetched from /api/apps for provenance. */
20
+ interface InstalledPack {
21
+ id: string;
22
+ name: string;
23
+ }
24
+
17
25
  const difficultyColors: Record<string, string> = {
18
26
  beginner: "border-green-500/30 bg-green-500/10 text-green-700 dark:text-green-400",
19
27
  intermediate: "border-yellow-500/30 bg-yellow-500/10 text-yellow-700 dark:text-yellow-400",
@@ -23,21 +31,51 @@ const difficultyColors: Record<string, string> = {
23
31
  export function BlueprintGallery() {
24
32
  const router = useRouter();
25
33
  const [blueprints, setBlueprints] = useState<WorkflowBlueprint[]>([]);
34
+ const [installedPacks, setInstalledPacks] = useState<InstalledPack[]>([]);
26
35
  const [loaded, setLoaded] = useState(false);
27
36
  const [search, setSearch] = useState("");
28
37
  const [domainFilter, setDomainFilter] = useState<"all" | "work" | "personal">("all");
38
+ // FEAT-7 — "all" or a specific installed pack id.
39
+ const [packFilter, setPackFilter] = useState<string>("all");
29
40
 
30
41
  useEffect(() => {
31
- fetch("/api/blueprints")
32
- .then((r) => (r.ok ? r.json() : []))
33
- .then((data) => setBlueprints(data))
42
+ // Blueprints + installed packs in parallel; the pack list resolves each
43
+ // blueprint's provenance (packOf) for the pill (FEAT-8) and filter (FEAT-7).
44
+ Promise.all([
45
+ fetch("/api/blueprints").then((r) => (r.ok ? r.json() : [])),
46
+ fetch("/api/apps").then((r) => (r.ok ? r.json() : [])),
47
+ ])
48
+ .then(([bps, apps]) => {
49
+ setBlueprints(bps);
50
+ setInstalledPacks(
51
+ (apps as Array<{ id: string; name: string }>).map((a) => ({
52
+ id: a.id,
53
+ name: a.name,
54
+ }))
55
+ );
56
+ })
34
57
  .finally(() => setLoaded(true));
35
58
  }, []);
36
59
 
60
+ const packNameById = useMemo(
61
+ () => new Map(installedPacks.map((p) => [p.id, p.name])),
62
+ [installedPacks]
63
+ );
64
+ const installedPackIds = useMemo(
65
+ () => new Set(installedPacks.map((p) => p.id)),
66
+ [installedPacks]
67
+ );
68
+ const packIdFor = (bp: WorkflowBlueprint) =>
69
+ packOf({ kind: "blueprint", id: bp.id }, installedPackIds);
70
+
37
71
  const filtered = useMemo(() => {
38
72
  const q = search.toLowerCase();
39
73
  return blueprints.filter((bp) => {
40
74
  if (domainFilter !== "all" && bp.domain !== domainFilter) return false;
75
+ if (packFilter !== "all") {
76
+ if (packOf({ kind: "blueprint", id: bp.id }, installedPackIds) !== packFilter)
77
+ return false;
78
+ }
41
79
  if (!q) return true;
42
80
  return (
43
81
  bp.name.toLowerCase().includes(q) ||
@@ -45,7 +83,7 @@ export function BlueprintGallery() {
45
83
  bp.tags.some((t) => t.toLowerCase().includes(q))
46
84
  );
47
85
  });
48
- }, [blueprints, search, domainFilter]);
86
+ }, [blueprints, search, domainFilter, packFilter, installedPackIds]);
49
87
 
50
88
  return (
51
89
  <div className="space-y-6">
@@ -84,6 +122,23 @@ export function BlueprintGallery() {
84
122
  <TabsTrigger value="personal">Personal</TabsTrigger>
85
123
  </TabsList>
86
124
  </Tabs>
125
+ {/* FEAT-7 — filter by installed pack. Only shown when a pack is
126
+ installed, so the control never appears empty on a fresh instance. */}
127
+ {installedPacks.length > 0 && (
128
+ <select
129
+ value={packFilter}
130
+ onChange={(e) => setPackFilter(e.target.value)}
131
+ aria-label="Filter by pack"
132
+ className="h-9 rounded-md border border-input bg-background px-3 text-sm"
133
+ >
134
+ <option value="all">All packs</option>
135
+ {installedPacks.map((p) => (
136
+ <option key={p.id} value={p.id}>
137
+ {p.name}
138
+ </option>
139
+ ))}
140
+ </select>
141
+ )}
87
142
  </div>
88
143
 
89
144
  {/* Grid */}
@@ -141,6 +196,15 @@ export function BlueprintGallery() {
141
196
  <p className="text-xs text-muted-foreground line-clamp-2">
142
197
  {bp.description}
143
198
  </p>
199
+ {(() => {
200
+ const packId = packIdFor(bp);
201
+ const packName = packId ? packNameById.get(packId) : null;
202
+ return packName ? (
203
+ <div className="mt-2">
204
+ <PackPill packName={packName} />
205
+ </div>
206
+ ) : null;
207
+ })()}
144
208
  <div className="flex flex-wrap items-center gap-2 mt-2 text-xs text-muted-foreground">
145
209
  <span>{patternLabels[bp.pattern] ?? bp.pattern}</span>
146
210
  <span>&middot;</span>
@@ -0,0 +1,83 @@
1
+ import { extractAppIdFromArtifactId } from "./composition-detector";
2
+ import { parseAppScheduleId } from "./app-schedule-id";
3
+
4
+ /**
5
+ * Primitive → pack source-of-truth resolver (spec:
6
+ * features/fix-app-shell-activation-redesign.md → "Grooming decision (S40)").
7
+ *
8
+ * The pack that installed a primitive is NOT recorded as a first-class field —
9
+ * it is encoded by convention, differently per kind:
10
+ *
11
+ * - profiles / blueprints : the `<packId>--<name>` id prefix (file-dropped
12
+ * verbatim from the pack manifest, where `prefix === pack.meta.id`).
13
+ * - tables : `projectId === pack.meta.id` (a DB column set at
14
+ * install; the id itself is a fresh UUID).
15
+ * - schedules : the `app:<packId>:<sid>` composite id (most
16
+ * specific) AND `projectId === pack.meta.id`.
17
+ *
18
+ * This resolver unifies those signals behind ONE function so the four listing
19
+ * views (FEAT-7 filter) and the provenance pill (FEAT-8) never branch on a raw
20
+ * id shape, and so the pack-aware seed gate (BUG-6) has a single question to
21
+ * ask. Chosen over adding a persisted `packId` field because the prefix already
22
+ * encodes the answer AND is load-bearing for uninstall (`deleteAppCascade`) —
23
+ * a new field would force an uninstall rewrite + a backfill migration for zero
24
+ * functional gain (Principles #5/#7).
25
+ *
26
+ * PURE by design: the installed-pack set is passed in, never read here. That
27
+ * keeps the resolver testable without a filesystem/DB and usable both in a
28
+ * server component (pass `new Set(listApps().map(a => a.id))`) and client-side
29
+ * (pass a prefetched set). No I/O, no runtime-registry-adjacent imports.
30
+ *
31
+ * The installed-set gate is the whole point: a `--` id or a `projectId` alone
32
+ * is ambiguous — a user's hand-authored `my-notes--triage` profile or a normal
33
+ * project must NOT be mis-attributed to a pack. A candidate pack id is only
34
+ * returned when it is a member of the installed set.
35
+ */
36
+
37
+ export type PackableKind = "profile" | "blueprint" | "table" | "schedule";
38
+
39
+ export interface PackablePrimitive {
40
+ kind: PackableKind;
41
+ /** The primitive's id: `<pack>--<name>` (files), a UUID (tables), or `app:<pack>:<sid>` (schedules). */
42
+ id: string;
43
+ /** DB column for tables/schedules; equals the pack id when pack-installed. Absent on file kinds. */
44
+ projectId?: string | null;
45
+ }
46
+
47
+ /**
48
+ * Resolve the id of the pack that installed `primitive`, or `null` when it is
49
+ * not attributable to any installed pack. `installedPackIds` is the gate — a
50
+ * candidate is returned only if it is a member.
51
+ */
52
+ export function packOf(
53
+ primitive: PackablePrimitive,
54
+ installedPackIds: ReadonlySet<string>
55
+ ): string | null {
56
+ const candidate = candidatePackId(primitive);
57
+ if (candidate && installedPackIds.has(candidate)) return candidate;
58
+ return null;
59
+ }
60
+
61
+ /**
62
+ * The best pack-id candidate from the primitive's own signals, BEFORE the
63
+ * installed-set gate. Order per kind is deliberate: the most specific,
64
+ * least-mutable signal wins (schedules prefer the composite id over the
65
+ * user-reassignable projectId).
66
+ */
67
+ function candidatePackId(primitive: PackablePrimitive): string | null {
68
+ switch (primitive.kind) {
69
+ case "profile":
70
+ case "blueprint":
71
+ return extractAppIdFromArtifactId(primitive.id);
72
+ case "table":
73
+ return normalizeProjectId(primitive.projectId);
74
+ case "schedule": {
75
+ const fromId = parseAppScheduleId(primitive.id)?.appId ?? null;
76
+ return fromId ?? normalizeProjectId(primitive.projectId);
77
+ }
78
+ }
79
+ }
80
+
81
+ function normalizeProjectId(projectId: string | null | undefined): string | null {
82
+ return projectId && projectId.length > 0 ? projectId : null;
83
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Pack-aware seed step (BUG-6).
3
+ *
4
+ * `clearAllData()` wipes ALL user tables — including the ones a paid pack
5
+ * materialized at install (e.g. Agency Pro's `Engagements` ledger). The generic
6
+ * seed generators only recreate demo tables, so after a seed the just-installed
7
+ * pack's cockpit reads an empty table ("No transactions yet") and every windowed
8
+ * KPI is zero. That is the reported symptom.
9
+ *
10
+ * Fix: after the generic seed, re-apply every installed pack via the SAME
11
+ * idempotent `installPack()` path that install uses. It rebuilds the pack's
12
+ * tables from its bundled manifest and re-seeds them from `seed/tables/*.json`
13
+ * (the ledger sample ships there). Reusing `installPack` — rather than
14
+ * hand-rolling create-table + addRows here — keeps the seed and install paths
15
+ * from ever drifting: whatever a pack ships as seed data, both surfaces produce
16
+ * identically.
17
+ *
18
+ * Best-effort per pack: one pack failing (e.g. an unlicensed premium pack whose
19
+ * license gate refuses) must not abort the whole seed. Failures are reported,
20
+ * never swallowed (engineering principle #1).
21
+ *
22
+ * `installPack` is runtime-registry-adjacent (CLAUDE.md smoke budget), so it is
23
+ * dynamically imported inside the function body, never a top-level static import.
24
+ */
25
+
26
+ export interface PackReseedResult {
27
+ packId: string;
28
+ tablesCreated: number;
29
+ rowsSeeded: number;
30
+ error?: string;
31
+ }
32
+
33
+ export interface ReseedInstalledPacksOptions {
34
+ /** Override the apps dir scanned for installed packs (defaults to data dir). */
35
+ appsDir?: string;
36
+ /** Override where a bare pack id resolves to its bundled template (tests). */
37
+ templatesDir?: string;
38
+ /** Override the profiles drop dir (threaded to installPack). */
39
+ profilesDir?: string;
40
+ /** Override the blueprints drop dir (threaded to installPack). */
41
+ blueprintsDir?: string;
42
+ }
43
+
44
+ /**
45
+ * Re-apply seed data for every installed pack. Returns one result per pack so
46
+ * the caller can fold the counts into the seed report and surface failures.
47
+ *
48
+ * Options exist mainly for tests (point `templatesDir`/`appsDir` at a fixture)
49
+ * and for non-default-data-dir instances; production passes none and the
50
+ * install machinery falls back to the configured data dir.
51
+ */
52
+ export async function reseedInstalledPacks(
53
+ options: ReseedInstalledPacksOptions = {}
54
+ ): Promise<PackReseedResult[]> {
55
+ const { listApps } = await import("@/lib/apps/registry");
56
+ const { installPack } = await import("@/lib/packs/install");
57
+
58
+ const installed = listApps(options.appsDir);
59
+ const results: PackReseedResult[] = [];
60
+
61
+ for (const app of installed) {
62
+ try {
63
+ // A bare pack id resolves to its bundled template (which carries the
64
+ // seed/tables/*.json files). installPack is idempotent: it reuses tables
65
+ // by name, dedupes rows by hash, and upserts schedules/profiles — so
66
+ // re-running it against an already-installed pack repopulates the tables
67
+ // clearAllData wiped and no-ops on everything already present.
68
+ const report = await installPack(app.id, {
69
+ appsDir: options.appsDir,
70
+ profilesDir: options.profilesDir,
71
+ blueprintsDir: options.blueprintsDir,
72
+ templatesDir: options.templatesDir,
73
+ });
74
+ results.push({
75
+ packId: app.id,
76
+ tablesCreated: report.tablesCreated,
77
+ rowsSeeded: report.rowsSeeded,
78
+ });
79
+ } catch (err) {
80
+ const message = err instanceof Error ? err.message : String(err);
81
+ // Named, visible failure — a premium pack without a valid license, a
82
+ // core-version mismatch, or a malformed manifest lands here. The seed
83
+ // continues; the reason travels back in the report.
84
+ console.error(
85
+ `[seed] pack re-seed failed for "${app.id}": ${message}`
86
+ );
87
+ results.push({
88
+ packId: app.id,
89
+ tablesCreated: 0,
90
+ rowsSeeded: 0,
91
+ error: message,
92
+ });
93
+ }
94
+ }
95
+
96
+ return results;
97
+ }
@@ -373,10 +373,34 @@ export async function seedSampleData() {
373
373
  tableIds: tableIdList,
374
374
  });
375
375
 
376
+ // 25. Pack-aware seed — repopulate every installed pack's tables (BUG-6).
377
+ // clearAllData() wiped the pack tables the customer just installed; re-apply
378
+ // each pack's bundled seed data via the idempotent install path so e.g. the
379
+ // Agency Pro ledger cockpit reads non-zero instead of "No transactions yet".
380
+ const { reseedInstalledPacks } = await import(
381
+ "./seed-data/installed-packs"
382
+ );
383
+ const packReseeds = await reseedInstalledPacks();
384
+ const packTablesSeeded = packReseeds.reduce(
385
+ (sum, p) => sum + p.tablesCreated,
386
+ 0
387
+ );
388
+ const packRowsSeeded = packReseeds.reduce((sum, p) => sum + p.rowsSeeded, 0);
389
+ const packReseedErrors = packReseeds.filter((p) => p.error);
390
+
376
391
  // Quiet the unused-`now` flag; the helpers above reuse Date.now() inline.
377
392
  void now;
378
393
 
379
394
  return {
395
+ packsReseeded: packReseeds.length,
396
+ packTablesSeeded,
397
+ packRowsSeeded,
398
+ // Surface any per-pack failures (unlicensed premium pack, bad manifest)
399
+ // rather than swallowing them — the route/UI can show what didn't seed.
400
+ packReseedErrors: packReseedErrors.map((p) => ({
401
+ packId: p.packId,
402
+ error: p.error as string,
403
+ })),
380
404
  profiles: profileCount,
381
405
  projects: projectSeeds.length,
382
406
  tasks: taskSeeds.length,
@@ -1,5 +1,5 @@
1
1
  id: relay-agency-pro
2
- version: "0.3.0"
2
+ version: "0.4.0"
3
3
  name: Relay Agency Pro
4
4
  description: >
5
5
  The agency operating system on top of the free Relay Agency verbs: a
@@ -54,10 +54,16 @@ tables:
54
54
  # The engagements ledger: one row per billing/cost line, signed amount
55
55
  # (+billing, -cost), so the ledger kit's inflow/outflow/margin math works
56
56
  # without configuration. The month-end close writes draft invoice lines here.
57
+ # SEEDED: seed/tables/engagements.json ships a current-month sample ledger so
58
+ # the finance cockpit's MTD KPIs (billed/costs/margin) read non-zero the moment
59
+ # the pack installs. Only THIS table is seeded — intake/grants below carry
60
+ # row-insert triggers, and seeding them would dispatch their pipeline
61
+ # blueprints on install/seed (see src/lib/data/tables.ts addRows). Work queues
62
+ # are meant to be filled by the operator to TRIGGER work, so they ship empty.
57
63
  - id: engagements
58
64
  columns: [client, date, category, description, amount, status]
59
65
  # The intake work queue: drop a row, the intake pipeline fires (row-insert
60
- # trigger) and routes by `kind` under the named client.
66
+ # trigger) and routes by `kind` under the named client. Ships empty by design.
61
67
  - id: intake
62
68
  columns: [client, kind, source, status, notes]
63
69
  # The grant pipeline: drop an opportunity row, the deep grant pipeline
@@ -0,0 +1,28 @@
1
+ [
2
+ { "client": "Meridian Commercial Realty", "date": "2026-07-01", "category": "Retainer", "description": "July retainer — monthly CRE marketing", "amount": "1200", "status": "billed" },
3
+ { "client": "Meridian Commercial Realty", "date": "2026-07-02", "category": "Project", "description": "Listing photography package — 3 properties", "amount": "2400", "status": "billed" },
4
+ { "client": "Meridian Commercial Realty", "date": "2026-07-02", "category": "Labor", "description": "Account team hours — Meridian", "amount": "-1450", "status": "posted" },
5
+ { "client": "Meridian Commercial Realty", "date": "2026-07-02", "category": "AI cost", "description": "Prospect research + proposal drafting agents", "amount": "-190", "status": "posted" },
6
+ { "client": "Summit CRE Advisors", "date": "2026-07-01", "category": "Retainer", "description": "July retainer — advisory + content", "amount": "600", "status": "billed" },
7
+ { "client": "Summit CRE Advisors", "date": "2026-07-03", "category": "Project", "description": "Q3 market report production", "amount": "1800", "status": "billed" },
8
+ { "client": "Summit CRE Advisors", "date": "2026-07-03", "category": "Labor", "description": "Analyst + editor hours — market report", "amount": "-980", "status": "posted" },
9
+ { "client": "Summit CRE Advisors", "date": "2026-07-03", "category": "AI cost", "description": "Market-report research agent runs", "amount": "-140", "status": "posted" },
10
+ { "client": "Parkview Property Management", "date": "2026-07-01", "category": "Retainer", "description": "July retainer — leasing marketing", "amount": "500", "status": "billed" },
11
+ { "client": "Parkview Property Management", "date": "2026-07-04", "category": "Project", "description": "Tenant renewal campaign build", "amount": "1500", "status": "draft" },
12
+ { "client": "Parkview Property Management", "date": "2026-07-02", "category": "Subcontractor", "description": "Freelance copywriter — renewal emails", "amount": "-350", "status": "posted" },
13
+ { "client": "Parkview Property Management", "date": "2026-07-02", "category": "Labor", "description": "Campaign build hours", "amount": "-720", "status": "posted" },
14
+ { "client": "Community Impact Alliance", "date": "2026-07-01", "category": "Retainer", "description": "July retainer — grants + comms", "amount": "450", "status": "billed" },
15
+ { "client": "Community Impact Alliance", "date": "2026-07-03", "category": "Project", "description": "Annual report design + copy", "amount": "2200", "status": "billed" },
16
+ { "client": "Community Impact Alliance", "date": "2026-07-03", "category": "Labor", "description": "Design + copy hours — annual report", "amount": "-1180", "status": "posted" },
17
+ { "client": "Community Impact Alliance", "date": "2026-07-03", "category": "AI cost", "description": "Grant-fit scoring + LOI drafting agents", "amount": "-160", "status": "posted" },
18
+ { "client": "Cornerstone Community Foundation", "date": "2026-07-01", "category": "Retainer", "description": "July retainer — donor communications", "amount": "400", "status": "billed" },
19
+ { "client": "Cornerstone Community Foundation", "date": "2026-07-04", "category": "Project", "description": "Year-end giving campaign kickoff", "amount": "1900", "status": "draft" },
20
+ { "client": "Cornerstone Community Foundation", "date": "2026-07-02", "category": "Labor", "description": "Strategy + kickoff hours", "amount": "-850", "status": "posted" },
21
+ { "client": "Cornerstone Community Foundation", "date": "2026-07-02", "category": "Software", "description": "Email platform pass-through — July", "amount": "-90", "status": "posted" },
22
+ { "client": "Lakeshore Family Services", "date": "2026-07-01", "category": "Retainer", "description": "July retainer — program marketing", "amount": "200", "status": "billed" },
23
+ { "client": "Lakeshore Family Services", "date": "2026-07-02", "category": "Project", "description": "Volunteer recruitment landing page", "amount": "800", "status": "billed" },
24
+ { "client": "Lakeshore Family Services", "date": "2026-07-02", "category": "Labor", "description": "Landing page build hours", "amount": "-410", "status": "posted" },
25
+ { "client": "Lakeshore Family Services", "date": "2026-07-02", "category": "AI cost", "description": "Intake routing + response drafting", "amount": "-70", "status": "posted" },
26
+ { "client": "Agency overhead", "date": "2026-07-01", "category": "Software", "description": "Design + scheduling tool subscriptions", "amount": "-420", "status": "posted" },
27
+ { "client": "Agency overhead", "date": "2026-07-01", "category": "AI cost", "description": "Month-end close automation runs", "amount": "-110", "status": "posted" }
28
+ ]
@@ -1,5 +1,5 @@
1
1
  id: relay-agency-pro
2
- version: "0.3.0"
2
+ version: "0.4.0"
3
3
  name: Relay Agency Pro
4
4
  author: Orionfold
5
5
  # This description renders as the what-you-get preview on the locked /packs
@@ -65,4 +65,8 @@ changelog:
65
65
  A new home screen that shows all six workflows as cards you can run with
66
66
  one click. It tells you where to start, and each card shows its last run.
67
67
  No more hunting through menus to find what your app can do.
68
+ "0.4.0": >-
69
+ Your finance cockpit now arrives with a month of sample billing and cost
70
+ entries, so you see real numbers the moment you install. Billed, costs, and
71
+ margin light up right away instead of a blank ledger.
68
72
  customers: []
@@ -1,6 +1,6 @@
1
1
  id: echo-server
2
2
  version: 0.1.0
3
- apiVersion: "0.26"
3
+ apiVersion: "0.27"
4
4
  kind: chat-tools
5
5
  name: Echo Server
6
6
  description: |
@@ -1,6 +1,6 @@
1
1
  id: finance-pack
2
2
  version: 0.1.0
3
- apiVersion: "0.26"
3
+ apiVersion: "0.27"
4
4
  kind: primitives-bundle
5
5
  name: Finance Pack
6
6
  description: |
@@ -1,6 +1,6 @@
1
1
  id: reading-radar
2
2
  version: 0.1.0
3
- apiVersion: "0.26"
3
+ apiVersion: "0.27"
4
4
  kind: primitives-bundle
5
5
  name: Reading Radar
6
6
  description: |
@@ -53,7 +53,7 @@ import type { ScheduleSpec } from "@/lib/validators/schedule-spec";
53
53
  // unfixed from 0.15.0 through 0.16.0 — treat the window test's failure as
54
54
  // a release blocker, not noise). The 0.13→0.14 three-MINOR bridge is over;
55
55
  // this is the standard 2-MINOR window now.
56
- const SUPPORTED_API_VERSIONS = new Set([CURRENT_PLUGIN_API_VERSION, "0.25"]);
56
+ const SUPPORTED_API_VERSIONS = new Set([CURRENT_PLUGIN_API_VERSION, "0.26"]);
57
57
 
58
58
  /** Test-helper export so the window-enforcement test can read state. */
59
59
  export function isSupportedApiVersion(apiVersion: string): boolean {
@@ -6,7 +6,7 @@ import { z } from "zod";
6
6
  // (a hardcoded copy there once drifted to "0.14" — scaffolded plugins would
7
7
  // have been disabled on load the moment the window tightened). Bump on every
8
8
  // MINOR release; api-version-window.test.ts fails if this goes stale.
9
- export const CURRENT_PLUGIN_API_VERSION = "0.26";
9
+ export const CURRENT_PLUGIN_API_VERSION = "0.27";
10
10
 
11
11
  // Shared capability tuple — single source of truth used by Zod schema and
12
12
  // capability-check.ts hash derivation. Exported so consumers don't need a