Files
buzz/desktop/src/features/projects/projectChannelCreation.ts
thomaspblock de188ebf06 feat(projects): add agent and CLI project-home support (#6590)
## Summary
- inject bounded project-home identity and repository context into
managed agent sessions
- add project-aware CLI flows for creating projects, repositories,
issues, and related channels
- match project homes through the existing relay query surface, then
filter channel metadata client-side

This is Part 1 of the channel-first Projects stack. Part 2 contains
project creation and model foundations.

## Testing
- `cargo fmt --all -- --check`
- `cargo clippy -p buzz-cli -p buzz-acp --all-targets -- -D warnings`
- `cargo test -p buzz-cli -p buzz-acp` — 1,184 tests passed, 1 doc test
ignored
- full pre-push gate passed

## Post-Deploy Monitoring & Validation
- validate project-home agent context and project-aware CLI commands
against a staging relay
- healthy signals: project context matches the active channel, explicit
repo coordinates remain stable, and normal channels receive no project
block
- failure signals: cross-channel project context, duplicate project
creation, or commands targeting an unrelated repository; mitigate by
reverting this PR

---------

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
Signed-off-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
Co-authored-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
2026-08-26 12:09:06 -07:00

72 lines
2.3 KiB
TypeScript

import type { RelayEvent } from "@/shared/api/types";
import { KIND_PROJECT_ANNOUNCEMENT } from "@/shared/constants/kinds";
import {
isValidProjectChannelId,
MAX_PROJECT_RELATED_CHANNELS,
PROJECT_RELATED_CHANNEL_TAG,
validateProjectEventEnvelope,
} from "@/features/projects/projectModels";
import type { ProjectEventTemplate } from "./projectCreation";
/**
* Appends a `buzz-related-channel` tag to a live project head. Every other
* tag is preserved so adding a stream cannot erase unknown metadata.
*/
export function buildProjectRelatedChannelPatchTemplate({
channelId,
liveHead,
ownerPubkey,
}: {
channelId: string;
liveHead: RelayEvent;
ownerPubkey: string;
}): { alreadyBound: boolean; project: ProjectEventTemplate } {
const normalizedOwner = ownerPubkey.trim().toLowerCase();
if (normalizedOwner !== liveHead.pubkey.toLowerCase()) {
throw new Error("Only the project owner can add channels.");
}
const normalizedChannelId = channelId.trim();
if (!isValidProjectChannelId(normalizedChannelId)) {
throw new Error("Project channel is invalid.");
}
const homeChannelId = liveHead.tags.find(
(tag) => tag[0] === "buzz-channel",
)?.[1];
if (homeChannelId === normalizedChannelId) {
throw new Error("That channel is already this project's home.");
}
const existingRelated = liveHead.tags
.filter((tag) => tag[0] === PROJECT_RELATED_CHANNEL_TAG)
.map((tag) => tag[1])
.filter((value): value is string => Boolean(value));
if (existingRelated.includes(normalizedChannelId)) {
validateProjectEventEnvelope(liveHead.tags, liveHead.content);
return {
alreadyBound: true,
project: {
kind: KIND_PROJECT_ANNOUNCEMENT,
content: liveHead.content,
tags: liveHead.tags.map((tag) => [...tag]),
},
};
}
if (existingRelated.length >= MAX_PROJECT_RELATED_CHANNELS) {
throw new Error(
`A project cannot contain more than ${MAX_PROJECT_RELATED_CHANNELS} extra channels.`,
);
}
const tags = [
...liveHead.tags.map((tag) => [...tag]),
[PROJECT_RELATED_CHANNEL_TAG, normalizedChannelId],
];
validateProjectEventEnvelope(tags, liveHead.content);
return {
alreadyBound: false,
project: {
kind: KIND_PROJECT_ANNOUNCEMENT,
content: liveHead.content,
tags,
},
};
}