删除策划 V1 的测试脚本入口并同步退役文档
Project CI / Repository checks (pull_request) Failing after 12s
Project CI / Backend tests (pull_request) Failing after 9s
Project CI / Frontend tests (pull_request) Successful in 3m15s
Project CI / Native shell tests (pull_request) Failing after 15m21s

删除 agent-swarm-test-chat.mjs 的 --plan 模式、自动 GDD 审批回路与 planning 产物检查
删除根与应用 package.json 的 test:plan / test:plan:manual / agc:test:plan* 四条脚本
同步删除 agentSwarmTestEntry.test.ts 中只覆盖 V1 审批回路与 --plan 参数的用例
GddApprovalCard.tsx 注释改指 planning_gdd_model.rs 的现行路径权威定义
立项策划Agent(Fast GDD)方案文档头部标注已退役,仅作历史推导记录
策划会话 Runtime V2 方案文档状态更新为 P5 已完成并记录源码删除执行清单
Provider 兼容性缺陷文档的缺陷 4 标注相关代码已随 V1 退役删除
decision-log 新增 2026-09-08 策划 V1 链路源码整体退役决策记录
This commit is contained in:
2026-09-08 10:43:38 +00:00
parent 78a606ab5e
commit 2fa967a93a
9 changed files with 33 additions and 426 deletions
-2
View File
@@ -19,8 +19,6 @@
"config": "node scripts/game-creator-config-wizard.mjs",
"test:chat": "node scripts/agent-swarm-test-chat.mjs --task \"制作一个可直接试玩的原创植物塔防小游戏:玩家选择并放置原创守卫阻挡敌人,完成波次后可以进入下一关并重新开始。主题、单位名称与视觉语言必须原创,不使用任何现有游戏角色、单位名、Logo 或受保护视觉语言。请自主完成正式产物、静态检查和双视口试玩验证。\" --no-open",
"test:chat:manual": "node scripts/agent-swarm-test-chat.mjs",
"test:plan": "node scripts/agent-swarm-test-chat.mjs --plan --task \"我想做一款原创横版像素解谜小游戏,主角是一个能操控自己影子的小机器人,影子可以变成平台和开关。请完成立项策划并给出 Fast GDD。主题、角色名与视觉语言必须原创,不使用任何现有游戏角色、名称、Logo 或受保护视觉语言。\"",
"test:plan:manual": "node scripts/agent-swarm-test-chat.mjs --plan",
"agent-run": "node scripts/run-cli-with-config.mjs --agent-run",
"agent-run:smoke": "node scripts/smoke-agent-run-local-provider.mjs",
"agent-runtime:real-e2e": "node scripts/agent-runtime-real-e2e.mjs",
@@ -36,8 +36,6 @@ export const ungeneratedGameEntryMarker =
'还没有生成游戏。回到聊天输入创意并确认生成后';
export const defaultRealSwarmTestTask =
'制作一个可直接试玩的原创植物塔防小游戏:玩家选择并放置原创守卫阻挡敌人,完成波次后可以进入下一关并重新开始。主题、单位名称与视觉语言必须原创,不使用任何现有游戏角色、单位名、Logo 或受保护视觉语言。请自主完成正式产物、静态检查和双视口试玩验证。';
export const defaultRealSwarmPlanTask =
'我想做一款原创横版像素解谜小游戏,主角是一个能操控自己影子的小机器人,影子可以变成平台和开关。请完成立项策划并给出 Fast GDD。主题、角色名与视觉语言必须原创,不使用任何现有游戏角色、名称、Logo 或受保护视觉语言。';
export const swarmTurnReportPrefix = '[turn.report] ';
export const swarmTurnReportSchema = 'game-creator-swarm-turn-report.v1';
@@ -140,15 +138,10 @@ export const usage = `用法:
--keep-project 保留自动创建的一次性项目
--no-open 手工模式启动预览但不自动打开浏览器
--task <需求> 通过 manual 入口非交互提交自定义需求
--plan 走「做方案」立项策划入口,不做游戏,不做产物验收和试玩
--timeout-minutes <分钟> 设置本次执行期限;自动任务默认 50 分钟,--plan 默认 6 分钟,手工模式默认不限时
--timeout-minutes <分钟> 设置本次执行期限;自动任务默认 50 分钟,手工模式默认不限时
--dry-run 只检查目录发现和项目准备,不启动 LLM
-h, --help 显示帮助
环境变量:
AGC_PLAN_GDD_DECISION 审批卡自动应答动作,默认 approverevise/reject 必须
同时用 AGC_PLAN_GDD_COMMENT 给出真实修改意见
AGC_PLAN_GDD_COMMENT revise/reject 的意见原文`;
`;
function readOptionValue(args, index, option) {
const value = args[index + 1]?.trim();
@@ -177,7 +170,6 @@ export function parseSwarmTestArguments(args) {
keepProject: false,
openBrowser: true,
task: null,
plan: false,
timeoutMinutes: null,
dryRun: false,
help: false,
@@ -202,8 +194,6 @@ export function parseSwarmTestArguments(args) {
if (task.length > 4_000) throw new Error('--task 不能超过 4000 字符');
options.task = task;
index += 1;
} else if (argument === '--plan') {
options.plan = true;
} else if (argument === '--timeout-minutes') {
if (options.timeoutMinutes !== null) {
throw new Error('--timeout-minutes 只能指定一次');
@@ -222,16 +212,11 @@ export function parseSwarmTestArguments(args) {
}
export function shouldStartPersistentPreview(options) {
// 立项策划链路只出 GDD,没有可试玩产物,任何模式都不该起预览。
return !options.task && !options.plan;
return !options.task;
}
export function resolveSwarmTestTimeoutMs(options) {
// 立项策划的设计目标是五分钟出方案,给一分钟余量;再久就是卡住了,早失败
// 比让 harness 空等更有用。做游戏那条链路的 50 分钟不变。
const planMinutes = options.plan ? 6 : null;
const minutes =
options.timeoutMinutes ?? planMinutes ?? (options.task ? 50 : null);
const minutes = options.timeoutMinutes ?? (options.task ? 50 : null);
return minutes === null ? null : minutes * 60_000;
}
@@ -979,25 +964,12 @@ export function swarmAutoPilotShouldCloseInput(output, promptsAfterSubmit) {
return swarmAutoPilotSitsAtPrompt(output) && promptsAfterSubmit >= 1;
}
// GDD 审批位不能等 CLI 退出之后再处理:Run 停在这里时状态是 waiting-for-user-input
// 而 swarm CLI 恰好把这个状态算作「本轮还在跑」,turn 永远不 settleCLI 也就永远
// 不退出。所以审批必须在 CLI 还活着的时候并发做完,让 Run 自己继续跑到收束。
// 这一句是 PlanGddCompletionBlockerKind::AwaitingApprovalDecision 专有的投影文案,
// 另外三个 blocked 子状态都不会打出它;即便认错了,真正的判据也是随后那次
// --plan-gdd-status,没有待决定审批时不会有任何写入。
const planGddApprovalWaitPattern = /等待 Fast GDD 审批决定/u;
export function swarmOutputAwaitsPlanGddApproval(line) {
return planGddApprovalWaitPattern.test(line);
}
async function runTaskCargo(
cliArguments,
task,
setActiveChild,
timeoutMs,
autoPilot = false,
onPlanGddApprovalWait = null,
) {
const child = spawnChild(cargoCommand, buildCargoCliArguments(cliArguments), {
stdio: ['pipe', 'pipe', 'inherit'],
@@ -1009,25 +981,6 @@ async function runTaskCargo(
let taskSubmitted = false;
let promptsSeen = 0;
let sittingAtPrompt = false;
let planGddApproval = null;
let planGddApprovalError = null;
let planGddApprovalStarted = false;
let planGddApprovalPromise = null;
const startPlanGddApproval = () => {
planGddApprovalStarted = true;
console.log(
`[自动审批] 检测到 Fast GDD 审批位,正在提交 ${resolvePlanGddAutoDecision().action}`,
);
planGddApprovalPromise = onPlanGddApprovalWait()
.then((value) => {
planGddApproval = value;
})
.catch((error) => {
planGddApprovalError = error;
// 审批没成的话 Run 会一直停在等待位,干等到超时只会把真正的原因埋掉。
void terminateChildTree(child).catch(() => {});
});
};
child.stdout.setEncoding('utf8');
child.stdout.on('data', (chunk) => {
process.stdout.write(chunk);
@@ -1040,13 +993,6 @@ async function runTaskCargo(
reportLines.push(normalizedLine);
settled = true;
}
if (
onPlanGddApprovalWait &&
!planGddApprovalStarted &&
swarmOutputAwaitsPlanGddApproval(normalizedLine)
) {
startPlanGddApproval();
}
}
if (!autoPilot || child.stdin.writableEnded) return;
const atPrompt = swarmAutoPilotSitsAtPrompt(pendingLine);
@@ -1085,12 +1031,9 @@ async function runTaskCargo(
if (normalizedPendingLine.startsWith(swarmTurnReportPrefix)) {
reportLines.push(normalizedPendingLine);
}
await planGddApprovalPromise;
if (planGddApprovalError) throw planGddApprovalError;
return {
...result,
turnReportOutput: reportLines.join('\n'),
planGddApproval,
};
} finally {
setActiveChild(null);
@@ -1780,196 +1723,6 @@ export async function validateSwarmProjectArtifacts(projectPath, options) {
return inspection;
}
// 这四条路径的权威定义都在 Rust 侧 `planning_storage.rs``PLAN_SESSION_PATH`、
// `PLAN_GDD_INDEX_PATH`、`PLAN_STORAGE_ROOT`、`PLAN_FAST_GDD_PATH`)。跨语言没有共享
// 常量的通道,改路径时要连同 `GddApprovalCard.tsx` 一起动。
export const planningOutputPaths = [
'.agent/planning/session.json',
'.agent/planning/index.json',
'.agent/planning/pending.json',
'game/fast_gdd.md',
];
export async function inspectPlanningOutputs(projectPath) {
const outputs = [];
for (const relativePath of planningOutputPaths) {
const absolutePath = path.join(projectPath, ...relativePath.split('/'));
const metadata = await lstat(absolutePath).catch((error) => {
if (error?.code === 'ENOENT') return null;
throw error;
});
outputs.push({
path: relativePath,
exists: Boolean(metadata?.isFile()),
bytes: metadata?.isFile() ? metadata.size : 0,
});
}
return outputs;
}
async function reportPlanningOutputs(projectPath) {
const outputs = await inspectPlanningOutputs(projectPath);
console.log('\n立项策划产物:');
for (const output of outputs) {
console.log(
output.exists
? ` [有] ${output.path}${output.bytes} 字节)`
: ` [无] ${output.path}`,
);
}
}
const planGddApprovalTimeoutMs = 60_000;
export const planGddStatusOutputPrefix = 'planGddStateJson=';
export const planGddDecisionOutputPrefix = 'planGddDecisionJson=';
function parsePrefixedJsonLine(output, prefix, label) {
const line = output
.split('\n')
.map((value) => (value.endsWith('\r') ? value.slice(0, -1) : value))
.find((value) => value.startsWith(prefix));
if (!line) throw new Error(`${label}缺少 ${prefix} 输出`);
try {
return JSON.parse(line.slice(prefix.length));
} catch (error) {
throw new Error(`解析${label}失败:${error.message}`);
}
}
export function parsePlanGddStatusOutput(output) {
return parsePrefixedJsonLine(
output,
planGddStatusOutputPrefix,
'Fast GDD 审批状态',
);
}
export function parsePlanGddDecisionOutput(output) {
return parsePrefixedJsonLine(
output,
planGddDecisionOutputPrefix,
'Fast GDD 审批回执',
);
}
// 审批卡是这条链路唯一的人类判据,所以自动应答默认只投 approve,且只在投影确实有
// 一张待决定审批时出手。revise/reject 需要一段真实的修改意见,让机器编一段等于把
// 判据换成噪声——所以那两条分支只在跑的人自己用 AGC_PLAN_GDD_COMMENT 给出意见时
// 才走。手工调 --plan-gdd-decide 也能达到同样效果,但那要求 plan 根 run 仍然活着,
// 而它恰好是本进程持有的 CLI 子进程。
export function planGddAutoApprovalIsPending(state) {
return Boolean(state?.pendingApproval);
}
export function resolvePlanGddAutoDecision(env = process.env) {
const action = (env.AGC_PLAN_GDD_DECISION ?? 'approve').trim();
if (!['approve', 'revise', 'reject'].includes(action)) {
throw new Error('AGC_PLAN_GDD_DECISION 只能是 approve / revise / reject');
}
const comment = (env.AGC_PLAN_GDD_COMMENT ?? '').trim();
if (action === 'approve') return { action, comment: null };
if (!comment) {
throw new Error(
`${action} 必须同时设 AGC_PLAN_GDD_COMMENT 提供真实修改意见`,
);
}
return { action, comment };
}
async function settlePlanGddApproval(
projectPath,
runtimeConfigPath,
setActiveChild,
) {
const readStatus = async () => {
const result = await runCapturedCargo(
['--config-dir', runtimeConfigPath, '--plan-gdd-status', projectPath],
setActiveChild,
{
timeoutMs: planGddApprovalTimeoutMs,
label: 'Fast GDD 审批状态查询',
},
);
if (result.code !== 0 || result.signal) {
throw new Error(
`读取 Fast GDD 审批状态失败:${result.stderr.trim() || result.stdout.trim()}`,
);
}
return parsePlanGddStatusOutput(result.stdout);
};
const before = await readStatus();
if (!planGddAutoApprovalIsPending(before)) {
return { decided: false, state: before };
}
const { action, comment } = resolvePlanGddAutoDecision();
const decision = await runCapturedCargo(
[
'--config-dir',
runtimeConfigPath,
'--plan-gdd-decide',
projectPath,
action,
...(comment === null ? [] : ['--stdin']),
],
setActiveChild,
{
timeoutMs: planGddApprovalTimeoutMs,
label: 'Fast GDD 审批决定',
stdin: comment,
},
);
if (decision.code !== 0 || decision.signal) {
throw new Error(
`提交 Fast GDD 审批决定失败:${decision.stderr.trim() || decision.stdout.trim()}`,
);
}
const receipt = parsePlanGddDecisionOutput(decision.stdout);
// 回执落盘和唤醒后台任务是两件事:decide 命令把唤醒失败降级成 recoveryPending
// 于是审批已经生效、Run 却仍停在 waiting-for-user-input。实测就是这样——只有
// 补一次 --agent-resume 才会重新起 turn。这是仓库自己给这个状态定义的恢复动作。
let recovered = false;
if (receipt.recoveryPending) {
const resume = await runCapturedCargo(
['--config-dir', runtimeConfigPath, '--agent-resume', projectPath],
setActiveChild,
{
timeoutMs: planGddApprovalTimeoutMs,
label: 'Fast GDD 审批后恢复后台任务',
},
);
if (resume.code !== 0 || resume.signal) {
throw new Error(
`审批已提交但恢复后台任务失败:${resume.stderr.trim() || resume.stdout.trim()}`,
);
}
recovered = true;
}
return { decided: true, receipt, recovered, state: await readStatus() };
}
async function reportPlanGddApproval(approval) {
const { state } = approval;
console.log('\nFast GDD 审批:');
if (!approval.decided) {
console.log(` [无待决定审批] 当前投影状态=${state.state}`);
return;
}
console.log(
` [已决定 ${approval.receipt.decisionRef.action}] outcome=${approval.receipt.outcome} v${approval.receipt.decisionRef.version} 投影状态=${state.state}`,
);
if (approval.recovered) {
console.log(
' [已恢复] 审批回执的 recoveryPending 由一次 --agent-resume 收口',
);
}
if (state.session) {
console.log(
` 澄清轮次=${state.session.clarificationRound} 返工深度=${state.session.repairDepth} phase=${state.session.phase}`,
);
}
}
export async function hasConfiguredEditorApiKey(configDir) {
let configured = false;
for (const fileName of [configFileName, localConfigFileName]) {
@@ -2122,13 +1875,7 @@ export async function runSwarmTestChat(options) {
const setActiveChild = (child) => {
activeChild = child;
};
// GDD 审批要和 swarm CLI 并发跑,两者不能共用 activeChild 这一个槽位:审批子进程
// 结束时的 setActiveChild(null) 会把 CLI 从槽里抹掉,Ctrl-C 就杀不到它了。
const concurrentChildren = new Set();
const setConcurrentChild = (child) => {
if (child) concurrentChildren.add(child);
else concurrentChildren.clear();
};
const stopRequested = () => receivedSignal !== null;
const handleSignal = (signal) => {
const repeatedSignal = receivedSignal !== null;
@@ -2206,11 +1953,10 @@ export async function runSwarmTestChat(options) {
);
}
console.log('LLM 配置已就绪。');
const requirementNoun = options.plan ? '立项策划需求' : '游戏需求';
console.log(
options.task
? `已提交一条非交互${requirementNoun},正在等待 Swarm 自主完成。\n`
: `输入一条${requirementNoun}并回车;提交后按 Ctrl+D,让 Swarm 自主完成。\n`,
? '已提交一条非交互游戏需求,正在等待 Swarm 自主完成。\n'
: '输入一条游戏需求并回车;提交后按 Ctrl+D,让 Swarm 自主完成。\n',
);
phase = 'chat';
@@ -2220,8 +1966,7 @@ export async function runSwarmTestChat(options) {
runtimeConfig.path,
'--swarm-chat',
'--init',
// 做方案链路只能跑 standard 档,后端对 plan + autonomous 是硬否决。
options.plan ? '--plan' : '--autonomous-game-build',
'--autonomous-game-build',
project.path,
];
let chat;
@@ -2234,15 +1979,6 @@ export async function runSwarmTestChat(options) {
timeoutDeadline === null
? null
: Math.max(1, timeoutDeadline - Date.now()),
options.plan,
options.plan
? () =>
settlePlanGddApproval(
project.path,
runtimeConfig.path,
setConcurrentChild,
)
: null,
)
: await runInteractiveCargo(chatArguments, setActiveChild);
} catch (error) {
@@ -2258,32 +1994,6 @@ export async function runSwarmTestChat(options) {
if (options.task) {
turnReport = parseSettledSwarmTurnReport(chat.turnReportOutput);
}
if (options.plan) {
// 立项策划不出游戏产物,正式验收在 GDD 审批卡上;这里只报告落盘情况,
// 是否收束已经由 CLI 的退出码判过了。
// 自动任务档的审批已经在 CLI 运行期间并发做完了;手工档(人自己敲 Ctrl+D
// 退出)没有那次触发,退出后补一次,没有待决定审批时它是只读的。
phase = 'plan-approval';
const approval =
chat.planGddApproval ??
(await settlePlanGddApproval(
project.path,
runtimeConfig.path,
setConcurrentChild,
));
if (receivedSignal) break session;
phase = 'plan-report';
await reportPlanGddApproval(approval);
await reportPlanningOutputs(project.path);
phase = 'complete';
console.log(
approval.decided
? '\n立项策划链路已收束:Fast GDD 已批准,策划产物见上方清单。'
: '\n立项策划链路已收束:Run 正常结束但没有待决定审批,策划产物见上方清单。',
);
break session;
}
phase = 'artifact-validation';
const requireEditorImages = await hasConfiguredEditorApiKey(
runtimeConfig.path,
);
@@ -7,12 +7,10 @@ import type {
} from '../../app/types';
/**
* 立项策划唯一的产品产物,由 `plan.submit_gdd` 与审批回执渲染到项目内。
* 立项策划唯一的产品产物,由策划会话提交与审批回执渲染到项目内。
*
* 权威定义在 Rust 侧 `planning_storage.rs` 的 `PLAN_FAST_GDD_PATH`,那里同时管着渲染
* 落盘和写入守卫。跨语言没有共享常量的通道,这里只能重复一份;改路径时处要一起动
* `PLAN_FAST_GDD_PATH`、这里、以及 `scripts/agent-swarm-test-chat.mjs` 的
* `planningOutputPaths`。
* 权威定义在 Rust 侧 `planning_gdd_model.rs` 的 `PLAN_FAST_GDD_PATH`,那里同时管着渲染
* 落盘和写入守卫。跨语言没有共享常量的通道,这里只能重复一份;改路径时处要一起动
*/
const PLAN_FAST_GDD_RELATIVE_PATH = 'game/fast_gdd.md';
@@ -26,7 +26,6 @@ import {
cleanupSwarmTestProject,
cleanupSwarmTestRuntimeConfig,
configFileName,
defaultRealSwarmPlanTask,
defaultRealSwarmTestTask,
defaultRuntimeConfigDirCandidates,
discoverRuntimeConfigDir,
@@ -36,12 +35,9 @@ import {
inspectSwarmProjectArtifacts,
localConfigFileName,
nextSwarmAutoPilotReply,
parsePlanGddDecisionOutput,
parsePlanGddStatusOutput,
parseRunnerShutdownOutput,
parseSettledSwarmTurnReport,
parseSwarmTestArguments,
planGddAutoApprovalIsPending,
prepareSwarmTestProject,
prepareSwarmTestRuntimeConfig,
removeDirectoryWithTimeout,
@@ -51,7 +47,6 @@ import {
shouldStartPersistentPreview,
swarmAutoPilotShouldCloseInput,
swarmAutoPilotSitsAtPrompt,
swarmOutputAwaitsPlanGddApproval,
terminateChildTree,
testProjectPrefix,
testProjectSentinelName,
@@ -658,7 +653,6 @@ describe('Swarm test argument parsing', () => {
keepProject: false,
openBrowser: true,
task: null,
plan: false,
timeoutMinutes: null,
dryRun: false,
help: false,
@@ -679,7 +673,6 @@ describe('Swarm test argument parsing', () => {
'--no-open',
'--task',
'生成一款可试玩的塔防游戏',
'--plan',
'--timeout-minutes',
'75',
'--dry-run',
@@ -691,7 +684,6 @@ describe('Swarm test argument parsing', () => {
keepProject: true,
openBrowser: false,
task: '生成一款可试玩的塔防游戏',
plan: true,
timeoutMinutes: 75,
dryRun: true,
help: true,
@@ -700,7 +692,7 @@ describe('Swarm test argument parsing', () => {
expect(() => parseSwarmTestArguments(['--task', ''])).toThrowError();
});
it('auto-answers only the prompts the plan lane actually blocks on', () => {
it('auto-answers only the confirmation and choice prompts', () => {
expect(
nextSwarmAutoPilotReply(
'[待确认] agent=project-supervisor run=r action=a tool=agent.delegate\n输入 approve 或 reject',
@@ -740,75 +732,6 @@ describe('Swarm test argument parsing', () => {
).toBe(false);
});
it('triggers the approval while the chat CLI is still running', () => {
// Run 停在审批位时状态是 waiting-for-user-input,而 swarm CLI 把这个状态算作
// 「本轮还在跑」,turn 永远不 settle。等 CLI 退出再审批那一步根本到不了,所以
// 触发必须认这条运行期状态行。
expect(
swarmOutputAwaitsPlanGddApproval(
'[状态] project-supervisor running/waiting-for-user-input run=r queue=0/1/0/0 | 等待 Fast GDD 审批决定',
),
).toBe(true);
// 另外三个 plan_gdd blocked 子状态各有自己的文案,都不该触发审批。
expect(
swarmOutputAwaitsPlanGddApproval(
'[状态] project-supervisor running/planning | 推进本根 Run 的 Fast GDD 提交',
),
).toBe(false);
expect(
swarmOutputAwaitsPlanGddApproval(
'[状态] project-supervisor running/planning | 等待 Fast GDD 审批投影收尾',
),
).toBe(false);
expect(
swarmOutputAwaitsPlanGddApproval(
'[状态] project-supervisor needs-reconciliation | Fast GDD 审批投影需要人工核对',
),
).toBe(false);
});
it('reads the headless Fast GDD approval projection off the CLI output', () => {
const state = parsePlanGddStatusOutput(
[
'plan.gdd.status',
'planGddStateJson={"state":"ready_for_approval","pendingApproval":{"gddRef":{"gddId":"gdd-1","version":1,"fingerprint":"fp"},"pendingActionId":"action-1","approvalRequestId":"approval-1"},"session":{"clarificationRound":2,"phase":"awaiting_gdd_approval"}}',
'',
].join('\n'),
);
expect(state.state).toBe('ready_for_approval');
expect(state.session.clarificationRound).toBe(2);
expect(planGddAutoApprovalIsPending(state)).toBe(true);
// CLI 在 Windows 上按行输出会带 CR,解析必须先剥掉再判前缀。
expect(
parsePlanGddStatusOutput('planGddStateJson={"state":"approved"}\r\n')
.state,
).toBe('approved');
expect(
parsePlanGddDecisionOutput(
'plan.gdd.decided\nplanGddDecisionJson={"outcome":"decided","decisionRef":{"version":1}}',
).outcome,
).toBe('decided');
});
it('auto-approves only when the projection really carries a pending card', () => {
// 审批状态是「有没有待决定」的唯一权威。没有待决定时自动批准会把一次空跑
// 报成通过,所以这里必须 false 而不是继续往下走。
expect(
planGddAutoApprovalIsPending({
state: 'approved',
pendingApproval: null,
}),
).toBe(false);
expect(planGddAutoApprovalIsPending({ state: 'draft' })).toBe(false);
expect(planGddAutoApprovalIsPending(null)).toBe(false);
// 缺前缀或 JSON 坏掉都要报错,不能静默当成「没有待决定」。
expect(() => parsePlanGddStatusOutput('plan.gdd.status')).toThrowError();
expect(() =>
parsePlanGddStatusOutput('planGddStateJson={oops'),
).toThrowError();
expect(() => parsePlanGddDecisionOutput('plan.gdd.decided')).toThrowError();
});
it('keeps persistent preview only for manual chat mode', () => {
expect(shouldStartPersistentPreview(parseSwarmTestArguments([]))).toBe(
true,
@@ -818,32 +741,6 @@ describe('Swarm test argument parsing', () => {
parseSwarmTestArguments(['--task', '生成一款可试玩的塔防游戏']),
),
).toBe(false);
// 做方案链路不出可试玩产物,交互模式同样不该起预览。
expect(
shouldStartPersistentPreview(parseSwarmTestArguments(['--plan'])),
).toBe(false);
});
it('bounds the plan lane to its own five-minute design target', () => {
// 交互模式也要有上限:立项策划没有可试玩产物可等,卡住就该早失败。
expect(resolveSwarmTestTimeoutMs(parseSwarmTestArguments(['--plan']))).toBe(
6 * 60_000,
);
expect(
resolveSwarmTestTimeoutMs(
parseSwarmTestArguments([
'--plan',
'--task',
'做一个解谜游戏的立项方案',
]),
),
).toBe(6 * 60_000);
// 显式 --timeout-minutes 仍然压过默认值。
expect(
resolveSwarmTestTimeoutMs(
parseSwarmTestArguments(['--plan', '--timeout-minutes', '20']),
),
).toBe(20 * 60_000);
});
it('applies a bounded default only to non-interactive tasks', () => {
@@ -1964,12 +1861,6 @@ describe('package script registration', () => {
expect(rootPackage.scripts?.['agc:test:chat:manual']).toBe(
'npm --prefix apps/ai-game-creator-shell run test:chat:manual --',
);
expect(rootPackage.scripts?.['agc:test:plan']).toBe(
'npm --prefix apps/ai-game-creator-shell run test:plan --',
);
expect(rootPackage.scripts?.['agc:test:plan:manual']).toBe(
'npm --prefix apps/ai-game-creator-shell run test:plan:manual --',
);
expect(appPackage.scripts?.['test:chat']).toBe(
`node scripts/agent-swarm-test-chat.mjs --task "${defaultRealSwarmTestTask}" --no-open`,
);
@@ -1979,12 +1870,6 @@ describe('package script registration', () => {
expect(appPackage.scripts?.['test:chat:manual']).toBe(
'node scripts/agent-swarm-test-chat.mjs',
);
expect(appPackage.scripts?.['test:plan']).toBe(
`node scripts/agent-swarm-test-chat.mjs --plan --task "${defaultRealSwarmPlanTask}"`,
);
expect(appPackage.scripts?.['test:plan:manual']).toBe(
'node scripts/agent-swarm-test-chat.mjs --plan',
);
expect(checkConfigSource).toMatch(
/packageConfig\.scripts\?\.config\s*!==\s*'node scripts\/game-creator-config-wizard\.mjs'/u,
);
@@ -8156,3 +8156,11 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
- 配置契约提升为 `game-creator-config.v2`:新模板默认 `stream=true`、受控搜索开启;无版本旧 DirectProject 配置仅在省略 `webSearchEnabled` 时把历史默认补为开启,旧配置显式 `false` 不覆盖,v2 显式 `false` 同样保留;Provider / Anthropic 未提供搜索覆盖时保持关闭。本地覆盖配置只补 schema 版本,不凭不完整 overlay 推断或写入 `agentMode` / 搜索布尔值;未知版本失败关闭。
- 搜索结果始终是不可信外部输入,只可作为资料;工具 schema、参数、客户端权限、Agent 身份、系统规则和工具协议不得由网页内容修改。搜索结果与状态投影不携带 API Key、请求头、宿主绝对路径或 Provider 原始错误正文。
- 验证锁定:`configuration``direct_tools_mcp``direct_tool_bridge``codex_app_server` 定向 Rust 测试,以及前端状态格式化 / AppSurface 测试;真实 Provider 登录态与真实公网搜索仍需单独现场 smoke。
## 2026-09-08 策划 V1 链路源码整体退役(四不写)
- 策划会话 Runtime V2 全量接管“做方案”入口后,旧 V1 链路(`project-supervisor-plan` 根 Run、`project-planning` 子 Agent、`plan.submit_gdd` 工具、Fast GDD 审批门禁与恢复车道)按四不写原则整体删除源码,不保留兼容实现、墓碑注释或防御性测试。
- 删除范围:`runtime_protocol` 六个 V1 模块与四条 V1 提示词、prompt manifest planning 目录与生成常量、`capabilities.planning` 配置开关、CLI `--swarm-chat --plan``--plan-gdd-status/--plan-gdd-decide`、provider_retry 的 planning session binding、provider_action_batch 的 V1 提交批次校验、main_loop/pending_recovery/recovery_scan/acceptance_graph 的 plan 根特判、agent_db 三条 V1 专用持久车道(`agent.runtime.plan.provider_usage``agent.runtime.plan.gdd_decided``agent.runtime.plan_submit_gdd.committed`)及预留配额、前端旧 V1 读模型、`scripts/agent-swarm-test-chat.mjs --plan``test:plan*` 脚本。
- 保留项:V2 与 V1 共用的 GDD 数据模型收敛到 `runtime_protocol/planning_gdd_model.rs``.agent/planning/` 旧文件不迁移、不删除,旧 `fast_gdd.md` 仍可只读打开;前端 `PROJECT_SUPERVISOR_PLAN_SOURCE` 字符串与 `GddApprovalCard.tsx``PlanGddSurface`)是 V2 现役的适配/展示面,不属于退役对象。
- 旧 sidecar 中带已删字段(`clarification``continuation``requestId``turnId``pendingApproval` 等)的记录会因 `deny_unknown_fields` 拒绝反序列化,这是退役语义的一部分,不做迁移。
- 测试基线说明:收尾时测试套件存在 25 个既有失败(mock LLM 时序敏感类,HEAD worktree 对照验证与本次无关),后续清理时不要误记到本次退役头上。
@@ -1,6 +1,7 @@
# 立项策划 AgentFast GDD)技术方案
- 日期:2026-08-10
- 状态:**已退役**。本文描述的 V1 策划链路(`project-supervisor-plan` 根 Run、`project-planning` 子 Agent、`plan.submit_gdd` 工具、Fast GDD 审批门禁与恢复机制)已由策划会话 Runtime V2 取代,源码已于 2026-09 按四不写原则整体删除;现行方案见 `【技术方案】策划会话RuntimeV2接入与旧链路退役-2026-09-03.md`。本文仅作为历史推导记录保留。
- 适用范围:AI 游戏创作独立 App、Project Supervisor、Agent Runtime、本地项目策划 sidecar 与后续完整构建准入
> 当前口径(2026-08-30):以本文件中标注的 D11 / 最新修订和当前 `apps/ai-game-creator-shell` 实现为准。D6~D9 等被明确标注为作废或被取代的段落仅保留推导背景,不得作为现行拓扑、入口或 Runtime 真相;产品入口与 DirectProject 总体口径见 `docs/README.md` 和 App 实施计划。
@@ -1,7 +1,7 @@
# 策划会话 Runtime V2 接入与旧链路退役方案
- 日期:2026-09-03
- 状态:P0 合同冻结、P1 内核、P2 产物闭环、P3 入口/UI 接入P4 灰度回归验收已完成P5 待实施;本文是新生产实现的目标方案与阶段验收合同
- 状态:P0 合同冻结、P1 内核、P2 产物闭环、P3 入口/UI 接入P4 灰度回归验收与 P5 旧链路退役均已完成;本文是新生产实现的目标方案与阶段验收合同
- 适用范围:AGC 桌面 App 的“做方案”入口、策划会话、GDD 产物与审批
> 本文规定新策划 Agent 的生产接入和旧链路退役方式。P3 开始修改正式 AGC 入口与工作台,但旧 `project-supervisor-plan` / `project-planning` 源码仍保留,直到 P5 完成退役;V2 切换时旧链路直接封存,所有未完成旧会话强制失败,旧 Fast GDD 文档之后只作为历史记录依据。
@@ -737,7 +737,7 @@ hydrate_planning_session_v2
- 前端通过 V2 适配层复用现有聊天区、澄清卡、GDD 审批卡和阶段进度条;V2 Session 的 hydrate 结果额外携带 `conversation`,用于刷新/重启恢复历史消息。
- V2 只将结构化工具调用结果投影到澄清选项卡和 GDD 审批卡;旧 Runtime 轮询、专业 Agent 轮询和旧 Runtime 事件在 V2 策划会话中关闭。Provider 中间文本不作为策划用户对话展示。
- 对已存在 V2 Session 的项目,打开项目时先 hydrate V2;没有 V2 authority 的旧项目继续走旧读取路径,避免误把旧项目数据当成 V2。
- P3 已完成;P4 的真实 Provider、前端工作台、失败恢复和安全门禁验收已通过。旧会话 `legacy_retired` 封存旧入口彻底关闭仍属于 P5
- P3 已完成;P4 的真实 Provider、前端工作台、失败恢复和安全门禁验收已通过。旧会话 `legacy_retired` 封存旧入口关闭与旧 Runtime 源码删除均已在 P5 完成
P3 之后的协议修正:V2 不再用正文 JSON 输出问询/GDDProvider 请求挂 `plan_ask_question` / `plan_submit_gdd``tool_choice=required`,形状由工具 schema 承担。system prompt 只保留三项核心闭环等策略和当前问询进度;数量和字数由既有校验器在失败时回灌。`plan_submit_gdd` 的工具参数只包含决定和原型验证内容,不包含任何 Runtime 分配的 ID;Runtime 在落盘前为决定分配首项 `initial-request`、后续 `decision-{序号}`,并按 `prototype_pending` 顺序绑定原型验证项。入参不必回声 `schemaVersion`,落盘 GDD 仍写 `plan-gdd.v2`。失败结果不重复渲染,严格解析和失败不落盘成功产物的规则保持不变。历史 V2 数据不做兼容转换,按现有恢复/失败策略处理。
@@ -767,10 +767,17 @@ P3 之后的协议修正:V2 不再用正文 JSON 输出问询/GDDProvider
依赖:P3。
### P5:旧链路退役(最简方案
### P5:旧链路退役(已完成
目标:停止新业务进入旧 Supervisor,并将所有未完成旧会话一次性封存为失败。
执行情况(2026-09):在 P5-1~P5-4 的“入口关闭、会话封存、产物只读”之上,进一步按四不写原则删除了旧 V1 Runtime 源码本体,避免退役代码继续占用编译与维护成本:
- 前端数据通路与命令入口:`planGddState/planGddDecision` 读模型、`--plan-gdd-status/--plan-gdd-decide` CLI 等旧 V1 投影入口。
- Runtime 源码:`runtime_protocol``planning_storage/planning_submit/planning_approval/planning_coordinator/planning_hydrate/planning_provider_usage` 六个模块、prompt manifest 中 planning Agent 目录与 plan sections、四个 V1 提示词文件、`game-creator.config.json``capabilities.planning` 开关、CLI `--swarm-chat --plan` 入口、provider_retry 的 planning session binding、provider_action_batch 的 V1 提交批次校验、tool_policy/main_loop/pending_recovery/recovery_scan/acceptance_graph 中的 plan 根特判与恢复车道、agent_db 的三条 V1 专用持久车道(`agent.runtime.plan.provider_usage``agent.runtime.plan.gdd_decided``agent.runtime.plan_submit_gdd.committed`)及对应测试。
- 测试脚本:`scripts/agent-swarm-test-chat.mjs``--plan` 模式与自动 GDD 审批回路、根/应用 `package.json``test:plan*` / `agc:test:plan*` 脚本。
- 保留项:V2 与 V1 共用的 GDD 数据模型抽到 `planning_gdd_model.rs` 继续复用;`.agent/planning/` 旧文件保持原样只读(不迁移、不删除);`fast_gdd.md` 仍可通过既有命令打开查看;前端 `GddApprovalCard.tsx``PlanGddSurface`)是 V2 现役的 GDD 展示面,不属于退役对象。
任务:
| ID | 任务 | 产出 |
@@ -176,7 +176,9 @@ error=unable to locate image at `<project>/memory/README.md`: (os error 2)
---
## 4. 缺陷 4:普通 action 批次带 plan update 时预检自相矛盾(已修)
## 4. 缺陷 4:普通 action 批次带 plan update 时预检自相矛盾(已修2026-09 随策划 V1 退役删除相关代码
> 本节引用的 `plan.submit_gdd` 特判、`provider_batch_plan_update` 与 `planning_session_binding` 字段属于已退役的策划 V1 链路,2026-09 已按四不写原则整体删除;回归测试 `supervisor_collaboration_batch_keeps_plan_update_off_members` 保留并收敛为「非 planning 批次的 plan update 不落入成员」这一不变量。
换成 `gpt-5.6-terra` 后 LLM 链路可用,但首次复现曾在第二步「委派 project-planning 产出 Fast GDD」失败。这一条与 Provider 无关,是当时实现引入、现已修复的回归。
-2
View File
@@ -166,8 +166,6 @@
"agc:test": "npm --prefix apps/ai-game-creator-shell run agent-runtime:supervisor-autonomous-playable-lane-defense-deterministic-e2e --",
"agc:test:chat": "npm --prefix apps/ai-game-creator-shell run test:chat --",
"agc:test:chat:manual": "npm --prefix apps/ai-game-creator-shell run test:chat:manual --",
"agc:test:plan": "npm --prefix apps/ai-game-creator-shell run test:plan --",
"agc:test:plan:manual": "npm --prefix apps/ai-game-creator-shell run test:plan:manual --",
"ai-game-creator-shell:dev": "npm --prefix apps/ai-game-creator-shell run dev",
"ai-game-creator-shell:dev-server": "npm --prefix apps/ai-game-creator-shell run dev-server",
"ai-game-creator-shell:build": "npm --prefix apps/ai-game-creator-shell run build --",