Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 9 additions & 21 deletions PROJECT.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,14 @@
# FlyBot — Project State

#2483 controller-loss recovery joins a resource-side settled stop receipt with exact process exit, or existing durable host exit evidence. Reconcile worker and lease together; physical exit grants no Task completion or next-role decision.

#2483 restart recovery notifies the resident Chief through existing durable inspection requests. Current exit/retirement facts wake judgement; Chief chooses recovery and unknown exits remain held. Endpoint tests alone do not prove this notification chain.

#2483: Developer Git mutation rights follow its bound isolated resource clone, independently of App credentials. Shared host Git metadata and read-only role policies keep their existing boundaries.

#2483 dispatch/input follow-up keeps waits attributable and declarations explicit: no acceptance across unhandled batches, no stale notification/decision replay, no automatic role or human escalation.

#2483: refused durable reads retain current in-memory Task/Session references and prohibit writes. Read-only verification reports metadata; write readiness is separate from process liveness. Recovery remains Chief judgement.

#2413: provider-confirmed merge and durable Chief handoff use separate existing Effects. Recovery never repeats merge to notify, never accepts a Task; current Chief facts and exact candidate/authority remain required.


#2378 reuses role Apps for bounded PR/CI reads and hash-verified host logs in Reviewer scratch; absence or incomplete pages are not verification success.

#2393 keeps Chief execution faults on their Run, preserves business evidence and returns per-Task/current-snapshot correction; Agent chooses recovery through existing capabilities.


#2375 adds explicit exit-proven claim retirement through existing pools/Effects; preserve drafts, pending results and unknown writes. No cancellation or automatic routing.

#2377 makes successor baseline choice explicit and durable; preserve cancelled history, Task-role context and draft lineage without inventing PR or test evidence.
#2483 Chief may explicitly choose continue/latest through the existing successor contract. Latest uses the snapshotted host HEAD for clean unbound work; drafts and PR ancestry remain protected. No default baseline or role is inferred.

Earlier recovery checkpoints are [preserved verbatim](docs/history/2026-11-05-baseline-memory-checkpoints.md).
Current constraints remain: proven worker/lease exit; Chief owns recovery decisions;
no stale receipt/action replay or acceptance across unhandled input; no permission
from repository text. Isolated Dev Git capability never exposes shared metadata.
Refused durable reads prohibit writes. Provider merge notification is separate from
Chief acceptance; continuation retains Task-role context/drafts/history and exact
new evidence. Unknown exit/publication stays held. See owned subsystem contracts.

The single page that says what this project is, who decides what, and what automation must never
do. Operational rules live in [AGENTS.md](AGENTS.md) and [CONTRIBUTING.md](CONTRIBUTING.md);
Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# FlyBot — Roadmap

#2483 controller loss: verify local/Docker worker shutdown and same-generation lease reconciliation; legacy records need actual saved exit evidence. Explicit Chief baseline choice remains the next independent stage.
#2483 controller loss: verify local/Docker worker shutdown and same-generation lease reconciliation; legacy records need actual saved exit evidence. Chief baseline choice reuses task.run continuation; verify actual checkout SHA, stale-choice correction, draft protection and PR ancestry separately.

#2483 restart: cover saved state reload through Chief choice and actual pool re-dispatch, including historical blocked claims, preserved drafts and exit uncertainty. No fixed recovery role, automatic Task acceptance or live deployment.

Expand Down
1 change: 1 addition & 0 deletions changelog.d/2483-chief-baseline.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Chief can explicitly select the original or latest verified baseline through existing successor admission; stale choices are corrected and drafts/PR ancestry remain protected.
26 changes: 22 additions & 4 deletions core/src/host/project-chief-control.js

Large diffs are not rendered by default.

