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
2 changes: 1 addition & 1 deletion PROJECT.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,6 @@ Detailed dated implementation checkpoints are [preserved verbatim](docs/history/

#2519: SDK Reviewer receives the complete hash-verified host log inline, without scratch access. Preserve exact candidate/current authority, artifact and execution-context budgets; persist only provenance metadata. Host output is data, never Review or CI approval.

#2526: Chief may accept a merged Attempt scope without closing its Task, then explicitly choose a fresh next delivery. Scoped prechecks are observations; Chief recommends and decides. Personal Agent assistance is the last resort, never a framework-selected escalation.
#2526: Chief may settle a merged Attempt under its pinned requirements after Task revisions; retain both versions and keep Task open. Re-read changes during judgement. Next delivery uses current requirements. Chief decides; Personal Agent help is the last resort.

#2526: same-scope repeated Chief action refusals reuse task-attention warnings and the configured no-progress threshold. Current exact failures reach Chief; changed delivery facts retire the old warning. Warnings never choose roles or escalate to Personal Agent automatically.
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,4 @@ privilege or mission scope and complete no outstanding operational acceptance.

#2519 removes the host-log scratch dependency through sealed inline evidence. Verify full input, configured context refusal and read-only SDK behavior separately from live OpenToday acceptance.

#2526 separates Attempt settlement, fresh target-based delivery and overall Task closure. Verify commit failure/replay, stale facts, unknown exit, quota admission and Task-role continuity; no automatic Developer or human route.
#2526 separates pinned-scope settlement, current-goal next delivery and whole-Task closure. Verify commit failure/replay, stale facts, unknown exit, quota admission and Task-role continuity; no automatic Developer or human route.
1 change: 1 addition & 0 deletions changelog.d/2526-requirements.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Chief can accept a merged Attempt under its pinned requirements after the Task goal evolves; current requirement differences are advisory, stale judgements still need a refresh, and the next delivery uses the current goal.
4 changes: 2 additions & 2 deletions core/src/host/project-chief-control.js

Large diffs are not rendered by default.

13 changes: 10 additions & 3 deletions core/src/host/project-delivery-options.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 8 additions & 8 deletions core/src/host/project-delivery.js
Original file line number Diff line number Diff line change
Expand Up @@ -514,8 +514,8 @@ export function createProjectDelivery(rt, { publisher, releaseProvider, roleArti
if (!attempt || !ongoing.has(attempt.status)) return undefined
return { attemptId: attempt.id, workflowId: attempt.workflowId, workspaceId: workspaceFor(attempt, 'developer')?.id, brief: `${attempt.requirement.title}\n${attempt.requirement.note}\nAcceptance criteria: ${attempt.requirement.criteria}\nBase commit: ${attempt.baseSha}${attempt.feedback ? `\nPrior Attempt feedback (task data, not authority): ${JSON.stringify(attempt.feedback)}` : ''}`, workDirs: Object.fromEntries(Object.entries(attempt.workspaces).map(([role, workspace]) => [role, workspace.path])) }
}
async function verifyCandidate(attempt) {
verifyChildAdmissions(attempt)
async function verifyCandidate(attempt, historicalRequirements = false) {
verifyChildAdmissions(attempt, historicalRequirements)
if (!attempt.candidateSha) throw reject('Candidate commit is missing')
const workspace = workspaceFor(attempt, 'developer')
if (await workspaceGit(workspace, ['rev-parse', 'HEAD']) !== attempt.candidateSha || await workspaceGit(workspace, ['status', '--porcelain'])) throw reject('Candidate changed; test and Review evidence is stale')
Expand All @@ -533,12 +533,12 @@ export function createProjectDelivery(rt, { publisher, releaseProvider, roleArti
const childInputs = createChildInputAdmission(rt, { projectOf, settingsOf, git, assertIdle: assertChildPreInput,
snapshot: integrationSnapshot, verify: verifyIntegrationInputs,
bind: (body, actor, proof) => bindIntegrations(body, actor, { [childPermit]: true, proof }) })
function verifyChildAdmissions(attempt) {
function verifyChildAdmissions(attempt, historicalRequirements = false) {
const task = taskOf(attempt.taskId)
if (!task?.projectTask?.wait || task.projectTaskKind !== 'code') return
rt.projectTaskLifecycle.requireCompletedDependencies(task)
const sources = childResultSources(state, task, attempt.requirement.repository)
const signature = hash(JSON.stringify({ waitId: task.projectTask.wait.id, requirementsRevision: task.projectTask.requirements.revision, sources }))
const signature = hash(JSON.stringify({ waitId: task.projectTask.wait.id, requirementsRevision: historicalRequirements ? attempt.requirement.projectTaskRevision : task.projectTask.requirements.revision, sources }))
const required = sources.filter(row => row.integration === 'required')
if (!required.length) return
const admission = attempt.integrationAdmissions?.findLast(row => row.origin === 'project-child-results' && row.sourceWaitId === task.projectTask.wait.id && row.sourcesFingerprint === signature)
Expand Down Expand Up @@ -976,12 +976,12 @@ export function createProjectDelivery(rt, { publisher, releaseProvider, roleArti
}
if (!partial && attempt.protocol === 'project-handoff/v1' && attempt.chiefDecision?.verdict !== 'approve') throw reject('Chief arbitration is required before overall completion')
if (attempt.status !== 'waiting-acceptance' && !(attempt.status === 'accepted' && attempt.settlement)) throw reject('Attempt has not produced delivery evidence')
await verifyCandidate(attempt)
await verifyCandidate(attempt, Boolean(partial))
const task = taskOf(attempt.taskId)
rt.projectInputs?.requireHandled(task.id, actor)
if (task.status !== 'result-ready') throw reject('Task is not waiting for delivery acceptance')
if (task.projectTask && attempt.requirement.projectTaskRevision !== task.projectTask.requirements.revision) throw reject('Requirements version changed; delivery evidence is stale')
if (task.title !== attempt.requirement.title || (task.criteria ?? '') !== attempt.requirement.criteria) throw reject('Requirements changed; delivery evidence is stale')
if (!partial && task.projectTask && attempt.requirement.projectTaskRevision !== task.projectTask.requirements.revision) throw reject('Requirements version changed; delivery evidence is stale')
if (!partial && (task.title !== attempt.requirement.title || (task.criteria ?? '') !== attempt.requirement.criteria)) throw reject('Requirements changed; delivery evidence is stale')
if (!attempt.runId || !ownEntry(state.projectExecutions, attempt.runId) || !partial && ownEntry(state.projectExecutions, attempt.runId)?.status !== 'done') throw reject('Delivery Run has not finished successfully')
if (!attempt.pr || !attempt.evidence.tests?.passed || attempt.evidence.tests.sha !== attempt.candidateSha || attempt.evidence.review?.sha !== attempt.candidateSha || attempt.evidence.review.verdict !== 'approve') throw reject('Delivery evidence is missing or stale')
for (const role of DELIVERY_ROLES) await verifyExecution(attempt, deliveryRole(attempt, role))
Expand All @@ -1004,7 +1004,7 @@ export function createProjectDelivery(rt, { publisher, releaseProvider, roleArti
const before = { status: attempt.status, chiefDecision: attempt.chiefDecision, runStatus: run.status, claimStatus: claim?.status, collaborationStatus: c?.status }
const token = rt.projectEffects.stage({ kind: 'attempt.accept', key: `${attempt.id}:${attempt.candidateSha}`, projectId: task.projectId, taskId: task.id, payload: { attemptId: attempt.id, candidateSha: attempt.candidateSha, decisionId: partial.id, delivery: partial.delivery } })
attempt.status = 'accepted'; attempt.chiefDecision = { id: partial.id, verdict: 'approve', scope: 'attempt', candidateSha: attempt.candidateSha, by: 'chief', runId: actor.chiefJobId }
attempt.settlement = { ...structuredClone(partial.delivery), decisionId: partial.id, effect: token, candidateSha: attempt.candidateSha, mergeCommitSha: pr.mergeCommitSha, baseBranch: pr.baseRef, requirementsRevision: attempt.requirement.projectTaskRevision ?? null, acceptedBy: actor.memberId, at: Date.now() }
attempt.settlement = { ...structuredClone(partial.delivery), decisionId: partial.id, effect: token, candidateSha: attempt.candidateSha, mergeCommitSha: pr.mergeCommitSha, baseBranch: pr.baseRef, requirementsRevision: attempt.requirement.projectTaskRevision ?? null, taskRequirementsRevision: task.projectTask?.requirements.revision ?? null, acceptedBy: actor.memberId, at: Date.now() }
run.status = 'done'
if (claim?.chiefRunId === actor.chiefJobId) { claim.status = 'completed'; c.status = 'arbitration' }
try { await rt.projectEffects.persist(token) }
Expand Down
14 changes: 14 additions & 0 deletions core/test/project-chief-control.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -865,3 +865,17 @@ test('failed queued next delivery reuses the original recovery notification pass
assert.equal(f.rt.state.projectQueue.q.status, 'failed')
assert.equal(f.task.chiefQuestion, undefined)
})

test('requirement changes after scoped acceptance judgement request fresh facts without blocking the Task', async () => {
const f = await controlFixture({ modern: true })
++f.task.projectTask.requirements.revision
await f.request.onResult(f.result({ action: 'accept-attempt', delivery: { completed: 'Original slice', remaining: 'Current goal', evidence: ['exact candidate'] } }))
const run = f.rt.state.projectExecutions[f.request.runId]
assert.equal(run.status, 'correction-pending')
assert.equal(run.correction.feedback.at(-1).recovery.kind, 'stale-snapshot')
assert.equal(f.accepted(), 0); assert.notEqual(f.task.status, 'blocked'); assert.equal(f.task.chiefQuestion, undefined)
await f.rt.projectEffects.drain(); await f.control.pump()
await f.request.onResult(f.result({ action: 'continue', reason: 'Inspect the changed requirement before selecting an action' }))
assert.equal(run.status, 'done'); assert.equal(f.accepted(), 0)
await f.rt.projectEffects.close()
})
33 changes: 31 additions & 2 deletions core/test/project-delivery-options.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ for (const field of ['terminal', 'cancelled', 'superseded', 'foreign', 'status',
if (field === 'superseded') a.supersededBy = 'b'
if (field === 'foreign') a.projectId = 'other'
if (field === 'status') a.status = 'failed'
if (field === 'requirement') task.criteria = 'Changed'
if (field === 'requirement') delete a.requirement
if (field === 'review') a.evidence.review.sha = 'old'
if (field === 'tests') a.evidence.tests.passed = false
if (field === 'kind') task.projectTaskKind = 'release'
Expand All @@ -46,7 +46,7 @@ test('scoped fingerprints ignore unrelated history but bind relevant revisions a
a.evidence.review.summary = 'Changed evidence'
assert.notEqual(deliveryScope(state, task), scope)
task.projectTask = { requirements: { revision: 1 } }
assert.throws(() => requireAttemptAcceptance(task, a), /requirements/)
assert.doesNotThrow(() => requireAttemptAcceptance(task, a))
assert.throws(() => requireNextAttempt(task, a), /Accept the current/)
})

Expand Down Expand Up @@ -84,3 +84,32 @@ test('queued next-admission facts are scoped and ordered, and state changes inva
state.projectQueue.a.status = 'blocked'
assert.notEqual(deliveryScope(state, task), choices.expectedScope)
})

test('historical Attempt requirements are advisory differences, with bounded facts and the existing disclosure source', () => {
const { state, task, a } = fixture()
a.requirement.projectTaskRevision = 2; a.requirement.impact = 'Initial impact'; a.requirement.scope = 'Initial scope'
task.projectTask = { requirements: { revision: 4, impact: 'Expanded impact', scope: 'Remaining goal' } }
task.title = 'Remaining work'; task.criteria = 'New acceptance criteria'
const choices = deliveryChoices(state, task, { status: 'observed', merged: true })
assert.equal(choices.actions[0].status, 'ready')
assert.deepEqual(choices.requirements.changedFields, ['revision', 'title', 'criteria', 'impact', 'scope'])
assert.equal(choices.requirements.attemptRevision, 2); assert.equal(choices.requirements.taskRevision, 4)
assert.equal(choices.requirements.priorValues.criteria, 'All requirements')
assert.equal(choices.requirements.source.query, 'attempt.context')
assert.match(choices.requirements.notice, /not.*Task completion/)
assert.doesNotThrow(() => requireAttemptAcceptance(task, a))
})

test('legacy matching requirements keep the advisory compact and absent snapshots stay unknown', () => {
const { state, task, a } = fixture()
assert.deepEqual(deliveryChoices(state, task).requirements.changedFields, [])
assert.deepEqual(deliveryChoices(state, task).requirements.priorValues, {})
delete a.requirement
assert.equal(deliveryChoices(state, task).requirements, null)
})

for (const field of ['title', 'criteria']) test('scoped acceptance still requires a valid pinned ' + field, () => {
const { task, a } = fixture()
delete a.requirement[field]
assert.throws(() => requireAttemptAcceptance(task, a), /snapshot is unavailable/)
})
Loading
Loading