diff --git a/docs/product-name-audit.md b/docs/product-name-audit.md index 2530d581..4fdd2961 100644 --- a/docs/product-name-audit.md +++ b/docs/product-name-audit.md @@ -21,7 +21,7 @@ git grep -n -i -E 'kapi|ilchul' -- ':!package-lock.json' ':!node_modules' | Storage/config | `.ilchul`, `~/.ilchul`, `.kapi` historical references, and `ILCHUL_*` environment variables appear in docs/tests/runtime setup. | storage/config namespace | `.ilchul` is active storage; `.kapi` is legacy evidence only and must not be deleted by this issue. | | GitHub review integration | `kapi-agent`, `kapi-agent/review`, and `kapi-review` remain literal external integration names. | external integration | Keep literal names while the GitHub App and required checks use them. | | Documentation/history | README, policy docs, references, and historical explanations still mention Kapi and Ilchul. | documentation/history | New public runtime examples should use `ilchul`; internal implementation examples may use `runctl` / `runtime`; historical and compatibility prose may keep product names with context. | -| Core/domain/application identifiers | Existing exported types and classes such as `KapiRegistryEntry`, `KapiService`, and `KapiStore` remain product-prefixed. The local factory export is now `createLocalWorkflowService`. | known reusable-code leakage reduced by bounded slices | Follow-up slices should rename these by boundary (`RegistryEntry`, `WorkflowService`, `WorkflowStore`, etc.) with compatibility exports where safe. | +| Core/domain/application identifiers | The registry entry type now uses `WorkflowRegistryEntry`; exported classes such as `KapiService` and `KapiStore` remain product-prefixed. The local factory export is now `createLocalWorkflowService`. | known reusable-code leakage reduced by bounded slices | Follow-up slices should rename remaining service/store identifiers by boundary (`WorkflowService`, `WorkflowStore`, etc.) with compatibility exports where safe. | | Application service filenames | The service implementation and factory now use semantic filenames: `src/application/workflow-service.ts` and `src/adapters/workflow-service-factory.ts`. | internal semantic implementation | Keep imports on the semantic service paths; exported class/function names are intentionally left for a smaller follow-up compatibility slice. | | CLI worker/runtime helper filenames | Worker event, runtime observation, and GitHub issue context helpers now use semantic filenames: `src/cli/worker-events.ts`, `src/cli/worker-runtime.ts`, and `src/cli/github-issue-context.ts`. | internal semantic implementation | Keep imports on the semantic helper paths; remaining worker event payload names such as `kapi.worker.*` are external event contracts, not filenames. | @@ -33,6 +33,8 @@ git grep -n -i -E 'kapi|ilchul' -- ':!package-lock.json' ':!node_modules' - Application service implementation import paths no longer use `kapi-*` filenames for the generic workflow service and local service factory. - The local service factory export uses the semantic `createLocalWorkflowService` name instead of `createLocalKapiService`. - Presentation helper exports use semantic workflow/tool names for generic helpers (`WorkflowToolDefinition`, `shouldBlockWorkflowToolCall`, `formatWorkflowError`). +- PR review state helpers use generic pull-request / agent-review names (`PullRequestReviewState`, `PullRequestReviewView`, `latestAgentReview`, `reviewCheckConclusion`). +- Registry entry types now use the semantic `WorkflowRegistryEntry` name across the domain, adapter, CLI, presentation, and tests. ## Residual scan after service filename rename diff --git a/src/adapters/registry-store.ts b/src/adapters/registry-store.ts index 5cfede6a..a2117cf7 100644 --- a/src/adapters/registry-store.ts +++ b/src/adapters/registry-store.ts @@ -1,4 +1,4 @@ -import type { KapiRegistryEntry, RegistryActivePointer, RegistryIndex, RegistryListResult } from "../domain/registry.js"; +import type { WorkflowRegistryEntry, RegistryActivePointer, RegistryIndex, RegistryListResult } from "../domain/registry.js"; import { fs, isNotFoundError, path } from "./fs-path.js"; const SAFE_REGISTRY_SLUG = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/; @@ -10,7 +10,7 @@ export function assertSafeRegistrySlug(slug: string): void { export class FileRegistryStore { constructor(private readonly rootName = ".ilchul") {} - async saveEntry(baseRepo: string, entry: KapiRegistryEntry, options: { setActive?: boolean } = {}): Promise { + async saveEntry(baseRepo: string, entry: WorkflowRegistryEntry, options: { setActive?: boolean } = {}): Promise { const file = this.entryPath(baseRepo, entry.slug); await this.writeJsonAtomic(file, entry); const entries = await this.listEntries(baseRepo); @@ -19,19 +19,19 @@ export class FileRegistryStore { if (options.setActive) await this.setActive(baseRepo, entry); } - async loadEntry(baseRepo: string, slug: string): Promise { - const entry = await this.readJson(this.entryPath(baseRepo, slug)); + async loadEntry(baseRepo: string, slug: string): Promise { + const entry = await this.readJson(this.entryPath(baseRepo, slug)); if (!entry) return undefined; return this.validateLoadedEntry(entry, slug); } - async listEntries(baseRepo: string): Promise { + async listEntries(baseRepo: string): Promise { return (await this.listEntriesTolerant(baseRepo, { strict: true })).entries; } async listEntriesTolerant(baseRepo: string, options: { strict?: boolean } = {}): Promise { const dir = this.workflowsDir(baseRepo); - const entries: KapiRegistryEntry[] = []; + const entries: WorkflowRegistryEntry[] = []; const warnings: RegistryListResult["warnings"] = []; try { for (const item of await fs.readdir(dir, { withFileTypes: true })) { @@ -40,7 +40,7 @@ export class FileRegistryStore { if (!this.isSafeRegistrySlug(fileSlug)) continue; const file = path.join(dir, item.name); try { - const entry = await this.readJson(file); + const entry = await this.readJson(file); if (!entry) continue; const validEntry = this.validateListedEntry(entry, fileSlug); if (validEntry) entries.push(validEntry); @@ -55,7 +55,7 @@ export class FileRegistryStore { return { entries: entries.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt) || a.slug.localeCompare(b.slug)), warnings }; } - async setActive(baseRepo: string, entry: KapiRegistryEntry): Promise { + async setActive(baseRepo: string, entry: WorkflowRegistryEntry): Promise { const active: RegistryActivePointer = { schemaVersion: 1, slug: entry.slug, @@ -66,7 +66,7 @@ export class FileRegistryStore { await this.writeJsonAtomic(this.activePath(baseRepo), active); } - async loadActive(baseRepo: string): Promise { + async loadActive(baseRepo: string): Promise { const active = await this.readJson(this.activePath(baseRepo)); if (!active) return undefined; return this.loadEntry(baseRepo, active.slug); @@ -106,18 +106,18 @@ export class FileRegistryStore { return SAFE_REGISTRY_SLUG.test(slug); } - private validateLoadedEntry(entry: KapiRegistryEntry, expectedSlug: string): KapiRegistryEntry { + private validateLoadedEntry(entry: WorkflowRegistryEntry, expectedSlug: string): WorkflowRegistryEntry { assertSafeRegistrySlug(entry.slug); if (entry.slug !== expectedSlug) throw new Error(`Kapi registry entry slug mismatch: expected ${expectedSlug}, found ${entry.slug}`); return entry; } - private validateListedEntry(entry: KapiRegistryEntry, fileSlug: string): KapiRegistryEntry | undefined { + private validateListedEntry(entry: WorkflowRegistryEntry, fileSlug: string): WorkflowRegistryEntry | undefined { if (!this.isSafeRegistrySlug(entry.slug)) return undefined; return entry.slug === fileSlug ? entry : undefined; } - private async writeIndex(baseRepo: string, entries: KapiRegistryEntry[]): Promise { + private async writeIndex(baseRepo: string, entries: WorkflowRegistryEntry[]): Promise { const index: RegistryIndex = { schemaVersion: 1, updatedAt: new Date().toISOString(), diff --git a/src/cli/runctl-cli.ts b/src/cli/runctl-cli.ts index 31dc4cec..cd72827a 100644 --- a/src/cli/runctl-cli.ts +++ b/src/cli/runctl-cli.ts @@ -6,7 +6,7 @@ import { assertSafeRegistrySlug, FileRegistryStore } from "../adapters/registry- import { LocalWorkerWorkspacePreparer } from "../adapters/worker-substrate.js"; import { mapWorkflowStateToGitHubWorkflowAdapter, type GitHubWorkflowRunContractAdapterView } from "../application/github-run-contract-adapter.js"; import type { WorkerPrepareResult, WorkerWorkspacePreparer } from "../application/ports.js"; -import { branchPrefixForWorkflow, promptKindForWorkflow, type GitHubIssueContextProbe, type KapiRegistryEntry, type RegistryListWarning, type RuntimePlan, workflowShortName } from "../domain/registry.js"; +import { branchPrefixForWorkflow, promptKindForWorkflow, type GitHubIssueContextProbe, type WorkflowRegistryEntry, type RegistryListWarning, type RuntimePlan, workflowShortName } from "../domain/registry.js"; import { buildQualityProbeMatrix, formatQualityProbeMatrix, type QualityMode, type QualityProbeMatrix } from "../domain/quality-probe.js"; import { renderRunContractPrompt } from "../domain/run-contract-prompt-renderer.js"; import { slugify, validateWorkflowState, WORKFLOW_DEFINITIONS, type WorkflowId, type WorkflowState, type WorkflowValidationReport } from "../application/service-domain.js"; @@ -292,14 +292,14 @@ async function assertNoRuntimeCollision(plan: RuntimePlan, registry: FileRegistr throw new Error(`Workflow runtime collision for ${plan.slug}: ${reasons} already owned by ${collision.workflowId}/${collision.slug} (${collision.status}${collision.piLaunch?.status ? `, pi ${collision.piLaunch.status}` : ""}). Inspect it with ${commandName} status/report/doctor or choose a unique --slug; terminal or stale registry entries remain inspectable but do not block retries.`); } -function blocksRuntimeStart(entry: KapiRegistryEntry): boolean { +function blocksRuntimeStart(entry: WorkflowRegistryEntry): boolean { if (["completed", "failed", "cancelled", "inactive"].includes(entry.status)) return false; return entry.piLaunch?.status !== "stale-registry"; } -async function createRegistryEntry(plan: RuntimePlan, now: () => string, registry: FileRegistryStore): Promise { +async function createRegistryEntry(plan: RuntimePlan, now: () => string, registry: FileRegistryStore): Promise { const timestamp = now(); - const entry: KapiRegistryEntry = { + const entry: WorkflowRegistryEntry = { schemaVersion: 1, slug: plan.slug, workflowId: plan.workflowId, @@ -321,7 +321,7 @@ async function createRegistryEntry(plan: RuntimePlan, now: () => string, registr return entry; } -function markRegistryActive(entry: KapiRegistryEntry, worker: WorkerPrepareResult, updatedAt: string): KapiRegistryEntry { +function markRegistryActive(entry: WorkflowRegistryEntry, worker: WorkerPrepareResult, updatedAt: string): WorkflowRegistryEntry { return { ...entry, status: "active", @@ -331,7 +331,7 @@ function markRegistryActive(entry: KapiRegistryEntry, worker: WorkerPrepareResul }; } -function buildRuntimePrompt(plan: RuntimePlan, goal: string, entry: KapiRegistryEntry): string { +function buildRuntimePrompt(plan: RuntimePlan, goal: string, entry: WorkflowRegistryEntry): string { const workflow = WORKFLOW_DEFINITIONS.find((definition) => definition.id === plan.workflowId); return `${renderRunContractPrompt({ mode: "start", @@ -372,7 +372,7 @@ async function readPromptAppendFile(baseRepo: string, file: string): Promise { +async function requireRegistryEntry(registry: FileRegistryStore, baseRepo: string, slug: string): Promise { const entry = await registry.loadEntry(baseRepo, slug); if (!entry) throw new Error(`Kapi registry entry not found: ${slug}`); return entry; } -function retainRegistryEntry(entry: KapiRegistryEntry, reason: string | undefined, actor: string | undefined, retainedAt: string): KapiRegistryEntry { +function retainRegistryEntry(entry: WorkflowRegistryEntry, reason: string | undefined, actor: string | undefined, retainedAt: string): WorkflowRegistryEntry { return { ...entry, retention: { @@ -425,7 +425,7 @@ function retainRegistryEntry(entry: KapiRegistryEntry, reason: string | undefine }; } -async function assertRetainableRegistryEntry(baseRepo: string, entry: KapiRegistryEntry, tmuxInspector: TmuxInspector): Promise { +async function assertRetainableRegistryEntry(baseRepo: string, entry: WorkflowRegistryEntry, tmuxInspector: TmuxInspector): Promise { if (!entry.tmuxSession) throw new Error(`${entry.slug}: cannot retain without a recorded tmux session`); const tmux = await tmuxInspector(entry.tmuxSession); const runtimeObservation = observeRuntimeFromTmux(baseRepo, entry, tmux); @@ -433,7 +433,7 @@ async function assertRetainableRegistryEntry(baseRepo: string, entry: KapiRegist if (!tmux.exists) throw new Error(`${entry.slug}: cannot retain because tmux session ${entry.tmuxSession} is not live (${runtimeObservation.state})`); } -function releaseRegistryEntry(entry: KapiRegistryEntry, actor: string | undefined, releasedAt: string): KapiRegistryEntry { +function releaseRegistryEntry(entry: WorkflowRegistryEntry, actor: string | undefined, releasedAt: string): WorkflowRegistryEntry { return { ...entry, retention: { @@ -448,7 +448,7 @@ function releaseRegistryEntry(entry: KapiRegistryEntry, actor: string | undefine }; } -async function safeCleanupRegistryEntry(baseRepo: string, entry: KapiRegistryEntry, tmuxInspector: TmuxInspector, tmuxKiller: TmuxKiller, updatedAt: string): Promise<{ cleaned: boolean; updated: boolean; notes: string[]; entry: KapiRegistryEntry; runtimeObservation: RuntimeObservation }> { +async function safeCleanupRegistryEntry(baseRepo: string, entry: WorkflowRegistryEntry, tmuxInspector: TmuxInspector, tmuxKiller: TmuxKiller, updatedAt: string): Promise<{ cleaned: boolean; updated: boolean; notes: string[]; entry: WorkflowRegistryEntry; runtimeObservation: RuntimeObservation }> { const tmux = entry.tmuxSession ? await tmuxInspector(entry.tmuxSession) : { exists: false }; const runtimeObservation = observeRuntimeFromTmux(baseRepo, entry, tmux); if (!entry.tmuxSession) return { cleaned: false, updated: false, notes: [`${entry.slug}: no tmux session recorded`], entry, runtimeObservation }; @@ -474,7 +474,7 @@ async function safeCleanupRegistryEntry(baseRepo: string, entry: KapiRegistryEnt }; } -function releasedRetention(entry: KapiRegistryEntry): NonNullable { +function releasedRetention(entry: WorkflowRegistryEntry): NonNullable { return { retained: false, ...(entry.retention?.reason ? { reason: entry.retention.reason } : {}), @@ -493,7 +493,7 @@ type DurableArtifactHealth = { exists: boolean; parseStatus: "ok" | "missing" | type EventArtifactHealth = DurableArtifactHealth & { lastEvents: unknown[] }; type SnapshotArtifactHealth = DurableArtifactHealth & { updatedAt?: string }; interface GitChangeSummary { changedFiles: string[]; contentChanges: string[]; modeOnlyChanges: string[]; changeSummaries: string[]; recommendation?: string } -interface WorkerReport { slug: string; registry: KapiRegistryEntry | null; runtime: { baseRepo: string; worktreePath?: string; branch?: string; tmuxSession?: string; artifactRoot?: string }; githubIssueContext?: GitHubIssueContextProbe; runtimeObservation?: RuntimeObservation; tmux: { session?: string; status: "running" | "missing" | "not-recorded"; lastLines: string[] }; worktree: { path?: string; exists: boolean; dirty: boolean } & GitChangeSummary; artifacts: { root?: string; exists: boolean; files: string[] }; evidence: Array<{ kind?: string; ref?: string; summary?: string; verdict?: string }>; events: EventArtifactHealth; snapshot: SnapshotArtifactHealth; qualityProbe?: QualityProbeMatrix; prReview?: PullRequestReviewState; githubRunContract?: GitHubWorkflowRunContractAdapterView; warnings: string[] } +interface WorkerReport { slug: string; registry: WorkflowRegistryEntry | null; runtime: { baseRepo: string; worktreePath?: string; branch?: string; tmuxSession?: string; artifactRoot?: string }; githubIssueContext?: GitHubIssueContextProbe; runtimeObservation?: RuntimeObservation; tmux: { session?: string; status: "running" | "missing" | "not-recorded"; lastLines: string[] }; worktree: { path?: string; exists: boolean; dirty: boolean } & GitChangeSummary; artifacts: { root?: string; exists: boolean; files: string[] }; evidence: Array<{ kind?: string; ref?: string; summary?: string; verdict?: string }>; events: EventArtifactHealth; snapshot: SnapshotArtifactHealth; qualityProbe?: QualityProbeMatrix; prReview?: PullRequestReviewState; githubRunContract?: GitHubWorkflowRunContractAdapterView; warnings: string[] } function boundedLineLimit(value: string | undefined): number { const parsed = Number(value ?? 40); return Number.isInteger(parsed) && parsed > 0 ? Math.min(parsed, 80) : 40; @@ -566,7 +566,7 @@ function summarizeReviewBody(body: string): string { return source.split(/\r?\n/).map((line) => line.replace(/^[-*]\s*/, "").trim()).filter(Boolean).slice(0, 3).join(" ").slice(0, 500); } -type WorkerReportInput = { baseRepo: string; slug: string; entry: KapiRegistryEntry | undefined; lines: number; env: NodeJS.ProcessEnv; githubPrInspector: GithubPrInspector }; +type WorkerReportInput = { baseRepo: string; slug: string; entry: WorkflowRegistryEntry | undefined; lines: number; env: NodeJS.ProcessEnv; githubPrInspector: GithubPrInspector }; async function buildWorkerReport(input: WorkerReportInput): Promise { const { baseRepo, slug, entry, lines, env, githubPrInspector } = input; @@ -696,7 +696,7 @@ async function inspectSnapshotHealth(root: string | undefined, exists: boolean, } } -async function buildQualityProbeForEntry(entry: KapiRegistryEntry, runtime: { worktree: boolean; terminal: boolean; promptDispatched: boolean }, artifactRootSafe = true): Promise { +async function buildQualityProbeForEntry(entry: WorkflowRegistryEntry, runtime: { worktree: boolean; terminal: boolean; promptDispatched: boolean }, artifactRootSafe = true): Promise { const mode = qualityModeForWorkflow(entry.workflowId); if (!mode || !entry.artifactRoot) return undefined; if (!artifactRootSafe) { @@ -712,7 +712,7 @@ function qualityModeForWorkflow(workflowId: WorkflowId): QualityMode | undefined return undefined; } -async function inspectQualityArtifacts(entry: KapiRegistryEntry, mode: QualityMode): Promise<{ validation: WorkflowValidationReport; parseability: { state?: boolean; events?: boolean; snapshot?: boolean; ledger?: boolean } }> { +async function inspectQualityArtifacts(entry: WorkflowRegistryEntry, mode: QualityMode): Promise<{ validation: WorkflowValidationReport; parseability: { state?: boolean; events?: boolean; snapshot?: boolean; ledger?: boolean } }> { const parseability: { state?: boolean; events?: boolean; snapshot?: boolean; ledger?: boolean } = {}; const state = await readQualityJson(path.join(entry.artifactRoot ?? "", "state.json")); let validation: WorkflowValidationReport; @@ -841,16 +841,16 @@ interface DoctorReport { entries: Array<{ slug: string; workflowId: WorkflowId; - status: KapiRegistryEntry["status"]; + status: WorkflowRegistryEntry["status"]; recommendedAction: string; recommended_action: SupervisorAction; runtimeObservation: RuntimeObservation; - diagnostics: { worktree: { status: "present" | "missing"; path?: string }; tmux: { status: "present" | "missing"; session?: string; paneSignal?: string }; promptDispatch: KapiRegistryEntry["promptDispatch"]; artifactRoot: { status: "present" | "missing"; path?: string }; worktreeChanges: { status: "clean" | "dirty" | "unknown"; summary: string[] }; contradictions: string[] }; + diagnostics: { worktree: { status: "present" | "missing"; path?: string }; tmux: { status: "present" | "missing"; session?: string; paneSignal?: string }; promptDispatch: WorkflowRegistryEntry["promptDispatch"]; artifactRoot: { status: "present" | "missing"; path?: string }; worktreeChanges: { status: "clean" | "dirty" | "unknown"; summary: string[] }; contradictions: string[] }; checks: DoctorCheck[]; }>; } -async function buildDoctorReport(baseRepo: string, env: NodeJS.ProcessEnv, entries: KapiRegistryEntry[], tmuxInspector: TmuxInspector, registryWarnings: RegistryListWarning[] = [], commandName = "runctl"): Promise { +async function buildDoctorReport(baseRepo: string, env: NodeJS.ProcessEnv, entries: WorkflowRegistryEntry[], tmuxInspector: TmuxInspector, registryWarnings: RegistryListWarning[] = [], commandName = "runctl"): Promise { const reports = await Promise.all(entries.map(async (entry) => { const warnings: string[] = []; const worktreePresent = await safeDirectory(entry.worktreePath, allowedWorktreeRoots(baseRepo, env), "worktree", warnings); @@ -942,17 +942,17 @@ function check(ok: boolean, pass: string, warn: string, detail: string): DoctorC return { name: ok ? pass : warn, status: ok ? "pass" : "warn", detail }; } -async function observeEntriesRuntime(baseRepo: string, entries: KapiRegistryEntry[], tmuxInspector: TmuxInspector): Promise { +async function observeEntriesRuntime(baseRepo: string, entries: WorkflowRegistryEntry[], tmuxInspector: TmuxInspector): Promise { return Promise.all(entries.map((entry) => observeEntryRuntime(baseRepo, entry, tmuxInspector))); } -async function observeEntryRuntime(baseRepo: string, entry: KapiRegistryEntry, tmuxInspector: TmuxInspector): Promise { +async function observeEntryRuntime(baseRepo: string, entry: WorkflowRegistryEntry, tmuxInspector: TmuxInspector): Promise { const tmux = entry.tmuxSession ? await tmuxInspector(entry.tmuxSession) : { exists: false }; return observeRuntimeFromTmux(baseRepo, entry, tmux); } -function describeSupervisorAction(action: SupervisorAction, entry: KapiRegistryEntry, baseRepo: string, commandName = "runctl"): string { +function describeSupervisorAction(action: SupervisorAction, entry: WorkflowRegistryEntry, baseRepo: string, commandName = "runctl"): string { if (action === "restart-not-recommended") return "restart-not-recommended: capture live tmux pane and inspect the report before restarting"; if (action === "manual-dispatch") return "manual-dispatch: attach to the live pane or dispatch the prompt manually before classifying failure"; if (action === "wait") return "wait: worker appears alive; continue observation before intervening"; diff --git a/src/cli/worker-events.ts b/src/cli/worker-events.ts index eae47704..37f55456 100644 --- a/src/cli/worker-events.ts +++ b/src/cli/worker-events.ts @@ -1,5 +1,5 @@ import { path } from "../adapters/fs-path.js"; -import { workflowShortName, type KapiRegistryEntry } from "../domain/registry.js"; +import { workflowShortName, type WorkflowRegistryEntry } from "../domain/registry.js"; import { classifyWorkerMarker, compactSignal, digestFor, observeRuntimeFromTmux, type RuntimeObservation, type RuntimeObservationState, type SupervisorAction, type TmuxInspection, type TmuxInspector } from "./worker-runtime.js"; export { compactSignal, observeRuntimeFromTmux }; @@ -11,7 +11,7 @@ export interface WorkerEvent { type: WorkerEventType; payload: { repo: string; r export interface WorkerEventsReport { cursor: string | null; events: WorkerEvent[] } type WorkerEventInput = { type: WorkerEventType; status: string; reason: string; tail?: string; observation?: RuntimeObservation; prReview?: PullRequestReviewState }; -export async function buildWorkerEvents(baseRepo: string, entries: KapiRegistryEntry[], options: { since?: string; staleMinutes: number; now: string; tmuxInspector: TmuxInspector; githubPrInspector: GithubPrInspector; env: NodeJS.ProcessEnv }): Promise { +export async function buildWorkerEvents(baseRepo: string, entries: WorkflowRegistryEntry[], options: { since?: string; staleMinutes: number; now: string; tmuxInspector: TmuxInspector; githubPrInspector: GithubPrInspector; env: NodeJS.ProcessEnv }): Promise { const events: WorkerEvent[] = []; for (const entry of entries) { const tmux = entry.tmuxSession ? await options.tmuxInspector(entry.tmuxSession) : { exists: false }; @@ -50,7 +50,7 @@ export function filterWorkerEventsSince(events: WorkerEvent[], snapshotCursor: s }); } -export function pushWorkerEvent(events: WorkerEvent[], repo: string, entry: KapiRegistryEntry, event: WorkerEventInput): void { +export function pushWorkerEvent(events: WorkerEvent[], repo: string, entry: WorkflowRegistryEntry, event: WorkerEventInput): void { const at = entry.updatedAt; const signal = digestFor([event.type, event.status, event.reason, event.tail ?? ""]); const cursor = `${at}|${entry.slug}|${event.type}|${signal}`; @@ -64,7 +64,7 @@ export function pushWorkerEvent(events: WorkerEvent[], repo: string, entry: Kapi export function eventSnapshotCursor(events: WorkerEvent[]): string | null { return events.length ? `snapshot:${digestFor(events.map((event) => event.payload.cursor))}` : null; } export function formatWorkerEvents(events: WorkerEvent[]): string { return events.length ? events.map((event) => `${event.type} ${event.payload.slug} — ${event.payload.status}: ${event.payload.reason}\n next: ${event.payload.recommended_action}\n cursor: ${event.payload.cursor}`).join("\n") : "Kapi events: none"; } export function reportCommandArgv(slug: string, baseRepo: string): string[] { return ["ilchul", "report", slug, "--from", baseRepo, "--json"]; } -function pushPrReviewEvents(events: WorkerEvent[], repo: string, entry: KapiRegistryEntry, prReview: PullRequestReviewState): void { +function pushPrReviewEvents(events: WorkerEvent[], repo: string, entry: WorkflowRegistryEntry, prReview: PullRequestReviewState): void { if (prReview.status !== "found") return; const pushPrEvent = (type: WorkerEventType, status: string, reason: string) => { const event = { type, status, reason, prReview }; diff --git a/src/cli/worker-runtime.ts b/src/cli/worker-runtime.ts index e71847c8..3be91f67 100644 --- a/src/cli/worker-runtime.ts +++ b/src/cli/worker-runtime.ts @@ -1,5 +1,5 @@ import { createHash } from "node:crypto"; -import type { KapiRegistryEntry } from "../domain/registry.js"; +import type { WorkflowRegistryEntry } from "../domain/registry.js"; export type TmuxInspection = { exists: boolean; paneSignal?: string }; export type TmuxInspector = (session: string) => Promise; @@ -50,7 +50,7 @@ export function compactSignal(output: string): string | undefined { return lines.length ? lines.join(" | ").slice(0, 240) : undefined; } -export function observeRuntimeFromTmux(baseRepo: string, entry: KapiRegistryEntry, tmux: TmuxInspection): RuntimeObservation { +export function observeRuntimeFromTmux(baseRepo: string, entry: WorkflowRegistryEntry, tmux: TmuxInspection): RuntimeObservation { const marker = classifyWorkerMarker(tmux.paneSignal ?? entry.piLaunch?.lastProbeOutput ?? ""); const nextCommandArgv = ["ilchul", "report", entry.slug, "--from", baseRepo, "--json"]; const base = { tmuxSession: entry.tmuxSession, registryPromptDispatch: entry.promptDispatch?.status, registryPiLaunch: entry.piLaunch?.status, next_command: nextCommandArgv.join(" "), nextCommandArgv, next_command_argv: nextCommandArgv }; diff --git a/src/domain/registry.ts b/src/domain/registry.ts index fe933177..6afd75e9 100644 --- a/src/domain/registry.ts +++ b/src/domain/registry.ts @@ -22,7 +22,7 @@ export interface GitHubIssueContextProbe { reason?: string; supervisorContextProvided: boolean; recommendedAction: string; } -export interface KapiRegistryEntry { +export interface WorkflowRegistryEntry { schemaVersion: 1; slug: string; workflowId: WorkflowId; @@ -60,7 +60,7 @@ export interface RegistryListWarning { } export interface RegistryListResult { - entries: KapiRegistryEntry[]; + entries: WorkflowRegistryEntry[]; warnings: RegistryListWarning[]; } diff --git a/src/presentation/runctl-formatters.ts b/src/presentation/runctl-formatters.ts index 42dba9e8..c4d8f6c2 100644 --- a/src/presentation/runctl-formatters.ts +++ b/src/presentation/runctl-formatters.ts @@ -1,26 +1,26 @@ import { formatGitHubWorkflowRunContractAdapter, type GitHubWorkflowRunContractAdapterView } from "../application/github-run-contract-adapter.js"; import type { WorkflowId } from "../application/service-domain.js"; -import type { GitHubIssueContextProbe, KapiRegistryEntry, RegistryListWarning } from "../domain/registry.js"; +import type { GitHubIssueContextProbe, WorkflowRegistryEntry, RegistryListWarning } from "../domain/registry.js"; import { formatQualityProbeMatrix, type QualityProbeMatrix } from "../domain/quality-probe.js"; export type SupervisorAction = "manual-dispatch" | "wait" | "inspect-report" | "restart-not-recommended" | "cleanup-safe"; export interface RuntimeObservationView { state: string; detail: string; recommended_action: SupervisorAction } export interface PullRequestReviewView { status: "found" | "not-found" | "unavailable"; diagnostics: string[]; pr?: { number: number; url: string; baseRefName: string; headRefName: string; headRefOid: string }; latestAgentReview?: { state: string; commitOid?: string; bodySummary?: string }; currentHeadReview: boolean; reviewCheckConclusion?: string; staleReviewDiagnostic?: string; recommendedAction: string } -export interface WorkerReportView { slug: string; registry: KapiRegistryEntry | null; githubIssueContext?: GitHubIssueContextProbe; runtimeObservation?: RuntimeObservationView; tmux: { session?: string; status: "running" | "missing" | "not-recorded"; lastLines: string[] }; worktree: { path?: string; exists: boolean; dirty: boolean; contentChanges: string[]; modeOnlyChanges: string[]; recommendation?: string }; artifacts: { files: string[] }; evidence: unknown[]; events: { parseStatus: string; lastEvents: unknown[] }; snapshot: { parseStatus: string; updatedAt?: string }; qualityProbe?: QualityProbeMatrix; prReview?: PullRequestReviewView; githubRunContract?: GitHubWorkflowRunContractAdapterView; warnings: string[] } +export interface WorkerReportView { slug: string; registry: WorkflowRegistryEntry | null; githubIssueContext?: GitHubIssueContextProbe; runtimeObservation?: RuntimeObservationView; tmux: { session?: string; status: "running" | "missing" | "not-recorded"; lastLines: string[] }; worktree: { path?: string; exists: boolean; dirty: boolean; contentChanges: string[]; modeOnlyChanges: string[]; recommendation?: string }; artifacts: { files: string[] }; evidence: unknown[]; events: { parseStatus: string; lastEvents: unknown[] }; snapshot: { parseStatus: string; updatedAt?: string }; qualityProbe?: QualityProbeMatrix; prReview?: PullRequestReviewView; githubRunContract?: GitHubWorkflowRunContractAdapterView; warnings: string[] } export type DoctorCheckView = { name: string; status: "pass" | "warn"; detail: string }; -export interface DoctorReportView { ok: boolean; checked: number; registryWarnings: RegistryListWarning[]; entries: Array<{ slug: string; workflowId: WorkflowId; status: KapiRegistryEntry["status"]; checks: DoctorCheckView[] }> } +export interface DoctorReportView { ok: boolean; checked: number; registryWarnings: RegistryListWarning[]; entries: Array<{ slug: string; workflowId: WorkflowId; status: WorkflowRegistryEntry["status"]; checks: DoctorCheckView[] }> } export function formatGitHubIssueContextLine(context: GitHubIssueContextProbe | undefined): string { return context ? `GitHub issue context: ${context.status}${context.issue ? ` ${context.issue.repo}#${context.issue.number}` : ""}; supervisor context embedded: ${context.supervisorContextProvided ? "yes" : "no"}${context.reason ? ` — ${context.reason}` : ""}` : ""; } -export function formatRegistryEntry(entry: KapiRegistryEntry, event: string): string { +export function formatRegistryEntry(entry: WorkflowRegistryEntry, event: string): string { const github = formatGitHubIssueContextLine(entry.githubIssueContext); const retention = formatRetentionLine(entry); return `Kapi registry ${event}: ${entry.workflowId}/${entry.slug}\nStatus: ${entry.status}\nPhase: ${entry.phase}\nWorktree: ${entry.worktreePath ?? "not planned"}\nTmux: ${entry.tmuxSession ?? "not planned"}${retention ? `\n${retention}` : ""}${github ? `\n${github}` : ""}`; } -export function formatRegistryList(entries: KapiRegistryEntry[], observations: RuntimeObservationView[] = [], warnings: RegistryListWarning[] = []): string { +export function formatRegistryList(entries: WorkflowRegistryEntry[], observations: RuntimeObservationView[] = [], warnings: RegistryListWarning[] = []): string { const bySlug = new Map(observations.map((observation, index) => [entries[index]?.slug, observation])); const warningLines = warnings.map((warning) => `warning: registry-entry-unreadable — ${warning.path}: ${warning.message}`); const entryLines = entries.map((entry) => { @@ -30,7 +30,7 @@ export function formatRegistryList(entries: KapiRegistryEntry[], observations: R return entryLines.length || warningLines.length ? [...entryLines, ...warningLines].join("\n") : "Kapi registry: no workflows."; } -export function formatRegistryStatus(entry: KapiRegistryEntry | undefined, observation?: RuntimeObservationView): string { +export function formatRegistryStatus(entry: WorkflowRegistryEntry | undefined, observation?: RuntimeObservationView): string { if (!entry) return "Kapi registry: no matching workflow."; const github = formatGitHubIssueContextLine(entry.githubIssueContext); return `${entry.workflowId}/${entry.slug} @@ -71,12 +71,12 @@ function formatPullRequestReview(review: PullRequestReviewView | undefined): str return [`kapi-agent PR review: ${review.pr ? `#${review.pr.number} ${review.pr.url}` : "unknown PR"}`, ` head: ${review.pr?.headRefName ?? "unknown"} -> ${review.pr?.baseRefName ?? "unknown"} @ ${review.pr?.headRefOid ?? "unknown"}`, ` latest: ${latest}`, ` current-head-review: ${review.currentHeadReview}`, ` check: ${review.reviewCheckConclusion ?? "missing"}`, ` action: ${review.recommendedAction}`, ...(review.staleReviewDiagnostic ? [` diagnostic: ${review.staleReviewDiagnostic}`] : []), ...(review.latestAgentReview?.bodySummary ? [` summary: ${review.latestAgentReview.bodySummary}`] : [])].join("\n"); } -function formatRetentionLine(entry: KapiRegistryEntry): string { +function formatRetentionLine(entry: WorkflowRegistryEntry): string { const value = formatRetentionValue(entry); return value === "none" ? "" : `Retention: ${value}`; } -function formatRetentionValue(entry: KapiRegistryEntry): string { +function formatRetentionValue(entry: WorkflowRegistryEntry): string { if (entry.retention?.retained) return `retained${entry.retention.reason ? ` (${entry.retention.reason})` : ""}`; if (entry.retention?.releasedAt) return `released @ ${entry.retention.releasedAt}`; return "none"; diff --git a/test/cli-args.test.ts b/test/cli-args.test.ts index e777e5b6..bd298384 100644 --- a/test/cli-args.test.ts +++ b/test/cli-args.test.ts @@ -7,7 +7,7 @@ import { promisify } from "node:util"; import { test } from "node:test"; import { FileRegistryStore } from "../src/adapters/registry-store.js"; import { buildRuntimePlan, parseRuntimeArgs, runRuntimeCli } from "../src/cli/runctl-cli.js"; -import type { KapiRegistryEntry } from "../src/domain/registry.js"; +import type { WorkflowRegistryEntry } from "../src/domain/registry.js"; const execFileAsync = promisify(execFile); @@ -170,7 +170,7 @@ test("live start rejects non-git --from directories before worker preparation", } }); -function registryEntry(baseRepo: string, overrides: Partial = {}): KapiRegistryEntry { +function registryEntry(baseRepo: string, overrides: Partial = {}): WorkflowRegistryEntry { const slug = overrides.slug ?? "recorded-worker"; return { schemaVersion: 1, slug, workflowId: "kapi-ralph", status: "active", phase: "planning", baseRepo, baseBranch: "dev", baseCommit: "abc123", @@ -215,7 +215,7 @@ test("CLI retain and release mark registry tmux holds without touching the sessi tmuxInspector: async () => ({ exists: true, paneSignal: "Done." }), }); assert.equal(retained.exitCode, 0); - const retainedData = JSON.parse(retained.stdout) as { entry: KapiRegistryEntry }; + const retainedData = JSON.parse(retained.stdout) as { entry: WorkflowRegistryEntry }; assert.equal(retainedData.entry.retention?.retained, true); assert.equal(retainedData.entry.retention?.reason, "manual inspection"); assert.equal(retainedData.entry.retention?.retainedBy, "kade"); @@ -227,7 +227,7 @@ test("CLI retain and release mark registry tmux holds without touching the sessi registry: store, }); assert.equal(released.exitCode, 0); - const releasedData = JSON.parse(released.stdout) as { entry: KapiRegistryEntry }; + const releasedData = JSON.parse(released.stdout) as { entry: WorkflowRegistryEntry }; assert.equal(releasedData.entry.retention?.retained, false); assert.equal(releasedData.entry.retention?.releasedAt, "2026-05-16T00:01:00.000Z"); assert.equal(releasedData.entry.retention?.releasedBy, "kade"); diff --git a/test/cli-worker-events.test.ts b/test/cli-worker-events.test.ts index 725ddc54..1d299192 100644 --- a/test/cli-worker-events.test.ts +++ b/test/cli-worker-events.test.ts @@ -1,9 +1,9 @@ import assert from "node:assert/strict"; import test from "node:test"; import { buildWorkerEvents, compactSignal, formatWorkerEvents, observeRuntimeFromTmux, type GithubPrInspector } from "../src/cli/worker-events.js"; -import type { KapiRegistryEntry } from "../src/domain/registry.js"; +import type { WorkflowRegistryEntry } from "../src/domain/registry.js"; -function entry(overrides: Partial = {}): KapiRegistryEntry { +function entry(overrides: Partial = {}): WorkflowRegistryEntry { return { schemaVersion: 1, slug: "ralph-helper-extract", workflowId: "kapi-ralph", status: "active", phase: "verify", baseRepo: "/tmp/devkade/ilchul", baseBranch: "dev", baseCommit: "abc1234", diff --git a/test/cli-worker-runtime.test.ts b/test/cli-worker-runtime.test.ts index b76a5585..b46524bc 100644 --- a/test/cli-worker-runtime.test.ts +++ b/test/cli-worker-runtime.test.ts @@ -1,9 +1,9 @@ import assert from "node:assert/strict"; import test from "node:test"; import { classifyWorkerMarker, compactSignal, digestFor, observeRuntimeFromTmux } from "../src/cli/worker-runtime.js"; -import type { KapiRegistryEntry } from "../src/domain/registry.js"; +import type { WorkflowRegistryEntry } from "../src/domain/registry.js"; -function entry(overrides: Partial = {}): KapiRegistryEntry { +function entry(overrides: Partial = {}): WorkflowRegistryEntry { return { schemaVersion: 1, slug: "ralph-runtime", diff --git a/test/registry-store.test.ts b/test/registry-store.test.ts index 74e21830..40d9fdbb 100644 --- a/test/registry-store.test.ts +++ b/test/registry-store.test.ts @@ -4,9 +4,9 @@ import os from "node:os"; import path from "node:path"; import { test } from "node:test"; import { FileRegistryStore } from "../src/adapters/registry-store.js"; -import type { KapiRegistryEntry } from "../src/domain/registry.js"; +import type { WorkflowRegistryEntry } from "../src/domain/registry.js"; -function entry(overrides: Partial): KapiRegistryEntry { +function entry(overrides: Partial): WorkflowRegistryEntry { return { schemaVersion: 1, slug: "ralph-registry-test", diff --git a/test/runctl-formatters.test.ts b/test/runctl-formatters.test.ts index 59c8e373..bc4b02c8 100644 --- a/test/runctl-formatters.test.ts +++ b/test/runctl-formatters.test.ts @@ -1,9 +1,9 @@ import { strict as assert } from "node:assert"; import { test } from "node:test"; -import type { KapiRegistryEntry } from "../src/domain/registry.js"; +import type { WorkflowRegistryEntry } from "../src/domain/registry.js"; import { formatDoctorReport, formatRegistryStatus, formatWorkerReport, type WorkerReportView } from "../src/presentation/runctl-formatters.js"; -const entry: KapiRegistryEntry = { +const entry: WorkflowRegistryEntry = { schemaVersion: 1, slug: "formatters", workflowId: "kapi-ralph", status: "active", phase: "build", baseRepo: "/repo", baseBranch: "dev", baseCommit: "abc123", worktreePath: "/repo/.ilchul/worktrees/formatters", branch: "feat/formatters", tmuxSession: "formatters", piLaunch: { command: "pi", status: "ready" },