import { Schema, Data, Deferred, Effect, Result, Exit, Context, Layer, } from "effect" import { join, relative, resolve, sep } from "node:path" import { locateItem } from "../workbase/item-selector" import { FileSystemService } from "./FileSystemService" import { TicketService } from "./TicketService" import { WorkbaseService } from "./WorkbaseService" import { normalizePullRequestRecord } from "../workbase/delivery-command" import { RepositoryService } from "./RepositoryService" import { aggregateProgress, readinessState } from "../readiness" import { parseFrontmatter } from "../workbase/frontmatter" import { documentRevision } from "../workbase/document-revision" import { VersionControlService } from "./VersionControlService" import { EpicFrontmatter, PhaseFrontmatter, TaskFrontmatter, type Dependency, type EpicFrontmatter as EpicData, type PhaseFrontmatter as PhaseData, type RepositoryReference, type TaskFrontmatter as TaskData, type WorkStatus, } from "../workbase/schemas" import { archivedEpicDirectory, archivedPhaseDirectory, archivedTaskDirectory, } from "../workbase/archive" class ContextError extends Data.TaggedError("ContextError")<{ readonly message: string readonly target?: string }> {} interface Document { readonly id: string readonly path: string readonly sha256: string readonly data: T readonly body: string } interface Target { readonly kind: "epic" | "task" | "phase" readonly archived: boolean readonly epicId?: string readonly taskId?: string readonly phaseId?: string readonly path: string } interface ExecutionRecord { readonly kind: "task" | "phase" readonly taskId: string readonly phaseId?: string readonly data: TaskData | PhaseData } interface CheckoutInspection { readonly materialized: boolean readonly registered: boolean readonly checkoutCommit: string | null readonly checkoutBranch: string | null readonly detached: boolean | null readonly dirty: boolean | null } type ExecutionData = PhaseData & Partial> interface ReferenceCheckout extends CheckoutInspection { readonly repo: string readonly ref: string readonly repositoryPath: string readonly checkoutPath: string readonly resolvedCommit: string | null } const decode = >( schema: S, input: unknown, ) => { const result = Schema.decodeUnknownResult(schema, { errors: "all", onExcessProperty: "error", })(input) return Result.isFailure(result) ? { ok: false as const, error: result.failure.message } : { ok: true as const, value: result.success } } const isWithin = (root: string, path: string) => { const child = relative(root, path) return child === "" || (!child.startsWith(`..${sep}`) && child !== "..") } const runGit = ( fs: FileSystemService["Service"], cwd: string, args: readonly string[], ) => fs.runCommand(["git", "-C", cwd, ...args], { captureOutput: true }).pipe( Effect.map((result) => result.exitCode === 0 ? result.stdout.trim() || null : null, ), Effect.catch(() => Effect.succeed(null)), ) const runGitText = ( fs: FileSystemService["Service"], cwd: string, args: readonly string[], ) => fs.runCommand(["git", "-C", cwd, ...args], { captureOutput: true }).pipe( Effect.map((result) => result.exitCode === 0 ? result.stdout.trim() : null, ), Effect.catch(() => Effect.succeed(null)), ) const worktreePaths = (output: string | null) => new Set( (output ?? "") .split("\n") .filter((line) => line.startsWith("worktree ")) .map((line) => line.slice("worktree ".length)), ) export class ContextService extends Context.Service()( "ContextService", { make: Effect.sync(() => ({ get: (options: { readonly target?: string readonly cwd?: string readonly compact?: boolean readonly full?: boolean readonly include?: readonly "ticket"[] }) => Effect.gen(function* () { const sourceFs = yield* FileSystemService const workbase = yield* WorkbaseService const repositoryService = yield* RepositoryService const versionControl = yield* VersionControlService const memoize = ( cache: Map>, key: string, effect: Effect.Effect, ) => { const cached = cache.get(key) if (cached) return Deferred.await(cached) return Effect.gen(function* () { const deferred = yield* Deferred.make() cache.set(key, deferred) const exit = yield* Effect.exit(effect) yield* Deferred.done(deferred, exit) return yield* Exit.match(exit, { onFailure: Effect.failCause, onSuccess: Effect.succeed, }) }) } const existsCache = new Map< string, Deferred.Deferred< Effect.Success>, Effect.Error> > >() const directoryCache = new Map< string, Deferred.Deferred< Effect.Success>, Effect.Error> > >() const readCache = new Map< string, Deferred.Deferred< Effect.Success>, Effect.Error> > >() const listingCache = new Map< string, Deferred.Deferred< Effect.Success>, Effect.Error> > >() const realPathCache = new Map< string, Deferred.Deferred< Effect.Success>, Effect.Error> > >() const fs = { ...sourceFs, exists: (path: string) => memoize(existsCache, path, sourceFs.exists(path)), isDirectory: (path: string) => memoize(directoryCache, path, sourceFs.isDirectory(path)), readFile: (path: string) => memoize(readCache, path, sourceFs.readFile(path)), readDirectory: (path: string) => memoize(listingCache, path, sourceFs.readDirectory(path)), realPath: (path: string) => memoize(realPathCache, path, sourceFs.realPath(path)), } satisfies FileSystemService["Service"] const cwd = resolve(options.cwd ?? process.cwd()) const suppliedTarget = options.target ?? "." const candidate = resolve(cwd, suppliedTarget) const candidateExists = yield* fs.exists(candidate) const { root, config } = yield* workbase.loadConfig( candidateExists ? candidate : cwd, ) const sourceBackend = yield* versionControl.forWorkbase(root) const revisionCache = new Map< string, Deferred.Deferred< Effect.Success>, Effect.Error> > >() const workspaceCache = new Map< string, Deferred.Deferred< Effect.Success>, Effect.Error> > >() const backend = { ...sourceBackend, resolveRevision: (repositoryPath: string, revision: string) => memoize( revisionCache, `${repositoryPath}\0${revision}`, sourceBackend.resolveRevision(repositoryPath, revision), ), listWorkspaces: (repositoryPath: string) => memoize( workspaceCache, repositoryPath, sourceBackend.listWorkspaces(repositoryPath), ), } const contextWorkbase = { ...workbase, discover: (startPath?: string) => workbase .discover(startPath) .pipe(Effect.provideService(FileSystemService, fs)), loadConfig: (startPath?: string) => workbase .loadConfig(startPath) .pipe(Effect.provideService(FileSystemService, fs)), repositoryAliases: (startPath?: string) => workbase .repositoryAliases(startPath) .pipe(Effect.provideService(FileSystemService, fs)), } const provideContextServices = ( effect: Effect.Effect, ) => effect.pipe( Effect.provideService(FileSystemService, fs), Effect.provideService(WorkbaseService, contextWorkbase), ) if (relative(root, candidate) === "") { const compact = !options.full const validation = yield* provideContextServices( workbase.validate(root, { includeDocuments: true }), ) const projectDiscoveryDocument = ( document: { readonly id: string readonly path: string readonly content: string readonly revision: string readonly data: T }, extra: Record = {}, ) => Effect.gen(function* () { const body = compact ? {} : { body: (yield* parseFrontmatter( document.content, document.path, )).body, } return { ...extra, id: document.id, path: document.path, sha256: document.revision, data: document.data, ...body, } }) const documents = validation.documents! const epics = yield* Effect.all( documents.epics.map((document) => projectDiscoveryDocument(document), ), { concurrency: "unbounded" }, ) const tasks = yield* Effect.all( documents.tasks.map((document) => projectDiscoveryDocument(document), ), { concurrency: "unbounded" }, ) const phases = yield* Effect.all( [...documents.phasesByTask].flatMap(([taskId, records]) => records.map((document) => projectDiscoveryDocument(document, { taskId }), ), ), { concurrency: "unbounded" }, ) return { projection: compact ? "compact" : "complete", workbase: { root, configPath: join(root, "agency.json"), version: config.version, autoArchive: config.autoArchive ?? false, vcs: "git", }, target: { kind: "workbase", path: root }, hint: compact ? "Run agency context . --full --json to include document prose." : null, discovery: { epics, tasks, phases }, authority: { mode: "orchestration", writable: null, references: [], }, validation: { valid: validation.valid, warnings: validation.issues, }, } } const inferTarget = (): Target | null => { if (!candidateExists && !suppliedTarget.includes(sep)) { return { kind: "task", archived: false, taskId: suppliedTarget, path: join(root, "tasks", suppliedTarget, "TASK.md"), } } if (!isWithin(root, candidate)) return null const location = locateItem(root, candidate) if (!location) return null const { archived } = location if (location.kind === "epic") { return { kind: "epic", archived, epicId: location.epicId, path: archived ? join( archivedEpicDirectory(root, location.epicId), "EPIC.md", ) : join(root, "epics", location.epicId, "EPIC.md"), } } if (location.kind === "phase") { return { kind: "phase", archived, taskId: location.taskId, phaseId: location.phaseId, path: archived ? join( archivedPhaseDirectory( root, location.taskId, location.phaseId, ), "PHASE.md", ) : join( root, "tasks", location.taskId, "phases", location.phaseId, "PHASE.md", ), } } return { kind: "task", archived, taskId: location.taskId, path: archived ? join(archivedTaskDirectory(root, location.taskId), "TASK.md") : join(root, "tasks", location.taskId, "TASK.md"), } } const inferredTarget = inferTarget() if (!inferredTarget) { return yield* new ContextError({ target: suppliedTarget, message: `Cannot infer an Agency target from ${candidate}`, }) } let target = inferredTarget if (!target.archived && !(yield* fs.exists(target.path))) { const archivedPath = target.kind === "epic" ? join(archivedEpicDirectory(root, target.epicId!), "EPIC.md") : target.kind === "phase" ? join( archivedPhaseDirectory( root, target.taskId!, target.phaseId!, ), "PHASE.md", ) : join(archivedTaskDirectory(root, target.taskId!), "TASK.md") if (yield* fs.exists(archivedPath)) { target = { ...target, archived: true, path: archivedPath } } } const readDocument = >( id: string, path: string, schema: S, ) => Effect.gen(function* () { if (!(yield* fs.exists(path))) { return yield* new ContextError({ target: suppliedTarget, message: `Target document does not exist: ${path}`, }) } const content = yield* fs.readFile(path) const parsed = yield* parseFrontmatter(content, path).pipe( Effect.mapError( (error) => new ContextError({ target: suppliedTarget, message: `Invalid target document ${path}: ${error.message}`, }), ), ) const decoded = decode(schema, parsed.data) if (!decoded.ok) { return yield* new ContextError({ target: suppliedTarget, message: `Invalid target document ${path}:\n${decoded.error}`, }) } return { id, path, sha256: documentRevision(content), data: decoded.value, body: parsed.body, } satisfies Document }) const readOptionalDocument = >( id: string, path: string, schema: S, ) => Effect.gen(function* () { if (!(yield* fs.exists(path))) return null return yield* readDocument(id, path, schema) }) const activeTaskPath = target.taskId ? join(root, "tasks", target.taskId, "TASK.md") : null const taskPath = target.taskId ? target.kind === "task" ? target.path : activeTaskPath && (yield* fs.exists(activeTaskPath)) ? activeTaskPath : join(archivedTaskDirectory(root, target.taskId), "TASK.md") : null const task = target.taskId && taskPath ? yield* readDocument(target.taskId, taskPath, TaskFrontmatter) : null const phase = target.phaseId ? yield* readDocument(target.phaseId, target.path, PhaseFrontmatter) : null const epicId = target.epicId ?? (task?.data && "epic" in task.data ? task.data.epic : undefined) const epic = epicId ? yield* readOptionalDocument( epicId, target.kind === "epic" ? target.path : (yield* fs.exists(join(root, "epics", epicId, "EPIC.md"))) ? join(root, "epics", epicId, "EPIC.md") : join(archivedEpicDirectory(root, epicId), "EPIC.md"), EpicFrontmatter, ) : null if (target.kind === "epic" && !epic) { return yield* new ContextError({ target: suppliedTarget, message: `Target document does not exist: ${target.path}`, }) } const validation = yield* provideContextServices( workbase.validate(root, { includeDocuments: true }), ) const validationDocuments = validation.documents! const taskDocuments = new Map>( validationDocuments.tasks.map((document) => [ document.id, { id: document.id, path: document.path, sha256: document.revision, data: document.data, body: "", }, ]), ) const phaseDocuments = new Map>() for (const [taskId, documents] of validationDocuments.phasesByTask) { for (const document of documents) { phaseDocuments.set(`${taskId}/${document.id}`, { id: document.id, path: document.path, sha256: document.revision, data: document.data, body: "", }) } } if (task && !taskDocuments.has(task.id)) taskDocuments.set(task.id, task) if (phase && target.taskId) { phaseDocuments.set(`${target.taskId}/${phase.id}`, phase) } const relevantTaskIds = new Set([ ...(target.taskId ? [target.taskId] : []), ...(epic?.data.tasks.map((child: Dependency) => child.id) ?? []), ]) for (const taskId of relevantTaskIds) { let document = taskDocuments.get(taskId) if (!document) { const archived = yield* Effect.result( readOptionalDocument( taskId, join(archivedTaskDirectory(root, taskId), "TASK.md"), TaskFrontmatter, ), ) document = Result.isSuccess(archived) ? (archived.success ?? undefined) : undefined if (document) taskDocuments.set(taskId, document) } if (!document || !("phases" in document.data)) continue for (const child of document.data.phases) { const key = `${taskId}/${child.id}` if (phaseDocuments.has(key)) continue const archived = yield* Effect.result( readOptionalDocument( child.id, join( archivedPhaseDirectory(root, taskId, child.id), "PHASE.md", ), PhaseFrontmatter, ), ) if (Result.isSuccess(archived) && archived.success) { phaseDocuments.set(key, archived.success) } } } const executionStatus = (record: ExecutionRecord): WorkStatus => { if (record.kind === "phase") return (record.data as PhaseData).status const data = record.data as TaskData if (!("phases" in data)) return data.status return aggregateProgress( data.phases.map( (item) => phaseDocuments.get(`${record.taskId}/${item.id}`)?.data .status ?? "open", ), ).status } const taskDependencyEntries = ( taskId: string, ): readonly Dependency[] => { const parentId = taskDocuments.get(taskId)?.data && "epic" in taskDocuments.get(taskId)!.data ? taskDocuments.get(taskId)!.data.epic : undefined if (!parentId) return [] if (epic?.id === parentId) return epic.data.tasks return [] } const dependencyStatus = (taskId: string): WorkStatus => { const document = taskDocuments.get(taskId) return document ? executionStatus({ kind: "task", taskId, data: document.data }) : "open" } const taskDependencies = (taskId: string) => taskDependencyEntries(taskId).find((item) => item.id === taskId) ?.dependsOn ?? [] const taskReady = (taskId: string): boolean => { if ( taskDependencies(taskId).some( (dependency) => dependencyStatus(dependency) !== "done", ) ) { return false } const document = taskDocuments.get(taskId) if (!document) return false if (!("phases" in document.data)) return document.data.status === "open" return document.data.phases.some((item) => { const status = phaseDocuments.get(`${taskId}/${item.id}`)?.data .status return ( status === "open" && (item.dependsOn ?? []).every( (dependency) => phaseDocuments.get(`${taskId}/${dependency}`)?.data .status === "done", ) ) }) } const relevantPaths = new Set( [epic?.path, task?.path, phase?.path] .filter((path): path is string => Boolean(path)) .map((path) => relative(root, path)), ) if (target.kind === "task" && target.taskId) { for (const [key, document] of phaseDocuments) { if (key.startsWith(`${target.taskId}/`)) { relevantPaths.add(relative(root, document.path)) } } } if (target.kind === "epic" && epic) { for (const child of epic.data.tasks) { const childTask = taskDocuments.get(child.id) if (childTask) relevantPaths.add(relative(root, childTask.path)) for (const [key, document] of phaseDocuments) { if (key.startsWith(`${child.id}/`)) { relevantPaths.add(relative(root, document.path)) } } } } const blockers: Array<{ kind: "dependency" | "validation" | "status" id: string status?: WorkStatus reason: string }> = [] let dependencies: readonly string[] = [] let dependents: readonly string[] = [] let targetStatus: WorkStatus = "open" let aggregateStatuses: WorkStatus[] = [] let descendantsReady = false if (target.kind === "phase" && task && phase) { const phaseEntries: readonly Dependency[] = "phases" in task.data ? task.data.phases : [] const declaration = phaseEntries.find( (item: Dependency) => item.id === target.phaseId, ) dependencies = [...(declaration?.dependsOn ?? [])].sort() dependents = phaseEntries .filter((item: Dependency) => item.dependsOn?.includes(target.phaseId!), ) .map((item: Dependency) => item.id) .sort() targetStatus = phase.data.status aggregateStatuses = [targetStatus] for (const dependency of dependencies) { const status = phaseDocuments.get( `${target.taskId}/${dependency}`, )?.data.status if (status !== "done") { blockers.push({ kind: "dependency", id: dependency, status, reason: status ? `Phase dependency is ${status}` : "Phase dependency is missing", }) } } for (const dependency of taskDependencies(target.taskId!)) { const status = dependencyStatus(dependency) if (status !== "done") { blockers.push({ kind: "dependency", id: dependency, status, reason: `Parent task dependency is ${status}`, }) } } } else if (target.kind === "task" && task) { const entries = taskDependencyEntries(target.taskId!) const declaration = entries.find( (item) => item.id === target.taskId, ) dependencies = [...(declaration?.dependsOn ?? [])].sort() dependents = entries .filter((item) => item.dependsOn?.includes(target.taskId!)) .map((item) => item.id) .sort() targetStatus = executionStatus({ kind: "task", taskId: target.taskId!, data: task.data, }) aggregateStatuses = "phases" in task.data ? task.data.phases.map( (item: Dependency) => phaseDocuments.get(`${target.taskId}/${item.id}`)?.data .status ?? "open", ) : [task.data.status] descendantsReady = taskReady(target.taskId!) for (const dependency of dependencies) { const status = dependencyStatus(dependency) if (status !== "done") { blockers.push({ kind: "dependency", id: dependency, status, reason: `Task dependency is ${status}`, }) } } if ("phases" in task.data) { for (const child of task.data.phases) { const childStatus = phaseDocuments.get( `${target.taskId}/${child.id}`, )?.data.status if (childStatus === "dropped") { blockers.push({ kind: "status", id: child.id, status: childStatus, reason: "Child phase is dropped", }) } if (childStatus === "open") { for (const dependency of child.dependsOn ?? []) { const status = phaseDocuments.get( `${target.taskId}/${dependency}`, )?.data.status if (status !== "done") { blockers.push({ kind: "dependency", id: `${child.id}:${dependency}`, status, reason: status ? `Phase '${child.id}' dependency '${dependency}' is ${status}` : `Phase '${child.id}' dependency '${dependency}' is missing`, }) } } } } } } else if (target.kind === "epic" && epic) { aggregateStatuses = epic.data.tasks.flatMap((item: Dependency) => { const child = taskDocuments.get(item.id) if (!child) return [] return "phases" in child.data ? child.data.phases.map( (childPhase) => phaseDocuments.get(`${item.id}/${childPhase.id}`)?.data .status ?? "open", ) : [child.data.status] }) targetStatus = aggregateProgress(aggregateStatuses).status descendantsReady = epic.data.tasks.some((item: Dependency) => taskReady(item.id), ) for (const child of epic.data.tasks) { for (const dependency of child.dependsOn ?? []) { const status = dependencyStatus(dependency) if (status !== "done") { blockers.push({ kind: "dependency", id: `${child.id}:${dependency}`, status, reason: `Task '${child.id}' dependency '${dependency}' is ${status}`, }) } } const childTask = taskDocuments.get(child.id) if (!childTask) continue if (!("phases" in childTask.data)) { if (childTask.data.status === "dropped") { blockers.push({ kind: "status", id: child.id, status: "dropped", reason: "Child task is dropped", }) } continue } for (const childPhase of childTask.data.phases) { const status = phaseDocuments.get( `${child.id}/${childPhase.id}`, )?.data.status if (status === "dropped") { blockers.push({ kind: "status", id: `${child.id}/${childPhase.id}`, status, reason: "Child phase is dropped", }) } if (status === "open") { for (const dependency of childPhase.dependsOn ?? []) { const dependencyState = phaseDocuments.get( `${child.id}/${dependency}`, )?.data.status if (dependencyState !== "done") { blockers.push({ kind: "dependency", id: `${child.id}/${childPhase.id}:${dependency}`, status: dependencyState, reason: dependencyState ? `Phase '${childPhase.id}' dependency '${dependency}' is ${dependencyState}` : `Phase '${childPhase.id}' dependency '${dependency}' is missing`, }) } } } } } } const orchestrationTarget = target.kind === "epic" || (target.kind === "task" && task !== null && "phases" in task.data) if (target.archived) { blockers.push({ kind: "status", id: target.phaseId ?? target.taskId ?? target.epicId ?? suppliedTarget, reason: "Target is archived; restore it before mutation or execution", }) } if (!orchestrationTarget && targetStatus !== "open") { blockers.push({ kind: "status", id: target.phaseId ?? target.taskId ?? target.epicId ?? suppliedTarget, status: targetStatus, reason: `Target status is ${targetStatus}`, }) } for (const issue of validation.issues.filter((item) => relevantPaths.has(item.path), )) { blockers.push({ kind: "validation", id: issue.path, reason: issue.message, }) } const ready = orchestrationTarget ? !target.archived && descendantsReady && !blockers.some((blocker) => blocker.kind === "validation") : targetStatus === "open" && blockers.length === 0 const executionData: ExecutionData | null = target.archived ? null : phase?.data ? phase.data : task?.data && "repo" in task.data ? (task.data as ExecutionData) : null const reviewData = !target.archived && task?.data && "review" in task.data ? task.data.review : null const writableDocuments = reviewData ? task ? [task.path] : [] : executionData && task ? phase ? [task.path, phase.path] : [task.path] : [] const references: readonly RepositoryReference[] = target.archived ? [] : reviewData ? [{ repo: reviewData.repo, ref: reviewData.commit }] : executionData ? (executionData.repos ?? []) : (epic?.data.repos ?? []) const entityDirectory = target.path.replace( /\/(?:EPIC|TASK|PHASE)\.md$/, "", ) const codePath = join(entityDirectory, "code") const inspectionWarnings: string[] = [] const repositories = new Map( reviewData ? (yield* provideContextServices( repositoryService.list(root), )).map((repository) => [repository.alias, repository]) : [], ) const inspectCheckout = ( repositoryPath: string, checkoutPath: string, ) => Effect.gen(function* (): Generator { const [materialized, listed, canonicalRoot] = yield* Effect.all( [ fs.isDirectory(checkoutPath), runGit(fs, repositoryPath, [ "worktree", "list", "--porcelain", ]), fs.realPath(root), ], { concurrency: "unbounded" }, ) if (listed === null) { inspectionWarnings.push( `Unable to inspect worktree registrations for ${repositoryPath}`, ) } const listedPaths = worktreePaths(listed) const canonicalCheckoutPath = join( canonicalRoot, relative(root, checkoutPath), ) let registered = listedPaths.has(checkoutPath) || listedPaths.has(canonicalCheckoutPath) if (!materialized) { return { materialized: false, registered, checkoutCommit: null, checkoutBranch: null, detached: null, dirty: null, } } const [ checkoutCommit, checkoutBranch, checkoutStatus, resolvedCheckoutPath, ] = yield* Effect.all( [ runGit(fs, checkoutPath, ["rev-parse", "HEAD"]), runGit(fs, checkoutPath, [ "symbolic-ref", "--quiet", "--short", "HEAD", ]), runGitText(fs, checkoutPath, ["status", "--porcelain"]), fs.realPath(checkoutPath), ], { concurrency: "unbounded" }, ) registered = registered || listedPaths.has(resolvedCheckoutPath) if (checkoutCommit === null) { inspectionWarnings.push( `Unable to inspect checkout ${checkoutPath}`, ) } return { materialized: true, registered, checkoutCommit, checkoutBranch, detached: checkoutBranch === null, dirty: checkoutStatus === null ? null : checkoutStatus.length > 0, } }) const writable = executionData ? yield* Effect.gen(function* () { const repository = repositories.get(executionData.repo) const repositoryPath = repository?.path ?? join(root, "repos", executionData.repo) const checkoutPath = join(codePath, executionData.repo) const [branchCommit, baseCommit, checkout] = yield* Effect.all( [ backend.resolveRevision( repositoryPath, executionData.branch, ), backend.resolveRevision(repositoryPath, executionData.base), inspectCheckout(repositoryPath, checkoutPath), ], { concurrency: "unbounded" }, ) if (branchCommit === null) { inspectionWarnings.push( `Unable to resolve branch '${executionData.branch}' in ${repositoryPath}`, ) } if (baseCommit === null) { inspectionWarnings.push( `Unable to resolve base '${executionData.base}' in ${repositoryPath}`, ) } return { repo: executionData.repo, repositoryPath, checkoutPath, branch: executionData.branch, base: executionData.base, branchCommit, baseCommit, ...checkout, } }) : null const referenceCheckouts: ReferenceCheckout[] = [] for (const reference of references) { const repository = repositories.get(reference.repo) const repositoryPath = repository?.path ?? join(root, "repos", reference.repo) const checkoutPath = join(codePath, reference.repo) const resolvedCommit = yield* backend.resolveRevision( repositoryPath, reference.ref, ) if (resolvedCommit === null) { inspectionWarnings.push( `Unable to resolve reference '${reference.ref}' in ${repositoryPath}`, ) } referenceCheckouts.push({ repo: reference.repo, ref: reference.ref, repositoryPath, checkoutPath, resolvedCommit, ...(yield* inspectCheckout(repositoryPath, checkoutPath)), }) } const reviewRepository = reviewData ? repositories.get(reviewData.repo) : null const reviewSourceCommit = reviewData && reviewRepository?.remote ? yield* fs .runCommand( [ "git", "ls-remote", reviewRepository.remote, reviewData.source.kind === "pull-request" ? reviewData.source.fetchRef : reviewData.source.ref .replace(/^refs\/remotes\/origin\//, "") .replace(/^origin\//, ""), ], { captureOutput: true }, ) .pipe( Effect.map((result) => result.exitCode === 0 ? result.stdout.trim() || null : null, ), Effect.catch(() => Effect.succeed(null)), ) : null const checkoutStates = [writable, ...referenceCheckouts].filter( (value): value is NonNullable => value !== null, ) const materializedCount = checkoutStates.filter( (value) => value.materialized, ).length const materialization = checkoutStates.length === 0 || materializedCount === 0 ? "absent" : materializedCount === checkoutStates.length ? "complete" : "partial" const compact = !options.full const projectDocument = (document: Document | null) => document ? compact ? { id: document.id, path: document.path, sha256: document.sha256, data: document.data, } : document : null const pullRequestData = phase?.data ?? (task?.data && "repo" in task.data ? task.data : null) const ticket = options.include?.includes("ticket") ? yield* TicketService.pipe( Effect.flatMap((service) => service.read( task ? task.data.ticketUrl : (epic?.data.ticketUrl ?? null), ), ), ) : undefined return { projection: compact ? "compact" : "complete", workbase: { root, configPath: join(root, "agency.json"), version: config.version, autoArchive: config.autoArchive ?? false, vcs: "git", }, target, documents: { epic: projectDocument(epic), task: projectDocument(task), phase: projectDocument(phase), }, graph: { parent: target.kind === "phase" ? { kind: "task", id: target.taskId } : target.kind === "task" && epicId ? { kind: "epic", id: epicId } : null, dependencies, dependents, readiness: { ...readinessState(targetStatus, blockers, ready), blockers, }, aggregate: aggregateProgress(aggregateStatuses), }, authority: { mode: reviewData ? "review" : executionData ? "execution" : "orchestration", writable: writable ? { repo: writable.repo, repositoryPath: writable.repositoryPath, checkoutPath: writable.checkoutPath, branch: writable.branch, base: writable.base, } : null, references: referenceCheckouts.map((reference) => ({ repo: reference.repo, ref: reference.ref, repositoryPath: reference.repositoryPath, checkoutPath: reference.checkoutPath, })), documents: { writable: writableDocuments, }, }, workspace: compact ? { codePath, materialization, writable: writable ? { materialized: writable.materialized, registered: writable.registered, } : null, references: referenceCheckouts.map((reference) => ({ repo: reference.repo, materialized: reference.materialized, registered: reference.registered, })), warnings: inspectionWarnings, } : { codePath, materialization, writable, references: referenceCheckouts, warnings: inspectionWarnings, }, review: reviewData ? { ...reviewData, sourceObservation: { available: reviewSourceCommit !== null, commit: reviewSourceCommit?.split(/\s+/)[0] ?? null, }, checkout: referenceCheckouts[0] ?? null, } : null, pr: pullRequestData?.pr ? normalizePullRequestRecord(pullRequestData.pr) : { url: null, state: "none" }, ticket, validation: { valid: validation.valid, warnings: validation.issues, }, } }), })), }, ) { static readonly layer = Layer.effect(this, this.make) }