// FSM object artifact compiled from ./release.gears.md by gears2fsm. // It declares only the machine, its actor contracts, and typed inputs; the // linked runtime supplies the `captain` and `player` actor implementations. import { assign, fromPromise, setup } from 'xstate'; // --------------------------------------------------------------------------- // Identities // --------------------------------------------------------------------------- /** Canonical lowercase local ids of the source roles Releaser and Approver. */ export type ReleaseRole = 'releaser' | 'approver'; /** One role-id array per parallel group; RELEASE declares no parallel group. */ export const concurrentRoleSets: readonly (readonly ReleaseRole[])[] = []; /** Delegated-player working leaves (release-1 through release-4). */ export type PlayerStateId = | 'assembling' | 'firstReview' | 'answeringFindings' | 'finalReview'; /** Direct-Captain working leaf (release-5). */ export type CaptainStateId = 'approval'; /** Every Captain- or player-invoking working leaf. */ export type WorkingStateId = PlayerStateId | CaptainStateId; /** Working leaves that may suspend for, and resume from, a Boss reply. */ export type ResumableStateId = WorkingStateId; /** Root `BOSS_INTERRUPT` targets. */ export type JumpableStateId = WorkingStateId; export type ReleaseSourceItem = | 'release-1' | 'release-2' | 'release-3' | 'release-4' | 'release-5'; /** Releaser's accepted outcome of its latest turn. */ export type ReleaserOutcome = 'assembled' | 'fixed' | 'noChanges'; /** Approver's accepted outcome of its latest review round. */ export type ApproverOutcome = 'hasFindings' | 'noFindings'; /** How RELEASE ended; selects the declared terminal output. */ export type ReleaseVerdict = 'approved' | 'heldByOwner' | 'heldWithFindings'; // --------------------------------------------------------------------------- // Boss-reply suspension (scalar form: one active task at a time) // --------------------------------------------------------------------------- export type BossQuestionAsker = | { readonly kind: 'captain' } | { readonly kind: 'role'; readonly roleId: ReleaseRole }; export interface PendingBossQuestion { readonly questionId: ResumableStateId; readonly resumeStateId: ResumableStateId; readonly sourceItem: ReleaseSourceItem; readonly asker: BossQuestionAsker; readonly question: string; } // --------------------------------------------------------------------------- // Delegated player actor contract // --------------------------------------------------------------------------- interface PlayerInputBase { /** The source item's full final prompt, verbatim. */ readonly prompt: string; /** This state's local result contract: guard name → description. */ readonly result: Readonly>; /** ``: the owner's release request naming the change and its rollout plan. */ readonly callerInput?: string; readonly pendingBossQuestion?: PendingBossQuestion; readonly bossReply?: string; } export interface AssemblingPlayerInput extends PlayerInputBase { readonly stateId: 'assembling'; readonly role: 'releaser'; readonly sourceItem: 'release-1'; } export interface FirstReviewPlayerInput extends PlayerInputBase { readonly stateId: 'firstReview'; readonly role: 'approver'; readonly sourceItem: 'release-2'; /** ``: the change commit's short hash naming the bundle directory. */ readonly change?: string; /** ``: the receipt-owned latest bundle commit. */ readonly latestCommit?: string; /** ``: Releaser's verbatim final text of its latest turn. */ readonly releaserOutput?: string; } export interface AnsweringFindingsPlayerInput extends PlayerInputBase { readonly stateId: 'answeringFindings'; readonly role: 'releaser'; readonly sourceItem: 'release-3'; /** ``: the change commit's short hash naming the bundle directory. */ readonly change?: string; /** ``: the receipt-owned latest bundle commit. */ readonly latestCommit?: string; /** ``: Approver's verbatim final text of its latest round. */ readonly approverFindings?: string; } export interface FinalReviewPlayerInput extends PlayerInputBase { readonly stateId: 'finalReview'; readonly role: 'approver'; readonly sourceItem: 'release-4'; /** ``: the change commit's short hash naming the bundle directory. */ readonly change?: string; /** ``: Approver's verbatim final text of the first round. */ readonly approverFindings?: string; /** ``: the receipt-owned latest bundle commit. */ readonly latestCommit?: string; /** ``: Releaser's verbatim answer to the first-round findings. */ readonly releaserOutput?: string; } export type PlayerInput = | AssemblingPlayerInput | FirstReviewPlayerInput | AnsweringFindingsPlayerInput | FinalReviewPlayerInput; export type NeedsBossReplyOutput = { readonly guard: 'needsBossReply'; readonly question: string; }; /** Releaser outcomes (release-1, release-3). */ export type ReleaserOutput = | { readonly guard: 'assembled'; readonly change: string; /** Effect-owned: the runtime fills it from the repository receipt. */ readonly latestCommit: string; readonly releaserOutput: string; } | { readonly guard: 'fixed'; /** Effect-owned: the runtime fills it from the repository receipt. */ readonly latestCommit: string; readonly releaserOutput: string; } | { readonly guard: 'noChanges'; readonly releaserOutput: string } | NeedsBossReplyOutput; /** Approver outcomes (release-2, release-4). */ export type ApproverOutput = | { readonly guard: 'hasFindings'; readonly approverFindings: string } | { readonly guard: 'noFindings' } | NeedsBossReplyOutput; export type PlayerOutput = ReleaserOutput | ApproverOutput; type PlayerGuard = PlayerOutput['guard']; // --------------------------------------------------------------------------- // Direct Captain actor contract // --------------------------------------------------------------------------- export interface CaptainInput { readonly stateId: 'approval'; readonly sourceItem: 'release-5'; /** The source item's full final prompt, verbatim. */ readonly prompt: string; /** This state's local result contract: guard name → description. */ readonly result: Readonly>; /** ``: the change commit's short hash. */ readonly change?: string; /** ``: the receipt-owned latest bundle commit. */ readonly latestCommit?: string; readonly pendingBossQuestion?: PendingBossQuestion; readonly bossReply?: string; } export type CaptainOutput = | { readonly guard: 'approved'; readonly bundlePath: string; readonly releaseCommit: string; } | { readonly guard: 'held'; readonly holdReason: string } | NeedsBossReplyOutput; type CaptainGuard = CaptainOutput['guard']; export interface ErrorRecord { readonly name: string; readonly message: string; readonly stack?: string; } // --------------------------------------------------------------------------- // Machine input, context, events, and output // --------------------------------------------------------------------------- /** RELEASE's terminal return to its caller. */ export type ReleaseOutput = | { readonly status: 'approved'; /** The bundle path, approved for release. */ readonly bundlePath: string; /** The latest bundle commit hash, approved for release. */ readonly releaseCommit: string; } | { readonly status: 'held'; /** The owner's reason, or that none was given. */ readonly holdReason: string; } | { readonly status: 'held'; /** Approver's open findings after the second review round. */ readonly approverFindings: string; }; /** The machine reads no input: the caller input arrives on `START_RELEASE`. */ export type ReleaseInput = Readonly>; export interface ReleaseContext { /** ``: the owner's release request. */ readonly callerInput?: string; /** ``: the change commit's short hash. */ readonly change?: string; /** ``: the accepted, receipt-owned latest bundle commit. */ readonly latestCommit?: string; /** ``: Releaser's verbatim final text of its latest turn. */ readonly releaserOutput?: string; /** ``: Approver's verbatim findings of its latest round with findings. */ readonly approverFindings?: string; readonly releaserOutcome?: ReleaserOutcome; readonly approverOutcome?: ApproverOutcome; readonly bundlePath?: string; readonly releaseCommit?: string; readonly holdReason?: string; readonly verdict?: ReleaseVerdict; readonly lastError?: ErrorRecord; readonly pendingBossQuestion?: PendingBossQuestion; readonly bossReply?: string; /** * The stable question id the current `bossReply` answers, copied from the * pending question when the reply resumes its working leaf; cleared with the * Q/A context. */ readonly answeredQuestionId?: ResumableStateId; } export type ReleaseEvent = | { readonly type: 'START_RELEASE'; readonly callerInput: string } | { readonly type: 'BOSS_INTERRUPT'; readonly targetId: JumpableStateId } | { readonly type: 'BOSS_REPLY'; readonly answer: string; readonly questionId?: ResumableStateId; }; // --------------------------------------------------------------------------- // Prompts and result contracts (verbatim from release.gears.md) // --------------------------------------------------------------------------- const ASSEMBLING_PROMPT = [ 'Assemble the release evidence bundle for the change named below.', 'Confirm that the named commit is on the current branch and that the named rollout plan exists under docs/rollouts/ and is written for that commit; if either fails, ask the owner one question and wait before writing anything.', "Write the bundle in a directory under docs/releases/ named by the change commit's short hash, replacing any earlier bundle there.", 'Write these files:', "- tests.md: run the project's full test suite at the change commit (for example in a temporary worktree you remove afterwards), and record the exact command, the commit it ran at, its exit status, whether it passed, and its complete output;", "- rollout.md: the rollout plan's path and its approval line quoted verbatim, or the statement that the plan has no approval line;", "- rollback.md: the rollback plan, collecting every rollback command from the rollout plan verbatim, in the plan's order, each under the rollout stage it rolls back;", '- release-note.md: the change commit and its subject, the intent record the change implements (its id and path under specs/intents/), and the risk class that intent record states.', 'Never invent a test result, rollback command, intent record, or risk class the sources do not give; where a source lacks one, say so in the bundle.', 'Do not change any file outside that bundle directory.', 'Commit the bundle in exactly one new commit on the current branch, with a message naming the change.', "Reply with the change commit's short hash, the bundle path, the commit hash, whether the tests passed, and a summary of each file.", '', '> Release request: ', ].join('\n'); const FIRST_REVIEW_PROMPT = [ 'Review the release evidence bundle under docs/releases// on behalf of the change advisory board.', "Read the bundle, the rollout plan it names, and the intent record its release note names, and verify each check against those sources, not only against the bundle's own claims:", "- tests.md shows the project's tests ran at the change commit and passed;", "- rollback.md covers every stage of the rollout plan, each with that stage's rollback commands from the plan, in the plan's order;", '- release-note.md names the intent record and the risk class that intent record states.', 'Flag only failures of these checks — not wording or style.', 'Number every finding.', 'If you find nothing to flag, say plainly that you have no findings.', 'Do not edit files or commit; report findings only.', '', '> Release request: ', '> Bundle commit: ', '> Releaser reply: ', ].join('\n'); const ANSWERING_FINDINGS_PROMPT = [ 'The Approver reviewed your release evidence bundle under docs/releases//; the findings are below.', 'For each finding, fix it in the bundle, or explain why you cannot: it needs a change to the code, the rollout plan, or the intent record, which you must not make here, or you judge it mistaken, citing the bundle or its sources.', 'Never invent a test result, rollback command, intent record, or risk class the sources do not give.', 'Do not re-run the tests unless a finding concerns how they were run or recorded.', 'Do not change any file outside docs/releases//.', 'If you fix any finding, commit the fixes in exactly one new commit on the current branch, with a message naming the change and the findings fixed; never amend or rewrite an existing commit.', 'If you fix none, change nothing and make no commit.', "Reply with every finding's disposition and, if you committed, the commit hash.", '', '> Release request: ', '> Bundle commit: ', '> Approver findings: ', ].join('\n'); const FINAL_REVIEW_PROMPT = [ 'The Releaser answered your findings on the release evidence bundle under docs/releases//; its reply and the latest bundle commit are below.', "For each earlier finding, confirm it is resolved, accept the Releaser's explanation, or keep it with your reasoning.", "Read the bundle, the rollout plan it names, and the intent record its release note names, and verify each check against those sources, not only against the bundle's own claims:", "- tests.md shows the project's tests ran at the change commit and passed;", "- rollback.md covers every stage of the rollout plan, each with that stage's rollback commands from the plan, in the plan's order;", '- release-note.md names the intent record and the risk class that intent record states.', 'Flag only failures of these checks — not wording or style.', 'Keep the numbers of earlier findings you keep, and number new findings after them.', 'If no finding remains, say plainly that you have no findings.', 'Do not edit files or commit; report findings only.', '', '> Release request: ', '> Your earlier findings: ', '> Latest bundle commit: ', '> Releaser reply: ', ].join('\n'); const APPROVAL_PROMPT = [ 'Ask the owner exactly this one question and wait for the answer: "Approve the release of ?"', 'If the owner answers yes, report the bundle path and the bundle commit hash below as approved for release.', "If the owner answers no, report the release of as held, with the owner's reason in the owner's words, or that the owner gave none.", '', '> Bundle path: docs/releases//', '> Bundle commit: ', ].join('\n'); const NEEDS_BOSS_REPLY_DESCRIPTION = "The acting agent's prose surfaces a clarifying question for Boss that the agent cannot answer alone. Output shall include `question: `."; const ASSEMBLING_RESULT = { assembled: 'Releaser wrote the bundle under docs/releases/ and added it in exactly one new commit on the current branch; a progress report or promise of a later bundle supports no outcome. Output shall include `change: `, `latestCommit: `, and `releaserOutput: `.', needsBossReply: NEEDS_BOSS_REPLY_DESCRIPTION, } as const; const FIRST_REVIEW_RESULT = { hasFindings: 'Approver raised one or more numbered findings; a progress report or promise of a later review supports no outcome. Output shall include `approverFindings: `.', noFindings: 'Approver affirmatively reported that the review is complete with no findings; a progress report or promise of a later review supports no outcome.', needsBossReply: NEEDS_BOSS_REPLY_DESCRIPTION, } as const; const ANSWERING_FINDINGS_RESULT = { fixed: 'Releaser fixed one or more findings in the bundle and added exactly one new commit on the current branch; a progress report or promise of a later fix supports no outcome. Output shall include `latestCommit: ` and `releaserOutput: `.', noChanges: 'Releaser fixed no finding, explained each disposition, changed nothing, and made no commit; a progress report or promise of a later fix supports no outcome. Output shall include `releaserOutput: `.', needsBossReply: NEEDS_BOSS_REPLY_DESCRIPTION, } as const; const FINAL_REVIEW_RESULT = { hasFindings: "Approver kept or raised one or more numbered findings; a progress report or promise of a later review supports no outcome. On this outcome no further review takes place, the owner is not asked to approve, and `release` ends held, returning Approver's open findings listed. Output shall include `approverFindings: `.", noFindings: 'Approver affirmatively reported that the review is complete with no findings remaining; a progress report or promise of a later review supports no outcome.', needsBossReply: NEEDS_BOSS_REPLY_DESCRIPTION, } as const; const APPROVAL_RESULT = { approved: 'The owner answered yes and Captain reported the release as approved. On this outcome `release` returns the bundle path and the latest bundle commit hash as approved for release. Output shall include `bundlePath: ` and `releaseCommit: `.', held: "The owner answered no and Captain reported the release as held. On this outcome `release` returns the release as held with the owner's reason. Output shall include `holdReason: `.", needsBossReply: NEEDS_BOSS_REPLY_DESCRIPTION, } as const; const STATE_DESCRIPTIONS = { ready: 'Waiting for the owner to name a change and its rollout plan.', assembling: 'Releaser assembles the release evidence bundle for the named change and commits it.', firstReview: 'Approver runs the first review round over the release evidence bundle.', answeringFindings: "Releaser fixes or explains each of Approver's first-round findings.", finalReview: 'Approver runs the second and final review round over the release evidence bundle.', approval: 'Captain asks the owner to approve the release after a review round with no findings.', awaitBossReply: "Waiting for Boss to answer the acting agent's question.", failed: 'RELEASE parked after a failure and waits for the owner to restart or resume it.', approved: 'RELEASE ended approved: Approver had no findings and the owner approved; the bundle path and the latest bundle commit hash are returned as approved for release.', heldByOwner: "RELEASE ended held: the owner did not approve the release; the owner's reason, or that the owner gave none, is returned.", heldWithFindings: "RELEASE ended held: findings remained after the second review round, so the owner was not asked to approve; Approver's open findings are returned.", } as const; type StateKey = keyof typeof STATE_DESCRIPTIONS; type FinalStateKey = 'approved' | 'heldByOwner' | 'heldWithFindings'; const RESUME_METADATA = { assembling: { sourceItem: 'release-1', asker: { kind: 'role', roleId: 'releaser' }, }, firstReview: { sourceItem: 'release-2', asker: { kind: 'role', roleId: 'approver' }, }, answeringFindings: { sourceItem: 'release-3', asker: { kind: 'role', roleId: 'releaser' }, }, finalReview: { sourceItem: 'release-4', asker: { kind: 'role', roleId: 'approver' }, }, approval: { sourceItem: 'release-5', asker: { kind: 'captain' }, }, } as const satisfies Record< ResumableStateId, { readonly sourceItem: ReleaseSourceItem; readonly asker: BossQuestionAsker } >; // --------------------------------------------------------------------------- // Structural helpers // --------------------------------------------------------------------------- const isRecord = ( value: unknown, ): value is Readonly> => typeof value === 'object' && value !== null && !Array.isArray(value); const isNonEmptyString = (value: unknown): value is string => typeof value === 'string' && value.trim().length > 0; const doneOutputOf = (event: unknown): unknown => isRecord(event) ? event.output : undefined; const actorErrorOf = (event: unknown): unknown => isRecord(event) ? event.error : undefined; /** Narrows a player done event to the declared player output contract. */ function playerOutputOf(event: unknown): PlayerOutput | undefined { const output = doneOutputOf(event); if (!isRecord(output)) return undefined; switch (output.guard) { case 'assembled': return isNonEmptyString(output.change) && isNonEmptyString(output.latestCommit) && isNonEmptyString(output.releaserOutput) ? { guard: 'assembled', change: output.change, latestCommit: output.latestCommit, releaserOutput: output.releaserOutput, } : undefined; case 'fixed': return isNonEmptyString(output.latestCommit) && isNonEmptyString(output.releaserOutput) ? { guard: 'fixed', latestCommit: output.latestCommit, releaserOutput: output.releaserOutput, } : undefined; case 'noChanges': return isNonEmptyString(output.releaserOutput) ? { guard: 'noChanges', releaserOutput: output.releaserOutput } : undefined; case 'hasFindings': return isNonEmptyString(output.approverFindings) ? { guard: 'hasFindings', approverFindings: output.approverFindings } : undefined; case 'noFindings': return { guard: 'noFindings' }; case 'needsBossReply': return isNonEmptyString(output.question) ? { guard: 'needsBossReply', question: output.question } : undefined; default: return undefined; } } /** Narrows a Captain done event to the declared Captain output contract. */ function captainOutputOf(event: unknown): CaptainOutput | undefined { const output = doneOutputOf(event); if (!isRecord(output)) return undefined; switch (output.guard) { case 'approved': return isNonEmptyString(output.bundlePath) && isNonEmptyString(output.releaseCommit) ? { guard: 'approved', bundlePath: output.bundlePath, releaseCommit: output.releaseCommit, } : undefined; case 'held': return isNonEmptyString(output.holdReason) ? { guard: 'held', holdReason: output.holdReason } : undefined; case 'needsBossReply': return isNonEmptyString(output.question) ? { guard: 'needsBossReply', question: output.question } : undefined; default: return undefined; } } /** Normalizes any thrown or malformed value to a JSON-safe error record. */ function errorRecord(value: unknown): ErrorRecord { if (value instanceof Error) { return { name: value.name || 'Error', message: value.message, ...(typeof value.stack === 'string' ? { stack: value.stack } : {}), }; } if (isRecord(value) && typeof value.message === 'string') { return { name: isNonEmptyString(value.name) ? value.name : 'Error', message: value.message, }; } return { name: 'Error', message: String(value) }; } // --------------------------------------------------------------------------- // Guards // --------------------------------------------------------------------------- type EventArgs = { readonly event: unknown }; type ContextEventArgs = { readonly context: ReleaseContext; readonly event: unknown; }; type MachineArgs = { readonly context: ReleaseContext; readonly event: ReleaseEvent; }; const acceptsPlayerOutcome = (outcome: PlayerGuard) => ({ event }: EventArgs): boolean => playerOutputOf(event)?.guard === outcome; /** * The owner's decision is available only after the owner has given a nonempty * answer to the approval question this same working leaf asked (release-5). * The `approval` question id binds the leaf, its source item, and its Captain * asker (RESUME_METADATA.approval). */ const ownerAnsweredApproval = (context: ReleaseContext): boolean => context.answeredQuestionId === 'approval' && isNonEmptyString(context.bossReply); const acceptsOwnerDecision = (outcome: Exclude) => ({ context, event }: ContextEventArgs): boolean => captainOutputOf(event)?.guard === outcome && ownerAnsweredApproval(context); const acceptCaptainNeedsBossReply = ({ event }: EventArgs): boolean => captainOutputOf(event)?.guard === 'needsBossReply'; // The entry guard validates the text the current event supplies, before the // entry action copies it into context. const validStartRelease = ({ event }: MachineArgs): boolean => event.type === 'START_RELEASE' && isNonEmptyString(event.callerInput); const repliesToPendingQuestion = ({ context, event }: MachineArgs): boolean => event.type === 'BOSS_REPLY' && context.pendingBossQuestion !== undefined && (event.questionId === undefined || event.questionId === context.pendingBossQuestion.questionId); const emptyBossReply = (args: MachineArgs): boolean => repliesToPendingQuestion(args) && args.event.type === 'BOSS_REPLY' && !isNonEmptyString(args.event.answer); /** Typed context each jumpable working leaf needs to be entered safely. */ const INTERRUPT_PRECONDITIONS = { assembling: (context: ReleaseContext) => isNonEmptyString(context.callerInput), firstReview: (context: ReleaseContext) => isNonEmptyString(context.callerInput) && isNonEmptyString(context.change) && isNonEmptyString(context.latestCommit) && isNonEmptyString(context.releaserOutput), answeringFindings: (context: ReleaseContext) => isNonEmptyString(context.callerInput) && isNonEmptyString(context.change) && isNonEmptyString(context.latestCommit) && context.approverOutcome === 'hasFindings' && isNonEmptyString(context.approverFindings), finalReview: (context: ReleaseContext) => isNonEmptyString(context.callerInput) && isNonEmptyString(context.change) && isNonEmptyString(context.latestCommit) && isNonEmptyString(context.approverFindings) && (context.releaserOutcome === 'fixed' || context.releaserOutcome === 'noChanges') && isNonEmptyString(context.releaserOutput), approval: (context: ReleaseContext) => isNonEmptyString(context.change) && isNonEmptyString(context.latestCommit) && context.approverOutcome === 'noFindings', } as const satisfies Record< JumpableStateId, (context: ReleaseContext) => boolean >; // --------------------------------------------------------------------------- // Transition helpers // --------------------------------------------------------------------------- /** One guarded root transition per jumpable state. */ function bossInterrupts( ids: Ids, ) { return ids.map((id) => ({ guard: ({ context, event }: MachineArgs) => event.type === 'BOSS_INTERRUPT' && event.targetId === id && INTERRUPT_PRECONDITIONS[id](context), target: `#${id}` as const, reenter: true, actions: 'resetForInterrupt' as const, })); } /** One guarded `BOSS_REPLY` resume transition per suspended working leaf. */ function resumableStates( ids: Ids, ) { return ids.map((id) => ({ guard: (args: MachineArgs) => repliesToPendingQuestion(args) && args.event.type === 'BOSS_REPLY' && isNonEmptyString(args.event.answer) && args.context.pendingBossQuestion?.resumeStateId === id, target: `#${id}` as const, reenter: true, actions: 'rememberBossReply' as const, })); } const bossReplyFields = (context: ReleaseContext) => ({ ...(context.pendingBossQuestion !== undefined ? { pendingBossQuestion: context.pendingBossQuestion } : {}), ...(context.bossReply !== undefined ? { bossReply: context.bossReply } : {}), }); const callerInputField = (context: ReleaseContext) => context.callerInput !== undefined ? { callerInput: context.callerInput } : {}; const changeField = (context: ReleaseContext) => context.change !== undefined ? { change: context.change } : {}; const latestCommitField = (context: ReleaseContext) => context.latestCommit !== undefined ? { latestCommit: context.latestCommit } : {}; const releaserOutputField = (context: ReleaseContext) => context.releaserOutput !== undefined ? { releaserOutput: context.releaserOutput } : {}; const approverFindingsField = (context: ReleaseContext) => context.approverFindings !== undefined ? { approverFindings: context.approverFindings } : {}; const acceptedOutcome = ( source: PlayerStateId, target: StateKey, outcome: PlayerGuard, ) => ({ type: 'playbook.acceptedOutcome', params: { source, target, acceptedOutcome: outcome }, }) as const; /** Approver affirmed a round with no findings: the owner is asked next (release-5). */ const noFindingsArm = (source: 'firstReview' | 'finalReview') => ({ guard: 'acceptNoFindings', target: '#approval', actions: [ acceptedOutcome(source, 'approval', 'noFindings'), 'rememberNoFindings', 'clearBossReplyContext', ], }) as const; const suspendArm = (source: PlayerStateId) => ({ guard: 'acceptNeedsBossReply', target: '#awaitBossReply', actions: [ acceptedOutcome(source, 'awaitBossReply', 'needsBossReply'), { type: 'setPendingBossQuestion', params: { resumeStateId: source } }, ], }) as const; const malformedPlayerOutputArm = { target: '#failed', actions: ['rememberMalformedPlayerOutput', 'clearBossReplyContext'], } as const; const actorOnError = { target: '#failed', actions: ['rememberActorError', 'clearBossReplyContext'], } as const; const startReleaseArm = { guard: 'validStartRelease', target: '#assembling', actions: 'startRelease', } as const; const meta = ( stateId: Exclude, role?: ReleaseRole, ) => ({ playbook: { stateId, description: STATE_DESCRIPTIONS[stateId], ...(role === undefined ? {} : { role }), }, }); const finalMeta = (stateId: FinalStateKey, terminal: 'success' | 'failure') => ({ playbook: { stateId, description: STATE_DESCRIPTIONS[stateId], terminal, }, }); // --------------------------------------------------------------------------- // Machine // --------------------------------------------------------------------------- export const releaseMachine = setup({ types: { context: {} as ReleaseContext, events: {} as ReleaseEvent, input: {} as ReleaseInput, output: {} as ReleaseOutput, }, actors: { captain: fromPromise(async () => { throw new Error('captain actor must be provided by the runner'); }), player: fromPromise(async () => { throw new Error('player actor must be provided by the runner'); }), }, actions: { 'playbook.acceptedOutcome': ( _args, _params: { readonly source: string; readonly target: string; readonly acceptedOutcome: string; }, ) => undefined, startRelease: assign(({ event }) => event.type === 'START_RELEASE' ? { callerInput: event.callerInput, change: undefined, latestCommit: undefined, releaserOutput: undefined, approverFindings: undefined, releaserOutcome: undefined, approverOutcome: undefined, bundlePath: undefined, releaseCommit: undefined, holdReason: undefined, verdict: undefined, lastError: undefined, pendingBossQuestion: undefined, bossReply: undefined, answeredQuestionId: undefined, } : {}, ), resetForInterrupt: assign({ bundlePath: undefined, releaseCommit: undefined, holdReason: undefined, verdict: undefined, lastError: undefined, pendingBossQuestion: undefined, bossReply: undefined, answeredQuestionId: undefined, }), rememberAssembled: assign(({ event }) => { const output = playerOutputOf(event); return output?.guard === 'assembled' ? { change: output.change, latestCommit: output.latestCommit, releaserOutput: output.releaserOutput, releaserOutcome: 'assembled' as const, lastError: undefined, } : {}; }), rememberFindingsAnswer: assign(({ event }) => { const output = playerOutputOf(event); if (output?.guard === 'fixed') { return { latestCommit: output.latestCommit, releaserOutput: output.releaserOutput, releaserOutcome: 'fixed' as const, lastError: undefined, }; } if (output?.guard === 'noChanges') { return { releaserOutput: output.releaserOutput, releaserOutcome: 'noChanges' as const, lastError: undefined, }; } return {}; }), rememberFindings: assign(({ event }) => { const output = playerOutputOf(event); return output?.guard === 'hasFindings' ? { approverFindings: output.approverFindings, approverOutcome: 'hasFindings' as const, lastError: undefined, } : {}; }), rememberNoFindings: assign({ approverOutcome: 'noFindings' as const, lastError: undefined, }), holdWithFindings: assign({ verdict: 'heldWithFindings' as const, }), rememberApproval: assign(({ event }) => { const output = captainOutputOf(event); return output?.guard === 'approved' ? { bundlePath: output.bundlePath, releaseCommit: output.releaseCommit, verdict: 'approved' as const, lastError: undefined, } : {}; }), rememberOwnerHold: assign(({ event }) => { const output = captainOutputOf(event); return output?.guard === 'held' ? { holdReason: output.holdReason, verdict: 'heldByOwner' as const, lastError: undefined, } : {}; }), setPendingBossQuestion: assign( ({ event }, params: { readonly resumeStateId: ResumableStateId }) => { const output = doneOutputOf(event); if ( !isRecord(output) || output.guard !== 'needsBossReply' || !isNonEmptyString(output.question) ) { return {}; } const metadata = RESUME_METADATA[params.resumeStateId]; const pendingBossQuestion: PendingBossQuestion = { questionId: params.resumeStateId, resumeStateId: params.resumeStateId, sourceItem: metadata.sourceItem, asker: metadata.asker, question: output.question, }; return { pendingBossQuestion, bossReply: undefined, answeredQuestionId: undefined, lastError: undefined, }; }, ), rememberBossReply: assign(({ context, event }) => event.type === 'BOSS_REPLY' && context.pendingBossQuestion !== undefined ? { bossReply: event.answer, answeredQuestionId: context.pendingBossQuestion.questionId, } : {}, ), clearBossReplyContext: assign({ pendingBossQuestion: undefined, bossReply: undefined, answeredQuestionId: undefined, }), rememberActorError: assign(({ event }) => ({ lastError: errorRecord(actorErrorOf(event)), })), rememberMalformedPlayerOutput: assign(({ event }) => { const output = doneOutputOf(event); return { lastError: { name: 'MalformedPlayerOutput', message: isRecord(output) && output.guard === 'needsBossReply' ? 'Player output declared needsBossReply without a question.' : 'Player output did not match an outcome declared for the working state.', }, }; }), rememberMalformedCaptainOutput: assign(({ event }) => { const output = doneOutputOf(event); const claimed = captainOutputOf(event); return { lastError: { name: 'MalformedCaptainOutput', message: isRecord(output) && output.guard === 'needsBossReply' ? 'Captain output declared needsBossReply without a question.' : claimed !== undefined ? "Captain reported the owner's decision before the owner answered the approval question." : 'Captain output did not match an outcome declared for the working state.', }, }; }), rememberMalformedBossReply: assign({ lastError: { name: 'MalformedBossReply', message: 'BOSS_REPLY carried an empty answer.', }, }), }, guards: { acceptAssembled: acceptsPlayerOutcome('assembled'), acceptFixed: acceptsPlayerOutcome('fixed'), acceptNoChanges: acceptsPlayerOutcome('noChanges'), acceptHasFindings: acceptsPlayerOutcome('hasFindings'), acceptNoFindings: acceptsPlayerOutcome('noFindings'), acceptNeedsBossReply: acceptsPlayerOutcome('needsBossReply'), acceptApproved: acceptsOwnerDecision('approved'), acceptHeld: acceptsOwnerDecision('held'), acceptCaptainNeedsBossReply, validStartRelease, emptyBossReply, }, }).createMachine({ id: 'release', initial: 'ready', context: {}, output: ({ context }): ReleaseOutput => { switch (context.verdict) { case 'approved': if ( !isNonEmptyString(context.bundlePath) || !isNonEmptyString(context.releaseCommit) ) { throw new Error( 'RELEASE ended approved without the bundle path and bundle commit hash', ); } return { status: 'approved', bundlePath: context.bundlePath, releaseCommit: context.releaseCommit, }; case 'heldByOwner': if (!isNonEmptyString(context.holdReason)) { throw new Error("RELEASE ended held by the owner without the owner's reason"); } return { status: 'held', holdReason: context.holdReason }; case 'heldWithFindings': if (!isNonEmptyString(context.approverFindings)) { throw new Error("RELEASE ended held without Approver's open findings"); } return { status: 'held', approverFindings: context.approverFindings }; default: throw new Error('RELEASE completed without a recorded verdict'); } }, on: { BOSS_INTERRUPT: bossInterrupts([ 'assembling', 'firstReview', 'answeringFindings', 'finalReview', 'approval', ] as const), }, states: { ready: { id: 'ready', tags: 'playbook.parked', description: STATE_DESCRIPTIONS.ready, meta: meta('ready'), on: { START_RELEASE: startReleaseArm, }, }, assembling: { id: 'assembling', tags: 'playbook.busy', description: STATE_DESCRIPTIONS.assembling, meta: meta('assembling', 'releaser'), invoke: { src: 'player', input: ({ context }): AssemblingPlayerInput => ({ stateId: 'assembling', role: 'releaser', sourceItem: 'release-1', prompt: ASSEMBLING_PROMPT, result: ASSEMBLING_RESULT, ...callerInputField(context), ...bossReplyFields(context), }), onDone: [ { guard: 'acceptAssembled', target: '#firstReview', actions: [ acceptedOutcome('assembling', 'firstReview', 'assembled'), 'rememberAssembled', 'clearBossReplyContext', ], }, suspendArm('assembling'), malformedPlayerOutputArm, ], onError: actorOnError, }, }, firstReview: { id: 'firstReview', tags: 'playbook.busy', description: STATE_DESCRIPTIONS.firstReview, meta: meta('firstReview', 'approver'), invoke: { src: 'player', input: ({ context }): FirstReviewPlayerInput => ({ stateId: 'firstReview', role: 'approver', sourceItem: 'release-2', prompt: FIRST_REVIEW_PROMPT, result: FIRST_REVIEW_RESULT, ...changeField(context), ...callerInputField(context), ...latestCommitField(context), ...releaserOutputField(context), ...bossReplyFields(context), }), onDone: [ { guard: 'acceptHasFindings', target: '#answeringFindings', actions: [ acceptedOutcome('firstReview', 'answeringFindings', 'hasFindings'), 'rememberFindings', 'clearBossReplyContext', ], }, noFindingsArm('firstReview'), suspendArm('firstReview'), malformedPlayerOutputArm, ], onError: actorOnError, }, }, answeringFindings: { id: 'answeringFindings', tags: 'playbook.busy', description: STATE_DESCRIPTIONS.answeringFindings, meta: meta('answeringFindings', 'releaser'), invoke: { src: 'player', input: ({ context }): AnsweringFindingsPlayerInput => ({ stateId: 'answeringFindings', role: 'releaser', sourceItem: 'release-3', prompt: ANSWERING_FINDINGS_PROMPT, result: ANSWERING_FINDINGS_RESULT, ...changeField(context), ...callerInputField(context), ...latestCommitField(context), ...approverFindingsField(context), ...bossReplyFields(context), }), onDone: [ { guard: 'acceptFixed', target: '#finalReview', actions: [ acceptedOutcome('answeringFindings', 'finalReview', 'fixed'), 'rememberFindingsAnswer', 'clearBossReplyContext', ], }, { guard: 'acceptNoChanges', target: '#finalReview', actions: [ acceptedOutcome('answeringFindings', 'finalReview', 'noChanges'), 'rememberFindingsAnswer', 'clearBossReplyContext', ], }, suspendArm('answeringFindings'), malformedPlayerOutputArm, ], onError: actorOnError, }, }, finalReview: { id: 'finalReview', tags: 'playbook.busy', description: STATE_DESCRIPTIONS.finalReview, meta: meta('finalReview', 'approver'), invoke: { src: 'player', input: ({ context }): FinalReviewPlayerInput => ({ stateId: 'finalReview', role: 'approver', sourceItem: 'release-4', prompt: FINAL_REVIEW_PROMPT, result: FINAL_REVIEW_RESULT, ...changeField(context), ...callerInputField(context), ...approverFindingsField(context), ...latestCommitField(context), ...releaserOutputField(context), ...bossReplyFields(context), }), onDone: [ { guard: 'acceptHasFindings', target: '#heldWithFindings', actions: [ acceptedOutcome('finalReview', 'heldWithFindings', 'hasFindings'), 'rememberFindings', 'holdWithFindings', 'clearBossReplyContext', ], }, noFindingsArm('finalReview'), suspendArm('finalReview'), malformedPlayerOutputArm, ], onError: actorOnError, }, }, approval: { id: 'approval', tags: 'playbook.busy', description: STATE_DESCRIPTIONS.approval, meta: meta('approval'), invoke: { src: 'captain', input: ({ context }): CaptainInput => ({ stateId: 'approval', sourceItem: 'release-5', prompt: APPROVAL_PROMPT, result: APPROVAL_RESULT, ...changeField(context), ...latestCommitField(context), ...bossReplyFields(context), }), onDone: [ { guard: 'acceptApproved', target: '#approved', actions: ['rememberApproval', 'clearBossReplyContext'], }, { guard: 'acceptHeld', target: '#heldByOwner', actions: ['rememberOwnerHold', 'clearBossReplyContext'], }, { guard: 'acceptCaptainNeedsBossReply', target: '#awaitBossReply', actions: { type: 'setPendingBossQuestion', params: { resumeStateId: 'approval' }, }, }, { target: '#failed', actions: ['rememberMalformedCaptainOutput', 'clearBossReplyContext'], }, ], onError: actorOnError, }, }, awaitBossReply: { id: 'awaitBossReply', tags: 'playbook.parked', description: STATE_DESCRIPTIONS.awaitBossReply, meta: meta('awaitBossReply'), on: { BOSS_REPLY: [ { guard: 'emptyBossReply', target: 'failed', actions: ['rememberMalformedBossReply', 'clearBossReplyContext'], }, ...resumableStates([ 'assembling', 'firstReview', 'answeringFindings', 'finalReview', 'approval', ] as const), ], START_RELEASE: startReleaseArm, }, }, failed: { id: 'failed', tags: 'playbook.parked', description: STATE_DESCRIPTIONS.failed, meta: meta('failed'), on: { START_RELEASE: startReleaseArm, }, }, approved: { id: 'approved', type: 'final', description: STATE_DESCRIPTIONS.approved, meta: finalMeta('approved', 'success'), }, heldByOwner: { id: 'heldByOwner', type: 'final', description: STATE_DESCRIPTIONS.heldByOwner, meta: finalMeta('heldByOwner', 'success'), }, heldWithFindings: { id: 'heldWithFindings', type: 'final', description: STATE_DESCRIPTIONS.heldWithFindings, meta: finalMeta('heldWithFindings', 'success'), }, }, }); export default releaseMachine;