diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e0a4e35..726057bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ ### Added +- #621: Add explicit task collaborators and responsibilities, validated dependency graphs, acceptance-based readiness, scoped delivery context and immutable execution snapshots with live project/review inspection. +- Versioned project task briefings: server-owned goal/task requirements and acceptance criteria enter execution and persist for conversation/review inspection, with stale-plan protection and preserved retry/approval bindings (#619). - #617:接力任务支持可选本步要求,以及与模型输入一致的交接快照;可查看前序结果来源、裁剪和过滤提示,共享现有上下文预算并去除重复来源。新增本地记录字段,降级旧版本前请备份工作区。 ### Added @@ -77,7 +79,6 @@ - #559:新增协作区人工验收契约,验收通过必须完整覆盖计划中的全部验收项;限制标识符、时间戳和产物路径,拒绝遗漏或越界的裁决。 - #469:可选 Laya 接力停止建议。仅在 `ROLEWEAVE_LAYA_ENABLED` 开启、已完成步骤得到正向建议且仍有未执行步骤时暂停;桌面端由 owner 显式选择“停止剩余步骤”或“继续接力”。30 秒超时、SSE 断连、无效或失败建议均继续原 mention order;Laya 只接收 `status`、`errorCode`、`hasOutput`,不接收输入、输出或 handoff 文本。已接受的消息与 spawn 清单保持不可变。Refs #422。 ->>>>>>> origin/main - #360:工作区 `work/` 领地布局。initialize/create 脚手架 `work/README.md`(已有工作区不加强制迁移)。hire 创建 `work//`,仅给 hire 岗位生成 SKILL.md Territory 段,并把新角色的 `memoryScope` 写成 `./work//`;项目负责人保持 `memoryScope: "/"`,SKILL 不声称不存在的 `work//`。默认 `toolAllow` 只有 Read/Grep/Glob,不含 Write/Edit/Bash。hire 任何 staging 失败都回滚声明文件。employee.json 摘要密封机制不变。Refs #360。 diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 19c18472..cc0ff3ab 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -23,7 +23,7 @@ "electron": "43.7.6", "react": "^18.3.1", "react-dom": "^18.3.1", - "vite": "^6.0.0", + "vite": "^6.4.4", "vitest": "4.1.11" }, "dependencies": { diff --git a/apps/desktop/packaging/runtime-layout.cjs b/apps/desktop/packaging/runtime-layout.cjs index 7dc8e709..46d2f2cc 100644 --- a/apps/desktop/packaging/runtime-layout.cjs +++ b/apps/desktop/packaging/runtime-layout.cjs @@ -106,6 +106,7 @@ const SERVER_RUNTIME_FILES = [ "dist/src/experiments/service.js", "dist/src/experiments/store.js", "dist/src/gemini-binary.js", + "dist/src/goals/collaboration.js", "dist/src/goals/store.js", "dist/src/groups/relay-stop.js", "dist/src/groups/store.js", @@ -163,6 +164,7 @@ const SERVER_RUNTIME_FILES = [ "dist/src/stable-read.js", "dist/src/tasks/store.js", "dist/src/turns/budget-remaining-advice.js", + "dist/src/turns/context-snapshot.js", "dist/src/turns/delta-forwarder.js", "dist/src/turns/envelope.js", "dist/src/turns/progress.js", @@ -170,6 +172,7 @@ const SERVER_RUNTIME_FILES = [ "dist/src/turns/response-format.js", "dist/src/turns/running.js", "dist/src/turns/store.js", + "dist/src/turns/task-briefing.js", "dist/src/turns/thread-context.js", "dist/src/vault/attachments.js", "dist/src/vault/context.js", @@ -196,6 +199,7 @@ const SHARED_RUNTIME_FILES = [ "position-id.cjs", "turn-engines.cjs", "relay-steps.cjs", + "task-briefing.cjs", "dist/acceptance.js", "dist/agent-binding.js", "dist/api.js", @@ -230,6 +234,8 @@ const SHARED_RUNTIME_FILES = [ "dist/services.js", "dist/sessions.js", "dist/task-board.js", + "dist/task-briefing.js", + "dist/task-collaboration.js", "dist/tool-output.js", "dist/turn-progress.js", "dist/turns.js", @@ -331,6 +337,12 @@ const APP_RESOURCE_REQUIRED_ENTRIES = [ "node_modules/@roleweave/shared/relay-steps.cjs", "node_modules/@roleweave/shared/dist/relay-steps.js", "apps/server/dist/src/turns/relay-handoff.js", + "apps/server/dist/src/turns/context-snapshot.js", + "apps/server/dist/src/turns/task-briefing.js", + "apps/server/dist/src/goals/collaboration.js", + "node_modules/@roleweave/shared/dist/task-collaboration.js", + "node_modules/@roleweave/shared/task-briefing.cjs", + "node_modules/@roleweave/shared/dist/task-briefing.js", "examples/oss-maintainer/workspace.json", "examples/oss-maintainer/organization.v1alpha1.json", ]; diff --git a/apps/desktop/renderer/src/goals/ProjectBoard.tsx b/apps/desktop/renderer/src/goals/ProjectBoard.tsx index 2e9aec50..215f8f4b 100644 --- a/apps/desktop/renderer/src/goals/ProjectBoard.tsx +++ b/apps/desktop/renderer/src/goals/ProjectBoard.tsx @@ -18,6 +18,9 @@ import { useEngineLabel } from "../turns/engine-select"; import { TaskAcceptanceDrawer } from "./TaskAcceptanceDrawer"; import { ProjectTaskProgress } from "./ProjectTaskProgress"; import { useProjectTaskProgress } from "./useProjectTaskProgress"; +import { taskDependenciesReady, taskParticipantError, taskDependencyGraphError } from "@roleweave/shared/task-collaboration"; +import { TaskCollaborationEditor } from "./TaskCollaborationEditor"; +import { TaskCollaborationDetails } from "./TaskCollaborationDetails"; import "./project-board.css"; export interface ProjectBoardProps { @@ -188,6 +191,16 @@ export function ProjectBoard({ !!own(accepted, item.taskId) || !!own(detail.taskExecutions, item.taskId) || !!own(detail.taskDeliveries, item.taskId) || !!own(detail.taskDeliveryUnavailable, item.taskId) || !!detail.executionUnavailable || !!detail.goal.taskAcceptances?.some((record) => record.taskId === item.taskId); + const dependenciesReady = (item: GoalWorkItem) => { + if (!item.dependsOn?.length) return true; + const context = own(detail.taskCollaborations, item.taskId); + return !!context && taskDependenciesReady(context); + }; + const relations = (item: GoalWorkItem) => { + const context = own(detail.taskCollaborations, item.taskId); + return context && (context.participants.length > 0 || context.dependencies.length > 0 || context.dependents.length > 0 || context.omittedDependentCount > 0) + ? : null; + }; const openAcceptance = (item: GoalWorkItem) => setAcceptanceTask({ item: { ...item }, scopeKey }); const acceptanceItem = acceptanceTask?.scopeKey === scopeKey @@ -286,8 +299,11 @@ export function ProjectBoard({ setEditor((current) => current ? { ...current, item: { ...current.item, ...fields } } : current, ); + const relationError = editor ? taskParticipantError(editor.item.participants ?? [], editor.item.assigneePositionId) || + taskDependencyGraphError([...items.filter(item => item.taskId !== editor.item.taskId), editor.item]) : null; const validEditor = !!editor?.item.title.trim() && + !relationError && validDate(editor.item.startDate) && validDate(editor.item.dueDate) && !( @@ -316,6 +332,8 @@ export function ProjectBoard({ : {}), ...(draft.startDate ? { startDate: draft.startDate } : {}), ...(draft.dueDate ? { dueDate: draft.dueDate } : {}), + ...(draft.participants !== undefined ? { participants: draft.participants.map(p => ({ ...p, responsibility: p.responsibility.trim() })) } : {}), + ...(draft.dependsOn !== undefined ? { dependsOn: draft.dependsOn } : {}), }; void save( editor.isNew @@ -331,6 +349,10 @@ export function ProjectBoard({ const deleteTask = async () => { if (!editor || editor.isNew || busy) return; const item = editor.item; + if (items.some(other => other.dependsOn?.includes(item.taskId))) { + setError(t("taskCollaboration.referencedDelete")); + return; + } if ( !window.confirm( t("project.deleteTaskConfirm", { title: item.title }), @@ -357,6 +379,8 @@ export function ProjectBoard({ const isCurrent = () => alive.current && scopeRef.current === launchScope; if ( !positionId || + !workspaceKey || + !dependenciesReady(item) || !engine || launchLocks.current.has(lockKey) || executionOf(item)?.status === "running" || @@ -394,6 +418,7 @@ export function ProjectBoard({ input: [item.title, item.description].filter(Boolean).join("\n\n"), goalId: detail.goal.goalId, branchId: item.taskId, + taskBriefing: { expectedWorkspacePath: workspaceKey, expectedUpdatedAt: detail.goal.updatedAt }, }); if (!isCurrent()) return; if ( @@ -401,7 +426,8 @@ export function ProjectBoard({ result.status !== 201 && result.status !== 202 ) - throw new Error(responseError(result.body, t("project.runError"))); + throw new Error(result.body && typeof result.body === "object" && "code" in result.body && result.body.code === "goal_dependency_blocked" + ? t("taskCollaboration.blocked") : result.status === 409 ? t("taskBriefing.launchConflict") : responseError(result.body, t("project.runError"))); setAccepted((current) => ({ ...current, [item.taskId]: { @@ -552,7 +578,7 @@ export function ProjectBoard({ !!own(launching, item.taskId) || running || !engine || - !!detail.executionUnavailable + !!detail.executionUnavailable || !workspaceKey || !dependenciesReady(item) } onClick={() => void launch(item)} > @@ -560,6 +586,8 @@ export function ProjectBoard({ {acceptanceEntry(item)} + {relations(item)} + {!dependenciesReady(item) &&

{t("taskCollaboration.blocked")}

} {item.assigneePositionId && !engine && (

{t("project.configureEngine")} @@ -862,6 +890,7 @@ export function ProjectBoard({ {label} {execution(item)} {acceptanceEntry(item)} + {relations(item)}

+ + {relationError &&

{t("taskCollaboration.invalid")}

}