Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/product-name-audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ git grep -n -i -E 'kapi|ilchul' -- ':!package-lock.json' ':!node_modules'
- Store abstractions now use semantic `WorkflowStore` / `FileWorkflowStore` names across application ports, adapters, and runtime probe scripts; compatibility imports have been migrated and the temporary store alias has been removed.
- Service source surfaces now use semantic `WorkflowService` in the implementation, local factory, presentation layer, runtime probe scripts, and active tests; the temporary `KapiService` export has been removed after migrating the large remaining test clusters.
- Presentation registration/UI action helpers now use semantic workflow names (`registerWorkflowExtension`, `registerWorkflowCommandsAndTools`, `registerWorkflowTools`, `WorkflowUiActionContext`, `installWorkflowAutocomplete`, `toggleWorkflowWidgetExpanded`) while leaving public `kapi_*` tool names and `/kapi-*` contracts intact.
- Adapter/domain private helper names now use semantic state/workflow naming for symlink guards, managed autoresearch symlinks, worker branch validation, and prompt rule rendering while preserving user-facing Kapi contract text.

## Residual scan after service filename rename

Expand Down
10 changes: 5 additions & 5 deletions src/adapters/autoresearch-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,16 +145,16 @@ export class FileAutoresearchBridgePreparer {
const activeFiles = new Set(bridgeFiles.map((file) => file.rootName));
for (const file of AUTORESEARCH_ARTIFACT_NAMES) {
if (activeFiles.has(file)) continue;
await this.removeKapiOwnedSymlink(workspace, file);
await this.removeManagedSymlink(workspace, file);
}
}

