feat(P01-P05): multi-session support & execute-phase persona specialization — SESSION-01..05, PERSONA-01..11, CLI-01..04, INTEG-01..05
CI / build-and-test (push) Has been cancelled
Publish to npm / publish (push) Has been cancelled

---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:
Jon Chery
2026-06-01 17:43:06 +00:00
parent 6d0034dc88
commit 8c975352b8
20 changed files with 2398 additions and 13 deletions
+1
View File
@@ -55,6 +55,7 @@ export interface CIAgentMetadata {
phase: number;
milestone: string;
project?: string;
session?: string;
plan?: string;
task?: string;
status: PipelineStage;
+33 -2
View File
@@ -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"] },
],
},
};
+168
View File
@@ -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;
}
+29
View File
@@ -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",
};