refactor: package code organization wave 10 (#2328)

## Summary

Wave 10 of package code organization (plan:
`docs/plans/2026-07-14-001-refactor-package-code-organization-plan.md`),
after #2274.

### Peels

| New module | Parent |
|---|---|
| `types/agents.ts` | `types.ts` (permissions, Agent entity, ratings,
reflections, heartbeat run types) |
| `app/api/missions.ts` | `legacy.ts` (hierarchy, assertions,
validation, autopilot) |
| `app/api/messaging.ts` | `legacy.ts` (mailbox, approvals,
reflections/ratings, budget) |
| `app/api/plugins-and-skills.ts` | `legacy.ts` |
| `app/api/todo.ts` | `legacy.ts` |
| `app/api/insights.ts` | `legacy.ts` |
| `app/api/system-panel.ts` | `legacy.ts` |
| `task-store/workflow-definitions.ts` | rename of `remaining-ops-8` |

Mission interview SSE streams stay in `legacy.ts` until
`createResilientEventSource` is shared.

Public paths stay stable via re-exports.

### LOC

- `types.ts` ~7101 → ~6165
- `legacy.ts` ~10273 → ~8913

### Shims

- `types.ts` → `types/agents.ts` (delete-when: consumers import agents
domain directly)
- `legacy.ts` → peels above (delete-when: dashboard imports domain
modules)
- `remaining-ops-8` → `workflow-definitions` (rename complete)

## Test plan

- [x] `@fusion/core` typecheck
- [x] eslint on peeled dashboard API modules
- [x] `pnpm check:line-count`
- [x] `agent-permissions` + `agent-permission-policy` tests
- [x] `plugin-setup-api` tests
- [ ] CI merge gate

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added typed dashboard API support for missions, milestones/features,
validation loops, and autopilot.
  * Added insights browsing and management, including run triggering.
* Added messaging/mailboxes and approvals, plus agent reflections,
ratings, and budget controls.
  * Added plugins & skills management and discovery.
  * Added todo lists and items with reordering.
* Added system monitoring controls: rebuilds/restarts, logs, and
research finding promotion.
* **Improvements**
* Centralized agent-related type contracts for safer browser
consumption.
* Enhanced legacy API compatibility and improved AI session deletion
error handling.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
gsxdsm
2026-07-19 09:47:58 -07:00
committed by GitHub
parent ecffdbb14b
commit 3bf7f92be0
16 changed files with 3250 additions and 2736 deletions

View File

@@ -0,0 +1,162 @@
/**
* FNXC:CodeOrganization 2026-07-18-14:00:
* Insights client API peeled from legacy.ts.
*/
import type {
Insight,
InsightCategory,
InsightStatus,
InsightRun,
InsightRunTrigger,
} from "@fusion/core";
import { api } from "./client.js";
import { withProjectId } from "./health.js";
// ── Insights API ─────────────────────────────────────────────────────────────
export interface InsightsListResponse {
insights: Insight[];
count: number;
}
export interface RunsListResponse {
runs: InsightRun[];
}
/**
* List insights for a project with optional filtering.
*/
export function fetchInsights(
options: {
category?: InsightCategory;
status?: InsightStatus;
runId?: string;
limit?: number;
offset?: number;
} = {},
projectId?: string,
): Promise<InsightsListResponse> {
const params = new URLSearchParams();
if (options.category) params.set("category", options.category);
if (options.status) params.set("status", options.status);
if (options.runId) params.set("runId", options.runId);
if (options.limit !== undefined) params.set("limit", String(options.limit));
if (options.offset !== undefined) params.set("offset", String(options.offset));
const suffix = params.size > 0 ? `?${params.toString()}` : "";
return api<InsightsListResponse>(withProjectId(`/insights${suffix}`, projectId));
}
/**
* Get a single insight by ID.
*/
export function fetchInsight(id: string, projectId?: string): Promise<Insight> {
return api<Insight>(withProjectId(`/insights/${encodeURIComponent(id)}`, projectId));
}
/**
* Update an insight.
*/
export function updateInsight(
id: string,
updates: {
title?: string;
content?: string | null;
category?: InsightCategory;
status?: InsightStatus;
},
projectId?: string,
): Promise<Insight> {
return api<Insight>(withProjectId(`/insights/${encodeURIComponent(id)}`, projectId), {
method: "PATCH",
body: JSON.stringify(updates),
});
}
/**
* Delete an insight.
*/
export function deleteInsight(id: string, projectId?: string): Promise<void> {
return api<void>(withProjectId(`/insights/${encodeURIComponent(id)}`, projectId), {
method: "DELETE",
});
}
/**
* Dismiss an insight (set status to dismissed).
*/
export function dismissInsight(id: string, projectId?: string): Promise<Insight> {
return api<Insight>(withProjectId(`/insights/${encodeURIComponent(id)}/dismiss`, projectId), {
method: "POST",
});
}
/**
* Archive an insight (set status to archived).
*/
export function archiveInsight(id: string, projectId?: string): Promise<Insight> {
return api<Insight>(withProjectId(`/insights/${encodeURIComponent(id)}/archive`, projectId), {
method: "POST",
});
}
/**
* Unarchive an insight (set status back to confirmed).
*/
export function unarchiveInsight(id: string, projectId?: string): Promise<Insight> {
return api<Insight>(withProjectId(`/insights/${encodeURIComponent(id)}/unarchive`, projectId), {
method: "POST",
});
}
/**
* Trigger a manual insight generation run.
*/
export function triggerInsightRun(
trigger: InsightRunTrigger = "manual",
inputMetadata?: InsightRun["inputMetadata"],
projectId?: string,
modelProvider?: string,
modelId?: string,
thinkingLevel?: string,
): Promise<InsightRun> {
const body: Record<string, unknown> = { trigger, inputMetadata };
if (modelProvider) body.modelProvider = modelProvider;
if (modelId) body.modelId = modelId;
if (thinkingLevel) body.thinkingLevel = thinkingLevel;
return api<InsightRun>(withProjectId("/insights/run", projectId), {
method: "POST",
body: JSON.stringify(body),
});
}
/**
* List insight generation runs.
*/
export function fetchInsightRuns(projectId?: string): Promise<RunsListResponse> {
return api<RunsListResponse>(withProjectId("/insights/runs", projectId));
}
/**
* Get a single insight run by ID.
*/
export function fetchInsightRun(id: string, projectId?: string): Promise<InsightRun> {
return api<InsightRun>(withProjectId(`/insights/runs/${encodeURIComponent(id)}`, projectId));
}
/**
* Get data needed to create a task from an insight.
*/
export function getInsightCreateTaskData(
id: string,
projectId?: string,
): Promise<{
success: boolean;
insight: Insight;
suggestedTitle: string;
suggestedDescription: string;
}> {
return api(withProjectId(`/insights/${encodeURIComponent(id)}/create-task`, projectId), {
method: "POST",
});
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,326 @@
/**
* FNXC:CodeOrganization 2026-07-18-14:00:
* Mailbox, approvals, agent reflections/ratings, and budget client API peeled from legacy.ts.
*/
import type {
Message,
MessageMetadata,
MessageType,
ParticipantType,
AgentRating,
AgentRatingSummary,
AgentRatingInput,
AgentReflection,
AgentPerformanceSummary,
AgentBudgetStatus,
ApprovalRequestStatus,
} from "@fusion/core";
import { api } from "./client.js";
import { withProjectId } from "./health.js";
// ── Messages API ──────────────────────────────────────────────────────────
/** Response shape for GET /messages/inbox */
export interface InboxResponse {
messages: Message[];
total: number;
unreadCount: number;
}
/** Response shape for GET /messages/outbox */
export interface OutboxResponse {
messages: Message[];
total: number;
}
/** Response shape for GET /messages/unread-count */
export interface UnreadCountResponse {
unreadCount: number;
pendingApprovalCount?: number;
}
/** Response shape for POST /messages/read-all */
export interface MarkAllReadResponse {
markedAsRead: number;
}
/** Response shape for GET /agents/:id/mailbox */
export interface AgentMailboxResponse {
ownerId: string;
ownerType: ParticipantType;
unreadCount: number;
lastMessage?: Message;
messages: Message[]; // Backward compat alias for inbox
inbox: Message[];
outbox: Message[];
}
/** Response shape for GET /agents/mailbox/all */
export interface AllAgentsMailboxResponse {
messages: Message[];
total: number;
unreadCount: number;
}
/** Input for sending a message via the dashboard */
export interface SendMessageInput {
toId: string;
toType: ParticipantType;
content: string;
type: MessageType;
metadata?: MessageMetadata;
wakeImmediately?: boolean;
}
export interface ApprovalRequestSummary {
id: string;
status: ApprovalRequestStatus;
actionCategory: string;
actionSummary: string;
agentId: string;
taskId?: string;
createdAt: string;
updatedAt: string;
decidedAt?: string;
decidedBy?: string;
}
export interface ApprovalRequestDetail extends ApprovalRequestSummary {
requester: {
actorId: string;
actorType: "agent" | "user" | "system";
actorName: string;
};
runId?: string;
requestedAt: string;
completedAt?: string;
targetAction: {
category: string;
action: string;
summary: string;
resourceType: string;
resourceId: string;
context?: Record<string, unknown>;
};
history: Array<{
id: string;
eventType: string;
actor: {
actorId: string;
actorType: "agent" | "user" | "system";
actorName: string;
};
note?: string;
createdAt: string;
}>;
}
export interface ApprovalListResponse {
requests: ApprovalRequestSummary[];
total: number;
pendingCount: number;
}
/** Fetch inbox messages for the current user. */
export function fetchInbox(
options?: { limit?: number; offset?: number; unreadOnly?: boolean; type?: MessageType },
projectId?: string,
): Promise<InboxResponse> {
const params = new URLSearchParams();
if (options?.limit !== undefined) params.set("limit", String(options.limit));
if (options?.offset !== undefined) params.set("offset", String(options.offset));
if (options?.unreadOnly) params.set("unreadOnly", "true");
if (options?.type) params.set("type", options.type);
if (projectId) params.set("projectId", projectId);
const query = params.size > 0 ? `?${params.toString()}` : "";
return api<InboxResponse>(`/messages/inbox${query}`);
}
/** Fetch sent messages for the current user. */
export function fetchOutbox(
options?: { limit?: number; offset?: number; type?: MessageType },
projectId?: string,
): Promise<OutboxResponse> {
const params = new URLSearchParams();
if (options?.limit !== undefined) params.set("limit", String(options.limit));
if (options?.offset !== undefined) params.set("offset", String(options.offset));
if (options?.type) params.set("type", options.type);
if (projectId) params.set("projectId", projectId);
const query = params.size > 0 ? `?${params.toString()}` : "";
return api<OutboxResponse>(`/messages/outbox${query}`);
}
/** Fetch unread message count (lightweight, for header badge). */
export function fetchUnreadCount(projectId?: string): Promise<UnreadCountResponse> {
return api<UnreadCountResponse>(withProjectId("/messages/unread-count", projectId));
}
/** Fetch a single message by ID. */
export function fetchMessage(id: string, projectId?: string): Promise<Message> {
return api<Message>(withProjectId(`/messages/${encodeURIComponent(id)}`, projectId));
}
/** Send a new message. */
export function sendMessage(input: SendMessageInput, projectId?: string): Promise<Message> {
return api<Message>(withProjectId("/messages", projectId), {
method: "POST",
body: JSON.stringify(input),
});
}
/** Materialize an operator-approved task proposal exactly once. */
export function createProposedTask(id: string, projectId?: string): Promise<{ task: import("@fusion/core").Task; proposal: Message }> {
return api(withProjectId(`/messages/${encodeURIComponent(id)}/create-proposed-task`, projectId), { method: "POST" });
}
/** Mark a specific message as read. */
export function markMessageRead(id: string, projectId?: string): Promise<Message> {
return api<Message>(withProjectId(`/messages/${encodeURIComponent(id)}/read`, projectId), {
method: "POST",
});
}
/** Mark all inbox messages as read. */
export function markAllMessagesRead(projectId?: string): Promise<MarkAllReadResponse> {
return api<MarkAllReadResponse>(withProjectId("/messages/read-all", projectId), {
method: "POST",
});
}
/** Delete a message. */
export function deleteMessage(id: string, projectId?: string): Promise<void> {
return api<void>(withProjectId(`/messages/${encodeURIComponent(id)}`, projectId), {
method: "DELETE",
});
}
/** Fetch conversation between current user and a specific participant. */
export function fetchConversation(
participantId: string,
participantType: ParticipantType,
projectId?: string,
): Promise<Message[]> {
const path = `/messages/conversation/${encodeURIComponent(participantType)}/${encodeURIComponent(participantId)}`;
return api<Message[]>(withProjectId(path, projectId));
}
/** Fetch an agent's mailbox (admin read-only view). */
export function fetchAgentMailbox(agentId: string, projectId?: string): Promise<AgentMailboxResponse> {
return api<AgentMailboxResponse>(withProjectId(`/agents/${encodeURIComponent(agentId)}/mailbox`, projectId));
}
/** Fetch aggregate mailbox across all agent-to-agent messages (admin read-only view). */
export function fetchAllAgentMailbox(projectId?: string): Promise<AllAgentsMailboxResponse> {
return api<AllAgentsMailboxResponse>(withProjectId("/agents/mailbox/all", projectId));
}
export function fetchApprovals(
options?: { status?: ApprovalRequestStatus; limit?: number; offset?: number },
projectId?: string,
): Promise<ApprovalListResponse> {
const params = new URLSearchParams();
if (options?.status) params.set("status", options.status);
if (options?.limit !== undefined) params.set("limit", String(options.limit));
if (options?.offset !== undefined) params.set("offset", String(options.offset));
if (projectId) params.set("projectId", projectId);
const query = params.size > 0 ? `?${params.toString()}` : "";
return api<ApprovalListResponse>(`/approvals${query}`);
}
export function fetchApprovalDetail(id: string, projectId?: string): Promise<ApprovalRequestDetail> {
return api<ApprovalRequestDetail>(withProjectId(`/approvals/${encodeURIComponent(id)}`, projectId));
}
export function decideApproval(
id: string,
input: { decision: "approve" | "deny"; comment?: string },
projectId?: string,
): Promise<ApprovalRequestDetail> {
return api<ApprovalRequestDetail>(withProjectId(`/approvals/${encodeURIComponent(id)}/decision`, projectId), {
method: "POST",
body: JSON.stringify(input),
});
}
/** Fetch reflection history for an agent. */
export function fetchAgentReflections(agentId: string, limit?: number, projectId?: string): Promise<AgentReflection[]> {
const params = new URLSearchParams();
if (limit !== undefined) params.set("limit", String(limit));
if (projectId) params.set("projectId", projectId);
const query = params.size > 0 ? `?${params.toString()}` : "";
return api<AgentReflection[]>(`/agents/${encodeURIComponent(agentId)}/reflections${query}`);
}
/** Fetch the most recent reflection for an agent. */
export function fetchAgentReflection(agentId: string, projectId?: string): Promise<AgentReflection> {
return api<AgentReflection>(withProjectId(`/agents/${encodeURIComponent(agentId)}/reflections/latest`, projectId));
}
/** Trigger a manual reflection for an agent. */
export function triggerAgentReflection(agentId: string, projectId?: string): Promise<AgentReflection | null> {
return api<AgentReflection | null>(withProjectId(`/agents/${encodeURIComponent(agentId)}/reflections`, projectId), {
method: "POST",
});
}
/** Fetch aggregated performance summary for an agent. */
export function fetchAgentPerformance(agentId: string, windowMs?: number, projectId?: string): Promise<AgentPerformanceSummary> {
const params = new URLSearchParams();
if (windowMs !== undefined) params.set("windowMs", String(windowMs));
if (projectId) params.set("projectId", projectId);
const query = params.size > 0 ? `?${params.toString()}` : "";
return api<AgentPerformanceSummary>(`/agents/${encodeURIComponent(agentId)}/performance${query}`);
}
/** Fetch ratings for an agent */
export function fetchAgentRatings(
agentId: string,
options?: { limit?: number; category?: string },
projectId?: string,
): Promise<AgentRating[]> {
const params = new URLSearchParams();
if (options?.limit !== undefined) params.set("limit", String(options.limit));
if (options?.category) params.set("category", options.category);
if (projectId) params.set("projectId", projectId);
const query = params.size > 0 ? `?${params.toString()}` : "";
return api<AgentRating[]>(`/agents/${encodeURIComponent(agentId)}/ratings${query}`);
}
/** Add a rating for an agent */
export function addAgentRating(
agentId: string,
input: AgentRatingInput,
projectId?: string,
): Promise<AgentRating> {
return api<AgentRating>(withProjectId(`/agents/${encodeURIComponent(agentId)}/ratings`, projectId), {
method: "POST",
body: JSON.stringify(input),
});
}
/** Fetch rating summary for an agent */
export function fetchAgentRatingSummary(agentId: string, projectId?: string): Promise<AgentRatingSummary> {
return api<AgentRatingSummary>(withProjectId(`/agents/${encodeURIComponent(agentId)}/ratings/summary`, projectId));
}
/** Delete a specific rating */
export function deleteAgentRating(agentId: string, ratingId: string, projectId?: string): Promise<void> {
return api<void>(withProjectId(`/agents/${encodeURIComponent(agentId)}/ratings/${encodeURIComponent(ratingId)}`, projectId), {
method: "DELETE",
});
}
// ── Agent Budget API ──────────────────────────────────────────────────────
/** Fetch budget status for an agent */
export function fetchAgentBudgetStatus(agentId: string, projectId?: string): Promise<AgentBudgetStatus> {
return api<AgentBudgetStatus>(withProjectId(`/agents/${encodeURIComponent(agentId)}/budget`, projectId));
}
/** Reset budget usage for an agent */
export function resetAgentBudget(agentId: string, projectId?: string): Promise<void> {
return api<void>(withProjectId(`/agents/${encodeURIComponent(agentId)}/budget/reset`, projectId), {
method: "POST",
});
}

View File

@@ -0,0 +1,665 @@
/**
* FNXC:CodeOrganization 2026-07-18-14:00:
* Mission hierarchy, contract assertions, validation loop, and autopilot client API peeled from legacy.ts.
* Mission interview SSE streams remain in legacy until createResilientEventSource is shared.
*/
import type { MissionEvent, MissionHealth, MissionEventType, CommitAssociationDiffBackfillReport } from "@fusion/core";
import type { MilestoneValidationTelemetry } from "../components/mission-types";
import { api } from "./client.js";
import { withProjectId } from "./health.js";
// ── Mission API ───────────────────────────────────────────────────────────
/** Mission status values */
export type MissionStatus = "planning" | "active" | "blocked" | "complete" | "archived";
/** Milestone status values */
export type MilestoneStatus = "planning" | "active" | "blocked" | "complete";
/** Slice status values */
export type SliceStatus = "pending" | "active" | "complete";
/** Feature status values */
export type FeatureStatus = "defined" | "triaged" | "in-progress" | "done" | "blocked";
/** Autopilot state values for mission autonomous progression */
export type AutopilotState = "inactive" | "watching" | "activating" | "completing";
/** Autopilot status for a mission */
export interface AutopilotStatus {
enabled: boolean;
state: AutopilotState;
watched: boolean;
lastActivityAt?: string;
nextScheduledCheck?: string;
}
/** Mission entity */
export interface Mission {
id: string;
title: string;
description?: string;
baseBranch?: string;
branchStrategy?: {
mode: "project-default" | "existing" | "custom-new" | "auto-per-task";
branchName?: string;
};
status: MissionStatus;
interviewState: "not_started" | "in_progress" | "completed" | "needs_update";
autoAdvance?: boolean;
/** When true, enable autopilot monitoring system for this mission */
autopilotEnabled?: boolean;
/** Current autopilot runtime state */
autopilotState?: AutopilotState;
/** ISO-8601 timestamp of last autopilot activity */
lastAutopilotActivityAt?: string;
createdAt: string;
updatedAt: string;
}
/** Status summary for a mission card, computed from hierarchy */
export interface MissionSummary {
totalMilestones: number;
completedMilestones: number;
totalFeatures: number;
completedFeatures: number;
linkedGoalCount: number;
eventCount: number;
progressPercent: number;
}
/** Mission with optional status summary (returned by list endpoint) */
export type MissionWithSummary = Mission & { summary?: MissionSummary };
/** Milestone entity */
export interface Milestone {
id: string;
missionId: string;
title: string;
description?: string;
status: MilestoneStatus;
orderIndex: number;
interviewState: "not_started" | "in_progress" | "completed" | "needs_update";
dependencies: string[];
acceptanceCriteria?: string;
createdAt: string;
updatedAt: string;
}
/** Slice entity */
export interface Slice {
id: string;
milestoneId: string;
title: string;
description?: string;
status: SliceStatus;
orderIndex: number;
activatedAt?: string;
createdAt: string;
updatedAt: string;
}
/** Feature entity */
export interface MissionFeature {
id: string;
sliceId: string;
taskId?: string;
title: string;
description?: string;
acceptanceCriteria?: string;
status: FeatureStatus;
createdAt: string;
updatedAt: string;
}
/** Milestone with slices (each slice has features) */
export interface MilestoneWithSlices extends Milestone {
slices: SliceWithFeatures[];
}
/** Slice with features */
export interface SliceWithFeatures extends Slice {
features: MissionFeature[];
}
/** Full mission hierarchy */
export interface MissionWithHierarchy extends Mission {
/** Unfiltered total of all mission lifecycle events, matching MissionSummary.eventCount and getMissionEvents total with no eventType filter */
eventCount?: number;
milestones: MilestoneWithSlices[];
}
/** Fetch all missions with status summary */
export function fetchMissions(projectId?: string): Promise<MissionWithSummary[]> {
return api<MissionWithSummary[]>(withProjectId("/missions", projectId));
}
/** Create a new mission */
export function createMission(input: { title: string; description?: string; autoAdvance?: boolean; autopilotEnabled?: boolean; autoMerge?: boolean; baseBranch?: string; branchStrategy?: Mission["branchStrategy"] }, projectId?: string): Promise<Mission> {
return api<Mission>(withProjectId("/missions", projectId), {
method: "POST",
body: JSON.stringify(input),
});
}
/** Get mission with full hierarchy */
export function fetchMission(missionId: string, projectId?: string): Promise<MissionWithHierarchy> {
return api<MissionWithHierarchy>(withProjectId(`/missions/${encodeURIComponent(missionId)}`, projectId));
}
/** Update mission */
export function updateMission(missionId: string, updates: Partial<Mission> & { autoMerge?: boolean | null }, projectId?: string): Promise<Mission> {
return api<Mission>(withProjectId(`/missions/${encodeURIComponent(missionId)}`, projectId), {
method: "PATCH",
body: JSON.stringify(updates),
});
}
/** Delete mission */
export function deleteMission(missionId: string, projectId?: string): Promise<void> {
return api<void>(withProjectId(`/missions/${encodeURIComponent(missionId)}`, projectId), {
method: "DELETE",
});
}
/** Get mission computed status */
export function fetchMissionStatus(missionId: string, projectId?: string): Promise<{ status: string }> {
return api<{ status: string }>(withProjectId(`/missions/${encodeURIComponent(missionId)}/status`, projectId));
}
export interface MissionAssertionBackfillRepairRow {
featureId: string;
milestoneId: string;
assertionId: string;
textSource: "acceptanceCriteria" | "description" | "title" | "fallback";
}
export interface MissionAssertionBackfillErrorRow {
featureId: string;
message: string;
}
export interface MissionAssertionBackfillReport {
scanned: number;
alreadyLinked: number;
repaired: MissionAssertionBackfillRepairRow[];
skippedErrors: MissionAssertionBackfillErrorRow[];
}
/** Backfill store-managed mission assertions for unlinked features. Defaults to dry-run. */
export function backfillMissionAssertions(
missionId: string,
options?: { dryRun?: boolean },
projectId?: string,
): Promise<MissionAssertionBackfillReport> {
return api<MissionAssertionBackfillReport>(
withProjectId(`/missions/${encodeURIComponent(missionId)}/backfill-assertions`, projectId),
{
method: "POST",
body: JSON.stringify({ dryRun: options?.dryRun ?? true }),
},
);
}
/** Backfill historical Command Center LOC stats for commit associations. Defaults to dry-run. */
export function backfillCommitAssociationDiffStats(
options?: { dryRun?: boolean },
projectId?: string,
): Promise<CommitAssociationDiffBackfillReport> {
return api<CommitAssociationDiffBackfillReport>(
withProjectId("/command-center/productivity/backfill-loc", projectId),
{
method: "POST",
body: JSON.stringify({ dryRun: options?.dryRun ?? true }),
},
);
}
/** Query options for paginated mission event logs. */
export interface MissionEventQueryOptions {
limit?: number;
offset?: number;
eventType?: MissionEventType;
}
/** Paginated mission event log response. */
export interface MissionEventsResponse {
events: MissionEvent[];
total: number;
limit: number;
offset: number;
}
/** Fetch paginated mission observability events. */
export function fetchMissionEvents(
missionId: string,
options?: MissionEventQueryOptions,
projectId?: string,
): Promise<MissionEventsResponse> {
const query = new URLSearchParams();
if (options?.limit !== undefined) query.set("limit", String(options.limit));
if (options?.offset !== undefined) query.set("offset", String(options.offset));
if (options?.eventType !== undefined) query.set("eventType", options.eventType);
const suffix = query.size > 0 ? `?${query.toString()}` : "";
return api<MissionEventsResponse>(
withProjectId(`/missions/${encodeURIComponent(missionId)}/events${suffix}`, projectId),
);
}
/** Fetch computed mission health metrics. */
export function fetchMissionHealth(missionId: string, projectId?: string): Promise<MissionHealth> {
return api<MissionHealth>(withProjectId(`/missions/${encodeURIComponent(missionId)}/health`, projectId));
}
/** Fetch health metrics for all missions in a single batched request. */
export function fetchMissionsHealth(projectId?: string): Promise<Record<string, MissionHealth>> {
return api<Record<string, MissionHealth>>(withProjectId("/missions/health", projectId));
}
/** Add milestone to mission */
export function createMilestone(
missionId: string,
input: { title: string; description?: string; acceptanceCriteria?: string; dependencies?: string[] },
projectId?: string
): Promise<Milestone> {
return api<Milestone>(withProjectId(`/missions/${encodeURIComponent(missionId)}/milestones`, projectId), {
method: "POST",
body: JSON.stringify(input),
});
}
/** Update milestone */
export function updateMilestone(milestoneId: string, updates: Partial<Milestone>, projectId?: string): Promise<Milestone> {
return api<Milestone>(withProjectId(`/missions/milestones/${encodeURIComponent(milestoneId)}`, projectId), {
method: "PATCH",
body: JSON.stringify(updates),
});
}
/** Delete milestone */
export function deleteMilestone(milestoneId: string, projectId?: string): Promise<void> {
return api<void>(withProjectId(`/missions/milestones/${encodeURIComponent(milestoneId)}`, projectId), {
method: "DELETE",
});
}
/** Reorder milestones */
export function reorderMilestones(missionId: string, orderedIds: string[], projectId?: string): Promise<void> {
return api<void>(withProjectId(`/missions/${encodeURIComponent(missionId)}/milestones/reorder`, projectId), {
method: "POST",
body: JSON.stringify({ orderedIds }),
});
}
/** Add slice to milestone */
export function createSlice(
milestoneId: string,
input: { title: string; description?: string },
projectId?: string
): Promise<Slice> {
return api<Slice>(withProjectId(`/missions/milestones/${encodeURIComponent(milestoneId)}/slices`, projectId), {
method: "POST",
body: JSON.stringify(input),
});
}
/** Update slice */
export function updateSlice(sliceId: string, updates: Partial<Slice>, projectId?: string): Promise<Slice> {
return api<Slice>(withProjectId(`/missions/slices/${encodeURIComponent(sliceId)}`, projectId), {
method: "PATCH",
body: JSON.stringify(updates),
});
}
/** Delete slice */
export function deleteSlice(sliceId: string, projectId?: string): Promise<void> {
return api<void>(withProjectId(`/missions/slices/${encodeURIComponent(sliceId)}`, projectId), {
method: "DELETE",
});
}
/** Activate slice */
export function activateSlice(sliceId: string, projectId?: string): Promise<Slice> {
return api<Slice>(withProjectId(`/missions/slices/${encodeURIComponent(sliceId)}/activate`, projectId), {
method: "POST",
});
}
/** Reorder slices */
export function reorderSlices(milestoneId: string, orderedIds: string[], projectId?: string): Promise<void> {
return api<void>(withProjectId(`/missions/milestones/${encodeURIComponent(milestoneId)}/slices/reorder`, projectId), {
method: "POST",
body: JSON.stringify({ orderedIds }),
});
}
/** Add feature to slice */
export function createFeature(
sliceId: string,
input: { title: string; description?: string; acceptanceCriteria?: string },
projectId?: string
): Promise<MissionFeature> {
return api<MissionFeature>(withProjectId(`/missions/slices/${encodeURIComponent(sliceId)}/features`, projectId), {
method: "POST",
body: JSON.stringify(input),
});
}
/** Update feature */
export function updateFeature(featureId: string, updates: Partial<MissionFeature>, projectId?: string): Promise<MissionFeature> {
return api<MissionFeature>(withProjectId(`/missions/features/${encodeURIComponent(featureId)}`, projectId), {
method: "PATCH",
body: JSON.stringify(updates),
});
}
/** Delete feature */
export function deleteFeature(featureId: string, projectId?: string): Promise<void> {
return api<void>(withProjectId(`/missions/features/${encodeURIComponent(featureId)}`, projectId), {
method: "DELETE",
});
}
/** Link feature to task */
export function linkFeatureToTask(featureId: string, taskId: string, projectId?: string): Promise<MissionFeature> {
return api<MissionFeature>(withProjectId(`/missions/features/${encodeURIComponent(featureId)}/link-task`, projectId), {
method: "POST",
body: JSON.stringify({ taskId }),
});
}
/** Unlink feature from task */
export function unlinkFeatureFromTask(featureId: string, projectId?: string): Promise<MissionFeature> {
return api<MissionFeature>(withProjectId(`/missions/features/${encodeURIComponent(featureId)}/unlink-task`, projectId), {
method: "POST",
});
}
/** Triage a feature — create a task from the feature and link it */
export function triageFeature(
featureId: string,
taskTitle?: string,
taskDescription?: string,
projectId?: string,
options?: {
branchSelection?: {
mode: "project-default" | "auto-new" | "existing" | "custom-new";
branchName?: string;
baseBranch?: string;
};
branchAssignment?: { mode: "shared" | "per-task-derived" };
workflowId?: string | null;
},
): Promise<MissionFeature> {
return api<MissionFeature>(withProjectId(`/missions/features/${encodeURIComponent(featureId)}/triage`, projectId), {
method: "POST",
body: JSON.stringify({ taskTitle, taskDescription, ...options }),
});
}
/** Triage all "defined" features in a slice */
export function triageAllSliceFeatures(
sliceId: string,
projectId?: string,
options?: {
branchSelection?: {
mode: "project-default" | "auto-new" | "existing" | "custom-new";
branchName?: string;
baseBranch?: string;
};
branchAssignment?: { mode: "shared" | "per-task-derived" };
workflowId?: string | null;
},
): Promise<{ triaged: MissionFeature[]; count: number }> {
return api<{ triaged: MissionFeature[]; count: number }>(withProjectId(`/missions/slices/${encodeURIComponent(sliceId)}/triage-all`, projectId), {
method: "POST",
body: JSON.stringify(options ?? {}),
});
}
// ── Contract Assertion API ─────────────────────────────────────────────────────
/** Contract assertion status */
export type MissionAssertionStatus = "pending" | "passed" | "failed" | "blocked";
/** A contract assertion represents an explicit behavioral test or requirement associated with a milestone */
export interface MissionContractAssertion {
id: string;
milestoneId: string;
title: string;
assertion: string;
status: MissionAssertionStatus;
orderIndex: number;
createdAt: string;
updatedAt: string;
}
/** Input for creating a contract assertion */
export interface ContractAssertionCreateInput {
title: string;
assertion: string;
status?: MissionAssertionStatus;
}
/** Input for updating a contract assertion */
export interface ContractAssertionUpdateInput {
title?: string;
assertion?: string;
status?: MissionAssertionStatus;
}
/** List assertions for a milestone, ordered by orderIndex */
export function fetchAssertions(milestoneId: string, projectId?: string): Promise<MissionContractAssertion[]> {
return api<MissionContractAssertion[]>(withProjectId(`/missions/milestones/${encodeURIComponent(milestoneId)}/assertions`, projectId));
}
/** Create a new assertion for a milestone */
export function createAssertion(milestoneId: string, input: ContractAssertionCreateInput, projectId?: string): Promise<MissionContractAssertion> {
return api<MissionContractAssertion>(withProjectId(`/missions/milestones/${encodeURIComponent(milestoneId)}/assertions`, projectId), {
method: "POST",
body: JSON.stringify(input),
});
}
/** Reorder assertions within a milestone */
export function reorderAssertions(milestoneId: string, orderedIds: string[], projectId?: string): Promise<void> {
return api<void>(withProjectId(`/missions/milestones/${encodeURIComponent(milestoneId)}/assertions/reorder`, projectId), {
method: "POST",
body: JSON.stringify({ orderedIds }),
});
}
/** Get a single assertion by ID */
export function fetchAssertion(assertionId: string, projectId?: string): Promise<MissionContractAssertion> {
return api<MissionContractAssertion>(withProjectId(`/missions/assertions/${encodeURIComponent(assertionId)}`, projectId));
}
/** Update an assertion */
export function updateAssertion(assertionId: string, updates: ContractAssertionUpdateInput, projectId?: string): Promise<MissionContractAssertion> {
return api<MissionContractAssertion>(withProjectId(`/missions/assertions/${encodeURIComponent(assertionId)}`, projectId), {
method: "PATCH",
body: JSON.stringify(updates),
});
}
/** Delete an assertion */
export function deleteAssertion(assertionId: string, projectId?: string): Promise<void> {
return api<void>(withProjectId(`/missions/assertions/${encodeURIComponent(assertionId)}`, projectId), {
method: "DELETE",
});
}
/** Link a feature to an assertion */
export function linkFeatureToAssertion(featureId: string, assertionId: string, projectId?: string): Promise<{ success: boolean }> {
return api<{ success: boolean }>(withProjectId(`/missions/features/${encodeURIComponent(featureId)}/assertions/${encodeURIComponent(assertionId)}/link`, projectId), {
method: "POST",
});
}
/** Unlink a feature from an assertion */
export function unlinkFeatureFromAssertion(featureId: string, assertionId: string, projectId?: string): Promise<{ success: boolean }> {
return api<{ success: boolean }>(withProjectId(`/missions/features/${encodeURIComponent(featureId)}/assertions/${encodeURIComponent(assertionId)}/unlink`, projectId), {
method: "POST",
});
}
/** List assertions linked to a feature */
export function fetchAssertionsForFeature(featureId: string, projectId?: string): Promise<MissionContractAssertion[]> {
return api<MissionContractAssertion[]>(withProjectId(`/missions/features/${encodeURIComponent(featureId)}/assertions`, projectId));
}
/** List features linked to an assertion */
export function fetchFeaturesForAssertion(assertionId: string, projectId?: string): Promise<MissionFeature[]> {
return api<MissionFeature[]>(withProjectId(`/missions/assertions/${encodeURIComponent(assertionId)}/features`, projectId));
}
/** Validation rollup for a milestone */
export interface MilestoneValidationRollup {
milestoneId: string;
totalAssertions: number;
passedAssertions: number;
failedAssertions: number;
blockedAssertions: number;
pendingAssertions: number;
unlinkedAssertions: number;
hasProseButNoAssertions: boolean;
state: "not_started" | "needs_coverage" | "ready" | "passed" | "failed" | "blocked";
}
/** Get milestone validation rollup */
export function fetchMilestoneValidation(milestoneId: string, projectId?: string): Promise<MilestoneValidationRollup> {
return api<MilestoneValidationRollup>(withProjectId(`/missions/milestones/${encodeURIComponent(milestoneId)}/validation`, projectId));
}
/** Fetch grouped validation telemetry for a milestone */
export function fetchMilestoneValidationTelemetry(milestoneId: string, projectId?: string): Promise<MilestoneValidationTelemetry> {
return api<MilestoneValidationTelemetry>(withProjectId(`/missions/milestones/${encodeURIComponent(milestoneId)}/validation-telemetry`, projectId));
}
// ── Validation Loop API ───────────────────────────────────────────────────────
/** Loop state snapshot for a feature */
export interface MissionFeatureLoopSnapshot {
featureId: string;
feature: MissionFeature;
loopState: "idle" | "implementing" | "validating" | "needs_fix" | "passed" | "blocked";
implementationAttemptCount: number;
validatorAttemptCount: number;
lastValidatorRunId?: string;
lastValidatorStatus?: "running" | "passed" | "failed" | "blocked" | "error";
generatedFromFeatureId?: string;
generatedFromRunId?: string;
retryBudgetRemaining: number;
}
/** Validator run */
export interface MissionValidatorRun {
id: string;
featureId: string;
milestoneId: string;
sliceId: string;
status: "running" | "passed" | "failed" | "blocked" | "error";
triggerType: string;
implementationAttempt: number;
validatorAttempt: number;
summary?: string;
blockedReason?: string;
startedAt: string;
completedAt?: string;
createdAt: string;
updatedAt: string;
}
/** Trigger validation for a feature */
export function triggerValidation(featureId: string, projectId?: string): Promise<{ runId: string; featureId: string; status: string; triggerType: string; implementationAttempt: number; validatorAttempt: number; startedAt: string }> {
return api(withProjectId(`/missions/features/${encodeURIComponent(featureId)}/validate`, projectId), {
method: "POST",
});
}
/** Get validation loop state for a feature */
export function fetchValidationLoopState(featureId: string, projectId?: string): Promise<MissionFeatureLoopSnapshot> {
return api<MissionFeatureLoopSnapshot>(withProjectId(`/missions/features/${encodeURIComponent(featureId)}/validation-loop`, projectId));
}
/** Paginated response wrapper for validation runs */
export interface ValidationRunsResponse {
runs: MissionValidatorRun[];
total: number;
limit: number;
offset: number;
}
/** List validation runs for a feature */
export function fetchValidationRuns(featureId: string, options?: { limit?: number; offset?: number }, projectId?: string): Promise<MissionValidatorRun[]> {
const params = new URLSearchParams();
if (options?.limit !== undefined) params.set("limit", String(options.limit));
if (options?.offset !== undefined) params.set("offset", String(options.offset));
const suffix = params.size > 0 ? `?${params.toString()}` : "";
return api<ValidationRunsResponse>(withProjectId(`/missions/features/${encodeURIComponent(featureId)}/validation-runs${suffix}`, projectId))
.then((response) => response.runs);
}
/** Get a single validator run */
export function fetchValidationRun(runId: string, projectId?: string): Promise<MissionValidatorRun & { failures?: Array<{ id: string; assertionId: string; message?: string; expected?: string; actual?: string }> }> {
return api(withProjectId(`/missions/validation-runs/${encodeURIComponent(runId)}`, projectId));
}
/** Pause a mission (sets status to "blocked", in-flight tasks continue) */
export function pauseMission(missionId: string, projectId?: string): Promise<Mission> {
return api<Mission>(withProjectId(`/missions/${encodeURIComponent(missionId)}/pause`, projectId), {
method: "POST",
});
}
/** Resume a paused mission (sets status back to "active") */
export function resumeMission(missionId: string, projectId?: string): Promise<Mission> {
return api<Mission>(withProjectId(`/missions/${encodeURIComponent(missionId)}/resume`, projectId), {
method: "POST",
});
}
/** Stop a mission (sets status to "blocked" and pauses all linked tasks) */
export function stopMission(missionId: string, projectId?: string): Promise<Mission & { pausedTaskIds: string[] }> {
return api<Mission & { pausedTaskIds: string[] }>(withProjectId(`/missions/${encodeURIComponent(missionId)}/stop`, projectId), {
method: "POST",
});
}
/** Start a planning mission: sets status to "active" and activates the first pending slice */
export function startMission(missionId: string, projectId?: string): Promise<MissionWithHierarchy> {
return api<MissionWithHierarchy>(withProjectId(`/missions/${encodeURIComponent(missionId)}/start`, projectId), {
method: "POST",
});
}
// ── Mission Autopilot API ────────────────────────────────────────────────
/** Fetch autopilot status for a mission */
export function fetchMissionAutopilotStatus(missionId: string, projectId?: string): Promise<AutopilotStatus> {
return api<AutopilotStatus>(withProjectId(`/missions/${encodeURIComponent(missionId)}/autopilot`, projectId));
}
/** Update autopilot settings for a mission (enable/disable) */
export function updateMissionAutopilot(missionId: string, updates: { enabled?: boolean }, projectId?: string): Promise<AutopilotStatus> {
return api<AutopilotStatus>(withProjectId(`/missions/${encodeURIComponent(missionId)}/autopilot`, projectId), {
method: "PATCH",
body: JSON.stringify(updates),
});
}
/** Manually start autopilot watching for a mission */
export function startMissionAutopilot(missionId: string, projectId?: string): Promise<AutopilotStatus> {
return api<AutopilotStatus>(withProjectId(`/missions/${encodeURIComponent(missionId)}/autopilot/start`, projectId), {
method: "POST",
});
}
/** Manually stop autopilot watching for a mission */
export function stopMissionAutopilot(missionId: string, projectId?: string): Promise<AutopilotStatus> {
return api<AutopilotStatus>(withProjectId(`/missions/${encodeURIComponent(missionId)}/autopilot/stop`, projectId), {
method: "POST",
});
}

View File

@@ -0,0 +1,278 @@
/**
* FNXC:CodeOrganization 2026-07-18-14:00:
* Plugin management and skills client API peeled from legacy.ts.
*/
import type {
PluginInstallation,
PluginSetupCheckResult,
PluginState,
PluginUiSlotDefinition,
PluginUiContributionDefinition,
PluginDashboardViewDefinition,
} from "@fusion/core";
import type {
DiscoveredSkill,
CatalogFetchResult,
ToggleSkillResult,
SkillContent,
SkillFileContent,
} from "@fusion/dashboard";
import { api } from "./client.js";
import { withProjectId } from "./health.js";
import { dedupe } from "./dedupe.js";
// ── Plugin Management ────────────────────────────────────────────────────────
export interface RegistryPluginEntry {
id: string;
name: string;
description: string;
version: string;
author: string;
category: "runtime" | "integration";
npmPackage?: string;
path?: string;
homepage?: string;
tags?: string[];
installed: boolean;
state?: PluginState;
installedVersion?: string;
canInstall: boolean;
}
/** Fetch all installed plugins */
export async function fetchPlugins(projectId?: string): Promise<PluginInstallation[]> {
return api<PluginInstallation[]>(withProjectId("/plugins", projectId));
}
/** Fetch curated registry plugins with installed-state metadata */
export async function fetchPluginRegistry(
query?: string,
category?: string,
projectId?: string,
): Promise<RegistryPluginEntry[]> {
const params = new URLSearchParams();
if (query?.trim()) {
params.set("q", query.trim());
}
if (category?.trim()) {
params.set("category", category.trim());
}
const suffix = params.size > 0 ? `?${params.toString()}` : "";
const response = await api<{ plugins: RegistryPluginEntry[] }>(withProjectId(`/plugins/registry${suffix}`, projectId));
return response.plugins;
}
/** Fetch a single plugin by ID */
export async function fetchPluginDetail(id: string, projectId?: string): Promise<PluginInstallation> {
return api<PluginInstallation>(withProjectId(`/plugins/${encodeURIComponent(id)}`, projectId));
}
/** Install a plugin from local path or npm package */
export async function installPlugin(
source: { path: string; aiScanOnLoad?: boolean } | { package: string; aiScanOnLoad?: boolean },
projectId?: string,
): Promise<PluginInstallation> {
return api<PluginInstallation>(withProjectId("/plugins", projectId), {
method: "POST",
body: JSON.stringify({ mode: "install", ...source }),
});
}
/** Enable a plugin */
export async function enablePlugin(id: string, projectId?: string): Promise<PluginInstallation> {
return api<PluginInstallation>(withProjectId(`/plugins/${encodeURIComponent(id)}/enable`, projectId), {
method: "POST",
});
}
/** Disable a plugin */
export async function disablePlugin(id: string, projectId?: string): Promise<PluginInstallation> {
return api<PluginInstallation>(withProjectId(`/plugins/${encodeURIComponent(id)}/disable`, projectId), {
method: "POST",
});
}
/** Uninstall a plugin */
export async function uninstallPlugin(id: string, projectId?: string): Promise<void> {
return api<void>(withProjectId(`/plugins/${encodeURIComponent(id)}`, projectId), {
method: "DELETE",
});
}
/** Fetch plugin settings */
export async function fetchPluginSettings(id: string, projectId?: string): Promise<Record<string, unknown>> {
return api<Record<string, unknown>>(withProjectId(`/plugins/${encodeURIComponent(id)}/settings`, projectId));
}
/** Update plugin settings */
export async function updatePluginSettings(
id: string,
settings: Record<string, unknown>,
projectId?: string,
): Promise<Record<string, unknown>> {
return api<Record<string, unknown>>(withProjectId(`/plugins/${encodeURIComponent(id)}/settings`, projectId), {
method: "PUT",
body: JSON.stringify({ settings }),
});
}
export type PluginSetupStatusResponse =
| { hasSetup: false }
| ({ hasSetup: true } & PluginSetupCheckResult)
| {
hasSetup: true;
setupCheckDeferred: true;
deferredReason: "plugin-not-started";
pluginState: PluginInstallation["state"];
};
/** Fetch plugin setup status */
export async function fetchPluginSetupStatus(id: string, projectId?: string): Promise<PluginSetupStatusResponse> {
return api<PluginSetupStatusResponse>(withProjectId(`/plugins/${encodeURIComponent(id)}/setup-status`, projectId));
}
/** Trigger plugin setup install hook */
export async function installPluginSetup(id: string, projectId?: string): Promise<{ success: boolean; error?: string }> {
return api<{ success: boolean; error?: string }>(withProjectId(`/plugins/${encodeURIComponent(id)}/setup/install`, projectId), {
method: "POST",
});
}
/** Reload a running plugin with updated code */
export async function reloadPlugin(id: string, projectId?: string): Promise<PluginInstallation> {
return api<PluginInstallation>(withProjectId(`/plugins/${encodeURIComponent(id)}/reload`, projectId), {
method: "POST",
});
}
/** Update plugin security-scan configuration */
export async function updatePlugin(id: string, updates: { aiScanOnLoad: boolean }, projectId?: string): Promise<PluginInstallation> {
return api<PluginInstallation>(withProjectId(`/plugins/${encodeURIComponent(id)}`, projectId), {
method: "PATCH",
body: JSON.stringify(updates),
});
}
/** Trigger plugin rescan + reload flow */
export async function rescanPlugin(id: string, projectId?: string): Promise<PluginInstallation> {
return api<PluginInstallation>(withProjectId(`/plugins/${encodeURIComponent(id)}/rescan`, projectId), {
method: "POST",
});
}
/** A UI slot entry returned by GET /api/plugins/ui-slots */
export interface PluginUiSlotEntry {
pluginId: string;
slot: PluginUiSlotDefinition;
}
/** A structured UI contribution entry returned by GET /api/plugins/ui-contributions */
export interface PluginUiContributionEntry {
pluginId: string;
contribution: PluginUiContributionDefinition;
}
/** A dashboard view entry returned by GET /api/plugins/dashboard-views */
export interface PluginDashboardViewEntry {
pluginId: string;
view: PluginDashboardViewDefinition;
}
/** Plugin runtime metadata returned by GET /api/plugins/runtimes */
export interface PluginRuntimeInfo {
pluginId: string;
runtimeId: string;
name: string;
description?: string;
version?: string;
}
/** Fetch all UI slot definitions from active plugins */
export async function fetchPluginUiSlots(projectId?: string): Promise<PluginUiSlotEntry[]> {
const path = withProjectId("/plugins/ui-slots", projectId);
return dedupe(path, () => api<PluginUiSlotEntry[]>(path));
}
/** Fetch all structured UI contributions from active plugins */
export async function fetchPluginUiContributions(projectId?: string): Promise<PluginUiContributionEntry[]> {
return api<PluginUiContributionEntry[]>(withProjectId("/plugins/ui-contributions", projectId));
}
/** Fetch all top-level dashboard view definitions from active plugins */
export async function fetchPluginDashboardViews(projectId?: string): Promise<PluginDashboardViewEntry[]> {
return api<PluginDashboardViewEntry[]>(withProjectId("/plugins/dashboard-views", projectId));
}
/** Fetch all plugin runtime metadata from active plugins */
export async function fetchPluginRuntimes(projectId?: string): Promise<PluginRuntimeInfo[]> {
return api<PluginRuntimeInfo[]>(withProjectId("/plugins/runtimes", projectId));
}
// ── Skills Management ─────────────────────────────────────────────────────────
/** Fetch all discovered skills with their enabled state */
export async function fetchDiscoveredSkills(projectId?: string): Promise<DiscoveredSkill[]> {
const response = await api<{ skills: DiscoveredSkill[] }>(withProjectId("/skills/discovered", projectId));
return response.skills;
}
/** Toggle a skill's enabled/disabled state */
export async function toggleExecutionSkill(
skillId: string,
enabled: boolean,
projectId?: string,
): Promise<ToggleSkillResult> {
return api<ToggleSkillResult>(withProjectId("/skills/execution", projectId), {
method: "PATCH",
body: JSON.stringify({ skillId, enabled }),
});
}
/** Install a catalog skill from skills.sh */
export async function installSkill(
source: string,
skill: string | undefined,
projectId?: string,
): Promise<{ success: true }> {
return api<{ success: true }>(withProjectId("/skills/install", projectId), {
method: "POST",
body: JSON.stringify({ source, skill }),
});
}
/** Fetch the skills.sh catalog */
export async function fetchSkillsCatalog(
query?: string,
limit?: number,
projectId?: string,
): Promise<CatalogFetchResult> {
const params = new URLSearchParams();
if (query) params.set("q", query);
if (limit !== undefined) params.set("limit", String(limit));
const suffix = params.size > 0 ? `?${params.toString()}` : "";
return api<CatalogFetchResult>(withProjectId(`/skills/catalog${suffix}`, projectId));
}
/** Fetch the contents of a skill's SKILL.md file */
export async function fetchSkillContent(skillId: string, projectId?: string): Promise<SkillContent> {
const response = await api<{ content: SkillContent }>(
withProjectId(`/skills/${encodeURIComponent(skillId)}/content`, projectId)
);
return response.content;
}
/*
FNXC:Skills 2026-06-23-04:15:
Fetch one supplementary file's content for the SkillsView detail-pane file viewer. The skill-dir-relative path is passed as an encoded `path` query param; the server resolves + traversal-guards it. Returns isText:false for binary/oversized files so the UI shows a non-previewable notice.
*/
export async function fetchSkillFileContent(skillId: string, relativePath: string, projectId?: string): Promise<SkillFileContent> {
const base = withProjectId(`/skills/${encodeURIComponent(skillId)}/file`, projectId);
const sep = base.includes("?") ? "&" : "?";
const response = await api<{ file: SkillFileContent }>(
`${base}${sep}path=${encodeURIComponent(relativePath)}`
);
return response.file;
}

View File

@@ -0,0 +1,158 @@
/**
* FNXC:CodeOrganization 2026-07-18-14:00:
* Command Center System panel client API peeled from legacy.ts.
*/
import { api } from "./client.js";
import { withProjectId } from "./health.js";
// ── System Panel (Command Center → System) ──────────────────────────────────
/*
FNXC:SystemPanel 2026-07-12-11:35:
Typed client for the /api/system operator controls: capability discovery,
in-place restart, rebuild jobs with streamed output, engine/agent restarts,
plugin reload, and the host-process log viewer.
*/
/*
FNXC:SystemPanelFnBinary 2026-07-15-09:54:
Job snapshots cover rebuild scopes and the fn-binary link-local / use-global
actions that stream into the same System panel log viewer.
*/
export interface SystemRebuildJobSnapshot {
id: string;
kind: "rebuild" | "fn-binary";
scope: "app" | "full" | "plugins" | "link-local" | "use-global";
restartAfter: boolean;
status: "running" | "succeeded" | "failed";
startedAt: number;
finishedAt?: number;
exitCode?: number | null;
error?: string;
restartScheduled?: boolean;
pluginsReloaded?: string[];
droppedLines: number;
lineCount: number;
lines?: SystemRebuildJobLine[];
}
export interface SystemRebuildJobLine {
i: number;
ts: number;
stream: "stdout" | "stderr" | "system";
text: string;
}
export interface SystemInfoResponse {
supervised: boolean;
restartSupported: boolean;
rebuildSupported: boolean;
/** True when the host is a Fusion source checkout (dev) — can build & link local fn. */
fnBinaryLinkLocalSupported?: boolean;
/** Always true when the route is wired; UI may still disable while a job runs. */
fnBinaryUseGlobalSupported?: boolean;
sourceWorkspaceRoot?: string;
logsSupported: boolean;
engineAvailable: boolean;
pluginReloadSupported: boolean;
pid: number;
uptimeSeconds: number;
nodeVersion: string;
platform: string;
arch: string;
memoryRssBytes: number;
activeRebuild: SystemRebuildJobSnapshot | null;
lastRebuild: SystemRebuildJobSnapshot | null;
}
export interface SystemLogEntryDto {
timestamp: string;
level: "info" | "warn" | "error";
message: string;
prefix?: string;
}
export function fetchSystemInfo(): Promise<SystemInfoResponse> {
return api<SystemInfoResponse>("/system/info");
}
export function requestSystemRestart(reason?: string): Promise<{ scheduled: boolean }> {
return api<{ scheduled: boolean }>("/system/restart", {
method: "POST",
body: JSON.stringify({ reason }),
});
}
export function startSystemRebuild(
scope: "app" | "full" | "plugins",
restart?: boolean,
): Promise<SystemRebuildJobSnapshot> {
return api<SystemRebuildJobSnapshot>("/system/rebuild", {
method: "POST",
body: JSON.stringify({ scope, restart }),
});
}
/*
FNXC:SystemPanelFnBinary 2026-07-15-09:54:
Client wrappers for System panel fn-binary actions. Both return a job snapshot
that the panel streams via /system/jobs/:id/stream (same path as rebuild).
*/
export function startFnBinaryLinkLocal(): Promise<SystemRebuildJobSnapshot> {
return api<SystemRebuildJobSnapshot>("/system/fn-binary/link-local", { method: "POST" });
}
export function startFnBinaryUseGlobal(): Promise<SystemRebuildJobSnapshot> {
return api<SystemRebuildJobSnapshot>("/system/fn-binary/use-global", { method: "POST" });
}
export function fetchCurrentSystemRebuild(): Promise<{ job: SystemRebuildJobSnapshot | null }> {
return api<{ job: SystemRebuildJobSnapshot | null }>("/system/rebuild/current");
}
export function restartSystemEngines(): Promise<{
restarted: string[];
failed: Array<{ projectId: string; error: string }>;
}> {
return api("/system/engine/restart", { method: "POST" });
}
export function restartAllSystemAgents(projectId?: string): Promise<{
restarted: string[];
failed: Array<{ agentId: string; error: string }>;
}> {
return api(withProjectId("/system/agents/restart-all", projectId), { method: "POST" });
}
export function reloadAllSystemPlugins(): Promise<{
reloaded: string[];
failed: Array<{ id: string; error: string }>;
}> {
return api("/system/plugins/reload-all", { method: "POST" });
}
export function fetchSystemLogs(limit?: number): Promise<{ entries: SystemLogEntryDto[] }> {
const suffix = limit ? `?limit=${limit}` : "";
return api<{ entries: SystemLogEntryDto[] }>(`/system/logs${suffix}`);
}
export type ResearchFindingPromotionInput = {
findingId: string;
sliceId: string;
title?: string;
description?: string;
acceptanceCriteria?: string;
triage?: boolean;
taskId?: string;
};
export function promoteResearchFinding(
runId: string,
input: ResearchFindingPromotionInput,
projectId?: string,
): Promise<{ runId: string; findingId: string; feature: { id: string; status: string; taskId?: string }; citations: string[]; reused: boolean }> {
return api(withProjectId(`/research/runs/${encodeURIComponent(runId)}/findings/${encodeURIComponent(input.findingId)}/promote`, projectId), {
method: "POST",
body: JSON.stringify(input),
});
}

View File

@@ -0,0 +1,85 @@
/**
* FNXC:CodeOrganization 2026-07-18-14:00:
* Todo lists/items client API peeled from legacy.ts.
*/
import type {
TodoList,
TodoItem,
TodoListWithItems,
TodoListCreateInput,
TodoListUpdateInput,
TodoItemCreateInput,
TodoItemUpdateInput,
} from "@fusion/core";
import { api } from "./client.js";
import { withProjectId } from "./health.js";
// ── Todo API ─────────────────────────────────────────────────────────────────
/** Fetch all todo lists with their items */
export function fetchTodoLists(projectId?: string): Promise<TodoListWithItems[]> {
return api<TodoListWithItems[]>(withProjectId("/todos", projectId));
}
/** Create a new todo list */
export function createTodoList(title: string, projectId?: string): Promise<TodoList> {
const input: TodoListCreateInput = { title };
return api<TodoList>(withProjectId("/todos", projectId), {
method: "POST",
body: JSON.stringify(input),
});
}
/** Update a todo list title */
export function updateTodoList(id: string, title: string, projectId?: string): Promise<TodoList> {
const updates: TodoListUpdateInput = { title };
return api<TodoList>(withProjectId(`/todos/${encodeURIComponent(id)}`, projectId), {
method: "PATCH",
body: JSON.stringify(updates),
});
}
/** Delete a todo list and all its items */
export function deleteTodoList(id: string, projectId?: string): Promise<void> {
return api<void>(withProjectId(`/todos/${encodeURIComponent(id)}`, projectId), {
method: "DELETE",
});
}
/** Create a new item in a todo list */
export function createTodoItem(listId: string, text: string, projectId?: string): Promise<TodoItem> {
const input: TodoItemCreateInput = { text };
return api<TodoItem>(withProjectId(`/todos/${encodeURIComponent(listId)}/items`, projectId), {
method: "POST",
body: JSON.stringify(input),
});
}
/** Update a todo item (text and/or completed) */
export function updateTodoItem(
id: string,
data: { text?: string; completed?: boolean },
projectId?: string
): Promise<TodoItem> {
const updates: TodoItemUpdateInput = data;
return api<TodoItem>(withProjectId(`/todos/items/${encodeURIComponent(id)}`, projectId), {
method: "PATCH",
body: JSON.stringify(updates),
});
}
/** Delete a todo item */
export function deleteTodoItem(id: string, projectId?: string): Promise<void> {
return api<void>(withProjectId(`/todos/items/${encodeURIComponent(id)}`, projectId), {
method: "DELETE",
});
}
/** Reorder items within a todo list */
export function reorderTodoItems(listId: string, itemIds: string[], projectId?: string): Promise<void> {
return api<void>(withProjectId(`/todos/${encodeURIComponent(listId)}/items/reorder`, projectId), {
method: "POST",
body: JSON.stringify({ itemIds }),
});
}