Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ describe("CheckpointDiffQueryLive", () => {
getThreadCheckpointContext: () => Effect.succeed(Option.some(threadCheckpointContext)),
getThreadShellById: () => Effect.succeed(Option.none()),
getThreadDetailById: () => Effect.succeed(Option.none()),
listAllTags: () => Effect.succeed([]),
getTagById: () => Effect.succeed(Option.none()),
}),
),
);
Expand Down Expand Up @@ -150,6 +152,8 @@ describe("CheckpointDiffQueryLive", () => {
getThreadCheckpointContext: () => Effect.succeed(Option.none()),
getThreadShellById: () => Effect.succeed(Option.none()),
getThreadDetailById: () => Effect.succeed(Option.none()),
listAllTags: () => Effect.succeed([]),
getTagById: () => Effect.succeed(Option.none()),
}),
),
);
Expand Down
7 changes: 2 additions & 5 deletions apps/server/src/git/Prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,7 @@ export function buildCommitMessagePrompt(input: CommitMessagePromptInput) {
"- subject must be imperative, <= 72 chars, and no trailing period",
"- body can be empty string or short bullet points",
...(wantsBranch
? [
"- branch must be a short semantic git branch fragment for this change",
]
? ["- branch must be a short semantic git branch fragment for this change"]
: []),
"- capture the primary user-visible or developer-visible change",
"",
Expand Down Expand Up @@ -136,8 +134,7 @@ interface PromptFromMessageInput {

function buildPromptFromMessage(input: PromptFromMessageInput): string {
const attachmentLines = (input.attachments ?? []).map(
(attachment) =>
`- ${attachment.name} (${attachment.mimeType}, ${attachment.sizeBytes} bytes)`,
(attachment) => `- ${attachment.name} (${attachment.mimeType}, ${attachment.sizeBytes} bytes)`,
);

const promptSections = [
Expand Down
4 changes: 3 additions & 1 deletion apps/server/src/open.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,9 @@ export const launchDetached = (launch: EditorLaunch) =>
const handleError = (cause: Error) => {
if (!settled) {
settled = true;
resume(Effect.fail(new OpenError({ message: "failed to spawn detached process", cause })));
resume(
Effect.fail(new OpenError({ message: "failed to spawn detached process", cause })),
);
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ describe("OrchestrationEngine", () => {
const projectionSnapshot = {
snapshotSequence: 7,
updatedAt: "2026-03-03T00:00:04.000Z",
tags: [],
projects: [
{
id: asProjectId("project-bootstrap"),
Expand All @@ -108,6 +109,7 @@ describe("OrchestrationEngine", () => {
model: "gpt-5-codex",
},
scripts: [],
tags: [],
createdAt: "2026-03-03T00:00:00.000Z",
updatedAt: "2026-03-03T00:00:01.000Z",
deletedAt: null,
Expand Down Expand Up @@ -149,6 +151,7 @@ describe("OrchestrationEngine", () => {
snapshotSequence: projectionSnapshot.snapshotSequence,
projects: [],
threads: [],
tags: [],
updatedAt: projectionSnapshot.updatedAt,
}),
getCounts: () => Effect.succeed({ projectCount: 1, threadCount: 1 }),
Expand All @@ -158,6 +161,8 @@ describe("OrchestrationEngine", () => {
getThreadCheckpointContext: () => Effect.succeed(Option.none()),
getThreadShellById: () => Effect.succeed(Option.none()),
getThreadDetailById: () => Effect.succeed(Option.none()),
listAllTags: () => Effect.succeed([]),
getTagById: () => Effect.succeed(Option.none()),
}),
),
Layer.provide(
Expand Down
12 changes: 10 additions & 2 deletions apps/server/src/orchestration/Layers/OrchestrationEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {
OrchestrationEvent,
OrchestrationReadModel,
ProjectId,
TagId,
ThreadId,
} from "@t3tools/contracts";
import { OrchestrationCommand } from "@t3tools/contracts";
Expand Down Expand Up @@ -51,8 +52,8 @@ interface CommandEnvelope {
}

function commandToAggregateRef(command: OrchestrationCommand): {
readonly aggregateKind: "project" | "thread";
readonly aggregateId: ProjectId | ThreadId;
readonly aggregateKind: "project" | "thread" | "tag";
readonly aggregateId: ProjectId | ThreadId | TagId;
} {
switch (command.type) {
case "project.create":
Expand All @@ -62,6 +63,13 @@ function commandToAggregateRef(command: OrchestrationCommand): {
aggregateKind: "project",
aggregateId: command.projectId,
};
case "tag.create":
case "tag.rename":
case "tag.delete":
return {
aggregateKind: "tag",
aggregateId: command.tagId,
};
default:
return {
aggregateKind: "thread",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ it.layer(BaseTestLayer)("OrchestrationProjectionPipeline", (it) => {
workspaceRoot: "/tmp/project-1",
defaultModelSelection: null,
scripts: [],
tags: [],
createdAt: now,
updatedAt: now,
},
Expand Down Expand Up @@ -344,6 +345,7 @@ it.layer(BaseTestLayer)("OrchestrationProjectionPipeline", (it) => {
workspaceRoot: "/tmp/project-clear-attachments",
defaultModelSelection: null,
scripts: [],
tags: [],
createdAt: now,
updatedAt: now,
},
Expand Down Expand Up @@ -472,6 +474,7 @@ it.layer(
workspaceRoot: "/tmp/project-overwrite",
defaultModelSelection: null,
scripts: [],
tags: [],
createdAt: now,
updatedAt: now,
},
Expand Down Expand Up @@ -620,6 +623,7 @@ it.layer(
workspaceRoot: "/tmp/project-rollback",
defaultModelSelection: null,
scripts: [],
tags: [],
createdAt: now,
updatedAt: now,
},
Expand Down Expand Up @@ -749,6 +753,7 @@ it.layer(
workspaceRoot: "/tmp/project-revert-files",
defaultModelSelection: null,
scripts: [],
tags: [],
createdAt: now,
updatedAt: now,
},
Expand Down Expand Up @@ -957,6 +962,7 @@ it.layer(Layer.fresh(makeProjectionPipelinePrefixedTestLayer("t3-projection-atta
workspaceRoot: "/tmp/project-delete-files",
defaultModelSelection: null,
scripts: [],
tags: [],
createdAt: now,
updatedAt: now,
},
Expand Down Expand Up @@ -1120,6 +1126,7 @@ it.layer(BaseTestLayer)("OrchestrationProjectionPipeline", (it) => {
workspaceRoot: "/tmp/project-a",
defaultModelSelection: null,
scripts: [],
tags: [],
createdAt: now,
updatedAt: now,
},
Expand Down Expand Up @@ -1247,6 +1254,7 @@ it.layer(BaseTestLayer)("OrchestrationProjectionPipeline", (it) => {
workspaceRoot: "/tmp/project-empty",
defaultModelSelection: null,
scripts: [],
tags: [],
createdAt: now,
updatedAt: now,
},
Expand Down
53 changes: 53 additions & 0 deletions apps/server/src/orchestration/Layers/ProjectionPipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { OrchestrationEventStore } from "../../persistence/Services/Orchestratio
import { ProjectionPendingApprovalRepository } from "../../persistence/Services/ProjectionPendingApprovals.ts";
import { ProjectionProjectRepository } from "../../persistence/Services/ProjectionProjects.ts";
import { ProjectionStateRepository } from "../../persistence/Services/ProjectionState.ts";
import { ProjectionTagRepository } from "../../persistence/Services/ProjectionTags.ts";
import { ProjectionThreadActivityRepository } from "../../persistence/Services/ProjectionThreadActivities.ts";
import { type ProjectionThreadActivity } from "../../persistence/Services/ProjectionThreadActivities.ts";
import {
Expand All @@ -31,13 +32,15 @@ import { ProjectionThreadRepository } from "../../persistence/Services/Projectio
import { ProjectionPendingApprovalRepositoryLive } from "../../persistence/Layers/ProjectionPendingApprovals.ts";
import { ProjectionProjectRepositoryLive } from "../../persistence/Layers/ProjectionProjects.ts";
import { ProjectionStateRepositoryLive } from "../../persistence/Layers/ProjectionState.ts";
import { ProjectionTagRepositoryLive } from "../../persistence/Layers/ProjectionTags.ts";
import { ProjectionThreadActivityRepositoryLive } from "../../persistence/Layers/ProjectionThreadActivities.ts";
import { ProjectionThreadMessageRepositoryLive } from "../../persistence/Layers/ProjectionThreadMessages.ts";
import { ProjectionThreadProposedPlanRepositoryLive } from "../../persistence/Layers/ProjectionThreadProposedPlans.ts";
import { ProjectionThreadSessionRepositoryLive } from "../../persistence/Layers/ProjectionThreadSessions.ts";
import { ProjectionTurnRepositoryLive } from "../../persistence/Layers/ProjectionTurns.ts";
import { ProjectionThreadRepositoryLive } from "../../persistence/Layers/ProjectionThreads.ts";
import { ServerConfig } from "../../config.ts";
import { normalizeTagNameForDedup } from "../commandInvariants.ts";
import {
OrchestrationProjectionPipeline,
type OrchestrationProjectionPipelineShape,
Expand All @@ -59,6 +62,7 @@ export const ORCHESTRATION_PROJECTOR_NAMES = {
threadTurns: "projection.thread-turns",
checkpoints: "projection.checkpoints",
pendingApprovals: "projection.pending-approvals",
tags: "projection.tags",
} as const;

type ProjectorName =
Expand Down Expand Up @@ -444,6 +448,7 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
const eventStore = yield* OrchestrationEventStore;
const projectionStateRepository = yield* ProjectionStateRepository;
const projectionProjectRepository = yield* ProjectionProjectRepository;
const projectionTagRepository = yield* ProjectionTagRepository;
const projectionThreadRepository = yield* ProjectionThreadRepository;
const projectionThreadMessageRepository = yield* ProjectionThreadMessageRepository;
const projectionThreadProposedPlanRepository = yield* ProjectionThreadProposedPlanRepository;
Expand All @@ -467,6 +472,7 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
workspaceRoot: event.payload.workspaceRoot,
defaultModelSelection: event.payload.defaultModelSelection,
scripts: event.payload.scripts,
tags: event.payload.tags,
createdAt: event.payload.createdAt,
updatedAt: event.payload.updatedAt,
deletedAt: null,
Expand All @@ -490,6 +496,7 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
? { defaultModelSelection: event.payload.defaultModelSelection }
: {}),
...(event.payload.scripts !== undefined ? { scripts: event.payload.scripts } : {}),
...(event.payload.tags !== undefined ? { tags: event.payload.tags } : {}),
updatedAt: event.payload.updatedAt,
});
return;
Expand All @@ -515,6 +522,47 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
}
});

const applyTagsProjection: ProjectorDefinition["apply"] = Effect.fn("applyTagsProjection")(
function* (event, _attachmentSideEffects) {
switch (event.type) {
case "tag.created":
yield* projectionTagRepository.upsert({
tagId: event.payload.tagId,
name: event.payload.name,
nameNormalized: normalizeTagNameForDedup(event.payload.name),
createdAt: event.payload.createdAt,
updatedAt: event.payload.updatedAt,
});
return;

case "tag.renamed": {
const existingRow = yield* projectionTagRepository.getById({
tagId: event.payload.tagId,
});
if (Option.isNone(existingRow)) {
return;
}
yield* projectionTagRepository.upsert({
...existingRow.value,
name: event.payload.name,
nameNormalized: normalizeTagNameForDedup(event.payload.name),
updatedAt: event.payload.updatedAt,
});
return;
}

case "tag.deleted":
yield* projectionTagRepository.deleteById({
tagId: event.payload.tagId,
});
return;

default:
return;
}
},
);

const refreshThreadShellSummary = Effect.fn("refreshThreadShellSummary")(function* (
threadId: ThreadId,
) {
Expand Down Expand Up @@ -1371,6 +1419,10 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
name: ORCHESTRATION_PROJECTOR_NAMES.threads,
apply: applyThreadsProjection,
},
{
name: ORCHESTRATION_PROJECTOR_NAMES.tags,
apply: applyTagsProjection,
},
];

const runProjectorForEvent = Effect.fn("runProjectorForEvent")(function* (
Expand Down Expand Up @@ -1474,4 +1526,5 @@ export const OrchestrationProjectionPipelineLive = Layer.effect(
Layer.provideMerge(ProjectionTurnRepositoryLive),
Layer.provideMerge(ProjectionPendingApprovalRepositoryLive),
Layer.provideMerge(ProjectionStateRepositoryLive),
Layer.provideMerge(ProjectionTagRepositoryLive),
);
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => {
runOnWorktreeCreate: false,
},
],
tags: [],
createdAt: "2026-02-24T00:00:00.000Z",
updatedAt: "2026-02-24T00:00:01.000Z",
deletedAt: null,
Expand Down Expand Up @@ -375,6 +376,7 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => {
runOnWorktreeCreate: false,
},
],
tags: [],
createdAt: "2026-02-24T00:00:00.000Z",
updatedAt: "2026-02-24T00:00:01.000Z",
},
Expand Down
Loading
Loading