feat(P01-P05): multi-session support & execute-phase persona specialization — SESSION-01..05, PERSONA-01..11, CLI-01..04, INTEG-01..05
---ci---
phase: 1-5
milestone: v0.11
project: ci
status: execute
decisions:
- id: D-092
decision: Independent sessions via AgentSession (not shared state)
rationale: Aligns with git-native model; sessions communicate through commits and .ciagent/ files
confidence: 0.90
- id: D-093
decision: Personas as runtime configs (not new Agent classes)
rationale: Less code, more flexible. Persona md files define domain knowledge and framework opinions.
confidence: 0.88
- id: D-094
decision: Lead developer as task decomposer (not separate pipeline stage)
rationale: EXECUTE stays one stage. Lead decomposes before execution, each persona group runs.
confidence: 0.85
- id: D-095
decision: File-based git locking (not DB or IPC)
rationale: Git-native. .session-lock files are simple JSON with session ID, timestamp, project slug.
confidence: 0.87
- id: D-096
decision: Territory enforcement with warn/strict modes
rationale: Warn for teams learning boundaries. Strict for mature projects. Configurable per-project.
confidence: 0.82
- id: D-097
decision: Task decomposition by file patterns + requirement IDs
rationale: File patterns are deterministic; no LLM needed. Requirement IDs in PLAN.md already map to domains.
confidence: 0.88
requirements:
covered: [SESSION-01, SESSION-02, SESSION-03, SESSION-04, SESSION-05, PERSONA-01, PERSONA-02, PERSONA-03, PERSONA-04, PERSONA-05, PERSONA-06, PERSONA-07, PERSONA-08, PERSONA-09, PERSONA-10, PERSONA-11, CLI-01, CLI-02, CLI-03, CLI-04, INTEG-01, INTEG-02, INTEG-03, INTEG-04, INTEG-05]
---/ci---
This commit is contained in:
@@ -55,6 +55,7 @@ export interface CIAgentMetadata {
|
||||
phase: number;
|
||||
milestone: string;
|
||||
project?: string;
|
||||
session?: string;
|
||||
plan?: string;
|
||||
task?: string;
|
||||
status: PipelineStage;
|
||||
|
||||
+33
-2
@@ -1,5 +1,4 @@
|
||||
import { BackendConfigSection } from "../backends/types.js";
|
||||
import { IdeationConfig, IdeationCategory } from "./ideation.js";
|
||||
import { TerritoryEnforcement, ExecutePersonaConfig } from "./persona.js";
|
||||
|
||||
export type AutonomyLevel = "full" | "supervised" | "guided";
|
||||
|
||||
@@ -94,8 +93,25 @@ export interface CIAgentConfig {
|
||||
backend: BackendConfigSection;
|
||||
gitea?: GiteaConfig;
|
||||
ideation?: IdeationConfig;
|
||||
sessions?: SessionConfig;
|
||||
personas?: PersonaConfigSection;
|
||||
}
|
||||
|
||||
export interface SessionConfig {
|
||||
max_concurrent_sessions: number;
|
||||
session_timeout_ms: number;
|
||||
session_isolation: "branch";
|
||||
}
|
||||
|
||||
export interface PersonaConfigSection {
|
||||
enabled: boolean;
|
||||
territory_enforcement: TerritoryEnforcement;
|
||||
personas: ExecutePersonaConfig[];
|
||||
}
|
||||
|
||||
import { BackendConfigSection } from "../backends/types.js";
|
||||
import { IdeationConfig, IdeationCategory } from "./ideation.js";
|
||||
|
||||
export const DEFAULT_CIAGENT_CONFIG: CIAgentConfig = {
|
||||
projects: [],
|
||||
active_project: "",
|
||||
@@ -190,4 +206,19 @@ export const DEFAULT_CIAGENT_CONFIG: CIAgentConfig = {
|
||||
scenarios: ["backend_unavailable", "requirement_change", "test_coverage_drop"],
|
||||
},
|
||||
},
|
||||
sessions: {
|
||||
max_concurrent_sessions: 3,
|
||||
session_timeout_ms: 3600000,
|
||||
session_isolation: "branch",
|
||||
},
|
||||
personas: {
|
||||
enabled: true,
|
||||
territory_enforcement: "warn",
|
||||
personas: [
|
||||
{ name: "lead-developer", domain: "coordination", frameworks: [], constraints: ["pragmatic", "battle-tested defaults"], territory: [] },
|
||||
{ name: "data-engineer", domain: "data", frameworks: ["drizzle", "postgresql"], constraints: ["schema-first", "type-safe ORM", "migration-driven"], territory: ["**/migrations/**", "**/schema/**", "**/models/**", "**/db/**", "prisma/schema.prisma", "drizzle/**", "**/*.sql"] },
|
||||
{ name: "backend-engineer", domain: "backend", frameworks: ["fastify", "hono"], constraints: ["api-first", "strict-typing", "dependency-injection"], territory: ["**/api/**", "**/routes/**", "**/services/**", "**/middleware/**", "**/controllers/**", "**/auth/**"] },
|
||||
{ name: "frontend-engineer", domain: "frontend", frameworks: ["react", "next.js"], constraints: ["component-first", "server-components", "minimal-client-js"], territory: ["**/components/**", "**/pages/**", "**/hooks/**", "**/styles/**", "**/*.tsx", "**/*.css", "**/*.vue"] },
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,168 @@
|
||||
export type PersonaDomain = "data" | "backend" | "frontend" | "coordination";
|
||||
|
||||
export type TerritoryEnforcement = "warn" | "strict";
|
||||
|
||||
export interface ExecutePersonaConfig {
|
||||
name: string;
|
||||
domain: PersonaDomain;
|
||||
frameworks: string[];
|
||||
constraints: string[];
|
||||
territory: string[];
|
||||
}
|
||||
|
||||
export interface DecomposedTask {
|
||||
taskId: string;
|
||||
persona: string;
|
||||
domain: PersonaDomain;
|
||||
description: string;
|
||||
files: string[];
|
||||
dependencies: string[];
|
||||
}
|
||||
|
||||
export interface DecomposedPlan {
|
||||
tasks: DecomposedTask[];
|
||||
dataTasks: DecomposedTask[];
|
||||
backendTasks: DecomposedTask[];
|
||||
frontendTasks: DecomposedTask[];
|
||||
coordinationTasks: DecomposedTask[];
|
||||
conflicts: TerritoryConflict[];
|
||||
}
|
||||
|
||||
export interface TerritoryConflict {
|
||||
type: "data-backend" | "backend-frontend" | "data-frontend";
|
||||
file: string;
|
||||
personas: string[];
|
||||
description: string;
|
||||
resolution?: string;
|
||||
}
|
||||
|
||||
export const DEFAULT_PERSONAS: ExecutePersonaConfig[] = [
|
||||
{
|
||||
name: "lead-developer",
|
||||
domain: "coordination",
|
||||
frameworks: [],
|
||||
constraints: ["pragmatic", "battle-tested defaults"],
|
||||
territory: [],
|
||||
},
|
||||
{
|
||||
name: "data-engineer",
|
||||
domain: "data",
|
||||
frameworks: ["drizzle", "postgresql"],
|
||||
constraints: ["schema-first", "type-safe ORM", "migration-driven"],
|
||||
territory: [
|
||||
"**/migrations/**",
|
||||
"**/schema/**",
|
||||
"**/models/**",
|
||||
"**/db/**",
|
||||
"prisma/schema.prisma",
|
||||
"drizzle/**",
|
||||
"**/*.sql",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "backend-engineer",
|
||||
domain: "backend",
|
||||
frameworks: ["fastify", "hono"],
|
||||
constraints: ["api-first", "strict-typing", "dependency-injection"],
|
||||
territory: [
|
||||
"**/api/**",
|
||||
"**/routes/**",
|
||||
"**/services/**",
|
||||
"**/middleware/**",
|
||||
"**/controllers/**",
|
||||
"**/auth/**",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "frontend-engineer",
|
||||
domain: "frontend",
|
||||
frameworks: ["react", "next.js"],
|
||||
constraints: ["component-first", "server-components", "minimal-client-js"],
|
||||
territory: [
|
||||
"**/components/**",
|
||||
"**/pages/**",
|
||||
"**/hooks/**",
|
||||
"**/styles/**",
|
||||
"**/*.tsx",
|
||||
"**/*.css",
|
||||
"**/*.vue",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export function matchFileToPersona(
|
||||
filePath: string,
|
||||
personas: ExecutePersonaConfig[]
|
||||
): ExecutePersonaConfig | null {
|
||||
const normalizedPath = filePath.replace(/\\/g, "/");
|
||||
|
||||
for (const persona of personas) {
|
||||
if (persona.domain === "coordination") continue;
|
||||
|
||||
for (const pattern of persona.territory) {
|
||||
const normalizedPattern = pattern.replace(/\\/g, "/");
|
||||
if (globMatch(normalizedPattern, normalizedPath)) {
|
||||
return persona;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function globMatch(pattern: string, path: string): boolean {
|
||||
const regexStr = pattern
|
||||
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
||||
.replace(/\*\*/g, "§§")
|
||||
.replace(/\*/g, "[^/]*")
|
||||
.replace(/§§/g, ".*")
|
||||
.replace(/\?/g, "[^/]");
|
||||
const regex = new RegExp(`^${regexStr}$`);
|
||||
return regex.test(path);
|
||||
}
|
||||
|
||||
export function detectConflicts(
|
||||
tasks: DecomposedTask[],
|
||||
personas: ExecutePersonaConfig[]
|
||||
): TerritoryConflict[] {
|
||||
const conflicts: TerritoryConflict[] = [];
|
||||
const filePersonaMap = new Map<string, string[]>();
|
||||
|
||||
for (const task of tasks) {
|
||||
for (const file of task.files) {
|
||||
if (!filePersonaMap.has(file)) {
|
||||
filePersonaMap.set(file, []);
|
||||
}
|
||||
const personas_list = filePersonaMap.get(file)!;
|
||||
if (!personas_list.includes(task.persona)) {
|
||||
personas_list.push(task.persona);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [file, claimingPersonas] of filePersonaMap) {
|
||||
if (claimingPersonas.length > 1) {
|
||||
const domains = claimingPersonas
|
||||
.map((p) => personas.find((pe) => pe.name === p)?.domain)
|
||||
.filter((d): d is PersonaDomain => d !== undefined);
|
||||
|
||||
let conflictType: TerritoryConflict["type"];
|
||||
if (domains.includes("data") && domains.includes("backend")) {
|
||||
conflictType = "data-backend";
|
||||
} else if (domains.includes("backend") && domains.includes("frontend")) {
|
||||
conflictType = "backend-frontend";
|
||||
} else {
|
||||
conflictType = "data-frontend";
|
||||
}
|
||||
|
||||
conflicts.push({
|
||||
type: conflictType,
|
||||
file,
|
||||
personas: claimingPersonas,
|
||||
description: `File ${file} claimed by multiple personas: ${claimingPersonas.join(", ")}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return conflicts;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { PipelineStage } from "./pipeline.js";
|
||||
|
||||
export type SessionStatus = "pending" | "running" | "paused" | "completed" | "failed" | "cancelled";
|
||||
|
||||
export type SessionIsolation = "branch";
|
||||
|
||||
export interface SessionConfig {
|
||||
max_concurrent_sessions: number;
|
||||
session_timeout_ms: number;
|
||||
session_isolation: SessionIsolation;
|
||||
}
|
||||
|
||||
export interface SessionInfo {
|
||||
id: string;
|
||||
project_slug: string;
|
||||
project_path: string;
|
||||
phase: number;
|
||||
stage: PipelineStage;
|
||||
status: SessionStatus;
|
||||
started_at: string;
|
||||
last_updated: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export const DEFAULT_SESSION_CONFIG: SessionConfig = {
|
||||
max_concurrent_sessions: 3,
|
||||
session_timeout_ms: 3600000,
|
||||
session_isolation: "branch",
|
||||
};
|
||||
Reference in New Issue
Block a user