Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
9e6ade4
docs: define proposal execution plan read contract
Oct 24, 2026
947b35e
test: cover canonical proposal plan readback
Oct 24, 2026
8aec9e9
test: continue proposal read failure assertions
Oct 24, 2026
5611046
test: assert proposal source failure distinctions
Oct 24, 2026
8f97e38
test: enforce project access in proposal fixture
Oct 24, 2026
14733f4
test: verify exact proposal fixture provenance
Oct 24, 2026
33661e5
test: type proposal read fixture strictly
Oct 24, 2026
0942829
test: keep proposal source setup self-contained
Oct 24, 2026
04cfef9
test: reject invalid proposal source versions
Oct 24, 2026
78cf50a
test: read proposal through persistence and output schema
Oct 24, 2026
b542983
test: isolate proposal output schema field retention
Oct 24, 2026
416ecaf
test: bind proposal persistence query strictly
Oct 24, 2026
fbcb698
test: carry proposal detail through REST response
Oct 24, 2026
8b9e042
test: route proposal show through REST parameters
Oct 24, 2026
bdacf2d
feat: expose exact proposal execution plan
Oct 24, 2026
2b91e2d
Integrate group-authored canonical proposal plan readback
adrianwebb Oct 24, 2026
88e652e
Bind authored proposal plan readback to native component scene
adrianwebb Oct 24, 2026
974ec2a
fix: bind default reviews to their own product and exclude archived p…
adrianwebb Oct 24, 2026
88d2676
fix(planning): admit own estimates into discussed contribution plans
adrianwebb Oct 24, 2026
e8e5cac
test(governance): assert own-product review authority in native decis…
adrianwebb Oct 24, 2026
680057e
test(scenes): bind own-product review cases by their exact current names
adrianwebb Oct 24, 2026
cc2d050
fix(api): isolate non-admissible retained contribution history
adrianwebb Oct 24, 2026
4e3cee8
fix(api): authorize exact inbox question paths
adrianwebb Oct 25, 2026
f4d9b67
fix(api): scope question authoring workspace at creation
adrianwebb Oct 25, 2026
9054ba7
fix(api): request writable question authoring workspaces
adrianwebb Oct 25, 2026
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/workflows/verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ jobs:
uses: actions/checkout@v4
with:
repository: treeseed-ai/platform
ref: 28df04061cbed62d5ef8f94ad3e40532f9a19c63
ref: cddfafc226a950e28fc63f07ffbbf61c16a9e3a3
path: .treeseed/tools/platform
submodules: false
- uses: dtolnay/rust-toolchain@stable
Expand All @@ -77,7 +77,7 @@ jobs:
- name: Build the verified native TreeDX engine
working-directory: .treeseed/tools/treedx/apps/api
run: MIX_ENV=dev mix compile
- uses: treeseed-ai/sdk/.github/actions/install-exact-sdk@8e07d9adaf45ff75e9ea16de691b90d182aa30f8
- uses: treeseed-ai/sdk/.github/actions/install-exact-sdk@83fa5a5d72ae827a70c7846a52b3b2b74fa50678
env:
NODE_ENV: production
with:
Expand Down
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,37 @@ Use this package when you operate or develop the Treeseed backend. Ordinary admi

Site and external clients reach this package through its REST or MCP surfaces.

### Governance proposal detail contract

The existing project-authorized `governance.proposals.show` operation is intended
to include an `executionPlan` field alongside its proposal metadata, votes,
events, readiness, and decision data. The plan's canonical source is the exact
TreeDX proposal identified by that proposal row's
`metadata.contentProvenance` (`repositoryId`, `contentPath`, `commitSha`, and
`digest`), resolved and validated through `readExactProposal()` in
`src/api/governance/executable-proposal.ts`. The SQL proposal row is a query
projection, not the authority for plan contents.

The response must preserve the authored plan fields and values, including work
item IDs, permissions, context references, and estimate references. It must
also preserve whether optional fields were authored: validation must not add
defaults or invent an empty `workItems` array. If the exact, valid canonical
proposal genuinely has no `executionPlan`, the detail response uses
`executionPlan: null`. Failure to find or read that source, a moved or changed
source, a digest mismatch, or invalid proposal content remains an explicit
source error; these cases must not be represented as a missing plan.

