完成M1D-1策划状态与审批卡
新增严格输入的策划状态 hydrate command 与 plan-gdd-state-view.v1 read model 接入 GDD 审批卡、正文详情弹层、决定幂等与恢复重试 补充 M1D-1 开发日志和技术方案状态
This commit is contained in:
@@ -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::*;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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
@@ -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"
|
||||||
|
|||||||
@@ -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-closed;hydrate 响应使用序列号丢弃过期并发结果。
|
||||||
|
- **必要回归与验证**:保留一条必要 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、审批命令、receipt;receipt 写入上述 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、审批命令、receipt;receipt 写入上述 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 observation;coordinator 的超三轮 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 observation;coordinator 的超三轮 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-closed,A/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-closed,A/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 节测试矩阵中跨层场景 |
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user