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
4 changes: 2 additions & 2 deletions .github/scripts/backend-docs.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ test('public API tables enumerate the actual complete command/query contracts',
const commandText = api.split('## v1命令')[1].split('## Execution')[0]
assert.deepEqual(rows(queryText), [...queries].sort())
assert.deepEqual(rows(commandText), operations.filter(name => !queries.includes(name)).sort())
assert.equal(queries.length, 29); assert.equal(rows(commandText).length, 35)
for (const name of ['README.md', 'README.zh.md']) assert.match(read(name), /29查询\/35命令/)
assert.equal(queries.length, 30); assert.equal(rows(commandText).length, 35)
for (const name of ['README.md', 'README.zh.md']) assert.match(read(name), /30查询\/35命令/)
})

test('quickstart JSON, shell syntax and local documentation links stay usable', () => {
Expand Down
16 changes: 3 additions & 13 deletions PROJECT.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

#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.

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);
architecture lives in [docs/architecture.md](docs/architecture.md) and
Expand Down Expand Up @@ -62,19 +64,7 @@ complete live App/provider/release/multi-repository/UI acceptance or authorize d

## Fork Project State — 2026-10-31

### Backend delivery branch — 2026-11-02

`delivery/ai-centric-development-service` packages the complete S1–S6 backend at1811e3b8f,
including verified-exit, approved integration and Chief/SDK recovery fixes. Its root
README documents existing-service authentication, direct v1/legacy HTTP usage,
explicit task publication/start, isolated source demo setup and evidence limits.
This is a documentation/branch handoff, not another service listener, schema change,
main merge, runtime rollout or takeover of original frontend Task40/PR48.
No deployment credentials or operator private files are included.

### Earlier service implementation evidence

Detailed S1–S6 implementation/recovery history is [archived verbatim](docs/history/2026-11-03-service-integration.md). Existing contracts remain in their subsystem docs. New convergence work is not deployed; preserve healthy frontend Task40/PR48 and native schedules. Backend work remains in this owner-authorized session.
Earlier source snapshot/deployment checkpoints are [preserved verbatim](docs/history/2026-11-04-recovery-source-checkpoints.md). Current interfaces follow the README and subsystem contracts. Keep original frontend Task40/PR48 and healthy schedules; this backend work is direct-session source delivery without deployment.

## Historical upstream Project State

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ HTTP 入口为 `POST /api/bot`、事件为 `GET /api/project-events`,

| 需要 | 文档 |
| --- | --- |
| 对象、权限、全部29查询/35命令、原生接口、幂等与SSE | [API参考](docs/development-service-api.md) |
| 对象、权限、全部30查询/35命令、原生接口、幂等与SSE | [API参考](docs/development-service-api.md) |
| Chief/角色/Task/Handoff、咨询、个人代理、安全与值守、状态与上下文 | [协作指南](docs/development-service-guide.md) |
| 数据目录、账号平台、模型/SDK/仓库、停止重启、备份升级、排障 | [部署运维](docs/backend-operations.md) |
| 个人Chief、多Bot、群聊、公共Bot、原版安装与桌面功能 | [原版FlyBot](README.native.zh.md) · [English](README.native.md) |
Expand Down
2 changes: 1 addition & 1 deletion README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ HTTP 入口为 `POST /api/bot`、事件为 `GET /api/project-events`,

| 需要 | 文档 |
| --- | --- |
| 对象、权限、全部29查询/35命令、原生接口、幂等与SSE | [API参考](docs/development-service-api.md) |
| 对象、权限、全部30查询/35命令、原生接口、幂等与SSE | [API参考](docs/development-service-api.md) |
| Chief/角色/Task/Handoff、咨询、个人代理、安全与值守、状态与上下文 | [协作指南](docs/development-service-guide.md) |
| 数据目录、账号平台、模型/SDK/仓库、停止重启、备份升级、排障 | [部署运维](docs/backend-operations.md) |
| 个人Chief、多Bot、群聊、公共Bot、原版安装与桌面功能 | [原版FlyBot](README.native.zh.md) · [English](README.native.md) |
Expand Down
13 changes: 3 additions & 10 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

#2375: retire proven exited responsibility before fresh integration admission; source/isolated tests remain separate from live recovery.

#2377: continue the verified original seed or explicitly integrate a new baseline; no silent draft reset or cancelled Attempt revival.

## Resource authentication reliability — 2026-11-04

#2398: existing profile binding, correct SDK auth feedback and same-profile setup;
Expand Down Expand Up @@ -119,16 +121,7 @@ Completed M7.2–M7.5 implementation checkpoints are retained verbatim in
[the historical record](docs/history/2026-11-03-completed-m7-checkpoints.md). Current
role/resource/Session contracts and outstanding acceptance remain below and in the active roadmap.

### M7 validation update — 2026-11-01

Owner requested Codex-only real acceptance on the current machine using zy95-12 GitHub.
Run the opt-in dedicated-profile SDK/Workflow/PR/return/retry/acceptance path and record
its exact source SHA. Do not block this scoped demonstration on Claude configuration;
keep the broader mixed-Harness/two-resource criteria explicitly outstanding.

Codex-only local demonstration: passed at `73161f3`; real artifact PR #31 returned,
#32 re-tested/re-reviewed and script-accepted at its exact SHA. No merge/deployment.
Broader Claude/mixed-resource/browser-auth/restart checks remain separate future work.
Earlier Codex demo acceptance is [preserved verbatim](docs/history/2026-11-04-recovery-source-checkpoints.md); it grants no new live-provider, deployment or credential authority. Current tests and live acceptance remain distinct.

## Owner-authorized collaboration repairs — 2026-11-01

Expand Down
1 change: 1 addition & 0 deletions changelog.d/2377.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Preserve cancelled delivery drafts through an explicit, durable successor baseline choice and retain structured recovery lineage across queueing.
62 changes: 57 additions & 5 deletions core/src/host/project-delivery.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { execFile } from 'node:child_process'
import { createHash, randomUUID } from 'node:crypto'
import { mkdir, readFile, realpath, rm, writeFile, lstat } from 'node:fs/promises'
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
import { promisify } from 'node:util'
import { promisify, isDeepStrictEqual } from 'node:util'
import { ownEntry, setEntry } from './store.js'
import { createProjectReleases } from './project-releases.js'
import { sealedLogPage } from './project-history-pages.js'
Expand Down Expand Up @@ -102,6 +102,7 @@ export function createProjectDelivery(rt, { publisher, releaseProvider, roleArti
context.assertCurrent()
if (task.deliveryAttemptId !== attempt.id || attempt.cancelRequested || attempt.candidateSha !== effect.payload.candidateSha) throw new EffectCancelled('Publication changed during validation')
}
rt.projectEffects?.register('attempt.continue', { run: row => ({ attemptId: row.payload.attemptId, status: 'durable-continuation' }) }, { replace: true })
rt.projectEffects?.register('integration.admit', { run: row => ({ admissionId: row.payload.admissionId, status: 'durable-input-admission' }) }, { replace: true })
rt.projectEffects?.register('github.publish', {
validate: validatePublication,
Expand Down Expand Up @@ -229,6 +230,7 @@ export function createProjectDelivery(rt, { publisher, releaseProvider, roleArti
attempt.status = status
attempt.failure = { message: String(error?.message ?? error).slice(0, 1000), at: Date.now() }
attempt.updatedAt = Date.now()
if (attempt.protocol === 'project-handoff/v1' && attempt.runId) rt.bindProject({ kind: 'run', id: attempt.runId, projectId: attempt.projectId, taskId: attempt.taskId, attemptId: attempt.id, status: 'failed' })
rt.taskDeliveryState?.(attempt.taskId, attempt.id, 'failed', attempt.failure.message)
for (const lease of Object.values(attempt.resourceLeases ?? {})) await rt.projectResources?.release(lease.id, lease.generation)
await rt.save()
Expand Down Expand Up @@ -314,6 +316,36 @@ export function createProjectDelivery(rt, { publisher, releaseProvider, roleArti
await git(repository, ['worktree', 'add', '--detach', path, attempt.baseSha]); workspace.status = 'allocated'; await rt.save()
}
}
async function continuationState(attempt, actor, permission = 'configure') {
const task = taskOf(attempt.taskId)
rt.requireProjectAccess(actor, task.projectId, permission, task)
integrationReleased(attempt)
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)
if (task.deliveryAttemptId !== attempt.id) throw reject('Continuation Attempt changed during snapshot')
return { attemptId: attempt.id, generation: task.collaboration?.generation ?? 0, snapshot, latestBaseSha, baseSha: attempt.baseSha ?? null, seedSha: attempt.candidateSha ?? attempt.seedSha ?? attempt.baseSha ?? null,
retainedDraft: Boolean(snapshot && await workspaceGit(workspace, ['status', '--porcelain'])), inputs: structuredClone(attempt.integrationInputs ?? []), modes: ['continue', 'latest'] }
}
async function continuationOf(body, actor, task, previous, project, repositoryBinding) {
if (!previous || previous.protocol !== 'project-handoff/v1') {
if (body.continuation !== undefined) throw reject('Continuation needs an existing pool Attempt')
return null
}
if (body.continuation === undefined && !workspaceFor(previous, 'developer')) return null
const facts = await continuationState(previous, actor, body.continuation ? 'configure' : 'task-write'), selected = body.continuation
if (!selected) {
if (facts.retainedDraft) throw Object.assign(reject('Preserved draft requires an explicit continuation choice before starting another Attempt'), { code: 'bot/workspace-baseline', details: { kind: 'continuation-required', ...facts } })
return null
}
if (!['continue', 'latest'].includes(selected.mode) || selected.previousAttemptId !== previous.id || selected.generation !== facts.generation || !isDeepStrictEqual(selected.expectedWorkspace, facts.snapshot) || selected.mode === 'latest' && selected.expectedBaseSha !== facts.latestBaseSha) throw reject('Continuation workspace or generation changed; fetch a fresh continuation snapshot')
if (previous.requirement.repository !== project.repository || JSON.stringify(previous.repositoryBinding) !== JSON.stringify(repositoryBinding)) throw reject('Continuation repository binding changed')
if (selected.mode === 'continue' && JSON.stringify(previous.requirement) !== JSON.stringify(requirementsOf(task, project.repository))) throw reject('Continuation requirements changed; choose a new baseline explicitly')
if (selected.mode === 'continue') await verifyIntegrationInputs(previous)
const currentFacts = await continuationState(previous, actor)
if (!isDeepStrictEqual(currentFacts, facts)) throw reject('Continuation workspace changed during admission')
return { version: 'project-continuation/v1', mode: selected.mode, previousAttemptId: previous.id, sourceWorkspaceId: facts.snapshot?.workspaceId ?? null, snapshot: facts.snapshot, generation: facts.generation, latestBaseSha: facts.latestBaseSha, baseSha: facts.baseSha, seedSha: facts.seedSha, retainedDraft: facts.retainedDraft, admissions: (previous.integrationAdmissions ?? []).map(row => row.id), by: actor.memberId, at: Date.now() }
}
async function start(body, actor) {
const task = taskOf(body.taskId)
if (!task?.projectId) throw reject('A delivery needs a Project Task')
Expand All @@ -325,7 +357,7 @@ export function createProjectDelivery(rt, { publisher, releaseProvider, roleArti
const requestKey = hash(`${project.id}\n${task.id}\n${body.requestId}`)
const prior = Object.values(attempts()).find(attempt => attempt.requestKey === requestKey)
if (prior) {
if (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')
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')
Expand All @@ -334,16 +366,32 @@ export function createProjectDelivery(rt, { publisher, releaseProvider, roleArti
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) {
const currentFacts = await continuationState(previous, actor)
if (current(task.id) !== previous || currentFacts.generation !== continuation.generation || !isDeepStrictEqual(currentFacts.snapshot, continuation.snapshot) || continuation.mode === 'latest' && currentFacts.latestBaseSha !== continuation.latestBaseSha || continuation.mode === 'continue' && JSON.stringify(previous.requirement) !== JSON.stringify(requirementsOf(task, project.repository))) throw reject('Continuation changed before durable admission')
}
const feedback = previous?.failure ? { previousAttemptId: previous.id, kind: previous.returned ? 'human-return' : 'execution-failure', reason: previous.failure.message, candidateSha: previous.candidateSha ?? null } : null
const at = Date.now()
const attempt = { id: `attempt-${randomUUID()}`, projectId: project.id, taskId: task.id, requestKey,
workflowVersion: DELIVERY_VERSION, status: 'preparing', initiatedBy: actor.memberId, executionOwnerId: project.ownerUserId,
repositoryBinding, requirement: requirementsOf(task, project.repository), ...(initialTarget ? { initialTarget } : {}),
...(feedback ? { feedback } : {}), ...(publicationBinding ? { publicationBinding, seedSha: previous.candidateSha ?? previous.seedSha ?? publicationBinding.previousSha, baseSha: previous.baseSha } : {}), workspaces: {}, evidence: {}, createdAt: at, updatedAt: at }
...(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) }
setEntry(attempts(), attempt.id, attempt)
task.deliveryAttemptId = attempt.id
await rt.save() // Reserve idempotency and ownership before any filesystem effects.
if (continuation) {
if (!rt.projectEffects) throw reject('Continuation needs durable Effects')
const token = rt.projectEffects.stage({ kind: 'attempt.continue', key: requestKey, projectId: task.projectId, taskId: task.id, payload: { attemptId: attempt.id, previousAttemptId: previous.id, continuation } })
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 }
throw error
}
} else await rt.save() // Reserve ownership before filesystem effects.

try {
const definitions = rt.projectAgents?.deliveryDefinitions(project.id)
attempt.agentDefinitions = definitions
Expand Down Expand Up @@ -583,7 +631,7 @@ export function createProjectDelivery(rt, { publisher, releaseProvider, roleArti
if (!attempt || attempt.taskId !== task.id || attempt.projectId !== task.projectId) return null
try {
const project = projectOf(task.projectId, attempt)
return { 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,
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,
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 Expand Up @@ -952,6 +1000,10 @@ export function createProjectDelivery(rt, { publisher, releaseProvider, roleArti
if (ownEntry(attempts(), body.attemptId) !== attempt || source() !== evidence) throw reject('Log source changed during query')
return page
}
if (endpoint === 'project-attempt-continuation-state') {
const { attempt } = integrationAttempt(body, actor)
return continuationState(attempt, actor)
}
if (endpoint === 'project-attempt-integration-state') {
const { attempt, task } = integrationAttempt(body, actor)
integrationReleased(attempt)
Expand Down
Loading
Loading