From bbf90a1a622d3bb78786f9f4a205ebde35c4e7a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sat, 23 May 2026 18:39:05 -0400 Subject: [PATCH] Add SDLC review and eval controls --- AgentCompany-New/src/main/app/ipc-handlers.ts | 5 + .../database/migrations/018-sdlc-controls.ts | 137 ++++++++ AgentCompany-New/src/main/database/schema.ts | 2 + .../src/main/services/migration-service.ts | 15 + .../src/main/services/organization-service.ts | 4 + .../src/main/services/sdlc-service.ts | 292 ++++++++++++++++++ .../src/main/services/snapshot-service.ts | 10 + AgentCompany-New/src/preload/index.ts | 3 + .../src/renderer/src/pages/SettingsPage.tsx | 23 ++ .../renderer/src/pages/settings/SdlcPanel.tsx | 240 ++++++++++++++ AgentCompany-New/src/shared/commands.ts | 19 ++ AgentCompany-New/src/shared/desktop-api.ts | 6 + AgentCompany-New/src/shared/domain.ts | 3 + AgentCompany-New/src/shared/ipc.ts | 3 + AgentCompany-New/src/shared/sdlc-domain.ts | 48 +++ AgentCompany-New/tests/e2e/desktop.spec.ts | 10 +- .../tests/unit/domain-services.test.ts | 5 +- .../tests/unit/migration-service.test.ts | 16 + .../tests/unit/sdlc-service.test.ts | 88 ++++++ 19 files changed, 927 insertions(+), 2 deletions(-) create mode 100644 AgentCompany-New/src/main/database/migrations/018-sdlc-controls.ts create mode 100644 AgentCompany-New/src/main/services/sdlc-service.ts create mode 100644 AgentCompany-New/src/renderer/src/pages/settings/SdlcPanel.tsx create mode 100644 AgentCompany-New/src/shared/sdlc-domain.ts create mode 100644 AgentCompany-New/tests/unit/sdlc-service.test.ts diff --git a/AgentCompany-New/src/main/app/ipc-handlers.ts b/AgentCompany-New/src/main/app/ipc-handlers.ts index 587d9b94..d96e946a 100644 --- a/AgentCompany-New/src/main/app/ipc-handlers.ts +++ b/AgentCompany-New/src/main/app/ipc-handlers.ts @@ -17,6 +17,7 @@ import { ComplianceService } from "../services/compliance-service"; import { SearchService } from "../services/search-service"; import { IdentityAccessService } from "../services/identity-access-service"; import { OperationsService } from "../services/operations-service"; +import { SdlcService } from "../services/sdlc-service"; export interface IpcDeps { connection: DatabaseConnection; @@ -38,6 +39,7 @@ export function registerIpcHandlers(deps: IpcDeps): void { const search = new SearchService(deps.connection.db); const identityAccess = new IdentityAccessService(deps.connection.db); const operations = new OperationsService(deps.connection.db); + const sdlc = new SdlcService(deps.connection.db); const onboarding = new OnboardingService({ db: deps.connection.db, saveProviderCredential: (input) => providers.saveCredential(input), @@ -157,6 +159,9 @@ export function registerIpcHandlers(deps: IpcDeps): void { ipcMain.handle(IPC_CHANNELS.createMigrationReport, (_event, input) => mutate(() => migrations.createReport(input))); ipcMain.handle(IPC_CHANNELS.setFeatureFlag, (_event, input) => mutate(() => operations.setFeatureFlag(input))); ipcMain.handle(IPC_CHANNELS.runChaosDrill, (_event, input) => mutate(() => operations.runChaosDrill(input))); + ipcMain.handle(IPC_CHANNELS.runSdlcReview, (_event, input) => mutate(() => sdlc.runReview(input))); + ipcMain.handle(IPC_CHANNELS.runEvalSuite, (_event, input) => mutate(() => sdlc.runEvalSuite(input))); + ipcMain.handle(IPC_CHANNELS.setPrBotGate, (_event, input) => mutate(() => sdlc.setPrBotGate(input))); ipcMain.handle(IPC_CHANNELS.createComplianceExport, (_event, input) => mutate(() => compliance.createExport(input))); ipcMain.handle(IPC_CHANNELS.createLegalHold, (_event, input) => mutate(() => compliance.createLegalHold(input))); ipcMain.handle(IPC_CHANNELS.setLegalHoldState, (_event, input) => mutate(() => compliance.setLegalHoldState(input))); diff --git a/AgentCompany-New/src/main/database/migrations/018-sdlc-controls.ts b/AgentCompany-New/src/main/database/migrations/018-sdlc-controls.ts new file mode 100644 index 00000000..7d2818b8 --- /dev/null +++ b/AgentCompany-New/src/main/database/migrations/018-sdlc-controls.ts @@ -0,0 +1,137 @@ +export const sdlcControlsSchema = ` + create table if not exists sdlc_review_agents ( + id text primary key, + org_id text not null references organizations(id) on delete cascade, + key text not null, + label text not null, + specialty text not null, + enabled integer not null check(enabled in (0, 1)), + created_at text not null, + updated_at text not null, + unique(org_id, key) + ); + + create table if not exists sdlc_review_runs ( + id text primary key, + org_id text not null references organizations(id) on delete cascade, + review_agent_id text not null references sdlc_review_agents(id) on delete cascade, + target_type text not null, + target_ref text not null, + state text not null, + result_json text not null, + created_at text not null, + completed_at text not null + ); + + create table if not exists eval_suites ( + id text primary key, + org_id text not null references organizations(id) on delete cascade, + key text not null, + label text not null, + scope text not null, + created_at text not null, + updated_at text not null, + unique(org_id, key) + ); + + create table if not exists eval_cases ( + id text primary key, + org_id text not null references organizations(id) on delete cascade, + suite_id text not null references eval_suites(id) on delete cascade, + title text not null, + source_type text not null, + source_ref text not null, + expected_signal text not null, + created_at text not null, + unique(suite_id, title) + ); + + create table if not exists eval_runs ( + id text primary key, + org_id text not null references organizations(id) on delete cascade, + suite_id text not null references eval_suites(id) on delete cascade, + state text not null, + passed_count integer not null, + failed_count integer not null, + result_json text not null, + created_at text not null, + completed_at text not null + ); + + create table if not exists pr_bot_gates ( + id text primary key, + org_id text not null references organizations(id) on delete cascade, + key text not null, + label text not null, + gate_type text not null, + enabled integer not null check(enabled in (0, 1)), + created_at text not null, + updated_at text not null, + unique(org_id, key) + ); + + create index if not exists idx_sdlc_review_runs_org_agent_created + on sdlc_review_runs(org_id, review_agent_id, created_at); + + create index if not exists idx_eval_runs_org_suite_created + on eval_runs(org_id, suite_id, created_at); + + insert into sdlc_review_agents(id, org_id, key, label, specialty, enabled, created_at, updated_at) + select 'sra_' || lower(hex(randomblob(16))), organizations.id, seed.key, seed.label, seed.specialty, 1, datetime('now'), datetime('now') + from organizations + cross join ( + select 'code_reviewer' as key, 'Code Review Agent' as label, 'static quality and architecture' as specialty + union all select 'migration_reviewer', 'Migration Review Agent', 'schema diff and rollback evidence' + union all select 'policy_reviewer', 'Policy Review Agent', 'provider, approval, and access policy' + ) seed + where not exists ( + select 1 from sdlc_review_agents + where sdlc_review_agents.org_id = organizations.id and sdlc_review_agents.key = seed.key + ); + + insert into eval_suites(id, org_id, key, label, scope, created_at, updated_at) + select 'evs_' || lower(hex(randomblob(16))), organizations.id, seed.key, seed.label, seed.scope, datetime('now'), datetime('now') + from organizations + cross join ( + select 'contract' as key, 'Contract checks' as label, 'provider adapters, event schemas, slash commands' as scope + union all select 'integration', 'Integration checks', 'chat, approval, file, skill resolution flows' + union all select 'replay', 'Replay checks', 'audit timeline and historical decision regression' + ) seed + where not exists ( + select 1 from eval_suites + where eval_suites.org_id = organizations.id and eval_suites.key = seed.key + ); + + insert into eval_cases(id, org_id, suite_id, title, source_type, source_ref, expected_signal, created_at) + select 'evc_' || lower(hex(randomblob(16))), eval_suites.org_id, eval_suites.id, seed.title, seed.source_type, seed.source_ref, seed.expected_signal, datetime('now') + from eval_suites + join ( + select 'contract' as suite_key, 'Provider catalog has adapters' as title, 'provider_gateway' as source_type, 'providers' as source_ref, 'adapter_coverage' as expected_signal + union all select 'contract', 'Schema ledger is current', 'database', 'schema_migrations', 'migration_ledger' + union all select 'integration', 'Approval routes are assigned', 'org_governance', 'approval_assignments', 'route_coverage' + union all select 'integration', 'Skills can resolve runtime snapshots', 'skills_platform', 'skill_runtime_snapshots', 'skill_resolution' + union all select 'replay', 'Audit timeline has domain events', 'audit', 'audit_events', 'replayable_timeline' + union all select 'replay', 'Operational signals are captured', 'operations', 'operational_events', 'slo_signal' + ) seed on seed.suite_key = eval_suites.key + where not exists ( + select 1 from eval_cases + where eval_cases.suite_id = eval_suites.id and eval_cases.title = seed.title + ); + + insert into pr_bot_gates(id, org_id, key, label, gate_type, enabled, created_at, updated_at) + select 'pbg_' || lower(hex(randomblob(16))), organizations.id, seed.key, seed.label, seed.gate_type, 1, datetime('now'), datetime('now') + from organizations + cross join ( + select 'static_checks' as key, 'Static checks' as label, 'typecheck_lint_test' as gate_type + union all select 'migration_diff', 'Migration diff' as label, 'schema_review' as gate_type + union all select 'policy_diff', 'Policy diff' as label, 'policy_review' as gate_type + union all select 'human_approval', 'Human approval' as label, 'approval_required' as gate_type + ) seed + where not exists ( + select 1 from pr_bot_gates + where pr_bot_gates.org_id = organizations.id and pr_bot_gates.key = seed.key + ); + + insert into schema_migrations(version, name, applied_at) + values (18, 'sdlc_controls', datetime('now')); +`; diff --git a/AgentCompany-New/src/main/database/schema.ts b/AgentCompany-New/src/main/database/schema.ts index 45408b68..e7c47b6f 100644 --- a/AgentCompany-New/src/main/database/schema.ts +++ b/AgentCompany-New/src/main/database/schema.ts @@ -16,6 +16,7 @@ import { providerPolicyUsageSchema } from "./migrations/014-provider-policy-usag import { identityAccessSchema } from "./migrations/015-identity-access"; import { orgGovernanceSchema } from "./migrations/016-org-governance"; import { operationsControlsSchema } from "./migrations/017-operations-controls"; +import { sdlcControlsSchema } from "./migrations/018-sdlc-controls"; interface Migration { version: number; @@ -41,6 +42,7 @@ const MIGRATIONS: Migration[] = [ { version: 15, name: "identity_access", sql: identityAccessSchema }, { version: 16, name: "org_governance", sql: orgGovernanceSchema }, { version: 17, name: "operations_controls", sql: operationsControlsSchema }, + { version: 18, name: "sdlc_controls", sql: sdlcControlsSchema }, ]; const SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1].version; diff --git a/AgentCompany-New/src/main/services/migration-service.ts b/AgentCompany-New/src/main/services/migration-service.ts index ffa52683..c7be8090 100644 --- a/AgentCompany-New/src/main/services/migration-service.ts +++ b/AgentCompany-New/src/main/services/migration-service.ts @@ -31,6 +31,12 @@ const LEGACY_TABLES = [ "operational_events", "chaos_drills", "chaos_drill_runs", + "sdlc_review_agents", + "sdlc_review_runs", + "eval_suites", + "eval_cases", + "eval_runs", + "pr_bot_gates", "chat_spaces", "chat_messages", "chat_threads", @@ -142,6 +148,15 @@ const RELATION_CHECKS: RelationCheck[] = [ { table: "chaos_drills", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" }, { table: "chaos_drill_runs", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" }, { table: "chaos_drill_runs", column: "drill_id", referencedTable: "chaos_drills", referencedColumn: "id", reason: "dangling_drill_id" }, + { table: "sdlc_review_agents", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" }, + { table: "sdlc_review_runs", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" }, + { table: "sdlc_review_runs", column: "review_agent_id", referencedTable: "sdlc_review_agents", referencedColumn: "id", reason: "dangling_review_agent_id" }, + { table: "eval_suites", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" }, + { table: "eval_cases", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" }, + { table: "eval_cases", column: "suite_id", referencedTable: "eval_suites", referencedColumn: "id", reason: "dangling_suite_id" }, + { table: "eval_runs", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" }, + { table: "eval_runs", column: "suite_id", referencedTable: "eval_suites", referencedColumn: "id", reason: "dangling_suite_id" }, + { table: "pr_bot_gates", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" }, { table: "chat_spaces", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" }, { table: "chat_messages", column: "org_id", referencedTable: "organizations", referencedColumn: "id", reason: "dangling_org_id" }, { table: "chat_messages", column: "space_id", referencedTable: "chat_spaces", referencedColumn: "id", reason: "dangling_space_id" }, diff --git a/AgentCompany-New/src/main/services/organization-service.ts b/AgentCompany-New/src/main/services/organization-service.ts index 6b3b4598..73fceda1 100644 --- a/AgentCompany-New/src/main/services/organization-service.ts +++ b/AgentCompany-New/src/main/services/organization-service.ts @@ -8,6 +8,7 @@ import { PolicyService } from "./policy-service"; import { IdentityAccessService } from "./identity-access-service"; import { OrgGovernanceService } from "./org-governance-service"; import { OperationsService } from "./operations-service"; +import { SdlcService } from "./sdlc-service"; import { assertUsableMentionName } from "./mention-handles"; export class OrganizationService { @@ -16,6 +17,7 @@ export class OrganizationService { private readonly identityAccess: IdentityAccessService; private readonly governance: OrgGovernanceService; private readonly operations: OperationsService; + private readonly sdlc: SdlcService; constructor(private readonly db: DatabaseSync) { this.audit = new AuditService(db); @@ -23,6 +25,7 @@ export class OrganizationService { this.identityAccess = new IdentityAccessService(db); this.governance = new OrgGovernanceService(db); this.operations = new OperationsService(db); + this.sdlc = new SdlcService(db); } createOrganization(input: CreateOrganizationInput): string { @@ -52,6 +55,7 @@ export class OrganizationService { this.identityAccess.seedEmployeeAccess(orgId, employeeId, now); this.governance.seedOrganizationGovernance(orgId, employeeId, now); this.operations.seedOrganizationOperations(orgId, now); + this.sdlc.seedOrganizationSdlc(orgId, now); for (const space of [ ["company", "general", "Company coordination", "workspace"], diff --git a/AgentCompany-New/src/main/services/sdlc-service.ts b/AgentCompany-New/src/main/services/sdlc-service.ts new file mode 100644 index 00000000..dc43c5a4 --- /dev/null +++ b/AgentCompany-New/src/main/services/sdlc-service.ts @@ -0,0 +1,292 @@ +import type { DatabaseSync } from "node:sqlite"; +import type { RunEvalSuiteInput, RunSdlcReviewInput, SetPrBotGateInput } from "../../shared/commands"; +import { + EVAL_SUITE_KEY, + PR_BOT_GATE_KEY, + SDLC_REVIEW_AGENT_KEY, + SDLC_REVIEW_TARGET_TYPE, + SDLC_RUN_STATE, + type EvalSuiteKey, + type SdlcReviewAgentKey, + type SdlcReviewTargetType, +} from "../../shared/domain"; +import { PROVIDER_CATALOG } from "../../shared/provider-catalog"; +import { createId } from "../../shared/ids"; +import { AuditService } from "./audit-service"; +import { providerAdapters } from "./provider-adapters"; + +const REVIEW_AGENTS = [ + { key: SDLC_REVIEW_AGENT_KEY.CODE_REVIEWER, label: "Code Review Agent", specialty: "static quality and architecture" }, + { key: SDLC_REVIEW_AGENT_KEY.MIGRATION_REVIEWER, label: "Migration Review Agent", specialty: "schema diff and rollback evidence" }, + { key: SDLC_REVIEW_AGENT_KEY.POLICY_REVIEWER, label: "Policy Review Agent", specialty: "provider, approval, and access policy" }, +] as const; + +const EVAL_SUITES = [ + { key: EVAL_SUITE_KEY.CONTRACT, label: "Contract checks", scope: "provider adapters, event schemas, slash commands" }, + { key: EVAL_SUITE_KEY.INTEGRATION, label: "Integration checks", scope: "chat, approval, file, skill resolution flows" }, + { key: EVAL_SUITE_KEY.REPLAY, label: "Replay checks", scope: "audit timeline and historical decision regression" }, +] as const; + +const EVAL_CASES = [ + { suiteKey: EVAL_SUITE_KEY.CONTRACT, title: "Provider catalog has adapters", sourceType: "provider_gateway", sourceRef: "providers", expectedSignal: "adapter_coverage" }, + { suiteKey: EVAL_SUITE_KEY.CONTRACT, title: "Schema ledger is current", sourceType: "database", sourceRef: "schema_migrations", expectedSignal: "migration_ledger" }, + { suiteKey: EVAL_SUITE_KEY.INTEGRATION, title: "Approval routes are assigned", sourceType: "org_governance", sourceRef: "approval_assignments", expectedSignal: "route_coverage" }, + { suiteKey: EVAL_SUITE_KEY.INTEGRATION, title: "Skills can resolve runtime snapshots", sourceType: "skills_platform", sourceRef: "skill_runtime_snapshots", expectedSignal: "skill_resolution" }, + { suiteKey: EVAL_SUITE_KEY.REPLAY, title: "Audit timeline has domain events", sourceType: "audit", sourceRef: "audit_events", expectedSignal: "replayable_timeline" }, + { suiteKey: EVAL_SUITE_KEY.REPLAY, title: "Operational signals are captured", sourceType: "operations", sourceRef: "operational_events", expectedSignal: "slo_signal" }, +] as const; + +const PR_BOT_GATES = [ + { key: PR_BOT_GATE_KEY.STATIC_CHECKS, label: "Static checks", gateType: "typecheck_lint_test" }, + { key: PR_BOT_GATE_KEY.MIGRATION_DIFF, label: "Migration diff", gateType: "schema_review" }, + { key: PR_BOT_GATE_KEY.POLICY_DIFF, label: "Policy diff", gateType: "policy_review" }, + { key: PR_BOT_GATE_KEY.HUMAN_APPROVAL, label: "Human approval", gateType: "approval_required" }, +] as const; + +interface ReviewAgentRow { + id: string; + key: SdlcReviewAgentKey; + enabled: number; +} + +interface EvalSuiteRow { + id: string; + key: EvalSuiteKey; +} + +interface GateCheck { + key: string; + passed: boolean; + value: number | string | boolean; +} + +export class SdlcService { + private readonly audit: AuditService; + + constructor(private readonly db: DatabaseSync) { + this.audit = new AuditService(db); + } + + seedOrganizationSdlc(orgId: string, now: string): void { + for (const agent of REVIEW_AGENTS) { + this.db.prepare(` + insert into sdlc_review_agents(id, org_id, key, label, specialty, enabled, created_at, updated_at) + values (?, ?, ?, ?, ?, 1, ?, ?) + on conflict(org_id, key) do nothing + `).run(createId("sra"), orgId, agent.key, agent.label, agent.specialty, now, now); + } + for (const suite of EVAL_SUITES) { + this.db.prepare(` + insert into eval_suites(id, org_id, key, label, scope, created_at, updated_at) + values (?, ?, ?, ?, ?, ?, ?) + on conflict(org_id, key) do nothing + `).run(createId("evs"), orgId, suite.key, suite.label, suite.scope, now, now); + } + for (const item of EVAL_CASES) { + const suite = this.requireSuiteByKey(orgId, item.suiteKey); + this.db.prepare(` + insert into eval_cases(id, org_id, suite_id, title, source_type, source_ref, expected_signal, created_at) + values (?, ?, ?, ?, ?, ?, ?, ?) + on conflict(suite_id, title) do nothing + `).run(createId("evc"), orgId, suite.id, item.title, item.sourceType, item.sourceRef, item.expectedSignal, now); + } + for (const gate of PR_BOT_GATES) { + this.db.prepare(` + insert into pr_bot_gates(id, org_id, key, label, gate_type, enabled, created_at, updated_at) + values (?, ?, ?, ?, ?, 1, ?, ?) + on conflict(org_id, key) do nothing + `).run(createId("pbg"), orgId, gate.key, gate.label, gate.gateType, now, now); + } + } + + runReview(input: RunSdlcReviewInput): string { + const agent = this.db.prepare("select id, key, enabled from sdlc_review_agents where id = ? and org_id = ?") + .get(input.reviewAgentId, input.orgId) as ReviewAgentRow | undefined; + if (!agent) throw new Error("SDLC review agent was not found."); + if (agent.enabled !== 1) throw new Error("SDLC review agent is disabled."); + this.assertTarget(input.orgId, input.targetType, input.targetRef); + const checks = this.reviewChecks(input.orgId, agent.key, input.targetType); + const state = checks.every((check) => check.passed) ? SDLC_RUN_STATE.PASSED : SDLC_RUN_STATE.FAILED; + const id = createId("srr"); + const now = new Date().toISOString(); + this.db.prepare(` + insert into sdlc_review_runs(id, org_id, review_agent_id, target_type, target_ref, state, result_json, created_at, completed_at) + values (?, ?, ?, ?, ?, ?, ?, ?, ?) + `).run(id, input.orgId, input.reviewAgentId, input.targetType, input.targetRef, state, JSON.stringify({ checks }), now, now); + this.audit.record({ + orgId: input.orgId, + entityType: "sdlc_review_run", + entityId: id, + actorType: "system", + actorId: null, + action: "created", + payload: { reviewAgentKey: agent.key, state, targetType: input.targetType }, + }); + return id; + } + + runEvalSuite(input: RunEvalSuiteInput): string { + const suite = this.db.prepare("select id, key from eval_suites where id = ? and org_id = ?") + .get(input.suiteId, input.orgId) as EvalSuiteRow | undefined; + if (!suite) throw new Error("Eval suite was not found."); + const checks = this.evalChecks(input.orgId, suite.key); + const passedCount = checks.filter((check) => check.passed).length; + const failedCount = checks.length - passedCount; + const state = failedCount === 0 ? SDLC_RUN_STATE.PASSED : SDLC_RUN_STATE.FAILED; + const id = createId("evr"); + const now = new Date().toISOString(); + this.db.prepare(` + insert into eval_runs(id, org_id, suite_id, state, passed_count, failed_count, result_json, created_at, completed_at) + values (?, ?, ?, ?, ?, ?, ?, ?, ?) + `).run(id, input.orgId, input.suiteId, state, passedCount, failedCount, JSON.stringify({ checks }), now, now); + this.audit.record({ + orgId: input.orgId, + entityType: "eval_run", + entityId: id, + actorType: "system", + actorId: null, + action: "created", + payload: { suiteKey: suite.key, state, passedCount, failedCount }, + }); + return id; + } + + setPrBotGate(input: SetPrBotGateInput): void { + const gate = this.db.prepare("select key from pr_bot_gates where id = ? and org_id = ?") + .get(input.gateId, input.orgId) as { key: string } | undefined; + if (!gate) throw new Error("PR bot gate was not found."); + const now = new Date().toISOString(); + const result = this.db.prepare("update pr_bot_gates set enabled = ?, updated_at = ? where id = ? and org_id = ?") + .run(input.enabled ? 1 : 0, now, input.gateId, input.orgId); + if (result.changes === 0) throw new Error("PR bot gate was not found."); + this.audit.record({ + orgId: input.orgId, + entityType: "pr_bot_gate", + entityId: input.gateId, + actorType: "system", + actorId: null, + action: "updated", + payload: { key: gate.key, enabled: input.enabled }, + }); + } + + private requireSuiteByKey(orgId: string, key: EvalSuiteKey): { id: string } { + const suite = this.db.prepare("select id from eval_suites where org_id = ? and key = ?") + .get(orgId, key) as { id: string } | undefined; + if (!suite) throw new Error("Eval suite is required."); + return suite; + } + + private assertTarget(orgId: string, type: SdlcReviewTargetType, ref: string): void { + if (!Object.values(SDLC_REVIEW_TARGET_TYPE).includes(type)) throw new Error("SDLC review target type is invalid."); + const targetRef = ref.trim(); + if (!targetRef) throw new Error("SDLC review target is required."); + if (type === SDLC_REVIEW_TARGET_TYPE.WORKSPACE) { + const org = this.db.prepare("select id from organizations where id = ?").get(targetRef); + if (!org || targetRef !== orgId) throw new Error("SDLC workspace target is invalid."); + return; + } + if (type === SDLC_REVIEW_TARGET_TYPE.MIGRATION) { + const report = this.db.prepare("select id from migration_reports where id = ? and org_id = ?").get(targetRef, orgId); + if (!report) throw new Error("SDLC migration target was not found."); + return; + } + const policy = this.db.prepare("select id from policy_rules where id = ? and org_id = ?").get(targetRef, orgId); + if (!policy) throw new Error("SDLC policy target was not found."); + } + + private reviewChecks(orgId: string, agentKey: SdlcReviewAgentKey, targetType: SdlcReviewTargetType): GateCheck[] { + if (agentKey === SDLC_REVIEW_AGENT_KEY.MIGRATION_REVIEWER) return this.migrationReviewChecks(orgId); + if (agentKey === SDLC_REVIEW_AGENT_KEY.POLICY_REVIEWER) return this.policyReviewChecks(orgId); + return this.codeReviewChecks(orgId, targetType); + } + + private codeReviewChecks(orgId: string, targetType: SdlcReviewTargetType): GateCheck[] { + const enabledGates = count(this.db, "select count(*) as count from pr_bot_gates where org_id = ? and enabled = 1", orgId); + const auditEvents = count(this.db, "select count(*) as count from audit_events where org_id = ?", orgId); + return [ + { key: "target_type", passed: targetType === SDLC_REVIEW_TARGET_TYPE.WORKSPACE, value: targetType }, + { key: "enabled_pr_gates", passed: enabledGates >= 3, value: enabledGates }, + { key: "recent_audit_events", passed: auditEvents > 0, value: auditEvents }, + ]; + } + + private migrationReviewChecks(orgId: string): GateCheck[] { + const reports = this.db.prepare("select report_json from migration_reports where org_id = ?").all(orgId) as Array<{ report_json: string }>; + const rollbackPacks = reports.filter((report) => hasObjectField(report.report_json, "rollbackPack")).length; + const migrationGateEnabled = this.isGateEnabled(orgId, PR_BOT_GATE_KEY.MIGRATION_DIFF); + return [ + { key: "migration_gate_enabled", passed: migrationGateEnabled, value: migrationGateEnabled }, + { key: "rollback_pack_coverage", passed: reports.length === rollbackPacks, value: `${rollbackPacks}/${reports.length}` }, + ]; + } + + private policyReviewChecks(orgId: string): GateCheck[] { + const providerConflicts = count(this.db, "select count(*) as count from providers where org_id = ? and data_policy = 'zero_data_retention' and background_enabled = 1", orgId); + const routeRules = count(this.db, "select count(*) as count from approval_routing_rules where org_id = ?", orgId); + const policyGateEnabled = this.isGateEnabled(orgId, PR_BOT_GATE_KEY.POLICY_DIFF); + return [ + { key: "policy_gate_enabled", passed: policyGateEnabled, value: policyGateEnabled }, + { key: "provider_policy_conflicts", passed: providerConflicts === 0, value: providerConflicts }, + { key: "approval_routes", passed: routeRules > 0, value: routeRules }, + ]; + } + + private evalChecks(orgId: string, suiteKey: EvalSuiteKey): GateCheck[] { + if (suiteKey === EVAL_SUITE_KEY.CONTRACT) return this.contractEvalChecks(); + if (suiteKey === EVAL_SUITE_KEY.INTEGRATION) return this.integrationEvalChecks(orgId); + return this.replayEvalChecks(orgId); + } + + private contractEvalChecks(): GateCheck[] { + const catalogKeys = PROVIDER_CATALOG.map((provider) => provider.key); + const adapterKeys = Object.keys(providerAdapters); + const missing = catalogKeys.filter((key) => !adapterKeys.includes(key)); + const schemaVersion = count(this.db, "select coalesce(max(version), 0) as count from schema_migrations"); + return [ + { key: "provider_adapter_coverage", passed: missing.length === 0, value: `${adapterKeys.length}/${catalogKeys.length}` }, + { key: "schema_version", passed: schemaVersion >= 18, value: schemaVersion }, + ]; + } + + private integrationEvalChecks(orgId: string): GateCheck[] { + const chatSpaces = count(this.db, "select count(*) as count from chat_spaces where org_id = ?", orgId); + const approvalRoutes = count(this.db, "select count(*) as count from approval_routing_rules where org_id = ?", orgId); + const skillSnapshots = count(this.db, ` + select count(*) as count + from skill_runtime_snapshots + join agents on agents.id = skill_runtime_snapshots.agent_id + where agents.org_id = ? + `, orgId); + return [ + { key: "chat_spaces", passed: chatSpaces > 0, value: chatSpaces }, + { key: "approval_routes", passed: approvalRoutes > 0, value: approvalRoutes }, + { key: "skill_runtime_snapshots", passed: skillSnapshots > 0, value: skillSnapshots }, + ]; + } + + private replayEvalChecks(orgId: string): GateCheck[] { + const auditEvents = count(this.db, "select count(*) as count from audit_events where org_id = ?", orgId); + const operationalEvents = count(this.db, "select count(*) as count from operational_events where org_id = ?", orgId); + return [ + { key: "audit_events", passed: auditEvents > 0, value: auditEvents }, + { key: "operational_events", passed: operationalEvents > 0, value: operationalEvents }, + ]; + } + + private isGateEnabled(orgId: string, key: string): boolean { + const row = this.db.prepare("select enabled from pr_bot_gates where org_id = ? and key = ?") + .get(orgId, key) as { enabled: number } | undefined; + if (!row) throw new Error("PR bot gate is required."); + return row.enabled === 1; + } +} + +function count(db: DatabaseSync, sql: string, ...params: string[]): number { + return (db.prepare(sql).get(...params) as { count: number }).count; +} + +function hasObjectField(json: string, field: string): boolean { + const parsed = JSON.parse(json) as Record; + return typeof parsed[field] === "object" && parsed[field] !== null; +} diff --git a/AgentCompany-New/src/main/services/snapshot-service.ts b/AgentCompany-New/src/main/services/snapshot-service.ts index 204ed586..73e6a3c3 100644 --- a/AgentCompany-New/src/main/services/snapshot-service.ts +++ b/AgentCompany-New/src/main/services/snapshot-service.ts @@ -59,6 +59,10 @@ export class SnapshotService { .map((event) => ({ ...event, success: toBool(event.success) })); const chaosDrills = list(this.db, "chaos_drills", "key asc") .map((drill) => ({ ...drill, enabled: toBool(drill.enabled) })); + const sdlcReviewAgents = list(this.db, "sdlc_review_agents", "key asc") + .map((agent) => ({ ...agent, enabled: toBool(agent.enabled) })); + const prBotGates = list(this.db, "pr_bot_gates", "key asc") + .map((gate) => ({ ...gate, enabled: toBool(gate.enabled) })); return { activeOrgId, @@ -111,6 +115,12 @@ export class SnapshotService { operationalEvents, chaosDrills, chaosDrillRuns: list(this.db, "chaos_drill_runs", "started_at desc"), + sdlcReviewAgents, + sdlcReviewRuns: list(this.db, "sdlc_review_runs", "created_at desc"), + evalSuites: list(this.db, "eval_suites", "key asc"), + evalCases: list(this.db, "eval_cases", "title asc"), + evalRuns: list(this.db, "eval_runs", "created_at desc"), + prBotGates, auditEvents: list(this.db, "audit_events", "created_at desc").slice(0, 200), }; } diff --git a/AgentCompany-New/src/preload/index.ts b/AgentCompany-New/src/preload/index.ts index c8b1655b..8534b9e2 100644 --- a/AgentCompany-New/src/preload/index.ts +++ b/AgentCompany-New/src/preload/index.ts @@ -40,6 +40,9 @@ const api: DesktopApi = { createMigrationReport: (input) => invoke(IPC_CHANNELS.createMigrationReport, input), setFeatureFlag: (input) => invoke(IPC_CHANNELS.setFeatureFlag, input), runChaosDrill: (input) => invoke(IPC_CHANNELS.runChaosDrill, input), + runSdlcReview: (input) => invoke(IPC_CHANNELS.runSdlcReview, input), + runEvalSuite: (input) => invoke(IPC_CHANNELS.runEvalSuite, input), + setPrBotGate: (input) => invoke(IPC_CHANNELS.setPrBotGate, input), createComplianceExport: (input) => invoke(IPC_CHANNELS.createComplianceExport, input), createLegalHold: (input) => invoke(IPC_CHANNELS.createLegalHold, input), setLegalHoldState: (input) => invoke(IPC_CHANNELS.setLegalHoldState, input), diff --git a/AgentCompany-New/src/renderer/src/pages/SettingsPage.tsx b/AgentCompany-New/src/renderer/src/pages/SettingsPage.tsx index 7f534333..0b51a8d9 100644 --- a/AgentCompany-New/src/renderer/src/pages/SettingsPage.tsx +++ b/AgentCompany-New/src/renderer/src/pages/SettingsPage.tsx @@ -7,6 +7,7 @@ import { BillingPanel } from "./settings/BillingPanel"; import { ModelsPanel } from "./settings/ModelsPanel"; import { PoliciesPanel } from "./settings/PoliciesPanel"; import { OperationsPanel } from "./settings/OperationsPanel"; +import { SdlcPanel } from "./settings/SdlcPanel"; import { MigrationPanel } from "./settings/MigrationPanel"; import { CompliancePanel } from "./settings/CompliancePanel"; import { NotificationsPanel, ProfilePanel, SecurityPanel } from "./settings/AccountPanels"; @@ -22,6 +23,7 @@ type SettingsTab = | "skills" | "policies" | "operations" + | "sdlc" | "compliance" | "migration" | "profile" @@ -44,6 +46,7 @@ const AGENTS_GROUP: NavSpec[] = [ { id: "skills", label: "Skills" }, { id: "policies", label: "Policies" }, { id: "operations", label: "Operations" }, + { id: "sdlc", label: "SDLC" }, { id: "compliance", label: "Compliance" }, { id: "migration", label: "Migration" }, ]; @@ -91,6 +94,12 @@ export function SettingsPage({ snapshot, actions }: PageProps) { const operationalEvents = useMemo(() => snapshot.operationalEvents.filter((item) => item.orgId === orgId), [snapshot.operationalEvents, orgId]); const chaosDrills = useMemo(() => snapshot.chaosDrills.filter((item) => item.orgId === orgId), [snapshot.chaosDrills, orgId]); const chaosDrillRuns = useMemo(() => snapshot.chaosDrillRuns.filter((item) => item.orgId === orgId), [snapshot.chaosDrillRuns, orgId]); + const sdlcReviewAgents = useMemo(() => snapshot.sdlcReviewAgents.filter((item) => item.orgId === orgId), [snapshot.sdlcReviewAgents, orgId]); + const sdlcReviewRuns = useMemo(() => snapshot.sdlcReviewRuns.filter((item) => item.orgId === orgId), [snapshot.sdlcReviewRuns, orgId]); + const evalSuites = useMemo(() => snapshot.evalSuites.filter((item) => item.orgId === orgId), [snapshot.evalSuites, orgId]); + const evalCases = useMemo(() => snapshot.evalCases.filter((item) => item.orgId === orgId), [snapshot.evalCases, orgId]); + const evalRuns = useMemo(() => snapshot.evalRuns.filter((item) => item.orgId === orgId), [snapshot.evalRuns, orgId]); + const prBotGates = useMemo(() => snapshot.prBotGates.filter((item) => item.orgId === orgId), [snapshot.prBotGates, orgId]); return (
@@ -158,6 +167,20 @@ export function SettingsPage({ snapshot, actions }: PageProps) { actions={actions} /> )} + {tab === "sdlc" && ( + + )} {tab === "compliance" && ( +

SDLC

+

{reviewAgents.length} agents / {evalSuites.length} suites / {prBotGates.length} gates.

+ + + +
+ ); +} + +function ReviewAgentsTable({ + orgId, + agents, + runs, + migrationReports, + policies, + actions, +}: { + orgId: string; + agents: SdlcReviewAgentRecord[]; + runs: SdlcReviewRunRecord[]; + migrationReports: MigrationReportRecord[]; + policies: PolicyRuleRecord[]; + actions: SnapshotActions; +}) { + return ( + <> +
Review agents
+
+
+
Agent
Target
Last run
Action
+
+ {agents.map((agent, index) => { + const target = resolveReviewTarget(agent, orgId, migrationReports, policies); + const run = runs.find((item) => item.reviewAgentId === agent.id) ?? null; + return ( +
+
+
{agent.label}
+
{agent.specialty}
+
+
{target.label}
+
{run ? `${run.state} · ${formatRelative(run.createdAt)}` : "0 runs"}
+ +
+ ); + })} +
+ + ); +} + +function EvalSuitesTable({ + orgId, + suites, + cases, + runs, + actions, +}: { + orgId: string; + suites: EvalSuiteRecord[]; + cases: EvalCaseRecord[]; + runs: EvalRunRecord[]; + actions: SnapshotActions; +}) { + return ( + <> +
Eval suites
+
+
+
Suite
Cases
Last run
Action
+
+ {suites.map((suite, index) => { + const run = runs.find((item) => item.suiteId === suite.id) ?? null; + const caseCount = cases.filter((item) => item.suiteId === suite.id).length; + return ( +
+
+
{suite.label}
+
{suite.scope}
+
+
{caseCount}
+
{run ? `${run.state} · ${formatRelative(run.createdAt)}` : "0 runs"}
+ +
+ ); + })} +
+ + ); +} + +function PrBotGatesTable({ + orgId, + gates, + actions, +}: { + orgId: string; + gates: PrBotGateRecord[]; + actions: SnapshotActions; +}) { + return ( + <> +
PR bot gates
+
+
+
Gate
State
Updated
+
+ {gates.map((gate, index) => ( +
+
+
{gate.label}
+
{gate.gateType}
+
+ +
{formatRelative(gate.updatedAt)}
+
+ ))} +
+ + ); +} + +function resolveReviewTarget( + agent: SdlcReviewAgentRecord, + orgId: string, + migrationReports: MigrationReportRecord[], + policies: PolicyRuleRecord[], +): { type: SdlcReviewTargetType; ref: string; label: string; ready: boolean } { + if (agent.key === SDLC_REVIEW_AGENT_KEY.MIGRATION_REVIEWER) { + const report = migrationReports[0] ?? null; + return { + type: SDLC_REVIEW_TARGET_TYPE.MIGRATION, + ref: report?.id ?? orgId, + label: report ? "migration" : "migration report", + ready: Boolean(report), + }; + } + if (agent.key === SDLC_REVIEW_AGENT_KEY.POLICY_REVIEWER) { + const policy = policies[0] ?? null; + return { + type: SDLC_REVIEW_TARGET_TYPE.POLICY, + ref: policy?.id ?? orgId, + label: policy ? "policy" : "policy rule", + ready: Boolean(policy), + }; + } + return { type: SDLC_REVIEW_TARGET_TYPE.WORKSPACE, ref: orgId, label: "workspace", ready: true }; +} diff --git a/AgentCompany-New/src/shared/commands.ts b/AgentCompany-New/src/shared/commands.ts index 8e7a421b..3b938260 100644 --- a/AgentCompany-New/src/shared/commands.ts +++ b/AgentCompany-New/src/shared/commands.ts @@ -11,6 +11,7 @@ import type { RiskLevel, RoleKey, SearchEntityType, + SdlcReviewTargetType, SocialPlatform, TaskPriority, TaskStatus, @@ -288,6 +289,24 @@ export interface RunChaosDrillInput { drillId: string; } +export interface RunSdlcReviewInput { + orgId: string; + reviewAgentId: string; + targetType: SdlcReviewTargetType; + targetRef: string; +} + +export interface RunEvalSuiteInput { + orgId: string; + suiteId: string; +} + +export interface SetPrBotGateInput { + orgId: string; + gateId: string; + enabled: boolean; +} + export interface BootstrapOnboardingInput { companyName: string; companyDescription: string; diff --git a/AgentCompany-New/src/shared/desktop-api.ts b/AgentCompany-New/src/shared/desktop-api.ts index 63c6b1c8..959ccc1f 100644 --- a/AgentCompany-New/src/shared/desktop-api.ts +++ b/AgentCompany-New/src/shared/desktop-api.ts @@ -27,6 +27,8 @@ import type { LaunchSocialLoginInput, ResolveAgentSkillsInput, RunChaosDrillInput, + RunEvalSuiteInput, + RunSdlcReviewInput, SaveProviderCredentialInput, SearchInput, SendMessageInput, @@ -34,6 +36,7 @@ import type { SetFilePinnedInput, SetFeatureFlagInput, SetLegalHoldStateInput, + SetPrBotGateInput, SetRetentionFreezeStateInput, SetScheduledPostStateInput, SyncEmployeeIdentityInput, @@ -79,6 +82,9 @@ export interface DesktopApi { createMigrationReport(input: CreateMigrationReportInput): Promise>; setFeatureFlag(input: SetFeatureFlagInput): Promise>; runChaosDrill(input: RunChaosDrillInput): Promise>; + runSdlcReview(input: RunSdlcReviewInput): Promise>; + runEvalSuite(input: RunEvalSuiteInput): Promise>; + setPrBotGate(input: SetPrBotGateInput): Promise>; createComplianceExport(input: CreateComplianceExportInput): Promise>; createLegalHold(input: CreateLegalHoldInput): Promise>; setLegalHoldState(input: SetLegalHoldStateInput): Promise>; diff --git a/AgentCompany-New/src/shared/domain.ts b/AgentCompany-New/src/shared/domain.ts index 2366337c..71a39ca2 100644 --- a/AgentCompany-New/src/shared/domain.ts +++ b/AgentCompany-New/src/shared/domain.ts @@ -4,12 +4,14 @@ import type { AccessDecisionRecord, EmployeeIdentityRecord, RoleGrantRecord, Ser import type { ApprovalAssignmentRecord, ApprovalRoutingRuleRecord, HiringPolicyRecord, OrgDepartmentRecord } from "./org-governance-domain"; import type { ChaosDrillRecord, ChaosDrillRunRecord, FeatureFlagRecord, OperationalEventRecord, SloTargetRecord } from "./operations-domain"; import type { ProviderDataPolicy, ProviderExecutionMode } from "./provider-policy-domain"; +import type { EvalCaseRecord, EvalRunRecord, EvalSuiteRecord, PrBotGateRecord, SdlcReviewAgentRecord, SdlcReviewRunRecord } from "./sdlc-domain"; export { BOARD_INTERVENTION_ACTION, BOARD_INTERVENTION_MODE, CHAT_THREAD_BOARD_STATE, type BoardInterventionAction, type BoardInterventionMode, type BoardInterventionRecord, type ChatThreadBoardState } from "./board-domain"; export { COMPLIANCE_EXPORT_STATE, COMPLIANCE_EXPORT_TYPE, type ComplianceExportRecord, type ComplianceExportState, type ComplianceExportType, type ComplianceSubjectType, type LegalHoldEntityType, type LegalHoldRecord, type RetentionFreezeRecord, type RetentionFreezeScopeType } from "./compliance-domain"; export { ACCESS_DECISION, IDENTITY_SOURCE, ROLE_KEY, type AccessDecision, type AccessDecisionRecord, type EmployeeIdentityRecord, type IdentitySource, type PrincipalType, type RoleGrantRecord, type RoleKey, type ServiceIdentityRecord } from "./identity-domain"; export { ROUTE_TO_TYPE, type ApprovalAssignmentRecord, type ApprovalRoutingRuleRecord, type HiringPolicyRecord, type OrgDepartmentRecord, type RouteToType } from "./org-governance-domain"; export { CHAOS_DRILL_SCENARIO, CHAOS_DRILL_STATE, FEATURE_FLAG_KEY, SLO_SIGNAL_KEY, type ChaosDrillRecord, type ChaosDrillRunRecord, type ChaosDrillScenario, type ChaosDrillState, type FeatureFlagKey, type FeatureFlagRecord, type OperationalEventRecord, type SloSignalKey, type SloTargetRecord } from "./operations-domain"; export { PROVIDER_DATA_POLICY, PROVIDER_EXECUTION_MODE, type ProviderDataPolicy, type ProviderExecutionMode } from "./provider-policy-domain"; +export { EVAL_SUITE_KEY, PR_BOT_GATE_KEY, SDLC_REVIEW_AGENT_KEY, SDLC_REVIEW_TARGET_TYPE, SDLC_RUN_STATE, type EvalCaseRecord, type EvalRunRecord, type EvalSuiteKey, type EvalSuiteRecord, type PrBotGateKey, type PrBotGateRecord, type SdlcReviewAgentKey, type SdlcReviewAgentRecord, type SdlcReviewRunRecord, type SdlcReviewTargetType, type SdlcRunState } from "./sdlc-domain"; export { SEARCH_ENTITY_TYPES, type SearchEntityType, type SearchResultRecord } from "./search-domain"; export const TASK_STATUS = { BACKLOG: "backlog", IN_PROGRESS: "in_progress", REVIEW: "review", DONE: "done" } as const; export const APPROVAL_STATE = { PENDING: "pending", APPROVED: "approved", DECLINED: "declined" } as const; @@ -474,5 +476,6 @@ export interface AppSnapshot { employeeIdentities: EmployeeIdentityRecord[]; serviceIdentities: ServiceIdentityRecord[]; roleGrants: RoleGrantRecord[]; accessDecisions: AccessDecisionRecord[]; orgDepartments: OrgDepartmentRecord[]; hiringPolicies: HiringPolicyRecord[]; approvalRoutingRules: ApprovalRoutingRuleRecord[]; approvalAssignments: ApprovalAssignmentRecord[]; featureFlags: FeatureFlagRecord[]; sloTargets: SloTargetRecord[]; operationalEvents: OperationalEventRecord[]; chaosDrills: ChaosDrillRecord[]; chaosDrillRuns: ChaosDrillRunRecord[]; + sdlcReviewAgents: SdlcReviewAgentRecord[]; sdlcReviewRuns: SdlcReviewRunRecord[]; evalSuites: EvalSuiteRecord[]; evalCases: EvalCaseRecord[]; evalRuns: EvalRunRecord[]; prBotGates: PrBotGateRecord[]; auditEvents: AuditEventRecord[]; } diff --git a/AgentCompany-New/src/shared/ipc.ts b/AgentCompany-New/src/shared/ipc.ts index d51d3544..a2eb4f44 100644 --- a/AgentCompany-New/src/shared/ipc.ts +++ b/AgentCompany-New/src/shared/ipc.ts @@ -32,6 +32,9 @@ export const IPC_CHANNELS = { createMigrationReport: "agentcompany:create-migration-report", setFeatureFlag: "agentcompany:set-feature-flag", runChaosDrill: "agentcompany:run-chaos-drill", + runSdlcReview: "agentcompany:run-sdlc-review", + runEvalSuite: "agentcompany:run-eval-suite", + setPrBotGate: "agentcompany:set-pr-bot-gate", createComplianceExport: "agentcompany:create-compliance-export", createLegalHold: "agentcompany:create-legal-hold", setLegalHoldState: "agentcompany:set-legal-hold-state", diff --git a/AgentCompany-New/src/shared/sdlc-domain.ts b/AgentCompany-New/src/shared/sdlc-domain.ts new file mode 100644 index 00000000..65407536 --- /dev/null +++ b/AgentCompany-New/src/shared/sdlc-domain.ts @@ -0,0 +1,48 @@ +type ValueOf = T[keyof T]; + +export const SDLC_REVIEW_AGENT_KEY = { + CODE_REVIEWER: "code_reviewer", + MIGRATION_REVIEWER: "migration_reviewer", + POLICY_REVIEWER: "policy_reviewer", +} as const; + +export const SDLC_REVIEW_TARGET_TYPE = { WORKSPACE: "workspace", MIGRATION: "migration", POLICY: "policy" } as const; +export const EVAL_SUITE_KEY = { CONTRACT: "contract", INTEGRATION: "integration", REPLAY: "replay" } as const; +export const SDLC_RUN_STATE = { PASSED: "passed", FAILED: "failed" } as const; +export const PR_BOT_GATE_KEY = { + STATIC_CHECKS: "static_checks", + MIGRATION_DIFF: "migration_diff", + POLICY_DIFF: "policy_diff", + HUMAN_APPROVAL: "human_approval", +} as const; + +export type SdlcReviewAgentKey = ValueOf; +export type SdlcReviewTargetType = ValueOf; +export type EvalSuiteKey = ValueOf; +export type SdlcRunState = ValueOf; +export type PrBotGateKey = ValueOf; + +export interface SdlcReviewAgentRecord { + id: string; orgId: string; key: SdlcReviewAgentKey; label: string; specialty: string; enabled: boolean; createdAt: string; updatedAt: string; +} + +export interface SdlcReviewRunRecord { + id: string; orgId: string; reviewAgentId: string; targetType: SdlcReviewTargetType; targetRef: string; + state: SdlcRunState; resultJson: string; createdAt: string; completedAt: string; +} + +export interface EvalSuiteRecord { + id: string; orgId: string; key: EvalSuiteKey; label: string; scope: string; createdAt: string; updatedAt: string; +} + +export interface EvalCaseRecord { + id: string; orgId: string; suiteId: string; title: string; sourceType: string; sourceRef: string; expectedSignal: string; createdAt: string; +} + +export interface EvalRunRecord { + id: string; orgId: string; suiteId: string; state: SdlcRunState; passedCount: number; failedCount: number; resultJson: string; createdAt: string; completedAt: string; +} + +export interface PrBotGateRecord { + id: string; orgId: string; key: PrBotGateKey; label: string; gateType: string; enabled: boolean; createdAt: string; updatedAt: string; +} diff --git a/AgentCompany-New/tests/e2e/desktop.spec.ts b/AgentCompany-New/tests/e2e/desktop.spec.ts index ca2f32b5..311cf82e 100644 --- a/AgentCompany-New/tests/e2e/desktop.spec.ts +++ b/AgentCompany-New/tests/e2e/desktop.spec.ts @@ -53,6 +53,14 @@ test("onboards, hires, and creates work in the Electron shell", async () => { await expect(page.getByText("Chaos drills").first()).toBeVisible(); await page.getByRole("button", { name: "Run" }).first().click(); await expect(page.getByText("passed").first()).toBeVisible(); + await page.getByRole("button", { name: "SDLC", exact: true }).click(); + await expect(page.getByText("Review agents", { exact: true })).toBeVisible(); + await expect(page.getByText("Eval suites", { exact: true })).toBeVisible(); + await expect(page.getByText("PR bot gates", { exact: true })).toBeVisible(); + await page.getByRole("button", { name: "Run" }).first().click(); + await expect(page.getByText("passed").first()).toBeVisible(); + await page.getByRole("button", { name: "Run" }).nth(3).click(); + await expect(page.getByText(/passed ·/).first()).toBeVisible(); await page.getByRole("button", { name: "Work", exact: true }).click(); await page.getByRole("button", { name: "New task" }).click(); @@ -134,7 +142,7 @@ async function verifyPagesDoNotOverflow(page: Page): Promise { await page.getByRole("button", { name: nav, exact: true }).click(); await expectPageToFit(page); } - for (const tab of ["Workspace", "Members", "Billing", "Models", "Providers", "Skills", "Policies", "Operations", "Compliance", "Migration", "Profile", "Notifications", "Security"]) { + for (const tab of ["Workspace", "Members", "Billing", "Models", "Providers", "Skills", "Policies", "Operations", "SDLC", "Compliance", "Migration", "Profile", "Notifications", "Security"]) { await page.getByRole("button", { name: "Settings" }).click(); await page.getByRole("button", { name: tab, exact: true }).click(); await expectPageToFit(page); diff --git a/AgentCompany-New/tests/unit/domain-services.test.ts b/AgentCompany-New/tests/unit/domain-services.test.ts index e0880584..483f5014 100644 --- a/AgentCompany-New/tests/unit/domain-services.test.ts +++ b/AgentCompany-New/tests/unit/domain-services.test.ts @@ -23,7 +23,7 @@ describe("domain services", () => { const connection = createConnection(); const rows = connection.db.prepare("select version from schema_migrations order by version asc").all() as Array<{ version: number }>; - expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]); + expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18]); connection.close(); }); @@ -51,6 +51,9 @@ describe("domain services", () => { expect(state.featureFlags.map((flag) => flag.key)).toContain("chaos_drills"); expect(state.sloTargets.map((target) => target.signalKey)).toContain("provider_request_success"); expect(state.chaosDrills.map((drill) => drill.scenarioKey)).toContain("approval_backlog_gate"); + expect(state.sdlcReviewAgents.map((agent) => agent.key)).toContain("code_reviewer"); + expect(state.evalSuites.map((suite) => suite.key)).toContain("contract"); + expect(state.prBotGates.map((gate) => gate.key)).toContain("static_checks"); expect(state.chatSpaces.map((space) => space.name)).toContain("board"); expect(state.policyRules[0]?.action).toBe("task_create"); expect(state.auditEvents[0]?.action).toBe("created"); diff --git a/AgentCompany-New/tests/unit/migration-service.test.ts b/AgentCompany-New/tests/unit/migration-service.test.ts index ccc72cec..f1e2b9c6 100644 --- a/AgentCompany-New/tests/unit/migration-service.test.ts +++ b/AgentCompany-New/tests/unit/migration-service.test.ts @@ -113,6 +113,12 @@ describe("migration service", () => { create table operational_events(id text primary key, org_id text); create table chaos_drills(id text primary key, org_id text); create table chaos_drill_runs(id text primary key, org_id text, drill_id text); + create table sdlc_review_agents(id text primary key, org_id text); + create table sdlc_review_runs(id text primary key, org_id text, review_agent_id text); + create table eval_suites(id text primary key, org_id text); + create table eval_cases(id text primary key, org_id text, suite_id text); + create table eval_runs(id text primary key, org_id text, suite_id text); + create table pr_bot_gates(id text primary key, org_id text); create table chat_spaces(id text primary key, org_id text); create table chat_messages(id text primary key, org_id text, space_id text, thread_id text); create table chat_threads(id text primary key, org_id text, space_id text, root_message_id text); @@ -173,6 +179,12 @@ describe("migration service", () => { insert into operational_events(id, org_id) values ('op-1', 'org-1'); insert into chaos_drills(id, org_id) values ('drill-1', 'org-1'); insert into chaos_drill_runs(id, org_id, drill_id) values ('drill-run-1', 'org-1', 'drill-1'); + insert into sdlc_review_agents(id, org_id) values ('reviewer-1', 'org-1'); + insert into sdlc_review_runs(id, org_id, review_agent_id) values ('review-run-1', 'org-1', 'reviewer-1'); + insert into eval_suites(id, org_id) values ('suite-1', 'org-1'); + insert into eval_cases(id, org_id, suite_id) values ('case-1', 'org-1', 'suite-1'); + insert into eval_runs(id, org_id, suite_id) values ('eval-run-1', 'org-1', 'suite-1'); + insert into pr_bot_gates(id, org_id) values ('gate-1', 'org-1'); insert into chat_threads(id, org_id, space_id, root_message_id) values ('thread-1', 'org-1', 'space-1', null); insert into chat_messages(id, org_id, space_id, thread_id) values ('msg-1', 'org-1', 'space-1', 'thread-1'); insert into chat_memberships(space_id, principal_type, principal_id) values ('space-1', 'employee', 'emp-1'); @@ -219,6 +231,8 @@ describe("migration service", () => { expect(body.tableCounts.hiring_policies).toBe(1); expect(body.tableCounts.feature_flags).toBe(1); expect(body.tableCounts.chaos_drill_runs).toBe(1); + expect(body.tableCounts.sdlc_review_runs).toBe(1); + expect(body.tableCounts.eval_cases).toBe(1); expect(body.tableCounts.role_grants).toBe(1); expect(body.tableCounts.skill_artifacts).toBe(1); expect(body.tableCounts.chat_read_state).toBe(1); @@ -235,6 +249,8 @@ describe("migration service", () => { expect(body.idMaps.approval_assignments).toEqual(["assign-1"]); expect(body.idMaps.operational_events).toEqual(["op-1"]); expect(body.idMaps.chaos_drill_runs).toEqual(["drill-run-1"]); + expect(body.idMaps.sdlc_review_runs).toEqual(["review-run-1"]); + expect(body.idMaps.eval_runs).toEqual(["eval-run-1"]); expect(body.idMaps.board_interventions).toEqual(["bdi-1"]); expect(body.idMaps.retention_freezes).toEqual(["freeze-1"]); expect(body.idMaps.provider_usage_windows).toEqual(["usage-1"]); diff --git a/AgentCompany-New/tests/unit/sdlc-service.test.ts b/AgentCompany-New/tests/unit/sdlc-service.test.ts new file mode 100644 index 00000000..723dcbf5 --- /dev/null +++ b/AgentCompany-New/tests/unit/sdlc-service.test.ts @@ -0,0 +1,88 @@ +import { randomUUID } from "node:crypto"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { describe, expect, test } from "vitest"; +import { DatabaseConnection } from "../../src/main/database/connection"; +import { OrganizationService } from "../../src/main/services/organization-service"; +import { SdlcService } from "../../src/main/services/sdlc-service"; +import { SnapshotService } from "../../src/main/services/snapshot-service"; +import { SDLC_REVIEW_TARGET_TYPE } from "../../src/shared/domain"; + +function createConnection(): DatabaseConnection { + return new DatabaseConnection(join(tmpdir(), `agentcompany-new-sdlc-${randomUUID()}.db`)); +} + +function createOrganization(organization: OrganizationService): string { + return organization.createOrganization({ + organizationName: "Acme Operations", + employeeName: "Dana Lee", + mission: "Coordinate agents with auditable approvals.", + goal: "Ship a verified control plane.", + }); +} + +describe("SDLC service", () => { + test("seeds review agents, eval suites, eval cases, and PR bot gates", () => { + const connection = createConnection(); + const organization = new OrganizationService(connection.db); + const snapshot = new SnapshotService(connection.db); + + connection.transaction(() => { + createOrganization(organization); + }); + + const state = snapshot.load(); + expect(state.sdlcReviewAgents.map((agent) => agent.key).sort()).toEqual(["code_reviewer", "migration_reviewer", "policy_reviewer"]); + expect(state.evalSuites.map((suite) => suite.key).sort()).toEqual(["contract", "integration", "replay"]); + expect(state.evalCases).toHaveLength(6); + expect(state.prBotGates.map((gate) => gate.key).sort()).toEqual(["human_approval", "migration_diff", "policy_diff", "static_checks"]); + connection.close(); + }); + + test("runs review agents against validated workspace targets", () => { + const connection = createConnection(); + const organization = new OrganizationService(connection.db); + const sdlc = new SdlcService(connection.db); + const snapshot = new SnapshotService(connection.db); + let orgId = ""; + let reviewAgentId = ""; + + connection.transaction(() => { + orgId = createOrganization(organization); + reviewAgentId = (connection.db.prepare("select id from sdlc_review_agents where org_id = ? and key = ?") + .get(orgId, "code_reviewer") as { id: string }).id; + sdlc.runReview({ orgId, reviewAgentId, targetType: SDLC_REVIEW_TARGET_TYPE.WORKSPACE, targetRef: orgId }); + }); + + const state = snapshot.load(); + expect(state.sdlcReviewRuns[0]).toMatchObject({ orgId, reviewAgentId, targetType: SDLC_REVIEW_TARGET_TYPE.WORKSPACE, state: "passed" }); + expect(state.auditEvents.some((event) => event.entityType === "sdlc_review_run" && event.action === "created")).toBe(true); + connection.close(); + }); + + test("runs contract eval suites and toggles PR bot gates", () => { + const connection = createConnection(); + const organization = new OrganizationService(connection.db); + const sdlc = new SdlcService(connection.db); + const snapshot = new SnapshotService(connection.db); + let orgId = ""; + let suiteId = ""; + let gateId = ""; + + connection.transaction(() => { + orgId = createOrganization(organization); + suiteId = (connection.db.prepare("select id from eval_suites where org_id = ? and key = ?") + .get(orgId, "contract") as { id: string }).id; + gateId = (connection.db.prepare("select id from pr_bot_gates where org_id = ? and key = ?") + .get(orgId, "static_checks") as { id: string }).id; + sdlc.runEvalSuite({ orgId, suiteId }); + sdlc.setPrBotGate({ orgId, gateId, enabled: false }); + }); + + const state = snapshot.load(); + expect(state.evalRuns[0]).toMatchObject({ orgId, suiteId, state: "passed", failedCount: 0 }); + expect(state.prBotGates.find((gate) => gate.id === gateId)?.enabled).toBe(false); + expect(state.auditEvents.some((event) => event.entityType === "pr_bot_gate" && event.action === "updated")).toBe(true); + connection.close(); + }); +});