From 3bf7f92be0db77d9fc36cc203377dabf22f9a5d7 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sun, 19 Jul 2026 09:47:58 -0700 Subject: [PATCH] refactor: package code organization wave 10 (#2328) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 ## 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. --- packages/core/src/store.ts | 2 +- .../core/src/task-store/remaining-ops-1.ts | 2 +- .../core/src/task-store/remaining-ops-2.ts | 2 +- .../core/src/task-store/remaining-ops-5.ts | 2 +- ...ining-ops-8.ts => workflow-definitions.ts} | 4 +- packages/core/src/types.ts | 1283 ++---------- packages/core/src/types/agents.ts | 1126 ++++++++++ packages/dashboard/app/api/insights.ts | 162 ++ packages/dashboard/app/api/legacy.ts | 1833 ++--------------- packages/dashboard/app/api/messaging.ts | 326 +++ packages/dashboard/app/api/missions.ts | 665 ++++++ .../dashboard/app/api/plugins-and-skills.ts | 278 +++ packages/dashboard/app/api/system-panel.ts | 158 ++ packages/dashboard/app/api/todo.ts | 85 + packages/dashboard/app/types/html2canvas.d.ts | 28 + scripts/line-count-baseline.json | 30 +- 16 files changed, 3250 insertions(+), 2736 deletions(-) rename packages/core/src/task-store/{remaining-ops-8.ts => workflow-definitions.ts} (99%) create mode 100644 packages/core/src/types/agents.ts create mode 100644 packages/dashboard/app/api/insights.ts create mode 100644 packages/dashboard/app/api/messaging.ts create mode 100644 packages/dashboard/app/api/missions.ts create mode 100644 packages/dashboard/app/api/plugins-and-skills.ts create mode 100644 packages/dashboard/app/api/system-panel.ts create mode 100644 packages/dashboard/app/api/todo.ts create mode 100644 packages/dashboard/app/types/html2canvas.d.ts diff --git a/packages/core/src/store.ts b/packages/core/src/store.ts index 7e920dac9e..382c4ea145 100644 --- a/packages/core/src/store.ts +++ b/packages/core/src/store.ts @@ -101,7 +101,7 @@ import { recordGoalCitationsImpl, insertTaskWithFtsRecoveryImpl2, assertTaskIdAv import { applyLegacyWorkflowStepOverridesImpl, archiveDbImpl, assertNoDependencyCycleImpl, atomicCreateTaskJsonImpl, buildActiveTaskDependencyLookupImpl, buildArchivedAgentLogFieldsImpl, buildTaskIdIntegrityFallbackReportImpl, createBranchGroupImpl, dbImpl, detectAndCacheTaskIdIntegrityReportImpl, findLiveDependentsImpl, findLiveLineageChildrenImpl, getLegacyWorkflowStepSnapshotImpl, getMalformedTaskMetadataReasonImpl, getMergeQueuedTaskIdsAsyncImpl, insertRunAuditEventRowImpl, insertTaskImpl, invokeTaskCreatedHookImpl, isTaskArchivedImpl, isTaskIdPresentInArchivedTasksTableImpl, logTaskCreateConflictImpl, maybeResolveTombstonedTaskIdImpl, mergeTaskIdIntegrityReportsImpl, optionalGroupIdSetImpl, patchTaskRowInTransactionImpl, readConfigFastImpl, readConfigImpl, readPromptForArchiveImpl, readTaskFromDbImpl, reconcileDistributedTaskIdStateOnOpenImpl, recordActivityFromListenerImpl, recordDependencyCycleRejectedAuditImpl, refreshTaskIdIntegrityReportImpl, resolveLocalNodeIdForTaskAllocationImpl, runTaskFtsWriteWithRecoveryImpl, scanAndRecordCitationsImpl, taskIdExistsAnywhereImpl, throwSoftDeletedWriteBlockedImpl, toBuiltInWorkflowStepImpl, trackDeferredTaskCreatedWorkImpl, upsertTaskImpl, withConfigLockImpl, withTaskLockImpl, withWorktreeAllocationLockImpl } from "./task-store/remaining-ops-5.js"; import { claimNextToolFailureRetryImpl, createTaskVerificationRequestImpl, claimTaskVerificationRequestImpl, finishTaskVerificationRequestImpl, clearNearDuplicateReferencesToFailSoftImpl, clearWorkflowRunStepInstancesAsyncImpl, clearWorkflowRunStepInstancesImpl, computeMovedSettingsTargetWorkflowIdsImpl, ensureBranchGroupForSourceImpl, ensurePrEntityForSourceImpl, findRecentTasksByContentFingerprintImpl, getActiveMergingTaskImpl, getActivePrEntityBySourceImpl, getBranchGroupByBranchNameImpl, getBranchGroupBySourceImpl, getBranchGroupImpl, getBranchProgressByTaskImpl, getMutationsForRunImpl, getPrEntityByNumberImpl, getPrEntityImpl, getPrThreadStateImpl, getTasksByAssignedAgentImpl, getWorkflowPromptOverridesAsyncImpl, getWorkflowSettingValuesAsyncImpl, getWorkflowSettingValuesImpl, getWorkflowSettingsProjectIdImpl, getWorkflowWorkItemImpl, insertCompletionHandoffWorkflowWorkAuditImpl, listActivePrEntitiesImpl, listBranchGroupsImpl, listPrThreadStatesImpl, listTasksByBranchGroupImpl, listWorkflowSettingValuesForProjectImpl, loadWorkflowRunBranchesImpl, loadWorkflowRunStepInstancesAsyncImpl, loadWorkflowRunStepInstancesImpl, markToolFailureRetryExhaustedAuditImpl, mergeCustomFieldPatchImpl, normalizeMergeRequestStateImpl, normalizeWorkflowWorkItemKindImpl, normalizeWorkflowWorkItemStateImpl, parseWorkflowPromptOverrideJsonImpl, recordPrThreadOutcomeImpl, resetAllStepsToPendingImpl, resetPromptCheckboxesImpl, resolveWorkflowMoveActorImpl, resolveWorkflowSettingDeclarationsImpl, saveWorkflowRunStepInstanceAsyncImpl, saveWorkflowRunStepInstanceImpl, transitionMergeRequestStateImpl, transitionWorkflowWorkItemSyncImpl, updateTaskImpl, updateWorkflowPromptOverridesImpl, upsertMergeRequestRecordImpl, workflowStateForMergeRequestStateImpl } from "./task-store/remaining-ops-6.js"; import { addPrInfoImpl, addSteeringCommentImpl, archiveAllDoneImpl, cleanupStaleMergeQueueRowsImpl, clearCompletionHandoffAcceptedMarkerImpl, clearDoneTransientFieldsImpl, clearStaleExecutionStartBranchReferencesImpl, computeWorkflowColumnsGraduationReportImpl, deleteTaskCommentImpl, deleteTaskDocumentImpl, emitUsageEventImpl, enqueueMergeQueueImpl, getAgentLogCountImpl, getAgentLogsImpl, getArtifactImpl, getArtifactsImpl, getAttachmentImpl, getCompletionHandoffAcceptedMarkerImpl, getTaskDocumentImpl, getTaskDocumentRevisionsImpl, getTaskDocumentsImpl, insertArtifactRowImpl, linkGithubIssueImpl, listWorkflowWorkItemsForTaskSyncImpl, moveToDoneImpl, parseDependenciesFromPromptImpl, parseFileScopeFromPromptImpl, parseStepsFromPromptImpl, peekMergeQueueHeadImpl, peekMergeQueueImpl, readPreArchiveColumnFromTaskFileImpl, recordPluginActivationImpl, recordRunAuditEventBackendImpl, removePrInfoByNumberImpl, resolvePrimaryPrInfoImpl, resolveUnarchiveTargetColumnImpl, rewriteLineageChildrenForRemovalImpl, runGitCommandImpl, stopWatchingImpl, syncAgentTaskLinkOnReassignmentImpl, updateArtifactImpl, updateGithubTrackingImpl, updatePrInfoByNumberImpl, updateTaskCommentImpl, upsertPrInfoByNumberImpl, writeArtifactDataImpl } from "./task-store/remaining-ops-7.js"; -import { approveCliAutonomyImpl, approveWorkflowCliCommandImpl, cleanupOrphanedMaterializedStepsImpl, consumePluginGateVerdictsImpl, getAgentLogsByTimeRangeImpl, getDatabaseHealthImpl, getDistributedTaskIdAllocatorImpl, getExperimentSessionStoreImpl, getInReviewDurationEventsImpl, getMissionStoreImpl, getIdeationStoreImpl, getPluginStoreImpl, getSecretsStoreImpl, getSettingsSyncImpl, getTaskMergedTaskIdsImpl, getTaskWorkflowSelectionImpl, getImportTranslationImpl, recordImportTranslationImpl, pruneImportTranslationsImpl, type ImportTranslationCacheKey, type ImportTranslationCacheEntry, getVerificationCacheHitImpl, getWorkflowDefinitionImpl, healthCheckImpl, importLegacyAgentLogsOnceImpl, insertWorkflowDefinitionSyncImpl, isCliAutonomyApprovedImpl, isPluginInstalledImpl, isWorkflowCliCommandApprovedImpl, listWorkflowDefinitionsImpl, materializeExplicitWorkflowStepsImpl, materializeWorkflowStepsImpl, migrateActiveArchivedTasksToArchiveDbImpl, migrateLegacyArchiveEntriesToArchiveDbImpl, nextWorkflowDefinitionIdImpl, occupantsByColumnForWorkflowImpl, parseWorkflowLayoutImpl, pruneAgentLogFilesImpl, purgeTaskWorkflowSelectionRowsImpl, readAllWorkflowDefinitionsImpl, readRawProjectSettingsImpl, recordPluginGateVerdictImpl, recordVerificationCachePassImpl, removeMaterializedSelectionImpl, resolvePluginWorkflowStepImpl, resolveTaskWorkflowIrSyncImpl, revokeCliAutonomyImpl, selectTaskWorkflowAndReconcileImpl, writeTaskWorkflowSelectionImpl, getTaskWorkflowSelectionAsyncImpl, } from "./task-store/remaining-ops-8.js"; +import { approveCliAutonomyImpl, approveWorkflowCliCommandImpl, cleanupOrphanedMaterializedStepsImpl, consumePluginGateVerdictsImpl, getAgentLogsByTimeRangeImpl, getDatabaseHealthImpl, getDistributedTaskIdAllocatorImpl, getExperimentSessionStoreImpl, getInReviewDurationEventsImpl, getMissionStoreImpl, getIdeationStoreImpl, getPluginStoreImpl, getSecretsStoreImpl, getSettingsSyncImpl, getTaskMergedTaskIdsImpl, getTaskWorkflowSelectionImpl, getImportTranslationImpl, recordImportTranslationImpl, pruneImportTranslationsImpl, type ImportTranslationCacheKey, type ImportTranslationCacheEntry, getVerificationCacheHitImpl, getWorkflowDefinitionImpl, healthCheckImpl, importLegacyAgentLogsOnceImpl, insertWorkflowDefinitionSyncImpl, isCliAutonomyApprovedImpl, isPluginInstalledImpl, isWorkflowCliCommandApprovedImpl, listWorkflowDefinitionsImpl, materializeExplicitWorkflowStepsImpl, materializeWorkflowStepsImpl, migrateActiveArchivedTasksToArchiveDbImpl, migrateLegacyArchiveEntriesToArchiveDbImpl, nextWorkflowDefinitionIdImpl, occupantsByColumnForWorkflowImpl, parseWorkflowLayoutImpl, pruneAgentLogFilesImpl, purgeTaskWorkflowSelectionRowsImpl, readAllWorkflowDefinitionsImpl, readRawProjectSettingsImpl, recordPluginGateVerdictImpl, recordVerificationCachePassImpl, removeMaterializedSelectionImpl, resolvePluginWorkflowStepImpl, resolveTaskWorkflowIrSyncImpl, revokeCliAutonomyImpl, selectTaskWorkflowAndReconcileImpl, writeTaskWorkflowSelectionImpl, getTaskWorkflowSelectionAsyncImpl, } from "./task-store/workflow-definitions.js"; import { getTaskCommitAssociationsByLineageIdImpl, replaceLegacyTaskCommitAssociationsImpl } from "./task-store/task-commit-associations.js"; import { findRecentTasksBySourceParentTaskIdImpl } from "./task-store/remaining-ops-6.js"; import { addTaskCommentImpl, applyBuiltInPromptOverridesSyncImpl, areAllDependenciesDoneImpl, artifactStoredNameImpl, assertWorkflowIrTraitsValidImpl, clearActivityLogImpl, clearTaskWorkflowSelectionImpl, deleteTaskByIdImpl, getDefaultWorkflowIdImpl, getInsightStoreImpl, getMergeQueuedTaskIdsImpl, getMergeRequestRecordImpl, getMergeRequestRecordAsyncImpl, getResearchStoreImpl, getTaskIdFromDirImpl, getTodoStoreImpl, getWorkflowWorkItemByIdentityImpl, hasActiveTaskImpl, invalidateConfigCacheAfterMigrationImpl, isTaskIdConflictErrorImpl, listLegacyAutoMergeStampCandidatesImpl, readTaskRowFromDbImpl, recordBranchGroupMemberLandedImpl, refreshDatabaseHealthImpl, resolveEffectiveWorkflowIdSyncImpl, resolveTaskCustomFieldDefsSyncImpl, resolveWorkflowBypassGuardsImpl, serializeConfigForDiskImpl, setPluginWorkflowStepTemplatesImpl, shouldSkipWorkflowMovePoliciesImpl, suppressWatcherImpl, upsertTaskWithFtsRecoveryImpl } from "./task-store/task-store-helpers.js"; diff --git a/packages/core/src/task-store/remaining-ops-1.ts b/packages/core/src/task-store/remaining-ops-1.ts index c47d43cd78..6d98fc98d2 100644 --- a/packages/core/src/task-store/remaining-ops-1.ts +++ b/packages/core/src/task-store/remaining-ops-1.ts @@ -34,7 +34,7 @@ import {generateTaskLineageId} from "../task-lineage.js"; import {sanitizeFileScopeInPromptContent} from "../task-store/file-scope.js"; import {type TaskRow} from "../task-store/persistence.js"; import {__setTaskActivityLogLimitsForTesting} from "../task-store/comments.js"; -import {nextWorkflowDefinitionIdAsyncImpl} from "../task-store/remaining-ops-8.js"; +import {nextWorkflowDefinitionIdAsyncImpl} from "../task-store/workflow-definitions.js"; import {upsertTaskRowInTransaction, buildTaskInsertValues} from "../task-store/async-persistence.js"; import {readTaskRowInTransaction} from "../task-store/async-persistence.js"; import {recordActivityLogEntry as recordActivityLogEntryAsync} from "../task-store/async-audit.js"; diff --git a/packages/core/src/task-store/remaining-ops-2.ts b/packages/core/src/task-store/remaining-ops-2.ts index 2275ea5e66..ba7e2711ae 100644 --- a/packages/core/src/task-store/remaining-ops-2.ts +++ b/packages/core/src/task-store/remaining-ops-2.ts @@ -27,7 +27,7 @@ import {__setTaskActivityLogLimitsForTesting} from "../task-store/comments.js"; import {assertSafeGitBranchName} from "../task-store/shell-safety.js"; import {readTaskRow as readTaskRowAsync, readTaskRowInTransaction} from "../task-store/async-persistence.js"; import {upsertArchivedTaskEntry} from "./async-archive-lineage.js"; -import {purgeTaskWorkflowSelectionRowsAsyncImpl} from "./remaining-ops-8.js"; +import {purgeTaskWorkflowSelectionRowsAsyncImpl} from "./workflow-definitions.js"; import * as schema from "../postgres/schema/index.js"; import {and, asc, eq, isNotNull, isNull, sql} from "drizzle-orm"; import {recoverExpiredMergeQueueLeases as recoverExpiredMergeQueueLeasesAsync} from "../task-store/async-merge-coordination.js"; diff --git a/packages/core/src/task-store/remaining-ops-5.ts b/packages/core/src/task-store/remaining-ops-5.ts index 1eb64c25b2..e980e71514 100644 --- a/packages/core/src/task-store/remaining-ops-5.ts +++ b/packages/core/src/task-store/remaining-ops-5.ts @@ -25,7 +25,7 @@ import { findLiveLineageChildren as findLiveLineageChildrenAsync } from "./async import { recordRunAuditEvent as recordRunAuditEventAsync } from "./async-audit.js"; import { insertTaskRowInTransaction, isTaskIdConflictError, readTaskRow, readTaskRowInTransaction } from "./async-persistence.js"; import { TASK_PERSIST_SQL_COLUMNS, TASK_UPSERT_SQL_ASSIGNMENTS, type TaskRow } from "./persistence.js"; -import { purgeTaskWorkflowSelectionRowsAsyncImpl } from "./remaining-ops-8.js"; +import { purgeTaskWorkflowSelectionRowsAsyncImpl } from "./workflow-definitions.js"; import { ConfigRow } from "./row-types.js"; import { ARCHIVE_AGENT_LOG_SNAPSHOT_LIMIT } from "./serialization.js"; import { ActivityLogEntry, ArchiveAgentLogMode, ArchivedTaskEntry, BoardConfig, BranchGroup, BranchGroupCreateInput, Column, GoalCitationInput, GoalCitationSurface, RunAuditEventInput, Settings, Task, TaskCreateInput } from "../types.js"; diff --git a/packages/core/src/task-store/remaining-ops-8.ts b/packages/core/src/task-store/workflow-definitions.ts similarity index 99% rename from packages/core/src/task-store/remaining-ops-8.ts rename to packages/core/src/task-store/workflow-definitions.ts index 57da6aa227..a3ba163bce 100644 --- a/packages/core/src/task-store/remaining-ops-8.ts +++ b/packages/core/src/task-store/workflow-definitions.ts @@ -1,5 +1,7 @@ /** - * remaining-ops-8 operations. + * FNXC:CodeOrganization 2026-07-18-14:00: + * Domain rename from remaining-ops-8: workflow definition CRUD, selection + * materialization, plugin gate verdicts, CLI autonomy approvals, and related store ops. * * FNXC:StoreModularization 2026-06-25-00:00: * Extracted from the monolithic packages/core/src/store.ts as a pure diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 1b5767871c..c7b7b8fbbb 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -5613,18 +5613,9 @@ export { }; export type { AgentState }; -/** Single heartbeat event recorded for an agent */ -export interface AgentHeartbeatEvent { - /** ISO-8601 timestamp of when the heartbeat was recorded */ - timestamp: string; - /** Status of the heartbeat */ - status: "ok" | "missed" | "recovered"; - /** ID of the heartbeat run this event belongs to */ - runId: string; -} - -/** What triggered a heartbeat run */ -export type HeartbeatInvocationSource = "on_demand" | "timer" | "assignment" | "automation" | "routine"; +/** Heartbeat event/run types peeled into types/agents.ts */ +import type { AgentHeartbeatEvent, AgentHeartbeatRun, HeartbeatInvocationSource } from "./types/agents.js"; +export type { AgentHeartbeatEvent, AgentHeartbeatRun, HeartbeatInvocationSource }; /* FNXC:AutomationTools 2026-06-26-00:00: @@ -5644,55 +5635,9 @@ export interface BlockedStateSnapshot { contextHash: string; } -/** A continuous heartbeat session/run for an agent */ -export interface AgentHeartbeatRun { - /** Unique identifier for this run */ - id: string; - /** ID of the agent this run belongs to */ - agentId: string; - /** Task ID associated with this heartbeat run when bound to a task. */ - taskId?: string; - /** ISO-8601 timestamp when the run started */ - startedAt: string; - /** ISO-8601 timestamp when the run ended (null if active) */ - endedAt: string | null; - /** Status of the run */ - status: "active" | "completed" | "terminated" | "failed"; - /** What triggered this run */ - invocationSource?: HeartbeatInvocationSource; - /** Trigger detail (manual, ping, scheduler, system) */ - triggerDetail?: string; - /** PID of the agent process */ - processPid?: number; - /** Exit code of the agent process */ - exitCode?: number; - /** Session ID before execution (for continuity tracking) */ - sessionIdBefore?: string; - /** Session ID after execution */ - sessionIdAfter?: string; - /** Token usage for this run */ - usageJson?: { inputTokens: number; outputTokens: number; cachedTokens: number; cacheWriteTokens: number }; - /** Structured result from the run */ - resultJson?: Record; - /** Snapshot of context at run start (taskId, projectId, etc.). - * May include optional comment-wake fields: - * - `triggeringCommentIds?: string[]` - * - `triggeringCommentType?: "steering" | "task" | "pr"` */ - contextSnapshot?: Record; - /** Excerpt of stdout output */ - stdoutExcerpt?: string; - /** Excerpt of stderr output */ - stderrExcerpt?: string; - /** Full assembled system prompt sent to the LLM for this run (truncated to 100,000 chars). */ - systemPrompt?: string; - /** Full per-tick execution prompt sent to the LLM for this run (truncated to 100,000 chars). */ - executionPrompt?: string; - /** Whether the run used a custom heartbeat procedure, the built-in default, or the no-task default override. */ - heartbeatProcedureSource?: "default" | "custom" | "default-no-task-override"; -} - /** Capabilities/roles an agent can have */ -export type AgentCapability = "triage" | "executor" | "reviewer" | "merger" | "scheduler" | "engineer" | "custom"; +import type { AgentCapability } from "./types/agents.js"; +export type { AgentCapability }; /** A configurable agent role prompt template. */ export interface AgentPromptTemplate { @@ -5899,1059 +5844,177 @@ export interface PlannerInterventionEntry { /** Canonical run-audit mutation type used to persist planner-intervention entries. Single writer: `recordPlannerIntervention` (see `packages/core/src/planner-intervention.ts`); FN-7520 reuses this helper rather than emitting `overseer:intervention` events directly. */ export const OVERSEER_INTERVENTION_MUTATION = "overseer:intervention" as const; -// ── Agent Permission Types ────────────────────────────────────────────────── +// ── Agent Permission / Entity Types ───────────────────────────────────────── +// FNXC:CodeOrganization 2026-07-18-14:00: Peels live in types/agents.ts; keep stable re-exports here. -/** Canonical permission identifiers for agent access control. - * Each string represents a discrete capability that can be granted or denied. */ -export const AGENT_PERMISSIONS = [ - "tasks:assign", // Assign tasks to agents - "tasks:create", // Create new tasks - "tasks:execute", // Execute/run tasks - "tasks:review", // Review task output (code, specs) - "tasks:merge", // Merge completed task branches - "tasks:delete", // Delete tasks - "tasks:archive", // Archive/unarchive tasks - "agents:create", // Create new agents - "agents:update", // Update agent configuration - "agents:delete", // Delete agents - "agents:view", // View agent details and logs - "settings:read", // Read project settings - "settings:update", // Modify project settings - "workflows:manage", // Create/edit/delete workflow steps - "missions:manage", // Create/edit/delete missions and slices - "automations:manage", // Create/edit/delete scheduled automations - "messages:send", // Send messages to agents/users - "messages:read", // Read mailbox messages -] as const; - -/** A single canonical permission string. */ -export type AgentPermission = (typeof AGENT_PERMISSIONS)[number]; - -/** - * Canonical v1 action categories for permanent-agent runtime gating. - * - * `none` is a classifier-only result for positively-recognized read-only actions. - * It is never stored as a policy rule key. - */ -/** - * FNXC:ToolPermissions 2026-07-09-00:00: - * FN-7728 adds `review_gate_bypass` as a first-class sensitive action category distinct from `task_agent_mutation`. It governs merge-gate override tools (e.g. `fn_task_bypass_review`, delivered by FN-7720) so operators can independently allow/require-approval/block "who may bypass a failed review gate" without touching ordinary task-mutation policy. It defaults to a stricter disposition than the uniform preset default (see agent-permission-policy.ts) and is resolved identically by both evaluateAgentActionGate and the permanent-agent gate via the shared gating-classifications.ts source. - * - * FNXC:ToolPermissions 2026-07-09-08:30: - * FN-7737 adds `file_scope` as a first-class sensitive action category governing the File Scope additional-approval action (`fn_task_file_scope_add`, an executor-visible tool that extends a task's declared `## File Scope` beyond its initial spec at runtime). Unlike `review_gate_bypass`, `file_scope` intentionally keeps the UNIFORM grant-all disposition — the `unrestricted` preset resolves it to `allow` via `buildRules("allow")` with no override patch, since File Scope self-extension is an ordinary executor-scope action, not a merge-gate override. It is resolved identically by both evaluateAgentActionGate and the permanent-agent gate via the shared `FILE_SCOPE_FN_TOOLS` set in gating-classifications.ts. - */ -export const PERMANENT_AGENT_ACTION_CATEGORIES = [ - "git_write", - "file_write_delete", - "command_execution", - "network_api", - "task_agent_mutation", - "review_gate_bypass", - "file_scope", - "none", -] as const; - -/** A single v1 permanent-agent action category. */ -export type PermanentAgentActionCategory = (typeof PERMANENT_AGENT_ACTION_CATEGORIES)[number]; - -/** Sensitive runtime categories covered by policy rules (excludes classifier-only `none`). */ -export type PermanentAgentSensitiveActionCategory = Exclude; - -/** Runtime action categories governed by agent permission policy presets. */ -export const AGENT_PERMISSION_POLICY_ACTION_CATEGORIES: readonly PermanentAgentSensitiveActionCategory[] = [ - "git_write", - "file_write_delete", - "command_execution", - "network_api", - "task_agent_mutation", - "review_gate_bypass", - "file_scope", -] as const; - -export const AGENT_PERMISSION_POLICY_CATEGORY_TOOL_EXAMPLES: Record< - PermanentAgentSensitiveActionCategory, - readonly string[] -> = { - git_write: ["git commit", "git push", "git merge", "git branch -d", "git worktree add", "write", "edit"], - file_write_delete: ["write", "edit", "fn_task_attach"], - command_execution: ["bash (non-git)", "fn_run_verification", "fn_acquire_repo_worktree", "read", "find", "grep", "ls"], - network_api: ["fn_research_run (web/research)", "fn_research_cancel", "fn_web_fetch", "worktrunk_install"], - /* FNXC:ToolGovernance 2026-06-27-16:51: Dashboard policy examples must mirror action-gate mutation exports. Identity reflection is exempt heartbeat coordination, so it is intentionally not advertised as task_agent_mutation. - * FNXC:WorkflowAuthoringTools 2026-06-29-23:40: Published workflow authoring tools are now agent-visible, so policy examples include the mutating workflow create/update/delete/settings/select surface operators can approve or block. - * FNXC:ToolGovernance 2026-07-09-09:36: FN-7733 — the GitLab browse tools (fn_task_browse_gitlab_project_issues, fn_task_browse_gitlab_group_issues, fn_task_browse_gitlab_merge_requests) are read-only discovery tools that never create task rows and are already classified under READONLY_FN_TOOLS in gating-classifications.ts; they were never members of ACTION_GATE_TASK_AGENT_MANAGEMENT_TOOLS. Listing them here as task_agent_mutation examples broke the invariant that this list must be a subset of the action-gate mutation classification, so they are intentionally excluded. The mutating fn_task_import_gitlab_* variants (which do create task rows) remain listed below. */ - task_agent_mutation: [ - "fn_task_create", - "fn_delegate_task", - "fn_task_import_github", - "fn_task_import_github_issue", - "fn_task_import_gitlab_project_issues", - "fn_task_import_gitlab_group_issues", - "fn_task_import_gitlab_merge_requests", - "fn_spawn_agent", - "fn_update_agent_config", - "fn_task_update", - "fn_task_assign", - "fn_workflow_create", - "fn_workflow_update", - "fn_workflow_delete", - "fn_workflow_settings", - "fn_workflow_select", - "fn_task_promote", - "fn_task_refine", - ], - /* FNXC:ToolPermissions 2026-07-09-00:00: FN-7728 — review_gate_bypass governs merge-gate override tools as a distinct, more-restricted permission from ordinary task mutation. fn_task_bypass_review (FN-7720) is CLI/pi-extension operator-tool-only; it is never exposed to executor/reviewer/triage agent tool lists. */ - review_gate_bypass: ["fn_task_bypass_review"], - /* FNXC:ToolPermissions 2026-07-09-08:30: FN-7737 — file_scope governs the File Scope additional-approval action (fn_task_file_scope_add), which lets an executing agent extend its task's declared ## File Scope beyond the initial spec at runtime. Unlike review_gate_bypass, it keeps the uniform grant-all default (handled by "allow" under the unrestricted preset), so it is not patched by a *Override-style function. */ - file_scope: ["fn_task_file_scope_add"], +import { + AGENT_PERMISSIONS, + AGENT_PERMISSION_POLICY_ACTION_CATEGORIES, + AGENT_PERMISSION_POLICY_CATEGORY_TOOL_EXAMPLES, + AGENT_PERMISSION_POLICY_EXEMPT_TOOL_EXAMPLES, + AGENT_PERMISSION_POLICY_PRESET_IDS, + AGENT_PROVISIONING_APPROVAL_MODES, + APPROVAL_REQUEST_AUDIT_EVENT_TYPES, + APPROVAL_REQUEST_STATUSES, + DEFAULT_HEARTBEAT_PROCEDURE_PATH, + LEGACY_AGENT_PERMISSION_POLICY_ACTION_CATEGORY_ALIASES, + PERMANENT_AGENT_ACTION_CATEGORIES, + SANDBOX_PROVISIONING_APPROVAL_MODES, + SECRET_ACCESS_POLICIES, + agentToConfigSnapshot, + diffConfigSnapshots, + getCanonicalAgentAssetDirectoryName, + getCanonicalAgentInstructionsBundleDirName, + getDefaultHeartbeatProcedurePath, + getLegacyAgentAssetDirectoryName, + getLegacyAgentInstructionsBundleDirName, + getSafeAgentAssetIdSegment, + isValidApprovalRequestTransition, + normalizeApprovalRequestActionCategory, +} from "./types/agents.js"; +export { + AGENT_PERMISSIONS, + AGENT_PERMISSION_POLICY_ACTION_CATEGORIES, + AGENT_PERMISSION_POLICY_CATEGORY_TOOL_EXAMPLES, + AGENT_PERMISSION_POLICY_EXEMPT_TOOL_EXAMPLES, + AGENT_PERMISSION_POLICY_PRESET_IDS, + AGENT_PROVISIONING_APPROVAL_MODES, + APPROVAL_REQUEST_AUDIT_EVENT_TYPES, + APPROVAL_REQUEST_STATUSES, + DEFAULT_HEARTBEAT_PROCEDURE_PATH, + LEGACY_AGENT_PERMISSION_POLICY_ACTION_CATEGORY_ALIASES, + PERMANENT_AGENT_ACTION_CATEGORIES, + SANDBOX_PROVISIONING_APPROVAL_MODES, + SECRET_ACCESS_POLICIES, + agentToConfigSnapshot, + diffConfigSnapshots, + getCanonicalAgentAssetDirectoryName, + getCanonicalAgentInstructionsBundleDirName, + getDefaultHeartbeatProcedurePath, + getLegacyAgentAssetDirectoryName, + getLegacyAgentInstructionsBundleDirName, + getSafeAgentAssetIdSegment, + isValidApprovalRequestTransition, + normalizeApprovalRequestActionCategory, }; - -export const AGENT_PERMISSION_POLICY_EXEMPT_TOOL_EXAMPLES: readonly string[] = [ - "fn_send_message", - "fn_post_room_message", - "fn_read_messages", - "fn_task_log", - "fn_task_logs_read", - "fn_task_done", - "fn_heartbeat_done", - "fn_task_document_write", - "fn_task_document_read", - "fn_workflow_list", - "fn_workflow_get", - "fn_trait_list", - "fn_memory_search", - "fn_memory_get", - "fn_memory_append", - "fn_read_evaluations", - "fn_reflect_on_performance", -]; - -export const AGENT_PROVISIONING_APPROVAL_MODES = ["always", "trusted-only", "never"] as const; -export type AgentProvisioningApprovalMode = (typeof AGENT_PROVISIONING_APPROVAL_MODES)[number]; - -export const SECRET_ACCESS_POLICIES = ["auto", "prompt", "deny"] as const; -export type SecretAccessPolicy = (typeof SECRET_ACCESS_POLICIES)[number]; - -export const SANDBOX_PROVISIONING_APPROVAL_MODES = ["always", "trusted-only", "never"] as const; -export type SandboxProvisioningApprovalMode = (typeof SANDBOX_PROVISIONING_APPROVAL_MODES)[number]; - -/** A single runtime action category governed by permission policy. */ -export type AgentPermissionPolicyActionCategory = PermanentAgentSensitiveActionCategory; -export type ApprovalRequestActionCategory = - | AgentPermissionPolicyActionCategory - | "agent_provisioning" - | "sandbox_provisioning" - | "secrets_access"; - -/** How a runtime action category is handled by permission policy. */ -export type AgentPermissionPolicyDisposition = "allow" | "block" | "require-approval"; - -/** Exact tool-name permission overrides layered above category rules. */ -export type AgentPermissionPolicyToolRules = Record; - -/** Minimum portable agent gating context consumed by engine runtime wrappers. The legacy name is retained for API compatibility, but the context applies to permanent identity agents and ephemeral task-worker agents. */ -export interface PermanentAgentGatingContext { - permissionPolicy?: { - presetId: string; - rules: Partial>; - toolRules?: AgentPermissionPolicyToolRules; - }; - requester?: ApprovalRequestActorSnapshot; - taskId?: string; - runId?: string; - sessionId?: string; - createApprovalRequest?: (input: { - category: AgentPermissionPolicyActionCategory; - toolName: string; - args: Record; - /** - * FNXC:AgentGating 2026-07-05-00:00: - * FN-7609: the dedupe key must be persisted into the created request's - * targetAction.context so a retrying heartbeat's findPendingApprovalRequest - * lookup (which matches on context.approvalDedupeKey) can actually find and - * reuse the pending request instead of minting a new blank one every tick. - */ - approvalDedupeKey?: string; - }) => Promise; - findPendingApprovalRequest?: (dedupeKey: string) => Promise; -} - -/** Built-in permission policy preset identifiers for agent runtime policies. */ -export const AGENT_PERMISSION_POLICY_PRESET_IDS = ["unrestricted", "approval-required", "locked-down", "custom"] as const; - -/** A single built-in permission policy preset identifier. */ -export type AgentPermissionPolicyPresetId = (typeof AGENT_PERMISSION_POLICY_PRESET_IDS)[number]; - -/** Canonical category->disposition map for a permission policy. */ -export type AgentPermissionPolicyRules = Record< +import type { + Agent, + AgentAccessState, + AgentApiKey, + AgentApiKeyCreateResult, + AgentBudgetConfig, + AgentBudgetStatus, + AgentConfigRevision, + AgentConfigSnapshot, + AgentCreateInput, + AgentDetail, + AgentHeartbeatConfig, + AgentPerformanceSummary, + AgentPermission, + AgentPermissionPolicy, AgentPermissionPolicyActionCategory, - AgentPermissionPolicyDisposition ->; - -/** - * First-class persisted permission policy contract for permanent and ephemeral agents. - * - * FNXC:ToolPermissions 2026-07-01-00:00: - * Operators must be able to block a single governed tool such as `fn_task_create` without blocking every task-agent mutation. `toolRules` stores exact tool-name overrides and the engine resolves them before category rules while leaving heartbeat-critical exempt tools non-configurable. - */ -export interface AgentPermissionPolicy { - presetId: AgentPermissionPolicyPresetId; - rules: AgentPermissionPolicyRules; - toolRules?: AgentPermissionPolicyToolRules; -} - -/** Approval request lifecycle statuses. */ -export const APPROVAL_REQUEST_STATUSES = ["pending", "approved", "denied", "completed"] as const; - -/** A single approval request lifecycle status. */ -export type ApprovalRequestStatus = (typeof APPROVAL_REQUEST_STATUSES)[number]; - -/** Append-only audit event types for approval requests. */ -export const APPROVAL_REQUEST_AUDIT_EVENT_TYPES = [ - "created", - "approved", - "denied", - "completed", -] as const; - -/** A single append-only audit event type for approval requests. */ -export type ApprovalRequestAuditEventType = (typeof APPROVAL_REQUEST_AUDIT_EVENT_TYPES)[number]; - -/** Immutable actor identity snapshot captured at request/audit event time. */ -export interface ApprovalRequestActorSnapshot { - actorId: string; - actorType: "agent" | "user" | "system"; - actorName: string; -} - -/** Legacy action-category aliases accepted for backward compatibility. */ -export const LEGACY_AGENT_PERMISSION_POLICY_ACTION_CATEGORY_ALIASES = [ - "file_write", - "file_delete", - "command_execute", - "network_access", - "task_mutation", - "agent_mutation", -] as const; - -export type LegacyAgentPermissionPolicyActionCategory = - (typeof LEGACY_AGENT_PERMISSION_POLICY_ACTION_CATEGORY_ALIASES)[number]; - -/** Canonical + compatibility action-category input accepted at boundaries. */ -export type ApprovalRequestActionCategoryInput = - | ApprovalRequestActionCategory - | LegacyAgentPermissionPolicyActionCategory; - -/** Normalize legacy action-category aliases to canonical v1 categories. */ -export function normalizeApprovalRequestActionCategory( - category: ApprovalRequestActionCategoryInput, -): ApprovalRequestActionCategory { - switch (category) { - case "file_write": - case "file_delete": - return "file_write_delete"; - case "command_execute": - return "command_execution"; - case "network_access": - return "network_api"; - case "task_mutation": - case "agent_mutation": - return "task_agent_mutation"; - case "agent_provisioning": - return "agent_provisioning"; - case "sandbox_provisioning": - return "sandbox_provisioning"; - case "secrets_access": - return "secrets_access"; - default: - return category; - } -} - -/** Action payload gated by an approval request. */ -export interface ApprovalRequestTargetAction { - category: ApprovalRequestActionCategory; - action: string; - summary: string; - resourceType: string; - resourceId: string; - context?: Record; -} - -/** Append-only audit event row for approval request history. */ -export interface ApprovalRequestAuditEvent { - id: string; - requestId: string; - eventType: ApprovalRequestAuditEventType; - actor: ApprovalRequestActorSnapshot; - note?: string; - createdAt: string; -} - -/** Durable approval request record used by engine and dashboard surfaces. */ -export interface ApprovalRequest { - id: string; - status: ApprovalRequestStatus; - requester: ApprovalRequestActorSnapshot; - targetAction: ApprovalRequestTargetAction; - taskId?: string; - runId?: string; - requestedAt: string; - decidedAt?: string; - completedAt?: string; - createdAt: string; - updatedAt: string; -} - -/** Create input for a new pending approval request. */ -export interface ApprovalRequestCreateInput { - requester: ApprovalRequestActorSnapshot; - targetAction: Omit & { - category: ApprovalRequestActionCategoryInput; - }; - taskId?: string; - runId?: string; -} - -/** Input for pending->approved / pending->denied decisions. */ -export interface ApprovalRequestDecisionInput { - actor: ApprovalRequestActorSnapshot; - note?: string; -} - -/** Input for approved->completed transition. */ -export interface ApprovalRequestCompletionInput { - actor: ApprovalRequestActorSnapshot; - note?: string; -} - -/** Query filters for approval request listings. */ -export interface ApprovalRequestListInput { - status?: ApprovalRequestStatus; - requesterActorId?: string; - taskId?: string; - runId?: string; - limit?: number; - offset?: number; -} - -/** True when a transition is valid for approval request lifecycle rules. */ -export function isValidApprovalRequestTransition( - from: ApprovalRequestStatus, - to: ApprovalRequestStatus, -): boolean { - if (from === to) { - return true; - } - if (from === "pending") { - return to === "approved" || to === "denied"; - } - if (from === "approved") { - return to === "completed"; - } - return false; -} - -/** Describes how an agent's task assignment capability was determined. */ -export type TaskAssignSource = - | "role_default" // Granted automatically by role (e.g., scheduler gets tasks:assign) - | "explicit_grant" // Explicitly granted via permissions field - | "denied"; // Not granted by any source - -/** Computed access state for an agent, derived from its role and permissions. */ -export interface AgentAccessState { - /** The agent ID this access state belongs to. */ - agentId: string; - /** Whether this agent can assign tasks to other agents. */ - canAssignTasks: boolean; - /** How the tasks:assign permission was determined. */ - taskAssignSource: TaskAssignSource; - /** Whether this agent can create new agents. */ - canCreateAgents: boolean; - /** Whether this agent can execute tasks. */ - canExecuteTasks: boolean; - /** Whether this agent can review task output. */ - canReviewTasks: boolean; - /** Whether this agent can merge task branches. */ - canMergeTasks: boolean; - /** Whether this agent can delete agents. */ - canDeleteAgents: boolean; - /** Whether this agent can manage missions. */ - canManageMissions: boolean; - /** Whether this agent can send messages. */ - canSendMessages: boolean; - /** Full set of resolved permissions (union of role defaults + explicit grants). */ - resolvedPermissions: Set; - /** Permissions explicitly granted on this agent (from the permissions field). */ - explicitPermissions: Set; - /** Permissions granted by role default (not explicitly set). */ - roleDefaultPermissions: Set; -} - -/** Agent record stored in the system */ -export interface Agent { - /** Unique identifier (e.g., "agent-001") */ - id: string; - /** Display name */ - name: string; - /** Role/capability of the agent */ - role: AgentCapability; - /** Current lifecycle state */ - state: AgentState; - /** ID of the task this agent is currently working on (if any) */ - taskId?: string; - /** ISO-8601 timestamp when the agent was created */ - createdAt: string; - /** ISO-8601 timestamp of last update */ - updatedAt: string; - /** ISO-8601 timestamp of last successful heartbeat */ - lastHeartbeatAt?: string; - /** Optional metadata */ - metadata: Record; - /** Job title / description for the agent */ - title?: string; - /** Custom icon identifier */ - icon?: string; - /** Uploaded avatar image URL */ - imageUrl?: string; - /** Agent ID this agent reports to (org hierarchy) */ - reportsTo?: string; - /** Runtime configuration. Supports: AgentHeartbeatConfig keys (heartbeatIntervalMs, heartbeatTimeoutMs, maxConcurrentRuns) */ - runtimeConfig?: Record; - /** Why the agent was paused (error, manual, etc.) */ - pauseReason?: string; - /** Capability permission flags */ - permissions?: Record; - /** Runtime action gating policy (preset + normalized category rules). */ - permissionPolicy?: AgentPermissionPolicy; - /** Cumulative input tokens across all runs */ - totalInputTokens?: number; - /** Cumulative output tokens across all runs */ - totalOutputTokens?: number; - /** Last error message */ - lastError?: string; - /** Number of currently pending approvals requested by this agent. */ - pendingApprovalCount?: number; - /** - * FNXC:AgentTaskStateDrift 2026-06-27-16:20: - * Dashboard/API responses need a transient linked-task column so coordinators can distinguish legitimate parked/active agent linkages from execution drift; unresolved lookups use the response-only "unresolved" sentinel. This is resolved per request and must not be persisted by AgentStore. - */ - taskColumn?: string; - /** Path to a markdown file containing custom instructions (resolved relative to project root). - * Must end in `.md`, no `..` traversal. Max 500 chars. */ - instructionsPath?: string; - /** Inline custom instructions appended to the agent's system prompt at execution time. Max 50,000 chars. */ - instructionsText?: string; - /** Agent personality/identity description — defines the agent's character, tone, and behavioral traits. Max 10,000 chars. */ - soul?: string; - /** Per-agent accumulated knowledge — stores learnings, preferences, and context the agent has gathered. Max 50,000 chars. */ - memory?: string; - /** Structured instruction bundle configuration for managed/external markdown files. */ - bundleConfig?: InstructionsBundleConfig; - /** Optional path to a markdown file containing this agent's per-tick heartbeat procedure - * (overrides the default HEARTBEAT_PROCEDURE constant). Resolved relative to project root. - * Must end in `.md`, no `..` traversal. Max 500 chars. */ - heartbeatProcedurePath?: string; -} - -/** Recursive node in the agent org tree. */ -export interface OrgTreeNode { - agent: Agent; - children: OrgTreeNode[]; -} - -export type MessageResponseMode = "immediate" | "on-heartbeat"; - -/** Per-agent heartbeat configuration, stored in agent.runtimeConfig */ -export interface AgentHeartbeatConfig { - /** Whether heartbeat triggers are enabled for this agent (default: true) */ - enabled?: boolean; - /** Whether this agent should auto-claim relevant unowned tasks during no-task heartbeats (default: true when unset). */ - autoClaimRelevantTasks?: boolean; - /** - * FNXC:AgentRouting 2026-07-12-11:20: - * Per-agent task-routing eligibility (GitHub issue Runfusion/Fusion#2015). "auto" (default) = current behavior; - * "explicit-only" = never auto-assigned/auto-claimed but accepts explicit delegation; "none" = never bound to - * implementation tasks by ANY path, including delegation with override=true. Set "none" on liaison/observer agents. - */ - assignmentPolicy?: "auto" | "explicit-only" | "none"; - /** Number of auto-claim candidates to inject into no-task heartbeat prompts. Default: 5, range: 0-10. */ - autoClaimCandidatesInPrompt?: number; - /** Per-agent override for opting engineer-role agents into no-task backlog auto-claim. Default: project setting or false. */ - engineerBacklogAutoClaim?: boolean; - /** Polling interval in ms (default: 30000). Min: 1000 */ - heartbeatIntervalMs?: number; - /** Heartbeat timeout in ms (default: 60000). Min: 5000 */ - heartbeatTimeoutMs?: number; - /** Max concurrent heartbeat runs per agent (default: 1). Min: 1 */ - maxConcurrentRuns?: number; - /** Whether periodic self-improvement is enabled (default: true) */ - selfImproveEnabled?: boolean; - /** Interval between self-improvement cycles in ms (default: 14400000 = 4h). Min: 3600000 (1h) */ - selfImproveIntervalMs?: number; - /** ISO timestamp of last self-improvement run */ - lastSelfImproveAt?: string; - /** - * How this agent responds to incoming messages. - * "immediate" triggers a heartbeat run when a message arrives. - * "on-heartbeat" defers message handling to the next scheduled heartbeat (default). - */ - messageResponseMode?: MessageResponseMode; - /** Per-agent budget governance configuration. When set, enables budget tracking and enforcement. */ - budgetConfig?: AgentBudgetConfig; - /** Per-agent override for memory prompt inclusion mode. */ - agentMemoryInclusionMode?: AgentMemoryInclusionMode; - /** Per-agent override for heartbeat scope-discipline procedure mode. */ - heartbeatScopeDiscipline?: HeartbeatScopeDisciplineMode; - /** Per-agent override for heartbeat execution prompt template mode. */ - heartbeatPromptTemplate?: HeartbeatPromptTemplate; - /** Last resolved memory inclusion mode recorded by engine for transition logging. */ - lastAgentMemoryInclusionMode?: AgentMemoryInclusionMode; - /** - * When true, the engine fires a catch-up heartbeat at server startup if the - * agent's last heartbeat is older than its interval — i.e., the server was - * down across a scheduled tick. Default: false. - */ - runMissedHeartbeatOnStartup?: boolean; - /** - * When true (default), an agent's heartbeat runs and its task execution session can run - * concurrently. When false, the two paths serialize: a heartbeat will not start while the - * agent's bound task has an active executor session, and an executor session will not start - * while the agent has an active heartbeat run. - * - * Permanent agents only — ignored for ephemeral agents. Default: true when unset. - */ - allowParallelExecution?: boolean; - /** - * When true, timer-triggered heartbeats are skipped while the agent has no currently assigned - * task (`agent.taskId` is unset). Assignment and on-demand triggers are unaffected. - * Default: false (timer fires regardless of assignment). - */ - skipHeartbeatWhenIdle?: boolean; -} - -/** Per-agent budget configuration, stored in agent.runtimeConfig.budgetConfig */ -export interface AgentBudgetConfig { - /** Total token cap (input + output). When undefined, no budget limit is enforced. */ - tokenBudget?: number; - /** Warning threshold as a fraction (0–1). Default: 0.8. Triggers isOverThreshold when usagePercent >= this value * 100. */ - usageThreshold?: number; - /** Budget accumulation period. Default: "lifetime". */ - budgetPeriod?: "daily" | "weekly" | "monthly" | "lifetime"; - /** Day of month/week for period reset (1–31 for monthly, 0–6 for weekly where 0=Sunday). Only used when budgetPeriod is "monthly" or "weekly". */ - resetDay?: number; -} - -/** Computed budget status for an agent at a point in time. */ -export interface AgentBudgetStatus { - /** The agent this status belongs to */ - agentId: string; - /** Total tokens consumed (input + output) */ - currentUsage: number; - /** Token cap from config, or null when no budget is configured */ - budgetLimit: number | null; - /** Usage as a percentage of budget (0–100), or null when no budget */ - usagePercent: number | null; - /** The configured threshold fraction (e.g., 0.8), or null when no budget */ - thresholdPercent: number | null; - /** Whether currentUsage >= budgetLimit */ - isOverBudget: boolean; - /** Whether usagePercent >= thresholdPercent * 100 */ - isOverThreshold: boolean; - /** ISO-8601 timestamp of the last budget reset, or null */ - lastResetAt: string | null; - /** ISO-8601 timestamp of the next scheduled reset, or null for lifetime/no budget */ - nextResetAt: string | null; -} - -/** Configuration for an agent's instruction bundle — a collection of markdown files - * that together form the agent's custom instructions. */ -export interface InstructionsBundleConfig { - /** Bundle mode — "managed" = system-managed directory, "external" = user-specified path */ - mode: "managed" | "external"; - /** Primary instructions file name (default: "AGENTS.md") */ - entryFile: string; - /** List of all file names in the bundle directory */ - files: string[]; - /** User-specified directory path for external mode (required when mode is "external") */ - externalPath?: string; -} - -/** Extended agent information including heartbeat history */ -export interface AgentDetail extends Agent { - /** Recent heartbeat events (last N events) */ - heartbeatHistory: AgentHeartbeatEvent[]; - /** Current active heartbeat run (if any) */ - activeRun?: AgentHeartbeatRun; - /** All completed runs for this agent */ - completedRuns: AgentHeartbeatRun[]; -} - -/** Input for creating a new agent */ -export interface AgentCreateInput { - name: string; - role: AgentCapability; - metadata?: Record; - title?: string; - icon?: string; - imageUrl?: string; - reportsTo?: string; - runtimeConfig?: Record; - permissions?: Record; - permissionPolicy?: AgentPermissionPolicy; - instructionsPath?: string; - instructionsText?: string; - soul?: string; - memory?: string; - bundleConfig?: InstructionsBundleConfig; - heartbeatProcedurePath?: string; -} - -/** Input for updating an existing agent */ -export interface AgentUpdateInput { - name?: string; - role?: AgentCapability; - metadata?: Record; - title?: string; - icon?: string; - imageUrl?: string; - reportsTo?: string; - runtimeConfig?: Record; - pauseReason?: string; - permissions?: Record; - permissionPolicy?: AgentPermissionPolicy; - lastError?: string; - totalInputTokens?: number; - totalOutputTokens?: number; - instructionsPath?: string; - instructionsText?: string; - soul?: string; - memory?: string; - bundleConfig?: InstructionsBundleConfig; - heartbeatProcedurePath?: string; -} - -/** An API key associated with an agent for bearer token authentication. */ -export interface AgentApiKey { - /** Unique key identifier (e.g., "key-a1b2c3d4") */ - id: string; - /** The agent this key belongs to */ - agentId: string; - /** SHA-256 hash of the plaintext token (hex-encoded, 64 chars) */ - tokenHash: string; - /** Optional human-readable label for the key */ - label?: string; - /** ISO-8601 timestamp when the key was created */ - createdAt: string; - /** ISO-8601 timestamp when the key was revoked, null if active */ - revokedAt?: string; -} - -/** Result returned when creating a new API key — includes the plaintext token exactly once. */ -export interface AgentApiKeyCreateResult { - /** The persisted key metadata (不含 plaintext token) */ - key: AgentApiKey; - /** The plaintext token — shown only at creation, never stored */ - token: string; -} - -/** Per-task session persistence for an agent */ -export interface AgentTaskSession { - /** Agent ID */ - agentId: string; - /** Task ID */ - taskId: string; - /** Session state for resuming context across runs */ - sessionParams: Record; - /** Human-readable session identifier */ - sessionDisplayId?: string; - /** ISO-8601 timestamp when session was created */ - createdAt: string; - /** ISO-8601 timestamp of last update */ - updatedAt: string; -} - -/** A single performance rating for an agent */ -export interface AgentRating { - id: string; - agentId: string; - raterType: "user" | "agent" | "system"; - raterId?: string; - score: number; - category?: string; - comment?: string; - runId?: string; - taskId?: string; - createdAt: string; -} - -/** Aggregated rating statistics for an agent */ -export interface AgentRatingSummary { - agentId: string; - averageScore: number; - totalRatings: number; - categoryAverages: Record; - recentRatings: AgentRating[]; - trend: "improving" | "declining" | "stable" | "insufficient-data"; -} - -/** Input payload for creating an agent rating */ -export interface AgentRatingInput { - raterType: "user" | "agent" | "system"; - raterId?: string; - score: number; - category?: string; - comment?: string; - runId?: string; - taskId?: string; -} - -/** Trackable configuration fields for revision history. - * Excludes budget-related items, state, taskId, token counts, and timestamps. */ -export interface AgentConfigSnapshot { - name: string; - role: AgentCapability; - title?: string; - icon?: string; - imageUrl?: string; - reportsTo?: string; - runtimeConfig?: Record; - permissions?: Record; - permissionPolicy?: AgentPermissionPolicy; - instructionsPath?: string; - instructionsText?: string; - soul?: string; - memory?: string; - bundleConfig?: InstructionsBundleConfig; - heartbeatProcedurePath?: string; - metadata: Record; -} - -/** A single key-value change within a config revision */ -export interface RevisionFieldDiff { - field: string; - oldValue: unknown; - newValue: unknown; -} - -/* -FNXC:ConfigVersioning 2026-07-18-00:00: -FN-8282 requires every durable configuration mutation to retain an immutable -before/after snapshot. Targets stay structured JSON; target keys are derived -from canonical JSON rather than delimiter-concatenated identifiers. - -FNXC:ConfigVersioning 2026-07-18-10:30: -Every provenance variant carries a stable ID. This makes agent and authenticated -human writes auditable and makes intentional internal writes explicit as the -system actor instead of allowing anonymous history rows. -*/ -export type ConfigKind = "project-settings" | "global-settings" | "workflow-settings" | "routine" | "automation"; -export type ConfigChangedBy = - | { kind: "human"; id: string } - | { kind: "agent"; id: string } - | { kind: "system"; id: string } - | { kind: "rollback"; id: string }; -export type ConfigurationOwnerScope = "project" | "global"; -export type ConfigurationTarget = Readonly>; -export interface ConfigurationRevision { - id: string; - projectId: string; - ownerScope: ConfigurationOwnerScope; - configKind: ConfigKind; - configTarget: ConfigurationTarget; - /** Canonical JSON representation used only for exact target indexing. */ - configTargetKey: string; - before: unknown; - after: unknown; - diffs: RevisionFieldDiff[]; - changedBy: ConfigChangedBy; - createdAt: string; - source: "mutation" | "rollback"; - rollbackToRevisionId?: string; -} - -/** A revision entry recording a configuration change to an agent */ -export interface AgentConfigRevision { - /** Unique revision identifier */ - id: string; - /** Agent ID this revision belongs to */ - agentId: string; - /** ISO-8601 timestamp when the revision was created */ - createdAt: string; - /** Snapshot of config BEFORE the change */ - before: AgentConfigSnapshot; - /** Snapshot of config AFTER the change */ - after: AgentConfigSnapshot; - /** Field-level diffs between before and after */ - diffs: RevisionFieldDiff[]; - /** Description of what changed (e.g., "Updated runtimeConfig, name") */ - summary: string; - /** Who or what triggered the change */ - source: "user" | "system" | "rollback"; - /** If this was a rollback, the revision ID that was restored */ - rollbackToRevisionId?: string; -} - -/** - * Legacy project-relative shared path for the heartbeat procedure markdown - * file. Older builds defaulted every non-ephemeral agent to this single - * file, which prevented per-agent customization. New code should use - * {@link getDefaultHeartbeatProcedurePath} instead. This constant is kept - * exported only so migrations can detect agents still pointing at the - * shared path and re-route them to their own per-agent file. - * - * @deprecated Use {@link getDefaultHeartbeatProcedurePath} for new agent - * creation and upgrade flows. - */ -export const DEFAULT_HEARTBEAT_PROCEDURE_PATH = ".fusion/HEARTBEAT.md"; - -function slugifyAgentAssetSegment(value: string): string { - return value - .toLowerCase() - .replace(/[^a-z0-9]+/g, "-") - .replace(/^-+|-+$/g, ""); -} - -export function getSafeAgentAssetIdSegment(agentId: string): string { - const slug = slugifyAgentAssetSegment(agentId); - return slug || "agent"; -} - -/** - * Compute the canonical per-agent asset directory segment. - * - * Canonical format: `-`. - * Example: `CEO` + `agent2736` => `ceo-agent2736`. - * - * If the display-name slug is empty (for example name has only symbols), the - * id-derived segment is used as the directory prefix so the result is always - * filesystem-safe and non-empty. - */ -export function getCanonicalAgentAssetDirectoryName(agentName: string, agentId: string): string { - if (!agentId || typeof agentId !== "string") { - throw new Error("getCanonicalAgentAssetDirectoryName requires a non-empty agentId"); - } - const safeId = getSafeAgentAssetIdSegment(agentId); - const nameSlug = slugifyAgentAssetSegment(agentName ?? ""); - const prefix = nameSlug || safeId; - return `${prefix}-${safeId}`; -} - -/** Legacy per-agent asset directory segment used by older builds. */ -export function getLegacyAgentAssetDirectoryName(agentId: string): string { - if (!agentId || typeof agentId !== "string") { - throw new Error("getLegacyAgentAssetDirectoryName requires a non-empty agentId"); - } - return agentId; -} - -/** Canonical managed instruction bundle directory name for an agent. */ -export function getCanonicalAgentInstructionsBundleDirName(agentName: string, agentId: string): string { - return `${getCanonicalAgentAssetDirectoryName(agentName, agentId)}-instructions`; -} - -/** Legacy managed instruction bundle directory name used by older builds. */ -export function getLegacyAgentInstructionsBundleDirName(agentId: string): string { - return `${getLegacyAgentAssetDirectoryName(agentId)}-instructions`; -} - -/** - * Compute the project-relative default heartbeat procedure file path for a - * given agent. Each agent gets their own editable HEARTBEAT.md so operators - * can tune the per-tick procedure without changes leaking across the team. - * - * The path is laid out under `.fusion/agents//HEARTBEAT.md`. - */ -export function getDefaultHeartbeatProcedurePath(agentId: string, agentName?: string): string { - if (!agentId || typeof agentId !== "string") { - throw new Error("getDefaultHeartbeatProcedurePath requires a non-empty agentId"); - } - const directory = agentName - ? getCanonicalAgentAssetDirectoryName(agentName, agentId) - : getLegacyAgentAssetDirectoryName(agentId); - return `.fusion/agents/${directory}/HEARTBEAT.md`; -} - -/** Extract trackable config fields from an Agent into a snapshot */ -export function agentToConfigSnapshot(agent: Agent): AgentConfigSnapshot { - return { - name: agent.name, - role: agent.role, - title: agent.title, - icon: agent.icon, - imageUrl: agent.imageUrl, - reportsTo: agent.reportsTo, - runtimeConfig: agent.runtimeConfig ? { ...agent.runtimeConfig } : undefined, - permissions: agent.permissions ? { ...agent.permissions } : undefined, - permissionPolicy: agent.permissionPolicy - ? { - presetId: agent.permissionPolicy.presetId, - rules: { ...agent.permissionPolicy.rules }, - ...(agent.permissionPolicy.toolRules ? { toolRules: { ...agent.permissionPolicy.toolRules } } : {}), - } - : undefined, - instructionsPath: agent.instructionsPath, - instructionsText: agent.instructionsText, - soul: agent.soul, - memory: agent.memory, - bundleConfig: agent.bundleConfig - ? { - ...agent.bundleConfig, - files: [...agent.bundleConfig.files], - } - : undefined, - heartbeatProcedurePath: agent.heartbeatProcedurePath, - metadata: { ...agent.metadata }, - }; -} - -/** Compare two config snapshots and return field-level diffs */ -export function diffConfigSnapshots( - before: AgentConfigSnapshot, - after: AgentConfigSnapshot, -): RevisionFieldDiff[] { - const trackedFields: Array = [ - "name", - "role", - "title", - "icon", - "imageUrl", - "reportsTo", - "runtimeConfig", - "permissions", - "permissionPolicy", - "instructionsPath", - "instructionsText", - "soul", - "memory", - "bundleConfig", - "heartbeatProcedurePath", - "metadata", - ]; - - const diffs: RevisionFieldDiff[] = []; - - for (const field of trackedFields) { - const oldVal = before[field]; - const newVal = after[field]; - - if (JSON.stringify(oldVal) !== JSON.stringify(newVal)) { - diffs.push({ field, oldValue: oldVal, newValue: newVal }); - } - } - - return diffs; -} - -/** Aggregate statistics for agents */ -export interface AgentStats { - /** Number of agents in active/running state */ - activeCount: number; - /** Number of tasks assigned to agents */ - assignedTaskCount: number; - /** Total completed runs */ - completedRuns: number; - /** Total failed runs */ - failedRuns: number; - /** Success rate (0-1) */ - successRate: number; - /** Number of idle non-ephemeral agents available for queue drain */ - idleNonEphemeralCount: number; - /** Number of tasks currently in the todo column */ - todoTaskCount: number; -} - -/** Trigger source for an agent self-reflection run */ -export type ReflectionTrigger = "periodic" | "post-task" | "manual" | "user-requested"; - -/** - * FNXC:AgentReflection 2026-07-04-00:00: - * FN-7528 adds a deterministic, non-LLM post-task performance capture that runs on every - * completed task (guarded by settings.reflectionEnabled), distinct from the LLM-backed - * generateReflection path. These extra fields are a compact structured snapshot — duration - * drivers, packages/files touched, verification command(s)/scope, and retry/rework count. - * All fields are optional (backward-compatible with existing JSONL records) and outcome-only: - * no free-form prose, prompt text, or reflection narrative is ever stored here or emitted to - * run-audit (FN-7158 ids/counts/outcomes-only contract). Omit a field rather than fabricate it - * when its source data is unavailable. - */ -export interface ReflectionMetrics { - /** Tasks completed in the analysis window */ - tasksCompleted?: number; - /** Tasks failed in the analysis window */ - tasksFailed?: number; - /** Average task duration in milliseconds */ - avgDurationMs?: number; - /** Total tokens consumed in the analysis window */ - totalTokensUsed?: number; - /** Number of errors encountered */ - errorCount?: number; - /** Recurring error patterns */ - commonErrors?: string[]; - /** Single task's wall-clock duration in milliseconds (distinct from the aggregate avgDurationMs) */ - durationMs?: number; - /** Short deterministic labels describing what drove the duration (e.g. "retries:2", "rework:1", "verification-broad") — never free-form prose */ - durationDrivers?: string[]; - /** Package names derived from touched file paths (e.g. "@fusion/core" or "packages/core") */ - packagesTouched?: string[]; - /** Count of files touched, when available */ - filesTouchedCount?: number; - /** Verification command(s) recorded for the task */ - verificationCommands?: string[]; - /** reworkCount + retry/recovery count */ - retryReworkCount?: number; - /** True when verification was file-scoped, false when broader/full-suite */ - verificationFileScoped?: boolean; - /** Short reason label when verification scope was broader (e.g. "whole-package test script has no file-scoped filter"); omitted when file-scoped */ - verificationScopeReason?: string; -} - -/** A persisted self-reflection generated by an agent */ -export interface AgentReflection { - /** Unique reflection ID */ - id: string; - /** The agent this reflection belongs to */ - agentId: string; - /** ISO-8601 timestamp when the reflection was created */ - timestamp: string; - /** What caused this reflection */ - trigger: ReflectionTrigger; - /** Optional trigger detail context */ - triggerDetail?: string; - /** Associated task ID (for post-task reflections) */ - taskId?: string; - /** Quantitative reflection metrics */ - metrics: ReflectionMetrics; - /** Key observations from self-analysis */ - insights: string[]; - /** Suggested improvements for future runs */ - suggestedImprovements: string[]; - /** One-paragraph narrative summary */ - summary: string; -} - -/** Aggregated performance summary derived from recent reflections */ -export interface AgentPerformanceSummary { - /** Agent identifier */ - agentId: string; - /** Total tasks completed in the analysis window */ - totalTasksCompleted: number; - /** Total tasks failed in the analysis window */ - totalTasksFailed: number; - /** Average task duration in milliseconds */ - avgDurationMs: number; - /** Success ratio from 0 to 1 */ - successRate: number; - /** Top recurring errors */ - commonErrors: string[]; - /** Derived strengths from successful patterns */ - strengths: string[]; - /** Derived weaknesses from failure patterns */ - weaknesses: string[]; - /** Number of reflections considered in this summary */ - recentReflectionCount: number; - /** ISO-8601 timestamp when summary was computed */ - computedAt: string; -} + AgentPermissionPolicyDisposition, + AgentPermissionPolicyPresetId, + AgentPermissionPolicyRules, + AgentPermissionPolicyToolRules, + AgentProvisioningApprovalMode, + AgentRating, + AgentRatingInput, + AgentRatingSummary, + AgentReflection, + AgentStats, + AgentTaskSession, + AgentUpdateInput, + ApprovalRequest, + ApprovalRequestActionCategory, + ApprovalRequestActionCategoryInput, + ApprovalRequestActorSnapshot, + ApprovalRequestAuditEvent, + ApprovalRequestAuditEventType, + ApprovalRequestCompletionInput, + ApprovalRequestCreateInput, + ApprovalRequestDecisionInput, + ApprovalRequestListInput, + ApprovalRequestStatus, + ApprovalRequestTargetAction, + ConfigChangedBy, + ConfigKind, + ConfigurationOwnerScope, + ConfigurationRevision, + ConfigurationTarget, + InstructionsBundleConfig, + LegacyAgentPermissionPolicyActionCategory, + MessageResponseMode, + OrgTreeNode, + PermanentAgentActionCategory, + PermanentAgentGatingContext, + PermanentAgentSensitiveActionCategory, + ReflectionMetrics, + ReflectionTrigger, + RevisionFieldDiff, + SandboxProvisioningApprovalMode, + SecretAccessPolicy, + TaskAssignSource, +} from "./types/agents.js"; +export type { + Agent, + AgentAccessState, + AgentApiKey, + AgentApiKeyCreateResult, + AgentBudgetConfig, + AgentBudgetStatus, + AgentConfigRevision, + AgentConfigSnapshot, + AgentCreateInput, + AgentDetail, + AgentHeartbeatConfig, + AgentPerformanceSummary, + AgentPermission, + AgentPermissionPolicy, + AgentPermissionPolicyActionCategory, + AgentPermissionPolicyDisposition, + AgentPermissionPolicyPresetId, + AgentPermissionPolicyRules, + AgentPermissionPolicyToolRules, + AgentProvisioningApprovalMode, + AgentRating, + AgentRatingInput, + AgentRatingSummary, + AgentReflection, + AgentStats, + AgentTaskSession, + AgentUpdateInput, + ApprovalRequest, + ApprovalRequestActionCategory, + ApprovalRequestActionCategoryInput, + ApprovalRequestActorSnapshot, + ApprovalRequestAuditEvent, + ApprovalRequestAuditEventType, + ApprovalRequestCompletionInput, + ApprovalRequestCreateInput, + ApprovalRequestDecisionInput, + ApprovalRequestListInput, + ApprovalRequestStatus, + ApprovalRequestTargetAction, + ConfigChangedBy, + ConfigKind, + ConfigurationOwnerScope, + ConfigurationRevision, + ConfigurationTarget, + InstructionsBundleConfig, + LegacyAgentPermissionPolicyActionCategory, + MessageResponseMode, + OrgTreeNode, + PermanentAgentActionCategory, + PermanentAgentGatingContext, + PermanentAgentSensitiveActionCategory, + ReflectionMetrics, + ReflectionTrigger, + RevisionFieldDiff, + SandboxProvisioningApprovalMode, + SecretAccessPolicy, + TaskAssignSource, +}; // ── Multi-Project First-Run & Migration Types ─────────────────────────────── diff --git a/packages/core/src/types/agents.ts b/packages/core/src/types/agents.ts new file mode 100644 index 0000000000..4e8d60925a --- /dev/null +++ b/packages/core/src/types/agents.ts @@ -0,0 +1,1126 @@ +/** + * FNXC:CodeOrganization 2026-07-18-14:00: + * Agent permissions, entity, ratings, and reflection types peeled from types.ts. + * types.ts re-exports these for the Vite @fusion/core alias and package barrel. + */ + +import type { AgentState } from "./agent-state.js"; + +export type AgentCapability = "triage" | "executor" | "reviewer" | "merger" | "scheduler" | "engineer" | "custom"; + + +/** Single heartbeat event recorded for an agent */ +export interface AgentHeartbeatEvent { + /** ISO-8601 timestamp of when the heartbeat was recorded */ + timestamp: string; + /** Status of the heartbeat */ + status: "ok" | "missed" | "recovered"; + /** ID of the heartbeat run this event belongs to */ + runId: string; +} + +/** What triggered a heartbeat run */ +export type HeartbeatInvocationSource = "on_demand" | "timer" | "assignment" | "automation" | "routine"; + +/** A continuous heartbeat session/run for an agent */ +export interface AgentHeartbeatRun { + /** Unique identifier for this run */ + id: string; + /** ID of the agent this run belongs to */ + agentId: string; + /** Task ID associated with this heartbeat run when bound to a task. */ + taskId?: string; + /** ISO-8601 timestamp when the run started */ + startedAt: string; + /** ISO-8601 timestamp when the run ended (null if active) */ + endedAt: string | null; + /** Status of the run */ + status: "active" | "completed" | "terminated" | "failed"; + /** What triggered this run */ + invocationSource?: HeartbeatInvocationSource; + /** Trigger detail (manual, ping, scheduler, system) */ + triggerDetail?: string; + /** PID of the agent process */ + processPid?: number; + /** Exit code of the agent process */ + exitCode?: number; + /** Session ID before execution (for continuity tracking) */ + sessionIdBefore?: string; + /** Session ID after execution */ + sessionIdAfter?: string; + /** Token usage for this run */ + usageJson?: { inputTokens: number; outputTokens: number; cachedTokens: number; cacheWriteTokens: number }; + /** Structured result from the run */ + resultJson?: Record; + /** Snapshot of context at run start (taskId, projectId, etc.). + * May include optional comment-wake fields: + * - `triggeringCommentIds?: string[]` + * - `triggeringCommentType?: "steering" | "task" | "pr"` */ + contextSnapshot?: Record; + /** Excerpt of stdout output */ + stdoutExcerpt?: string; + /** Excerpt of stderr output */ + stderrExcerpt?: string; + /** Full assembled system prompt sent to the LLM for this run (truncated to 100,000 chars). */ + systemPrompt?: string; + /** Full per-tick execution prompt sent to the LLM for this run (truncated to 100,000 chars). */ + executionPrompt?: string; + /** Whether the run used a custom heartbeat procedure, the built-in default, or the no-task default override. */ + heartbeatProcedureSource?: "default" | "custom" | "default-no-task-override"; +} + + +// ── Agent Permission Types ────────────────────────────────────────────────── + +/** Canonical permission identifiers for agent access control. + * Each string represents a discrete capability that can be granted or denied. */ +export const AGENT_PERMISSIONS = [ + "tasks:assign", // Assign tasks to agents + "tasks:create", // Create new tasks + "tasks:execute", // Execute/run tasks + "tasks:review", // Review task output (code, specs) + "tasks:merge", // Merge completed task branches + "tasks:delete", // Delete tasks + "tasks:archive", // Archive/unarchive tasks + "agents:create", // Create new agents + "agents:update", // Update agent configuration + "agents:delete", // Delete agents + "agents:view", // View agent details and logs + "settings:read", // Read project settings + "settings:update", // Modify project settings + "workflows:manage", // Create/edit/delete workflow steps + "missions:manage", // Create/edit/delete missions and slices + "automations:manage", // Create/edit/delete scheduled automations + "messages:send", // Send messages to agents/users + "messages:read", // Read mailbox messages +] as const; + +/** A single canonical permission string. */ +export type AgentPermission = (typeof AGENT_PERMISSIONS)[number]; + +/** + * Canonical v1 action categories for permanent-agent runtime gating. + * + * `none` is a classifier-only result for positively-recognized read-only actions. + * It is never stored as a policy rule key. + */ +/** + * FNXC:ToolPermissions 2026-07-09-00:00: + * FN-7728 adds `review_gate_bypass` as a first-class sensitive action category distinct from `task_agent_mutation`. It governs merge-gate override tools (e.g. `fn_task_bypass_review`, delivered by FN-7720) so operators can independently allow/require-approval/block "who may bypass a failed review gate" without touching ordinary task-mutation policy. It defaults to a stricter disposition than the uniform preset default (see agent-permission-policy.ts) and is resolved identically by both evaluateAgentActionGate and the permanent-agent gate via the shared gating-classifications.ts source. + * + * FNXC:ToolPermissions 2026-07-09-08:30: + * FN-7737 adds `file_scope` as a first-class sensitive action category governing the File Scope additional-approval action (`fn_task_file_scope_add`, an executor-visible tool that extends a task's declared `## File Scope` beyond its initial spec at runtime). Unlike `review_gate_bypass`, `file_scope` intentionally keeps the UNIFORM grant-all disposition — the `unrestricted` preset resolves it to `allow` via `buildRules("allow")` with no override patch, since File Scope self-extension is an ordinary executor-scope action, not a merge-gate override. It is resolved identically by both evaluateAgentActionGate and the permanent-agent gate via the shared `FILE_SCOPE_FN_TOOLS` set in gating-classifications.ts. + */ +export const PERMANENT_AGENT_ACTION_CATEGORIES = [ + "git_write", + "file_write_delete", + "command_execution", + "network_api", + "task_agent_mutation", + "review_gate_bypass", + "file_scope", + "none", +] as const; + +/** A single v1 permanent-agent action category. */ +export type PermanentAgentActionCategory = (typeof PERMANENT_AGENT_ACTION_CATEGORIES)[number]; + +/** Sensitive runtime categories covered by policy rules (excludes classifier-only `none`). */ +export type PermanentAgentSensitiveActionCategory = Exclude; + +/** Runtime action categories governed by agent permission policy presets. */ +export const AGENT_PERMISSION_POLICY_ACTION_CATEGORIES: readonly PermanentAgentSensitiveActionCategory[] = [ + "git_write", + "file_write_delete", + "command_execution", + "network_api", + "task_agent_mutation", + "review_gate_bypass", + "file_scope", +] as const; + +export const AGENT_PERMISSION_POLICY_CATEGORY_TOOL_EXAMPLES: Record< + PermanentAgentSensitiveActionCategory, + readonly string[] +> = { + git_write: ["git commit", "git push", "git merge", "git branch -d", "git worktree add", "write", "edit"], + file_write_delete: ["write", "edit", "fn_task_attach"], + command_execution: ["bash (non-git)", "fn_run_verification", "fn_acquire_repo_worktree", "read", "find", "grep", "ls"], + network_api: ["fn_research_run (web/research)", "fn_research_cancel", "fn_web_fetch", "worktrunk_install"], + /* FNXC:ToolGovernance 2026-06-27-16:51: Dashboard policy examples must mirror action-gate mutation exports. Identity reflection is exempt heartbeat coordination, so it is intentionally not advertised as task_agent_mutation. + * FNXC:WorkflowAuthoringTools 2026-06-29-23:40: Published workflow authoring tools are now agent-visible, so policy examples include the mutating workflow create/update/delete/settings/select surface operators can approve or block. + * FNXC:ToolGovernance 2026-07-09-09:36: FN-7733 — the GitLab browse tools (fn_task_browse_gitlab_project_issues, fn_task_browse_gitlab_group_issues, fn_task_browse_gitlab_merge_requests) are read-only discovery tools that never create task rows and are already classified under READONLY_FN_TOOLS in gating-classifications.ts; they were never members of ACTION_GATE_TASK_AGENT_MANAGEMENT_TOOLS. Listing them here as task_agent_mutation examples broke the invariant that this list must be a subset of the action-gate mutation classification, so they are intentionally excluded. The mutating fn_task_import_gitlab_* variants (which do create task rows) remain listed below. */ + task_agent_mutation: [ + "fn_task_create", + "fn_delegate_task", + "fn_task_import_github", + "fn_task_import_github_issue", + "fn_task_import_gitlab_project_issues", + "fn_task_import_gitlab_group_issues", + "fn_task_import_gitlab_merge_requests", + "fn_spawn_agent", + "fn_update_agent_config", + "fn_task_update", + "fn_task_assign", + "fn_workflow_create", + "fn_workflow_update", + "fn_workflow_delete", + "fn_workflow_settings", + "fn_workflow_select", + "fn_task_promote", + "fn_task_refine", + ], + /* FNXC:ToolPermissions 2026-07-09-00:00: FN-7728 — review_gate_bypass governs merge-gate override tools as a distinct, more-restricted permission from ordinary task mutation. fn_task_bypass_review (FN-7720) is CLI/pi-extension operator-tool-only; it is never exposed to executor/reviewer/triage agent tool lists. */ + review_gate_bypass: ["fn_task_bypass_review"], + /* FNXC:ToolPermissions 2026-07-09-08:30: FN-7737 — file_scope governs the File Scope additional-approval action (fn_task_file_scope_add), which lets an executing agent extend its task's declared ## File Scope beyond the initial spec at runtime. Unlike review_gate_bypass, it keeps the uniform grant-all default (handled by "allow" under the unrestricted preset), so it is not patched by a *Override-style function. */ + file_scope: ["fn_task_file_scope_add"], +}; + +export const AGENT_PERMISSION_POLICY_EXEMPT_TOOL_EXAMPLES: readonly string[] = [ + "fn_send_message", + "fn_post_room_message", + "fn_read_messages", + "fn_task_log", + "fn_task_logs_read", + "fn_task_done", + "fn_heartbeat_done", + "fn_task_document_write", + "fn_task_document_read", + "fn_workflow_list", + "fn_workflow_get", + "fn_trait_list", + "fn_memory_search", + "fn_memory_get", + "fn_memory_append", + "fn_read_evaluations", + "fn_reflect_on_performance", +]; + +export const AGENT_PROVISIONING_APPROVAL_MODES = ["always", "trusted-only", "never"] as const; +export type AgentProvisioningApprovalMode = (typeof AGENT_PROVISIONING_APPROVAL_MODES)[number]; + +export const SECRET_ACCESS_POLICIES = ["auto", "prompt", "deny"] as const; +export type SecretAccessPolicy = (typeof SECRET_ACCESS_POLICIES)[number]; + +export const SANDBOX_PROVISIONING_APPROVAL_MODES = ["always", "trusted-only", "never"] as const; +export type SandboxProvisioningApprovalMode = (typeof SANDBOX_PROVISIONING_APPROVAL_MODES)[number]; + +/** A single runtime action category governed by permission policy. */ +export type AgentPermissionPolicyActionCategory = PermanentAgentSensitiveActionCategory; +export type ApprovalRequestActionCategory = + | AgentPermissionPolicyActionCategory + | "agent_provisioning" + | "sandbox_provisioning" + | "secrets_access"; + +/** How a runtime action category is handled by permission policy. */ +export type AgentPermissionPolicyDisposition = "allow" | "block" | "require-approval"; + +/** Exact tool-name permission overrides layered above category rules. */ +export type AgentPermissionPolicyToolRules = Record; + +/** Minimum portable agent gating context consumed by engine runtime wrappers. The legacy name is retained for API compatibility, but the context applies to permanent identity agents and ephemeral task-worker agents. */ +export interface PermanentAgentGatingContext { + permissionPolicy?: { + presetId: string; + rules: Partial>; + toolRules?: AgentPermissionPolicyToolRules; + }; + requester?: ApprovalRequestActorSnapshot; + taskId?: string; + runId?: string; + sessionId?: string; + createApprovalRequest?: (input: { + category: AgentPermissionPolicyActionCategory; + toolName: string; + args: Record; + /** + * FNXC:AgentGating 2026-07-05-00:00: + * FN-7609: the dedupe key must be persisted into the created request's + * targetAction.context so a retrying heartbeat's findPendingApprovalRequest + * lookup (which matches on context.approvalDedupeKey) can actually find and + * reuse the pending request instead of minting a new blank one every tick. + */ + approvalDedupeKey?: string; + }) => Promise; + findPendingApprovalRequest?: (dedupeKey: string) => Promise; +} + +/** Built-in permission policy preset identifiers for agent runtime policies. */ +export const AGENT_PERMISSION_POLICY_PRESET_IDS = ["unrestricted", "approval-required", "locked-down", "custom"] as const; + +/** A single built-in permission policy preset identifier. */ +export type AgentPermissionPolicyPresetId = (typeof AGENT_PERMISSION_POLICY_PRESET_IDS)[number]; + +/** Canonical category->disposition map for a permission policy. */ +export type AgentPermissionPolicyRules = Record< + AgentPermissionPolicyActionCategory, + AgentPermissionPolicyDisposition +>; + +/** + * First-class persisted permission policy contract for permanent and ephemeral agents. + * + * FNXC:ToolPermissions 2026-07-01-00:00: + * Operators must be able to block a single governed tool such as `fn_task_create` without blocking every task-agent mutation. `toolRules` stores exact tool-name overrides and the engine resolves them before category rules while leaving heartbeat-critical exempt tools non-configurable. + */ +export interface AgentPermissionPolicy { + presetId: AgentPermissionPolicyPresetId; + rules: AgentPermissionPolicyRules; + toolRules?: AgentPermissionPolicyToolRules; +} + +/** Approval request lifecycle statuses. */ +export const APPROVAL_REQUEST_STATUSES = ["pending", "approved", "denied", "completed"] as const; + +/** A single approval request lifecycle status. */ +export type ApprovalRequestStatus = (typeof APPROVAL_REQUEST_STATUSES)[number]; + +/** Append-only audit event types for approval requests. */ +export const APPROVAL_REQUEST_AUDIT_EVENT_TYPES = [ + "created", + "approved", + "denied", + "completed", +] as const; + +/** A single append-only audit event type for approval requests. */ +export type ApprovalRequestAuditEventType = (typeof APPROVAL_REQUEST_AUDIT_EVENT_TYPES)[number]; + +/** Immutable actor identity snapshot captured at request/audit event time. */ +export interface ApprovalRequestActorSnapshot { + actorId: string; + actorType: "agent" | "user" | "system"; + actorName: string; +} + +/** Legacy action-category aliases accepted for backward compatibility. */ +export const LEGACY_AGENT_PERMISSION_POLICY_ACTION_CATEGORY_ALIASES = [ + "file_write", + "file_delete", + "command_execute", + "network_access", + "task_mutation", + "agent_mutation", +] as const; + +export type LegacyAgentPermissionPolicyActionCategory = + (typeof LEGACY_AGENT_PERMISSION_POLICY_ACTION_CATEGORY_ALIASES)[number]; + +/** Canonical + compatibility action-category input accepted at boundaries. */ +export type ApprovalRequestActionCategoryInput = + | ApprovalRequestActionCategory + | LegacyAgentPermissionPolicyActionCategory; + +/** Normalize legacy action-category aliases to canonical v1 categories. */ +export function normalizeApprovalRequestActionCategory( + category: ApprovalRequestActionCategoryInput, +): ApprovalRequestActionCategory { + switch (category) { + case "file_write": + case "file_delete": + return "file_write_delete"; + case "command_execute": + return "command_execution"; + case "network_access": + return "network_api"; + case "task_mutation": + case "agent_mutation": + return "task_agent_mutation"; + case "agent_provisioning": + return "agent_provisioning"; + case "sandbox_provisioning": + return "sandbox_provisioning"; + case "secrets_access": + return "secrets_access"; + default: + return category; + } +} + +/** Action payload gated by an approval request. */ +export interface ApprovalRequestTargetAction { + category: ApprovalRequestActionCategory; + action: string; + summary: string; + resourceType: string; + resourceId: string; + context?: Record; +} + +/** Append-only audit event row for approval request history. */ +export interface ApprovalRequestAuditEvent { + id: string; + requestId: string; + eventType: ApprovalRequestAuditEventType; + actor: ApprovalRequestActorSnapshot; + note?: string; + createdAt: string; +} + +/** Durable approval request record used by engine and dashboard surfaces. */ +export interface ApprovalRequest { + id: string; + status: ApprovalRequestStatus; + requester: ApprovalRequestActorSnapshot; + targetAction: ApprovalRequestTargetAction; + taskId?: string; + runId?: string; + requestedAt: string; + decidedAt?: string; + completedAt?: string; + createdAt: string; + updatedAt: string; +} + +/** Create input for a new pending approval request. */ +export interface ApprovalRequestCreateInput { + requester: ApprovalRequestActorSnapshot; + targetAction: Omit & { + category: ApprovalRequestActionCategoryInput; + }; + taskId?: string; + runId?: string; +} + +/** Input for pending->approved / pending->denied decisions. */ +export interface ApprovalRequestDecisionInput { + actor: ApprovalRequestActorSnapshot; + note?: string; +} + +/** Input for approved->completed transition. */ +export interface ApprovalRequestCompletionInput { + actor: ApprovalRequestActorSnapshot; + note?: string; +} + +/** Query filters for approval request listings. */ +export interface ApprovalRequestListInput { + status?: ApprovalRequestStatus; + requesterActorId?: string; + taskId?: string; + runId?: string; + limit?: number; + offset?: number; +} + +/** True when a transition is valid for approval request lifecycle rules. */ +export function isValidApprovalRequestTransition( + from: ApprovalRequestStatus, + to: ApprovalRequestStatus, +): boolean { + if (from === to) { + return true; + } + if (from === "pending") { + return to === "approved" || to === "denied"; + } + if (from === "approved") { + return to === "completed"; + } + return false; +} + +/** Describes how an agent's task assignment capability was determined. */ +export type TaskAssignSource = + | "role_default" // Granted automatically by role (e.g., scheduler gets tasks:assign) + | "explicit_grant" // Explicitly granted via permissions field + | "denied"; // Not granted by any source + +/** Computed access state for an agent, derived from its role and permissions. */ +export interface AgentAccessState { + /** The agent ID this access state belongs to. */ + agentId: string; + /** Whether this agent can assign tasks to other agents. */ + canAssignTasks: boolean; + /** How the tasks:assign permission was determined. */ + taskAssignSource: TaskAssignSource; + /** Whether this agent can create new agents. */ + canCreateAgents: boolean; + /** Whether this agent can execute tasks. */ + canExecuteTasks: boolean; + /** Whether this agent can review task output. */ + canReviewTasks: boolean; + /** Whether this agent can merge task branches. */ + canMergeTasks: boolean; + /** Whether this agent can delete agents. */ + canDeleteAgents: boolean; + /** Whether this agent can manage missions. */ + canManageMissions: boolean; + /** Whether this agent can send messages. */ + canSendMessages: boolean; + /** Full set of resolved permissions (union of role defaults + explicit grants). */ + resolvedPermissions: Set; + /** Permissions explicitly granted on this agent (from the permissions field). */ + explicitPermissions: Set; + /** Permissions granted by role default (not explicitly set). */ + roleDefaultPermissions: Set; +} + +/** Agent record stored in the system */ +export interface Agent { + /** Unique identifier (e.g., "agent-001") */ + id: string; + /** Display name */ + name: string; + /** Role/capability of the agent */ + role: AgentCapability; + /** Current lifecycle state */ + state: AgentState; + /** ID of the task this agent is currently working on (if any) */ + taskId?: string; + /** ISO-8601 timestamp when the agent was created */ + createdAt: string; + /** ISO-8601 timestamp of last update */ + updatedAt: string; + /** ISO-8601 timestamp of last successful heartbeat */ + lastHeartbeatAt?: string; + /** Optional metadata */ + metadata: Record; + /** Job title / description for the agent */ + title?: string; + /** Custom icon identifier */ + icon?: string; + /** Uploaded avatar image URL */ + imageUrl?: string; + /** Agent ID this agent reports to (org hierarchy) */ + reportsTo?: string; + /** Runtime configuration. Supports: AgentHeartbeatConfig keys (heartbeatIntervalMs, heartbeatTimeoutMs, maxConcurrentRuns) */ + runtimeConfig?: Record; + /** Why the agent was paused (error, manual, etc.) */ + pauseReason?: string; + /** Capability permission flags */ + permissions?: Record; + /** Runtime action gating policy (preset + normalized category rules). */ + permissionPolicy?: AgentPermissionPolicy; + /** Cumulative input tokens across all runs */ + totalInputTokens?: number; + /** Cumulative output tokens across all runs */ + totalOutputTokens?: number; + /** Last error message */ + lastError?: string; + /** Number of currently pending approvals requested by this agent. */ + pendingApprovalCount?: number; + /** + * FNXC:AgentTaskStateDrift 2026-06-27-16:20: + * Dashboard/API responses need a transient linked-task column so coordinators can distinguish legitimate parked/active agent linkages from execution drift; unresolved lookups use the response-only "unresolved" sentinel. This is resolved per request and must not be persisted by AgentStore. + */ + taskColumn?: string; + /** Path to a markdown file containing custom instructions (resolved relative to project root). + * Must end in `.md`, no `..` traversal. Max 500 chars. */ + instructionsPath?: string; + /** Inline custom instructions appended to the agent's system prompt at execution time. Max 50,000 chars. */ + instructionsText?: string; + /** Agent personality/identity description — defines the agent's character, tone, and behavioral traits. Max 10,000 chars. */ + soul?: string; + /** Per-agent accumulated knowledge — stores learnings, preferences, and context the agent has gathered. Max 50,000 chars. */ + memory?: string; + /** Structured instruction bundle configuration for managed/external markdown files. */ + bundleConfig?: InstructionsBundleConfig; + /** Optional path to a markdown file containing this agent's per-tick heartbeat procedure + * (overrides the default HEARTBEAT_PROCEDURE constant). Resolved relative to project root. + * Must end in `.md`, no `..` traversal. Max 500 chars. */ + heartbeatProcedurePath?: string; +} + +/** Recursive node in the agent org tree. */ +export interface OrgTreeNode { + agent: Agent; + children: OrgTreeNode[]; +} + +export type MessageResponseMode = "immediate" | "on-heartbeat"; + +/** Per-agent heartbeat configuration, stored in agent.runtimeConfig */ +export interface AgentHeartbeatConfig { + /** Whether heartbeat triggers are enabled for this agent (default: true) */ + enabled?: boolean; + /** Whether this agent should auto-claim relevant unowned tasks during no-task heartbeats (default: true when unset). */ + autoClaimRelevantTasks?: boolean; + /** + * FNXC:AgentRouting 2026-07-12-11:20: + * Per-agent task-routing eligibility (GitHub issue Runfusion/Fusion#2015). "auto" (default) = current behavior; + * "explicit-only" = never auto-assigned/auto-claimed but accepts explicit delegation; "none" = never bound to + * implementation tasks by ANY path, including delegation with override=true. Set "none" on liaison/observer agents. + */ + assignmentPolicy?: "auto" | "explicit-only" | "none"; + /** Number of auto-claim candidates to inject into no-task heartbeat prompts. Default: 5, range: 0-10. */ + autoClaimCandidatesInPrompt?: number; + /** Per-agent override for opting engineer-role agents into no-task backlog auto-claim. Default: project setting or false. */ + engineerBacklogAutoClaim?: boolean; + /** Polling interval in ms (default: 30000). Min: 1000 */ + heartbeatIntervalMs?: number; + /** Heartbeat timeout in ms (default: 60000). Min: 5000 */ + heartbeatTimeoutMs?: number; + /** Max concurrent heartbeat runs per agent (default: 1). Min: 1 */ + maxConcurrentRuns?: number; + /** Whether periodic self-improvement is enabled (default: true) */ + selfImproveEnabled?: boolean; + /** Interval between self-improvement cycles in ms (default: 14400000 = 4h). Min: 3600000 (1h) */ + selfImproveIntervalMs?: number; + /** ISO timestamp of last self-improvement run */ + lastSelfImproveAt?: string; + /** + * How this agent responds to incoming messages. + * "immediate" triggers a heartbeat run when a message arrives. + * "on-heartbeat" defers message handling to the next scheduled heartbeat (default). + */ + messageResponseMode?: MessageResponseMode; + /** Per-agent budget governance configuration. When set, enables budget tracking and enforcement. */ + budgetConfig?: AgentBudgetConfig; + /** Per-agent override for memory prompt inclusion mode. */ + agentMemoryInclusionMode?: "full" | "index" | "off"; + /** Per-agent override for heartbeat scope-discipline procedure mode. */ + heartbeatScopeDiscipline?: "strict" | "lite" | "off"; + /** Per-agent override for heartbeat execution prompt template mode. */ + heartbeatPromptTemplate?: "default" | "compact"; + /** Last resolved memory inclusion mode recorded by engine for transition logging. */ + lastAgentMemoryInclusionMode?: "full" | "index" | "off"; + /** + * When true, the engine fires a catch-up heartbeat at server startup if the + * agent's last heartbeat is older than its interval — i.e., the server was + * down across a scheduled tick. Default: false. + */ + runMissedHeartbeatOnStartup?: boolean; + /** + * When true (default), an agent's heartbeat runs and its task execution session can run + * concurrently. When false, the two paths serialize: a heartbeat will not start while the + * agent's bound task has an active executor session, and an executor session will not start + * while the agent has an active heartbeat run. + * + * Permanent agents only — ignored for ephemeral agents. Default: true when unset. + */ + allowParallelExecution?: boolean; + /** + * When true, timer-triggered heartbeats are skipped while the agent has no currently assigned + * task (`agent.taskId` is unset). Assignment and on-demand triggers are unaffected. + * Default: false (timer fires regardless of assignment). + */ + skipHeartbeatWhenIdle?: boolean; +} + +/** Per-agent budget configuration, stored in agent.runtimeConfig.budgetConfig */ +export interface AgentBudgetConfig { + /** Total token cap (input + output). When undefined, no budget limit is enforced. */ + tokenBudget?: number; + /** Warning threshold as a fraction (0–1). Default: 0.8. Triggers isOverThreshold when usagePercent >= this value * 100. */ + usageThreshold?: number; + /** Budget accumulation period. Default: "lifetime". */ + budgetPeriod?: "daily" | "weekly" | "monthly" | "lifetime"; + /** Day of month/week for period reset (1–31 for monthly, 0–6 for weekly where 0=Sunday). Only used when budgetPeriod is "monthly" or "weekly". */ + resetDay?: number; +} + +/** Computed budget status for an agent at a point in time. */ +export interface AgentBudgetStatus { + /** The agent this status belongs to */ + agentId: string; + /** Total tokens consumed (input + output) */ + currentUsage: number; + /** Token cap from config, or null when no budget is configured */ + budgetLimit: number | null; + /** Usage as a percentage of budget (0–100), or null when no budget */ + usagePercent: number | null; + /** The configured threshold fraction (e.g., 0.8), or null when no budget */ + thresholdPercent: number | null; + /** Whether currentUsage >= budgetLimit */ + isOverBudget: boolean; + /** Whether usagePercent >= thresholdPercent * 100 */ + isOverThreshold: boolean; + /** ISO-8601 timestamp of the last budget reset, or null */ + lastResetAt: string | null; + /** ISO-8601 timestamp of the next scheduled reset, or null for lifetime/no budget */ + nextResetAt: string | null; +} + +/** Configuration for an agent's instruction bundle — a collection of markdown files + * that together form the agent's custom instructions. */ +export interface InstructionsBundleConfig { + /** Bundle mode — "managed" = system-managed directory, "external" = user-specified path */ + mode: "managed" | "external"; + /** Primary instructions file name (default: "AGENTS.md") */ + entryFile: string; + /** List of all file names in the bundle directory */ + files: string[]; + /** User-specified directory path for external mode (required when mode is "external") */ + externalPath?: string; +} + +/** Extended agent information including heartbeat history */ +export interface AgentDetail extends Agent { + /** Recent heartbeat events (last N events) */ + heartbeatHistory: AgentHeartbeatEvent[]; + /** Current active heartbeat run (if any) */ + activeRun?: AgentHeartbeatRun; + /** All completed runs for this agent */ + completedRuns: AgentHeartbeatRun[]; +} + +/** Input for creating a new agent */ +export interface AgentCreateInput { + name: string; + role: AgentCapability; + metadata?: Record; + title?: string; + icon?: string; + imageUrl?: string; + reportsTo?: string; + runtimeConfig?: Record; + permissions?: Record; + permissionPolicy?: AgentPermissionPolicy; + instructionsPath?: string; + instructionsText?: string; + soul?: string; + memory?: string; + bundleConfig?: InstructionsBundleConfig; + heartbeatProcedurePath?: string; +} + +/** Input for updating an existing agent */ +export interface AgentUpdateInput { + name?: string; + role?: AgentCapability; + metadata?: Record; + title?: string; + icon?: string; + imageUrl?: string; + reportsTo?: string; + runtimeConfig?: Record; + pauseReason?: string; + permissions?: Record; + permissionPolicy?: AgentPermissionPolicy; + lastError?: string; + totalInputTokens?: number; + totalOutputTokens?: number; + instructionsPath?: string; + instructionsText?: string; + soul?: string; + memory?: string; + bundleConfig?: InstructionsBundleConfig; + heartbeatProcedurePath?: string; +} + +/** An API key associated with an agent for bearer token authentication. */ +export interface AgentApiKey { + /** Unique key identifier (e.g., "key-a1b2c3d4") */ + id: string; + /** The agent this key belongs to */ + agentId: string; + /** SHA-256 hash of the plaintext token (hex-encoded, 64 chars) */ + tokenHash: string; + /** Optional human-readable label for the key */ + label?: string; + /** ISO-8601 timestamp when the key was created */ + createdAt: string; + /** ISO-8601 timestamp when the key was revoked, null if active */ + revokedAt?: string; +} + +/** Result returned when creating a new API key — includes the plaintext token exactly once. */ +export interface AgentApiKeyCreateResult { + /** The persisted key metadata (不含 plaintext token) */ + key: AgentApiKey; + /** The plaintext token — shown only at creation, never stored */ + token: string; +} + +/** Per-task session persistence for an agent */ +export interface AgentTaskSession { + /** Agent ID */ + agentId: string; + /** Task ID */ + taskId: string; + /** Session state for resuming context across runs */ + sessionParams: Record; + /** Human-readable session identifier */ + sessionDisplayId?: string; + /** ISO-8601 timestamp when session was created */ + createdAt: string; + /** ISO-8601 timestamp of last update */ + updatedAt: string; +} + +/** A single performance rating for an agent */ +export interface AgentRating { + id: string; + agentId: string; + raterType: "user" | "agent" | "system"; + raterId?: string; + score: number; + category?: string; + comment?: string; + runId?: string; + taskId?: string; + createdAt: string; +} + +/** Aggregated rating statistics for an agent */ +export interface AgentRatingSummary { + agentId: string; + averageScore: number; + totalRatings: number; + categoryAverages: Record; + recentRatings: AgentRating[]; + trend: "improving" | "declining" | "stable" | "insufficient-data"; +} + +/** Input payload for creating an agent rating */ +export interface AgentRatingInput { + raterType: "user" | "agent" | "system"; + raterId?: string; + score: number; + category?: string; + comment?: string; + runId?: string; + taskId?: string; +} + +/** Trackable configuration fields for revision history. + * Excludes budget-related items, state, taskId, token counts, and timestamps. */ +export interface AgentConfigSnapshot { + name: string; + role: AgentCapability; + title?: string; + icon?: string; + imageUrl?: string; + reportsTo?: string; + runtimeConfig?: Record; + permissions?: Record; + permissionPolicy?: AgentPermissionPolicy; + instructionsPath?: string; + instructionsText?: string; + soul?: string; + memory?: string; + bundleConfig?: InstructionsBundleConfig; + heartbeatProcedurePath?: string; + metadata: Record; +} + +/** A single key-value change within a config revision */ +export interface RevisionFieldDiff { + field: string; + oldValue: unknown; + newValue: unknown; +} + +/* +FNXC:ConfigVersioning 2026-07-18-00:00: +FN-8282 requires every durable configuration mutation to retain an immutable +before/after snapshot. Targets stay structured JSON; target keys are derived +from canonical JSON rather than delimiter-concatenated identifiers. + +FNXC:ConfigVersioning 2026-07-18-10:30: +Every provenance variant carries a stable ID. This makes agent and authenticated +human writes auditable and makes intentional internal writes explicit as the +system actor instead of allowing anonymous history rows. +*/ +export type ConfigKind = "project-settings" | "global-settings" | "workflow-settings" | "routine" | "automation"; +export type ConfigChangedBy = + | { kind: "human"; id: string } + | { kind: "agent"; id: string } + | { kind: "system"; id: string } + | { kind: "rollback"; id: string }; +export type ConfigurationOwnerScope = "project" | "global"; +export type ConfigurationTarget = Readonly>; +export interface ConfigurationRevision { + id: string; + projectId: string; + ownerScope: ConfigurationOwnerScope; + configKind: ConfigKind; + configTarget: ConfigurationTarget; + /** Canonical JSON representation used only for exact target indexing. */ + configTargetKey: string; + before: unknown; + after: unknown; + diffs: RevisionFieldDiff[]; + changedBy: ConfigChangedBy; + createdAt: string; + source: "mutation" | "rollback"; + rollbackToRevisionId?: string; +} + +/** A revision entry recording a configuration change to an agent */ +export interface AgentConfigRevision { + /** Unique revision identifier */ + id: string; + /** Agent ID this revision belongs to */ + agentId: string; + /** ISO-8601 timestamp when the revision was created */ + createdAt: string; + /** Snapshot of config BEFORE the change */ + before: AgentConfigSnapshot; + /** Snapshot of config AFTER the change */ + after: AgentConfigSnapshot; + /** Field-level diffs between before and after */ + diffs: RevisionFieldDiff[]; + /** Description of what changed (e.g., "Updated runtimeConfig, name") */ + summary: string; + /** Who or what triggered the change */ + source: "user" | "system" | "rollback"; + /** If this was a rollback, the revision ID that was restored */ + rollbackToRevisionId?: string; +} + +/** + * Legacy project-relative shared path for the heartbeat procedure markdown + * file. Older builds defaulted every non-ephemeral agent to this single + * file, which prevented per-agent customization. New code should use + * {@link getDefaultHeartbeatProcedurePath} instead. This constant is kept + * exported only so migrations can detect agents still pointing at the + * shared path and re-route them to their own per-agent file. + * + * @deprecated Use {@link getDefaultHeartbeatProcedurePath} for new agent + * creation and upgrade flows. + */ +export const DEFAULT_HEARTBEAT_PROCEDURE_PATH = ".fusion/HEARTBEAT.md"; + +function slugifyAgentAssetSegment(value: string): string { + return value + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, ""); +} + +export function getSafeAgentAssetIdSegment(agentId: string): string { + const slug = slugifyAgentAssetSegment(agentId); + return slug || "agent"; +} + +/** + * Compute the canonical per-agent asset directory segment. + * + * Canonical format: `-`. + * Example: `CEO` + `agent2736` => `ceo-agent2736`. + * + * If the display-name slug is empty (for example name has only symbols), the + * id-derived segment is used as the directory prefix so the result is always + * filesystem-safe and non-empty. + */ +export function getCanonicalAgentAssetDirectoryName(agentName: string, agentId: string): string { + if (!agentId || typeof agentId !== "string") { + throw new Error("getCanonicalAgentAssetDirectoryName requires a non-empty agentId"); + } + const safeId = getSafeAgentAssetIdSegment(agentId); + const nameSlug = slugifyAgentAssetSegment(agentName ?? ""); + const prefix = nameSlug || safeId; + return `${prefix}-${safeId}`; +} + +/** Legacy per-agent asset directory segment used by older builds. */ +export function getLegacyAgentAssetDirectoryName(agentId: string): string { + if (!agentId || typeof agentId !== "string") { + throw new Error("getLegacyAgentAssetDirectoryName requires a non-empty agentId"); + } + return agentId; +} + +/** Canonical managed instruction bundle directory name for an agent. */ +export function getCanonicalAgentInstructionsBundleDirName(agentName: string, agentId: string): string { + return `${getCanonicalAgentAssetDirectoryName(agentName, agentId)}-instructions`; +} + +/** Legacy managed instruction bundle directory name used by older builds. */ +export function getLegacyAgentInstructionsBundleDirName(agentId: string): string { + return `${getLegacyAgentAssetDirectoryName(agentId)}-instructions`; +} + +/** + * Compute the project-relative default heartbeat procedure file path for a + * given agent. Each agent gets their own editable HEARTBEAT.md so operators + * can tune the per-tick procedure without changes leaking across the team. + * + * The path is laid out under `.fusion/agents//HEARTBEAT.md`. + */ +export function getDefaultHeartbeatProcedurePath(agentId: string, agentName?: string): string { + if (!agentId || typeof agentId !== "string") { + throw new Error("getDefaultHeartbeatProcedurePath requires a non-empty agentId"); + } + const directory = agentName + ? getCanonicalAgentAssetDirectoryName(agentName, agentId) + : getLegacyAgentAssetDirectoryName(agentId); + return `.fusion/agents/${directory}/HEARTBEAT.md`; +} + +/** Extract trackable config fields from an Agent into a snapshot */ +export function agentToConfigSnapshot(agent: Agent): AgentConfigSnapshot { + return { + name: agent.name, + role: agent.role, + title: agent.title, + icon: agent.icon, + imageUrl: agent.imageUrl, + reportsTo: agent.reportsTo, + runtimeConfig: agent.runtimeConfig ? { ...agent.runtimeConfig } : undefined, + permissions: agent.permissions ? { ...agent.permissions } : undefined, + permissionPolicy: agent.permissionPolicy + ? { + presetId: agent.permissionPolicy.presetId, + rules: { ...agent.permissionPolicy.rules }, + ...(agent.permissionPolicy.toolRules ? { toolRules: { ...agent.permissionPolicy.toolRules } } : {}), + } + : undefined, + instructionsPath: agent.instructionsPath, + instructionsText: agent.instructionsText, + soul: agent.soul, + memory: agent.memory, + bundleConfig: agent.bundleConfig + ? { + ...agent.bundleConfig, + files: [...agent.bundleConfig.files], + } + : undefined, + heartbeatProcedurePath: agent.heartbeatProcedurePath, + metadata: { ...agent.metadata }, + }; +} + +/** Compare two config snapshots and return field-level diffs */ +export function diffConfigSnapshots( + before: AgentConfigSnapshot, + after: AgentConfigSnapshot, +): RevisionFieldDiff[] { + const trackedFields: Array = [ + "name", + "role", + "title", + "icon", + "imageUrl", + "reportsTo", + "runtimeConfig", + "permissions", + "permissionPolicy", + "instructionsPath", + "instructionsText", + "soul", + "memory", + "bundleConfig", + "heartbeatProcedurePath", + "metadata", + ]; + + const diffs: RevisionFieldDiff[] = []; + + for (const field of trackedFields) { + const oldVal = before[field]; + const newVal = after[field]; + + if (JSON.stringify(oldVal) !== JSON.stringify(newVal)) { + diffs.push({ field, oldValue: oldVal, newValue: newVal }); + } + } + + return diffs; +} + +/** Aggregate statistics for agents */ +export interface AgentStats { + /** Number of agents in active/running state */ + activeCount: number; + /** Number of tasks assigned to agents */ + assignedTaskCount: number; + /** Total completed runs */ + completedRuns: number; + /** Total failed runs */ + failedRuns: number; + /** Success rate (0-1) */ + successRate: number; + /** Number of idle non-ephemeral agents available for queue drain */ + idleNonEphemeralCount: number; + /** Number of tasks currently in the todo column */ + todoTaskCount: number; +} + +/** Trigger source for an agent self-reflection run */ +export type ReflectionTrigger = "periodic" | "post-task" | "manual" | "user-requested"; + +/** + * FNXC:AgentReflection 2026-07-04-00:00: + * FN-7528 adds a deterministic, non-LLM post-task performance capture that runs on every + * completed task (guarded by settings.reflectionEnabled), distinct from the LLM-backed + * generateReflection path. These extra fields are a compact structured snapshot — duration + * drivers, packages/files touched, verification command(s)/scope, and retry/rework count. + * All fields are optional (backward-compatible with existing JSONL records) and outcome-only: + * no free-form prose, prompt text, or reflection narrative is ever stored here or emitted to + * run-audit (FN-7158 ids/counts/outcomes-only contract). Omit a field rather than fabricate it + * when its source data is unavailable. + */ +export interface ReflectionMetrics { + /** Tasks completed in the analysis window */ + tasksCompleted?: number; + /** Tasks failed in the analysis window */ + tasksFailed?: number; + /** Average task duration in milliseconds */ + avgDurationMs?: number; + /** Total tokens consumed in the analysis window */ + totalTokensUsed?: number; + /** Number of errors encountered */ + errorCount?: number; + /** Recurring error patterns */ + commonErrors?: string[]; + /** Single task's wall-clock duration in milliseconds (distinct from the aggregate avgDurationMs) */ + durationMs?: number; + /** Short deterministic labels describing what drove the duration (e.g. "retries:2", "rework:1", "verification-broad") — never free-form prose */ + durationDrivers?: string[]; + /** Package names derived from touched file paths (e.g. "@fusion/core" or "packages/core") */ + packagesTouched?: string[]; + /** Count of files touched, when available */ + filesTouchedCount?: number; + /** Verification command(s) recorded for the task */ + verificationCommands?: string[]; + /** reworkCount + retry/recovery count */ + retryReworkCount?: number; + /** True when verification was file-scoped, false when broader/full-suite */ + verificationFileScoped?: boolean; + /** Short reason label when verification scope was broader (e.g. "whole-package test script has no file-scoped filter"); omitted when file-scoped */ + verificationScopeReason?: string; +} + +/** A persisted self-reflection generated by an agent */ +export interface AgentReflection { + /** Unique reflection ID */ + id: string; + /** The agent this reflection belongs to */ + agentId: string; + /** ISO-8601 timestamp when the reflection was created */ + timestamp: string; + /** What caused this reflection */ + trigger: ReflectionTrigger; + /** Optional trigger detail context */ + triggerDetail?: string; + /** Associated task ID (for post-task reflections) */ + taskId?: string; + /** Quantitative reflection metrics */ + metrics: ReflectionMetrics; + /** Key observations from self-analysis */ + insights: string[]; + /** Suggested improvements for future runs */ + suggestedImprovements: string[]; + /** One-paragraph narrative summary */ + summary: string; +} + +/** Aggregated performance summary derived from recent reflections */ +export interface AgentPerformanceSummary { + /** Agent identifier */ + agentId: string; + /** Total tasks completed in the analysis window */ + totalTasksCompleted: number; + /** Total tasks failed in the analysis window */ + totalTasksFailed: number; + /** Average task duration in milliseconds */ + avgDurationMs: number; + /** Success ratio from 0 to 1 */ + successRate: number; + /** Top recurring errors */ + commonErrors: string[]; + /** Derived strengths from successful patterns */ + strengths: string[]; + /** Derived weaknesses from failure patterns */ + weaknesses: string[]; + /** Number of reflections considered in this summary */ + recentReflectionCount: number; + /** ISO-8601 timestamp when summary was computed */ + computedAt: string; +} + diff --git a/packages/dashboard/app/api/insights.ts b/packages/dashboard/app/api/insights.ts new file mode 100644 index 0000000000..3f8f60f337 --- /dev/null +++ b/packages/dashboard/app/api/insights.ts @@ -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 { + 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(withProjectId(`/insights${suffix}`, projectId)); +} + +/** + * Get a single insight by ID. + */ +export function fetchInsight(id: string, projectId?: string): Promise { + return api(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 { + return api(withProjectId(`/insights/${encodeURIComponent(id)}`, projectId), { + method: "PATCH", + body: JSON.stringify(updates), + }); +} + +/** + * Delete an insight. + */ +export function deleteInsight(id: string, projectId?: string): Promise { + return api(withProjectId(`/insights/${encodeURIComponent(id)}`, projectId), { + method: "DELETE", + }); +} + +/** + * Dismiss an insight (set status to dismissed). + */ +export function dismissInsight(id: string, projectId?: string): Promise { + return api(withProjectId(`/insights/${encodeURIComponent(id)}/dismiss`, projectId), { + method: "POST", + }); +} + +/** + * Archive an insight (set status to archived). + */ +export function archiveInsight(id: string, projectId?: string): Promise { + return api(withProjectId(`/insights/${encodeURIComponent(id)}/archive`, projectId), { + method: "POST", + }); +} + +/** + * Unarchive an insight (set status back to confirmed). + */ +export function unarchiveInsight(id: string, projectId?: string): Promise { + return api(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 { + const body: Record = { trigger, inputMetadata }; + if (modelProvider) body.modelProvider = modelProvider; + if (modelId) body.modelId = modelId; + if (thinkingLevel) body.thinkingLevel = thinkingLevel; + return api(withProjectId("/insights/run", projectId), { + method: "POST", + body: JSON.stringify(body), + }); +} + +/** + * List insight generation runs. + */ +export function fetchInsightRuns(projectId?: string): Promise { + return api(withProjectId("/insights/runs", projectId)); +} + +/** + * Get a single insight run by ID. + */ +export function fetchInsightRun(id: string, projectId?: string): Promise { + return api(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", + }); +} + diff --git a/packages/dashboard/app/api/legacy.ts b/packages/dashboard/app/api/legacy.ts index 1fb7817bf2..a0de5fe0f4 100644 --- a/packages/dashboard/app/api/legacy.ts +++ b/packages/dashboard/app/api/legacy.ts @@ -11,46 +11,17 @@ import type { ActivityEventType, WorkflowStep, WorkflowStepResult, - PluginInstallation, - PluginSetupCheckResult, - PluginState, - PluginUiSlotDefinition, - PluginUiContributionDefinition, - PluginDashboardViewDefinition, - - Message, - MessageMetadata, - MessageType, - ParticipantType, NodeConfig, NodeStatus, MeshClusterSnapshot, SystemMetrics, DiscoveryConfig, - MissionEvent, - MissionHealth, - MissionEventType, - AgentRating, - AgentRatingSummary, - AgentRatingInput, ChatAttachment, ChatMessage, ChatRoom, ChatRoomMember, ChatRoomMessage, EnrichedChatSession, - TodoList, - TodoItem, - TodoListWithItems, - TodoListCreateInput, - TodoListUpdateInput, - TodoItemCreateInput, - TodoItemUpdateInput, - Insight, - InsightCategory, - InsightStatus, - InsightRun, - InsightRunTrigger, EvalRun, EvalTaskResult, ResearchRunStatus, @@ -65,14 +36,13 @@ import type { DockerExtraCli, DockerNodeStatus, ProjectNodePathMapping, - ApprovalRequestStatus, CommitAssociationDiffBackfillReport, } from "@fusion/core"; // Consumers import backfill report types from the legacy API barrel. export type { CommitAssociationDiffBackfillReport }; import type { PlanningQuestion, PlanningSummary } from "@fusion/core"; import type { ScheduledTask, ScheduledTaskCreateInput, ScheduledTaskUpdateInput, AutomationRunResult, Routine, RoutineCreateInput, RoutineUpdateInput, RoutineExecutionResult } from "@fusion/core"; -import type { MilestoneValidationTelemetry, MissionInterviewDraftSummary } from "../components/mission-types"; +import type { MissionInterviewDraftSummary } from "../components/mission-types"; import type { ResearchAvailability, ResearchRunDetail, @@ -6819,660 +6789,95 @@ export function fetchTaskFileDiffs(taskId: string, projectId?: string): Promise< return api(withProjectId(`/tasks/${encodeURIComponent(taskId)}/file-diffs`, projectId)); } -// ── 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 { - return api(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 { - return api(withProjectId("/missions", projectId), { - method: "POST", - body: JSON.stringify(input), - }); -} - -/** Get mission with full hierarchy */ -export function fetchMission(missionId: string, projectId?: string): Promise { - return api(withProjectId(`/missions/${encodeURIComponent(missionId)}`, projectId)); -} - -/** Update mission */ -export function updateMission(missionId: string, updates: Partial & { autoMerge?: boolean | null }, projectId?: string): Promise { - return api(withProjectId(`/missions/${encodeURIComponent(missionId)}`, projectId), { - method: "PATCH", - body: JSON.stringify(updates), - }); -} - -/** Delete mission */ -export function deleteMission(missionId: string, projectId?: string): Promise { - return api(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 { - return api( - 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 { - return api( - 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 { - 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( - withProjectId(`/missions/${encodeURIComponent(missionId)}/events${suffix}`, projectId), - ); -} - -/** Fetch computed mission health metrics. */ -export function fetchMissionHealth(missionId: string, projectId?: string): Promise { - return api(withProjectId(`/missions/${encodeURIComponent(missionId)}/health`, projectId)); -} - -/** Fetch health metrics for all missions in a single batched request. */ -export function fetchMissionsHealth(projectId?: string): Promise> { - return api>(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 { - return api(withProjectId(`/missions/${encodeURIComponent(missionId)}/milestones`, projectId), { - method: "POST", - body: JSON.stringify(input), - }); -} - -/** Update milestone */ -export function updateMilestone(milestoneId: string, updates: Partial, projectId?: string): Promise { - return api(withProjectId(`/missions/milestones/${encodeURIComponent(milestoneId)}`, projectId), { - method: "PATCH", - body: JSON.stringify(updates), - }); -} - -/** Delete milestone */ -export function deleteMilestone(milestoneId: string, projectId?: string): Promise { - return api(withProjectId(`/missions/milestones/${encodeURIComponent(milestoneId)}`, projectId), { - method: "DELETE", - }); -} - -/** Reorder milestones */ -export function reorderMilestones(missionId: string, orderedIds: string[], projectId?: string): Promise { - return api(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 { - return api(withProjectId(`/missions/milestones/${encodeURIComponent(milestoneId)}/slices`, projectId), { - method: "POST", - body: JSON.stringify(input), - }); -} - -/** Update slice */ -export function updateSlice(sliceId: string, updates: Partial, projectId?: string): Promise { - return api(withProjectId(`/missions/slices/${encodeURIComponent(sliceId)}`, projectId), { - method: "PATCH", - body: JSON.stringify(updates), - }); -} - -/** Delete slice */ -export function deleteSlice(sliceId: string, projectId?: string): Promise { - return api(withProjectId(`/missions/slices/${encodeURIComponent(sliceId)}`, projectId), { - method: "DELETE", - }); -} - -/** Activate slice */ -export function activateSlice(sliceId: string, projectId?: string): Promise { - return api(withProjectId(`/missions/slices/${encodeURIComponent(sliceId)}/activate`, projectId), { - method: "POST", - }); -} - -/** Reorder slices */ -export function reorderSlices(milestoneId: string, orderedIds: string[], projectId?: string): Promise { - return api(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 { - return api(withProjectId(`/missions/slices/${encodeURIComponent(sliceId)}/features`, projectId), { - method: "POST", - body: JSON.stringify(input), - }); -} - -/** Update feature */ -export function updateFeature(featureId: string, updates: Partial, projectId?: string): Promise { - return api(withProjectId(`/missions/features/${encodeURIComponent(featureId)}`, projectId), { - method: "PATCH", - body: JSON.stringify(updates), - }); -} - -/** Delete feature */ -export function deleteFeature(featureId: string, projectId?: string): Promise { - return api(withProjectId(`/missions/features/${encodeURIComponent(featureId)}`, projectId), { - method: "DELETE", - }); -} - -/** Link feature to task */ -export function linkFeatureToTask(featureId: string, taskId: string, projectId?: string): Promise { - return api(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 { - return api(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 { - return api(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 { - return api(withProjectId(`/missions/milestones/${encodeURIComponent(milestoneId)}/assertions`, projectId)); -} - -/** Create a new assertion for a milestone */ -export function createAssertion(milestoneId: string, input: ContractAssertionCreateInput, projectId?: string): Promise { - return api(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 { - return api(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 { - return api(withProjectId(`/missions/assertions/${encodeURIComponent(assertionId)}`, projectId)); -} - -/** Update an assertion */ -export function updateAssertion(assertionId: string, updates: ContractAssertionUpdateInput, projectId?: string): Promise { - return api(withProjectId(`/missions/assertions/${encodeURIComponent(assertionId)}`, projectId), { - method: "PATCH", - body: JSON.stringify(updates), - }); -} - -/** Delete an assertion */ -export function deleteAssertion(assertionId: string, projectId?: string): Promise { - return api(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 { - return api(withProjectId(`/missions/features/${encodeURIComponent(featureId)}/assertions`, projectId)); -} - -/** List features linked to an assertion */ -export function fetchFeaturesForAssertion(assertionId: string, projectId?: string): Promise { - return api(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 { - return api(withProjectId(`/missions/milestones/${encodeURIComponent(milestoneId)}/validation`, projectId)); -} - -/** Fetch grouped validation telemetry for a milestone */ -export function fetchMilestoneValidationTelemetry(milestoneId: string, projectId?: string): Promise { - return api(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 { - return api(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 { - 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(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 }> { - 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 { - return api(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 { - return api(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 { - return api(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 { - return api(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 { - return api(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 { - return api(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 { - return api(withProjectId(`/missions/${encodeURIComponent(missionId)}/autopilot/start`, projectId), { - method: "POST", - }); -} - -/** Manually stop autopilot watching for a mission */ -export function stopMissionAutopilot(missionId: string, projectId?: string): Promise { - return api(withProjectId(`/missions/${encodeURIComponent(missionId)}/autopilot/stop`, projectId), { - method: "POST", - }); -} +/* + * FNXC:CodeOrganization 2026-07-18-14:00: + * Preserve legacy `missions` imports while implementations live in missions.ts. + */ +export { + activateSlice, + backfillCommitAssociationDiffStats, + backfillMissionAssertions, + createAssertion, + createFeature, + createMilestone, + createMission, + createSlice, + deleteAssertion, + deleteFeature, + deleteMilestone, + deleteMission, + deleteSlice, + fetchAssertion, + fetchAssertions, + fetchAssertionsForFeature, + fetchFeaturesForAssertion, + fetchMilestoneValidation, + fetchMilestoneValidationTelemetry, + fetchMission, + fetchMissionAutopilotStatus, + fetchMissionEvents, + fetchMissionHealth, + fetchMissionStatus, + fetchMissions, + fetchMissionsHealth, + fetchValidationLoopState, + fetchValidationRun, + fetchValidationRuns, + linkFeatureToAssertion, + linkFeatureToTask, + pauseMission, + reorderAssertions, + reorderMilestones, + reorderSlices, + resumeMission, + startMission, + startMissionAutopilot, + stopMission, + stopMissionAutopilot, + triageAllSliceFeatures, + triageFeature, + triggerValidation, + unlinkFeatureFromAssertion, + unlinkFeatureFromTask, + updateAssertion, + updateFeature, + updateMilestone, + updateMission, + updateMissionAutopilot, + updateSlice, +} from "./missions.js"; +export type { + AutopilotState, + AutopilotStatus, + ContractAssertionCreateInput, + ContractAssertionUpdateInput, + FeatureStatus, + Milestone, + MilestoneStatus, + MilestoneValidationRollup, + MilestoneWithSlices, + Mission, + MissionAssertionBackfillErrorRow, + MissionAssertionBackfillRepairRow, + MissionAssertionBackfillReport, + MissionAssertionStatus, + MissionContractAssertion, + MissionEventQueryOptions, + MissionEventsResponse, + MissionFeature, + MissionFeatureLoopSnapshot, + MissionStatus, + MissionSummary, + MissionValidatorRun, + MissionWithHierarchy, + MissionWithSummary, + Slice, + SliceStatus, + SliceWithFeatures, + ValidationRunsResponse, +} from "./missions.js"; +// FNXC:CodeOrganization 2026-07-18-16:30: re-export does not bind mission types locally; interview helpers still type against them. +import type { Milestone, MissionWithHierarchy, Slice } from "./missions.js"; // ── Mission Interview API ───────────────────────────────────────────────── @@ -8050,74 +7455,20 @@ export async function previewEnrichedDescription( } } -// ── Todo API ───────────────────────────────────────────────────────────────── - -/** Fetch all todo lists with their items */ -export function fetchTodoLists(projectId?: string): Promise { - return api(withProjectId("/todos", projectId)); -} - -/** Create a new todo list */ -export function createTodoList(title: string, projectId?: string): Promise { - const input: TodoListCreateInput = { title }; - return api(withProjectId("/todos", projectId), { - method: "POST", - body: JSON.stringify(input), - }); -} - -/** Update a todo list title */ -export function updateTodoList(id: string, title: string, projectId?: string): Promise { - const updates: TodoListUpdateInput = { title }; - return api(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 { - return api(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 { - const input: TodoItemCreateInput = { text }; - return api(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 { - const updates: TodoItemUpdateInput = data; - return api(withProjectId(`/todos/items/${encodeURIComponent(id)}`, projectId), { - method: "PATCH", - body: JSON.stringify(updates), - }); -} - -/** Delete a todo item */ -export function deleteTodoItem(id: string, projectId?: string): Promise { - return api(withProjectId(`/todos/items/${encodeURIComponent(id)}`, projectId), { - method: "DELETE", - }); -} - -/** Reorder items within a todo list */ -export function reorderTodoItems(listId: string, itemIds: string[], projectId?: string): Promise { - return api(withProjectId(`/todos/${encodeURIComponent(listId)}/items/reorder`, projectId), { - method: "POST", - body: JSON.stringify({ itemIds }), - }); -} +/* + * FNXC:CodeOrganization 2026-07-18-14:00: + * Preserve legacy `todo` imports while implementations live in todo.ts. + */ +export { + createTodoItem, + createTodoList, + deleteTodoItem, + deleteTodoList, + fetchTodoLists, + reorderTodoItems, + updateTodoItem, + updateTodoList, +} from "./todo.js"; // ── AI Sessions ──────────────────────────────────────────────────────────── @@ -8310,566 +7661,88 @@ export function summarizePlanningDraftTitle( ); } -// ── 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; - }; - 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 { - 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(`/messages/inbox${query}`); -} - -/** Fetch sent messages for the current user. */ -export function fetchOutbox( - options?: { limit?: number; offset?: number; type?: MessageType }, - projectId?: string, -): Promise { - 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(`/messages/outbox${query}`); -} - -/** Fetch unread message count (lightweight, for header badge). */ -export function fetchUnreadCount(projectId?: string): Promise { - return api(withProjectId("/messages/unread-count", projectId)); -} - -/** Fetch a single message by ID. */ -export function fetchMessage(id: string, projectId?: string): Promise { - return api(withProjectId(`/messages/${encodeURIComponent(id)}`, projectId)); -} - -/** Send a new message. */ -export function sendMessage(input: SendMessageInput, projectId?: string): Promise { - return api(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 { - return api(withProjectId(`/messages/${encodeURIComponent(id)}/read`, projectId), { - method: "POST", - }); -} - -/** Mark all inbox messages as read. */ -export function markAllMessagesRead(projectId?: string): Promise { - return api(withProjectId("/messages/read-all", projectId), { - method: "POST", - }); -} - -/** Delete a message. */ -export function deleteMessage(id: string, projectId?: string): Promise { - return api(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 { - const path = `/messages/conversation/${encodeURIComponent(participantType)}/${encodeURIComponent(participantId)}`; - return api(withProjectId(path, projectId)); -} - -/** Fetch an agent's mailbox (admin read-only view). */ -export function fetchAgentMailbox(agentId: string, projectId?: string): Promise { - return api(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 { - return api(withProjectId("/agents/mailbox/all", projectId)); -} - -export function fetchApprovals( - options?: { status?: ApprovalRequestStatus; limit?: number; offset?: number }, - projectId?: string, -): Promise { - 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(`/approvals${query}`); -} - -export function fetchApprovalDetail(id: string, projectId?: string): Promise { - return api(withProjectId(`/approvals/${encodeURIComponent(id)}`, projectId)); -} - -export function decideApproval( - id: string, - input: { decision: "approve" | "deny"; comment?: string }, - projectId?: string, -): Promise { - return api(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 { - 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(`/agents/${encodeURIComponent(agentId)}/reflections${query}`); -} - -/** Fetch the most recent reflection for an agent. */ -export function fetchAgentReflection(agentId: string, projectId?: string): Promise { - return api(withProjectId(`/agents/${encodeURIComponent(agentId)}/reflections/latest`, projectId)); -} - -/** Trigger a manual reflection for an agent. */ -export function triggerAgentReflection(agentId: string, projectId?: string): Promise { - return api(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 { - 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(`/agents/${encodeURIComponent(agentId)}/performance${query}`); -} - -/** Fetch ratings for an agent */ -export function fetchAgentRatings( - agentId: string, - options?: { limit?: number; category?: string }, - projectId?: string, -): Promise { - 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(`/agents/${encodeURIComponent(agentId)}/ratings${query}`); -} - -/** Add a rating for an agent */ -export function addAgentRating( - agentId: string, - input: AgentRatingInput, - projectId?: string, -): Promise { - return api(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 { - return api(withProjectId(`/agents/${encodeURIComponent(agentId)}/ratings/summary`, projectId)); -} - -/** Delete a specific rating */ -export function deleteAgentRating(agentId: string, ratingId: string, projectId?: string): Promise { - return api(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 { - return api(withProjectId(`/agents/${encodeURIComponent(agentId)}/budget`, projectId)); -} - -/** Reset budget usage for an agent */ -export function resetAgentBudget(agentId: string, projectId?: string): Promise { - return api(withProjectId(`/agents/${encodeURIComponent(agentId)}/budget/reset`, projectId), { - method: "POST", - }); -} - -// ── 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 { - return api(withProjectId("/plugins", projectId)); -} - -/** Fetch curated registry plugins with installed-state metadata */ -export async function fetchPluginRegistry( - query?: string, - category?: string, - projectId?: string, -): Promise { - 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 { - return api(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 { - return api(withProjectId("/plugins", projectId), { - method: "POST", - body: JSON.stringify({ mode: "install", ...source }), - }); -} - -/** Enable a plugin */ -export async function enablePlugin(id: string, projectId?: string): Promise { - return api(withProjectId(`/plugins/${encodeURIComponent(id)}/enable`, projectId), { - method: "POST", - }); -} - -/** Disable a plugin */ -export async function disablePlugin(id: string, projectId?: string): Promise { - return api(withProjectId(`/plugins/${encodeURIComponent(id)}/disable`, projectId), { - method: "POST", - }); -} - -/** Uninstall a plugin */ -export async function uninstallPlugin(id: string, projectId?: string): Promise { - return api(withProjectId(`/plugins/${encodeURIComponent(id)}`, projectId), { - method: "DELETE", - }); -} - -/** Fetch plugin settings */ -export async function fetchPluginSettings(id: string, projectId?: string): Promise> { - return api>(withProjectId(`/plugins/${encodeURIComponent(id)}/settings`, projectId)); -} - -/** Update plugin settings */ -export async function updatePluginSettings( - id: string, - settings: Record, - projectId?: string, -): Promise> { - return api>(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 { - return api(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 { - return api(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 { - return api(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 { - return api(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 { - const path = withProjectId("/plugins/ui-slots", projectId); - return dedupe(path, () => api(path)); -} - - -/** Fetch all structured UI contributions from active plugins */ -export async function fetchPluginUiContributions(projectId?: string): Promise { - return api(withProjectId("/plugins/ui-contributions", projectId)); -} - -/** Fetch all top-level dashboard view definitions from active plugins */ -export async function fetchPluginDashboardViews(projectId?: string): Promise { - return api(withProjectId("/plugins/dashboard-views", projectId)); -} - -/** Fetch all plugin runtime metadata from active plugins */ -export async function fetchPluginRuntimes(projectId?: string): Promise { - return api(withProjectId("/plugins/runtimes", projectId)); -} - -// ── Skills Management ───────────────────────────────────────────────────────── - -/** Fetch all discovered skills with their enabled state */ -export async function fetchDiscoveredSkills(projectId?: string): Promise { - 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 { - return api(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 { - 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(withProjectId(`/skills/catalog${suffix}`, projectId)); -} - -/** Fetch the contents of a skill's SKILL.md file */ -export async function fetchSkillContent(skillId: string, projectId?: string): Promise { - const response = await api<{ content: SkillContent }>( - withProjectId(`/skills/${encodeURIComponent(skillId)}/content`, projectId) - ); - return response.content; -} +/* + * FNXC:CodeOrganization 2026-07-18-14:00: + * Preserve legacy `messaging` imports while implementations live in messaging.ts. + */ +export { + addAgentRating, + createProposedTask, + decideApproval, + deleteAgentRating, + deleteMessage, + fetchAgentBudgetStatus, + fetchAgentMailbox, + fetchAgentPerformance, + fetchAgentRatingSummary, + fetchAgentRatings, + fetchAgentReflection, + fetchAgentReflections, + fetchAllAgentMailbox, + fetchApprovalDetail, + fetchApprovals, + fetchConversation, + fetchInbox, + fetchMessage, + fetchOutbox, + fetchUnreadCount, + markAllMessagesRead, + markMessageRead, + resetAgentBudget, + sendMessage, + triggerAgentReflection, +} from "./messaging.js"; +export type { + AgentMailboxResponse, + AllAgentsMailboxResponse, + ApprovalListResponse, + ApprovalRequestDetail, + ApprovalRequestSummary, + InboxResponse, + MarkAllReadResponse, + OutboxResponse, + SendMessageInput, + UnreadCountResponse, +} from "./messaging.js"; /* -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 { - 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; -} + * FNXC:CodeOrganization 2026-07-18-14:00: + * Preserve legacy `plugins-and-skills` imports while implementations live in plugins-and-skills.ts. + */ +export { + disablePlugin, + enablePlugin, + fetchDiscoveredSkills, + fetchPluginDashboardViews, + fetchPluginDetail, + fetchPluginRegistry, + fetchPluginRuntimes, + fetchPluginSettings, + fetchPluginSetupStatus, + fetchPluginUiContributions, + fetchPluginUiSlots, + fetchPlugins, + fetchSkillContent, + fetchSkillFileContent, + fetchSkillsCatalog, + installPlugin, + installPluginSetup, + installSkill, + reloadPlugin, + rescanPlugin, + toggleExecutionSkill, + uninstallPlugin, + updatePlugin, + updatePluginSettings, +} from "./plugins-and-skills.js"; +export type { + PluginDashboardViewEntry, + PluginRuntimeInfo, + PluginSetupStatusResponse, + PluginUiContributionEntry, + PluginUiSlotEntry, + RegistryPluginEntry, +} from "./plugins-and-skills.js"; // ── Chat API ───────────────────────────────────────────────────────────────── @@ -9817,153 +8690,27 @@ export function attachChatStream( } -// ── Insights API ───────────────────────────────────────────────────────────── - -export interface InsightsListResponse { - insights: Insight[]; - count: number; -} - -export interface RunsListResponse { - runs: InsightRun[]; -} - -/** - * List insights for a project with optional filtering. +/* + * FNXC:CodeOrganization 2026-07-18-14:00: + * Preserve legacy `insights` imports while implementations live in insights.ts. */ -export function fetchInsights( - options: { - category?: InsightCategory; - status?: InsightStatus; - runId?: string; - limit?: number; - offset?: number; - } = {}, - projectId?: string, -): Promise { - 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(withProjectId(`/insights${suffix}`, projectId)); -} - -/** - * Get a single insight by ID. - */ -export function fetchInsight(id: string, projectId?: string): Promise { - return api(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 { - return api(withProjectId(`/insights/${encodeURIComponent(id)}`, projectId), { - method: "PATCH", - body: JSON.stringify(updates), - }); -} - -/** - * Delete an insight. - */ -export function deleteInsight(id: string, projectId?: string): Promise { - return api(withProjectId(`/insights/${encodeURIComponent(id)}`, projectId), { - method: "DELETE", - }); -} - -/** - * Dismiss an insight (set status to dismissed). - */ -export function dismissInsight(id: string, projectId?: string): Promise { - return api(withProjectId(`/insights/${encodeURIComponent(id)}/dismiss`, projectId), { - method: "POST", - }); -} - -/** - * Archive an insight (set status to archived). - */ -export function archiveInsight(id: string, projectId?: string): Promise { - return api(withProjectId(`/insights/${encodeURIComponent(id)}/archive`, projectId), { - method: "POST", - }); -} - -/** - * Unarchive an insight (set status back to confirmed). - */ -export function unarchiveInsight(id: string, projectId?: string): Promise { - return api(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 { - const body: Record = { trigger, inputMetadata }; - if (modelProvider) body.modelProvider = modelProvider; - if (modelId) body.modelId = modelId; - if (thinkingLevel) body.thinkingLevel = thinkingLevel; - return api(withProjectId("/insights/run", projectId), { - method: "POST", - body: JSON.stringify(body), - }); -} - -/** - * List insight generation runs. - */ -export function fetchInsightRuns(projectId?: string): Promise { - return api(withProjectId("/insights/runs", projectId)); -} - -/** - * Get a single insight run by ID. - */ -export function fetchInsightRun(id: string, projectId?: string): Promise { - return api(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", - }); -} +export { + archiveInsight, + deleteInsight, + dismissInsight, + fetchInsight, + fetchInsightRun, + fetchInsightRuns, + fetchInsights, + getInsightCreateTaskData, + triggerInsightRun, + unarchiveInsight, + updateInsight, +} from "./insights.js"; +export type { + InsightsListResponse, + RunsListResponse, +} from "./insights.js"; // ── Research API ──────────────────────────────────────────────────────────── @@ -10146,154 +8893,28 @@ export function getResearchStats(projectId?: string): Promise(withProjectId("/research/stats", projectId)); } -// ── 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:CodeOrganization 2026-07-18-14:00: + * Preserve legacy `system-panel` imports while implementations live in system-panel.ts. + */ +export { + fetchCurrentSystemRebuild, + fetchSystemInfo, + fetchSystemLogs, + promoteResearchFinding, + reloadAllSystemPlugins, + requestSystemRestart, + restartAllSystemAgents, + restartSystemEngines, + startFnBinaryLinkLocal, + startFnBinaryUseGlobal, + startSystemRebuild, +} from "./system-panel.js"; +export type { + ResearchFindingPromotionInput, + SystemInfoResponse, + SystemLogEntryDto, + SystemRebuildJobLine, + SystemRebuildJobSnapshot, +} from "./system-panel.js"; -/* -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 { - return api("/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 { - return api("/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 { - return api("/system/fn-binary/link-local", { method: "POST" }); -} - -export function startFnBinaryUseGlobal(): Promise { - return api("/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), - }); -} diff --git a/packages/dashboard/app/api/messaging.ts b/packages/dashboard/app/api/messaging.ts new file mode 100644 index 0000000000..4073abf398 --- /dev/null +++ b/packages/dashboard/app/api/messaging.ts @@ -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; + }; + 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 { + 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(`/messages/inbox${query}`); +} + +/** Fetch sent messages for the current user. */ +export function fetchOutbox( + options?: { limit?: number; offset?: number; type?: MessageType }, + projectId?: string, +): Promise { + 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(`/messages/outbox${query}`); +} + +/** Fetch unread message count (lightweight, for header badge). */ +export function fetchUnreadCount(projectId?: string): Promise { + return api(withProjectId("/messages/unread-count", projectId)); +} + +/** Fetch a single message by ID. */ +export function fetchMessage(id: string, projectId?: string): Promise { + return api(withProjectId(`/messages/${encodeURIComponent(id)}`, projectId)); +} + +/** Send a new message. */ +export function sendMessage(input: SendMessageInput, projectId?: string): Promise { + return api(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 { + return api(withProjectId(`/messages/${encodeURIComponent(id)}/read`, projectId), { + method: "POST", + }); +} + +/** Mark all inbox messages as read. */ +export function markAllMessagesRead(projectId?: string): Promise { + return api(withProjectId("/messages/read-all", projectId), { + method: "POST", + }); +} + +/** Delete a message. */ +export function deleteMessage(id: string, projectId?: string): Promise { + return api(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 { + const path = `/messages/conversation/${encodeURIComponent(participantType)}/${encodeURIComponent(participantId)}`; + return api(withProjectId(path, projectId)); +} + +/** Fetch an agent's mailbox (admin read-only view). */ +export function fetchAgentMailbox(agentId: string, projectId?: string): Promise { + return api(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 { + return api(withProjectId("/agents/mailbox/all", projectId)); +} + +export function fetchApprovals( + options?: { status?: ApprovalRequestStatus; limit?: number; offset?: number }, + projectId?: string, +): Promise { + 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(`/approvals${query}`); +} + +export function fetchApprovalDetail(id: string, projectId?: string): Promise { + return api(withProjectId(`/approvals/${encodeURIComponent(id)}`, projectId)); +} + +export function decideApproval( + id: string, + input: { decision: "approve" | "deny"; comment?: string }, + projectId?: string, +): Promise { + return api(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 { + 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(`/agents/${encodeURIComponent(agentId)}/reflections${query}`); +} + +/** Fetch the most recent reflection for an agent. */ +export function fetchAgentReflection(agentId: string, projectId?: string): Promise { + return api(withProjectId(`/agents/${encodeURIComponent(agentId)}/reflections/latest`, projectId)); +} + +/** Trigger a manual reflection for an agent. */ +export function triggerAgentReflection(agentId: string, projectId?: string): Promise { + return api(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 { + 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(`/agents/${encodeURIComponent(agentId)}/performance${query}`); +} + +/** Fetch ratings for an agent */ +export function fetchAgentRatings( + agentId: string, + options?: { limit?: number; category?: string }, + projectId?: string, +): Promise { + 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(`/agents/${encodeURIComponent(agentId)}/ratings${query}`); +} + +/** Add a rating for an agent */ +export function addAgentRating( + agentId: string, + input: AgentRatingInput, + projectId?: string, +): Promise { + return api(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 { + return api(withProjectId(`/agents/${encodeURIComponent(agentId)}/ratings/summary`, projectId)); +} + +/** Delete a specific rating */ +export function deleteAgentRating(agentId: string, ratingId: string, projectId?: string): Promise { + return api(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 { + return api(withProjectId(`/agents/${encodeURIComponent(agentId)}/budget`, projectId)); +} + +/** Reset budget usage for an agent */ +export function resetAgentBudget(agentId: string, projectId?: string): Promise { + return api(withProjectId(`/agents/${encodeURIComponent(agentId)}/budget/reset`, projectId), { + method: "POST", + }); +} + diff --git a/packages/dashboard/app/api/missions.ts b/packages/dashboard/app/api/missions.ts new file mode 100644 index 0000000000..4decb034c3 --- /dev/null +++ b/packages/dashboard/app/api/missions.ts @@ -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 { + return api(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 { + return api(withProjectId("/missions", projectId), { + method: "POST", + body: JSON.stringify(input), + }); +} + +/** Get mission with full hierarchy */ +export function fetchMission(missionId: string, projectId?: string): Promise { + return api(withProjectId(`/missions/${encodeURIComponent(missionId)}`, projectId)); +} + +/** Update mission */ +export function updateMission(missionId: string, updates: Partial & { autoMerge?: boolean | null }, projectId?: string): Promise { + return api(withProjectId(`/missions/${encodeURIComponent(missionId)}`, projectId), { + method: "PATCH", + body: JSON.stringify(updates), + }); +} + +/** Delete mission */ +export function deleteMission(missionId: string, projectId?: string): Promise { + return api(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 { + return api( + 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 { + return api( + 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 { + 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( + withProjectId(`/missions/${encodeURIComponent(missionId)}/events${suffix}`, projectId), + ); +} + +/** Fetch computed mission health metrics. */ +export function fetchMissionHealth(missionId: string, projectId?: string): Promise { + return api(withProjectId(`/missions/${encodeURIComponent(missionId)}/health`, projectId)); +} + +/** Fetch health metrics for all missions in a single batched request. */ +export function fetchMissionsHealth(projectId?: string): Promise> { + return api>(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 { + return api(withProjectId(`/missions/${encodeURIComponent(missionId)}/milestones`, projectId), { + method: "POST", + body: JSON.stringify(input), + }); +} + +/** Update milestone */ +export function updateMilestone(milestoneId: string, updates: Partial, projectId?: string): Promise { + return api(withProjectId(`/missions/milestones/${encodeURIComponent(milestoneId)}`, projectId), { + method: "PATCH", + body: JSON.stringify(updates), + }); +} + +/** Delete milestone */ +export function deleteMilestone(milestoneId: string, projectId?: string): Promise { + return api(withProjectId(`/missions/milestones/${encodeURIComponent(milestoneId)}`, projectId), { + method: "DELETE", + }); +} + +/** Reorder milestones */ +export function reorderMilestones(missionId: string, orderedIds: string[], projectId?: string): Promise { + return api(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 { + return api(withProjectId(`/missions/milestones/${encodeURIComponent(milestoneId)}/slices`, projectId), { + method: "POST", + body: JSON.stringify(input), + }); +} + +/** Update slice */ +export function updateSlice(sliceId: string, updates: Partial, projectId?: string): Promise { + return api(withProjectId(`/missions/slices/${encodeURIComponent(sliceId)}`, projectId), { + method: "PATCH", + body: JSON.stringify(updates), + }); +} + +/** Delete slice */ +export function deleteSlice(sliceId: string, projectId?: string): Promise { + return api(withProjectId(`/missions/slices/${encodeURIComponent(sliceId)}`, projectId), { + method: "DELETE", + }); +} + +/** Activate slice */ +export function activateSlice(sliceId: string, projectId?: string): Promise { + return api(withProjectId(`/missions/slices/${encodeURIComponent(sliceId)}/activate`, projectId), { + method: "POST", + }); +} + +/** Reorder slices */ +export function reorderSlices(milestoneId: string, orderedIds: string[], projectId?: string): Promise { + return api(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 { + return api(withProjectId(`/missions/slices/${encodeURIComponent(sliceId)}/features`, projectId), { + method: "POST", + body: JSON.stringify(input), + }); +} + +/** Update feature */ +export function updateFeature(featureId: string, updates: Partial, projectId?: string): Promise { + return api(withProjectId(`/missions/features/${encodeURIComponent(featureId)}`, projectId), { + method: "PATCH", + body: JSON.stringify(updates), + }); +} + +/** Delete feature */ +export function deleteFeature(featureId: string, projectId?: string): Promise { + return api(withProjectId(`/missions/features/${encodeURIComponent(featureId)}`, projectId), { + method: "DELETE", + }); +} + +/** Link feature to task */ +export function linkFeatureToTask(featureId: string, taskId: string, projectId?: string): Promise { + return api(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 { + return api(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 { + return api(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 { + return api(withProjectId(`/missions/milestones/${encodeURIComponent(milestoneId)}/assertions`, projectId)); +} + +/** Create a new assertion for a milestone */ +export function createAssertion(milestoneId: string, input: ContractAssertionCreateInput, projectId?: string): Promise { + return api(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 { + return api(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 { + return api(withProjectId(`/missions/assertions/${encodeURIComponent(assertionId)}`, projectId)); +} + +/** Update an assertion */ +export function updateAssertion(assertionId: string, updates: ContractAssertionUpdateInput, projectId?: string): Promise { + return api(withProjectId(`/missions/assertions/${encodeURIComponent(assertionId)}`, projectId), { + method: "PATCH", + body: JSON.stringify(updates), + }); +} + +/** Delete an assertion */ +export function deleteAssertion(assertionId: string, projectId?: string): Promise { + return api(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 { + return api(withProjectId(`/missions/features/${encodeURIComponent(featureId)}/assertions`, projectId)); +} + +/** List features linked to an assertion */ +export function fetchFeaturesForAssertion(assertionId: string, projectId?: string): Promise { + return api(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 { + return api(withProjectId(`/missions/milestones/${encodeURIComponent(milestoneId)}/validation`, projectId)); +} + +/** Fetch grouped validation telemetry for a milestone */ +export function fetchMilestoneValidationTelemetry(milestoneId: string, projectId?: string): Promise { + return api(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 { + return api(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 { + 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(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 }> { + 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 { + return api(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 { + return api(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 { + return api(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 { + return api(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 { + return api(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 { + return api(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 { + return api(withProjectId(`/missions/${encodeURIComponent(missionId)}/autopilot/start`, projectId), { + method: "POST", + }); +} + +/** Manually stop autopilot watching for a mission */ +export function stopMissionAutopilot(missionId: string, projectId?: string): Promise { + return api(withProjectId(`/missions/${encodeURIComponent(missionId)}/autopilot/stop`, projectId), { + method: "POST", + }); +} + diff --git a/packages/dashboard/app/api/plugins-and-skills.ts b/packages/dashboard/app/api/plugins-and-skills.ts new file mode 100644 index 0000000000..e556abdd34 --- /dev/null +++ b/packages/dashboard/app/api/plugins-and-skills.ts @@ -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 { + return api(withProjectId("/plugins", projectId)); +} + +/** Fetch curated registry plugins with installed-state metadata */ +export async function fetchPluginRegistry( + query?: string, + category?: string, + projectId?: string, +): Promise { + 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 { + return api(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 { + return api(withProjectId("/plugins", projectId), { + method: "POST", + body: JSON.stringify({ mode: "install", ...source }), + }); +} + +/** Enable a plugin */ +export async function enablePlugin(id: string, projectId?: string): Promise { + return api(withProjectId(`/plugins/${encodeURIComponent(id)}/enable`, projectId), { + method: "POST", + }); +} + +/** Disable a plugin */ +export async function disablePlugin(id: string, projectId?: string): Promise { + return api(withProjectId(`/plugins/${encodeURIComponent(id)}/disable`, projectId), { + method: "POST", + }); +} + +/** Uninstall a plugin */ +export async function uninstallPlugin(id: string, projectId?: string): Promise { + return api(withProjectId(`/plugins/${encodeURIComponent(id)}`, projectId), { + method: "DELETE", + }); +} + +/** Fetch plugin settings */ +export async function fetchPluginSettings(id: string, projectId?: string): Promise> { + return api>(withProjectId(`/plugins/${encodeURIComponent(id)}/settings`, projectId)); +} + +/** Update plugin settings */ +export async function updatePluginSettings( + id: string, + settings: Record, + projectId?: string, +): Promise> { + return api>(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 { + return api(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 { + return api(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 { + return api(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 { + return api(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 { + const path = withProjectId("/plugins/ui-slots", projectId); + return dedupe(path, () => api(path)); +} + + +/** Fetch all structured UI contributions from active plugins */ +export async function fetchPluginUiContributions(projectId?: string): Promise { + return api(withProjectId("/plugins/ui-contributions", projectId)); +} + +/** Fetch all top-level dashboard view definitions from active plugins */ +export async function fetchPluginDashboardViews(projectId?: string): Promise { + return api(withProjectId("/plugins/dashboard-views", projectId)); +} + +/** Fetch all plugin runtime metadata from active plugins */ +export async function fetchPluginRuntimes(projectId?: string): Promise { + return api(withProjectId("/plugins/runtimes", projectId)); +} + +// ── Skills Management ───────────────────────────────────────────────────────── + +/** Fetch all discovered skills with their enabled state */ +export async function fetchDiscoveredSkills(projectId?: string): Promise { + 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 { + return api(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 { + 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(withProjectId(`/skills/catalog${suffix}`, projectId)); +} + +/** Fetch the contents of a skill's SKILL.md file */ +export async function fetchSkillContent(skillId: string, projectId?: string): Promise { + 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 { + 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; +} + diff --git a/packages/dashboard/app/api/system-panel.ts b/packages/dashboard/app/api/system-panel.ts new file mode 100644 index 0000000000..b0d14fe329 --- /dev/null +++ b/packages/dashboard/app/api/system-panel.ts @@ -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 { + return api("/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 { + return api("/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 { + return api("/system/fn-binary/link-local", { method: "POST" }); +} + +export function startFnBinaryUseGlobal(): Promise { + return api("/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), + }); +} diff --git a/packages/dashboard/app/api/todo.ts b/packages/dashboard/app/api/todo.ts new file mode 100644 index 0000000000..3668f6ac3b --- /dev/null +++ b/packages/dashboard/app/api/todo.ts @@ -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 { + return api(withProjectId("/todos", projectId)); +} + +/** Create a new todo list */ +export function createTodoList(title: string, projectId?: string): Promise { + const input: TodoListCreateInput = { title }; + return api(withProjectId("/todos", projectId), { + method: "POST", + body: JSON.stringify(input), + }); +} + +/** Update a todo list title */ +export function updateTodoList(id: string, title: string, projectId?: string): Promise { + const updates: TodoListUpdateInput = { title }; + return api(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 { + return api(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 { + const input: TodoItemCreateInput = { text }; + return api(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 { + const updates: TodoItemUpdateInput = data; + return api(withProjectId(`/todos/items/${encodeURIComponent(id)}`, projectId), { + method: "PATCH", + body: JSON.stringify(updates), + }); +} + +/** Delete a todo item */ +export function deleteTodoItem(id: string, projectId?: string): Promise { + return api(withProjectId(`/todos/items/${encodeURIComponent(id)}`, projectId), { + method: "DELETE", + }); +} + +/** Reorder items within a todo list */ +export function reorderTodoItems(listId: string, itemIds: string[], projectId?: string): Promise { + return api(withProjectId(`/todos/${encodeURIComponent(listId)}/items/reorder`, projectId), { + method: "POST", + body: JSON.stringify({ itemIds }), + }); +} + diff --git a/packages/dashboard/app/types/html2canvas.d.ts b/packages/dashboard/app/types/html2canvas.d.ts new file mode 100644 index 0000000000..bd5da31a13 --- /dev/null +++ b/packages/dashboard/app/types/html2canvas.d.ts @@ -0,0 +1,28 @@ +/* +FNXC:ReportPipeline 2026-07-19-00:25: +html2canvas ships typings via package "typings", but dashboard app typecheck +(`moduleResolution: "bundler"`, explicit `compilerOptions.types`) fails to resolve +the package module on CI with TS2307. Ambient module keeps capture-screenshot +type-safe without relying on package resolution. + +FNXC:ReportPipeline 2026-07-19-12:00: +Omit catch-all index signature on Options — upstream html2canvas@1.4.1 Options is +explicit; Partial already covers optional fields; index signature hides typos. +*/ +declare module "html2canvas" { + export type Options = { + backgroundColor?: string | null; + scale?: number; + useCORS?: boolean; + logging?: boolean; + foreignObjectRendering?: boolean; + removeContainer?: boolean; + }; + + function html2canvas( + element: HTMLElement, + options?: Partial, + ): Promise; + + export default html2canvas; +} diff --git a/scripts/line-count-baseline.json b/scripts/line-count-baseline.json index 56dd73ce09..157177f5be 100644 --- a/scripts/line-count-baseline.json +++ b/scripts/line-count-baseline.json @@ -8,17 +8,17 @@ "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/async-mission-store-queries.ts": 2093, "packages/core/src/central-core.ts": 4486, - "packages/core/src/index.gate.ts": 2217, - "packages/core/src/index.ts": 2501, + "packages/core/src/index.gate.ts": 2228, + "packages/core/src/index.ts": 2512, "packages/core/src/mission-store.ts": 4369, - "packages/core/src/postgres/schema/project.ts": 2150, + "packages/core/src/postgres/schema/project.ts": 2155, "packages/core/src/postgres/sqlite-migrator.ts": 2112, "packages/core/src/store.ts": 2746, - "packages/core/src/types.ts": 7101, + "packages/core/src/types.ts": 6165, "packages/dashboard/app/App.tsx": 2059, - "packages/dashboard/app/api/legacy.ts": 10273, + "packages/dashboard/app/api/legacy.ts": 8913, "packages/dashboard/app/components/AgentDetailView.tsx": 5567, "packages/dashboard/app/components/AgentsView.tsx": 2254, "packages/dashboard/app/components/ChatView.tsx": 4020, @@ -31,7 +31,7 @@ "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/TaskDetailModal.tsx": 6509, "packages/dashboard/app/components/TerminalModal.tsx": 3322, "packages/dashboard/app/components/WorkflowNodeEditor.tsx": 5685, "packages/dashboard/app/components/__tests__/AgentsView.test.tsx": 2854, @@ -50,7 +50,7 @@ "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.rendering.test.tsx": 2821, "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, @@ -64,9 +64,9 @@ "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": 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__/routes-github.test.ts": 3798, + "packages/dashboard/src/__tests__/routes-planning.test.ts": 5271, + "packages/dashboard/src/__tests__/routes-tasks.test.ts": 3024, "packages/dashboard/src/__tests__/usage.test.ts": 5261, "packages/dashboard/src/chat.ts": 3069, "packages/dashboard/src/github.ts": 4877, @@ -81,7 +81,7 @@ "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": 4455, + "packages/engine/src/__tests__/heartbeat-executor.test.ts": 4462, "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, @@ -91,10 +91,10 @@ "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/__tests__/triage.test.ts": 7398, "packages/engine/src/agent-heartbeat.ts": 5370, - "packages/engine/src/agent-tools.ts": 5230, - "packages/engine/src/executor.ts": 20110, + "packages/engine/src/agent-tools.ts": 5258, + "packages/engine/src/executor.ts": 20123, "packages/engine/src/merger-ai.ts": 2401, "packages/engine/src/merger.ts": 11208, "packages/engine/src/pi.ts": 2814,