Operators should use the existing proposal detail operation to inspect the
authored plan before coordinating a revision. The returned authored work items
and estimates provide the basis for that review without reconstructing them
from readiness summaries or host-side state. The operation retains its existing
project authorization and unrelated response fields.

This describes the intended API contract. At source revision
`3497c1dc81328994383bfe9804e06039fac3c673`, the proposal detail method does not
yet return `executionPlan`; implementation and regression coverage are separate
work.

### MCP mutation requests

Tool input and output schemas declare JSON Schema draft-07 and retain the SDK's native validation constraints.
Expand Down
19 changes: 19 additions & 0 deletions guarantees/agent/golden/scenes/component-boundaries.scene.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ id: api.golden.component-boundaries
title: api golden component boundaries
scope: local-component-tests
workflow:
- id: inbox-question-scope-native
action: { verifier: api.golden.inbox-question-scope-native }
expect: { status: passed }
- id: retired-class-profile-unit
action: { verifier: api.golden.retired-class-profile-unit }
expect: { status: passed }
Expand All @@ -21,6 +24,18 @@ workflow:
- id: legacy-graph-unissued
action: { verifier: api.golden.legacy-graph-unissued }
expect: { status: passed }
- id: retained-contribution-inactive
action: { verifier: api.golden.retained-contribution-inactive }
expect: { status: passed }
- id: retained-contribution-active
action: { verifier: api.golden.retained-contribution-active }
expect: { status: passed }
- id: retained-contribution-unmaterialized
action: { verifier: api.golden.retained-contribution-unmaterialized }
expect: { status: passed }
- id: retained-contribution-no-callback
action: { verifier: api.golden.retained-contribution-no-callback }
expect: { status: passed }
- id: subscriber-execution-context-owner
action: { verifier: api.golden.subscriber-execution-context-owner }
expect: { status: passed }
Expand Down Expand Up @@ -3132,6 +3147,10 @@ workflow:
action: { verifier: api.golden.proposal-contract-authority-native }
expect: { status: passed }

- id: proposal-plan-readback-native
action: { verifier: api.golden.proposal-plan-readback-native }
expect: { status: passed }

