diff --git a/src/tools/docShared.ts b/src/tools/docShared.ts index 2811b9d..c97409d 100644 --- a/src/tools/docShared.ts +++ b/src/tools/docShared.ts @@ -162,13 +162,29 @@ export async function nextDocOrder( export async function touchDocForEdit( docRef: DocumentReference, currentStatus: unknown, + change?: { + actor?: string; + action?: string; + blockId?: string; + summary?: string; + }, ): Promise { + const now = FieldValue.serverTimestamp(); const patch: Record = { revision: FieldValue.increment(1), - updatedAt: FieldValue.serverTimestamp(), + updatedAt: now, }; if (currentStatus === "draft" || currentStatus == null) { patch.status = "under_discussion"; } - await docRef.set(patch, { merge: true }); + const batch = docRef.firestore.batch(); + batch.set(docRef, patch, { merge: true }); + batch.set(docRef.collection("revisions").doc(), { + actor: change?.actor ?? "agent", + action: change?.action ?? "doc_edited", + blockId: change?.blockId ?? null, + summary: change?.summary ?? null, + createdAt: now, + }); + await batch.commit(); } diff --git a/src/tools/proposePlan.ts b/src/tools/proposePlan.ts index 65c921e..64b8032 100644 --- a/src/tools/proposePlan.ts +++ b/src/tools/proposePlan.ts @@ -43,6 +43,14 @@ export const proposePlan: Tool = { }; } const { docRef } = refs; + const current = await docRef.get(); + if (current.data()?.status === "materialized") { + return { + ok: false, + errorClass: "BAD_INPUT", + message: "This plan is already approved and materialized; it cannot be proposed again.", + }; + } const blocksSnap = await docRef.collection("blocks").get(); let groups = 0; @@ -85,6 +93,13 @@ export const proposePlan: Tool = { ); await batch.commit(); + await docRef.collection("revisions").doc().set({ + actor: ctx.convId ?? "agent", + action: "plan_proposed", + summary: `Proposed ${tasks} task${tasks === 1 ? "" : "s"} across ${groups} group${groups === 1 ? "" : "s"}.`, + createdAt: FieldValue.serverTimestamp(), + }); + // Notify the team in THIS doc's discussion. await docRef.collection("messages").doc().set({ from: "agent", diff --git a/src/tools/upsertPlanGroup.ts b/src/tools/upsertPlanGroup.ts index 6e322f7..3dd53ad 100644 --- a/src/tools/upsertPlanGroup.ts +++ b/src/tools/upsertPlanGroup.ts @@ -53,6 +53,13 @@ export const upsertPlanGroup: Tool = { const { docRef } = refs; const docSnap = await docRef.get(); const status = (docSnap.data() as Record)?.status; + if (status === "materialized") { + return { + ok: false, + errorClass: "BAD_INPUT", + message: "This plan is already materialized. Discuss changes in the doc instead of rewriting the approved task structure.", + }; + } const blocksCol = docRef.collection("blocks"); const groupRef = args.groupId @@ -91,7 +98,12 @@ export const upsertPlanGroup: Tool = { { merge: true }, ); - await touchDocForEdit(docRef, status); + await touchDocForEdit(docRef, status, { + actor: ctx.convId, + action: existing && existing.exists ? "plan_group_updated" : "plan_group_created", + blockId: groupRef.id, + summary: args.title, + }); return { ok: true, data: { docId: refs.docId, groupId: groupRef.id } }; }, diff --git a/src/tools/upsertPlanTask.ts b/src/tools/upsertPlanTask.ts index 7280855..0adf632 100644 --- a/src/tools/upsertPlanTask.ts +++ b/src/tools/upsertPlanTask.ts @@ -61,9 +61,32 @@ export const upsertPlanTask: Tool = { const { docRef } = refs; const docSnap = await docRef.get(); const status = (docSnap.data() as Record)?.status; + if (status === "materialized") { + return { + ok: false, + errorClass: "BAD_INPUT", + message: "This plan is already materialized. Do not add or rewrite approved tasks.", + }; + } const blocksCol = docRef.collection("blocks"); + if (args.suggestedAgent) { + const project = await docRef.parent.parent!.get(); + const roster = Array.isArray(project.data()?.agentIds) + ? (project.data()?.agentIds as unknown[]).filter( + (name): name is string => typeof name === "string" && name.length > 0, + ) + : []; + if (!roster.includes(args.suggestedAgent)) { + return { + ok: false, + errorClass: "BAD_INPUT", + message: `Unknown suggestedAgent "${args.suggestedAgent}". Use one of the project's exact agent names: ${roster.join(", ") || "(no agents assigned)"}.`, + }; + } + } + // The referenced group must exist and be a planGroup. const groupSnap = await blocksCol.doc(args.groupId).get(); if (!groupSnap.exists || (groupSnap.data() as Record).type !== "planGroup") { @@ -111,7 +134,12 @@ export const upsertPlanTask: Tool = { { merge: true }, ); - await touchDocForEdit(docRef, status); + await touchDocForEdit(docRef, status, { + actor: ctx.convId, + action: existing && existing.exists ? "plan_task_updated" : "plan_task_created", + blockId: taskRef.id, + summary: args.title, + }); return { ok: true, data: { docId: refs.docId, taskId: taskRef.id } }; },