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 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/#2560: Settle pinned Attempt scope after Task revisions; Task stays open. Chief chooses current-goal next delivery with a validated snapshot reference. Evidence/identity/rights/exit remain gates; human help is Chief's 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 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.
#2526 separates pinned-scope settlement, current-goal next delivery via validated references (#2560), 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/2560.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Chief can start the next delivery by referencing its observed continuation facts, without copying workspace and SHA fields; ambiguous targets, stale facts and explicitly incorrect fields still require correction, and actual bindings remain auditable.
54 changes: 39 additions & 15 deletions core/src/host/project-chief-control.js

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion core/src/host/project-chief-protocol.js

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

2 changes: 1 addition & 1 deletion core/src/host/project-delivery-options.js

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

95 changes: 95 additions & 0 deletions core/test/project-chief-control.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import assert from 'node:assert/strict'
import { createHash } from 'node:crypto'
import { test } from 'node:test'
import { createProjectChiefControl, inspectionFacts, allocationProof, inspectionResources } from '../src/host/project-chief-control.js'
const createHashForTest = value => createHash('sha256').update(JSON.stringify(value)).digest('hex')
const thresholds = { maxHandoffsWithoutProgress: 3, longExecutionMs: 10000, missingHeartbeatMs: 5000, longWaitMs: 8000 }
test('Chief inspection counts verified artifact progress rather than message or transfer churn', () => {
const task = { id: 't', updatedAt: 1, deliveryAttemptId: 'a', collaboration: { status: 'queued', generation: 7, history: Array.from({ length: 5 }, (_, index) => ({ evidence: [], reason: 'Progress message ' + index, createdAt: index + 1 })) } }
Expand Down Expand Up @@ -879,3 +881,96 @@ test('requirement changes after scoped acceptance judgement request fresh facts
assert.equal(run.status, 'done'); assert.equal(f.accepted(), 0)
await f.rt.projectEffects.close()
})

async function referenceFixture({ modes = ['next'], targetCount = 1, policyRole = 'developer', read } = {}) {
const { deliveryScope } = await import('../src/host/project-delivery-options.js')
let facts, calls = [], catalog, reads = 0
const f = await controlFixture({ prepare(rt, task) {
const role = policyRole === 'developer' ? 'developer' : 'implementer'
catalog = { revision: 1, roles: [{ id: 'p:chief', role: 'chief' }, ...Array.from({ length: targetCount }, (_, index) => ({ id: 'p:' + role + (index ? index : ''), role: role + (index ? index : ''), policyRole: 'developer' }))] }
rt.projectAgents.deliveryDefinitions = () => catalog
const scope = deliveryScope(rt.state, task)
facts = { attemptId: 'a', generation: 1, snapshot: { workspaceId: 'w', sha256: 'workspace' }, latestBaseSha: 'a'.repeat(40), modes, expectedScope: scope }
rt.projectDelivery.recoveryFacts = async () => { facts.expectedScope = deliveryScope(rt.state, task); return { continuationOptions: structuredClone(facts), nextActions: { actions: [{ action: 'start-next', status: 'ready' }] } } }
rt.projectDelivery.endpoint = async () => { ++reads; return read ? read(facts, rt, task, catalog, reads) : structuredClone(facts) }
rt.projectScheduler = { wake() {}, request: async (args) => { calls.push(structuredClone(args)); return { queue: { id: 'queue-reference' } } } }
} })
return { ...f, facts, calls, catalog }
}

test('next snapshot reference binds current fields and the unique custom implementation role without mutating the decision', async () => {
const f = await referenceFixture({ policyRole: 'custom' })
const decision = { action: 'start-next', mode: 'next', continuationRef: f.facts.expectedScope }
const original = structuredClone(decision), result = f.result(decision)
await f.request.onResult(result); await f.request.onResult(result)
assert.deepEqual(decision, original); assert.equal(f.calls.length, 1, JSON.stringify(f.rt.state.projectExecutions))
assert.equal(f.calls[0].targetRole, 'implementer')
const audit = Object.values(f.rt.state.projectChiefDecisions).find(row => row.status === 'applied')
assert.equal(audit.continuationBinding.source, 'continuation-ref')
assert.equal(audit.continuationBinding.targetBotId, 'p:implementer')
assert.equal(audit.queueId, 'queue-reference'); assert.equal(audit.successorAttemptId, null)
assert.equal(f.calls[0].continuation.expectedBaseSha, f.facts.latestBaseSha)
assert.deepEqual(f.calls[0].continuation.expectedWorkspace, f.facts.snapshot)
const intent = createHashForTest(JSON.parse(result.text).decisions[0])
assert.equal(f.rt.state.projectExecutions[f.request.runId].appliedDecisions[0].intent, intent)
const visible = f.control.view('p', { memberId: 'owner' }).runs.find(run => run.id === f.request.runId).appliedDecisions[0].continuationBinding
assert.deepEqual(visible, audit.continuationBinding)
visible.continuation.expectedBaseSha = 'changed-view-only'; assert.equal(audit.continuationBinding.continuation.expectedBaseSha, f.facts.latestBaseSha)
audit.projectId = 'foreign'
assert.equal(f.control.view('p', { memberId: 'owner' }).runs.find(run => run.id === f.request.runId).appliedDecisions[0].continuationBinding, undefined)
})

for (const kind of ['ambiguous-target', 'no-target', 'ambiguous-mode', 'unavailable', 'wrong-ref', 'bad-fields', 'bad-mode', 'not-ready', 'foreign-attempt', 'target-changed', 'workspace-changed', 'local-changed', 'roles-changed', 'rights-revoked']) test('next reference refuses ' + kind + ' without dispatch or inferred recovery', async () => {
const f = await referenceFixture({ targetCount: kind === 'ambiguous-target' ? 2 : kind === 'no-target' ? 0 : 1, modes: kind === 'ambiguous-mode' ? ['next', 'latest'] : ['next'], read: async (facts, rt, task, catalog, reads) => {
if (kind === 'target-changed') return { ...structuredClone(facts), latestBaseSha: 'b'.repeat(40) }
if (kind === 'workspace-changed') return { ...structuredClone(facts), snapshot: { workspaceId: 'other' } }
if (kind === 'local-changed' && reads === 2) task.criteria = 'Changed during validation'
if (kind === 'roles-changed' && reads === 2) catalog.revision++
if (kind === 'rights-revoked' && reads === 2) rt.requireProjectAccess = () => { throw Error('Current role denied') }
return structuredClone(facts)
} })
const saved = f.request.projectFacts.tasks[0]
if (kind === 'unavailable') saved.repositoryRecovery.continuationOptions.status = 'unavailable'
if (kind === 'not-ready') saved.repositoryRecovery.nextActions.actions[0].status = 'blocked'
if (kind === 'foreign-attempt') saved.repositoryRecovery.continuationOptions.attemptId = 'other'
const selection = kind === 'wrong-ref' ? { continuationRef: 'wrong' } : kind === 'bad-fields' ? { continuation: { mode: 'next', previousAttemptId: 'a', generation: 1, expectedWorkspace: {}, expectedBaseSha: f.facts.latestBaseSha } } : kind === 'bad-mode' ? { mode: 'latest' } : {}
await f.request.onResult(f.result({ action: 'start-next', ...selection }))
assert.equal(f.calls.length, 0)
assert.equal(f.task.chiefQuestion, undefined)
const run = f.rt.state.projectExecutions[f.request.runId]
assert.notEqual(run.status, 'done')
const expected = kind === 'roles-changed' ? /role catalog changed/ : kind === 'rights-revoked' ? /Current role denied/ : kind === 'local-changed' ? /Task scope changed/ : ['target-changed', 'workspace-changed'].includes(kind) ? /fresh facts after/ : kind === 'wrong-ref' ? /expectedScope/ : kind === 'bad-mode' ? /next only/ : kind.includes('target') ? /active implementation role/ : /continuation/
assert.match(run.failure, expected)
if (['target-changed', 'workspace-changed', 'local-changed', 'roles-changed'].includes(kind)) assert.equal(run.correction.feedback.at(-1).recovery.kind, 'stale-snapshot')
})

test('explicit target resolves multiple implementation roles, and explicit scope reference resolves multiple modes', async () => {
const f = await referenceFixture({ targetCount: 2, modes: ['next', 'latest'] })
await f.request.onResult(f.result({ action: 'start-next', targetBotId: 'p:developer1', continuationRef: f.facts.expectedScope }))
assert.equal(f.calls.length, 1, JSON.stringify(f.rt.state.projectExecutions)); assert.equal(f.calls[0].targetRole, 'developer1')
})

for (const targetBotId of [null, '', 'p:unknown', 'p:chief', 3]) test('explicit invalid next target remains a protocol error: ' + JSON.stringify(targetBotId), async () => {
const f = await referenceFixture()
await f.request.onResult(f.result({ action: 'start-next', targetBotId }))
assert.equal(f.calls.length, 0)
assert.match(f.rt.state.projectExecutions[f.request.runId].failure, /targetBotId/)
})
test('an incomplete explicit continuation is refused rather than silently filled from the snapshot', async () => {
const f = await referenceFixture()
await f.request.onResult(f.result({ action: 'start-next', continuation: { mode: 'next' } }))
assert.equal(f.calls.length, 0); assert.match(f.rt.state.projectExecutions[f.request.runId].failure, /exact current continuationOptions/)
})
for (const ref of [null, {}, '']) test('invalid reference type is not treated as omission: ' + JSON.stringify(ref), async () => {
const f = await referenceFixture()
await f.request.onResult(f.result({ action: 'start-next', continuationRef: ref }))
assert.equal(f.calls.length, 0); assert.match(f.rt.state.projectExecutions[f.request.runId].failure, /continuationRef/)
})

for (const [field, value] of [['mode', 'next'], ['continuationRef', 'scope']]) test('next-only reference field cannot be silently used on another action: ' + field, async () => {
const f = await referenceFixture()
await f.request.onResult(f.result({ action: 'continue', [field]: value }))
assert.equal(f.calls.length, 0)
assert.equal(f.rt.state.projectExecutions[f.request.runId].status, 'correction-pending')
assert.match(f.rt.state.projectExecutions[f.request.runId].failure, new RegExp(field))
})
11 changes: 9 additions & 2 deletions core/test/project-delivery.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2559,7 +2559,7 @@ test('pending external write and unknown native exit block scoped acceptance; cu
await assert.rejects(f.rt.projectEndpoint('project-attempt-cleanup', { attemptId: f.a.id }, alice), /unsettled Task/)
})