- id: stopped-reporter-history
action: { verifier: api.golden.stopped-reporter-history }
expect: { status: passed }
Expand Down
37 changes: 34 additions & 3 deletions guarantees/verifiers/golden.verifiers.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
schemaVersion: treeseed.guarantee-verifiers/v1
ownerPackage: "@treeseed/api"
verifiers:
api.golden.inbox-question-scope-native:
kind: vitestCase
ownerPackage: "@treeseed/api"
testFile: tests/unit/control-plane/knowledge/workspaces/architecture/relation-authoring-native.test.ts
testName: "real native inbox question authoring grants only its question path and retains exact Git bytes and relations"
api.golden.retired-class-profile-unit:
kind: vitestCase
ownerPackage: "@treeseed/api"
Expand Down Expand Up @@ -31,6 +36,26 @@ verifiers:
ownerPackage: "@treeseed/api"
testFile: tests/unit/control-plane/capacity/execution/graph/executable-proposal-source.test.ts
testName: "preserves legacy authority and condition history while retiring unissued work: unissued"
api.golden.retained-contribution-inactive:
kind: vitestCase
ownerPackage: "@treeseed/api"
testFile: tests/unit/control-plane/capacity/execution/graph/contributions/contribution-history.test.ts
testName: "isolates retained contribution drift without weakening issued or unmaterialized authority: inactive"
api.golden.retained-contribution-active:
kind: vitestCase
ownerPackage: "@treeseed/api"
testFile: tests/unit/control-plane/capacity/execution/graph/contributions/contribution-history.test.ts
testName: "isolates retained contribution drift without weakening issued or unmaterialized authority: active"
api.golden.retained-contribution-unmaterialized:
kind: vitestCase
ownerPackage: "@treeseed/api"
testFile: tests/unit/control-plane/capacity/execution/graph/contributions/contribution-history.test.ts
testName: "isolates retained contribution drift without weakening issued or unmaterialized authority: unmaterialized"
api.golden.retained-contribution-no-callback:
kind: vitestCase
ownerPackage: "@treeseed/api"
testFile: tests/unit/control-plane/capacity/execution/graph/contributions/contribution-history.test.ts
testName: "isolates retained contribution drift without weakening issued or unmaterialized authority: no-callback"
api.golden.subscriber-execution-context-owner:
kind: vitestCase
ownerPackage: "@treeseed/api"
Expand Down Expand Up @@ -70,7 +95,7 @@ verifiers:
kind: vitestCase
ownerPackage: "@treeseed/api"
testFile: tests/unit/control-plane/capacity/execution/graph/contributions/contribution-graph.test.ts
testName: "keeps cross-project subscribers with the same configured name independent without a hidden project filter"
testName: "keeps each cross-project subscriber bound to its own review when configured names coincide"
api.golden.contribution-readiness-counter:
kind: vitestCase
ownerPackage: "@treeseed/api"
Expand All @@ -80,7 +105,7 @@ verifiers:
kind: vitestCase
ownerPackage: "@treeseed/api"
testFile: tests/unit/control-plane/capacity/execution/graph/contributions/contribution-graph.test.ts
testName: "binds each same-class author to its own acting work and another author own review budget in documentation-first dependency order"
testName: "binds each same-class author to its own acting and reviewing profiles and budgets in documentation-first dependency order"
api.golden.contribution-current-profile-custody:
kind: vitestCase
ownerPackage: "@treeseed/api"
Expand All @@ -90,7 +115,7 @@ verifiers:
kind: vitestCase
ownerPackage: "@treeseed/api"
testFile: tests/unit/control-plane/capacity/execution/graph/contributions/contribution-graph.test.ts
testName: "refuses missing forged moved foreign or unsubscribed contributor authority and infeasible independent review"
testName: "refuses missing forged moved foreign or unsubscribed contributor authority and missing own review budgets"
api.golden.contribution-cohort-lifecycle:
kind: vitestCase
ownerPackage: "@treeseed/api"
Expand Down Expand Up @@ -5298,6 +5323,12 @@ verifiers:
testFile: tests/unit/control-plane/governance/proposals/architecture/proposal-contract-authority-native.test.ts
testName: "creates from the selected governed schema without replacing local products and denies unpublished local-only types"

api.golden.proposal-plan-readback-native:
kind: vitestCase
ownerPackage: "@treeseed/api"
testFile: tests/unit/control-plane/governance/proposals/proposal-show-plan-readback-native.test.ts
testName: "returns the authored plan from the exact TreeDX source and distinguishes absence, source failure, and project denial"