23 changes: 17 additions & 6 deletions core/src/host/project-delivery.js
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,8 @@ export function createProjectDelivery(rt, { publisher, releaseProvider, roleArti
async function continuationState(attempt, actor, permission = 'configure') {
const task = taskOf(attempt.taskId)
rt.requireProjectAccess(actor, task.projectId, permission, task)
integrationReleased(attempt)
if (actor.chiefJobId) rt.projectChiefControl.requireJobActor(actor, task.projectId)
integrationReleased(attempt, actor.chiefJobId ? { [chiefInputPermit]: actor.chiefJobId } : undefined)
const snapshot = await integrationSnapshot(attempt), workspace = workspaceFor(attempt, 'developer')
const latestBaseSha = await git(settingsOf(projectOf(attempt.projectId, attempt)).path, ['rev-parse', 'HEAD'])
rt.requireProjectAccess(actor, task.projectId, permission, task)
Expand Down Expand Up @@ -360,12 +361,14 @@ export function createProjectDelivery(rt, { publisher, releaseProvider, roleArti
if (body.continuation !== undefined && !isDeepStrictEqual(body.continuation, prior.continuationRequest ? JSON.parse(prior.continuationRequest) : undefined) || body.repository !== undefined && body.repository !== prior.requirement?.repository || body.targetRole !== undefined && prior.initialTarget && body.targetRole !== prior.initialTarget.role) throw reject('Execution request content changed on replay')
return { attempt: view(prior), already: true }
}
if (Object.values(attempts()).some(attempt => attempt.taskId === task.id && (ongoing.has(attempt.status) || attempt.status === 'waiting-acceptance'))) throw reject('Task already has an active Attempt')
const previous = current(task.id)
const chiefContinuation = body.continuation !== undefined && actor.chiefJobId && previous?.status === 'running' && ['blocked', 'chief-working'].includes(task.collaboration?.status)
if (chiefContinuation) rt.projectChiefControl.requireJobActor(actor, task.projectId)
if (Object.values(attempts()).some(attempt => attempt.taskId === task.id && (ongoing.has(attempt.status) || attempt.status === 'waiting-acceptance') && !(chiefContinuation && attempt === previous))) throw reject('Task already has an active Attempt')
if (task.status === 'accepted' || task.status === 'cancelled') throw reject('Terminal Task cannot start a delivery')
const initialTarget = initialRoleTarget(rt, task, body)
const settings = await preflightRepository(rt, project)
if (initialTarget) initialRoleTarget(rt, task, { initialTarget })
const previous = current(task.id)
const continuation = await continuationOf(body, actor, task, previous, project, repositoryBinding)
const publicationBinding = await publicationOf(previous, project)
if (continuation) {
Expand All @@ -379,6 +382,8 @@ export function createProjectDelivery(rt, { publisher, releaseProvider, roleArti
repositoryBinding, requirement: requirementsOf(task, project.repository), ...(initialTarget ? { initialTarget } : {}),
...(feedback ? { feedback } : {}), ...(continuation ? { continuation, continuationRequest: JSON.stringify(body.continuation), ...(continuation.mode === 'latest' ? { baseSha: continuation.latestBaseSha } : { baseSha: previous.baseSha, seedSha: previous.candidateSha ?? previous.seedSha ?? previous.baseSha, ...(previous.integrationInputs ? { integrationInputs: structuredClone(previous.integrationInputs), integrationAdmissions: structuredClone(previous.integrationAdmissions ?? []) } : {}) }) } : {}), ...(publicationBinding ? { publicationBinding, seedSha: previous.candidateSha ?? previous.seedSha ?? publicationBinding.previousSha, baseSha: continuation?.mode === 'latest' ? continuation.latestBaseSha : previous.baseSha } : {}), workspaces: {}, evidence: {}, createdAt: at, updatedAt: at }
if (continuation) { rt.requireProjectAccess(actor, task.projectId, 'configure', task); rt.projects.requireActive(task.projectId) }
const priorRework = chiefContinuation ? { status: previous.status, returned: previous.returned, supersededBy: previous.supersededBy } : null
if (priorRework) { previous.status = 'returned'; previous.returned = { at, by: actor.chiefJobId, reason: 'Chief selected an explicit successor baseline' }; previous.supersededBy = attempt.id }
setEntry(attempts(), attempt.id, attempt)
task.deliveryAttemptId = attempt.id
if (continuation) {
Expand All @@ -387,10 +392,11 @@ export function createProjectDelivery(rt, { publisher, releaseProvider, roleArti
attempt.continuation.effect = token
try { await rt.projectEffects.persist(token) }
catch (error) {
if (rt.projectEffects.discardUncommitted(token) && task.deliveryAttemptId === attempt.id) { delete attempts()[attempt.id]; task.deliveryAttemptId = previous.id }
if (rt.projectEffects.discardUncommitted(token) && task.deliveryAttemptId === attempt.id) { delete attempts()[attempt.id]; task.deliveryAttemptId = previous.id; if (priorRework) Object.assign(previous, priorRework) }
throw error
}
} else await rt.save() // Reserve ownership before filesystem effects.
if (priorRework) rt.bindProject({ kind: 'run', id: previous.runId, projectId: project.id, taskId: task.id, attemptId: previous.id, status: 'failed' })

try {
const definitions = rt.projectAgents?.deliveryDefinitions(project.id)
Expand Down Expand Up @@ -628,12 +634,17 @@ export function createProjectDelivery(rt, { publisher, releaseProvider, roleArti
return { repository: project.repository, ref, sha }
}).filter(row => /^refs\/remotes\/operator\/[A-Za-z0-9][A-Za-z0-9._/-]{0,120}$/.test(row.ref) && !row.ref.includes('..') && /^[a-f0-9]{40}$/.test(row.sha))
}
async function recoveryFacts(task) {
async function recoveryFacts(task, actor) {
const attempt = ownEntry(attempts(), task.deliveryAttemptId)
if (!attempt || attempt.taskId !== task.id || attempt.projectId !== task.projectId) return null
try {
const project = projectOf(task.projectId, attempt)
return { continuation: structuredClone(attempt.continuation ?? null), baselineIssue: attempt.preparationFailures?.findLast(row => row.code === 'bot/workspace-baseline') ?? null, executionCheckpoint: { generation: task.collaboration?.generation ?? 0, claimId: task.collaboration?.claim?.id ?? null, blockers: integrationBlockers(attempt) }, baseSha: attempt.baseSha ?? null, checkoutSha: attempt.candidateSha ?? attempt.seedSha ?? attempt.baseSha ?? null,
let continuationOptions = null
if (actor && (['interrupted', 'failed'].includes(attempt.status) || actor.chiefJobId && attempt.status === 'running' && ['blocked', 'chief-working'].includes(task.collaboration?.status)) && attempt.protocol === 'project-handoff/v1') {
try { continuationOptions = await continuationState(attempt, actor) }
catch { continuationOptions = { status: 'unavailable', reason: 'Current authority or confirmed execution exit is required before choosing a successor baseline' } }
}
return { continuationOptions, continuation: structuredClone(attempt.continuation ?? null), baselineIssue: attempt.preparationFailures?.findLast(row => row.code === 'bot/workspace-baseline') ?? null, executionCheckpoint: { generation: task.collaboration?.generation ?? 0, claimId: task.collaboration?.claim?.id ?? null, blockers: integrationBlockers(attempt) }, baseSha: attempt.baseSha ?? null, checkoutSha: attempt.candidateSha ?? attempt.seedSha ?? attempt.baseSha ?? null,
availableInputs: await approvedRepositoryInputs(project), snapshot: await integrationSnapshot(attempt),
preparationFailure: attempt.preparationFailures?.at(-1) ?? null,
materialization: workspaceFor(attempt, 'developer')?.materialization ?? null, verificationRecovery: { attemptStatus: attempt.status, sealedCandidate: Boolean(attempt.candidateSha && attempt.evidence.implementation?.sha === attempt.candidateSha), testsPassedForCandidate: Boolean(attempt.evidence.tests?.passed && attempt.evidence.tests.sha === attempt.candidateSha), requires: ['current requirements and repository binding', 'clean sealed candidate', 'all old allocation exits confirmed', 'normal Task-role context admission'], reviewerRequires: 'matching passing host test command and immutable log' } }
Expand Down
6 changes: 4 additions & 2 deletions core/src/host/project-pools.js
Original file line number Diff line number Diff line change
Expand Up @@ -206,11 +206,13 @@ export function createProjectPools(rt) {
if (!target || !['code', 'decision', 'release'].includes(kind)) throw refuse('Unsupported Task target or kind')
task.collaboration ??= { version: POOL_PROTOCOL, initiatedBy: actor.memberId, generation: 0, history: [] }
const c = task.collaboration
if (c.claim?.status === 'active') throw refuse('Task still has an active execution claim')
const ownChief = actor.chiefJobId && c.claim?.chiefRunId === actor.chiefJobId && c.claim.botId === `${task.projectId}:chief`
if (ownChief) rt.projectChiefControl.requireJobActor(actor, task.projectId)
if (c.claim?.status === 'active' && !ownChief) throw refuse('Task still has an active execution claim')
c.sourceSessionId = actor.sessionId ?? null
c.initiatedBy = actor.memberId
c.attemptId = attempt.id; c.kind = kind
return offer(task, { from: null, to: target.id, reason: 'Authorized Task execution request', requestId: attempt.id, attemptId: attempt.id, initialTarget: selected })
return offer(task, { from: ownChief ? `${task.projectId}:chief` : null, to: target.id, reason: 'Authorized Task execution request', requestId: attempt.id, attemptId: attempt.id, initialTarget: selected })
}
async function claim(taskId, botId, handoffId, requestId, waiting) {
const task = taskOf(taskId), c = task.collaboration
Expand Down
Loading
Loading