test('Chief chooses acceptance then a fresh explicit next delivery through its original decision loop', async t => {
for (const form of ['explicit', 'snapshot', 'snapshot-with-target', 'reference-token']) test('Chief chooses acceptance then a fresh next delivery through its original decision loop: ' + form, async t => {
const f = await mergedDelivery(t, { modern: true }), requests = [], rt = f.rt
const { reviseTask } = await import('../src/host/project-task-lifecycle.js')
const revision = { projectId: 'p1', taskId: f.task.id, executorRole: 'chief', runId: 'fixture-chief', initiatedBy: alice.memberId, reason: 'Record remaining work' }
Expand All @@ -2584,6 +2584,7 @@ test('Chief chooses acceptance then a fresh explicit next delivery through its o
assert.match(requests[0].prompt, /repositoryRecovery\.nextActions and continuationOptions in projectFacts/)
assert.match(requests[0].prompt, /Public TaskView\.nextActions lists caller-permitted service commands, not all Chief decision actions/)
assert.match(requests[0].prompt, /not an instruction or permission to clear publicationBinding/)
assert.match(requests[0].prompt, /Minimal start-next decision:/)
await reply(requests[0], { action: 'accept-attempt', reason: 'Accept the first verified delivery, keep the overall goal open', delivery: { completed: 'Verified addition implementation', remaining: 'Deliver the next requirement', evidence: ['candidate, tests, Review and provider merge'] } })
assert.equal(rt.state.projectExecutions[requests[0].runId].status, 'done')
assert.equal(f.a.status, 'accepted'); assert.notEqual(f.task.status, 'accepted')
Expand All @@ -2595,11 +2596,17 @@ test('Chief chooses acceptance then a fresh explicit next delivery through its o
assert.equal(saved.repositoryRecovery.nextActions.actions.find(row => row.action === 'return').status, 'blocked')
assert.equal(saved.repositoryRecovery.nextActions.recommendation.action, 'accept-attempt')
assert.equal(saved.repositoryRecovery.nextActions.recommendation.alreadyApplied, true)
await reply(requests[1], { action: 'start-next', targetBotId: 'p1:developer', reason: 'Choose the next independent delivery from verified target HEAD', continuation: { mode: 'next', previousAttemptId: facts.attemptId, generation: facts.generation, expectedWorkspace: facts.snapshot, expectedBaseSha: facts.latestBaseSha } })
const selection = form === 'explicit' ? { targetBotId: 'p1:developer', continuation: { mode: 'next', previousAttemptId: facts.attemptId, generation: facts.generation, expectedWorkspace: facts.snapshot, expectedBaseSha: facts.latestBaseSha, clientNote: 'not an execution field' } } : form === 'snapshot-with-target' ? { targetBotId: 'p1:developer' } : form === 'reference-token' ? { continuationRef: facts.expectedScope } : {}
await reply(requests[1], { action: 'start-next', reason: 'Choose the next independent delivery from verified target HEAD', ...selection })
assert.equal(rt.state.projectExecutions[requests[1].runId].status, 'done', rt.state.projectExecutions[requests[1].runId].failure)
const next = rt.state.projectAttempts[f.task.deliveryAttemptId]
assert.notEqual(next.id, f.a.id); assert.equal(next.baseSha, f.a.candidateSha); assert.equal(next.publicationBinding, undefined)
assert.equal(next.requirement.projectTaskRevision, 4)
const audit = Object.values(rt.state.projectChiefDecisions).find(row => row.action === 'start-next' && row.status === 'applied')
assert.equal(audit.continuationBinding.targetBotId, 'p1:developer')
assert.equal(audit.continuationBinding.continuation.expectedBaseSha, facts.latestBaseSha)
assert.deepEqual(audit.continuationBinding.continuation.expectedWorkspace, facts.snapshot)
assert.equal(audit.continuationBinding.continuation.clientNote, undefined)
assert.notEqual(f.task.status, 'accepted'); assert.equal(f.task.chiefQuestion, undefined)
})

Expand Down
Loading
Loading