orionfold-relay 0.25.1 → 0.26.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.25";
1189
+ CURRENT_PLUGIN_API_VERSION = "0.26";
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.24"]);
12985
+ SUPPORTED_API_VERSIONS = /* @__PURE__ */ new Set([CURRENT_PLUGIN_API_VERSION, "0.25"]);
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.25.1")) {
25917
- return "0.25.1";
25916
+ if (semver.valid("0.26.0")) {
25917
+ return "0.26.0";
25918
25918
  }
25919
25919
  try {
25920
25920
  const root = getAppRoot(import.meta.dirname, 3);
@@ -26092,6 +26092,11 @@ async function installPack(source, options = {}) {
26092
26092
  const { reloadBlueprints: reloadBlueprints2 } = await Promise.resolve().then(() => (init_registry3(), registry_exports3));
26093
26093
  reloadBlueprints2();
26094
26094
  }
26095
+ try {
26096
+ const { revalidateTag } = await import("next/cache");
26097
+ revalidateTag(`app-runtime:${pack.meta.id}`, { expire: 0 });
26098
+ } catch {
26099
+ }
26095
26100
  return {
26096
26101
  packId: pack.meta.id,
26097
26102
  packVersion: pack.meta.version,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orionfold-relay",
3
- "version": "0.25.1",
3
+ "version": "0.26.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",
@@ -4,7 +4,14 @@ import { isDataOpsAllowed } from "@/lib/data/staging-gate";
4
4
 
5
5
  export async function POST() {
6
6
  if (!isDataOpsAllowed()) {
7
- return NextResponse.json(null, { status: 404 });
7
+ return NextResponse.json(
8
+ {
9
+ success: false,
10
+ error:
11
+ "Clearing data is a staging-only tool and is disabled on this build.",
12
+ },
13
+ { status: 403 }
14
+ );
8
15
  }
9
16
 
10
17
  try {
@@ -4,7 +4,14 @@ import { isDataOpsAllowed } from "@/lib/data/staging-gate";
4
4
 
5
5
  export async function POST() {
6
6
  if (!isDataOpsAllowed()) {
7
- return NextResponse.json(null, { status: 404 });
7
+ return NextResponse.json(
8
+ {
9
+ success: false,
10
+ error:
11
+ "Sample data seeding is a staging-only tool and is disabled on this build.",
12
+ },
13
+ { status: 403 }
14
+ );
8
15
  }
9
16
 
10
17
  try {
@@ -14,10 +14,12 @@ import { ChannelsSection } from "@/components/settings/channels-section";
14
14
  import { InstanceSection } from "@/components/instance/instance-section";
15
15
  import { LicenseSection } from "@/components/settings/license-section";
16
16
  import { PageShell } from "@/components/shared/page-shell";
17
+ import { isDataOpsAllowed } from "@/lib/data/staging-gate";
17
18
 
18
19
  export const dynamic = "force-dynamic";
19
20
 
20
21
  export default function SettingsPage() {
22
+ const dataOpsAllowed = isDataOpsAllowed();
21
23
  return (
22
24
  <PageShell
23
25
  title="Settings"
@@ -38,7 +40,7 @@ export default function SettingsPage() {
38
40
  <BudgetGuardrailsSection />
39
41
  <PermissionsSections />
40
42
  <DatabaseSnapshotsSection />
41
- <DataManagementSection />
43
+ <DataManagementSection allowed={dataOpsAllowed} />
42
44
  </div>
43
45
  </PageShell>
44
46
  );
@@ -22,8 +22,8 @@ interface HeaderSlotProps {
22
22
  export function HeaderSlotView({ slot, manifestPane }: HeaderSlotProps) {
23
23
  const { title, description, status, actions, cadenceChip, runNowBlueprintId, runNowVariables, periodChip, triggerSourceChip } = slot;
24
24
  return (
25
- <div className="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
26
- <div className="min-w-0 flex-1">
25
+ <div className="flex flex-col gap-2 sm:flex-row sm:flex-wrap sm:items-center sm:justify-between sm:gap-x-4 sm:gap-y-2">
26
+ <div className="min-w-0 flex-1 sm:min-w-[16rem]">
27
27
  <h1 className="text-xl font-semibold tracking-tight line-clamp-2" title={title}>
28
28
  {title}
29
29
  </h1>
@@ -33,7 +33,13 @@ export function HeaderSlotView({ slot, manifestPane }: HeaderSlotProps) {
33
33
  </p>
34
34
  )}
35
35
  </div>
36
- <div className="flex flex-wrap items-center gap-2 mt-2 sm:mt-0 sm:flex-nowrap sm:shrink-0">
36
+ {/*
37
+ * Action group stays a single non-wrapping row (chips + Run + manifest
38
+ * never split across lines). When the title can't yield enough inline
39
+ * space, the PARENT's flex-wrap drops this whole group to its own row —
40
+ * an intentional stack, not an accidental two-line break (FEAT-4).
41
+ */}
42
+ <div className="flex flex-nowrap items-center gap-2 sm:shrink-0">
37
43
  {status && <StatusChip status={status} size="md" />}
38
44
  {cadenceChip && (
39
45
  <ScheduleCadenceChip
@@ -5,6 +5,7 @@ import { Button } from "@/components/ui/button";
5
5
  import { Play } from "lucide-react";
6
6
  import { toast } from "sonner";
7
7
  import { RunNowSheet } from "./run-now-sheet";
8
+ import { toastDraftCreated } from "./run-now-toast";
8
9
  import type { BlueprintVariable } from "@/lib/workflows/blueprints/types";
9
10
 
10
11
  interface RunNowButtonProps {
@@ -63,12 +64,15 @@ export function RunNowButton({
63
64
  });
64
65
  if (!res.ok) {
65
66
  const err = (await res.json().catch(() => ({}))) as { error?: string };
66
- toast.error(err.error ?? `Failed to start (${res.status})`);
67
+ toast.error(err.error ?? `Failed to create draft (${res.status})`);
67
68
  return;
68
69
  }
69
- toast.success("Run started");
70
+ const body = (await res.json().catch(() => ({}))) as {
71
+ workflowId?: string;
72
+ };
73
+ toastDraftCreated(body.workflowId);
70
74
  } catch (err) {
71
- toast.error(err instanceof Error ? err.message : "Run failed");
75
+ toast.error(err instanceof Error ? err.message : "Could not create draft");
72
76
  } finally {
73
77
  setPending(false);
74
78
  }
@@ -13,6 +13,7 @@ import {
13
13
  } from "@/components/ui/sheet";
14
14
  import { VariableInput } from "@/components/workflows/variable-input";
15
15
  import { validateVariables } from "@/lib/workflows/blueprints/validate-variables";
16
+ import { toastDraftCreated } from "./run-now-toast";
16
17
  import type { BlueprintVariable } from "@/lib/workflows/blueprints/types";
17
18
 
18
19
  interface RunNowSheetProps {
@@ -74,10 +75,13 @@ export function RunNowSheet({
74
75
  }
75
76
  return;
76
77
  }
77
- toast.success("Run started");
78
+ const body = (await res.json().catch(() => ({}))) as {
79
+ workflowId?: string;
80
+ };
81
+ toastDraftCreated(body.workflowId);
78
82
  setOpen(false);
79
83
  } catch (err) {
80
- toast.error(err instanceof Error ? err.message : "Run failed");
84
+ toast.error(err instanceof Error ? err.message : "Could not create draft");
81
85
  } finally {
82
86
  setPending(false);
83
87
  }
@@ -0,0 +1,27 @@
1
+ import { toast } from "sonner";
2
+
3
+ /**
4
+ * BUG-4: "Run now" POSTs the blueprint instantiate endpoint, which creates a
5
+ * `draft` workflow and does NOT dispatch it. The old toast said "Run started",
6
+ * which is a lie — nothing runs until the user hits Execute on the workflow
7
+ * detail page. Surface the truth: a draft was created, and deep-link to where
8
+ * it can be executed.
9
+ *
10
+ * Shared by `run-now-button.tsx` (direct POST) and `run-now-sheet.tsx`
11
+ * (variable path) so the honest copy can't drift between the two.
12
+ */
13
+ export function toastDraftCreated(workflowId: string | undefined): void {
14
+ if (!workflowId) {
15
+ // No id to link to — still don't claim it started.
16
+ toast.success("Draft created. Open it in Workflows to Execute.");
17
+ return;
18
+ }
19
+ toast.success("Draft created. Open it in Workflows to Execute.", {
20
+ action: {
21
+ label: "Open workflow",
22
+ onClick: () => {
23
+ window.location.assign(`/workflows/${workflowId}`);
24
+ },
25
+ },
26
+ });
27
+ }
@@ -15,7 +15,35 @@ import { ConfirmDialog } from "@/components/shared/confirm-dialog";
15
15
  import { toast } from "sonner";
16
16
  import { Loader2, Trash2, Database } from "lucide-react";
17
17
 
18
- export function DataManagementSection() {
18
+ /**
19
+ * Reads a JSON body defensively. A gated route returns an explanatory
20
+ * `{success:false,error}` body with a non-2xx status; a genuine network/parse
21
+ * failure has no parseable body. Distinguishing the two is what stops a
22
+ * deliberate gate (403) from being disguised as "Network error" (BUG-5).
23
+ */
24
+ async function readResult(
25
+ res: Response
26
+ ): Promise<{ ok: boolean; body: Record<string, unknown> | null }> {
27
+ let body: Record<string, unknown> | null = null;
28
+ try {
29
+ body = (await res.json()) as Record<string, unknown> | null;
30
+ } catch {
31
+ body = null;
32
+ }
33
+ return { ok: res.ok, body };
34
+ }
35
+
36
+ /** Prefer the route's explanatory reason; fall back to the HTTP status. */
37
+ function failureMessage(
38
+ res: Response,
39
+ body: Record<string, unknown> | null,
40
+ fallback: string
41
+ ): string {
42
+ if (body && typeof body.error === "string") return body.error;
43
+ return `${fallback} (HTTP ${res.status})`;
44
+ }
45
+
46
+ export function DataManagementSection({ allowed = true }: { allowed?: boolean }) {
19
47
  const [clearOpen, setClearOpen] = useState(false);
20
48
  const [seedOpen, setSeedOpen] = useState(false);
21
49
  const [loading, setLoading] = useState(false);
@@ -24,14 +52,14 @@ export function DataManagementSection() {
24
52
  setLoading(true);
25
53
  try {
26
54
  const res = await fetch("/api/data/clear", { method: "POST" });
27
- const data = await res.json();
28
- if (data.success) {
29
- const d = data.deleted;
55
+ const { ok, body } = await readResult(res);
56
+ if (ok && body?.success) {
57
+ const d = body.deleted as Record<string, number>;
30
58
  toast.success(
31
59
  `Cleared ${d.projects} projects, ${d.tasks} tasks, ${d.workflows} workflows, ${d.schedules} schedules, ${d.documents} documents, ${d.conversations} conversations, ${d.chatMessages} messages, ${d.learnedContext} learned context, ${d.views} views, ${d.usageLedger} usage entries, ${d.agentLogs} logs, ${d.notifications} notifications, ${d.sampleProfiles} sample profiles, ${d.files} files`
32
60
  );
33
61
  } else {
34
- toast.error(`Clear failed: ${data.error}`);
62
+ toast.error(failureMessage(res, body, "Clear failed"));
35
63
  }
36
64
  } catch {
37
65
  toast.error("Clear failed. Network error");
@@ -44,14 +72,14 @@ export function DataManagementSection() {
44
72
  setLoading(true);
45
73
  try {
46
74
  const res = await fetch("/api/data/seed", { method: "POST" });
47
- const data = await res.json();
48
- if (data.success) {
49
- const s = data.seeded;
75
+ const { ok, body } = await readResult(res);
76
+ if (ok && body?.success) {
77
+ const s = body.seeded as Record<string, number>;
50
78
  toast.success(
51
79
  `Seeded ${s.profiles} profiles, ${s.projects} projects, ${s.tasks} tasks, ${s.workflows} workflows, ${s.schedules} schedules, ${s.documents} documents, ${s.userTables} tables (${s.userTableRows} rows, ${s.tableViews} views, ${s.tableTriggers} triggers, ${s.tableRelationships} links), ${s.conversations} conversations, ${s.chatMessages} messages, ${s.agentMemory} memories, ${s.agentMessages} handoffs, ${s.channelConfigs} channels (${s.channelBindings} bindings), ${s.environmentScans} scans (${s.environmentArtifacts} artifacts, ${s.environmentCheckpoints} checkpoints, ${s.environmentTemplates} templates), ${s.workflowExecutionStats} workflow-stats, ${s.scheduleFiringMetrics} firing-metrics, ${s.usageLedger} usage entries, ${s.learnedContext} learned context, ${s.views} views, ${s.profileTestResults} test results, ${s.repoImports} repo imports, ${s.agentLogs} logs, ${s.notifications} notifications`
52
80
  );
53
81
  } else {
54
- toast.error(`Seed failed: ${data.error}`);
82
+ toast.error(failureMessage(res, body, "Seed failed"));
55
83
  }
56
84
  } catch {
57
85
  toast.error("Seed failed. Network error");
@@ -60,6 +88,25 @@ export function DataManagementSection() {
60
88
  }
61
89
  }
62
90
 
91
+ if (!allowed) {
92
+ return (
93
+ <Card className="surface-card">
94
+ <CardHeader>
95
+ <CardTitle>Data Management</CardTitle>
96
+ <CardDescription>
97
+ Reset or populate your Orionfold Relay instance
98
+ </CardDescription>
99
+ </CardHeader>
100
+ <CardContent>
101
+ <p className="text-sm text-muted-foreground">
102
+ Seeding and clearing sample data are staging-only tools. They are
103
+ turned off on this build so your real data stays safe.
104
+ </p>
105
+ </CardContent>
106
+ </Card>
107
+ );
108
+ }
109
+
63
110
  return (
64
111
  <>
65
112
  <Card className="surface-card">
@@ -162,6 +162,7 @@ async function loadRuntimeStateUncached(
162
162
 
163
163
  async function loadBaseline(app: AppDetail): Promise<RuntimeState> {
164
164
  let recentTaskCount: number | undefined;
165
+ let activeRunCount = 0;
165
166
  try {
166
167
  const rows = db
167
168
  .select({ value: count() })
@@ -169,12 +170,22 @@ async function loadBaseline(app: AppDetail): Promise<RuntimeState> {
169
170
  .where(eq(tasks.projectId, app.id))
170
171
  .all();
171
172
  recentTaskCount = rows[0]?.value ?? 0;
173
+ // BUG-2: the header status must reflect whether the app is ACTUALLY
174
+ // running something, not a hardcoded literal. Count in-flight tasks
175
+ // (status = "running") for this app so kits can render Ready vs Running.
176
+ const running = db
177
+ .select({ value: count() })
178
+ .from(tasks)
179
+ .where(and(eq(tasks.projectId, app.id), eq(tasks.status, "running")))
180
+ .all();
181
+ activeRunCount = running[0]?.value ?? 0;
172
182
  } catch {
173
183
  recentTaskCount = undefined;
184
+ activeRunCount = 0;
174
185
  }
175
186
  const firstCron = app.manifest.schedules[0]?.cron;
176
187
  const scheduleCadence = firstCron ? humanizeCron(firstCron) : null;
177
- return { app, recentTaskCount, scheduleCadence };
188
+ return { app, recentTaskCount, scheduleCadence, activeRunCount };
178
189
  }
179
190
 
180
191
  async function loadCadence(
@@ -0,0 +1,14 @@
1
+ import type { HeaderSlot, RuntimeState } from "./types";
2
+
3
+ /**
4
+ * BUG-2: derive the app header's status chip from real run state instead of a
5
+ * hardcoded `"running"` literal. An app pulses "Running" ONLY while a task is
6
+ * actually in flight; otherwise it reads a calm, non-pulsing "Ready".
7
+ *
8
+ * All 7 view-kit builders share this so the fake-green-pulse-on-idle defect
9
+ * can't reappear in one kit. `activeRunCount` is populated once in the data
10
+ * layer's `loadBaseline` and spread into every kit's RuntimeState.
11
+ */
12
+ export function headerStatus(runtime: RuntimeState): HeaderSlot["status"] {
13
+ return (runtime.activeRunCount ?? 0) > 0 ? "running" : "ready";
14
+ }
@@ -9,6 +9,7 @@ import type {
9
9
  RuntimeState,
10
10
  ViewModel,
11
11
  } from "../types";
12
+ import { headerStatus } from "../header-status";
12
13
  import type { BlueprintVariable } from "@/lib/workflows/blueprints/types";
13
14
 
14
15
  interface CoachProjection extends KitProjection {
@@ -82,7 +83,7 @@ export const coachKit: KitDefinition = {
82
83
  header: {
83
84
  title: app.name,
84
85
  description: app.description ?? undefined,
85
- status: "running",
86
+ status: headerStatus(runtime),
86
87
  cadenceChip: runtime.cadence ?? undefined,
87
88
  runNowBlueprintId: projection.runsBlueprintId,
88
89
  runNowVariables: projection.runsBlueprintVars,
@@ -12,6 +12,7 @@ import type {
12
12
  TriggerSource,
13
13
  ViewModel,
14
14
  } from "../types";
15
+ import { headerStatus } from "../header-status";
15
16
  import type { BlueprintVariable } from "@/lib/workflows/blueprints/types";
16
17
 
17
18
  interface InboxProjection extends KitProjection {
@@ -113,7 +114,7 @@ export const inboxKit: KitDefinition = {
113
114
  header: {
114
115
  title: app.name,
115
116
  description: app.description ?? undefined,
116
- status: "running",
117
+ status: headerStatus(runtime),
117
118
  runNowBlueprintId: isRowInsert ? undefined : projection.draftBlueprintId,
118
119
  runNowVariables: projection.draftBlueprintVars,
119
120
  triggerSourceChip: projection.triggerSource,
@@ -13,6 +13,7 @@ import type {
13
13
  RuntimeState,
14
14
  ViewModel,
15
15
  } from "../types";
16
+ import { headerStatus } from "../header-status";
16
17
  import type { BlueprintVariable } from "@/lib/workflows/blueprints/types";
17
18
 
18
19
  type KpiSpec = NonNullable<ViewConfig["bindings"]["kpis"]>[number];
@@ -134,7 +135,7 @@ export const ledgerKit: KitDefinition = {
134
135
  header: {
135
136
  title: app.name,
136
137
  description: app.description ?? undefined,
137
- status: "running",
138
+ status: headerStatus(runtime),
138
139
  runNowBlueprintId: projection.runsBlueprintId,
139
140
  runNowVariables: projection.runsBlueprintVars,
140
141
  periodChip: { current: projection.period },
@@ -8,6 +8,7 @@ import type {
8
8
  RuntimeState,
9
9
  ViewModel,
10
10
  } from "../types";
11
+ import { headerStatus } from "../header-status";
11
12
 
12
13
  /**
13
14
  * Phase 1.1 placeholder kit. Lands the seam: every app dispatches through
@@ -41,7 +42,7 @@ export const placeholderKit: KitDefinition = {
41
42
  header: {
42
43
  title: app.name,
43
44
  description: app.description ?? "Composed app",
44
- status: "running",
45
+ status: headerStatus(runtime),
45
46
  },
46
47
  footer: {
47
48
  appId: app.id,
@@ -11,6 +11,7 @@ import type {
11
11
  RuntimeState,
12
12
  ViewModel,
13
13
  } from "../types";
14
+ import { headerStatus } from "../header-status";
14
15
  import type { BlueprintVariable } from "@/lib/workflows/blueprints/types";
15
16
 
16
17
  interface ResearchProjection extends KitProjection {
@@ -106,7 +107,7 @@ export const researchKit: KitDefinition = {
106
107
  header: {
107
108
  title: app.name,
108
109
  description: app.description ?? undefined,
109
- status: "running",
110
+ status: headerStatus(runtime),
110
111
  cadenceChip: runtime.cadence ?? undefined,
111
112
  runNowBlueprintId: projection.synthesisBlueprintId,
112
113
  runNowVariables: projection.synthesisBlueprintVars,
@@ -11,6 +11,7 @@ import type {
11
11
  RuntimeState,
12
12
  ViewModel,
13
13
  } from "../types";
14
+ import { headerStatus } from "../header-status";
14
15
 
15
16
  type KpiSpec = NonNullable<ViewConfig["bindings"]["kpis"]>[number];
16
17
 
@@ -103,7 +104,7 @@ export const trackerKit: KitDefinition = {
103
104
  header: {
104
105
  title: app.name,
105
106
  description: app.description ?? undefined,
106
- status: "running",
107
+ status: headerStatus(runtime),
107
108
  cadenceChip: runtime.cadence ?? undefined,
108
109
  runNowBlueprintId: projection.runsBlueprintId,
109
110
  },
@@ -11,6 +11,7 @@ import type {
11
11
  RuntimeState,
12
12
  ViewModel,
13
13
  } from "../types";
14
+ import { headerStatus } from "../header-status";
14
15
 
15
16
  type KpiSpec = NonNullable<ViewConfig["bindings"]["kpis"]>[number];
16
17
 
@@ -120,7 +121,7 @@ export const workflowHubKit: KitDefinition = {
120
121
  header: {
121
122
  title: app.name,
122
123
  description: app.description ?? "Composed app",
123
- status: "running",
124
+ status: headerStatus(runtime),
124
125
  cadenceChip: runtime.cadence ?? undefined,
125
126
  },
126
127
  kpis: runtime.evaluatedKpis ?? [],
@@ -65,6 +65,12 @@ export interface TimelineRun {
65
65
  export interface RuntimeState {
66
66
  app: AppDetail;
67
67
  recentTaskCount?: number;
68
+ /**
69
+ * BUG-2: number of tasks currently in flight (status = "running") for this
70
+ * app. Drives the header status chip — Ready (idle) vs Running — so an idle
71
+ * app never pulses a fake "Running" literal.
72
+ */
73
+ activeRunCount?: number;
68
74
  scheduleCadence?: string | null;
69
75
  /** Phase 2: hero table content for Tracker kit (columns + last-N rows). */
70
76
  heroTable?: HeroTableData | null;
@@ -176,7 +182,7 @@ export type TriggerSource =
176
182
  export interface HeaderSlot {
177
183
  title: string;
178
184
  description?: string;
179
- status?: "running" | "queued" | "completed" | "failed" | "planned";
185
+ status?: "ready" | "running" | "queued" | "completed" | "failed" | "planned";
180
186
  /** Right-aligned actions; rendered as ReactNode so kits can compose. */
181
187
  actions?: ReactNode;
182
188
  /** Phase 2: render a ScheduleCadenceChip when present. */
@@ -41,6 +41,11 @@ export function searchFiles(
41
41
  encoding: "utf-8",
42
42
  maxBuffer: 10 * 1024 * 1024,
43
43
  timeout: 3000,
44
+ // Route git's stderr to a pipe (discarded via the catch) instead of
45
+ // inheriting the console, so a non-git cwd can't leak a raw
46
+ // `fatal: not a git repository` line to a customer's console. Mirrors
47
+ // src/lib/environment/workspace-context.ts / src/lib/instance/git-ops.ts.
48
+ stdio: ["ignore", "pipe", "pipe"],
44
49
  }
45
50
  );
46
51
  } catch {
@@ -41,12 +41,24 @@ export interface StatusDefinition {
41
41
  live?: boolean;
42
42
  }
43
43
 
44
+ // Neutral, non-pulsing surface token (used by planned/ready idle states).
45
+ const MUTED = "muted-foreground";
46
+
44
47
  // ── Lifecycle statuses ───────────────────────────────────────────
45
48
  export const lifecycleStatuses: Record<string, StatusDefinition> = {
46
49
  planned: {
47
50
  label: "Planned",
48
51
  icon: Circle,
49
- colorToken: "muted-foreground",
52
+ colorToken: MUTED,
53
+ badgeVariant: "outline",
54
+ },
55
+ // BUG-2: installed & healthy with nothing in flight. Calm, NON-pulsing —
56
+ // an idle app must not show the green "Running" ping. Neutral (muted) reads
57
+ // as "steady/ready", not a success/completion state.
58
+ ready: {
59
+ label: "Ready",
60
+ icon: CheckCircle2,
61
+ colorToken: MUTED,
50
62
  badgeVariant: "outline",
51
63
  },
52
64
  queued: {
@@ -19,6 +19,11 @@ function git(args: string[], cwd: string): GitResult {
19
19
  cwd,
20
20
  encoding: "utf-8",
21
21
  timeout: 10000,
22
+ // Route git's stderr to a pipe (discarded here) instead of inheriting
23
+ // the console, so a non-git cwd can't leak a raw `fatal: not a git
24
+ // repository` line to a customer's first-run log. Mirrors
25
+ // src/lib/environment/workspace-context.ts / src/lib/instance/git-ops.ts.
26
+ stdio: ["ignore", "pipe", "pipe"],
22
27
  }).trim();
23
28
  return { success: true, output };
24
29
  } catch (e) {
@@ -361,6 +361,25 @@ export async function installPack(
361
361
  reloadBlueprints();
362
362
  }
363
363
 
364
+ // Defense-in-depth for the app-detail Data Cache. `loadRuntimeState`
365
+ // (src/lib/apps/view-kits/data.ts) wraps its projection in
366
+ // unstable_cache({ revalidate:30, tags:['app-runtime:<id>'] }). Reloading
367
+ // the registry above fixes getBlueprint(), but a Data-Cache snapshot taken
368
+ // in the 30s window before enrichment populated could still serve husk
369
+ // cards (raw ids, no Run button). Invalidating the tag here guarantees the
370
+ // freshly-installed app's page reads a live projection, not a stale one.
371
+ // (Did not reproduce on a from-scratch install — the registry singleton is
372
+ // populated before first render — but this closes the latent race.)
373
+ try {
374
+ const { revalidateTag } = await import("next/cache");
375
+ // Next 16 requires a profile/expiry arg. expire:0 = purge the tagged
376
+ // entry immediately so the next render recomputes the projection.
377
+ revalidateTag(`app-runtime:${pack.meta.id}`, { expire: 0 });
378
+ } catch {
379
+ // revalidateTag is a no-op outside a Next request/render scope (e.g. the
380
+ // CLI install path). Never let cache housekeeping fail an install.
381
+ }
382
+
364
383
  // 6. Report — zero silent steps.
365
384
  return {
366
385
  packId: pack.meta.id,
@@ -1,6 +1,6 @@
1
1
  id: echo-server
2
2
  version: 0.1.0
3
- apiVersion: "0.25"
3
+ apiVersion: "0.26"
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.25"
3
+ apiVersion: "0.26"
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.25"
3
+ apiVersion: "0.26"
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.24"]);
56
+ const SUPPORTED_API_VERSIONS = new Set([CURRENT_PLUGIN_API_VERSION, "0.25"]);
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.25";
9
+ export const CURRENT_PLUGIN_API_VERSION = "0.26";
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