refactor: package code organization wave 9 (#2274)
## Summary Wave 9 of package code organization (plan: `docs/plans/2026-07-14-001-refactor-package-code-organization-plan.md`), after #2252. ### Peels | New module | Parent | |---|---| | `app/api/global-and-pi-settings.ts` | `legacy.ts` | | `app/api/task-content.ts` | `legacy.ts` (attachments/logs/comments/docs/artifacts) | | `types/messages.ts` | `types.ts` | | `merger-diff-volume-gate` helpers | `merger.ts` | Public paths stay stable via re-exports. ### LOC - `legacy.ts` ~10561 → ~10269 - `types.ts` ~6998 → ~6937 - `merger.ts` ~11223 → ~11208 ## Test plan - [x] core/engine/dashboard typecheck (app) - [x] eslint on peeled modules - [x] api-tasks + memory/agent-log focused tests - [ ] CI merge gate <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added dashboard API endpoints for global and project-scoped settings, PI configuration, notification testing, and PI package install/reinstall. * Added task content APIs covering attachments, agent logs, session files, task comments, task documents, artifacts, and project markdown files. * Added structured messaging contracts and participant/message normalization. * Added diff-volume gate settings resolution and formatted findings output. * **Refactor** * Organized messaging and dashboard API surface into dedicated modules via re-exports. * Removed a deprecated verification-output alias. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -6980,131 +6980,41 @@ export interface MigrationResult {
|
||||
}
|
||||
|
||||
// ── Messaging Types ──────────────────────────────────────────────────────────
|
||||
// FNXC:CodeOrganization 2026-07-18-00:35: Keep stable re-exports after main
|
||||
// landed task-proposal metadata + ephemeral policy on the mailbox contract.
|
||||
|
||||
/** Participant types for message routing */
|
||||
export type ParticipantType = "agent" | "user" | "system";
|
||||
|
||||
/** Canonical recipient ID for dashboard user mailbox routing. */
|
||||
export const DASHBOARD_USER_ID = "dashboard";
|
||||
|
||||
const DASHBOARD_USER_ALIASES = new Set([DASHBOARD_USER_ID, "user", "user:dashboard", "User: user:dashboard"]);
|
||||
|
||||
/** Normalize participant identity for durable mailbox routing. */
|
||||
export function normalizeMessageParticipant(id: string, type: ParticipantType): { id: string; type: ParticipantType } {
|
||||
if (type !== "user") {
|
||||
return { id, type };
|
||||
}
|
||||
|
||||
if (DASHBOARD_USER_ALIASES.has(id)) {
|
||||
return { id: DASHBOARD_USER_ID, type };
|
||||
}
|
||||
|
||||
return { id, type };
|
||||
}
|
||||
|
||||
/** Message types/categories */
|
||||
export type MessageType = "agent-to-agent" | "agent-to-user" | "user-to-agent" | "system";
|
||||
|
||||
/** Stable metadata contract for linking a reply to an earlier message. */
|
||||
export interface MessageReplyReference {
|
||||
/** ID of the message this one is replying to. */
|
||||
messageId: string;
|
||||
}
|
||||
|
||||
/** Optional metadata attached to mailbox messages. */
|
||||
export type EphemeralTaskCreationPolicy = "allow" | "upon_validation" | "deny";
|
||||
|
||||
/** Resolve the non-default policy without masking legacy persisted settings. */
|
||||
export function resolveEphemeralTaskCreationPolicy(settings: Pick<Settings, "ephemeralAgentTaskCreationPolicy" | "ephemeralAgentsCanCreateTasks">): EphemeralTaskCreationPolicy {
|
||||
if (settings.ephemeralAgentTaskCreationPolicy === "allow" || settings.ephemeralAgentTaskCreationPolicy === "upon_validation" || settings.ephemeralAgentTaskCreationPolicy === "deny") return settings.ephemeralAgentTaskCreationPolicy;
|
||||
return settings.ephemeralAgentsCanCreateTasks === false ? "deny" : "allow";
|
||||
}
|
||||
|
||||
export interface ProposedTaskMetadata {
|
||||
title: string;
|
||||
description: string;
|
||||
priority?: TaskPriority;
|
||||
workflowId?: string;
|
||||
dependencies?: string[];
|
||||
}
|
||||
|
||||
export interface MessageMetadata extends Record<string, unknown> {
|
||||
/** Optional link to the original message when this message is a reply. */
|
||||
replyTo?: MessageReplyReference;
|
||||
/**
|
||||
* If true, the recipient agent is woken immediately on receipt regardless
|
||||
* of their own `messageResponseMode` setting. Sender-initiated override —
|
||||
* use sparingly for urgent messages. Ignored when recipient is a user.
|
||||
*/
|
||||
wakeRecipient?: boolean;
|
||||
/** Structured operator-approved follow-up task proposal. */
|
||||
kind?: string;
|
||||
proposedTask?: ProposedTaskMetadata;
|
||||
proposalStatus?: "pending" | "creating" | "created" | "dismissed";
|
||||
createdTaskId?: string;
|
||||
/** Stable proposal key issued at send time and never rotated across reclaims. */
|
||||
proposalIdempotencyKey?: string;
|
||||
/** Transient owner token for the current creating lease only. */
|
||||
claimOwnerToken?: string;
|
||||
/** Durable ISO timestamp used to reclaim a creator that died before task persistence. */
|
||||
claimStartedAt?: string;
|
||||
}
|
||||
|
||||
/** Message record stored in the system */
|
||||
export interface Message {
|
||||
/** Unique identifier */
|
||||
id: string;
|
||||
/** Sender identifier */
|
||||
fromId: string;
|
||||
/** Sender type */
|
||||
fromType: ParticipantType;
|
||||
/** Recipient identifier */
|
||||
toId: string;
|
||||
/** Recipient type */
|
||||
toType: ParticipantType;
|
||||
/** Message body */
|
||||
content: string;
|
||||
/** Message category */
|
||||
type: MessageType;
|
||||
/** Whether the recipient has read this message */
|
||||
read: boolean;
|
||||
/** Optional extra data */
|
||||
metadata?: MessageMetadata;
|
||||
/** ISO-8601 timestamp of creation */
|
||||
createdAt: string;
|
||||
/** ISO-8601 timestamp of last update */
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/** Input for creating a new message */
|
||||
export interface MessageCreateInput {
|
||||
/** Sender identifier (auto-filled by the transport layer if omitted) */
|
||||
fromId?: string;
|
||||
/** Sender type (auto-filled by the transport layer if omitted) */
|
||||
fromType?: ParticipantType;
|
||||
/** Recipient identifier */
|
||||
toId: string;
|
||||
/** Recipient type */
|
||||
toType: ParticipantType;
|
||||
/** Message body */
|
||||
content: string;
|
||||
/** Message category */
|
||||
type: MessageType;
|
||||
/** Optional extra data */
|
||||
metadata?: MessageMetadata;
|
||||
}
|
||||
|
||||
/** Filter options for querying messages */
|
||||
export interface MessageFilter {
|
||||
/** Filter by message type */
|
||||
type?: MessageType;
|
||||
/** Filter by read status */
|
||||
read?: boolean;
|
||||
/** Maximum number of messages to return */
|
||||
limit?: number;
|
||||
/** Number of messages to skip (for pagination) */
|
||||
offset?: number;
|
||||
}
|
||||
import {
|
||||
DASHBOARD_USER_ID,
|
||||
normalizeMessageParticipant,
|
||||
resolveEphemeralTaskCreationPolicy,
|
||||
} from "./types/messages.js";
|
||||
export {
|
||||
DASHBOARD_USER_ID,
|
||||
normalizeMessageParticipant,
|
||||
resolveEphemeralTaskCreationPolicy,
|
||||
};
|
||||
import type {
|
||||
ParticipantType,
|
||||
MessageType,
|
||||
MessageReplyReference,
|
||||
EphemeralTaskCreationPolicy,
|
||||
ProposedTaskMetadata,
|
||||
MessageMetadata,
|
||||
Message,
|
||||
MessageCreateInput,
|
||||
MessageFilter,
|
||||
} from "./types/messages.js";
|
||||
export type {
|
||||
ParticipantType,
|
||||
MessageType,
|
||||
MessageReplyReference,
|
||||
EphemeralTaskCreationPolicy,
|
||||
ProposedTaskMetadata,
|
||||
MessageMetadata,
|
||||
Message,
|
||||
MessageCreateInput,
|
||||
MessageFilter,
|
||||
};
|
||||
|
||||
/** Validate mailbox metadata, including reply-link contract when present. */
|
||||
export function validateMessageMetadata(metadata: MessageMetadata | undefined): void {
|
||||
|
||||
143
packages/core/src/types/messages.ts
Normal file
143
packages/core/src/types/messages.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* FNXC:CodeOrganization 2026-07-17-12:00:
|
||||
* Messaging domain types peeled from types.ts.
|
||||
*
|
||||
* FNXC:CodeOrganization 2026-07-18-00:35:
|
||||
* Main landed task-proposal metadata and ephemeral task-creation policy on the
|
||||
* mailbox contract. Keep those symbols in this peel so types.ts only re-exports.
|
||||
*/
|
||||
|
||||
import type { TaskPriority } from "./board.js";
|
||||
|
||||
export type ParticipantType = "agent" | "user" | "system";
|
||||
|
||||
/** Canonical recipient ID for dashboard user mailbox routing. */
|
||||
export const DASHBOARD_USER_ID = "dashboard";
|
||||
|
||||
const DASHBOARD_USER_ALIASES = new Set([DASHBOARD_USER_ID, "user", "user:dashboard", "User: user:dashboard"]);
|
||||
|
||||
/** Normalize participant identity for durable mailbox routing. */
|
||||
export function normalizeMessageParticipant(id: string, type: ParticipantType): { id: string; type: ParticipantType } {
|
||||
if (type !== "user") {
|
||||
return { id, type };
|
||||
}
|
||||
|
||||
if (DASHBOARD_USER_ALIASES.has(id)) {
|
||||
return { id: DASHBOARD_USER_ID, type };
|
||||
}
|
||||
|
||||
return { id, type };
|
||||
}
|
||||
|
||||
/** Message types/categories */
|
||||
export type MessageType = "agent-to-agent" | "agent-to-user" | "user-to-agent" | "system";
|
||||
|
||||
/** Stable metadata contract for linking a reply to an earlier message. */
|
||||
export interface MessageReplyReference {
|
||||
/** ID of the message this one is replying to. */
|
||||
messageId: string;
|
||||
}
|
||||
|
||||
/** Optional metadata attached to mailbox messages. */
|
||||
export type EphemeralTaskCreationPolicy = "allow" | "upon_validation" | "deny";
|
||||
|
||||
/** Resolve the non-default policy without masking legacy persisted settings. */
|
||||
export function resolveEphemeralTaskCreationPolicy(settings: {
|
||||
ephemeralAgentTaskCreationPolicy?: EphemeralTaskCreationPolicy;
|
||||
ephemeralAgentsCanCreateTasks?: boolean;
|
||||
}): EphemeralTaskCreationPolicy {
|
||||
if (
|
||||
settings.ephemeralAgentTaskCreationPolicy === "allow" ||
|
||||
settings.ephemeralAgentTaskCreationPolicy === "upon_validation" ||
|
||||
settings.ephemeralAgentTaskCreationPolicy === "deny"
|
||||
) {
|
||||
return settings.ephemeralAgentTaskCreationPolicy;
|
||||
}
|
||||
return settings.ephemeralAgentsCanCreateTasks === false ? "deny" : "allow";
|
||||
}
|
||||
|
||||
export interface ProposedTaskMetadata {
|
||||
title: string;
|
||||
description: string;
|
||||
priority?: TaskPriority;
|
||||
workflowId?: string;
|
||||
dependencies?: string[];
|
||||
}
|
||||
|
||||
export interface MessageMetadata extends Record<string, unknown> {
|
||||
/** Optional link to the original message when this message is a reply. */
|
||||
replyTo?: MessageReplyReference;
|
||||
/**
|
||||
* If true, the recipient agent is woken immediately on receipt regardless
|
||||
* of their own `messageResponseMode` setting. Sender-initiated override —
|
||||
* use sparingly for urgent messages. Ignored when recipient is a user.
|
||||
*/
|
||||
wakeRecipient?: boolean;
|
||||
/** Structured operator-approved follow-up task proposal. */
|
||||
kind?: string;
|
||||
proposedTask?: ProposedTaskMetadata;
|
||||
proposalStatus?: "pending" | "creating" | "created" | "dismissed";
|
||||
createdTaskId?: string;
|
||||
/** Stable proposal key issued at send time and never rotated across reclaims. */
|
||||
proposalIdempotencyKey?: string;
|
||||
/** Transient owner token for the current creating lease only. */
|
||||
claimOwnerToken?: string;
|
||||
/** Durable ISO timestamp used to reclaim a creator that died before task persistence. */
|
||||
claimStartedAt?: string;
|
||||
}
|
||||
|
||||
/** Message record stored in the system */
|
||||
export interface Message {
|
||||
/** Unique identifier */
|
||||
id: string;
|
||||
/** Sender identifier */
|
||||
fromId: string;
|
||||
/** Sender type */
|
||||
fromType: ParticipantType;
|
||||
/** Recipient identifier */
|
||||
toId: string;
|
||||
/** Recipient type */
|
||||
toType: ParticipantType;
|
||||
/** Message body */
|
||||
content: string;
|
||||
/** Message category */
|
||||
type: MessageType;
|
||||
/** Whether the recipient has read this message */
|
||||
read: boolean;
|
||||
/** Optional extra data */
|
||||
metadata?: MessageMetadata;
|
||||
/** ISO-8601 timestamp of creation */
|
||||
createdAt: string;
|
||||
/** ISO-8601 timestamp of last update */
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/** Input for creating a new message */
|
||||
export interface MessageCreateInput {
|
||||
/** Sender identifier (auto-filled by the transport layer if omitted) */
|
||||
fromId?: string;
|
||||
/** Sender type (auto-filled by the transport layer if omitted) */
|
||||
fromType?: ParticipantType;
|
||||
/** Recipient identifier */
|
||||
toId: string;
|
||||
/** Recipient type */
|
||||
toType: ParticipantType;
|
||||
/** Message body */
|
||||
content: string;
|
||||
/** Message category */
|
||||
type: MessageType;
|
||||
/** Optional extra data */
|
||||
metadata?: MessageMetadata;
|
||||
}
|
||||
|
||||
/** Filter options for querying messages */
|
||||
export interface MessageFilter {
|
||||
/** Filter by message type */
|
||||
type?: MessageType;
|
||||
/** Filter by read status */
|
||||
read?: boolean;
|
||||
/** Maximum number of messages to return */
|
||||
limit?: number;
|
||||
/** Number of messages to skip (for pagination) */
|
||||
offset?: number;
|
||||
}
|
||||
117
packages/dashboard/app/api/global-and-pi-settings.ts
Normal file
117
packages/dashboard/app/api/global-and-pi-settings.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* FNXC:CodeOrganization 2026-07-17-12:00:
|
||||
* Global settings and pi-extension/package client API peeled from legacy.ts.
|
||||
*/
|
||||
import type { GlobalSettings, ProjectSettings, Settings } from "@fusion/core";
|
||||
import { api } from "./client.js";
|
||||
import type { FetchOptions } from "./client.js";
|
||||
import { withProjectId } from "./health.js";
|
||||
import { dedupe } from "./dedupe.js";
|
||||
|
||||
export function fetchGlobalSettings(options?: FetchOptions): Promise<GlobalSettings> {
|
||||
return dedupe("/settings/global", () => api<GlobalSettings>("/settings/global"), options);
|
||||
}
|
||||
|
||||
/** Update global (user-level) settings. These persist across all fn projects. */
|
||||
export function updateGlobalSettings(settings: Partial<GlobalSettings>): Promise<Settings> {
|
||||
return api<Settings>("/settings/global", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(settings),
|
||||
});
|
||||
}
|
||||
|
||||
/** Fetch settings separated by scope: { global, project } */
|
||||
export function fetchSettingsByScope(projectId?: string): Promise<{ global: GlobalSettings; project: Partial<ProjectSettings> }> {
|
||||
return api<{ global: GlobalSettings; project: Partial<ProjectSettings> }>(withProjectId("/settings/scopes", projectId));
|
||||
}
|
||||
|
||||
export interface PiExtensionEntry {
|
||||
id: string;
|
||||
name: string;
|
||||
path: string;
|
||||
source: "fusion-global" | "pi-global" | "fusion-project" | "pi-project" | "package";
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface PiExtensionSettings {
|
||||
extensions: PiExtensionEntry[];
|
||||
disabledIds: string[];
|
||||
settingsPath: string;
|
||||
}
|
||||
|
||||
export function fetchPiExtensions(projectId?: string): Promise<PiExtensionSettings> {
|
||||
return api<PiExtensionSettings>(withProjectId("/settings/pi-extensions", projectId));
|
||||
}
|
||||
|
||||
export function updatePiExtensions(disabledIds: string[], projectId?: string): Promise<PiExtensionSettings> {
|
||||
return api<PiExtensionSettings>(withProjectId("/settings/pi-extensions", projectId), {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ disabledIds }),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Test a notification provider by sending a test notification.
|
||||
* Supports "ntfy" and "webhook" provider IDs.
|
||||
*/
|
||||
export function testNotification(providerId: string, config?: Record<string, unknown>, projectId?: string): Promise<{ success: boolean }> {
|
||||
return api<{ success: boolean }>(withProjectId("/settings/test-notification", projectId), {
|
||||
method: "POST",
|
||||
// Pin providerId last so config.providerId cannot override the selected provider.
|
||||
body: JSON.stringify({ ...(config ?? {}), providerId }),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Backward-compatible ntfy test helper.
|
||||
* Wraps testNotification() while preserving the legacy function signature.
|
||||
*/
|
||||
export function testNtfyNotification(
|
||||
config?: {
|
||||
ntfyEnabled?: boolean;
|
||||
ntfyTopic?: string;
|
||||
ntfyBaseUrl?: string;
|
||||
ntfyAccessToken?: string;
|
||||
},
|
||||
projectId?: string,
|
||||
): Promise<{ success: boolean }> {
|
||||
return testNotification("ntfy", config as Record<string, unknown> | undefined, projectId);
|
||||
}
|
||||
|
||||
/** Pi extension settings from ~/.pi/agent/settings.json (global scope) */
|
||||
export interface PiSettings {
|
||||
packages: Array<string | { source: string; extensions?: string[]; skills?: string[]; prompts?: string[]; themes?: string[] }>;
|
||||
extensions: string[];
|
||||
skills: string[];
|
||||
prompts: string[];
|
||||
themes: string[];
|
||||
}
|
||||
|
||||
/** Fetch pi extension settings (global scope from ~/.pi/agent/settings.json) */
|
||||
export function fetchPiSettings(): Promise<PiSettings> {
|
||||
return api<PiSettings>("/pi-settings");
|
||||
}
|
||||
|
||||
/** Update pi extension settings (partial update, global scope) */
|
||||
export async function updatePiSettings(settings: Partial<PiSettings>): Promise<{ success: boolean }> {
|
||||
return api<{ success: boolean }>("/pi-settings", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(settings),
|
||||
});
|
||||
}
|
||||
|
||||
/** Install a new pi package source (adds to ~/.pi/agent/settings.json) */
|
||||
export async function installPiPackage(source: string): Promise<{ success: boolean }> {
|
||||
return api<{ success: boolean }>("/pi-settings/packages", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ source }),
|
||||
});
|
||||
}
|
||||
|
||||
/** Reinstall Fusion's bundled pi package and ensure it remains in global Pi settings. */
|
||||
export async function reinstallFusionPiPackage(projectId?: string): Promise<{ success: boolean; source: string }> {
|
||||
return api<{ success: boolean; source: string }>(withProjectId("/pi-settings/reinstall-fusion", projectId), {
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,10 +2,7 @@ import type {
|
||||
Task,
|
||||
TaskDetail,
|
||||
TaskReviewData,
|
||||
TaskAttachment,
|
||||
TaskComment,
|
||||
AgentLogEntry,
|
||||
Settings,
|
||||
GlobalSettings,
|
||||
ProjectSettings,
|
||||
BatchStatusResult,
|
||||
@@ -20,12 +17,6 @@ import type {
|
||||
PluginUiSlotDefinition,
|
||||
PluginUiContributionDefinition,
|
||||
PluginDashboardViewDefinition,
|
||||
TaskDocument,
|
||||
TaskDocumentRevision,
|
||||
TaskDocumentWithTask,
|
||||
Artifact,
|
||||
ArtifactType,
|
||||
ArtifactWithTask,
|
||||
|
||||
Message,
|
||||
MessageMetadata,
|
||||
@@ -223,6 +214,64 @@ export {
|
||||
} from "./settings.js";
|
||||
export type { UpdateInstallResponse } from "./settings.js";
|
||||
|
||||
/*
|
||||
* FNXC:CodeOrganization 2026-07-17-12:00:
|
||||
* Preserve legacy global/pi settings and task-content imports via satellites.
|
||||
*/
|
||||
export {
|
||||
fetchGlobalSettings,
|
||||
updateGlobalSettings,
|
||||
fetchSettingsByScope,
|
||||
fetchPiExtensions,
|
||||
updatePiExtensions,
|
||||
testNotification,
|
||||
testNtfyNotification,
|
||||
fetchPiSettings,
|
||||
updatePiSettings,
|
||||
installPiPackage,
|
||||
reinstallFusionPiPackage,
|
||||
} from "./global-and-pi-settings.js";
|
||||
export type {
|
||||
PiExtensionEntry,
|
||||
PiExtensionSettings,
|
||||
PiSettings,
|
||||
} from "./global-and-pi-settings.js";
|
||||
|
||||
export {
|
||||
uploadAttachment,
|
||||
deleteAttachment,
|
||||
fetchAgentLogs,
|
||||
fetchAgentLogsWithMeta,
|
||||
fetchSessionFiles,
|
||||
fetchTaskComments,
|
||||
addTaskComment,
|
||||
updateTaskComment,
|
||||
deleteTaskComment,
|
||||
fetchTaskDocuments,
|
||||
fetchTaskDocument,
|
||||
fetchTaskDocumentRevisions,
|
||||
fetchArtifacts,
|
||||
artifactMediaUrl,
|
||||
artifactMediaUrlWithToken,
|
||||
fetchArtifact,
|
||||
updateArtifact,
|
||||
fetchAllDocuments,
|
||||
fetchProjectMarkdownFiles,
|
||||
putTaskDocument,
|
||||
deleteTaskDocument,
|
||||
} from "./task-content.js";
|
||||
export type {
|
||||
FetchAllDocumentsOptions,
|
||||
MarkdownFileEntry,
|
||||
MarkdownFileListResponse,
|
||||
FetchArtifactsOptions,
|
||||
FetchProjectMarkdownFilesOptions,
|
||||
UpdateArtifactInput,
|
||||
} from "./task-content.js";
|
||||
// Artifact types still re-exported from core for callers of legacy barrel
|
||||
export type { Artifact, ArtifactType, ArtifactWithTask } from "@fusion/core";
|
||||
|
||||
|
||||
/*
|
||||
* FNXC:CodeOrganization 2026-07-16-20:00:
|
||||
* Preserve legacy board/remote/memory imports while implementations live in satellites.
|
||||
@@ -321,368 +370,6 @@ export type {
|
||||
SkillFileContent,
|
||||
};
|
||||
|
||||
export function fetchGlobalSettings(options?: FetchOptions): Promise<GlobalSettings> {
|
||||
return dedupe("/settings/global", () => api<GlobalSettings>("/settings/global"), options);
|
||||
}
|
||||
|
||||
/** Update global (user-level) settings. These persist across all fn projects. */
|
||||
export function updateGlobalSettings(settings: Partial<GlobalSettings>): Promise<Settings> {
|
||||
return api<Settings>("/settings/global", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(settings),
|
||||
});
|
||||
}
|
||||
|
||||
/** Fetch settings separated by scope: { global, project } */
|
||||
export function fetchSettingsByScope(projectId?: string): Promise<{ global: GlobalSettings; project: Partial<ProjectSettings> }> {
|
||||
return api<{ global: GlobalSettings; project: Partial<ProjectSettings> }>(withProjectId("/settings/scopes", projectId));
|
||||
}
|
||||
|
||||
export interface PiExtensionEntry {
|
||||
id: string;
|
||||
name: string;
|
||||
path: string;
|
||||
source: "fusion-global" | "pi-global" | "fusion-project" | "pi-project" | "package";
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface PiExtensionSettings {
|
||||
extensions: PiExtensionEntry[];
|
||||
disabledIds: string[];
|
||||
settingsPath: string;
|
||||
}
|
||||
|
||||
export function fetchPiExtensions(projectId?: string): Promise<PiExtensionSettings> {
|
||||
return api<PiExtensionSettings>(withProjectId("/settings/pi-extensions", projectId));
|
||||
}
|
||||
|
||||
export function updatePiExtensions(disabledIds: string[], projectId?: string): Promise<PiExtensionSettings> {
|
||||
return api<PiExtensionSettings>(withProjectId("/settings/pi-extensions", projectId), {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ disabledIds }),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Test a notification provider by sending a test notification.
|
||||
* Supports "ntfy" and "webhook" provider IDs.
|
||||
*/
|
||||
export function testNotification(providerId: string, config?: Record<string, unknown>, projectId?: string): Promise<{ success: boolean }> {
|
||||
return api<{ success: boolean }>(withProjectId("/settings/test-notification", projectId), {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ providerId, ...(config ?? {}) }),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Backward-compatible ntfy test helper.
|
||||
* Wraps testNotification() while preserving the legacy function signature.
|
||||
*/
|
||||
export function testNtfyNotification(
|
||||
config?: {
|
||||
ntfyEnabled?: boolean;
|
||||
ntfyTopic?: string;
|
||||
ntfyBaseUrl?: string;
|
||||
ntfyAccessToken?: string;
|
||||
},
|
||||
projectId?: string,
|
||||
): Promise<{ success: boolean }> {
|
||||
return testNotification("ntfy", config as Record<string, unknown> | undefined, projectId);
|
||||
}
|
||||
|
||||
/** Pi extension settings from ~/.pi/agent/settings.json (global scope) */
|
||||
export interface PiSettings {
|
||||
packages: Array<string | { source: string; extensions?: string[]; skills?: string[]; prompts?: string[]; themes?: string[] }>;
|
||||
extensions: string[];
|
||||
skills: string[];
|
||||
prompts: string[];
|
||||
themes: string[];
|
||||
}
|
||||
|
||||
/** Fetch pi extension settings (global scope from ~/.pi/agent/settings.json) */
|
||||
export function fetchPiSettings(): Promise<PiSettings> {
|
||||
return api<PiSettings>("/pi-settings");
|
||||
}
|
||||
|
||||
/** Update pi extension settings (partial update, global scope) */
|
||||
export async function updatePiSettings(settings: Partial<PiSettings>): Promise<{ success: boolean }> {
|
||||
return api<{ success: boolean }>("/pi-settings", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(settings),
|
||||
});
|
||||
}
|
||||
|
||||
/** Install a new pi package source (adds to ~/.pi/agent/settings.json) */
|
||||
export async function installPiPackage(source: string): Promise<{ success: boolean }> {
|
||||
return api<{ success: boolean }>("/pi-settings/packages", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ source }),
|
||||
});
|
||||
}
|
||||
|
||||
/** Reinstall Fusion's bundled pi package and ensure it remains in global Pi settings. */
|
||||
export async function reinstallFusionPiPackage(projectId?: string): Promise<{ success: boolean; source: string }> {
|
||||
return api<{ success: boolean; source: string }>(withProjectId("/pi-settings/reinstall-fusion", projectId), {
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
|
||||
export async function uploadAttachment(id: string, file: File, projectId?: string): Promise<TaskAttachment> {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
const res = await fetch(buildApiUrl(withProjectId(`/tasks/${id}/attachments`, projectId)), {
|
||||
method: "POST",
|
||||
headers: withTokenHeader(),
|
||||
body: formData,
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error((data as { error?: string }).error || "Upload failed");
|
||||
return data as TaskAttachment;
|
||||
}
|
||||
|
||||
export async function deleteAttachment(id: string, filename: string, projectId?: string): Promise<Task> {
|
||||
return api<Task>(withProjectId(`/tasks/${id}/attachments/${filename}`, projectId), { method: "DELETE" });
|
||||
}
|
||||
|
||||
export function fetchAgentLogs(
|
||||
taskId: string,
|
||||
projectId?: string,
|
||||
options?: { limit?: number; offset?: number },
|
||||
): Promise<AgentLogEntry[]> {
|
||||
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.toString() ? `?${params.toString()}` : "";
|
||||
return api<AgentLogEntry[]>(withProjectId(`/tasks/${taskId}/logs${suffix}`, projectId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch agent logs with pagination metadata.
|
||||
* Returns entries along with total count and hasMore flag from response headers.
|
||||
*/
|
||||
export async function fetchAgentLogsWithMeta(
|
||||
taskId: string,
|
||||
projectId?: string,
|
||||
options?: { limit?: number; offset?: number },
|
||||
): Promise<{ entries: AgentLogEntry[]; total: number; hasMore: boolean }> {
|
||||
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.toString() ? `?${params.toString()}` : "";
|
||||
const url = withProjectId(`/tasks/${taskId}/logs${suffix}`, projectId);
|
||||
|
||||
const response = await fetch(buildApiUrl(url), {
|
||||
headers: withTokenHeader(),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({ error: "Failed to fetch agent logs" }));
|
||||
throw new Error((data as { error?: string }).error || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const entries = await response.json() as AgentLogEntry[];
|
||||
|
||||
// Read pagination headers
|
||||
const total = response.headers.has("X-Total-Count")
|
||||
? parseInt(response.headers.get("X-Total-Count")!, 10)
|
||||
: entries.length;
|
||||
const hasMore = response.headers.has("X-Has-More")
|
||||
? response.headers.get("X-Has-More") === "true"
|
||||
: false;
|
||||
|
||||
return { entries, total, hasMore };
|
||||
}
|
||||
|
||||
export function fetchSessionFiles(taskId: string, projectId?: string): Promise<string[]> {
|
||||
return api<string[]>(withProjectId(`/tasks/${taskId}/session-files`, projectId));
|
||||
}
|
||||
|
||||
export function fetchTaskComments(id: string, projectId?: string): Promise<TaskComment[]> {
|
||||
return api<TaskComment[]>(withProjectId(`/tasks/${id}/comments`, projectId));
|
||||
}
|
||||
|
||||
export function addTaskComment(id: string, text: string, author?: string, projectId?: string): Promise<Task> {
|
||||
return api<Task>(withProjectId(`/tasks/${id}/comments`, projectId), {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ text, author }),
|
||||
});
|
||||
}
|
||||
|
||||
export function updateTaskComment(id: string, commentId: string, text: string, projectId?: string): Promise<Task> {
|
||||
return api<Task>(withProjectId(`/tasks/${id}/comments/${commentId}`, projectId), {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ text }),
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteTaskComment(id: string, commentId: string, projectId?: string): Promise<Task> {
|
||||
return api<Task>(withProjectId(`/tasks/${id}/comments/${commentId}`, projectId), {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
|
||||
// ── Task Document API Functions ──────────────────────────────────────────────
|
||||
|
||||
export function fetchTaskDocuments(taskId: string, projectId?: string): Promise<TaskDocument[]> {
|
||||
return api<TaskDocument[]>(withProjectId(`/tasks/${taskId}/documents`, projectId));
|
||||
}
|
||||
|
||||
export function fetchTaskDocument(taskId: string, key: string, projectId?: string): Promise<TaskDocument> {
|
||||
return api<TaskDocument>(withProjectId(`/tasks/${taskId}/documents/${key}`, projectId));
|
||||
}
|
||||
|
||||
export function fetchTaskDocumentRevisions(taskId: string, key: string, projectId?: string): Promise<TaskDocumentRevision[]> {
|
||||
return api<TaskDocumentRevision[]>(withProjectId(`/tasks/${taskId}/documents/${key}/revisions`, projectId));
|
||||
}
|
||||
|
||||
export interface FetchAllDocumentsOptions {
|
||||
q?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
export interface MarkdownFileEntry {
|
||||
path: string;
|
||||
name: string;
|
||||
size: number;
|
||||
mtime: string;
|
||||
}
|
||||
|
||||
export interface MarkdownFileListResponse {
|
||||
files: MarkdownFileEntry[];
|
||||
}
|
||||
|
||||
export type { Artifact, ArtifactType, ArtifactWithTask };
|
||||
|
||||
export interface FetchArtifactsOptions {
|
||||
type?: ArtifactType;
|
||||
authorId?: string;
|
||||
taskId?: string;
|
||||
q?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
export async function fetchArtifacts(
|
||||
options?: FetchArtifactsOptions,
|
||||
projectId?: string,
|
||||
): Promise<ArtifactWithTask[]> {
|
||||
const params = new URLSearchParams();
|
||||
if (options?.type) params.set("type", options.type);
|
||||
if (options?.authorId) params.set("authorId", options.authorId);
|
||||
if (options?.taskId) params.set("taskId", options.taskId);
|
||||
if (options?.q) params.set("q", options.q);
|
||||
if (options?.limit !== undefined) params.set("limit", String(options.limit));
|
||||
if (options?.offset !== undefined) params.set("offset", String(options.offset));
|
||||
const queryString = params.toString();
|
||||
const path = `/artifacts${queryString ? `?${queryString}` : ""}`;
|
||||
return api<ArtifactWithTask[]>(withProjectId(path, projectId));
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:ArtifactRegistry 2026-07-15-12:00:
|
||||
Keep artifactMediaUrl token-free so fetch callers and script-capable HTML previews can authenticate via Authorization without putting the daemon token into a URL that executable artifact content could read.
|
||||
|
||||
FNXC:ArtifactMediaAuth 2026-07-15-14:24:
|
||||
Main previously always-tokenized this helper for browser-native media loads. FN-7976 supersedes that by splitting tokenized element/link loads into artifactMediaUrlWithToken while this base URL stays clean for header-auth fetch and HTML blob previews.
|
||||
*/
|
||||
export function artifactMediaUrl(id: string, projectId?: string): string {
|
||||
return buildApiUrl(withProjectId(`/artifacts/${encodeURIComponent(id)}/media`, projectId));
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:ArtifactRegistry 2026-07-15-12:00:
|
||||
Artifact media element loads and link navigations cannot attach an Authorization header, so authenticated daemon media routes require the dashboard-owned fn_token query fallback. Keep artifactMediaUrl token-free for fetch callers and script-capable HTML previews; consumers that hand the URL to an img, video, audio, iframe, or anchor must use this helper unless executable content could read the URL.
|
||||
*/
|
||||
export function artifactMediaUrlWithToken(id: string, projectId?: string): string {
|
||||
return appendTokenQuery(artifactMediaUrl(id, projectId));
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:ArtifactRegistry 2026-07-10-15:20:
|
||||
The Artifacts view document viewer needs the full artifact INCLUDING inline content (list responses strip content), and edit mode persists title/description/content through PATCH.
|
||||
*/
|
||||
export async function fetchArtifact(id: string, projectId?: string): Promise<Artifact> {
|
||||
return api<Artifact>(withProjectId(`/artifacts/${encodeURIComponent(id)}`, projectId));
|
||||
}
|
||||
|
||||
export interface UpdateArtifactInput {
|
||||
title?: string;
|
||||
description?: string;
|
||||
content?: string;
|
||||
}
|
||||
|
||||
export async function updateArtifact(id: string, updates: UpdateArtifactInput, projectId?: string): Promise<Artifact> {
|
||||
return api<Artifact>(withProjectId(`/artifacts/${encodeURIComponent(id)}`, projectId), {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(updates),
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchAllDocuments(
|
||||
options?: FetchAllDocumentsOptions,
|
||||
projectId?: string,
|
||||
): Promise<TaskDocumentWithTask[]> {
|
||||
const params = new URLSearchParams();
|
||||
if (options?.q) params.set("q", options.q);
|
||||
if (options?.limit !== undefined) params.set("limit", String(options.limit));
|
||||
if (options?.offset !== undefined) params.set("offset", String(options.offset));
|
||||
const queryString = params.toString();
|
||||
const path = `/documents${queryString ? `?${queryString}` : ""}`;
|
||||
return api<TaskDocumentWithTask[]>(withProjectId(path, projectId));
|
||||
}
|
||||
|
||||
export interface FetchProjectMarkdownFilesOptions {
|
||||
showHidden?: boolean;
|
||||
}
|
||||
|
||||
export function fetchProjectMarkdownFiles(
|
||||
projectId?: string,
|
||||
options?: FetchProjectMarkdownFilesOptions,
|
||||
): Promise<MarkdownFileListResponse> {
|
||||
const params = new URLSearchParams();
|
||||
if (options?.showHidden) {
|
||||
params.set("showHidden", "1");
|
||||
}
|
||||
|
||||
const query = params.toString();
|
||||
const path = `/files/markdown-list${query ? `?${query}` : ""}`;
|
||||
|
||||
return api<MarkdownFileListResponse>(withProjectId(path, projectId));
|
||||
}
|
||||
|
||||
export function putTaskDocument(
|
||||
taskId: string,
|
||||
key: string,
|
||||
content: string,
|
||||
opts?: { author?: string; metadata?: Record<string, unknown> },
|
||||
projectId?: string,
|
||||
): Promise<TaskDocument> {
|
||||
return api<TaskDocument>(withProjectId(`/tasks/${taskId}/documents/${key}`, projectId), {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
content,
|
||||
author: opts?.author,
|
||||
metadata: opts?.metadata,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteTaskDocument(taskId: string, key: string, projectId?: string): Promise<void> {
|
||||
return api<void>(withProjectId(`/tasks/${taskId}/documents/${key}`, projectId), {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
|
||||
export function addSteeringComment(id: string, text: string, projectId?: string): Promise<Task> {
|
||||
return api<Task>(withProjectId(`/tasks/${id}/steer`, projectId), {
|
||||
method: "POST",
|
||||
|
||||
277
packages/dashboard/app/api/task-content.ts
Normal file
277
packages/dashboard/app/api/task-content.ts
Normal file
@@ -0,0 +1,277 @@
|
||||
/**
|
||||
* FNXC:CodeOrganization 2026-07-17-12:00:
|
||||
* Task attachments, logs, comments, documents, and artifacts client API peeled from legacy.ts.
|
||||
*/
|
||||
import type {
|
||||
Task,
|
||||
TaskAttachment,
|
||||
TaskComment,
|
||||
TaskDocument,
|
||||
TaskDocumentRevision,
|
||||
TaskDocumentWithTask,
|
||||
Artifact,
|
||||
ArtifactType,
|
||||
ArtifactWithTask,
|
||||
AgentLogEntry,
|
||||
} from "@fusion/core";
|
||||
import { appendTokenQuery, withTokenHeader } from "../auth";
|
||||
import { api, buildApiUrl } from "./client.js";
|
||||
import { withProjectId } from "./health.js";
|
||||
|
||||
export async function uploadAttachment(id: string, file: File, projectId?: string): Promise<TaskAttachment> {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
const res = await fetch(buildApiUrl(withProjectId(`/tasks/${id}/attachments`, projectId)), {
|
||||
method: "POST",
|
||||
headers: withTokenHeader(),
|
||||
body: formData,
|
||||
});
|
||||
// Non-JSON error bodies (e.g. gateway 413/502 HTML) must not throw SyntaxError over the HTTP failure.
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error((data as { error?: string }).error || `Upload failed: HTTP ${res.status}`);
|
||||
return data as TaskAttachment;
|
||||
}
|
||||
|
||||
export async function deleteAttachment(id: string, filename: string, projectId?: string): Promise<Task> {
|
||||
return api<Task>(withProjectId(`/tasks/${id}/attachments/${encodeURIComponent(filename)}`, projectId), { method: "DELETE" });
|
||||
}
|
||||
|
||||
export function fetchAgentLogs(
|
||||
taskId: string,
|
||||
projectId?: string,
|
||||
options?: { limit?: number; offset?: number },
|
||||
): Promise<AgentLogEntry[]> {
|
||||
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.toString() ? `?${params.toString()}` : "";
|
||||
return api<AgentLogEntry[]>(withProjectId(`/tasks/${taskId}/logs${suffix}`, projectId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch agent logs with pagination metadata.
|
||||
* Returns entries along with total count and hasMore flag from response headers.
|
||||
*/
|
||||
export async function fetchAgentLogsWithMeta(
|
||||
taskId: string,
|
||||
projectId?: string,
|
||||
options?: { limit?: number; offset?: number },
|
||||
): Promise<{ entries: AgentLogEntry[]; total: number; hasMore: boolean }> {
|
||||
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.toString() ? `?${params.toString()}` : "";
|
||||
const url = withProjectId(`/tasks/${taskId}/logs${suffix}`, projectId);
|
||||
|
||||
const response = await fetch(buildApiUrl(url), {
|
||||
headers: withTokenHeader(),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({ error: "Failed to fetch agent logs" }));
|
||||
throw new Error((data as { error?: string }).error || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const entries = await response.json() as AgentLogEntry[];
|
||||
|
||||
// Read pagination headers
|
||||
const total = response.headers.has("X-Total-Count")
|
||||
? parseInt(response.headers.get("X-Total-Count")!, 10)
|
||||
: entries.length;
|
||||
const hasMore = response.headers.has("X-Has-More")
|
||||
? response.headers.get("X-Has-More") === "true"
|
||||
: false;
|
||||
|
||||
return { entries, total, hasMore };
|
||||
}
|
||||
|
||||
export function fetchSessionFiles(taskId: string, projectId?: string): Promise<string[]> {
|
||||
return api<string[]>(withProjectId(`/tasks/${taskId}/session-files`, projectId));
|
||||
}
|
||||
|
||||
export function fetchTaskComments(id: string, projectId?: string): Promise<TaskComment[]> {
|
||||
return api<TaskComment[]>(withProjectId(`/tasks/${id}/comments`, projectId));
|
||||
}
|
||||
|
||||
export function addTaskComment(id: string, text: string, author?: string, projectId?: string): Promise<Task> {
|
||||
return api<Task>(withProjectId(`/tasks/${id}/comments`, projectId), {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ text, author }),
|
||||
});
|
||||
}
|
||||
|
||||
export function updateTaskComment(id: string, commentId: string, text: string, projectId?: string): Promise<Task> {
|
||||
return api<Task>(withProjectId(`/tasks/${id}/comments/${commentId}`, projectId), {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ text }),
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteTaskComment(id: string, commentId: string, projectId?: string): Promise<Task> {
|
||||
return api<Task>(withProjectId(`/tasks/${id}/comments/${commentId}`, projectId), {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
|
||||
// ── Task Document API Functions ──────────────────────────────────────────────
|
||||
|
||||
export function fetchTaskDocuments(taskId: string, projectId?: string): Promise<TaskDocument[]> {
|
||||
return api<TaskDocument[]>(withProjectId(`/tasks/${taskId}/documents`, projectId));
|
||||
}
|
||||
|
||||
export function fetchTaskDocument(taskId: string, key: string, projectId?: string): Promise<TaskDocument> {
|
||||
return api<TaskDocument>(withProjectId(`/tasks/${taskId}/documents/${encodeURIComponent(key)}`, projectId));
|
||||
}
|
||||
|
||||
export function fetchTaskDocumentRevisions(taskId: string, key: string, projectId?: string): Promise<TaskDocumentRevision[]> {
|
||||
return api<TaskDocumentRevision[]>(withProjectId(`/tasks/${taskId}/documents/${encodeURIComponent(key)}/revisions`, projectId));
|
||||
}
|
||||
|
||||
export interface FetchAllDocumentsOptions {
|
||||
q?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
export interface MarkdownFileEntry {
|
||||
path: string;
|
||||
name: string;
|
||||
size: number;
|
||||
mtime: string;
|
||||
}
|
||||
|
||||
export interface MarkdownFileListResponse {
|
||||
files: MarkdownFileEntry[];
|
||||
}
|
||||
|
||||
export type { Artifact, ArtifactType, ArtifactWithTask };
|
||||
|
||||
export interface FetchArtifactsOptions {
|
||||
type?: ArtifactType;
|
||||
authorId?: string;
|
||||
taskId?: string;
|
||||
q?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
export async function fetchArtifacts(
|
||||
options?: FetchArtifactsOptions,
|
||||
projectId?: string,
|
||||
): Promise<ArtifactWithTask[]> {
|
||||
const params = new URLSearchParams();
|
||||
if (options?.type) params.set("type", options.type);
|
||||
if (options?.authorId) params.set("authorId", options.authorId);
|
||||
if (options?.taskId) params.set("taskId", options.taskId);
|
||||
if (options?.q) params.set("q", options.q);
|
||||
if (options?.limit !== undefined) params.set("limit", String(options.limit));
|
||||
if (options?.offset !== undefined) params.set("offset", String(options.offset));
|
||||
const queryString = params.toString();
|
||||
const path = `/artifacts${queryString ? `?${queryString}` : ""}`;
|
||||
return api<ArtifactWithTask[]>(withProjectId(path, projectId));
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:ArtifactRegistry 2026-07-15-12:00:
|
||||
Keep artifactMediaUrl token-free so fetch callers and script-capable HTML previews can authenticate via Authorization without putting the daemon token into a URL that executable artifact content could read.
|
||||
|
||||
FNXC:ArtifactMediaAuth 2026-07-15-14:24:
|
||||
Main previously always-tokenized this helper for browser-native media loads. FN-7976 supersedes that by splitting tokenized element/link loads into artifactMediaUrlWithToken while this base URL stays clean for header-auth fetch and HTML blob previews.
|
||||
*/
|
||||
export function artifactMediaUrl(id: string, projectId?: string): string {
|
||||
return buildApiUrl(withProjectId(`/artifacts/${encodeURIComponent(id)}/media`, projectId));
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:ArtifactRegistry 2026-07-15-12:00:
|
||||
Artifact media element loads and link navigations cannot attach an Authorization header, so authenticated daemon media routes require the dashboard-owned fn_token query fallback. Keep artifactMediaUrl token-free for fetch callers and script-capable HTML previews; consumers that hand the URL to an img, video, audio, iframe, or anchor must use this helper unless executable content could read the URL.
|
||||
*/
|
||||
export function artifactMediaUrlWithToken(id: string, projectId?: string): string {
|
||||
return appendTokenQuery(artifactMediaUrl(id, projectId));
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:ArtifactRegistry 2026-07-10-15:20:
|
||||
The Artifacts view document viewer needs the full artifact INCLUDING inline content (list responses strip content), and edit mode persists title/description/content through PATCH.
|
||||
*/
|
||||
export async function fetchArtifact(id: string, projectId?: string): Promise<Artifact> {
|
||||
return api<Artifact>(withProjectId(`/artifacts/${encodeURIComponent(id)}`, projectId));
|
||||
}
|
||||
|
||||
export interface UpdateArtifactInput {
|
||||
title?: string;
|
||||
description?: string;
|
||||
content?: string;
|
||||
}
|
||||
|
||||
export async function updateArtifact(id: string, updates: UpdateArtifactInput, projectId?: string): Promise<Artifact> {
|
||||
return api<Artifact>(withProjectId(`/artifacts/${encodeURIComponent(id)}`, projectId), {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(updates),
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchAllDocuments(
|
||||
options?: FetchAllDocumentsOptions,
|
||||
projectId?: string,
|
||||
): Promise<TaskDocumentWithTask[]> {
|
||||
const params = new URLSearchParams();
|
||||
if (options?.q) params.set("q", options.q);
|
||||
if (options?.limit !== undefined) params.set("limit", String(options.limit));
|
||||
if (options?.offset !== undefined) params.set("offset", String(options.offset));
|
||||
const queryString = params.toString();
|
||||
const path = `/documents${queryString ? `?${queryString}` : ""}`;
|
||||
return api<TaskDocumentWithTask[]>(withProjectId(path, projectId));
|
||||
}
|
||||
|
||||
export interface FetchProjectMarkdownFilesOptions {
|
||||
showHidden?: boolean;
|
||||
}
|
||||
|
||||
export function fetchProjectMarkdownFiles(
|
||||
projectId?: string,
|
||||
options?: FetchProjectMarkdownFilesOptions,
|
||||
): Promise<MarkdownFileListResponse> {
|
||||
const params = new URLSearchParams();
|
||||
if (options?.showHidden) {
|
||||
params.set("showHidden", "1");
|
||||
}
|
||||
|
||||
const query = params.toString();
|
||||
const path = `/files/markdown-list${query ? `?${query}` : ""}`;
|
||||
|
||||
return api<MarkdownFileListResponse>(withProjectId(path, projectId));
|
||||
}
|
||||
|
||||
export function putTaskDocument(
|
||||
taskId: string,
|
||||
key: string,
|
||||
content: string,
|
||||
opts?: { author?: string; metadata?: Record<string, unknown> },
|
||||
projectId?: string,
|
||||
): Promise<TaskDocument> {
|
||||
return api<TaskDocument>(withProjectId(`/tasks/${taskId}/documents/${encodeURIComponent(key)}`, projectId), {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
content,
|
||||
author: opts?.author,
|
||||
metadata: opts?.metadata,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteTaskDocument(taskId: string, key: string, projectId?: string): Promise<void> {
|
||||
return api<void>(withProjectId(`/tasks/${taskId}/documents/${encodeURIComponent(key)}`, projectId), {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import type { Settings } from "@fusion/core";
|
||||
import { GENERATED_PATTERNS, LOCKFILE_PATTERNS, matchGlob } from "./merger.js";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
@@ -101,3 +102,32 @@ export async function checkDiffVolume({
|
||||
throw new DiffVolumeRegressionError(findings);
|
||||
}
|
||||
}
|
||||
|
||||
export interface DiffVolumeGateSettings {
|
||||
minLines: number;
|
||||
threshold: number;
|
||||
allowlistGlobs: string[];
|
||||
}
|
||||
|
||||
/*
|
||||
* FNXC:CodeOrganization 2026-07-17-12:00:
|
||||
* Diff-volume settings normalization and finding formatting peeled from merger.ts.
|
||||
*/
|
||||
export function resolveDiffVolumeGateSettings(settings?: Settings): DiffVolumeGateSettings {
|
||||
const minLinesRaw = settings?.mergeDiffVolumeMinLines ?? 20;
|
||||
const thresholdRaw = settings?.mergeDiffVolumeThreshold ?? 0.2;
|
||||
return {
|
||||
minLines: Math.max(1, Math.trunc(Number.isFinite(minLinesRaw) ? minLinesRaw : 20)),
|
||||
threshold: Math.min(1, Math.max(0, Number.isFinite(thresholdRaw) ? thresholdRaw : 0.2)),
|
||||
allowlistGlobs: Array.isArray(settings?.mergeDiffVolumeAllowlist)
|
||||
? settings.mergeDiffVolumeAllowlist.filter((glob): glob is string => typeof glob === "string" && glob.trim().length > 0)
|
||||
: [],
|
||||
};
|
||||
}
|
||||
|
||||
export function formatDiffVolumeFindings(findings: ReadonlyArray<{ file: string; branchNet: number; staged: number; ratio: number }>): string {
|
||||
return findings
|
||||
.map((finding) => `${finding.file} (branchNet=${finding.branchNet}, staged=${finding.staged}, ratio=${finding.ratio.toFixed(3)})`)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
|
||||
@@ -261,7 +261,16 @@ import {
|
||||
type SquashAuditFindings,
|
||||
} from "./merger-squash-audit.js";
|
||||
import { detectMergeOverlap, restoreBranchWinsFiles } from "./merger-overlap-guard.js";
|
||||
import { checkDiffVolume, DiffVolumeRegressionError } from "./merger-diff-volume-gate.js";
|
||||
import {
|
||||
checkDiffVolume,
|
||||
DiffVolumeRegressionError,
|
||||
resolveDiffVolumeGateSettings,
|
||||
formatDiffVolumeFindings,
|
||||
} from "./merger-diff-volume-gate.js";
|
||||
export {
|
||||
resolveDiffVolumeGateSettings,
|
||||
formatDiffVolumeFindings,
|
||||
} from "./merger-diff-volume-gate.js";
|
||||
import { detectAlreadyLandedOnMain, type AlreadyMergedDetectionStrategy } from "./already-merged-detector.js";
|
||||
import { decideAutoPrerebase, probeDivergence, runAutoPrerebase } from "./merger-auto-prerebase.js";
|
||||
import {
|
||||
@@ -498,30 +507,6 @@ const MERGE_USER_COMMENTS_MAX_CHARS = 4000;
|
||||
export const summarizeVerificationOutputLocal = summarizeVerificationOutput;
|
||||
|
||||
|
||||
interface DiffVolumeGateSettings {
|
||||
minLines: number;
|
||||
threshold: number;
|
||||
allowlistGlobs: string[];
|
||||
}
|
||||
|
||||
function resolveDiffVolumeGateSettings(settings?: Settings): DiffVolumeGateSettings {
|
||||
const minLinesRaw = settings?.mergeDiffVolumeMinLines ?? 20;
|
||||
const thresholdRaw = settings?.mergeDiffVolumeThreshold ?? 0.2;
|
||||
return {
|
||||
minLines: Math.max(1, Math.trunc(Number.isFinite(minLinesRaw) ? minLinesRaw : 20)),
|
||||
threshold: Math.min(1, Math.max(0, Number.isFinite(thresholdRaw) ? thresholdRaw : 0.2)),
|
||||
allowlistGlobs: Array.isArray(settings?.mergeDiffVolumeAllowlist)
|
||||
? settings.mergeDiffVolumeAllowlist.filter((glob): glob is string => typeof glob === "string" && glob.trim().length > 0)
|
||||
: [],
|
||||
};
|
||||
}
|
||||
|
||||
function formatDiffVolumeFindings(findings: ReadonlyArray<{ file: string; branchNet: number; staged: number; ratio: number }>): string {
|
||||
return findings
|
||||
.map((finding) => `${finding.file} (branchNet=${finding.branchNet}, staged=${finding.staged}, ratio=${finding.ratio.toFixed(3)})`)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
async function resetToIntegrationTarget(rootDir: string, integrationTargetSha: string): Promise<void> {
|
||||
await execAsync(`git reset --hard ${quoteArg(integrationTargetSha)}`, {
|
||||
cwd: rootDir,
|
||||
|
||||
@@ -1,103 +1,107 @@
|
||||
{
|
||||
"packages/cli/src/__tests__/extension.test.ts": 4558,
|
||||
"packages/cli/src/bin.ts": 2307,
|
||||
"packages/cli/src/commands/__tests__/dashboard.test.ts": 3709,
|
||||
"packages/cli/src/commands/__tests__/serve.test.ts": 2250,
|
||||
"packages/cli/src/commands/__tests__/task.test.ts": 3585,
|
||||
"packages/cli/src/__tests__/extension.test.ts": 4451,
|
||||
"packages/cli/src/bin.ts": 2331,
|
||||
"packages/cli/src/commands/__tests__/serve.test.ts": 2313,
|
||||
"packages/cli/src/commands/__tests__/task.test.ts": 3638,
|
||||
"packages/cli/src/commands/dashboard-tui/app.tsx": 4674,
|
||||
"packages/cli/src/commands/dashboard.ts": 3645,
|
||||
"packages/cli/src/commands/task.ts": 2373,
|
||||
"packages/cli/src/extension.ts": 5575,
|
||||
"packages/core/src/agent-store.ts": 3475,
|
||||
"packages/cli/src/commands/dashboard.ts": 3699,
|
||||
"packages/cli/src/commands/task.ts": 2366,
|
||||
"packages/cli/src/extension.ts": 5687,
|
||||
"packages/core/src/agent-store.ts": 3484,
|
||||
"packages/core/src/async-mission-store-queries.ts": 2067,
|
||||
"packages/core/src/central-core.ts": 4474,
|
||||
"packages/core/src/index.gate.ts": 2157,
|
||||
"packages/core/src/index.ts": 2388,
|
||||
"packages/core/src/mission-store.ts": 4364,
|
||||
"packages/core/src/postgres/sqlite-migrator.ts": 2068,
|
||||
"packages/core/src/store.ts": 2631,
|
||||
"packages/core/src/types.ts": 6928,
|
||||
"packages/dashboard/app/api/legacy.ts": 10561,
|
||||
"packages/dashboard/app/components/AgentDetailView.tsx": 5552,
|
||||
"packages/core/src/central-core.ts": 4486,
|
||||
"packages/core/src/index.gate.ts": 2217,
|
||||
"packages/core/src/index.ts": 2501,
|
||||
"packages/core/src/mission-store.ts": 4369,
|
||||
"packages/core/src/postgres/schema/project.ts": 2150,
|
||||
"packages/core/src/postgres/sqlite-migrator.ts": 2112,
|
||||
"packages/core/src/store.ts": 2746,
|
||||
"packages/core/src/types.ts": 7101,
|
||||
"packages/dashboard/app/App.tsx": 2059,
|
||||
"packages/dashboard/app/api/legacy.ts": 10273,
|
||||
"packages/dashboard/app/components/AgentDetailView.tsx": 5567,
|
||||
"packages/dashboard/app/components/AgentsView.tsx": 2254,
|
||||
"packages/dashboard/app/components/ChatView.tsx": 3850,
|
||||
"packages/dashboard/app/components/ChatView.tsx": 4020,
|
||||
"packages/dashboard/app/components/GitHubImportModal.tsx": 2481,
|
||||
"packages/dashboard/app/components/GitManagerModal.tsx": 3560,
|
||||
"packages/dashboard/app/components/ListView.tsx": 3087,
|
||||
"packages/dashboard/app/components/MissionManager.tsx": 5010,
|
||||
"packages/dashboard/app/components/ModelOnboardingModal.tsx": 3590,
|
||||
"packages/dashboard/app/components/PlanningModeModal.tsx": 3783,
|
||||
"packages/dashboard/app/components/QuickEntryBox.tsx": 2518,
|
||||
"packages/dashboard/app/components/SettingsModal.tsx": 4572,
|
||||
"packages/dashboard/app/components/TaskCard.tsx": 3856,
|
||||
"packages/dashboard/app/components/TaskDetailModal.tsx": 6114,
|
||||
"packages/dashboard/app/components/ListView.tsx": 3118,
|
||||
"packages/dashboard/app/components/MissionManager.tsx": 5140,
|
||||
"packages/dashboard/app/components/ModelOnboardingModal.tsx": 3594,
|
||||
"packages/dashboard/app/components/PlanningModeModal.tsx": 4012,
|
||||
"packages/dashboard/app/components/QuickEntryBox.tsx": 2562,
|
||||
"packages/dashboard/app/components/SettingsModal.tsx": 5002,
|
||||
"packages/dashboard/app/components/TaskCard.tsx": 3818,
|
||||
"packages/dashboard/app/components/TaskDetailModal.tsx": 6506,
|
||||
"packages/dashboard/app/components/TerminalModal.tsx": 3322,
|
||||
"packages/dashboard/app/components/WorkflowNodeEditor.tsx": 5683,
|
||||
"packages/dashboard/app/components/__tests__/AgentsView.test.tsx": 2845,
|
||||
"packages/dashboard/app/components/__tests__/App.test.tsx": 4567,
|
||||
"packages/dashboard/app/components/WorkflowNodeEditor.tsx": 5685,
|
||||
"packages/dashboard/app/components/__tests__/AgentsView.test.tsx": 2854,
|
||||
"packages/dashboard/app/components/__tests__/App.test.tsx": 4640,
|
||||
"packages/dashboard/app/components/__tests__/Board.test.tsx": 2107,
|
||||
"packages/dashboard/app/components/__tests__/ChatView.mobile.test.tsx": 2395,
|
||||
"packages/dashboard/app/components/__tests__/GitHubImportModal.test.tsx": 2622,
|
||||
"packages/dashboard/app/components/__tests__/GitManagerModal.test.tsx": 3788,
|
||||
"packages/dashboard/app/components/__tests__/ListView.test.tsx": 5198,
|
||||
"packages/dashboard/app/components/__tests__/MailboxView.test.tsx": 2377,
|
||||
"packages/dashboard/app/components/__tests__/ModelOnboardingModal.test.tsx": 5140,
|
||||
"packages/dashboard/app/components/__tests__/NewTaskModal.test.tsx": 2079,
|
||||
"packages/dashboard/app/components/__tests__/PlanningModeModal.planning-flow.test.tsx": 4040,
|
||||
"packages/dashboard/app/components/__tests__/QuickEntryBox.test.tsx": 5337,
|
||||
"packages/dashboard/app/components/__tests__/TaskCard.test.tsx": 7115,
|
||||
"packages/dashboard/app/components/__tests__/TaskChatTab.test.tsx": 2891,
|
||||
"packages/dashboard/app/components/__tests__/TaskDetailModal.inline-editing-and-integrations.test.tsx": 3340,
|
||||
"packages/dashboard/app/components/__tests__/TaskDetailModal.rendering.test.tsx": 2812,
|
||||
"packages/dashboard/app/components/__tests__/ChatView.mobile.test.tsx": 2436,
|
||||
"packages/dashboard/app/components/__tests__/GitHubImportModal.test.tsx": 3171,
|
||||
"packages/dashboard/app/components/__tests__/GitManagerModal.test.tsx": 3790,
|
||||
"packages/dashboard/app/components/__tests__/ListView.test.tsx": 5380,
|
||||
"packages/dashboard/app/components/__tests__/MailboxView.test.tsx": 2600,
|
||||
"packages/dashboard/app/components/__tests__/ModelOnboardingModal.test.tsx": 5146,
|
||||
"packages/dashboard/app/components/__tests__/NewTaskModal.test.tsx": 2081,
|
||||
"packages/dashboard/app/components/__tests__/PlanningModeModal.planning-flow.test.tsx": 4537,
|
||||
"packages/dashboard/app/components/__tests__/QuickEntryBox.test.tsx": 5433,
|
||||
"packages/dashboard/app/components/__tests__/SettingsModal.general.test.tsx": 2021,
|
||||
"packages/dashboard/app/components/__tests__/TaskCard.test.tsx": 7548,
|
||||
"packages/dashboard/app/components/__tests__/TaskChatTab.test.tsx": 3141,
|
||||
"packages/dashboard/app/components/__tests__/TaskDetailModal.inline-editing-and-integrations.test.tsx": 3438,
|
||||
"packages/dashboard/app/components/__tests__/TaskDetailModal.rendering.test.tsx": 2801,
|
||||
"packages/dashboard/app/components/__tests__/TaskDetailModal.responsive-and-dependencies.test.tsx": 2042,
|
||||
"packages/dashboard/app/components/__tests__/TerminalModal.test.tsx": 9176,
|
||||
"packages/dashboard/app/components/__tests__/UsageIndicator.test.tsx": 2931,
|
||||
"packages/dashboard/app/components/__tests__/WorkflowNodeEditor.test.tsx": 4353,
|
||||
"packages/dashboard/app/components/__tests__/WorkflowResultsTab.test.tsx": 2062,
|
||||
"packages/dashboard/app/components/__tests__/workflow-flow-mapping.test.ts": 2458,
|
||||
"packages/dashboard/app/hooks/__tests__/useChat.test.ts": 5355,
|
||||
"packages/dashboard/app/hooks/__tests__/useTasks.test.ts": 3712,
|
||||
"packages/dashboard/src/__tests__/chat-manager.test.ts": 3695,
|
||||
"packages/dashboard/app/hooks/__tests__/useChat.test.ts": 5376,
|
||||
"packages/dashboard/app/hooks/__tests__/useTasks.test.ts": 3766,
|
||||
"packages/dashboard/src/__tests__/chat-manager.test.ts": 3796,
|
||||
"packages/dashboard/src/__tests__/file-service.test.ts": 2310,
|
||||
"packages/dashboard/src/__tests__/github.test.ts": 2319,
|
||||
"packages/dashboard/src/__tests__/github.test.ts": 2453,
|
||||
"packages/dashboard/src/__tests__/routes-auth.test.ts": 5510,
|
||||
"packages/dashboard/src/__tests__/routes-automation.test.ts": 3068,
|
||||
"packages/dashboard/src/__tests__/routes-github.test.ts": 3169,
|
||||
"packages/dashboard/src/__tests__/routes-planning.test.ts": 5119,
|
||||
"packages/dashboard/src/__tests__/routes-automation.test.ts": 3089,
|
||||
"packages/dashboard/src/__tests__/routes-github.test.ts": 3792,
|
||||
"packages/dashboard/src/__tests__/routes-planning.test.ts": 5265,
|
||||
"packages/dashboard/src/__tests__/routes-tasks.test.ts": 2949,
|
||||
"packages/dashboard/src/__tests__/usage.test.ts": 5261,
|
||||
"packages/dashboard/src/chat.ts": 2861,
|
||||
"packages/dashboard/src/github.ts": 4587,
|
||||
"packages/dashboard/src/mission-routes.ts": 3895,
|
||||
"packages/dashboard/src/planning.ts": 3364,
|
||||
"packages/dashboard/src/routes.ts": 5600,
|
||||
"packages/dashboard/src/routes/register-git-github.ts": 5984,
|
||||
"packages/dashboard/src/routes/register-settings-memory-routes.ts": 2382,
|
||||
"packages/dashboard/src/routes/register-task-workflow-routes.ts": 5366,
|
||||
"packages/dashboard/src/server.ts": 2860,
|
||||
"packages/dashboard/src/chat.ts": 3069,
|
||||
"packages/dashboard/src/github.ts": 4877,
|
||||
"packages/dashboard/src/mission-routes.ts": 3909,
|
||||
"packages/dashboard/src/planning.ts": 3625,
|
||||
"packages/dashboard/src/routes.ts": 5629,
|
||||
"packages/dashboard/src/routes/register-git-github.ts": 6342,
|
||||
"packages/dashboard/src/routes/register-settings-memory-routes.ts": 2403,
|
||||
"packages/dashboard/src/routes/register-task-workflow-routes.ts": 5490,
|
||||
"packages/dashboard/src/server.ts": 2862,
|
||||
"packages/dashboard/src/usage.ts": 2398,
|
||||
"packages/engine/src/__tests__/cron-runner.test.ts": 2326,
|
||||
"packages/engine/src/__tests__/executor-prompt.test.ts": 2685,
|
||||
"packages/engine/src/__tests__/cron-runner.test.ts": 2334,
|
||||
"packages/engine/src/__tests__/executor-prompt.test.ts": 2716,
|
||||
"packages/engine/src/__tests__/executor-worktree.test.ts": 2723,
|
||||
"packages/engine/src/__tests__/heartbeat-executor.test.ts": 4295,
|
||||
"packages/engine/src/__tests__/heartbeat-scheduler.test.ts": 3018,
|
||||
"packages/engine/src/__tests__/merger-merge-lifecycle.test.ts": 3693,
|
||||
"packages/engine/src/__tests__/merger-verification.test.ts": 3163,
|
||||
"packages/engine/src/__tests__/mission-execution-loop.test.ts": 2904,
|
||||
"packages/engine/src/__tests__/pi-create-fn-agent.test.ts": 3067,
|
||||
"packages/engine/src/__tests__/project-engine.test.ts": 3482,
|
||||
"packages/engine/src/__tests__/restart.integration.test.ts": 2013,
|
||||
"packages/engine/src/__tests__/self-healing.test.ts": 11148,
|
||||
"packages/engine/src/__tests__/step-session-executor.test.ts": 3217,
|
||||
"packages/engine/src/__tests__/triage.test.ts": 6605,
|
||||
"packages/engine/src/agent-heartbeat.ts": 5109,
|
||||
"packages/engine/src/agent-tools.ts": 4793,
|
||||
"packages/engine/src/executor.ts": 19107,
|
||||
"packages/engine/src/merger-ai.ts": 2148,
|
||||
"packages/engine/src/merger.ts": 11218,
|
||||
"packages/engine/src/pi.ts": 2799,
|
||||
"packages/engine/src/project-engine.ts": 5051,
|
||||
"packages/engine/src/scheduler.ts": 3370,
|
||||
"packages/engine/src/self-healing.ts": 12342,
|
||||
"packages/engine/src/triage.ts": 3133,
|
||||
"packages/engine/src/__tests__/heartbeat-executor.test.ts": 4455,
|
||||
"packages/engine/src/__tests__/heartbeat-scheduler.test.ts": 3327,
|
||||
"packages/engine/src/__tests__/merger-merge-lifecycle.test.ts": 3697,
|
||||
"packages/engine/src/__tests__/merger-verification.test.ts": 3177,
|
||||
"packages/engine/src/__tests__/mission-execution-loop.test.ts": 2984,
|
||||
"packages/engine/src/__tests__/pi-create-fn-agent.test.ts": 3148,
|
||||
"packages/engine/src/__tests__/project-engine.test.ts": 3691,
|
||||
"packages/engine/src/__tests__/restart.integration.test.ts": 2022,
|
||||
"packages/engine/src/__tests__/self-healing.test.ts": 11692,
|
||||
"packages/engine/src/__tests__/step-session-executor.test.ts": 3226,
|
||||
"packages/engine/src/__tests__/triage.test.ts": 7392,
|
||||
"packages/engine/src/agent-heartbeat.ts": 5370,
|
||||
"packages/engine/src/agent-tools.ts": 5230,
|
||||
"packages/engine/src/executor.ts": 20110,
|
||||
"packages/engine/src/merger-ai.ts": 2401,
|
||||
"packages/engine/src/merger.ts": 11208,
|
||||
"packages/engine/src/pi.ts": 2814,
|
||||
"packages/engine/src/project-engine.ts": 5410,
|
||||
"packages/engine/src/scheduler.ts": 3399,
|
||||
"packages/engine/src/self-healing.ts": 12399,
|
||||
"packages/engine/src/triage.ts": 3602,
|
||||
"packages/pi-claude-cli/src/__tests__/provider.test.ts": 2028,
|
||||
"plugins/fusion-plugin-roadmap/src/dashboard/RoadmapsView.tsx": 2583,
|
||||
"scripts/__tests__/test-changed.test.mjs": 2116
|
||||
|
||||
Reference in New Issue
Block a user