private async removeKapiOwnedSymlink(workspace: string, file: AutoresearchArtifactName): Promise<void> {
private async removeManagedSymlink(workspace: string, file: AutoresearchArtifactName): Promise<void> {
const linkPath = path.join(workspace, file);
const stat = await this.lstatIfExists(linkPath);
if (!stat?.isSymbolicLink()) return;
const currentTarget = await fs.readlink(linkPath);
if (this.isKapiAutoresearchTarget(workspace, path.resolve(workspace, currentTarget))) await fs.unlink(linkPath);
if (this.isManagedAutoresearchTarget(workspace, path.resolve(workspace, currentTarget))) await fs.unlink(linkPath);
}

private async ensureSymlink(input: { workspace: string; artifactRoot: string; file: BridgeArtifact }): Promise<void> {
Expand All @@ -176,15 +176,15 @@ export class FileAutoresearchBridgePreparer {
const resolvedCurrent = path.resolve(input.workspace, currentTarget);
const resolvedTarget = path.resolve(targetPath);
if (resolvedCurrent === resolvedTarget) return;
if (this.isKapiAutoresearchTarget(input.workspace, resolvedCurrent)) {
if (this.isManagedAutoresearchTarget(input.workspace, resolvedCurrent)) {
await fs.unlink(linkPath);
await fs.symlink(relativeTarget, linkPath);
return;
}
throw new Error(`Cannot prepare Kapi autoresearch bridge: ${input.file.rootName} points to ${currentTarget}, not ${relativeTarget}.`);
}

private isKapiAutoresearchTarget(workspace: string, targetPath: string): boolean {
private isManagedAutoresearchTarget(workspace: string, targetPath: string): boolean {
const relative = path.relative(workspace, targetPath).replace(/\\/g, "/");
return relative.startsWith(".ilchul/workflows/autoresearch/");
}
Expand Down
32 changes: 16 additions & 16 deletions src/adapters/file-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,18 @@ interface ActiveIndex {
export class FileWorkflowStore implements WorkflowStore {
async saveWorkflow(state: WorkflowState): Promise<void> {
const file = path.join(state.artifactRoot, "state.json");
await this.prepareWritableKapiFile(state.workspace, file);
await this.prepareWritableStateFile(state.workspace, file);
await fs.writeFile(file, `${JSON.stringify(state, null, 2)}\n`, "utf8");
}

async loadWorkflow(workspace: string, workflowId: WorkflowState["workflowId"], slug: string): Promise<WorkflowState | undefined> {
const file = this.statePath(workspace, workflowId, slug);
await this.assertKapiAncestorsAreNotSymlinks(workspace, file);
await this.assertStateAncestorsAreNotSymlinks(workspace, file);
return this.readJson<WorkflowState>(file);
}

async loadActive(workspace: string): Promise<WorkflowState | undefined> {
await this.assertKapiAncestorsAreNotSymlinks(workspace, this.activePath(workspace));
await this.assertStateAncestorsAreNotSymlinks(workspace, this.activePath(workspace));
const active = await this.readJsonOrClearActivePointer<ActiveIndex>(workspace, this.activePath(workspace));
if (!active) return undefined;
return this.resolveActivePointer(workspace, active);
Expand All @@ -38,7 +38,7 @@ export class FileWorkflowStore implements WorkflowStore {
updatedAt: state.updatedAt,
};
const file = this.activePath(state.workspace);
await this.prepareWritableKapiFile(state.workspace, file);
await this.prepareWritableStateFile(state.workspace, file);
await fs.writeFile(file, `${JSON.stringify(active, null, 2)}\n`, "utf8");
}

Expand All @@ -51,13 +51,13 @@ export class FileWorkflowStore implements WorkflowStore {
}

async ensureArtifacts(state: WorkflowState): Promise<void> {
await this.assertKapiAncestorsAreNotSymlinks(state.workspace, path.join(state.artifactRoot, "state.json"));
await this.assertStateAncestorsAreNotSymlinks(state.workspace, path.join(state.artifactRoot, "state.json"));
await fs.mkdir(state.artifactRoot, { recursive: true });
await Promise.all(
state.artifacts.map(async (artifact) => {
this.assertArtifactPathInsideRoot(state, artifact);
const file = artifact.path;
await this.assertKapiAncestorsAreNotSymlinks(state.workspace, file);
await this.assertStateAncestorsAreNotSymlinks(state.workspace, file);
if (this.isDirectoryArtifact(state, artifact.name)) {
await this.ensureDirectoryArtifact(state, artifact.name, file);
return;
Expand All @@ -79,20 +79,20 @@ export class FileWorkflowStore implements WorkflowStore {

async readArtifact(state: WorkflowState, artifactName: string): Promise<string | undefined> {
const artifact = this.resolveArtifact(state, artifactName);
await this.assertKapiAncestorsAreNotSymlinks(state.workspace, artifact.path);
await this.assertStateAncestorsAreNotSymlinks(state.workspace, artifact.path);
return this.readFileIfExists(artifact.path);
}

async artifactExists(state: WorkflowState, artifactName: string): Promise<boolean> {
const artifact = this.resolveArtifact(state, artifactName);
await this.assertKapiAncestorsAreNotSymlinks(state.workspace, artifact.path);
await this.assertStateAncestorsAreNotSymlinks(state.workspace, artifact.path);
if (this.isDirectoryArtifact(state, artifact.name)) return this.directoryArtifactExists(artifact.name, artifact.path);
return this.regularFileExists(artifact.path);
}

async artifactMetadata(state: WorkflowState, artifactName: string): Promise<{ exists: boolean; isFile: boolean; isSymlink: boolean; executable: boolean }> {
const artifact = this.resolveArtifact(state, artifactName);
await this.assertKapiAncestorsAreNotSymlinks(state.workspace, artifact.path);
await this.assertStateAncestorsAreNotSymlinks(state.workspace, artifact.path);
const stat = await this.lstatIfExists(artifact.path);
if (!stat) return { exists: false, isFile: false, isSymlink: false, executable: false };
const isSymlink = stat.isSymbolicLink();
Expand All @@ -113,7 +113,7 @@ export class FileWorkflowStore implements WorkflowStore {
await this.assertArtifactFileIsNotSymlink(artifactPath);
if (artifactName !== "specs") return;
const readmePath = path.join(artifactPath, "README.md");
await this.assertKapiAncestorsAreNotSymlinks(state.workspace, readmePath);
await this.assertStateAncestorsAreNotSymlinks(state.workspace, readmePath);
if (!(await this.fileExists(readmePath))) {
await fs.writeFile(readmePath, this.initialSpecsReadmeContent(state), "utf8");
}
Expand Down Expand Up @@ -154,7 +154,7 @@ export class FileWorkflowStore implements WorkflowStore {

private async writableArtifactPath(state: WorkflowState, artifactName: string): Promise<string> {
const artifact = this.resolveArtifact(state, artifactName);
await this.prepareWritableKapiFile(state.workspace, artifact.path);
await this.prepareWritableStateFile(state.workspace, artifact.path);
return artifact.path;
}

Expand Down Expand Up @@ -193,7 +193,7 @@ export class FileWorkflowStore implements WorkflowStore {

private async listWorkflowRoot(workspace: string): Promise<WorkflowState[]> {
const root = path.join(workspace, ".ilchul", "workflows");
await this.assertKapiAncestorsAreNotSymlinks(workspace, path.join(root, ".list"));
await this.assertStateAncestorsAreNotSymlinks(workspace, path.join(root, ".list"));
const states: WorkflowState[] = [];
try {
for (const workflowDir of await fs.readdir(root, { withFileTypes: true })) {
Expand Down Expand Up @@ -227,7 +227,7 @@ export class FileWorkflowStore implements WorkflowStore {
return this.clearActivePointer(workspace);
}

await this.assertKapiAncestorsAreNotSymlinks(workspace, active.statePath);
await this.assertStateAncestorsAreNotSymlinks(workspace, active.statePath);
const state = await this.readJsonOrClearActivePointer<WorkflowState>(workspace, active.statePath);
if (!state) return this.clearActivePointer(workspace);
if (!this.isStateConsistentWithWorkspace(workspace, state)) return this.clearActivePointer(workspace);
Expand Down Expand Up @@ -270,8 +270,8 @@ export class FileWorkflowStore implements WorkflowStore {
return fs.readFile(file, "utf8");
}

private async prepareWritableKapiFile(workspace: string, file: string): Promise<void> {
await this.assertKapiAncestorsAreNotSymlinks(workspace, file);
private async prepareWritableStateFile(workspace: string, file: string): Promise<void> {
await this.assertStateAncestorsAreNotSymlinks(workspace, file);
await fs.mkdir(path.dirname(file), { recursive: true });
await this.assertArtifactFileIsNotSymlink(file);
}
Expand All @@ -281,7 +281,7 @@ export class FileWorkflowStore implements WorkflowStore {
if (stat) this.assertNotSymlink(file, stat);
}

private async assertKapiAncestorsAreNotSymlinks(workspace: string, file: string): Promise<void> {
private async assertStateAncestorsAreNotSymlinks(workspace: string, file: string): Promise<void> {
const workspaceRoot = path.resolve(workspace);
const resolvedFile = path.resolve(file);
const relative = path.relative(workspaceRoot, resolvedFile);
Expand Down
4 changes: 2 additions & 2 deletions src/adapters/worker-substrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ function defaultWorkerBranchName(request: WorkerPrepareInput, id: string): strin
return `${type}/${mode}-${request.workflowSlug}-${id.slice(-8)}`;
}

function assertKapiOwnedBranchName(branch: string): void {
function assertWorkflowOwnedBranchName(branch: string): void {
if (/^(feat|fix|perf|refactor|test|docs|chore|build|ci)\/[a-z0-9][a-z0-9-]*$/.test(branch)) return;
throw new Error(`Kapi worktree branch must use a Conventional Commit prefix and kebab-case slug: ${branch}`);
}
Expand Down Expand Up @@ -135,7 +135,7 @@ export async function prepareWorkerWorkspace(
workerWorkspace = request.worktreePath ?? path.join(worktreeRoot, id);
await assertWorktreePathInsideBoundary(worktreeRoot, workerWorkspace);
const branch = request.branchName ?? defaultWorkerBranchName(request, id);
assertKapiOwnedBranchName(branch);
assertWorkflowOwnedBranchName(branch);
assertBranchInsideBoundary(branch, request.branchPrefix);
await assertCleanBaseCheckout(runner, gitRoot.output);
await assertTargetBranchAvailable(runner, gitRoot.output, request.targetBranch);
Expand Down
4 changes: 2 additions & 2 deletions src/domain/run-contract-prompt-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export function renderRunContractPrompt(options: RenderRunContractPromptOptions)
if (layout === "workflow" && options.workerCapabilitySnapshot !== undefined) sections.push(`Worker capability snapshot:\n${options.workerCapabilitySnapshot}`);

const rules = layout === "runtime" ? runtimeRuleLines(options.additionalRuleLines) : workflowRuleLines(options.additionalRuleLines);
sections.push(formatKapiRules(rules, { artifactGuidance: layout === "runtime" ? artifactGuidance : [], resumeNotes: options.resumeNotes }));
sections.push(formatWorkflowRules(rules, { artifactGuidance: layout === "runtime" ? artifactGuidance : [], resumeNotes: options.resumeNotes }));

if (options.summaryState) sections.push(summarizeWorkflow(options.summaryState));
return sections.join("\n\n");
Expand Down Expand Up @@ -88,7 +88,7 @@ function formatSharedLifecycle(options: RenderRunContractPromptOptions, layout:
].join("\n");
}

function formatKapiRules(options: readonly string[], extra: { artifactGuidance: readonly string[]; resumeNotes?: readonly string[] }): string {
function formatWorkflowRules(options: readonly string[], extra: { artifactGuidance: readonly string[]; resumeNotes?: readonly string[] }): string {
const lines = ["Kapi rules:", ...options.map((rule) => `- ${rule}`)];
if (extra.artifactGuidance.length) lines.push("Artifact guidance:", ...extra.artifactGuidance.map((item) => `- ${item}`));
if (extra.resumeNotes?.length) lines.push("", "Resume notes:", ...extra.resumeNotes.map((note) => `- ${note}`));
Expand Down
6 changes: 3 additions & 3 deletions src/domain/workflow-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ function isWorktreePathInsideBoundary(root: string, value: string): boolean {
return Boolean(boundary) && candidate.startsWith(`${boundary}/`);
}

function hasValidKapiBranchPrefix(value: string): boolean {
function hasValidWorkflowBranchPrefix(value: string): boolean {
return /^(feat|fix|perf|refactor|test|docs|chore|build|ci)\/[a-z0-9][a-z0-9-]*$/.test(value);
}

Expand Down Expand Up @@ -336,7 +336,7 @@ export function validateWorkflowState(state: WorkflowState): WorkflowValidationR
if (!collapsePathSegments(state.worktreeBoundary.root)) {
issues.push({ severity: "error", code: "invalid_worktree_boundary_root", message: `Kapi worktree boundary root must be non-empty, got ${state.worktreeBoundary.root}.` });
}
if (!hasValidKapiBranchPrefix(state.worktreeBoundary.branchPrefix)) {
if (!hasValidWorkflowBranchPrefix(state.worktreeBoundary.branchPrefix)) {
issues.push({ severity: "error", code: "invalid_worktree_branch_prefix", message: `Kapi worktree branchPrefix must use a conventional prefix and kebab-case slug: ${state.worktreeBoundary.branchPrefix}.` });
}
}
Expand Down Expand Up @@ -390,7 +390,7 @@ export function validateWorkflowState(state: WorkflowState): WorkflowValidationR
code: "worker_isolated_workspace_missing_branch",
message: `Kapi isolated workspace worker ${worker.id} must record the created branchName handle.`,
});
} else if (!hasValidKapiBranchPrefix(worker.handles.branchName)) {
} else if (!hasValidWorkflowBranchPrefix(worker.handles.branchName)) {
issues.push({
severity: "error",
code: "worker_isolated_workspace_invalid_branch",
Expand Down
10 changes: 10 additions & 0 deletions test/architecture.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@ test("presentation helper exports avoid product-prefixed generic names", async (
assert.match(presentation, /formatWorkflowError/);
});

test("internal adapter and domain helpers avoid product-prefixed generic names", async () => {
const source = [await readTsFiles(path.join(process.cwd(), "src", "adapters")), await readTsFiles(path.join(process.cwd(), "src", "domain"))].join("\n");
assert.doesNotMatch(source, /prepareWritableKapiFile|assertKapiAncestorsAreNotSymlinks|removeKapiOwnedSymlink|isKapiAutoresearchTarget|assertKapiOwnedBranchName|hasValidKapiBranchPrefix|formatKapiRules/);
assert.match(source, /prepareWritableStateFile/);
assert.match(source, /assertStateAncestorsAreNotSymlinks/);
assert.match(source, /assertWorkflowOwnedBranchName/);
assert.match(source, /hasValidWorkflowBranchPrefix/);
assert.match(source, /formatWorkflowRules/);
});

test("domain layer stays independent from Pi, filesystem, tmux, git commands, and process adapters", async () => {
const domain = await readTsFiles(path.join(process.cwd(), "src", "domain"));
assert.doesNotMatch(domain, /@mariozechner/);
Expand Down
Loading