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
10 changes: 7 additions & 3 deletions src/api/capacity/policy/execution/workday-execution-projector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ const stable = (value: unknown): string => {
const digest = (value: unknown) => `sha256:${createHash('sha256').update(stable(value)).digest('hex')}`;
const edgeId = (values: unknown[]) => `edge_${createHash('sha256').update(stable(values)).digest('base64url').slice(0, 32)}`;

const nodeKind = (activity: WorkdayParticipant['activity']): ExecutionNode['kind'] => activity === 'chat' ? 'communication' : activity;
const capability = (activity: WorkdayParticipant['activity']): string => ({
planning: 'treeseed.coordination.planning',
estimating: 'treeseed.coordination.estimation',
Expand All @@ -30,6 +29,7 @@ export interface ActiveWorkdayProjectionSource {
teamId: string;
parameters: Row;
proposalsByProjectId?: Record<string, Row>;
proposalStatusesByProjectId?: Record<string, string>;
}

function sourceRef(workday: ReturnType<typeof appliedWorkdaySchema.parse>): ExactEntityReference {
Expand Down Expand Up @@ -79,11 +79,15 @@ export function projectActiveWorkdays(input: { teamId: string; revision: number;
`Estimate the generated review of work item ${text(item.id)} independently: minimumSeconds, expectedSeconds, maximumSeconds, and rationale.`)
: [`Estimate work item ${text(workItem[0]?.id)}: minimumSeconds, expectedSeconds, maximumSeconds, and rationale.`,
...array(workItem[0]?.acceptanceCriteria).map(text)];
const proposalStatus = source.proposalStatusesByProjectId?.[projectId];
const node = executionNodeSchema.parse({ schemaVersion: 'treeseed.execution-node/v1', id: plannedId,
teamId: input.teamId, projectId, workdayId: workday.id, kind: nodeKind(activity), pairRole: null,
teamId: input.teamId, projectId, workdayId: workday.id, kind: activity, pairRole: null,
...(activity === 'estimating' && workItem[0] ? { workItemId: text(workItem[0].id) } : {}),
sourceRef: nodeSource, authorityRefs: [reference], ruleRevision: 1, nodeRevision: 1,
agentClass: definition.agentClass, status: 'blocked',
agentClass: definition.agentClass,
status: activity === 'estimating' && proposalStatus
&& !['draft', 'submitted', 'open'].includes(proposalStatus)
? 'cancelled' : 'blocked',
estimate: { minimumSeconds: 1, expectedSeconds: workday.policySnapshot.planningTurnMaximumSeconds,
maximumSeconds: workday.policySnapshot.planningTurnMaximumSeconds },
requiredCapabilities: [capability(activity)], requestedPermissions: profile.permissions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ export async function integrateAssignmentEstimate(
const proposal = await store.getGovernanceProposal(attempt.sourceRef.id);
if (!proposal || proposal.projectId !== assignment.projectId || proposal.teamId !== assignment.teamId) throw new CapacityGovernanceError(
'assignment_estimate_proposal_mismatch', 'Estimating assignment proposal is outside its project and team.', 409);
if (!['draft', 'submitted', 'open'].includes(text(proposal.status))) throw new CapacityGovernanceError(
'assignment_estimate_proposal_closed', 'Estimating cannot update a proposal after voting or decision.', 409);
const frozen = await readExactProposal(store, proposal, attempt.sourceRef);
const current = await readExactProposal(store, proposal);
const matches = result.references.filter((reference): reference is Extract<AssignmentResult['references'][number], { kind: 'treedx' }> => reference.kind === 'treedx'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ async function loadActiveWorkdays(store: any, teamId: string) {
return Promise.all(sources.map(async (source: { id: string; teamId: string; parameters: Row }) => {
if (!Object.keys(record(source.parameters.planningSourceByProjectId)).length) return source;
const proposalsByProjectId: Record<string, Row> = {};
const proposalStatusesByProjectId: Record<string, string> = {};
for (const [projectId, value] of Object.entries(record(source.parameters.planningSourceByProjectId))) {
const reference = record(value);
const proposal = await store.getGovernanceProposal(text(reference.id));
Expand All @@ -100,9 +101,10 @@ async function loadActiveWorkdays(store: any, teamId: string) {
if (stable(exact.ref) !== stable(reference)) throw new CapacityOperationError(
409, 'estimating_proposal_source_moved', 'The frozen estimating proposal revision changed.');
proposalsByProjectId[projectId] = exact.definition;
proposalStatusesByProjectId[projectId] = text(proposal.status);
}
// Transient exact TreeDX reads, never another persisted plan authority.
return { ...source, proposalsByProjectId };
return { ...source, proposalsByProjectId, proposalStatusesByProjectId };
}));
}

Expand Down
5 changes: 3 additions & 2 deletions src/api/discussions/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ export async function loadDiscussions(input: {

export async function commitDiscussionMessage(input: {
store: any; projectId: string; teamId: string; principal: Row; body: string;
lookupWorkday?: (teamId: string, workdayId: string) => Promise<{ executionMode: string } | null>;
intent: 'discuss' | 'propose'; discussionId?: string; topic?: string; fileRefs?: unknown[]; contextRefs?: AgentAtlasContextReference[];
authorType?: 'user' | 'agent' | 'system'; messageId?: string;
createDiscussion?: boolean;
Expand All @@ -190,8 +191,8 @@ export async function commitDiscussionMessage(input: {
authoringRef?: string | null;
authoringWorkspace?: { workspaceId: string; baseCommitSha: string; baseRef: string; allowedPaths?: string[] } | null;
}) {
const workday = input.parentWorkdayId
? await input.store.getCapacityWorkdayRun(input.teamId, input.parentWorkdayId) : null;
const workday = input.parentWorkdayId && input.lookupWorkday
? await input.lookupWorkday(input.teamId, input.parentWorkdayId) : null;
if (input.parentWorkdayId && !workday) throw Object.assign(new Error('The addressed workday is unavailable.'), {
status: 409, code: 'discussion_workday_unavailable',
});
Expand Down
6 changes: 6 additions & 0 deletions src/api/discussions/discussion-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ function administrator(principal: Principal) { return principal?.roles?.some((ro
function failure(error: unknown, status: 409 | 503, code: string): never {
if (error instanceof DiscussionServiceError) throw error;
const value = record(error);
const cause = record(value.cause);
console.error('discussion.operation.failed', {
code: text(value.code, code), kind: text(value.name, 'Error'), status: Number(value.status) || status,
causeCode: text(cause.code), causeKind: text(cause.name), causeStatus: Number(cause.status) || null,
});
throw new DiscussionServiceError(Number.isInteger(value.status) ? value.status : status, text(value.code, code),
'TreeDX Discussion operation failed.');
}
Expand Down Expand Up @@ -168,6 +173,7 @@ export function createDiscussionService(dependencies: { store: any; capacity: an
authored = await commitDiscussionMessage({ store, projectId, teamId, principal, body: messageBody,
intent: body.intent === 'propose' ? 'propose' : 'discuss', discussionId, messageId,
parentWorkdayId,
lookupWorkday: (teamId, workdayId) => invocationStore.getCapacityWorkdayRun(teamId, workdayId),
createDiscussion: !text(body.discussionId) || (body.createDiscussion === true && existing.discussions.length === 0), topic: text(record(existing.discussions[0]?.frontmatter).topic) || text(body.topic) || undefined,
fileRefs: Array.isArray(body.fileRefs) ? body.fileRefs : [], contextRefs,
recipients: Array.isArray(body.recipients) ? body.recipients.map(String) : [],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { describe, expect, it } from 'vitest';
import { compileWorkday, validateExecutionGraph } from '@treeseed/sdk/agent-capacity';
import { compileWorkday, validateExecutionGraph, type AgentDefinition } from '@treeseed/sdk/agent-capacity';
import { projectActiveWorkdays } from '../../../../../../src/api/capacity/policy/execution/workday-execution-projector.ts';
import { workdayParticipants } from '../../../../../../src/api/capacity/policy/execution/workday-participants.ts';

const permissions = { content: { read: ['proposal'], write: ['proposal'] }, tools: ['discussion'] };
const definition = (agentClass: string, dependsOn: string[] = []) => ({
const permissions: NonNullable<AgentDefinition['activityProfiles']['planning']>['permissions'] = {
content: { read: ['proposal'], write: ['proposal'] }, tools: ['discussion'],
};
const definition = (agentClass: string, dependsOn: string[] = []): AgentDefinition => ({
schemaVersion: 'treeseed.agent/v1' as const, id: `sdk/${agentClass}`, name: agentClass, agentClass,
purpose: `Perform ${agentClass} work.`, responsibilities: ['Return exact results.'], capabilities: ['reasoning'],
context: { include: ['project-objectives'] }, activityProfiles: {
Expand Down Expand Up @@ -60,20 +62,28 @@ describe('workday living-graph projection', () => {
policy: { durationSeconds: 1800, maximumConcurrency: 1, planningTurnMaximumSeconds: 60,
communicationConcurrency: 1, projectPercentages: {}, agentClassPercentages: {} },
agentIds: classes.map((agentClass) => `sdk/sdk/${agentClass}:estimating`), startsAt: '2026-09-14T12:00:00.000Z' });
const graph = projectActiveWorkdays({ teamId: 'team', revision: 1, profiles,
const projectionInput: Parameters<typeof projectActiveWorkdays>[0] = { teamId: 'team', revision: 1, profiles,
sources: [{ id: 'seven-estimates', teamId: 'team', proposalsByProjectId: { sdk: { executionPlan: {
workItems: owners.map((agentClass) => ({ id: `${agentClass}-work`, agentClass, review: 'required', acceptanceCriteria: ['Meet the exact work-item boundary.'] })),
} } }, parameters: { appliedPlan, scheduledProjectIds: ['sdk'],
planningSourceByProjectId: { sdk: { store: 'treedx', model: 'proposal', id: 'proposal', revision: 1,
repository: 'sdk-library', commit: 'b'.repeat(40), path: 'proposals/golden.mdx' } },
agentProfilesByProjectId: { sdk: { agents: Object.values(profiles).map((agent) => ({ definition: agent, activities: ['estimating'] })) } },
} }] });
} }] };
const graph = projectActiveWorkdays(projectionInput);
expect(graph.nodes).toHaveLength(7);
expect(graph.edges).toHaveLength(0);
expect(graph.nodes.filter((node) => node.workItemId).map((node) => node.workItemId).sort())
.toEqual(owners.map((agentClass) => `${agentClass}-work`).sort());
expect(graph.nodes.find((node) => node.agentClass === 'reviewer')?.acceptanceCriteria).toHaveLength(6);
expect(validateExecutionGraph(graph.nodes, graph.edges)).toMatchObject({ ok: true });
const accepted = projectActiveWorkdays({ ...projectionInput,
sources: projectionInput.sources.map((source) => ({ ...source,
proposalStatusesByProjectId: { sdk: 'accepted' } })) });
expect(accepted.nodes.filter((node) => node.kind === 'estimating'))
.toHaveLength(7);
expect(accepted.nodes.every((node) => node.status === 'cancelled')).toBe(true);
expect(validateExecutionGraph(accepted.nodes, accepted.edges)).toMatchObject({ ok: true });
});
it('projects dependency-ordered planning and closing Reporter work', () => {
const profiles = { 'sdk:architect': definition('architect'), 'sdk:engineer': definition('engineer', ['architect']),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { mergeAssignmentEstimate } from '../../../../../src/api/capacity/services/capacity/assignments/planning/estimates/integration.ts';
import { integrateAssignmentEstimate, mergeAssignmentEstimate } from '../../../../../src/api/capacity/services/capacity/assignments/planning/estimates/integration.ts';

const estimate = { minimumSeconds: 100, expectedSeconds: 200, maximumSeconds: 300, rationale: 'Exact source inspection.' };
const frozen = { id: 'proposal-1', projectId: 'project-1', status: 'draft', executionPlan: { workItems: [
Expand Down Expand Up @@ -56,4 +56,14 @@ describe('exact estimator result integration', () => {
executionPlan: { workItems: [reviewCandidate.executionPlan.workItems[0], frozen.executionPlan.workItems[1]] } },
current, workItemId: null })).toThrow('each assigned work item');
});
it('rejects a racing estimator result before reading or writing a closed proposal', async () => {
let reads = 0;
const store = { getGovernanceProposal: async () => ({ id: 'proposal-1', projectId: 'project-1', teamId: 'team-1', status: 'accepted' }),
getProjectTreeDxLibrary: async () => { reads += 1; return null; } };
const assignment = { id: 'assignment-1', projectId: 'project-1', teamId: 'team-1', agentId: 'sdk/engineer',
assignmentAttempt: { effectiveProfile: { activity: 'estimating' }, sourceRef: { model: 'proposal', id: 'proposal-1' } } };
await expect(integrateAssignmentEstimate(store as never, assignment as never, { references: [] } as never))
.rejects.toThrow('after voting or decision');
expect(reads).toBe(0);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -49,19 +49,24 @@ describe('Discussion message custody', () => {
});

it('keeps a simulation-bound user message on its workday ref without publishing or replicating it', async () => {
const store = { getCapacityWorkdayRun: vi.fn(async () => ({ id: 'workday-1', executionMode: 'simulation' })) };
const store = {};
const lookupWorkday = vi.fn(async () => ({ id: 'workday-1', executionMode: 'simulation' }));
await commitDiscussionMessage({ store, projectId: 'sdk', teamId: 'team-1', principal: { id: 'user-1' },
body: 'Review the SDK proposal.', intent: 'discuss', parentWorkdayId: 'workday-1',
lookupWorkday,
discussionId: 'discussion-1', messageId: 'message-1', createDiscussion: true });
expect(lookupWorkday).toHaveBeenCalledWith('team-1', 'workday-1');
expect(mocks.openWorkspace).toHaveBeenCalledWith(expect.objectContaining({ branchName: 'refs/heads/workday-1' }));
expect(mocks.authoring).toHaveBeenCalledWith(store, 'unpublished', expect.objectContaining({ ref: 'refs/heads/workday-1' }));
expect(mocks.project).not.toHaveBeenCalled();
});

it('preserves shared projection for an ordinary production message', async () => {
const store = { getCapacityWorkdayRun: vi.fn(async () => ({ id: 'workday-1', executionMode: 'production' })) };
const store = {};
const lookupWorkday = vi.fn(async () => ({ id: 'workday-1', executionMode: 'production' }));
await commitDiscussionMessage({ store, projectId: 'sdk', teamId: 'team-1', principal: { id: 'user-1' },
body: 'Review the SDK proposal.', intent: 'discuss', parentWorkdayId: 'workday-1',
lookupWorkday,
discussionId: 'discussion-1', messageId: 'message-1', createDiscussion: true });
expect(mocks.authoring).toHaveBeenCalledWith(store, 'integrated', expect.anything());
expect(mocks.project).toHaveBeenCalledOnce();
Expand Down
Loading