/** * Solution bundle apply engine. * * Deploys a bundle template in one step, with progress: * * 1. `team` — create the team(s) with filled prompts (StorageService) * 2. `norms` — write norms, review points and SOPs where the team's * norms and SOPs live (`teams//norms`, `teams//sops`) * 3. `skills` — install missing skills (marketplace, behind an interface) * 4. `connectors` — report services not yet connected, with /connections links * 5. `slack` — team channels + extra channels (SlackTeamChannelService), * only when Slack is connected; otherwise pending * 6. `schedules` — recurring tasks (CronTaskService) * 7. `first_week` — day-0 tasks go to the orchestrator now (the chat path the * onboarding first task uses); later days are delivered by * {@link BundleApplyService.deliverDue} * * Idempotent and resumable: the deployment is persisted after every step; * a re-run skips steps that are done, and each step skips items it already * did (existing teams are kept, sent tasks are not re-sent, schedules and * channels are recorded). One failing step never stops the others, except * that everything needs the team. * * All collaborators are injected (`bundle-apply.factory.ts` wires the * backend's; the CLI wires an offline set), so the engine is testable with * plain fakes and works with or without a running backend. * * @module services/bundle/bundle-apply.service */ import type { Team } from '../../types/index.js'; import type { CreateCronTaskRequest } from '../../types/cron-task.types.js'; import type { BundleConnector, BundleConnectorState, BundleDeployment, BundleFirstWeekState, BundleStepState, LoadedBundle } from '../../types/solution-bundle.types.js'; import { type BundleAnswerProblem } from './bundle-placeholders.js'; import type { BundleDeploymentStore } from './bundle-state.store.js'; /** Installs skills. Switches to the skill-autoinstall runner when it lands. */ export interface BundleSkillInstaller { /** Whether the skill is already usable (bundled or installed) */ isAvailable(skillId: string): Promise; /** Install it */ install(skillId: string): Promise<{ ok: boolean; message: string; }>; } /** The Slack operations the engine needs. */ export interface BundleSlackApi { isConnected(): boolean; /** The team's own channel (SlackTeamChannelService.ensureTeamChannel) */ ensureTeamChannel(team: Team): Promise<{ slackChannelId: string; slackChannelName: string; }>; /** An extra channel for some agents */ ensureAgentChannel(input: { name: string; purpose: string; memberSessions: string[]; existingChannelId?: string; }): Promise<{ slackChannelId: string; slackChannelName: string; }>; } /** Creates recurring tasks (CronTaskService.create; idempotent). */ export interface BundleScheduleApi { create(request: CreateCronTaskRequest): Promise<{ id: string; }>; } /** Hands an owner message to the orchestrator (the chat path). */ export interface BundleOrchestratorApi { send(content: string, metadata: Record): Promise<{ conversationId: string | null; forwarded: boolean; queued: boolean; error: string | null; }>; } /** Reads whether a connector is connected. */ export interface BundleConnectorApi { check(connector: BundleConnector): Promise; } /** Everything the engine uses. `null` collaborators mean "not available here" (e.g. the CLI without a backend). */ export interface BundleApplyDeps { catalog: { get(templateId: string): LoadedBundle | null; }; store: BundleDeploymentStore; /** Crewly home (norms and SOPs are written under `teams//`) */ crewlyHome: string; teams: { get(teamId: string): Promise; save(team: Team): Promise; }; skills: BundleSkillInstaller; slack: BundleSlackApi | null; schedules: BundleScheduleApi | null; orchestrator: BundleOrchestratorApi | null; connectors: BundleConnectorApi | null; /** Pick the runtime: the requested one, else a sensible default for this machine */ resolveRuntime(recommended: string, requested: string | undefined): Promise; now(): Date; /** Job id factory (tests) */ newJobId?(): string; } /** What `POST /api/bundles/apply` sends. */ export interface BundleApplyRequest { templateId: string; /** Answers by question id; omitted on a re-run to reuse the stored ones */ answers?: unknown; runtime?: string; /** Deploy a `draft` bundle */ allowDraft?: boolean; } /** Error codes of the engine. */ export type BundleErrorCode = 'unknown_bundle' | 'bundle_not_ready' | 'invalid_runtime' | 'invalid_answers' | 'job_not_found'; /** A request the engine refuses before anything is written. */ export declare class BundleError extends Error { readonly code: BundleErrorCode; readonly details?: { missing: BundleAnswerProblem[]; invalid: BundleAnswerProblem[]; } | undefined; /** * @param code - Machine-readable code (mapped to an HTTP status by the controller) * @param message - Owner-facing message * @param details - Missing / invalid answers for `invalid_answers` */ constructor(code: BundleErrorCode, message: string, details?: { missing: BundleAnswerProblem[]; invalid: BundleAnswerProblem[]; } | undefined); } /** * A fresh step list (all queued). * * @returns Steps in run order */ export declare function initialSteps(): BundleStepState[]; /** * The overall status from the steps. * * @param steps - Steps * @returns `done` when everything is done (or had nothing to do), `failed` * when the team could not be created, else `partial` */ export declare function overallStatus(steps: BundleStepState[]): BundleDeployment['status']; /** * Markdown with YAML frontmatter, the shape get-team-norms / get-sops read. * * @param fields - Frontmatter fields (undefined values are dropped) * @param body - Markdown body * @returns File content */ export declare function withFrontmatter(fields: Record, body: string): string; /** * The message a first-week task reaches the orchestrator as. * * @param task - Task state * @param team - Team it is for * @param memberName - Member it is for * @param body - Filled task text * @returns Message content */ export declare function buildFirstWeekMessage(task: BundleFirstWeekState, team: Pick, memberName: string, body: string): string; /** * The apply engine. See the module docs. */ export declare class BundleApplyService { private readonly deps; /** Deployments being applied right now, by template id */ private readonly running; /** Starts still validating / preparing, by template id (so a double tap joins them) */ private readonly starting; /** Serialises writes per template */ private readonly writes; /** * @param deps - Collaborators */ constructor(deps: BundleApplyDeps); /** * Validate a request and start applying it in the background. * * A second call while the same template is being applied returns that job * (a double tap on a phone must not deploy twice). Answers left out reuse * the ones stored by an earlier run. * * @param request - Template id, answers, runtime * @returns The deployment snapshot with its `jobId` * @throws BundleError `unknown_bundle` | `bundle_not_ready` | `invalid_runtime` | `invalid_answers` */ start(request: BundleApplyRequest): Promise; /** * Apply and wait for the result (CLI, tests). * * @param request - Template id, answers, runtime * @returns The finished deployment * @throws BundleError like {@link start} */ applyAndWait(request: BundleApplyRequest): Promise; /** * Progress of a job: the live deployment while it runs, else the stored one. * * @param jobId - Job id from {@link start} * @returns The deployment * @throws BundleError `job_not_found` */ getJob(jobId: string): Promise; /** * The deployment of a template on this machine. * * @param templateId - Template id * @returns The deployment, or null when it was never applied */ getDeployment(templateId: string): Promise; /** * Deliver first-week tasks that are due (the backend calls this on a timer). * * @returns Number of tasks sent */ deliverDue(): Promise; /** * Re-run deployments that were waiting on something that is now there: * the backend (after a CLI deploy without one) or Slack. * * @returns Template ids that were restarted */ resumeWaiting(): Promise; /** * Validate, prepare the deployment and start the run. * * @param request - Request * @returns The deployment and a promise of the finished one */ private begin; /** * Validate the request, build and persist the deployment, start the run. * * @param request - Request * @returns The deployment and a promise of the finished one * @throws BundleError for a refused request */ private prepare; /** * Shared context for a deployment. * * @param entry - Loaded bundle * @param deployment - Deployment * @param answers - Answers * @returns Context */ private contextFor; /** * Run every step that is not done, persisting after each. * * @param ctx - Context * @returns The finished deployment */ private run; /** * Apply an outcome to a step. * * @param step - Step * @param outcome - Outcome */ private finishStep; /** * Stamp and persist. * * @param deployment - Deployment */ private touch; /** * Persist a deployment; writes of one template never overlap. * * @param deployment - Deployment */ private persist; /** * The saved team for a bundle team key. * * @param ctx - Context * @param key - Team key * @returns The team * @throws Error when it is missing (deleted since the team step) */ private savedTeam; /** * Placeholder values of a saved team. * * @param ctx - Context * @param team - Saved team * @returns Values (answers + team_name + lead_name) */ private valuesFor; /** * Resolve a member ref (default: the main team's lead). * * @param ctx - Context * @param ref - `role` / `team/role`, or undefined * @returns Team and member session / name */ private resolveRef; /** Step `team`: create every team that does not exist yet. */ private stepTeam; /** Step `norms`: norms, review points and SOPs into the teams' folders. */ private stepNorms; /** * The body of a norm / SOP: inline content or the referenced file. * * @param ctx - Context * @param doc - Norm or SOP * @returns Markdown body (unfilled) */ private docBody; /** * Write a file atomically, creating its folder. * * @param file - Absolute path * @param content - Content */ private writeDoc; /** Step `skills`: install what is missing. */ private stepSkills; /** Step `connectors`: which services still need connecting. */ private stepConnectors; /** Step `slack`: team channels and extra channels. */ private stepSlack; /** Step `schedules`: recurring tasks through the cron-task scheduler. */ private stepSchedules; /** * A valid timezone from a (placeholder) value, the bundle's, or the default. * * @param ctx - Context * @param values - Placeholder values * @param override - Schedule's own timezone * @returns IANA timezone * @throws Error when the filled timezone is not valid */ private timezoneFor; /** Step `first_week`: send today's tasks, schedule the rest. */ private stepFirstWeek; /** * Send one first-week task through the orchestrator and record the result. * * @param ctx - Context * @param state - Task state (updated in place) * @returns True when it was handed over */ private sendFirstWeekTask; /** * The first-week step's state from its tasks. * * @param deployment - Deployment * @returns Outcome */ private firstWeekOutcome; } //# sourceMappingURL=bundle-apply.service.d.ts.map