diff --git a/packages/kernel/src/__snapshots__/schema-drift-regression.test.ts.snap b/packages/kernel/src/__snapshots__/schema-drift-regression.test.ts.snap index 7b7ed27..34d24e0 100644 --- a/packages/kernel/src/__snapshots__/schema-drift-regression.test.ts.snap +++ b/packages/kernel/src/__snapshots__/schema-drift-regression.test.ts.snap @@ -1119,6 +1119,117 @@ exports[`schema drift regression > locks MCP tool metadata and input schemas 1`] "name": "workgraph_checkpoint_create", "title": "Checkpoint Create", }, + { + "annotations": { + "idempotentHint": true, + "readOnlyHint": true, + }, + "description": "Return company context graph view for an actor.", + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "actor": { + "type": "string", + }, + }, + "type": "object", + }, + "name": "workgraph_company_context", + "title": "Workgraph Company Context", + }, + { + "annotations": { + "destructiveHint": true, + "idempotentHint": false, + }, + "description": "Create a decision primitive with rationale, participants, and alternatives.", + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "actor": { + "type": "string", + }, + "alternatives": { + "items": { + "type": "string", + }, + "type": "array", + }, + "body": { + "type": "string", + }, + "consequences": { + "items": { + "type": "string", + }, + "type": "array", + }, + "contextRefs": { + "items": { + "type": "string", + }, + "type": "array", + }, + "date": { + "type": "string", + }, + "decidedBy": { + "type": "string", + }, + "externalLinks": { + "items": { + "type": "string", + }, + "type": "array", + }, + "participants": { + "items": { + "type": "string", + }, + "type": "array", + }, + "rationale": { + "type": "string", + }, + "relatedRefs": { + "items": { + "type": "string", + }, + "type": "array", + }, + "status": { + "enum": [ + "draft", + "proposed", + "approved", + "active", + "superseded", + "reverted", + ], + "type": "string", + }, + "supersedes": { + "type": "string", + }, + "tags": { + "items": { + "type": "string", + }, + "type": "array", + }, + "title": { + "minLength": 1, + "type": "string", + }, + }, + "required": [ + "title", + ], + "type": "object", + }, + "name": "workgraph_create_decision", + "title": "Decision Create", + }, { "annotations": { "destructiveHint": true, @@ -1807,6 +1918,136 @@ exports[`schema drift regression > locks MCP tool metadata and input schemas 1`] "name": "workgraph_query", "title": "Workgraph Query", }, + { + "annotations": { + "destructiveHint": true, + "idempotentHint": false, + }, + "description": "Record a lesson with severity and source event context.", + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "actor": { + "type": "string", + }, + "appliesTo": { + "items": { + "type": "string", + }, + "type": "array", + }, + "body": { + "type": "string", + }, + "confidence": { + "type": "string", + }, + "contextRefs": { + "items": { + "type": "string", + }, + "type": "array", + }, + "date": { + "type": "string", + }, + "relatedRefs": { + "items": { + "type": "string", + }, + "type": "array", + }, + "severity": { + "enum": [ + "critical", + "important", + "minor", + ], + "type": "string", + }, + "sourceEvent": { + "type": "string", + }, + "tags": { + "items": { + "type": "string", + }, + "type": "array", + }, + "title": { + "minLength": 1, + "type": "string", + }, + }, + "required": [ + "title", + ], + "type": "object", + }, + "name": "workgraph_record_lesson", + "title": "Lesson Record", + }, + { + "annotations": { + "destructiveHint": true, + "idempotentHint": false, + }, + "description": "Record a reusable pattern with steps and exceptions.", + "inputSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "actor": { + "type": "string", + }, + "appliesTo": { + "items": { + "type": "string", + }, + "type": "array", + }, + "body": { + "type": "string", + }, + "description": { + "type": "string", + }, + "exceptions": { + "items": { + "type": "string", + }, + "type": "array", + }, + "relatedRefs": { + "items": { + "type": "string", + }, + "type": "array", + }, + "steps": { + "items": { + "type": "string", + }, + "type": "array", + }, + "tags": { + "items": { + "type": "string", + }, + "type": "array", + }, + "title": { + "minLength": 1, + "type": "string", + }, + }, + "required": [ + "title", + ], + "type": "object", + }, + "name": "workgraph_record_pattern", + "title": "Pattern Record", + }, { "annotations": { "destructiveHint": true, diff --git a/packages/kernel/src/orientation.test.ts b/packages/kernel/src/orientation.test.ts index 534c1cf..ec96f6a 100644 --- a/packages/kernel/src/orientation.test.ts +++ b/packages/kernel/src/orientation.test.ts @@ -74,6 +74,10 @@ describe('orientation core module', () => { expect(brief.blockedThreads.map((entry) => entry.path)).toContain('threads/someone-else.md'); expect(brief.nextReadyThreads).toHaveLength(1); expect(brief.recentActivity).toHaveLength(1); + expect(brief.companyContext.teams).toEqual([]); + expect(brief.companyContext.clients).toEqual([]); + expect(brief.companyContext.recentDecisions).toEqual([]); + expect(brief.companyContext.patterns).toEqual([]); }); it('creates checkpoint primitives with explicit next/blocked sections', () => { @@ -139,4 +143,83 @@ describe('orientation core module', () => { expect(brief.myOpenThreads).toEqual([]); expect(brief.nextReadyThreads.length).toBeGreaterThanOrEqual(1); }); + + it('includes company context in actor brief', () => { + const now = new Date().toISOString(); + store.create( + workspacePath, + 'org', + { + title: 'Versatly', + mission: 'Make autonomous coordination reliable.', + strategy: 'Invest in company context graph primitives.', + }, + 'Org context', + 'agent-seed', + ); + store.create( + workspacePath, + 'team', + { + title: 'Platform', + members: ['agent-focus', 'agent-other'], + responsibilities: ['runtime', 'mcp'], + }, + 'Team context', + 'agent-seed', + ); + store.create( + workspacePath, + 'client', + { + name: 'Acme Corp', + status: 'active', + description: 'Strategic customer', + }, + 'Client context', + 'agent-seed', + ); + store.create( + workspacePath, + 'decision', + { + title: 'Adopt company context graph', + date: now, + status: 'draft', + decided_by: 'agent-focus', + }, + 'Decision context', + 'agent-seed', + ); + store.create( + workspacePath, + 'pattern', + { + title: 'Weekly context sync', + description: 'Capture and refresh context every Friday', + }, + 'Pattern context', + 'agent-seed', + ); + store.create( + workspacePath, + 'agent', + { + name: 'agent-focus', + capabilities: ['briefing', 'coordination'], + permissions: ['mcp:write'], + }, + 'Agent profile', + 'agent-seed', + ); + + const brief = orientation.brief(workspacePath, 'agent-focus'); + expect(brief.companyContext.org?.title).toBe('Versatly'); + expect(brief.companyContext.teams[0]?.title).toBe('Platform'); + expect(brief.companyContext.clients[0]?.title).toBe('Acme Corp'); + expect(brief.companyContext.recentDecisions[0]?.decidedBy).toBe('agent-focus'); + expect(brief.companyContext.patterns[0]?.title).toBe('Weekly context sync'); + expect(brief.companyContext.agentProfile?.name).toBe('agent-focus'); + expect(brief.companyContext.agentProfile?.permissions).toEqual(['mcp:write']); + }); }); diff --git a/packages/kernel/src/orientation.ts b/packages/kernel/src/orientation.ts index 274e636..752d33b 100644 --- a/packages/kernel/src/orientation.ts +++ b/packages/kernel/src/orientation.ts @@ -6,7 +6,12 @@ import * as ledger from './ledger.js'; import * as query from './query.js'; import * as store from './store.js'; import * as thread from './thread.js'; -import type { PrimitiveInstance, WorkgraphBrief, WorkgraphStatusSnapshot } from './types.js'; +import type { + CompanyContext, + PrimitiveInstance, + WorkgraphBrief, + WorkgraphStatusSnapshot, +} from './types.js'; export function statusSnapshot(workspacePath: string): WorkgraphStatusSnapshot { const threads = store.list(workspacePath, 'thread'); @@ -59,6 +64,81 @@ export function brief(workspacePath: string, actor: string, options: { recentCou blockedThreads: store.blockedThreads(workspacePath), nextReadyThreads: thread.listReadyThreads(workspacePath).slice(0, options.nextCount ?? 5), recentActivity: ledger.recent(workspacePath, options.recentCount ?? 12), + companyContext: companyContext(workspacePath, actor), + }; +} + +export function companyContext(workspacePath: string, actor: string): CompanyContext { + const orgs = query.queryPrimitives(workspacePath, { type: 'org' }); + const teams = query.queryPrimitives(workspacePath, { type: 'team' }); + const clients = query.queryPrimitives(workspacePath, { type: 'client' }); + const decisions = query.queryPrimitives(workspacePath, { type: 'decision' }); + const patterns = query.queryPrimitives(workspacePath, { type: 'pattern' }); + const agents = query.queryPrimitives(workspacePath, { type: 'agent' }); + + const primaryOrg = orgs + .slice() + .sort((left, right) => compareByRecency(left, right, ['updated', 'created'])) + .at(0); + const actorAgent = agents.find((instance) => normalizeText(instance.fields.name) === actor); + + return { + ...(primaryOrg + ? { + org: { + title: normalizeText(primaryOrg.fields.title) ?? 'Organization', + ...(normalizeText(primaryOrg.fields.mission) + ? { mission: normalizeText(primaryOrg.fields.mission) } + : {}), + ...(normalizeText(primaryOrg.fields.strategy) + ? { strategy: normalizeText(primaryOrg.fields.strategy) } + : {}), + }, + } + : {}), + teams: teams.map((instance) => ({ + title: normalizeText(instance.fields.title) ?? 'Untitled team', + members: normalizeStringList(instance.fields.members), + responsibilities: normalizeStringList(instance.fields.responsibilities), + })), + clients: clients.map((instance) => ({ + title: normalizeText(instance.fields.title) + ?? normalizeText(instance.fields.name) + ?? 'Untitled client', + status: normalizeText(instance.fields.status) ?? 'unknown', + ...(normalizeText(instance.fields.description) + ? { description: normalizeText(instance.fields.description) } + : {}), + })), + recentDecisions: decisions + .slice() + .sort((left, right) => compareByRecency(left, right, ['date', 'updated', 'created'])) + .slice(0, 10) + .map((instance) => ({ + title: normalizeText(instance.fields.title) ?? 'Untitled decision', + ...(normalizeText(instance.fields.decided_by) + ? { decidedBy: normalizeText(instance.fields.decided_by) } + : {}), + ...(normalizeText(instance.fields.date) + ? { date: normalizeText(instance.fields.date) } + : {}), + status: normalizeText(instance.fields.status) ?? 'draft', + })), + patterns: patterns.map((instance) => ({ + title: normalizeText(instance.fields.title) ?? 'Untitled pattern', + ...(normalizeText(instance.fields.description) + ? { description: normalizeText(instance.fields.description) } + : {}), + })), + ...(actorAgent + ? { + agentProfile: { + name: normalizeText(actorAgent.fields.name) ?? actor, + capabilities: normalizeStringList(actorAgent.fields.capabilities), + permissions: normalizeStringList(actorAgent.fields.permissions), + }, + } + : {}), }; } @@ -115,3 +195,34 @@ export function intake( tags: ['intake', ...(options.tags ?? [])], }); } + +function normalizeText(value: unknown): string | undefined { + if (typeof value !== 'string') return undefined; + const trimmed = value.trim(); + return trimmed.length > 0 ? trimmed : undefined; +} + +function normalizeStringList(value: unknown): string[] { + if (!Array.isArray(value)) return []; + return value + .map((item) => normalizeText(item)) + .filter((item): item is string => !!item); +} + +function compareByRecency( + left: PrimitiveInstance, + right: PrimitiveInstance, + fieldPriority: string[], +): number { + return getMostRecentTimestamp(right, fieldPriority) - getMostRecentTimestamp(left, fieldPriority); +} + +function getMostRecentTimestamp(instance: PrimitiveInstance, fieldPriority: string[]): number { + for (const fieldName of fieldPriority) { + const raw = normalizeText(instance.fields[fieldName]); + if (!raw) continue; + const parsed = Date.parse(raw); + if (Number.isFinite(parsed)) return parsed; + } + return 0; +} diff --git a/packages/kernel/src/orientation/brief.ts b/packages/kernel/src/orientation/brief.ts index b469476..9cd6d78 100644 --- a/packages/kernel/src/orientation/brief.ts +++ b/packages/kernel/src/orientation/brief.ts @@ -1 +1 @@ -export { brief } from '../orientation.js'; +export { brief, companyContext } from '../orientation.js'; diff --git a/packages/kernel/src/query-orientation.test.ts b/packages/kernel/src/query-orientation.test.ts index 76b4a8e..998a2fb 100644 --- a/packages/kernel/src/query-orientation.test.ts +++ b/packages/kernel/src/query-orientation.test.ts @@ -52,6 +52,8 @@ describe('query and orientation', () => { const brief = orientation.brief(workspacePath, 'agent-b'); expect(brief.myClaims).toHaveLength(1); expect(brief.recentActivity.length).toBeGreaterThan(0); + expect(Array.isArray(brief.companyContext.teams)).toBe(true); + expect(Array.isArray(brief.companyContext.clients)).toBe(true); const checkpoint = orientation.checkpoint( workspacePath, diff --git a/packages/kernel/src/registry.test.ts b/packages/kernel/src/registry.test.ts index 2300ac0..7b9bac7 100644 --- a/packages/kernel/src/registry.test.ts +++ b/packages/kernel/src/registry.test.ts @@ -29,6 +29,11 @@ describe('registry', () => { expect(reg.types.person).toBeDefined(); expect(reg.types.project).toBeDefined(); expect(reg.types.client).toBeDefined(); + expect(reg.types.org).toBeDefined(); + expect(reg.types.team).toBeDefined(); + expect(reg.types.pattern).toBeDefined(); + expect(reg.types.relationship).toBeDefined(); + expect(reg.types.strategic_note).toBeDefined(); expect(reg.types.mission).toBeDefined(); expect(reg.types.conversation).toBeDefined(); expect(reg.types['plan-step']).toBeDefined(); @@ -37,6 +42,18 @@ describe('registry', () => { expect(reg.types.thread.builtIn).toBe(true); }); + it('adds company-context fields to existing built-in types without removing legacy fields', () => { + const reg = loadRegistry(workspacePath); + expect(reg.types.decision.fields.decided_by).toBeDefined(); + expect(reg.types.decision.fields.context_refs).toBeDefined(); + expect(reg.types.lesson.fields.severity).toBeDefined(); + expect(reg.types.person.fields.communication_preference).toBeDefined(); + expect(reg.types.agent.fields.permissions).toBeDefined(); + expect(reg.types.client.fields.key_contacts).toBeDefined(); + expect(reg.types.project.fields.priority).toBeDefined(); + expect(reg.types.policy.fields.scope_type).toBeDefined(); + }); + it('persists registry to disk', () => { const reg = loadRegistry(workspacePath); saveRegistry(workspacePath, reg); diff --git a/packages/kernel/src/registry.ts b/packages/kernel/src/registry.ts index c86c54e..0f8c632 100644 --- a/packages/kernel/src/registry.ts +++ b/packages/kernel/src/registry.ts @@ -82,6 +82,14 @@ const BUILT_IN_TYPES: PrimitiveTypeDefinition[] = [ title: { type: 'string', required: true }, date: { type: 'date', required: true }, status: { type: 'string', default: 'draft', enum: ['draft', 'proposed', 'approved', 'active', 'superseded', 'reverted'], description: 'draft | proposed | approved | active | superseded | reverted' }, + decided_by: { type: 'string' }, + participants:{ type: 'list', default: [] }, + alternatives:{ type: 'list', default: [] }, + rationale: { type: 'string' }, + consequences:{ type: 'list', default: [] }, + supersedes: { type: 'ref', refTypes: ['decision'] }, + related_refs:{ type: 'list', default: [] }, + external_links: { type: 'list', default: [] }, context_refs:{ type: 'list', default: [], description: 'What informed this decision' }, tags: { type: 'list', default: [] }, }, @@ -97,10 +105,106 @@ const BUILT_IN_TYPES: PrimitiveTypeDefinition[] = [ title: { type: 'string', required: true }, date: { type: 'date', required: true }, confidence: { type: 'string', default: 'medium', description: 'high | medium | low' }, + severity: { type: 'string', default: 'minor', enum: ['critical', 'important', 'minor'] }, + source_event:{ type: 'string' }, + applies_to: { type: 'list', default: [] }, + related_refs:{ type: 'list', default: [] }, context_refs:{ type: 'list', default: [] }, tags: { type: 'list', default: [] }, }, }, + { + name: 'org', + description: 'Top-level organizational context for mission, strategy, and structure.', + directory: 'orgs', + builtIn: true, + createdAt: '2026-01-01T00:00:00.000Z', + createdBy: 'system', + fields: { + title: { type: 'string', required: true }, + description: { type: 'string' }, + mission: { type: 'string' }, + strategy: { type: 'string' }, + structure: { type: 'string' }, + external_links: { type: 'list', default: [] }, + tags: { type: 'list', default: [] }, + created: { type: 'date', required: true }, + updated: { type: 'date', required: true }, + }, + }, + { + name: 'team', + description: 'A team boundary with members, ownership, and responsibilities.', + directory: 'teams', + builtIn: true, + createdAt: '2026-01-01T00:00:00.000Z', + createdBy: 'system', + fields: { + title: { type: 'string', required: true }, + description: { type: 'string' }, + members: { type: 'list', default: [] }, + owner: { type: 'string' }, + responsibilities: { type: 'list', default: [] }, + tags: { type: 'list', default: [] }, + created: { type: 'date', required: true }, + updated: { type: 'date', required: true }, + }, + }, + { + name: 'pattern', + description: 'A reusable delivery or operating pattern with steps and caveats.', + directory: 'patterns', + builtIn: true, + createdAt: '2026-01-01T00:00:00.000Z', + createdBy: 'system', + fields: { + title: { type: 'string', required: true }, + description: { type: 'string' }, + steps: { type: 'list', default: [] }, + exceptions: { type: 'list', default: [] }, + applies_to: { type: 'list', default: [] }, + related_refs: { type: 'list', default: [] }, + tags: { type: 'list', default: [] }, + created: { type: 'date', required: true }, + updated: { type: 'date', required: true }, + }, + }, + { + name: 'relationship', + description: 'An explicit typed edge between primitives or entities in the context graph.', + directory: 'relationships', + builtIn: true, + createdAt: '2026-01-01T00:00:00.000Z', + createdBy: 'system', + fields: { + title: { type: 'string', required: true }, + from_ref: { type: 'ref' }, + to_ref: { type: 'ref' }, + nature: { type: 'string' }, + context: { type: 'string' }, + strength: { type: 'string', default: 'moderate', enum: ['strong', 'moderate', 'weak'] }, + tags: { type: 'list', default: [] }, + created: { type: 'date', required: true }, + updated: { type: 'date', required: true }, + }, + }, + { + name: 'strategic_note', + description: 'Strategic note capturing decisions or focus across company/team/project/client scopes.', + directory: 'strategic-notes', + builtIn: true, + createdAt: '2026-01-01T00:00:00.000Z', + createdBy: 'system', + fields: { + title: { type: 'string', required: true }, + scope: { type: 'string', default: 'company', enum: ['company', 'team', 'project', 'client'] }, + timeframe: { type: 'string' }, + related_refs: { type: 'list', default: [] }, + tags: { type: 'list', default: [] }, + created: { type: 'date', required: true }, + updated: { type: 'date', required: true }, + }, + }, { name: 'fact', description: 'A structured piece of knowledge with optional temporal validity.', @@ -131,7 +235,13 @@ const BUILT_IN_TYPES: PrimitiveTypeDefinition[] = [ fields: { name: { type: 'string', required: true }, role: { type: 'string', description: 'What this agent specializes in' }, + description: { type: 'string' }, + runtime: { type: 'string' }, + location: { type: 'string' }, + connection_info: { type: 'any' }, capabilities: { type: 'list', default: [], description: 'What this agent can do' }, + permissions: { type: 'list', default: [] }, + team: { type: 'ref', refTypes: ['team'] }, active_threads: { type: 'list', default: [], description: 'Threads currently claimed' }, last_seen: { type: 'date' }, }, @@ -169,9 +279,14 @@ const BUILT_IN_TYPES: PrimitiveTypeDefinition[] = [ fields: { name: { type: 'string', required: true }, email: { type: 'string', template: 'email' }, + phone: { type: 'string' }, role: { type: 'string' }, + organization: { type: 'string' }, + relationship_context: { type: 'string' }, + communication_preference: { type: 'string', enum: ['email', 'phone', 'slack', 'whatsapp', 'telegram'] }, client: { type: 'ref', refTypes: ['client'] }, project_refs: { type: 'list', default: [] }, + external_links: { type: 'list', default: [] }, tags: { type: 'list', default: [] }, created: { type: 'date', required: true }, updated: { type: 'date', required: true }, @@ -186,10 +301,16 @@ const BUILT_IN_TYPES: PrimitiveTypeDefinition[] = [ createdBy: 'system', fields: { name: { type: 'string', required: true }, + description: { type: 'string' }, + industry: { type: 'string' }, + location: { type: 'string' }, + relationship_context: { type: 'string' }, status: { type: 'string', default: 'active', enum: ['prospect', 'active', 'paused', 'closed'] }, owner: { type: 'string' }, contact_ref: { type: 'ref', refTypes: ['person'] }, + key_contacts: { type: 'list', default: [] }, project_refs: { type: 'list', default: [] }, + external_links: { type: 'list', default: [] }, tags: { type: 'list', default: [] }, created: { type: 'date', required: true }, updated: { type: 'date', required: true }, @@ -204,11 +325,16 @@ const BUILT_IN_TYPES: PrimitiveTypeDefinition[] = [ createdBy: 'system', fields: { title: { type: 'string', required: true }, + description: { type: 'string' }, status: { type: 'string', default: 'active', enum: ['planned', 'active', 'blocked', 'done', 'cancelled'] }, + priority: { type: 'string', default: 'medium', enum: ['urgent', 'high', 'medium', 'low'] }, owner: { type: 'string' }, client: { type: 'ref', refTypes: ['client'] }, member_refs: { type: 'list', default: [] }, thread_refs: { type: 'list', default: [] }, + start_date: { type: 'date' }, + target_date: { type: 'date' }, + external_links: { type: 'list', default: [] }, tags: { type: 'list', default: [] }, created: { type: 'date', required: true }, updated: { type: 'date', required: true }, @@ -359,6 +485,10 @@ const BUILT_IN_TYPES: PrimitiveTypeDefinition[] = [ title: { type: 'string', required: true }, status: { type: 'string', default: 'draft', enum: ['draft', 'proposed', 'approved', 'active', 'retired'] }, scope: { type: 'string', default: 'workspace' }, + scope_type: { type: 'string', default: 'workspace', enum: ['workspace', 'team', 'project', 'runtime'] }, + enforcement: { type: 'string', default: 'advisory', enum: ['strict', 'advisory'] }, + exceptions: { type: 'list', default: [] }, + owner: { type: 'string' }, approvers: { type: 'list', default: [] }, created: { type: 'date', required: true }, updated: { type: 'date', required: true }, diff --git a/packages/kernel/src/starter-kit.ts b/packages/kernel/src/starter-kit.ts index a0337f6..4a29fa5 100644 --- a/packages/kernel/src/starter-kit.ts +++ b/packages/kernel/src/starter-kit.ts @@ -30,6 +30,7 @@ export interface StarterKitSeedResult { policies: StarterKitSeedSummary; gates: StarterKitSeedSummary; spaces: StarterKitSeedSummary; + orgs: StarterKitSeedSummary; trustTokens: StarterKitSeedSummary; bootstrapTrustToken: string; bootstrapTrustTokenPath: string; @@ -43,6 +44,7 @@ export function seedStarterKit(workspacePath: string): StarterKitSeedResult { const policySeeds = seedGroup(workspacePath, buildPolicySeeds()); const gateSeeds = seedGroup(workspacePath, buildGateSeeds()); const spaceSeeds = seedGroup(workspacePath, buildSpaceSeeds()); + const orgSeeds = seedGroup(workspacePath, buildOrgSeeds()); const configuredBootstrapPath = loadServerConfig(workspacePath)?.registration.bootstrapTokenPath ?? BOOTSTRAP_TRUST_TOKEN_PATH; const tokenSeed = seedBootstrapTrustToken(workspacePath, configuredBootstrapPath); @@ -55,6 +57,7 @@ export function seedStarterKit(workspacePath: string): StarterKitSeedResult { policies: policySeeds, gates: gateSeeds, spaces: spaceSeeds, + orgs: orgSeeds, trustTokens: { created: tokenSeed.created ? [tokenSeed.instance.path] : [], existing: tokenSeed.created ? [] : [tokenSeed.instance.path], @@ -336,6 +339,34 @@ function buildSpaceSeeds(): PrimitiveSeedSpec[] { ]; } +function buildOrgSeeds(): PrimitiveSeedSpec[] { + return [ + { + typeName: 'org', + path: 'orgs/company.md', + fields: { + title: 'Company', + mission: 'Deliver reliable outcomes through coordinated, context-rich execution.', + strategy: 'Scale agent-native workflows with explicit company context and decision memory.', + structure: 'Cross-functional teams organized around product and customer outcomes.', + external_links: [], + tags: ['starter-kit', 'company-context'], + }, + body: [ + '# Company Context', + '', + 'This starter org primitive anchors company-wide context for teams, decisions, and patterns.', + '', + '## Suggested use', + '', + '- Keep mission and strategy current.', + '- Link strategic notes, teams, and key relationships from this node.', + '', + ].join('\n'), + }, + ]; +} + function seedBootstrapTrustToken(workspacePath: string, tokenPath: string): { created: boolean; instance: PrimitiveInstance; diff --git a/packages/kernel/src/trigger-engine.test.ts b/packages/kernel/src/trigger-engine.test.ts index 70bbc3b..7785f56 100644 --- a/packages/kernel/src/trigger-engine.test.ts +++ b/packages/kernel/src/trigger-engine.test.ts @@ -618,9 +618,10 @@ describe('trigger engine', () => { expect(Number(payload.count)).toBeGreaterThanOrEqual(1); const outbox = transport.listTransportOutbox(workspacePath); - expect(outbox).toHaveLength(1); - expect(outbox[0]?.status).toBe('delivered'); - expect(outbox[0]?.deliveryTarget).toBe(server.url); + const webhookOutbox = outbox.filter((record) => record.deliveryHandler === 'trigger-webhook'); + expect(webhookOutbox).toHaveLength(1); + expect(webhookOutbox[0]?.status).toBe('delivered'); + expect(webhookOutbox[0]?.deliveryTarget).toBe(server.url); const webhookEntries = ledger.readAll(workspacePath).filter((entry) => entry.target === webhookTrigger.path && entry.data?.action === 'webhook' @@ -662,8 +663,9 @@ describe('trigger engine', () => { expect(triggerResult?.error).toContain('Webhook request failed'); const outbox = transport.listTransportOutbox(workspacePath); - expect(outbox).toHaveLength(1); - expect(outbox[0]?.status).toBe('failed'); + const webhookOutbox = outbox.filter((record) => record.deliveryHandler === 'trigger-webhook'); + expect(webhookOutbox).toHaveLength(1); + expect(webhookOutbox[0]?.status).toBe('failed'); const webhookEntries = ledger.readAll(workspacePath).filter((entry) => entry.target === webhookTrigger.path && entry.data?.action === 'webhook' diff --git a/packages/kernel/src/types.ts b/packages/kernel/src/types.ts index 1d55d56..1cf5324 100644 --- a/packages/kernel/src/types.ts +++ b/packages/kernel/src/types.ts @@ -380,6 +380,15 @@ export interface WorkgraphStatusSnapshot { }; } +export interface CompanyContext { + org?: { title: string; mission?: string; strategy?: string }; + teams: Array<{ title: string; members: string[]; responsibilities: string[] }>; + clients: Array<{ title: string; status: string; description?: string }>; + recentDecisions: Array<{ title: string; decidedBy?: string; date?: string; status: string }>; + patterns: Array<{ title: string; description?: string }>; + agentProfile?: { name: string; capabilities: string[]; permissions: string[] }; +} + export interface WorkgraphBrief { generatedAt: string; actor: string; @@ -388,6 +397,7 @@ export interface WorkgraphBrief { blockedThreads: PrimitiveInstance[]; nextReadyThreads: PrimitiveInstance[]; recentActivity: LedgerEntry[]; + companyContext: CompanyContext; } export type WorkgraphLensId = diff --git a/packages/kernel/src/workspace.test.ts b/packages/kernel/src/workspace.test.ts index 71aac3f..3e598f8 100644 --- a/packages/kernel/src/workspace.test.ts +++ b/packages/kernel/src/workspace.test.ts @@ -43,6 +43,7 @@ describe('workspace init', () => { expect(fs.existsSync(path.join(workspacePath, 'policies/escalation.md'))).toBe(true); expect(fs.existsSync(path.join(workspacePath, 'policy-gates/completion.md'))).toBe(true); expect(fs.existsSync(path.join(workspacePath, 'spaces/general.md'))).toBe(true); + expect(fs.existsSync(path.join(workspacePath, 'orgs/company.md'))).toBe(true); expect(fs.existsSync(path.join(workspacePath, result.bootstrapTrustTokenPath))).toBe(true); expect(result.bootstrapTrustToken).toMatch(/^wg-bootstrap-[a-f0-9]{24}$/); expect(result.seededTypes).toContain('thread'); diff --git a/packages/kernel/src/workspace.ts b/packages/kernel/src/workspace.ts index 75c2bf8..e54908e 100644 --- a/packages/kernel/src/workspace.ts +++ b/packages/kernel/src/workspace.ts @@ -164,6 +164,7 @@ Starter workgraph workspace seeded by \`workgraph init\`. This workspace includes editable default primitives: +- Org: \`orgs/company.md\` - Roles: \`roles/admin.md\`, \`roles/ops.md\`, \`roles/contributor.md\`, \`roles/viewer.md\` - Policies: \`policies/registration-approval.md\`, \`policies/thread-lifecycle.md\`, \`policies/escalation.md\` - Gate: \`policy-gates/completion.md\` diff --git a/packages/mcp-server/src/mcp-http-server.test.ts b/packages/mcp-server/src/mcp-http-server.test.ts index 1ca3f40..0cac9da 100644 --- a/packages/mcp-server/src/mcp-http-server.test.ts +++ b/packages/mcp-server/src/mcp-http-server.test.ts @@ -63,6 +63,7 @@ describe('mcp streamable http server', () => { try { const tools = await client.listTools(); expect(tools.tools.some((tool) => tool.name === 'workgraph_status')).toBe(true); + expect(tools.tools.some((tool) => tool.name === 'workgraph_company_context')).toBe(true); const status = await client.callTool({ name: 'workgraph_status', @@ -70,6 +71,14 @@ describe('mcp streamable http server', () => { }); expect(isToolError(status)).toBe(false); + const companyContext = await client.callTool({ + name: 'workgraph_company_context', + arguments: { + actor: 'http-operator', + }, + }); + expect(isToolError(companyContext)).toBe(false); + const runCreated = await client.callTool({ name: 'workgraph_dispatch_create', arguments: { diff --git a/packages/mcp-server/src/mcp-server.test.ts b/packages/mcp-server/src/mcp-server.test.ts index 29ec67f..fc947cf 100644 --- a/packages/mcp-server/src/mcp-server.test.ts +++ b/packages/mcp-server/src/mcp-server.test.ts @@ -63,6 +63,7 @@ describe('workgraph mcp server', () => { const tools = await client.listTools(); const toolNames = tools.tools.map((entry) => entry.name); expect(toolNames).toContain('workgraph_status'); + expect(toolNames).toContain('workgraph_company_context'); expect(toolNames).toContain('workgraph_primitive_schema'); expect(toolNames).toContain('workgraph_ledger_reconcile'); expect(toolNames).toContain('workgraph_thread_create'); @@ -76,6 +77,9 @@ describe('workgraph mcp server', () => { expect(toolNames).toContain('workgraph_dispatch_execute'); expect(toolNames).toContain('workgraph_trigger_create'); expect(toolNames).toContain('workgraph_trigger_fire'); + expect(toolNames).toContain('workgraph_create_decision'); + expect(toolNames).toContain('workgraph_record_lesson'); + expect(toolNames).toContain('workgraph_record_pattern'); expect(toolNames).toContain('workgraph_create_mission'); expect(toolNames).toContain('workgraph_mission_status'); @@ -87,6 +91,24 @@ describe('workgraph mcp server', () => { const statusPayload = getStructured<{ threads: { total: number } }>(statusTool); expect(statusPayload.threads.total).toBeGreaterThan(0); + const companyContextResult = await client.callTool({ + name: 'workgraph_company_context', + arguments: { + actor: 'agent-mcp', + }, + }); + expect(isToolError(companyContextResult)).toBe(false); + const companyContextPayload = getStructured<{ + teams: unknown[]; + clients: unknown[]; + recentDecisions: unknown[]; + patterns: unknown[]; + }>(companyContextResult); + expect(Array.isArray(companyContextPayload.teams)).toBe(true); + expect(Array.isArray(companyContextPayload.clients)).toBe(true); + expect(Array.isArray(companyContextPayload.recentDecisions)).toBe(true); + expect(Array.isArray(companyContextPayload.patterns)).toBe(true); + const statusResource = await client.readResource({ uri: 'workgraph://status' }); const firstContent = statusResource.contents[0]; const statusText = firstContent && 'text' in firstContent ? firstContent.text : ''; @@ -134,6 +156,63 @@ describe('workgraph mcp server', () => { ], }); + const createdDecision = await client.callTool({ + name: 'workgraph_create_decision', + arguments: { + title: 'Adopt company context graph', + actor: 'agent-mcp', + rationale: 'Improve organizational context quality for autonomous agents.', + participants: ['agent-mcp', 'agent-mcp-2'], + alternatives: ['Keep ad-hoc context notes'], + }, + }); + expect(isToolError(createdDecision)).toBe(false); + const decisionPayload = getStructured<{ decision: { path: string; fields: { title: string } } }>(createdDecision); + expect(decisionPayload.decision.path).toMatch(/^decisions\//); + expect(decisionPayload.decision.fields.title).toBe('Adopt company context graph'); + + const recordedLesson = await client.callTool({ + name: 'workgraph_record_lesson', + arguments: { + title: 'Track decisions with explicit rationale', + actor: 'agent-mcp', + severity: 'important', + sourceEvent: 'postmortem-2026-03-14', + }, + }); + expect(isToolError(recordedLesson)).toBe(false); + const lessonPayload = getStructured<{ lesson: { path: string; fields: { severity: string } } }>(recordedLesson); + expect(lessonPayload.lesson.path).toMatch(/^lessons\//); + expect(lessonPayload.lesson.fields.severity).toBe('important'); + + const recordedPattern = await client.callTool({ + name: 'workgraph_record_pattern', + arguments: { + title: 'Decision preflight checklist', + actor: 'agent-mcp', + steps: ['Gather stakeholders', 'Capture alternatives', 'Record rationale'], + exceptions: ['Emergency incident response'], + }, + }); + expect(isToolError(recordedPattern)).toBe(false); + const patternPayload = getStructured<{ pattern: { path: string; fields: { title: string } } }>(recordedPattern); + expect(patternPayload.pattern.path).toMatch(/^patterns\//); + expect(patternPayload.pattern.fields.title).toBe('Decision preflight checklist'); + + const companyContextAfterWrites = await client.callTool({ + name: 'workgraph_company_context', + arguments: { + actor: 'agent-mcp', + }, + }); + expect(isToolError(companyContextAfterWrites)).toBe(false); + const contextAfterWritesPayload = getStructured<{ + recentDecisions: Array<{ title: string }>; + patterns: Array<{ title: string }>; + }>(companyContextAfterWrites); + expect(contextAfterWritesPayload.recentDecisions.some((entry) => entry.title === 'Adopt company context graph')).toBe(true); + expect(contextAfterWritesPayload.patterns.some((entry) => entry.title === 'Decision preflight checklist')).toBe(true); + const created = await client.callTool({ name: 'workgraph_thread_create', arguments: { @@ -344,6 +423,7 @@ describe('workgraph mcp server', () => { const expectedTools = [ 'workgraph_status', 'workgraph_brief', + 'workgraph_company_context', 'workgraph_query', 'workgraph_primitive_schema', 'workgraph_thread_list', @@ -361,6 +441,9 @@ describe('workgraph mcp server', () => { 'workgraph_thread_join', 'workgraph_thread_done', 'workgraph_checkpoint_create', + 'workgraph_create_decision', + 'workgraph_record_lesson', + 'workgraph_record_pattern', 'workgraph_dispatch_create', 'workgraph_dispatch_execute', 'workgraph_dispatch_followup', diff --git a/packages/mcp-server/src/mcp/tools/read-tools.ts b/packages/mcp-server/src/mcp/tools/read-tools.ts index b4a2190..8490e91 100644 --- a/packages/mcp-server/src/mcp/tools/read-tools.ts +++ b/packages/mcp-server/src/mcp/tools/read-tools.ts @@ -81,6 +81,33 @@ export function registerReadTools(server: McpServer, options: WorkgraphMcpServer }, ); + server.registerTool( + 'workgraph_company_context', + { + title: 'Workgraph Company Context', + description: 'Return company context graph view for an actor.', + inputSchema: { + actor: z.string().optional(), + }, + annotations: { + readOnlyHint: true, + idempotentHint: true, + }, + }, + async (args) => { + try { + const actor = resolveActor(options.workspacePath, args.actor, options.defaultActor); + const companyContext = orientation.companyContext(options.workspacePath, actor); + return okResult( + companyContext, + `Company context for ${actor}: teams=${companyContext.teams.length}, clients=${companyContext.clients.length}.`, + ); + } catch (error) { + return errorResult(error); + } + }, + ); + server.registerTool( 'workgraph_query', { diff --git a/packages/mcp-server/src/mcp/tools/write-tools.ts b/packages/mcp-server/src/mcp/tools/write-tools.ts index 083873b..b846f4c 100644 --- a/packages/mcp-server/src/mcp/tools/write-tools.ts +++ b/packages/mcp-server/src/mcp/tools/write-tools.ts @@ -7,6 +7,7 @@ import { mission as missionModule, missionOrchestrator as missionOrchestratorModule, orientation as orientationModule, + store as storeModule, transport as transportModule, threadContext as threadContextModule, thread as threadModule, @@ -23,6 +24,7 @@ const dispatch = dispatchModule; const mission = missionModule; const missionOrchestrator = missionOrchestratorModule; const orientation = orientationModule; +const store = storeModule; const transport = transportModule; const threadContext = threadContextModule; const thread = threadModule; @@ -617,6 +619,175 @@ export function registerWriteTools(server: McpServer, options: WorkgraphMcpServe }, ); + server.registerTool( + 'workgraph_create_decision', + { + title: 'Decision Create', + description: 'Create a decision primitive with rationale, participants, and alternatives.', + inputSchema: { + title: z.string().min(1), + actor: z.string().optional(), + status: z.enum(['draft', 'proposed', 'approved', 'active', 'superseded', 'reverted']).optional(), + date: z.string().optional(), + decidedBy: z.string().optional(), + participants: z.array(z.string()).optional(), + alternatives: z.array(z.string()).optional(), + rationale: z.string().optional(), + consequences: z.array(z.string()).optional(), + supersedes: z.string().optional(), + relatedRefs: z.array(z.string()).optional(), + externalLinks: z.array(z.string()).optional(), + contextRefs: z.array(z.string()).optional(), + tags: z.array(z.string()).optional(), + body: z.string().optional(), + }, + annotations: { + destructiveHint: true, + idempotentHint: false, + }, + }, + async (args) => { + try { + const actor = resolveActor(options.workspacePath, args.actor, options.defaultActor); + const gate = checkWriteGate(options, actor, ['mcp:write'], { + action: 'mcp.decision.create', + target: 'decisions', + }); + if (!gate.allowed) return errorResult(gate.reason); + const decision = store.create( + options.workspacePath, + 'decision', + { + title: args.title, + date: args.date ?? new Date().toISOString(), + status: args.status, + decided_by: args.decidedBy ?? actor, + participants: args.participants ?? [], + alternatives: args.alternatives ?? [], + rationale: args.rationale, + consequences: args.consequences ?? [], + supersedes: args.supersedes, + related_refs: args.relatedRefs ?? [], + external_links: args.externalLinks ?? [], + context_refs: args.contextRefs ?? [], + tags: args.tags ?? [], + }, + args.body ?? '', + actor, + ); + return okResult({ decision }, `Created decision ${decision.path}.`); + } catch (error) { + return errorResult(error); + } + }, + ); + + server.registerTool( + 'workgraph_record_lesson', + { + title: 'Lesson Record', + description: 'Record a lesson with severity and source event context.', + inputSchema: { + title: z.string().min(1), + actor: z.string().optional(), + date: z.string().optional(), + confidence: z.string().optional(), + severity: z.enum(['critical', 'important', 'minor']).optional(), + sourceEvent: z.string().optional(), + appliesTo: z.array(z.string()).optional(), + relatedRefs: z.array(z.string()).optional(), + contextRefs: z.array(z.string()).optional(), + tags: z.array(z.string()).optional(), + body: z.string().optional(), + }, + annotations: { + destructiveHint: true, + idempotentHint: false, + }, + }, + async (args) => { + try { + const actor = resolveActor(options.workspacePath, args.actor, options.defaultActor); + const gate = checkWriteGate(options, actor, ['mcp:write'], { + action: 'mcp.lesson.record', + target: 'lessons', + }); + if (!gate.allowed) return errorResult(gate.reason); + const lesson = store.create( + options.workspacePath, + 'lesson', + { + title: args.title, + date: args.date ?? new Date().toISOString(), + confidence: args.confidence, + severity: args.severity, + source_event: args.sourceEvent, + applies_to: args.appliesTo ?? [], + related_refs: args.relatedRefs ?? [], + context_refs: args.contextRefs ?? [], + tags: args.tags ?? [], + }, + args.body ?? '', + actor, + ); + return okResult({ lesson }, `Recorded lesson ${lesson.path}.`); + } catch (error) { + return errorResult(error); + } + }, + ); + + server.registerTool( + 'workgraph_record_pattern', + { + title: 'Pattern Record', + description: 'Record a reusable pattern with steps and exceptions.', + inputSchema: { + title: z.string().min(1), + actor: z.string().optional(), + description: z.string().optional(), + steps: z.array(z.string()).optional(), + exceptions: z.array(z.string()).optional(), + appliesTo: z.array(z.string()).optional(), + relatedRefs: z.array(z.string()).optional(), + tags: z.array(z.string()).optional(), + body: z.string().optional(), + }, + annotations: { + destructiveHint: true, + idempotentHint: false, + }, + }, + async (args) => { + try { + const actor = resolveActor(options.workspacePath, args.actor, options.defaultActor); + const gate = checkWriteGate(options, actor, ['mcp:write'], { + action: 'mcp.pattern.record', + target: 'patterns', + }); + if (!gate.allowed) return errorResult(gate.reason); + const pattern = store.create( + options.workspacePath, + 'pattern', + { + title: args.title, + description: args.description, + steps: args.steps ?? [], + exceptions: args.exceptions ?? [], + applies_to: args.appliesTo ?? [], + related_refs: args.relatedRefs ?? [], + tags: args.tags ?? [], + }, + args.body ?? '', + actor, + ); + return okResult({ pattern }, `Recorded pattern ${pattern.path}.`); + } catch (error) { + return errorResult(error); + } + }, + ); + server.registerTool( 'workgraph_dispatch_create', {