完成M1D-1策划状态与审批卡
Project CI / Backend tests (pull_request) Successful in 4m1s
Project CI / Repository checks (pull_request) Failing after 1m0s
Project CI / Frontend tests (pull_request) Successful in 3m2s
Project CI / Native shell tests (pull_request) Failing after 11m50s

新增严格输入的策划状态 hydrate command 与 plan-gdd-state-view.v1 read model

接入 GDD 审批卡、正文详情弹层、决定幂等与恢复重试

补充 M1D-1 开发日志和技术方案状态
This commit is contained in:
2026-08-18 07:56:20 +00:00
parent 14c00017c6
commit 0052a80dad
11 changed files with 1373 additions and 2 deletions
@@ -10,6 +10,7 @@ mod json_sidecar;
mod models; mod models;
mod planning_approval; mod planning_approval;
mod planning_coordinator; mod planning_coordinator;
mod planning_hydrate;
mod planning_provider_usage; mod planning_provider_usage;
mod planning_storage; mod planning_storage;
mod planning_submit; mod planning_submit;
@@ -28,6 +29,7 @@ pub(in crate::agent) use json_sidecar::*;
pub(in crate::agent) use models::*; pub(in crate::agent) use models::*;
pub(crate) use planning_approval::*; pub(crate) use planning_approval::*;
pub(crate) use planning_coordinator::*; pub(crate) use planning_coordinator::*;
pub(crate) use planning_hydrate::*;
pub(crate) use planning_provider_usage::*; pub(crate) use planning_provider_usage::*;
pub(crate) use planning_storage::*; pub(crate) use planning_storage::*;
pub(crate) use planning_submit::*; pub(crate) use planning_submit::*;
@@ -1,5 +1,23 @@
use super::*; use super::*;
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct HydratePlanGddStateInput {
project_path: String,
}
fn parse_hydrate_plan_gdd_state_input(
request: &tauri::ipc::Request<'_>,
) -> Result<HydratePlanGddStateInput, String> {
match request.body() {
tauri::ipc::InvokeBody::Json(value) => serde_json::from_value(value.clone())
.map_err(|error| format!("hydrate_game_creator_plan_gdd_state 输入无效:{error}")),
tauri::ipc::InvokeBody::Raw(_) => {
Err("hydrate_game_creator_plan_gdd_state 只接受 JSON 输入 {projectPath}".to_string())
}
}
}
const AUTOMATIC_PROJECTS_DIRECTORY_NAME: &str = "Genarrative GameAgent"; const AUTOMATIC_PROJECTS_DIRECTORY_NAME: &str = "Genarrative GameAgent";
fn automatic_local_game_projects_root(app: &tauri::AppHandle) -> Result<PathBuf, String> { fn automatic_local_game_projects_root(app: &tauri::AppHandle) -> Result<PathBuf, String> {
@@ -1248,6 +1266,16 @@ pub(crate) fn decide_game_creator_plan_gdd(
Ok(result) Ok(result)
} }
#[tauri::command]
pub(crate) fn hydrate_game_creator_plan_gdd_state(
request: tauri::ipc::Request<'_>,
) -> Result<PlanGddStateViewV1, String> {
let input = parse_hydrate_plan_gdd_state_input(&request)?;
let root = validated_local_project_directory_path(input.project_path.trim())?;
enforce_project_permission_policy(&root, "conversation.read")?;
hydrate_game_creator_plan_gdd_state_at(&root).map_err(|error| error.to_string())
}
#[tauri::command] #[tauri::command]
pub(crate) fn read_game_creator_agent_runtime( pub(crate) fn read_game_creator_agent_runtime(
project_path: String, project_path: String,
@@ -2209,6 +2209,7 @@ fn main() {
reject_game_creator_agent_runtime_task, reject_game_creator_agent_runtime_task,
answer_game_creator_agent_runtime_user_input, answer_game_creator_agent_runtime_user_input,
decide_game_creator_plan_gdd, decide_game_creator_plan_gdd,
hydrate_game_creator_plan_gdd_state,
read_game_creator_agent_runtime, read_game_creator_agent_runtime,
read_game_creator_agent_runtimes, read_game_creator_agent_runtimes,
resume_game_creator_agent_runtime_tasks, resume_game_creator_agent_runtime_tasks,
+135
View File
@@ -81,6 +81,8 @@ import type {
MemoryScope, MemoryScope,
MemoryWriteMode, MemoryWriteMode,
OpenCanvasProjectResult, OpenCanvasProjectResult,
PlanGddDecisionAction,
PlanGddStateViewV1,
PendingCommand, PendingCommand,
PendingUiConfirmation, PendingUiConfirmation,
ProjectPermissionPolicy, ProjectPermissionPolicy,
@@ -602,6 +604,7 @@ export function App({
: { status }, : { status },
); );
} }
const [chatInput, setChatInput] = useState(() => const [chatInput, setChatInput] = useState(() =>
(supervisorChatOnly || gameChatOnly) && initialProjectPath (supervisorChatOnly || gameChatOnly) && initialProjectPath
? readSupervisorChatDraft(initialProjectPath) ? readSupervisorChatDraft(initialProjectPath)
@@ -617,6 +620,133 @@ export function App({
useState<AgentRuntimeResponseStream | null>(null); useState<AgentRuntimeResponseStream | null>(null);
const [projectSupervisorRuntimeError, setProjectSupervisorRuntimeError] = const [projectSupervisorRuntimeError, setProjectSupervisorRuntimeError] =
useState(''); useState('');
const [planGddState, setPlanGddState] = useState<PlanGddStateViewV1 | null>(
null,
);
const planGddStateRef = useRef<PlanGddStateViewV1 | null>(null);
planGddStateRef.current = planGddState;
const planGddHydrateSequenceRef = useRef(0);
const [planGddHydrateBusy, setPlanGddHydrateBusy] = useState(false);
const [planGddDecisionBusy, setPlanGddDecisionBusy] = useState(false);
const [planGddError, setPlanGddError] = useState<string | null>(null);
const planGddDecisionResponseIdsRef = useRef(new Map<string, string>());
const hydratePlanGddState = useCallback(
async (nextProjectPath?: string) => {
const targetProjectPath =
nextProjectPath?.trim() || localProjectPathRef.current || projectPath;
const invoke = resolveTauriInvoke();
if (!invoke || !targetProjectPath.trim()) {
setPlanGddState(null);
return;
}
const requestSequence = ++planGddHydrateSequenceRef.current;
setPlanGddHydrateBusy(true);
setPlanGddError(null);
try {
const nextState = await invoke<PlanGddStateViewV1>(
'hydrate_game_creator_plan_gdd_state',
{ projectPath: targetProjectPath },
);
if (
requestSequence === planGddHydrateSequenceRef.current &&
localProjectPathRef.current === targetProjectPath
) {
setPlanGddState(nextState);
}
} catch (error) {
if (
requestSequence === planGddHydrateSequenceRef.current &&
localProjectPathRef.current === targetProjectPath
) {
setPlanGddError(String(error));
}
} finally {
if (requestSequence === planGddHydrateSequenceRef.current) {
setPlanGddHydrateBusy(false);
}
}
},
[projectPath],
);
const decidePlanGdd = useCallback(
async (action: PlanGddDecisionAction, comment: string | null) => {
const current = planGddStateRef.current;
const pending = current?.pendingApproval;
const targetProjectPath =
resolveChatProjectPath(localProject) ?? projectPath;
const invoke = resolveTauriInvoke();
if (!pending || !current?.displayGdd || !invoke || !targetProjectPath) {
throw new Error('当前没有可提交的 GDD 审批决定');
}
const responseKey = `${pending.approvalRequestId}:${action}`;
const responseId =
planGddDecisionResponseIdsRef.current.get(responseKey) ??
`gdd-response-${crypto.randomUUID()}`;
planGddDecisionResponseIdsRef.current.set(responseKey, responseId);
setPlanGddDecisionBusy(true);
setPlanGddError(null);
try {
await invoke('decide_game_creator_plan_gdd', {
projectPath: targetProjectPath,
gddId: pending.gddRef.gddId,
version: pending.gddRef.version,
fingerprint: pending.gddRef.fingerprint,
pendingActionId: pending.pendingActionId,
approvalRequestId: pending.approvalRequestId,
responseId,
action,
comment,
});
await hydratePlanGddState(targetProjectPath);
} catch (error) {
setPlanGddError(String(error));
throw error;
} finally {
setPlanGddDecisionBusy(false);
}
},
[hydratePlanGddState, localProject, projectPath],
);
useEffect(() => {
const targetProjectPath = localProject?.projectPath;
if (!targetProjectPath) {
setPlanGddState(null);
setPlanGddError(null);
return;
}
void hydratePlanGddState(targetProjectPath);
}, [hydratePlanGddState, localProject?.projectPath]);
useEffect(() => {
if (!localProject?.projectPath || !projectSupervisorRuntime) {
return;
}
void hydratePlanGddState(localProject.projectPath);
}, [
hydratePlanGddState,
localProject?.projectPath,
projectSupervisorRuntime?.phase,
projectSupervisorRuntime?.status,
projectSupervisorRuntime?.updatedAt,
]);
useEffect(() => {
const hydrateOnResume = () => {
if (document.visibilityState === 'hidden' || !localProject?.projectPath) {
return;
}
void hydratePlanGddState(localProject.projectPath);
};
window.addEventListener('focus', hydrateOnResume);
document.addEventListener('visibilitychange', hydrateOnResume);
return () => {
window.removeEventListener('focus', hydrateOnResume);
document.removeEventListener('visibilitychange', hydrateOnResume);
};
}, [hydratePlanGddState, localProject?.projectPath]);
const [projectSupervisorExpectedRunId, setProjectSupervisorExpectedRunId] = const [projectSupervisorExpectedRunId, setProjectSupervisorExpectedRunId] =
useState<string | null>(null); useState<string | null>(null);
const chatInputRef = useRef<HTMLInputElement | null>(null); const chatInputRef = useRef<HTMLInputElement | null>(null);
@@ -11278,6 +11408,11 @@ export function App({
projectSupervisorRuntime={projectSupervisorRuntime} projectSupervisorRuntime={projectSupervisorRuntime}
projectSupervisorRuntimeError={projectSupervisorRuntimeError} projectSupervisorRuntimeError={projectSupervisorRuntimeError}
projectSupervisorTransientReply={projectSupervisorTransientReply} projectSupervisorTransientReply={projectSupervisorTransientReply}
planGddState={planGddState}
planGddHydrateBusy={planGddHydrateBusy || planGddDecisionBusy}
planGddError={planGddError}
onPlanGddRefresh={() => void hydratePlanGddState()}
onPlanGddDecision={decidePlanGdd}
queueAgentRunControlFromPanel={queueAgentRunControlFromPanel} queueAgentRunControlFromPanel={queueAgentRunControlFromPanel}
queueOrExecuteProjectIndex={queueOrExecuteProjectIndex} queueOrExecuteProjectIndex={queueOrExecuteProjectIndex}
queuePendingCommand={queuePendingCommand} queuePendingCommand={queuePendingCommand}
+153
View File
@@ -129,6 +129,159 @@ export interface GenerateLocalGameDraftResult {
manifest: GameCreationAppManifest; manifest: GameCreationAppManifest;
} }
export type PlanGddDecisionAction = 'approve' | 'revise' | 'reject';
export interface PlanGddStateViewV1 {
schemaVersion: 'plan-gdd-state-view.v1';
projectId: string;
gddId: string | null;
state:
| 'not_started'
| 'draft'
| 'ready_for_approval'
| 'revision_requested'
| 'approved'
| 'rejected';
session: {
sessionId: string;
sessionRevision: number;
sessionFingerprint: string;
phase:
| 'collecting'
| 'awaiting_user_input'
| 'awaiting_gdd_approval'
| 'revision_requested'
| 'approved'
| 'rejected'
| 'recovery_required';
clarificationRound: number;
repairDepth: number;
accumulatedAgentMillis: number;
activeRunId: string | null;
awaitingAnswerFor: {
delegationId: string;
requestId: string;
questionId: string;
round: number;
} | null;
decisionStateCounts: {
confirmed: number;
defaultPending: number;
prototypePending: number;
};
} | null;
versions: Array<{
gddRef: { gddId: string; version: number; fingerprint: string };
status:
| 'ready_for_approval'
| 'revision_requested'
| 'approved'
| 'rejected'
| 'superseded';
approvalRequestId: string;
createdAtUtc: string;
decision: { action: PlanGddDecisionAction; decidedAtUtc: string } | null;
}>;
displayGdd: {
schemaVersion: string;
projectId: string;
gddId: string;
version: number;
submissionId: string;
approvalRequestId: string;
actionFingerprint: string;
agentId: string;
source: string;
runProfile: string;
runProfileBindingFingerprint: string;
rootAgentId: string;
rootRunId: string;
delegationId: string;
sessionId: string;
sourceSessionRevision: number;
sourceSessionFingerprint: string;
createdByRunId: string;
createdAtUtc: string;
fingerprint: string;
game: {
title: string;
oneLiner: string;
genre: { primary: string; fusion: string | null };
artStyle: {
visualType: string;
keywords: string[];
moodAndColor: string;
mvpArtBoundary: string;
};
pillars: Array<{
name: string;
playerFeel: string;
mechanism: string;
decisionState: string;
basis: null;
}>;
coreLoop: string[];
targetUsers: {
coreUsers: string;
preferences: string;
sessionLength: string;
referenceGames: string[];
};
platformFacts: {
runtime: string;
viewports: string[];
inputs: string[];
preview: string;
};
mvpSystems: Array<{
system: string;
minimalFunction: string;
whyRequired: string;
verifyMethod: string;
decisionState: string;
basis: null;
}>;
outOfScope: string[];
creatorTips: {
doFirst: string;
deferForNow: string;
howToVerify: string;
expandWhen: string;
};
};
decisions: Array<{
id: string;
topic: string;
state: 'confirmed' | 'default_pending' | 'prototype_pending';
answerSource: 'user_option' | 'user_freeform' | 'default';
round: number;
answerSummary: string;
basis: null;
}>;
prototypeValidationItems: Array<{
id: string;
question: string;
microPrototype: string;
observation: string;
passCriterion: string;
}>;
} | null;
pendingApproval: {
gddRef: { gddId: string; version: number; fingerprint: string };
pendingActionId: string;
actionFingerprint: string;
approvalRequestId: string;
sessionId: string;
runId: string;
} | null;
approvedGddRef: {
gddId: string;
version: number;
fingerprint: string;
} | null;
recoveryPending: boolean;
}
export interface GameCreatorChatAgentReply { export interface GameCreatorChatAgentReply {
replyText: string; replyText: string;
} }
@@ -0,0 +1,313 @@
import { useState } from 'react';
import type {
PlanGddDecisionAction,
PlanGddStateViewV1,
} from '../../app/types';
type GddApprovalCardProps = {
state: PlanGddStateViewV1 | null;
busy: boolean;
error: string | null;
onRefresh: () => void;
onDecision: (
action: PlanGddDecisionAction,
comment: string | null,
) => Promise<void>;
};
const stateLabels: Record<PlanGddStateViewV1['state'], string> = {
not_started: '未开始',
draft: '策划中',
ready_for_approval: '待审批',
revision_requested: '待修订',
approved: '已批准',
rejected: '已退回',
};
const decisionStateLabels = {
confirmed: '已确认',
default_pending: '待确认默认项',
prototype_pending: '待原型验证',
};
const visibleGddSections = (
gdd: NonNullable<PlanGddStateViewV1['displayGdd']>,
) => [
{
title: '类型与美术',
content: [
`类型:${gdd.game.genre.primary}${gdd.game.genre.fusion ? ` / ${gdd.game.genre.fusion}` : ''}`,
`视觉:${gdd.game.artStyle.visualType}`,
`关键词:${gdd.game.artStyle.keywords.join('、')}`,
`氛围:${gdd.game.artStyle.moodAndColor}`,
`MVP 美术边界:${gdd.game.artStyle.mvpArtBoundary}`,
].join('\n'),
},
{
title: '游戏支柱',
content: gdd.game.pillars
.map(
(pillar) => `${pillar.name}${pillar.playerFeel}${pillar.mechanism}`,
)
.join('\n'),
},
{ title: '核心循环', content: gdd.game.coreLoop.join(' → ') },
{
title: '目标用户',
content: [
`核心用户:${gdd.game.targetUsers.coreUsers}`,
`偏好:${gdd.game.targetUsers.preferences}`,
`单局时长:${gdd.game.targetUsers.sessionLength}`,
`参考作品:${gdd.game.targetUsers.referenceGames.join('、') || '无'}`,
].join('\n'),
},
{
title: '平台事实',
content: [
`运行时:${gdd.game.platformFacts.runtime}`,
`视口:${gdd.game.platformFacts.viewports.join('、')}`,
`输入:${gdd.game.platformFacts.inputs.join('、')}`,
`预览:${gdd.game.platformFacts.preview}`,
].join('\n'),
},
{
title: 'MVP 系统',
content: gdd.game.mvpSystems
.map(
(system) =>
`${system.system}${system.minimalFunction}\n为什么需要:${system.whyRequired}\n验证:${system.verifyMethod}`,
)
.join('\n'),
},
{ title: '暂不纳入', content: gdd.game.outOfScope.join('、') },
{
title: '创作者提示',
content: [
`先做:${gdd.game.creatorTips.doFirst}`,
`暂缓:${gdd.game.creatorTips.deferForNow}`,
`验证:${gdd.game.creatorTips.howToVerify}`,
`扩展条件:${gdd.game.creatorTips.expandWhen}`,
].join('\n'),
},
{
title: '原型验证项',
content: gdd.prototypeValidationItems
.map(
(item) =>
`${item.question}\n微型原型:${item.microPrototype}\n观察:${item.observation}\n通过标准:${item.passCriterion}`,
)
.join('\n'),
},
];
export function GddApprovalCard({
state,
busy,
error,
onRefresh,
onDecision,
}: GddApprovalCardProps) {
const [commentAction, setCommentAction] = useState<Exclude<
PlanGddDecisionAction,
'approve'
> | null>(null);
const [comment, setComment] = useState('');
const [gddDetailsOpen, setGddDetailsOpen] = useState(false);
if (
!state?.displayGdd ||
(!state.pendingApproval && !state.recoveryPending)
) {
return null;
}
const gdd = state.displayGdd;
const pending = state.pendingApproval;
const canDecide = Boolean(
pending && state.state === 'ready_for_approval' && !state.recoveryPending,
);
const submitComment = () => {
if (!commentAction || !comment.trim()) {
return;
}
void onDecision(commentAction, comment.trim())
.then(() => {
setCommentAction(null);
setComment('');
})
.catch(() => undefined);
};
return (
<section className="gdd-approval-card" aria-label="GDD 审批卡">
<header className="gdd-approval-card__header">
<div>
<span className="gdd-approval-card__eyebrow"></span>
<h2>{gdd.game.title || `Fast GDD v${gdd.version}`}</h2>
<p>{gdd.game.oneLiner}</p>
</div>
<span className="gdd-approval-card__state">
{stateLabels[state.state]}
</span>
</header>
<div className="gdd-approval-card__meta">
<span>{`版本 v${gdd.version}`}</span>
<span>{`指纹 ${gdd.fingerprint.slice(0, 12)}`}</span>
<span>{`${gdd.decisions.length} 项决定`}</span>
</div>
{gdd.decisions.length > 0 ? (
<div className="gdd-approval-card__decisions" aria-label="决定状态">
{gdd.decisions.map((decision) => (
<article key={decision.id}>
<strong>{decision.topic}</strong>
<span>{decisionStateLabels[decision.state]}</span>
<small>{decision.answerSummary}</small>
</article>
))}
</div>
) : null}
<button
type="button"
className="gdd-approval-card__details-trigger"
onClick={() => setGddDetailsOpen(true)}
>
GDD
</button>
{state.recoveryPending ? (
<div className="gdd-approval-card__recovery" role="status">
<span></span>
<button type="button" disabled={busy} onClick={onRefresh}>
{busy ? '恢复中' : '重试恢复'}
</button>
</div>
) : null}
{error ? (
<div className="gdd-approval-card__error" role="alert">
{error}
</div>
) : null}
{pending ? (
<div className="gdd-approval-card__actions">
<button
type="button"
disabled={!canDecide || busy}
onClick={() =>
void onDecision('approve', null).catch(() => undefined)
}
>
{busy ? '处理中' : `批准 v${pending.gddRef.version}`}
</button>
<button
type="button"
disabled={!canDecide || busy}
onClick={() => setCommentAction('revise')}
>
</button>
<button
type="button"
disabled={!canDecide || busy}
onClick={() => setCommentAction('reject')}
>
退
</button>
</div>
) : null}
{commentAction ? (
<div
className="gdd-approval-card__dialog-backdrop"
role="presentation"
onMouseDown={(event) => {
if (event.target === event.currentTarget) {
setCommentAction(null);
setComment('');
}
}}
>
<section
className="gdd-approval-card__dialog"
role="dialog"
aria-modal="true"
aria-labelledby="gdd-approval-comment-title"
>
<h3 id="gdd-approval-comment-title">
{commentAction === 'revise' ? '填写修改意见' : '填写退回原因'}
</h3>
<textarea
autoFocus
rows={5}
value={comment}
placeholder="请输入原因"
onChange={(event) => setComment(event.currentTarget.value)}
/>
<div className="gdd-approval-card__dialog-actions">
<button
type="button"
disabled={busy}
onClick={() => {
setCommentAction(null);
setComment('');
}}
>
</button>
<button
type="button"
disabled={busy || !comment.trim()}
onClick={submitComment}
>
{busy ? '提交中' : '提交决定'}
</button>
</div>
</section>
</div>
) : null}
{gddDetailsOpen ? (
<div
className="gdd-approval-card__dialog-backdrop"
role="presentation"
onMouseDown={(event) => {
if (event.target === event.currentTarget) {
setGddDetailsOpen(false);
}
}}
>
<section
className="gdd-approval-card__dialog gdd-approval-card__details"
role="dialog"
aria-modal="true"
aria-labelledby="gdd-approval-details-title"
>
<header>
<h3 id="gdd-approval-details-title">{gdd.game.title}</h3>
<p>{gdd.game.oneLiner}</p>
</header>
{visibleGddSections(gdd)
.filter((section) => section.content)
.map((section) => (
<article key={section.title}>
<strong>{section.title}</strong>
<p>{section.content}</p>
</article>
))}
<div className="gdd-approval-card__dialog-actions">
<button type="button" onClick={() => setGddDetailsOpen(false)}>
</button>
</div>
</section>
</div>
) : null}
</section>
);
}
@@ -23,6 +23,8 @@ import type {
LocalProjectCheckpointSummary, LocalProjectCheckpointSummary,
LocalProjectFileEntry, LocalProjectFileEntry,
MemoryScope, MemoryScope,
PlanGddDecisionAction,
PlanGddStateViewV1,
PendingCommand, PendingCommand,
PendingUiConfirmation, PendingUiConfirmation,
} from '../../app/types'; } from '../../app/types';
@@ -50,6 +52,7 @@ import {
pendingCommandTitle, pendingCommandTitle,
} from './pendingCommandPresentation'; } from './pendingCommandPresentation';
import { resolvePendingCommandProjectPath } from './projectCommandPolicy'; import { resolvePendingCommandProjectPath } from './projectCommandPolicy';
import { GddApprovalCard } from './GddApprovalCard';
type ProjectWorkspaceChatPaneProps = { type ProjectWorkspaceChatPaneProps = {
agentRunStatus: string; agentRunStatus: string;
@@ -175,6 +178,14 @@ type ProjectWorkspaceChatPaneProps = {
projectSupervisorRuntime: AgentRuntimeState | null; projectSupervisorRuntime: AgentRuntimeState | null;
projectSupervisorRuntimeError: string; projectSupervisorRuntimeError: string;
projectSupervisorTransientReply: string; projectSupervisorTransientReply: string;
planGddState: PlanGddStateViewV1 | null;
planGddHydrateBusy: boolean;
planGddError: string | null;
onPlanGddRefresh: () => void;
onPlanGddDecision: (
action: PlanGddDecisionAction,
comment: string | null,
) => Promise<void>;
queueAgentRunControlFromPanel: (action: 'kill' | 'retry' | 'resume') => void; queueAgentRunControlFromPanel: (action: 'kill' | 'retry' | 'resume') => void;
queueOrExecuteProjectIndex: () => Promise<void>; queueOrExecuteProjectIndex: () => Promise<void>;
queuePendingCommand: (command: PendingCommand) => void; queuePendingCommand: (command: PendingCommand) => void;
@@ -258,6 +269,11 @@ export function ProjectWorkspaceChatPane({
projectSupervisorRuntime, projectSupervisorRuntime,
projectSupervisorRuntimeError, projectSupervisorRuntimeError,
projectSupervisorTransientReply, projectSupervisorTransientReply,
planGddState,
planGddHydrateBusy,
planGddError,
onPlanGddRefresh,
onPlanGddDecision,
queueAgentRunControlFromPanel, queueAgentRunControlFromPanel,
queueOrExecuteProjectIndex, queueOrExecuteProjectIndex,
queuePendingCommand, queuePendingCommand,
@@ -347,6 +363,13 @@ export function ProjectWorkspaceChatPane({
</button> </button>
</div> </div>
</header> </header>
<GddApprovalCard
state={planGddState}
busy={planGddHydrateBusy}
error={planGddError}
onRefresh={onPlanGddRefresh}
onDecision={onPlanGddDecision}
/>
<div className="chat-quick-actions" aria-label="预览快捷操作"> <div className="chat-quick-actions" aria-label="预览快捷操作">
<button <button
type="button" type="button"
+188
View File
@@ -3263,6 +3263,194 @@ textarea {
gap: 8px; gap: 8px;
} }
.gdd-approval-card {
display: grid;
gap: 14px;
padding: 18px;
border: 1px solid #cfd7e6;
border-radius: 10px;
background: #f8fbff;
}
.gdd-approval-card__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
}
.gdd-approval-card__eyebrow {
color: #526173;
font-size: 12px;
}
.gdd-approval-card h2,
.gdd-approval-card h3,
.gdd-approval-card p {
margin: 0;
}
.gdd-approval-card h2 {
margin-top: 3px;
font-size: 18px;
}
.gdd-approval-card__header p {
margin-top: 5px;
color: #526173;
overflow-wrap: anywhere;
}
.gdd-approval-card__state {
flex: 0 0 auto;
padding: 4px 8px;
border-radius: 999px;
color: #0c4a6e;
background: #dff3ff;
font-size: 12px;
}
.gdd-approval-card__meta {
display: flex;
flex-wrap: wrap;
gap: 8px 14px;
color: #526173;
font-size: 12px;
}
.gdd-approval-card__details-trigger {
justify-self: start;
min-height: 30px;
padding: 0 10px;
border: 1px solid #cfd7e6;
border-radius: 6px;
color: #27364a;
background: #fff;
}
.gdd-approval-card__decisions {
display: grid;
gap: 8px;
}
.gdd-approval-card__decisions article {
display: grid;
gap: 3px;
padding: 9px 10px;
border: 1px solid #e2e8f0;
border-radius: 7px;
background: #fff;
}
.gdd-approval-card__decisions article span {
color: #1f6feb;
font-size: 12px;
}
.gdd-approval-card__decisions article small {
color: #526173;
overflow-wrap: anywhere;
}
.gdd-approval-card__actions,
.gdd-approval-card__dialog-actions,
.gdd-approval-card__recovery {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
}
.gdd-approval-card__actions button,
.gdd-approval-card__dialog-actions button,
.gdd-approval-card__recovery button {
min-height: 32px;
padding: 0 12px;
border: 1px solid #cfd7e6;
border-radius: 6px;
color: #fff;
background: #1f6feb;
}
.gdd-approval-card__actions button:nth-child(n + 2),
.gdd-approval-card__dialog-actions button:first-child {
color: #27364a;
background: #fff;
}
.gdd-approval-card button:disabled {
cursor: not-allowed;
opacity: 0.55;
}
.gdd-approval-card__recovery {
justify-content: space-between;
padding: 9px 10px;
border-radius: 7px;
color: #854d0e;
background: #fff7df;
font-size: 13px;
}
.gdd-approval-card__error {
padding: 9px 10px;
border-radius: 7px;
color: #991b1b;
background: #fff1f2;
overflow-wrap: anywhere;
}
.gdd-approval-card__dialog-backdrop {
position: fixed;
inset: 0;
z-index: 220;
display: grid;
padding: 24px;
background: rgb(0 0 0 / 56%);
place-items: center;
}
.gdd-approval-card__dialog {
display: grid;
gap: 12px;
width: min(520px, 100%);
padding: 20px;
border: 1px solid #e5e7eb;
border-radius: 8px;
background: #fff;
box-shadow: 0 18px 52px rgb(0 0 0 / 18%);
}
.gdd-approval-card__details {
max-height: min(760px, calc(100vh - 32px));
overflow: auto;
}
.gdd-approval-card__details header,
.gdd-approval-card__details article {
display: grid;
gap: 5px;
}
.gdd-approval-card__details article {
padding-top: 10px;
border-top: 1px solid #e2e8f0;
}
.gdd-approval-card__details p {
white-space: pre-line;
overflow-wrap: anywhere;
}
.gdd-approval-card__dialog textarea {
width: 100%;
min-height: 110px;
padding: 9px;
border: 1px solid #cfd7e6;
border-radius: 6px;
resize: vertical;
}
.panel-header { .panel-header {
margin-bottom: 10px; margin-bottom: 10px;
} }
@@ -1,5 +1,14 @@
# 决策记录 # 决策记录
## 2026-08-18 M1D-1 隔离工作树实现:hydrate/read model 与 GDD 审批卡
- **隔离基线与范围**:在 `codex/genarrative-isolated`、基线 `14c00017c` 上开工;只实现 M1D-1 的前端 hydrate/read model 与 GDD 审批卡,不接 M1D-2 入口分流、完整构建按钮、构建准入或 M1E 下游链路。
- **Runtime hydrate**:新增 `hydrate_game_creator_plan_gdd_state``plan-gdd-state-view.v1`。command 通过严格 `deny_unknown_fields``{projectPath}` JSON 输入并经过项目权限校验;Rust 只读取 canonical GDD/receipt/session/pending/index authority,空 planning 项目返回 `not_started` 且不创建目录,页面不扫描 sidecar/Markdown/index。
- **审批卡链路**:工作台在项目打开、Runtime 状态推进、窗口恢复时 hydrate;待审 GDD 由 hydrate 提供,审批决定调用既有 `decide_game_creator_plan_gdd`,按 `(approvalRequestId, action)` 复用 `gdd-response-<uuid>`,决定返回后再次 hydrate。卡片提供 approve/revise/reject,后两者必须填写原因;正文通过独立详情弹层展示,不在卡片下无限堆叠。
- **恢复与安全边界**`recoveryPending` 时卡片只显示恢复重试,禁止决定;审批 pending 只有验收门已落下的 `gdd-approval` sidecar 才能成为可操作事实,不能把 `awaiting_gdd_approval` session 误当验收通过。未审批 GDD 缺失或错绑 session successor 时返回 `PLAN_SESSION_RECOVERY_REQUIRED`pending/receipt 身份不一致 fail-closedhydrate 响应使用序列号丢弃过期并发结果。
- **必要回归与验证**:保留一条必要 Rust 回归,验证已初始化但无 planning 目录 hydrate 返回完整空 view 且不创建存储;该测试通过。`cargo check --offline --all-targets --target-dir target-m1d1``cargo fmt --check``git diff --check` 通过。前端 TypeScript 复用原工作树依赖 junction 做检查,新增代码无类型错误;仓库现有缺少 `@tauri-apps/api/event``@tauri-apps/plugin-http``@tauri-apps/plugin-clipboard-manager``@tauri-apps/plugin-opener` 依赖的问题仍保留。
- **自审保留项**:真实验收门等待期间 pending 尚未建立时不显示可操作审批卡;M1D-1 不新增完整跨层故障注入或构建链测试,留给 M1E。所有检查以完整触发链路为准,未发现需要扩大到 M1D-2/M1E 的问题。隔离实现及本包验证已完成,待提交/合回。
## 2026-08-18 `M1C-2c` 隔离工作树开工:A/B 决策卡语义与信封合同收口 ## 2026-08-18 `M1C-2c` 隔离工作树开工:A/B 决策卡语义与信封合同收口
- **隔离基线**:在 `codex/genarrative-isolated` 上从 `0199fb6e4` 开工;`M1C-2b` 已合回 `feat/five_min_design`,本包不回改其三轮上限、continuation 幂等、答案绑定或预算折叠。 - **隔离基线**:在 `codex/genarrative-isolated` 上从 `0199fb6e4` 开工;`M1C-2b` 已合回 `feat/five_min_design`,本包不回改其三轮上限、continuation 幂等、答案绑定或预算折叠。
@@ -2022,9 +2022,8 @@ M0 完成不表示完整策划闭环已经上线。`M1A-1``M1A-4`、`M1B-1`
| `M1C-1` | `gdd-approval` pending、审批命令、receiptreceipt 写入上述 status 与 plan 根完成门 | `M1B-2``M1C-0`(前向兼容粒度另见 `M1C-0b` | **已落地并合入**:三动作幂等、版本/指纹竞态防护、receipt 后 index/Markdown/audit/terminal observation/session 投影与恢复、generic v5/v4 anchor 精确消费、terminal summary 完整性校验,以及仅作用于 exact plan 根的只读 completion blocker;生产 acceptance-gate pending caller 与验收前置取证门按拆包纪律由 `M1C-2a` 承接。审批 UI / 澄清中转 / 构建准入仍未完成。连续修订 barrier 与 `UserRevisionRequested` 规则按第 23.7 节执行 | | `M1C-1` | `gdd-approval` pending、审批命令、receiptreceipt 写入上述 status 与 plan 根完成门 | `M1B-2``M1C-0`(前向兼容粒度另见 `M1C-0b` | **已落地并合入**:三动作幂等、版本/指纹竞态防护、receipt 后 index/Markdown/audit/terminal observation/session 投影与恢复、generic v5/v4 anchor 精确消费、terminal summary 完整性校验,以及仅作用于 exact plan 根的只读 completion blocker;生产 acceptance-gate pending caller 与验收前置取证门按拆包纪律由 `M1C-2a` 承接。审批 UI / 澄清中转 / 构建准入仍未完成。连续修订 barrier 与 `UserRevisionRequested` 规则按第 23.7 节执行 |
| `M1C-2a` | Goal Contract 接线:turn 1 冻结、固定验收图、审批前置门取证 | `M1C-1``M1A-3` | **当前隔离 worktree 已完成并通过本包门禁,尚未合回**turn 1 的 request-scoped schema 与格式修复都只允许一个固定 `agent.goal_contract`;按项目变化的四项之外,`preferences=[]`、唯一验收节点及证据工具均冻结。Fast GDD evidence 只接受当前 Supervisor 根 run 对 `game/fast_gdd.md` 从第 1 行到 EOF 的同 hash 完整分页;无/旧证据先继续读取,显式 failed 才给原 delivery 的 `repairOfDelegationId`passed 且 delivery 已认领才建 pending。pending/recovery/completion/finalization 均按同 identity 幂等,审批后 Markdown 改写不损坏 Graph。格式、Provider 强判据、M1C-2a、Acceptance Graph、planning submit/approval、finalization、all-targets、编码与 diff 门禁均通过;扩展 autonomous completion 整组的无关 game-chat 并行超时及精确复跑结果见 decision-log 同日条,不改该路径。不包含 `M1C-2b`、UI 或构建准入 | | `M1C-2a` | Goal Contract 接线:turn 1 冻结、固定验收图、审批前置门取证 | `M1C-1``M1A-3` | **当前隔离 worktree 已完成并通过本包门禁,尚未合回**turn 1 的 request-scoped schema 与格式修复都只允许一个固定 `agent.goal_contract`;按项目变化的四项之外,`preferences=[]`、唯一验收节点及证据工具均冻结。Fast GDD evidence 只接受当前 Supervisor 根 run 对 `game/fast_gdd.md` 从第 1 行到 EOF 的同 hash 完整分页;无/旧证据先继续读取,显式 failed 才给原 delivery 的 `repairOfDelegationId`passed 且 delivery 已认领才建 pending。pending/recovery/completion/finalization 均按同 identity 幂等,审批后 Markdown 改写不损坏 Graph。格式、Provider 强判据、M1C-2a、Acceptance Graph、planning submit/approval、finalization、all-targets、编码与 diff 门禁均通过;扩展 autonomous completion 整组的无关 game-chat 并行超时及精确复跑结果见 decision-log 同日条,不改该路径。不包含 `M1C-2b`、UI 或构建准入 |
| `M1C-2b` | 澄清中转接线、轮次派生、预算注入 | `M1C-2a` | **实现与本包门禁已完成并已快进合回 `feat/five_min_design`**:首 child 的 revision 1 session、`NeedsUserInput → awaiting_user_input`、回答绑定后 continuation 的确定性 session 投影、审批后 `revise/reject` 用户修订谱系及 Provider 活跃时间 usage fact/fold 已接线;末次 `plan.submit_gdd` usage 在 receipt/session successor 落盘且 standalone/v4 anchors 精确消费后于同一项目锁内折叠,真实 receipt 回归证明累计值恰好推进一次,重复审批与 recovery reconcile 不二次推进。`planning_clarification_*` **13 passed / 0 failed**(M1C-2c 语义回归另见本包),另有 static deliveries 44、planning storage 13、planning submit 53、Provider usage 4、末次 usage receipt 1、barrier detail 3 条定向回归通过;格式、offline all-targets、编码与 diff 门禁通过。锁序承诺只适用于 **M1C-2b 新增的 planning 澄清写投影路径**`main_loop` 既有通用 completion blocker 的 execution→project 路径不在本包。第 4 轮信封在正常路径不可达:`agent.delegate` 已在工具边界按血缘上限硬拒并返回 failed observationcoordinator 的超三轮 reconciliation 仅用于损坏血缘纵深防御。审批 UI、hydrate、构建准入和下游完整构建不在本包范围 | | `M1C-2b` | 澄清中转接线、轮次派生、预算注入 | `M1C-2a` | **实现与本包门禁已完成并已快进合回 `feat/five_min_design`**:首 child 的 revision 1 session、`NeedsUserInput → awaiting_user_input`、回答绑定后 continuation 的确定性 session 投影、审批后 `revise/reject` 用户修订谱系及 Provider 活跃时间 usage fact/fold 已接线;末次 `plan.submit_gdd` usage 在 receipt/session successor 落盘且 standalone/v4 anchors 精确消费后于同一项目锁内折叠,真实 receipt 回归证明累计值恰好推进一次,重复审批与 recovery reconcile 不二次推进。`planning_clarification_*` **13 passed / 0 failed**(M1C-2c 语义回归另见本包),另有 static deliveries 44、planning storage 13、planning submit 53、Provider usage 4、末次 usage receipt 1、barrier detail 3 条定向回归通过;格式、offline all-targets、编码与 diff 门禁通过。锁序承诺只适用于 **M1C-2b 新增的 planning 澄清写投影路径**`main_loop` 既有通用 completion blocker 的 execution→project 路径不在本包。第 4 轮信封在正常路径不可达:`agent.delegate` 已在工具边界按血缘上限硬拒并返回 failed observationcoordinator 的超三轮 reconciliation 仅用于损坏血缘纵深防御。审批 UI、hydrate、构建准入和下游完整构建不在本包范围 |
| `M1D-1` | 前端 hydrate 与 GDD 审批卡 | `M1C-2b` | 前端只经 `hydrate_game_creator_plan_gdd_state` 读权威状态,不在页面侧合成批准事实 |
| `M1C-2c` | 决策卡 A/B 语义(第 23.9 节,2026-08-18 实现完成并合回):选项 → 台账映射改为 A/B 均 `confirmed/user_option`、信封 label 形状校验、planning role brief 与 Supervisor playbook/final-reply 文案(B 必须是真实岔路、第 3 项恒定且 description 须给出可执行验证方式、改口转述规则、提问纪律) | `M1C-2b` | **实现与门禁完成,已由 `6e4bd9703` 合回 `feat/five_min_design`**Runtime 已实现 A/B/固定第三项校验、B 不再生成 `default_pending``answerSummary` 逐字保真;非法 C/缺项 fail-closedA/B/自由填写回归已通过。`planning_clarification_*` 13、`project_planning` prompt 5、`planning_submit` 定向回归、prompt bundle、格式、编码、diff、offline all-targets 均通过;不含 M1D-1 前端、hydrate、构建准入或下游完整构建 | | `M1C-2c` | 决策卡 A/B 语义(第 23.9 节,2026-08-18 实现完成并合回):选项 → 台账映射改为 A/B 均 `confirmed/user_option`、信封 label 形状校验、planning role brief 与 Supervisor playbook/final-reply 文案(B 必须是真实岔路、第 3 项恒定且 description 须给出可执行验证方式、改口转述规则、提问纪律) | `M1C-2b` | **实现与门禁完成,已由 `6e4bd9703` 合回 `feat/five_min_design`**Runtime 已实现 A/B/固定第三项校验、B 不再生成 `default_pending``answerSummary` 逐字保真;非法 C/缺项 fail-closedA/B/自由填写回归已通过。`planning_clarification_*` 13、`project_planning` prompt 5、`planning_submit` 定向回归、prompt bundle、格式、编码、diff、offline all-targets 均通过;不含 M1D-1 前端、hydrate、构建准入或下游完整构建 |
| `M1D-1` | 前端 hydrate 与 GDD 审批卡;决策卡按第 23.9 节实现(label 动态渲染、默认焦点 A、Other 槽不变) | `M1C-2b` | 前端只经 `hydrate_game_creator_plan_gdd_state` 读权威状态,不在页面侧合成批准事实 | | `M1D-1` | 前端 hydrate 与 GDD 审批卡;决策卡按第 23.9 节实现(label 动态渲染、默认焦点 A、Other 槽不变) | `M1C-2b` | **隔离工作树实现完成,待提交/合回**:新增严格 `{projectPath}` hydrate command、`plan-gdd-state-view.v1` Rust read model、审批卡与独立 GDD 正文详情弹层;页面只消费 hydrate,决定 responseId 按审批请求/动作复用,`recoveryPending` 仅提供恢复重试。已修正“仅 session awaiting 即露出审批动作”及未审批 GDD 缺 session 的 fail-closed 边界;定向 hydrate 空项目回归、offline all-targets cargo check、格式、编码与 diff 门禁通过。未接 M1D-2 入口分流、完整构建按钮或 M1E 下游链路 |
| `M1D-2` | 入口分流与阶段进度 | `M1D-1` | 「直接开建」跳过路径与现状零差异 | | `M1D-2` | 入口分流与阶段进度 | `M1D-1` | 「直接开建」跳过路径与现状零差异 |
| `M1E` | 端到端与故障注入收口 | `M1D-2` | 第 21 节测试矩阵中跨层场景 | | `M1E` | 端到端与故障注入收口 | `M1D-2` | 第 21 节测试矩阵中跨层场景 |