api.golden.stopped-reporter-history:
kind: vitestCase
ownerPackage: "@treeseed/api"
Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
"@react-email/render": "^2.0.8",
"@treeseed/deployment": "https://github.com/treeseed-ai/deployment/releases/download/0.1.0-rc.321/treeseed-deployment-runtime-0.1.0-rc.321.tgz",
"@treeseed/identity": "0.1.0-rc.19",
"@treeseed/sdk": "git+https://github.com/treeseed-ai/sdk.git#8e07d9adaf45ff75e9ea16de691b90d182aa30f8",
"@treeseed/sdk": "git+https://github.com/treeseed-ai/sdk.git#83fa5a5d72ae827a70c7846a52b3b2b74fa50678",
"@treeseed/treedx": "0.3.0-rc.18",
"drizzle-orm": "^0.45.2",
"hono": "4.13.3",
Expand Down
29 changes: 5 additions & 24 deletions src/api/capacity/policy/execution/contribution-bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,30 +45,11 @@ export function contributionBindings(source: { teamId: string; frontmatter: Row;
};
const acting = new Map(items.map(item => [text(item.id), bind(item, 'acting')]));
const targets = items.filter(item => item.review === 'required');
const budgets = items.filter(item => Object.keys(record(item.reviewEstimate)).length > 0);
const targetByBudget = new Map<string, string>();
const eligible = (target: Row, budget: Row) => {
const actor = owners.get(text(target.id))!, reviewer = owners.get(text(budget.id))!;
return (actor.attempt.projectId !== reviewer.attempt.projectId || actor.definition.id !== reviewer.definition.id)
&& Boolean(reviewer.definition.activityProfiles.reviewing);
};
// Each estimated review budget is used once. Matching never invents, scales or
// duplicates a subscriber's promised effort, and never chooses by role/class.
const allocate = (target: Row, visited: Set<string>): boolean => {
for (const budget of budgets) {
const id = text(budget.id); if (visited.has(id) || !eligible(target, budget)) continue;
visited.add(id);
const previous = targetByBudget.get(id);
if (!previous || allocate(targets.find(item => item.id === previous)!, visited)) {
targetByBudget.set(id, text(target.id)); return true;
}
}
return false;
};
for (const target of targets) if (!allocate(target, new Set())) deny(`Work item ${target.id} lacks a separately estimated independent peer review.`);
const reviewing = new Map([...targetByBudget].map(([budgetId, targetId]) => {
const budget = items.find(item => item.id === budgetId)!;
return [targetId, { profile: bind(budget, 'reviewing'), estimate: record(budget.reviewEstimate), budgetId }] as const;
// Omission selects a separate review execution by the product's own agent.
// Each product retains its own instructions, estimate and authority.
const reviewing = new Map(targets.map(item => {
if (!Object.keys(record(item.reviewEstimate)).length) deny(`Work item ${item.id} lacks its own separately estimated review.`);
return [text(item.id), { profile: bind(item, 'reviewing'), estimate: record(item.reviewEstimate), budgetId: text(item.id) }] as const;
}));
return { acting, reviewing };
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { validatePortableContentData } from '@treeseed/sdk/content-validation';
import { setTimeout as delay } from 'node:timers/promises';
import { branchMovedConflict } from '../../../../../../discussions/discussion-workspace.ts';
import type { AssignmentResult, ExactEntityReference } from '@treeseed/sdk/agent-capacity';
import { assignmentAttemptSchema } from '@treeseed/sdk/agent-capacity';
import { assignmentAttemptSchema, estimateSchema } from '@treeseed/sdk/agent-capacity';
import { CapacityGovernanceError } from '../../../../../database.ts';
import type { DurableProviderAssignment } from '../../../../../repositories/capacity/assignments/assignment.ts';
import { hasCompleteExecutablePlan, proposalApprovalFingerprint, readExactProposal } from '../../../../../../governance/executable-proposal.ts';
Expand Down Expand Up @@ -62,15 +62,27 @@ function mergeSubscriberEstimate(input: EstimateMerge): Row {
const own = candidateItems.filter(owns), peers = candidateItems.filter(item => !owns(item));
if (!own.length || own.some(item => stable(item.estimateRef) !== stable(ref))) return deny('assignment_estimate_owner_missing',
'Each estimating author must submit its own work with the exact current attempt provenance.');
if (own.some(item => !estimateSchema.safeParse(item.estimate).success
|| (item.review === 'required' && !estimateSchema.safeParse(item.reviewEstimate).success)))
return deny('assignment_estimate_work_item_invalid', 'An estimating result must supply its actual estimate and required review estimate.');
const ordered = (items: Row[]) => [...items].sort((left, right) => text(left.id).localeCompare(text(right.id)));
if (stable(ordered(peers)) !== stable(ordered(frozenItems.filter(item => !owns(item))))) return deny('assignment_estimate_unrelated_change',
const firstIds = new Set(own.map(item => text(item.id)));
const planning = ['draft', 'discussing'].includes(text(input.frozen.status)) && ['draft', 'discussing'].includes(text(input.current.status));
const firstClaim = (item: Row) => planning && firstIds.has(text(item.id)) && item.estimateRef === undefined
&& item.estimate === undefined && item.reviewEstimate === undefined;
for (const planned of frozenItems.filter(firstClaim)) {
const current = currentItems.find(item => item.id === planned.id);
if (!current || (firstClaim(current) && stable(current) !== stable(planned))) return deny('assignment_estimate_source_changed',
'A newer discussed contribution must not be overwritten or restored by a stale first claim.');
}
if (stable(ordered(peers)) !== stable(ordered(frozenItems.filter(item => !owns(item) && !firstClaim(item))))) return deny('assignment_estimate_unrelated_change',
'An estimator cannot add, remove or change another subscriber contribution.');
// Submitted peer fields stay frozen above; preserve newer peer contributions
// from the current plan when merging only this author's work below.
const currentOwn = currentItems.filter(owns);
if (stable(ordered(currentOwn)) !== stable(ordered(frozenItems.filter(owns))) && stable(ordered(currentOwn)) !== stable(ordered(own))) return deny(
'assignment_estimate_source_changed', 'A newer contribution from this author must not be overwritten.');
const merged = { ...input.current, executionPlan: { workItems: ordered([...currentItems.filter(item => !owns(item)), ...own]) } };
const merged = { ...input.current, executionPlan: { workItems: ordered([...currentItems.filter(item => !owns(item) && !firstClaim(item)), ...own]) } };
if (!validatePortableContentData('proposal', merged).ok) return deny('assignment_estimate_work_item_invalid',
'The combined contributions contain conflicting IDs, dependencies or invalid work.');
// Workflow order is authored content. An unchanged contribution must remain
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,13 @@ export async function loadTeamExecutableProposalSources(store: any, teamId: stri
? await loadProposalGroupContributions(store, { id: row.proposal_id, teamId, projectId: row.project_id }, exact.definition) : null;
if (contributions && !contributions.current) {
if (!accepted) continue;
// Team growth must not let non-admissible retained history block other
// objectives. Reuse the existing exact-revision freeze; active or never
// materialized authority must still fail rather than disappear.
if (state?.count && state.active === 0 && onFrozenInvalid) {
onFrozenInvalid({ id: text(row.proposal_id), digest: `sha256:${text(row.active_content_hash)}` });
continue;
}
throw new CapacityOperationError(409, 'proposal_contributions_unverified',
'Accepted work requires every subscriber contribution and its actual estimating source to remain verified.',
{ missing: contributions.missing });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export function compileWorkdayAgentProfileSnapshot(agentClasses: unknown[], sele
const available: WorkdayAgentSelectionSnapshot[] = [];
for (const value of agentClasses) {
const row = record(value);
if (row.status === 'archived') continue;
for (const candidate of array(record(row.handlerRefs ?? row.handler_refs ?? row.handler_refs_json).agents)) {
const membership = record(candidate).groupIds;
if (groupIds !== undefined && !(Array.isArray(membership) && membership.some(id => groupIds.includes(String(id))))) continue;
Expand Down
7 changes: 6 additions & 1 deletion src/api/control-plane/governance/governance-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { reconcileExecutionGraph } from '../repositories/capacity/execution/exec
import { resolveKnowledgeGatewayConnection } from '../../knowledge/gateway-treedx-connection.ts';
import { exactEntityReferenceSchema } from '@treeseed/sdk/agent-capacity';
import { assertGovernanceProposalReady } from '../../store/governance/policy/contracts/governance-proposal-readiness.ts';
import { readExactProposal } from '../../governance/executable-proposal.ts';
import { parseFrontmatterDocument } from '../../content/frontmatter.ts';

type Principal = { id: string; roles?: string[]; permissions?: string[]; metadata?: Record<string, unknown> } | undefined;

Expand Down Expand Up @@ -296,7 +298,10 @@ export function createGovernanceService(store: any, discussions: { create: (prin
async proposal(principal: Principal, projectId: string, proposalId: string) {
await projectFor(store, principal, projectId);
const proposal = await proposalFor(store, projectId, proposalId);
return { ...proposal, votes: await store.listGovernanceProposalVotes(proposal.id),
const exact = await readExactProposal(store, proposal);
const authored = parseFrontmatterDocument(exact.source).frontmatter;
return { ...proposal, executionPlan: Object.hasOwn(authored, 'executionPlan') ? authored.executionPlan : null,
votes: await store.listGovernanceProposalVotes(proposal.id),
events: await store.listGovernanceEvents({ proposalId: proposal.id, limit: 100 }),
readiness: await store.governanceProposalReadiness(proposal.id),
decision: proposal.decisionId ? await store.getGovernanceDecision(proposal.decisionId) : null };
Expand Down
Loading
Loading