diff --git a/.env.example b/.env.example index ac31e1dd9..60dc47b7e 100644 --- a/.env.example +++ b/.env.example @@ -237,14 +237,10 @@ GENARRATIVE_ENABLE_IMAGE_EDITOR_AGENT_SIDEBAR="false" # 官网客户端下载检测渠道:dev、release 或自定义渠道;修改后重启 API 服务。 # Windows/macOS 是系统维度,不填写 dev-win/dev-mac。 +# 客户端埋点也复用该渠道:dev 对应 https://dev.genarrative.world,release 对应 https://www.genarrative.world。 +# 埋点不接受其它渠道;本地 dev 且 GENARRATIVE_ENV 为 development(默认)/test/container 时允许 loopback 地址及可变端口。 GENARRATIVE_CLIENT_DOWNLOAD_CHANNEL="dev" -# 客户端埋点接收绑定的公开 origin,由 API Server 运行时读取;修改后重启服务。 -# 必须与客户端登录地址一致,不带 /api、路径或尾部斜杠;未配置/非法时上传接口返回 503。 -# 本地端口按实际启动结果填写(端口漂移后需同步),localhost 与 127.0.0.1 不可混用。 -# dev 使用 https://dev.genarrative.world;release 使用 https://www.genarrative.world。 -GENARRATIVE_AGC_ANALYTICS_ORIGIN="http://127.0.0.1:8082" - # Optional: official VikingDB credentials for regenerating build-tag similarities # with the Python embedding script. The script auto-loads `.env.local` and uses # the fixed `bge-large-zh` embedding model. diff --git a/CONTEXT.md b/CONTEXT.md index 0ab68ba28..d2c271f90 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -194,6 +194,14 @@ _Avoid_: 进度通知、快照轮询、第二套历史 把项目对话历史条目与运行态事件转换成消息气泡和工具卡片的读取期转换;不持久化,也不构成事实源。 _Avoid_: 投影缓存文件、已脱敏卡片库、第二套 reducer +**项目对话输入**: +AGC 项目对话的输入只有自然语言回合(含 `@` 素材引用与附件);需要动作时由 Runtime 工具与确认卡承接,不从输入文本解析控制词。 +_Avoid_: 斜杠命令、聊天命令草稿、命令发现列表 + +**项目命令 id**: +AGC 运行期工具与项目权限策略使用的稳定标识(`GAME_CREATION_APP_COMMANDS` 与 `GameCreationAppPermission`),由 Rust 运行期策略校验与 App 权限审计 / 项目前置条件判定消费,不是用户输入语法。 +_Avoid_: 把命令 id 当作可输入的聊天命令、为权限位补聊天入口 + **引用候选**: 输入区可以命中的对象集合(`@` 素材、`$` Skill),由宿主按种类注入;输入区不判断候选属于哪一类。 _Avoid_: 输入区自己读项目清单或应用目录、把候选取值写死在组件里 diff --git a/apps/admin-web/src/pages/AdminAgcTemplatesPage.test.tsx b/apps/admin-web/src/pages/AdminAgcTemplatesPage.test.tsx index 667e44af5..11c3f5493 100644 --- a/apps/admin-web/src/pages/AdminAgcTemplatesPage.test.tsx +++ b/apps/admin-web/src/pages/AdminAgcTemplatesPage.test.tsx @@ -339,7 +339,10 @@ test.each([409, 503])( await confirmWrite(); const message = await screen.findByRole('alert'); const feedback = message.parentElement!; - expect(document.activeElement).toBe(feedback); + // 聚焦发生在 React passive effect 里(AdminAgcTemplatesPage 的 feedback 聚焦 useEffect), + // 而 findByRole 在 alert 节点一挂上就返回,可能早于该 effect 执行;这里等聚焦落地, + // 避免在 CI 负载下抢跑。断言口径不变:焦点最终必须落在提示区而不是弹窗面板。 + await waitFor(() => expect(document.activeElement).toBe(feedback)); expect(feedback.tabIndex).toBe(-1); expect(focus).toHaveBeenCalledWith({ preventScroll: true }); expect(viewport.scrollTop).toBe(20); diff --git a/apps/ai-game-creator-shell/package.json b/apps/ai-game-creator-shell/package.json index 385f52277..c9c7a2d3a 100644 --- a/apps/ai-game-creator-shell/package.json +++ b/apps/ai-game-creator-shell/package.json @@ -15,20 +15,10 @@ "skill-pack:test": "node --test scripts/check-skill-pack.test.mjs", "llm-status": "node scripts/run-cli-with-config.mjs --llm-status", "agent-task": "node scripts/run-cli-with-config.mjs --agent-task", - "chat": "node scripts/run-cli-with-config.mjs --swarm-chat", - "swarm": "node scripts/run-cli-with-config.mjs --swarm-chat", "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", "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", - "agent-runtime:collaboration-policy-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-collaboration-policy-mixed-recovery", - "agent-runtime:mixed-swarm-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-static-isolated-autonomous-chat", - "agent-runtime:supervisor-swarm-autonomous-chat-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-autonomous-chat", - "agent-runtime:supervisor-autonomous-playable-lane-defense-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-autonomous-playable-lane-defense", - "agent-runtime:supervisor-autonomous-playable-lane-defense-deterministic-e2e": "node scripts/agent-runtime-deterministic-playable-e2e.mjs", - "agent-runtime:supervisor-autonomous-playable-lane-defense-deterministic-self-test": "node scripts/agent-runtime-deterministic-playable-e2e.mjs --self-test", "agent-runtime:supervisor-swarm-transient-retry-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-transient-retry", "agent-runtime:supervisor-swarm-final-reply-transient-retry-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-final-reply-transient-retry", "agent-runtime:supervisor-swarm-tool-plan-handoff-runner-kill-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-tool-plan-handoff-runner-kill", diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-deterministic-playable-e2e.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-deterministic-playable-e2e.mjs deleted file mode 100644 index ef18c3780..000000000 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-deterministic-playable-e2e.mjs +++ /dev/null @@ -1,2030 +0,0 @@ -import { spawn } from 'node:child_process'; -import { createHash, randomUUID } from 'node:crypto'; -import fs from 'node:fs/promises'; -import os from 'node:os'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; - -import { - createDeterministicLaneDefenseRouter, - deterministicLaneDefenseCanonicalHtml, - deterministicLaneDefenseInitialHtml, - deterministicLaneDefenseModel, - deterministicManifestReadyAgentIds, - hiddenCanvasCss, - startDeterministicLaneDefenseProvider, - visibleCanvasCss, -} from './deterministic-lane-defense-provider.mjs'; -import { withLoopbackNoProxy } from './llm-transient-fault-proxy.mjs'; - -const appRoot = path.resolve(fileURLToPath(new URL('..', import.meta.url))); -const repoRoot = path.resolve(appRoot, '../..'); -const realE2eScript = path.join(appRoot, 'scripts/agent-runtime-real-e2e.mjs'); -const suite = 'supervisor-autonomous-playable-lane-defense'; -const wrapperSuite = - 'supervisor-autonomous-playable-lane-defense-deterministic'; -const configFileName = 'game-creator.config.json'; -const configSentinelName = '.deterministic-provider-e2e.json'; -const configSentinelSchema = 'genarrative-deterministic-provider-e2e-config.v1'; -const platformSessionFixtureEnv = 'GENARRATIVE_AGC_PLATFORM_SESSION_FIXTURE'; -const platformSessionFixtureName = '.deterministic-platform-session.json'; -const platformSessionFixtureSchema = - 'genarrative-agc-platform-session-fixture.v1'; -const outputLimit = 32 * 1024 * 1024; - -function hashValue(value) { - return createHash('sha256').update(value).digest('hex'); -} - -function assert(condition, code) { - if (condition) return; - const error = new Error(code); - error.code = code; - throw error; -} - -function parseArguments(args) { - let keepProject = false; - let selfTest = false; - for (const arg of args) { - if (arg === '--keep-project') keepProject = true; - else if (arg === '--self-test') selfTest = true; - else throw new Error('unknown-argument'); - } - assert(!(keepProject && selfTest), 'self-test-keep-project-conflict'); - return { keepProject, selfTest }; -} - -function deterministicRuntimeConfig(apiKey, provider) { - return { - agentMode: 'provider', - editorApi: { - apiKey, - baseUrl: provider.editorBaseUrl, - }, - llm: { - apiKey, - baseUrl: provider.baseUrl, - model: deterministicLaneDefenseModel, - apiKind: 'openai_chat', - reasoningEffort: 'max', - stream: false, - requestTimeoutMs: 30_000, - maxRetries: 0, - retryBackoffMs: 100, - }, - agentLlm: { - 'project-supervisor': { - reasoningEffort: 'max', - }, - }, - }; -} - -function deterministicPlatformSessionFixture(apiKey, provider) { - return { - schemaVersion: platformSessionFixtureSchema, - userId: 'deterministic-e2e-user', - accessToken: apiKey, - apiBaseUrl: provider.editorBaseUrl, - generation: 1, - }; -} - -function appendBounded(current, chunk) { - const combined = Buffer.concat([current, chunk]); - if (combined.length > outputLimit) throw new Error('child-output-too-large'); - return combined; -} - -function runChild(args, environment) { - return new Promise((resolve) => { - const child = spawn(process.execPath, args, { - cwd: appRoot, - env: environment, - stdio: ['ignore', 'pipe', 'pipe'], - }); - let stdout = Buffer.alloc(0); - let stderr = Buffer.alloc(0); - let outputError = null; - child.stdout.on('data', (chunk) => { - try { - stdout = appendBounded(stdout, chunk); - } catch (error) { - outputError = error; - child.kill('SIGTERM'); - } - }); - child.stderr.on('data', (chunk) => { - try { - stderr = appendBounded(stderr, chunk); - } catch (error) { - outputError = error; - child.kill('SIGTERM'); - } - }); - child.once('error', (error) => - resolve({ code: null, signal: null, error, stdout, stderr }), - ); - child.once('close', (code, signal) => - resolve({ code, signal, error: outputError, stdout, stderr }), - ); - }); -} - -function safeChildDiagnostic(result) { - return { - exitCode: Number.isInteger(result.code) ? result.code : null, - signal: typeof result.signal === 'string' ? result.signal : null, - errorCode: - typeof result.error?.code === 'string' ? result.error.code : null, - stdoutBytes: result.stdout.length, - stdoutSha256: hashValue(result.stdout), - stderrBytes: result.stderr.length, - stderrSha256: hashValue(result.stderr), - }; -} - -function parseChildReport(result) { - try { - const report = JSON.parse(result.stdout.toString('utf8')); - assert(report && typeof report === 'object', 'child-report-root-invalid'); - return report; - } catch (error) { - if (error?.code) throw error; - const wrapped = new Error('child-report-json-invalid'); - wrapped.code = 'child-report-json-invalid'; - throw wrapped; - } -} - -function expectedProviderStats(stats) { - const manifestReadyTasks = manifestReadyTaskEvidence(stats); - const projectSupervisorRuntimePlanningCount = - Number.isInteger(stats.byAgent?.['project-supervisor']?.planning) && - Number.isInteger(stats.interactionExecuteCount) - ? stats.byAgent['project-supervisor'].planning - - stats.interactionExecuteCount - : null; - return ( - Number.isInteger(stats.requestCount) && - stats.requestCount >= 40 && - stats.planningRequestCount + - stats.finalReplyRequestCount + - (stats.contextCompactionRequestCount ?? 0) + - (stats.imageInspectionRequestCount ?? 0) === - stats.requestCount && - stats.finalReplyRequestCount === 0 && - stats.interactionExecuteCount === 1 && - stats.goalContractCount === 1 && - stats.actionHistoryCount === 1 && - stats.acceptanceUpdateCount === 1 && - stats.delegateActionCount === 0 && - stats.runStatusCount >= 1 && - stats.sourceWriteCount >= 7 && - stats.staticSmokeCount >= 4 && - stats.previewValidationCount >= 2 && - manifestReadyTasks.exactlyOnce && - stats.manifestReadyTaskFileWriteCount >= 7 && - stats.manifestReadyTaskPreviewValidationCount >= 1 && - stats.manifestReadyTaskCanvasGenerationCount >= - stats.canvasGenerationRequestCount && - stats.manifestReadyTaskCanvasGenerationCount <= - stats.canvasGenerationRequestCount + 16 && - stats.imageInspectionRequestCount === 1 && - stats.canvasGenerationRequestCount === 3 && - stats.canvasDownloadRequestCount === 3 && - stats.canvasSliceDownloadRequestCount === 4 && - stats.canvasGeneratedAspectRatios?.['16:9'] === 1 && - stats.canvasGeneratedAspectRatios?.['1:1'] === 2 && - stats.canonicalCodeRunCount === 1 && - stats.canonicalCodeAssetListCount === 1 && - stats.unexpectedRequestCount === 0 && - Object.keys(stats.rejectionCodes ?? {}).length === 0 && - projectSupervisorRuntimePlanningCount >= 5 && - stats.byAgent?.['project-supervisor']?.finalReply === 0 && - stats.byAgent?.['project-supervisor']?.projectMutation === 0 && - stats.byAgent?.['code-prototype']?.planning >= 5 && - stats.byAgent?.['code-prototype']?.finalReply === 0 && - stats.byAgent?.['code-prototype']?.projectMutation === 1 && - stats.byAgent?.['quality-review']?.finalReply === 0 && - stats.byAgent?.['quality-review']?.projectMutation === 0 - ); -} - -function manifestReadyTaskEvidence(stats) { - const rawCounts = stats?.readyTaskCountsByAgent; - const byAgent = - rawCounts && typeof rawCounts === 'object' && !Array.isArray(rawCounts) - ? Object.fromEntries( - Object.entries(rawCounts) - .sort(([left], [right]) => left.localeCompare(right)) - .map(([agentId, counts]) => [ - agentId, - { - run: counts?.run ?? null, - completion: counts?.completion ?? null, - }, - ]), - ) - : {}; - const actualAgentIds = Object.keys(byAgent); - const expectedAgentIds = [...deterministicManifestReadyAgentIds].sort(); - const exactAgentSet = - actualAgentIds.length === expectedAgentIds.length && - actualAgentIds.every( - (agentId, index) => agentId === expectedAgentIds[index], - ); - const runTotal = Object.values(byAgent).reduce( - (total, counts) => total + (Number.isInteger(counts.run) ? counts.run : 0), - 0, - ); - const completionTotal = Object.values(byAgent).reduce( - (total, counts) => - total + (Number.isInteger(counts.completion) ? counts.completion : 0), - 0, - ); - const perAgentExactlyOnce = - exactAgentSet && - expectedAgentIds.every( - (agentId) => - byAgent[agentId]?.run === 1 && byAgent[agentId]?.completion === 1, - ); - return { - expectedAgentIds, - byAgent, - runTotal, - completionTotal, - exactAgentSet, - perAgentExactlyOnce, - exactlyOnce: - perAgentExactlyOnce && - runTotal === deterministicManifestReadyAgentIds.length && - completionTotal === deterministicManifestReadyAgentIds.length && - stats?.manifestReadyTaskRunCount === - deterministicManifestReadyAgentIds.length && - stats?.manifestReadyTaskCompletionCount === - deterministicManifestReadyAgentIds.length, - }; -} - -const requiredZeroChildEvidenceFields = Object.freeze([ - 'activeRunnerKillCount', - 'approveInputCount', - 'answerInputCount', - 'steerInputCount', - 'turnReportWaitingForConfirmationCount', - 'turnReportWaitingForUserInputCount', - 'turnReportReconciliationAgentCount', - 'providerLifecycleFailedCount', - 'openProviderLifecycleCount', - 'pendingActionCount', - 'confirmationSidecarCount', - 'userInputSidecarCount', - 'providerActionBatchSidecarCount', - 'providerRetrySidecarCount', - 'providerHandoffSidecarCount', - 'toolPlanHandoffSidecarCount', - 'finalizationJournalCount', - 'reconciliationResidueCount', -]); - -function expectedChildReport(report, options, providerStats) { - const evidence = report?.evidence; - const providerRequestCount = providerStats?.requestCount; - const runtimeProviderRequestCount = - Number.isInteger(providerRequestCount) && - Number.isInteger(providerStats?.interactionExecuteCount) && - Number.isInteger(providerStats?.imageInspectionRequestCount) - ? providerRequestCount - - providerStats.interactionExecuteCount - - providerStats.imageInspectionRequestCount - : null; - return ( - report?.status === 'PASS' && - report?.suite === suite && - report?.errorCount === 0 && - Array.isArray(report?.blocked) && - report.blocked.length === 0 && - report?.cleanup?.performed === !options.keepProject && - report?.cleanup?.kept === options.keepProject && - evidence?.evidenceCompleteness === 'complete' && - evidence?.dedicatedZeroInterventionPath === true && - evidence?.stdinTaskCount === 1 && - evidence?.stdinEndedAfterTask === true && - evidence?.turnReportOutcome === 'settled' && - evidence?.parentTaskStatus === 'completed' && - evidence?.parentRuntimeStatus === 'idle' && - evidence?.parentRuntimePhase === 'completed' && - evidence?.laneDefensePlaytestPassed === true && - evidence?.laneDefenseAssertionCount === 37 && - evidence?.laneDefensePassedAssertionCount === 37 && - evidence?.browserValidationPassed === true && - evidence?.staticSmokePassed === true && - evidence?.gameIndexChanged === true && - Number.isInteger(evidence?.projectRevisionDelta) && - evidence.projectRevisionDelta > 0 && - evidence?.finalSupervisorAssistantCount === 1 && - evidence?.professionalAssistantCount >= 3 && - evidence?.providerRequestIdentityCount === runtimeProviderRequestCount && - evidence?.providerLifecycleStartedCount === runtimeProviderRequestCount && - evidence?.providerLifecycleTerminalCount === runtimeProviderRequestCount && - evidence?.providerLifecycleCompletedCount === runtimeProviderRequestCount && - requiredZeroChildEvidenceFields.every((field) => evidence?.[field] === 0) - ); -} - -function syntheticPayload(agentId, runId, tools, extraContext = '') { - return { - model: deterministicLaneDefenseModel, - stream: false, - messages: [ - { - role: 'user', - content: `- templateAgentId: ${agentId}\n- runId: ${runId}\n${extraContext}`, - }, - ], - tools: tools.map((name) => ({ type: 'function', function: { name } })), - }; -} - -function syntheticInteractionExecutePayload() { - return { - model: deterministicLaneDefenseModel, - stream: false, - reasoning_effort: 'max', - messages: [ - { - role: 'system', - content: '你现在位于统一的 Agent interaction loop。', - }, - { - role: 'user', - content: '用户这轮输入:\n制作一个三消经营游戏', - }, - ], - tools: ['runtime_execute', 'runtime_resume', 'project_location'].map( - (name) => ({ type: 'function', function: { name } }), - ), - }; -} - -function syntheticUiPrototypeInspectionPayload() { - return { - model: deterministicLaneDefenseModel, - stream: false, - messages: [ - { - role: 'system', - content: '你是游戏界面视觉检查 Agent。只分析画面。', - }, - { - role: 'user', - content: [ - { - type: 'text', - text: '请逐项检查 informationHud、gameplaySurface、objectiveEntities、primaryControls、failureRestartFlow、responsiveLayout、implementationClarity、originalTheme,并只返回严格 JSON。', - }, - { - type: 'image_url', - image_url: { url: 'data:image/png;base64,AA==' }, - }, - ], - }, - ], - }; -} - -function responseFunctionNames(response) { - return response.choices[0].message.tool_calls.map( - (call) => call.function.name, - ); -} - -function responseFunctionCalls(response) { - return response.choices[0].message.tool_calls.map((call) => ({ - name: call.function.name, - arguments: JSON.parse(call.function.arguments), - })); -} - -async function runSelfTest() { - const html = deterministicLaneDefenseInitialHtml(); - assert([...html].length <= 8_000, 'self-test-html-source-budget-invalid'); - assert( - html.includes(hiddenCanvasCss) && - !html.includes(visibleCanvasCss) && - html.includes('playable-web-game-state.v1') && - html.includes('data-playtest-id="next-level"') && - html.includes('Goal: defend the garden and win every wave.') && - html.includes('requestAnimationFrame'), - 'self-test-html-contract-invalid', - ); - const canonicalHtml = deterministicLaneDefenseCanonicalHtml(); - assert( - canonicalHtml.includes(visibleCanvasCss) && - canonicalHtml.includes("atlasArt.src='../assets/art-spritesheet.png'") && - [ - 'player.png', - 'blocks-and-targets.png', - 'obstacles-and-scene.png', - 'feedback-effects.png', - ].every((fileName) => - canonicalHtml.includes(`../assets/art-spritesheet-slices/${fileName}`), - ) && - (canonicalHtml.match(/ctx\.drawImage\(/g) ?? []).length >= 5, - 'self-test-canonical-visible-art-contract-invalid', - ); - const apiKey = `deterministic-self-test-${randomUUID()}`; - const runtimeConfig = deterministicRuntimeConfig(apiKey, { - baseUrl: 'http://127.0.0.1:41001/v1', - editorBaseUrl: 'http://127.0.0.1:41002', - }); - assert( - runtimeConfig.agentMode === 'provider' && - runtimeConfig.llm.apiKind === 'openai_chat' && - runtimeConfig.llm.reasoningEffort === 'max' && - runtimeConfig.agentLlm?.['project-supervisor']?.reasoningEffort === - 'max' && - runtimeConfig.llm.apiKey === apiKey && - runtimeConfig.editorApi.apiKey === apiKey, - 'self-test-provider-runtime-config-invalid', - ); - const captureProviderErrorCode = (operation) => { - try { - operation(); - return null; - } catch (error) { - return error?.code ?? null; - } - }; - const allTools = [ - 'update_agent_plan', - 'respond_to_user', - 'runtime_tool_agent_action_history', - 'runtime_tool_agent_acceptance_update', - 'runtime_tool_agent_run_status', - 'runtime_tool_command_run_limited', - 'runtime_tool_file_patch', - 'runtime_tool_file_write', - 'runtime_tool_preview_validate', - 'runtime_tool_task_list', - ]; - const manifestReadyTools = [ - ...allTools, - 'runtime_tool_asset_list', - 'runtime_tool_file_read', - ]; - const rootRunId = 'parent-run'; - const contractFingerprint = 'a'.repeat(64); - const contractContext = `[Root Goal Contract]\n${JSON.stringify({ contractFingerprint })}`; - const observationContext = (observations) => - `${contractContext}\n已有工具观察:\n${JSON.stringify(observations)}`; - const goalContractTools = ['runtime_tool_agent_goal_contract']; - const goalContractRouter = createDeterministicLaneDefenseRouter({ apiKey }); - goalContractRouter.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticPayload( - 'project-supervisor', - 'goal-contract-order-run', - goalContractTools, - ), - }); - assert( - captureProviderErrorCode(() => - goalContractRouter.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticPayload( - 'project-supervisor', - 'goal-contract-order-run', - goalContractTools, - ), - }), - ) === 'provider-goal-contract-out-of-order', - 'self-test-goal-contract-duplicate-not-rejected', - ); - - const router = createDeterministicLaneDefenseRouter({ apiKey }); - const route = (agentId, runId, tools = allTools, extraContext = '') => - router.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticPayload(agentId, runId, tools, extraContext), - }); - const interactionExecuteResponse = router.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticInteractionExecutePayload(), - }); - const interactionExecuteCalls = responseFunctionCalls( - interactionExecuteResponse, - ); - const interactionExecuteStats = router.getStats(); - assert( - interactionExecuteCalls.length === 1 && - interactionExecuteCalls[0]?.name === 'runtime_execute' && - Object.keys(interactionExecuteCalls[0]?.arguments ?? {}).length === 0 && - interactionExecuteStats.requestCount === 1 && - interactionExecuteStats.planningRequestCount === 1 && - interactionExecuteStats.interactionExecuteCount === 1 && - interactionExecuteStats.manifestReadyTaskRunCount === 0 && - interactionExecuteStats.manifestReadyTaskCompletionCount === 0 && - Object.keys(interactionExecuteStats.readyTaskCountsByAgent).length === 0, - 'self-test-interaction-max-execute-sequence-invalid', - ); - const visualInspectionResponse = router.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticUiPrototypeInspectionPayload(), - }); - const visualInspection = JSON.parse( - visualInspectionResponse.choices[0].message.content, - ); - assert( - Object.keys(visualInspection?.checks ?? {}).join(',') === - 'informationHud,gameplaySurface,objectiveEntities,primaryControls,failureRestartFlow,responsiveLayout,implementationClarity,originalTheme' && - Object.values(visualInspection.checks).every((value) => value === true) && - Array.isArray(visualInspection.issues) && - visualInspection.issues.length === 0 && - router.getStats().imageInspectionRequestCount === 1 && - router.getStats().unexpectedRequestCount === 0, - 'self-test-ui-prototype-inspection-request-not-routed', - ); - const goalContractResponse = route( - 'project-supervisor', - rootRunId, - goalContractTools, - ); - const goalContractCalls = responseFunctionCalls(goalContractResponse); - const acceptanceNodes = - goalContractCalls[0]?.arguments?.input?.acceptanceNodes ?? []; - assert( - goalContractCalls.length === 1 && - goalContractCalls[0]?.name === 'runtime_tool_agent_goal_contract' && - goalContractCalls[0]?.arguments?.input?.outcome?.includes( - '可完整游玩的原创植物塔防网页游戏', - ) && - acceptanceNodes.length === 2 && - acceptanceNodes[0]?.criterionId === 'static-current-revision' && - JSON.stringify(acceptanceNodes[0]?.requiredEvidence) === - JSON.stringify(['tool:command.run_limited']) && - acceptanceNodes[1]?.criterionId === 'playable-current-revision' && - JSON.stringify(acceptanceNodes[1]?.requiredEvidence) === - JSON.stringify(['tool:preview.validate']) && - JSON.stringify(acceptanceNodes[1]?.dependsOn) === - JSON.stringify(['static-current-revision']), - 'self-test-goal-contract-invalid', - ); - assert( - responseFunctionNames( - route( - 'project-supervisor', - rootRunId, - ['runtime_tool_task_list', 'runtime_tool_agent_run_status'], - `${contractContext}\n当前正式 manifest DAG 仍有专业 task 在运行`, - ), - ).join(',') === 'runtime_tool_task_list,runtime_tool_agent_run_status', - 'self-test-manifest-wait-invalid', - ); - - const visualReadyAgentIds = new Set([ - 'art-director', - 'design-foundation', - 'art-asset-plan', - ]); - const canvasCallsByAgent = new Map(); - const callBatchesByAgent = new Map(); - for (const agentId of deterministicManifestReadyAgentIds) { - const runId = `manifest-ready-${agentId}`; - const extraContext = - `处理 manifest ready 任务:${agentId}\n` + `任务 ID:${agentId}`; - const readyTools = visualReadyAgentIds.has(agentId) - ? [ - ...manifestReadyTools, - 'runtime_tool_canvas_asset_generate', - 'runtime_tool_image_inspect', - ] - : manifestReadyTools; - let terminalCompletionCount = 0; - for (let attempt = 0; attempt < 8; attempt += 1) { - const attemptTools = - agentId === 'art-director' && attempt >= 2 - ? ['respond_to_user'] - : readyTools; - const response = route(agentId, runId, attemptTools, extraContext); - const calls = responseFunctionCalls(response); - const batches = callBatchesByAgent.get(agentId) ?? []; - batches.push(calls.map((call) => call.name)); - callBatchesByAgent.set(agentId, batches); - const canvasCall = calls.find( - (call) => call.name === 'runtime_tool_canvas_asset_generate', - ); - if (canvasCall) { - assert( - !canvasCallsByAgent.has(agentId), - `self-test-canvas-generation-duplicate:${agentId}`, - ); - canvasCallsByAgent.set(agentId, canvasCall); - } - if (calls.some((call) => call.name === 'respond_to_user')) { - terminalCompletionCount += 1; - break; - } - } - assert( - terminalCompletionCount === 1, - `self-test-manifest-ready-terminal-invalid:${agentId}`, - ); - } - const artDirectorAssetCall = canvasCallsByAgent.get('art-director'); - const designFoundationAssetCall = canvasCallsByAgent.get('design-foundation'); - const artAssetPlanAssetCall = canvasCallsByAgent.get('art-asset-plan'); - assert( - JSON.stringify(callBatchesByAgent.get('art-asset-plan')) === - JSON.stringify([ - ['runtime_tool_asset_list'], - ['runtime_tool_file_write'], - ['runtime_tool_canvas_asset_generate'], - ['runtime_tool_file_read', 'runtime_tool_asset_list'], - ['update_agent_plan', 'respond_to_user'], - ]), - 'self-test-art-asset-plan-revision-stages-invalid', - ); - assert( - JSON.stringify(callBatchesByAgent.get('code-prototype')) === - JSON.stringify([ - [ - 'runtime_tool_file_read', - 'runtime_tool_file_read', - 'runtime_tool_file_read', - ], - [ - 'runtime_tool_file_read', - 'runtime_tool_file_read', - 'runtime_tool_file_read', - 'runtime_tool_asset_list', - ], - ['runtime_tool_file_write'], - ['runtime_tool_command_run_limited'], - ['update_agent_plan', 'respond_to_user'], - ]), - 'self-test-code-prototype-asset-audit-sequence-invalid', - ); - assert( - canvasCallsByAgent.size === 3 && - artDirectorAssetCall?.arguments?.input?.outputPath === - 'assets/art-spec.png' && - artDirectorAssetCall.arguments.input.aspectRatio === '1:1' && - artDirectorAssetCall.arguments.input.assetKind === 'icon-spec' && - designFoundationAssetCall?.arguments?.input?.outputPath === - 'assets/ui-prototype.png' && - designFoundationAssetCall.arguments.input.aspectRatio === '16:9' && - designFoundationAssetCall.arguments.input.assetKind === 'ui-design' && - artAssetPlanAssetCall?.arguments?.input?.outputPath === - 'assets/art-spritesheet.png' && - artAssetPlanAssetCall.arguments.input.aspectRatio === '1:1' && - artAssetPlanAssetCall.arguments.input.assetKind === 'icon-spritesheet', - 'self-test-visual-assets-invalid', - ); - - const visualRecoveryRouter = createDeterministicLaneDefenseRouter({ apiKey }); - const visualRecoveryRunId = 'visual-recovery-design-foundation'; - const visualRecoveryTools = [ - ...manifestReadyTools, - 'runtime_tool_canvas_asset_generate', - 'runtime_tool_image_inspect', - ]; - const visualRecoveryRoute = ( - tools = visualRecoveryTools, - extraContext = '', - ) => - visualRecoveryRouter.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticPayload( - 'design-foundation', - visualRecoveryRunId, - tools, - `处理 manifest ready 任务:design-foundation\n任务 ID:design-foundation${extraContext}`, - ), - }); - visualRecoveryRoute(); - visualRecoveryRoute(); - const visualRecoveryCanvasResponse = visualRecoveryRoute(); - const visualRecoveryInitialInspectResponse = visualRecoveryRoute(); - assert( - responseFunctionNames(visualRecoveryCanvasResponse).join(',') === - 'runtime_tool_canvas_asset_generate' && - responseFunctionNames(visualRecoveryInitialInspectResponse).includes( - 'runtime_tool_image_inspect', - ), - 'self-test-ui-prototype-initial-inspection-sequence-invalid', - ); - const failedInspectObservation = { - tool: 'image.inspect', - status: 'failed', - summary: '视觉模型调用失败', - detail: 'upstream-422', - }; - const prematureCompletionResponse = visualRecoveryRoute( - ['update_agent_plan', 'respond_to_user'], - `\n已有工具观察:\n${JSON.stringify([failedInspectObservation])}`, - ); - assert( - responseFunctionNames(prematureCompletionResponse).includes( - 'respond_to_user', - ), - 'self-test-ui-prototype-premature-completion-fixture-invalid', - ); - const visualBlockedObservation = { - tool: 'runtime.visual_asset', - status: 'blocked', - summary: 'UI 原型视觉检查尚未通过', - detail: - 'expectedPath=assets/ui-prototype.png · requiredInspection=image.inspect', - }; - const blockedRetryResponse = visualRecoveryRoute( - visualRecoveryTools, - `\n已有工具观察:\n${JSON.stringify([visualBlockedObservation])}`, - ); - assert( - responseFunctionNames(blockedRetryResponse).join(',') === - 'runtime_tool_image_inspect', - 'self-test-ui-prototype-visual-block-did-not-retry-inspection', - ); - const failedRetryResponse = visualRecoveryRoute( - visualRecoveryTools, - `\n已有工具观察:\n${JSON.stringify([failedInspectObservation])}`, - ); - assert( - responseFunctionNames(failedRetryResponse).join(',') === - 'runtime_tool_image_inspect', - 'self-test-ui-prototype-failed-inspection-did-not-retry', - ); - const successfulInspectObservation = { - tool: 'image.inspect', - status: 'ok', - summary: '视觉检查已完成,共分析 1 张图片', - detail: null, - }; - const visualRecoveryFinalResponse = visualRecoveryRoute( - ['update_agent_plan', 'respond_to_user'], - `\n已有工具观察:\n${JSON.stringify([successfulInspectObservation])}`, - ); - const visualRecoveryStats = visualRecoveryRouter.getStats(); - assert( - responseFunctionNames(visualRecoveryFinalResponse).includes( - 'respond_to_user', - ) && - visualRecoveryStats.manifestReadyTaskRunCount === 1 && - visualRecoveryStats.manifestReadyTaskCompletionCount === 1 && - visualRecoveryStats.manifestReadyTaskCanvasGenerationCount === 1 && - visualRecoveryStats.byAgent?.['design-foundation']?.projectMutation === - 3 && - visualRecoveryStats.unexpectedRequestCount === 0, - 'self-test-ui-prototype-visual-recovery-not-exactly-once', - ); - - const finalStaticResponse = route( - 'project-supervisor', - rootRunId, - ['runtime_tool_command_run_limited'], - `${contractContext}\n已有工具观察:\nseedTaskCounts: completed=16 running=0 pending=0 waiting=0 failed=0 total=16`, - ); - assert( - responseFunctionNames(finalStaticResponse).join(',') === - 'runtime_tool_command_run_limited', - 'self-test-final-static-not-exclusive', - ); - const successfulFinalStaticObservation = { - tool: 'command.run_limited', - status: 'ok', - summary: 'game.static_smoke 已完成', - detail: null, - }; - const finalPreviewResponse = route( - 'project-supervisor', - rootRunId, - ['runtime_tool_preview_validate'], - observationContext([successfulFinalStaticObservation]), - ); - assert( - responseFunctionNames(finalPreviewResponse).join(',') === - 'runtime_tool_preview_validate', - 'self-test-final-preview-not-exclusive', - ); - const successfulFinalPreviewObservation = { - tool: 'preview.validate', - status: 'ok', - summary: '桌面与移动视口真实试玩已通过', - detail: null, - }; - const finalHistoryResponse = route( - 'project-supervisor', - rootRunId, - ['runtime_tool_agent_action_history'], - observationContext([successfulFinalPreviewObservation]), - ); - const finalHistoryCalls = responseFunctionCalls(finalHistoryResponse); - assert( - finalHistoryCalls.length === 1 && - finalHistoryCalls[0]?.name === 'runtime_tool_agent_action_history' && - JSON.stringify(finalHistoryCalls[0]?.arguments?.input) === - JSON.stringify({ - runId: null, - actionId: null, - tool: null, - status: 'ok', - limit: 10, - }), - 'self-test-final-action-history-invalid', - ); - const staticActionId = `action-${'1'.repeat(24)}`; - const previewActionId = `action-${'2'.repeat(24)}`; - const actionHistoryDetail = JSON.stringify({ - runId: rootRunId, - count: 2, - truncated: false, - outputTruncated: false, - actions: [ - { - agentId: 'project-supervisor', - runId: rootRunId, - actionId: staticActionId, - tool: 'command.run_limited', - status: 'ok', - }, - { - agentId: 'project-supervisor', - runId: rootRunId, - actionId: previewActionId, - tool: 'preview.validate', - status: 'ok', - }, - ], - }); - const finalAcceptanceResponse = route( - 'project-supervisor', - rootRunId, - ['runtime_tool_agent_acceptance_update'], - observationContext([ - { - tool: 'agent.action_history', - status: 'ok', - summary: '已读取当前 Agent 的 2 条终态动作', - detail: actionHistoryDetail, - }, - ]), - ); - const finalAcceptanceCalls = responseFunctionCalls(finalAcceptanceResponse); - const acceptanceInput = finalAcceptanceCalls[0]?.arguments?.input; - assert( - finalAcceptanceCalls.length === 1 && - finalAcceptanceCalls[0]?.name === - 'runtime_tool_agent_acceptance_update' && - acceptanceInput?.contractFingerprint === contractFingerprint && - acceptanceInput?.evaluations?.length === 2 && - acceptanceInput.evaluations[0]?.criterionId === - 'static-current-revision' && - acceptanceInput.evaluations[0]?.evidence?.[0]?.actionId === - staticActionId && - acceptanceInput.evaluations[1]?.criterionId === - 'playable-current-revision' && - acceptanceInput.evaluations[1]?.evidence?.[0]?.actionId === - previewActionId, - 'self-test-final-acceptance-update-invalid', - ); - const finalRespondResponse = route( - 'project-supervisor', - rootRunId, - ['respond_to_user'], - observationContext([ - { - tool: 'agent.acceptance_update', - status: 'ok', - summary: 'Acceptance Graph 已更新', - detail: null, - }, - ]), - ); - assert( - responseFunctionNames(finalRespondResponse).join(',') === 'respond_to_user', - 'self-test-final-respond-not-exclusive', - ); - - const planRejectionRouter = createDeterministicLaneDefenseRouter({ apiKey }); - planRejectionRouter.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticPayload( - 'project-supervisor', - 'plan-rejection-run', - goalContractTools, - ), - }); - const planRejectionResponse = planRejectionRouter.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticPayload( - 'project-supervisor', - 'plan-rejection-run', - ['respond_to_user', 'runtime_tool_file_patch', 'runtime_tool_file_write'], - '当前父 run 已进入只编排模式;本轮只允许收束或安全编排。', - ), - }); - const planRejectionWaitResponse = planRejectionRouter.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticPayload( - 'project-supervisor', - 'plan-rejection-run', - ['runtime_tool_task_list', 'runtime_tool_agent_run_status'], - '当前正式 manifest DAG 仍有专业 task 在运行', - ), - }); - const planRejectionStats = planRejectionRouter.getStats(); - assert( - responseFunctionNames(planRejectionResponse).join(',') === - 'respond_to_user' && - responseFunctionNames(planRejectionWaitResponse).join(',') === - 'runtime_tool_task_list,runtime_tool_agent_run_status' && - planRejectionStats.byAgent?.['project-supervisor']?.projectMutation === - 0 && - planRejectionStats.delegateActionCount === 0 && - planRejectionStats.runStatusCount === 1 && - planRejectionStats.unexpectedRequestCount === 0, - 'self-test-supervisor-plan-rejection-did-not-converge-read-only', - ); - - const earlyFinalReplyRouter = createDeterministicLaneDefenseRouter({ - apiKey, - }); - assert( - captureProviderErrorCode(() => - earlyFinalReplyRouter.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticPayload( - 'project-supervisor', - 'early-final-reply-run', - [], - '给用户一个正常中文回复', - ), - }), - ) === 'provider-parent-final-reply-before-acceptance', - 'self-test-parent-final-reply-bypassed-acceptance', - ); - - const acceptanceGuardRunId = 'acceptance-guard-run'; - const acceptanceGuardRouter = createDeterministicLaneDefenseRouter({ - apiKey, - }); - const acceptanceGuardRoute = (tools, extraContext = '') => - acceptanceGuardRouter.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticPayload( - 'project-supervisor', - acceptanceGuardRunId, - tools, - extraContext, - ), - }); - acceptanceGuardRoute(goalContractTools); - acceptanceGuardRoute( - ['runtime_tool_command_run_limited'], - `${contractContext}\n已有工具观察:\nseedTaskCounts: completed=16 running=0 pending=0 waiting=0 failed=0 total=16`, - ); - acceptanceGuardRoute( - ['runtime_tool_preview_validate'], - observationContext([successfulFinalStaticObservation]), - ); - acceptanceGuardRoute( - ['runtime_tool_agent_action_history'], - observationContext([successfulFinalPreviewObservation]), - ); - const acceptanceGuardDetail = JSON.parse(actionHistoryDetail); - acceptanceGuardDetail.runId = acceptanceGuardRunId; - for (const action of acceptanceGuardDetail.actions) { - action.runId = acceptanceGuardRunId; - } - const actionHistoryObservationFor = (detail) => ({ - tool: 'agent.action_history', - status: 'ok', - summary: '已读取当前 Agent 的 2 条终态动作', - detail: JSON.stringify(detail), - }); - const ambiguousContractContext = `[Root Goal Contract]\n{"contractFingerprint":"${contractFingerprint}","nested":{"contractFingerprint":"${'b'.repeat(64)}"}}`; - assert( - captureProviderErrorCode(() => - acceptanceGuardRoute( - ['runtime_tool_agent_acceptance_update'], - `${ambiguousContractContext}\n已有工具观察:\n${JSON.stringify([ - actionHistoryObservationFor(acceptanceGuardDetail), - ])}`, - ), - ) === 'provider-goal-contract-fingerprint-missing', - 'self-test-ambiguous-goal-contract-fingerprint-accepted', - ); - const invalidActionHistoryDetail = structuredClone(acceptanceGuardDetail); - invalidActionHistoryDetail.actions[0].actionId = 'action-invalid'; - assert( - captureProviderErrorCode(() => - acceptanceGuardRoute( - ['runtime_tool_agent_acceptance_update'], - `${contractContext}\n已有工具观察:\n${JSON.stringify([ - actionHistoryObservationFor(invalidActionHistoryDetail), - ])}`, - ), - ) === 'provider-final-acceptance-evidence-missing', - 'self-test-invalid-action-history-id-accepted', - ); - assert( - responseFunctionNames( - acceptanceGuardRoute( - ['runtime_tool_agent_acceptance_update'], - `${contractContext}\n已有工具观察:\n${JSON.stringify([ - actionHistoryObservationFor(acceptanceGuardDetail), - ])}`, - ), - ).join(',') === 'runtime_tool_agent_acceptance_update', - 'self-test-acceptance-stage-advanced-after-rejected-history', - ); - - const stats = router.getStats(); - const manifestReadyTasks = manifestReadyTaskEvidence(stats); - assert( - manifestReadyTasks.exactlyOnce && - stats.goalContractCount === 1 && - stats.actionHistoryCount === 1 && - stats.acceptanceUpdateCount === 1 && - stats.interactionExecuteCount === 1 && - stats.delegateActionCount === 0 && - stats.imageInspectionRequestCount === 1 && - stats.manifestReadyTaskCanvasGenerationCount === 3 && - stats.byAgent?.['project-supervisor']?.projectMutation === 0 && - stats.byAgent?.['code-prototype']?.projectMutation === 1 && - stats.byAgent?.['quality-review']?.projectMutation === 0, - 'self-test-manifest-ready-exactly-once-invalid', - ); - - const missingReadyTaskStats = structuredClone(stats); - const missingAgentId = deterministicManifestReadyAgentIds.at(-1); - delete missingReadyTaskStats.readyTaskCountsByAgent[missingAgentId]; - missingReadyTaskStats.manifestReadyTaskRunCount -= 1; - missingReadyTaskStats.manifestReadyTaskCompletionCount -= 1; - assert( - !manifestReadyTaskEvidence(missingReadyTaskStats).exactlyOnce, - 'self-test-manifest-ready-missing-accepted', - ); - - const duplicateReadyTaskStats = structuredClone(stats); - const duplicateAgentId = deterministicManifestReadyAgentIds[0]; - duplicateReadyTaskStats.readyTaskCountsByAgent[duplicateAgentId].completion += - 1; - duplicateReadyTaskStats.manifestReadyTaskCompletionCount += 1; - assert( - !manifestReadyTaskEvidence(duplicateReadyTaskStats).exactlyOnce, - 'self-test-manifest-ready-duplicate-accepted', - ); - - const retryObservationContext = (observations) => - `\n已有工具观察:\n${JSON.stringify(observations)}`; - const staleObservation = { - tool: 'runtime.verification', - status: 'blocked', - summary: '最终回复生成期间项目 revision 已变化', - detail: 'responseRevision=1, currentRevision=2', - }; - const successfulSmokeObservation = { - tool: 'command.run_limited', - status: 'ok', - summary: 'game.static_smoke 已完成', - detail: null, - }; - const blockedSmokeObservation = { - tool: 'command.run_limited', - status: 'blocked', - summary: '仓库规范或启动上下文已漂移,旧动作未执行', - detail: - 'repositoryContextDrift=true · 请在同一 run 下一轮 planning 重新确认适用规范', - }; - const projectLockSmokeObservations = [ - { - tool: 'command.run_limited', - status: 'failed', - summary: 'game.static_smoke 无法取得项目验证锁', - detail: '项目正在被其他写操作占用:$PROJECT_ROOT/.agent/project.lock', - }, - { - tool: 'command.run_limited', - status: 'failed', - summary: 'game.static_smoke 无法取得项目验证锁', - detail: '项目正在被其他写操作占用:$PROJECT_ROOT\\.agent\\project.lock', - }, - ]; - const projectRevisionDriftSmokeObservation = { - tool: 'command.run_limited', - status: 'blocked', - summary: '并行项目变更使旧动作过期,旧动作未执行', - detail: - 'projectRevisionDrift=true · expectedRevision=4 · currentRevision=5 · replanRequired=true', - }; - const projectRevisionDriftCanvasObservation = { - tool: 'canvas.asset_generate', - status: 'blocked', - summary: '并行项目变更使旧动作过期,旧动作未执行', - detail: - 'projectRevisionDrift=true · expectedRevision=5 · currentRevision=6 · replanRequired=true', - }; - const successfulProjectMutationObservation = { - tool: 'project.patchset', - status: 'ok', - summary: 'project.patchset 已原子应用 2 项变更', - detail: 'checkpointId=checkpoint-concurrent-ready-batch', - }; - const missingArtAssetObservation = { - tool: 'runtime.visual_asset', - status: 'blocked', - summary: '首版美术素材图尚未按正式视觉流程生成并登记,不能完成任务', - detail: '[redacted sensitive context]', - }; - const successfulCanvasMutationObservation = { - tool: 'canvas.asset_generate', - status: 'ok', - summary: '素材画布图片已生成并登记', - detail: null, - }; - - const artAssetRecoveryAgentId = 'art-asset-plan'; - const artAssetRecoveryRunId = 'art-asset-visual-recovery-ready-run'; - const artAssetRecoveryTools = [ - ...manifestReadyTools, - 'runtime_tool_canvas_asset_generate', - ]; - const artAssetRecoveryRouter = createDeterministicLaneDefenseRouter({ - apiKey, - }); - const artAssetRecoveryRoute = (extraContext = '') => - artAssetRecoveryRouter.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticPayload( - artAssetRecoveryAgentId, - artAssetRecoveryRunId, - artAssetRecoveryTools, - `处理 manifest ready 任务:${artAssetRecoveryAgentId}\n任务 ID:${artAssetRecoveryAgentId}${extraContext}`, - ), - }); - for (let attempt = 0; attempt < 8; attempt += 1) { - if ( - responseFunctionNames(artAssetRecoveryRoute()).includes('respond_to_user') - ) { - break; - } - } - const artAssetRegenerationResponse = artAssetRecoveryRoute( - retryObservationContext([missingArtAssetObservation]), - ); - const artAssetReverificationResponse = artAssetRecoveryRoute( - retryObservationContext([successfulCanvasMutationObservation]), - ); - const artAssetRefinalizationResponse = artAssetRecoveryRoute( - retryObservationContext([successfulSmokeObservation]), - ); - const artAssetRecoveryStats = artAssetRecoveryRouter.getStats(); - assert( - responseFunctionNames(artAssetRegenerationResponse).join(',') === - 'runtime_tool_canvas_asset_generate' && - responseFunctionNames(artAssetReverificationResponse).join(',') === - 'runtime_tool_command_run_limited' && - responseFunctionNames(artAssetRefinalizationResponse).includes( - 'respond_to_user', - ) && - artAssetRecoveryStats.readyTaskCountsByAgent[artAssetRecoveryAgentId] - ?.completion === 1 && - artAssetRecoveryStats.manifestReadyTaskCanvasGenerationCount === 2 && - artAssetRecoveryStats.unexpectedRequestCount === 0, - 'self-test-art-asset-visual-recovery-not-exactly-once', - ); - - const artAssetRevisionDriftRunId = 'art-asset-revision-drift-ready-run'; - const artAssetRevisionDriftRouter = createDeterministicLaneDefenseRouter({ - apiKey, - }); - const artAssetRevisionDriftRoute = (extraContext = '') => - artAssetRevisionDriftRouter.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticPayload( - artAssetRecoveryAgentId, - artAssetRevisionDriftRunId, - artAssetRecoveryTools, - `处理 manifest ready 任务:${artAssetRecoveryAgentId}\n任务 ID:${artAssetRecoveryAgentId}${extraContext}`, - ), - }); - const artAssetInitialListResponse = artAssetRevisionDriftRoute(); - const artAssetManifestWriteResponse = artAssetRevisionDriftRoute(); - const artAssetInitialCanvasResponse = artAssetRevisionDriftRoute(); - const artAssetRetryCanvasResponse = artAssetRevisionDriftRoute( - retryObservationContext([projectRevisionDriftCanvasObservation]), - ); - const artAssetPostRetryReadResponse = artAssetRevisionDriftRoute( - retryObservationContext([successfulCanvasMutationObservation]), - ); - const artAssetRevisionDriftFinalizationResponse = - artAssetRevisionDriftRoute(); - const artAssetRevisionDriftStats = artAssetRevisionDriftRouter.getStats(); - assert( - responseFunctionNames(artAssetInitialListResponse).join(',') === - 'runtime_tool_asset_list' && - responseFunctionNames(artAssetManifestWriteResponse).join(',') === - 'runtime_tool_file_write' && - responseFunctionNames(artAssetInitialCanvasResponse).join(',') === - 'runtime_tool_canvas_asset_generate' && - responseFunctionNames(artAssetRetryCanvasResponse).join(',') === - 'runtime_tool_canvas_asset_generate' && - responseFunctionNames(artAssetPostRetryReadResponse).join(',') === - 'runtime_tool_file_read,runtime_tool_asset_list' && - responseFunctionNames(artAssetRevisionDriftFinalizationResponse).includes( - 'respond_to_user', - ) && - artAssetRevisionDriftStats.readyTaskCountsByAgent[artAssetRecoveryAgentId] - ?.run === 1 && - artAssetRevisionDriftStats.readyTaskCountsByAgent[artAssetRecoveryAgentId] - ?.completion === 1 && - artAssetRevisionDriftStats.manifestReadyTaskCanvasGenerationCount === 2 && - artAssetRevisionDriftStats.unexpectedRequestCount === 0, - 'self-test-art-asset-revision-drift-recovery-not-exactly-once', - ); - - const artAssetRevisionDriftLimitRunId = - 'art-asset-revision-drift-limit-ready-run'; - const artAssetRevisionDriftLimitRouter = createDeterministicLaneDefenseRouter( - { apiKey }, - ); - const artAssetRevisionDriftLimitRoute = (extraContext = '') => - artAssetRevisionDriftLimitRouter.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticPayload( - artAssetRecoveryAgentId, - artAssetRevisionDriftLimitRunId, - artAssetRecoveryTools, - `处理 manifest ready 任务:${artAssetRecoveryAgentId}\n任务 ID:${artAssetRecoveryAgentId}${extraContext}`, - ), - }); - artAssetRevisionDriftLimitRoute(); - artAssetRevisionDriftLimitRoute(); - artAssetRevisionDriftLimitRoute(); - for (let retry = 0; retry < 16; retry += 1) { - assert( - responseFunctionNames( - artAssetRevisionDriftLimitRoute( - retryObservationContext([projectRevisionDriftCanvasObservation]), - ), - ).join(',') === 'runtime_tool_canvas_asset_generate', - `self-test-art-asset-revision-drift-retry-invalid:${retry}`, - ); - } - const artAssetRevisionDriftLimitCode = captureProviderErrorCode(() => - artAssetRevisionDriftLimitRoute( - retryObservationContext([projectRevisionDriftCanvasObservation]), - ), - ); - const artAssetRevisionDriftLimitStats = - artAssetRevisionDriftLimitRouter.getStats(); - assert( - artAssetRevisionDriftLimitCode === - 'provider-ready-art-canvas-retry-exhausted' && - artAssetRevisionDriftLimitStats.readyTaskCountsByAgent[ - artAssetRecoveryAgentId - ]?.run === 1 && - artAssetRevisionDriftLimitStats.readyTaskCountsByAgent[ - artAssetRecoveryAgentId - ]?.completion === 0 && - artAssetRevisionDriftLimitStats.manifestReadyTaskCanvasGenerationCount === - 17, - 'self-test-art-asset-revision-drift-limit-invalid', - ); - - const readOnlyCommandRouter = createDeterministicLaneDefenseRouter({ - apiKey, - }); - const readOnlyCommandRunId = 'read-only-command-only-ready-run'; - const readOnlyCommandRoute = (tools) => - readOnlyCommandRouter.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticPayload( - 'quality-review', - readOnlyCommandRunId, - tools, - '处理 manifest ready 任务:quality-review\n任务 ID:quality-review', - ), - }); - readOnlyCommandRoute(manifestReadyTools); - readOnlyCommandRoute(manifestReadyTools); - const readOnlyCommandCode = captureProviderErrorCode(() => - readOnlyCommandRoute(['runtime_tool_command_run_limited']), - ); - const readOnlyCommandStats = readOnlyCommandRouter.getStats(); - assert( - readOnlyCommandCode === - 'provider-ready-finalization-tools-invalid:quality-review:runtime_tool_command_run_limited' && - readOnlyCommandStats.readyTaskCountsByAgent['quality-review'] - ?.completion === 0 && - readOnlyCommandStats.manifestReadyTaskStaticSmokeCount === 0, - 'self-test-read-only-command-only-not-rejected', - ); - - const preCompletionRouter = createDeterministicLaneDefenseRouter({ apiKey }); - const preCompletionAgentId = 'preview-readiness'; - const preCompletionRunId = 'pre-completion-transient-ready-run'; - const preCompletionRoute = (tools, extraContext = '') => - preCompletionRouter.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticPayload( - preCompletionAgentId, - preCompletionRunId, - tools, - `处理 manifest ready 任务:${preCompletionAgentId}\n任务 ID:${preCompletionAgentId}${extraContext}`, - ), - }); - preCompletionRoute(manifestReadyTools); - for (let retry = 0; retry < 16; retry += 1) { - assert( - responseFunctionNames( - preCompletionRoute( - ['runtime_tool_command_run_limited'], - retryObservationContext([blockedSmokeObservation]), - ), - ).join(',') === 'runtime_tool_command_run_limited', - `self-test-ready-precompletion-transient-invalid:${retry}`, - ); - } - const preCompletionLimitCode = captureProviderErrorCode(() => - preCompletionRoute( - ['runtime_tool_command_run_limited'], - retryObservationContext([blockedSmokeObservation]), - ), - ); - const preCompletionStats = preCompletionRouter.getStats(); - assert( - preCompletionLimitCode === - `provider-ready-precompletion-verification-exhausted:${preCompletionAgentId}` && - preCompletionStats.readyTaskCountsByAgent[preCompletionAgentId] - ?.completion === 0 && - preCompletionStats.manifestReadyTaskStaticSmokeCount === 17, - 'self-test-ready-precompletion-limit-invalid', - ); - - const projectLockRouter = createDeterministicLaneDefenseRouter({ apiKey }); - const projectLockRunId = 'project-lock-transient-ready-run'; - const projectLockRoute = (tools, extraContext = '') => - projectLockRouter.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticPayload( - preCompletionAgentId, - projectLockRunId, - tools, - `处理 manifest ready 任务:${preCompletionAgentId}\n任务 ID:${preCompletionAgentId}${extraContext}`, - ), - }); - projectLockRoute(manifestReadyTools); - for (const observation of projectLockSmokeObservations) { - assert( - responseFunctionNames( - projectLockRoute( - ['runtime_tool_command_run_limited'], - retryObservationContext([observation]), - ), - ).join(',') === 'runtime_tool_command_run_limited', - 'self-test-ready-project-lock-path-separator-not-retried', - ); - } - - const duplicateRouter = createDeterministicLaneDefenseRouter({ apiKey }); - const duplicateRoute = (runId, extraContext = '') => - duplicateRouter.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticPayload( - duplicateAgentId, - runId, - manifestReadyTools, - `处理 manifest ready 任务:${duplicateAgentId}\n任务 ID:${duplicateAgentId}${extraContext}`, - ), - }); - duplicateRoute('duplicate-ready-run'); - duplicateRoute('duplicate-ready-run'); - let duplicateTerminalCode = null; - try { - duplicateRoute('duplicate-ready-run'); - } catch (error) { - duplicateTerminalCode = error?.code ?? null; - } - let duplicateRunCode = null; - try { - duplicateRoute('second-ready-run'); - } catch (error) { - duplicateRunCode = error?.code ?? null; - } - assert( - duplicateTerminalCode === - `provider-ready-run-terminal-duplicate:${duplicateAgentId}` && - duplicateRunCode === - `provider-ready-agent-run-duplicate:${duplicateAgentId}`, - 'self-test-manifest-ready-provider-duplicate-not-rejected', - ); - - const retryAgentId = 'preview-readiness'; - const retryRunId = 'retry-ready-run'; - const retryRouter = createDeterministicLaneDefenseRouter({ apiKey }); - const retryRoute = (extraContext = '') => - retryRouter.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticPayload( - retryAgentId, - retryRunId, - manifestReadyTools, - `处理 manifest ready 任务:${retryAgentId}\n任务 ID:${retryAgentId}${extraContext}`, - ), - }); - for (let attempt = 0; attempt < 8; attempt += 1) { - if (responseFunctionNames(retryRoute()).includes('respond_to_user')) break; - } - for (let retry = 0; retry < 16; retry += 1) { - assert( - responseFunctionNames( - retryRoute(retryObservationContext([staleObservation])), - ).join(',') === 'runtime_tool_command_run_limited', - `self-test-ready-retry-verification-call-invalid:${retry}`, - ); - if (retry === 0) { - let missingObservationCode = null; - try { - retryRoute(); - } catch (error) { - missingObservationCode = error?.code ?? null; - } - assert( - missingObservationCode === - `provider-ready-retry-verification-invalid:${retryAgentId}`, - 'self-test-ready-retry-missing-observation-accepted', - ); - } - assert( - responseFunctionNames( - retryRoute(retryObservationContext([successfulSmokeObservation])), - ).includes('respond_to_user'), - `self-test-ready-retry-finalization-invalid:${retry}`, - ); - } - let retryLimitCode = null; - try { - retryRoute(retryObservationContext([staleObservation])); - } catch (error) { - retryLimitCode = error?.code ?? null; - } - const retryStats = retryRouter.getStats(); - assert( - retryLimitCode === - `provider-ready-run-terminal-duplicate:${retryAgentId}` && - retryStats.readyTaskCountsByAgent[retryAgentId]?.run === 1 && - retryStats.readyTaskCountsByAgent[retryAgentId]?.completion === 1, - 'self-test-ready-retry-limit-or-exactly-once-invalid', - ); - - const verifiedDeliveryRepairRouter = createDeterministicLaneDefenseRouter({ - apiKey, - }); - const verifiedDeliveryRepairRunId = 'verified-delivery-repair-ready-run'; - const verifiedDeliveryRepairRoute = (tools, extraContext = '') => - verifiedDeliveryRepairRouter.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticPayload( - retryAgentId, - verifiedDeliveryRepairRunId, - tools, - `处理 manifest ready 任务:${retryAgentId}\n任务 ID:${retryAgentId}${extraContext}`, - ), - }); - for (let attempt = 0; attempt < 8; attempt += 1) { - if ( - responseFunctionNames( - verifiedDeliveryRepairRoute(manifestReadyTools), - ).includes('respond_to_user') - ) { - break; - } - } - const rejectedVerificationPlan = verifiedDeliveryRepairRoute( - manifestReadyTools, - retryObservationContext([staleObservation]), - ); - const verifiedDeliveryRepairResponse = verifiedDeliveryRepairRoute( - ['respond_to_user'], - `${retryObservationContext([staleObservation])}\n上一条输出不符合工具计划协议:当前 revision 已通过验证。本次修复的原生工具目录只保留 respond_to_user;必须立即调用它交付专业合同结论。`, - ); - const verifiedDeliveryRepairStats = verifiedDeliveryRepairRouter.getStats(); - assert( - responseFunctionNames(rejectedVerificationPlan).join(',') === - 'runtime_tool_command_run_limited' && - responseFunctionNames(verifiedDeliveryRepairResponse).join(',') === - 'respond_to_user' && - verifiedDeliveryRepairStats.readyTaskCountsByAgent[retryAgentId] - ?.completion === 1 && - verifiedDeliveryRepairStats.unexpectedRequestCount === 0, - 'self-test-ready-verified-delivery-repair-invalid', - ); - - const transientRetryRouter = createDeterministicLaneDefenseRouter({ apiKey }); - const transientRetryRunId = 'transient-retry-ready-run'; - const transientRetryRoute = (extraContext = '') => - transientRetryRouter.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticPayload( - retryAgentId, - transientRetryRunId, - manifestReadyTools, - `处理 manifest ready 任务:${retryAgentId}\n任务 ID:${retryAgentId}${extraContext}`, - ), - }); - for (let attempt = 0; attempt < 8; attempt += 1) { - if ( - responseFunctionNames(transientRetryRoute()).includes('respond_to_user') - ) { - break; - } - } - transientRetryRoute(retryObservationContext([staleObservation])); - assert( - responseFunctionNames( - transientRetryRoute(retryObservationContext([blockedSmokeObservation])), - ).join(',') === 'runtime_tool_command_run_limited' && - responseFunctionNames( - transientRetryRoute( - retryObservationContext([successfulSmokeObservation]), - ), - ).includes('respond_to_user') && - transientRetryRouter.getStats().readyTaskCountsByAgent[retryAgentId] - ?.completion === 1, - 'self-test-ready-transient-verification-retry-invalid', - ); - - const standaloneRetryAgentId = 'design-foundation'; - const standaloneRetryRunId = 'standalone-retry-ready-run'; - const standaloneRetryRouter = createDeterministicLaneDefenseRouter({ - apiKey, - }); - const standaloneRetryRoute = (extraContext = '') => - standaloneRetryRouter.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticPayload( - standaloneRetryAgentId, - standaloneRetryRunId, - manifestReadyTools, - `处理 manifest ready 任务:${standaloneRetryAgentId}\n任务 ID:${standaloneRetryAgentId}${extraContext}`, - ), - }); - for (let attempt = 0; attempt < 8; attempt += 1) { - if ( - responseFunctionNames(standaloneRetryRoute()).includes('respond_to_user') - ) { - break; - } - } - const standaloneReplayResponse = standaloneRetryRoute( - retryObservationContext([successfulSmokeObservation]), - ); - const standaloneReplayDuplicateCode = captureProviderErrorCode(() => - standaloneRetryRoute(retryObservationContext([successfulSmokeObservation])), - ); - assert( - responseFunctionNames(standaloneReplayResponse).includes( - 'respond_to_user', - ) && - standaloneReplayDuplicateCode === - `provider-ready-run-terminal-duplicate:${standaloneRetryAgentId}` && - standaloneRetryRouter.getStats().readyTaskCountsByAgent[ - standaloneRetryAgentId - ]?.completion === 1, - 'self-test-ready-standalone-verification-replay-invalid', - ); - - const concurrentMutationAgentId = 'design-foundation'; - const concurrentMutationRunId = 'concurrent-mutation-ready-run'; - const concurrentMutationRouter = createDeterministicLaneDefenseRouter({ - apiKey, - }); - const concurrentMutationRoute = (extraContext = '') => - concurrentMutationRouter.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticPayload( - concurrentMutationAgentId, - concurrentMutationRunId, - manifestReadyTools, - `处理 manifest ready 任务:${concurrentMutationAgentId}\n任务 ID:${concurrentMutationAgentId}${extraContext}`, - ), - }); - for (let attempt = 0; attempt < 8; attempt += 1) { - if ( - responseFunctionNames(concurrentMutationRoute()).includes( - 'respond_to_user', - ) - ) { - break; - } - } - assert( - responseFunctionNames( - concurrentMutationRoute( - retryObservationContext([successfulProjectMutationObservation]), - ), - ).join(',') === 'runtime_tool_command_run_limited' && - responseFunctionNames( - concurrentMutationRoute( - retryObservationContext([successfulSmokeObservation]), - ), - ).includes('respond_to_user') && - concurrentMutationRouter.getStats().readyTaskCountsByAgent[ - concurrentMutationAgentId - ]?.completion === 1, - 'self-test-ready-concurrent-mutation-reverification-invalid', - ); - - const repairedWriterAgentId = 'balance-seed'; - const repairedWriterRunId = 'repaired-writer-ready-run'; - const repairedWriterRouter = createDeterministicLaneDefenseRouter({ apiKey }); - const repairedWriterRoute = (tools = manifestReadyTools, extraContext = '') => - repairedWriterRouter.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticPayload( - repairedWriterAgentId, - repairedWriterRunId, - tools, - `处理 manifest ready 任务:${repairedWriterAgentId}\n任务 ID:${repairedWriterAgentId}${extraContext}`, - ), - }); - for (let attempt = 0; attempt < 8; attempt += 1) { - if ( - responseFunctionNames(repairedWriterRoute()).includes('respond_to_user') - ) { - break; - } - } - const repairedWriterMutation = repairedWriterRoute( - ['runtime_tool_file_write'], - retryObservationContext([successfulSmokeObservation]), - ); - const repairedWriterVerification = repairedWriterRoute( - manifestReadyTools, - retryObservationContext([successfulProjectMutationObservation]), - ); - const repairedWriterFinalization = repairedWriterRoute( - manifestReadyTools, - retryObservationContext([successfulSmokeObservation]), - ); - const repairedWriterDuplicateCode = captureProviderErrorCode(() => - repairedWriterRoute( - manifestReadyTools, - retryObservationContext([successfulSmokeObservation]), - ), - ); - assert( - responseFunctionNames(repairedWriterMutation).join(',') === - 'runtime_tool_file_write' && - responseFunctionNames(repairedWriterVerification).join(',') === - 'runtime_tool_command_run_limited' && - responseFunctionNames(repairedWriterFinalization).includes( - 'respond_to_user', - ) && - repairedWriterDuplicateCode === - `provider-ready-run-terminal-duplicate:${repairedWriterAgentId}` && - repairedWriterRouter.getStats().readyTaskCountsByAgent[ - repairedWriterAgentId - ]?.completion === 1, - 'self-test-ready-repaired-writer-reverification-invalid', - ); - - const projectRevisionDriftAgentId = 'preview-readiness'; - const projectRevisionDriftRunId = 'project-revision-drift-ready-run'; - const projectRevisionDriftRouter = createDeterministicLaneDefenseRouter({ - apiKey, - }); - const projectRevisionDriftRoute = (extraContext = '') => - projectRevisionDriftRouter.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticPayload( - projectRevisionDriftAgentId, - projectRevisionDriftRunId, - manifestReadyTools, - `处理 manifest ready 任务:${projectRevisionDriftAgentId}\n任务 ID:${projectRevisionDriftAgentId}${extraContext}`, - ), - }); - for (let attempt = 0; attempt < 8; attempt += 1) { - if ( - responseFunctionNames(projectRevisionDriftRoute()).includes( - 'respond_to_user', - ) - ) { - break; - } - } - assert( - responseFunctionNames( - projectRevisionDriftRoute( - retryObservationContext([projectRevisionDriftSmokeObservation]), - ), - ).join(',') === 'runtime_tool_command_run_limited' && - responseFunctionNames( - projectRevisionDriftRoute( - retryObservationContext([successfulSmokeObservation]), - ), - ).includes('respond_to_user') && - projectRevisionDriftRouter.getStats().readyTaskCountsByAgent[ - projectRevisionDriftAgentId - ]?.completion === 1, - 'self-test-ready-project-revision-drift-retry-invalid', - ); - - const readOnlyRetryAgentId = 'quality-review'; - const readOnlyRetryRunId = 'read-only-retry-ready-run'; - const readOnlyRetryRouter = createDeterministicLaneDefenseRouter({ apiKey }); - const readOnlyRetryRoute = (extraContext = '') => - readOnlyRetryRouter.route({ - authorization: `Bearer ${apiKey}`, - payload: syntheticPayload( - readOnlyRetryAgentId, - readOnlyRetryRunId, - manifestReadyTools, - `处理 manifest ready 任务:${readOnlyRetryAgentId}\n任务 ID:${readOnlyRetryAgentId}${extraContext}`, - ), - }); - for (let attempt = 0; attempt < 8; attempt += 1) { - if ( - responseFunctionNames(readOnlyRetryRoute()).includes('respond_to_user') - ) { - break; - } - } - assert( - responseFunctionNames( - readOnlyRetryRoute(retryObservationContext([staleObservation])), - ).includes('respond_to_user') && - readOnlyRetryRouter.getStats().manifestReadyTaskStaticSmokeCount === 0, - 'self-test-read-only-retry-used-forbidden-verification', - ); - assert( - stats.goalContractCount === 1 && - stats.actionHistoryCount === 1 && - stats.acceptanceUpdateCount === 1 && - stats.delegateActionCount === 0 && - stats.manifestReadyTaskCanvasGenerationCount === 3 && - stats.byAgent?.['project-supervisor']?.projectMutation === 0 && - stats.byAgent?.['code-prototype']?.projectMutation === 1 && - stats.byAgent?.['quality-review']?.projectMutation === 0 && - stats.unexpectedRequestCount === 0, - 'self-test-provider-core-stats-invalid', - ); - - const syntheticRuntimeProviderRequestCount = - stats.requestCount - - stats.interactionExecuteCount - - stats.imageInspectionRequestCount; - - const syntheticChildReport = { - status: 'PASS', - suite, - errorCount: 0, - blocked: [], - cleanup: { performed: true, kept: false }, - evidence: { - ...Object.fromEntries( - requiredZeroChildEvidenceFields.map((field) => [field, 0]), - ), - evidenceCompleteness: 'complete', - dedicatedZeroInterventionPath: true, - stdinTaskCount: 1, - stdinEndedAfterTask: true, - turnReportOutcome: 'settled', - parentTaskStatus: 'completed', - parentRuntimeStatus: 'idle', - parentRuntimePhase: 'completed', - laneDefensePlaytestPassed: true, - laneDefenseAssertionCount: 37, - laneDefensePassedAssertionCount: 37, - browserValidationPassed: true, - staticSmokePassed: true, - gameIndexChanged: true, - projectRevisionDelta: 2, - finalSupervisorAssistantCount: 1, - professionalAssistantCount: 3, - providerRequestIdentityCount: syntheticRuntimeProviderRequestCount, - providerLifecycleStartedCount: syntheticRuntimeProviderRequestCount, - providerLifecycleTerminalCount: syntheticRuntimeProviderRequestCount, - providerLifecycleCompletedCount: syntheticRuntimeProviderRequestCount, - }, - }; - const incompletePlaytestReport = structuredClone(syntheticChildReport); - incompletePlaytestReport.evidence.laneDefensePassedAssertionCount = 36; - const manualInputReport = structuredClone(syntheticChildReport); - manualInputReport.evidence.approveInputCount = 1; - const residualSidecarReport = structuredClone(syntheticChildReport); - residualSidecarReport.evidence.providerHandoffSidecarCount = 1; - assert( - expectedChildReport(syntheticChildReport, { keepProject: false }, stats) && - !expectedChildReport( - incompletePlaytestReport, - { keepProject: false }, - stats, - ) && - !expectedChildReport(manualInputReport, { keepProject: false }, stats) && - !expectedChildReport( - residualSidecarReport, - { keepProject: false }, - stats, - ), - 'self-test-child-hard-gates-invalid', - ); - - const rootPackage = JSON.parse( - await fs.readFile(path.join(repoRoot, 'package.json'), 'utf8'), - ); - const shellPackage = JSON.parse( - await fs.readFile(path.join(appRoot, 'package.json'), 'utf8'), - ); - assert( - shellPackage.scripts?.[ - 'agent-runtime:supervisor-autonomous-playable-lane-defense-deterministic-e2e' - ] === 'node scripts/agent-runtime-deterministic-playable-e2e.mjs' && - rootPackage.scripts?.[ - 'ai-game-creator-shell:agent-runtime:supervisor-autonomous-playable-lane-defense-deterministic-e2e' - ] === - 'npm --prefix apps/ai-game-creator-shell run agent-runtime:supervisor-autonomous-playable-lane-defense-deterministic-e2e --', - 'self-test-package-command-invalid', - ); - return { - status: 'PASS', - suite: `${wrapperSuite}-self-test`, - providerUsed: false, - htmlChars: [...html].length, - providerStats: stats, - manifestReadyTasks, - manifestReadyFailureSamplesValidated: ['missing', 'duplicate'], - manifestReadyProviderDuplicateRejections: { - terminal: duplicateTerminalCode, - run: duplicateRunCode, - }, - terminalRetryContractsValidated: { - readOnlyCommandOnlyRejected: true, - preCompletionTransientRetryLimit: 16, - projectLockPathSeparatorsRetried: ['/', '\\'], - }, - providerRuntimeModeValidated: true, - interactionExecutionValidated: { - reasoningEffort: 'max', - requestCount: stats.interactionExecuteCount, - responseTools: interactionExecuteCalls.map((call) => call.name), - readyTaskRunCountBeforeRuntime: - interactionExecuteStats.manifestReadyTaskRunCount, - readyTaskCompletionCountBeforeRuntime: - interactionExecuteStats.manifestReadyTaskCompletionCount, - }, - goalContractValidated: true, - finalizationSequenceValidated: [ - 'command.run_limited', - 'preview.validate', - 'agent.action_history', - 'agent.acceptance_update', - 'respond_to_user', - ], - canvasArtifactsValidated: [ - 'assets/art-spec.png', - 'assets/ui-prototype.png', - 'assets/art-spritesheet.png', - ], - uiPrototypeInspectionValidated: true, - uiPrototypeVisualRecoveryExactlyOnceValidated: true, - artAssetVisualRecoveryExactlyOnceValidated: true, - artAssetRevisionDriftRecoveryExactlyOnceValidated: true, - artAssetRevisionDriftRetryLimitValidated: 16, - supervisorPlanRejectionMode: 'respond-only-then-read-only-wait', - childHardGatesValidated: true, - packageCommandsRegistered: true, - }; -} - -async function runE2e(options) { - const token = randomUUID(); - const apiKey = `deterministic-runtime-${randomUUID()}`; - const configDir = await fs.mkdtemp( - path.join(os.tmpdir(), 'genarrative-deterministic-provider-config-'), - ); - let provider = null; - let childResult = null; - let childReport = null; - let configRemoved = false; - let providerStats = null; - let failureCode = null; - try { - if (process.platform !== 'win32') await fs.chmod(configDir, 0o700); - await fs.writeFile( - path.join(configDir, configSentinelName), - `${JSON.stringify({ schemaVersion: configSentinelSchema, token })}\n`, - { flag: 'wx', mode: 0o600 }, - ); - provider = await startDeterministicLaneDefenseProvider({ apiKey }); - const config = deterministicRuntimeConfig(apiKey, provider); - const fixturePath = path.join(configDir, platformSessionFixtureName); - await fs.writeFile( - fixturePath, - `${JSON.stringify(deterministicPlatformSessionFixture(apiKey, provider))}\n`, - { flag: 'wx', mode: 0o600 }, - ); - await fs.writeFile( - path.join(configDir, configFileName), - `${JSON.stringify(config)}\n`, - { flag: 'wx', mode: 0o600 }, - ); - const childArgs = [ - realE2eScript, - '--suite', - suite, - '--config-dir', - configDir, - ]; - if (options.keepProject) childArgs.push('--keep-project'); - childResult = await runChild( - childArgs, - withLoopbackNoProxy({ - ...process.env, - NO_COLOR: '1', - [platformSessionFixtureEnv]: fixturePath, - GENARRATIVE_AGC_DEBUG_PROVIDER_E2E: '1', - }), - ); - childReport = parseChildReport(childResult); - } catch (error) { - failureCode = error?.code ?? 'deterministic-e2e-unexpected-error'; - } finally { - if (provider) { - try { - await provider.stop(); - providerStats = provider.getStats(); - } catch { - failureCode ??= 'deterministic-provider-stop-failed'; - } - } - try { - const sentinel = JSON.parse( - await fs.readFile(path.join(configDir, configSentinelName), 'utf8'), - ); - assert( - sentinel.schemaVersion === configSentinelSchema && - sentinel.token === token, - 'deterministic-config-sentinel-invalid', - ); - await fs.rm(configDir, { recursive: true, force: false }); - configRemoved = true; - } catch (error) { - failureCode ??= error?.code ?? 'deterministic-config-cleanup-failed'; - } - } - - const childPassed = - childResult?.code === 0 && - childResult?.signal === null && - !childResult?.error && - expectedChildReport(childReport, options, providerStats); - const providerPassed = - providerStats?.stopped === true && expectedProviderStats(providerStats); - const manifestReadyTasks = manifestReadyTaskEvidence(providerStats); - const status = - !failureCode && childPassed && providerPassed && configRemoved - ? 'PASS' - : 'FAIL'; - if (status !== 'PASS' && !failureCode) { - failureCode = !childPassed - ? 'deterministic-child-e2e-failed' - : !providerPassed - ? 'deterministic-provider-contract-failed' - : 'deterministic-config-not-cleaned'; - } - return { - status, - suite: wrapperSuite, - providerMode: 'deterministic-loopback-openai-chat', - delegatedSuite: suite, - child: childReport, - provider: providerStats, - manifestReadyTasks, - finalizationContract: { - goalContractCount: providerStats?.goalContractCount ?? null, - actionHistoryCount: providerStats?.actionHistoryCount ?? null, - acceptanceUpdateCount: providerStats?.acceptanceUpdateCount ?? null, - delegateActionCount: providerStats?.delegateActionCount ?? null, - supervisorProjectMutationCount: - providerStats?.byAgent?.['project-supervisor']?.projectMutation ?? null, - }, - cleanup: { - providerStopped: providerStats?.stopped === true, - configRemoved, - projectKept: options.keepProject, - }, - childDiagnostic: childResult ? safeChildDiagnostic(childResult) : null, - failureCode, - }; -} - -const options = parseArguments(process.argv.slice(2)); -const report = options.selfTest ? await runSelfTest() : await runE2e(options); -process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); -process.exitCode = report.status === 'PASS' ? 0 : 1; diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/assertions/core.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/assertions/core.mjs index f91c928e8..e2e02ff7f 100644 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/assertions/core.mjs +++ b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/assertions/core.mjs @@ -1,69 +1,8 @@ import { createHash } from '../dependencies.mjs'; import { isPlainObject } from '../harness/config.mjs'; -import { - interactiveCliOutput, - writeInteractiveCliLine, -} from '../harness/process.mjs'; -import { - shutdownWaiters, - state, - userInputAnswerText, -} from '../runtime-state.mjs'; +import { shutdownWaiters, state } from '../runtime-state.mjs'; import { disposableProjectPathVariants } from './runtime.mjs'; -export async function answerRemainingInteractiveQuestions(session) { - let answeredPromptCount = 1; - const deadline = Date.now() + 60_000; - while (Date.now() < deadline) { - const output = interactiveCliOutput(session); - if (output.includes(`[\u5df2\u56de\u7b54] ${state.userInput.requestId}`)) - return; - const promptCount = output.split('或直接输入其他答案:').length - 1; - while (answeredPromptCount < promptCount && answeredPromptCount < 3) { - writeInteractiveCliLine(session, userInputAnswerText); - answeredPromptCount += 1; - } - if (output.includes('[待确认]')) { - throw codedError('user-input-unexpected-tool-confirmation'); - } - if (session.closed) throw codedError('user-input-cli-closed-before-answer'); - await sleep(100); - } - throw codedError('user-input-answer-timeout'); -} - -export function parseSingleSwarmTurnReport(output, codePrefix) { - const reportLines = output - .split(/\r?\n/u) - .filter((line) => line.startsWith('[turn.report] ')); - assert(reportLines.length === 1, `${codePrefix}-turn-report-count-invalid`); - const report = JSON.parse(reportLines[0].slice('[turn.report] '.length)); - assert( - isPlainObject(report) && - JSON.stringify(Object.keys(report).sort()) === - JSON.stringify( - [ - 'schemaVersion', - 'outcome', - 'parentAgentId', - 'sessionId', - 'parentRunId', - 'runtimeCount', - 'busyRuntimeCount', - 'pendingTaskCount', - 'runningTaskCount', - 'waitingForConfirmationCount', - 'waitingForUserInputCount', - 'newAssistantMessageCount', - 'finalReplyChars', - 'reconciliationAgentCount', - ].sort(), - ), - `${codePrefix}-turn-report-shape-invalid`, - ); - return report; -} - export function isFailedTask(task) { return ( ['failed', 'cancelled', 'budget-exhausted'].includes(task.status) || diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/assertions/runtime.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/assertions/runtime.mjs index 4b49e89d9..ee48a17c4 100644 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/assertions/runtime.mjs +++ b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/assertions/runtime.mjs @@ -2419,24 +2419,6 @@ export function finalMessageId(agentId, sessionId, runId) { ).slice(0, 32)}`; } -export function runtimePublicStatusMessageId( - agentId, - sessionId, - runId, - status, -) { - const correlationId = runtimeMessageCorrelationId( - agentId, - sessionId, - runId, - ).slice(0, 32); - const statusFingerprint = createHash('sha256') - .update(status) - .digest('hex') - .slice(0, 16); - return `runtime-public-status-${correlationId}-${statusFingerprint}`; -} - export function backgroundTaskMessageId(agentId, sessionId, runId, source) { const fingerprint = createHash('sha256') .update(`${agentId}\n${sessionId}\n${runId}\n${source}`) diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/entry.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/entry.mjs index 8b20293b9..56c6f0231 100644 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/entry.mjs +++ b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/entry.mjs @@ -20,15 +20,6 @@ import { stopOwnedIsolatedRunner, } from './harness/app-data.mjs'; import { loadConfig, parseArguments } from './harness/config.mjs'; -import { - activeInteractiveCliSessions, - captureOwnedProcessCleanupSnapshot, - closeInteractiveCli, - destroyInteractiveCliOutputStreams, - interactiveCliOutput, - verifyOwnedProcessCleanupSnapshot, - waitForInteractiveCliStdioClose, -} from './harness/process.mjs'; import { checkPrerequisites } from './harness/project.mjs'; import { buildSummary, @@ -48,8 +39,6 @@ import { state, steerInstruction, StreamingSecretScanner, - userInputAnswerCanary, - userInputAnswerText, } from './runtime-state.mjs'; import { collectPartialContextCompactionEvidence, @@ -101,29 +90,14 @@ import { isSteerRunnerKillSuite, runSteerRunnerKillE2e, } from './suites/steer-runner-kill.mjs'; -import { - collectPartialSupervisorAutonomousPlayableEvidence, - emptySupervisorAutonomousPlayableEvidence, - isSupervisorAutonomousPlayableLaneDefenseSuite, - runSupervisorAutonomousPlayableLaneDefenseE2e, -} from './suites/supervisor-autonomous-playable.mjs'; import { collectPartialSupervisorSwarmEvidence, emptySupervisorSwarmEvidence, - isSupervisorSwarmInteractiveChatSuite, isSupervisorSwarmSuite, isSupervisorSwarmTransientRetrySuite, - recordSupervisorSwarmChatSessionFailureDiagnostic, runSupervisorSwarmE2e, - supervisorSwarmChatSessionFailureEvidence, supervisorSwarmToolPlanHandoffProxyNeedsCleanup, } from './suites/supervisor-swarm.mjs'; -import { - collectPartialUserInputEvidence, - emptyUserInputEvidence, - isUserInputRuntimeSuite, - runUserInputRuntimeE2e, -} from './suites/user-input.mjs'; import { collectPartialWebSearchEvidence, emptyWebSearchEvidence, @@ -175,13 +149,9 @@ if (selfTestRequested) { if (isContextCompactionSuite()) { state.evidence = emptyContextCompactionEvidence(); } - if (isUserInputRuntimeSuite()) state.evidence = emptyUserInputEvidence(); if (isScopedAgentsSuite()) state.evidence = emptyScopedAgentsEvidence(); if (isProjectSkillSuite()) state.evidence = emptyProjectSkillEvidence(); if (isParallelReadSuite()) state.evidence = emptyParallelReadEvidence(); - if (isSupervisorAutonomousPlayableLaneDefenseSuite()) { - state.evidence = emptySupervisorAutonomousPlayableEvidence(); - } if (isSupervisorSwarmSuite()) { state.evidence = emptySupervisorSwarmEvidence(); } @@ -192,11 +162,9 @@ if (selfTestRequested) { if ( isWebSearchSuite() || isContextCompactionSuite() || - isUserInputRuntimeSuite() || isScopedAgentsSuite() || isProjectSkillSuite() || isParallelReadSuite() || - isSupervisorAutonomousPlayableLaneDefenseSuite() || isSupervisorSwarmSuite() || isSteerRunnerKillSuite() ) { @@ -208,9 +176,8 @@ if (selfTestRequested) { state.transcriptScanner = new StreamingSecretScanner(state.secrets); state.config = await checkPrerequisites(loaded.config); - const required = isSupervisorAutonomousPlayableLaneDefenseSuite() - ? ['llmConfigured', 'chromeAvailable'] - : isProcessSessionSuite() || isIsolatedRunnerSuite() + const required = + isProcessSessionSuite() || isIsolatedRunnerSuite() ? ['llmConfigured'] : ['llmConfigured', 'chromeAvailable']; if (state.suite === 'full') { @@ -232,16 +199,12 @@ if (selfTestRequested) { await runWebSearchE2e(); } else if (isContextCompactionSuite()) { await runContextCompactionE2e(); - } else if (isUserInputRuntimeSuite()) { - await runUserInputRuntimeE2e(); } else if (isScopedAgentsSuite()) { await runScopedAgentsE2e(); } else if (isProjectSkillSuite()) { await runProjectSkillE2e(); } else if (isParallelReadSuite()) { await runParallelReadE2e(); - } else if (isSupervisorAutonomousPlayableLaneDefenseSuite()) { - await runSupervisorAutonomousPlayableLaneDefenseE2e(); } else if (isSupervisorSwarmSuite()) { await runSupervisorSwarmE2e(); } else if (isProcessSessionSuite()) { @@ -262,107 +225,6 @@ if (selfTestRequested) { recordError(error?.code ?? 'unexpected-error', error); } finally { state.cleanupInProgress = true; - const stateTrackedInteractiveCliSessions = new Set( - [ - state.userInputCliSession, - state.supervisorAutonomousPlayableCliSession, - state.supervisorSwarmCliSession, - ].filter(Boolean), - ); - const interactiveCliSessions = [ - ...new Set([ - ...stateTrackedInteractiveCliSessions, - ...activeInteractiveCliSessions, - ]), - ]; - const supervisorAutonomousPlayableCliSession = - state.supervisorAutonomousPlayableCliSession; - if (isUserInputRuntimeSuite() && state.userInputCliSession) { - try { - await closeInteractiveCli(state.userInputCliSession); - } catch (error) { - state.status = 'FAIL'; - recordError('user-input-cli-cleanup-failed', error); - } - state.userInputCliSession = null; - } - if ( - isSupervisorAutonomousPlayableLaneDefenseSuite() && - state.supervisorAutonomousPlayableCliSession - ) { - try { - await closeInteractiveCli(state.supervisorAutonomousPlayableCliSession); - } catch (error) { - state.status = 'FAIL'; - recordError('supervisor-autonomous-playable-cli-cleanup-failed', error); - } - state.supervisorAutonomousPlayableCliSession = null; - } - if ( - isSupervisorSwarmInteractiveChatSuite() && - state.supervisorSwarmCliSession - ) { - try { - if ( - state.status !== 'PASS' && - state.supervisorSwarmCliSession.closed && - !state.supervisorSwarm.chatSessionFailureDiagnostic - ) { - recordSupervisorSwarmChatSessionFailureDiagnostic( - state.supervisorSwarmCliSession, - ); - } - await closeInteractiveCli(state.supervisorSwarmCliSession); - } catch (error) { - state.status = 'FAIL'; - recordError( - 'supervisor-swarm-autonomous-chat-cli-cleanup-failed', - error, - ); - } - state.supervisorSwarmCliSession = null; - } - for (const session of interactiveCliSessions) { - if (stateTrackedInteractiveCliSessions.has(session)) continue; - try { - await closeInteractiveCli(session); - } catch (error) { - state.status = 'FAIL'; - recordError('interactive-cli-cleanup-failed', error); - } - } - if ( - isSupervisorAutonomousPlayableLaneDefenseSuite() && - state.isolatedRunner.appDataDir - ) { - try { - const runnerPid = state.isolatedRunner.current?.pid ?? null; - const helperPid = - state.isolatedRunner.current?.killHandle?.child?.pid ?? null; - state.supervisorAutonomousPlayable.ownedProcessCleanupSnapshot = - await captureOwnedProcessCleanupSnapshot({ - runnerPid, - helperPids: Number.isSafeInteger(helperPid) ? [helperPid] : [], - rootPids: [ - ...interactiveCliSessions.map((session) => session.child?.pid), - ...[...activeCommandChildren].map((child) => child.pid), - ].filter((pid) => Number.isSafeInteger(pid) && pid > 0), - }); - const observed = - state.supervisorAutonomousPlayable.ownedProcessCleanupSnapshot - .observedCounts; - assert( - observed.runner === 1 && observed.helper === 1, - 'supervisor-autonomous-playable-owned-process-snapshot-incomplete', - ); - } catch (error) { - state.status = 'FAIL'; - recordError( - 'supervisor-autonomous-playable-owned-process-snapshot-failed', - error, - ); - } - } if (isIsolatedRunnerSuite() && state.isolatedRunner.appDataDir) { try { await stopOwnedIsolatedRunner(); @@ -380,25 +242,6 @@ if (selfTestRequested) { ).catch(() => {}); } } - for (const session of interactiveCliSessions) { - try { - await waitForInteractiveCliStdioClose(session, 10_000); - } catch (error) { - destroyInteractiveCliOutputStreams(session); - state.status = 'FAIL'; - recordError( - error?.code === 'interactive-cli-stdio-close-timeout' - ? error.code - : 'interactive-cli-stdio-cleanup-failed', - error, - ); - } - } - if (supervisorAutonomousPlayableCliSession) { - state.supervisorAutonomousPlayable.cliOutput = interactiveCliOutput( - supervisorAutonomousPlayableCliSession, - ); - } if (isIsolatedRunnerSuite() && state.isolatedRunner.appDataDir) { if (state.isolatedRunner.stopped) { try { @@ -500,28 +343,6 @@ if (selfTestRequested) { state.status = 'FAIL'; recordError('web-search-formal-config-cli-call-detected'); } - } else if (isUserInputRuntimeSuite()) { - state.evidence.userInputRunnerStopped = state.isolatedRunner.stopped; - state.evidence.userInputAppDataCleanupPerformed = - state.isolatedRunner.cleanupPerformed; - state.evidence.userInputRunnerKillMethod = killMethod; - state.evidence.userInputRunnerPidfdClaimCount = - state.isolatedRunner.pidfdClaimCount; - state.evidence.userInputRunnerPidfdSignalCount = - state.isolatedRunner.pidfdSignalCount; - state.evidence.formalConfigCliCallCount = - state.isolatedRunner.sourceConfigCliCallCount; - state.evidence.sourceRunnerEndpointUnchanged = - state.isolatedRunner.sourceRunnerEndpointUnchanged; - state.evidence.sourceConfigHardlinkCount = - state.isolatedRunner.configLinks.length; - state.evidence.sourceConfigLinksVerified = - state.isolatedRunner.sourceConfigLinksVerified; - state.evidence.isolatedAppDataUsed = true; - if (state.isolatedRunner.sourceConfigCliCallCount > 0) { - state.status = 'FAIL'; - recordError('user-input-formal-config-cli-call-detected'); - } } else if (isScopedAgentsSuite()) { state.evidence.scopedAgentsRunnerStopped = state.isolatedRunner.stopped; state.evidence.scopedAgentsAppDataCleanupPerformed = @@ -588,28 +409,6 @@ if (selfTestRequested) { state.status = 'FAIL'; recordError('parallel-read-formal-config-cli-call-detected'); } - } else if (isSupervisorAutonomousPlayableLaneDefenseSuite()) { - state.evidence.supervisorAutonomousPlayableRunnerStopped = - state.isolatedRunner.stopped; - state.evidence.supervisorAutonomousPlayableAppDataCleanupPerformed = - state.isolatedRunner.cleanupPerformed; - state.evidence.formalConfigCliCallCount = - state.isolatedRunner.sourceConfigCliCallCount; - state.evidence.sourceRunnerEndpointUnchanged = - state.isolatedRunner.sourceRunnerEndpointUnchanged; - state.evidence.sourceAppDataDirectoryUntouched = - state.isolatedRunner.sourceAppDataDirectoryUntouched; - state.evidence.sourceConfigReplicaCount = - state.isolatedRunner.configLinks.length; - state.evidence.sourceConfigReplicasVerified = - state.isolatedRunner.sourceConfigLinksVerified; - state.evidence.isolatedAppDataUsed = true; - if (state.isolatedRunner.sourceConfigCliCallCount > 0) { - state.status = 'FAIL'; - recordError( - 'supervisor-autonomous-playable-formal-config-cli-call-detected', - ); - } } else if (isSupervisorSwarmSuite()) { state.evidence.supervisorSwarmRunnerStopped = state.isolatedRunner.stopped; @@ -719,61 +518,6 @@ if (selfTestRequested) { ); } } - if ( - isSupervisorAutonomousPlayableLaneDefenseSuite() && - state.isolatedRunner.appDataDir - ) { - const snapshot = - state.supervisorAutonomousPlayable.ownedProcessCleanupSnapshot; - if (snapshot) { - try { - const cleanup = await verifyOwnedProcessCleanupSnapshot(snapshot); - state.evidence.ownedProcessIdentityCaptured = true; - state.evidence.ownedRunnerObservedCount = - snapshot.observedCounts.runner; - state.evidence.ownedHelperObservedCount = - snapshot.observedCounts.helper; - state.evidence.ownedNodeDescendantObservedCount = - snapshot.observedCounts.node; - state.evidence.ownedBrowserDescendantObservedCount = - snapshot.observedCounts.browser; - state.evidence.ownedCommandDescendantObservedCount = - snapshot.observedCounts.command; - state.evidence.ownedRunnerResidualCount = - cleanup.residualCounts.runner; - state.evidence.ownedHelperResidualCount = - cleanup.residualCounts.helper; - state.evidence.ownedNodeDescendantResidualCount = - cleanup.residualCounts.node; - state.evidence.ownedBrowserDescendantResidualCount = - cleanup.residualCounts.browser; - state.evidence.ownedCommandDescendantResidualCount = - cleanup.residualCounts.command; - state.evidence.activeCommandChildrenAfterCleanup = - cleanup.activeCommandChildCount; - state.evidence.activeInteractiveCliSessionsAfterCleanup = - cleanup.activeInteractiveCliSessionCount; - state.evidence.ownedProcessCleanupPassed = cleanup.clean; - if (!cleanup.clean) { - state.status = 'FAIL'; - recordError( - 'supervisor-autonomous-playable-owned-process-residual-detected', - ); - } - } catch (error) { - state.status = 'FAIL'; - recordError( - 'supervisor-autonomous-playable-owned-process-verification-failed', - error, - ); - } - } else { - state.status = 'FAIL'; - recordError( - 'supervisor-autonomous-playable-owned-process-snapshot-missing', - ); - } - } if ( isSteerRunnerKillSuite() && state.projectRoot && @@ -836,20 +580,6 @@ if (selfTestRequested) { recordError('context-compaction-partial-evidence-read-failed', error); } } - if ( - isUserInputRuntimeSuite() && - state.projectRoot && - state.status !== 'PASS' - ) { - try { - state.evidence = { - ...state.evidence, - ...(await collectPartialUserInputEvidence()), - }; - } catch (error) { - recordError('user-input-partial-evidence-read-failed', error); - } - } if (isScopedAgentsSuite() && state.projectRoot && state.status !== 'PASS') { try { state.evidence = { @@ -880,24 +610,6 @@ if (selfTestRequested) { recordError('parallel-read-partial-evidence-read-failed', error); } } - if ( - isSupervisorAutonomousPlayableLaneDefenseSuite() && - state.projectRoot && - state.status !== 'PASS' && - state.evidence.evidenceCompleteness !== 'complete' - ) { - try { - state.evidence = { - ...state.evidence, - ...(await collectPartialSupervisorAutonomousPlayableEvidence()), - }; - } catch (error) { - recordError( - 'supervisor-autonomous-playable-partial-evidence-read-failed', - error, - ); - } - } if ( isSupervisorSwarmSuite() && state.projectRoot && @@ -1062,23 +774,6 @@ if (selfTestRequested) { report = JSON.stringify(summary, null, 2); } } - if (isUserInputRuntimeSuite()) { - state.userInput.reportLeakCount = countExactSecrets( - Buffer.from(report), - [ - userInputAnswerCanary, - userInputAnswerText, - ...state.userInput.privateValues, - ].filter(isNonEmptyString), - ); - state.evidence.userInputReportLeakCount = state.userInput.reportLeakCount; - if (state.userInput.reportLeakCount > 0) { - state.status = 'FAIL'; - recordError('user-input-private-body-report-leak-detected'); - summary = buildSummary(); - report = JSON.stringify(summary, null, 2); - } - } if (isScopedAgentsSuite()) { state.scopedAgents.reportLeakCount = countExactSecrets( Buffer.from(report), @@ -1121,20 +816,6 @@ if (selfTestRequested) { report = JSON.stringify(summary, null, 2); } } - if (isSupervisorAutonomousPlayableLaneDefenseSuite()) { - state.supervisorAutonomousPlayable.reportLeakCount = countExactSecrets( - Buffer.from(report), - state.supervisorAutonomousPlayable.privateValues, - ); - state.evidence.supervisorAutonomousPlayableReportLeakCount = - state.supervisorAutonomousPlayable.reportLeakCount; - if (state.supervisorAutonomousPlayable.reportLeakCount > 0) { - state.status = 'FAIL'; - recordError('supervisor-autonomous-playable-report-body-leak-detected'); - summary = buildSummary(); - report = JSON.stringify(summary, null, 2); - } - } if (isSupervisorSwarmSuite()) { state.supervisorSwarm.reportLeakCount = countExactSecrets( Buffer.from(report), @@ -1164,11 +845,9 @@ if (selfTestRequested) { if ( isWebSearchSuite() || isContextCompactionSuite() || - isUserInputRuntimeSuite() || isScopedAgentsSuite() || isProjectSkillSuite() || isParallelReadSuite() || - isSupervisorAutonomousPlayableLaneDefenseSuite() || isSupervisorSwarmSuite() || isSteerRunnerKillSuite() ) { @@ -1211,16 +890,6 @@ if (selfTestRequested) { const remainingWebSearchReportLeakCount = isWebSearchSuite() ? countExactSecrets(Buffer.from(report), webSearchPrivateLeakValues()) : 0; - const remainingUserInputReportLeakCount = isUserInputRuntimeSuite() - ? countExactSecrets( - Buffer.from(report), - [ - userInputAnswerCanary, - userInputAnswerText, - ...state.userInput.privateValues, - ].filter(isNonEmptyString), - ) - : 0; const remainingScopedAgentsReportLeakCount = isScopedAgentsSuite() ? countExactSecrets(Buffer.from(report), state.scopedAgents.privateValues) : 0; @@ -1230,13 +899,6 @@ if (selfTestRequested) { const remainingParallelReadReportLeakCount = isParallelReadSuite() ? countExactSecrets(Buffer.from(report), state.parallelRead.privateValues) : 0; - const remainingSupervisorAutonomousPlayableReportLeakCount = - isSupervisorAutonomousPlayableLaneDefenseSuite() - ? countExactSecrets( - Buffer.from(report), - state.supervisorAutonomousPlayable.privateValues, - ) - : 0; const remainingSupervisorSwarmReportLeakCount = isSupervisorSwarmSuite() ? countExactSecrets( Buffer.from(report), @@ -1246,11 +908,9 @@ if (selfTestRequested) { const remainingFormalConfigPathReportLeakCount = isWebSearchSuite() || isContextCompactionSuite() || - isUserInputRuntimeSuite() || isScopedAgentsSuite() || isProjectSkillSuite() || isParallelReadSuite() || - isSupervisorAutonomousPlayableLaneDefenseSuite() || isSupervisorSwarmSuite() || isSteerRunnerKillSuite() ? countExactSecrets(Buffer.from(report), formalConfigPathVariants()) @@ -1259,11 +919,9 @@ if (selfTestRequested) { remainingProjectPathReportLeakCount > 0 || remainingResponseStreamReportLeakCount > 0 || remainingWebSearchReportLeakCount > 0 || - remainingUserInputReportLeakCount > 0 || remainingScopedAgentsReportLeakCount > 0 || remainingProjectSkillReportLeakCount > 0 || remainingParallelReadReportLeakCount > 0 || - remainingSupervisorAutonomousPlayableReportLeakCount > 0 || remainingSupervisorSwarmReportLeakCount > 0 || remainingFormalConfigPathReportLeakCount > 0 ) { @@ -1273,21 +931,17 @@ if (selfTestRequested) { ? 'disposable-project-path-report-redaction-required' : remainingResponseStreamReportLeakCount > 0 ? 'response-stream-report-redaction-required' - : remainingUserInputReportLeakCount > 0 - ? 'user-input-report-redaction-required' - : remainingScopedAgentsReportLeakCount > 0 - ? 'scoped-agents-report-redaction-required' - : remainingProjectSkillReportLeakCount > 0 - ? 'project-skill-report-redaction-required' - : remainingParallelReadReportLeakCount > 0 - ? 'parallel-read-report-redaction-required' - : remainingSupervisorAutonomousPlayableReportLeakCount > 0 - ? 'supervisor-autonomous-playable-report-redaction-required' - : remainingSupervisorSwarmReportLeakCount > 0 - ? 'supervisor-swarm-report-redaction-required' - : remainingFormalConfigPathReportLeakCount > 0 - ? 'formal-config-path-report-redaction-required' - : 'web-search-report-redaction-required', + : remainingScopedAgentsReportLeakCount > 0 + ? 'scoped-agents-report-redaction-required' + : remainingProjectSkillReportLeakCount > 0 + ? 'project-skill-report-redaction-required' + : remainingParallelReadReportLeakCount > 0 + ? 'parallel-read-report-redaction-required' + : remainingSupervisorSwarmReportLeakCount > 0 + ? 'supervisor-swarm-report-redaction-required' + : remainingFormalConfigPathReportLeakCount > 0 + ? 'formal-config-path-report-redaction-required' + : 'web-search-report-redaction-required', ); const safeSummary = { status: state.status, @@ -1302,19 +956,13 @@ if (selfTestRequested) { projectPathReportLeakCount: remainingProjectPathReportLeakCount, responseStreamReportLeakCount: remainingResponseStreamReportLeakCount, webSearchReportLeakCount: remainingWebSearchReportLeakCount, - userInputReportLeakCount: remainingUserInputReportLeakCount, scopedAgentsReportLeakCount: remainingScopedAgentsReportLeakCount, projectSkillReportLeakCount: remainingProjectSkillReportLeakCount, parallelReadReportLeakCount: remainingParallelReadReportLeakCount, - supervisorAutonomousPlayableReportLeakCount: - remainingSupervisorAutonomousPlayableReportLeakCount, supervisorSwarmReportLeakCount: remainingSupervisorSwarmReportLeakCount, formalConfigPathReportLeakCount: remainingFormalConfigPathReportLeakCount, - ...(isSupervisorSwarmSuite() - ? supervisorSwarmChatSessionFailureEvidence() - : {}), }, errorCount: state.errors.length, errorHashes: state.errors.map(summarizeRecordedError), diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/harness/app-data.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/harness/app-data.mjs index 3d8e052c6..c5ed62fa2 100644 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/harness/app-data.mjs +++ b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/harness/app-data.mjs @@ -37,26 +37,16 @@ import { state, steerRunnerKillAppDataSentinelFileName, steerRunnerKillAppDataSentinelSchema, - supervisorAutonomousPlayableAppDataSentinelFileName, - supervisorAutonomousPlayableAppDataSentinelSchema, supervisorSwarmAppDataSentinelFileName, supervisorSwarmAppDataSentinelSchema, - supervisorSwarmAutonomousChatAppDataSentinelFileName, - supervisorSwarmAutonomousChatAppDataSentinelSchema, - supervisorSwarmCollaborationPolicyAppDataSentinelFileName, - supervisorSwarmCollaborationPolicyAppDataSentinelSchema, supervisorSwarmFinalReplyTransientRetryAppDataSentinelFileName, supervisorSwarmFinalReplyTransientRetryAppDataSentinelSchema, supervisorSwarmRequiredAgentIds, - supervisorSwarmStaticIsolatedAutonomousChatAppDataSentinelFileName, - supervisorSwarmStaticIsolatedAutonomousChatAppDataSentinelSchema, supervisorSwarmToolPlanHandoffRunnerKillAppDataSentinelFileName, supervisorSwarmToolPlanHandoffRunnerKillAppDataSentinelSchema, supervisorSwarmTransientRetryAppDataSentinelFileName, supervisorSwarmTransientRetryAppDataSentinelSchema, supervisorSwarmTransientRetryTargetAgentId, - userInputAppDataSentinelFileName, - userInputAppDataSentinelSchema, webSearchAppDataSentinelFileName, webSearchAppDataSentinelSchema, windowsProcessHandleHelperSource, @@ -68,21 +58,14 @@ import { isProjectSkillSuite } from '../suites/project-skill.mjs'; import { isResponseStreamSuite } from '../suites/response-stream.mjs'; import { isScopedAgentsSuite } from '../suites/scoped-agents.mjs'; import { isSteerRunnerKillSuite } from '../suites/steer-runner-kill.mjs'; -import { isSupervisorAutonomousPlayableLaneDefenseSuite } from '../suites/supervisor-autonomous-playable.mjs'; import { - isSupervisorSwarmAutonomousChatSuite, - isSupervisorSwarmCollaborationPolicyMixedRecoverySuite, isSupervisorSwarmFinalReplyTransientRetrySuite, isSupervisorSwarmInitialTransientRetrySuite, - isSupervisorSwarmInteractiveChatSuite, - isSupervisorSwarmMixedHarnessSuite, - isSupervisorSwarmStaticIsolatedAutonomousChatSuite, isSupervisorSwarmSuite, isSupervisorSwarmToolPlanHandoffRunnerKillSuite, isSupervisorSwarmTransientRetrySuite, rebuildSupervisorSwarmTranscriptScanner, } from '../suites/supervisor-swarm.mjs'; -import { isUserInputRuntimeSuite } from '../suites/user-input.mjs'; import { isWebSearchSuite, sameEffectiveAgentLlmWithoutWebSearch, @@ -100,19 +83,10 @@ import { appendBounded, runProcess } from './process.mjs'; import { decodeUtf8Fatal, isIsolatedRunnerSuite } from './reporting.mjs'; import { killRunnerOnce, readRunnerStatus, runnerBootId } from './runtime.mjs'; -const platformSessionFixtureEnv = 'GENARRATIVE_AGC_PLATFORM_SESSION_FIXTURE'; -const platformSessionFixtureMaxBytes = 16 * 1024; -const isolatedPlatformSessionFixtureName = - '.deterministic-platform-session.json'; -const platformSessionFixtureSchema = - 'genarrative-agc-platform-session-fixture.v1'; - export function isolatedSuiteProtectsSourceAppData() { return ( isSupervisorSwarmTransientRetrySuite() || - isSupervisorSwarmToolPlanHandoffRunnerKillSuite() || - isSupervisorAutonomousPlayableLaneDefenseSuite() || - isSupervisorSwarmInteractiveChatSuite() + isSupervisorSwarmToolPlanHandoffRunnerKillSuite() ); } @@ -120,27 +94,6 @@ export function isolatedSuiteUsesSiblingAppData() { return isWebSearchSuite() || isolatedSuiteProtectsSourceAppData(); } -export function sameSupervisorPlayableProviderBinding(left, right) { - const scalarFields = [ - 'providerAgentMode', - 'providerModel', - 'providerApiKind', - 'providerReasoningEffort', - 'providerBaseUrlSha256', - ]; - return ( - left != null && - right != null && - scalarFields.every((field) => left[field] === right[field]) && - Array.isArray(left.boundAgentIds) && - Array.isArray(right.boundAgentIds) && - left.boundAgentIds.length === right.boundAgentIds.length && - left.boundAgentIds.every( - (agentId, index) => agentId === right.boundAgentIds[index], - ) - ); -} - export function isolatedSuiteAppDataProfile() { if (isSteerRunnerKillSuite()) { return { @@ -174,14 +127,6 @@ export function isolatedSuiteAppDataProfile() { codePrefix: 'context-compaction-appdata', }; } - if (isUserInputRuntimeSuite()) { - return { - prefix: '.agent-runtime-real-e2e-user-input-', - sentinelName: userInputAppDataSentinelFileName, - sentinelSchema: userInputAppDataSentinelSchema, - codePrefix: 'user-input-appdata', - }; - } if (isScopedAgentsSuite()) { return { prefix: '.agent-runtime-real-e2e-scoped-agents-', @@ -236,41 +181,6 @@ export function isolatedSuiteAppDataProfile() { codePrefix: 'supervisor-swarm-transient-retry-appdata', }; } - if (isSupervisorAutonomousPlayableLaneDefenseSuite()) { - return { - prefix: '.agent-runtime-real-e2e-supervisor-autonomous-playable-', - sentinelName: supervisorAutonomousPlayableAppDataSentinelFileName, - sentinelSchema: supervisorAutonomousPlayableAppDataSentinelSchema, - codePrefix: 'supervisor-autonomous-playable-appdata', - }; - } - if (isSupervisorSwarmAutonomousChatSuite()) { - return { - prefix: '.agent-runtime-real-e2e-supervisor-swarm-autonomous-chat-', - sentinelName: supervisorSwarmAutonomousChatAppDataSentinelFileName, - sentinelSchema: supervisorSwarmAutonomousChatAppDataSentinelSchema, - codePrefix: 'supervisor-swarm-autonomous-chat-appdata', - }; - } - if (isSupervisorSwarmStaticIsolatedAutonomousChatSuite()) { - return { - prefix: - '.agent-runtime-real-e2e-supervisor-swarm-static-isolated-autonomous-chat-', - sentinelName: - supervisorSwarmStaticIsolatedAutonomousChatAppDataSentinelFileName, - sentinelSchema: - supervisorSwarmStaticIsolatedAutonomousChatAppDataSentinelSchema, - codePrefix: 'supervisor-swarm-static-isolated-autonomous-chat-appdata', - }; - } - if (isSupervisorSwarmCollaborationPolicyMixedRecoverySuite()) { - return { - prefix: '.agent-runtime-real-e2e-supervisor-swarm-collaboration-policy-', - sentinelName: supervisorSwarmCollaborationPolicyAppDataSentinelFileName, - sentinelSchema: supervisorSwarmCollaborationPolicyAppDataSentinelSchema, - codePrefix: 'supervisor-swarm-collaboration-policy-appdata', - }; - } if (isSupervisorSwarmSuite()) { return { prefix: '.agent-runtime-real-e2e-supervisor-swarm-', @@ -505,120 +415,6 @@ export async function verifySourceAppDataDirectoryUntouched() { state.isolatedRunner.sourceAppDataDirectoryUntouched = true; } -async function readPlatformSessionFixtureForIsolatedSuite(sourceConfigDir) { - const rawPath = process.env[platformSessionFixtureEnv]; - assert( - isNonEmptyString(rawPath) && path.isAbsolute(rawPath), - 'supervisor-autonomous-playable-platform-session-fixture-missing', - ); - const sourceRealPath = await fs.realpath(sourceConfigDir); - const requestedPath = path.resolve(rawPath); - const requestedMetadata = await fs.lstat(requestedPath).catch((error) => { - if (error?.code === 'ENOENT') return null; - throw error; - }); - assert( - requestedMetadata?.isFile() && !requestedMetadata.isSymbolicLink(), - 'supervisor-autonomous-playable-platform-session-fixture-not-regular', - ); - assert( - requestedMetadata.size <= platformSessionFixtureMaxBytes, - 'supervisor-autonomous-playable-platform-session-fixture-too-large', - ); - const realPath = await fs.realpath(requestedPath); - assert( - isPathInside(sourceRealPath, realPath), - 'supervisor-autonomous-playable-platform-session-fixture-outside-config', - ); - const bytes = await fs.readFile(realPath); - assert( - bytes.length <= platformSessionFixtureMaxBytes, - 'supervisor-autonomous-playable-platform-session-fixture-too-large', - ); - let fixture; - try { - fixture = JSON.parse( - decodeUtf8Fatal(bytes, 'platform-session-fixture-invalid-utf8'), - ); - } catch (error) { - throw codedError( - 'supervisor-autonomous-playable-platform-session-fixture-invalid', - error, - ); - } - const expectedKeys = [ - 'schemaVersion', - 'userId', - 'accessToken', - 'apiBaseUrl', - 'generation', - ]; - assert( - isPlainObject(fixture) && - JSON.stringify(Object.keys(fixture).sort()) === - JSON.stringify([...expectedKeys].sort()) && - fixture.schemaVersion === platformSessionFixtureSchema && - isNonEmptyString(fixture.userId) && - isNonEmptyString(fixture.accessToken) && - isNonEmptyString(fixture.apiBaseUrl) && - Number.isSafeInteger(fixture.generation) && - fixture.generation > 0, - 'supervisor-autonomous-playable-platform-session-fixture-invalid', - ); - return { - sourcePath: realPath, - bytes, - fixture, - sha256: createHash('sha256').update(bytes).digest('hex'), - }; -} - -async function installPlatformSessionFixtureIntoIsolatedAppData( - sourceConfigDir, - appDataDir, -) { - if (!isSupervisorAutonomousPlayableLaneDefenseSuite()) return; - const source = - await readPlatformSessionFixtureForIsolatedSuite(sourceConfigDir); - const isolatedPath = path.join( - appDataDir, - isolatedPlatformSessionFixtureName, - ); - await fs.copyFile( - source.sourcePath, - isolatedPath, - fsConstants.COPYFILE_EXCL | fsConstants.COPYFILE_FICLONE, - ); - await fs.chmod(isolatedPath, 0o600).catch(() => {}); - const isolatedMetadata = await fs.lstat(isolatedPath); - assert( - isolatedMetadata.isFile() && - !isolatedMetadata.isSymbolicLink() && - isolatedMetadata.size === source.bytes.length, - 'supervisor-autonomous-playable-platform-session-fixture-copy-invalid', - ); - const isolatedBytes = await fs.readFile(isolatedPath); - assert( - createHash('sha256').update(isolatedBytes).digest('hex') === source.sha256, - 'supervisor-autonomous-playable-platform-session-fixture-copy-mismatch', - ); - state.isolatedRunner.platformSessionFixtureSourcePath = source.sourcePath; - state.isolatedRunner.platformSessionFixturePath = isolatedPath; - state.isolatedRunner.platformSessionFixtureSha256 = source.sha256; - state.isolatedRunner.platformSessionFixturePreviousEnv = - Object.prototype.hasOwnProperty.call(process.env, platformSessionFixtureEnv) - ? process.env[platformSessionFixtureEnv] - : undefined; - process.env[platformSessionFixtureEnv] = isolatedPath; - state.formalConfigPathTranscriptScanner?.addSecrets( - absolutePathVariants(source.sourcePath, isolatedPath), - ); - const previousLeakCount = state.transcriptScanner?.count ?? 0; - state.secrets = [...new Set([...state.secrets, source.fixture.accessToken])]; - rebuildSupervisorSwarmTranscriptScanner(); - state.transcriptScanner.count = previousLeakCount; -} - export async function prepareIsolatedSuiteAppData({ streamAgentId = null, webSearchAgentId = null, @@ -773,8 +569,7 @@ export async function prepareIsolatedSuiteAppData({ isScopedAgentsSuite() || isProjectSkillSuite() || isParallelReadSuite() || - isSupervisorSwarmSuite() || - isSupervisorAutonomousPlayableLaneDefenseSuite() + isSupervisorSwarmSuite() ? 'private-copy' : 'hardlink'; try { @@ -891,10 +686,6 @@ export async function prepareIsolatedSuiteAppData({ state.secrets = [...suiteSecrets]; rebuildSupervisorSwarmTranscriptScanner(); state.transcriptScanner.count = previousLeakCount; - await installPlatformSessionFixtureIntoIsolatedAppData( - sourceConfigDir, - appDataDir, - ); const unexpectedEndpoint = await fs .lstat(path.join(appDataDir, runnerEndpointFileName)) .catch((error) => { @@ -903,46 +694,6 @@ export async function prepareIsolatedSuiteAppData({ }); assert(!unexpectedEndpoint, `${profile.codePrefix}-endpoint-preexisted`); state.runtimeConfigDir = appDataDir; - if (isSupervisorAutonomousPlayableLaneDefenseSuite()) { - const isolatedConfig = await loadConfig(appDataDir); - const expectedBinding = state.config.providerBinding; - const agentIds = [projectSupervisorAgentId]; - const bindings = agentIds.map((agentId) => { - const effective = effectiveAgentLlmConfig(isolatedConfig.config, agentId); - assert( - ['apiKey', 'baseUrl', 'model', 'apiKind', 'reasoningEffort'].every( - (key) => isNonEmptyString(effective[key]), - ), - 'supervisor-autonomous-playable-effective-provider-incomplete', - ); - return { - providerModel: effective.model.trim(), - providerApiKind: effective.apiKind.trim(), - providerReasoningEffort: effective.reasoningEffort.trim(), - providerBaseUrlSha256: hashValue(effective.baseUrl.trim()), - }; - }); - const effectiveBinding = { - ...bindings[0], - boundAgentIds: [...agentIds].sort(), - }; - assert( - expectedBinding && - bindings.every( - (binding) => JSON.stringify(binding) === JSON.stringify(bindings[0]), - ) && - sameSupervisorPlayableProviderBinding( - effectiveBinding, - expectedBinding, - ), - 'supervisor-autonomous-playable-effective-provider-binding-mismatch', - ); - state.supervisorAutonomousPlayable.expectedProviderBinding = { - ...expectedBinding, - }; - state.supervisorAutonomousPlayable.effectiveProviderBinding = - effectiveBinding; - } if (streamAgentId) { const isolatedConfig = await loadConfig(appDataDir); const isolatedEffective = effectiveAgentLlmConfig( @@ -981,22 +732,6 @@ export async function prepareIsolatedSuiteAppData({ ); state.webSearch.effectiveEnabled = true; } - if (isUserInputRuntimeSuite()) { - const isolatedConfig = await loadConfig(appDataDir); - const isolatedEffective = effectiveAgentLlmConfig( - isolatedConfig.config, - projectSupervisorAgentId, - ); - assert( - isolatedEffective.model === 'gpt-5.5' && - ['apiKey', 'baseUrl', 'model'].every( - (key) => - typeof isolatedEffective[key] === 'string' && - isolatedEffective[key].trim().length > 0, - ), - 'user-input-effective-gpt-5-5-config-invalid', - ); - } if (isScopedAgentsSuite()) { const isolatedConfig = await loadConfig(appDataDir); const isolatedEffective = effectiveAgentLlmConfig( @@ -1073,9 +808,8 @@ export async function prepareIsolatedSuiteAppData({ effective.requestTimeoutMs > 0 && Number.isSafeInteger(effective.maxRetries) && effective.maxRetries >= - (isSupervisorSwarmInteractiveChatSuite() || (isSupervisorSwarmTransientRetrySuite() && - agentId !== supervisorSwarmTransientRetryTargetAgentId) + agentId !== supervisorSwarmTransientRetryTargetAgentId ? 0 : 1) && effective.maxRetries <= 3 && @@ -1095,24 +829,6 @@ export async function prepareIsolatedSuiteAppData({ { llm: isolatedConfig.config.llm }, '', ); - if (isSupervisorSwarmMixedHarnessSuite()) { - assert( - globalEffective.model === 'gpt-5.5' && - globalEffective.apiKind === 'openai_chat' && - isNonEmptyString(globalEffective.reasoningEffort) && - Number.isSafeInteger(globalEffective.requestTimeoutMs) && - globalEffective.requestTimeoutMs > 0 && - Number.isSafeInteger(globalEffective.maxRetries) && - globalEffective.maxRetries >= 0 && - globalEffective.maxRetries <= 3 && - Number.isSafeInteger(globalEffective.retryBackoffMs) && - globalEffective.retryBackoffMs > 0 && - ['apiKey', 'baseUrl', 'model'].every((key) => - isNonEmptyString(globalEffective[key]), - ), - 'supervisor-swarm-mixed-default-provider-policy-invalid', - ); - } const configuredAgentIds = Object.keys( isPlainObject(isolatedConfig.config.agentLlm) ? isolatedConfig.config.agentLlm @@ -2000,31 +1716,6 @@ export async function verifyIsolatedSuiteConfigLinksUnchanged() { 'isolated-source-config-changed-during-suite', ); } - const fixture = state.isolatedRunner; - if ( - fixture.platformSessionFixtureSourcePath && - fixture.platformSessionFixturePath && - fixture.platformSessionFixtureSha256 - ) { - const [sourceMetadata, isolatedMetadata, sourceBytes, isolatedBytes] = - await Promise.all([ - fs.lstat(fixture.platformSessionFixtureSourcePath), - fs.lstat(fixture.platformSessionFixturePath), - fs.readFile(fixture.platformSessionFixtureSourcePath), - fs.readFile(fixture.platformSessionFixturePath), - ]); - assert( - sourceMetadata.isFile() && - !sourceMetadata.isSymbolicLink() && - isolatedMetadata.isFile() && - !isolatedMetadata.isSymbolicLink() && - createHash('sha256').update(sourceBytes).digest('hex') === - fixture.platformSessionFixtureSha256 && - createHash('sha256').update(isolatedBytes).digest('hex') === - fixture.platformSessionFixtureSha256, - 'supervisor-autonomous-playable-platform-session-fixture-changed', - ); - } } export async function verifySourceConfigLinkCountsRestored() { @@ -2064,22 +1755,7 @@ export async function removeIsolatedSuiteAppData() { } catch (error) { ownershipError = error; } - try { - await fs.rm(appDataDir, { recursive: true, force: false }); - } finally { - if (state.isolatedRunner.platformSessionFixtureSourcePath) { - const previous = state.isolatedRunner.platformSessionFixturePreviousEnv; - if (previous === undefined) { - delete process.env[platformSessionFixtureEnv]; - } else { - process.env[platformSessionFixtureEnv] = previous; - } - } - state.isolatedRunner.platformSessionFixturePath = null; - state.isolatedRunner.platformSessionFixtureSourcePath = null; - state.isolatedRunner.platformSessionFixtureSha256 = null; - state.isolatedRunner.platformSessionFixturePreviousEnv = undefined; - } + await fs.rm(appDataDir, { recursive: true, force: false }); state.runtimeConfigDir = state.options.configDir; try { await verifySourceConfigLinkCountsRestored(); diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/harness/config.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/harness/config.mjs index 54efb5c75..43768d3b3 100644 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/harness/config.mjs +++ b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/harness/config.mjs @@ -13,15 +13,10 @@ import { responseStreamSuite, scopedAgentsSuite, steerRunnerKillSuite, - supervisorAutonomousPlayableLaneDefenseSuite, - supervisorSwarmAutonomousChatSuite, - supervisorSwarmCollaborationPolicyMixedRecoverySuite, supervisorSwarmFinalReplyTransientRetrySuite, - supervisorSwarmStaticIsolatedAutonomousChatSuite, supervisorSwarmSuite, supervisorSwarmToolPlanHandoffRunnerKillSuite, supervisorSwarmTransientRetrySuite, - userInputRuntimeSuite, webSearchSuite, } from '../runtime-state.mjs'; import { collectApiKeys, isPathInside } from './io.mjs'; @@ -58,7 +53,6 @@ export function parseArguments(args) { suite === responseStreamSuite || suite === webSearchSuite || suite === contextCompactionSuite || - suite === userInputRuntimeSuite || suite === scopedAgentsSuite || suite === projectSkillSuite || suite === parallelReadSuite || @@ -66,10 +60,6 @@ export function parseArguments(args) { suite === supervisorSwarmTransientRetrySuite || suite === supervisorSwarmFinalReplyTransientRetrySuite || suite === supervisorSwarmToolPlanHandoffRunnerKillSuite || - suite === supervisorSwarmAutonomousChatSuite || - suite === supervisorAutonomousPlayableLaneDefenseSuite || - suite === supervisorSwarmStaticIsolatedAutonomousChatSuite || - suite === supervisorSwarmCollaborationPolicyMixedRecoverySuite || suite === steerRunnerKillSuite || processSessionSuites.has(suite), 'unsupported-suite', diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/harness/process.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/harness/process.mjs index 950b9646a..1594f2ed0 100644 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/harness/process.mjs +++ b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/harness/process.mjs @@ -2,7 +2,6 @@ import { assert, codedError, hashValue, - sleep, throwIfShutdownRequested, } from '../assertions/core.mjs'; import { fs, path, spawn, withLoopbackNoProxy } from '../dependencies.mjs'; @@ -13,14 +12,9 @@ import { manifestPath, state, } from '../runtime-state.mjs'; -import { - isSupervisorSwarmTransientRetrySuite, - recordSupervisorSwarmChatSessionFailureDiagnostic, -} from '../suites/supervisor-swarm.mjs'; +import { isSupervisorSwarmTransientRetrySuite } from '../suites/supervisor-swarm.mjs'; import { isIsolatedRunnerSuite } from './reporting.mjs'; -export const activeInteractiveCliSessions = new Set(); - export async function prepareCliBinary() { const cargo = process.platform === 'win32' ? 'cargo.exe' : 'cargo'; await runProcess( @@ -84,8 +78,8 @@ export function buildCliChildEnvironment() { NO_COLOR: '1', RUST_BACKTRACE: '0', }; - // The deterministic playable suite copies its account fixture into the - // sibling isolated AppData directory. Set the path explicitly here so + // The isolated runner copies its account fixture into the sibling isolated + // AppData directory. Set the path explicitly here so // every CLI and the Runner it launches use the isolated copy, even if the // parent harness environment was restored or changed after setup. if (state.isolatedRunner.platformSessionFixturePath) { @@ -144,197 +138,6 @@ export function codedProcessError(code, details) { return error; } -export function startInteractiveCli(args) { - assert(Boolean(state.cliBinary), 'interactive-cli-binary-not-ready'); - assert(Boolean(state.runtimeConfigDir), 'interactive-config-dir-not-ready'); - if ( - isIsolatedRunnerSuite() && - state.options?.configDir && - path.resolve(state.runtimeConfigDir) === - path.resolve(state.options.configDir) - ) { - state.isolatedRunner.sourceConfigCliCallCount += 1; - } - const child = spawn( - state.cliBinary, - [...args, '--config-dir', state.runtimeConfigDir], - { - cwd: appRoot, - env: buildCliChildEnvironment(), - stdio: ['pipe', 'pipe', 'pipe'], - }, - ); - return createInteractiveCliSession(child); -} - -export function createInteractiveCliSession(child) { - activeCommandChildren.add(child); - const session = { - child, - stdout: Buffer.alloc(0), - stderr: Buffer.alloc(0), - exited: false, - exitInfo: null, - exitPromise: null, - closed: false, - closeInfo: null, - closePromise: null, - stdioClosed: false, - stdioCloseInfo: null, - spawnError: null, - stdinError: null, - }; - activeInteractiveCliSessions.add(session); - session.exitPromise = new Promise((resolve) => { - const settle = (result) => { - if (session.exited) return; - activeCommandChildren.delete(child); - session.exited = true; - session.closed = true; - session.exitInfo = result; - session.closeInfo = result; - resolve(result); - }; - child.once('error', (error) => { - session.spawnError = error; - settle({ code: null, signal: null, error }); - }); - child.once('exit', (code, signal) => { - settle({ code, signal, error: null }); - }); - }); - session.closePromise = new Promise((resolve) => { - child.once('close', (code, signal) => { - activeCommandChildren.delete(child); - activeInteractiveCliSessions.delete(session); - session.stdioClosed = true; - session.stdioCloseInfo = { - code, - signal, - error: session.spawnError, - }; - resolve(session.stdioCloseInfo); - }); - }); - child.stdin?.on('error', (error) => { - session.stdinError ??= error; - }); - child.stdout.on('data', (chunk) => { - state.transcriptScanner?.scan('interactive-stdout', chunk); - state.projectPathTranscriptScanner?.scan('interactive-stdout', chunk); - state.formalConfigPathTranscriptScanner?.scan('interactive-stdout', chunk); - session.stdout = appendBounded(session.stdout, chunk, commandOutputLimit); - }); - child.stderr.on('data', (chunk) => { - state.transcriptScanner?.scan('interactive-stderr', chunk); - state.projectPathTranscriptScanner?.scan('interactive-stderr', chunk); - state.formalConfigPathTranscriptScanner?.scan('interactive-stderr', chunk); - session.stderr = appendBounded(session.stderr, chunk, commandOutputLimit); - }); - return session; -} - -export function interactiveCliOutput(session) { - return `${session.stdout.toString('utf8')}\n${session.stderr.toString('utf8')}`; -} - -export function writeInteractiveCliLine(session, line) { - assert(!session.closed, 'interactive-cli-already-closed'); - assert(session.child.stdin.writable, 'interactive-cli-stdin-not-writable'); - session.child.stdin.write(`${line}\n`); -} - -export async function waitForInteractiveCliOutput( - session, - predicate, - code, - timeoutMs, - { allowAfterProcessExit = false } = {}, -) { - const deadline = Date.now() + timeoutMs; - while (Date.now() < deadline) { - const output = interactiveCliOutput(session); - if (predicate(output)) return output; - if (session.exited && !allowAfterProcessExit) { - if (session === state.supervisorSwarmCliSession) { - recordSupervisorSwarmChatSessionFailureDiagnostic(session); - } - throw codedError(`${code}-cli-exited`); - } - if (session.stdioClosed) { - if (session === state.supervisorSwarmCliSession) { - recordSupervisorSwarmChatSessionFailureDiagnostic(session); - } - throw codedError(`${code}-cli-stdio-closed`); - } - await sleep(50); - } - throw codedError(code); -} - -export async function waitForInteractiveCliExit(session, timeoutMs) { - const result = await Promise.race([ - session.exitPromise, - sleep(timeoutMs).then(() => null), - ]); - if (!result) throw codedError('interactive-cli-exit-timeout'); - if (result.error) throw codedError('interactive-cli-process-error'); - assert( - result.code === 0 && result.signal === null, - 'interactive-cli-exit-invalid', - ); - return result; -} - -export async function closeInteractiveCli(session) { - if (!session) return null; - if (session.exited) return session.exitInfo; - if ( - session.child.stdin.writable && - !session.child.stdin.writableEnded && - !session.child.stdin.destroyed - ) { - session.child.stdin.write('/quit\n'); - } - let result = await Promise.race([ - session.exitPromise, - sleep(3_000).then(() => null), - ]); - if (!result && !session.exited) { - session.child.kill('SIGTERM'); - result = await Promise.race([ - session.exitPromise, - sleep(2_000).then(() => null), - ]); - } - if (!result && !session.exited) { - session.child.kill('SIGKILL'); - result = await Promise.race([ - session.exitPromise, - sleep(5_000).then(() => null), - ]); - } - assert(Boolean(result), 'interactive-cli-cleanup-timeout'); - return result; -} - -export async function waitForInteractiveCliStdioClose(session, timeoutMs) { - if (!session || session.stdioClosed) return session?.stdioCloseInfo ?? null; - const result = await Promise.race([ - session.closePromise, - sleep(timeoutMs).then(() => null), - ]); - if (!result) throw codedError('interactive-cli-stdio-close-timeout'); - return result; -} - -export function destroyInteractiveCliOutputStreams(session) { - if (!session) return; - for (const stream of [session.child.stdout, session.child.stderr]) { - if (stream && !stream.destroyed) stream.destroy(); - } -} - export async function runProcess( program, args, @@ -406,214 +209,6 @@ export async function runProcess( }); } -export async function listSystemProcessIdentities() { - if (process.platform === 'win32') { - const systemRoot = process.env.SystemRoot ?? process.env.SYSTEMROOT; - assert( - typeof systemRoot === 'string' && path.isAbsolute(systemRoot), - 'owned-process-snapshot-system-root-invalid', - ); - const powershell = path.join( - systemRoot, - 'System32/WindowsPowerShell/v1.0/powershell.exe', - ); - const metadata = await fs.lstat(powershell); - assert( - metadata.isFile() && !metadata.isSymbolicLink(), - 'owned-process-snapshot-powershell-invalid', - ); - const result = await runProcess( - powershell, - [ - '-NoProfile', - '-NonInteractive', - '-Command', - '$processes = @(Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,CreationDate,Name); $processes | ConvertTo-Json -Compress', - ], - { - cwd: appRoot, - timeoutMs: 30_000, - env: { ...process.env, NO_COLOR: '1', RUST_BACKTRACE: '0' }, - }, - ); - const parsed = JSON.parse(result.stdout); - return (Array.isArray(parsed) ? parsed : [parsed]) - .map((record) => ({ - pid: Number(record?.ProcessId), - parentPid: Number(record?.ParentProcessId), - startedAt: String(record?.CreationDate ?? ''), - name: String(record?.Name ?? ''), - })) - .filter(validSystemProcessIdentity); - } - assert( - process.platform === 'linux' || process.platform === 'darwin', - 'owned-process-snapshot-platform-unsupported', - ); - const result = await runProcess( - 'ps', - ['-A', '-o', 'pid=', '-o', 'ppid=', '-o', 'lstart=', '-o', 'comm='], - { cwd: appRoot, timeoutMs: 30_000 }, - ); - return result.stdout - .split(/\r?\n/u) - .map((line) => line.trim()) - .filter(Boolean) - .map((line) => { - const fields = line.split(/\s+/u); - return { - pid: Number(fields[0]), - parentPid: Number(fields[1]), - startedAt: fields.slice(2, 7).join(' '), - name: fields.slice(7).join(' '), - }; - }) - .filter(validSystemProcessIdentity); -} - -function validSystemProcessIdentity(record) { - return ( - Number.isSafeInteger(record?.pid) && - record.pid > 0 && - Number.isSafeInteger(record.parentPid) && - record.parentPid >= 0 && - typeof record.startedAt === 'string' && - record.startedAt.length > 0 && - typeof record.name === 'string' && - record.name.length > 0 - ); -} - -export function buildOwnedProcessCleanupSnapshot( - processRecords, - { rootPids = [], runnerPid = null, helperPids = [] } = {}, -) { - assert( - Array.isArray(processRecords) && - Array.isArray(rootPids) && - Array.isArray(helperPids), - 'owned-process-snapshot-input-invalid', - ); - const records = processRecords.filter(validSystemProcessIdentity); - const byPid = new Map(records.map((record) => [record.pid, record])); - const childrenByParent = new Map(); - for (const record of records) { - const children = childrenByParent.get(record.parentPid) ?? []; - children.push(record.pid); - childrenByParent.set(record.parentPid, children); - } - const normalizedRunnerPid = Number.isSafeInteger(runnerPid) - ? runnerPid - : null; - const helperPidSet = new Set( - helperPids.filter((pid) => Number.isSafeInteger(pid) && pid > 0), - ); - const roots = [ - ...new Set( - [...rootPids, normalizedRunnerPid, ...helperPidSet].filter( - (pid) => Number.isSafeInteger(pid) && pid > 0, - ), - ), - ]; - assert(roots.length > 0, 'owned-process-snapshot-root-missing'); - const ownedPids = new Set(); - const queue = [...roots]; - while (queue.length > 0) { - const pid = queue.shift(); - if (ownedPids.has(pid)) continue; - ownedPids.add(pid); - queue.push(...(childrenByParent.get(pid) ?? [])); - } - const identities = [...ownedPids] - .map((pid) => byPid.get(pid)) - .filter(Boolean) - .map((record) => ({ - pid: record.pid, - startedAt: record.startedAt, - name: record.name, - kind: ownedProcessKind(record, normalizedRunnerPid, helperPidSet), - })) - .sort((left, right) => left.pid - right.pid); - return { - identities, - observedCounts: countOwnedProcessKinds(identities), - }; -} - -function ownedProcessKind(record, runnerPid, helperPids) { - if (record.pid === runnerPid) return 'runner'; - if (helperPids.has(record.pid)) return 'helper'; - const name = path.basename(record.name).toLowerCase(); - if (/^node(?:\.exe)?$/u.test(name)) return 'node'; - if (/^(?:chrome|chromium|msedge|google-chrome)(?:\.exe)?$/u.test(name)) { - return 'browser'; - } - return 'command'; -} - -function countOwnedProcessKinds(identities) { - const counts = { - runner: 0, - helper: 0, - node: 0, - browser: 0, - command: 0, - total: identities.length, - }; - for (const identity of identities) counts[identity.kind] += 1; - return counts; -} - -export function inspectOwnedProcessCleanupResiduals( - snapshot, - processRecords, - { activeCommandChildCount = 0, activeInteractiveCliSessionCount = 0 } = {}, -) { - assert( - Array.isArray(snapshot?.identities) && Array.isArray(processRecords), - 'owned-process-residual-input-invalid', - ); - const currentByPid = new Map( - processRecords - .filter(validSystemProcessIdentity) - .map((record) => [record.pid, record]), - ); - const residualIdentities = snapshot.identities.filter((identity) => { - const current = currentByPid.get(identity.pid); - return ( - current?.startedAt === identity.startedAt && - current?.name === identity.name - ); - }); - return { - residualCounts: countOwnedProcessKinds(residualIdentities), - activeCommandChildCount, - activeInteractiveCliSessionCount, - clean: - residualIdentities.length === 0 && - activeCommandChildCount === 0 && - activeInteractiveCliSessionCount === 0, - }; -} - -export async function captureOwnedProcessCleanupSnapshot(options) { - return buildOwnedProcessCleanupSnapshot( - await listSystemProcessIdentities(), - options, - ); -} - -export async function verifyOwnedProcessCleanupSnapshot(snapshot) { - return inspectOwnedProcessCleanupResiduals( - snapshot, - await listSystemProcessIdentities(), - { - activeCommandChildCount: activeCommandChildren.size, - activeInteractiveCliSessionCount: activeInteractiveCliSessions.size, - }, - ); -} - export function appendBounded(current, chunk, limit) { const combined = Buffer.concat([current, chunk]); return combined.length <= limit diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/harness/project.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/harness/project.mjs index 9fa9ea193..883e5fbea 100644 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/harness/project.mjs +++ b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/harness/project.mjs @@ -28,12 +28,10 @@ import { isGoalRuntimeSuite, } from '../suites/goal.mjs'; import { isResponseStreamSuite } from '../suites/response-stream.mjs'; -import { isSupervisorAutonomousPlayableLaneDefenseSuite } from '../suites/supervisor-autonomous-playable.mjs'; import { isSupervisorSwarmSuite, supervisorSwarmVerificationFixtureSource, } from '../suites/supervisor-swarm.mjs'; -import { isUserInputRuntimeSuite } from '../suites/user-input.mjs'; import { isWebSearchSuite } from '../suites/web-search.mjs'; import { createSentinelOwnedTempDirectory } from './app-data.mjs'; import { effectiveAgentLlmConfig } from './config.mjs'; @@ -41,19 +39,15 @@ import { runProcess } from './process.mjs'; import { isIsolatedRunnerSuite } from './reporting.mjs'; export function requiredAgentIdsForSuite() { - return isUserInputRuntimeSuite() - ? [projectSupervisorAgentId] - : isSupervisorAutonomousPlayableLaneDefenseSuite() - ? [projectSupervisorAgentId] - : isSupervisorSwarmSuite() - ? [ - projectSupervisorAgentId, - supervisorSwarmDesignAgentId, - supervisorSwarmQualityAgentId, - ] - : isIsolatedRunnerSuite() - ? [mainAgentId] - : [mainAgentId, 'quality-review']; + return isSupervisorSwarmSuite() + ? [ + projectSupervisorAgentId, + supervisorSwarmDesignAgentId, + supervisorSwarmQualityAgentId, + ] + : isIsolatedRunnerSuite() + ? [mainAgentId] + : [mainAgentId, 'quality-review']; } export function expectedProviderBindingForSuite(config) { @@ -120,11 +114,9 @@ export async function checkPrerequisites(config) { return { llmConfigured, providerBinding, - chromeAvailable: - !isIsolatedRunnerSuite() || - isSupervisorAutonomousPlayableLaneDefenseSuite() - ? Boolean(await findSupportedBrowser()) - : false, + chromeAvailable: !isIsolatedRunnerSuite() + ? Boolean(await findSupportedBrowser()) + : false, editorApiConfigured, }; } @@ -267,8 +259,7 @@ export async function seedDisposableProject({ ? supervisorSwarmVerificationFixtureSource() : isGoalRuntimeSuite() || isResponseStreamSuite() || - isWebSearchSuite() || - isSupervisorAutonomousPlayableLaneDefenseSuite() + isWebSearchSuite() ? goalRevisionOneVerificationFixtureSource() : goalRevisionTwoVerificationFixtureSource(), ), @@ -405,23 +396,6 @@ export function seededGameHtml() { `; } -export function productionDefaultGameIndexHtml() { - return ` - - - - - Genarrative Game Draft - - -
还没有生成游戏。回到聊天输入创意并确认生成后,这里会写入可试玩原型。
- -`; -} - export function buildTaskPrompt(suite) { const editorAssetOutcome = suite === 'full' diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/harness/reporting.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/harness/reporting.mjs index a176cf335..c59503ec2 100644 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/harness/reporting.mjs +++ b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/harness/reporting.mjs @@ -20,9 +20,7 @@ import { isProjectSkillSuite } from '../suites/project-skill.mjs'; import { isResponseStreamSuite } from '../suites/response-stream.mjs'; import { isScopedAgentsSuite } from '../suites/scoped-agents.mjs'; import { isSteerRunnerKillSuite } from '../suites/steer-runner-kill.mjs'; -import { isSupervisorAutonomousPlayableLaneDefenseSuite } from '../suites/supervisor-autonomous-playable.mjs'; import { isSupervisorSwarmSuite } from '../suites/supervisor-swarm.mjs'; -import { isUserInputRuntimeSuite } from '../suites/user-input.mjs'; import { isWebSearchSuite } from '../suites/web-search.mjs'; import { isPathInside, readJson } from './io.mjs'; @@ -88,17 +86,12 @@ export function buildSummary() { config: state.config, blocked: state.blocked, run: { - agentId: - isUserInputRuntimeSuite() || - isSupervisorAutonomousPlayableLaneDefenseSuite() || - isSupervisorSwarmSuite() - ? projectSupervisorAgentId - : mainAgentId, + agentId: isSupervisorSwarmSuite() + ? projectSupervisorAgentId + : mainAgentId, runIdHash: hashValue(state.initialRunId), sessionIdHash: hashValue(state.initialSessionId), - runnerKilled: isSupervisorAutonomousPlayableLaneDefenseSuite() - ? false - : state.runnerKilled, + runnerKilled: state.runnerKilled, resumed: state.resumed, identityStable: state.identityStable, }, @@ -110,11 +103,9 @@ export function buildSummary() { projectPathReportLeakCount: state.projectPathReportLeakCount, ...(isWebSearchSuite() || isContextCompactionSuite() || - isUserInputRuntimeSuite() || isScopedAgentsSuite() || isProjectSkillSuite() || isParallelReadSuite() || - isSupervisorAutonomousPlayableLaneDefenseSuite() || isSupervisorSwarmSuite() || isSteerRunnerKillSuite() ? { @@ -408,11 +399,9 @@ export function isIsolatedRunnerSuite() { isResponseStreamSuite() || isWebSearchSuite() || isContextCompactionSuite() || - isUserInputRuntimeSuite() || isScopedAgentsSuite() || isProjectSkillSuite() || isParallelReadSuite() || - isSupervisorAutonomousPlayableLaneDefenseSuite() || isSupervisorSwarmSuite() ); } diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/harness/runtime.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/harness/runtime.mjs index f6f6ae0a4..d2f2cd567 100644 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/harness/runtime.mjs +++ b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/harness/runtime.mjs @@ -87,11 +87,7 @@ import { import { isProjectSkillSuite } from '../suites/project-skill.mjs'; import { isScopedAgentsSuite } from '../suites/scoped-agents.mjs'; import { isSteerRunnerKillSuite } from '../suites/steer-runner-kill.mjs'; -import { - confirmSupervisorSwarmPendingActionsInChat, - isSupervisorSwarmInteractiveChatSuite, - isSupervisorSwarmSuite, -} from '../suites/supervisor-swarm.mjs'; +import { isSupervisorSwarmSuite } from '../suites/supervisor-swarm.mjs'; import { killRunnerPidOnce, verifyOwnedRunnerForKill } from './app-data.mjs'; import { isPlainObject } from './config.mjs'; import { @@ -1038,13 +1034,6 @@ export async function confirmPendingActions( allowedTools = null, shouldConfirm = () => true, ) { - if (isSupervisorSwarmInteractiveChatSuite()) { - await confirmSupervisorSwarmPendingActionsInChat( - allowedTools, - shouldConfirm, - ); - return; - } for (const pending of await findPendingActions()) { if (state.confirmedActionIds.has(pending.actionId)) continue; if (!shouldConfirm(pending)) continue; diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/runtime-state.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/runtime-state.mjs index c27fa5aea..88e662cfc 100644 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/runtime-state.mjs +++ b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/runtime-state.mjs @@ -43,12 +43,6 @@ export const contextCompactionAppDataSentinelFileName = export const contextCompactionAppDataSentinelSchema = 'genarrative-agent-runtime-real-e2e-context-compaction-appdata.v1'; -export const userInputAppDataSentinelFileName = - '.agent-runtime-real-e2e-user-input-appdata.json'; - -export const userInputAppDataSentinelSchema = - 'genarrative-agent-runtime-real-e2e-user-input-appdata.v1'; - export const scopedAgentsAppDataSentinelFileName = '.agent-runtime-real-e2e-scoped-agents-appdata.json'; @@ -109,30 +103,6 @@ export const supervisorSwarmToolPlanHandoffCheckpointReachedFileName = export const supervisorSwarmToolPlanHandoffCheckpointReachedSchema = 'game-creator-tool-plan-handoff-runner-kill-checkpoint-reached.v1'; -export const supervisorSwarmAutonomousChatAppDataSentinelFileName = - '.agent-runtime-real-e2e-supervisor-swarm-autonomous-chat-appdata.json'; - -export const supervisorSwarmAutonomousChatAppDataSentinelSchema = - 'genarrative-agent-runtime-real-e2e-supervisor-swarm-autonomous-chat-appdata.v1'; - -export const supervisorAutonomousPlayableAppDataSentinelFileName = - '.agent-runtime-real-e2e-supervisor-autonomous-playable-appdata.json'; - -export const supervisorAutonomousPlayableAppDataSentinelSchema = - 'genarrative-agent-runtime-real-e2e-supervisor-autonomous-playable-appdata.v1'; - -export const supervisorSwarmStaticIsolatedAutonomousChatAppDataSentinelFileName = - '.agent-runtime-real-e2e-supervisor-swarm-static-isolated-autonomous-chat-appdata.json'; - -export const supervisorSwarmStaticIsolatedAutonomousChatAppDataSentinelSchema = - 'genarrative-agent-runtime-real-e2e-supervisor-swarm-static-isolated-autonomous-chat-appdata.v1'; - -export const supervisorSwarmCollaborationPolicyAppDataSentinelFileName = - '.agent-runtime-real-e2e-supervisor-swarm-collaboration-policy-appdata.json'; - -export const supervisorSwarmCollaborationPolicyAppDataSentinelSchema = - 'genarrative-agent-runtime-real-e2e-supervisor-swarm-collaboration-policy-appdata.v1'; - export const mainAgentId = 'code-prototype'; export const projectSupervisorAgentId = 'project-supervisor'; @@ -179,8 +149,6 @@ export const webSearchSuite = 'web-search'; export const contextCompactionSuite = 'context-compaction'; -export const userInputRuntimeSuite = 'user-input-runtime'; - export const scopedAgentsSuite = 'scoped-agents'; export const projectSkillSuite = 'project-skill'; @@ -213,18 +181,6 @@ export const supervisorSwarmTransientRetryBackoffMs = 30_000; export const supervisorSwarmTransientRetryPreDueGuardMs = 1_500; -export const supervisorSwarmAutonomousChatSuite = - 'supervisor-swarm-autonomous-chat'; - -export const supervisorAutonomousPlayableLaneDefenseSuite = - 'supervisor-autonomous-playable-lane-defense'; - -export const supervisorSwarmStaticIsolatedAutonomousChatSuite = - 'supervisor-swarm-static-isolated-autonomous-chat'; - -export const supervisorSwarmCollaborationPolicyMixedRecoverySuite = - 'supervisor-swarm-collaboration-policy-mixed-recovery'; - export const runtimeContextBundleSchemaVersion = 'game-creator-runtime-context-bundle.v5'; @@ -234,17 +190,6 @@ export const providerRequestLifecycleSchemaVersion = export const providerActionBatchSchemaVersion = 'game-creator-provider-action-batch.v2'; -export const runProfileBindingSchemaVersion = - 'game-creator-run-profile-binding.v1'; - -export const autonomousCompletionContractSchemaVersion = - 'game-creator-autonomous-completion-contract.v2'; - -export const autonomousPlaytestReceiptSchemaVersion = - 'game-creator-autonomous-playtest-receipt.v2'; - -export const autonomousGameBuildRunProfile = 'autonomous-game-build'; - export const supervisorCollaborationPolicySchemaVersion = 'game-creator-supervisor-collaboration-policy.v1'; @@ -287,12 +232,6 @@ export const contextCompactionTriggerTurns = new Set([4, 8]); export const contextCompactionConstraintCanary = `GENARRATIVE_CONTEXT_CONSTRAINT_${randomUUID().replaceAll('-', '').slice(0, 20)}`; -export const userInputAnswerCanary = `GENARRATIVE_USER_CHOICE_${randomUUID() - .replaceAll('-', '') - .slice(0, 20)}`; - -export const userInputAnswerText = `选择轻量像素风,优先保证移动端轮廓和动作可读性;确认标记 ${userInputAnswerCanary}`; - export const scopedAgentsRootCanary = `GENARRATIVE_SCOPE_ROOT_${randomUUID() .replaceAll('-', '') .slice(0, 20)}`; @@ -461,143 +400,6 @@ export const supervisorSwarmConfirmedTools = [ 'project.verify', ]; -export const supervisorSwarmAutonomousTask = - '请把这个试玩项目推进到可以交给首批玩家体验的状态。以仓库现有的正式交付要求、临时检查要求和实际验证结果为准;按阶段生效的临时检查必须在各自生效后全部完成,不能把后续检查并入前置检查或漏掉。完成后简短说明交付内容、验证结论和仍需关注的问题。'; - -export const supervisorAutonomousPlayableLaneDefenseTask = - '做一个植物大战僵尸式的塔防游戏,要能选择植物、阻挡敌人、正常闯关,并且生成后可以直接试玩。'; - -export const supervisorAutonomousPlayableSourceFieldMaxChars = 8_000; - -export const supervisorAutonomousPlayableSourceTotalMaxChars = 10_000; - -export const supervisorAutonomousPlayableSafeStatuses = new Set([ - 'cancelling', - 'budget-exhausted', - 'cancelled', - 'completed', - 'failed', - 'idle', - 'needs-reconciliation', - 'paused', - 'pausing', - 'pending', - 'running', - 'waiting-for-confirmation', - 'waiting-for-user-input', -]); - -export const supervisorAutonomousPlayableSafePhases = new Set([ - 'action', - 'budget-exhausted', - 'cancelled', - 'cancelling', - 'completion-contract-failed', - 'completed', - 'conversation-write-failed', - 'executing', - 'failed', - 'finalizing', - 'idle', - 'needs-reconciliation', - 'observation', - 'parent-terminal', - 'paused', - 'pausing', - 'planning', - 'provider-action-batch', - 'queued', - 'response', - 'running', - 'waiting-for-agent', - 'waiting-for-confirmation', - 'waiting-for-delegate-receipts', - 'waiting-for-isolated-join', - 'waiting-for-process-session', - 'waiting-for-provider-retry', - 'waiting-for-user-input', -]); - -export const supervisorAutonomousPlayableSafeDeliveryStatuses = new Set([ - 'claimed-by-parent', - 'dispatched', - 'ready', - 'suppressed', -]); - -export const supervisorAutonomousPlayableSafeTerminalStatuses = new Set([ - 'budget-exhausted', - 'cancelled', - 'completed', - 'failed', -]); - -export const supervisorAutonomousPlayableSafeTurnOutcomes = new Set([ - 'failed', - 'incomplete', - 'needs-reconciliation', - 'settled', -]); - -export const laneDefensePlaytestRequiredAssertions = [ - 'state-surface-valid', - 'level-positive', - 'start-control-clicked', - 'start-sequence-advanced', - 'start-phase-playing', - 'defender-option-control-clicked', - 'defender-selection-sequence-advanced', - 'defender-selection-recorded', - 'lane-cell-control-clicked', - 'defender-placement-sequence-advanced', - 'defender-count-increased', - 'enemies-present-after-placement', - 'speed-up-control-clicked', - 'battle-sequence-advanced', - 'battle-sequence-monotonic', - 'enemy-position-changed', - 'enemy-health-decreased', - 'phase-won', - 'next-level-control-clicked', - 'next-level-sequence-advanced', - 'level-increased', - 'restart-control-clicked', - 'restart-sequence-advanced', - 'restart-phase-ready-or-playing', -]; - -export const supervisorSwarmAutonomousRoutingTerms = [ - supervisorSwarmDesignAgentId, - supervisorSwarmQualityAgentId, - 'agent.delegate', - 'agent.run_status', - '同一个 planning 轮次', - '同轮', - '并行', - '返工', - 'repair', - '一次', - '两个 Agent', - '两个专业', - 'Runner', - 'pidfd', - 'actionId', - 'delegationId', -]; - -export const supervisorSwarmCollaborationPolicyControlTerms = [ - '.agent/collaboration-policy.json', - supervisorCollaborationPolicySchemaVersion, - 'requiredInitialWave', - 'minStaticDelegates', - 'requiredStaticAgentIds', - 'minIsolatedChildren', - 'minIsolatedGroupsBeforeClaim', - 'orchestratorOnlyAfterDelegation', - 'policyFingerprint', - 'contractFingerprint', -]; - export const supervisorSwarmIsolatedReviews = [ { path: 'e2e/isolated-a/evidence.txt', @@ -625,17 +427,6 @@ export const supervisorSwarmIsolatedReviews = [ }, ]; -export const supervisorSwarmIsolatedReviewGroups = [ - supervisorSwarmIsolatedReviews.slice(0, 2), - supervisorSwarmIsolatedReviews.slice(2), -]; - -export const supervisorSwarmInitialIsolatedReviews = - supervisorSwarmIsolatedReviewGroups[0]; - -export const supervisorSwarmFollowupIsolatedReviews = - supervisorSwarmIsolatedReviewGroups[1]; - export const webSearchBaselineApiUrl = 'https://api.github.com/repos/nodejs/node/releases/latest'; @@ -703,8 +494,6 @@ export const pollIntervalMs = 750; export const runTimeoutMs = 30 * 60 * 1000; -export const supervisorAutonomousPlayableRunTimeoutMs = 60 * 60 * 1000; - export const supervisorSwarmTerminalSidecarCleanupTimeoutMs = 10_000; export const processRunnerKillStartTimeoutMs = 5 * 60 * 1000; @@ -938,10 +727,6 @@ export class BlockedError extends Error { export const isolatedRunnerState = { appDataDir: null, - platformSessionFixturePath: null, - platformSessionFixtureSourcePath: null, - platformSessionFixtureSha256: null, - platformSessionFixturePreviousEnv: undefined, ownerToken: null, createdAt: 0, current: null, @@ -968,9 +753,6 @@ export const state = { linuxPidfdPythonPath: null, windowsProcessHandlePowerShellPath: null, cleanupInProgress: false, - userInputCliSession: null, - supervisorSwarmCliSession: null, - supervisorAutonomousPlayableCliSession: null, status: 'FAIL', suite: null, options: null, @@ -1082,23 +864,6 @@ export const state = { finalReplyFingerprint: null, reportLeakCount: 0, }, - userInput: { - requestId: null, - responseId: null, - actionId: null, - questionMessageId: null, - answerMessageId: null, - questionCount: 0, - optionCount: 0, - providerStartedBeforeKill: 0, - providerStartedAfterRestart: 0, - conversationCountBeforeKill: 0, - conversationCountAfterRestart: 0, - oldRunnerBootId: null, - newRunnerBootId: null, - privateValues: [], - reportLeakCount: 0, - }, scopedAgents: { effectiveModel: null, effectiveApiKind: null, @@ -1120,20 +885,6 @@ export const state = { privateValues: [], reportLeakCount: 0, }, - supervisorAutonomousPlayable: { - freshInitBaselineUsed: false, - initialGameIndexSha256: null, - expectedProviderBinding: null, - effectiveProviderBinding: null, - stdinWriteCount: 0, - stdinEnded: false, - stdinBytes: 0, - turnReport: null, - cliOutput: '', - privateValues: [], - reportLeakCount: 0, - ownedProcessCleanupSnapshot: null, - }, supervisorSwarm: { effectiveModel: null, effectiveApiKind: null, @@ -1215,47 +966,7 @@ export const state = { toolPlanHandoffLifecycleClosedExactlyOnce: false, toolPlanHandoffAuditIdempotent: false, toolPlanHandoffRecoveredPlanFingerprintMatched: false, - autonomousTaskRecipeFree: false, - autonomousRepositoryRecipeFree: false, - interactiveCliUsed: false, - chatSessionUnexpectedlyClosed: false, - chatSessionFailureKind: 'none', - chatSessionExitCode: 'none', - chatSessionCloseSignal: 'none', - chatSessionProcessErrorCode: 'none', - chatSessionStderrChars: 0, - chatSessionStderrSha256: 'none', - turnReport: null, - mixedSpawnActionId: null, - mixedSpawnRequestHash: null, - mixedFollowupSpawnActionId: null, - mixedFollowupSpawnRequestHash: null, - staticIsolatedProviderRequestIds: [], - staticIsolatedProviderOverlapObserved: false, - preKillMixedIdentity: null, collaborationPolicyWritten: false, - collaborationPolicySnapshotInitialRecord: null, - collaborationPolicySnapshotInitialBytes: null, - collaborationPolicySnapshotInitialBytesSha256: null, - collaborationPolicySnapshotInitialIdentityHash: null, - collaborationPolicySnapshotBindingInitialRecord: null, - collaborationPolicySnapshotBindingInitialBytes: null, - collaborationPolicySnapshotBindingInitialBytesSha256: null, - chatSessionFailureDiagnostic: null, - collaborationPolicyDriftFixtureWritten: false, - collaborationPolicyDriftFixturePolicyFingerprint: null, - collaborationPolicyDriftedMinIsolatedGroupsBeforeClaim: null, - initialBatchRecoveryBoundaryObserved: false, - initialBatchRecoveryOldRunnerBootId: null, - initialBatchRecoveryNewRunnerBootId: null, - initialBatchRecoveryPreKillIdentity: null, - initialBatchRecoveryPostRecoveryIdentity: null, - initialBatchRecoveryPreKillSideEffects: null, - initialBatchRecoveryPostRecoverySideEffects: null, - initialBatchRecoveryIdentityStable: false, - initialBatchRecoveryConfirmationRestoredCount: 0, - initialBatchRecoveryPidfdClaimCount: 0, - initialBatchRecoveryPidfdSignalCount: 0, }, confirmedActionIds: new Set(), cleanupPerformed: false, diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/self-test.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/self-test.mjs index b3f83ea49..f427b7b71 100644 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/self-test.mjs +++ b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/self-test.mjs @@ -8,10 +8,8 @@ import { collectToolPlanRepairAuditEvidence, countExactSecrets, emptyToolPlanRepairCountsByProtocolErrorKind, - finalMessageId, hasSafeToolPlanAuditPayload, isNonEmptyString, - runtimePublicStatusMessageId, sumObjectValues, toolPlanRepairEvidenceHasNoFatalLocalRepair, } from '../assertions/runtime.mjs'; @@ -22,68 +20,37 @@ import { ensureOwnedRunnerStableKillSupport, isolatedSuiteAppDataProfile, isolatedSuiteProtectsSourceAppData, - isolatedSuiteUsesSiblingAppData, isProcessAlive, openOwnedRunnerKillHandle, - sameSupervisorPlayableProviderBinding, signalOwnedRunnerKillHandle, sourceAppDataDirectoryEventIsViolation, waitForChildClose, } from '../harness/app-data.mjs'; import { parseArguments } from '../harness/config.mjs'; -import { - buildOwnedProcessCleanupSnapshot, - closeInteractiveCli, - createInteractiveCliSession, - destroyInteractiveCliOutputStreams, - inspectOwnedProcessCleanupResiduals, - listSystemProcessIdentities, - safeProcessFailureDiagnostic, - waitForInteractiveCliExit, - waitForInteractiveCliOutput, - waitForInteractiveCliStdioClose, -} from '../harness/process.mjs'; -import { - expectedProviderBindingForSuite, - seededGameHtml, -} from '../harness/project.mjs'; +import { seededGameHtml } from '../harness/project.mjs'; import { buildSummary, isIsolatedRunnerSuite, providerUsedFromEvidence, } from '../harness/reporting.mjs'; -import { - agentConversationPath, - isEmptyExecutionOwnerLock, -} from '../harness/runtime.mjs'; +import { isEmptyExecutionOwnerLock } from '../harness/runtime.mjs'; import { activeCommandChildren, appRoot, commandFailureMarker, commandPassedMarker, - isolatedAgentJoinClaimSchemaVersion, projectSupervisorAgentId, providerActionBatchSchemaVersion, repoRoot, runnerEndpointFileName, state, StreamingSecretScanner, - supervisorAutonomousPlayableAppDataSentinelFileName, - supervisorAutonomousPlayableAppDataSentinelSchema, - supervisorAutonomousPlayableLaneDefenseSuite, - supervisorAutonomousPlayableLaneDefenseTask, supervisorCollaborationContractSchemaVersion, supervisorCollaborationPolicySchemaVersion, supervisorCollaborationPolicySnapshotInitialBatchBinding, supervisorCollaborationPolicySnapshotSchemaVersion, - supervisorSwarmAutonomousChatSuite, - supervisorSwarmCollaborationPolicyMixedRecoverySuite, - supervisorSwarmFollowupIsolatedReviews, - supervisorSwarmInitialIsolatedReviews, - supervisorSwarmIsolatedReviewGroups, supervisorSwarmIsolatedReviews, supervisorSwarmSessionId, - supervisorSwarmStaticIsolatedAutonomousChatSuite, supervisorSwarmToolPlanHandoffRunnerKillAppDataSentinelFileName, supervisorSwarmToolPlanHandoffRunnerKillAppDataSentinelSchema, supervisorSwarmToolPlanHandoffRunnerKillSuite, @@ -92,19 +59,7 @@ import { toolPlanRepairAuditSafeFields, } from '../runtime-state.mjs'; import { - buildSupervisorAutonomousPlayablePartialEvidence, - buildSupervisorAutonomousPlayableStdin, - collectPartialSupervisorAutonomousPlayableEvidence, - inspectSupervisorAutonomousPlayableConversationBoundary, - isSupervisorAutonomousPlayableAcceptedPublicStatus, - isSupervisorAutonomousPlayableLaneDefenseSuite, - supervisorAutonomousPlayableStaticSmokeMatchesFinalIndex, - waitForSupervisorAutonomousPlayableCliExit, -} from './supervisor-autonomous-playable.mjs'; -import { - buildSupervisorSwarmEvidence, collectSupervisorSwarmDynamicPrivateValues, - driftedSupervisorSwarmCollaborationPolicy, duplicateSupervisorSwarmCollaborationPolicySnapshotBindingCount, duplicateSupervisorSwarmCollaborationPolicySnapshotCount, expectedSupervisorSwarmCollaborationPolicy, @@ -120,18 +75,11 @@ import { supervisorSwarmCollaborationPolicyDriftObservationCount, supervisorSwarmCollaborationPolicySnapshotBinding, supervisorSwarmEvidenceFieldTemplate, - supervisorSwarmExpectedIsolatedReviewGroups, supervisorSwarmFinalReplyFaultInjectionAllowed, supervisorSwarmFinalReplyFaultPrerequisites, - supervisorSwarmFollowupBeforeClaimOrderValid, supervisorSwarmHostVerificationPassed, supervisorSwarmInitialBatchBindsPolicySnapshot, - supervisorSwarmIsolatedReviewGroupIndex, - supervisorSwarmIsolatedWriteScopeRoots, - supervisorSwarmMixedIsolatedGroupEntries, - supervisorSwarmObservedJoinClaimForGroups, supervisorSwarmProfessionalSessions, - supervisorSwarmReadyIsolatedGroupIds, supervisorSwarmResidualSidecarsEmpty, supervisorSwarmSeedPrivateValues, supervisorSwarmToolPlanHandoffProxyNeedsCleanup, @@ -141,33 +89,12 @@ import { } from './supervisor-swarm.mjs'; export async function runAgentRuntimeRealE2eSelfTests() { - const supervisorAcceptedPublicStatusLifecycle = - validateSupervisorAcceptedPublicStatusSelfTest(); - const interactiveCliLifecycle = - await validateInteractiveCliInheritedStdioLifecycle(); - const providerBindingLifecycle = - validateSupervisorPlayableProviderBindingSelfTest(); const providerUsedReportingLifecycle = validateProviderUsedReportingSelfTest(); - const ownedProcessCleanupLifecycle = - validateOwnedProcessCleanupIdentitySelfTest(); - const liveProcessIdentities = await listSystemProcessIdentities(); - assert( - liveProcessIdentities.some( - (identity) => - identity.pid === process.pid && - Number.isSafeInteger(identity.parentPid) && - isNonEmptyString(identity.startedAt) && - isNonEmptyString(identity.name), - ), - 'agent-runtime-real-e2e-self-test-live-process-identity-missing', - ); const professionalSessionLifecycle = validateSupervisorSwarmProfessionalSessionsSelfTest(); const executionOwnerLockScanLifecycle = validateExecutionOwnerLockScanSelfTest(); - const staticSmokeBindingLifecycle = - validateStaticSmokeFinalIndexBindingSelfTest(); const seededGameHtmlLifecycle = validateSeededGameHtmlContractSelfTest(); const lateRegisteredScanner = new StreamingSecretScanner([ 'initial-scanner-value', @@ -302,345 +229,6 @@ export async function runAgentRuntimeRealE2eSelfTests() { const shellPackage = JSON.parse( readFileSync(path.join(appRoot, 'package.json'), 'utf8'), ); - const previousSuiteForAutonomousPlayable = state.suite; - state.suite = supervisorAutonomousPlayableLaneDefenseSuite; - const autonomousPlayableProfile = isolatedSuiteAppDataProfile(); - const autonomousPlayableParsedArguments = parseArguments([ - '--config-dir', - path.resolve('synthetic-autonomous-playable-config'), - '--suite', - supervisorAutonomousPlayableLaneDefenseSuite, - ]); - const autonomousPlayableStdin = buildSupervisorAutonomousPlayableStdin(); - const autonomousPlayablePackageCommandsRegistered = - shellPackage.scripts?.[ - 'agent-runtime:supervisor-autonomous-playable-lane-defense-real-e2e' - ] === - 'node scripts/agent-runtime-real-e2e.mjs --suite supervisor-autonomous-playable-lane-defense' && - rootPackage.scripts?.[ - 'ai-game-creator-shell:agent-runtime:supervisor-autonomous-playable-lane-defense-real-e2e' - ] === - 'npm --prefix apps/ai-game-creator-shell run agent-runtime:supervisor-autonomous-playable-lane-defense-real-e2e --'; - const autonomousPlayableSuiteRegistered = - autonomousPlayableParsedArguments.suite === - supervisorAutonomousPlayableLaneDefenseSuite && - isSupervisorAutonomousPlayableLaneDefenseSuite() && - !isSupervisorSwarmSuite() && - isIsolatedRunnerSuite() && - isolatedSuiteProtectsSourceAppData() && - isolatedSuiteUsesSiblingAppData() && - autonomousPlayableProfile.sentinelName === - supervisorAutonomousPlayableAppDataSentinelFileName && - autonomousPlayableProfile.sentinelSchema === - supervisorAutonomousPlayableAppDataSentinelSchema && - autonomousPlayableStdin.equals( - Buffer.from(`${supervisorAutonomousPlayableLaneDefenseTask}\n`, 'utf8'), - ) && - autonomousPlayablePackageCommandsRegistered; - const previousInitialRunIdForAutonomousPlayable = state.initialRunId; - const previousProjectRootForAutonomousPlayable = state.projectRoot; - const previousInitialTaskForAutonomousPlayable = state.initialTask; - const previousAutonomousPlayableTurnReport = - state.supervisorAutonomousPlayable.turnReport; - const previousAutonomousPlayablePrivateValues = [ - ...state.supervisorAutonomousPlayable.privateValues, - ]; - const previousSupervisorSwarmPrivateValues = [ - ...state.supervisorSwarm.privateValues, - ]; - const previousSupervisorSwarmInitialProviderBatch = - state.supervisorSwarm.initialProviderBatch; - const autonomousPlayablePartialCanary = 'private-autonomous-partial-canary'; - const syntheticAutonomousPlayableRunId = - 'synthetic-autonomous-playable-partial-root'; - state.initialRunId = syntheticAutonomousPlayableRunId; - state.supervisorAutonomousPlayable.privateValues = [ - ...new Set([ - ...state.supervisorAutonomousPlayable.privateValues, - autonomousPlayablePartialCanary, - ]), - ]; - const syntheticAutonomousPlayablePartialEvidence = - buildSupervisorAutonomousPlayablePartialEvidence( - { - taskSnapshot: { - latest: [ - { - agentId: projectSupervisorAgentId, - runId: syntheticAutonomousPlayableRunId, - status: 'running', - phase: 'waiting-for-agent', - task: autonomousPlayablePartialCanary, - }, - { - agentId: 'code-prototype', - sessionId: 'synthetic-original-session', - runId: 'synthetic-original-run', - parentAgentId: projectSupervisorAgentId, - parentRunId: syntheticAutonomousPlayableRunId, - delegationId: 'synthetic-original-delivery', - status: 'failed', - phase: 'failed', - }, - { - agentId: 'code-prototype', - sessionId: 'synthetic-repair-session', - runId: 'synthetic-repair-run', - parentAgentId: projectSupervisorAgentId, - parentRunId: syntheticAutonomousPlayableRunId, - delegationId: 'synthetic-repair-delivery', - status: 'running', - phase: 'planning', - }, - ], - }, - runtimeStates: [ - { - agentId: projectSupervisorAgentId, - runId: syntheticAutonomousPlayableRunId, - status: 'running', - phase: 'waiting-for-agent', - currentTask: autonomousPlayablePartialCanary, - }, - { - agentId: 'code-prototype', - runId: 'synthetic-original-run', - parentAgentId: projectSupervisorAgentId, - parentRunId: syntheticAutonomousPlayableRunId, - status: 'failed', - phase: 'failed', - }, - { - agentId: 'code-prototype', - runId: 'synthetic-repair-run', - parentAgentId: projectSupervisorAgentId, - parentRunId: syntheticAutonomousPlayableRunId, - status: 'running', - phase: 'waiting-for-provider-retry', - }, - ], - deliveries: [ - { - parentAgentId: projectSupervisorAgentId, - parentRunId: syntheticAutonomousPlayableRunId, - delegationId: 'synthetic-original-delivery', - repairOfDelegationId: null, - targetAgentId: 'code-prototype', - targetSessionId: 'synthetic-original-session', - targetRunId: 'synthetic-original-run', - status: 'ready', - terminalStatus: 'failed', - structuredResult: { contractStatus: 'needs-repair' }, - }, - { - parentAgentId: projectSupervisorAgentId, - parentRunId: syntheticAutonomousPlayableRunId, - delegationId: 'synthetic-repair-delivery', - repairOfDelegationId: 'synthetic-original-delivery', - targetAgentId: 'code-prototype', - targetSessionId: 'synthetic-repair-session', - targetRunId: 'synthetic-repair-run', - status: 'dispatched', - terminalStatus: null, - }, - ], - agentDb: [ - { - recordType: 'agent.runtime.provider_request.lifecycle', - agentId: 'code-prototype', - runId: 'synthetic-original-run', - requestId: 'synthetic-provider-request', - status: 'started', - }, - { - recordType: 'agent.runtime.provider_request.lifecycle', - agentId: 'code-prototype', - runId: 'synthetic-original-run', - requestId: 'synthetic-provider-request', - status: 'failed', - }, - { - recordType: 'agent.runtime.provider_request.retry', - agentId: 'code-prototype', - runId: 'synthetic-original-run', - errorKind: 'transport', - }, - ], - supervisorConversation: [ - { - role: 'user', - content: autonomousPlayablePartialCanary, - }, - ], - professionalConversations: [], - failureEvidenceErrors: {}, - }, - { - residualSidecars: { providerRetries: 1 }, - pendingActionCount: 0, - turnReport: { - outcome: 'incomplete', - parentAgentId: projectSupervisorAgentId, - sessionId: supervisorSwarmSessionId, - parentRunId: syntheticAutonomousPlayableRunId, - runtimeCount: 3, - busyRuntimeCount: 2, - pendingTaskCount: 0, - runningTaskCount: 2, - waitingForConfirmationCount: 0, - waitingForUserInputCount: 0, - reconciliationAgentCount: 0, - }, - }, - ); - const autonomousPlayablePartialEvidenceValidated = - syntheticAutonomousPlayablePartialEvidence.evidenceCompleteness === - 'partial' && - syntheticAutonomousPlayablePartialEvidence.partialEvidenceCollected === - true && - syntheticAutonomousPlayablePartialEvidence.partialPrivacyScanComplete === - false && - syntheticAutonomousPlayablePartialEvidence.rootRunObserved === true && - syntheticAutonomousPlayablePartialEvidence.parentRuntimePhase === - 'waiting-for-agent' && - syntheticAutonomousPlayablePartialEvidence.childTaskStatusCounts.failed === - 1 && - syntheticAutonomousPlayablePartialEvidence.childTaskStatusCounts.running === - 1 && - syntheticAutonomousPlayablePartialEvidence.failedOriginalChildCount === 1 && - syntheticAutonomousPlayablePartialEvidence.failedRepairChildCount === 0 && - syntheticAutonomousPlayablePartialEvidence.awaitingRepairCount === 1 && - syntheticAutonomousPlayablePartialEvidence.repairDeliveryStatusCounts - .dispatched === 1 && - syntheticAutonomousPlayablePartialEvidence.providerLifecycleFailedCount === - 1 && - syntheticAutonomousPlayablePartialEvidence.providerRetryAuditCount === 1 && - syntheticAutonomousPlayablePartialEvidence.providerTransportRetryAuditCount === - 1 && - syntheticAutonomousPlayablePartialEvidence.providerRetrySidecarCount === - 1 && - syntheticAutonomousPlayablePartialEvidence.turnReportOutcome === - 'incomplete' && - syntheticAutonomousPlayablePartialEvidence.turnReportCaptured === true && - syntheticAutonomousPlayablePartialEvidence.turnReportParentIdentityStable === - true && - syntheticAutonomousPlayablePartialEvidence.turnReportPrivateLeakCount === - 0 && - !JSON.stringify(syntheticAutonomousPlayablePartialEvidence).includes( - autonomousPlayablePartialCanary, - ); - const syntheticAutonomousPlayableCollectorRoot = await fs.mkdtemp( - path.join(os.tmpdir(), 'genarrative-autonomous-partial-self-test-'), - ); - let syntheticAutonomousPlayableCollectorEvidence; - try { - state.projectRoot = syntheticAutonomousPlayableCollectorRoot; - state.initialTask = { - bytes: Buffer.byteLength(autonomousPlayablePartialCanary, 'utf8'), - sha256: hashValue(autonomousPlayablePartialCanary), - }; - state.supervisorSwarm.privateValues = [autonomousPlayablePartialCanary]; - state.supervisorSwarm.initialProviderBatch = null; - state.supervisorAutonomousPlayable.turnReport = { - outcome: 'needs-reconciliation', - parentAgentId: projectSupervisorAgentId, - sessionId: supervisorSwarmSessionId, - parentRunId: syntheticAutonomousPlayableRunId, - runtimeCount: 1, - busyRuntimeCount: 0, - pendingTaskCount: 0, - runningTaskCount: 0, - waitingForConfirmationCount: 0, - waitingForUserInputCount: 0, - reconciliationAgentCount: 1, - privateDiagnostic: autonomousPlayablePartialCanary, - }; - const taskPath = path.join( - state.projectRoot, - '.agent/runtime/tasks/project-supervisor.jsonl', - ); - const runtimePath = path.join( - state.projectRoot, - '.agent/runtime/agents/project-supervisor.json', - ); - const conversationPath = agentConversationPath( - projectSupervisorAgentId, - supervisorSwarmSessionId, - ); - await fs.mkdir(path.dirname(taskPath), { recursive: true }); - await fs.mkdir(path.dirname(runtimePath), { recursive: true }); - await fs.mkdir(path.dirname(conversationPath), { recursive: true }); - await fs.writeFile( - taskPath, - `${JSON.stringify({ - agentId: projectSupervisorAgentId, - runId: syntheticAutonomousPlayableRunId, - status: 'budget-exhausted', - phase: 'budget-exhausted', - task: autonomousPlayablePartialCanary, - })}\n`, - ); - await fs.writeFile( - runtimePath, - JSON.stringify({ - agentId: projectSupervisorAgentId, - runId: syntheticAutonomousPlayableRunId, - status: 'needs-reconciliation', - phase: 'needs-reconciliation', - currentTask: autonomousPlayablePartialCanary, - }), - ); - await fs.writeFile( - conversationPath, - `${JSON.stringify({ - role: 'user', - content: autonomousPlayablePartialCanary, - })}\n`, - ); - syntheticAutonomousPlayableCollectorEvidence = - await collectPartialSupervisorAutonomousPlayableEvidence(); - } finally { - state.projectRoot = previousProjectRootForAutonomousPlayable; - state.initialTask = previousInitialTaskForAutonomousPlayable; - state.supervisorAutonomousPlayable.turnReport = - previousAutonomousPlayableTurnReport; - state.supervisorAutonomousPlayable.privateValues = - previousAutonomousPlayablePrivateValues; - state.supervisorSwarm.privateValues = previousSupervisorSwarmPrivateValues; - state.supervisorSwarm.initialProviderBatch = - previousSupervisorSwarmInitialProviderBatch; - await fs.rm(syntheticAutonomousPlayableCollectorRoot, { - recursive: true, - force: true, - }); - } - const autonomousPlayablePartialCollectorPrivacyValidated = - syntheticAutonomousPlayableCollectorEvidence.evidenceCompleteness === - 'partial' && - syntheticAutonomousPlayableCollectorEvidence.rootRunObserved === true && - syntheticAutonomousPlayableCollectorEvidence.parentTaskStatus === - 'budget-exhausted' && - syntheticAutonomousPlayableCollectorEvidence.parentTaskPhase === - 'budget-exhausted' && - syntheticAutonomousPlayableCollectorEvidence.parentRuntimeStatus === - 'needs-reconciliation' && - syntheticAutonomousPlayableCollectorEvidence.parentRuntimePhase === - 'needs-reconciliation' && - syntheticAutonomousPlayableCollectorEvidence.supervisorUserMessageCount === - 1 && - syntheticAutonomousPlayableCollectorEvidence.turnReportPrivateLeakCount === - 1 && - !JSON.stringify(syntheticAutonomousPlayableCollectorEvidence).includes( - autonomousPlayablePartialCanary, - ); - state.initialRunId = previousInitialRunIdForAutonomousPlayable; - state.suite = previousSuiteForAutonomousPlayable; - assert( - autonomousPlayableSuiteRegistered && - autonomousPlayablePartialEvidenceValidated && - autonomousPlayablePartialCollectorPrivacyValidated, - 'agent-runtime-real-e2e-self-test-autonomous-playable-suite-invalid', - ); const rootToolPlanHandoffCommand = 'npm --prefix apps/ai-game-creator-shell run agent-runtime:supervisor-swarm-tool-plan-handoff-runner-kill-real-e2e --'; const shellToolPlanHandoffCommand = @@ -1210,94 +798,15 @@ export async function runAgentRuntimeRealE2eSelfTests() { ), 'agent-runtime-real-e2e-self-test-generic-boundary-term-private', ); - const syntheticMixedGroups = [ - { - delegationGroupId: 'synthetic-followup-group', - request: { - children: supervisorSwarmFollowupIsolatedReviews.map((review) => ({ - expectedArtifacts: [review.path], - })), - }, - }, - { - delegationGroupId: 'synthetic-initial-group', - request: { - children: supervisorSwarmInitialIsolatedReviews.map((review) => ({ - expectedArtifacts: [review.path], - })), - }, - }, - ]; - const syntheticMixedEntries = supervisorSwarmMixedIsolatedGroupEntries( - syntheticMixedGroups, - supervisorSwarmIsolatedReviewGroups, - ); - const syntheticSingleGroupEntries = supervisorSwarmMixedIsolatedGroupEntries( - [ - { - delegationGroupId: 'synthetic-single-group', - request: { - children: supervisorSwarmIsolatedReviews.map((review) => ({ - expectedArtifacts: [review.path], - })), - }, - }, - ], - [supervisorSwarmIsolatedReviews], - ); - const syntheticReadyGroupIds = supervisorSwarmReadyIsolatedGroupIds( - `readyIsolatedJoins: ${JSON.stringify({ - ready: true, - joins: syntheticMixedEntries.map(({ group }) => ({ - delegationGroupId: group.delegationGroupId, - })), - })}\n\nagentId: ${projectSupervisorAgentId}`, - ); - const syntheticObservedJoinClaim = supervisorSwarmObservedJoinClaimForGroups( - [ - { - schemaVersion: isolatedAgentJoinClaimSchemaVersion, - parentAgentId: projectSupervisorAgentId, - parentRunId: 'synthetic-parent-run', - actionId: 'synthetic-claim-action', - status: 'observed', - joins: syntheticMixedEntries.map(({ group }) => ({ - delegationGroupId: group.delegationGroupId, - })), - }, - ], - syntheticReadyGroupIds, - ); - const syntheticWriteScopeRoots = supervisorSwarmIsolatedWriteScopeRoots( - supervisorSwarmIsolatedReviewGroups.flatMap((reviews) => - reviews.map((review) => ({ writeScopes: [review.scope] })), - ), - ); - let syntheticOverlappingWriteScopesRejected = false; - try { - supervisorSwarmIsolatedWriteScopeRoots([ - { writeScopes: ['e2e/**'] }, - { writeScopes: ['e2e/isolated-a/**'] }, - ]); - } catch (error) { - syntheticOverlappingWriteScopesRejected = - error?.code === 'supervisor-swarm-mixed-child-write-scopes-overlap'; - } - const syntheticFollowupOrderValidated = - supervisorSwarmFollowupBeforeClaimOrderValid(3, 5, [8, 9]) && - !supervisorSwarmFollowupBeforeClaimOrderValid(3, 8, [8, 9]); - const originalSuite = state.suite; const originalInitialRunId = state.initialRunId; - state.suite = supervisorSwarmStaticIsolatedAutonomousChatSuite; - const multiGroupPolicy = expectedSupervisorSwarmCollaborationPolicy(); - const multiGroupCount = supervisorSwarmExpectedIsolatedReviewGroups().length; + const collaborationPolicy = expectedSupervisorSwarmCollaborationPolicy(); const syntheticSnapshotExpected = { projectId: 'synthetic-project-id', parentAgentId: projectSupervisorAgentId, parentRunId: 'synthetic-parent-run', boundFrom: supervisorCollaborationPolicySnapshotInitialBatchBinding, - policy: multiGroupPolicy, - policyFingerprint: hashValue(JSON.stringify(multiGroupPolicy)), + policy: collaborationPolicy, + policyFingerprint: hashValue(JSON.stringify(collaborationPolicy)), }; const syntheticSnapshotBindingBatch = { schemaVersion: providerActionBatchSchemaVersion, @@ -1311,7 +820,7 @@ export async function runAgentRuntimeRealE2eSelfTests() { collaborationContract: { schemaVersion: supervisorCollaborationContractSchemaVersion, policySchemaVersion: supervisorCollaborationPolicySchemaVersion, - policySnapshot: multiGroupPolicy, + policySnapshot: collaborationPolicy, policyFingerprint: syntheticSnapshotExpected.policyFingerprint, contractFingerprint: hashValue('synthetic-collaboration-contract'), }, @@ -1359,7 +868,6 @@ export async function runAgentRuntimeRealE2eSelfTests() { const syntheticSnapshotBindingBytes = JSON.stringify( syntheticSnapshotBinding, ); - const syntheticDriftedPolicy = driftedSupervisorSwarmCollaborationPolicy(); state.initialRunId = syntheticSnapshotExpected.parentRunId; const syntheticDriftObservationCount = supervisorSwarmCollaborationPolicyDriftObservationCount([ @@ -1412,38 +920,17 @@ export async function runAgentRuntimeRealE2eSelfTests() { duplicateSupervisorSwarmCollaborationPolicySnapshotBindingCount([ syntheticSnapshotBinding, ]); - const syntheticEnospcDiagnostic = safeProcessFailureDiagnostic({ - code: 1, - stderr: 'No space left on device (os error 28)', - }); - const syntheticChatSessionFailureEvidence = buildSupervisorSwarmEvidence({ - chatSessionUnexpectedlyClosed: true, - chatSessionFailureKind: syntheticEnospcDiagnostic.failureKind, - chatSessionExitCode: syntheticEnospcDiagnostic.exitCode, - chatSessionCloseSignal: syntheticEnospcDiagnostic.signal, - chatSessionProcessErrorCode: syntheticEnospcDiagnostic.processErrorCode, - chatSessionStderrChars: syntheticEnospcDiagnostic.stderrChars, - chatSessionStderrSha256: syntheticEnospcDiagnostic.stderrSha256, - }); - state.suite = supervisorSwarmCollaborationPolicyMixedRecoverySuite; - const recoveryPolicy = expectedSupervisorSwarmCollaborationPolicy(); - const recoveryGroupCount = - supervisorSwarmExpectedIsolatedReviewGroups().length; - state.suite = supervisorSwarmAutonomousChatSuite; const syntheticSnapshotBindingBatchSuiteIndependent = supervisorSwarmInitialBatchBindsPolicySnapshot( syntheticSnapshotBindingBatch, syntheticSnapshotExpected.parentRunId, ); - state.suite = originalSuite; assert( syntheticSnapshotInspection.identityHashMatched && syntheticSnapshotBindingInspection.snapshotMatched && syntheticSnapshotBytes === JSON.stringify(syntheticSnapshot) && syntheticSnapshotBindingBytes === JSON.stringify(syntheticSnapshotBinding) && - multiGroupPolicy.minIsolatedGroupsBeforeClaim === 2 && - syntheticDriftedPolicy.minIsolatedGroupsBeforeClaim === 1 && syntheticDriftObservationCount === 1 && syntheticSnapshotTamperRejected && syntheticSnapshotBindingTamperRejected && @@ -1454,47 +941,9 @@ export async function runAgentRuntimeRealE2eSelfTests() { syntheticSnapshotBindingBatchAccepted && syntheticSnapshotBindingBatchRejections && syntheticSnapshotBindingBatchSuiteIndependent && - syntheticEnospcDiagnostic.failureKind === 'enospc' && - syntheticEnospcDiagnostic.stderrChars > 0 && - /^[0-9a-f]{64}$/u.test(syntheticEnospcDiagnostic.stderrSha256) && - syntheticChatSessionFailureEvidence.chatSessionUnexpectedlyClosed === - true && - syntheticChatSessionFailureEvidence.chatSessionFailureKind === 'enospc', - 'agent-runtime-real-e2e-self-test-collaboration-policy-snapshot-binding-invalid', - ); - assert( - JSON.stringify( - syntheticMixedEntries.map(({ groupIndex }) => groupIndex), - ) === JSON.stringify([0, 1]) && - JSON.stringify( - syntheticSingleGroupEntries.map(({ groupIndex }) => groupIndex), - ) === JSON.stringify([0]) && - supervisorSwarmIsolatedReviewGroupIndex( - supervisorSwarmIsolatedReviews.map((review) => ({ - expectedArtifacts: [review.path], - })), - ) === -1 && - JSON.stringify(syntheticReadyGroupIds) === - JSON.stringify( - syntheticMixedGroups.map((group) => group.delegationGroupId).sort(), - ) && - syntheticObservedJoinClaim.actionId === 'synthetic-claim-action' && - syntheticWriteScopeRoots.length === - supervisorSwarmIsolatedReviews.length && - new Set(syntheticWriteScopeRoots).size === - supervisorSwarmIsolatedReviews.length && - syntheticOverlappingWriteScopesRejected && - syntheticFollowupOrderValidated && - multiGroupPolicy.minIsolatedChildren === 2 && - Object.hasOwn(multiGroupPolicy, 'minIsolatedGroupsBeforeClaim') && - multiGroupPolicy.minIsolatedGroupsBeforeClaim === 2 && - multiGroupCount === 2 && - recoveryPolicy.minIsolatedChildren === 3 && - !Object.hasOwn(recoveryPolicy, 'minIsolatedGroupsBeforeClaim') && - (recoveryPolicy.minIsolatedGroupsBeforeClaim ?? 0) === 0 && - recoveryGroupCount === 1, - 'agent-runtime-real-e2e-self-test-mixed-isolated-groups-invalid', + 'agent-runtime-real-e2e-self-test-collaboration-policy-snapshot-binding-invalid', ); + const syntheticIdentity = { agentId: 'private-agent-id', taskId: 'private-task-id', @@ -1732,29 +1181,21 @@ export async function runAgentRuntimeRealE2eSelfTests() { dynamicPrivateBodyCount: expectedPrivateValues.length, evidenceMetadataExcluded: true, genericBoundaryTermsExcluded: true, - mixedIsolatedGroupTopologyValidated: true, - mixedReadyJoinObservationValidated: true, - mixedObservedJoinClaimValidated: true, - mixedCrossGroupWriteScopesValidated: true, - mixedFollowupBeforeClaimOrderValidated: true, - legacySingleIsolatedGroupTopologyValidated: true, - mixedSuitePolicyIsolationValidated: true, + isolatedReviewGroupTopologyValidated: true, + isolatedReadyJoinObservationValidated: true, + isolatedObservedJoinClaimValidated: true, + isolatedWriteScopesValidated: true, + isolatedFollowupBeforeClaimOrderValidated: true, collaborationPolicySnapshotCaptured: true, collaborationPolicySnapshotStable: true, collaborationPolicySnapshotBindingCaptured: true, collaborationPolicySnapshotBindingStable: true, durableSnapshotEligibilityAndContractBindingValidated: true, sourceEndpointAbsentLifecycleGuardValidated, - ...interactiveCliLifecycle, - ...providerBindingLifecycle, ...providerUsedReportingLifecycle, - ...ownedProcessCleanupLifecycle, - liveProcessIdentityEnumerationValidated: true, ...professionalSessionLifecycle, ...executionOwnerLockScanLifecycle, - ...staticSmokeBindingLifecycle, ...seededGameHtmlLifecycle, - ...supervisorAcceptedPublicStatusLifecycle, isolatedAppDataLatePathScannerValidated: true, ...childCloseTimeoutLifecycle, stableKillHandlePlatformValidated, @@ -1762,11 +1203,6 @@ export async function runAgentRuntimeRealE2eSelfTests() { stableKillHandleSignalValidated, stableKillHandleCommandFailureCleanupValidated, windowsOwnedTempPathValidated, - autonomousPlayableSuiteRegistered, - autonomousPlayablePackageCommandsRegistered, - autonomousPlayableDedicatedPathValidated: true, - autonomousPlayableExactStdinValidated: true, - autonomousPlayablePartialCollectorPrivacyValidated, toolPlanHandoffSuiteRegistered, toolPlanHandoffPackageCommandsRegistered, toolPlanHandoffSourceGuardRegistered, @@ -1776,7 +1212,6 @@ export async function runAgentRuntimeRealE2eSelfTests() { toolPlanHandoffEvidenceFieldsRegistered, toolPlanHandoffUnknownResultBoundaryPreserved: true, finalReplyFaultPrerequisiteValidated: true, - collaborationPolicyDriftFixtureValidated: true, collaborationPolicyDriftStatusObserved: true, duplicateCollaborationPolicySnapshotCount: syntheticSnapshotDuplicateCount, duplicateCollaborationPolicySnapshotBindingCount: @@ -1787,7 +1222,6 @@ export async function runAgentRuntimeRealE2eSelfTests() { collaborationPolicyPartialMissingEvidenceSafe: syntheticMissingSnapshotSafe && syntheticMissingBindingSafe, enospcFailureDiagnosticClassified: true, - chatSessionFailureEvidenceSchemaValidated: true, toolPlanRepairCount: fullRepairEvidence.toolPlanRepairCount, toolPlanRepairedLoopCount: fullRepairEvidence.toolPlanRepairedLoopCount, toolPlanSecondRepairCount: fullRepairEvidence.toolPlanSecondRepairCount, @@ -1927,139 +1361,6 @@ function validateExecutionOwnerLockScanSelfTest() { }; } -function validateSupervisorAcceptedPublicStatusSelfTest() { - const runId = 'synthetic-supervisor-accepted-status-run'; - const correlationId = finalMessageId( - projectSupervisorAgentId, - supervisorSwarmSessionId, - runId, - ).slice('agent-finalization-'.length); - const expectedMessageId = `runtime-public-status-${correlationId}-070c160a6299c543`; - const canonicalAcceptedStatus = { - role: 'assistant', - agentId: null, - sessionId: null, - messageId: expectedMessageId, - content: '任务已接收,项目总控 Agent 正在启动处理。', - }; - const rejectedVariants = [ - { ...canonicalAcceptedStatus, role: 'user' }, - { ...canonicalAcceptedStatus, agentId: projectSupervisorAgentId }, - { ...canonicalAcceptedStatus, sessionId: supervisorSwarmSessionId }, - { ...canonicalAcceptedStatus, messageId: `${expectedMessageId}-legacy` }, - { ...canonicalAcceptedStatus, content: '任务已接收。' }, - ]; - const canonicalBoundary = - inspectSupervisorAutonomousPlayableConversationBoundary( - [canonicalAcceptedStatus], - [canonicalAcceptedStatus], - runId, - ); - const rejectedBoundaries = [ - ...rejectedVariants.map((message) => [canonicalAcceptedStatus, message]), - [canonicalAcceptedStatus, { role: 'user', content: 'legacy-user' }], - ].map((legacyConversation) => - inspectSupervisorAutonomousPlayableConversationBoundary( - legacyConversation, - legacyConversation, - runId, - ), - ); - assert( - runtimePublicStatusMessageId( - projectSupervisorAgentId, - supervisorSwarmSessionId, - runId, - 'accepted', - ) === expectedMessageId && - isSupervisorAutonomousPlayableAcceptedPublicStatus( - canonicalAcceptedStatus, - runId, - ) && - rejectedVariants.every( - (message) => - !isSupervisorAutonomousPlayableAcceptedPublicStatus(message, runId), - ) && - canonicalBoundary.legacyUserFacingMessageCount === 1 && - canonicalBoundary.acceptedPublicStatusCount === 1 && - canonicalBoundary.wrongAgentFinalAssistantCount === 0 && - rejectedBoundaries.every( - (boundary) => - boundary.legacyUserFacingMessageCount !== 1 || - boundary.acceptedPublicStatusCount !== 1 || - boundary.wrongAgentFinalAssistantCount !== 0, - ), - 'agent-runtime-real-e2e-self-test-supervisor-accepted-public-status-invalid', - ); - return { - supervisorAcceptedPublicStatusIdValidated: true, - supervisorAcceptedPublicStatusShapeValidated: true, - supervisorAcceptedPublicStatusLegacyVariantsRejected: true, - }; -} - -function validateSupervisorPlayableProviderBindingSelfTest() { - const previousSuite = state.suite; - state.suite = supervisorAutonomousPlayableLaneDefenseSuite; - try { - const config = { - agentMode: 'provider', - llm: { - apiKey: 'synthetic-provider-key-not-for-network', - baseUrl: 'https://synthetic-provider.invalid/gpt/v1', - model: 'gpt-5.6-sol', - apiKind: 'openai_responses', - reasoningEffort: 'max', - }, - }; - const binding = expectedProviderBindingForSuite(config); - assert( - binding?.providerModel === 'gpt-5.6-sol' && - binding.providerApiKind === 'openai_responses' && - binding.providerReasoningEffort === 'max' && - /^[0-9a-f]{64}$/u.test(binding.providerBaseUrlSha256) && - binding.boundAgentIds.length === 1 && - !JSON.stringify(binding).includes(config.llm.apiKey) && - !JSON.stringify(binding).includes(config.llm.baseUrl), - 'agent-runtime-real-e2e-self-test-provider-binding-invalid', - ); - const reorderedBinding = { - boundAgentIds: [...binding.boundAgentIds], - providerBaseUrlSha256: binding.providerBaseUrlSha256, - providerReasoningEffort: binding.providerReasoningEffort, - providerApiKind: binding.providerApiKind, - providerModel: binding.providerModel, - }; - assert( - sameSupervisorPlayableProviderBinding(binding, reorderedBinding) && - !sameSupervisorPlayableProviderBinding(binding, { - ...reorderedBinding, - providerReasoningEffort: 'high', - }), - 'agent-runtime-real-e2e-self-test-provider-binding-comparison-invalid', - ); - const drifted = expectedProviderBindingForSuite({ - ...config, - agentLlm: { 'art-director': { reasoningEffort: 'high' } }, - }); - assert( - drifted === null, - 'agent-runtime-real-e2e-self-test-provider-binding-drift-accepted', - ); - return { - providerAgentModeBindingValidated: true, - providerModelBindingValidated: true, - providerApiKindBindingValidated: true, - providerReasoningEffortBindingValidated: true, - providerBaseUrlHashBindingValidated: true, - providerBindingPropertyOrderInsensitiveValidated: true, - providerBindingDriftRejected: true, - }; - } finally { - state.suite = previousSuite; - } -} - function validateProviderUsedReportingSelfTest() { const completeProviderEvidence = { evidenceCompleteness: 'complete', @@ -2121,110 +1422,6 @@ function validateProviderUsedReportingSelfTest() { providerUsedBindingMismatchRejected: true, }; } - -function validateOwnedProcessCleanupIdentitySelfTest() { - const processRecords = [ - { - pid: 41001, - parentPid: 1, - startedAt: 'runner-start', - name: 'genarrative-ai-game-creator-shell.exe', - }, - { - pid: 41002, - parentPid: 41001, - startedAt: 'helper-start', - name: 'powershell.exe', - }, - { - pid: 41003, - parentPid: 41001, - startedAt: 'node-start', - name: 'node.exe', - }, - { - pid: 41004, - parentPid: 41003, - startedAt: 'browser-start', - name: 'chrome.exe', - }, - { - pid: 41005, - parentPid: 41001, - startedAt: 'command-start', - name: 'cmd.exe', - }, - { - pid: 42000, - parentPid: 1, - startedAt: 'unrelated-start', - name: 'node.exe', - }, - ]; - const snapshot = buildOwnedProcessCleanupSnapshot(processRecords, { - runnerPid: 41001, - helperPids: [41002], - rootPids: [41001, 41002], - }); - const live = inspectOwnedProcessCleanupResiduals(snapshot, processRecords, { - activeCommandChildCount: 1, - activeInteractiveCliSessionCount: 1, - }); - const reusedPids = processRecords.map((record) => - record.pid >= 41001 && record.pid <= 41005 - ? { ...record, startedAt: `${record.startedAt}-reused` } - : record, - ); - const cleaned = inspectOwnedProcessCleanupResiduals(snapshot, reusedPids, { - activeCommandChildCount: 0, - activeInteractiveCliSessionCount: 0, - }); - assert( - snapshot.observedCounts.runner === 1 && - snapshot.observedCounts.helper === 1 && - snapshot.observedCounts.node === 1 && - snapshot.observedCounts.browser === 1 && - snapshot.observedCounts.command === 1 && - live.clean === false && - live.residualCounts.total === 5 && - cleaned.clean === true && - cleaned.residualCounts.total === 0, - 'agent-runtime-real-e2e-self-test-owned-process-cleanup-invalid', - ); - return { - ownedRunnerResidualIdentityValidated: true, - ownedHelperResidualIdentityValidated: true, - ownedNodeResidualIdentityValidated: true, - ownedBrowserResidualIdentityValidated: true, - ownedCommandResidualIdentityValidated: true, - ownedProcessPidReuseRejected: true, - activeChildRegistryCleanupValidated: true, - }; -} - -function validateStaticSmokeFinalIndexBindingSelfTest() { - const sha256 = 'a'.repeat(64); - const gate = { - lastVerificationStatus: 'passed', - verifiedRevision: 7, - staticSmokeVerifiedRevision: 7, - staticSmokeVerifiedGameIndexSha256: sha256, - }; - assert( - supervisorAutonomousPlayableStaticSmokeMatchesFinalIndex(gate, 7, sha256) && - !supervisorAutonomousPlayableStaticSmokeMatchesFinalIndex( - gate, - 7, - 'b'.repeat(64), - ), - 'agent-runtime-real-e2e-self-test-static-smoke-final-index-binding-invalid', - ); - return { - staticSmokeFinalIndexSha256BindingValidated: true, - staticSmokeStaleIndexSha256Rejected: true, - }; -} - function validateSeededGameHtmlContractSelfTest() { const html = seededGameHtml(); const lowerHtml = html.toLowerCase(); @@ -2365,133 +1562,3 @@ async function validateStableKillHandleCommandFailureCleanup() { await stopSelfTestChild(victim); } } - -async function validateInteractiveCliInheritedStdioLifecycle() { - const expectedReport = { - schemaVersion: 'game-creator-swarm-turn-report.v1', - outcome: 'settled', - parentAgentId: 'project-supervisor', - sessionId: 'interactive-cli-fd3-self-test-session', - parentRunId: 'interactive-cli-fd3-self-test-run', - runtimeCount: 1, - busyRuntimeCount: 0, - pendingTaskCount: 0, - runningTaskCount: 0, - waitingForConfirmationCount: 0, - waitingForUserInputCount: 0, - newAssistantMessageCount: 1, - finalReplyChars: 1, - reconciliationAgentCount: 0, - }; - const stderrMarker = 'interactive-cli-fd3-holder-stderr-marker'; - const previousProjectPathScanner = state.projectPathTranscriptScanner; - const projectPathScanner = new StreamingSecretScanner([stderrMarker]); - state.projectPathTranscriptScanner = projectPathScanner; - const reportLine = `[turn.report] ${JSON.stringify(expectedReport)}\n`; - const splitIndex = Math.max(1, Math.floor(reportLine.length / 2)); - const holderSource = ` -process.stdin.setEncoding('utf8'); -let input = ''; -const safetyTimer = setTimeout(() => process.exit(74), 15_000); -process.stdin.on('data', (chunk) => { - input += chunk; - if (!input.includes('CLOSE\\n')) return; - clearTimeout(safetyTimer); - process.exit(0); -}); -process.stdin.on('end', () => process.exit(75)); -process.stdin.resume(); -`; - const wrapperSource = ` -const { spawn } = require('node:child_process'); -const holder = spawn( - process.execPath, - ['--input-type=commonjs', '-e', ${JSON.stringify(holderSource)}], - { stdio: [3, 'inherit', 'inherit'], windowsHide: true }, -); -holder.once('error', () => { - process.exitCode = 73; -}); -holder.unref(); -const reportLine = ${JSON.stringify(reportLine)}; -process.stdout.write(reportLine.slice(0, ${splitIndex}), () => { - setImmediate(() => { - process.stdout.write(reportLine.slice(${splitIndex}), () => { - process.stderr.write(${JSON.stringify(`${stderrMarker}\n`)}, () => { - process.exit(process.exitCode ?? 0); - }); - }); - }); -}); -`; - const wrapper = spawn( - process.execPath, - ['--input-type=commonjs', '-e', wrapperSource], - { - stdio: ['ignore', 'pipe', 'pipe', 'pipe'], - windowsHide: true, - }, - ); - const session = createInteractiveCliSession(wrapper); - const originalKill = wrapper.kill.bind(wrapper); - let killCallCount = 0; - wrapper.kill = (...args) => { - killCallCount += 1; - return originalKill(...args); - }; - let controlStreamError = null; - wrapper.stdio[3].on('error', (error) => { - controlStreamError ??= error; - }); - try { - await waitForInteractiveCliExit(session, 10_000); - assert( - session.exited && !session.stdioClosed, - 'agent-runtime-real-e2e-self-test-interactive-cli-two-phase-not-observed', - ); - const parsedReport = - await waitForSupervisorAutonomousPlayableCliExit(session); - await waitForInteractiveCliOutput( - session, - () => session.stderr.toString('utf8').includes(`${stderrMarker}\n`), - 'agent-runtime-real-e2e-self-test-interactive-cli-output-timeout', - 10_000, - { allowAfterProcessExit: true }, - ); - assert( - JSON.stringify(canonicalJsonValue(parsedReport)) === - JSON.stringify(canonicalJsonValue(expectedReport)), - 'agent-runtime-real-e2e-self-test-interactive-cli-report-mismatch', - ); - await closeInteractiveCli(session); - assert( - killCallCount === 0 && !session.stdioClosed, - 'agent-runtime-real-e2e-self-test-interactive-cli-exit-cleanup-killed', - ); - wrapper.stdio[3].end('CLOSE\n'); - await waitForInteractiveCliStdioClose(session, 10_000); - assert( - controlStreamError === null && - session.stdioClosed && - projectPathScanner.count === 1, - 'agent-runtime-real-e2e-self-test-interactive-cli-stdio-close-invalid', - ); - return { - interactiveCliExitBeforeStdioCloseValidated: true, - interactiveCliPostExitReportValidated: true, - interactiveCliExitCleanupDidNotKillValidated: true, - interactiveCliStdioCloseAfterHolderReleaseValidated: true, - interactiveCliProjectPathScannerValidated: true, - }; - } finally { - if (!wrapper.stdio[3].destroyed && !wrapper.stdio[3].writableEnded) { - wrapper.stdio[3].end('CLOSE\n'); - } - if (!session.stdioClosed) { - await waitForInteractiveCliStdioClose(session, 10_000).catch(() => { - destroyInteractiveCliOutputStreams(session); - }); - } - state.projectPathTranscriptScanner = previousProjectPathScanner; - } -} diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-autonomous-playable.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-autonomous-playable.mjs deleted file mode 100644 index 6d988a64a..000000000 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-autonomous-playable.mjs +++ /dev/null @@ -1,1890 +0,0 @@ -import { - assert, - codedError, - hashJsonValue, - hashValue, - isFailedTask, - parseSingleSwarmTurnReport, - recordError, - sleep, -} from '../assertions/core.mjs'; -import { - absolutePathVariants, - actionAuditIdentity, - countExactSecrets, - duplicateCount, - finalMessageId, - isNonEmptyString, - runtimePublicStatusMessageId, -} from '../assertions/runtime.mjs'; -import { fs, path } from '../dependencies.mjs'; -import { - claimOwnedRunner, - ensureOwnedRunnerStableKillSupport, - prepareIsolatedSuiteAppData, -} from '../harness/app-data.mjs'; -import { isPlainObject } from '../harness/config.mjs'; -import { isLiveTask, isPathInside, listFiles } from '../harness/io.mjs'; -import { - codedProcessError, - interactiveCliOutput, - prepareCliBinary, - startInteractiveCli, - waitForInteractiveCliOutput, -} from '../harness/process.mjs'; -import { - productionDefaultGameIndexHtml, - seedDisposableProject, -} from '../harness/project.mjs'; -import { decodeUtf8Fatal } from '../harness/reporting.mjs'; -import { - countLureLeaks, - findPendingActions, - readRuntime, -} from '../harness/runtime.mjs'; -import { - autonomousCompletionContractSchemaVersion, - autonomousGameBuildRunProfile, - autonomousPlaytestReceiptSchemaVersion, - laneDefensePlaytestRequiredAssertions, - projectSupervisorAgentId, - providerRequestLifecycleSchemaVersion, - runProfileBindingSchemaVersion, - state, - supervisorAutonomousPlayableLaneDefenseSuite, - supervisorAutonomousPlayableLaneDefenseTask, - supervisorAutonomousPlayableRunTimeoutMs, - supervisorAutonomousPlayableSafeDeliveryStatuses, - supervisorAutonomousPlayableSafePhases, - supervisorAutonomousPlayableSafeStatuses, - supervisorAutonomousPlayableSafeTerminalStatuses, - supervisorAutonomousPlayableSafeTurnOutcomes, - supervisorAutonomousPlayableSourceFieldMaxChars, - supervisorAutonomousPlayableSourceTotalMaxChars, - supervisorSwarmSessionId, - supervisorSwarmTerminalSidecarCleanupTimeoutMs, -} from '../runtime-state.mjs'; -import { - collectSupervisorSwarmPublicLeakEvidence, - countSupervisorSwarmSteerRecords, - readSupervisorSwarmJsonDirectory, - readSupervisorSwarmPersistence, - readSupervisorSwarmProjectRevision, - supervisorSwarmParentDeliveries, - validateSupervisorSwarmFinalization, -} from './supervisor-swarm.mjs'; - -const projectSupervisorCliSource = 'project-supervisor-cli'; -const projectSupervisorAcceptedPublicStatus = 'accepted'; -const projectSupervisorAcceptedPublicStatusContent = - '任务已接收,项目总控 Agent 正在启动处理。'; - -export function isSupervisorAutonomousPlayableAcceptedPublicStatus( - message, - runId, -) { - return ( - isPlainObject(message) && - message.role === 'assistant' && - message.agentId == null && - message.sessionId == null && - message.messageId === - runtimePublicStatusMessageId( - projectSupervisorAgentId, - supervisorSwarmSessionId, - runId, - projectSupervisorAcceptedPublicStatus, - ) && - message.content === projectSupervisorAcceptedPublicStatusContent - ); -} - -export function inspectSupervisorAutonomousPlayableConversationBoundary( - legacyConversation, - userFacing, - runId, -) { - const legacyUserFacingMessages = legacyConversation.filter((message) => - ['user', 'assistant'].includes(message.role), - ); - const acceptedPublicStatuses = legacyUserFacingMessages.filter((message) => - isSupervisorAutonomousPlayableAcceptedPublicStatus(message, runId), - ); - const wrongAgentFinalAssistants = userFacing.filter( - (message) => - message.role === 'assistant' && - message.agentId !== projectSupervisorAgentId && - !isSupervisorAutonomousPlayableAcceptedPublicStatus(message, runId), - ); - return { - legacyUserFacingMessageCount: legacyUserFacingMessages.length, - acceptedPublicStatusCount: acceptedPublicStatuses.length, - wrongAgentFinalAssistantCount: wrongAgentFinalAssistants.length, - }; -} - -export function supervisorAutonomousPlayableMode() { - return { - rootSource: projectSupervisorCliSource, - evidenceAgentId: projectSupervisorAgentId, - scenario: 'project-supervisor-autonomous-playable-lane-defense', - cliFlags: [], - }; -} - -export function buildSupervisorAutonomousPlayableStdin() { - const task = supervisorAutonomousPlayableLaneDefenseTask; - assert( - task === - '做一个植物大战僵尸式的塔防游戏,要能选择植物、阻挡敌人、正常闯关,并且生成后可以直接试玩。' && - !/[\r\n]/u.test(task), - 'supervisor-autonomous-playable-task-not-exact', - ); - for (const forbidden of [ - projectSupervisorAgentId, - autonomousGameBuildRunProfile, - 'Agent', - 'Runtime', - 'approve', - 'answer', - 'steer', - 'runner', - 'tool', - 'actionId', - 'runId', - ]) { - assert( - !task.includes(forbidden), - 'supervisor-autonomous-playable-task-recipe-leak', - ); - } - return Buffer.from(`${task}\n`, 'utf8'); -} - -export async function waitForSupervisorAutonomousPlayableParentRuntime(task) { - const deadline = Date.now() + 120_000; - while (Date.now() < deadline) { - const runtime = await readRuntime(projectSupervisorAgentId).catch( - () => null, - ); - if ( - runtime?.agentId === projectSupervisorAgentId && - runtime.sessionId === supervisorSwarmSessionId && - isNonEmptyString(runtime.runId) && - runtime.currentTask === task && - runtime.runProfile === autonomousGameBuildRunProfile && - /^[0-9a-f]{64}$/u.test(runtime.runProfileBindingFingerprint ?? '') - ) { - return runtime; - } - const session = state.supervisorAutonomousPlayableCliSession; - if (session?.closed && session.closeInfo?.code !== 0) { - throw codedProcessError('supervisor-autonomous-playable-cli-failed', { - ...session.closeInfo, - stdout: session.stdout, - stderr: session.stderr, - }); - } - await sleep(50); - } - throw codedError('supervisor-autonomous-playable-parent-runtime-timeout'); -} - -export async function waitForSupervisorAutonomousPlayableCliExit(session) { - let timeoutHandle; - const timeout = new Promise((resolve) => { - timeoutHandle = setTimeout( - () => resolve(null), - supervisorAutonomousPlayableRunTimeoutMs, - ); - }); - let result; - try { - result = await Promise.race([session.exitPromise, timeout]); - } finally { - clearTimeout(timeoutHandle); - } - if (!result) { - throw codedError('supervisor-autonomous-playable-cli-timeout'); - } - if (result.error || result.code !== 0 || result.signal !== null) { - state.supervisorAutonomousPlayable.cliOutput = - interactiveCliOutput(session); - throw codedProcessError('supervisor-autonomous-playable-cli-failed', { - ...result, - stdout: session.stdout, - stderr: session.stderr, - }); - } - const output = await waitForInteractiveCliOutput( - session, - () => - /(?:^|\r?\n)\[turn\.report\] [^\r\n]+\r?\n/u.test( - session.stdout.toString('utf8'), - ), - 'supervisor-autonomous-playable-turn-report-timeout', - 10_000, - { allowAfterProcessExit: true }, - ); - state.supervisorAutonomousPlayable.cliOutput = output; - const report = parseSingleSwarmTurnReport( - output, - 'supervisor-autonomous-playable', - ); - state.supervisorAutonomousPlayable.turnReport = report; - return report; -} - -export async function readSupervisorAutonomousPlayableResidualSidecarCounts() { - const roots = { - confirmations: '.agent/runtime/confirmations', - finalizations: '.agent/runtime/finalizations', - parallelReadBatches: '.agent/runtime/parallel-read-batches', - pendingActions: '.agent/runtime/pending-actions', - providerActionBatches: '.agent/runtime/provider-action-batches', - providerHandoffs: '.agent/runtime/provider-handoffs', - providerRetries: '.agent/runtime/provider-retries', - toolPlanHandoffs: '.agent/runtime/tool-plan-handoffs', - userInput: '.agent/runtime/user-input', - }; - const counts = {}; - for (const [name, relativePath] of Object.entries(roots)) { - counts[name] = ( - await listFiles(path.join(state.projectRoot, relativePath)) - ).length; - } - return counts; -} - -export function supervisorAutonomousPlayableSafeLifecycleLabel(value, allowed) { - if (value == null || value === '') return 'absent'; - return typeof value === 'string' && allowed.has(value) ? value : 'unknown'; -} - -export function countSupervisorAutonomousPlayableLifecycleLabels( - records, - field, - allowed, -) { - const counts = {}; - for (const record of records) { - const label = supervisorAutonomousPlayableSafeLifecycleLabel( - record?.[field], - allowed, - ); - counts[label] = (counts[label] ?? 0) + 1; - } - return counts; -} - -export function supervisorAutonomousPlayableSafeTurnCount(report, field) { - const value = report?.[field]; - return Number.isSafeInteger(value) && value >= 0 ? value : null; -} - -export function summarizeSupervisorAutonomousPlayableSourcePayloadAudits( - records, -) { - const protocols = records.filter( - (record) => record.recordType === 'agent.runtime.tool_plan.protocol', - ); - let sourceMutationActionMax = 0; - let sourcePayloadMaxFieldChars = 0; - let sourcePayloadMaxTotalChars = 0; - let sourcePayloadPolicyViolationCount = 0; - for (const record of protocols) { - const mutationCount = record.autonomousSourceMutationActionCount; - const maxFieldChars = record.autonomousSourceMaxFieldChars; - const totalChars = record.autonomousSourceTotalChars; - const valid = - record.autonomousSourcePayloadValidated === true && - Number.isSafeInteger(mutationCount) && - mutationCount >= 0 && - mutationCount <= 1 && - Number.isSafeInteger(maxFieldChars) && - maxFieldChars >= 0 && - maxFieldChars <= supervisorAutonomousPlayableSourceFieldMaxChars && - Number.isSafeInteger(totalChars) && - totalChars >= maxFieldChars && - totalChars <= supervisorAutonomousPlayableSourceTotalMaxChars; - if (!valid) sourcePayloadPolicyViolationCount += 1; - if (Number.isSafeInteger(mutationCount)) { - sourceMutationActionMax = Math.max( - sourceMutationActionMax, - mutationCount, - ); - } - if (Number.isSafeInteger(maxFieldChars)) { - sourcePayloadMaxFieldChars = Math.max( - sourcePayloadMaxFieldChars, - maxFieldChars, - ); - } - if (Number.isSafeInteger(totalChars)) { - sourcePayloadMaxTotalChars = Math.max( - sourcePayloadMaxTotalChars, - totalChars, - ); - } - } - return { - acceptedAutonomousToolPlanCount: protocols.length, - sourceMutationActionMax, - sourcePayloadMaxFieldChars, - sourcePayloadMaxTotalChars, - sourcePayloadPolicyViolationCount, - }; -} - -export function buildSupervisorAutonomousPlayablePartialEvidence( - persistence, - { - residualSidecars = {}, - pendingActionCount = 0, - turnReport = null, - partialEvidenceReadErrorCount = 0, - } = {}, -) { - const productionDefaultGameIndexSha256 = hashValue( - Buffer.from(productionDefaultGameIndexHtml(), 'utf8'), - ); - const parentTask = persistence.taskSnapshot.latest.find( - (task) => - task.agentId === projectSupervisorAgentId && - task.runId === state.initialRunId, - ); - const parentRuntime = persistence.runtimeStates.find( - (runtime) => - runtime.agentId === projectSupervisorAgentId && - runtime.runId === state.initialRunId, - ); - const childTasks = persistence.taskSnapshot.latest.filter( - (task) => - task.parentAgentId === projectSupervisorAgentId && - task.parentRunId === state.initialRunId, - ); - const childRuntimes = persistence.runtimeStates.filter( - (runtime) => - runtime.parentAgentId === projectSupervisorAgentId && - runtime.parentRunId === state.initialRunId, - ); - const deliveries = (persistence.deliveries ?? []).filter( - (delivery) => - delivery.parentAgentId === projectSupervisorAgentId && - delivery.parentRunId === state.initialRunId, - ); - const initialDeliveries = deliveries.filter( - (delivery) => delivery.repairOfDelegationId == null, - ); - const repairDeliveries = deliveries.filter( - (delivery) => delivery.repairOfDelegationId != null, - ); - const repairDeliveryIds = new Set( - repairDeliveries.map((delivery) => delivery.delegationId), - ); - const failedRepairChildCount = childTasks.filter( - (task) => isFailedTask(task) && repairDeliveryIds.has(task.delegationId), - ).length; - const failedOriginalTasks = childTasks.filter( - (task) => isFailedTask(task) && !repairDeliveryIds.has(task.delegationId), - ); - let recoveredOriginalFailureCount = 0; - for (const task of failedOriginalTasks) { - const original = initialDeliveries.find( - (delivery) => - delivery.delegationId === task.delegationId && - delivery.targetAgentId === task.agentId && - delivery.targetSessionId === task.sessionId && - delivery.targetRunId === task.runId, - ); - if ( - original && - repairDeliveries.some( - (repair) => - repair.repairOfDelegationId === original.delegationId && - repair.terminalStatus === 'completed' && - repair.structuredResult?.contractStatus === 'evidence-ready', - ) - ) { - recoveredOriginalFailureCount += 1; - } - } - - const relevantRunKeys = new Set(); - for (const record of [ - parentTask, - parentRuntime, - ...childTasks, - ...childRuntimes, - ]) { - if (isNonEmptyString(record?.agentId) && isNonEmptyString(record?.runId)) { - relevantRunKeys.add(`${record.agentId}\0${record.runId}`); - } - } - const lifecycle = persistence.agentDb.filter( - (record) => - record.recordType === 'agent.runtime.provider_request.lifecycle' && - relevantRunKeys.has(`${record.agentId}\0${record.runId}`), - ); - const retryAudits = persistence.agentDb.filter( - (record) => - record.recordType === 'agent.runtime.provider_request.retry' && - relevantRunKeys.has(`${record.agentId}\0${record.runId}`), - ); - const lifecycleByRequest = new Map(); - let duplicateProviderLifecycleCount = 0; - for (const record of lifecycle) { - if (!isNonEmptyString(record.requestId)) continue; - const statuses = lifecycleByRequest.get(record.requestId) ?? new Set(); - if (statuses.has(record.status)) duplicateProviderLifecycleCount += 1; - statuses.add(record.status); - lifecycleByRequest.set(record.requestId, statuses); - } - const terminalTaskCount = persistence.taskSnapshot.latest.filter((task) => - ['budget-exhausted', 'cancelled', 'completed', 'failed'].includes( - task.status, - ), - ).length; - const supervisorMessages = persistence.supervisorConversation ?? []; - const turnReportCaptured = isPlainObject(turnReport); - const turnReportOutcome = turnReportCaptured - ? supervisorAutonomousPlayableSafeTurnOutcomes.has(turnReport.outcome) - ? turnReport.outcome - : 'unknown' - : null; - const turnReportParentIdentityStable = turnReportCaptured - ? turnReport.parentAgentId === projectSupervisorAgentId && - turnReport.sessionId === supervisorSwarmSessionId && - turnReport.parentRunId === state.initialRunId - : false; - const turnReportPrivateLeakCount = turnReportCaptured - ? countExactSecrets( - Buffer.from(JSON.stringify(turnReport)), - state.supervisorAutonomousPlayable.privateValues, - ) - : null; - const sourcePayload = - summarizeSupervisorAutonomousPlayableSourcePayloadAudits( - persistence.agentDb, - ); - const providerBinding = - state.supervisorAutonomousPlayable.effectiveProviderBinding ?? - state.supervisorAutonomousPlayable.expectedProviderBinding; - - return { - evidenceCompleteness: 'partial', - partialEvidenceCollected: true, - partialEvidenceReadErrorCount, - partialPrivacyScanComplete: false, - rootRunObserved: Boolean(parentTask || parentRuntime), - parentTaskStatus: supervisorAutonomousPlayableSafeLifecycleLabel( - parentTask?.status, - supervisorAutonomousPlayableSafeStatuses, - ), - parentTaskPhase: supervisorAutonomousPlayableSafeLifecycleLabel( - parentTask?.phase, - supervisorAutonomousPlayableSafePhases, - ), - parentRuntimeStatus: supervisorAutonomousPlayableSafeLifecycleLabel( - parentRuntime?.status, - supervisorAutonomousPlayableSafeStatuses, - ), - parentRuntimePhase: supervisorAutonomousPlayableSafeLifecycleLabel( - parentRuntime?.phase, - supervisorAutonomousPlayableSafePhases, - ), - childTaskStatusCounts: countSupervisorAutonomousPlayableLifecycleLabels( - childTasks, - 'status', - supervisorAutonomousPlayableSafeStatuses, - ), - childTaskPhaseCounts: countSupervisorAutonomousPlayableLifecycleLabels( - childTasks, - 'phase', - supervisorAutonomousPlayableSafePhases, - ), - childRuntimeStatusCounts: countSupervisorAutonomousPlayableLifecycleLabels( - childRuntimes, - 'status', - supervisorAutonomousPlayableSafeStatuses, - ), - childRuntimePhaseCounts: countSupervisorAutonomousPlayableLifecycleLabels( - childRuntimes, - 'phase', - supervisorAutonomousPlayableSafePhases, - ), - initialDeliveryStatusCounts: - countSupervisorAutonomousPlayableLifecycleLabels( - initialDeliveries, - 'status', - supervisorAutonomousPlayableSafeDeliveryStatuses, - ), - repairDeliveryStatusCounts: - countSupervisorAutonomousPlayableLifecycleLabels( - repairDeliveries, - 'status', - supervisorAutonomousPlayableSafeDeliveryStatuses, - ), - repairTerminalStatusCounts: - countSupervisorAutonomousPlayableLifecycleLabels( - repairDeliveries, - 'terminalStatus', - supervisorAutonomousPlayableSafeTerminalStatuses, - ), - failedOriginalChildCount: failedOriginalTasks.length, - failedRepairChildCount, - recoveredSpecialistFailureCount: recoveredOriginalFailureCount, - recoveredOriginalFailureCount, - awaitingRepairCount: - failedOriginalTasks.length - recoveredOriginalFailureCount, - stdinTaskCount: state.supervisorAutonomousPlayable.stdinWriteCount, - stdinEndedAfterTask: state.supervisorAutonomousPlayable.stdinEnded, - stdinBytes: state.supervisorAutonomousPlayable.stdinBytes, - taskSha256: state.initialTask?.sha256 ?? null, - providerAgentMode: providerBinding?.providerAgentMode ?? null, - providerModel: providerBinding?.providerModel ?? null, - providerApiKind: providerBinding?.providerApiKind ?? null, - providerReasoningEffort: providerBinding?.providerReasoningEffort ?? null, - providerBaseUrlSha256: providerBinding?.providerBaseUrlSha256 ?? null, - providerBoundAgentCount: providerBinding?.boundAgentIds?.length ?? 0, - providerBindingMatched: - state.supervisorAutonomousPlayable.effectiveProviderBinding != null && - JSON.stringify( - state.supervisorAutonomousPlayable.effectiveProviderBinding, - ) === - JSON.stringify( - state.supervisorAutonomousPlayable.expectedProviderBinding, - ), - turnReportCaptured, - turnReportParentIdentityStable, - turnReportNewAssistantMessageCount: - supervisorAutonomousPlayableSafeTurnCount( - turnReport, - 'newAssistantMessageCount', - ), - turnReportFinalReplyChars: supervisorAutonomousPlayableSafeTurnCount( - turnReport, - 'finalReplyChars', - ), - turnReportPrivateLeakCount, - turnReportOutcome, - turnReportRuntimeCount: supervisorAutonomousPlayableSafeTurnCount( - turnReport, - 'runtimeCount', - ), - turnReportBusyRuntimeCount: supervisorAutonomousPlayableSafeTurnCount( - turnReport, - 'busyRuntimeCount', - ), - turnReportPendingTaskCount: supervisorAutonomousPlayableSafeTurnCount( - turnReport, - 'pendingTaskCount', - ), - turnReportRunningTaskCount: supervisorAutonomousPlayableSafeTurnCount( - turnReport, - 'runningTaskCount', - ), - turnReportWaitingForConfirmationCount: - supervisorAutonomousPlayableSafeTurnCount( - turnReport, - 'waitingForConfirmationCount', - ), - turnReportWaitingForUserInputCount: - supervisorAutonomousPlayableSafeTurnCount( - turnReport, - 'waitingForUserInputCount', - ), - turnReportReconciliationAgentCount: - supervisorAutonomousPlayableSafeTurnCount( - turnReport, - 'reconciliationAgentCount', - ), - taskCount: persistence.taskSnapshot.latest.length, - terminalTaskCount, - childTaskCount: childTasks.length, - runtimeCount: persistence.runtimeStates.length, - childRuntimeCount: childRuntimes.length, - finalSupervisorAssistantCount: supervisorMessages.filter( - (message) => message.role === 'assistant', - ).length, - supervisorUserMessageCount: supervisorMessages.filter( - (message) => message.role === 'user', - ).length, - professionalAssistantCount: ( - persistence.professionalConversations ?? [] - ).reduce( - (count, conversation) => - count + - conversation.messages.filter((message) => message.role === 'assistant') - .length, - 0, - ), - providerRequestIdentityCount: lifecycleByRequest.size, - providerLifecycleStartedCount: lifecycle.filter( - (record) => record.status === 'started', - ).length, - providerLifecycleTerminalCount: lifecycle.filter((record) => - ['completed', 'failed'].includes(record.status), - ).length, - providerLifecycleCompletedCount: lifecycle.filter( - (record) => record.status === 'completed', - ).length, - providerLifecycleFailedCount: lifecycle.filter( - (record) => record.status === 'failed', - ).length, - providerRetryAuditCount: retryAudits.length, - providerConnectivityRetryAuditCount: retryAudits.filter( - (record) => record.errorKind === 'connectivity', - ).length, - providerTransportRetryAuditCount: retryAudits.filter( - (record) => record.errorKind === 'transport', - ).length, - providerTimeoutRetryAuditCount: retryAudits.filter( - (record) => record.errorKind === 'timeout', - ).length, - ...sourcePayload, - openProviderLifecycleCount: [...lifecycleByRequest.values()].filter( - (statuses) => - statuses.has('started') && - !statuses.has('completed') && - !statuses.has('failed'), - ).length, - duplicateProviderLifecycleCount, - pendingActionCount, - confirmationSidecarCount: residualSidecars.confirmations ?? 0, - userInputSidecarCount: residualSidecars.userInput ?? 0, - providerActionBatchSidecarCount: - residualSidecars.providerActionBatches ?? 0, - providerRetrySidecarCount: residualSidecars.providerRetries ?? 0, - providerHandoffSidecarCount: residualSidecars.providerHandoffs ?? 0, - toolPlanHandoffSidecarCount: residualSidecars.toolPlanHandoffs ?? 0, - finalizationJournalCount: residualSidecars.finalizations ?? 0, - reconciliationResidueCount: - supervisorAutonomousPlayableReconciliationResidueCount(persistence), - freshInitBaselineUsed: - state.supervisorAutonomousPlayable.freshInitBaselineUsed, - initialGameIndexMatchesProductionDefault: - state.supervisorAutonomousPlayable.initialGameIndexSha256 === - productionDefaultGameIndexSha256, - initialGameIndexSha256: - state.supervisorAutonomousPlayable.initialGameIndexSha256, - }; -} - -export async function collectPartialSupervisorAutonomousPlayableEvidence() { - const persistence = await readSupervisorSwarmPersistence({ - tolerateErrors: true, - }); - let partialEvidenceReadErrorCount = Object.keys( - persistence.failureEvidenceErrors, - ).length; - let residualSidecars = {}; - try { - residualSidecars = - await readSupervisorAutonomousPlayableResidualSidecarCounts(); - } catch { - partialEvidenceReadErrorCount += 1; - recordError('supervisor-autonomous-playable-partial-sidecar-read-failed'); - } - let pendingActionCount = 0; - try { - pendingActionCount = (await findPendingActions()).length; - } catch { - partialEvidenceReadErrorCount += 1; - recordError('supervisor-autonomous-playable-partial-pending-read-failed'); - } - state.supervisorAutonomousPlayable.privateValues = [ - ...new Set([ - ...state.supervisorAutonomousPlayable.privateValues, - ...state.supervisorSwarm.privateValues, - ]), - ]; - return buildSupervisorAutonomousPlayablePartialEvidence(persistence, { - residualSidecars, - pendingActionCount, - turnReport: state.supervisorAutonomousPlayable.turnReport, - partialEvidenceReadErrorCount, - }); -} - -export function supervisorAutonomousPlayableReconciliationResidueCount( - persistence, -) { - const taskCount = persistence.taskSnapshot.latest.filter( - (task) => task.phase === 'needs-reconciliation', - ).length; - const runtimeCount = persistence.runtimeStates.filter( - (runtime) => runtime.phase === 'needs-reconciliation', - ).length; - const auditCount = persistence.agentDb.filter( - (record) => - record.status === 'needs-reconciliation' || - record.phase === 'needs-reconciliation' || - String(record.recordType ?? '').includes('needs_reconciliation'), - ).length; - return taskCount + runtimeCount + auditCount; -} - -export function supervisorAutonomousPlayableFailureDisposition( - persistence, - { requireRecovered = false } = {}, -) { - const deliveries = supervisorSwarmParentDeliveries( - persistence.deliveries ?? [], - ); - const parentTask = persistence.taskSnapshot.latest.find( - (task) => - task.agentId === projectSupervisorAgentId && - task.runId === state.initialRunId, - ); - const parentRuntime = persistence.runtimeStates.find( - (runtime) => - runtime.agentId === projectSupervisorAgentId && - runtime.runId === state.initialRunId, - ); - assert( - !isFailedTask(parentTask ?? {}) && !isFailedTask(parentRuntime ?? {}), - 'supervisor-autonomous-playable-root-failed', - ); - const parentActive = - (parentTask && isLiveTask(parentTask)) || - [ - 'pending', - 'running', - 'waiting-for-confirmation', - 'waiting-for-user-input', - ].includes(parentRuntime?.status) || - ['queued', 'running', 'executing', 'finalizing'].includes( - parentRuntime?.phase, - ); - let recoveredOriginalFailureCount = 0; - let awaitingRepairCount = 0; - for (const task of persistence.taskSnapshot.latest.filter( - (candidate) => - candidate.parentAgentId === projectSupervisorAgentId && - candidate.parentRunId === state.initialRunId && - isFailedTask(candidate), - )) { - const delivery = deliveries.find( - (candidate) => - candidate.delegationId === task.delegationId && - candidate.targetAgentId === task.agentId && - candidate.targetSessionId === task.sessionId && - candidate.targetRunId === task.runId, - ); - assert( - delivery && delivery.repairOfDelegationId == null, - delivery?.repairOfDelegationId != null - ? 'supervisor-autonomous-playable-repair-child-failed' - : 'supervisor-autonomous-playable-unrecoverable-child-failed', - ); - const repairs = deliveries.filter( - (candidate) => candidate.repairOfDelegationId === delivery.delegationId, - ); - assert( - repairs.length <= 1, - 'supervisor-autonomous-playable-duplicate-repair', - ); - const repairable = - delivery.status !== 'suppressed' && - ((delivery.acceptanceCriteria?.length ?? 0) > 0 || - (delivery.expectedArtifacts?.length ?? 0) > 0) && - (delivery.structuredResult == null || - delivery.structuredResult.contractStatus === 'needs-repair'); - const recovered = repairs.some( - (repair) => - repair.terminalStatus === 'completed' && - repair.structuredResult?.contractStatus === 'evidence-ready', - ); - assert( - repairable && (recovered || parentActive || !requireRecovered), - 'supervisor-autonomous-playable-specialist-failure-unrecovered', - ); - if (recovered) recoveredOriginalFailureCount += 1; - else awaitingRepairCount += 1; - } - if (requireRecovered) { - assert( - awaitingRepairCount === 0, - 'supervisor-autonomous-playable-terminal-repair-incomplete', - ); - } - return { recoveredOriginalFailureCount, awaitingRepairCount }; -} - -export function assertSupervisorAutonomousPlayableRuntimeHealthy(persistence) { - for (const task of persistence.taskSnapshot.latest) { - if (task.phase === 'needs-reconciliation') { - throw codedError( - 'supervisor-autonomous-playable-runtime-needs-reconciliation', - ); - } - } - for (const runtime of persistence.runtimeStates) { - if (runtime.phase === 'needs-reconciliation') { - throw codedError( - 'supervisor-autonomous-playable-runtime-needs-reconciliation', - ); - } - } - return supervisorAutonomousPlayableFailureDisposition(persistence); -} - -export async function waitForSupervisorAutonomousPlayableDurableQuiescence() { - const deadline = Date.now() + supervisorSwarmTerminalSidecarCleanupTimeoutMs; - let quietPolls = 0; - while (Date.now() < deadline) { - const persistence = await readSupervisorSwarmPersistence(); - assertSupervisorAutonomousPlayableRuntimeHealthy(persistence); - const pending = await findPendingActions(); - const residualSidecars = - await readSupervisorAutonomousPlayableResidualSidecarCounts(); - const noLiveTasks = persistence.taskSnapshot.latest.every( - (task) => - !isLiveTask(task) && - !['waiting-for-user-input', 'waiting-for-confirmation'].includes( - task.status, - ), - ); - const noLiveRuntimes = persistence.runtimeStates.every( - (runtime) => - !isNonEmptyString(runtime.runId) || - ['completed', 'cancelled'].includes(runtime.phase), - ); - const settled = - noLiveTasks && - noLiveRuntimes && - pending.length === 0 && - Object.values(residualSidecars).every((count) => count === 0) && - supervisorAutonomousPlayableReconciliationResidueCount(persistence) === 0; - if (settled) { - quietPolls += 1; - if (quietPolls >= 3) { - const failureDisposition = - supervisorAutonomousPlayableFailureDisposition(persistence, { - requireRecovered: true, - }); - return { persistence, residualSidecars, failureDisposition }; - } - } else { - quietPolls = 0; - } - await sleep(50); - } - throw codedError('supervisor-autonomous-playable-residue-timeout'); -} - -export function supervisorAutonomousPlayableStaticSmokeMatchesFinalIndex( - gate, - revision, - finalGameIndexSha256, -) { - return ( - gate?.lastVerificationStatus === 'passed' && - gate.verifiedRevision === revision && - gate.staticSmokeVerifiedRevision === revision && - gate.staticSmokeVerifiedGameIndexSha256 === finalGameIndexSha256 - ); -} - -export async function validateSupervisorAutonomousPlayableEvidence( - persistence, - residualSidecars, -) { - const task = supervisorAutonomousPlayableLaneDefenseTask; - const mode = supervisorAutonomousPlayableMode(); - const report = state.supervisorAutonomousPlayable.turnReport; - assert( - report?.schemaVersion === 'game-creator-swarm-turn-report.v1' && - report.outcome === 'settled' && - report.parentAgentId === projectSupervisorAgentId && - report.sessionId === supervisorSwarmSessionId && - report.parentRunId === state.initialRunId && - report.runtimeCount >= 1 && - report.busyRuntimeCount === 0 && - report.pendingTaskCount === 0 && - report.runningTaskCount === 0 && - report.waitingForConfirmationCount === 0 && - report.waitingForUserInputCount === 0 && - report.newAssistantMessageCount === 1 && - report.finalReplyChars > 0 && - report.reconciliationAgentCount === 0, - 'supervisor-autonomous-playable-turn-report-invalid', - ); - - const failureDisposition = supervisorAutonomousPlayableFailureDisposition( - persistence, - { - requireRecovered: true, - }, - ); - const parentTask = persistence.taskSnapshot.latest.find( - (candidate) => - candidate.agentId === projectSupervisorAgentId && - candidate.sessionId === supervisorSwarmSessionId && - candidate.runId === state.initialRunId, - ); - const parentRuntime = persistence.runtimeStates.find( - (candidate) => - candidate.agentId === projectSupervisorAgentId && - candidate.runId === state.initialRunId, - ); - assert( - parentTask?.task === task && - parentTask.source === mode.rootSource && - parentTask.status === 'completed' && - parentTask.phase === 'completed' && - parentTask.runProfile === autonomousGameBuildRunProfile && - parentRuntime?.sessionId === supervisorSwarmSessionId && - parentRuntime.phase === 'completed' && - parentRuntime.pendingToolAction == null && - parentRuntime.pendingAction == null && - parentRuntime.queuedSteerCount === 0 && - parentRuntime.runProfile === autonomousGameBuildRunProfile && - persistence.taskSnapshot.latest.every( - (candidate) => - !isLiveTask(candidate) && - !['waiting-for-user-input', 'waiting-for-confirmation'].includes( - candidate.status, - ), - ) && - persistence.runtimeStates.every( - (candidate) => - candidate.phase !== 'needs-reconciliation' && - ![ - 'pending', - 'running', - 'waiting-for-user-input', - 'waiting-for-confirmation', - ].includes(candidate.status), - ), - 'supervisor-autonomous-playable-runtime-not-terminal', - ); - - const supervisorUsers = persistence.supervisorConversation.filter( - (message) => message.role === 'user', - ); - const supervisorAssistants = persistence.supervisorConversation.filter( - (message) => message.role === 'assistant', - ); - const assistant = supervisorAssistants[0]; - const professionalMessages = persistence.professionalConversations.flatMap( - (entry) => entry.messages, - ); - const isolatedMessages = persistence.isolatedConversations.flatMap( - (entry) => entry.messages, - ); - const userFacing = [ - ...persistence.supervisorConversation, - ...persistence.legacyConversation, - ]; - const conversationBoundary = - inspectSupervisorAutonomousPlayableConversationBoundary( - persistence.legacyConversation, - userFacing, - state.initialRunId, - ); - const allMessages = [ - ...userFacing, - ...professionalMessages, - ...isolatedMessages, - ]; - const duplicateMessageCount = duplicateCount( - allMessages.map((message) => message.messageId).filter(Boolean), - ); - const parentAssistantAudits = persistence.agentDb.filter( - (record) => - record.recordType === 'conversation.message' && - record.role === 'assistant' && - record.agentId === projectSupervisorAgentId && - record.sessionId === supervisorSwarmSessionId && - record.messageId === assistant?.messageId, - ); - const parentCompletedAudits = persistence.agentDb.filter( - (record) => - record.recordType === 'agent.runtime.completed' && - record.agentId === projectSupervisorAgentId && - record.runId === state.initialRunId, - ); - assert( - supervisorUsers.length === 1 && - supervisorUsers[0].content === task && - supervisorAssistants.length === 1 && - assistant.agentId === projectSupervisorAgentId && - assistant.messageId === - finalMessageId( - projectSupervisorAgentId, - supervisorSwarmSessionId, - state.initialRunId, - ) && - conversationBoundary.legacyUserFacingMessageCount === 1 && - conversationBoundary.acceptedPublicStatusCount === 1 && - conversationBoundary.wrongAgentFinalAssistantCount === 0 && - parentAssistantAudits.length === 1 && - parentCompletedAudits.length === 1 && - duplicateMessageCount === 0, - 'supervisor-autonomous-playable-final-assistant-invalid', - ); - const finalization = validateSupervisorSwarmFinalization( - persistence.agentDb, - { - agentId: projectSupervisorAgentId, - taskId: parentTask.taskId, - sessionId: supervisorSwarmSessionId, - runId: state.initialRunId, - }, - assistant, - ); - - const actionRecords = persistence.agentDb.filter( - (record) => - isNonEmptyString(record.actionId) && - [ - 'agent.runtime.tool_action.executing', - 'agent.runtime.tool_action.observed', - 'agent.runtime.tool_observation', - 'agent.runtime.action_receipt', - ].includes(record.recordType), - ); - const actionReceipts = actionRecords.filter( - (record) => record.recordType === 'agent.runtime.action_receipt', - ); - const duplicateActionLifecycleCount = duplicateCount( - actionRecords.map(actionAuditIdentity), - ); - const duplicateReceiptCount = duplicateCount( - actionReceipts.map( - (record) => `${record.agentId}\0${record.runId}\0${record.actionId}`, - ), - ); - assert( - duplicateActionLifecycleCount === 0 && duplicateReceiptCount === 0, - 'supervisor-autonomous-playable-duplicate-action-or-receipt', - ); - - const runProfileBindings = await readSupervisorSwarmJsonDirectory( - '.agent/runtime/run-profile-bindings', - ); - const autonomousRunKeys = new Set( - runProfileBindings - .filter( - (candidate) => - candidate.profile === autonomousGameBuildRunProfile && - candidate.rootAgentId === projectSupervisorAgentId && - candidate.rootRunId === state.initialRunId, - ) - .map((candidate) => `${candidate.agentId}\0${candidate.runId}`), - ); - const sourcePayload = - summarizeSupervisorAutonomousPlayableSourcePayloadAudits( - persistence.agentDb.filter((record) => - autonomousRunKeys.has(`${record.agentId}\0${record.runId}`), - ), - ); - assert( - sourcePayload.acceptedAutonomousToolPlanCount > 0 && - sourcePayload.sourcePayloadPolicyViolationCount === 0, - 'supervisor-autonomous-playable-source-payload-policy-invalid', - ); - const matchingBindings = runProfileBindings.filter( - (binding) => - binding.agentId === projectSupervisorAgentId && - binding.runId === state.initialRunId, - ); - const binding = matchingBindings[0]; - const bindingIdentity = binding && { - schemaVersion: binding.schemaVersion, - projectId: binding.projectId, - agentId: binding.agentId, - runId: binding.runId, - rootAgentId: binding.rootAgentId, - rootRunId: binding.rootRunId, - parentAgentId: binding.parentAgentId, - parentRunId: binding.parentRunId, - source: binding.source, - profile: binding.profile, - profileFingerprint: binding.profileFingerprint, - parentBindingFingerprint: binding.parentBindingFingerprint, - boundAt: binding.boundAt, - }; - assert( - matchingBindings.length === 1 && - binding.schemaVersion === runProfileBindingSchemaVersion && - binding.rootAgentId === projectSupervisorAgentId && - binding.rootRunId === state.initialRunId && - binding.parentAgentId == null && - binding.parentRunId == null && - binding.source === mode.rootSource && - binding.profile === autonomousGameBuildRunProfile && - binding.profileFingerprint === - hashJsonValue({ - schemaVersion: runProfileBindingSchemaVersion, - profile: autonomousGameBuildRunProfile, - }) && - binding.bindingFingerprint === hashJsonValue(bindingIdentity), - 'supervisor-autonomous-playable-run-profile-binding-invalid', - ); - - const contracts = await readSupervisorSwarmJsonDirectory( - '.agent/runtime/autonomous-completion-contracts', - ); - const receipts = await readSupervisorSwarmJsonDirectory( - '.agent/runtime/autonomous-playtest-receipts', - ); - assert( - contracts.length === 1 && receipts.length === 1, - 'supervisor-autonomous-playable-autonomous-artifact-count-invalid', - ); - const contract = contracts[0]; - const receipt = receipts[0]; - const evidenceBinding = binding; - const evidenceRunId = state.initialRunId; - const contractIdentity = { - schemaVersion: contract.schemaVersion, - projectId: contract.projectId, - agentId: contract.agentId, - runId: contract.runId, - runProfileBindingFingerprint: contract.runProfileBindingFingerprint, - taskSha256: contract.taskSha256, - baselineRevision: contract.baselineRevision, - baselineIndexSha256: contract.baselineIndexSha256, - baselineArtifacts: contract.baselineArtifacts, - playtestScenario: contract.playtestScenario, - createdAt: contract.createdAt, - }; - assert( - contract.schemaVersion === autonomousCompletionContractSchemaVersion && - contract.projectId === binding.projectId && - contract.agentId === projectSupervisorAgentId && - contract.runId === state.initialRunId && - contract.runProfileBindingFingerprint === binding.bindingFingerprint && - contract.taskSha256 === hashValue(task) && - contract.baselineIndexSha256 === - state.supervisorAutonomousPlayable.initialGameIndexSha256 && - Array.isArray(contract.baselineArtifacts) && - contract.baselineArtifacts.length === 1 && - contract.baselineArtifacts[0]?.path === 'game/index.html' && - contract.baselineArtifacts[0]?.sha256 === - state.supervisorAutonomousPlayable.initialGameIndexSha256 && - Number.isInteger(contract.baselineArtifacts[0]?.sizeBytes) && - contract.baselineArtifacts[0].sizeBytes > 0 && - contract.playtestScenario === 'lane-defense-v1' && - contract.contractFingerprint === hashJsonValue(contractIdentity), - 'supervisor-autonomous-playable-completion-contract-invalid', - ); - - const revision = await readSupervisorSwarmProjectRevision(); - const finalGameIndex = await fs.readFile( - path.join(state.projectRoot, 'game/index.html'), - ); - const finalGameIndexSha256 = hashValue(finalGameIndex); - assert( - revision.schemaVersion === 'game-creator-project-revision.v1' && - revision.projectId === contract.projectId && - revision.revision > contract.baselineRevision && - finalGameIndexSha256 !== - state.supervisorAutonomousPlayable.initialGameIndexSha256 && - finalGameIndexSha256 !== contract.baselineIndexSha256, - 'supervisor-autonomous-playable-project-output-invalid', - ); - - const gates = ( - await readSupervisorSwarmJsonDirectory('.agent/runtime/verification') - ).filter( - (gate) => - gate.agentId === mode.evidenceAgentId && gate.runId === evidenceRunId, - ); - const gate = gates[0]; - const staticSmokeAudits = persistence.agentDb.filter( - (record) => - record.recordType === 'agent.runtime.command.run_limited' && - record.agentId === mode.evidenceAgentId && - record.runId === evidenceRunId && - record.commandId === 'game.static_smoke' && - record.status === 'completed', - ); - assert( - gates.length === 1 && - supervisorAutonomousPlayableStaticSmokeMatchesFinalIndex( - gate, - revision.revision, - finalGameIndexSha256, - ) && - staticSmokeAudits.length >= 1, - 'supervisor-autonomous-playable-static-smoke-invalid', - ); - - const receiptIdentity = { - schemaVersion: receipt.schemaVersion, - projectId: receipt.projectId, - agentId: receipt.agentId, - runId: receipt.runId, - runProfileBindingFingerprint: receipt.runProfileBindingFingerprint, - executorAgentId: receipt.executorAgentId, - executorRunId: receipt.executorRunId, - executorSource: receipt.executorSource, - executorRunProfileBindingFingerprint: - receipt.executorRunProfileBindingFingerprint, - actionId: receipt.actionId, - actionFingerprint: receipt.actionFingerprint, - revision: receipt.revision, - gameIndex: receipt.gameIndex, - playtestScenario: receipt.playtestScenario, - scenarioFingerprint: receipt.scenarioFingerprint, - report: receipt.report, - screenshots: receipt.screenshots, - createdAt: receipt.createdAt, - }; - assert( - receipt.schemaVersion === autonomousPlaytestReceiptSchemaVersion && - receipt.projectId === contract.projectId && - receipt.agentId === mode.evidenceAgentId && - receipt.runId === evidenceRunId && - receipt.runProfileBindingFingerprint === - evidenceBinding.bindingFingerprint && - receipt.revision === revision.revision && - receipt.gameIndex.path === 'game/index.html' && - receipt.gameIndex.sha256 === finalGameIndexSha256 && - receipt.gameIndex.sizeBytes === finalGameIndex.length && - receipt.playtestScenario === 'lane-defense-v1' && - receipt.receiptFingerprint === hashJsonValue(receiptIdentity) && - actionReceipts.filter( - (record) => - record.agentId === mode.evidenceAgentId && - record.runId === evidenceRunId && - record.actionId === receipt.actionId && - record.actionFingerprint === receipt.actionFingerprint && - record.tool === 'preview.validate' && - record.status === 'ok', - ).length === 1, - 'supervisor-autonomous-playable-playtest-receipt-invalid', - ); - - const readDigest = async (digest, expectedSuffix) => { - assert( - isNonEmptyString(digest?.path) && - !path.isAbsolute(digest.path) && - digest.path.endsWith(expectedSuffix), - 'supervisor-autonomous-playable-evidence-path-invalid', - ); - const file = path.resolve(state.projectRoot, digest.path); - assert( - isPathInside(state.projectRoot, file), - 'supervisor-autonomous-playable-evidence-path-escape', - ); - const metadata = await fs.lstat(file); - assert( - metadata.isFile() && !metadata.isSymbolicLink(), - 'supervisor-autonomous-playable-evidence-file-invalid', - ); - const bytes = await fs.readFile(file); - assert( - hashValue(bytes) === digest.sha256 && bytes.length === digest.sizeBytes, - 'supervisor-autonomous-playable-evidence-digest-invalid', - ); - return bytes; - }; - const reportBytes = await readDigest(receipt.report, '/validation.json'); - const browserReport = JSON.parse( - decodeUtf8Fatal( - reportBytes, - 'supervisor-autonomous-playable-browser-report-invalid-utf8', - ), - ); - const screenshotEntries = await Promise.all( - receipt.screenshots.map(async (digest) => ({ - digest, - bytes: await readDigest(digest, '.png'), - })), - ); - assert( - screenshotEntries.length === 2 && - screenshotEntries.every(({ bytes }) => - bytes.subarray(0, 8).equals(Buffer.from('\x89PNG\r\n\x1a\n', 'binary')), - ), - 'supervisor-autonomous-playable-screenshot-invalid', - ); - const desktop = screenshotEntries.find(({ digest }) => - digest.path.endsWith('/desktop.png'), - ); - const mobile = screenshotEntries.find(({ digest }) => - digest.path.endsWith('/mobile.png'), - ); - const desktopViewport = browserReport.viewportResults?.find( - (entry) => entry.viewport === 'desktop', - ); - const mobileViewport = browserReport.viewportResults?.find( - (entry) => entry.viewport === 'mobile', - ); - const playtest = browserReport.playtest; - const assertionNames = new Set( - (playtest?.assertions ?? []).map((entry) => entry.name), - ); - assert( - desktop && - mobile && - browserReport.schemaVersion === 'browser-validation.v1' && - browserReport.passed === true && - Array.isArray(browserReport.viewportResults) && - browserReport.viewportResults.length === 2 && - JSON.stringify( - browserReport.viewportResults.map((entry) => entry.viewport).sort(), - ) === JSON.stringify(['desktop', 'mobile']) && - desktopViewport?.passed === true && - mobileViewport?.passed === true && - playtest?.scenario === 'lane-defense-v1' && - playtest.passed === true && - playtest.scenarioFingerprint === receipt.scenarioFingerprint && - playtest.assertions.every((entry) => entry.passed === true) && - laneDefensePlaytestRequiredAssertions.every((name) => - assertionNames.has(name), - ), - 'supervisor-autonomous-playable-lane-defense-playtest-invalid', - ); - const lifecycle = persistence.agentDb.filter( - (record) => - record.recordType === 'agent.runtime.provider_request.lifecycle', - ); - const retryAudits = persistence.agentDb.filter( - (record) => record.recordType === 'agent.runtime.provider_request.retry', - ); - const lifecycleByRequest = new Map(); - for (const record of lifecycle) { - assert( - record.auditSchemaVersion === providerRequestLifecycleSchemaVersion && - isNonEmptyString(record.requestId), - 'supervisor-autonomous-playable-provider-lifecycle-invalid', - ); - const records = lifecycleByRequest.get(record.requestId) ?? []; - records.push(record); - lifecycleByRequest.set(record.requestId, records); - } - for (const records of lifecycleByRequest.values()) { - assert( - records.length === 2 && - records[0].status === 'started' && - ['completed', 'failed'].includes(records[1].status) && - [ - 'agentId', - 'taskId', - 'sessionId', - 'runId', - 'source', - 'requestKind', - 'requestSlot', - ].every((field) => records[0][field] === records[1][field]), - 'supervisor-autonomous-playable-provider-lifecycle-open-or-duplicate', - ); - } - assert( - lifecycleByRequest.size > 0 && - retryAudits.every( - (retry) => - lifecycleByRequest.get(retry.requestId)?.[1]?.status === 'failed' && - lifecycle.some( - (record) => - record.status === 'started' && - record.agentId === retry.agentId && - record.runId === retry.runId && - record.requestSlot === retry.nextRequestSlot, - ), - ), - 'supervisor-autonomous-playable-provider-retry-invalid', - ); - - const pendingActions = await findPendingActions(); - const reconciliationResidueCount = - supervisorAutonomousPlayableReconciliationResidueCount(persistence); - const steerRecordCount = - (await countSupervisorSwarmSteerRecords()) + - persistence.agentDb.filter((record) => - String(record.recordType ?? '').startsWith('agent.runtime.steer'), - ).length; - const approvalAuditCount = persistence.agentDb.filter( - (record) => - record.recordType === 'agent.runtime.tool_confirmation.approved', - ).length; - const rejectionAuditCount = persistence.agentDb.filter( - (record) => - record.recordType === 'agent.runtime.tool_confirmation.rejected', - ).length; - const confirmationAuditCount = persistence.agentDb.filter((record) => - String(record.recordType ?? '').includes('confirmation'), - ).length; - const userInputAuditCount = persistence.agentDb.filter((record) => - String(record.recordType ?? '').startsWith('agent.runtime.user_input.'), - ).length; - assert( - pendingActions.length === 0 && - Object.values(residualSidecars).every((count) => count === 0) && - reconciliationResidueCount === 0 && - approvalAuditCount === 0 && - rejectionAuditCount === 0 && - confirmationAuditCount === 0 && - userInputAuditCount === 0 && - steerRecordCount === 0 && - !/\[待确认\]|\[Needs input\]|输入 approve 或 reject|请选择 1-/u.test( - state.supervisorAutonomousPlayable.cliOutput, - ), - 'supervisor-autonomous-playable-intervention-or-residue-detected', - ); - - const publicLeaks = collectSupervisorSwarmPublicLeakEvidence(persistence, { - requireZero: true, - }); - const projectLureLeakCount = await countLureLeaks(); - const cliLureLeakCount = countExactSecrets( - Buffer.from(state.supervisorAutonomousPlayable.cliOutput), - state.lures, - ); - state.lureLeakCount = projectLureLeakCount + cliLureLeakCount; - assert(state.lureLeakCount === 0, 'sensitive-lure-leak-detected'); - state.supervisorAutonomousPlayable.privateValues = [ - ...new Set(state.supervisorSwarm.privateValues), - ]; - const absolutePaths = absolutePathVariants( - state.projectRoot, - state.options?.configDir, - state.isolatedRunner.appDataDir, - ); - let logApiKeyLeakCount = 0; - let logPrivateBodyLeakCount = 0; - let logAbsolutePathLeakCount = 0; - for (const file of await listFiles( - path.join(state.projectRoot, '.agent/logs'), - )) { - const metadata = await fs.lstat(file); - if (!metadata.isFile() || metadata.isSymbolicLink()) continue; - const bytes = await fs.readFile(file); - logApiKeyLeakCount += countExactSecrets(bytes, state.secrets); - logPrivateBodyLeakCount += countExactSecrets( - bytes, - state.supervisorAutonomousPlayable.privateValues, - ); - logAbsolutePathLeakCount += countExactSecrets(bytes, absolutePaths); - } - const browserReportApiKeyLeakCount = countExactSecrets( - reportBytes, - state.secrets, - ); - const browserReportPrivateBodyLeakCount = countExactSecrets( - reportBytes, - state.supervisorAutonomousPlayable.privateValues, - ); - const browserReportAbsolutePathLeakCount = countExactSecrets( - reportBytes, - absolutePaths, - ); - assert( - logApiKeyLeakCount === 0 && - logPrivateBodyLeakCount === 0 && - logAbsolutePathLeakCount === 0 && - browserReportApiKeyLeakCount === 0 && - browserReportPrivateBodyLeakCount === 0 && - browserReportAbsolutePathLeakCount === 0, - 'supervisor-autonomous-playable-log-or-report-leak-detected', - ); - const lifecycleProjection = buildSupervisorAutonomousPlayablePartialEvidence( - persistence, - { - residualSidecars, - pendingActionCount: pendingActions.length, - turnReport: report, - }, - ); - const providerBinding = - state.supervisorAutonomousPlayable.effectiveProviderBinding; - assert( - providerBinding && - JSON.stringify(providerBinding) === - JSON.stringify( - state.supervisorAutonomousPlayable.expectedProviderBinding, - ) && - isNonEmptyString(providerBinding.providerModel) && - isNonEmptyString(providerBinding.providerApiKind) && - isNonEmptyString(providerBinding.providerReasoningEffort) && - /^[0-9a-f]{64}$/u.test(providerBinding.providerBaseUrlSha256), - 'supervisor-autonomous-playable-provider-binding-invalid', - ); - - return { - ...emptySupervisorAutonomousPlayableEvidence(), - ...lifecycleProjection, - evidenceCompleteness: 'complete', - partialEvidenceCollected: false, - partialEvidenceReadErrorCount: 0, - partialPrivacyScanComplete: true, - stdinTaskCount: state.supervisorAutonomousPlayable.stdinWriteCount, - stdinEndedAfterTask: state.supervisorAutonomousPlayable.stdinEnded, - stdinBytes: state.supervisorAutonomousPlayable.stdinBytes, - taskSha256: hashValue(task), - providerAgentMode: providerBinding.providerAgentMode, - providerModel: providerBinding.providerModel, - providerApiKind: providerBinding.providerApiKind, - providerReasoningEffort: providerBinding.providerReasoningEffort, - providerBaseUrlSha256: providerBinding.providerBaseUrlSha256, - providerBoundAgentCount: providerBinding.boundAgentIds.length, - providerBindingMatched: true, - turnReportCaptured: true, - turnReportParentIdentityStable: true, - turnReportNewAssistantMessageCount: report.newAssistantMessageCount, - turnReportFinalReplyChars: report.finalReplyChars, - turnReportPrivateLeakCount: 0, - turnReportOutcome: report.outcome, - turnReportRuntimeCount: report.runtimeCount, - turnReportBusyRuntimeCount: report.busyRuntimeCount, - turnReportPendingTaskCount: report.pendingTaskCount, - turnReportRunningTaskCount: report.runningTaskCount, - turnReportWaitingForConfirmationCount: report.waitingForConfirmationCount, - turnReportWaitingForUserInputCount: report.waitingForUserInputCount, - turnReportReconciliationAgentCount: report.reconciliationAgentCount, - taskCount: persistence.taskSnapshot.latest.length, - terminalTaskCount: persistence.taskSnapshot.latest.length, - childTaskCount: persistence.taskSnapshot.latest.filter( - (candidate) => candidate.parentRunId === state.initialRunId, - ).length, - runtimeCount: persistence.runtimeStates.length, - childRuntimeCount: persistence.runtimeStates.filter( - (candidate) => candidate.parentRunId === state.initialRunId, - ).length, - recoveredSpecialistFailureCount: - failureDisposition.recoveredOriginalFailureCount, - recoveredOriginalFailureCount: - failureDisposition.recoveredOriginalFailureCount, - finalSupervisorAssistantCount: supervisorAssistants.length, - supervisorUserMessageCount: supervisorUsers.length, - professionalAssistantCount: professionalMessages.filter( - (message) => message.role === 'assistant', - ).length, - isolatedAssistantCount: isolatedMessages.filter( - (message) => message.role === 'assistant', - ).length, - professionalUserFacingAssistantCount: 0, - completedAuditCount: parentCompletedAudits.length, - finalizationStageCount: finalization.stageCount, - providerRequestIdentityCount: lifecycleByRequest.size, - providerLifecycleStartedCount: lifecycle.filter( - (record) => record.status === 'started', - ).length, - providerLifecycleTerminalCount: lifecycle.filter((record) => - ['completed', 'failed'].includes(record.status), - ).length, - providerLifecycleCompletedCount: lifecycle.filter( - (record) => record.status === 'completed', - ).length, - providerLifecycleFailedCount: lifecycle.filter( - (record) => record.status === 'failed', - ).length, - providerRetryAuditCount: retryAudits.length, - providerConnectivityRetryAuditCount: retryAudits.filter( - (record) => record.errorKind === 'connectivity', - ).length, - providerTransportRetryAuditCount: retryAudits.filter( - (record) => record.errorKind === 'transport', - ).length, - providerTimeoutRetryAuditCount: retryAudits.filter( - (record) => record.errorKind === 'timeout', - ).length, - ...sourcePayload, - openProviderLifecycleCount: 0, - duplicateProviderLifecycleCount: 0, - duplicateMessageCount, - duplicateActionLifecycleCount, - duplicateReceiptCount, - runProfileBindingCount: matchingBindings.length, - baselineRevision: contract.baselineRevision, - projectRevision: revision.revision, - projectRevisionDelta: revision.revision - contract.baselineRevision, - initialGameIndexSha256: - state.supervisorAutonomousPlayable.initialGameIndexSha256, - finalGameIndexSha256, - gameIndexChanged: true, - gameIndexBytes: finalGameIndex.length, - staticSmokePassed: true, - staticSmokeAuditCount: staticSmokeAudits.length, - autonomousCompletionContractCount: contracts.length, - autonomousPlaytestReceiptCount: receipts.length, - playtestScenario: playtest.scenario, - laneDefensePlaytestPassed: true, - laneDefenseAssertionCount: playtest.assertions.length, - laneDefensePassedAssertionCount: playtest.assertions.filter( - (entry) => entry.passed, - ).length, - browserValidationPassed: true, - desktopScreenshotSha256: desktop.digest.sha256, - desktopScreenshotBytes: desktop.bytes.length, - mobileScreenshotSha256: mobile.digest.sha256, - mobileScreenshotBytes: mobile.bytes.length, - browserReportSha256: receipt.report.sha256, - browserReportBytes: reportBytes.length, - pendingActionCount: pendingActions.length, - confirmationSidecarCount: residualSidecars.confirmations, - userInputSidecarCount: residualSidecars.userInput, - providerActionBatchSidecarCount: residualSidecars.providerActionBatches, - providerRetrySidecarCount: residualSidecars.providerRetries, - providerHandoffSidecarCount: residualSidecars.providerHandoffs, - toolPlanHandoffSidecarCount: residualSidecars.toolPlanHandoffs, - finalizationJournalCount: residualSidecars.finalizations, - reconciliationResidueCount, - approvalAuditCount, - rejectionAuditCount, - userInputAuditCount, - steerRecordCount, - providerPayloadPublicLeakCount: publicLeaks.providerPayloadPublicLeakCount, - privateBodyPublicLeakCount: publicLeaks.privateBodyPublicLeakCount, - apiKeyPublicLeakCount: publicLeaks.apiKeyPublicLeakCount, - projectPathPublicLeakCount: publicLeaks.projectPathPublicLeakCount, - formalConfigPathPublicLeakCount: - publicLeaks.formalConfigPathPublicLeakCount, - logApiKeyLeakCount, - logPrivateBodyLeakCount, - logAbsolutePathLeakCount, - browserReportApiKeyLeakCount, - browserReportPrivateBodyLeakCount, - browserReportAbsolutePathLeakCount, - paths: [ - receipt.gameIndex.path, - receipt.report.path, - desktop.digest.path, - mobile.digest.path, - ], - }; -} - -export async function runSupervisorAutonomousPlayableLaneDefenseE2e() { - await ensureOwnedRunnerStableKillSupport(); - const mode = supervisorAutonomousPlayableMode(); - state.supervisorAutonomousPlayable.freshInitBaselineUsed = false; - await seedDisposableProject({ preserveProductionInitBaseline: false }); - const initialGameIndex = await fs.readFile( - path.join(state.projectRoot, 'game/index.html'), - ); - state.supervisorAutonomousPlayable.initialGameIndexSha256 = - hashValue(initialGameIndex); - state.cliBinary = await prepareCliBinary(); - await prepareIsolatedSuiteAppData(); - state.isolatedRunner.launchAttempted = true; - - const stdin = buildSupervisorAutonomousPlayableStdin(); - const task = supervisorAutonomousPlayableLaneDefenseTask; - state.initialTask = { - chars: [...task].length, - sha256: hashValue(task), - }; - state.supervisorAutonomousPlayable.privateValues = [task]; - state.supervisorSwarm.userTask = task; - state.supervisorSwarm.privateValues = [task]; - state.supervisorAutonomousPlayableCliSession = startInteractiveCli([ - '--swarm-chat', - '--init', - '--autonomous-game-build', - ...mode.cliFlags, - state.projectRoot, - ]); - state.supervisorAutonomousPlayable.stdinWriteCount = 1; - state.supervisorAutonomousPlayable.stdinBytes = stdin.length; - state.supervisorAutonomousPlayable.stdinEnded = true; - state.supervisorAutonomousPlayableCliSession.child.stdin.end(stdin); - - const started = await waitForSupervisorAutonomousPlayableParentRuntime(task); - state.initialRunId = started.runId; - state.initialSessionId = started.sessionId; - await claimOwnedRunner(); - const report = await waitForSupervisorAutonomousPlayableCliExit( - state.supervisorAutonomousPlayableCliSession, - ); - assert( - report.outcome === 'settled', - 'supervisor-autonomous-playable-turn-not-settled', - ); - - const { persistence, residualSidecars } = - await waitForSupervisorAutonomousPlayableDurableQuiescence(); - state.identityStable = true; - state.evidence = await validateSupervisorAutonomousPlayableEvidence( - persistence, - residualSidecars, - ); - assert(state.evidence.secretLeakCount === 0, 'loaded-key-leak-detected'); -} - -export function emptySupervisorAutonomousPlayableEvidence() { - const mode = supervisorAutonomousPlayableMode(); - return { - scenario: mode.scenario, - targetAgentId: projectSupervisorAgentId, - runProfile: autonomousGameBuildRunProfile, - dedicatedZeroInterventionPath: true, - faultInjectionUsed: false, - activeRunnerKillCount: 0, - approveInputCount: 0, - answerInputCount: 0, - steerInputCount: 0, - stdinTaskCount: 0, - stdinEndedAfterTask: false, - stdinBytes: 0, - taskSha256: null, - providerAgentMode: null, - providerModel: null, - providerApiKind: null, - providerReasoningEffort: null, - providerBaseUrlSha256: null, - providerBoundAgentCount: 0, - providerBindingMatched: false, - evidenceCompleteness: 'none', - partialEvidenceCollected: false, - partialEvidenceReadErrorCount: 0, - partialPrivacyScanComplete: false, - rootRunObserved: false, - parentTaskStatus: null, - parentTaskPhase: null, - parentRuntimeStatus: null, - parentRuntimePhase: null, - childTaskStatusCounts: {}, - childTaskPhaseCounts: {}, - childRuntimeStatusCounts: {}, - childRuntimePhaseCounts: {}, - initialDeliveryStatusCounts: {}, - repairDeliveryStatusCounts: {}, - repairTerminalStatusCounts: {}, - failedOriginalChildCount: 0, - failedRepairChildCount: 0, - recoveredSpecialistFailureCount: 0, - recoveredOriginalFailureCount: 0, - awaitingRepairCount: 0, - isolatedAppDataUsed: false, - formalConfigCliCallCount: 0, - sourceRunnerEndpointUnchanged: false, - sourceAppDataDirectoryUntouched: false, - sourceConfigReplicaCount: 0, - sourceConfigReplicasVerified: false, - turnReportOutcome: null, - turnReportCaptured: false, - turnReportParentIdentityStable: false, - turnReportNewAssistantMessageCount: null, - turnReportFinalReplyChars: null, - turnReportPrivateLeakCount: null, - turnReportRuntimeCount: 0, - turnReportBusyRuntimeCount: 0, - turnReportPendingTaskCount: 0, - turnReportRunningTaskCount: 0, - turnReportWaitingForConfirmationCount: 0, - turnReportWaitingForUserInputCount: 0, - turnReportReconciliationAgentCount: 0, - taskCount: 0, - terminalTaskCount: 0, - childTaskCount: 0, - runtimeCount: 0, - childRuntimeCount: 0, - uniqueRootRunCount: 0, - recursiveDescendantCount: 0, - completedDescendantCount: 0, - artChildCount: 0, - artDeliveryClaimCount: 0, - assetListBeforeArtDelegation: false, - finalSupervisorAssistantCount: 0, - supervisorUserMessageCount: 0, - professionalAssistantCount: 0, - isolatedAssistantCount: 0, - professionalUserFacingAssistantCount: 0, - completedAuditCount: 0, - finalizationStageCount: 0, - providerRequestIdentityCount: 0, - providerLifecycleStartedCount: 0, - providerLifecycleTerminalCount: 0, - providerLifecycleCompletedCount: 0, - providerLifecycleFailedCount: 0, - providerRetryAuditCount: 0, - providerConnectivityRetryAuditCount: 0, - providerTransportRetryAuditCount: 0, - providerTimeoutRetryAuditCount: 0, - acceptedAutonomousToolPlanCount: 0, - sourceMutationActionMax: 0, - sourcePayloadMaxFieldChars: 0, - sourcePayloadMaxTotalChars: 0, - sourcePayloadPolicyViolationCount: 0, - openProviderLifecycleCount: 0, - duplicateProviderLifecycleCount: 0, - duplicateMessageCount: 0, - duplicateActionLifecycleCount: 0, - duplicateReceiptCount: 0, - runProfileBindingCount: 0, - baselineRevision: 0, - projectRevision: 0, - projectRevisionDelta: 0, - freshInitBaselineUsed: false, - initialGameIndexMatchesProductionDefault: false, - initialGameIndexSha256: null, - finalGameIndexSha256: null, - gameIndexChanged: false, - finalGameIndexDiffersFromBaseline: false, - uniqueFixedChildCodePrototype: false, - gameIndexBytes: 0, - staticSmokePassed: false, - staticSmokeGameIndexSha256Matched: false, - staticSmokeAuditCount: 0, - autonomousCompletionContractCount: 0, - autonomousPlaytestReceiptCount: 0, - playtestScenario: null, - laneDefensePlaytestPassed: false, - laneDefenseAssertionCount: 0, - laneDefensePassedAssertionCount: 0, - browserValidationPassed: false, - desktopPlaytestPassed: false, - mobilePlaytestPassed: false, - desktopScreenshotSha256: null, - desktopScreenshotBytes: 0, - mobileScreenshotSha256: null, - mobileScreenshotBytes: 0, - browserReportSha256: null, - browserReportBytes: 0, - pendingActionCount: 0, - confirmationSidecarCount: 0, - userInputSidecarCount: 0, - providerActionBatchSidecarCount: 0, - providerRetrySidecarCount: 0, - providerHandoffSidecarCount: 0, - toolPlanHandoffSidecarCount: 0, - finalizationJournalCount: 0, - reconciliationResidueCount: 0, - approvalAuditCount: 0, - rejectionAuditCount: 0, - userInputAuditCount: 0, - steerRecordCount: 0, - providerPayloadPublicLeakCount: 0, - privateBodyPublicLeakCount: 0, - apiKeyPublicLeakCount: 0, - projectPathPublicLeakCount: 0, - formalConfigPathPublicLeakCount: 0, - logApiKeyLeakCount: 0, - logPrivateBodyLeakCount: 0, - logAbsolutePathLeakCount: 0, - browserReportApiKeyLeakCount: 0, - browserReportPrivateBodyLeakCount: 0, - browserReportAbsolutePathLeakCount: 0, - supervisorAutonomousPlayableReportLeakCount: 0, - supervisorAutonomousPlayableRunnerStopped: false, - supervisorAutonomousPlayableAppDataCleanupPerformed: false, - ownedProcessIdentityCaptured: false, - ownedRunnerObservedCount: 0, - ownedHelperObservedCount: 0, - ownedNodeDescendantObservedCount: 0, - ownedBrowserDescendantObservedCount: 0, - ownedCommandDescendantObservedCount: 0, - ownedRunnerResidualCount: 0, - ownedHelperResidualCount: 0, - ownedNodeDescendantResidualCount: 0, - ownedBrowserDescendantResidualCount: 0, - ownedCommandDescendantResidualCount: 0, - activeCommandChildrenAfterCleanup: 0, - activeInteractiveCliSessionsAfterCleanup: 0, - ownedProcessCleanupPassed: false, - secretLeakCount: 0, - lureLeakCount: 0, - paths: [], - }; -} - -export function isSupervisorAutonomousPlayableLaneDefenseSuite() { - return state.suite === supervisorAutonomousPlayableLaneDefenseSuite; -} diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm.mjs index 65f2668b1..80f725314 100644 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm.mjs +++ b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm.mjs @@ -1,4 +1,3 @@ -export * from './supervisor-swarm/chat-session.mjs'; export * from './supervisor-swarm/collaboration-assertions.mjs'; export * from './supervisor-swarm/collaboration-policy.mjs'; export * from './supervisor-swarm/evidence-schema.mjs'; diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/chat-session.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/chat-session.mjs deleted file mode 100644 index b968340a5..000000000 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/chat-session.mjs +++ /dev/null @@ -1,108 +0,0 @@ -import { - assert, - codedError, - commandPassedMarker, - findPendingActions, - readRuntime, - safeProcessFailureDiagnostic, - state, - supervisorSwarmConfirmedTools, - supervisorSwarmDesignContent, - supervisorSwarmDesignPath, - supervisorSwarmQualityContent, - supervisorSwarmQualityPath, - supervisorSwarmWeakQualityContent, - visibleText, - waitForInteractiveCliOutput, - writeInteractiveCliLine, -} from './shared.mjs'; - -export function supervisorSwarmVerificationFixtureSource() { - return `import fs from 'node:fs';\nconst html = fs.readFileSync('game/index.html', 'utf8');\nconst agents = fs.readFileSync('AGENTS.md', 'utf8');\nconst optionalExact = (file, expected) => !fs.existsSync(file) || fs.readFileSync(file, 'utf8') === expected;\nconst passed = html.includes(${JSON.stringify(visibleText)}) && html.includes(' { - if (state.confirmedActionIds.has(pending.actionId)) return false; - if (!shouldConfirm(pending)) return false; - assert( - allowed.has(pending.tool), - `pending-tool-not-allowed-in-scenario:${pending.tool}`, - ); - return true; - }); - if (candidates.length === 0) return; - const output = await waitForInteractiveCliOutput( - session, - (value) => - candidates.some((pending) => - value.includes( - `[待确认] agent=${pending.agentId} run=${pending.runId} action=${pending.actionId} tool=${pending.tool}`, - ), - ), - 'supervisor-swarm-chat-confirmation-prompt-timeout', - 120_000, - ); - const pending = candidates.find((candidate) => - output.includes( - `[待确认] agent=${candidate.agentId} run=${candidate.runId} action=${candidate.actionId} tool=${candidate.tool}`, - ), - ); - assert(pending, 'supervisor-swarm-chat-prompted-action-missing'); - const runtime = await readRuntime(pending.agentId); - assert(runtime.runId === pending.runId, 'pending-run-mismatch'); - writeInteractiveCliLine(session, 'approve'); - await waitForInteractiveCliOutput( - session, - (output) => output.includes(`[已批准] ${pending.actionId}`), - 'supervisor-swarm-chat-confirmation-result-timeout', - 120_000, - ); - state.confirmedActionIds.add(pending.actionId); -} diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/collaboration-assertions.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/collaboration-assertions.mjs index 321937131..fae07caa3 100644 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/collaboration-assertions.mjs +++ b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/collaboration-assertions.mjs @@ -1,35 +1,15 @@ -import { - assertSupervisorSwarmIsolatedChildBusinessContract, - supervisorSwarmIsolatedReviewGroupIndex, - supervisorSwarmIsolatedReviewsForGroupIndex, - supervisorSwarmIsolatedWriteScopeRoots, - validateSupervisorSwarmIsolatedSpawnInput, -} from './collaboration-policy.mjs'; import { supervisorSwarmDeliveryIdentity, supervisorSwarmEffectiveAgentPolicy, - supervisorSwarmIsolatedGroupIdentity, - supervisorSwarmIsolatedInstanceIdentity, - supervisorSwarmIsolatedJoinClaimIdentity, - supervisorSwarmIsolatedJoinIdentity, - supervisorSwarmIsolatedResultIdentity, supervisorSwarmParentIsolatedRecords, - supervisorSwarmTaskIdentity, } from './persistence.mjs'; import { assert, codedError, - hashJsonValue, hashValue, isFailedTask, isLiveTask, isNonEmptyString, - isolatedAgentGroupSchemaVersion, - isolatedAgentInstanceSchemaVersion, - isolatedAgentJoinClaimSchemaVersion, - isolatedAgentJoinDeliverySchemaVersion, - isolatedAgentResultSchemaVersion, - isolatedJoinDeliveryTarget, isPlainObject, projectSupervisorAgentId, state, @@ -37,18 +17,12 @@ import { staticDelegateDeliverySchemaVersion, supervisorSwarmDesignAgentId, supervisorSwarmDesignPath, - supervisorSwarmIsolatedReviews, supervisorSwarmProviderRetryAuditGraceSeconds, supervisorSwarmQualityAgentId, supervisorSwarmQualityPath, supervisorSwarmSessionId, supervisorSwarmWeakQualityContent, } from './shared.mjs'; -import { - isSupervisorSwarmMixedHarnessSuite, - isSupervisorSwarmMultiIsolatedHarnessSuite, - supervisorSwarmExpectedIsolatedReviewGroups, -} from './suite-selection.mjs'; export function supervisorSwarmProviderIntervals(agentDb, agentId, runId) { const records = agentDb @@ -113,46 +87,6 @@ export function observeSupervisorSwarmInitialProviderOverlap( return false; } -export function observeSupervisorSwarmStaticIsolatedProviderOverlap( - agentDb, - deliveries, - instances, -) { - if (!isSupervisorSwarmMixedHarnessSuite()) return false; - for (const delivery of deliveries) { - const staticIntervals = supervisorSwarmProviderIntervals( - agentDb, - delivery.targetAgentId, - delivery.targetRunId, - ); - for (const instance of instances) { - const isolatedIntervals = supervisorSwarmProviderIntervals( - agentDb, - instance.instanceId, - instance.runId, - ); - for (const staticInterval of staticIntervals) { - for (const isolatedInterval of isolatedIntervals) { - if ( - supervisorSwarmProviderIntervalsOverlap( - staticInterval, - isolatedInterval, - ) - ) { - state.supervisorSwarm.staticIsolatedProviderRequestIds = [ - staticInterval.started.record.requestId, - isolatedInterval.started.record.requestId, - ]; - state.supervisorSwarm.staticIsolatedProviderOverlapObserved = true; - return true; - } - } - } - } - } - return false; -} - export function supervisorSwarmParentDeliveries(deliveries) { return deliveries.filter( (delivery) => @@ -526,546 +460,6 @@ export function supervisorSwarmArtifactHash(delivery, expectedPath) { return artifact?.sha256 ?? null; } -export function supervisorSwarmMixedIsolatedGroupEntries( - groups, - expectedReviewGroups = supervisorSwarmExpectedIsolatedReviewGroups(), -) { - assert( - Array.isArray(groups) && groups.length === expectedReviewGroups.length, - 'supervisor-swarm-mixed-group-count-invalid', - ); - const entries = groups.map((group) => ({ - group, - groupIndex: supervisorSwarmIsolatedReviewGroupIndex( - group?.request?.children, - expectedReviewGroups, - ), - })); - assert( - entries.every(({ groupIndex }) => groupIndex >= 0) && - new Set(entries.map(({ groupIndex }) => groupIndex)).size === - expectedReviewGroups.length, - 'supervisor-swarm-mixed-group-review-partition-invalid', - ); - return entries.sort((left, right) => left.groupIndex - right.groupIndex); -} - -export function supervisorSwarmMixedSpawnRequestHashesStable(groups) { - const expectedReviewGroups = supervisorSwarmExpectedIsolatedReviewGroups(); - if (!Array.isArray(groups) || groups.length !== expectedReviewGroups.length) { - return false; - } - const entries = groups - .map((group) => ({ - group, - groupIndex: supervisorSwarmIsolatedReviewGroupIndex( - group?.request?.children, - expectedReviewGroups, - ), - })) - .sort((left, right) => left.groupIndex - right.groupIndex); - return ( - entries.every(({ groupIndex }) => groupIndex >= 0) && - new Set(entries.map(({ groupIndex }) => groupIndex)).size === - expectedReviewGroups.length && - hashJsonValue(entries[0].group.request) === - state.supervisorSwarm.mixedSpawnRequestHash && - (!isSupervisorSwarmMultiIsolatedHarnessSuite() || - hashJsonValue(entries[1].group.request) === - state.supervisorSwarm.mixedFollowupSpawnRequestHash) - ); -} - -export function supervisorSwarmReadyIsolatedGroupIds(detail) { - assert( - isNonEmptyString(detail), - 'supervisor-swarm-mixed-ready-join-detail-missing', - ); - const payloadBlocks = detail - .split('\n\n') - .filter((block) => block.startsWith('readyIsolatedJoins: ')); - assert( - payloadBlocks.length === 1, - 'supervisor-swarm-mixed-ready-join-prefix-count-invalid', - ); - let payload; - try { - payload = JSON.parse(payloadBlocks[0].slice('readyIsolatedJoins: '.length)); - } catch (error) { - throw codedError('supervisor-swarm-mixed-ready-join-json-invalid', error); - } - const groupIds = payload?.joins?.map((join) => join?.delegationGroupId); - assert( - payload?.ready === true && - Array.isArray(groupIds) && - groupIds.length > 0 && - groupIds.every(isNonEmptyString) && - new Set(groupIds).size === groupIds.length, - 'supervisor-swarm-mixed-ready-join-payload-invalid', - ); - return [...groupIds].sort(); -} - -export function supervisorSwarmObservedJoinClaimForGroups( - claims, - expectedGroupIds, -) { - assert( - Array.isArray(claims) && - claims.length === 1 && - Array.isArray(expectedGroupIds) && - expectedGroupIds.length > 0, - 'supervisor-swarm-mixed-observed-join-claim-count-invalid', - ); - const claim = claims[0]; - const claimedGroupIds = (claim?.joins ?? []) - .map((join) => join?.delegationGroupId) - .filter(isNonEmptyString) - .sort(); - assert( - claim?.schemaVersion === isolatedAgentJoinClaimSchemaVersion && - claim.status === 'observed' && - isNonEmptyString(claim.actionId) && - Array.isArray(claim.joins) && - claimedGroupIds.length === claim.joins.length && - new Set(claimedGroupIds).size === claimedGroupIds.length && - JSON.stringify(claimedGroupIds) === JSON.stringify(expectedGroupIds), - 'supervisor-swarm-mixed-observed-join-claim-invalid', - ); - return claim; -} - -export function supervisorSwarmFollowupBeforeClaimOrderValid( - initialParentWakeIndex, - followupSpawnIndex, - claimAuditIndexes, -) { - return ( - Number.isSafeInteger(initialParentWakeIndex) && - Number.isSafeInteger(followupSpawnIndex) && - Array.isArray(claimAuditIndexes) && - claimAuditIndexes.length > 0 && - claimAuditIndexes.every(Number.isSafeInteger) && - initialParentWakeIndex >= 0 && - initialParentWakeIndex < followupSpawnIndex && - followupSpawnIndex < Math.min(...claimAuditIndexes) - ); -} - -export function supervisorSwarmMixedIsolatedClaimsReady(persistence) { - const records = supervisorSwarmParentIsolatedRecords(persistence); - const expectedGroupCount = - supervisorSwarmExpectedIsolatedReviewGroups().length; - return ( - records.groups.length === expectedGroupCount && - records.instances.length === supervisorSwarmIsolatedReviews.length && - records.results.length === supervisorSwarmIsolatedReviews.length && - records.results.every((record) => record.result?.status === 'completed') && - records.joinDeliveries.length === expectedGroupCount && - records.joinDeliveries.every( - (delivery) => delivery.status === 'claimed-by-parent', - ) - ); -} - -export function validateSupervisorSwarmMixedIsolatedPersistence(persistence) { - assert( - isSupervisorSwarmMixedHarnessSuite(), - 'supervisor-swarm-mixed-validation-outside-suite', - ); - const { groups, instances, results, joinDeliveries } = - supervisorSwarmParentIsolatedRecords(persistence); - const groupEntries = supervisorSwarmMixedIsolatedGroupEntries(groups); - assert( - persistence.isolatedGroups.length === groupEntries.length && - isNonEmptyString(state.supervisorSwarm.mixedSpawnActionId) && - (!isSupervisorSwarmMultiIsolatedHarnessSuite() || - (isNonEmptyString(state.supervisorSwarm.mixedFollowupSpawnActionId) && - state.supervisorSwarm.mixedSpawnActionId !== - state.supervisorSwarm.mixedFollowupSpawnActionId)), - 'supervisor-swarm-mixed-group-invalid', - ); - for (const { group, groupIndex } of groupEntries) { - const expectedReviewGroups = supervisorSwarmExpectedIsolatedReviewGroups(); - const reviews = supervisorSwarmIsolatedReviewsForGroupIndex( - groupIndex, - expectedReviewGroups, - ); - const expectedActionId = - groupIndex === 0 - ? state.supervisorSwarm.mixedSpawnActionId - : state.supervisorSwarm.mixedFollowupSpawnActionId; - const expectedRequestHash = - groupIndex === 0 - ? state.supervisorSwarm.mixedSpawnRequestHash - : state.supervisorSwarm.mixedFollowupSpawnRequestHash; - assert( - group.schemaVersion === isolatedAgentGroupSchemaVersion && - group.parentAgentId === projectSupervisorAgentId && - group.parentSessionId === supervisorSwarmSessionId && - group.parentRunId === state.initialRunId && - group.parentActionId === expectedActionId && - group.joinMode === 'all' && - group.depth === 1 && - Array.isArray(group.instanceIds) && - group.instanceIds.length === reviews.length && - hashJsonValue(group.request) === expectedRequestHash, - 'supervisor-swarm-mixed-group-contract-invalid', - ); - validateSupervisorSwarmIsolatedSpawnInput( - group.request, - groupIndex, - 'supervisor-swarm-mixed-persisted', - expectedReviewGroups, - ); - } - supervisorSwarmIsolatedWriteScopeRoots( - groupEntries.flatMap(({ group }) => group.request.children), - ); - const groupById = new Map( - groupEntries.map((entry) => [entry.group.delegationGroupId, entry]), - ); - const expectedByPath = new Map( - supervisorSwarmIsolatedReviews.map((review) => [review.path, review]), - ); - assert( - instances.length === supervisorSwarmIsolatedReviews.length && - persistence.isolatedInstances.length === instances.length && - new Set(instances.map((instance) => instance.instanceId)).size === - instances.length && - new Set(instances.map((instance) => instance.delegationId)).size === - instances.length && - new Set(instances.map((instance) => instance.sessionId)).size === - instances.length && - new Set(instances.map((instance) => instance.runId)).size === - instances.length, - 'supervisor-swarm-mixed-instance-cardinality-invalid', - ); - for (const { group, groupIndex } of groupEntries) { - const groupInstances = instances.filter( - (instance) => instance.delegationGroupId === group.delegationGroupId, - ); - const expectedIndexes = supervisorSwarmIsolatedReviewsForGroupIndex( - groupIndex, - supervisorSwarmExpectedIsolatedReviewGroups(), - ).map((_, index) => index); - assert( - groupInstances.length === expectedIndexes.length && - JSON.stringify( - groupInstances - .map((instance) => instance.childIndex) - .sort((left, right) => left - right), - ) === JSON.stringify(expectedIndexes) && - JSON.stringify( - groupInstances.map((instance) => instance.instanceId).sort(), - ) === JSON.stringify([...group.instanceIds].sort()), - 'supervisor-swarm-mixed-group-instance-set-invalid', - ); - } - for (const instance of instances) { - const groupEntry = groupById.get(instance.delegationGroupId); - const group = groupEntry?.group; - const expectedPath = instance.expectedArtifacts?.[0]; - const review = expectedByPath.get(expectedPath); - const requestChild = group?.request?.children?.[instance.childIndex]; - assert( - groupEntry && - review && - instance.schemaVersion === isolatedAgentInstanceSchemaVersion && - instance.parentAgentId === projectSupervisorAgentId && - instance.parentSessionId === supervisorSwarmSessionId && - instance.parentRunId === state.initialRunId && - instance.parentActionId === group.parentActionId && - instance.delegationGroupId === group.delegationGroupId && - group.instanceIds.includes(instance.instanceId) && - isNonEmptyString(instance.templateAgentId) && - isNonEmptyString(instance.task) && - instance.depth === 1 && - JSON.stringify(instance.acceptanceCriteria) === - JSON.stringify(requestChild?.acceptanceCriteria) && - JSON.stringify(instance.expectedArtifacts) === - JSON.stringify([review.path]) && - JSON.stringify(requestChild?.expectedArtifacts) === - JSON.stringify(instance.expectedArtifacts) && - JSON.stringify(requestChild?.writeScopes) === - JSON.stringify(instance.writeScopes) && - requestChild?.templateAgentId === instance.templateAgentId && - requestChild?.task === instance.task, - 'supervisor-swarm-mixed-instance-contract-invalid', - ); - assertSupervisorSwarmIsolatedChildBusinessContract(requestChild, review); - } - assert( - results.length === instances.length && - persistence.isolatedResults.length === results.length && - new Set(results.map((record) => record.result?.instanceId)).size === - results.length, - 'supervisor-swarm-mixed-result-cardinality-invalid', - ); - for (const record of results) { - const instance = instances.find( - (candidate) => candidate.instanceId === record.result?.instanceId, - ); - const groupEntry = groupById.get(record.delegationGroupId); - const review = expectedByPath.get(instance?.expectedArtifacts?.[0]); - const artifact = record.result?.artifacts?.find( - (candidate) => candidate.path === review?.path, - ); - assert( - instance && - groupEntry && - review && - record.schemaVersion === isolatedAgentResultSchemaVersion && - record.delegationGroupId === instance.delegationGroupId && - record.childIndex === instance.childIndex && - record.result.delegationId === instance.delegationId && - record.result.templateAgentId === instance.templateAgentId && - record.result.runId === instance.runId && - record.result.status === 'completed' && - isNonEmptyString(record.result.summary) && - Array.isArray(record.result.artifacts) && - JSON.stringify(record.result.artifacts) === - JSON.stringify([ - { path: review.path, sha256: hashValue(review.content) }, - ]) && - artifact?.sha256 === hashValue(review.content) && - Array.isArray(record.result.evidence) && - (record.result.verifiedRevision == null || - (Number.isSafeInteger(record.result.verifiedRevision) && - record.result.verifiedRevision >= 0)) && - record.result.error == null, - 'supervisor-swarm-mixed-result-invalid', - ); - } - assert( - joinDeliveries.length === groupEntries.length && - persistence.isolatedJoinDeliveries.length === groupEntries.length, - 'supervisor-swarm-mixed-join-delivery-invalid', - ); - const joinDeliveryByGroupId = new Map( - joinDeliveries.map((delivery) => [delivery.delegationGroupId, delivery]), - ); - for (const { group } of groupEntries) { - const delivery = joinDeliveryByGroupId.get(group.delegationGroupId); - assert( - delivery?.schemaVersion === isolatedAgentJoinDeliverySchemaVersion && - delivery.parentAgentId === projectSupervisorAgentId && - delivery.parentRunId === state.initialRunId && - delivery.joinRunId === group.joinRunId && - delivery.status === 'claimed-by-parent' && - isolatedJoinDeliveryTarget(delivery) === 'parent-wake' && - delivery.queuedRunId == null && - isNonEmptyString(delivery.claimedByActionId), - 'supervisor-swarm-mixed-group-join-delivery-invalid', - ); - } - const isolatedTasks = persistence.taskSnapshot.latest.filter((task) => - instances.some( - (instance) => - task.agentId === instance.instanceId && - task.sessionId === instance.sessionId && - task.runId === instance.runId && - task.delegationId === instance.delegationId, - ), - ); - const joinRunIds = new Set(groupEntries.map(({ group }) => group.joinRunId)); - const continuationTasks = persistence.taskSnapshot.all.filter( - (task) => - task.source === 'agent-isolated-join' && joinRunIds.has(task.runId), - ); - assert( - isolatedTasks.length === instances.length && - isolatedTasks.every( - (task) => - task.source === 'agent-isolated-child' && - task.parentAgentId === projectSupervisorAgentId && - task.parentRunId === state.initialRunId && - task.status === 'completed' && - task.phase === 'completed', - ) && - continuationTasks.length === 0, - 'supervisor-swarm-mixed-child-or-continuation-task-invalid', - ); - const spawnAudits = persistence.agentDb.filter( - (record) => - record.recordType === 'agent.runtime.agent.spawn_isolated' && - record.agentId === projectSupervisorAgentId && - record.sessionId === supervisorSwarmSessionId && - record.runId === state.initialRunId && - groupById.has(record.delegationGroupId), - ); - const parentWakeAudits = persistence.agentDb.filter( - (record) => - record.recordType === - 'agent.runtime.agent.isolated_join.parent_wake.dispatched' && - record.agentId === projectSupervisorAgentId && - record.sessionId === supervisorSwarmSessionId && - record.parentRunId === state.initialRunId && - groupById.has(record.delegationGroupId), - ); - const continuationAudits = persistence.agentDb.filter( - (record) => - record.recordType === 'agent.runtime.agent.isolated_join.dispatched' && - record.parentRunId === state.initialRunId && - groupById.has(record.delegationGroupId), - ); - const claimAudits = persistence.agentDb - .map((record, index) => ({ record, index })) - .filter( - ({ record }) => - record.recordType === - 'agent.runtime.agent.isolated_join.claimed_by_parent' && - record.agentId === projectSupervisorAgentId && - record.runId === state.initialRunId && - groupById.has(record.delegationGroupId), - ); - const claimObservations = persistence.agentDb - .map((record, index) => ({ record, index })) - .filter( - ({ record }) => - record.recordType === 'agent.runtime.tool_observation' && - record.agentId === projectSupervisorAgentId && - record.runId === state.initialRunId && - record.tool === 'agent.run_status' && - record.status === 'ok' && - String(record.summary ?? '').includes('ready all-join'), - ); - const expectedGroupIds = [...groupById.keys()].sort(); - const joinClaims = (persistence.isolatedJoinClaims ?? []).filter( - (claim) => - claim.parentAgentId === projectSupervisorAgentId && - claim.parentRunId === state.initialRunId, - ); - const joinClaim = supervisorSwarmObservedJoinClaimForGroups( - joinClaims, - expectedGroupIds, - ); - assert( - spawnAudits.length === groupEntries.length && - parentWakeAudits.length === groupEntries.length && - continuationAudits.length === 0 && - claimAudits.length === groupEntries.length && - claimObservations.length === 1 && - persistence.isolatedJoinClaims.length === 1 && - joinClaim.actionId === claimObservations[0].record.actionId && - joinClaim.joins.length === groupEntries.length && - String(claimObservations[0].record.summary ?? '').includes( - `${groupEntries.length} 个 ready all-join`, - ) && - JSON.stringify( - spawnAudits.map((record) => record.delegationGroupId).sort(), - ) === JSON.stringify(expectedGroupIds) && - JSON.stringify( - parentWakeAudits.map((record) => record.delegationGroupId).sort(), - ) === JSON.stringify(expectedGroupIds) && - JSON.stringify( - claimAudits.map(({ record }) => record.delegationGroupId).sort(), - ) === JSON.stringify(expectedGroupIds) && - claimAudits.every( - ({ record, index }) => - record.actionId === claimObservations[0].record.actionId && - joinDeliveryByGroupId.get(record.delegationGroupId) - ?.claimedByActionId === record.actionId && - index < claimObservations[0].index, - ), - 'supervisor-swarm-mixed-spawn-or-claim-audit-invalid', - ); - if (isSupervisorSwarmMultiIsolatedHarnessSuite()) { - const initialGroupId = groupEntries[0].group.delegationGroupId; - const followupGroupId = groupEntries[1].group.delegationGroupId; - const initialParentWakeIndex = persistence.agentDb.findIndex( - (record) => - record.recordType === - 'agent.runtime.agent.isolated_join.parent_wake.dispatched' && - record.delegationGroupId === initialGroupId, - ); - const followupSpawnIndex = persistence.agentDb.findIndex( - (record) => - record.recordType === 'agent.runtime.agent.spawn_isolated' && - record.delegationGroupId === followupGroupId, - ); - assert( - supervisorSwarmFollowupBeforeClaimOrderValid( - initialParentWakeIndex, - followupSpawnIndex, - claimAudits.map(({ index }) => index), - ), - 'supervisor-swarm-mixed-followup-before-claim-order-invalid', - ); - } - for (const { group } of groupEntries) { - assert( - spawnAudits.filter( - (record) => - record.actionId === group.parentActionId && - record.delegationGroupId === group.delegationGroupId && - record.joinRunId === group.joinRunId && - Array.isArray(record.children) && - record.children.length === group.instanceIds.length, - ).length === 1 && - parentWakeAudits.filter( - (record) => - record.parentActionId === group.parentActionId && - record.delegationGroupId === group.delegationGroupId && - record.joinRunId === group.joinRunId, - ).length === 1 && - claimAudits.filter( - ({ record }) => - record.parentActionId === group.parentActionId && - record.delegationGroupId === group.delegationGroupId && - record.joinRunId === group.joinRunId, - ).length === 1 && - joinClaim.joins.filter( - (join) => - join.parentActionId === group.parentActionId && - join.delegationGroupId === group.delegationGroupId && - join.joinRunId === group.joinRunId && - join.parentAgentId === projectSupervisorAgentId && - join.parentSessionId === supervisorSwarmSessionId && - join.parentRunId === state.initialRunId && - join.source === 'agent-isolated-join', - ).length === 1, - 'supervisor-swarm-mixed-group-audit-identity-invalid', - ); - } - return { - groups: groupEntries.map(({ group }) => group), - instances, - results, - joinDeliveries: groupEntries.map(({ group }) => - joinDeliveryByGroupId.get(group.delegationGroupId), - ), - joinClaims, - isolatedTasks, - continuationTasks, - continuationAudits, - claimAuditIndex: Math.max(...claimAudits.map(({ index }) => index)), - claimObservationIndex: claimObservations[0].index, - identity: { - groups: groupEntries - .map(({ group }) => supervisorSwarmIsolatedGroupIdentity(group)) - .sort((left, right) => - left.delegationGroupId.localeCompare(right.delegationGroupId), - ), - instances: instances - .map(supervisorSwarmIsolatedInstanceIdentity) - .sort((left, right) => left.instanceId.localeCompare(right.instanceId)), - results: results - .map(supervisorSwarmIsolatedResultIdentity) - .sort((left, right) => left.instanceId.localeCompare(right.instanceId)), - joinDeliveries: joinDeliveries - .map(supervisorSwarmIsolatedJoinIdentity) - .sort((left, right) => - left.delegationGroupId.localeCompare(right.delegationGroupId), - ), - joinClaims: joinClaims.map(supervisorSwarmIsolatedJoinClaimIdentity), - tasks: isolatedTasks - .map(supervisorSwarmTaskIdentity) - .sort((left, right) => left.agentId.localeCompare(right.agentId)), - }, - }; -} - export function assertSupervisorSwarmWeakQualityDelivery(delivery) { assert( delivery.status === 'claimed-by-parent' && diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/collaboration-policy.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/collaboration-policy.mjs index 4a39435e1..da6f06b93 100644 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/collaboration-policy.mjs +++ b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/collaboration-policy.mjs @@ -1,45 +1,23 @@ import { assertSupervisorSwarmProviderFailureRecoverable } from './collaboration-assertions.mjs'; -import { - readSupervisorSwarmPersistence, - supervisorSwarmParentIsolatedRecords, -} from './persistence.mjs'; -import { - confirmSupervisorSwarmPendingActions, - readSupervisorSwarmExecutionOwner, - supervisorSwarmProviderStartedIdentities, -} from './repair-recovery.mjs'; -import { readSupervisorSwarmProjectRevision } from './runtime-setup.mjs'; -import { - expectedSupervisorSwarmCollaborationPolicy, - writeSupervisorSwarmCollaborationPolicyDriftFixture, -} from './setup.mjs'; +import { expectedSupervisorSwarmCollaborationPolicy } from './setup.mjs'; import { assert, canonicalJsonValue, - claimOwnedRunner, codedError, - configFileName, duplicateCount, fs, - gitSensitivePath, hasExactKeys, hashJsonValue, hashValue, isFailedTask, isNonEmptyString, isPlainObject, - killRunnerOnce, - listFiles, - localConfigFileName, path, projectSupervisorAgentId, providerActionBatchSchemaVersion, readJson, readOptionalJsonl, readTaskSnapshot, - runCli, - runProcess, - sentinelFileName, sleep, state, supervisorCollaborationContractSchemaVersion, @@ -49,23 +27,10 @@ import { supervisorCollaborationPolicySnapshotSchemaVersion, supervisorSwarmDesignAgentId, supervisorSwarmDesignPath, - supervisorSwarmIsolatedReviewGroups, - supervisorSwarmProjectMutationTools, supervisorSwarmQualityAgentId, supervisorSwarmQualityPath, supervisorSwarmSessionId, - verifyOwnedRunnerForKill, - waitForRunnerBootChange, } from './shared.mjs'; -import { - isSupervisorSwarmAutonomousChatSuite, - isSupervisorSwarmCollaborationPolicyMixedRecoverySuite, - isSupervisorSwarmInteractiveChatSuite, - isSupervisorSwarmMixedHarnessSuite, - isSupervisorSwarmMultiIsolatedHarnessSuite, - supervisorSwarmExpectedIsolatedReviewGroups, - supervisorSwarmInitialIsolatedReviewsForSuite, -} from './suite-selection.mjs'; export function assertSupervisorSwarmContractShape( input, @@ -101,164 +66,6 @@ export function assertSupervisorSwarmContractShape( ); } -export function supervisorSwarmIsolatedWriteScopeRoots(children) { - const roots = children.map((child) => { - assert( - Array.isArray(child.writeScopes) && - child.writeScopes.length === 1 && - isNonEmptyString(child.writeScopes[0]) && - child.writeScopes[0].endsWith('/**'), - 'supervisor-swarm-mixed-child-write-scope-invalid', - ); - const root = child.writeScopes[0].slice(0, -3); - assert( - isNonEmptyString(root) && - !path.posix.isAbsolute(root) && - path.posix.normalize(root) === root && - !root.split('/').includes('..') && - !['.agent', '.git'].some( - (privateRoot) => - root === privateRoot || root.startsWith(`${privateRoot}/`), - ), - 'supervisor-swarm-mixed-child-write-scope-unsafe', - ); - return root; - }); - for (let leftIndex = 0; leftIndex < roots.length; leftIndex += 1) { - for ( - let rightIndex = leftIndex + 1; - rightIndex < roots.length; - rightIndex += 1 - ) { - const left = roots[leftIndex]; - const right = roots[rightIndex]; - assert( - left !== right && - !left.startsWith(`${right}/`) && - !right.startsWith(`${left}/`), - 'supervisor-swarm-mixed-child-write-scopes-overlap', - ); - } - } - return roots; -} - -export function supervisorSwarmIsolatedReviewGroupIndex( - children, - reviewGroups = supervisorSwarmIsolatedReviewGroups, -) { - if (!Array.isArray(children) || children.length === 0) return -1; - const actualPaths = children - .map((child) => child?.expectedArtifacts?.[0]) - .filter(isNonEmptyString) - .sort(); - if (actualPaths.length !== children.length) return -1; - return reviewGroups.findIndex( - (reviews) => - JSON.stringify(actualPaths) === - JSON.stringify(reviews.map((review) => review.path).sort()), - ); -} - -export function supervisorSwarmIsolatedReviewsForGroupIndex( - groupIndex, - reviewGroups = supervisorSwarmIsolatedReviewGroups, -) { - const reviews = reviewGroups[groupIndex]; - assert( - Array.isArray(reviews) && reviews.length > 0, - 'supervisor-swarm-mixed-review-group-index-invalid', - ); - return reviews; -} - -export function assertSupervisorSwarmIsolatedChildBusinessContract( - child, - review, -) { - const contract = [child.task, ...(child.acceptanceCriteria ?? [])] - .join('\n') - .toLowerCase(); - assert( - review.boundaryTerms.some((term) => contract.includes(term)) && - /read[- ]?only|audit[- ]?only|do not (?:change|modify|write)|leave [^\n]* unchanged|只读|不得修改|不要修改/iu.test( - contract, - ), - 'supervisor-swarm-mixed-child-business-boundary-invalid', - ); -} - -export function validateSupervisorSwarmIsolatedSpawnInput( - input, - groupIndex, - codePrefix, - reviewGroups = supervisorSwarmIsolatedReviewGroups, -) { - const reviews = supervisorSwarmIsolatedReviewsForGroupIndex( - groupIndex, - reviewGroups, - ); - assert( - isPlainObject(input) && - JSON.stringify(Object.keys(input).sort()) === - JSON.stringify(['children', 'joinMode'].sort()) && - input.joinMode === 'all' && - Array.isArray(input.children) && - input.children.length === reviews.length && - supervisorSwarmIsolatedReviewGroupIndex(input.children, reviewGroups) === - groupIndex, - `${codePrefix}-spawn-action-invalid`, - ); - const expectedPaths = new Set(reviews.map((review) => review.path)); - for (const child of input.children) { - const review = reviews.find( - (candidate) => candidate.path === child?.expectedArtifacts?.[0], - ); - assert( - isPlainObject(child) && - JSON.stringify(Object.keys(child).sort()) === - JSON.stringify( - [ - 'templateAgentId', - 'task', - 'acceptanceCriteria', - 'expectedArtifacts', - 'writeScopes', - ].sort(), - ), - `${codePrefix}-child-shape-invalid`, - ); - assert( - isNonEmptyString(child.templateAgentId), - `${codePrefix}-child-template-invalid`, - ); - assert( - isNonEmptyString(child.task) && - Array.isArray(child.acceptanceCriteria) && - child.acceptanceCriteria.length > 0 && - child.acceptanceCriteria.every(isNonEmptyString), - `${codePrefix}-child-task-contract-invalid`, - ); - assert( - Array.isArray(child.expectedArtifacts) && - child.expectedArtifacts.length === 1 && - review != null && - expectedPaths.delete(child.expectedArtifacts[0]), - `${codePrefix}-child-artifact-contract-invalid`, - ); - assert( - Array.isArray(child.writeScopes) && - child.writeScopes.length === 1 && - child.writeScopes[0] === review.scope, - `${codePrefix}-child-write-scope-count-invalid`, - ); - assertSupervisorSwarmIsolatedChildBusinessContract(child, review); - } - supervisorSwarmIsolatedWriteScopeRoots(input.children); - assert(expectedPaths.size === 0, `${codePrefix}-child-boundaries-incomplete`); - return hashJsonValue(input); -} - export function supervisorSwarmCollaborationPolicySnapshotIdentity(snapshot) { if (!isPlainObject(snapshot)) return null; return { @@ -467,7 +274,7 @@ export function supervisorSwarmCollaborationPolicyDriftObservationCount( ).length; } -export function validateSupervisorSwarmCollaborationContract(contract, mixed) { +export function validateSupervisorSwarmCollaborationContract(contract) { const expectedPolicy = expectedSupervisorSwarmCollaborationPolicy(); const expectedStaticAgentIds = [ supervisorSwarmDesignAgentId, @@ -494,9 +301,8 @@ export function validateSupervisorSwarmCollaborationContract(contract, mixed) { JSON.stringify(contract.initialStaticAgentIds) === JSON.stringify(expectedStaticAgentIds) && contract.repairDelegateCount === 0 && - contract.isolatedSpawnCount === (mixed ? 1 : 0) && - contract.isolatedChildCount === - (mixed ? supervisorSwarmInitialIsolatedReviewsForSuite().length : 0) && + contract.isolatedSpawnCount === 0 && + contract.isolatedChildCount === 0 && /^[0-9a-f]{64}$/u.test(contract.policyFingerprint ?? '') && /^[0-9a-f]{64}$/u.test(contract.contractFingerprint ?? ''), 'supervisor-swarm-initial-collaboration-contract-invalid', @@ -563,8 +369,7 @@ export function supervisorSwarmInitialProviderBatchRecoveryIdentity(batch) { } export function validateSupervisorSwarmInitialProviderBatch(batch) { - const mixed = isSupervisorSwarmMixedHarnessSuite(); - const expectedActionCount = mixed ? 3 : 2; + const expectedActionCount = 2; assert( batch?.schemaVersion === providerActionBatchSchemaVersion && isNonEmptyString(batch.batchId) && @@ -587,22 +392,12 @@ export function validateSupervisorSwarmInitialProviderBatch(batch) { ); const collaborationContract = validateSupervisorSwarmCollaborationContract( batch.collaborationContract, - mixed, ); - if (mixed) { - assert( - batch.status === 'waiting-confirmation' && batch.nextActionIndex === 0, - 'supervisor-swarm-mixed-initial-confirmation-gate-missing', - ); - } const agentIds = new Set(); const actionIds = new Set(); const delegateActionIds = new Set(); - let spawnActionId = null; for (const [index, pending] of batch.actions.entries()) { const action = pending.action; - const confirmationExpected = - mixed && action?.tool === 'agent.spawn_isolated'; assert( pending.agentId === projectSupervisorAgentId && pending.sessionId === supervisorSwarmSessionId && @@ -611,74 +406,39 @@ export function validateSupervisorSwarmInitialProviderBatch(batch) { isNonEmptyString(pending.actionId) && /^[0-9a-f]{64}$/u.test(pending.actionFingerprint ?? '') && !actionIds.has(pending.actionId) && - pending.executionMode === - (confirmationExpected ? 'confirmation' : 'auto') && - (confirmationExpected - ? [ - 'pending-confirmation', - 'approved', - 'executing', - 'observed-approved', - ].includes(pending.status) - : ['approved', 'executing', 'observed-approved'].includes( - pending.status, - )) && + pending.executionMode === 'auto' && + ['approved', 'executing', 'observed-approved'].includes( + pending.status, + ) && + action?.tool === 'agent.delegate' && action?.tool === batch.plan.actions[index]?.tool && JSON.stringify(action.input) === JSON.stringify(batch.plan.actions[index].input), 'supervisor-swarm-initial-provider-batch-action-invalid', ); - if (action.tool === 'agent.delegate') { - const expectedPath = - action.input?.agentId === supervisorSwarmDesignAgentId - ? supervisorSwarmDesignPath - : supervisorSwarmQualityPath; - assertSupervisorSwarmContractShape( - action.input, - expectedPath, - 'supervisor-swarm-initial-provider-batch', - ); - assert( - [supervisorSwarmDesignAgentId, supervisorSwarmQualityAgentId].includes( - action.input.agentId, - ), - 'supervisor-swarm-initial-provider-batch-target-invalid', - ); - agentIds.add(action.input.agentId); - delegateActionIds.add(pending.actionId); - } else { - assert( - mixed && - action.tool === 'agent.spawn_isolated' && - spawnActionId == null, - 'supervisor-swarm-mixed-spawn-action-invalid', - ); - spawnActionId = pending.actionId; - state.supervisorSwarm.mixedSpawnRequestHash = - validateSupervisorSwarmIsolatedSpawnInput( - action.input, - 0, - 'supervisor-swarm-mixed-initial', - supervisorSwarmExpectedIsolatedReviewGroups(), - ); - } + const expectedPath = + action.input?.agentId === supervisorSwarmDesignAgentId + ? supervisorSwarmDesignPath + : supervisorSwarmQualityPath; + assertSupervisorSwarmContractShape( + action.input, + expectedPath, + 'supervisor-swarm-initial-provider-batch', + ); + assert( + [supervisorSwarmDesignAgentId, supervisorSwarmQualityAgentId].includes( + action.input.agentId, + ), + 'supervisor-swarm-initial-provider-batch-target-invalid', + ); + agentIds.add(action.input.agentId); + delegateActionIds.add(pending.actionId); actionIds.add(pending.actionId); } assert( - agentIds.size === 2 && (!mixed || isNonEmptyString(spawnActionId)), + agentIds.size === 2, 'supervisor-swarm-initial-provider-batch-target-count-invalid', ); - assert( - !mixed || - batch.actions.filter( - (pending) => - pending.actionId === spawnActionId && - pending.executionMode === 'confirmation' && - pending.status === 'pending-confirmation', - ).length === 1, - 'supervisor-swarm-initial-provider-batch-waiting-state-invalid', - ); - state.supervisorSwarm.mixedSpawnActionId = spawnActionId; state.supervisorSwarm.initialProviderBatch = { batchId: batch.batchId, projectId: batch.projectId, @@ -829,88 +589,6 @@ export async function readSupervisorSwarmCollaborationPolicySnapshotBindingFile( return { bindingPath, bytes, binding }; } -export async function captureSupervisorSwarmCollaborationPolicySnapshotAndDrift() { - if (!isSupervisorSwarmMultiIsolatedHarnessSuite()) return; - const expectedSnapshotPath = supervisorSwarmCollaborationPolicySnapshotPath(); - const expectedBindingPath = - supervisorSwarmCollaborationPolicySnapshotBindingPath(); - const deadline = Date.now() + 30_000; - while (Date.now() < deadline) { - const [snapshotFiles, bindingFiles] = await Promise.all([ - listFiles(supervisorSwarmCollaborationPolicySnapshotDirectory()), - listFiles(supervisorSwarmCollaborationPolicySnapshotBindingDirectory()), - ]); - if ( - !snapshotFiles.includes(expectedSnapshotPath) || - !bindingFiles.includes(expectedBindingPath) - ) { - await sleep(50); - continue; - } - assert( - snapshotFiles.length === 1 && - snapshotFiles[0] === expectedSnapshotPath && - bindingFiles.length === 1 && - bindingFiles[0] === expectedBindingPath, - 'supervisor-swarm-collaboration-policy-snapshot-surface-not-unique', - ); - const [initial, initialBinding] = await Promise.all([ - readSupervisorSwarmCollaborationPolicySnapshotFile(), - readSupervisorSwarmCollaborationPolicySnapshotBindingFile(), - ]); - const inspected = validateSupervisorSwarmCollaborationPolicySnapshot( - initial.snapshot, - supervisorSwarmExpectedCollaborationPolicySnapshot(), - ); - validateSupervisorSwarmCollaborationPolicySnapshotBinding( - initialBinding.binding, - initial.snapshot, - ); - state.supervisorSwarm.collaborationPolicySnapshotInitialRecord = JSON.parse( - JSON.stringify(initial.snapshot), - ); - state.supervisorSwarm.collaborationPolicySnapshotInitialBytes = - initial.bytes; - state.supervisorSwarm.collaborationPolicySnapshotInitialBytesSha256 = - hashValue(initial.bytes); - state.supervisorSwarm.collaborationPolicySnapshotInitialIdentityHash = - inspected.identityHash; - state.supervisorSwarm.collaborationPolicySnapshotBindingInitialRecord = - JSON.parse(JSON.stringify(initialBinding.binding)); - state.supervisorSwarm.collaborationPolicySnapshotBindingInitialBytes = - initialBinding.bytes; - state.supervisorSwarm.collaborationPolicySnapshotBindingInitialBytesSha256 = - hashValue(initialBinding.bytes); - - await writeSupervisorSwarmCollaborationPolicyDriftFixture(); - const [afterDrift, bindingAfterDrift] = await Promise.all([ - readSupervisorSwarmCollaborationPolicySnapshotFile(), - readSupervisorSwarmCollaborationPolicySnapshotBindingFile(), - ]); - validateSupervisorSwarmCollaborationPolicySnapshot( - afterDrift.snapshot, - supervisorSwarmExpectedCollaborationPolicySnapshot(), - ); - validateSupervisorSwarmCollaborationPolicySnapshotBinding( - bindingAfterDrift.binding, - afterDrift.snapshot, - ); - assert( - afterDrift.bytes === initial.bytes && - JSON.stringify(afterDrift.snapshot) === - JSON.stringify(initial.snapshot) && - bindingAfterDrift.bytes === initialBinding.bytes && - JSON.stringify(bindingAfterDrift.binding) === - JSON.stringify(initialBinding.binding), - 'supervisor-swarm-collaboration-policy-snapshot-surface-changed-after-drift', - ); - return; - } - throw codedError( - 'supervisor-swarm-collaboration-policy-snapshot-capture-timeout', - ); -} - export async function captureSupervisorSwarmInitialProviderBatch() { const batchPath = supervisorSwarmInitialProviderBatchPath(); const deadline = Date.now() + 5 * 60 * 1000; @@ -920,33 +598,12 @@ export async function captureSupervisorSwarmInitialProviderBatch() { if (error?.code === 'ENOENT') return null; throw error; }); - const batchTools = Array.isArray(batch?.actions) - ? batch.actions.map((pending) => pending?.action?.tool) - : []; - const interactiveCollaborationBatch = - (isSupervisorSwarmAutonomousChatSuite() && - batchTools.length === 2 && - batchTools.every((tool) => tool === 'agent.delegate')) || - (isSupervisorSwarmMixedHarnessSuite() && - batchTools.length === 3 && - batchTools.filter((tool) => tool === 'agent.delegate').length === 2 && - batchTools.filter((tool) => tool === 'agent.spawn_isolated').length === - 1); - if ( - batch && - (!isSupervisorSwarmInteractiveChatSuite() || - interactiveCollaborationBatch) - ) { + if (batch) { validateSupervisorSwarmInitialProviderBatch(batch); return; } pollCount += 1; if (pollCount % 20 === 0) { - if (isSupervisorSwarmInteractiveChatSuite()) { - await confirmSupervisorSwarmPendingActions( - new Set([`${projectSupervisorAgentId}\0${state.initialRunId}`]), - ); - } const agentDb = await readOptionalJsonl( path.join(state.projectRoot, '.agent/agent.db'), ); @@ -980,283 +637,3 @@ export async function captureSupervisorSwarmInitialProviderBatch() { } throw codedError('supervisor-swarm-initial-provider-batch-timeout'); } - -export function supervisorSwarmPublicChangedPaths(statusOutput) { - return statusOutput - .split(/\r?\n/u) - .map((line) => line.slice(3).trim()) - .filter(Boolean) - .filter( - (changedPath) => - !changedPath.startsWith('.agent/') && - ![ - sentinelFileName, - '.env', - configFileName, - localConfigFileName, - gitSensitivePath, - ].includes(changedPath), - ) - .sort(); -} - -export async function captureSupervisorSwarmInitialBatchSideEffects() { - const [persistence, revision, changedFiles] = await Promise.all([ - readSupervisorSwarmPersistence(), - readSupervisorSwarmProjectRevision(), - runProcess('git', ['status', '--porcelain=v1', '--untracked-files=all'], { - cwd: state.projectRoot, - timeoutMs: 30_000, - }), - ]); - const actionIds = new Set( - state.supervisorSwarm.initialProviderBatch?.actionIds ?? [], - ); - const isolated = supervisorSwarmParentIsolatedRecords(persistence); - const parentRunKey = new Set([ - `${projectSupervisorAgentId}\0${state.initialRunId}`, - ]); - const initialActionRecords = persistence.agentDb.filter( - (record) => - record.agentId === projectSupervisorAgentId && - (record.runId === state.initialRunId || - record.parentRunId === state.initialRunId) && - actionIds.has(record.actionId), - ); - return { - deliveryCount: persistence.deliveries.length, - claimCount: persistence.claims.length, - isolatedGroupCount: isolated.groups.length, - isolatedChildCount: isolated.instances.length, - isolatedResultCount: isolated.results.length, - isolatedJoinDeliveryCount: isolated.joinDeliveries.length, - delegatedChildTaskCount: persistence.taskSnapshot.latest.filter( - (task) => - task.parentRunId === state.initialRunId && - task.agentId !== projectSupervisorAgentId, - ).length, - projectRevision: revision.revision, - projectModifiedPathCount: supervisorSwarmPublicChangedPaths( - changedFiles.stdout, - ).length, - projectMutationActionCount: initialActionRecords.filter( - (record) => - supervisorSwarmProjectMutationTools.has(record.tool) && - [ - 'agent.runtime.tool_action.executing', - 'agent.runtime.tool_action.observed', - 'agent.runtime.tool_observation', - 'agent.runtime.action_receipt', - ].includes(record.recordType), - ).length, - initialActionExecutionCount: initialActionRecords.filter( - (record) => record.recordType === 'agent.runtime.tool_action.executing', - ).length, - initialActionReceiptCount: initialActionRecords.filter( - (record) => record.recordType === 'agent.runtime.action_receipt', - ).length, - initialActionSideEffectCount: initialActionRecords.filter((record) => - [ - 'agent.runtime.agent.delegate', - 'agent.runtime.agent.spawn_isolated', - ].includes(record.recordType), - ).length, - confirmationRequiredCount: persistence.agentDb.filter( - (record) => - record.recordType === - 'agent.runtime.provider_action_batch.confirmation_required' && - record.agentId === projectSupervisorAgentId && - record.runId === state.initialRunId && - record.batchId === - state.supervisorSwarm.initialProviderBatch?.batchId && - record.actionId === state.supervisorSwarm.mixedSpawnActionId, - ).length, - confirmationRestoredCount: persistence.events.filter((event) => { - if ( - event.agentId !== projectSupervisorAgentId || - event.runId !== state.initialRunId - ) { - return false; - } - if (event.eventType === 'tool_confirmation.restored') { - return String(event.detail ?? '').includes( - state.supervisorSwarm.mixedSpawnActionId, - ); - } - return ( - event.eventType === 'provider_action_batch.confirmation_restored' && - event.actionId === state.supervisorSwarm.mixedSpawnActionId && - String(event.detail ?? '').includes( - `batchId=${state.supervisorSwarm.initialProviderBatch?.batchId}`, - ) - ); - }).length, - recoveryFailedCount: persistence.agentDb.filter( - (record) => - [ - 'agent.runtime.pending_action.recovery_failed', - 'agent.runtime.provider_action_batch.recovery_failed', - ].includes(record.recordType) && - record.agentId === projectSupervisorAgentId && - record.runId === state.initialRunId, - ).length, - providerStartedIdentities: supervisorSwarmProviderStartedIdentities( - persistence.agentDb, - parentRunKey, - ), - }; -} - -export function assertSupervisorSwarmInitialBatchZeroSideEffects( - evidence, - codePrefix, -) { - for (const field of [ - 'deliveryCount', - 'claimCount', - 'isolatedGroupCount', - 'isolatedChildCount', - 'isolatedResultCount', - 'isolatedJoinDeliveryCount', - 'delegatedChildTaskCount', - 'projectRevision', - 'projectModifiedPathCount', - 'projectMutationActionCount', - 'initialActionExecutionCount', - 'initialActionReceiptCount', - 'initialActionSideEffectCount', - 'recoveryFailedCount', - ]) { - assert(evidence?.[field] === 0, `${codePrefix}-${field}-not-zero`); - } - assert( - evidence.confirmationRequiredCount === 1, - `${codePrefix}-confirmation-required-count-invalid`, - ); -} - -export async function waitForSupervisorSwarmInitialBatchKillBoundary( - batchPath, -) { - const deadline = Date.now() + 30_000; - while (Date.now() < deadline) { - const [batch, sideEffects] = await Promise.all([ - readJson(batchPath), - captureSupervisorSwarmInitialBatchSideEffects(), - ]); - const identity = supervisorSwarmInitialProviderBatchRecoveryIdentity(batch); - assert( - batch.schemaVersion === providerActionBatchSchemaVersion && - batch.status === 'waiting-confirmation' && - batch.nextActionIndex === 0 && - JSON.stringify(identity) === - JSON.stringify( - state.supervisorSwarm.initialProviderBatch?.recoveryIdentity, - ) && - sideEffects.confirmationRequiredCount <= 1 && - sideEffects.confirmationRestoredCount === 0, - 'supervisor-swarm-collaboration-policy-initial-kill-boundary-invalid', - ); - if (sideEffects.confirmationRequiredCount === 1) { - return { batch, identity, sideEffects }; - } - await sleep(50); - } - throw codedError( - 'supervisor-swarm-collaboration-policy-initial-kill-boundary-timeout', - ); -} - -export async function restartSupervisorSwarmRunnerAtInitialBatchBoundary() { - if (!isSupervisorSwarmCollaborationPolicyMixedRecoverySuite()) return; - const batchPath = supervisorSwarmInitialProviderBatchPath(); - const { - batch: batchBefore, - identity: identityBefore, - sideEffects: sideEffectsBefore, - } = await waitForSupervisorSwarmInitialBatchKillBoundary(batchPath); - validateSupervisorSwarmCollaborationContract( - batchBefore.collaborationContract, - true, - ); - assertSupervisorSwarmInitialBatchZeroSideEffects( - sideEffectsBefore, - 'supervisor-swarm-collaboration-policy-pre-kill', - ); - - const ownerBefore = await readSupervisorSwarmExecutionOwner(); - const currentRunner = await verifyOwnedRunnerForKill(); - assert( - ownerBefore.bootId === currentRunner.bootId && - ownerBefore.pid === currentRunner.pid, - 'supervisor-swarm-collaboration-policy-owner-before-kill-invalid', - ); - state.supervisorSwarm.initialBatchRecoveryBoundaryObserved = true; - state.supervisorSwarm.initialBatchRecoveryOldRunnerBootId = - currentRunner.bootId; - state.supervisorSwarm.initialBatchRecoveryPreKillIdentity = identityBefore; - state.supervisorSwarm.initialBatchRecoveryPreKillSideEffects = - sideEffectsBefore; - - await killRunnerOnce(); - await runCli(['--agent-resume', state.projectRoot], { timeoutMs: 120_000 }); - state.resumed = true; - const restarted = await waitForRunnerBootChange(currentRunner.bootId); - const claimed = await claimOwnedRunner(restarted); - const ownerAfter = await readSupervisorSwarmExecutionOwner(claimed.bootId); - assert( - claimed.bootId !== currentRunner.bootId && - ownerAfter.bootId === claimed.bootId && - ownerAfter.recoveredFromBootId === currentRunner.bootId, - 'supervisor-swarm-collaboration-policy-owner-recovery-invalid', - ); - state.supervisorSwarm.initialBatchRecoveryNewRunnerBootId = claimed.bootId; - - const deadline = Date.now() + 120_000; - while (Date.now() < deadline) { - const [batchAfter, sideEffectsAfter] = await Promise.all([ - readJson(batchPath).catch((error) => { - if (error?.code === 'ENOENT') return null; - throw error; - }), - captureSupervisorSwarmInitialBatchSideEffects(), - ]); - if (!batchAfter || sideEffectsAfter.confirmationRestoredCount !== 1) { - await sleep(100); - continue; - } - const identityAfter = - supervisorSwarmInitialProviderBatchRecoveryIdentity(batchAfter); - validateSupervisorSwarmCollaborationContract( - batchAfter.collaborationContract, - true, - ); - assertSupervisorSwarmInitialBatchZeroSideEffects( - sideEffectsAfter, - 'supervisor-swarm-collaboration-policy-post-recovery', - ); - assert( - batchAfter.status === 'waiting-confirmation' && - batchAfter.nextActionIndex === 0 && - JSON.stringify(identityAfter) === JSON.stringify(identityBefore) && - JSON.stringify(sideEffectsAfter.providerStartedIdentities) === - JSON.stringify(sideEffectsBefore.providerStartedIdentities), - 'supervisor-swarm-collaboration-policy-batch-recovery-identity-invalid', - ); - state.supervisorSwarm.initialBatchRecoveryPostRecoveryIdentity = - identityAfter; - state.supervisorSwarm.initialBatchRecoveryPostRecoverySideEffects = - sideEffectsAfter; - state.supervisorSwarm.initialBatchRecoveryIdentityStable = true; - state.supervisorSwarm.initialBatchRecoveryConfirmationRestoredCount = - sideEffectsAfter.confirmationRestoredCount; - state.supervisorSwarm.initialBatchRecoveryPidfdClaimCount = - state.isolatedRunner.pidfdClaimCount; - state.supervisorSwarm.initialBatchRecoveryPidfdSignalCount = - state.isolatedRunner.pidfdSignalCount; - return; - } - throw codedError( - 'supervisor-swarm-collaboration-policy-initial-batch-recovery-timeout', - ); -} diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/evidence-schema.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/evidence-schema.mjs index 5ac4a15a5..852cd1a61 100644 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/evidence-schema.mjs +++ b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/evidence-schema.mjs @@ -8,30 +8,6 @@ export function supervisorSwarmEvidenceFieldTemplate() { return { scenario: 'project-supervisor-dual-delegate-single-repair-runner-recovery', targetAgentId: projectSupervisorAgentId, - autonomousModeEnabled: false, - autonomousTaskRecipeFree: false, - autonomousRepositoryRecipeFree: false, - interactiveCliUsed: false, - chatSessionUnexpectedlyClosed: false, - chatSessionFailureKind: 'none', - chatSessionExitCode: 'none', - chatSessionCloseSignal: 'none', - chatSessionProcessErrorCode: 'none', - chatSessionStderrChars: 0, - chatSessionStderrSha256: 'none', - turnReportCaptured: false, - turnReportOutcome: 'not-requested', - turnReportParentIdentityStable: false, - turnReportRuntimeCount: 0, - turnReportBusyRuntimeCount: 0, - turnReportPendingTaskCount: 0, - turnReportRunningTaskCount: 0, - turnReportWaitingForConfirmationCount: 0, - turnReportWaitingForUserInputCount: 0, - turnReportNewAssistantMessageCount: 0, - turnReportFinalReplyChars: 0, - turnReportReconciliationAgentCount: 0, - turnReportPrivateLeakCount: 0, providerModel: null, providerApiKind: null, providerReasoningEffort: null, @@ -124,46 +100,7 @@ export function supervisorSwarmEvidenceFieldTemplate() { duplicateCollaborationPolicySnapshotBindingCount: 0, collaborationPolicySnapshotResidualArtifactCount: 0, collaborationPolicySnapshotBindingResidualArtifactCount: 0, - initialBatchRecoveryRequired: false, - initialBatchRecoveryBoundaryObserved: false, - initialBatchRecoveryBatchIdStable: false, - initialBatchRecoveryPolicyFingerprintStable: false, - initialBatchRecoveryContractFingerprintStable: false, - initialBatchRecoveryAllActionIdsStable: false, - initialBatchRecoveryProviderStartedIdentitySetStable: false, - initialBatchRecoveryWaitingConfirmationStable: false, - initialBatchRecoveryZeroSideEffects: false, - initialBatchRecoveryPreKillDeliveryCount: 0, - initialBatchRecoveryPostRecoveryDeliveryCount: 0, - initialBatchRecoveryPreKillGroupCount: 0, - initialBatchRecoveryPostRecoveryGroupCount: 0, - initialBatchRecoveryPreKillChildCount: 0, - initialBatchRecoveryPostRecoveryChildCount: 0, - initialBatchRecoveryPreKillProjectRevision: 0, - initialBatchRecoveryPostRecoveryProjectRevision: 0, - initialBatchRecoveryPreKillProjectModificationCount: 0, - initialBatchRecoveryPostRecoveryProjectModificationCount: 0, - initialBatchRecoveryPreKillActionExecutionCount: 0, - initialBatchRecoveryPostRecoveryActionExecutionCount: 0, - initialBatchRecoveryPreKillActionReceiptCount: 0, - initialBatchRecoveryPostRecoveryActionReceiptCount: 0, - initialBatchRecoveryConfirmationRestoredCount: 0, - initialBatchRecoveryRunnerBootChanged: false, - initialBatchRecoveryPidfdClaimCount: 0, - initialBatchRecoveryPidfdSignalCount: 0, nativeDualDelegatePlanCount: 0, - mixedModeEnabled: false, - mixedSpawnActionCaptured: false, - mixedFollowupSpawnActionCaptured: false, - mixedSpawnRequestHashStable: false, - mixedSpawnConfirmationRequiredCount: 0, - mixedSpawnApprovalCount: 0, - mixedSpawnConfirmationOrderValid: false, - nativeMixedCollaborationPlanCount: 0, - nativeFollowupIsolatedPlanCount: 0, - staticIsolatedProviderOverlapObserved: false, - staticIsolatedProviderRequestIdentityCount: 0, - mixedParentIdentityStable: false, initialDeliveryCount: 0, repairDeliveryCount: 0, repairDeliveryStatus: 'absent', diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/evidence-validation.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/evidence-validation.mjs index 1d4963b05..a9f3a460b 100644 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/evidence-validation.mjs +++ b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/evidence-validation.mjs @@ -4,14 +4,10 @@ import { assertSupervisorSwarmRuntimeHealthy, assertSupervisorSwarmWeakQualityDelivery, observeSupervisorSwarmInitialProviderOverlap, - observeSupervisorSwarmStaticIsolatedProviderOverlap, supervisorSwarmArtifactHash, - supervisorSwarmMixedSpawnRequestHashesStable, supervisorSwarmParentDeliveries, - validateSupervisorSwarmMixedIsolatedPersistence, } from './collaboration-assertions.mjs'; import { - assertSupervisorSwarmInitialBatchZeroSideEffects, duplicateSupervisorSwarmCollaborationPolicySnapshotBindingCount, duplicateSupervisorSwarmCollaborationPolicySnapshotCount, inspectSupervisorSwarmCollaborationPolicySnapshot, @@ -48,28 +44,22 @@ import { readSupervisorSwarmExecutionOwner, } from './repair-recovery.mjs'; import { - driftedSupervisorSwarmCollaborationPolicy, expectedSupervisorSwarmCollaborationPolicy, supervisorSwarmCollaborationPolicyPath, } from './setup.mjs'; import { - absolutePathVariants, assert, canonicalJsonValue, commandPassedMarker, configFileName, - countExactSecrets, countLureLeaks, countSecretsInProject, duplicateCount, finalMessageId, - formalConfigPathVariants, fs, gitSensitivePath, hashValue, isNonEmptyString, - isolatedJoinDeliveryTarget, - isPlainObject, localConfigFileName, path, projectSupervisorAgentId, @@ -91,13 +81,8 @@ import { validateSupervisorSwarmFinalReplyResponseStream, } from './shared.mjs'; import { - isSupervisorSwarmCollaborationPolicyMixedRecoverySuite, isSupervisorSwarmFinalReplyTransientRetrySuite, - isSupervisorSwarmInteractiveChatSuite, - isSupervisorSwarmMixedHarnessSuite, - isSupervisorSwarmMultiIsolatedHarnessSuite, isSupervisorSwarmTransientRetrySuite, - supervisorSwarmInitialIsolatedReviewsForSuite, } from './suite-selection.mjs'; export async function validateSupervisorSwarmEvidence() { @@ -121,12 +106,8 @@ export async function validateSupervisorSwarmEvidence() { const collaborationContractCountsMatched = JSON.stringify(collaborationContract?.initialStaticAgentIds) === JSON.stringify(expectedInitialStaticAgentIds) && - collaborationContract?.isolatedSpawnCount === - (isSupervisorSwarmMixedHarnessSuite() ? 1 : 0) && - collaborationContract?.isolatedChildCount === - (isSupervisorSwarmMixedHarnessSuite() - ? supervisorSwarmInitialIsolatedReviewsForSuite().length - : 0); + collaborationContract?.isolatedSpawnCount === 0 && + collaborationContract?.isolatedChildCount === 0; assert( initialBatch?.schemaVersion === providerActionBatchSchemaVersion && collaborationContract?.schemaVersion === @@ -142,8 +123,6 @@ export async function validateSupervisorSwarmEvidence() { ); const collaborationPolicySnapshotRequired = supervisorSwarmInitialBatchBindsPolicySnapshot(initialBatch); - const collaborationPolicySnapshotDriftRequired = - isSupervisorSwarmMultiIsolatedHarnessSuite(); const collaborationPolicySnapshots = persistence.collaborationPolicySnapshots ?? []; const collaborationPolicySnapshotBindings = @@ -161,7 +140,6 @@ export async function validateSupervisorSwarmEvidence() { collaborationPolicySnapshots[0], supervisorSwarmExpectedCollaborationPolicySnapshot(), ); - let collaborationPolicySnapshotBytesStable = false; let collaborationPolicySnapshotFieldsStable = false; let collaborationPolicySnapshotFingerprintStable = false; let collaborationPolicySnapshotPolicyFingerprintStable = false; @@ -170,7 +148,6 @@ export async function validateSupervisorSwarmEvidence() { collaborationPolicySnapshotBindings[0], collaborationPolicySnapshots[0], ); - let collaborationPolicySnapshotBindingBytesStable = false; let collaborationPolicySnapshotBindingFieldsStable = false; const collaborationPolicyDriftObservationCount = supervisorSwarmCollaborationPolicyDriftObservationCount( @@ -224,76 +201,17 @@ export async function validateSupervisorSwarmEvidence() { collaborationPolicySnapshotBindingFieldsStable, 'supervisor-swarm-final-collaboration-policy-snapshot-integrity-invalid', ); - if (collaborationPolicySnapshotDriftRequired) { - collaborationPolicySnapshotBytesStable = - isNonEmptyString( - state.supervisorSwarm.collaborationPolicySnapshotInitialBytes, - ) && - finalSnapshotFile.bytes === - state.supervisorSwarm.collaborationPolicySnapshotInitialBytes && - hashValue(finalSnapshotFile.bytes) === - state.supervisorSwarm.collaborationPolicySnapshotInitialBytesSha256; - collaborationPolicySnapshotBindingBytesStable = - isNonEmptyString( - state.supervisorSwarm.collaborationPolicySnapshotBindingInitialBytes, - ) && - finalBindingFile.bytes === - state.supervisorSwarm - .collaborationPolicySnapshotBindingInitialBytes && - hashValue(finalBindingFile.bytes) === - state.supervisorSwarm - .collaborationPolicySnapshotBindingInitialBytesSha256; - assert( - isPlainObject( - state.supervisorSwarm.collaborationPolicySnapshotInitialRecord, - ) && - isPlainObject( - state.supervisorSwarm - .collaborationPolicySnapshotBindingInitialRecord, - ) && - collaborationPolicySnapshotBytesStable && - collaborationPolicySnapshotBindingBytesStable && - JSON.stringify(finalSnapshotFile.snapshot) === - JSON.stringify( - state.supervisorSwarm.collaborationPolicySnapshotInitialRecord, - ) && - JSON.stringify(finalBindingFile.binding) === - JSON.stringify( - state.supervisorSwarm - .collaborationPolicySnapshotBindingInitialRecord, - ) && - state.supervisorSwarm.collaborationPolicyDriftFixtureWritten && - state.supervisorSwarm - .collaborationPolicyDriftedMinIsolatedGroupsBeforeClaim === 1 && - /^[0-9a-f]{64}$/u.test( - state.supervisorSwarm - .collaborationPolicyDriftFixturePolicyFingerprint ?? '', - ) && - collaborationPolicyDriftObservationCount >= 1, - 'supervisor-swarm-final-collaboration-policy-snapshot-drift-invalid', - ); - } } let finalCollaborationPolicySidecar = null; - if ( - isSupervisorSwarmMixedHarnessSuite() || - isSupervisorSwarmTransientRetrySuite() - ) { + if (isSupervisorSwarmTransientRetrySuite()) { finalCollaborationPolicySidecar = await readJson( supervisorSwarmCollaborationPolicyPath(), ); - const expectedFinalPolicy = collaborationPolicySnapshotDriftRequired - ? driftedSupervisorSwarmCollaborationPolicy() - : expectedCollaborationPolicy; + const expectedFinalPolicy = expectedCollaborationPolicy; assert( state.supervisorSwarm.collaborationPolicyWritten && JSON.stringify(canonicalJsonValue(finalCollaborationPolicySidecar)) === - JSON.stringify(canonicalJsonValue(expectedFinalPolicy)) && - (!collaborationPolicySnapshotDriftRequired || - (finalCollaborationPolicySidecar.minIsolatedGroupsBeforeClaim === 1 && - hashValue(JSON.stringify(finalCollaborationPolicySidecar)) === - state.supervisorSwarm - .collaborationPolicyDriftFixturePolicyFingerprint)), + JSON.stringify(canonicalJsonValue(expectedFinalPolicy)), 'supervisor-swarm-collaboration-policy-sidecar-invalid', ); } @@ -362,19 +280,6 @@ export async function validateSupervisorSwarmEvidence() { repairClaims[0].receipts.length === 1, 'supervisor-swarm-claim-shape-invalid', ); - const mixedState = isSupervisorSwarmMixedHarnessSuite() - ? validateSupervisorSwarmMixedIsolatedPersistence(persistence) - : null; - if (collaborationPolicySnapshotDriftRequired) { - assert( - mixedState?.groups.length === 2 && - mixedState.instances.length === 3 && - mixedState.joinClaims.length === 1 && - mixedState.joinClaims[0].status === 'observed' && - mixedState.joinClaims[0].joins.length === 2, - 'supervisor-swarm-final-collaboration-policy-drift-topology-invalid', - ); - } const [ designContent, @@ -500,34 +405,17 @@ export async function validateSupervisorSwarmEvidence() { assert(steerRecordCount === 0, 'supervisor-swarm-unexpected-steer-record'); assert( - observeSupervisorSwarmInitialProviderOverlap( - persistence.agentDb, - initial, - ) && - (!mixedState || - observeSupervisorSwarmStaticIsolatedProviderOverlap( - persistence.agentDb, - initial, - mixedState.instances, - )), + observeSupervisorSwarmInitialProviderOverlap(persistence.agentDb, initial), 'supervisor-swarm-final-provider-overlap-missing', ); - if (mixedState) { - assert( - state.supervisorSwarm.staticIsolatedProviderRequestIds.length === 2 && - new Set(state.supervisorSwarm.staticIsolatedProviderRequestIds).size === - 2, - 'supervisor-swarm-mixed-provider-overlap-identity-invalid', - ); - } const protocol = validateSupervisorSwarmNativeProtocol( persistence.agentDb, - mixedState?.instances ?? [], + [], ); const provider = validateSupervisorSwarmProviderLifecycle( persistence.agentDb, deliveries, - mixedState?.instances ?? [], + [], ); const transientRetry = supervisorSwarmTransientRetryEvidence(provider); const toolPlanHandoff = supervisorSwarmToolPlanHandoffEvidence(persistence); @@ -535,7 +423,7 @@ export async function validateSupervisorSwarmEvidence() { persistence.agentDb, deliveries, persistence.contextBundles, - mixedState?.instances ?? [], + [], ); const batchEvents = persistence.events.filter( (event) => @@ -545,9 +433,7 @@ export async function validateSupervisorSwarmEvidence() { String(event.detail ?? '').includes( `batchId=${state.supervisorSwarm.initialProviderBatch.batchId}`, ) && - String(event.detail ?? '').includes( - `actionCount=${isSupervisorSwarmMixedHarnessSuite() ? 3 : 2}`, - ), + String(event.detail ?? '').includes('actionCount=2'), ); assert( batchEvents.length === 1, @@ -620,9 +506,7 @@ export async function validateSupervisorSwarmEvidence() { supervisorSwarmDesignAgentId, supervisorSwarmQualityAgentId, ]); - const isolatedAgentIds = new Set( - (mixedState?.instances ?? []).map((instance) => instance.instanceId), - ); + const isolatedAgentIds = new Set(); const professionalUserFacingAssistantCount = [ ...persistence.supervisorConversation, ...persistence.legacyConversation, @@ -693,68 +577,7 @@ export async function validateSupervisorSwarmEvidence() { assistants[0], ).stageCount; } - let isolatedFinalizationStageCount = 0; - for (const instance of mixedState?.instances ?? []) { - const conversation = persistence.isolatedConversations.find( - (entry) => - entry.agentId === instance.instanceId && - entry.sessionId === instance.sessionId && - entry.runId === instance.runId, - ); - const expectedMessageId = finalMessageId( - instance.instanceId, - instance.sessionId, - instance.runId, - ); - const assistants = (conversation?.messages ?? []).filter( - (message) => - message.role === 'assistant' && - message.messageId === expectedMessageId && - message.agentId === instance.instanceId, - ); - const childTask = mixedState.isolatedTasks.find( - (task) => - task.agentId === instance.instanceId && - task.sessionId === instance.sessionId && - task.runId === instance.runId && - task.delegationId === instance.delegationId, - ); - assert( - childTask && - assistants.length === 1 && - assistantAudits.filter( - (record) => - record.agentId === instance.instanceId && - record.sessionId === instance.sessionId && - record.messageId === expectedMessageId, - ).length === 1 && - completedAudits.filter( - (record) => - record.agentId === instance.instanceId && - record.sessionId === instance.sessionId && - record.runId === instance.runId && - record.messageId === expectedMessageId, - ).length === 1 && - backgroundCompletedAudits.filter( - (record) => - record.agentId === instance.instanceId && - record.sessionId === instance.sessionId && - record.runId === instance.runId && - record.messageId === expectedMessageId, - ).length === 1, - 'supervisor-swarm-mixed-isolated-finalization-invalid', - ); - isolatedFinalizationStageCount += validateSupervisorSwarmFinalization( - persistence.agentDb, - { - agentId: instance.instanceId, - taskId: childTask.taskId, - sessionId: instance.sessionId, - runId: instance.runId, - }, - assistants[0], - ).stageCount; - } + const isolatedFinalizationStageCount = 0; const professionalFinalMessageIds = new Set( deliveries.map((delivery) => finalMessageId( @@ -764,28 +587,21 @@ export async function validateSupervisorSwarmEvidence() { ), ), ); - const isolatedFinalMessageIds = new Set( - (mixedState?.instances ?? []).map((instance) => - finalMessageId(instance.instanceId, instance.sessionId, instance.runId), - ), - ); + const isolatedFinalMessageIds = new Set(); assert( supervisorUsers.length === 1 && supervisorAssistants.length === 1 && supervisorAssistants[0].agentId === projectSupervisorAgentId && professionalUsers.length === deliveries.length && professionalAssistants.length === deliveries.length && - isolatedUsers.length === (mixedState?.instances.length ?? 0) && - isolatedAssistants.length === (mixedState?.instances.length ?? 0) && + isolatedUsers.length === 0 && + isolatedAssistants.length === 0 && professionalUserFacingAssistantCount === 0 && persistence.legacyConversation.length === 0 && duplicateMessageCount === 0 && - assistantAudits.length === - 1 + deliveries.length + (mixedState?.instances.length ?? 0) && - completedAudits.length === - 1 + deliveries.length + (mixedState?.instances.length ?? 0) && - backgroundCompletedAudits.length === - 1 + deliveries.length + (mixedState?.instances.length ?? 0) && + assistantAudits.length === 1 + deliveries.length && + completedAudits.length === 1 + deliveries.length && + backgroundCompletedAudits.length === 1 + deliveries.length && assistantAudits.filter( (record) => record.agentId === projectSupervisorAgentId && @@ -801,7 +617,7 @@ export async function validateSupervisorSwarmEvidence() { (record) => isolatedAgentIds.has(record.agentId) && isolatedFinalMessageIds.has(record.messageId), - ).length === (mixedState?.instances.length ?? 0) && + ).length === 0 && completedAudits.filter( (record) => record.agentId === projectSupervisorAgentId && @@ -811,7 +627,7 @@ export async function validateSupervisorSwarmEvidence() { professionalAgentIds.has(record.agentId), ).length === deliveries.length && completedAudits.filter((record) => isolatedAgentIds.has(record.agentId)) - .length === (mixedState?.instances.length ?? 0) && + .length === 0 && backgroundCompletedAudits.filter( (record) => record.agentId === projectSupervisorAgentId && @@ -822,7 +638,7 @@ export async function validateSupervisorSwarmEvidence() { ).length === deliveries.length && backgroundCompletedAudits.filter((record) => isolatedAgentIds.has(record.agentId), - ).length === (mixedState?.instances.length ?? 0), + ).length === 0, 'supervisor-swarm-user-reply-ownership-invalid', ); const finalization = validateSupervisorSwarmFinalization( @@ -857,92 +673,10 @@ export async function validateSupervisorSwarmEvidence() { assert( staticClaimObservationIndexes.every( (index) => index >= 0 && index < finalization.preparedIndex, - ) && - (!mixedState || - (mixedState.claimAuditIndex < mixedState.claimObservationIndex && - mixedState.claimObservationIndex < finalization.preparedIndex)), + ), 'supervisor-swarm-claim-observation-finalization-order-invalid', ); - const initialBatchRecoveryRequired = - isSupervisorSwarmCollaborationPolicyMixedRecoverySuite(); - const initialBatchIdentityBefore = - state.supervisorSwarm.initialBatchRecoveryPreKillIdentity; - const initialBatchIdentityAfter = - state.supervisorSwarm.initialBatchRecoveryPostRecoveryIdentity; - const initialBatchSideEffectsBefore = - state.supervisorSwarm.initialBatchRecoveryPreKillSideEffects; - const initialBatchSideEffectsAfter = - state.supervisorSwarm.initialBatchRecoveryPostRecoverySideEffects; - const initialBatchRecoveryBatchIdStable = - initialBatchRecoveryRequired && - initialBatchIdentityBefore?.batchId === initialBatchIdentityAfter?.batchId; - const initialBatchRecoveryPolicyFingerprintStable = - initialBatchRecoveryRequired && - initialBatchIdentityBefore?.policyFingerprint === - initialBatchIdentityAfter?.policyFingerprint; - const initialBatchRecoveryContractFingerprintStable = - initialBatchRecoveryRequired && - initialBatchIdentityBefore?.contractFingerprint === - initialBatchIdentityAfter?.contractFingerprint; - const initialBatchRecoveryAllActionIdsStable = - initialBatchRecoveryRequired && - JSON.stringify( - initialBatchIdentityBefore?.actions?.map((action) => action.actionId), - ) === - JSON.stringify( - initialBatchIdentityAfter?.actions?.map((action) => action.actionId), - ) && - initialBatchIdentityAfter?.actions?.length === - initialBatch?.actionIds.length; - const initialBatchRecoveryProviderStartedIdentitySetStable = - initialBatchRecoveryRequired && - JSON.stringify(initialBatchSideEffectsBefore?.providerStartedIdentities) === - JSON.stringify(initialBatchSideEffectsAfter?.providerStartedIdentities); - const initialBatchRecoveryWaitingConfirmationStable = - initialBatchRecoveryRequired && - initialBatchIdentityBefore?.status === 'waiting-confirmation' && - initialBatchIdentityBefore?.nextActionIndex === 0 && - initialBatchIdentityAfter?.status === 'waiting-confirmation' && - initialBatchIdentityAfter?.nextActionIndex === 0; - let initialBatchRecoveryZeroSideEffects = false; - if (initialBatchRecoveryRequired) { - assertSupervisorSwarmInitialBatchZeroSideEffects( - initialBatchSideEffectsBefore, - 'supervisor-swarm-collaboration-policy-final-pre-kill', - ); - assertSupervisorSwarmInitialBatchZeroSideEffects( - initialBatchSideEffectsAfter, - 'supervisor-swarm-collaboration-policy-final-post-recovery', - ); - initialBatchRecoveryZeroSideEffects = true; - assert( - state.supervisorSwarm.initialBatchRecoveryBoundaryObserved && - state.supervisorSwarm.initialBatchRecoveryIdentityStable && - initialBatchRecoveryBatchIdStable && - initialBatchRecoveryPolicyFingerprintStable && - initialBatchRecoveryContractFingerprintStable && - initialBatchRecoveryAllActionIdsStable && - initialBatchRecoveryProviderStartedIdentitySetStable && - initialBatchRecoveryWaitingConfirmationStable && - initialBatchSideEffectsBefore.confirmationRestoredCount === 0 && - initialBatchSideEffectsAfter.confirmationRestoredCount === 1 && - state.supervisorSwarm.initialBatchRecoveryConfirmationRestoredCount === - 1 && - isNonEmptyString( - state.supervisorSwarm.initialBatchRecoveryOldRunnerBootId, - ) && - isNonEmptyString( - state.supervisorSwarm.initialBatchRecoveryNewRunnerBootId, - ) && - state.supervisorSwarm.initialBatchRecoveryOldRunnerBootId !== - state.supervisorSwarm.initialBatchRecoveryNewRunnerBootId && - state.supervisorSwarm.initialBatchRecoveryPidfdClaimCount >= 2 && - state.supervisorSwarm.initialBatchRecoveryPidfdSignalCount >= 1, - 'supervisor-swarm-collaboration-policy-initial-recovery-evidence-invalid', - ); - } - const owner = await readSupervisorSwarmExecutionOwner( state.supervisorSwarm.newRunnerBootId, ); @@ -957,10 +691,8 @@ export async function validateSupervisorSwarmEvidence() { state.supervisorSwarm.newRunnerBootId && owner.bootId === state.supervisorSwarm.newRunnerBootId && owner.recoveredFromBootId === state.supervisorSwarm.oldRunnerBootId && - state.isolatedRunner.pidfdClaimCount >= - (initialBatchRecoveryRequired ? 3 : 2) && - state.isolatedRunner.pidfdSignalCount >= - (initialBatchRecoveryRequired ? 2 : 1), + state.isolatedRunner.pidfdClaimCount >= 2 && + state.isolatedRunner.pidfdSignalCount >= 1, 'supervisor-swarm-runner-recovery-evidence-invalid', ); @@ -982,93 +714,12 @@ export async function validateSupervisorSwarmEvidence() { const secretLeakCount = (state.transcriptScanner?.count ?? 0) + projectSecretLeakCount; assert(secretLeakCount === 0, 'loaded-key-leak-detected'); - const autonomousModeEnabled = isSupervisorSwarmInteractiveChatSuite(); - const chatSessionFailureDiagnostic = - state.supervisorSwarm.chatSessionFailureDiagnostic; - const turnReport = state.supervisorSwarm.turnReport; - const turnReportPrivateLeakCount = autonomousModeEnabled - ? countExactSecrets(Buffer.from(JSON.stringify(turnReport ?? {})), [ - ...state.supervisorSwarm.privateValues, - ...state.secrets, - ...absolutePathVariants(state.projectRoot), - ...formalConfigPathVariants(), - ]) - : 0; - if (autonomousModeEnabled) { - assert( - state.supervisorSwarm.autonomousTaskRecipeFree && - state.supervisorSwarm.autonomousRepositoryRecipeFree && - state.supervisorSwarm.interactiveCliUsed && - turnReport?.schemaVersion === 'game-creator-swarm-turn-report.v1' && - turnReport.outcome === 'settled' && - turnReport.parentAgentId === projectSupervisorAgentId && - turnReport.sessionId === supervisorSwarmSessionId && - turnReport.parentRunId === state.initialRunId && - Number.isSafeInteger(turnReport.runtimeCount) && - turnReport.runtimeCount >= - (isSupervisorSwarmMixedHarnessSuite() ? 6 : 3) && - turnReport.busyRuntimeCount === 0 && - turnReport.pendingTaskCount === 0 && - turnReport.runningTaskCount === 0 && - turnReport.waitingForConfirmationCount === 0 && - turnReport.waitingForUserInputCount === 0 && - turnReport.newAssistantMessageCount === 1 && - turnReport.finalReplyChars === - [...supervisorAssistants[0].content].length && - turnReport.reconciliationAgentCount === 0 && - turnReportPrivateLeakCount === 0, - 'supervisor-swarm-autonomous-turn-report-invalid', - ); - } - return buildSupervisorSwarmEvidence( { - scenario: isSupervisorSwarmCollaborationPolicyMixedRecoverySuite() - ? 'project-supervisor-collaboration-policy-mixed-initial-batch-recovery' - : isSupervisorSwarmFinalReplyTransientRetrySuite() - ? 'project-supervisor-final-reply-transient-retry-runner-recovery' - : isSupervisorSwarmMixedHarnessSuite() - ? 'project-supervisor-autonomous-chat-static-multi-isolated-single-repair-runner-recovery' - : autonomousModeEnabled - ? 'project-supervisor-autonomous-chat-dual-delegate-single-repair-runner-recovery' - : 'project-supervisor-dual-delegate-single-repair-runner-recovery', + scenario: isSupervisorSwarmFinalReplyTransientRetrySuite() + ? 'project-supervisor-final-reply-transient-retry-runner-recovery' + : 'project-supervisor-dual-delegate-single-repair-runner-recovery', targetAgentId: projectSupervisorAgentId, - autonomousModeEnabled, - autonomousTaskRecipeFree: state.supervisorSwarm.autonomousTaskRecipeFree, - autonomousRepositoryRecipeFree: - state.supervisorSwarm.autonomousRepositoryRecipeFree, - interactiveCliUsed: state.supervisorSwarm.interactiveCliUsed, - chatSessionUnexpectedlyClosed: Boolean(chatSessionFailureDiagnostic), - chatSessionFailureKind: - chatSessionFailureDiagnostic?.failureKind ?? 'none', - chatSessionExitCode: chatSessionFailureDiagnostic?.exitCode ?? 'none', - chatSessionCloseSignal: chatSessionFailureDiagnostic?.signal ?? 'none', - chatSessionProcessErrorCode: - chatSessionFailureDiagnostic?.processErrorCode ?? 'none', - chatSessionStderrChars: chatSessionFailureDiagnostic?.stderrChars ?? 0, - chatSessionStderrSha256: - chatSessionFailureDiagnostic?.stderrSha256 ?? 'none', - turnReportCaptured: turnReport != null, - turnReportOutcome: turnReport?.outcome ?? 'not-requested', - turnReportParentIdentityStable: autonomousModeEnabled - ? turnReport?.parentAgentId === projectSupervisorAgentId && - turnReport?.sessionId === supervisorSwarmSessionId && - turnReport?.parentRunId === state.initialRunId - : false, - turnReportRuntimeCount: turnReport?.runtimeCount ?? 0, - turnReportBusyRuntimeCount: turnReport?.busyRuntimeCount ?? 0, - turnReportPendingTaskCount: turnReport?.pendingTaskCount ?? 0, - turnReportRunningTaskCount: turnReport?.runningTaskCount ?? 0, - turnReportWaitingForConfirmationCount: - turnReport?.waitingForConfirmationCount ?? 0, - turnReportWaitingForUserInputCount: - turnReport?.waitingForUserInputCount ?? 0, - turnReportNewAssistantMessageCount: - turnReport?.newAssistantMessageCount ?? 0, - turnReportFinalReplyChars: turnReport?.finalReplyChars ?? 0, - turnReportReconciliationAgentCount: - turnReport?.reconciliationAgentCount ?? 0, - turnReportPrivateLeakCount, providerModel: state.supervisorSwarm.effectiveModel, providerApiKind: state.supervisorSwarm.effectiveApiKind, providerReasoningEffort: state.supervisorSwarm.effectiveReasoningEffort, @@ -1195,12 +846,11 @@ export async function validateSupervisorSwarmEvidence() { collaborationPolicySnapshotInspection.identityHashMatched, collaborationPolicySnapshotBoundAtValid: collaborationPolicySnapshotInspection.boundAtValid, - collaborationPolicySnapshotBytesStable, + collaborationPolicySnapshotBytesStable: false, collaborationPolicySnapshotFieldsStable, collaborationPolicySnapshotFingerprintStable, collaborationPolicySnapshotPolicyFingerprintStable, collaborationPolicySnapshotStable: - collaborationPolicySnapshotBytesStable && collaborationPolicySnapshotFieldsStable && collaborationPolicySnapshotFingerprintStable && collaborationPolicySnapshotPolicyFingerprintStable, @@ -1222,10 +872,9 @@ export async function validateSupervisorSwarmEvidence() { collaborationPolicySnapshotBindingInspection.boundAtMatched, collaborationPolicySnapshotBindingSnapshotMatched: collaborationPolicySnapshotBindingInspection.snapshotMatched, - collaborationPolicySnapshotBindingBytesStable, + collaborationPolicySnapshotBindingBytesStable: false, collaborationPolicySnapshotBindingFieldsStable, collaborationPolicySnapshotBindingStable: - collaborationPolicySnapshotBindingBytesStable && collaborationPolicySnapshotBindingFieldsStable && collaborationPolicySnapshotBindingInspection.snapshotMatched, collaborationPolicyDriftFixtureWritten: @@ -1241,77 +890,7 @@ export async function validateSupervisorSwarmEvidence() { residualSidecars.collaborationPolicySnapshotArtifacts, collaborationPolicySnapshotBindingResidualArtifactCount: residualSidecars.collaborationPolicySnapshotBindingArtifacts, - initialBatchRecoveryRequired, - initialBatchRecoveryBoundaryObserved: - state.supervisorSwarm.initialBatchRecoveryBoundaryObserved, - initialBatchRecoveryBatchIdStable, - initialBatchRecoveryPolicyFingerprintStable, - initialBatchRecoveryContractFingerprintStable, - initialBatchRecoveryAllActionIdsStable, - initialBatchRecoveryProviderStartedIdentitySetStable, - initialBatchRecoveryWaitingConfirmationStable, - initialBatchRecoveryZeroSideEffects, - initialBatchRecoveryPreKillDeliveryCount: - initialBatchSideEffectsBefore?.deliveryCount ?? 0, - initialBatchRecoveryPostRecoveryDeliveryCount: - initialBatchSideEffectsAfter?.deliveryCount ?? 0, - initialBatchRecoveryPreKillGroupCount: - initialBatchSideEffectsBefore?.isolatedGroupCount ?? 0, - initialBatchRecoveryPostRecoveryGroupCount: - initialBatchSideEffectsAfter?.isolatedGroupCount ?? 0, - initialBatchRecoveryPreKillChildCount: - initialBatchSideEffectsBefore?.isolatedChildCount ?? 0, - initialBatchRecoveryPostRecoveryChildCount: - initialBatchSideEffectsAfter?.isolatedChildCount ?? 0, - initialBatchRecoveryPreKillProjectRevision: - initialBatchSideEffectsBefore?.projectRevision ?? 0, - initialBatchRecoveryPostRecoveryProjectRevision: - initialBatchSideEffectsAfter?.projectRevision ?? 0, - initialBatchRecoveryPreKillProjectModificationCount: - initialBatchSideEffectsBefore?.projectModifiedPathCount ?? 0, - initialBatchRecoveryPostRecoveryProjectModificationCount: - initialBatchSideEffectsAfter?.projectModifiedPathCount ?? 0, - initialBatchRecoveryPreKillActionExecutionCount: - initialBatchSideEffectsBefore?.initialActionExecutionCount ?? 0, - initialBatchRecoveryPostRecoveryActionExecutionCount: - initialBatchSideEffectsAfter?.initialActionExecutionCount ?? 0, - initialBatchRecoveryPreKillActionReceiptCount: - initialBatchSideEffectsBefore?.initialActionReceiptCount ?? 0, - initialBatchRecoveryPostRecoveryActionReceiptCount: - initialBatchSideEffectsAfter?.initialActionReceiptCount ?? 0, - initialBatchRecoveryConfirmationRestoredCount: - state.supervisorSwarm.initialBatchRecoveryConfirmationRestoredCount, - initialBatchRecoveryRunnerBootChanged: - initialBatchRecoveryRequired && - state.supervisorSwarm.initialBatchRecoveryOldRunnerBootId !== - state.supervisorSwarm.initialBatchRecoveryNewRunnerBootId, - initialBatchRecoveryPidfdClaimCount: - state.supervisorSwarm.initialBatchRecoveryPidfdClaimCount, - initialBatchRecoveryPidfdSignalCount: - state.supervisorSwarm.initialBatchRecoveryPidfdSignalCount, nativeDualDelegatePlanCount: protocol.nativeDualDelegatePlanCount, - mixedModeEnabled: Boolean(mixedState), - mixedSpawnActionCaptured: mixedState - ? isNonEmptyString(state.supervisorSwarm.mixedSpawnActionId) - : false, - mixedFollowupSpawnActionCaptured: mixedState - ? isNonEmptyString(state.supervisorSwarm.mixedFollowupSpawnActionId) - : false, - mixedSpawnRequestHashStable: mixedState - ? supervisorSwarmMixedSpawnRequestHashesStable(mixedState.groups) - : false, - mixedSpawnConfirmationRequiredCount: - actions.mixedSpawnConfirmationRequiredCount, - mixedSpawnApprovalCount: actions.mixedSpawnApprovalCount, - mixedSpawnConfirmationOrderValid: - actions.mixedSpawnConfirmationOrderValid, - staticIsolatedProviderOverlapObserved: mixedState - ? state.supervisorSwarm.staticIsolatedProviderOverlapObserved - : false, - staticIsolatedProviderRequestIdentityCount: mixedState - ? new Set(state.supervisorSwarm.staticIsolatedProviderRequestIds).size - : 0, - mixedParentIdentityStable: Boolean(mixedState), initialDeliveryCount: initial.length, repairDeliveryCount: repairs.length, totalDeliveryCount: deliveries.length, @@ -1329,50 +908,28 @@ export async function validateSupervisorSwarmEvidence() { finalQualityArtifactMatched: true, hostVerificationPassed: state.supervisorSwarm.hostVerificationPassed, changedProjectFileCount: changedPaths.length, - isolatedGroupCount: mixedState?.groups.length ?? 0, - isolatedInstanceCount: mixedState?.instances.length ?? 0, - isolatedTaskCount: mixedState?.isolatedTasks.length ?? 0, - isolatedResultCount: mixedState?.results.length ?? 0, - isolatedCompletedResultCount: - mixedState?.results.filter( - (record) => record.result?.status === 'completed', - ).length ?? 0, - isolatedJoinDeliveryCount: mixedState?.joinDeliveries.length ?? 0, - isolatedClaimedJoinCount: - mixedState?.joinDeliveries.filter( - (delivery) => delivery.status === 'claimed-by-parent', - ).length ?? 0, - isolatedParentWakeJoinCount: - mixedState?.joinDeliveries.filter( - (delivery) => isolatedJoinDeliveryTarget(delivery) === 'parent-wake', - ).length ?? 0, - isolatedJoinClaimJournalCount: mixedState?.joinClaims.length ?? 0, - isolatedObservedJoinClaimCount: - mixedState?.joinClaims.filter((claim) => claim.status === 'observed') - .length ?? 0, - isolatedMaxGroupsPerClaim: mixedState - ? Math.max( - 0, - ...mixedState.joinClaims.map((claim) => claim.joins.length), - ) - : 0, - isolatedClaimAuditCount: mixedState?.groups.length ?? 0, - isolatedClaimObservationCount: mixedState ? 1 : 0, - isolatedContinuationTaskCount: mixedState?.continuationTasks.length ?? 0, - isolatedContinuationAuditCount: - mixedState?.continuationAudits.length ?? 0, + isolatedGroupCount: 0, + isolatedInstanceCount: 0, + isolatedTaskCount: 0, + isolatedResultCount: 0, + isolatedCompletedResultCount: 0, + isolatedJoinDeliveryCount: 0, + isolatedClaimedJoinCount: 0, + isolatedParentWakeJoinCount: 0, + isolatedJoinClaimJournalCount: 0, + isolatedObservedJoinClaimCount: 0, + isolatedMaxGroupsPerClaim: 0, + isolatedClaimAuditCount: 0, + isolatedClaimObservationCount: 0, + isolatedContinuationTaskCount: 0, + isolatedContinuationAuditCount: 0, isolatedProjectMutationCount: changedPaths.filter((changedPath) => supervisorSwarmIsolatedReviews.some( (review) => review.path === changedPath, ), ).length, isolatedMutationActionCount: actions.isolatedMutationActionCount, - isolatedEvidenceFilesUnchanged: mixedState - ? isolatedEvidenceContents.every( - (content, index) => - content === supervisorSwarmIsolatedReviews[index].content, - ) - : false, + isolatedEvidenceFilesUnchanged: false, runnerKillBoundaryObserved: state.supervisorSwarm.runnerKillBoundaryObserved, runnerBootChanged: true, @@ -1382,26 +939,14 @@ export async function validateSupervisorSwarmEvidence() { claimIdentitiesStableAcrossRecovery: true, pendingActionIdentityStableAcrossRecovery: true, providerStartedCountStableAcrossRecovery: true, - isolatedGroupIdentityStableAcrossRecovery: mixedState - ? state.identityStable - : false, - isolatedInstanceIdentitiesStableAcrossRecovery: mixedState - ? state.identityStable - : false, - isolatedResultIdentitiesStableAcrossRecovery: mixedState - ? state.identityStable - : false, - isolatedJoinIdentityStableAcrossRecovery: mixedState - ? state.identityStable - : false, - parentContextStableAcrossRecovery: mixedState - ? state.identityStable - : false, - providerIdentitySetStableAcrossRecovery: mixedState - ? state.identityStable - : false, + isolatedGroupIdentityStableAcrossRecovery: false, + isolatedInstanceIdentitiesStableAcrossRecovery: false, + isolatedResultIdentitiesStableAcrossRecovery: false, + isolatedJoinIdentityStableAcrossRecovery: false, + parentContextStableAcrossRecovery: false, + providerIdentitySetStableAcrossRecovery: false, staticClaimObservationBeforeFinalization: true, - isolatedClaimObservationBeforeFinalization: mixedState ? true : false, + isolatedClaimObservationBeforeFinalization: false, confirmedActionCount: state.supervisorSwarm.confirmedActionCount, ...protocol, providerRequestIdentityCount: provider.requestIdentityCount, @@ -1450,47 +995,11 @@ export async function validateSupervisorSwarmEvidence() { duplicateDeliveryCount: duplicateCount( deliveries.map((delivery) => delivery.delegationId), ), - duplicateIsolatedGroupCount: mixedState - ? duplicateCount( - persistence.isolatedGroups.map((group) => group.delegationGroupId), - ) - : 0, - duplicateIsolatedInstanceCount: mixedState - ? duplicateCount( - persistence.isolatedInstances.map( - (instance) => instance.instanceId, - ), - ) - : 0, - duplicateIsolatedResultCount: mixedState - ? duplicateCount( - persistence.isolatedResults.map( - (record) => record.result?.instanceId, - ), - ) - : 0, - duplicateIsolatedJoinDeliveryCount: mixedState - ? duplicateCount( - persistence.isolatedJoinDeliveries.map( - (delivery) => delivery.delegationGroupId, - ), - ) - : 0, - duplicateIsolatedClaimAuditCount: mixedState - ? duplicateCount( - persistence.agentDb - .filter( - (record) => - record.recordType === - 'agent.runtime.agent.isolated_join.claimed_by_parent' && - record.agentId === projectSupervisorAgentId && - record.runId === state.initialRunId, - ) - .map( - (record) => `${record.delegationGroupId}:${record.actionId}`, - ), - ) - : 0, + duplicateIsolatedGroupCount: 0, + duplicateIsolatedInstanceCount: 0, + duplicateIsolatedResultCount: 0, + duplicateIsolatedJoinDeliveryCount: 0, + duplicateIsolatedClaimAuditCount: 0, duplicateMessageCount, duplicateActionLifecycleCount: actions.duplicateActionLifecycleCount, duplicateExecutingActionIdCount: actions.duplicateExecutingActionIdCount, diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/execution.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/execution.mjs index c8116694a..b810bbba2 100644 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/execution.mjs +++ b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/execution.mjs @@ -1,12 +1,4 @@ -import { - recordSupervisorSwarmChatSessionFailureDiagnostic, - requireOpenSupervisorSwarmChatSession, -} from './chat-session.mjs'; -import { - captureSupervisorSwarmCollaborationPolicySnapshotAndDrift, - captureSupervisorSwarmInitialProviderBatch, - restartSupervisorSwarmRunnerAtInitialBatchBoundary, -} from './collaboration-policy.mjs'; +import { captureSupervisorSwarmInitialProviderBatch } from './collaboration-policy.mjs'; import { validateSupervisorSwarmEvidence } from './evidence-validation.mjs'; import { driveSupervisorSwarmRuntimeToCompletion, @@ -21,28 +13,19 @@ import { import { assert, claimOwnedRunner, - codedError, ensureOwnedRunnerStableKillSupport, hashValue, - isNonEmptyString, - isPlainObject, prepareCliBinary, projectSupervisorAgentId, readRuntime, requestedRunId, runCli, - sleep, - startInteractiveCli, state, supervisorSwarmSessionId, - waitForInteractiveCliExit, - waitForInteractiveCliOutput, - writeInteractiveCliLine, } from './shared.mjs'; import { isSupervisorSwarmFinalReplyTransientRetrySuite, isSupervisorSwarmInitialTransientRetrySuite, - isSupervisorSwarmInteractiveChatSuite, isSupervisorSwarmToolPlanHandoffRunnerKillSuite, } from './suite-selection.mjs'; import { captureSupervisorSwarmToolPlanHandoffRunnerKillCheckpoint } from './tool-plan-handoff.mjs'; @@ -62,42 +45,20 @@ export async function runSupervisorSwarmE2e() { chars: [...task].length, sha256: hashValue(task), }; - if (isSupervisorSwarmInteractiveChatSuite()) { - state.isolatedRunner.launchAttempted = true; - state.supervisorSwarmCliSession = startInteractiveCli([ - '--swarm-chat', + state.initialRunId = requestedRunId; + state.initialSessionId = supervisorSwarmSessionId; + await runCli( + [ + '--agent-enqueue', '--init', state.projectRoot, - ]); - await waitForInteractiveCliOutput( - state.supervisorSwarmCliSession, - (output) => - output.includes('Agent Swarm Chat') && - output.includes(`父 Agent:${projectSupervisorAgentId}`), - 'supervisor-swarm-autonomous-chat-banner-timeout', - 30_000, - ); - state.supervisorSwarm.interactiveCliUsed = true; - writeInteractiveCliLine(state.supervisorSwarmCliSession, task); - const started = await waitForSupervisorSwarmAutonomousParentRuntime(task); - state.initialRunId = started.runId; - state.initialSessionId = started.sessionId; - } else { - state.initialRunId = requestedRunId; - state.initialSessionId = supervisorSwarmSessionId; - await runCli( - [ - '--agent-enqueue', - '--init', - state.projectRoot, - projectSupervisorAgentId, - state.initialRunId, - task, - ], - { timeoutMs: 120_000 }, - ); - state.isolatedRunner.launchAttempted = true; - } + projectSupervisorAgentId, + state.initialRunId, + task, + ], + { timeoutMs: 120_000 }, + ); + state.isolatedRunner.launchAttempted = true; await claimOwnedRunner(); const runtime = await readRuntime(projectSupervisorAgentId); assert( @@ -114,86 +75,11 @@ export async function runSupervisorSwarmE2e() { await captureSupervisorSwarmToolPlanHandoffRunnerKillCheckpoint(); } await captureSupervisorSwarmInitialProviderBatch(); - await captureSupervisorSwarmCollaborationPolicySnapshotAndDrift(); - await restartSupervisorSwarmRunnerAtInitialBatchBoundary(); await driveSupervisorSwarmToRepairKillBoundary(); if (isSupervisorSwarmFinalReplyTransientRetrySuite()) { await captureSupervisorSwarmTransientRetryCheckpoint(); } await driveSupervisorSwarmRuntimeToCompletion(); - if (isSupervisorSwarmInteractiveChatSuite()) { - await captureSupervisorSwarmAutonomousTurnReport(); - writeInteractiveCliLine(state.supervisorSwarmCliSession, '/quit'); - await waitForInteractiveCliExit(state.supervisorSwarmCliSession, 30_000); - state.supervisorSwarmCliSession = null; - } state.evidence = await validateSupervisorSwarmEvidence(); assert(state.evidence.secretLeakCount === 0, 'loaded-key-leak-detected'); } - -export async function waitForSupervisorSwarmAutonomousParentRuntime(task) { - const deadline = Date.now() + 120_000; - while (Date.now() < deadline) { - const runtime = await readRuntime(projectSupervisorAgentId).catch( - () => null, - ); - if ( - runtime?.agentId === projectSupervisorAgentId && - runtime.sessionId === supervisorSwarmSessionId && - isNonEmptyString(runtime.runId) && - runtime.currentTask === task - ) { - return runtime; - } - if (state.supervisorSwarmCliSession?.closed) { - recordSupervisorSwarmChatSessionFailureDiagnostic( - state.supervisorSwarmCliSession, - ); - throw codedError('supervisor-swarm-autonomous-chat-closed-before-run'); - } - await sleep(50); - } - throw codedError('supervisor-swarm-autonomous-parent-runtime-timeout'); -} - -export async function captureSupervisorSwarmAutonomousTurnReport() { - const session = requireOpenSupervisorSwarmChatSession(); - const output = await waitForInteractiveCliOutput( - session, - (value) => value.includes('[turn.report] '), - 'supervisor-swarm-autonomous-turn-report-timeout', - 60_000, - ); - const reportLines = output - .split(/\r?\n/u) - .filter((line) => line.startsWith('[turn.report] ')); - assert( - reportLines.length === 1, - 'supervisor-swarm-autonomous-turn-report-count-invalid', - ); - const report = JSON.parse(reportLines[0].slice('[turn.report] '.length)); - assert( - isPlainObject(report) && - JSON.stringify(Object.keys(report).sort()) === - JSON.stringify( - [ - 'schemaVersion', - 'outcome', - 'parentAgentId', - 'sessionId', - 'parentRunId', - 'runtimeCount', - 'busyRuntimeCount', - 'pendingTaskCount', - 'runningTaskCount', - 'waitingForConfirmationCount', - 'waitingForUserInputCount', - 'newAssistantMessageCount', - 'finalReplyChars', - 'reconciliationAgentCount', - ].sort(), - ), - 'supervisor-swarm-autonomous-turn-report-shape-invalid', - ); - state.supervisorSwarm.turnReport = report; -} diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/partial-evidence.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/partial-evidence.mjs index d076cf64d..c459c46a7 100644 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/partial-evidence.mjs +++ b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/partial-evidence.mjs @@ -1,5 +1,4 @@ import { - supervisorSwarmMixedSpawnRequestHashesStable, supervisorSwarmParentDeliveries, supervisorSwarmRelevantRunKeys, } from './collaboration-assertions.mjs'; @@ -8,7 +7,6 @@ import { duplicateSupervisorSwarmCollaborationPolicySnapshotCount, inspectSupervisorSwarmCollaborationPolicySnapshot, inspectSupervisorSwarmCollaborationPolicySnapshotBinding, - readSupervisorSwarmCollaborationPolicySnapshotBindingFile, readSupervisorSwarmCollaborationPolicySnapshotFile, supervisorSwarmCollaborationPolicyDriftObservationCount, supervisorSwarmExpectedCollaborationPolicySnapshot, @@ -59,17 +57,9 @@ import { supervisorSwarmQualityAgentId, supervisorSwarmQualityContent, supervisorSwarmQualityPath, - supervisorSwarmSessionId, supervisorSwarmWeakQualityContent, } from './shared.mjs'; -import { - isSupervisorSwarmCollaborationPolicyMixedRecoverySuite, - isSupervisorSwarmFinalReplyTransientRetrySuite, - isSupervisorSwarmMixedHarnessSuite, - isSupervisorSwarmMultiIsolatedHarnessSuite, - supervisorSwarmExpectedIsolatedReviewGroups, - supervisorSwarmInitialIsolatedReviewsForSuite, -} from './suite-selection.mjs'; +import { isSupervisorSwarmFinalReplyTransientRetrySuite } from './suite-selection.mjs'; export async function collectPartialSupervisorSwarmEvidence(baseEvidence) { const persistence = await readSupervisorSwarmPersistence({ @@ -125,127 +115,6 @@ export async function collectPartialSupervisorSwarmEvidence(baseEvidence) { record.recordType === 'agent.runtime.provider_request.lifecycle' && relevantRuns.has(`${record.agentId}\0${record.runId}`), ); - const indexedAgentDb = persistence.agentDb.map((record, index) => ({ - record, - index, - })); - const initialBatchActions = - state.supervisorSwarm.initialProviderBatch?.actions ?? []; - const mixedSpawnAction = initialBatchActions.find( - (action) => action.actionId === state.supervisorSwarm.mixedSpawnActionId, - ); - const matchesMixedInitialAction = (record, action) => - action != null && - record.agentId === projectSupervisorAgentId && - record.runId === state.initialRunId && - record.actionId === action.actionId && - record.actionFingerprint === action.actionFingerprint && - record.tool === action.tool; - const mixedSpawnConfirmationRequirements = indexedAgentDb.filter( - ({ record }) => - record.recordType === - 'agent.runtime.provider_action_batch.confirmation_required' && - record.sessionId === supervisorSwarmSessionId && - record.batchId === state.supervisorSwarm.initialProviderBatch?.batchId && - matchesMixedInitialAction(record, mixedSpawnAction), - ); - const mixedSpawnApprovals = indexedAgentDb.filter( - ({ record }) => - record.recordType === 'agent.runtime.tool_confirmation.approved' && - record.sessionId === supervisorSwarmSessionId && - record.confirmedRunId === state.initialRunId && - matchesMixedInitialAction(record, mixedSpawnAction), - ); - const mixedDelegateExecutionIndexes = initialBatchActions - .filter((action) => action.tool === 'agent.delegate') - .flatMap((action) => - indexedAgentDb - .filter( - ({ record }) => - record.recordType === 'agent.runtime.tool_action.executing' && - matchesMixedInitialAction(record, action), - ) - .map(({ index }) => index), - ); - const mixedSpawnReceiptIndexes = indexedAgentDb - .filter( - ({ record }) => - record.recordType === 'agent.runtime.action_receipt' && - record.sessionId === supervisorSwarmSessionId && - record.status === 'ok' && - matchesMixedInitialAction(record, mixedSpawnAction), - ) - .map(({ index }) => index); - const mixedInitialSpawnConfirmationOrderValid = - mixedSpawnConfirmationRequirements.length === 1 && - mixedSpawnApprovals.length === 1 && - mixedDelegateExecutionIndexes.length === 2 && - mixedSpawnReceiptIndexes.length === 1 && - mixedSpawnConfirmationRequirements[0].index < - mixedSpawnApprovals[0].index && - mixedDelegateExecutionIndexes.every( - (index) => mixedSpawnApprovals[0].index < index, - ) && - mixedSpawnApprovals[0].index < mixedSpawnReceiptIndexes[0]; - const matchesMixedFollowupIdentity = (record) => - isNonEmptyString(state.supervisorSwarm.mixedFollowupSpawnActionId) && - record.agentId === projectSupervisorAgentId && - (record.runId === state.initialRunId || - record.confirmedRunId === state.initialRunId) && - record.actionId === state.supervisorSwarm.mixedFollowupSpawnActionId; - const matchesMixedFollowupAction = (record) => - matchesMixedFollowupIdentity(record) && - (record.tool === 'agent.spawn_isolated' || - record.commandId === 'agent.spawn_isolated'); - const mixedFollowupConfirmationRequirements = indexedAgentDb.filter( - ({ record }) => - record.recordType === - 'agent.runtime.provider_action_batch.confirmation_required' && - record.sessionId === supervisorSwarmSessionId && - matchesMixedFollowupAction(record), - ); - const mixedFollowupApprovals = indexedAgentDb.filter( - ({ record }) => - record.recordType === 'agent.runtime.tool_confirmation.approved' && - record.sessionId === supervisorSwarmSessionId && - matchesMixedFollowupAction(record), - ); - const mixedFollowupSideEffects = indexedAgentDb.filter( - ({ record }) => - record.recordType === 'agent.runtime.agent.spawn_isolated' && - matchesMixedFollowupIdentity(record), - ); - const mixedFollowupReceiptIndexes = indexedAgentDb - .filter( - ({ record }) => - record.recordType === 'agent.runtime.action_receipt' && - record.status === 'ok' && - matchesMixedFollowupAction(record), - ) - .map(({ index }) => index); - const mixedFollowupObservationIndexes = indexedAgentDb - .filter( - ({ record }) => - record.recordType === 'agent.runtime.tool_observation' && - record.status === 'ok' && - matchesMixedFollowupAction(record), - ) - .map(({ index }) => index); - const mixedFollowupSpawnConfirmationOrderValid = - mixedFollowupConfirmationRequirements.length === 1 && - mixedFollowupApprovals.length === 1 && - mixedFollowupSideEffects.length === 1 && - mixedFollowupReceiptIndexes.length === 1 && - mixedFollowupObservationIndexes.length === 1 && - mixedFollowupConfirmationRequirements[0].index < - mixedFollowupApprovals[0].index && - mixedFollowupApprovals[0].index < mixedFollowupSideEffects[0].index && - mixedFollowupSideEffects[0].index < mixedFollowupReceiptIndexes[0] && - mixedFollowupReceiptIndexes[0] < mixedFollowupObservationIndexes[0]; - const mixedSpawnConfirmationOrderValid = - mixedInitialSpawnConfirmationOrderValid && - (!isSupervisorSwarmMultiIsolatedHarnessSuite() || - mixedFollowupSpawnConfirmationOrderValid); const toleratePartialRead = async ( surface, reader, @@ -278,7 +147,6 @@ export async function collectPartialSupervisorSwarmEvidence(baseEvidence) { isolatedEvidenceContents, changedFiles, collaborationPolicySnapshotFile, - collaborationPolicySnapshotBindingFile, collaborationPolicySidecar, finalReplyResponseStream, ] = await Promise.all([ @@ -359,14 +227,6 @@ export async function collectPartialSupervisorSwarmEvidence(baseEvidence) { : null, null, ), - toleratePartialRead( - 'collaboration-policy-snapshot-binding', - () => - supervisorSwarmInitialBatchBindsPolicySnapshot() - ? readSupervisorSwarmCollaborationPolicySnapshotBindingFile() - : null, - null, - ), toleratePartialRead( 'collaboration-policy-sidecar', () => readJson(supervisorSwarmCollaborationPolicyPath()), @@ -469,63 +329,12 @@ export async function collectPartialSupervisorSwarmEvidence(baseEvidence) { partialCollaborationPolicySnapshots[0], supervisorSwarmExpectedCollaborationPolicySnapshot(), ); - const partialCollaborationPolicySnapshotBytesStable = - isNonEmptyString( - state.supervisorSwarm.collaborationPolicySnapshotInitialBytes, - ) && - collaborationPolicySnapshotFile?.bytes === - state.supervisorSwarm.collaborationPolicySnapshotInitialBytes && - hashValue(collaborationPolicySnapshotFile.bytes) === - state.supervisorSwarm.collaborationPolicySnapshotInitialBytesSha256; - const partialCollaborationPolicySnapshotFieldsStable = - isPlainObject( - state.supervisorSwarm.collaborationPolicySnapshotInitialRecord, - ) && - isPlainObject(collaborationPolicySnapshotFile?.snapshot) && - JSON.stringify(collaborationPolicySnapshotFile.snapshot) === - JSON.stringify( - state.supervisorSwarm.collaborationPolicySnapshotInitialRecord, - ) && - JSON.stringify(collaborationPolicySnapshotFile.snapshot) === - JSON.stringify(partialCollaborationPolicySnapshots[0]); - const partialCollaborationPolicySnapshotFingerprintStable = - partialCollaborationPolicySnapshotFieldsStable && - collaborationPolicySnapshotFile.snapshot.snapshotFingerprint === - state.supervisorSwarm.collaborationPolicySnapshotInitialRecord - .snapshotFingerprint && - partialCollaborationPolicySnapshotInspection.identityHash === - state.supervisorSwarm.collaborationPolicySnapshotInitialIdentityHash; - const partialCollaborationPolicySnapshotPolicyFingerprintStable = - partialCollaborationPolicySnapshotFieldsStable && - collaborationPolicySnapshotFile.snapshot.policyFingerprint === - state.supervisorSwarm.collaborationPolicySnapshotInitialRecord - .policyFingerprint; const partialCollaborationPolicySnapshotBindingInspection = inspectSupervisorSwarmCollaborationPolicySnapshotBinding( partialCollaborationPolicySnapshotBindings[0], collaborationPolicySnapshotFile?.snapshot ?? partialCollaborationPolicySnapshots[0], ); - const partialCollaborationPolicySnapshotBindingBytesStable = - isNonEmptyString( - state.supervisorSwarm.collaborationPolicySnapshotBindingInitialBytes, - ) && - collaborationPolicySnapshotBindingFile?.bytes === - state.supervisorSwarm.collaborationPolicySnapshotBindingInitialBytes && - hashValue(collaborationPolicySnapshotBindingFile.bytes) === - state.supervisorSwarm - .collaborationPolicySnapshotBindingInitialBytesSha256; - const partialCollaborationPolicySnapshotBindingFieldsStable = - isPlainObject( - state.supervisorSwarm.collaborationPolicySnapshotBindingInitialRecord, - ) && - isPlainObject(collaborationPolicySnapshotBindingFile?.binding) && - JSON.stringify(collaborationPolicySnapshotBindingFile.binding) === - JSON.stringify( - state.supervisorSwarm.collaborationPolicySnapshotBindingInitialRecord, - ) && - JSON.stringify(collaborationPolicySnapshotBindingFile.binding) === - JSON.stringify(partialCollaborationPolicySnapshotBindings[0]); const partialCollaborationPolicyDriftObservationCount = supervisorSwarmCollaborationPolicyDriftObservationCount( persistence.agentDb, @@ -538,62 +347,9 @@ export async function collectPartialSupervisorSwarmEvidence(baseEvidence) { duplicateSupervisorSwarmCollaborationPolicySnapshotBindingCount( partialCollaborationPolicySnapshotBindings, ); - const partialChatSessionFailureDiagnostic = - state.supervisorSwarm.chatSessionFailureDiagnostic; - const partialIdentityBefore = - state.supervisorSwarm.initialBatchRecoveryPreKillIdentity; - const partialIdentityAfter = - state.supervisorSwarm.initialBatchRecoveryPostRecoveryIdentity; - const partialSideEffectsBefore = - state.supervisorSwarm.initialBatchRecoveryPreKillSideEffects; - const partialSideEffectsAfter = - state.supervisorSwarm.initialBatchRecoveryPostRecoverySideEffects; - const partialRecoveryRequired = - isSupervisorSwarmCollaborationPolicyMixedRecoverySuite(); - const zeroSideEffectFields = [ - 'deliveryCount', - 'claimCount', - 'isolatedGroupCount', - 'isolatedChildCount', - 'isolatedResultCount', - 'isolatedJoinDeliveryCount', - 'delegatedChildTaskCount', - 'projectRevision', - 'projectModifiedPathCount', - 'projectMutationActionCount', - 'initialActionExecutionCount', - 'initialActionReceiptCount', - 'initialActionSideEffectCount', - 'recoveryFailedCount', - ]; - const partialRecoveryZeroSideEffects = - partialRecoveryRequired && - partialSideEffectsBefore != null && - partialSideEffectsAfter != null && - zeroSideEffectFields.every( - (field) => - partialSideEffectsBefore[field] === 0 && - partialSideEffectsAfter[field] === 0, - ); return buildSupervisorSwarmEvidence({ ...baseEvidence, - scenario: partialRecoveryRequired - ? 'project-supervisor-collaboration-policy-mixed-initial-batch-recovery' - : baseEvidence.scenario, - interactiveCliUsed: state.supervisorSwarm.interactiveCliUsed, - chatSessionUnexpectedlyClosed: Boolean(partialChatSessionFailureDiagnostic), - chatSessionFailureKind: - partialChatSessionFailureDiagnostic?.failureKind ?? 'none', - chatSessionExitCode: - partialChatSessionFailureDiagnostic?.exitCode ?? 'none', - chatSessionCloseSignal: - partialChatSessionFailureDiagnostic?.signal ?? 'none', - chatSessionProcessErrorCode: - partialChatSessionFailureDiagnostic?.processErrorCode ?? 'none', - chatSessionStderrChars: - partialChatSessionFailureDiagnostic?.stderrChars ?? 0, - chatSessionStderrSha256: - partialChatSessionFailureDiagnostic?.stderrSha256 ?? 'none', + scenario: baseEvidence.scenario, providerModel: state.supervisorSwarm.effectiveModel, providerApiKind: state.supervisorSwarm.effectiveApiKind, providerReasoningEffort: state.supervisorSwarm.effectiveReasoningEffort, @@ -701,12 +457,8 @@ export async function collectPartialSupervisorSwarmEvidence(baseEvidence) { ?.orchestratorOnlyAfterDelegation ?? false, initialCollaborationContractCountsMatched: partialCollaborationContract?.initialStaticAgentIds?.length === 2 && - partialCollaborationContract?.isolatedSpawnCount === - (isSupervisorSwarmMixedHarnessSuite() ? 1 : 0) && - partialCollaborationContract?.isolatedChildCount === - (isSupervisorSwarmMixedHarnessSuite() - ? supervisorSwarmInitialIsolatedReviewsForSuite().length - : 0), + partialCollaborationContract?.isolatedSpawnCount === 0 && + partialCollaborationContract?.isolatedChildCount === 0, initialCollaborationPolicyFingerprint: partialCollaborationContract?.policyFingerprint ?? null, initialCollaborationContractFingerprint: @@ -750,19 +502,6 @@ export async function collectPartialSupervisorSwarmEvidence(baseEvidence) { partialCollaborationPolicySnapshotInspection.identityHashMatched, collaborationPolicySnapshotBoundAtValid: partialCollaborationPolicySnapshotInspection.boundAtValid, - collaborationPolicySnapshotBytesStable: - partialCollaborationPolicySnapshotBytesStable, - collaborationPolicySnapshotFieldsStable: - partialCollaborationPolicySnapshotFieldsStable, - collaborationPolicySnapshotFingerprintStable: - partialCollaborationPolicySnapshotFingerprintStable, - collaborationPolicySnapshotPolicyFingerprintStable: - partialCollaborationPolicySnapshotPolicyFingerprintStable, - collaborationPolicySnapshotStable: - partialCollaborationPolicySnapshotBytesStable && - partialCollaborationPolicySnapshotFieldsStable && - partialCollaborationPolicySnapshotFingerprintStable && - partialCollaborationPolicySnapshotPolicyFingerprintStable, collaborationPolicySnapshotBindingCaptured: partialCollaborationPolicySnapshotRequired && partialCollaborationPolicySnapshotBindingInspection.exactShape, @@ -781,16 +520,6 @@ export async function collectPartialSupervisorSwarmEvidence(baseEvidence) { partialCollaborationPolicySnapshotBindingInspection.boundAtMatched, collaborationPolicySnapshotBindingSnapshotMatched: partialCollaborationPolicySnapshotBindingInspection.snapshotMatched, - collaborationPolicySnapshotBindingBytesStable: - partialCollaborationPolicySnapshotBindingBytesStable, - collaborationPolicySnapshotBindingFieldsStable: - partialCollaborationPolicySnapshotBindingFieldsStable, - collaborationPolicySnapshotBindingStable: - partialCollaborationPolicySnapshotBindingBytesStable && - partialCollaborationPolicySnapshotBindingFieldsStable && - partialCollaborationPolicySnapshotBindingInspection.snapshotMatched, - collaborationPolicyDriftFixtureWritten: - state.supervisorSwarm.collaborationPolicyDriftFixtureWritten, collaborationPolicySidecarMinIsolatedGroupsBeforeClaim: collaborationPolicySidecar?.minIsolatedGroupsBeforeClaim ?? 0, collaborationPolicyDriftStatusObserved: @@ -805,142 +534,6 @@ export async function collectPartialSupervisorSwarmEvidence(baseEvidence) { residualSidecars.collaborationPolicySnapshotArtifacts, collaborationPolicySnapshotBindingResidualArtifactCount: residualSidecars.collaborationPolicySnapshotBindingArtifacts, - initialBatchRecoveryRequired: partialRecoveryRequired, - initialBatchRecoveryBoundaryObserved: - state.supervisorSwarm.initialBatchRecoveryBoundaryObserved, - initialBatchRecoveryBatchIdStable: - partialRecoveryRequired && - partialIdentityBefore?.batchId === partialIdentityAfter?.batchId, - initialBatchRecoveryPolicyFingerprintStable: - partialRecoveryRequired && - partialIdentityBefore?.policyFingerprint === - partialIdentityAfter?.policyFingerprint, - initialBatchRecoveryContractFingerprintStable: - partialRecoveryRequired && - partialIdentityBefore?.contractFingerprint === - partialIdentityAfter?.contractFingerprint, - initialBatchRecoveryAllActionIdsStable: - partialRecoveryRequired && - JSON.stringify( - partialIdentityBefore?.actions?.map((action) => action.actionId), - ) === - JSON.stringify( - partialIdentityAfter?.actions?.map((action) => action.actionId), - ), - initialBatchRecoveryProviderStartedIdentitySetStable: - partialRecoveryRequired && - JSON.stringify(partialSideEffectsBefore?.providerStartedIdentities) === - JSON.stringify(partialSideEffectsAfter?.providerStartedIdentities), - initialBatchRecoveryWaitingConfirmationStable: - partialRecoveryRequired && - partialIdentityBefore?.status === 'waiting-confirmation' && - partialIdentityBefore?.nextActionIndex === 0 && - partialIdentityAfter?.status === 'waiting-confirmation' && - partialIdentityAfter?.nextActionIndex === 0, - initialBatchRecoveryZeroSideEffects: partialRecoveryZeroSideEffects, - initialBatchRecoveryPreKillDeliveryCount: - partialSideEffectsBefore?.deliveryCount ?? 0, - initialBatchRecoveryPostRecoveryDeliveryCount: - partialSideEffectsAfter?.deliveryCount ?? 0, - initialBatchRecoveryPreKillGroupCount: - partialSideEffectsBefore?.isolatedGroupCount ?? 0, - initialBatchRecoveryPostRecoveryGroupCount: - partialSideEffectsAfter?.isolatedGroupCount ?? 0, - initialBatchRecoveryPreKillChildCount: - partialSideEffectsBefore?.isolatedChildCount ?? 0, - initialBatchRecoveryPostRecoveryChildCount: - partialSideEffectsAfter?.isolatedChildCount ?? 0, - initialBatchRecoveryPreKillProjectRevision: - partialSideEffectsBefore?.projectRevision ?? 0, - initialBatchRecoveryPostRecoveryProjectRevision: - partialSideEffectsAfter?.projectRevision ?? 0, - initialBatchRecoveryPreKillProjectModificationCount: - partialSideEffectsBefore?.projectModifiedPathCount ?? 0, - initialBatchRecoveryPostRecoveryProjectModificationCount: - partialSideEffectsAfter?.projectModifiedPathCount ?? 0, - initialBatchRecoveryPreKillActionExecutionCount: - partialSideEffectsBefore?.initialActionExecutionCount ?? 0, - initialBatchRecoveryPostRecoveryActionExecutionCount: - partialSideEffectsAfter?.initialActionExecutionCount ?? 0, - initialBatchRecoveryPreKillActionReceiptCount: - partialSideEffectsBefore?.initialActionReceiptCount ?? 0, - initialBatchRecoveryPostRecoveryActionReceiptCount: - partialSideEffectsAfter?.initialActionReceiptCount ?? 0, - initialBatchRecoveryConfirmationRestoredCount: - state.supervisorSwarm.initialBatchRecoveryConfirmationRestoredCount, - initialBatchRecoveryRunnerBootChanged: - partialRecoveryRequired && - isNonEmptyString( - state.supervisorSwarm.initialBatchRecoveryOldRunnerBootId, - ) && - isNonEmptyString( - state.supervisorSwarm.initialBatchRecoveryNewRunnerBootId, - ) && - state.supervisorSwarm.initialBatchRecoveryOldRunnerBootId !== - state.supervisorSwarm.initialBatchRecoveryNewRunnerBootId, - initialBatchRecoveryPidfdClaimCount: - state.supervisorSwarm.initialBatchRecoveryPidfdClaimCount, - initialBatchRecoveryPidfdSignalCount: - state.supervisorSwarm.initialBatchRecoveryPidfdSignalCount, - mixedModeEnabled: isSupervisorSwarmMixedHarnessSuite(), - mixedSpawnActionCaptured: isNonEmptyString( - state.supervisorSwarm.mixedSpawnActionId, - ), - mixedFollowupSpawnActionCaptured: isNonEmptyString( - state.supervisorSwarm.mixedFollowupSpawnActionId, - ), - mixedSpawnRequestHashStable: supervisorSwarmMixedSpawnRequestHashesStable( - isolatedRecords.groups, - ), - mixedSpawnConfirmationRequiredCount: - mixedSpawnConfirmationRequirements.length + - mixedFollowupConfirmationRequirements.length, - mixedSpawnApprovalCount: - mixedSpawnApprovals.length + mixedFollowupApprovals.length, - mixedSpawnConfirmationOrderValid, - nativeMixedCollaborationPlanCount: persistence.agentDb.filter( - (record) => - record.recordType === 'agent.runtime.tool_plan.protocol' && - record.agentId === projectSupervisorAgentId && - record.runId === state.initialRunId && - Array.isArray(record.functionNames) && - record.functionNames.filter( - (name) => name === 'runtime_tool_agent_delegate', - ).length === 2 && - record.functionNames.filter( - (name) => name === 'runtime_tool_agent_spawn_isolated', - ).length === 1, - ).length, - nativeFollowupIsolatedPlanCount: - isSupervisorSwarmMultiIsolatedHarnessSuite() - ? persistence.agentDb.filter( - (record) => - record.recordType === 'agent.runtime.tool_plan.protocol' && - record.agentId === projectSupervisorAgentId && - record.runId === state.initialRunId && - Array.isArray(record.functionNames) && - record.functionNames.filter( - (name) => name === 'runtime_tool_agent_delegate', - ).length === 0 && - record.functionNames.filter( - (name) => name === 'runtime_tool_agent_spawn_isolated', - ).length === 1, - ).length - : 0, - staticIsolatedProviderOverlapObserved: - state.supervisorSwarm.staticIsolatedProviderOverlapObserved, - staticIsolatedProviderRequestIdentityCount: new Set( - state.supervisorSwarm.staticIsolatedProviderRequestIds, - ).size, - mixedParentIdentityStable: - isolatedRecords.groups.length === - supervisorSwarmExpectedIsolatedReviewGroups().length && - isolatedRecords.groups.every( - (group) => - group.parentAgentId === projectSupervisorAgentId && - group.parentSessionId === supervisorSwarmSessionId && - group.parentRunId === state.initialRunId, - ), initialDeliveryCount: initial.length, repairDeliveryCount: repairs.length, repairDeliveryStatus: observedRepairStatus, diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/persistence.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/persistence.mjs index 278c7acf3..c2ab0f2fc 100644 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/persistence.mjs +++ b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/persistence.mjs @@ -6,10 +6,8 @@ import { codedError, collectPartialRuntimeJsonlSurface, fs, - hashJsonValue, hashValue, isNonEmptyString, - isolatedJoinDeliveryTarget, isPlainObject, listFiles, path, @@ -586,104 +584,6 @@ export function supervisorSwarmTaskIdentity(task) { }; } -export function supervisorSwarmIsolatedGroupIdentity(group) { - return { - schemaVersion: group.schemaVersion, - parentAgentId: group.parentAgentId, - parentSessionId: group.parentSessionId, - parentRunId: group.parentRunId, - parentActionId: group.parentActionId, - delegationGroupId: group.delegationGroupId, - joinRunId: group.joinRunId, - depth: group.depth, - joinMode: group.joinMode, - requestSha256: hashJsonValue(group.request ?? null), - instanceIds: [...(group.instanceIds ?? [])].sort(), - createdAt: group.createdAt, - }; -} - -export function supervisorSwarmIsolatedInstanceIdentity(instance) { - return { - schemaVersion: instance.schemaVersion, - parentAgentId: instance.parentAgentId, - parentSessionId: instance.parentSessionId, - parentRunId: instance.parentRunId, - parentActionId: instance.parentActionId, - delegationGroupId: instance.delegationGroupId, - delegationId: instance.delegationId, - childIndex: instance.childIndex, - instanceId: instance.instanceId, - templateAgentId: instance.templateAgentId, - sessionId: instance.sessionId, - runId: instance.runId, - depth: instance.depth, - taskSha256: hashValue(instance.task ?? ''), - acceptanceCriteriaSha256: hashValue( - JSON.stringify(instance.acceptanceCriteria ?? []), - ), - expectedArtifacts: instance.expectedArtifacts, - writeScopes: instance.writeScopes, - createdAt: instance.createdAt, - }; -} - -export function supervisorSwarmIsolatedResultIdentity(record) { - const result = record.result ?? {}; - return { - schemaVersion: record.schemaVersion, - delegationGroupId: record.delegationGroupId, - childIndex: record.childIndex, - delegationId: result.delegationId, - instanceId: result.instanceId, - templateAgentId: result.templateAgentId, - runId: result.runId, - status: result.status, - summarySha256: hashValue(result.summary ?? ''), - artifacts: result.artifacts, - evidenceSha256: hashValue(JSON.stringify(result.evidence ?? [])), - verifiedRevision: result.verifiedRevision ?? null, - errorSha256: result.error == null ? null : hashValue(result.error), - recordedAt: record.recordedAt, - }; -} - -export function supervisorSwarmIsolatedJoinIdentity(delivery) { - return { - schemaVersion: delivery.schemaVersion, - parentAgentId: delivery.parentAgentId, - parentRunId: delivery.parentRunId, - delegationGroupId: delivery.delegationGroupId, - joinRunId: delivery.joinRunId, - status: delivery.status, - deliveryTarget: isolatedJoinDeliveryTarget(delivery), - queuedRunId: delivery.queuedRunId ?? null, - claimedByActionId: delivery.claimedByActionId ?? null, - updatedAt: delivery.updatedAt, - }; -} - -export function supervisorSwarmIsolatedJoinClaimIdentity(claim) { - return { - schemaVersion: claim.schemaVersion, - parentAgentId: claim.parentAgentId, - parentRunId: claim.parentRunId, - actionId: claim.actionId, - status: claim.status, - joins: (claim.joins ?? []) - .map((join) => ({ - parentActionId: join.parentActionId, - delegationGroupId: join.delegationGroupId, - joinRunId: join.joinRunId, - source: join.source, - })) - .sort((left, right) => - left.delegationGroupId.localeCompare(right.delegationGroupId), - ), - updatedAt: claim.updatedAt, - }; -} - export function supervisorSwarmParentIsolatedRecords(persistence) { const groups = (persistence.isolatedGroups ?? []).filter( (group) => diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/protocol-validation.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/protocol-validation.mjs index dbe6a7882..43c080801 100644 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/protocol-validation.mjs +++ b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/protocol-validation.mjs @@ -22,8 +22,6 @@ import { } from './shared.mjs'; import { isSupervisorSwarmFinalReplyTransientRetrySuite, - isSupervisorSwarmMixedHarnessSuite, - isSupervisorSwarmMultiIsolatedHarnessSuite, isSupervisorSwarmTransientRetrySuite, } from './suite-selection.mjs'; @@ -403,12 +401,10 @@ export function validateSupervisorSwarmNativeProtocol( toolPlanRepairEvidenceHasNoFatalLocalRepair(repairEvidence), 'supervisor-swarm-native-tool-protocol-required', ); - const mixed = isSupervisorSwarmMixedHarnessSuite(); - const expectedActionFunctionCount = mixed ? 3 : 2; + const expectedActionFunctionCount = 2; const allowedInitialFunctionNames = new Set([ 'update_agent_plan', 'runtime_tool_agent_delegate', - ...(mixed ? ['runtime_tool_agent_spawn_isolated'] : []), ]); const initialMultiCall = protocols.filter((record) => { if (!Array.isArray(record.functionNames)) return false; @@ -428,7 +424,7 @@ export function validateSupervisorSwarmNativeProtocol( ).length === 2 && actionFunctionNames.filter( (name) => name === 'runtime_tool_agent_spawn_isolated', - ).length === (mixed ? 1 : 0) && + ).length === 0 && record.functionNames.every((name) => allowedInitialFunctionNames.has(name), ) && @@ -445,40 +441,6 @@ export function validateSupervisorSwarmNativeProtocol( initialMultiCall.length === 1, 'supervisor-swarm-native-collaboration-plan-count-invalid', ); - const followupIsolatedPlans = isSupervisorSwarmMultiIsolatedHarnessSuite() - ? protocols.filter((record) => { - if (!Array.isArray(record.functionNames)) return false; - const actionFunctionNames = record.functionNames.filter((name) => - name.startsWith('runtime_tool_'), - ); - return ( - record.agentId === projectSupervisorAgentId && - record.sessionId === supervisorSwarmSessionId && - record.runId === state.initialRunId && - record.loopIteration > - state.supervisorSwarm.initialProviderBatch.loopIteration && - actionFunctionNames.length === 1 && - actionFunctionNames[0] === 'runtime_tool_agent_spawn_isolated' && - record.functionNames.every((name) => - ['update_agent_plan', 'runtime_tool_agent_spawn_isolated'].includes( - name, - ), - ) && - record.functionNames.filter((name) => name === 'update_agent_plan') - .length <= 1 && - hasValidToolPlanProtocolCallProjection(record) && - record.functionCallCount === record.functionNames.length && - Array.isArray(record.callIdSha256s) && - record.callIdSha256s.length === record.functionCallCount && - new Set(record.callIdSha256s).size === record.callIdSha256s.length - ); - }) - : []; - assert( - !isSupervisorSwarmMultiIsolatedHarnessSuite() || - followupIsolatedPlans.length === 1, - 'supervisor-swarm-native-followup-isolated-plan-count-invalid', - ); return { toolPlanProtocolCount: relevant.filter( (record) => record.recordType === 'agent.runtime.tool_plan.protocol', @@ -496,10 +458,6 @@ export function validateSupervisorSwarmNativeProtocol( (record) => record.protocol === 'text_json', ).length, nativeDualDelegatePlanCount: initialMultiCall.length, - nativeMixedCollaborationPlanCount: isSupervisorSwarmMixedHarnessSuite() - ? initialMultiCall.length - : 0, - nativeFollowupIsolatedPlanCount: followupIsolatedPlans.length, }; } @@ -637,9 +595,7 @@ export function validateSupervisorSwarmActionPersistence( } const initialBatchActions = state.supervisorSwarm.initialProviderBatch?.actions ?? []; - const expectedInitialActionCount = isSupervisorSwarmMixedHarnessSuite() - ? 3 - : 2; + const expectedInitialActionCount = 2; assert( initialBatchActions.length === expectedInitialActionCount, 'supervisor-swarm-initial-batch-action-count-invalid', @@ -651,284 +607,20 @@ export function validateSupervisorSwarmActionPersistence( record.actionId === action.actionId && record.actionFingerprint === action.actionFingerprint && record.tool === action.tool; - let mixedSpawnConfirmationRequiredCount = 0; - let mixedSpawnApprovalCount = 0; - let mixedSpawnConfirmationOrderValid = false; - let mixedInitialSpawnTerminalObservationIndex = -1; - if (isSupervisorSwarmMixedHarnessSuite()) { - const delegateActions = initialBatchActions.filter( - (action) => action.tool === 'agent.delegate', - ); - const spawnActions = initialBatchActions.filter( + assert( + initialBatchActions.every( (action) => - action.tool === 'agent.spawn_isolated' && - action.actionId === state.supervisorSwarm.mixedSpawnActionId, - ); - const spawnAction = spawnActions[0]; - const indexedAgentDb = agentDb.map((record, index) => ({ record, index })); - const initialActionTimeline = (action) => { - const auto = action.tool === 'agent.delegate'; - const executions = indexedAgentDb.filter( - ({ record }) => - record.recordType === 'agent.runtime.tool_action.executing' && - record.executionMode === 'auto' && - matchesInitialBatchAction(record, action), - ); - const autoObserved = indexedAgentDb.filter( - ({ record }) => - record.recordType === 'agent.runtime.tool_action.observed' && - record.executionMode === 'auto' && - record.observationStatus === 'ok' && - matchesInitialBatchAction(record, action), - ); - const receipts = indexedAgentDb.filter( - ({ record }) => - record.recordType === 'agent.runtime.action_receipt' && - record.sessionId === supervisorSwarmSessionId && - record.executionMode === (auto ? 'auto' : 'confirmation') && - record.status === 'ok' && - matchesInitialBatchAction(record, action), - ); - const terminalObservations = indexedAgentDb.filter( - ({ record }) => - record.recordType === 'agent.runtime.tool_observation' && - record.status === 'ok' && - record.decision === (auto ? 'auto' : 'approved') && - matchesInitialBatchAction(record, action), - ); - let sideEffects = []; - if (auto) { - const actionDeliveries = deliveries.filter( - (delivery) => - delivery.parentAgentId === projectSupervisorAgentId && - delivery.parentSessionId === supervisorSwarmSessionId && - delivery.parentRunId === state.initialRunId && - delivery.parentActionId === action.actionId && - delivery.repairOfDelegationId == null, - ); - sideEffects = - actionDeliveries.length === 1 - ? indexedAgentDb.filter( - ({ record }) => - record.recordType === 'agent.runtime.agent.delegate' && - record.parentRunId === state.initialRunId && - record.delegationId === actionDeliveries[0].delegationId && - record.targetAgentId === actionDeliveries[0].targetAgentId && - record.runId === actionDeliveries[0].targetRunId, - ) - : []; - } else { - sideEffects = indexedAgentDb.filter( - ({ record }) => - record.recordType === 'agent.runtime.agent.spawn_isolated' && - record.agentId === projectSupervisorAgentId && + executing.filter((record) => matchesInitialBatchAction(record, action)) + .length === 1 && + receipts.filter( + (record) => record.sessionId === supervisorSwarmSessionId && - record.runId === state.initialRunId && - record.actionId === action.actionId, - ); - } - return { - action, - auto, - executions, - autoObserved, - receipts, - terminalObservations, - sideEffects, - }; - }; - const actionTimelines = initialBatchActions - .map(initialActionTimeline) - .sort( - (left, right) => left.action.actionIndex - right.action.actionIndex, - ); - const delegateTimelines = actionTimelines.filter(({ auto }) => auto); - const spawnTimeline = actionTimelines.find(({ auto }) => !auto); - const confirmationRequired = spawnAction - ? indexedAgentDb.filter( - ({ record }) => - record.recordType === - 'agent.runtime.provider_action_batch.confirmation_required' && - record.agentId === projectSupervisorAgentId && - record.sessionId === supervisorSwarmSessionId && - record.runId === state.initialRunId && - record.batchId === - state.supervisorSwarm.initialProviderBatch.batchId && - record.actionCount === expectedInitialActionCount && - record.actionIndex === spawnAction.actionIndex && - matchesInitialBatchAction(record, spawnAction), - ) - : []; - const approvals = spawnAction - ? indexedAgentDb.filter( - ({ record }) => - record.recordType === 'agent.runtime.tool_confirmation.approved' && - record.sessionId === supervisorSwarmSessionId && - record.confirmedRunId === state.initialRunId && - record.commandId === 'agent.spawn_isolated' && - matchesInitialBatchAction(record, spawnAction), - ) - : []; - const actionLifecycleOrderValid = actionTimelines.every((timeline) => { - if ( - timeline.sideEffects.length !== 1 || - timeline.receipts.length !== 1 || - timeline.terminalObservations.length !== 1 - ) { - return false; - } - if (timeline.auto) { - return ( - timeline.executions.length === 1 && - timeline.autoObserved.length === 1 && - timeline.executions[0].index < timeline.sideEffects[0].index && - timeline.sideEffects[0].index < timeline.autoObserved[0].index && - timeline.autoObserved[0].index < timeline.receipts[0].index && - timeline.receipts[0].index < timeline.terminalObservations[0].index - ); - } - return ( - timeline.executions.length === 0 && - timeline.autoObserved.length === 0 && - approvals.length === 1 && - approvals[0].index < timeline.sideEffects[0].index && - timeline.sideEffects[0].index < timeline.receipts[0].index && - timeline.receipts[0].index < timeline.terminalObservations[0].index - ); - }); - const batchActionOrderValid = actionTimelines.every((timeline, index) => { - if (timeline.action.actionIndex !== index) return false; - if (index === 0) return true; - const previousEnd = - actionTimelines[index - 1].terminalObservations[0]?.index; - const currentStart = timeline.auto - ? timeline.executions[0]?.index - : timeline.sideEffects[0]?.index; - return ( - Number.isSafeInteger(previousEnd) && - Number.isSafeInteger(currentStart) && - previousEnd < currentStart - ); - }); - mixedSpawnConfirmationRequiredCount = confirmationRequired.length; - mixedSpawnApprovalCount = approvals.length; - mixedInitialSpawnTerminalObservationIndex = - spawnTimeline?.terminalObservations[0]?.index ?? -1; - mixedSpawnConfirmationOrderValid = - confirmationRequired.length === 1 && - approvals.length === 1 && - confirmationRequired[0].index < approvals[0].index && - actionTimelines.every( - (timeline) => - approvals[0].index < - (timeline.auto - ? timeline.executions[0]?.index - : timeline.sideEffects[0]?.index), - ) && - actionLifecycleOrderValid && - batchActionOrderValid; - assert( - delegateActions.length === 2 && - new Set(delegateActions.map((action) => action.actionId)).size === 2 && - spawnActions.length === 1 && - spawnAction.executionMode === 'confirmation' && - delegateActions.every((action) => action.executionMode === 'auto') && - delegateTimelines.length === 2 && - spawnTimeline != null && - mixedSpawnConfirmationRequiredCount === 1 && - mixedSpawnApprovalCount === 1 && - mixedSpawnConfirmationOrderValid, - 'supervisor-swarm-mixed-initial-batch-action-persistence-invalid', - ); - } else { - assert( - initialBatchActions.every( - (action) => - executing.filter((record) => + record.status === 'ok' && matchesInitialBatchAction(record, action), - ).length === 1 && - receipts.filter( - (record) => - record.sessionId === supervisorSwarmSessionId && - record.status === 'ok' && - matchesInitialBatchAction(record, action), - ).length === 1, - ), - 'supervisor-swarm-initial-batch-action-persistence-invalid', - ); - } - if (isSupervisorSwarmMultiIsolatedHarnessSuite()) { - const followupActionId = state.supervisorSwarm.mixedFollowupSpawnActionId; - assert( - isNonEmptyString(followupActionId) && - followupActionId !== state.supervisorSwarm.mixedSpawnActionId, - 'supervisor-swarm-followup-spawn-action-missing', - ); - const indexedAgentDb = agentDb.map((record, index) => ({ record, index })); - const matchesFollowupIdentity = (record) => - record.agentId === projectSupervisorAgentId && - (record.runId === state.initialRunId || - record.confirmedRunId === state.initialRunId) && - record.actionId === followupActionId; - const matchesFollowupToolRecord = (record) => - matchesFollowupIdentity(record) && - (record.tool === 'agent.spawn_isolated' || - record.commandId === 'agent.spawn_isolated'); - const confirmationRequired = indexedAgentDb.filter( - ({ record }) => - record.recordType === - 'agent.runtime.provider_action_batch.confirmation_required' && - record.sessionId === supervisorSwarmSessionId && - matchesFollowupToolRecord(record), - ); - const approvals = indexedAgentDb.filter( - ({ record }) => - record.recordType === 'agent.runtime.tool_confirmation.approved' && - record.sessionId === supervisorSwarmSessionId && - matchesFollowupToolRecord(record), - ); - const sideEffects = indexedAgentDb.filter( - ({ record }) => - record.recordType === 'agent.runtime.agent.spawn_isolated' && - record.sessionId === supervisorSwarmSessionId && - matchesFollowupIdentity(record), - ); - const followupReceipts = indexedAgentDb.filter( - ({ record }) => - record.recordType === 'agent.runtime.action_receipt' && - record.sessionId === supervisorSwarmSessionId && - record.executionMode === 'confirmation' && - record.status === 'ok' && - matchesFollowupToolRecord(record), - ); - const terminalObservations = indexedAgentDb.filter( - ({ record }) => - record.recordType === 'agent.runtime.tool_observation' && - record.status === 'ok' && - record.decision === 'approved' && - matchesFollowupToolRecord(record), - ); - const followupOrderValid = - confirmationRequired.length === 1 && - approvals.length === 1 && - sideEffects.length === 1 && - followupReceipts.length === 1 && - terminalObservations.length === 1 && - mixedInitialSpawnTerminalObservationIndex >= 0 && - mixedInitialSpawnTerminalObservationIndex < - confirmationRequired[0].index && - confirmationRequired[0].index < approvals[0].index && - approvals[0].index < sideEffects[0].index && - sideEffects[0].index < followupReceipts[0].index && - followupReceipts[0].index < terminalObservations[0].index; - assert( - followupOrderValid, - 'supervisor-swarm-followup-spawn-action-persistence-invalid', - ); - mixedSpawnConfirmationRequiredCount += confirmationRequired.length; - mixedSpawnApprovalCount += approvals.length; - mixedSpawnConfirmationOrderValid &&= followupOrderValid; - } + ).length === 1, + ), + 'supervisor-swarm-initial-batch-action-persistence-invalid', + ); const matchesRecoveredRepairPending = (record) => record.agentId === state.supervisorSwarm.repairTargetAgentId && record.runId === state.supervisorSwarm.repairTargetRunId && @@ -957,9 +649,6 @@ export function validateSupervisorSwarmActionPersistence( duplicateReceiptCount, duplicateExecutingActionIdCount, isolatedMutationActionCount: isolatedMutationActions.length, - mixedSpawnConfirmationRequiredCount, - mixedSpawnApprovalCount, - mixedSpawnConfirmationOrderValid, recoveredRepairPendingActionCount: recoveredRepairConfirmationLifecycle.receipts.length, failedRepairActionCount: repairAttempts.failedRepairActions.length, diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/repair-recovery.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/repair-recovery.mjs index 79274d129..e2b644de0 100644 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/repair-recovery.mjs +++ b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/repair-recovery.mjs @@ -6,14 +6,10 @@ import { assertSupervisorSwarmRuntimeHealthy, assertSupervisorSwarmWeakQualityDelivery, observeSupervisorSwarmInitialProviderOverlap, - observeSupervisorSwarmStaticIsolatedProviderOverlap, - supervisorSwarmMixedIsolatedClaimsReady, supervisorSwarmParentDeliveries, supervisorSwarmRelevantRunKeys, supervisorSwarmRepairAttemptEvidence, - validateSupervisorSwarmMixedIsolatedPersistence, } from './collaboration-assertions.mjs'; -import { validateSupervisorSwarmIsolatedSpawnInput } from './collaboration-policy.mjs'; import { readSupervisorSwarmResidualSidecarCounts, supervisorSwarmResidualSidecarsEmpty, @@ -33,8 +29,6 @@ import { codedError, confirmPendingActions, findPendingActions, - hashValue, - isNonEmptyString, isPlainObject, killRunnerOnce, path, @@ -45,7 +39,6 @@ import { sleep, state, supervisorSwarmConfirmedTools, - supervisorSwarmIsolatedReviews, supervisorSwarmQualityAgentId, supervisorSwarmQualityPath, supervisorSwarmSessionId, @@ -55,11 +48,7 @@ import { } from './shared.mjs'; import { isSupervisorSwarmInitialTransientRetrySuite, - isSupervisorSwarmInteractiveChatSuite, - isSupervisorSwarmMixedHarnessSuite, - isSupervisorSwarmMultiIsolatedHarnessSuite, isSupervisorSwarmTransientRetrySuite, - supervisorSwarmExpectedIsolatedReviewGroups, } from './suite-selection.mjs'; export async function confirmSupervisorSwarmPendingActions( @@ -68,50 +57,9 @@ export async function confirmSupervisorSwarmPendingActions( ) { const before = state.confirmedActionIds.size; const allowedTools = new Set(supervisorSwarmConfirmedTools); - if (isSupervisorSwarmMixedHarnessSuite()) { - allowedTools.add('agent.spawn_isolated'); - } await confirmPendingActions(allowedTools, (pending) => { const runKey = `${pending.agentId}\0${pending.runId}`; const deferred = deferredRunKeys.has(runKey); - if (pending.tool === 'agent.spawn_isolated') { - const initialSpawn = - pending.actionId === state.supervisorSwarm.mixedSpawnActionId; - if (!initialSpawn) { - assert( - isSupervisorSwarmMultiIsolatedHarnessSuite(), - 'supervisor-swarm-followup-spawn-outside-multi-group-suite', - ); - const requestHash = validateSupervisorSwarmIsolatedSpawnInput( - pending.action?.input, - 1, - 'supervisor-swarm-mixed-followup', - supervisorSwarmExpectedIsolatedReviewGroups(), - ); - assert( - isNonEmptyString(state.supervisorSwarm.mixedSpawnActionId) && - pending.actionId !== state.supervisorSwarm.mixedSpawnActionId && - (state.supervisorSwarm.mixedFollowupSpawnActionId == null || - state.supervisorSwarm.mixedFollowupSpawnActionId === - pending.actionId) && - (state.supervisorSwarm.mixedFollowupSpawnRequestHash == null || - state.supervisorSwarm.mixedFollowupSpawnRequestHash === - requestHash), - 'supervisor-swarm-followup-spawn-identity-invalid', - ); - state.supervisorSwarm.mixedFollowupSpawnActionId = pending.actionId; - state.supervisorSwarm.mixedFollowupSpawnRequestHash = requestHash; - } - assert( - isSupervisorSwarmMixedHarnessSuite() && - pending.agentId === projectSupervisorAgentId && - pending.runId === state.initialRunId && - (initialSpawn || - pending.actionId === - state.supervisorSwarm.mixedFollowupSpawnActionId), - 'supervisor-swarm-unexpected-spawn-confirmation', - ); - } assert( confirmRunKeys.has(runKey) || deferred, 'supervisor-swarm-unexpected-pending-run', @@ -121,14 +69,7 @@ export async function confirmSupervisorSwarmPendingActions( pending.agentId === projectSupervisorAgentId && pending.runId === state.initialRunId; assert( - !isParentRun || - pending.tool === 'project.verify' || - (isSupervisorSwarmMixedHarnessSuite() && - pending.tool === 'agent.spawn_isolated' && - [ - state.supervisorSwarm.mixedSpawnActionId, - state.supervisorSwarm.mixedFollowupSpawnActionId, - ].includes(pending.actionId)), + !isParentRun || pending.tool === 'project.verify', 'supervisor-swarm-parent-pending-tool-invalid', ); return true; @@ -255,9 +196,6 @@ export async function restartSupervisorSwarmRunnerAtRepairBoundary( repair, repairPending, ) { - const mixedState = isSupervisorSwarmMixedHarnessSuite() - ? validateSupervisorSwarmMixedIsolatedPersistence(persistence) - : null; const initial = supervisorSwarmParentDeliveries( persistence.deliveries, ).filter((delivery) => delivery.repairOfDelegationId == null); @@ -290,11 +228,6 @@ export async function restartSupervisorSwarmRunnerAtRepairBoundary( (delivery) => task.agentId === delivery.targetAgentId && task.runId === delivery.targetRunId, - ) || - (mixedState?.instances ?? []).some( - (instance) => - task.agentId === instance.instanceId && - task.runId === instance.runId, ), ) .map(supervisorSwarmTaskIdentity) @@ -312,14 +245,10 @@ export async function restartSupervisorSwarmRunnerAtRepairBoundary( .map(supervisorSwarmClaimIdentity) .sort((left, right) => left.actionId.localeCompare(right.actionId)); assert( - preKillTaskIdentities.length === (mixedState ? 7 : 4) && - preKillClaimIdentities.length === 1, + preKillTaskIdentities.length === 4 && preKillClaimIdentities.length === 1, 'supervisor-swarm-pre-kill-work-or-claim-identity-invalid', ); - const relevantRuns = supervisorSwarmRelevantRunKeys( - parentDeliveries, - mixedState?.instances ?? [], - ); + const relevantRuns = supervisorSwarmRelevantRunKeys(parentDeliveries); const preKillProviderStartedIdentities = supervisorSwarmProviderStartedIdentities(persistence.agentDb, relevantRuns); const preKillParentContext = persistence.contextBundles.find( @@ -353,12 +282,6 @@ export async function restartSupervisorSwarmRunnerAtRepairBoundary( .sort((left, right) => left.delegationId.localeCompare(right.delegationId)); state.supervisorSwarm.preKillClaimIdentities = preKillClaimIdentities; state.supervisorSwarm.preKillTaskIdentities = preKillTaskIdentities; - state.supervisorSwarm.preKillMixedIdentity = mixedState - ? { - isolated: mixedState.identity, - parentContextSha256: hashValue(JSON.stringify(preKillParentContext)), - } - : null; state.supervisorSwarm.runnerKillBoundaryObserved = true; await killRunnerOnce(); @@ -437,21 +360,11 @@ export async function restartSupervisorSwarmRunnerAtRepairBoundary( candidate.tool === repairPending.tool, ); if (recoveredPending) { - const currentMixedState = isSupervisorSwarmMixedHarnessSuite() - ? validateSupervisorSwarmMixedIsolatedPersistence(after) - : null; - const currentParentContext = after.contextBundles.find( - (bundle) => - bundle.agentId === projectSupervisorAgentId && - bundle.sessionId === supervisorSwarmSessionId && - bundle.runId === state.initialRunId, - ); const currentProviderStartedIdentities = supervisorSwarmProviderStartedIdentities( after.agentDb, supervisorSwarmRelevantRunKeys( supervisorSwarmParentDeliveries(after.deliveries), - currentMixedState?.instances ?? [], ), ); assert( @@ -468,15 +381,7 @@ export async function restartSupervisorSwarmRunnerAtRepairBoundary( JSON.stringify(currentProviderStartedIdentities) === JSON.stringify( state.supervisorSwarm.preKillProviderStartedIdentities, - ) && - (!currentMixedState || - (JSON.stringify(currentMixedState.identity) === - JSON.stringify( - state.supervisorSwarm.preKillMixedIdentity?.isolated, - ) && - hashValue(JSON.stringify(currentParentContext)) === - state.supervisorSwarm.preKillMixedIdentity - ?.parentContextSha256)), + ), 'supervisor-swarm-recovery-identity-or-provider-replay-invalid', ); state.identityStable = true; @@ -500,84 +405,18 @@ export async function driveSupervisorSwarmToRepairKillBoundary() { (delivery) => delivery.repairOfDelegationId == null, ); assert(initial.length <= 2, 'supervisor-swarm-extra-initial-delivery'); - if ( - isSupervisorSwarmMixedHarnessSuite() && - isNonEmptyString(state.supervisorSwarm.mixedSpawnActionId) && - !state.confirmedActionIds.has(state.supervisorSwarm.mixedSpawnActionId) - ) { - const pending = await findPendingActions(); - if ( - pending.some( - (candidate) => - candidate.agentId === projectSupervisorAgentId && - candidate.runId === state.initialRunId && - candidate.actionId === state.supervisorSwarm.mixedSpawnActionId && - candidate.tool === 'agent.spawn_isolated', - ) - ) { - await confirmSupervisorSwarmPendingActions( - new Set([`${projectSupervisorAgentId}\0${state.initialRunId}`]), - new Set( - initial.map( - (delivery) => - `${delivery.targetAgentId}\0${delivery.targetRunId}`, - ), - ), - ); - } - } - if (isSupervisorSwarmMultiIsolatedHarnessSuite()) { - const pending = await findPendingActions(); - const followupSpawnPending = pending.find( - (candidate) => - candidate.agentId === projectSupervisorAgentId && - candidate.runId === state.initialRunId && - candidate.tool === 'agent.spawn_isolated' && - candidate.actionId !== state.supervisorSwarm.mixedSpawnActionId, - ); - if ( - followupSpawnPending && - !state.confirmedActionIds.has(followupSpawnPending.actionId) - ) { - await confirmSupervisorSwarmPendingActions( - new Set([`${projectSupervisorAgentId}\0${state.initialRunId}`]), - new Set( - parentDeliveries.map( - (delivery) => - `${delivery.targetAgentId}\0${delivery.targetRunId}`, - ), - ), - ); - } - } if (initial.length === 2) { const { quality } = assertSupervisorSwarmInitialDeliveries(initial); observeSupervisorSwarmInitialProviderOverlap( persistence.agentDb, initial, ); - const isolatedInstances = - supervisorSwarmParentIsolatedRecords(persistence).instances; - observeSupervisorSwarmStaticIsolatedProviderOverlap( - persistence.agentDb, - initial, - isolatedInstances, - ); - if ( - state.supervisorSwarm.initialProviderOverlapObserved && - (!isSupervisorSwarmMixedHarnessSuite() || - state.supervisorSwarm.staticIsolatedProviderOverlapObserved) - ) { + if (state.supervisorSwarm.initialProviderOverlapObserved) { const confirmRunKeys = new Set( initial.map( (delivery) => `${delivery.targetAgentId}\0${delivery.targetRunId}`, ), ); - if (isSupervisorSwarmInteractiveChatSuite()) { - confirmRunKeys.add( - `${projectSupervisorAgentId}\0${state.initialRunId}`, - ); - } await confirmSupervisorSwarmPendingActions( confirmRunKeys, new Set( @@ -685,13 +524,6 @@ export async function driveSupervisorSwarmToRepairKillBoundary() { } return; } - if (isSupervisorSwarmMixedHarnessSuite()) { - if (!supervisorSwarmMixedIsolatedClaimsReady(persistence)) { - await sleep(50); - continue; - } - validateSupervisorSwarmMixedIsolatedPersistence(persistence); - } await restartSupervisorSwarmRunnerAtRepairBoundary( persistence, repair, @@ -790,29 +622,6 @@ export async function driveSupervisorSwarmRuntimeToCompletion() { delivery.delegationId === task.delegationId, ), ); - const isolatedRecords = supervisorSwarmParentIsolatedRecords(persistence); - observeSupervisorSwarmStaticIsolatedProviderOverlap( - persistence.agentDb, - deliveries.filter((delivery) => delivery.repairOfDelegationId == null), - isolatedRecords.instances, - ); - const mixedReady = - !isSupervisorSwarmMixedHarnessSuite() || - (isolatedRecords.groups.length === - supervisorSwarmExpectedIsolatedReviewGroups().length && - isolatedRecords.instances.length === - supervisorSwarmIsolatedReviews.length && - isolatedRecords.results.length === - supervisorSwarmIsolatedReviews.length && - isolatedRecords.results.every( - (record) => record.result?.status === 'completed', - ) && - isolatedRecords.joinDeliveries.length === - supervisorSwarmExpectedIsolatedReviewGroups().length && - isolatedRecords.joinDeliveries.every( - (delivery) => delivery.status === 'claimed-by-parent', - ) && - validateSupervisorSwarmMixedIsolatedPersistence(persistence)); const terminalStateObserved = deliveries.length === 3 && deliveries.every((delivery) => delivery.status === 'claimed-by-parent') && @@ -820,7 +629,6 @@ export async function driveSupervisorSwarmRuntimeToCompletion() { childTasks.every( (task) => task.status === 'completed' && task.phase === 'completed', ) && - Boolean(mixedReady) && parentRuntime?.runId === state.initialRunId && parentRuntime?.sessionId === supervisorSwarmSessionId && parentRuntime?.status === 'idle' && diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/setup.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/setup.mjs index 3d4c3d4f6..e39cdb29e 100644 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/setup.mjs +++ b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/suites/supervisor-swarm/setup.mjs @@ -5,8 +5,6 @@ import { commandPassedMarker, configFileName, fs, - hashValue, - mainAgentId, path, randomUUID, readJson, @@ -14,9 +12,6 @@ import { seedDisposableProject, state, supervisorCollaborationPolicySchemaVersion, - supervisorSwarmAutonomousRoutingTerms, - supervisorSwarmAutonomousTask, - supervisorSwarmCollaborationPolicyControlTerms, supervisorSwarmDeniedMutationTools, supervisorSwarmDesignAgentId, supervisorSwarmDesignContent, @@ -29,61 +24,15 @@ import { supervisorSwarmQualityMarker, supervisorSwarmQualityPath, supervisorSwarmWeakQualityContent, + visibleText, } from './shared.mjs'; -import { - isSupervisorSwarmAutonomousChatSuite, - isSupervisorSwarmInteractiveChatSuite, - isSupervisorSwarmMixedHarnessSuite, - isSupervisorSwarmMultiIsolatedHarnessSuite, - isSupervisorSwarmTransientRetrySuite, - supervisorSwarmInitialIsolatedReviewsForSuite, -} from './suite-selection.mjs'; + +export function supervisorSwarmVerificationFixtureSource() { + return `import fs from 'node:fs';\nconst html = fs.readFileSync('game/index.html', 'utf8');\nconst agents = fs.readFileSync('AGENTS.md', 'utf8');\nconst optionalExact = (file, expected) => !fs.existsSync(file) || fs.readFileSync(file, 'utf8') === expected;\nconst passed = html.includes(${JSON.stringify(visibleText)}) && html.includes(' output.includes('Agent Swarm Chat'), - 'user-input-cli-banner-timeout', - 30_000, - ); - writeInteractiveCliLine(state.userInputCliSession, task); - - const pending = await waitForPendingUserInputRequest(); - await waitForInteractiveCliOutput( - state.userInputCliSession, - (output) => - output.includes(`[Needs input] agent=${projectSupervisorAgentId}`) && - output.includes(`request=${pending.requestId}`), - 'user-input-cli-question-timeout', - 120_000, - ); - await claimOwnedRunner(); - const beforeKillRunner = await readRunnerStatus(); - state.userInput.oldRunnerBootId = runnerBootId(beforeKillRunner); - assert( - isNonEmptyString(state.userInput.oldRunnerBootId), - 'user-input-runner-boot-before-kill-missing', - ); - await captureUserInputWaitingBoundary('before-kill'); - await killRunnerOnce(); - await runCli(['--agent-resume', state.projectRoot], { timeoutMs: 120_000 }); - state.resumed = true; - const restarted = await waitForRunnerBootChange( - state.userInput.oldRunnerBootId, - ); - state.userInput.newRunnerBootId = runnerBootId(restarted); - await claimOwnedRunner(restarted); - await captureUserInputWaitingBoundary('after-restart'); - await sleep(750); - await captureUserInputWaitingBoundary('after-stable-window'); - - writeInteractiveCliLine(state.userInputCliSession, userInputAnswerText); - await answerRemainingInteractiveQuestions(state.userInputCliSession); - await waitForInteractiveCliOutput( - state.userInputCliSession, - (output) => output.includes(`[\u5df2\u56de\u7b54] ${pending.requestId}`), - 'user-input-cli-answer-timeout', - 60_000, - ); - await waitForUserInputRuntimeCompletion(); - await waitForInteractiveCliOutput( - state.userInputCliSession, - (output) => - output.includes('\nAgent> ') || - output.includes( - '[\u672c\u8f6e\u7ed3\u675f] 父 Agent 回复已完整流式输出。', - ), - 'user-input-cli-final-reply-timeout', - 120_000, - ); - writeInteractiveCliLine(state.userInputCliSession, '/quit'); - await waitForInteractiveCliExit(state.userInputCliSession, 30_000); - state.userInputCliSession = null; - - state.identityStable = true; - state.evidence = await validateUserInputRuntimeEvidence(); - assert(state.evidence.secretLeakCount === 0, 'loaded-key-leak-detected'); -} - -export async function readUserInputSidecars() { - const files = ( - await listFiles(path.join(state.projectRoot, '.agent/runtime/user-input')) - ) - .filter((file) => file.endsWith('.json')) - .sort(); - return Promise.all( - files.map(async (file) => ({ file, record: await readJson(file) })), - ); -} - -export async function readUserInputPersistence() { - const runtimeStatePath = path.join( - state.projectRoot, - '.agent/runtime/agents', - `${projectSupervisorAgentId}.json`, - ); - const [ - taskSnapshot, - events, - agentDb, - activity, - output, - runtimeState, - sidecars, - ] = await Promise.all([ - readTaskSnapshot(), - readAllRuntimeEvents(), - readOptionalJsonl(path.join(state.projectRoot, '.agent/agent.db')), - readOptionalJsonl(path.join(state.projectRoot, '.agent/activity.jsonl')), - readOptionalJsonl(path.join(state.projectRoot, '.agent/output.jsonl')), - readJson(runtimeStatePath).catch(() => null), - readUserInputSidecars(), - ]); - const sessionId = runtimeState?.sessionId ?? state.initialSessionId; - const conversations = isNonEmptyString(sessionId) - ? await readOptionalJsonl( - agentConversationPath(projectSupervisorAgentId, sessionId), - ) - : []; - return { - taskSnapshot, - events, - agentDb, - activity, - output, - runtimeState, - sidecars, - conversations, - }; -} - -export function userInputProviderLifecycleStarted(agentDb) { - return agentDb.filter( - (record) => - record.recordType === 'agent.runtime.provider_request.lifecycle' && - record.agentId === projectSupervisorAgentId && - record.runId === state.initialRunId && - record.status === 'started', - ); -} - -export async function waitForPendingUserInputRequest() { - const deadline = Date.now() + 8 * 60 * 1000; - while (Date.now() < deadline) { - const persistence = await readUserInputPersistence(); - const { runtimeState, sidecars, conversations, taskSnapshot, agentDb } = - persistence; - const latest = taskSnapshot.latest.find( - (task) => - task.agentId === projectSupervisorAgentId && - task.runId === runtimeState?.runId, - ); - if (latest && isFailedTask(latest)) { - throw codedError('user-input-runtime-failed-before-question'); - } - if ( - runtimeState?.agentId === projectSupervisorAgentId && - runtimeState.status === 'waiting-for-user-input' && - runtimeState.phase === 'waiting-for-user-input' && - sidecars.length === 1 - ) { - const record = sidecars[0].record; - assert( - record.schemaVersion === 'game-creator-runtime-user-input.v1' && - record.agentId === projectSupervisorAgentId && - record.runId === runtimeState.runId && - record.sessionId === runtimeState.sessionId && - record.status === 'pending' && - Array.isArray(record.questions) && - record.questions.length === 1 && - record.responseId == null && - Object.keys(record.answers ?? {}).length === 0, - 'user-input-pending-sidecar-invalid', - ); - const questionMessages = conversations.filter( - (message) => - message.role === 'assistant' && - message.messageId === record.questionMessageId, - ); - assert( - questionMessages.length === 1 && - questionMessages[0].content.includes(record.questions[0].question), - 'user-input-question-conversation-invalid', - ); - state.initialRunId = runtimeState.runId; - state.initialSessionId = runtimeState.sessionId; - state.userInput.requestId = record.requestId; - state.userInput.actionId = record.actionId; - state.userInput.questionMessageId = record.questionMessageId; - state.userInput.questionCount = record.questions.length; - state.userInput.optionCount = record.questions.reduce( - (count, question) => count + question.options.length, - 0, - ); - state.userInput.privateValues = [ - ...record.questions.map((question) => question.question), - ...record.questions.flatMap((question) => - question.options.map((option) => option.description), - ), - ].filter(isNonEmptyString); - state.userInput.providerStartedBeforeKill = - userInputProviderLifecycleStarted(agentDb).length; - state.userInput.conversationCountBeforeKill = conversations.length; - assert( - state.userInput.providerStartedBeforeKill > 0, - 'user-input-provider-planning-lifecycle-missing', - ); - return record; - } - if (runtimeState?.phase === 'needs-reconciliation') { - throw codedError('user-input-runtime-needs-reconciliation'); - } - await sleep(250); - } - throw codedError('user-input-question-timeout'); -} - -export async function captureUserInputWaitingBoundary(stage) { - const persistence = await readUserInputPersistence(); - const { runtimeState, sidecars, conversations, agentDb } = persistence; - assert( - runtimeState?.agentId === projectSupervisorAgentId && - runtimeState.runId === state.initialRunId && - runtimeState.sessionId === state.initialSessionId && - runtimeState.status === 'waiting-for-user-input' && - runtimeState.phase === 'waiting-for-user-input' && - sidecars.length === 1 && - sidecars[0].record.requestId === state.userInput.requestId && - sidecars[0].record.actionId === state.userInput.actionId && - sidecars[0].record.status === 'pending' && - sidecars[0].record.responseId == null && - conversations.length === state.userInput.conversationCountBeforeKill, - `user-input-${stage}-waiting-boundary-invalid`, - ); - const providerStarted = userInputProviderLifecycleStarted(agentDb).length; - assert( - providerStarted === state.userInput.providerStartedBeforeKill, - `user-input-${stage}-provider-called-while-waiting`, - ); - if (stage !== 'before-kill') { - state.userInput.providerStartedAfterRestart = providerStarted; - state.userInput.conversationCountAfterRestart = conversations.length; - } -} - -export async function waitForUserInputRuntimeCompletion() { - const deadline = Date.now() + 8 * 60 * 1000; - while (Date.now() < deadline) { - const persistence = await readUserInputPersistence(); - const { runtimeState, sidecars, taskSnapshot, conversations } = persistence; - if (sidecars.length > 1) { - throw codedError('user-input-unexpected-second-request'); - } - const latest = taskSnapshot.latest.find( - (task) => - task.agentId === projectSupervisorAgentId && - task.runId === state.initialRunId, - ); - if (latest && isFailedTask(latest)) { - throw codedError('user-input-runtime-failed-after-answer'); - } - if ( - runtimeState?.runId === state.initialRunId && - runtimeState.sessionId === state.initialSessionId && - runtimeState.status === 'idle' && - runtimeState.phase === 'completed' && - latest?.status === 'completed' && - latest.phase === 'completed' && - sidecars.length === 1 && - sidecars[0].record.status === 'answered' - ) { - const record = sidecars[0].record; - state.userInput.responseId = record.responseId; - state.userInput.answerMessageId = record.answerMessageId; - const finalAssistants = conversations.filter( - (message) => - message.role === 'assistant' && - message.messageId !== record.questionMessageId, - ); - if (finalAssistants.length === 1) return persistence; - } - if (runtimeState?.phase === 'needs-reconciliation') { - throw codedError('user-input-runtime-needs-reconciliation-after-answer'); - } - await sleep(250); - } - throw codedError('user-input-completion-timeout'); -} - -export function validateUserInputProviderLifecycle(agentDb) { - const lifecycle = agentDb.filter( - (record) => - record.recordType === 'agent.runtime.provider_request.lifecycle' && - record.agentId === projectSupervisorAgentId && - record.runId === state.initialRunId, - ); - const byRequest = new Map(); - for (const record of lifecycle) { - assert( - isNonEmptyString(record.requestId) && - isNonEmptyString(record.requestKind) && - isNonEmptyString(record.requestSlot), - 'user-input-provider-lifecycle-identity-invalid', - ); - const records = byRequest.get(record.requestId) ?? []; - records.push(record); - byRequest.set(record.requestId, records); - } - for (const records of byRequest.values()) { - assert( - records.length === 2 && - records[0].status === 'started' && - ['completed', 'failed', 'interrupted'].includes(records[1].status) && - records[0].requestKind === records[1].requestKind && - records[0].requestSlot === records[1].requestSlot && - records[0].runId === records[1].runId, - 'user-input-provider-lifecycle-sequence-invalid', - ); - } - const started = lifecycle.filter((record) => record.status === 'started'); - assert( - started.length >= 2 && - started.length === byRequest.size && - state.userInput.providerStartedBeforeKill === - state.userInput.providerStartedAfterRestart, - 'user-input-provider-lifecycle-count-invalid', - ); - return { - requestIdentityCount: byRequest.size, - startedCount: started.length, - terminalCount: lifecycle.length - started.length, - }; -} - -export async function validateUserInputRuntimeEvidence() { - const persistence = await readUserInputPersistence(); - const { - taskSnapshot, - events, - agentDb, - activity, - output, - runtimeState, - sidecars, - conversations, - } = persistence; - assert(sidecars.length === 1, 'user-input-sidecar-count-invalid'); - const sidecar = sidecars[0].record; - const latest = taskSnapshot.latest.find( - (task) => - task.agentId === projectSupervisorAgentId && - task.runId === state.initialRunId, - ); - assert( - runtimeState?.agentId === projectSupervisorAgentId && - runtimeState.runId === state.initialRunId && - runtimeState.sessionId === state.initialSessionId && - runtimeState.status === 'idle' && - runtimeState.phase === 'completed' && - latest?.status === 'completed' && - latest.phase === 'completed', - 'user-input-final-runtime-identity-invalid', - ); - assert( - sidecar.schemaVersion === 'game-creator-runtime-user-input.v1' && - sidecar.agentId === projectSupervisorAgentId && - sidecar.runId === state.initialRunId && - sidecar.sessionId === state.initialSessionId && - sidecar.requestId === state.userInput.requestId && - sidecar.actionId === state.userInput.actionId && - sidecar.status === 'answered' && - sidecar.responseId === state.userInput.responseId && - sidecar.questionMessageId === state.userInput.questionMessageId && - sidecar.answerMessageId === state.userInput.answerMessageId && - sidecar.questions.length === 1 && - Object.keys(sidecar.answers).length === 1 && - Object.values(sidecar.answers)[0] === userInputAnswerText, - 'user-input-final-sidecar-invalid', - ); - const questionMessages = conversations.filter( - (message) => - message.role === 'assistant' && - message.messageId === sidecar.questionMessageId, - ); - const answerMessages = conversations.filter( - (message) => - message.role === 'user' && message.messageId === sidecar.answerMessageId, - ); - const finalAssistants = conversations.filter( - (message) => - message.role === 'assistant' && - message.messageId !== sidecar.questionMessageId, - ); - assert( - questionMessages.length === 1 && - answerMessages.length === 1 && - answerMessages[0].content.includes(userInputAnswerCanary) && - finalAssistants.length === 1, - 'user-input-conversation-cardinality-invalid', - ); - const duplicateMessageCount = duplicateCount( - conversations.map((message) => message.messageId).filter(Boolean), - ); - assert(duplicateMessageCount === 0, 'user-input-duplicate-message-identity'); - const observations = agentDb.filter( - (record) => - record.recordType === 'agent.runtime.user_input.answered' && - record.agentId === projectSupervisorAgentId && - record.runId === state.initialRunId && - record.actionId === sidecar.actionId && - record.requestId === sidecar.requestId, - ); - assert( - observations.length === 1 && - JSON.stringify(observations[0]).includes('answerCount=1'), - 'user-input-public-observation-count-invalid', - ); - const lifecycle = validateUserInputProviderLifecycle(agentDb); - const completedAudits = agentDb.filter( - (record) => - record.recordType === 'agent.runtime.completed' && - record.agentId === projectSupervisorAgentId && - record.runId === state.initialRunId, - ); - assert( - completedAudits.length === 1, - 'user-input-completed-audit-count-invalid', - ); - const finalizationFiles = ( - await listFiles( - path.join(state.projectRoot, '.agent/runtime/finalizations'), - ) - ).filter((file) => file.endsWith('.json')); - assert( - finalizationFiles.length === 0, - 'user-input-finalization-journal-present', - ); - assert( - countExactSecrets( - Buffer.from(finalAssistants.map((message) => message.content).join('\n')), - disposableProjectPathVariants(), - ) === 0, - 'user-input-final-assistant-project-path-leak', - ); - - const publicSurfaces = { - event: events, - agentDb, - activity, - output, - runtimeState, - }; - const privateValues = [ - userInputAnswerCanary, - userInputAnswerText, - ...state.userInput.privateValues, - ].filter(isNonEmptyString); - const taskPrivateValues = privateValues.filter( - (value) => !buildUserInputTaskPrompt().includes(value), - ); - const privateBodyPublicCounts = countSensitiveValuesBySurface( - publicSurfaces, - privateValues, - 'user-input-private-body-public', - ); - const taskPrivateBodyPublicCounts = countSensitiveValuesBySurface( - { task: taskSnapshot.all }, - taskPrivateValues, - 'user-input-private-body-public', - ); - const apiKeyPublicCounts = countSensitiveValuesBySurface( - { task: taskSnapshot.all, ...publicSurfaces }, - state.secrets, - 'user-input-api-key-public', - ); - const projectPathPublicCounts = countSensitiveValuesBySurface( - { task: taskSnapshot.all, ...publicSurfaces }, - disposableProjectPathVariants(), - 'user-input-project-path-public', - ); - const sidecarSecretLeakCount = countExactSecrets( - Buffer.from(JSON.stringify(sidecar)), - state.secrets, - ); - assert( - sidecarSecretLeakCount === 0, - 'user-input-sidecar-secret-leak-detected', - ); - const secretLeakCount = await countSecretsInProject( - state.projectRoot, - state.secrets, - ); - assert(secretLeakCount === 0, 'user-input-project-secret-leak-detected'); - - return { - scenario: 'project-supervisor-needs-input-runner-restart', - targetAgentId: projectSupervisorAgentId, - providerModel: 'gpt-5.5', - isolatedAppDataUsed: true, - formalConfigCliCallCount: state.isolatedRunner.sourceConfigCliCallCount, - sourceRunnerEndpointUnchanged: false, - sourceConfigHardlinkCount: state.isolatedRunner.configLinks.length, - sourceConfigLinksVerified: false, - taskCount: taskSnapshot.all.length, - eventCount: events.length, - agentDbRecordCount: agentDb.length, - conversationMessageCount: conversations.length, - targetRunCount: new Set( - taskSnapshot.all - .filter((task) => task.agentId === projectSupervisorAgentId) - .map((task) => task.runId), - ).size, - stableSessionCount: new Set( - taskSnapshot.all - .filter((task) => task.agentId === projectSupervisorAgentId) - .map((task) => task.sessionId), - ).size, - userInputSidecarCount: sidecars.length, - userInputQuestionCount: sidecar.questions.length, - userInputOptionCount: state.userInput.optionCount, - userInputAnswerCount: Object.keys(sidecar.answers).length, - userInputQuestionMessageCount: questionMessages.length, - userInputAnswerMessageCount: answerMessages.length, - finalAssistantCount: finalAssistants.length, - completedAuditCount: completedAudits.length, - toolObservationCount: observations.length, - providerRequestIdentityCount: lifecycle.requestIdentityCount, - providerLifecycleStartedCount: lifecycle.startedCount, - providerLifecycleTerminalCount: lifecycle.terminalCount, - providerStartedBeforeRunnerKill: state.userInput.providerStartedBeforeKill, - providerStartedAfterRunnerRestart: - state.userInput.providerStartedAfterRestart, - providerCalledWhileWaiting: false, - conversationCountBeforeRunnerKill: - state.userInput.conversationCountBeforeKill, - conversationCountAfterRunnerRestart: - state.userInput.conversationCountAfterRestart, - runnerBootChanged: - state.userInput.oldRunnerBootId !== state.userInput.newRunnerBootId, - duplicateMessageCount, - finalizationJournalCount: finalizationFiles.length, - privateBodyPublicLeakCount: - sumObjectValues(privateBodyPublicCounts) + - sumObjectValues(taskPrivateBodyPublicCounts), - apiKeyPublicLeakCount: sumObjectValues(apiKeyPublicCounts), - projectPathPublicLeakCount: sumObjectValues(projectPathPublicCounts), - projectPathPublicSurfaceCount: Object.keys(projectPathPublicCounts).length, - userInputSidecarSecretLeakCount: sidecarSecretLeakCount, - userInputReportLeakCount: state.userInput.reportLeakCount, - userInputRunnerKillMethod: null, - userInputRunnerPidfdClaimCount: state.isolatedRunner.pidfdClaimCount, - userInputRunnerPidfdSignalCount: state.isolatedRunner.pidfdSignalCount, - userInputRunnerStopped: false, - userInputAppDataCleanupPerformed: false, - secretLeakCount, - lureLeakCount: state.lureLeakCount, - paths: [ - '.agent/runtime/user-input', - '.agent/runtime/tasks', - '.agent/runtime/events', - '.agent/agent.db', - '.agent/conversations', - ], - }; -} - -export async function collectPartialUserInputEvidence() { - const persistence = await readUserInputPersistence(); - return { - taskCount: persistence.taskSnapshot.all.length, - eventCount: persistence.events.length, - agentDbRecordCount: persistence.agentDb.length, - conversationMessageCount: persistence.conversations.length, - userInputSidecarCount: persistence.sidecars.length, - userInputQuestionCount: - persistence.sidecars[0]?.record?.questions?.length ?? 0, - userInputAnswerCount: Object.keys( - persistence.sidecars[0]?.record?.answers ?? {}, - ).length, - finalAssistantCount: persistence.conversations.filter( - (message) => - message.role === 'assistant' && - message.messageId !== - persistence.sidecars[0]?.record?.questionMessageId, - ).length, - }; -} - -export function emptyUserInputEvidence() { - return { - scenario: 'project-supervisor-needs-input-runner-restart', - targetAgentId: projectSupervisorAgentId, - providerModel: 'gpt-5.5', - isolatedAppDataUsed: false, - formalConfigCliCallCount: 0, - sourceRunnerEndpointUnchanged: false, - sourceConfigHardlinkCount: 0, - sourceConfigLinksVerified: false, - taskCount: 0, - eventCount: 0, - agentDbRecordCount: 0, - conversationMessageCount: 0, - targetRunCount: 0, - stableSessionCount: 0, - userInputSidecarCount: 0, - userInputQuestionCount: 0, - userInputOptionCount: 0, - userInputAnswerCount: 0, - userInputQuestionMessageCount: 0, - userInputAnswerMessageCount: 0, - finalAssistantCount: 0, - completedAuditCount: 0, - toolObservationCount: 0, - providerRequestIdentityCount: 0, - providerLifecycleStartedCount: 0, - providerLifecycleTerminalCount: 0, - providerStartedBeforeRunnerKill: 0, - providerStartedAfterRunnerRestart: 0, - providerCalledWhileWaiting: false, - conversationCountBeforeRunnerKill: 0, - conversationCountAfterRunnerRestart: 0, - runnerBootChanged: false, - duplicateMessageCount: 0, - finalizationJournalCount: 0, - privateBodyPublicLeakCount: 0, - apiKeyPublicLeakCount: 0, - projectPathPublicLeakCount: 0, - projectPathPublicSurfaceCount: 0, - userInputSidecarSecretLeakCount: 0, - userInputReportLeakCount: 0, - userInputRunnerKillMethod: null, - userInputRunnerPidfdClaimCount: 0, - userInputRunnerPidfdSignalCount: 0, - userInputRunnerStopped: false, - userInputAppDataCleanupPerformed: false, - secretLeakCount: 0, - lureLeakCount: 0, - paths: [], - }; -} - -export function isUserInputRuntimeSuite() { - return state.suite === userInputRuntimeSuite; -} diff --git a/apps/ai-game-creator-shell/scripts/agent-swarm-test-chat.mjs b/apps/ai-game-creator-shell/scripts/agent-swarm-test-chat.mjs deleted file mode 100644 index 45656fc67..000000000 --- a/apps/ai-game-creator-shell/scripts/agent-swarm-test-chat.mjs +++ /dev/null @@ -1,2149 +0,0 @@ -import { spawn } from 'node:child_process'; -import { randomUUID } from 'node:crypto'; -import { constants as fsConstants } from 'node:fs'; -import { - chmod, - copyFile, - lstat, - mkdir, - mkdtemp, - open, - readdir, - readFile, - realpath, - rm, - writeFile, -} from 'node:fs/promises'; -import os from 'node:os'; -import path from 'node:path'; -import { createInterface } from 'node:readline/promises'; -import { fileURLToPath, pathToFileURL } from 'node:url'; -import { inflateSync } from 'node:zlib'; - -import { AGC_APP_IDENTIFIER } from './channel-identity.mjs'; - -// 联调工具驱动的始终是默认渠道客户端:安装身份取渠道基线,不跟随发布渠道。 -export const appIdentifier = AGC_APP_IDENTIFIER; -export const configFileName = 'game-creator.config.json'; -export const localConfigFileName = 'game-creator.config.local.json'; -export const runnerEndpointFileName = 'agent-runner.endpoint.json'; -export const testProjectPrefix = 'genarrative-agc-swarm-test-'; -export const testProjectSentinelName = '.agc-swarm-test.json'; -export const testProjectSentinelSchema = - 'genarrative-agc-swarm-test-project.v1'; -export const testRuntimeConfigPrefix = 'genarrative-agc-swarm-config-'; -export const testRuntimeConfigSentinelName = '.agc-swarm-config.json'; -export const testRuntimeConfigSentinelSchema = - 'genarrative-agc-swarm-test-config.v1'; -export const ungeneratedGameEntryMarker = - '还没有生成游戏。回到聊天输入创意并确认生成后'; -export const defaultRealSwarmTestTask = - '制作一个可直接试玩的原创植物塔防小游戏:玩家选择并放置原创守卫阻挡敌人,完成波次后可以进入下一关并重新开始。主题、单位名称与视觉语言必须原创,不使用任何现有游戏角色、单位名、Logo 或受保护视觉语言。请自主完成正式产物、静态检查和双视口试玩验证。'; -export const swarmTurnReportPrefix = '[turn.report] '; -export const swarmTurnReportSchema = 'game-creator-swarm-turn-report.v1'; - -const swarmTurnReportKeys = [ - 'schemaVersion', - 'outcome', - 'parentAgentId', - 'sessionId', - 'parentRunId', - 'runtimeCount', - 'busyRuntimeCount', - 'pendingTaskCount', - 'runningTaskCount', - 'waitingForConfirmationCount', - 'waitingForUserInputCount', - 'newAssistantMessageCount', - 'finalReplyChars', - 'reconciliationAgentCount', -].sort(); -const settledZeroCountFields = [ - 'busyRuntimeCount', - 'pendingTaskCount', - 'runningTaskCount', - 'waitingForConfirmationCount', - 'waitingForUserInputCount', - 'reconciliationAgentCount', -]; - -const requiredFormalArtifactSpecs = [ - { path: 'memory/project.md', kind: 'file' }, - { path: 'game/game_design.md', kind: 'file' }, - { path: 'game/balance.json', kind: 'json' }, - { path: 'assets/manifest.art.json', kind: 'json' }, - { path: 'assets/manifest.audio.json', kind: 'json' }, - { path: 'game/index.html', kind: 'file' }, - { path: 'exports/README.md', kind: 'file' }, -]; -const editorImageArtifactSpecs = [ - { path: 'assets/ui-prototype.png', kind: 'image', aspectRatio: 16 / 9 }, - { path: 'assets/art-spritesheet.png', kind: 'image', aspectRatio: 1 }, -]; -export const requiredSwarmManifestTaskIds = Object.freeze([ - 'design-director', - 'design-foundation', - 'balance-director', - 'balance-seed', - 'art-director', - 'art-asset-plan', - 'art-polish', - 'audio-director', - 'audio-asset-plan', - 'code-director', - 'code-prototype', - 'quality-review', - 'preview-readiness', - 'preview-playtest', - 'publish-strategy', - 'publish-package', -]); - -const appRoot = path.resolve(fileURLToPath(new URL('..', import.meta.url))); -const cargoManifestPath = path.join(appRoot, 'src-tauri', 'Cargo.toml'); -const configWizardPath = path.join( - appRoot, - 'scripts', - 'game-creator-config-wizard.mjs', -); -const cargoCommand = process.platform === 'win32' ? 'cargo.exe' : 'cargo'; -const childTerminationGraceMs = 10_000; -const childForceTerminationWaitMs = 5_000; -const runnerShutdownTimeoutMs = 20_000; -const cleanupDirectoryTimeoutMs = 10_000; -const maximumValidatedPngBytes = 64 * 1024 * 1024; -const maximumValidatedPngPixels = 100_000_000; -const maximumInflatedPngBytes = 256 * 1024 * 1024; -const minimumMarkdownBodyCharacters = 24; -const minimumHtmlCharacters = 120; -const incompleteArtifactTextPattern = - /\b(?:todo|tbd|placeholder|coming[\t ]+soon|lorem[\t ]+ipsum)\b|待补充|待完善|占位|尚未完成|稍后补充|待填写|待验证|待复核|待确认|待定/iu; -const uncheckedMarkdownChecklistPattern = - /^[\t ]*(?:>[\t ]*)*(?:[-+*]|\d+[.)])[\t ]+\[[\t ]\](?:[\t ]|$)/mu; - -export function hasIncompleteArtifactMarker(content) { - return ( - incompleteArtifactTextPattern.test(content) || - uncheckedMarkdownChecklistPattern.test(content) - ); -} - -export const usage = `用法: - npm run agc:test:chat - npm run agc:test:chat:manual -- [选项] - -自动读取客户端 AppData 配置并复制到隔离目录,创建一次性项目后进入 Project Supervisor 自主测试。 -带 --task 时完成正式产物验收后自动退出;手工聊天模式完成后启动持续预览。 - -选项: - --config-dir <绝对路径> 显式指定客户端 AppData 配置来源目录 - --project-dir <绝对路径> 使用已有项目或空目录,不自动删除 - --keep-project 保留自动创建的一次性项目 - --no-open 手工模式启动预览但不自动打开浏览器 - --task <需求> 通过 manual 入口非交互提交自定义需求 - --timeout-minutes <分钟> 设置本次执行期限;自动任务默认 50 分钟,手工模式默认不限时 - --dry-run 只检查目录发现和项目准备,不启动 LLM - -h, --help 显示帮助 -`; - -function readOptionValue(args, index, option) { - const value = args[index + 1]?.trim(); - if (!value || value.startsWith('--')) { - throw new Error(`${option} 缺少路径`); - } - return value; -} - -function readTimeoutMinutes(args, index, option) { - const value = readOptionValue(args, index, option); - if (!/^[1-9]\d*$/u.test(value)) { - throw new Error(`${option} 必须是 1-1440 的整数分钟`); - } - const minutes = Number(value); - if (!Number.isSafeInteger(minutes) || minutes > 1_440) { - throw new Error(`${option} 必须是 1-1440 的整数分钟`); - } - return minutes; -} - -export function parseSwarmTestArguments(args) { - const options = { - configDir: null, - projectDir: null, - keepProject: false, - openBrowser: true, - task: null, - timeoutMinutes: null, - dryRun: false, - help: false, - }; - for (let index = 0; index < args.length; index += 1) { - const argument = args[index]; - if (argument === '--config-dir') { - if (options.configDir) throw new Error('--config-dir 只能指定一次'); - options.configDir = readOptionValue(args, index, argument); - index += 1; - } else if (argument === '--project-dir') { - if (options.projectDir) throw new Error('--project-dir 只能指定一次'); - options.projectDir = readOptionValue(args, index, argument); - index += 1; - } else if (argument === '--keep-project') { - options.keepProject = true; - } else if (argument === '--no-open') { - options.openBrowser = false; - } else if (argument === '--task') { - if (options.task) throw new Error('--task 只能指定一次'); - const task = readOptionValue(args, index, argument); - if (task.length > 4_000) throw new Error('--task 不能超过 4000 字符'); - options.task = task; - index += 1; - } else if (argument === '--timeout-minutes') { - if (options.timeoutMinutes !== null) { - throw new Error('--timeout-minutes 只能指定一次'); - } - options.timeoutMinutes = readTimeoutMinutes(args, index, argument); - index += 1; - } else if (argument === '--dry-run') { - options.dryRun = true; - } else if (argument === '--help' || argument === '-h') { - options.help = true; - } else { - throw new Error(`未知选项:${argument}`); - } - } - return options; -} - -export function shouldStartPersistentPreview(options) { - return !options.task; -} - -export function resolveSwarmTestTimeoutMs(options) { - const minutes = options.timeoutMinutes ?? (options.task ? 50 : null); - return minutes === null ? null : minutes * 60_000; -} - -function pushUnique(values, value) { - if (value && !values.includes(value)) values.push(value); -} - -export function defaultRuntimeConfigDirCandidates({ - platform = process.platform, - environment = process.env, - homeDirectory = os.homedir(), -} = {}) { - const candidates = []; - if (platform === 'win32') { - pushUnique( - candidates, - environment.APPDATA - ? path.win32.join(environment.APPDATA, appIdentifier) - : path.win32.join(homeDirectory, 'AppData', 'Roaming', appIdentifier), - ); - if (environment.LOCALAPPDATA) { - pushUnique( - candidates, - path.win32.join(environment.LOCALAPPDATA, appIdentifier), - ); - } - } else if (platform === 'darwin') { - pushUnique( - candidates, - path.posix.join( - homeDirectory, - 'Library', - 'Application Support', - appIdentifier, - ), - ); - } else { - const configuredRoot = environment.XDG_CONFIG_HOME; - const posixAbsoluteConfiguredRoot = - configuredRoot && path.posix.isAbsolute(configuredRoot); - const hostAbsoluteConfiguredRoot = - configuredRoot && - !posixAbsoluteConfiguredRoot && - path.isAbsolute(configuredRoot); - const configRoot = - configuredRoot && - (posixAbsoluteConfiguredRoot || hostAbsoluteConfiguredRoot) - ? configuredRoot - : path.posix.join(homeDirectory, '.config'); - pushUnique( - candidates, - hostAbsoluteConfiguredRoot - ? path.join(configRoot, appIdentifier) - : path.posix.join(configRoot, appIdentifier), - ); - } - return candidates; -} - -async function isRegularFileWithoutSymlink(filePath) { - const metadata = await lstat(filePath).catch((error) => { - if (error?.code === 'ENOENT') return null; - throw error; - }); - return Boolean(metadata?.isFile() && !metadata.isSymbolicLink()); -} - -export async function discoverRuntimeConfigDir( - explicitConfigDir, - platformContext, -) { - if (explicitConfigDir && !path.isAbsolute(explicitConfigDir)) { - throw new Error('--config-dir 必须是绝对路径'); - } - const candidates = explicitConfigDir - ? [explicitConfigDir] - : defaultRuntimeConfigDirCandidates(platformContext); - for (const candidate of candidates) { - const resolved = path.resolve(candidate); - const metadata = await lstat(resolved).catch((error) => { - if (error?.code === 'ENOENT') return null; - throw error; - }); - if (!metadata?.isDirectory() || metadata.isSymbolicLink()) continue; - if ( - !(await isRegularFileWithoutSymlink(path.join(resolved, configFileName))) - ) { - continue; - } - return realpath(resolved); - } - if (explicitConfigDir) { - throw new Error( - `指定目录中没有可用的 ${configFileName}:${explicitConfigDir}`, - ); - } - throw new Error( - `未找到客户端 AppData 配置。请运行 npm run agc:config,或启动 npm run agc 后在“运行时配置”中保存 LLM Provider。`, - ); -} - -export function canPromptForMissingRuntimeConfig( - stdinIsTty = process.stdin.isTTY, - stdoutIsTty = process.stdout.isTTY, -) { - return Boolean(stdinIsTty && stdoutIsTty); -} - -async function askToConfigureMissingRuntime() { - if (!canPromptForMissingRuntimeConfig()) return false; - const readline = createInterface({ - input: process.stdin, - output: process.stdout, - }); - try { - const answer = ( - await readline.question( - '未找到客户端 AppData 配置,是否现在进入安全配置向导? [Y/n]: ', - ) - ) - .trim() - .toLowerCase(); - return !answer || ['y', 'yes', '是'].includes(answer); - } finally { - readline.close(); - } -} - -export function buildMissingConfigWizardArguments(explicitConfigDir) { - return [ - configWizardPath, - '--configure-only', - ...(explicitConfigDir ? ['--config-dir', explicitConfigDir] : []), - ]; -} - -async function runMissingConfigWizard(setActiveChild, explicitConfigDir) { - const child = spawnChild( - process.execPath, - buildMissingConfigWizardArguments(explicitConfigDir), - { - stdio: 'inherit', - }, - ); - setActiveChild(child); - const result = await childExit(child); - setActiveChild(null); - if (result.code !== 0 || result.signal) { - throw new Error( - `配置向导未正常完成:code=${result.code ?? ''} signal=${result.signal ?? ''}`, - ); - } -} - -async function secureWindowsPrivateRuntimePath( - targetPath, - options, - secureWindowsPath = null, -) { - if (secureWindowsPath) { - await secureWindowsPath(targetPath, options); - return; - } - const { secureWindowsGameCreatorPathForCurrentUser } = await import( - './game-creator-config-wizard.mjs' - ); - await secureWindowsGameCreatorPathForCurrentUser(targetPath, options); -} - -async function copyPrivateRuntimeConfigEntry( - sourceConfigDir, - runtimeConfigDir, - fileName, - required, - secureWindowsPath = null, -) { - const sourcePath = path.join(sourceConfigDir, fileName); - const sourceMetadata = await lstat(sourcePath).catch((error) => { - if (error?.code === 'ENOENT') return null; - throw error; - }); - if (!sourceMetadata) { - if (required) throw new Error(`配置来源缺少 ${fileName}`); - return false; - } - if (!sourceMetadata.isFile() || sourceMetadata.isSymbolicLink()) { - throw new Error(`配置来源必须是无符号链接普通文件:${fileName}`); - } - - const destinationPath = path.join(runtimeConfigDir, fileName); - if (process.platform === 'win32') { - const sourceBytes = await readFile(sourcePath); - const destinationFile = await open(destinationPath, 'wx', 0o600); - try { - await secureWindowsPrivateRuntimePath( - destinationPath, - { isDirectory: false }, - secureWindowsPath, - ); - await destinationFile.writeFile(sourceBytes); - await destinationFile.sync(); - } finally { - await destinationFile.close(); - } - } else { - await copyFile(sourcePath, destinationPath, fsConstants.COPYFILE_EXCL); - await chmod(destinationPath, 0o600); - } - const [sourceMetadataAfterCopy, destinationMetadata] = await Promise.all([ - lstat(sourcePath), - lstat(destinationPath), - ]); - if ( - !sourceMetadataAfterCopy.isFile() || - sourceMetadataAfterCopy.isSymbolicLink() || - sourceMetadataAfterCopy.dev !== sourceMetadata.dev || - sourceMetadataAfterCopy.ino !== sourceMetadata.ino || - sourceMetadataAfterCopy.size !== sourceMetadata.size || - sourceMetadataAfterCopy.mtimeMs !== sourceMetadata.mtimeMs || - !destinationMetadata.isFile() || - destinationMetadata.isSymbolicLink() || - (process.platform !== 'win32' && - ((destinationMetadata.mode & 0o077) !== 0 || - (destinationMetadata.dev === sourceMetadata.dev && - destinationMetadata.ino === sourceMetadata.ino))) - ) { - throw new Error(`隔离配置副本身份或权限无效:${fileName}`); - } - return true; -} - -export async function prepareSwarmTestRuntimeConfig( - sourceConfigDir, - tempRoot, - { secureWindowsPath = null } = {}, -) { - if (!path.isAbsolute(sourceConfigDir)) { - throw new Error('配置来源目录必须是绝对路径'); - } - const sourceMetadata = await lstat(sourceConfigDir).catch((error) => { - if (error?.code === 'ENOENT') return null; - throw error; - }); - if (!sourceMetadata?.isDirectory() || sourceMetadata.isSymbolicLink()) { - throw new Error(`配置来源必须是无符号链接普通目录:${sourceConfigDir}`); - } - const canonicalSourceConfigDir = await realpath(sourceConfigDir); - const canonicalTempRoot = await realpath( - path.resolve(tempRoot ?? os.tmpdir()), - ); - const runtimeConfigDir = await mkdtemp( - path.join(canonicalTempRoot, testRuntimeConfigPrefix), - ); - try { - if (process.platform === 'win32') { - await secureWindowsPrivateRuntimePath( - runtimeConfigDir, - { isDirectory: true }, - secureWindowsPath, - ); - } else { - await chmod(runtimeConfigDir, 0o700); - } - const sentinelToken = randomUUID(); - await writeFile( - path.join(runtimeConfigDir, testRuntimeConfigSentinelName), - `${JSON.stringify({ - schemaVersion: testRuntimeConfigSentinelSchema, - token: sentinelToken, - })}\n`, - { flag: 'wx', mode: 0o600 }, - ); - await copyPrivateRuntimeConfigEntry( - canonicalSourceConfigDir, - runtimeConfigDir, - configFileName, - true, - secureWindowsPath, - ); - await copyPrivateRuntimeConfigEntry( - canonicalSourceConfigDir, - runtimeConfigDir, - localConfigFileName, - false, - secureWindowsPath, - ); - return { - path: await realpath(runtimeConfigDir), - sourcePath: canonicalSourceConfigDir, - owned: true, - sentinelToken, - }; - } catch (error) { - await rm(runtimeConfigDir, { recursive: true, force: true }); - throw error; - } -} - -const cleanupDirectoryChildProgram = String.raw` -const { rm } = require('node:fs/promises'); -const target = process.argv[1]; -rm(target, { recursive: true, force: false }).catch((error) => { - process.stderr.write(String(error && error.message || error)); - process.exitCode = 1; -}); -`; - -export async function removeDirectoryWithTimeout( - directoryPath, - { - timeoutMs = cleanupDirectoryTimeoutMs, - childProgram = cleanupDirectoryChildProgram, - } = {}, -) { - const child = spawnChild( - process.execPath, - ['-e', childProgram, directoryPath], - { - stdio: ['ignore', 'ignore', 'ignore'], - }, - ); - const result = await childExitWithTimeout( - child, - timeoutMs, - `清理目录 ${path.basename(directoryPath)}`, - { graceMs: 1_000, forceWaitMs: 2_000 }, - ); - if (result.code !== 0 || result.signal) { - throw new Error( - `清理目录失败:${path.basename(directoryPath)} code=${result.code ?? ''} signal=${result.signal ?? ''}`, - ); - } -} - -export async function cleanupSwarmTestRuntimeConfig(runtimeConfig) { - if (!runtimeConfig?.owned) return false; - const sentinelPath = path.join( - runtimeConfig.path, - testRuntimeConfigSentinelName, - ); - if (!(await isRegularFileWithoutSymlink(sentinelPath))) { - throw new Error('拒绝清理:隔离配置哨兵缺失或类型无效'); - } - let sentinel; - try { - sentinel = JSON.parse(await readFile(sentinelPath, 'utf8')); - } catch (error) { - throw new Error(`拒绝清理:隔离配置哨兵无效:${error.message}`); - } - if ( - sentinel.schemaVersion !== testRuntimeConfigSentinelSchema || - sentinel.token !== runtimeConfig.sentinelToken - ) { - throw new Error('拒绝清理:隔离配置哨兵身份不匹配'); - } - const canonical = await realpath(runtimeConfig.path); - if ( - canonical !== runtimeConfig.path || - canonical === runtimeConfig.sourcePath || - !path.basename(canonical).startsWith(testRuntimeConfigPrefix) - ) { - throw new Error('拒绝清理:隔离配置目录身份不匹配'); - } - const endpointMetadata = await lstat( - path.join(canonical, runnerEndpointFileName), - ).catch((error) => { - if (error?.code === 'ENOENT') return null; - throw error; - }); - if (endpointMetadata) { - throw new Error('拒绝清理:隔离 Agent Runner 尚未退出'); - } - await removeDirectoryWithTimeout(canonical); - return true; -} - -async function ensureExplicitProject(projectDir) { - if (!path.isAbsolute(projectDir)) { - throw new Error('--project-dir 必须是绝对路径'); - } - const resolved = path.resolve(projectDir); - const metadata = await lstat(resolved).catch((error) => { - if (error?.code === 'ENOENT') return null; - throw error; - }); - if (!metadata) { - await mkdir(resolved, { recursive: true, mode: 0o700 }); - } else if (!metadata.isDirectory() || metadata.isSymbolicLink()) { - throw new Error(`测试项目路径必须是普通目录:${resolved}`); - } - const canonical = await realpath(resolved); - const entries = await readdir(canonical); - const initialized = await isRegularFileWithoutSymlink( - path.join(canonical, '.agent', 'manifest.json'), - ); - if (entries.length > 0 && !initialized) { - throw new Error(`--project-dir 只能指向空目录或已初始化项目:${canonical}`); - } - return { - path: canonical, - owned: false, - sentinelToken: null, - }; -} - -export async function prepareSwarmTestProject(explicitProjectDir, tempRoot) { - if (explicitProjectDir) return ensureExplicitProject(explicitProjectDir); - const root = path.resolve(tempRoot ?? os.tmpdir()); - const projectPath = await mkdtemp(path.join(root, testProjectPrefix)); - try { - if (process.platform !== 'win32') await chmod(projectPath, 0o700); - const sentinelToken = randomUUID(); - await writeFile( - path.join(projectPath, testProjectSentinelName), - `${JSON.stringify({ - schemaVersion: testProjectSentinelSchema, - token: sentinelToken, - })}\n`, - { flag: 'wx', mode: 0o600 }, - ); - return { - path: await realpath(projectPath), - owned: true, - sentinelToken, - }; - } catch (error) { - await rm(projectPath, { recursive: true, force: true }); - throw error; - } -} - -export async function cleanupSwarmTestProject(project) { - if (!project?.owned) return false; - const sentinelPath = path.join(project.path, testProjectSentinelName); - if (!(await isRegularFileWithoutSymlink(sentinelPath))) { - throw new Error('拒绝清理:一次性项目哨兵缺失或类型无效'); - } - let sentinel; - try { - sentinel = JSON.parse(await readFile(sentinelPath, 'utf8')); - } catch (error) { - throw new Error(`拒绝清理:一次性项目哨兵无效:${error.message}`); - } - if ( - sentinel.schemaVersion !== testProjectSentinelSchema || - sentinel.token !== project.sentinelToken - ) { - throw new Error('拒绝清理:一次性项目哨兵身份不匹配'); - } - const canonical = await realpath(project.path); - if ( - canonical !== project.path || - !path.basename(canonical).startsWith(testProjectPrefix) - ) { - throw new Error('拒绝清理:一次性项目目录身份不匹配'); - } - await removeDirectoryWithTimeout(canonical); - return true; -} - -export function buildCargoCliArguments(cliArguments) { - // `--quiet` only silences cargo's own build chatter; compiler errors and the - // CLI's stdout still come through. Without it the crate's several hundred - // dead-code warnings are reprinted on every spawn and bury the run output - // this script exists to show. - return [ - 'run', - '--quiet', - '--manifest-path', - cargoManifestPath, - '--', - ...cliArguments, - ]; -} - -function spawnChild(command, args, options = {}) { - return spawn(command, args, { - cwd: appRoot, - env: process.env, - detached: process.platform !== 'win32', - ...options, - }); -} - -const childExitPromises = new WeakMap(); -const closedChildren = new WeakSet(); - -function childExit(child) { - const existing = childExitPromises.get(child); - if (existing) return existing; - const exitPromise = new Promise((resolve, reject) => { - child.once('error', reject); - child.once('close', (code, signal) => { - closedChildren.add(child); - resolve({ code, signal }); - }); - }); - childExitPromises.set(child, exitPromise); - return exitPromise; -} - -async function childExitWithin(exitPromise, timeoutMs) { - let timeoutHandle; - const timeoutPromise = new Promise((resolve) => { - timeoutHandle = setTimeout(() => resolve(null), timeoutMs); - }); - try { - return await Promise.race([exitPromise, timeoutPromise]); - } finally { - clearTimeout(timeoutHandle); - } -} - -export async function terminateChildTree( - child, - signal = 'SIGTERM', - force = false, -) { - if (!child || closedChildren.has(child) || !Number.isInteger(child.pid)) { - return; - } - if (process.platform === 'win32') { - const taskkill = spawn( - 'taskkill.exe', - ['/PID', String(child.pid), '/T', ...(force ? ['/F'] : [])], - { - stdio: 'ignore', - windowsHide: true, - }, - ); - const taskkillExit = childExit(taskkill).catch(() => null); - if (!(await childExitWithin(taskkillExit, childForceTerminationWaitMs))) { - try { - taskkill.kill('SIGKILL'); - } catch { - // The taskkill helper may have exited at the timeout boundary. - } - await childExitWithin(taskkillExit, childForceTerminationWaitMs); - } - return; - } - try { - process.kill(-child.pid, force ? 'SIGKILL' : signal); - } catch (error) { - try { - child.kill(force ? 'SIGKILL' : signal); - } catch { - if (error?.code !== 'ESRCH') throw error; - } - } -} - -async function terminateChildTreeAndWait( - child, - exitPromise, - signal, - label, - { - graceMs = childTerminationGraceMs, - forceWaitMs = childForceTerminationWaitMs, - } = {}, -) { - await terminateChildTree(child, signal, false); - const gracefulResult = await childExitWithin(exitPromise, graceMs); - if (gracefulResult) return gracefulResult; - - await terminateChildTree(child, 'SIGKILL', true); - const forcedResult = await childExitWithin(exitPromise, forceWaitMs); - if (forcedResult) return forcedResult; - throw new Error(`${label} 无法在强制终止进程树后关闭 stdio`); -} - -export async function childExitWithTimeout( - child, - timeoutMs, - label, - terminationOptions, -) { - const exitPromise = childExit(child); - if (timeoutMs === null) return exitPromise; - let timeoutHandle; - const timeoutPromise = new Promise((resolve) => { - timeoutHandle = setTimeout(() => resolve({ kind: 'timeout' }), timeoutMs); - }); - try { - const first = await Promise.race([ - exitPromise.then((result) => ({ kind: 'exit', result })), - timeoutPromise, - ]); - if (first.kind === 'exit') return first.result; - try { - await terminateChildTreeAndWait( - child, - exitPromise, - 'SIGTERM', - label, - terminationOptions, - ); - } catch (error) { - error.code = 'AGC_CHILD_TIMEOUT'; - throw error; - } - throw Object.assign( - new Error(`${label} 超过 ${Math.ceil(timeoutMs / 1000)} 秒期限`), - { code: 'AGC_CHILD_TIMEOUT' }, - ); - } finally { - clearTimeout(timeoutHandle); - } -} - -function isPlainObject(value) { - return ( - value !== null && - typeof value === 'object' && - !Array.isArray(value) && - Object.getPrototypeOf(value) === Object.prototype - ); -} - -function isNonNegativeSafeInteger(value) { - return Number.isSafeInteger(value) && value >= 0; -} - -export function parseSettledSwarmTurnReport(output) { - const reportLines = String(output) - .split(/\r?\n/u) - .filter((line) => line.startsWith(swarmTurnReportPrefix)); - if (reportLines.length !== 1) { - throw new Error( - `Agent Swarm 终态报告数量无效:expected=1 actual=${reportLines.length}`, - ); - } - - let report; - try { - report = JSON.parse(reportLines[0].slice(swarmTurnReportPrefix.length)); - } catch { - throw new Error('Agent Swarm 终态报告不是有效 JSON'); - } - if ( - !isPlainObject(report) || - JSON.stringify(Object.keys(report).sort()) !== - JSON.stringify(swarmTurnReportKeys) - ) { - throw new Error('Agent Swarm 终态报告结构无效'); - } - if (report.schemaVersion !== swarmTurnReportSchema) { - throw new Error('Agent Swarm 终态报告 schema 无效'); - } - if ( - typeof report.parentAgentId !== 'string' || - !report.parentAgentId.trim() || - typeof report.sessionId !== 'string' || - !report.sessionId.trim() || - typeof report.parentRunId !== 'string' || - !report.parentRunId.trim() - ) { - throw new Error('Agent Swarm 终态报告运行身份无效'); - } - const countFields = [ - 'runtimeCount', - ...settledZeroCountFields, - 'newAssistantMessageCount', - 'finalReplyChars', - ]; - if (countFields.some((field) => !isNonNegativeSafeInteger(report[field]))) { - throw new Error('Agent Swarm 终态报告计数无效'); - } - if ( - !['settled', 'failed', 'incomplete', 'needs-reconciliation'].includes( - report.outcome, - ) - ) { - throw new Error('Agent Swarm 终态报告 outcome 无效'); - } - if (report.outcome !== 'settled') { - throw new Error(`Agent Swarm 本轮未收束:outcome=${report.outcome}`); - } - const unsettledField = settledZeroCountFields.find( - (field) => report[field] !== 0, - ); - if (unsettledField) { - throw new Error( - `Agent Swarm 本轮仍有未收束工作:${unsettledField}=${report[unsettledField]}`, - ); - } - if (report.runtimeCount < 1) { - throw new Error('Agent Swarm 终态报告没有 Runtime'); - } - if ( - report.newAssistantMessageCount !== 1 || - report.finalReplyChars < 1 || - report.finalReplyChars > 1_000_000 - ) { - throw new Error('Agent Swarm 最终回复无效'); - } - return report; -} - -async function runCapturedCargo( - cliArguments, - setActiveChild, - { timeoutMs = null, label = 'Cargo 子命令', stdin = null } = {}, -) { - const child = spawnChild(cargoCommand, buildCargoCliArguments(cliArguments), { - stdio: [stdin === null ? 'ignore' : 'pipe', 'pipe', 'pipe'], - }); - setActiveChild(child); - if (stdin !== null) { - child.stdin.end(stdin); - } - let stdout = ''; - let stderr = ''; - child.stdout.setEncoding('utf8'); - child.stderr.setEncoding('utf8'); - child.stdout.on('data', (chunk) => { - stdout += chunk; - }); - child.stderr.on('data', (chunk) => { - stderr += chunk; - }); - try { - const result = await childExitWithTimeout(child, timeoutMs, label); - return { ...result, stdout, stderr }; - } finally { - setActiveChild(null); - } -} - -async function runInteractiveCargo(cliArguments, setActiveChild) { - const child = spawnChild(cargoCommand, buildCargoCliArguments(cliArguments), { - stdio: 'inherit', - }); - setActiveChild(child); - const result = await childExit(child); - setActiveChild(null); - return result; -} - -const swarmConfirmationPromptPattern = /输入 approve 或 reject:$/u; -const swarmUserInputPromptPattern = /请选择 1-\d+,或直接输入其他答案:$/u; - -export function nextSwarmAutoPilotReply(output) { - if (swarmConfirmationPromptPattern.test(output)) return 'approve'; - if (swarmUserInputPromptPattern.test(output)) return '1'; - return null; -} - -// CLI 的 REPL 是「先打印提示符再读行」,所以第一个「你>」出现时本轮还没开始跑: -// 它就是用来读我们这条任务的。收 stdin 必须等到投递之后的下一个提示符——那才是 -// 本轮结束、CLI 回到待输入状态。绝大多数情况下此前已经打印过 turn 回执,但总控也 -// 可能判定直接回复而不起持久 Run,那条路径没有回执,只等回执会一直干等到超时。 -const swarmChatPromptPattern = /(^|\n)你> $/u; - -export function swarmAutoPilotSitsAtPrompt(output) { - return swarmChatPromptPattern.test(output); -} - -export function swarmAutoPilotShouldCloseInput(output, promptsAfterSubmit) { - return swarmAutoPilotSitsAtPrompt(output) && promptsAfterSubmit >= 1; -} - -async function runTaskCargo( - cliArguments, - task, - setActiveChild, - timeoutMs, - autoPilot = false, -) { - const child = spawnChild(cargoCommand, buildCargoCliArguments(cliArguments), { - stdio: ['pipe', 'pipe', 'inherit'], - }); - setActiveChild(child); - const reportLines = []; - let pendingLine = ''; - let settled = false; - let taskSubmitted = false; - let promptsSeen = 0; - let sittingAtPrompt = false; - child.stdout.setEncoding('utf8'); - child.stdout.on('data', (chunk) => { - process.stdout.write(chunk); - pendingLine += chunk; - const lines = pendingLine.split('\n'); - pendingLine = lines.pop() ?? ''; - for (const line of lines) { - const normalizedLine = line.endsWith('\r') ? line.slice(0, -1) : line; - if (normalizedLine.startsWith(swarmTurnReportPrefix)) { - reportLines.push(normalizedLine); - settled = true; - } - } - if (!autoPilot || child.stdin.writableEnded) return; - const atPrompt = swarmAutoPilotSitsAtPrompt(pendingLine); - if (atPrompt && !sittingAtPrompt) promptsSeen += 1; - sittingAtPrompt = atPrompt; - // turn 已给出回执、或 CLI 回到了投递之后的下一个提示符,都说明本轮结束。 - if ( - settled || - (taskSubmitted && - swarmAutoPilotShouldCloseInput(pendingLine, promptsSeen - 1)) - ) { - child.stdin.end(); - return; - } - const reply = nextSwarmAutoPilotReply(pendingLine); - if (reply === null) return; - console.log(`[自动应答] ${reply}`); - pendingLine = ''; - child.stdin.write(`${reply}\n`); - }); - if (autoPilot) { - child.stdin.write(`${task}\n`); - taskSubmitted = true; - } else { - child.stdin.end(`${task}\n`); - } - try { - const result = await childExitWithTimeout( - child, - timeoutMs, - 'Agent Swarm 自动任务', - ); - const normalizedPendingLine = pendingLine.endsWith('\r') - ? pendingLine.slice(0, -1) - : pendingLine; - if (normalizedPendingLine.startsWith(swarmTurnReportPrefix)) { - reportLines.push(normalizedPendingLine); - } - return { - ...result, - turnReportOutput: reportLines.join('\n'), - }; - } finally { - setActiveChild(null); - } -} - -export function parseRunnerShutdownOutput(output) { - const match = output.match(/^runner\.stopped=(true|false)$/m); - if (!match) throw new Error('Runner 收束命令缺少 stopped 状态'); - return match[1] === 'true'; -} - -async function shutdownSwarmTestRunner(runtimeConfig, setActiveChild) { - const result = await runCapturedCargo( - ['--config-dir', runtimeConfig.path, '--runner-shutdown-if-idle'], - setActiveChild, - { - timeoutMs: runnerShutdownTimeoutMs, - label: '隔离 Agent Runner 收束命令', - }, - ); - if (result.code !== 0 || result.signal) { - throw new Error( - `隔离 Agent Runner 收束失败:${result.stderr.trim() || result.stdout.trim() || `code=${result.code ?? ''} signal=${result.signal ?? ''}`}`, - ); - } - return parseRunnerShutdownOutput(result.stdout); -} - -export function validatePreviewUrl(value) { - const url = new URL(value); - if ( - url.protocol !== 'http:' || - url.hostname !== '127.0.0.1' || - !url.port || - url.username || - url.password || - url.pathname !== '/' || - url.search || - url.hash - ) { - throw new Error('预览命令返回了非 loopback URL'); - } - return url.toString(); -} - -export async function hasGeneratedGameEntry(projectPath) { - const gameEntryPath = path.join(projectPath, 'game', 'index.html'); - if (!(await isRegularFileWithoutSymlink(gameEntryPath))) return false; - const html = await readFile(gameEntryPath, 'utf8'); - return html.trim().length > 0 && !html.includes(ungeneratedGameEntryMarker); -} - -const pngSignature = Buffer.from([ - 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, -]); -const pngCrcTable = Uint32Array.from({ length: 256 }, (_unused, index) => { - let value = index; - for (let bit = 0; bit < 8; bit += 1) { - value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1; - } - return value >>> 0; -}); - -function pngCrc32(bytes) { - let value = 0xffffffff; - for (const byte of bytes) { - value = pngCrcTable[(value ^ byte) & 0xff] ^ (value >>> 8); - } - return (value ^ 0xffffffff) >>> 0; -} - -export function validatePngBytes(bytes) { - if ( - !Buffer.isBuffer(bytes) || - bytes.length < 57 || - bytes.length > maximumValidatedPngBytes || - !bytes.subarray(0, pngSignature.length).equals(pngSignature) - ) { - throw new Error('PNG 签名或文件大小无效'); - } - - let offset = pngSignature.length; - let ihdr = null; - let ihdrCount = 0; - let idatSeen = false; - let idatEnded = false; - let iendSeen = false; - let plteCount = 0; - let plteEntries = 0; - const idatChunks = []; - while (offset < bytes.length) { - if (bytes.length - offset < 12) throw new Error('PNG chunk 被截断'); - const length = bytes.readUInt32BE(offset); - const typeOffset = offset + 4; - const dataOffset = typeOffset + 4; - const dataEnd = dataOffset + length; - const chunkEnd = dataEnd + 4; - if (dataEnd > bytes.length - 4 || chunkEnd > bytes.length) { - throw new Error('PNG chunk 长度越界'); - } - const type = bytes.subarray(typeOffset, dataOffset).toString('ascii'); - if (!/^[A-Za-z]{4}$/u.test(type)) throw new Error('PNG chunk 类型无效'); - const expectedCrc = bytes.readUInt32BE(dataEnd); - const actualCrc = pngCrc32(bytes.subarray(typeOffset, dataEnd)); - if (expectedCrc !== actualCrc) throw new Error(`${type} CRC 无效`); - - const data = bytes.subarray(dataOffset, dataEnd); - if (offset === pngSignature.length && type !== 'IHDR') { - throw new Error('IHDR 必须是第一个 chunk'); - } - if (type === 'IHDR') { - ihdrCount += 1; - if (ihdrCount !== 1 || length !== 13) { - throw new Error('IHDR 数量或长度无效'); - } - ihdr = { - width: data.readUInt32BE(0), - height: data.readUInt32BE(4), - bitDepth: data[8], - colorType: data[9], - compression: data[10], - filter: data[11], - interlace: data[12], - }; - } else if (type === 'PLTE') { - plteCount += 1; - if ( - !ihdr || - idatSeen || - iendSeen || - plteCount !== 1 || - length < 3 || - length > 768 || - length % 3 !== 0 - ) { - throw new Error('PLTE 数量、长度或顺序无效'); - } - plteEntries = length / 3; - } else if (type === 'IDAT') { - if (!ihdr || idatEnded || iendSeen) { - throw new Error('IDAT 顺序无效'); - } - idatSeen = true; - idatChunks.push(data); - } else if (type === 'IEND') { - if (!ihdr || !idatSeen || iendSeen || length !== 0) { - throw new Error('IEND 数量、长度或顺序无效'); - } - iendSeen = true; - if (chunkEnd !== bytes.length) throw new Error('IEND 后存在额外数据'); - } else { - if ((type.charCodeAt(0) & 0x20) === 0) { - throw new Error(`不支持的 PNG critical chunk:${type}`); - } - if (idatSeen) idatEnded = true; - } - offset = chunkEnd; - } - - if (!ihdr || ihdrCount !== 1 || !idatSeen || !iendSeen) { - throw new Error('PNG 缺少唯一 IHDR、IDAT 或 IEND'); - } - const validBitDepths = { - 0: [1, 2, 4, 8, 16], - 2: [8, 16], - 3: [1, 2, 4, 8], - 4: [8, 16], - 6: [8, 16], - }; - if ( - ihdr.width < 1 || - ihdr.height < 1 || - ihdr.width * ihdr.height > maximumValidatedPngPixels || - !validBitDepths[ihdr.colorType]?.includes(ihdr.bitDepth) || - ihdr.compression !== 0 || - ihdr.filter !== 0 || - ihdr.interlace !== 0 - ) { - throw new Error('IHDR 参数无效或不支持交错 PNG'); - } - if ( - (ihdr.colorType === 3 && - (plteCount !== 1 || plteEntries > 2 ** ihdr.bitDepth)) || - ([0, 4].includes(ihdr.colorType) && plteCount !== 0) - ) { - throw new Error('PLTE 与 PNG color type 或 bit depth 不匹配'); - } - - const channels = { 0: 1, 2: 3, 3: 1, 4: 2, 6: 4 }[ihdr.colorType]; - const rowBytes = Math.ceil((ihdr.width * channels * ihdr.bitDepth) / 8); - const expectedInflatedBytes = ihdr.height * (rowBytes + 1); - if ( - !Number.isSafeInteger(expectedInflatedBytes) || - expectedInflatedBytes > maximumInflatedPngBytes - ) { - throw new Error('PNG scanline 大小无效'); - } - let inflated; - try { - const compressed = Buffer.concat(idatChunks); - const result = inflateSync(compressed, { - maxOutputLength: expectedInflatedBytes + 1, - info: true, - }); - if (result.engine.bytesWritten !== compressed.length) { - throw new Error('trailing compressed bytes'); - } - inflated = result.buffer; - } catch { - throw new Error('IDAT zlib 数据无法完整解压'); - } - if (inflated.length !== expectedInflatedBytes) { - throw new Error('非交错 PNG scanline 长度无效'); - } - for (let row = 0; row < ihdr.height; row += 1) { - if (inflated[row * (rowBytes + 1)] > 4) { - throw new Error(`第 ${row + 1} 行 filter byte 无效`); - } - } - return { width: ihdr.width, height: ihdr.height }; -} - -async function validatePngFile(filePath) { - const metadata = await lstat(filePath); - if ( - !metadata.isFile() || - metadata.isSymbolicLink() || - metadata.size < 1_024 || - metadata.size > maximumValidatedPngBytes - ) { - throw new Error('PNG 文件缺失、类型无效或大小超限'); - } - const noFollowFlag = - process.platform === 'win32' ? 0 : (fsConstants.O_NOFOLLOW ?? 0); - const file = await open(filePath, fsConstants.O_RDONLY | noFollowFlag); - try { - return validatePngBytes(await file.readFile()); - } finally { - await file.close(); - } -} - -async function inspectFormalArtifact(projectPath, spec) { - const artifactPath = path.join(projectPath, ...spec.path.split('/')); - let metadata; - try { - metadata = await lstat(artifactPath); - } catch (error) { - return { - path: spec.path, - reason: - error?.code === 'ENOENT' || error?.code === 'ENOTDIR' - ? '缺失' - : '无法安全读取', - }; - } - if (!metadata.isFile() || metadata.isSymbolicLink()) { - return { path: spec.path, reason: '不是无符号链接普通文件' }; - } - if (metadata.size === 0) return { path: spec.path, reason: '空文件' }; - - const noFollowFlag = - process.platform === 'win32' ? 0 : (fsConstants.O_NOFOLLOW ?? 0); - let file; - try { - file = await open(artifactPath, fsConstants.O_RDONLY | noFollowFlag); - } catch { - return { path: spec.path, reason: '无法安全读取' }; - } - try { - const openedMetadata = await file.stat(); - if (!openedMetadata.isFile()) { - return { path: spec.path, reason: '不是无符号链接普通文件' }; - } - if (openedMetadata.size === 0) { - return { path: spec.path, reason: '空文件' }; - } - - if (spec.kind === 'json') { - let parsed; - try { - parsed = JSON.parse(await file.readFile('utf8')); - } catch { - return { path: spec.path, reason: 'JSON 无法解析' }; - } - if (!isPlainObject(parsed) || Object.keys(parsed).length === 0) { - return { path: spec.path, reason: 'JSON 必须是非空对象' }; - } - } else if (spec.kind === 'file' || spec.kind === 'game-entry') { - const content = await file.readFile('utf8'); - if (content.trim().length === 0) { - return { path: spec.path, reason: '空文件' }; - } - if (hasIncompleteArtifactMarker(content)) { - return { path: spec.path, reason: '仍包含占位标记' }; - } - const compactContent = content.replace(/\s/gu, ''); - if ( - spec.kind === 'file' && - compactContent.length < minimumMarkdownBodyCharacters - ) { - return { path: spec.path, reason: 'Markdown 正文过短' }; - } - if ( - spec.kind === 'game-entry' && - content.includes(ungeneratedGameEntryMarker) - ) { - return { path: spec.path, reason: '仍是初始化占位页' }; - } - if ( - spec.kind === 'game-entry' && - (compactContent.length < minimumHtmlCharacters || - !/]*>[\s\S]*<\/html>/iu.test(content) || - !/ maximumValidatedPngBytes) { - return { path: spec.path, reason: '图片文件大小超限' }; - } - let dimensions; - try { - dimensions = validatePngBytes(await file.readFile()); - } catch (error) { - return { - path: spec.path, - reason: `PNG 文件无效:${error.message}`, - }; - } - const ratio = dimensions.width / dimensions.height; - if (Math.abs(ratio - spec.aspectRatio) > 0.03) { - return { - path: spec.path, - reason: `图片比例无效:${dimensions.width}x${dimensions.height}`, - }; - } - } - } catch { - return { path: spec.path, reason: '无法安全读取' }; - } finally { - await file.close(); - } - return null; -} - -function resolveProjectEvidencePath(projectPath, relativePath) { - if ( - typeof relativePath !== 'string' || - !relativePath || - path.isAbsolute(relativePath) || - relativePath.includes('\\') - ) { - throw new Error('证据路径无效'); - } - const root = path.resolve(projectPath); - const targetPath = path.resolve(root, ...relativePath.split('/')); - if (!targetPath.startsWith(`${root}${path.sep}`)) { - throw new Error('证据路径越界'); - } - return targetPath; -} - -async function readSafeJson(projectPath, relativePath, maximumBytes) { - const targetPath = resolveProjectEvidencePath(projectPath, relativePath); - const metadata = await lstat(targetPath).catch(() => null); - if ( - !metadata?.isFile() || - metadata.isSymbolicLink() || - metadata.size < 2 || - metadata.size > maximumBytes - ) { - throw new Error('文件缺失、类型无效或大小超限'); - } - const noFollowFlag = - process.platform === 'win32' ? 0 : (fsConstants.O_NOFOLLOW ?? 0); - const file = await open(targetPath, fsConstants.O_RDONLY | noFollowFlag); - try { - return JSON.parse(await file.readFile('utf8')); - } finally { - await file.close(); - } -} - -async function readSafeJsonLines(projectPath, relativePath, maximumBytes) { - const targetPath = resolveProjectEvidencePath(projectPath, relativePath); - const metadata = await lstat(targetPath); - if ( - !metadata.isFile() || - metadata.isSymbolicLink() || - metadata.size < 2 || - metadata.size > maximumBytes - ) { - throw new Error('JSONL 文件缺失、类型无效或大小超限'); - } - const noFollowFlag = - process.platform === 'win32' ? 0 : (fsConstants.O_NOFOLLOW ?? 0); - const file = await open(targetPath, fsConstants.O_RDONLY | noFollowFlag); - try { - const content = await file.readFile('utf8'); - if (!content.endsWith('\n')) throw new Error('JSONL 尾记录不完整'); - return content - .split(/\r?\n/u) - .filter(Boolean) - .map((line) => JSON.parse(line)); - } finally { - await file.close(); - } -} - -async function inspectReadyTaskExactlyOnce( - projectPath, - parentRunId, - agentDbRecords, -) { - const issues = []; - for (const taskId of requiredSwarmManifestTaskIds) { - const journalPath = `.agent/runtime/tasks/${taskId}.jsonl`; - try { - const journal = await readSafeJsonLines( - projectPath, - journalPath, - 8 * 1024 * 1024, - ); - const currentRunRecords = journal.filter( - (record) => - record?.agentId === taskId && - record?.taskId === taskId && - record?.source === 'agent-ready-task-scheduler' && - record?.parentAgentId === 'project-supervisor' && - record?.parentRunId === parentRunId, - ); - const runIds = [ - ...new Set(currentRunRecords.map((record) => record?.runId)), - ].filter((runId) => typeof runId === 'string' && runId.length > 0); - if (runIds.length !== 1) throw new Error('logical-run-count'); - const runId = runIds[0]; - const latest = currentRunRecords - .filter((record) => record?.runId === runId) - .at(-1); - if ( - latest?.status !== 'completed' || - latest?.phase !== 'completed' || - latest?.runProfile !== 'autonomous-game-build' - ) { - throw new Error('logical-run-terminal'); - } - const count = (recordType) => - agentDbRecords.filter( - (record) => - record?.recordType === recordType && - record?.agentId === taskId && - record?.taskId === taskId && - record?.runId === runId && - record?.source === 'agent-ready-task-scheduler', - ).length; - const projections = agentDbRecords.filter( - (record) => - record?.recordType === - 'agent.runtime.autonomous_ready_task.manifest_projected' && - record?.agentId === taskId && - record?.taskId === taskId && - record?.runId === runId && - record?.source === 'agent-ready-task-scheduler' && - record?.parentAgentId === 'project-supervisor' && - record?.parentRunId === parentRunId && - record?.terminalPhase === 'completed' && - record?.manifestStatus === 'completed', - ); - if ( - count('agent.runtime.background_task') !== 1 || - count('agent.runtime.background_task.completed') !== 1 || - count('agent.runtime.background_task.failed') !== 0 || - count('agent.runtime.background_task.cancelled') !== 0 || - projections.length !== 1 - ) { - throw new Error('lifecycle-count'); - } - } catch { - issues.push({ - path: journalPath, - reason: `正式任务 ${taskId} 未在当前父 Run 中恰好启动并完成一次`, - }); - } - } - return issues; -} - -async function inspectSwarmRuntimeAcceptance(projectPath, parentRunId = null) { - const issues = []; - let manifest; - try { - manifest = await readSafeJson( - projectPath, - '.agent/manifest.json', - 2 * 1024 * 1024, - ); - } catch { - issues.push({ - path: '.agent/manifest.json', - reason: '无法读取正式任务图', - }); - } - if (manifest) { - const tasks = Array.isArray(manifest.tasks) ? manifest.tasks : []; - const taskIds = tasks.map((task) => task?.id); - const expectedIds = [...requiredSwarmManifestTaskIds].sort(); - const actualIds = [...taskIds].sort(); - if ( - tasks.length !== requiredSwarmManifestTaskIds.length || - new Set(taskIds).size !== taskIds.length || - JSON.stringify(actualIds) !== JSON.stringify(expectedIds) || - tasks.some((task) => task?.status !== 'completed') - ) { - issues.push({ - path: '.agent/manifest.json', - reason: '固定 16 个正式任务未全部且仅完成一次', - }); - } - } - - let revision; - try { - const revisionRecord = await readSafeJson( - projectPath, - '.agent/runtime/project-revision.json', - 64 * 1024, - ); - revision = revisionRecord?.revision; - if (!Number.isSafeInteger(revision) || revision < 1) throw new Error(); - } catch { - issues.push({ - path: '.agent/runtime/project-revision.json', - reason: '当前项目 revision 无效', - }); - } - - let records = []; - let recordsValid = true; - try { - const databasePath = path.join(projectPath, '.agent', 'agent.db'); - const metadata = await lstat(databasePath); - if ( - !metadata.isFile() || - metadata.isSymbolicLink() || - metadata.size < 2 || - metadata.size > 64 * 1024 * 1024 - ) { - throw new Error(); - } - records = (await readFile(databasePath, 'utf8')) - .split(/\r?\n/u) - .filter(Boolean) - .map((line) => JSON.parse(line)); - } catch { - recordsValid = false; - issues.push({ path: '.agent/agent.db', reason: 'Runtime 证据库无效' }); - } - if (!recordsValid) return issues; - if (parentRunId) { - issues.push( - ...(await inspectReadyTaskExactlyOnce(projectPath, parentRunId, records)), - ); - } - const staticSmoke = records.some( - (record) => - record?.recordType === 'agent.runtime.command.run_limited' && - record?.commandId === 'game.static_smoke' && - record?.status === 'completed' && - record?.revision === revision, - ); - if (!staticSmoke) { - issues.push({ - path: '.agent/agent.db', - reason: '缺少当前 revision 的静态检查通过凭证', - }); - } - const browserEvidence = records - .filter( - (record) => - record?.recordType === 'agent.runtime.preview.validation' && - record?.passed === true && - record?.playtestPassed === true && - record?.revision === revision, - ) - .sort((left, right) => (right.updatedAt ?? 0) - (left.updatedAt ?? 0))[0]; - if (!browserEvidence) { - issues.push({ - path: '.agent/agent.db', - reason: '缺少当前 revision 的桌面与移动试玩通过凭证', - }); - return issues; - } - const screenshots = Array.isArray(browserEvidence.screenshots) - ? browserEvidence.screenshots - : []; - for (const viewport of ['desktop', 'mobile']) { - const screenshot = screenshots.find((item) => - String(item).endsWith(`/${viewport}.png`), - ); - let screenshotValid = false; - try { - if (screenshot) { - await validatePngFile( - resolveProjectEvidencePath(projectPath, screenshot), - ); - screenshotValid = true; - } - } catch { - screenshotValid = false; - } - if (!screenshotValid) { - issues.push({ - path: '.agent/agent.db', - reason: `缺少 ${viewport} 试玩截图`, - }); - } - } - try { - const report = await readSafeJson( - projectPath, - browserEvidence.reportPath, - 2 * 1024 * 1024, - ); - const viewportResults = Array.isArray(report?.viewportResults) - ? report.viewportResults - : []; - if ( - report?.passed !== true || - report?.playtest?.passed !== true || - !['desktop', 'mobile'].every((viewport) => - viewportResults.some( - (result) => result?.viewport === viewport && result?.passed === true, - ), - ) - ) { - throw new Error(); - } - } catch { - issues.push({ - path: String( - browserEvidence.reportPath ?? '.agent/runtime/browser-validations', - ), - reason: '双视口试玩报告无效或已过期', - }); - } - return issues; -} - -export async function inspectSwarmProjectArtifacts( - projectPath, - { requireEditorImages = false, parentRunId = null } = {}, -) { - const specs = requireEditorImages - ? [...requiredFormalArtifactSpecs, ...editorImageArtifactSpecs] - : requiredFormalArtifactSpecs; - const issues = ( - await Promise.all( - specs.map((spec) => inspectFormalArtifact(projectPath, spec)), - ) - ).filter(Boolean); - issues.push( - ...(await inspectSwarmRuntimeAcceptance(projectPath, parentRunId)), - ); - return { - valid: issues.length === 0, - requireEditorImages, - invalidPaths: issues.map((issue) => issue.path), - issues, - }; -} - -export async function validateSwarmProjectArtifacts(projectPath, options) { - const inspection = await inspectSwarmProjectArtifacts(projectPath, options); - if (!inspection.valid) { - throw new Error( - `Agent Swarm 已退出,但最小正式产物检查失败:\n${inspection.issues - .map((issue) => `- ${issue.path}:${issue.reason}`) - .join('\n')}`, - ); - } - return inspection; -} - -export async function hasConfiguredEditorApiKey(configDir) { - let configured = false; - for (const fileName of [configFileName, localConfigFileName]) { - const configPath = path.join(configDir, fileName); - const metadata = await lstat(configPath).catch((error) => { - if (error?.code === 'ENOENT') return null; - throw error; - }); - if (!metadata) continue; - if (!metadata.isFile() || metadata.isSymbolicLink()) { - throw new Error(`运行配置必须是无符号链接普通文件:${fileName}`); - } - let config; - try { - config = JSON.parse(await readFile(configPath, 'utf8')); - } catch (error) { - throw new Error(`解析运行配置失败:${fileName}:${error.message}`); - } - if ( - config?.editorApi && - Object.prototype.hasOwnProperty.call(config.editorApi, 'apiKey') && - typeof config.editorApi.apiKey === 'string' - ) { - configured = config.editorApi.apiKey.trim().length > 0; - } - } - return configured; -} - -export async function openPreviewUrl(url, platform = process.platform) { - const validated = validatePreviewUrl(url); - const command = - platform === 'win32' - ? 'cmd.exe' - : platform === 'darwin' - ? 'open' - : 'xdg-open'; - const args = - platform === 'win32' - ? ['/d', '/s', '/c', 'start', '', validated] - : [validated]; - await new Promise((resolve, reject) => { - const child = spawn(command, args, { - detached: true, - stdio: 'ignore', - windowsHide: true, - }); - child.once('error', reject); - child.once('spawn', () => { - child.unref(); - resolve(); - }); - }); -} - -async function runPreview( - projectPath, - openBrowser, - setActiveChild, - stopRequested, -) { - const child = spawnChild( - cargoCommand, - buildCargoCliArguments(['--preview-serve', projectPath]), - { stdio: ['ignore', 'pipe', 'pipe'] }, - ); - setActiveChild(child); - child.stdout.setEncoding('utf8'); - child.stderr.setEncoding('utf8'); - child.stdout.pipe(process.stdout); - child.stderr.pipe(process.stderr); - - let pending = ''; - let previewUrl = null; - let resolvePreviewUrl; - let rejectPreviewUrl; - const previewUrlReady = new Promise((resolve, reject) => { - resolvePreviewUrl = resolve; - rejectPreviewUrl = reject; - }); - child.stdout.on('data', (chunk) => { - pending += chunk; - const lines = pending.split(/\r?\n/); - pending = lines.pop() ?? ''; - for (const line of lines) { - if (!line.startsWith('previewUrl=')) continue; - try { - previewUrl = validatePreviewUrl( - line.slice('previewUrl='.length).trim(), - ); - resolvePreviewUrl(previewUrl); - } catch (error) { - rejectPreviewUrl(error); - } - } - }); - - const exitPromise = childExit(child); - try { - exitPromise.then(({ code, signal }) => { - if (!previewUrl) { - rejectPreviewUrl( - new Error( - `预览进程提前退出:code=${code ?? ''} signal=${signal ?? ''}`, - ), - ); - } - }); - const url = await previewUrlReady; - console.log(`\n试玩地址:${url}`); - console.log('按 Ctrl+C 结束预览。'); - if (openBrowser) { - await openPreviewUrl(url).catch((error) => { - console.warn( - `无法自动打开浏览器,请手动访问上面的地址:${error.message}`, - ); - }); - } - const result = await exitPromise; - if (!stopRequested() && (result.code !== 0 || result.signal)) { - throw new Error( - `预览进程异常退出:code=${result.code ?? ''} signal=${result.signal ?? ''}`, - ); - } - } finally { - if (!closedChildren.has(child)) { - await terminateChildTreeAndWait( - child, - exitPromise, - 'SIGINT', - '持续预览进程', - ); - } - setActiveChild(null); - } -} - -export async function runSwarmTestChat(options) { - let sourceConfigDir = null; - let runtimeConfig = null; - let project = null; - let activeChild = null; - let receivedSignal = null; - let timedOut = false; - let phase = 'setup'; - let runnerMayHaveStarted = false; - let turnReport = null; - let primaryError = null; - let forceTerminationHandle = null; - const setActiveChild = (child) => { - activeChild = child; - }; - const concurrentChildren = new Set(); - const stopRequested = () => receivedSignal !== null; - const handleSignal = (signal) => { - const repeatedSignal = receivedSignal !== null; - receivedSignal ??= signal; - const targets = [activeChild, ...concurrentChildren].filter(Boolean); - if (targets.length === 0) return; - for (const target of targets) { - void terminateChildTree(target, signal, repeatedSignal).catch(() => {}); - } - if (repeatedSignal) return; - forceTerminationHandle = setTimeout(() => { - for (const target of [activeChild, ...concurrentChildren].filter( - Boolean, - )) { - void terminateChildTree(target, 'SIGKILL', true).catch(() => {}); - } - }, childTerminationGraceMs); - forceTerminationHandle.unref(); - }; - const clearForceTermination = () => { - if (forceTerminationHandle) { - clearTimeout(forceTerminationHandle); - forceTerminationHandle = null; - } - }; - const timeoutMs = resolveSwarmTestTimeoutMs(options); - const timeoutDeadline = timeoutMs === null ? null : Date.now() + timeoutMs; - const timeoutHandle = - timeoutMs === null - ? null - : setTimeout(() => { - timedOut = true; - console.error( - `agc:test:chat 已达到 ${options.timeoutMinutes ?? 50} 分钟执行期限,正在安全收束。`, - ); - handleSignal('SIGTERM'); - }, timeoutMs); - timeoutHandle?.unref(); - process.on('SIGINT', handleSignal); - process.on('SIGTERM', handleSignal); - - try { - session: { - try { - sourceConfigDir = await discoverRuntimeConfigDir(options.configDir); - } catch (error) { - if (!(await askToConfigureMissingRuntime())) throw error; - await runMissingConfigWizard(setActiveChild, options.configDir); - sourceConfigDir = await discoverRuntimeConfigDir(options.configDir); - } - runtimeConfig = await prepareSwarmTestRuntimeConfig(sourceConfigDir); - if (receivedSignal) break session; - project = await prepareSwarmTestProject(options.projectDir); - if (receivedSignal) break session; - - console.log(`配置来源:${path.join(sourceConfigDir, configFileName)}`); - console.log( - `隔离运行配置:${path.join(runtimeConfig.path, configFileName)}`, - ); - console.log(`测试项目:${project.path}`); - if (options.dryRun) { - console.log('测试环境检查通过;未启动 LLM。'); - break session; - } - - console.log('\n正在检查 LLM 配置...'); - const llmStatus = await runCapturedCargo( - ['--config-dir', runtimeConfig.path, '--llm-status'], - setActiveChild, - ); - if (receivedSignal) break session; - if (llmStatus.code !== 0 || llmStatus.signal) { - throw new Error( - `LLM 配置未就绪:${llmStatus.stderr.trim() || llmStatus.stdout.trim()}`, - ); - } - console.log('LLM 配置已就绪。'); - console.log( - options.task - ? '已提交一条非交互游戏需求,正在等待 Swarm 自主完成。\n' - : '输入一条游戏需求并回车;提交后按 Ctrl+D,让 Swarm 自主完成。\n', - ); - - phase = 'chat'; - runnerMayHaveStarted = true; - const chatArguments = [ - '--config-dir', - runtimeConfig.path, - '--swarm-chat', - '--init', - '--autonomous-game-build', - project.path, - ]; - let chat; - try { - chat = options.task - ? await runTaskCargo( - chatArguments, - options.task, - setActiveChild, - timeoutDeadline === null - ? null - : Math.max(1, timeoutDeadline - Date.now()), - ) - : await runInteractiveCargo(chatArguments, setActiveChild); - } catch (error) { - if (error?.code === 'AGC_CHILD_TIMEOUT') timedOut = true; - throw error; - } - if (receivedSignal) break session; - if (chat.code !== 0 || chat.signal) { - throw new Error( - `Agent Swarm 未正常收束:code=${chat.code ?? ''} signal=${chat.signal ?? ''}`, - ); - } - if (options.task) { - turnReport = parseSettledSwarmTurnReport(chat.turnReportOutput); - } - const requireEditorImages = await hasConfiguredEditorApiKey( - runtimeConfig.path, - ); - await validateSwarmProjectArtifacts(project.path, { - requireEditorImages, - parentRunId: turnReport?.parentRunId ?? null, - }); - - if (!shouldStartPersistentPreview(options)) { - phase = 'complete'; - console.log( - `\n真实 Swarm 测试通过:正式产物${ - requireEditorImages ? '及画布图片' : '' - }已验收,Runtime 已完成静态检查和双视口试玩。`, - ); - break session; - } - - phase = 'preview'; - console.log('\nAgent Swarm 已收束,正在启动试玩...'); - await runPreview( - project.path, - options.openBrowser, - setActiveChild, - stopRequested, - ); - phase = 'complete'; - } - } catch (error) { - primaryError = error; - } finally { - if (timeoutHandle) clearTimeout(timeoutHandle); - clearForceTermination(); - let cleanupError = null; - let runnerStopped = true; - if (runtimeConfig && runnerMayHaveStarted) { - try { - runnerStopped = await shutdownSwarmTestRunner( - runtimeConfig, - setActiveChild, - ); - if (!runnerStopped) { - cleanupError = new Error( - '隔离 Agent Runner 仍有任务,已保留测试项目和隔离运行配置', - ); - } - } catch (error) { - runnerStopped = false; - cleanupError = error; - } - } - const preserveFailedRun = - project?.owned && - (phase === 'chat' || - phase === 'artifact-validation' || - (phase === 'preview' && !receivedSignal)); - const preserveForRunner = project?.owned && !runnerStopped; - if ( - project?.owned && - (options.keepProject || preserveFailedRun || preserveForRunner) - ) { - if (preserveFailedRun && !options.keepProject) { - console.warn( - '测试尚未正常结束,为避免删除后台任务或失败证据,测试项目不会自动清理。', - ); - } - console.log(`已保留测试项目:${project.path}`); - } else if (project?.owned) { - try { - await cleanupSwarmTestProject(project); - console.log('已清理一次性测试项目。'); - } catch (error) { - cleanupError ??= error; - } - } - if (runtimeConfig) { - if (runnerStopped) { - try { - await cleanupSwarmTestRuntimeConfig(runtimeConfig); - console.log('已清理隔离运行配置。'); - } catch (error) { - cleanupError ??= error; - } - } else { - console.warn(`已保留隔离运行配置:${runtimeConfig.path}`); - } - } - if (cleanupError) { - if (primaryError) { - console.warn(`测试现场清理未完成:${cleanupError.message}`); - } else { - primaryError = cleanupError; - } - } - process.off('SIGINT', handleSignal); - process.off('SIGTERM', handleSignal); - } - if (timedOut) { - if (primaryError) { - console.warn(`超时收束附带错误:${primaryError.message}`); - } - primaryError = Object.assign(new Error('真实 Swarm 测试超过内部执行期限'), { - exitCode: 124, - }); - } else if (receivedSignal) { - if (primaryError) { - console.warn(`信号收束附带错误:${primaryError.message}`); - } - primaryError = Object.assign( - new Error(`真实 Swarm 测试收到 ${receivedSignal}`), - { exitCode: receivedSignal === 'SIGINT' ? 130 : 143 }, - ); - } - if (primaryError) throw primaryError; -} - -async function main() { - const options = parseSwarmTestArguments(process.argv.slice(2)); - if (options.help) { - console.log(usage); - return; - } - await runSwarmTestChat(options); -} - -const entryPath = process.argv[1] - ? pathToFileURL(path.resolve(process.argv[1])).href - : ''; -if (entryPath === import.meta.url) { - main().catch((error) => { - console.error(`agc:test:chat 失败:${error.message}`); - process.exitCode = Number.isInteger(error.exitCode) ? error.exitCode : 1; - }); -} diff --git a/apps/ai-game-creator-shell/scripts/build-release.mjs b/apps/ai-game-creator-shell/scripts/build-release.mjs index 742e4f8d1..85d4b63a5 100644 --- a/apps/ai-game-creator-shell/scripts/build-release.mjs +++ b/apps/ai-game-creator-shell/scripts/build-release.mjs @@ -21,7 +21,7 @@ import { prepareNsisToolsetForRelease } from './nsis-toolset.mjs'; import { stageNodeRuntime } from './stage-node-runtime.mjs'; const appRoot = fileURLToPath(new URL('..', import.meta.url)); -// 提交摘要里的 pathspec 与 `git log` 都以仓库根为基准,不能在应用目录里执行。 +// Git 命令用于读取 release revision,必须在仓库根执行,不能在应用目录里执行。 const repoRoot = path.resolve(appRoot, '..', '..'); const defaultReleaseTarget = 'x86_64-pc-windows-msvc'; function defaultTarget() { @@ -96,8 +96,8 @@ const defaultOssBaseUrl = export { resolveReleaseChannel } from './channel-identity.mjs'; /** - * 影响 Windows 客户端产物的路径。调度管线的发布范围判定与这里的提交摘要必须 - * 保持一致 —— `build-release.test.mjs` 有守卫用例逐条比对两边。 + * 影响 Windows 客户端产物的路径。调度管线的发布范围判定必须与这里保持一致, + * `build-release.test.mjs` 有守卫用例逐条比对两边。 */ export const agcReleasePathPatterns = [ 'apps/ai-game-creator-shell/', @@ -228,43 +228,6 @@ async function readManifestVersion(manifestUrl, label) { : parseVersion(manifest?.version, `${label} version`); } -/** 上一次发布的渠道清单:拿版本做高水位、拿 commit 生成自动更新摘要。 */ -async function readRemoteChannelManifest(channel, target) { - return fetchManifest(updateManifestUrl(channel, target), 'OSS 渠道清单'); -} - -/** - * 摘要锚点:上次发布对应的提交。 - * - * 首选渠道清单里的 `commit`(发布产物自己的事实来源);清单缺该字段时(首次启用 - * 摘要、或更换渠道后清单还没带过 commit)回退到 CI 传入的 `AGC_UPDATE_PREVIOUS_COMMIT` - * —— 它是上一次成功构建的 COMMIT_HASH,同样指向用户拿到的那个版本。 - */ -export async function resolvePreviousReleaseCommit( - channel = resolveReleaseChannel(), - { - override = process.env.AGC_UPDATE_PREVIOUS_COMMIT, - target = defaultTarget(), - } = {}, -) { - const explicit = override?.trim(); - if (explicit && /^[0-9a-f]{7,40}$/u.test(explicit)) { - return explicit; - } - try { - const manifest = await readRemoteChannelManifest(channel, target); - const commit = - typeof manifest?.commit === 'string' ? manifest.commit.trim() : ''; - return /^[0-9a-f]{7,40}$/u.test(commit) ? commit : null; - } catch (error) { - // 摘要只是附注:清单读不到(网络抖动等)不能把发布带崩,降级为「没有锚点」。 - console.warn( - `[ai-game-creator-shell] 读取摘要锚点失败,本次不写自动摘要:${error.message}`, - ); - return null; - } -} - /** * 版本高水位:渠道清单与旧协议迁移指针取较大值。 * @@ -632,7 +595,7 @@ export function createUpdateManifest( pub_date: publishedAt, platforms, downloads, - // 非标准字段:更新插件会忽略,发布脚本用它定位下一次自动更新摘要的起点。 + // 非标准字段:更新插件会忽略,仅保留源码 revision 供线上排障。 ...(commit ? { commit } : {}), }; } @@ -648,129 +611,6 @@ function readHeadCommit() { } } -/** - * Git 的 %s 会把「标题后紧接说明行、没有空行」的整个首段拼成一行; - * 更新摘要只允许展示原始提交消息的第一行,避免把说明暴露给用户。 - */ -function commitMessageTitle(message) { - return String(message ?? '') - .split(/\r?\n/u, 1)[0] - .trim(); -} - -/** 解析 `git log -z --format=%h%x09%B`,只保留每个 commit 的消息首行。 */ -function parseReleaseCommitLog(output) { - return output - .split('\0') - .map((record) => record.trimEnd()) - .filter(Boolean) - .map((record) => { - const separator = record.indexOf('\t'); - if (separator < 0) return null; - const sha = record.slice(0, separator); - const subject = commitMessageTitle(record.slice(separator + 1)); - return sha && subject ? { sha, subject } : null; - }) - .filter(Boolean); -} - -/** - * 上一次发布到本次之间的客户端相关提交。 - * - * 返回 null 表示无法判定(没有上一次 commit,或本地没有该提交),此时不生成摘要。 - */ -export function collectReleaseCommits( - previousCommit, - headCommit = 'HEAD', - { cwd = repoRoot, paths = agcReleasePathPatterns } = {}, -) { - if (!previousCommit) return null; - try { - for (const revision of [previousCommit, headCommit]) { - execFileSync('git', ['rev-parse', '--verify', `${revision}^{commit}`], { - cwd, - stdio: 'pipe', - }); - } - } catch { - return null; - } - let output; - try { - output = execFileSync( - 'git', - [ - 'log', - '-z', - '--no-merges', - '--format=%h%x09%B', - `${previousCommit}..${headCommit}`, - '--', - ...paths, - ], - { cwd, encoding: 'utf8' }, - ); - } catch { - return null; - } - return parseReleaseCommitLog(output); -} - -/** 自动更新摘要:逐条列客户端相关改动标题,超过上限时折叠并整体截断。 */ -export function formatReleaseNotes( - commits, - { limit = 12, subjectLength = 80, maxLength = 900 } = {}, -) { - if (!commits || commits.length === 0) return ''; - const lines = commits.slice(0, limit).map(({ subject }) => { - const title = commitMessageTitle(subject); - const trimmed = - title.length > subjectLength - ? `${title.slice(0, subjectLength - 1)}…` - : title; - return `- ${trimmed}`; - }); - if (commits.length > limit) { - lines.push(`- 其余 ${commits.length - limit} 项客户端改动省略`); - } - const text = lines.join('\n'); - return text.length > maxLength ? `${text.slice(0, maxLength - 1)}…` : text; -} - -/** 无锚点时的兜底:列出最近的客户端相关提交,并注明可能与上一版重复。 */ -export function collectRecentReleaseCommits({ - cwd = repoRoot, - paths = agcReleasePathPatterns, - limit = 8, -} = {}) { - let output; - try { - output = execFileSync( - 'git', - [ - 'log', - '-z', - '--no-merges', - `-n${limit}`, - '--format=%h%x09%B', - '--', - ...paths, - ], - { cwd, encoding: 'utf8' }, - ); - } catch { - return null; - } - const commits = parseReleaseCommitLog(output); - return commits.length > 0 ? commits : null; -} - -export function formatRecentReleaseNotes(commits) { - const notes = formatReleaseNotes(commits, { limit: 8 }); - if (!notes) return ''; - return `最近客户端改动(未定位到上一次发布提交,可能与上一版重复):\n${notes}`; -} - /** 旧协议(sha256)清单:只用于把已发布客户端带到新渠道协议,一个版本周期后整条删除。 */ export function createLegacyUpdateManifest( artifactPath, @@ -810,19 +650,10 @@ export async function generateUpdateManifest( version: readPackageJson().version, artifact, }); - const manualNotes = readReleaseNotes(); - const previousCommit = await resolvePreviousReleaseCommit(channel, { - target, - }); - const commits = collectReleaseCommits(previousCommit); - const recentCommits = previousCommit ? null : collectRecentReleaseCommits(); - const notes = - manualNotes || - formatReleaseNotes(commits) || - formatRecentReleaseNotes(recentCommits); - if (!manualNotes && !notes) { + const notes = readReleaseNotes(); + if (!notes) { console.log( - `[ai-game-creator-shell] 未生成自动更新摘要(上一发布 commit=${previousCommit ?? '未知'},客户端相关提交=${commits ? commits.length : '不可判定'},最近提交=${recentCommits ? recentCommits.length : '不可判定'})`, + '[ai-game-creator-shell] 未生成更新摘要;如需携带说明,请设置 AGC_UPDATE_RELEASE_NOTES', ); } const manifest = createUpdateManifest(artifact, { @@ -857,11 +688,9 @@ export async function generateUpdateManifest( console.log(`[ai-game-creator-shell] 安装包:${artifact}`); console.log(`[ai-game-creator-shell] 首装包:${downloadArtifact}`); console.log( - manualNotes + notes ? '[ai-game-creator-shell] 更新摘要:使用 AGC_UPDATE_RELEASE_NOTES 手动文案' - : notes && !previousCommit - ? `[ai-game-creator-shell] 更新摘要:无锚点,列出最近 ${recentCommits ? recentCommits.length : 0} 条客户端相关提交` - : `[ai-game-creator-shell] 更新摘要:自动汇总 ${commits ? commits.length : 0} 条客户端相关提交(起点 ${previousCommit ?? '无'})`, + : '[ai-game-creator-shell] 更新摘要:未生成', ); console.log(`[ai-game-creator-shell] 更新摘要文件:${notesPath}`); if (legacyManifestPath) { @@ -878,8 +707,6 @@ export async function generateUpdateManifest( manifestPath, notes, notesPath, - previousCommit, - commits, legacyManifest, legacyManifestPath, }; diff --git a/apps/ai-game-creator-shell/scripts/build-release.test.mjs b/apps/ai-game-creator-shell/scripts/build-release.test.mjs index 427166456..fefd89e67 100644 --- a/apps/ai-game-creator-shell/scripts/build-release.test.mjs +++ b/apps/ai-game-creator-shell/scripts/build-release.test.mjs @@ -1,12 +1,5 @@ import assert from 'node:assert/strict'; -import { execFileSync } from 'node:child_process'; -import { - mkdirSync, - mkdtempSync, - readFileSync, - rmSync, - writeFileSync, -} from 'node:fs'; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { test } from 'node:test'; @@ -16,18 +9,13 @@ import { agcReleasePathPatterns, buildRelease, buildTauriBuildArguments, - collectRecentReleaseCommits, - collectReleaseCommits, compareVersions, createChannelConfig, createLegacyUpdateManifest, createUpdateManifest, - formatRecentReleaseNotes, - formatReleaseNotes, generateUpdateManifest, nextPatchVersion, resolveManifestPlatformKeys, - resolvePreviousReleaseCommit, resolveReleaseChannel, resolveReleaseContext, resolveReleasePartition, @@ -897,9 +885,15 @@ for (const channel of ['release', 'beta-2']) { assert.equal(result.channel, channel); assert.equal(result.target, target); assert.equal(result.manifest.version, packageVersion); + assert.equal(result.notes, ''); + assert.equal(result.manifest.notes, undefined); + assert.equal( + readFileSync(result.notesPath, 'utf8'), + '(本次没有可用的更新摘要)\n', + ); assert.equal(result.legacyManifestPath, null); assert.equal(result.legacyManifest, null); - assert.equal(requests.length, 2); + assert.equal(requests.length, 1); for (const entry of [ ...Object.values(result.manifest.platforms), ...Object.values(result.manifest.downloads), @@ -973,96 +967,6 @@ test('version high water ignores the windows migration pointer for other channel ); }); -test('release notes anchor prefers the explicit commit and falls back to the manifest', async () => { - await withStubbedFetch( - () => jsonResponse({ version: '0.1.61', commit: 'abcdef1234567890' }), - async () => { - assert.equal( - await resolvePreviousReleaseCommit('dev', { - override: '6017d46088c04199e99cf89f347b12d67591475e', - }), - '6017d46088c04199e99cf89f347b12d67591475e', - ); - // 覆盖值非法时忽略,继续用清单里的 commit。 - assert.equal( - await resolvePreviousReleaseCommit('dev', { - override: 'not-a-sha', - }), - 'abcdef1234567890', - ); - assert.equal( - await resolvePreviousReleaseCommit('dev', { override: ' ' }), - 'abcdef1234567890', - ); - }, - ); - - await withStubbedFetch( - () => jsonResponse({ version: '0.1.61' }), - async () => { - assert.equal( - await resolvePreviousReleaseCommit('dev', { override: undefined }), - null, - ); - }, - ); -}); - -test('release notes anchor degrades to null when the manifest cannot be read', async () => { - const originalFetch = globalThis.fetch; - globalThis.fetch = async () => { - throw new Error('fetch failed'); - }; - try { - assert.equal( - await resolvePreviousReleaseCommit('dev', { override: undefined }), - null, - ); - } finally { - globalThis.fetch = originalFetch; - } -}); - -test('recent commit fallback marks that entries may repeat the previous release', () => { - const directory = mkdtempSync(path.join(os.tmpdir(), 'agc-recent-git-')); - const git = (...args) => - execFileSync('git', args, { cwd: directory, encoding: 'utf8' }); - try { - git('init', '--quiet'); - git('config', 'user.email', 'release@example.test'); - git('config', 'user.name', 'release test'); - mkdirSync(path.join(directory, 'apps/ai-game-creator-shell'), { - recursive: true, - }); - for (const name of ['one', 'two']) { - writeFileSync( - path.join(directory, `apps/ai-game-creator-shell/${name}.rs`), - `fn ${name}() {}\n`, - ); - git('add', '.'); - git('commit', '--quiet', '-m', `客户端:${name}`); - } - writeFileSync(path.join(directory, 'README.md'), '# 文档\n'); - git('add', '.'); - git('commit', '--quiet', '-m', '文档:说明'); - - const recent = collectRecentReleaseCommits({ cwd: directory, limit: 5 }); - assert.deepEqual( - recent.map((entry) => entry.subject), - ['客户端:two', '客户端:one'], - ); - const notes = formatRecentReleaseNotes(recent); - assert.match( - notes, - /^最近客户端改动(未定位到上一次发布提交,可能与上一版重复):\n- 客户端:two/u, - ); - assert.equal(formatRecentReleaseNotes([]), ''); - assert.equal(formatRecentReleaseNotes(null), ''); - } finally { - rmSync(directory, { recursive: true, force: true }); - } -}); - test('release entry forwards the built artifacts and dry-run mode to the uploader', () => { const source = readFileSync( new URL('./release-upload.mjs', import.meta.url), @@ -1077,112 +981,6 @@ test('release entry forwards the built artifacts and dry-run mode to the uploade assert.ok(source.includes('\n dryRun,\n')); }); -test('release notes list client commit subjects only and bound their size', () => { - const notes = formatReleaseNotes([ - { sha: 'a5fd25f1', subject: '客户端更新切换到官方更新插件' }, - { sha: '55af6014', subject: '修'.repeat(120) }, - ]); - const lines = notes.split('\n'); - assert.equal(lines.length, 2); - assert.equal(lines[0], '- 客户端更新切换到官方更新插件'); - const truncatedSubject = lines[1].replace(/^- /u, ''); - assert.equal(truncatedSubject.length, 80, `主题应截断到 80 字:${lines[1]}`); - assert.match(truncatedSubject, /…$/u); - - const many = formatReleaseNotes( - Array.from({ length: 20 }, (_, index) => ({ - sha: `sha${index}`, - subject: `改动 ${index}`, - })), - ); - assert.match(many, /- 其余 8 项客户端改动省略$/u); - assert.equal( - formatReleaseNotes([ - { - sha: 'ignored', - subject: '提交标题\n不应展示的说明一\n不应展示的说明二', - }, - ]), - '- 提交标题', - ); - assert.equal(formatReleaseNotes([]), ''); - assert.equal(formatReleaseNotes(null), ''); -}); - -test('release commits cover only client paths and skip merge commits', () => { - const directory = mkdtempSync(path.join(os.tmpdir(), 'agc-changelog-git-')); - const git = (...args) => - execFileSync('git', args, { cwd: directory, encoding: 'utf8' }); - try { - git('init', '--quiet'); - git('config', 'user.email', 'release@example.test'); - git('config', 'user.name', 'release test'); - git('commit', '--allow-empty', '--quiet', '-m', '基点'); - const base = git('rev-parse', 'HEAD').trim(); - - mkdirSync(path.join(directory, 'apps/ai-game-creator-shell'), { - recursive: true, - }); - mkdirSync(path.join(directory, 'docs'), { recursive: true }); - writeFileSync( - path.join(directory, 'apps/ai-game-creator-shell/main.rs'), - 'fn main() {}\n', - ); - const commitMessagePath = path.join( - directory, - '.git', - 'commit-message.txt', - ); - writeFileSync( - commitMessagePath, - '客户端:新增更新插件接入\n补充更新插件接入的详细说明\n', - ); - git('add', '.'); - git('commit', '--quiet', '-F', commitMessagePath); - - writeFileSync(path.join(directory, 'docs/readme.md'), '# 文档\n'); - git('add', '.'); - git('commit', '--quiet', '-m', '文档:补充说明'); - - writeFileSync( - path.join(directory, 'apps/ai-game-creator-shell/other.rs'), - 'fn other() {}\n', - ); - git('add', '.'); - git('commit', '--quiet', '-m', '客户端:修复版本回退'); - - git('checkout', '--quiet', '-b', 'side'); - writeFileSync( - path.join(directory, 'apps/ai-game-creator-shell/side.rs'), - 'fn side() {}\n', - ); - git('add', '.'); - git('commit', '--quiet', '-m', '客户端:侧分支改动'); - git('checkout', '--quiet', 'master'); - git('merge', '--quiet', '--no-ff', '--no-edit', 'side'); - - const commits = collectReleaseCommits(base, 'HEAD', { cwd: directory }); - assert.ok(commits, '应能在临时仓库里收集提交'); - const subjects = commits.map((entry) => entry.subject); - // 标题后没有空行时,git %s 会把说明行拼进标题;摘要必须取原始首行。 - // 合并提交本身被 --no-merges 排除,但它带入的客户端改动仍然计入。 - assert.deepEqual(subjects, [ - '客户端:侧分支改动', - '客户端:修复版本回退', - '客户端:新增更新插件接入', - ]); - assert.ok(commits.every((entry) => /^[0-9a-f]{7,}$/u.test(entry.sha))); - - assert.equal( - collectReleaseCommits('1234567890abcdef', 'HEAD', { cwd: directory }), - null, - ); - assert.equal(collectReleaseCommits(null, 'HEAD', { cwd: directory }), null); - } finally { - rmSync(directory, { recursive: true, force: true }); - } -}); - test('scheduler path filter stays in sync with client release paths', () => { const jenkinsfile = readFileSync( new URL( diff --git a/apps/ai-game-creator-shell/scripts/check-config.mjs b/apps/ai-game-creator-shell/scripts/check-config.mjs index 89edb4cf8..448f91c87 100644 --- a/apps/ai-game-creator-shell/scripts/check-config.mjs +++ b/apps/ai-game-creator-shell/scripts/check-config.mjs @@ -23,10 +23,6 @@ execFileSync( { stdio: 'inherit' }, ); -import { - appIdentifier, - defaultRealSwarmTestTask, -} from './agent-swarm-test-chat.mjs'; import { AGC_APP_IDENTIFIER, AGC_PRODUCT_NAME, @@ -83,10 +79,6 @@ const defaultAppConfig = JSON.parse( const rootPackageConfig = JSON.parse( fs.readFileSync(new URL('../../../package.json', import.meta.url), 'utf8'), ); -const swarmTestChatSource = fs.readFileSync( - new URL('../scripts/agent-swarm-test-chat.mjs', import.meta.url), - 'utf8', -); const viteConfigSource = fs.readFileSync( new URL('../vite.config.ts', import.meta.url), 'utf8', @@ -131,9 +123,9 @@ const rustSharedContractSource = fs.readFileSync( ); const allowedUncalledTauriCommands = [ 'append_direct_project_conversation_message', - // Supervisor 调试窗口、开发者面板、专业 Agent 对话与旧命令聊天的前端调用方已随 - // Supervisor 前端链路整体删除;命令本身仍注册在 Rust 侧并由 native Runtime、CLI - // swarm 与 Rust 测试使用,保留 present,仅不再出现在 App 前端源码里。 + // Supervisor 调试窗口、开发者面板与专业 Agent 对话的前端调用方已随 Supervisor + // 前端链路整体删除;命令本身仍注册在 Rust 侧供 native Runtime 与 Rust 测试使用, + // 保留 present,仅不出现在 App 前端源码里。 'answer_game_creator_agent_runtime_user_input', 'cancel_game_creator_agent_runtime_task', 'chat_with_game_creator_role_agent', @@ -141,20 +133,17 @@ const allowedUncalledTauriCommands = [ 'check_game_creator_llm_config', 'confirm_game_creator_agent_runtime_task', 'diff_local_project_checkpoint', - 'get_game_creation_agent_capabilities', - 'get_limited_local_commands', 'list_local_project_export_packages', 'read_game_creator_agent_runtime', 'read_local_agent_memory', 'read_local_game_memory', + 'read_local_project_file', 'reject_game_creator_agent_runtime_task', 'retry_game_creator_agent_runtime_task', 'schedule_game_creator_agent_ready_tasks', - 'start_game_creator_agent_runtime_task', - 'steer_game_creator_agent_runtime_task', 'write_local_agent_memory', 'write_local_game_memory', - // Agent 运行时会话 / 目标 / 协作命令由 native 侧与 CLI swarm 驱动,前端没有调用方。 + // Agent 运行时会话 / 目标 / 协作命令由 native 侧驱动,前端没有调用方。 'archive_game_creator_agent_session', 'clear_game_creator_agent_goal', 'compact_game_creator_agent_runtime_context', @@ -801,14 +790,14 @@ async function runConfigWizardRegressionChecks() { assertSafeGameCreatorConfigDestination(outsideConfigDir, { requireDedicatedLeaf: true, }), - new RegExp(appIdentifier.replaceAll('.', '\\.')), + new RegExp(AGC_APP_IDENTIFIER.replaceAll('.', '\\.')), ); - const dedicatedConfigDir = path.join(testRoot, appIdentifier); + const dedicatedConfigDir = path.join(testRoot, AGC_APP_IDENTIFIER); assert.equal( await assertSafeGameCreatorConfigDestination(dedicatedConfigDir, { requireDedicatedLeaf: true, }), - path.join(canonicalTestRoot, appIdentifier), + path.join(canonicalTestRoot, AGC_APP_IDENTIFIER), ); const injectedNonGitConfigDir = path.join(testRoot, 'injected-non-git'); @@ -1303,15 +1292,6 @@ if ( ); } -if ( - packageConfig.scripts?.swarm !== - 'node scripts/run-cli-with-config.mjs --swarm-chat' -) { - throw new Error( - 'AI game creator shell swarm must use client config before starting the interactive Agent runtime', - ); -} - if ( packageConfig.scripts?.config !== 'node scripts/game-creator-config-wizard.mjs' @@ -1321,31 +1301,6 @@ if ( ); } -if ( - packageConfig.scripts?.['test:chat'] !== - `node scripts/agent-swarm-test-chat.mjs --task ${JSON.stringify(defaultRealSwarmTestTask)} --no-open` -) { - throw new Error( - 'AI game creator shell test:chat must use the one-click Swarm test entry', - ); -} - -if ( - packageConfig.scripts?.['test:chat:manual'] !== - 'node scripts/agent-swarm-test-chat.mjs' -) { - throw new Error( - 'AI game creator shell test:chat:manual must keep the interactive Swarm test entry', - ); -} - -if ( - rootPackageConfig.scripts?.['agc:test'] !== - 'npm --prefix apps/ai-game-creator-shell run agent-runtime:supervisor-autonomous-playable-lane-defense-deterministic-e2e --' -) { - throw new Error('agc:test must delegate to the deterministic playable E2E'); -} - if ( rootPackageConfig.scripts?.['agc:config'] !== 'npm --prefix apps/ai-game-creator-shell run config --' @@ -1355,37 +1310,6 @@ if ( ); } -if ( - rootPackageConfig.scripts?.['agc:test:chat'] !== - 'npm --prefix apps/ai-game-creator-shell run test:chat --' -) { - throw new Error( - 'agc:test:chat must delegate to the one-click Swarm test entry', - ); -} - -if ( - rootPackageConfig.scripts?.['agc:test:chat:manual'] !== - 'npm --prefix apps/ai-game-creator-shell run test:chat:manual --' -) { - throw new Error( - 'agc:test:chat:manual must delegate to the interactive Swarm test entry', - ); -} - -for (const requiredSource of [ - "import { AGC_APP_IDENTIFIER } from './channel-identity.mjs'", - 'export const appIdentifier = AGC_APP_IDENTIFIER', - "'--swarm-chat'", - "'--autonomous-game-build'", - "'--preview-serve'", - 'cleanupSwarmTestProject(project)', -]) { - if (!swarmTestChatSource.includes(requiredSource)) { - throw new Error(`Swarm test entry contract drifted: ${requiredSource}`); - } -} - // 基线配置必须等于默认渠道的安装身份:默认渠道不能改身份,否则已发布客户端 // 的升级链路与既有安装目录都会断开。 const defaultChannelIdentity = resolveChannelInstallIdentity('dev'); @@ -1945,19 +1869,13 @@ for (const snippet of [ '陶泥儿智能创作(固定)', '官方账号服务(固定)', 'runtime_config.save', - "'/run:运行自检,启动本地 HTTP 预览并载入客户端运行视图'", '已载入客户端运行视图', 'async function executeRunLocal', 'function needsInitializedChatProject', - "'/remember [short|long|blackboard] 内容:追加短期、长期或黑板记忆'", - "'/memory-set [short|long|blackboard] 内容:覆盖保存对应记忆'", - 'function parseRememberInput', - "'/trace 或 /loop:查看最近一次 Agent loop trace'", "'permission.pending'", "'permission.confirm'", "'permission.cancel'", "'command.auto'", - 'function summarizeAgentRunTrace', ]) { if (!appSource.includes(snippet)) { throw new Error( @@ -1971,7 +1889,6 @@ for (const script of [ 'ai-game-creator-shell:dev-server', 'ai-game-creator-shell:build', 'ai-game-creator-shell:agent-task', - 'agc:swarm', 'ai-game-creator-shell:typecheck', 'ai-game-creator-shell:agent-run:smoke', 'ai-game-creator-shell:check', @@ -1981,13 +1898,6 @@ for (const script of [ } } -if ( - rootPackageConfig.scripts?.['agc:swarm'] !== - 'npm --prefix apps/ai-game-creator-shell run swarm --' -) { - throw new Error('root agc:swarm script must forward CLI args'); -} - if ( rootPackageConfig.scripts?.['ai-game-creator-shell:build'] !== 'npm --prefix apps/ai-game-creator-shell run build --' diff --git a/apps/ai-game-creator-shell/scripts/deterministic-lane-defense-provider.mjs b/apps/ai-game-creator-shell/scripts/deterministic-lane-defense-provider.mjs deleted file mode 100644 index 9c8318c57..000000000 --- a/apps/ai-game-creator-shell/scripts/deterministic-lane-defense-provider.mjs +++ /dev/null @@ -1,3040 +0,0 @@ -import http from 'node:http'; -import { deflateSync } from 'node:zlib'; - -const LOOPBACK_HOST = '127.0.0.1'; -const DEFAULT_FALLBACK_PORTS = Object.freeze( - Array.from({ length: 128 }, (_, index) => 62_128 + index), -); -const CHAT_COMPLETIONS_PATH = '/v1/chat/completions'; -const MAX_REQUEST_BYTES = 16 * 1024 * 1024; -const PNG_SIGNATURE = Buffer.from([ - 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, -]); -const PNG_CRC_TABLE = Object.freeze( - Array.from({ length: 256 }, (_, value) => { - let crc = value; - for (let bit = 0; bit < 8; bit += 1) { - crc = (crc & 1) !== 0 ? 0xedb88320 ^ (crc >>> 1) : crc >>> 1; - } - return crc >>> 0; - }), -); - -export const deterministicLaneDefenseModel = - 'deterministic-lane-defense-provider-v1'; -export const deterministicManifestReadyAgentIds = Object.freeze([ - 'design-director', - 'design-foundation', - 'balance-director', - 'balance-seed', - 'art-director', - 'art-asset-plan', - 'art-polish', - 'audio-director', - 'audio-asset-plan', - 'code-director', - 'code-prototype', - 'quality-review', - 'preview-readiness', - 'preview-playtest', - 'publish-strategy', - 'publish-package', -]); -const deterministicReadOnlyReadyAgentIds = new Set([ - 'design-director', - 'balance-director', - 'art-polish', - 'audio-director', - 'code-director', - 'quality-review', - 'preview-readiness', - 'preview-playtest', - 'publish-strategy', -]); -// These owner tasks are checked by Runtime's owner-artifact gate. Their -// request-scoped tool catalog deliberately removes command.run_limited, so a -// deterministic response must deliver after a successful fixed-path write -// instead of trying to emit a tool that the runtime did not advertise. -const deterministicOwnerArtifactValidationAgentIds = new Set([ - 'design-foundation', - 'balance-seed', - 'art-asset-plan', - 'audio-asset-plan', -]); -const deterministicProjectMutationTools = new Set([ - 'file.write', - 'file.patch', - 'file.delete', - 'project.patchset', - 'project.restore', - 'command.exec', - 'command.start', - 'canvas.asset_generate', -]); -export const hiddenCanvasCss = '#game{display:none;'; -export const visibleCanvasCss = '#game{display:block;'; - -function pngCrc32(bytes) { - let crc = 0xffffffff; - for (const byte of bytes) { - crc = PNG_CRC_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8); - } - return (crc ^ 0xffffffff) >>> 0; -} - -function pngChunk(type, data) { - const typeBytes = Buffer.from(type, 'ascii'); - const length = Buffer.alloc(4); - length.writeUInt32BE(data.length); - const crc = Buffer.alloc(4); - crc.writeUInt32BE(pngCrc32(Buffer.concat([typeBytes, data]))); - return Buffer.concat([length, typeBytes, data, crc]); -} - -function deterministicPng( - width, - height, - { variant = 0, transparent = false } = {}, -) { - const stride = width * 4 + 1; - const pixels = Buffer.alloc(stride * height); - for (let y = 0; y < height; y += 1) { - const row = y * stride; - pixels[row] = 0; - for (let x = 0; x < width; x += 1) { - const offset = row + 1 + x * 4; - const tile = (Math.floor(x / 48) + Math.floor(y / 48)) % 6; - pixels[offset] = (42 + x + tile * 29 + variant * 13) % 256; - pixels[offset + 1] = (86 + y * 2 + tile * 17 + variant * 19) % 256; - pixels[offset + 2] = (118 + x + y + tile * 31 + variant * 23) % 256; - pixels[offset + 3] = - transparent && (x + y + variant) % 11 === 0 ? 0 : 255; - } - } - const ihdr = Buffer.alloc(13); - ihdr.writeUInt32BE(width, 0); - ihdr.writeUInt32BE(height, 4); - ihdr[8] = 8; - ihdr[9] = 6; - return Buffer.concat([ - PNG_SIGNATURE, - pngChunk('IHDR', ihdr), - pngChunk('IDAT', deflateSync(pixels, { level: 6 })), - pngChunk('IEND', Buffer.alloc(0)), - ]); -} - -export function deterministicLaneDefenseInitialHtml() { - return ` - - - -灵露花园 - - -
Garden defenders -

灵露花园

GENARRATIVE_REAL_E2E_VISIBLE

Goal: defend the garden and win every wave.

-
-
-
Level 1 ready
- -
-`; -} - -const deterministicLaneDefenseBalance = Object.freeze({ - schemaVersion: 'lane-defense-balance.v1', - startingSun: 150, - defenderCosts: { 'nectar-bloom': 50, 'thorn-sentry': 100 }, - enemyHealth: 100, - enemySpeed: 20, - waveTimingsMs: [120, 300, 560], - scorePerEnemy: 100, - levelDifficultyMultiplier: 1.18, -}); - -export function deterministicLaneDefenseCanonicalHtml() { - const upstreamContract = JSON.stringify({ - schemaVersion: 'lane-defense-upstream-contract.v1', - design: 'game/game_design.md', - balance: 'game/balance.json', - art: 'assets/manifest.art.json', - audio: 'assets/manifest.audio.json', - }); - const balance = JSON.stringify(deterministicLaneDefenseBalance); - return deterministicLaneDefenseInitialHtml() - .replace(hiddenCanvasCss, visibleCanvasCss) - .replace( - '
', - '
', - ) - .replace( - '\n\n', - 'exports/README.md': - '# Export\n\n项目已完成静态检查与桌面、移动双视口试玩。\n', -}; - -const fixturePngSignature = Buffer.from([ - 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, -]); - -function fixtureCrc32(bytes: Buffer): number { - let value = 0xffffffff; - for (const byte of bytes) { - value ^= byte; - for (let bit = 0; bit < 8; bit += 1) { - value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1; - } - } - return (value ^ 0xffffffff) >>> 0; -} - -function fixturePngChunk(type: string, data: Buffer): Buffer { - const typeBytes = Buffer.from(type, 'ascii'); - const chunk = Buffer.alloc(12 + data.length); - chunk.writeUInt32BE(data.length, 0); - typeBytes.copy(chunk, 4); - data.copy(chunk, 8); - chunk.writeUInt32BE( - fixtureCrc32(Buffer.concat([typeBytes, data])), - 8 + data.length, - ); - return chunk; -} - -function fixturePng( - width: number, - height: number, - { invalidFilter = false, trailingCompressedBytes = false } = {}, -): Buffer { - const ihdr = Buffer.alloc(13); - ihdr.writeUInt32BE(width, 0); - ihdr.writeUInt32BE(height, 4); - ihdr[8] = 1; - ihdr[9] = 0; - const rowBytes = Math.ceil(width / 8); - const scanlines = Buffer.alloc((rowBytes + 1) * height); - let pseudoRandom = 0x12345678; - for (let row = 0; row < height; row += 1) { - const rowOffset = row * (rowBytes + 1); - scanlines[rowOffset] = invalidFilter && row === 0 ? 5 : 0; - for (let column = 0; column < rowBytes; column += 1) { - pseudoRandom = (Math.imul(pseudoRandom, 1664525) + 1013904223) >>> 0; - scanlines[rowOffset + column + 1] = pseudoRandom >>> 24; - } - } - return Buffer.concat([ - fixturePngSignature, - fixturePngChunk('IHDR', ihdr), - fixturePngChunk( - 'IDAT', - trailingCompressedBytes - ? Buffer.concat([deflateSync(scanlines), Buffer.from('junk')]) - : deflateSync(scanlines), - ), - fixturePngChunk('IEND', Buffer.alloc(0)), - ]); -} - -function fixtureIndexedPng({ - includePalette = true, - duplicatePalette = false, - unknownCriticalChunk = false, -} = {}): Buffer { - const ihdr = Buffer.alloc(13); - ihdr.writeUInt32BE(1, 0); - ihdr.writeUInt32BE(1, 4); - ihdr[8] = 8; - ihdr[9] = 3; - const palette = fixturePngChunk('PLTE', Buffer.from([0, 0, 0])); - return Buffer.concat([ - fixturePngSignature, - fixturePngChunk('IHDR', ihdr), - ...(includePalette ? [palette] : []), - ...(duplicatePalette ? [palette] : []), - ...(unknownCriticalChunk ? [fixturePngChunk('ABCD', Buffer.alloc(0))] : []), - fixturePngChunk('IDAT', deflateSync(Buffer.from([0, 0]))), - fixturePngChunk('IEND', Buffer.alloc(0)), - ]); -} - -async function writeMinimumFormalArtifacts(root: string): Promise { - for (const [relativePath, content] of Object.entries( - minimumFormalArtifactContents, - )) { - const targetPath = path.join(root, ...relativePath.split('/')); - await mkdir(path.dirname(targetPath), { recursive: true }); - await writeFile(targetPath, content); - } - const revision = 8; - const reportPath = - '.agent/runtime/browser-validations/publish-package/test-run/8/validation.json'; - const desktopPath = - '.agent/runtime/browser-validations/publish-package/test-run/8/desktop.png'; - const mobilePath = - '.agent/runtime/browser-validations/publish-package/test-run/8/mobile.png'; - await mkdir(path.join(root, '.agent', 'runtime'), { recursive: true }); - await writeFile( - path.join(root, '.agent', 'manifest.json'), - `${JSON.stringify({ - tasks: requiredSwarmManifestTaskIds.map((id) => ({ - id, - status: 'completed', - })), - })}\n`, - ); - await writeFile( - path.join(root, '.agent', 'runtime', 'project-revision.json'), - `${JSON.stringify({ revision })}\n`, - ); - const evidenceDirectory = path.dirname(path.join(root, reportPath)); - await mkdir(evidenceDirectory, { recursive: true }); - await writeFile(path.join(root, desktopPath), fixturePng(1280, 720)); - await writeFile(path.join(root, mobilePath), fixturePng(390, 844)); - await writeFile( - path.join(root, reportPath), - `${JSON.stringify({ - passed: true, - playtest: { passed: true }, - viewportResults: [ - { viewport: 'desktop', passed: true }, - { viewport: 'mobile', passed: true }, - ], - })}\n`, - ); - await writeFile( - path.join(root, '.agent', 'agent.db'), - [ - JSON.stringify({ - recordType: 'agent.runtime.command.run_limited', - commandId: 'game.static_smoke', - status: 'completed', - revision, - }), - JSON.stringify({ - recordType: 'agent.runtime.preview.validation', - passed: true, - playtestPassed: true, - revision, - reportPath, - screenshots: [desktopPath, mobilePath], - updatedAt: 1, - }), - '', - ].join('\n'), - ); -} - -async function writeReadyTaskExactlyOnceEvidence( - root: string, - parentRunId: string, -): Promise { - const databasePath = path.join(root, '.agent', 'agent.db'); - const database = await readFile(databasePath, 'utf8'); - const records: Array> = []; - for (const taskId of requiredSwarmManifestTaskIds) { - const runId = `autonomous-ready-${taskId}-fixture`; - const taskDirectory = path.join(root, '.agent', 'runtime', 'tasks'); - await mkdir(taskDirectory, { recursive: true }); - await writeFile( - path.join(taskDirectory, `${taskId}.jsonl`), - `${JSON.stringify({ - agentId: taskId, - taskId, - runId, - source: 'agent-ready-task-scheduler', - runProfile: 'autonomous-game-build', - parentAgentId: 'project-supervisor', - parentRunId, - status: 'completed', - phase: 'completed', - })}\n`, - ); - records.push( - { - recordType: 'agent.runtime.background_task', - agentId: taskId, - taskId, - runId, - source: 'agent-ready-task-scheduler', - }, - { - recordType: 'agent.runtime.background_task.completed', - agentId: taskId, - taskId, - runId, - source: 'agent-ready-task-scheduler', - }, - { - recordType: 'agent.runtime.autonomous_ready_task.manifest_projected', - agentId: taskId, - taskId, - runId, - source: 'agent-ready-task-scheduler', - parentAgentId: 'project-supervisor', - parentRunId, - terminalPhase: 'completed', - manifestStatus: 'completed', - }, - ); - } - await writeFile( - databasePath, - `${database.trimEnd()}\n${records.map((record) => JSON.stringify(record)).join('\n')}\n`, - ); -} - -describe('terminal configuration wizard arguments', () => { - it('parses the supported options and documented defaults', () => { - const configDir = path.resolve('fixture-config'); - - expect(parseConfigWizardArguments([])).toEqual({ - configDir: null, - configureOnly: false, - help: false, - }); - expect( - parseConfigWizardArguments([ - '--config-dir', - configDir, - '--configure-only', - '--help', - ]), - ).toEqual({ configDir, configureOnly: true, help: true }); - expect(parseConfigWizardArguments(['-h']).help).toBe(true); - }); - - it.each([ - ['separate API key argument', ['--api-key', 'fixture-secret'], 'API Key'], - ['inline API key argument', ['--api-key=fixture-secret'], 'API Key'], - ['relative config directory', ['--config-dir', 'relative'], '绝对路径'], - ['missing config directory', ['--config-dir'], '缺少目录路径'], - [ - 'duplicate config directory', - ['--config-dir', '/first', '--config-dir', '/second'], - '只能指定一次', - ], - ['unknown option', ['--unknown'], '未知选项'], - ])('rejects %s', (_label, args, marker) => { - expect(() => parseConfigWizardArguments(args)).toThrow(marker); - }); -}); - -describe('terminal configuration wizard AppData paths', () => { - it.each([ - { - label: 'Linux XDG config', - context: { - platform: 'linux', - environment: { XDG_CONFIG_HOME: '/fixture/xdg' }, - homeDirectory: '/fixture/home', - }, - candidate: path.posix.join('/fixture/xdg', appIdentifier), - }, - { - label: 'macOS Application Support', - context: { - platform: 'darwin', - environment: {}, - homeDirectory: '/Users/fixture', - }, - candidate: path.posix.join( - '/Users/fixture', - 'Library', - 'Application Support', - appIdentifier, - ), - }, - { - label: 'Windows roaming AppData', - context: { - platform: 'win32', - environment: { - APPDATA: 'C:\\Users\\fixture\\AppData\\Roaming', - LOCALAPPDATA: 'C:\\Users\\fixture\\AppData\\Local', - }, - homeDirectory: 'C:\\Users\\fixture', - }, - candidate: path.win32.join( - 'C:\\Users\\fixture\\AppData\\Roaming', - appIdentifier, - ), - }, - ])( - 'resolves the GUI-compatible $label directory', - ({ context, candidate }) => { - expect(resolveGameCreatorAppConfigDir(context)).toBe(candidate); - }, - ); - - it('uses an explicit absolute directory and rejects a relative one', () => { - const explicitConfigDir = path.resolve('explicit-config'); - - expect(resolveGameCreatorAppConfigDir({ explicitConfigDir })).toBe( - explicitConfigDir, - ); - expect(() => - resolveGameCreatorAppConfigDir({ explicitConfigDir: 'relative-config' }), - ).toThrow('绝对路径'); - }); -}); - -describe('terminal configuration wizard providers', () => { - it('publishes the supported provider presets exactly', () => { - expect(gameCreatorProviderPresets).toEqual([ - { - id: 'openai', - label: 'OpenAI', - baseUrl: 'https://api.openai.com/v1', - model: 'gpt-4.1', - apiKind: 'openai_responses', - }, - { - id: 'deepseek', - label: 'DeepSeek', - baseUrl: 'https://api.deepseek.com', - model: 'deepseek-chat', - apiKind: 'openai_chat', - }, - { - id: 'anthropic', - label: 'Anthropic', - baseUrl: 'https://api.anthropic.com', - model: 'claude-3-5-sonnet-latest', - apiKind: 'anthropic', - }, - { - id: 'ark', - label: '火山 Ark', - baseUrl: 'https://ark.cn-beijing.volces.com/api/v3', - model: 'doubao-seed-1-6', - apiKind: 'openai_chat', - }, - { - id: 'custom', - label: '自定义', - baseUrl: '', - model: '', - apiKind: 'openai_chat', - }, - ]); - expect(Object.isFrozen(gameCreatorProviderPresets)).toBe(true); - }); -}); - -describe('terminal configuration wizard config merge', () => { - it('updates the default LLM while retaining per-Agent and Editor API config', () => { - const existingConfig = { - schemaVersion: 'game-creator-config.v2', - agentMode: 'codex_app_server', - llm: { - apiKey: 'old-fixture-secret', - model: 'old-model', - maxRetries: 2, - }, - agentLlm: { - 'project-supervisor': { providerId: 'supervisor-provider' }, - }, - editorApi: { - apiKey: 'fixture-editor-secret', - baseUrl: 'https://editor.example.test/v1', - }, - }; - const originalConfig = structuredClone(existingConfig); - - const merged = buildGameCreatorWizardConfig(existingConfig, { - apiKey: ' new-fixture-secret ', - baseUrl: 'https://llm.example.test/v1/', - model: ' fixture-model ', - apiKind: 'openai_chat', - }); - - expect(merged).toEqual({ - ...originalConfig, - agentMode: 'provider', - llm: { - apiKey: 'new-fixture-secret', - model: 'fixture-model', - maxRetries: 2, - baseUrl: 'https://llm.example.test/v1', - apiKind: 'openai_chat', - reasoningEffort: 'high', - }, - }); - expect(merged.agentLlm).toEqual(originalConfig.agentLlm); - expect(merged.editorApi).toEqual(originalConfig.editorApi); - expect(existingConfig).toEqual(originalConfig); - }); - - it('uses provider-default reasoning for Anthropic', () => { - expect( - buildGameCreatorWizardConfig( - { llm: { webSearchEnabled: true } }, - { - apiKey: 'fixture-secret', - baseUrl: 'https://api.anthropic.com/', - model: 'claude-fixture', - apiKind: 'anthropic', - }, - ).llm, - ).toMatchObject({ reasoningEffort: 'default', webSearchEnabled: false }); - }); -}); - -describe('terminal configuration wizard URL safety', () => { - it.each([ - ['https://provider.example.test/v1///', 'https://provider.example.test/v1'], - ['http://127.0.0.1:8080/v1/', 'http://127.0.0.1:8080/v1'], - ['http://localhost:8080/', 'http://localhost:8080'], - ['http://[::1]:8080/v1/', 'http://[::1]:8080/v1'], - ])('accepts %s', (input, expected) => { - expect(normalizeWizardBaseUrl(input)).toBe(expected); - }); - - it.each([ - '', - 'not-a-url', - 'http://provider.example.test/v1', - 'ftp://provider.example.test/v1', - 'https://user:password@provider.example.test/v1', - 'https://provider.example.test/v1?token=fixture', - 'https://provider.example.test/v1#fragment', - ])('rejects unsafe Base URL %s', (input) => { - expect(() => normalizeWizardBaseUrl(input)).toThrowError(); - }); -}); - -describe('terminal configuration wizard persistence', () => { - it('atomically replaces a private AppData config file', async () => { - await withTemporaryRoot(async (root) => { - const configDir = path.join(root, 'nested', appIdentifier); - const configPath = path.join(configDir, configFileName); - const firstConfig = { llm: { model: 'first-fixture-model' } }; - const finalConfig = { - llm: { model: 'final-fixture-model', apiKey: 'fixture-secret' }, - agentLlm: { 'project-supervisor': { model: 'agent-fixture-model' } }, - }; - - await expect( - writeGameCreatorConfigAtomically( - configPath, - firstConfig, - skippedWindowsAclOptions, - ), - ).resolves.toBe(configPath); - const firstMetadata = await lstat(configPath); - await expect( - writeGameCreatorConfigAtomically( - configPath, - finalConfig, - skippedWindowsAclOptions, - ), - ).resolves.toBe(configPath); - - const [directoryMetadata, finalMetadata, entries, contents] = - await Promise.all([ - lstat(configDir), - lstat(configPath), - readdir(configDir), - readFile(configPath, 'utf8'), - ]); - expect(JSON.parse(contents)).toEqual(finalConfig); - expect(contents.endsWith('\n')).toBe(true); - expect(entries).toEqual([configFileName]); - expect(finalMetadata.isFile()).toBe(true); - expect(finalMetadata.isSymbolicLink()).toBe(false); - if (process.platform !== 'win32') { - expect(directoryMetadata.mode & 0o077).toBe(0); - expect(finalMetadata.mode & 0o077).toBe(0); - expect([finalMetadata.dev, finalMetadata.ino]).not.toEqual([ - firstMetadata.dev, - firstMetadata.ino, - ]); - } - }); - }); - - it('moves the default LLM to primary config and lets later GUI saves win', async () => { - await withTemporaryRoot(async (root) => { - const configDir = path.join(root, appIdentifier); - const primaryPath = path.join(configDir, configFileName); - const localPath = path.join(configDir, localConfigFileName); - await mkdir(configDir); - await writeFile( - primaryPath, - '{"llm":{"model":"primary","requestTimeoutMs":12345},"editorApi":{"apiKey":"canvas"}}\n', - ); - await writeFile( - localPath, - '{"llm":{"model":"stale-local","stream":true},"agentLlm":{"planner":{"model":"planner"}}}\n', - ); - - const state = await readGameCreatorWizardConfigState( - configDir, - skippedWindowsAclOptions, - ); - expect(state.configPath).toBe(primaryPath); - expect(state.effectiveConfig.llm.model).toBe('stale-local'); - const wizardConfig = buildGameCreatorWizardConfig(state.writeConfig, { - apiKey: 'wizard-key', - baseUrl: 'https://provider.example.test/v1', - model: 'wizard-model', - apiKind: 'openai_chat', - }); - await writeGameCreatorWizardConfig( - state, - wizardConfig, - skippedWindowsAclOptions, - ); - - const sanitizedLocal = JSON.parse(await readFile(localPath, 'utf8')); - expect(sanitizedLocal.llm).toBeUndefined(); - expect(sanitizedLocal.agentLlm.planner.model).toBe('planner'); - const afterWizard = await readGameCreatorWizardConfigState( - configDir, - skippedWindowsAclOptions, - ); - expect(afterWizard.effectiveConfig.llm).toMatchObject({ - apiKey: 'wizard-key', - model: 'wizard-model', - stream: true, - requestTimeoutMs: 12345, - }); - - const guiConfig = JSON.parse(await readFile(primaryPath, 'utf8')); - guiConfig.llm = { - ...guiConfig.llm, - apiKey: 'gui-key', - model: 'gui-model', - }; - await writeGameCreatorConfigAtomically( - primaryPath, - guiConfig, - skippedWindowsAclOptions, - ); - const afterGui = await readGameCreatorWizardConfigState( - configDir, - skippedWindowsAclOptions, - ); - expect(afterGui.effectiveConfig.llm.apiKey).toBe('gui-key'); - expect(afterGui.effectiveConfig.llm.model).toBe('gui-model'); - }); - }); -}); - -describe('Swarm test argument parsing', () => { - it('returns the documented defaults', () => { - expect(parseSwarmTestArguments([])).toEqual({ - configDir: null, - projectDir: null, - keepProject: false, - openBrowser: true, - task: null, - timeoutMinutes: null, - dryRun: false, - help: false, - }); - }); - - it('parses every supported option', () => { - const configDir = path.resolve('fixture-config'); - const projectDir = path.resolve('fixture-project'); - - expect( - parseSwarmTestArguments([ - '--config-dir', - configDir, - '--project-dir', - projectDir, - '--keep-project', - '--no-open', - '--task', - '生成一款可试玩的塔防游戏', - '--timeout-minutes', - '75', - '--dry-run', - '--help', - ]), - ).toEqual({ - configDir, - projectDir, - keepProject: true, - openBrowser: false, - task: '生成一款可试玩的塔防游戏', - timeoutMinutes: 75, - dryRun: true, - help: true, - }); - expect(parseSwarmTestArguments(['-h']).help).toBe(true); - expect(() => parseSwarmTestArguments(['--task', ''])).toThrowError(); - }); - - it('auto-answers only the confirmation and choice prompts', () => { - expect( - nextSwarmAutoPilotReply( - '[待确认] agent=project-supervisor run=r action=a tool=agent.delegate\n输入 approve 或 reject:', - ), - ).toBe('approve'); - expect(nextSwarmAutoPilotReply('请选择 1-3,或直接输入其他答案:')).toBe( - '1', - ); - // 提示词已经被上一轮消费掉、不在缓冲末尾时不得重复应答。 - expect( - nextSwarmAutoPilotReply('输入 approve 或 reject:\n[已批准] action-1\n'), - ).toBeNull(); - expect( - nextSwarmAutoPilotReply('[状态] project-supervisor running'), - ).toBeNull(); - }); - - it('closes the driven stdin only at the prompt that follows submission', () => { - // CLI 先打印提示符再读行:第一个「你>」是用来读我们这条任务的,此时本轮还没 - // 开始跑。在那里收 stdin,确认卡弹出来时已经是 EOF,自动应答器根本没机会回 - // approve——实测就这样把一轮跑成了 pending-confirmation。 - expect(swarmAutoPilotSitsAtPrompt('你> ')).toBe(true); - expect(swarmAutoPilotShouldCloseInput('你> ', 0)).toBe(false); - expect(swarmAutoPilotShouldCloseInput('你> ', 1)).toBe(true); - // 总控判定直接回复那条路径没有 turn 回执,同样靠这个提示符收口。 - expect(swarmAutoPilotShouldCloseInput('[意图] reply\n\n你> ', 1)).toBe( - true, - ); - // 提示符不在缓冲末尾、或本轮还在推进时不得收 stdin。 - expect( - swarmAutoPilotSitsAtPrompt( - '你> [决策] Agent 正在判断直接回复或调用持久能力。', - ), - ).toBe(false); - expect( - swarmAutoPilotSitsAtPrompt('[状态] project-supervisor running/planning'), - ).toBe(false); - }); - - it('keeps persistent preview only for manual chat mode', () => { - expect(shouldStartPersistentPreview(parseSwarmTestArguments([]))).toBe( - true, - ); - expect( - shouldStartPersistentPreview( - parseSwarmTestArguments(['--task', '生成一款可试玩的塔防游戏']), - ), - ).toBe(false); - }); - - it('applies a bounded default only to non-interactive tasks', () => { - expect(resolveSwarmTestTimeoutMs(parseSwarmTestArguments([]))).toBeNull(); - expect( - resolveSwarmTestTimeoutMs( - parseSwarmTestArguments(['--task', '生成原创塔防游戏']), - ), - ).toBe(50 * 60_000); - expect( - resolveSwarmTestTimeoutMs( - parseSwarmTestArguments(['--timeout-minutes', '12']), - ), - ).toBe(12 * 60_000); - }); - - it.each([ - { - label: 'duplicate config directory', - args: ['--config-dir', '/first', '--config-dir', '/second'], - marker: '--config-dir', - }, - { - label: 'duplicate project directory', - args: ['--project-dir', '/first', '--project-dir', '/second'], - marker: '--project-dir', - }, - { - label: 'missing config directory', - args: ['--config-dir'], - marker: '--config-dir', - }, - { - label: 'missing project directory', - args: ['--project-dir', '--dry-run'], - marker: '--project-dir', - }, - { - label: 'duplicate timeout', - args: ['--timeout-minutes', '10', '--timeout-minutes', '20'], - marker: '--timeout-minutes', - }, - { - label: 'invalid timeout', - args: ['--timeout-minutes', '0'], - marker: '1-1440', - }, - { - label: 'unknown option', - args: ['--unsupported'], - marker: '--unsupported', - }, - ])('rejects $label', ({ args, marker }) => { - expect(() => parseSwarmTestArguments(args)).toThrow(marker); - }); -}); - -describe('runtime config directory candidates', () => { - it('uses an absolute Linux XDG config root', () => { - expect( - defaultRuntimeConfigDirCandidates({ - platform: 'linux', - environment: { XDG_CONFIG_HOME: '/fixture/xdg' }, - homeDirectory: '/fixture/home', - }), - ).toEqual([path.posix.join('/fixture/xdg', appIdentifier)]); - }); - - it('falls back to the Linux home config root', () => { - expect( - defaultRuntimeConfigDirCandidates({ - platform: 'linux', - environment: { XDG_CONFIG_HOME: 'relative-xdg' }, - homeDirectory: '/fixture/home', - }), - ).toEqual([path.posix.join('/fixture/home', '.config', appIdentifier)]); - }); - - it('uses the macOS Application Support directory', () => { - expect( - defaultRuntimeConfigDirCandidates({ - platform: 'darwin', - environment: {}, - homeDirectory: '/Users/fixture', - }), - ).toEqual([ - path.posix.join( - '/Users/fixture', - 'Library', - 'Application Support', - appIdentifier, - ), - ]); - }); - - it('uses both Windows roaming and local AppData directories', () => { - const appData = 'C:\\Users\\fixture\\AppData\\Roaming'; - const localAppData = 'C:\\Users\\fixture\\AppData\\Local'; - - expect( - defaultRuntimeConfigDirCandidates({ - platform: 'win32', - environment: { - APPDATA: appData, - LOCALAPPDATA: localAppData, - }, - homeDirectory: 'C:\\Users\\fixture', - }), - ).toEqual([ - path.win32.join(appData, appIdentifier), - path.win32.join(localAppData, appIdentifier), - ]); - }); -}); - -describe('runtime config discovery', () => { - it('passes an empty explicit config directory through the TTY wizard only', () => { - const explicitConfigDir = path.resolve('empty-explicit-config'); - const argumentsForWizard = - buildMissingConfigWizardArguments(explicitConfigDir); - - expect(argumentsForWizard.slice(1)).toEqual([ - '--configure-only', - '--config-dir', - explicitConfigDir, - ]); - expect(argumentsForWizard[0]).toMatch(/game-creator-config-wizard\.mjs$/u); - expect(canPromptForMissingRuntimeConfig(true, true)).toBe(true); - expect(canPromptForMissingRuntimeConfig(false, true)).toBe(false); - expect(canPromptForMissingRuntimeConfig(true, false)).toBe(false); - }); - - it('discovers an explicitly selected config directory', async () => { - await withTemporaryRoot(async (root) => { - const configDir = path.join(root, 'explicit-config'); - await mkdir(configDir); - await writeFile(path.join(configDir, configFileName), '{}\n'); - - await expect( - discoverRuntimeConfigDir(configDir, { - platform: 'linux', - environment: { XDG_CONFIG_HOME: path.join(root, 'unused') }, - homeDirectory: path.join(root, 'unused-home'), - }), - ).resolves.toBe(await realpath(configDir)); - }); - }); - - it('discovers a config directory from an isolated XDG root', async () => { - await withTemporaryRoot(async (root) => { - const xdgRoot = path.join(root, 'xdg'); - const configDir = path.join(xdgRoot, appIdentifier); - await mkdir(configDir, { recursive: true }); - await writeFile(path.join(configDir, configFileName), '{}\n'); - - await expect( - discoverRuntimeConfigDir(null, { - platform: 'linux', - environment: { XDG_CONFIG_HOME: xdgRoot }, - homeDirectory: path.join(root, 'unused-home'), - }), - ).resolves.toBe(await realpath(configDir)); - }); - }); - - it.skipIf(process.platform === 'win32')( - 'rejects symlinked and non-file config entries', - async () => { - await withTemporaryRoot(async (root) => { - const target = path.join(root, 'config-target.json'); - await writeFile(target, '{}\n'); - - const symlinkConfigDir = path.join(root, 'symlink-config'); - await mkdir(symlinkConfigDir); - await symlink(target, path.join(symlinkConfigDir, configFileName)); - await expect( - discoverRuntimeConfigDir(symlinkConfigDir), - ).rejects.toThrow(configFileName); - - const directoryConfigDir = path.join(root, 'directory-config'); - await mkdir(path.join(directoryConfigDir, configFileName), { - recursive: true, - }); - await expect( - discoverRuntimeConfigDir(directoryConfigDir), - ).rejects.toThrow(configFileName); - }); - }, - ); - - it('rejects a relative explicit config directory', async () => { - await expect(discoverRuntimeConfigDir('relative-config')).rejects.toThrow( - '--config-dir', - ); - }); -}); - -describe('isolated Swarm runtime config', () => { - it('privately copies only active config files and removes the owned directory', async () => { - await withTemporaryRoot(async (root) => { - const sourceConfigDir = path.join(root, 'source-config'); - await mkdir(sourceConfigDir); - await writeFile( - path.join(sourceConfigDir, configFileName), - '{"llm":{"apiKey":"fixture-credential"}}\n', - { mode: 0o600 }, - ); - await writeFile( - path.join(sourceConfigDir, localConfigFileName), - '{"llm":{"model":"fixture-model"}}\n', - { mode: 0o600 }, - ); - await writeFile( - path.join(sourceConfigDir, runnerEndpointFileName), - '{"mustNotCopy":true}\n', - ); - await writeFile( - path.join(sourceConfigDir, 'agent-runner.lock'), - 'must-not-copy\n', - ); - await writeFile( - path.join(sourceConfigDir, `.${configFileName}.previous`), - 'must-not-copy\n', - ); - const sourceMetadata = await lstat( - path.join(sourceConfigDir, configFileName), - ); - - const runtimeConfig = await prepareSwarmTestRuntimeConfig( - sourceConfigDir, - root, - skippedWindowsAclOptions, - ); - const runtimeEntries = (await readdir(runtimeConfig.path)).sort(); - const runtimeDirectoryMetadata = await lstat(runtimeConfig.path); - const primaryMetadata = await lstat( - path.join(runtimeConfig.path, configFileName), - ); - const localMetadata = await lstat( - path.join(runtimeConfig.path, localConfigFileName), - ); - const sentinel = JSON.parse( - await readFile( - path.join(runtimeConfig.path, testRuntimeConfigSentinelName), - 'utf8', - ), - ); - - expect(runtimeConfig.sourcePath).toBe(await realpath(sourceConfigDir)); - expect(path.basename(runtimeConfig.path)).toMatch( - new RegExp(`^${testRuntimeConfigPrefix}`), - ); - expect(runtimeEntries).toEqual( - [ - configFileName, - localConfigFileName, - testRuntimeConfigSentinelName, - ].sort(), - ); - expect( - await readFile(path.join(runtimeConfig.path, configFileName), 'utf8'), - ).toBe('{"llm":{"apiKey":"fixture-credential"}}\n'); - expect( - await readFile( - path.join(runtimeConfig.path, localConfigFileName), - 'utf8', - ), - ).toBe('{"llm":{"model":"fixture-model"}}\n'); - expect(sentinel).toEqual({ - schemaVersion: testRuntimeConfigSentinelSchema, - token: runtimeConfig.sentinelToken, - }); - if (process.platform !== 'win32') { - expect(runtimeDirectoryMetadata.mode & 0o077).toBe(0); - expect(primaryMetadata.mode & 0o077).toBe(0); - expect(localMetadata.mode & 0o077).toBe(0); - expect([primaryMetadata.dev, primaryMetadata.ino]).not.toEqual([ - sourceMetadata.dev, - sourceMetadata.ino, - ]); - } - expect( - await pathExists(path.join(runtimeConfig.path, runnerEndpointFileName)), - ).toBe(false); - - await expect(cleanupSwarmTestRuntimeConfig(runtimeConfig)).resolves.toBe( - true, - ); - expect(await pathExists(runtimeConfig.path)).toBe(false); - expect(await pathExists(sourceConfigDir)).toBe(true); - expect( - await readFile(path.join(sourceConfigDir, configFileName), 'utf8'), - ).toBe('{"llm":{"apiKey":"fixture-credential"}}\n'); - }); - }); - - it('rejects a symlinked local override without leaving a temporary directory', async () => { - if (process.platform === 'win32') return; - await withTemporaryRoot(async (root) => { - const sourceConfigDir = path.join(root, 'source-config'); - const localTarget = path.join(root, 'local-target.json'); - await mkdir(sourceConfigDir); - await writeFile(path.join(sourceConfigDir, configFileName), '{}\n'); - await writeFile(localTarget, '{}\n'); - await symlink( - localTarget, - path.join(sourceConfigDir, localConfigFileName), - ); - - await expect( - prepareSwarmTestRuntimeConfig( - sourceConfigDir, - root, - skippedWindowsAclOptions, - ), - ).rejects.toThrow(localConfigFileName); - expect( - (await readdir(root)).filter((entry) => - entry.startsWith(testRuntimeConfigPrefix), - ), - ).toEqual([]); - }); - }); - - it('refuses to delete an isolated config while its Runner endpoint exists', async () => { - await withTemporaryRoot(async (root) => { - const sourceConfigDir = path.join(root, 'source-config'); - await mkdir(sourceConfigDir); - await writeFile(path.join(sourceConfigDir, configFileName), '{}\n'); - const runtimeConfig = await prepareSwarmTestRuntimeConfig( - sourceConfigDir, - root, - skippedWindowsAclOptions, - ); - const endpointPath = path.join( - runtimeConfig.path, - runnerEndpointFileName, - ); - await writeFile(endpointPath, '{}\n'); - - await expect( - cleanupSwarmTestRuntimeConfig(runtimeConfig), - ).rejects.toThrow('Runner'); - expect(await pathExists(runtimeConfig.path)).toBe(true); - - await rm(endpointPath); - await expect(cleanupSwarmTestRuntimeConfig(runtimeConfig)).resolves.toBe( - true, - ); - }); - }); -}); - -describe('Swarm test project ownership', () => { - it('creates a sentinel-owned project and removes it during cleanup', async () => { - await withTemporaryRoot(async (root) => { - const project = await prepareSwarmTestProject(null, root); - const sentinelPath = path.join(project.path, testProjectSentinelName); - const sentinelMetadata = await lstat(sentinelPath); - const sentinel = JSON.parse(await readFile(sentinelPath, 'utf8')); - - expect(project.owned).toBe(true); - expect(project.sentinelToken).toEqual(expect.any(String)); - expect(path.basename(project.path)).toMatch( - new RegExp(`^${testProjectPrefix}`), - ); - expect(sentinelMetadata.isFile()).toBe(true); - expect(sentinelMetadata.isSymbolicLink()).toBe(false); - expect(sentinel).toEqual({ - schemaVersion: testProjectSentinelSchema, - token: project.sentinelToken, - }); - - await expect(cleanupSwarmTestProject(project)).resolves.toBe(true); - expect(await pathExists(project.path)).toBe(false); - }); - }); - - it('refuses cleanup after the sentinel identity is changed', async () => { - await withTemporaryRoot(async (root) => { - const project = await prepareSwarmTestProject(null, root); - await writeFile( - path.join(project.path, testProjectSentinelName), - `${JSON.stringify({ - schemaVersion: testProjectSentinelSchema, - token: 'changed-token', - })}\n`, - ); - - await expect(cleanupSwarmTestProject(project)).rejects.toThrowError(); - expect(await pathExists(project.path)).toBe(true); - }); - }); - - it('keeps an explicit empty directory unowned', async () => { - await withTemporaryRoot(async (root) => { - const explicitProjectDir = path.join(root, 'explicit-project'); - await mkdir(explicitProjectDir); - - const project = await prepareSwarmTestProject(explicitProjectDir, root); - - expect(project).toEqual({ - path: await realpath(explicitProjectDir), - owned: false, - sentinelToken: null, - }); - await expect(cleanupSwarmTestProject(project)).resolves.toBe(false); - expect(await readdir(explicitProjectDir)).toEqual([]); - }); - }); - - it('rejects a non-empty uninitialized explicit directory', async () => { - await withTemporaryRoot(async (root) => { - const explicitProjectDir = path.join(root, 'uninitialized-project'); - await mkdir(explicitProjectDir); - await writeFile(path.join(explicitProjectDir, 'existing.txt'), 'fixture'); - - await expect( - prepareSwarmTestProject(explicitProjectDir, root), - ).rejects.toThrow('--project-dir'); - }); - }); -}); - -describe('cargo CLI argument construction', () => { - it('uses the shell manifest and separates cargo from application arguments', () => { - const cliArguments = ['--config-dir', 'fixture-config', '--llm-status']; - const cargoArguments = buildCargoCliArguments(cliArguments); - - // Assert the separator invariants rather than fixed positions: cargo flags - // may be added before `--`, but everything after it must reach the CLI - // unchanged, and the manifest must stay this shell's own. - const separatorIndex = cargoArguments.indexOf('--'); - expect(cargoArguments[0]).toBe('run'); - expect(separatorIndex).toBeGreaterThan(0); - expect(cargoArguments.slice(separatorIndex + 1)).toEqual(cliArguments); - - const manifestIndex = cargoArguments.indexOf('--manifest-path'); - expect(manifestIndex).toBeGreaterThan(0); - expect(manifestIndex).toBeLessThan(separatorIndex); - expect(path.isAbsolute(cargoArguments[manifestIndex + 1])).toBe(true); - expect(path.relative(appRoot, cargoArguments[manifestIndex + 1])).toBe( - path.join('src-tauri', 'Cargo.toml'), - ); - }); - - it('keeps cargo build chatter out of the run output', () => { - // The dead-code warnings are reprinted on every spawn; without --quiet they - // bury the swarm output this script exists to surface. - const cargoArguments = buildCargoCliArguments(['--llm-status']); - const separatorIndex = cargoArguments.indexOf('--'); - - expect(cargoArguments.slice(0, separatorIndex)).toContain('--quiet'); - }); - - it('parses the idle Runner shutdown marker', () => { - expect(parseRunnerShutdownOutput('runner.stopped=true\n')).toBe(true); - expect(parseRunnerShutdownOutput('runner.stopped=false\n')).toBe(false); - expect(() => parseRunnerShutdownOutput('runner.status=unknown\n')).toThrow( - 'stopped', - ); - }); -}); - -describe('bounded process-tree termination', () => { - it('kills a POSIX process group after its leader exits with stdio still open', async () => { - if (process.platform === 'win32') return; - const child = spawn( - process.execPath, - [ - '-e', - `const { spawn } = require('node:child_process'); -const grandchild = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)'], { - stdio: 'inherit', -}); -grandchild.once('spawn', () => { - process.stdout.write('grandchild-ready\\n'); - setTimeout(() => process.exit(0), 50); -}); -grandchild.once('error', () => process.exit(2));`, - ], - { detached: true, stdio: ['ignore', 'pipe', 'pipe'] }, - ); - try { - child.stdout.setEncoding('utf8'); - const grandchildReady = new Promise((resolve, reject) => { - let output = ''; - child.stdout.on('data', (chunk) => { - output += chunk; - if (output.includes('grandchild-ready')) resolve(); - }); - child.once('error', reject); - }); - child.stderr.resume(); - const leaderExited = new Promise((resolve, reject) => { - child.once('error', reject); - child.once('exit', () => resolve()); - }); - await Promise.all([leaderExited, grandchildReady]); - expect(child.exitCode).toBe(0); - const startedAt = Date.now(); - await expect( - childExitWithTimeout(child, 20, 'fixture process tree', { - graceMs: 50, - forceWaitMs: 500, - }), - ).rejects.toMatchObject({ code: 'AGC_CHILD_TIMEOUT' }); - expect(Date.now() - startedAt).toBeLessThan(2_000); - } finally { - await terminateChildTree(child, 'SIGKILL', true); - } - }); - - it('bounds recursive cleanup in an independently terminable child', async () => { - if (process.platform === 'win32') return; - const target = await mkdtemp( - path.join(os.tmpdir(), 'genarrative-bounded-cleanup-test-'), - ); - try { - await expect( - removeDirectoryWithTimeout(target, { - timeoutMs: 20, - childProgram: 'setInterval(() => {}, 1000);', - }), - ).rejects.toMatchObject({ code: 'AGC_CHILD_TIMEOUT' }); - expect(await pathExists(target)).toBe(true); - } finally { - await rm(target, { recursive: true, force: true }); - } - }); -}); - -describe('non-interactive Swarm turn report validation', () => { - const settledReport = { - schemaVersion: 'game-creator-swarm-turn-report.v1', - outcome: 'settled', - parentAgentId: 'project-supervisor', - sessionId: 'agent-session-project-supervisor', - parentRunId: 'swarm-project-supervisor-fixture', - runtimeCount: 5, - busyRuntimeCount: 0, - pendingTaskCount: 0, - runningTaskCount: 0, - waitingForConfirmationCount: 0, - waitingForUserInputCount: 0, - newAssistantMessageCount: 1, - finalReplyChars: 128, - reconciliationAgentCount: 0, - }; - const outputFor = (...reports: unknown[]) => - [ - '[状态] 正在收束', - ...reports.map((report) => `[turn.report] ${JSON.stringify(report)}`), - '[完成] 本轮结束', - ].join('\n'); - - it('accepts exactly one settled report with no remaining work', () => { - expect(parseSettledSwarmTurnReport(outputFor(settledReport))).toEqual( - settledReport, - ); - }); - - it.each([ - ['missing', '[状态] 没有报告'], - ['duplicate', outputFor(settledReport, settledReport)], - ['malformed JSON', '[turn.report] {invalid'], - ['non-object JSON', '[turn.report] []'], - [ - 'incomplete shape', - outputFor( - (({ finalReplyChars: _removed, ...report }) => report)(settledReport), - ), - ], - ['unknown shape', outputFor({ ...settledReport, unexpected: true })], - ])('rejects a %s report', (_label, output) => { - expect(() => parseSettledSwarmTurnReport(output)).toThrowError(); - }); - - it.each(['failed', 'incomplete', 'needs-reconciliation'])( - 'rejects the %s outcome', - (outcome) => { - expect(() => - parseSettledSwarmTurnReport(outputFor({ ...settledReport, outcome })), - ).toThrow(`outcome=${outcome}`); - }, - ); - - it.each([ - 'busyRuntimeCount', - 'pendingTaskCount', - 'runningTaskCount', - 'waitingForConfirmationCount', - 'waitingForUserInputCount', - 'reconciliationAgentCount', - ])('rejects non-zero %s', (field) => { - expect(() => - parseSettledSwarmTurnReport(outputFor({ ...settledReport, [field]: 1 })), - ).toThrow(field); - }); - - it.each([ - ['no Runtime', { runtimeCount: 0 }], - ['no assistant reply', { newAssistantMessageCount: 0 }], - ['multiple assistant replies', { newAssistantMessageCount: 2 }], - ['empty final reply', { finalReplyChars: 0 }], - ['implausibly large final reply', { finalReplyChars: 1_000_001 }], - ['fractional count', { pendingTaskCount: 0.5 }], - ['missing parent run', { parentRunId: null }], - ])('rejects %s', (_label, overrides) => { - expect(() => - parseSettledSwarmTurnReport( - outputFor({ ...settledReport, ...overrides }), - ), - ).toThrowError(); - }); -}); - -describe('generated game entry validation', () => { - it('rejects the initializer placeholder and accepts generated HTML', async () => { - await withTemporaryRoot(async (root) => { - const gameDir = path.join(root, 'game'); - const gameEntry = path.join(gameDir, 'index.html'); - await mkdir(gameDir); - - expect(await hasGeneratedGameEntry(root)).toBe(false); - await writeFile( - gameEntry, - `
${ungeneratedGameEntryMarker}
`, - ); - expect(await hasGeneratedGameEntry(root)).toBe(false); - - await writeFile(gameEntry, ''); - expect(await hasGeneratedGameEntry(root)).toBe(true); - }); - }); -}); - -describe('formal Swarm project artifact validation', () => { - it('reports every missing required artifact by relative path', async () => { - await withTemporaryRoot(async (root) => { - const inspection = await inspectSwarmProjectArtifacts(root); - - expect(inspection.valid).toBe(false); - expect(inspection.invalidPaths).toEqual([ - ...Object.keys(minimumFormalArtifactContents), - '.agent/manifest.json', - '.agent/runtime/project-revision.json', - '.agent/agent.db', - ]); - const validationError = await validateSwarmProjectArtifacts(root).then( - () => null, - (error: Error) => error, - ); - expect(validationError).toBeInstanceOf(Error); - for (const relativePath of Object.keys(minimumFormalArtifactContents)) { - expect(validationError?.message).toContain(relativePath); - } - - await mkdir(path.join(root, 'exports')); - await writeFile(path.join(root, 'exports', 'README.md'), ' \n'); - const emptyInspection = await inspectSwarmProjectArtifacts(root); - expect(emptyInspection.issues).toContainEqual({ - path: 'exports/README.md', - reason: '空文件', - }); - - if (process.platform !== 'win32') { - const target = path.join(root, 'project-target.md'); - await mkdir(path.join(root, 'memory')); - await writeFile(target, '# Outside artifact path\n'); - await symlink(target, path.join(root, 'memory', 'project.md')); - const symlinkInspection = await inspectSwarmProjectArtifacts(root); - expect(symlinkInspection.issues).toContainEqual({ - path: 'memory/project.md', - reason: '不是无符号链接普通文件', - }); - } - }); - }); - - it('rejects each malformed JSON artifact', async () => { - await withTemporaryRoot(async (root) => { - await writeMinimumFormalArtifacts(root); - const invalidJsonPaths = [ - 'game/balance.json', - 'assets/manifest.art.json', - 'assets/manifest.audio.json', - ]; - await Promise.all( - invalidJsonPaths.map((relativePath) => - writeFile(path.join(root, ...relativePath.split('/')), '{invalid'), - ), - ); - - const inspection = await inspectSwarmProjectArtifacts(root); - - expect(inspection.invalidPaths).toEqual(invalidJsonPaths); - expect(inspection.issues).toEqual( - invalidJsonPaths.map((relativePath) => ({ - path: relativePath, - reason: 'JSON 无法解析', - })), - ); - }); - }); - - it('rejects placeholder Markdown and empty JSON objects', async () => { - await withTemporaryRoot(async (root) => { - await writeMinimumFormalArtifacts(root); - const projectMemoryPath = path.join(root, 'memory', 'project.md'); - await writeFile(projectMemoryPath, '# TODO\n\n待补充项目说明。\n'); - let inspection = await inspectSwarmProjectArtifacts(root); - expect(inspection.issues).toContainEqual({ - path: 'memory/project.md', - reason: '仍包含占位标记', - }); - - await writeFile( - projectMemoryPath, - minimumFormalArtifactContents['memory/project.md'], - ); - const gameEntryPath = path.join(root, 'game', 'index.html'); - await writeFile( - gameEntryPath, - '\n', - ); - inspection = await inspectSwarmProjectArtifacts(root); - expect(inspection.issues).toContainEqual({ - path: 'game/index.html', - reason: '仍包含占位标记', - }); - - await writeFile( - gameEntryPath, - minimumFormalArtifactContents['game/index.html'], - ); - await writeFile(path.join(root, 'game', 'balance.json'), '{}\n'); - inspection = await inspectSwarmProjectArtifacts(root); - expect(inspection.issues).toContainEqual({ - path: 'game/balance.json', - reason: 'JSON 必须是非空对象', - }); - }); - }); - - it.each([ - ['TODO', 'TODO: 补齐导出说明'], - ['TBD', 'TBD - export notes'], - ['placeholder', 'This is a placeholder document.'], - ['coming soon', 'Release notes are coming soon.'], - ['lorem ipsum', 'Lorem ipsum dolor sit amet.'], - ['待补充', '导出说明待补充。'], - ['待完善', '移动端结论待完善。'], - ['占位', '本文件仅供流程占位。'], - ['尚未完成', '最终试玩尚未完成。'], - ['稍后补充', '截图说明稍后补充。'], - ['待填写', '版本信息待填写。'], - ['待验证', '桌面视口待验证。'], - ['待复核', '最终结论待复核。'], - ['待确认', '发布范围待确认。'], - ['待定', '交付日期待定。'], - ['unchecked checklist', '- [ ] 补齐移动端试玩记录'], - ])('rejects exports/README.md containing %s', async (_label, marker) => { - expect(hasIncompleteArtifactMarker(`# Export\n\n${marker}\n`)).toBe(true); - - await withTemporaryRoot(async (root) => { - await writeMinimumFormalArtifacts(root); - await writeFile( - path.join(root, 'exports', 'README.md'), - `# Export\n\n当前导出流程记录如下。\n\n${marker}\n`, - ); - - const inspection = await inspectSwarmProjectArtifacts(root); - - expect(inspection.issues).toContainEqual({ - path: 'exports/README.md', - reason: '仍包含占位标记', - }); - }); - }); - - it('accepts complete formal Markdown and checked task lists', async () => { - const completeReadme = [ - '# Export', - '', - '- [x] 桌面视口试玩验证已经通过', - '- [X] 移动视口试玩验证已经通过', - '', - '版本信息已经填写,静态检查、人工复核和发布范围确认均已完成。', - '', - ].join('\n'); - expect(hasIncompleteArtifactMarker(completeReadme)).toBe(false); - - await withTemporaryRoot(async (root) => { - await writeMinimumFormalArtifacts(root); - await writeFile(path.join(root, 'exports', 'README.md'), completeReadme); - - await expect(validateSwarmProjectArtifacts(root)).resolves.toMatchObject({ - valid: true, - }); - }); - }); - - it('requires all 16 manifest tasks and browser evidence for the current revision', async () => { - await withTemporaryRoot(async (root) => { - await writeMinimumFormalArtifacts(root); - const manifestPath = path.join(root, '.agent', 'manifest.json'); - const manifest = JSON.parse(await readFile(manifestPath, 'utf8')); - manifest.tasks[0].status = 'running'; - await writeFile(manifestPath, `${JSON.stringify(manifest)}\n`); - - let inspection = await inspectSwarmProjectArtifacts(root); - expect(inspection.issues).toContainEqual({ - path: '.agent/manifest.json', - reason: '固定 16 个正式任务未全部且仅完成一次', - }); - - manifest.tasks[0].status = 'completed'; - await writeFile(manifestPath, `${JSON.stringify(manifest)}\n`); - await writeFile( - path.join(root, '.agent', 'runtime', 'project-revision.json'), - '{"revision":9}\n', - ); - inspection = await inspectSwarmProjectArtifacts(root); - expect(inspection.issues).toContainEqual({ - path: '.agent/agent.db', - reason: '缺少当前 revision 的桌面与移动试玩通过凭证', - }); - }); - }); - - it('binds all 16 task lifecycles exactly once to the settled parent run', async () => { - await withTemporaryRoot(async (root) => { - const parentRunId = 'swarm-project-supervisor-exactly-once'; - await writeMinimumFormalArtifacts(root); - await writeReadyTaskExactlyOnceEvidence(root, parentRunId); - - await expect( - validateSwarmProjectArtifacts(root, { parentRunId }), - ).resolves.toMatchObject({ valid: true }); - - const databasePath = path.join(root, '.agent', 'agent.db'); - const baselineDatabase = await readFile(databasePath, 'utf8'); - const failedThenCompletedTaskId = requiredSwarmManifestTaskIds[0]; - await writeFile( - databasePath, - `${baselineDatabase}${JSON.stringify({ - recordType: 'agent.runtime.background_task.failed', - agentId: failedThenCompletedTaskId, - taskId: failedThenCompletedTaskId, - runId: `autonomous-ready-${failedThenCompletedTaskId}-fixture`, - source: 'agent-ready-task-scheduler', - })}\n`, - ); - const failedThenCompleted = await inspectSwarmProjectArtifacts(root, { - parentRunId, - }); - expect(failedThenCompleted.issues).toContainEqual({ - path: `.agent/runtime/tasks/${failedThenCompletedTaskId}.jsonl`, - reason: `正式任务 ${failedThenCompletedTaskId} 未在当前父 Run 中恰好启动并完成一次`, - }); - await writeFile(databasePath, baselineDatabase); - - const duplicateTaskId = requiredSwarmManifestTaskIds[0]; - await writeFile( - databasePath, - `${baselineDatabase}${JSON.stringify({ - recordType: 'agent.runtime.background_task', - agentId: duplicateTaskId, - taskId: duplicateTaskId, - runId: `autonomous-ready-${duplicateTaskId}-fixture`, - source: 'agent-ready-task-scheduler', - })}\n`, - ); - const duplicate = await inspectSwarmProjectArtifacts(root, { - parentRunId, - }); - expect(duplicate.issues).toContainEqual({ - path: `.agent/runtime/tasks/${duplicateTaskId}.jsonl`, - reason: `正式任务 ${duplicateTaskId} 未在当前父 Run 中恰好启动并完成一次`, - }); - - const secondRunTaskId = requiredSwarmManifestTaskIds[1]; - const journalPath = path.join( - root, - '.agent', - 'runtime', - 'tasks', - `${secondRunTaskId}.jsonl`, - ); - await writeFile( - journalPath, - `${await readFile(journalPath, 'utf8')}${JSON.stringify({ - agentId: secondRunTaskId, - taskId: secondRunTaskId, - runId: `autonomous-ready-${secondRunTaskId}-second-attempt`, - source: 'agent-ready-task-scheduler', - runProfile: 'autonomous-game-build', - parentAgentId: 'project-supervisor', - parentRunId, - status: 'completed', - phase: 'completed', - })}\n`, - ); - const secondRun = await inspectSwarmProjectArtifacts(root, { - parentRunId, - }); - expect(secondRun.invalidPaths).toContain( - `.agent/runtime/tasks/${secondRunTaskId}.jsonl`, - ); - }); - }); - - it('rejects a stale static smoke record from an older revision', async () => { - await withTemporaryRoot(async (root) => { - await writeMinimumFormalArtifacts(root); - const databasePath = path.join(root, '.agent', 'agent.db'); - const records = (await readFile(databasePath, 'utf8')) - .trim() - .split('\n') - .map((line) => JSON.parse(line)); - records[0].revision = 7; - await writeFile( - databasePath, - `${records.map((record) => JSON.stringify(record)).join('\n')}\n`, - ); - - const inspection = await inspectSwarmProjectArtifacts(root); - expect(inspection.issues).toContainEqual({ - path: '.agent/agent.db', - reason: '缺少当前 revision 的静态检查通过凭证', - }); - }); - }); - - it('requires valid image files only when the editor API key is configured', async () => { - await withTemporaryRoot(async (root) => { - await writeMinimumFormalArtifacts(root); - const configDir = path.join(root, 'config'); - await mkdir(configDir); - await writeFile( - path.join(configDir, configFileName), - '{"editorApi":{"apiKey":" "}}\n', - ); - expect(await hasConfiguredEditorApiKey(configDir)).toBe(false); - await expect(validateSwarmProjectArtifacts(root)).resolves.toMatchObject({ - valid: true, - requireEditorImages: false, - }); - - await writeFile( - path.join(configDir, localConfigFileName), - '{"editorApi":{"apiKey":"fixture-editor-key"}}\n', - ); - expect(await hasConfiguredEditorApiKey(configDir)).toBe(true); - await expect( - validateSwarmProjectArtifacts(root, { requireEditorImages: true }), - ).rejects.toThrow('assets/ui-prototype.png'); - - await writeFile( - path.join(root, 'assets', 'ui-prototype.png'), - Buffer.from('not-an-image'), - ); - await writeFile( - path.join(root, 'assets', 'art-spritesheet.png'), - Buffer.from([0xff, 0xd8, 0xff, 0xe0]), - ); - await expect( - validateSwarmProjectArtifacts(root, { requireEditorImages: true }), - ).rejects.toThrow('assets/ui-prototype.png'); - - await writeFile( - path.join(root, 'assets', 'ui-prototype.png'), - Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), - ); - await expect( - validateSwarmProjectArtifacts(root, { requireEditorImages: true }), - ).rejects.toThrow('assets/ui-prototype.png'); - - const headerOnlyPng = Buffer.alloc(1_024); - fixturePngSignature.copy(headerOnlyPng); - headerOnlyPng.writeUInt32BE(13, 8); - headerOnlyPng.write('IHDR', 12, 'ascii'); - headerOnlyPng.writeUInt32BE(1_600, 16); - headerOnlyPng.writeUInt32BE(900, 20); - await writeFile( - path.join(root, 'assets', 'ui-prototype.png'), - headerOnlyPng, - ); - await expect( - validateSwarmProjectArtifacts(root, { requireEditorImages: true }), - ).rejects.toThrow('assets/ui-prototype.png'); - - await writeFile( - path.join(root, 'assets', 'ui-prototype.png'), - fixturePng(1_600, 900), - ); - await writeFile( - path.join(root, 'assets', 'art-spritesheet.png'), - fixturePng(1_024, 1_024), - ); - await expect( - validateSwarmProjectArtifacts(root, { requireEditorImages: true }), - ).resolves.toMatchObject({ valid: true, requireEditorImages: true }); - }); - }); - - it('validates chunk CRC, zlib scanlines, filters, and screenshot PNGs', async () => { - const validPng = fixturePng(1_600, 900); - expect(validatePngBytes(validPng)).toEqual({ width: 1_600, height: 900 }); - - const crcCorrupted = Buffer.from(validPng); - crcCorrupted[42] ^= 0xff; - expect(() => validatePngBytes(crcCorrupted)).toThrow('CRC'); - expect(() => validatePngBytes(validPng.subarray(0, -1))).toThrowError(); - expect(() => - validatePngBytes(fixturePng(320, 180, { invalidFilter: true })), - ).toThrow('filter byte'); - expect(() => - validatePngBytes(fixturePng(320, 180, { trailingCompressedBytes: true })), - ).toThrow('zlib'); - expect(() => - validatePngBytes(fixtureIndexedPng({ includePalette: false })), - ).toThrow('PLTE'); - expect(() => - validatePngBytes(fixtureIndexedPng({ duplicatePalette: true })), - ).toThrow('PLTE'); - expect(() => - validatePngBytes(fixtureIndexedPng({ unknownCriticalChunk: true })), - ).toThrow('critical chunk'); - expect(validatePngBytes(fixtureIndexedPng())).toEqual({ - width: 1, - height: 1, - }); - - await withTemporaryRoot(async (root) => { - await writeMinimumFormalArtifacts(root); - const desktopScreenshot = path.join( - root, - '.agent', - 'runtime', - 'browser-validations', - 'publish-package', - 'test-run', - '8', - 'desktop.png', - ); - const fakeScreenshot = Buffer.alloc(2_048); - fixturePngSignature.copy(fakeScreenshot); - await writeFile(desktopScreenshot, fakeScreenshot); - - const inspection = await inspectSwarmProjectArtifacts(root); - expect(inspection.issues).toContainEqual({ - path: '.agent/agent.db', - reason: '缺少 desktop 试玩截图', - }); - }); - }); - - it('rejects non-PNG bytes at the fixed PNG artifact paths', async () => { - await withTemporaryRoot(async (root) => { - await writeMinimumFormalArtifacts(root); - await writeFile( - path.join(root, 'assets', 'ui-prototype.png'), - Buffer.from('RIFF\x04\x00\x00\x00WEBP', 'binary'), - ); - await writeFile( - path.join(root, 'assets', 'art-spritesheet.png'), - Buffer.from('GIF89a', 'ascii'), - ); - - await expect( - validateSwarmProjectArtifacts(root, { requireEditorImages: true }), - ).rejects.toThrow('assets/ui-prototype.png'); - }); - }); -}); - -describe('preview URL validation', () => { - it('accepts an HTTP URL on the numeric loopback host', () => { - const previewUrl = 'http://127.0.0.1:4173/'; - - expect(validatePreviewUrl(previewUrl)).toBe(previewUrl); - }); - - it.each([ - 'https://127.0.0.1:4173/', - 'http://localhost:4173/', - 'http://[::1]:4173/', - 'http://192.0.2.1:4173/', - 'http://127.0.0.1.example:4173/', - 'http://127.0.0.1/', - 'http://127.0.0.1:4173/play', - 'http://127.0.0.1:4173/?mode=test', - 'http://127.0.0.1:4173/#ready', - 'http://user@127.0.0.1:4173/', - 'file:///fixture/game/index.html', - 'not-a-url', - ])('rejects %s', (previewUrl) => { - expect(() => validatePreviewUrl(previewUrl)).toThrowError(); - }); -}); - -describe('package script registration', () => { - it('registers the root and app config and test commands exactly', async () => { - const [rootPackage, appPackage, checkConfigSource] = await Promise.all( - [ - new URL('../../../package.json', import.meta.url), - new URL('../package.json', import.meta.url), - new URL('../scripts/check-config.mjs', import.meta.url), - ].map(async (packageUrl) => - packageUrl.pathname.endsWith('.json') - ? JSON.parse(await readFile(packageUrl, 'utf8')) - : readFile(packageUrl, 'utf8'), - ), - ); - - expect(rootPackage.scripts?.['agc:config']).toBe( - 'npm --prefix apps/ai-game-creator-shell run config --', - ); - expect(rootPackage.scripts?.['agc:test']).toBe( - 'npm --prefix apps/ai-game-creator-shell run agent-runtime:supervisor-autonomous-playable-lane-defense-deterministic-e2e --', - ); - expect(rootPackage.scripts?.['agc:test:chat']).toBe( - 'npm --prefix apps/ai-game-creator-shell run test:chat --', - ); - expect(rootPackage.scripts?.['agc:test:chat:manual']).toBe( - 'npm --prefix apps/ai-game-creator-shell run test:chat:manual --', - ); - expect(appPackage.scripts?.['test:chat']).toBe( - `node scripts/agent-swarm-test-chat.mjs --task "${defaultRealSwarmTestTask}" --no-open`, - ); - expect(appPackage.scripts?.config).toBe( - 'node scripts/game-creator-config-wizard.mjs', - ); - expect(appPackage.scripts?.['test:chat:manual']).toBe( - 'node scripts/agent-swarm-test-chat.mjs', - ); - expect(checkConfigSource).toMatch( - /packageConfig\.scripts\?\.config\s*!==\s*'node scripts\/game-creator-config-wizard\.mjs'/u, - ); - expect(checkConfigSource).toMatch( - /rootPackageConfig\.scripts\?\.\['agc:config'\]\s*!==\s*'npm --prefix apps\/ai-game-creator-shell run config --'/u, - ); - const wrapperSource = await readFile( - new URL('../scripts/run-cli-with-config.mjs', import.meta.url), - 'utf8', - ); - expect(wrapperSource).toContain('resolveGameCreatorAppConfigDir'); - expect(wrapperSource).toContain( - "'--config-dir', resolveGameCreatorAppConfigDir()", - ); - }); -}); diff --git a/apps/ai-game-creator-shell/tests/agentTraceSummary.test.ts b/apps/ai-game-creator-shell/tests/agentTraceSummary.test.ts deleted file mode 100644 index a49f3db21..000000000 --- a/apps/ai-game-creator-shell/tests/agentTraceSummary.test.ts +++ /dev/null @@ -1,284 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { - createGameCreationAppSeedTasks, - GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, - type GameCreationAgentRunTrace, -} from '../../../packages/shared/src/contracts/gameCreationApp'; -import { summarizeAgentRunTrace } from '../src/App'; - -describe('AI 游戏创作 Agent loop 摘要', () => { - it('shows repair loop state in the chat trace summary', () => { - const tasks = createGameCreationAppSeedTasks(); - tasks[0]!.status = 'completed'; - tasks[1]!.status = 'completed'; - - const trace: GameCreationAgentRunTrace = { - schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, - runId: 'run-test', - commandId: 'game.generate_draft', - status: 'needs-revision', - lifecycleStatus: 'pending', - passes: 2, - maxPasses: 3, - toolCallCount: 12, - maxToolCalls: 128, - stopReason: 'max-passes-exhausted', - goal: '做一个弹幕厨房游戏', - coordination: 'Planner -> Orchestrator -> Generator -> Evaluator', - steps: [ - ...Array.from({ length: 7 }, (_, index) => ({ - pass: 1, - agent: `LLM-${index + 1}`, - phase: 'llm', - taskId: null, - group: null, - role: null, - status: 'completed', - inputPaths: [], - outputPaths: [], - summary: `LLM 调用 ${index + 1}`, - toolCalls: [ - { - toolId: `llm.call.${index + 1}`, - status: 'ok', - inputPaths: [], - outputPaths: [], - summary: `LLM 工具 ${index + 1}`, - }, - ], - })), - { - pass: 2, - agent: 'Orchestrator', - phase: 'plan', - taskId: 'code-director', - group: 'code', - role: 'Code', - status: 'completed', - inputPaths: ['.agent/findings.md'], - outputPaths: ['.agent/passes/pass-2/task-graph.json'], - summary: '重跑程序链路及下游发布包装', - toolCalls: [], - }, - ...Array.from({ length: 4 }, (_, index) => ({ - pass: 2, - agent: `Bridge-${index + 1}`, - phase: 'handoff', - taskId: null, - group: null, - role: null, - status: 'completed', - inputPaths: [], - outputPaths: [], - summary: `中间步骤 ${index + 1}`, - toolCalls: [], - })), - { - pass: 2, - agent: '美术组 / Asset', - phase: 'role-brief', - taskId: 'art-asset-plan', - group: 'art', - role: 'Asset', - status: 'completed', - inputPaths: ['.agent/manifest.json'], - outputPaths: ['.agent/passes/pass-2/groups/art/asset.md'], - summary: '需要回流画板角色素材', - toolCalls: [ - { - toolId: 'agent.role.brief.art.asset', - status: 'completed', - inputPaths: ['.agent/manifest.json'], - outputPaths: ['.agent/passes/pass-2/groups/art/asset.md'], - summary: '规划角色资产', - }, - { - toolId: 'agent.tool.suggest.canvas.project_sync', - status: 'suggested', - inputPaths: ['.agent/manifest.json'], - outputPaths: [], - summary: - '项目还没有画板回流资产;建议用户确认 /sync-canvas-project <画板项目ID>。', - }, - ...Array.from({ length: 5 }, (_, index) => ({ - toolId: `agent.tool.suggest.extra.${index + 1}`, - status: 'suggested', - inputPaths: ['.agent/manifest.json'], - outputPaths: [], - summary: `额外建议命令 ${index + 1}`, - })), - ], - }, - ], - artifacts: [ - { - path: '.agent/older-artifact.json', - sizeBytes: 64, - checksum: 'fnv1a64:older', - }, - { - path: '.agent/passes/pass-2/task-graph.json', - sizeBytes: 128, - checksum: 'fnv1a64:test', - }, - ...Array.from({ length: 4 }, (_, index) => ({ - path: `.agent/passes/pass-2/artifact-${index + 1}.json`, - sizeBytes: 128 + index, - checksum: `fnv1a64:artifact-${index + 1}`, - })), - ], - taskGraph: { - goal: '做一个弹幕厨房游戏', - readyTaskIds: ['code-director'], - activeTaskIds: ['code-director', 'quality-review', 'publish-package'], - carriedTaskIds: ['design-director'], - repairFocus: ['gameHtml 缺少 canvas'], - repairRoutes: [ - { - issue: 'gameHtml 缺少 canvas', - taskIds: [ - 'code-director', - 'quality-review', - 'preview-readiness', - 'publish-package', - ], - reason: 'code-repair+dependency-impact', - }, - { - issue: '缺少输入绑定', - taskIds: ['code-director'], - reason: 'input-binding', - }, - { - issue: '缺少胜负条件', - taskIds: ['quality-review'], - reason: 'win-condition', - }, - { - issue: '缺少发布说明', - taskIds: ['publish-package'], - reason: 'publish-readme', - }, - ], - tasks, - }, - passPlans: [ - { - pass: 1, - mode: 'initial', - summary: '第 1 轮全量调度', - activeTaskIds: ['design-director', 'code-director'], - carriedTaskIds: [], - dependencyWaves: [['design-director'], ['code-director']], - repairFocus: [], - repairRoutes: [], - }, - { - pass: 2, - mode: 'repair', - summary: '第 2 轮重跑程序链路及下游发布包装', - activeTaskIds: ['code-director', 'quality-review', 'publish-package'], - carriedTaskIds: ['design-director'], - dependencyWaves: [ - ['code-director'], - ['quality-review'], - ['publish-package'], - ], - repairFocus: ['gameHtml 缺少 canvas'], - repairRoutes: [ - { - issue: 'gameHtml 缺少 canvas', - taskIds: [ - 'code-director', - 'quality-review', - 'preview-readiness', - 'publish-package', - ], - reason: 'code-repair+dependency-impact', - }, - { - issue: '缺少输入绑定', - taskIds: ['code-director'], - reason: 'input-binding', - }, - { - issue: '缺少胜负条件', - taskIds: ['quality-review'], - reason: 'win-condition', - }, - { - issue: '缺少发布说明', - taskIds: ['publish-package'], - reason: 'publish-readme', - }, - ], - }, - { - pass: 3, - mode: 'repair', - summary: '第 3 轮复核', - activeTaskIds: ['quality-review'], - carriedTaskIds: ['design-director'], - dependencyWaves: [['quality-review']], - repairFocus: [], - repairRoutes: [], - }, - { - pass: 4, - mode: 'repair', - summary: '第 4 轮收尾', - activeTaskIds: ['publish-package'], - carriedTaskIds: [], - dependencyWaves: [['publish-package']], - repairFocus: [], - repairRoutes: [], - }, - ], - nextStep: 'repair-next-pass', - error: null, - updatedAt: 1, - }; - - const summary = summarizeAgentRunTrace(trace); - - expect(summary).toContain( - 'needs-revision / pending · 2/3 轮 · max-passes-exhausted', - ); - expect(summary).toContain('工具调用:12/128'); - expect(summary).toContain('任务:已完成 2'); - expect(summary).toContain( - 'active 任务:程序组 / Director 拆解程序实现(code-director), 程序组 / Review 执行质量评审(quality-review), 运营组 / Publish 整理发布包装(publish-package)', - ); - expect(summary).toContain( - 'carry-over 任务:设计实现组 / Director 拆解创作方向(design-director)', - ); - expect(summary).toContain('返工焦点:gameHtml 缺少 canvas'); - expect(summary).toContain( - '返工路线:code-repair+dependency-impact: 程序组 / Director 拆解程序实现(code-director), 程序组 / Review 执行质量评审(quality-review), 程序组 / Preview 执行静态自检(preview-readiness), 运营组 / Publish 整理发布包装(publish-package)', - ); - expect(summary).toContain('还有 1 条路线'); - expect(summary).not.toContain('publish-readme:'); - expect(summary).toContain('建议命令:'); - expect(summary).toContain('agent.tool.suggest.canvas.project_sync'); - expect(summary).toContain('/sync-canvas-project <画板项目ID>'); - expect(summary).toContain('agent.tool.suggest.extra.4'); - expect(summary).not.toContain('agent.tool.suggest.extra.5'); - expect(summary).toContain('还有 1 个建议命令'); - expect(summary).toContain('编排轮次:'); - expect(summary).toContain( - 'pass 2 · repair · active 程序组 / Director 拆解程序实现(code-director), 程序组 / Review 执行质量评审(quality-review), 运营组 / Publish 整理发布包装(publish-package) · carry 设计实现组 / Director 拆解创作方向(design-director) · waves 程序组 / Director 拆解程序实现(code-director) / 程序组 / Review 执行质量评审(quality-review) / 运营组 / Publish 整理发布包装(publish-package) · repair gameHtml 缺少 canvas · routes code-repair+dependency-impact: 程序组 / Director 拆解程序实现(code-director), 程序组 / Review 执行质量评审(quality-review), 程序组 / Preview 执行静态自检(preview-readiness), 运营组 / Publish 整理发布包装(publish-package)', - ); - expect(summary).not.toContain('pass 1 · initial'); - expect(summary).toContain('还有 1 个较早轮次'); - expect(summary).toContain('.agent/passes/pass-2/task-graph.json'); - expect(summary).not.toContain('.agent/older-artifact.json'); - expect(summary).toContain('还有 1 个较早产物'); - expect(summary).not.toContain('Orchestrator #2 · completed · plan'); - expect(summary).toContain('Bridge-1 #2 · completed · handoff'); - expect(summary).toContain('还有 8 个较早步骤'); - expect(summary).toContain('LLM-2 #1 · completed · llm · llm.call.2'); - expect(summary).not.toContain('LLM-1 #1 · completed · llm · llm.call.1'); - expect(summary).toContain('还有 1 个较早 LLM 步骤'); - }); -}); diff --git a/apps/ai-game-creator-shell/tests/appSurface/harness.ts b/apps/ai-game-creator-shell/tests/appSurface/harness.ts index a1ca82d1b..9322d1f5f 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/harness.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/harness.ts @@ -136,7 +136,6 @@ import { deriveAgentStatusCards, WorkspaceLauncher, } from '../../src/App'; -import { projectNameFromPath } from '../../src/features/agent-runtime/model'; import ProjectDevelopmentView from '../../src/view/project-development'; const testAuthUser: AuthUser = { @@ -314,11 +313,6 @@ async function setComposerText(element: HTMLElement, value: string) { await settleComposer(); } -async function submitChat(value: string) { - await setComposerText(screen.getByLabelText('创作想法'), value); - fireEvent.click(screen.getByRole('button', { name: '发送' })); -} - // 同一张资源卡上的按钮变少了:卡片本体只有「选中资源」与媒体播放钮两个(原先右上角 // 那个 @ 引用圆钮已挪进选中工具条)。选中按钮的可访问名模板固定为 // `选中资源:<分类标签> <资源文件名>`;分类标签可能自带空格(例如 `UI 交互`), @@ -1394,14 +1388,6 @@ function createProjectChatRuntimeHarness({ }; } -async function openMainProject(projectPath: string) { - await submitChat(`/project ${projectPath}`); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText(`已打开:${projectNameFromPath(projectPath)}`), - ).not.toBeNull(); -} - export function installResizeObserverStub() { let observerCount = 0; let observerDisconnected = false; @@ -1552,7 +1538,6 @@ export { it, mockRoleAgentReply, nativeClipboardMock, - openMainProject, openResourceFilterPanel, pickProjectFromLauncher, planningResponseStream, @@ -1568,7 +1553,6 @@ export { roleAgentMockReply, screen, setComposerText, - submitChat, TEST_LOCAL_PROJECT_PATH, testAuthUser, vi, diff --git a/apps/ai-game-creator-shell/tests/appSurface/home.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/home.suite.ts index e4607c0d0..e081afb8b 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/home.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/home.suite.ts @@ -2915,7 +2915,8 @@ export function registerRecentProjectsTests() { expect(screen.getByText('不是文件夹')).not.toBeNull(); expect(screen.getAllByText('未初始化').length).toBeGreaterThan(0); expect(screen.getByText('无法读取')).not.toBeNull(); - expect(screen.getByText('检查失败')).not.toBeNull(); + // 目录检查失败会先就地重试一次(失败不进终态),因此这里等它落成最终的失败状态。 + expect(await screen.findByText('检查失败')).not.toBeNull(); expect(screen.getByText('厨房突围')).not.toBeNull(); expect(screen.getByText('已完成 · 预览运行中')).not.toBeNull(); expect( diff --git a/apps/ai-game-creator-shell/tests/appSurface/project-conversation.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/project-conversation.suite.ts index bc3191d9a..61c65cead 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/project-conversation.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/project-conversation.suite.ts @@ -13,7 +13,6 @@ import { renderAppAt, roleAgentMockReply, screen, - submitChat, vi, waitFor, within, @@ -79,163 +78,6 @@ export function registerProjectConversationTests() { ); }); - it.skip('loads project conversation history after opening from chat command', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return emptyProjectPolicy(); - } - if (command === 'read_local_conversation') { - return { - path: '/tmp/authorized-game/.agent/conversations/project.jsonl', - agentId: null, - messages: [ - { - schemaVersion: 'game-creator-conversation.v1', - role: 'user', - content: '历史需求:保留弹幕厨房', - agentId: null, - updatedAt: 1, - }, - { - schemaVersion: 'game-creator-conversation.v1', - role: 'assistant', - content: '历史回复:继续做第二版', - agentId: null, - updatedAt: 2, - }, - ], - }; - } - if (command === 'read_local_project_file') { - throw new Error('missing trace'); - } - if (command === 'list_local_project_files') { - return { projectPath: String(args?.projectPath ?? ''), files: [] }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - expect(await screen.findByText('历史需求:保留弹幕厨房')).not.toBeNull(); - expect(screen.getByText('历史回复:继续做第二版')).not.toBeNull(); - expect( - screen.queryByText('已设置本地项目:/tmp/authorized-game'), - ).toBeNull(); - expect(invoke).toHaveBeenCalledWith('read_local_conversation', { - projectPath: '/tmp/authorized-game', - agentId: null, - }); - expect(invoke).not.toHaveBeenCalledWith( - 'append_local_conversation_message', - expect.anything(), - ); - }); - - it.skip('reloads project conversation history from chat on demand', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_project_permission_policy') { - return emptyProjectPolicy(); - } - if (command === 'chat_with_game_creator_agent') { - return { - replyText: `主聊天回复:${String(args?.prompt ?? '')}`, - }; - } - if (command === 'read_local_conversation') { - return { - path: '/tmp/authorized-game/.agent/conversations/project.jsonl', - agentId: null, - messages: [ - { - schemaVersion: 'game-creator-conversation.v1', - role: 'user', - content: '重载历史需求', - agentId: null, - updatedAt: 1, - }, - { - schemaVersion: 'game-creator-conversation.v1', - role: 'assistant', - content: '重载历史回复', - agentId: null, - updatedAt: 2, - }, - ], - }; - } - if (command === 'read_local_project_file') { - throw new Error('missing trace'); - } - if (command === 'list_local_project_files') { - return { projectPath: String(args?.projectPath ?? ''), files: [] }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/'); - - await submitChat('/project /tmp/authorized-game'); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect(await screen.findByText('重载历史需求')).not.toBeNull(); - - await submitChat('临时未保存的输入'); - expect(await screen.findByText('临时未保存的输入')).not.toBeNull(); - await submitChat('/history'); - - expect(await screen.findByText('重载历史回复')).not.toBeNull(); - expect(screen.queryByText('临时未保存的输入')).toBeNull(); - expect(screen.getByText('已读取项目对话历史:2 条')).not.toBeNull(); - - await submitChat('另一条临时未保存的输入'); - expect(await screen.findByText('另一条临时未保存的输入')).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '历史' })); - - expect(await screen.findByText('重载历史回复')).not.toBeNull(); - expect(screen.queryByText('另一条临时未保存的输入')).toBeNull(); - expect(invoke).toHaveBeenCalledWith('read_local_conversation', { - projectPath: '/tmp/authorized-game', - agentId: null, - }); - }); - it.skip('requires confirmation before reading a specific agent conversation when policy asks for it', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', diff --git a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts index 1d30225a8..066d1f28c 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts @@ -32,12 +32,9 @@ import { act, cleanup, createGameCreationAppManifest, - createGameCreationAppSeedTasks, expect, findResourceSelectButton, fireEvent, - GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, - type GameCreationAgentRunTrace, getResourceSelectButton, installResizeObserverStub, it, @@ -6732,169 +6729,6 @@ export function registerProjectAgentStatusTests() { ).toBe(false); }); - it.skip('confirms before refreshing agents when trace read policy requires it', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const trace: GameCreationAgentRunTrace = { - schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, - runId: 'run-agent-refresh-confirm', - commandId: 'game.generate_draft', - status: 'running', - passes: 1, - maxPasses: 3, - toolCallCount: 1, - maxToolCalls: 128, - stopReason: 'running', - goal: '做一个厨房弹幕游戏', - coordination: 'Planner', - steps: [], - artifacts: [], - taskGraph: { - goal: '做一个厨房弹幕游戏', - readyTaskIds: [], - activeTaskIds: [], - carriedTaskIds: [], - repairFocus: [], - repairRoutes: [], - tasks: createGameCreationAppSeedTasks(), - }, - passPlans: [], - nextStep: 'continue', - error: null, - updatedAt: 1, - }; - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_local_conversation') { - return { - path: '/tmp/authorized-game/.agent/conversations/project.jsonl', - agentId: null, - messages: [], - }; - } - if (command === 'read_project_permission_policy') { - return { - path: '.agent/policy.json', - policy: { - deniedCommands: [], - confirmCommands: ['agent.trace_read'], - }, - }; - } - if (command === 'read_local_project_file') { - return { - path: '.agent/run.latest.json', - absolutePath: `${String(args?.projectPath ?? '')}/.agent/run.latest.json`, - content: JSON.stringify(trace), - }; - } - if (command === 'list_local_project_files') { - return { projectPath: String(args?.projectPath ?? ''), files: [] }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game'); - invoke.mockClear(); - - fireEvent.click(screen.getByRole('button', { name: '刷新 Agent' })); - - expect(await screen.findByText('agent.trace_read')).not.toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'read_local_project_file', - expect.anything(), - ); - - fireEvent.click(screen.getByRole('button', { name: '确认' })); - - await waitFor(() => { - expect(invoke).toHaveBeenCalledWith('read_local_project_file', { - projectPath: '/tmp/authorized-game', - relativePath: '.agent/run.latest.json', - commandId: 'agent.trace_read', - }); - }); - }); - - it.skip('cancels agent run trace refresh policy confirmation from the panel', async () => { - const manifest = createGameCreationAppManifest( - 'local-project-draft', - '未命名游戏原型', - ); - const invoke = vi.fn( - async (command: string, args?: Record) => { - if (command === 'append_local_permission_log') { - return {}; - } - if (command === 'init_local_game_project') { - const projectPath = String(args?.projectPath ?? ''); - return { - projectPath, - manifestPath: `${projectPath}/.agent/manifest.json`, - manifest, - }; - } - if (command === 'read_local_conversation') { - return { - path: '/tmp/authorized-game/.agent/conversations/project.jsonl', - agentId: null, - messages: [], - }; - } - if (command === 'read_project_permission_policy') { - return { - path: '.agent/policy.json', - policy: { - deniedCommands: [], - confirmCommands: ['agent.trace_read'], - }, - }; - } - if (command === 'read_local_project_file') { - throw new Error('should wait for trace confirmation'); - } - if (command === 'list_local_project_files') { - return { projectPath: String(args?.projectPath ?? ''), files: [] }; - } - throw new Error(`unexpected invoke ${command}`); - }, - ); - window.__TAURI__ = { core: { invoke } }; - renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game'); - invoke.mockClear(); - - fireEvent.click(screen.getByRole('button', { name: '刷新 Agent' })); - - const traceReadCommand = await screen.findByText('agent.trace_read'); - fireEvent.click( - within( - traceReadCommand.closest('.pending-command') as HTMLElement, - ).getByRole('button', { name: '取消' }), - ); - - expect( - await screen.findByText('run: 已取消读取 Agent trace'), - ).not.toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'read_local_project_file', - expect.anything(), - ); - }); - /** * 资源总览(main 态)必须为**每个非空栏目**挂载真实卡片本体。 * diff --git a/apps/ai-game-creator-shell/tests/chatPromptPolish.test.tsx b/apps/ai-game-creator-shell/tests/chatPromptPolish.test.tsx index 0098d3bc7..c5727d28e 100644 --- a/apps/ai-game-creator-shell/tests/chatPromptPolish.test.tsx +++ b/apps/ai-game-creator-shell/tests/chatPromptPolish.test.tsx @@ -210,15 +210,6 @@ describe('发送前提醒判据', () => { reminderDisabled: false, }), ).toBe(false); - const command = `/${'长'.repeat(60)}`; - expect( - shouldRemindChatPromptPolish({ - content: textContent(command), - prompt: command, - acknowledgedDraftKey: null, - reminderDisabled: false, - }), - ).toBe(false); }); test('changes the draft key when the references change', () => { @@ -501,22 +492,12 @@ describe('聊天输入区 AI 润色与发送前提醒', () => { }); }); - test('does not hold short prompts or slash commands', () => { + test('does not hold short prompts', () => { const shortSubmit = renderComposer({ initialText: '做个跳跃游戏' }); fireEvent.click(sendButton()); expect(screen.queryByRole('dialog', { name: '发送前提醒' })).toBeNull(); expect(shortSubmit).toHaveBeenCalledWith({ content: textContent('做个跳跃游戏'), }); - - cleanup(); - const commandSubmit = renderComposer({ - initialText: `/${'命令'.repeat(40)}`, - }); - fireEvent.click(sendButton()); - expect(screen.queryByRole('dialog', { name: '发送前提醒' })).toBeNull(); - expect(commandSubmit).toHaveBeenCalledWith({ - content: textContent(`/${'命令'.repeat(40)}`), - }); }); }); diff --git a/apps/ai-game-creator-shell/tests/gameDistributionPublish.test.ts b/apps/ai-game-creator-shell/tests/gameDistributionPublish.test.ts index 8c0f34c9f..9110172ea 100644 --- a/apps/ai-game-creator-shell/tests/gameDistributionPublish.test.ts +++ b/apps/ai-game-creator-shell/tests/gameDistributionPublish.test.ts @@ -1,5 +1,5 @@ // @vitest-environment jsdom -import { beforeEach, expect, test, vi } from 'vitest'; +import { afterEach, beforeEach, expect, test, vi } from 'vitest'; const fetchClientHttp = vi.fn(); @@ -16,8 +16,11 @@ vi.mock('../src/services/errorReporting', () => ({ import type { GameCreationAppManifest } from '../../../packages/shared/src/contracts/gameCreationApp'; import { + generateGameDistributionCover, publishLocalProjectGame, + readGameCoverGenerationPrice, readGamePublishAvailability, + suggestGameDistributionPublishMetadata, } from '../src/services/gameDistributionPublish'; const MANIFEST = { @@ -46,6 +49,10 @@ beforeEach(() => { window.localStorage.clear(); }); +afterEach(() => { + vi.useRealTimers(); +}); + test('发布时携带本地项目标识,让重复发布复用同一个平台游戏', async () => { fetchClientHttp .mockResolvedValueOnce( @@ -191,3 +198,138 @@ test('发布灰度读取失败时抛出,由调用方按不开放处理', async fetchClientHttp.mockRejectedValueOnce(new Error('network down')); await expect(readGamePublishAvailability()).rejects.toThrow(); }); + +test('免费发布资料建议只上传脱敏上下文并返回白名单分类', async () => { + fetchClientHttp.mockResolvedValueOnce( + jsonResponse({ summary: '驾驶炮台守住轨道城', category: '策略' }), + ); + + await expect( + suggestGameDistributionPublishMetadata({ + name: '星轨防线', + goal: '守住轨道城', + context: '当前任务与状态:原型已完成;现有素材:image:assets/hero.png', + }), + ).resolves.toEqual({ + summary: '驾驶炮台守住轨道城', + category: '策略', + }); + + expect(fetchClientHttp.mock.calls[0]?.[0]).toBe( + '/api/game-distribution/publish-metadata/suggestions', + ); + const init = fetchClientHttp.mock.calls[0]?.[1] as RequestInit; + expect(JSON.parse(String(init.body))).toEqual({ + name: '星轨防线', + goal: '守住轨道城', + context: '当前任务与状态:原型已完成;现有素材:image:assets/hero.png', + }); +}); + +test('封面生成直接使用返回的平台素材 ID,不再次上传', async () => { + fetchClientHttp.mockResolvedValueOnce( + jsonResponse({ + imageSrc: 'https://assets.example.com/generated-cover.png', + assetObjectId: 'asset_generated_cover', + taskId: 'task_cover_1', + model: 'gpt-image-2', + }), + ); + + await expect( + generateGameDistributionCover({ + prompt: '为《星轨防线》生成游戏封面', + model: 'gpt-image-2', + aspectRatio: '16:9', + imageSize: '2K', + }), + ).resolves.toEqual({ + assetObjectId: 'asset_generated_cover', + previewUrl: 'https://assets.example.com/generated-cover.png', + taskId: 'task_cover_1', + model: 'gpt-image-2', + }); + + expect(fetchClientHttp.mock.calls[0]?.[0]).toBe( + '/api/editor/images/generations', + ); + const body = JSON.parse( + String((fetchClientHttp.mock.calls[0]?.[1] as RequestInit).body), + ); + expect(body).toEqual({ + prompt: '为《星轨防线》生成游戏封面', + kind: 'publication-material', + assetKind: 'publication-material', + model: 'gpt-image-2', + aspectRatio: '16:9', + imageSize: '2K', + assetLabel: '游戏封面', + }); +}); + +test('封面生成进入队列后轮询完成结果并返回同一平台素材', async () => { + vi.useFakeTimers(); + fetchClientHttp + .mockResolvedValueOnce( + jsonResponse({ + queueState: { + operationId: 'external-job-1', + status: 'queued', + phaseDetail: '排队中。', + }, + }), + ) + .mockResolvedValueOnce( + jsonResponse({ + job: { + operationId: 'external-job-1', + status: 'completed', + result: { + imageSrc: 'https://assets.example.com/queued-cover.png', + assetObjectId: 'asset_queued_cover', + taskId: 'task_queued_cover', + model: 'gpt-image-2', + }, + }, + }), + ); + + const generation = generateGameDistributionCover({ + prompt: '为《星轨防线》生成游戏封面', + model: 'gpt-image-2', + }); + await vi.advanceTimersByTimeAsync(1_600); + + await expect(generation).resolves.toEqual({ + assetObjectId: 'asset_queued_cover', + previewUrl: 'https://assets.example.com/queued-cover.png', + taskId: 'task_queued_cover', + model: 'gpt-image-2', + }); + expect(fetchClientHttp.mock.calls[1]?.[0]).toBe( + '/api/runtime/external-generation/jobs/external-job-1', + ); +}); + +test('封面价格读取后端运行时定价,不在前端硬编码', async () => { + fetchClientHttp.mockResolvedValueOnce( + jsonResponse({ + models: { + 'gpt-image-2': { + unit: 'perGeneration', + prices: { '2K': 5 }, + }, + }, + }), + ); + + await expect( + readGameCoverGenerationPrice({ + model: 'gpt-image-2', + imageSize: '2K', + }), + ).resolves.toBe(5); + expect(fetchClientHttp.mock.calls[0]?.[0]).toBe( + '/api/editor/generation-pricing', + ); +}); diff --git a/apps/ai-game-creator-shell/tests/gameDistributionPublishPanel.test.tsx b/apps/ai-game-creator-shell/tests/gameDistributionPublishPanel.test.tsx index 3fc008027..c4e34ede5 100644 --- a/apps/ai-game-creator-shell/tests/gameDistributionPublishPanel.test.tsx +++ b/apps/ai-game-creator-shell/tests/gameDistributionPublishPanel.test.tsx @@ -11,21 +11,33 @@ import { render, screen, waitFor, + within, } from '@testing-library/react'; -import { afterEach, describe, expect, test, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; import type { GameCreationAppManifest } from '../../../packages/shared/src/contracts/gameCreationApp'; import type { LocalProjectExportPackageResult } from '../src/app/types'; import { GameDistributionPublishPanel } from '../src/components/game-distribution/GameDistributionPublishPanel'; import { uploadPlatformMediaAsset } from '../src/services/assetDirectUpload'; -import { publishLocalProjectGame } from '../src/services/gameDistributionPublish'; +import { + generateGameDistributionCover, + publishLocalProjectGame, + readGameCoverGenerationPrice, + suggestGameDistributionPublishMetadata, +} from '../src/services/gameDistributionPublish'; vi.mock('../src/services/gameDistributionPublish', async (importOriginal) => { const actual = await importOriginal< typeof import('../src/services/gameDistributionPublish') >(); - return { ...actual, publishLocalProjectGame: vi.fn() }; + return { + ...actual, + generateGameDistributionCover: vi.fn(), + publishLocalProjectGame: vi.fn(), + readGameCoverGenerationPrice: vi.fn(), + suggestGameDistributionPublishMetadata: vi.fn(), + }; }); // 面板只负责选图与调用上传;这里替换掉真实直传,避免测试触达 Tauri/OSS。 @@ -101,16 +113,28 @@ function renderPanel( return { onClose, onPublished }; } +beforeEach(() => { + vi.mocked(readGameCoverGenerationPrice).mockImplementation( + () => new Promise(() => undefined), + ); + vi.mocked(suggestGameDistributionPublishMetadata).mockImplementation( + () => new Promise(() => undefined), + ); +}); + afterEach(() => { cleanup(); + vi.mocked(generateGameDistributionCover).mockReset(); vi.mocked(publishLocalProjectGame).mockReset(); + vi.mocked(readGameCoverGenerationPrice).mockReset(); + vi.mocked(suggestGameDistributionPublishMetadata).mockReset(); vi.mocked(uploadPlatformMediaAsset).mockReset(); delete (window as unknown as { __TAURI__?: unknown }).__TAURI__; window.localStorage.clear(); }); describe('GameDistributionPublishPanel', () => { - test('打开时预填游戏资料并展示发行包摘要', () => { + test('打开时预填游戏资料且不展示发行包技术摘要', async () => { installTauriInvoke(async () => undefined); renderPanel(); @@ -122,12 +146,88 @@ describe('GameDistributionPublishPanel', () => { 'value', '守住轨道城', ); - expect(screen.getByLabelText('发行包摘要').textContent).toContain( - 'exports/playtest-package-unit.zip', + expect(screen.queryByLabelText('发行包摘要')).toBeNull(); + expect( + screen.queryByText(/exports\/playtest-package-unit\.zip/u), + ).toBeNull(); + expect(screen.queryByText(/只上传已导出的 ZIP/u)).toBeNull(); + await screen.findByText(/简介和分类/u); + }); + + test('打开时根据创作上下文补全简介和分类', async () => { + vi.mocked(suggestGameDistributionPublishMetadata).mockResolvedValueOnce({ + summary: '驾驶星轨炮台守住轨道城', + category: '策略', + }); + installTauriInvoke(async () => undefined); + renderPanel(); + + await waitFor(() => { + expect(screen.getByLabelText('一句话简介')).toHaveProperty( + 'value', + '驾驶星轨炮台守住轨道城', + ); + }); + expect(screen.getByLabelText('分类')).toHaveProperty('value', '策略'); + expect( + screen.getByText( + 'AI 已根据创作内容生成简介和分类,免费;你可以直接修改。', + ), + ).not.toBeNull(); + }); + + test('确认后基于项目上下文生成封面并作为发布素材', async () => { + vi.mocked(readGameCoverGenerationPrice).mockResolvedValueOnce(5); + installTauriInvoke(async () => undefined); + vi.mocked(generateGameDistributionCover).mockResolvedValue({ + assetObjectId: 'asset_generated_cover', + previewUrl: 'https://assets.example.com/generated-cover.png', + taskId: 'task_cover_1', + model: 'gpt-image-2', + }); + vi.mocked(publishLocalProjectGame).mockResolvedValue({ + gameId: 'game_1', + versionId: 'gamever_1', + versionNumber: 1, + status: 'pending_review', + packageSha256: 'a'.repeat(64), + packageSizeBytes: 3, + fileCount: 2, + }); + renderPanel(); + + fireEvent.click( + await screen.findByRole('button', { name: 'AI 生成封面(5 泥点)' }), ); - expect(screen.getByLabelText('发行包摘要').textContent).toContain( - '2 个文件', + const confirm = await screen.findByRole('dialog', { + name: '确认生成游戏封面', + }); + expect(confirm.textContent ?? '').toContain('本次生成预计消耗 5 泥点'); + fireEvent.click(screen.getByRole('button', { name: '生成封面' })); + + await waitFor(() => + expect(generateGameDistributionCover).toHaveBeenCalledWith( + expect.objectContaining({ + model: 'gpt-image-2', + aspectRatio: '16:9', + imageSize: '2K', + }), + ), ); + expect( + await screen.findByText( + '封面已生成并自动设为发布封面;重新生成会再次消耗泥点。', + ), + ).not.toBeNull(); + + fireEvent.click(screen.getByRole('button', { name: '发布游戏' })); + await waitFor(() => + expect(publishLocalProjectGame).toHaveBeenCalledTimes(1), + ); + expect( + vi.mocked(publishLocalProjectGame).mock.calls[0]?.[0].metadata + ?.coverAssetId, + ).toBe('asset_generated_cover'); }); test('提交时带上项目路径、发行包与资料,成功后展示审核状态', async () => { @@ -157,7 +257,7 @@ describe('GameDistributionPublishPanel', () => { fireEvent.change(screen.getByLabelText(/游戏截图/u), { target: { files: [buildImageFile('shot-1.png')] }, }); - await screen.findByText('移除截图 1'); + await screen.findByRole('button', { name: '删除截图 1' }); fireEvent.click(screen.getByRole('button', { name: '发布游戏' })); await waitFor(() => @@ -271,6 +371,104 @@ describe('GameDistributionPublishPanel', () => { expect(uploadPlatformMediaAsset).toHaveBeenCalledTimes(1); }); + test('截图并行上传,单张失败只标记该张并在发布时跳过', async () => { + installTauriInvoke(async () => undefined); + let resolveShot1: (() => void) | undefined; + let resolveShot3: (() => void) | undefined; + let rejectShot2: (() => void) | undefined; + vi.mocked(uploadPlatformMediaAsset).mockImplementation((input) => { + const file = input.file; + if (file.name === 'cover.png') { + return Promise.resolve({ + assetObjectId: 'asset_cover', + objectKey: 'game-distribution/cover/cover.png', + }); + } + if (file.name === 'shot-1.png') { + return new Promise((resolve) => { + resolveShot1 = () => + resolve({ + assetObjectId: 'asset_shot_1', + objectKey: 'game-distribution/screenshot/shot-1.png', + }); + }); + } + if (file.name === 'shot-2.png') { + return new Promise((_, reject) => { + rejectShot2 = () => reject(new Error('截图 2 上传失败')); + }); + } + return new Promise((resolve) => { + resolveShot3 = () => + resolve({ + assetObjectId: 'asset_shot_3', + objectKey: 'game-distribution/screenshot/shot-3.png', + }); + }); + }); + vi.mocked(publishLocalProjectGame).mockResolvedValue({ + gameId: 'game_1', + versionId: 'gamever_1', + versionNumber: 1, + status: 'pending_review', + packageSha256: 'a'.repeat(64), + packageSizeBytes: 3, + fileCount: 2, + }); + renderPanel(); + await selectCover(); + + fireEvent.change(screen.getByLabelText(/游戏截图/u), { + target: { + files: [ + buildImageFile('shot-1.png'), + buildImageFile('shot-2.png'), + buildImageFile('shot-3.png'), + ], + }, + }); + + await waitFor(() => + expect(uploadPlatformMediaAsset).toHaveBeenCalledTimes(4), + ); + resolveShot3?.(); + rejectShot2?.(); + resolveShot1?.(); + + const failedError = await screen.findByText('截图 2 上传失败'); + expect( + within( + failedError.closest( + '.game-distribution-publish-panel__shot-media', + ) as HTMLElement, + ).getByRole('button', { name: '删除截图 2' }), + ).not.toBeNull(); + expect( + await screen.findByRole('button', { name: '删除截图 1' }), + ).not.toBeNull(); + expect( + await screen.findByRole('button', { name: '删除截图 3' }), + ).not.toBeNull(); + + fireEvent.click(screen.getByRole('button', { name: '预览截图 1' })); + expect( + await screen.findByRole('dialog', { name: '截图预览' }), + ).not.toBeNull(); + fireEvent.keyDown(window, { key: 'Escape' }); + await waitFor(() => { + expect(screen.queryByRole('dialog', { name: '截图预览' })).toBeNull(); + }); + + fireEvent.click(screen.getByRole('button', { name: '发布游戏' })); + await waitFor(() => + expect(publishLocalProjectGame).toHaveBeenCalledTimes(1), + ); + expect( + vi.mocked(publishLocalProjectGame).mock.calls[0]?.[0].metadata + ?.screenshots, + ).toEqual(['asset_shot_1', 'asset_shot_3']); + }); + test('截图超过 6 张时本地拦截且不上传', async () => { installTauriInvoke(async () => undefined); renderPanel(); @@ -316,8 +514,6 @@ describe('GameDistributionPublishPanel', () => { 'disabled', true, ); - expect(screen.getByLabelText('发行包摘要').textContent).toContain( - '未找到试玩包', - ); + expect(screen.queryByLabelText('发行包摘要')).toBeNull(); }); }); diff --git a/apps/ai-game-creator-shell/tests/gamePublishFeedback.test.tsx b/apps/ai-game-creator-shell/tests/gamePublishFeedback.test.tsx index baa020d5b..91b84b4a7 100644 --- a/apps/ai-game-creator-shell/tests/gamePublishFeedback.test.tsx +++ b/apps/ai-game-creator-shell/tests/gamePublishFeedback.test.tsx @@ -2,9 +2,11 @@ /** * 客户端「发布」入口的可见反馈。 * - * DirectProject 项目不发工作台状态行,发布动作的提示必须回到项目对话; - * 权限要求确认时也要在聊天里给出确认/取消,否则表现就是「点了没反应」。 + * 发布动作使用独立的全屏进度弹窗,不回到旧的聊天确认卡片; + * 失败必须留在弹窗内可见,成功后自动切换到发布资料面板。 */ +import { readFileSync } from 'node:fs'; + import { beforeEach, describe, it, vi } from 'vitest'; import type { GameCreationAppManifest } from '../../../packages/shared/src/contracts/gameCreationApp'; @@ -20,6 +22,12 @@ import { waitFor, within, } from './appSurface/harness'; +import { repoPath } from './repoPath'; +import { + declaration, + parseStyleSheet, + resolveDeclarations, +} from './styleCascade'; const readGamePublishAvailabilityMock = vi.hoisted(() => vi.fn(async () => true), @@ -39,18 +47,41 @@ vi.mock('../src/services/gameDistributionPublish', async (importOriginal) => { const PROJECT_PATH = '/tmp/game-publish-feedback-project'; const PROJECT_ID = 'game-publish-feedback-project'; +function withPrototypeStatus( + manifest: GameCreationAppManifest, + status: 'pending' | 'completed', +): GameCreationAppManifest { + return { + ...manifest, + tasks: manifest.tasks.map((task) => + task.id === 'code-prototype' ? { ...task, status } : task, + ), + }; +} + function createFixtureManifest(): GameCreationAppManifest { - return createGameCreationAppManifest(PROJECT_ID, '发布反馈项目'); + return withPrototypeStatus( + createGameCreationAppManifest(PROJECT_ID, '发布反馈项目'), + 'completed', + ); +} + +function createPendingPrototypeManifest(): GameCreationAppManifest { + return withPrototypeStatus( + createGameCreationAppManifest(PROJECT_ID, '原型未完成项目'), + 'pending', + ); } function installTauri( options: { exportPackage?: () => unknown; + manifest?: GameCreationAppManifest; policy?: ReturnType; readPolicy?: () => unknown; } = {}, ) { - const manifest = createFixtureManifest(); + const manifest = options.manifest ?? createFixtureManifest(); const chatHarness = createProjectChatRuntimeHarness({ projectPath: PROJECT_PATH, }); @@ -82,12 +113,11 @@ function installTauri( return { invoke }; } -function renderPublishProject() { +function renderPublishProject( + manifest: GameCreationAppManifest = createFixtureManifest(), +) { return render( - , + , ); } @@ -125,76 +155,124 @@ function createConfirmPolicy() { }; } +function createDeferred() { + let resolve!: (value: T) => void; + let reject!: (reason?: unknown) => void; + const promise = new Promise((resolvePromise, rejectPromise) => { + resolve = resolvePromise; + reject = rejectPromise; + }); + return { promise, resolve, reject }; +} + describe('客户端发布入口的可见反馈', () => { - it('导出成功时先回即时反馈,再回结果并打开发布面板', async () => { - installTauri({ exportPackage: createExportPackageResult }); - renderPublishProject(); + it('首个可运行原型未完成时显示独立阻断弹窗,不触发用户项目构建', async () => { + const exportPackage = vi.fn(createExportPackageResult); + const manifest = createPendingPrototypeManifest(); + installTauri({ exportPackage, manifest }); + + renderPublishProject(manifest); const surface = await clickPublish(); + const noticeDialog = await screen.findByRole('dialog', { + name: '发布提示', + }); + expect(noticeDialog.textContent ?? '').toContain( + '首个可运行原型尚未完成,暂不能发布;请先完成可运行原型并通过运行验证。', + ); + expect(surface.textContent ?? '').not.toContain( + '首个可运行原型尚未完成,暂不能发布', + ); + expect(surface.textContent ?? '').not.toContain('正在检查发布权限…'); + expect(exportPackage).not.toHaveBeenCalled(); + + fireEvent.click(within(noticeDialog).getByRole('button', { name: '关闭' })); await waitFor(() => { - expect(surface.textContent ?? '').toContain('正在检查发布权限…'); - expect(surface.textContent ?? '').toContain( - '正在构建并打包试玩包,请稍候…', - ); - expect(surface.textContent ?? '').toContain( - '已构建并打包试玩包:exports/game.zip', - ); + expect(screen.queryByRole('dialog', { name: '发布提示' })).toBeNull(); + }); + }); + + it('发布时显示全屏进度遮罩,成功后收起进度并打开发布面板', async () => { + const deferred = + createDeferred>(); + const exportPackage = vi.fn(() => deferred.promise); + installTauri({ exportPackage }); + renderPublishProject(); + + fireEvent.click( + await screen.findByRole('button', { name: '发布到游戏广场' }), + ); + + const progressDialog = await screen.findByRole('dialog', { + name: '发布进度', + }); + expect(progressDialog.textContent ?? '').toContain( + '正在构建并打包试玩包,请稍候…', + ); + expect(progressDialog.textContent ?? '').toContain( + '发布完成前请保持客户端开启,页面暂时不可操作。', + ); + expect( + progressDialog.closest('.game-publish-progress-overlay'), + ).not.toBeNull(); + expect(exportPackage).toHaveBeenCalledTimes(1); + expect(screen.queryByText('导出试玩包需要确认,确认后继续。')).toBeNull(); + + deferred.resolve(createExportPackageResult()); + await waitFor(() => { + expect(screen.queryByRole('dialog', { name: '发布进度' })).toBeNull(); }); expect( await screen.findByRole('dialog', { name: '发布到游戏广场' }), ).not.toBeNull(); }); - it('导出失败时把可读错误写回项目对话', async () => { + it('发布失败时在进度弹窗里显示错误并允许关闭', async () => { installTauri({ exportPackage: () => { - throw new Error('导出试玩包前需要先生成 exports/README.md'); + throw new Error('构建可玩版本失败:缺少入口'); }, }); renderPublishProject(); - const surface = await clickPublish(); + fireEvent.click( + await screen.findByRole('button', { name: '发布到游戏广场' }), + ); + + const failureDialog = await screen.findByRole('dialog', { + name: '发布失败', + }); + expect(failureDialog.textContent ?? '').toContain( + '构建可玩版本失败:缺少入口', + ); + fireEvent.click( + within(failureDialog).getByRole('button', { name: '关闭' }), + ); await waitFor(() => { - expect(surface.textContent ?? '').toContain( - '导出试玩包前需要先生成 exports/README.md', - ); + expect(screen.queryByRole('dialog', { name: '发布失败' })).toBeNull(); }); }); - it('策略要求确认时在 DirectProject 里展示确认卡片,确认后继续导出', async () => { - const exportPackage = vi.fn(createExportPackageResult); - installTauri({ exportPackage, policy: createConfirmPolicy() }); - renderPublishProject(); - - const publish = await screen.findByRole('button', { - name: '发布到游戏广场', - }); - fireEvent.click(publish); - - const commandLabel = await screen.findByText('project.export_package'); - const card = commandLabel.closest('.pending-command'); - expect(card).not.toBeNull(); - expect(exportPackage).not.toHaveBeenCalled(); - expect( - within(card as HTMLElement).getByText( - '导出试玩包并打开「发布到游戏广场」面板。', + it('发布进度遮罩固定覆盖整个工作区并压暗背景', () => { + const rules = parseStyleSheet( + readFileSync( + repoPath('apps/ai-game-creator-shell/src/styles.css'), + 'utf8', ), - ).not.toBeNull(); - - fireEvent.click( - within(card as HTMLElement).getByRole('button', { name: '确认' }), ); - await waitFor(() => expect(exportPackage).toHaveBeenCalledTimes(1)); - const surface = await screen.findByLabelText('陶泥儿项目对话'); - expect(surface.textContent ?? '').toContain( - '正在构建并打包试玩包,请稍候…', - ); - expect(surface.textContent ?? '').toContain( - '已构建并打包试玩包:exports/game.zip', + const overlay = resolveDeclarations( + rules, + ['.game-publish-progress-overlay'], + 1440, ); + expect(declaration(overlay, 'position')).toBe('fixed'); + expect(declaration(overlay, 'inset')).toBe('0'); + expect(declaration(overlay, 'z-index')).toBe('500'); + expect(declaration(overlay, 'pointer-events')).toBe('auto'); + expect(declaration(overlay, 'background')).toBe('rgb(35 24 19 / 62%)'); }); - it('取消权限确认时把取消结果写回项目对话', async () => { + it('策略要求确认时不再回到聊天确认卡片,直接进入进度弹窗', async () => { const exportPackage = vi.fn(createExportPackageResult); installTauri({ exportPackage, policy: createConfirmPolicy() }); renderPublishProject(); @@ -202,71 +280,9 @@ describe('客户端发布入口的可见反馈', () => { fireEvent.click( await screen.findByRole('button', { name: '发布到游戏广场' }), ); - const commandLabel = await screen.findByText('project.export_package'); - const card = commandLabel.closest('.pending-command'); - expect(card).not.toBeNull(); - fireEvent.click( - within(card as HTMLElement).getByRole('button', { name: '取消' }), - ); - const surface = await screen.findByLabelText('陶泥儿项目对话'); - await waitFor(() => { - expect(surface.textContent ?? '').toContain('已取消导出本地试玩包'); - }); - expect(exportPackage).not.toHaveBeenCalled(); - }); - - it('确认时策略已改为拒绝,也要把拒绝原因写回项目对话', async () => { - const exportPackage = vi.fn(createExportPackageResult); - let denyOnNextPolicyRead = false; - installTauri({ - exportPackage, - readPolicy: () => { - if (!denyOnNextPolicyRead) return createConfirmPolicy(); - return { - path: '.agent/policy.json', - policy: { - deniedCommands: ['project.export_package'], - confirmCommands: [], - }, - }; - }, - }); - renderPublishProject(); - - fireEvent.click( - await screen.findByRole('button', { name: '发布到游戏广场' }), - ); - const commandLabel = await screen.findByText('project.export_package'); - const card = commandLabel.closest('.pending-command'); - expect(card).not.toBeNull(); - denyOnNextPolicyRead = true; - fireEvent.click( - within(card as HTMLElement).getByRole('button', { name: '确认' }), - ); - - const surface = await screen.findByLabelText('陶泥儿项目对话'); - await waitFor(() => { - expect(surface.textContent ?? '').toContain( - '项目权限策略拒绝执行:project.export_package', - ); - }); - expect(exportPackage).not.toHaveBeenCalled(); - }); - - it('权限查询失败时也在项目对话里回显错误', async () => { - installTauri({ - readPolicy: () => { - throw new Error('策略文件损坏'); - }, - }); - renderPublishProject(); - - const surface = await clickPublish(); - await waitFor(() => { - expect(surface.textContent ?? '').toContain( - '发布前权限检查失败:策略文件损坏', - ); - }); + await waitFor(() => expect(exportPackage).toHaveBeenCalledTimes(1)); + expect(screen.queryByText('project.export_package')).toBeNull(); + expect(screen.queryByText('导出试玩包需要确认,确认后继续。')).toBeNull(); }); }); diff --git a/apps/ai-game-creator-shell/tests/recentProjectsHook.test.tsx b/apps/ai-game-creator-shell/tests/recentProjectsHook.test.tsx index e564a6fca..7abb15b64 100644 --- a/apps/ai-game-creator-shell/tests/recentProjectsHook.test.tsx +++ b/apps/ai-game-creator-shell/tests/recentProjectsHook.test.tsx @@ -91,3 +91,118 @@ test('刷新坏项目时保留其它项目已确认的正常状态', async () => await pendingInspection; }); }); + +test('单次目录检查失败会就地重试,不会把整行钉成「检查失败」', async () => { + let attempts = 0; + const invoke = vi.fn( + async (command: string, args?: Record) => { + if (command !== 'inspect_local_project_directory') { + throw new Error(`unexpected invoke ${command}`); + } + attempts += 1; + if (attempts === 1) { + throw new Error('Tauri IPC 瞬时失败'); + } + return READY_PROJECT; + }, + ); + window.__TAURI__ = { core: { invoke } }; + window.localStorage.setItem( + 'genarrative-ai-game-creator.recent-workspaces.v1', + JSON.stringify(['/tmp/ready-project']), + ); + + const { result } = renderHook(() => useRecentProjects(vi.fn())); + + await waitFor(() => { + expect(result.current.projectRows[0]).toMatchObject({ + status: '本地项目', + canOpen: true, + }); + }); + expect(attempts).toBe(2); + expect(result.current.projectRows[0]?.status).not.toBe('检查失败'); +}); + +test('失败结果不跨轮保留:刷新时该项回到「检查中」并重新检查', async () => { + const inspectCounts: Record = {}; + let secondRoundPending: (() => void) | null = null; + const secondRoundInspection = new Promise((resolve) => { + secondRoundPending = () => resolve(READY_PROJECT); + }); + let brokenRound = 0; + const invoke = vi.fn( + async (command: string, args?: Record) => { + if (command !== 'inspect_local_project_directory') { + throw new Error(`unexpected invoke ${command}`); + } + const projectPath = String(args?.projectPath ?? ''); + inspectCounts[projectPath] = (inspectCounts[projectPath] ?? 0) + 1; + if (projectPath === '/tmp/broken-project') { + brokenRound += 1; + if (brokenRound > 2) { + return secondRoundInspection; + } + throw new Error('Tauri IPC 持续失败'); + } + return { ...READY_PROJECT, projectPath }; + }, + ); + window.__TAURI__ = { core: { invoke } }; + window.localStorage.setItem( + 'genarrative-ai-game-creator.recent-workspaces.v1', + JSON.stringify(['/tmp/broken-project']), + ); + + const { result } = renderHook(() => useRecentProjects(vi.fn())); + + await waitFor(() => { + expect(result.current.projectRows[0]?.status).toBe('检查失败'); + }); + expect(inspectCounts['/tmp/broken-project']).toBe(2); + + act(() => { + result.current.rememberRecentWorkspace('/tmp/ready-project'); + }); + + // 上一轮的失败结果不进新投影:第二轮在途时该项目显示「检查中」而不是沿用「检查失败」。 + await waitFor(() => { + expect( + result.current.projectRows.find( + (row) => row.path === '/tmp/broken-project', + )?.status, + ).toBe('检查中'); + }); + + await act(async () => { + secondRoundPending?.(); + await secondRoundInspection; + }); +}); + +test('提权类失败不重试:不放大 UAC 弹窗', async () => { + let attempts = 0; + const invoke = vi.fn( + async (command: string, args?: Record) => { + if (command !== 'inspect_local_project_directory') { + throw new Error(`unexpected invoke ${command}`); + } + attempts += 1; + throw new Error( + '读取待修复私有对象失败:C:\\p\\.agent(DACL 不包含当前用户)', + ); + }, + ); + window.__TAURI__ = { core: { invoke } }; + window.localStorage.setItem( + 'genarrative-ai-game-creator.recent-workspaces.v1', + JSON.stringify(['/tmp/elevation-project']), + ); + + const { result } = renderHook(() => useRecentProjects(vi.fn())); + + await waitFor(() => { + expect(result.current.projectRows[0]?.status).toBe('检查失败'); + }); + expect(attempts).toBe(1); +}); diff --git a/apps/ai-game-creator-shell/tests/rememberCommand.test.ts b/apps/ai-game-creator-shell/tests/rememberCommand.test.ts index 95f4ed246..0ab3b9fd8 100644 --- a/apps/ai-game-creator-shell/tests/rememberCommand.test.ts +++ b/apps/ai-game-creator-shell/tests/rememberCommand.test.ts @@ -9,11 +9,10 @@ import { deriveAgentStatusCards, isAbsoluteProjectPath, needsInitializedChatProject, - parseRememberInput, resolveChatProjectPath, } from '../src/App'; -describe('AI 游戏创作聊天记忆命令', () => { +describe('AI 游戏创作项目路径与 Agent 状态卡', () => { it('recognizes local project absolute paths across desktop platforms', () => { expect(isAbsoluteProjectPath('/tmp/game')).toBe(true); expect(isAbsoluteProjectPath('C:\\Games\\demo')).toBe(true); @@ -21,34 +20,7 @@ describe('AI 游戏创作聊天记忆命令', () => { expect(isAbsoluteProjectPath('relative-game')).toBe(false); }); - it('defaults /remember to long memory and supports short and blackboard scopes', () => { - expect(parseRememberInput('主角喜欢反弹弹幕')).toEqual({ - scope: 'long', - content: '主角喜欢反弹弹幕', - }); - expect(parseRememberInput('short 本轮先修输入手感')).toEqual({ - scope: 'short', - content: '本轮先修输入手感', - }); - expect(parseRememberInput('长期 保留厨房主题')).toEqual({ - scope: 'long', - content: '保留厨房主题', - }); - expect(parseRememberInput('project 覆盖后的长期设定')).toEqual({ - scope: 'long', - content: '覆盖后的长期设定', - }); - expect(parseRememberInput('blackboard 共享美术约束')).toEqual({ - scope: 'blackboard', - content: '共享美术约束', - }); - expect(parseRememberInput('黑板 统一使用俯视角')).toEqual({ - scope: 'blackboard', - content: '统一使用俯视角', - }); - }); - - it('requires an initialized local project path before chat memory commands', () => { + it('requires an initialized local project path', () => { expect(resolveChatProjectPath(null)).toBeNull(); expect(resolveChatProjectPath({ projectPath: '/tmp/game' })).toBe( '/tmp/game', @@ -57,17 +29,9 @@ describe('AI 游戏创作聊天记忆命令', () => { expect( resolveChatProjectPath({ projectPath: '/tmp/bad\u0007game' }), ).toBeNull(); - expect(parseRememberInput('long')).toEqual({ - scope: 'long', - content: '', - }); - expect(parseRememberInput('short 覆盖本轮上下文')).toEqual({ - scope: 'short', - content: '覆盖本轮上下文', - }); }); - it('requires a project before chat commands write or run local artifacts', () => { + it('requires a project before local artifacts are written or run', () => { expect(needsInitializedChatProject('game.generate_draft')).toBe(true); expect(needsInitializedChatProject('asset.upload')).toBe(true); expect(needsInitializedChatProject('agent.kill')).toBe(true); diff --git a/apps/ai-game-creator-shell/tests/templateLibraryGrid.test.ts b/apps/ai-game-creator-shell/tests/templateLibraryGrid.test.ts index 2ddf16993..bd3aecbf6 100644 --- a/apps/ai-game-creator-shell/tests/templateLibraryGrid.test.ts +++ b/apps/ai-game-creator-shell/tests/templateLibraryGrid.test.ts @@ -4,10 +4,18 @@ import { buildTemplateRows, computeTemplateGridColumns, computeTemplateGridLayout, + computeTemplateGridLayoutWithScrollbar, computeTemplateRowHeight, + TEMPLATE_CARD_ACTIONS_HEIGHT, TEMPLATE_CARD_GAP, + TEMPLATE_CARD_META_HEIGHT, TEMPLATE_CARD_MIN_WIDTH, + TEMPLATE_CARD_SUMMARY_HEIGHT, + TEMPLATE_CARD_TAGS_HEIGHT, + TEMPLATE_CARD_TEXT_GAP, TEMPLATE_CARD_TEXT_HEIGHT, + TEMPLATE_CARD_TEXT_PADDING, + TEMPLATE_CARD_TITLE_HEIGHT, } from '../src/features/template-library/templateLibraryGrid'; import type { GameTemplateEntry } from '../src/features/template-library/templateLibraryModel'; @@ -50,6 +58,19 @@ describe('computeTemplateGridColumns', () => { }); describe('computeTemplateRowHeight', () => { + it('budgets the same height the card rows actually need', () => { + // 这些数字对应 TemplateCard 的 `h-5`/`h-4`/`h-8`/`h-5.5`/`h-7` 与 `p-3`/`gap-2`: + // 行高契约比真实内容小,被截断的就是卡片里的标题与简介。 + expect(TEMPLATE_CARD_TITLE_HEIGHT).toBe(20); + expect(TEMPLATE_CARD_META_HEIGHT).toBe(16); + expect(TEMPLATE_CARD_SUMMARY_HEIGHT).toBe(32); + expect(TEMPLATE_CARD_TAGS_HEIGHT).toBe(22); + expect(TEMPLATE_CARD_ACTIONS_HEIGHT).toBe(28); + expect(TEMPLATE_CARD_TEXT_PADDING).toBe(12); + expect(TEMPLATE_CARD_TEXT_GAP).toBe(8); + expect(TEMPLATE_CARD_TEXT_HEIGHT).toBe(174); + }); + it('keeps cover ratio + fixed text block', () => { // 列宽 300 → 卡片 286 → 封面 286*9/16 = 160.875 → 161 expect(computeTemplateRowHeight(300)).toBe( @@ -94,6 +115,59 @@ describe('computeTemplateGridLayout', () => { }); }); +describe('computeTemplateGridLayoutWithScrollbar', () => { + const innerWidth = (layout: { columnCount: number; columnWidth: number }) => + layout.columnCount * layout.columnWidth; + + it('reserves the classic scrollbar width once the grid scrolls vertically', () => { + // 经典滚动条(Windows/WebView2 ≈ 17px)不在包裹层宽度里,不预留就会多出一条横向滚动条。 + const plain = computeTemplateGridLayout({ + containerWidth: 1184, + itemCount: 13, + }); + const layout = computeTemplateGridLayoutWithScrollbar({ + containerWidth: 1184, + itemCount: 13, + viewportHeight: 500, + scrollbarWidth: 17, + }); + + expect(layout.columnCount).toBe(4); + // 内层宽度必须落在竖滚动条左侧的可用宽度里,否则又会出现横向滚动条。 + expect(innerWidth(layout)).toBeLessThanOrEqual(1184 - 17); + expect(innerWidth(layout)).toBeLessThan(innerWidth(plain)); + // 行高仍按预留后的列宽算,卡片内容不会被压。 + expect(layout.rowHeight).toBe(computeTemplateRowHeight(layout.columnWidth)); + }); + + it('keeps the plain layout when nothing overflows or the scrollbar is an overlay', () => { + const plain = computeTemplateGridLayout({ + containerWidth: 1184, + itemCount: 4, + }); + // 只有一行:不会竖向滚动,不需要预留,右侧不留白边。 + expect( + computeTemplateGridLayoutWithScrollbar({ + containerWidth: 1184, + itemCount: 4, + viewportHeight: 900, + scrollbarWidth: 17, + }), + ).toEqual(plain); + // overlay 滚动条占宽 0:与不预留完全一致。 + expect( + computeTemplateGridLayoutWithScrollbar({ + containerWidth: 1184, + itemCount: 13, + viewportHeight: 500, + scrollbarWidth: 0, + }), + ).toEqual( + computeTemplateGridLayout({ containerWidth: 1184, itemCount: 13 }), + ); + }); +}); + describe('buildTemplateRows', () => { it('chunks entries per row and pads the tail with nulls', () => { const rows = buildTemplateRows([entry('a'), entry('b'), entry('c')], 2); diff --git a/apps/ai-game-creator-shell/tests/templateLibraryModel.test.ts b/apps/ai-game-creator-shell/tests/templateLibraryModel.test.ts index 5eb52ca01..a3852fcdf 100644 --- a/apps/ai-game-creator-shell/tests/templateLibraryModel.test.ts +++ b/apps/ai-game-creator-shell/tests/templateLibraryModel.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'; import { collectGameTemplateRuntimes, + collectGameTemplateTagOptions, collectGameTemplateTags, EMPTY_TEMPLATE_LIBRARY_FILTERS, filterGameTemplates, @@ -145,6 +146,24 @@ describe('tag and runtime options', () => { ]); }); + it('keeps the same order while reporting how many templates carry each tag', () => { + // 筛选条上的标签 chip 要显示命中数量,顺序必须与 `collectGameTemplateTags` 完全一致。 + const withBlank = [ + ...templates, + template({ id: 'blank-tag', tags: ['', ' ', '经营'] }), + ]; + const options = collectGameTemplateTagOptions(withBlank); + expect(options).toEqual([ + { tag: '经营', assetCount: 3 }, + { tag: '三消', assetCount: 1 }, + { tag: '射击', assetCount: 1 }, + { tag: '像素', assetCount: 1 }, + ]); + expect(options.map((option) => option.tag)).toEqual( + collectGameTemplateTags(withBlank), + ); + }); + it('collects distinct runtimes and labels them', () => { expect(collectGameTemplateRuntimes(templates)).toEqual([ 'godot', diff --git a/apps/ai-game-creator-shell/tests/templateLibraryView.test.tsx b/apps/ai-game-creator-shell/tests/templateLibraryView.test.tsx index 7a27cdbd9..5e3d3086d 100644 --- a/apps/ai-game-creator-shell/tests/templateLibraryView.test.tsx +++ b/apps/ai-game-creator-shell/tests/templateLibraryView.test.tsx @@ -8,7 +8,6 @@ import type { TemplateLibraryFilters, } from '../src/features/template-library/templateLibraryModel'; import { - collectGameTemplateTags, EMPTY_TEMPLATE_LIBRARY_FILTERS, filterGameTemplates, } from '../src/features/template-library/templateLibraryModel'; @@ -81,7 +80,6 @@ function controller( notice: '', templates, visibleTemplates: templates, - tagOptions: ['空白', '2d', 'canvas', '网页'], runtimeOptions: ['html'], installedCount: 1, filters, @@ -190,6 +188,28 @@ describe('TemplateLibraryView', () => { expect(viewport?.querySelector('article')).not.toBeNull(); }); + it('pins every card text row to the height the grid contract budgets for it', () => { + // 回归点:文字区若是 `grid` 的 auto 行,行高会按 max-content 算成「一行」, + // 标题 / 简介 / 标签会被逐行截断(现场表现为标题文字被切掉)。这里钉住 + // 卡片每一行的固定高度,改动必须同时改 `TEMPLATE_CARD_*_HEIGHT` 那组常量。 + render( {}} />); + + const card = cardFor('空白网页工程'); + const textBlock = card.children[1] as HTMLElement; + const rows = Array.from(textBlock.children) as HTMLElement[]; + + expect(textBlock.className).toContain('flex-col'); + expect(rows.map((row) => row.className)).toEqual([ + expect.stringContaining('h-5'), + expect.stringContaining('h-4'), + expect.stringContaining('h-8'), + expect.stringContaining('h-5.5'), + expect.stringContaining('h-7'), + ]); + // 每行都不参与压缩,否则 flex 会把文字压回去。 + rows.forEach((row) => expect(row.className).toContain('shrink-0')); + }); + it('offers 更新 instead of 下载 when the installed version is stale', () => { const stale = template({ id: 'blank-web', @@ -253,6 +273,17 @@ describe('TemplateLibraryView', () => { expect(clearFilters).toHaveBeenCalled(); }); + it('hides a cover that failed to load instead of showing a broken image', () => { + render( {}} />); + + const cover = cardFor('空白网页工程').querySelector( + 'img', + ) as HTMLImageElement; + expect(cover.style.visibility).toBe(''); + fireEvent.error(cover); + expect(cover.style.visibility).toBe('hidden'); + }); + it('starts a download and a template project from the card actions', () => { const downloadTemplate = vi.fn(async () => undefined); const createProjectFromTemplate = vi.fn(async () => undefined); @@ -289,11 +320,16 @@ describe('TemplateLibraryView', () => { const busyCard = cardFor('空白二维画布工程'); const buttons = Array.from(busyCard.querySelectorAll('button')); + // 忙状态写在触发它的按钮上(文案就地变成「创建中」),动作行里不额外塞第三个元素, + // 否则最小卡宽(250px)下两个按钮的文案会被挤成两行、顶出卡片。 + expect(buttons).toHaveLength(2); expect(buttons.every((button) => button.hasAttribute('disabled'))).toBe( true, ); - expect(busyCard.textContent).toContain('正在创建项目'); - expect(cardFor('空白网页工程').textContent).not.toContain('正在创建项目'); + expect(busyCard.textContent).toContain('创建中'); + expect(busyCard.textContent).not.toContain('使用模板'); + expect(cardFor('空白网页工程').textContent).toContain('使用模板'); + expect(cardFor('空白网页工程').textContent).not.toContain('创建中'); }); it('shows empty, no-match, error and notice states', () => { @@ -390,7 +426,6 @@ describe('大库量渲染(1000 条假数据)', () => { templates: bulk, visibleTemplates: bulk, installedCount: bulk.filter((entry) => entry.installed).length, - tagOptions: collectGameTemplateTags(bulk), })} onBack={() => {}} />, diff --git a/apps/ai-game-creator-shell/tests/workbenchThemeContrast.test.ts b/apps/ai-game-creator-shell/tests/workbenchThemeContrast.test.ts index c54b50f16..8cde18820 100644 --- a/apps/ai-game-creator-shell/tests/workbenchThemeContrast.test.ts +++ b/apps/ai-game-creator-shell/tests/workbenchThemeContrast.test.ts @@ -98,7 +98,128 @@ function contrastRatio(first: Rgba, second: Rgba) { ); } +/** 取渐变里的色标(`linear-gradient(135deg, #b3542f, #8f3f22)` → 两个颜色)。 */ +function parseGradientStops(source: string): Rgba[] { + const body = source.slice(source.indexOf('(') + 1, source.lastIndexOf(')')); + return body + .split(',') + .map((part) => part.trim()) + .filter((part) => part.startsWith('#') || part.startsWith('rgb')) + .map((part) => parseCssColor(part.split(/\s+/)[0] ?? part)); +} + +/** 页面背景(`--platform-body-fill`)里的不透明色标:chip 实际落在这层之上。 */ +function parseBodyFillUnderlays(source: string): Rgba[] { + return Array.from(source.matchAll(/#[\da-f]{6}/gi)).map((match) => + parseCssColor(match[0]), + ); +} + describe('workbench theme contrast', () => { + /** + * 筛选 chip 的两态对比:用户反馈「选中和没选中的颜色看不出差别」,根因是选中态 + * 只换了低透明度的暖色底(两态对比 1.09:1)。这里把「选中 = 实心填充」这条口径 + * 钉死:反白文字在渐变两端都要过 AA,且与未选底色至少差 3:1。 + */ + it('keeps the chip selected state legible and distinct in both themes', () => { + const css = readFileSync(themePath, 'utf8'); + const themes = [ + { + name: 'light', + block: getCssBlock(css, '.platform-theme--light'), + // 浅色主题下 chip 落在页面底色上,用页面渐变的最亮与最暗色标夹住两种情况。 + idleUnderlays: parseBodyFillUnderlays( + getCssVariable( + getCssBlock(css, '.platform-theme--light'), + '--platform-body-fill', + ), + ), + }, + { + name: 'dark', + block: getCssBlock(css, '.platform-theme--dark'), + idleUnderlays: parseBodyFillUnderlays( + getCssVariable( + getCssBlock(css, '.platform-theme--dark'), + '--platform-body-fill', + ), + ), + }, + ]; + + expect( + parseGradientStops('linear-gradient(135deg, #b3542f, #8f3f22)'), + ).toEqual([ + [179, 84, 47, 1], + [143, 63, 34, 1], + ]); + + for (const theme of themes) { + const activeFill = parseGradientStops( + getCssVariable(theme.block, '--platform-chip-active-fill'), + ); + const activeText = parseCssColor( + getCssVariable(theme.block, '--platform-chip-active-text'), + ); + const idleFill = parseCssColor( + getCssVariable(theme.block, '--platform-chip-idle-fill'), + ); + expect(activeFill, `${theme.name} active gradient stops`).toHaveLength(2); + expect( + theme.idleUnderlays.length, + `${theme.name} body fill stops`, + ).toBeGreaterThan(0); + + // 反白文字:渐变两端都要过 AA,不能只保证深的那一端。 + for (const stop of activeFill) { + expect( + contrastRatio(activeText, stop), + `${theme.name} label on fill ${stop.slice(0, 3).join(',')}`, + ).toBeGreaterThanOrEqual(4.5); + } + + // 两态可分辨:选中填充与任意页面底色上的未选 chip 至少差 3:1。 + for (const underlay of theme.idleUnderlays) { + const idleChip = compositeColor(idleFill, underlay); + for (const stop of activeFill) { + expect( + contrastRatio(stop, idleChip), + `${theme.name} selected vs idle over ${underlay.slice(0, 3).join(',')}`, + ).toBeGreaterThanOrEqual(3); + } + } + } + }); + + /** + * 焦点环可见性:键盘用户靠它找焦点。旧口径是 15% 透明度的暖色,合成到页面底色只有 + * 1.17:1——等于没有焦点提示。这里按 WCAG 非文本对比 3:1 钉住两套皮肤。 + */ + it('keeps the keyboard focus ring visible in both themes', () => { + const css = readFileSync(themePath, 'utf8'); + for (const selector of [ + '.platform-theme--light', + '.platform-theme--dark', + ]) { + const block = getCssBlock(css, selector); + const ring = parseCssColor( + getCssVariable(block, '--platform-input-focus-ring'), + ); + const underlays = parseBodyFillUnderlays( + getCssVariable(block, '--platform-body-fill'), + ); + expect(underlays.length, `${selector} body fill stops`).toBeGreaterThan( + 0, + ); + for (const underlay of underlays) { + expect( + contrastRatio(ring, underlay), + `${selector} focus ring over ${underlay.slice(0, 3).join(',')}`, + ).toBeGreaterThanOrEqual(3); + } + } + }); + it('keeps warm user bubbles above WCAG AA text contrast', () => { const css = readFileSync(themePath, 'utf8'); const light = getCssBlock(css, '.platform-theme--light'); diff --git a/deploy/container/api-server.env.example b/deploy/container/api-server.env.example index 55f051ba0..e8b929f38 100644 --- a/deploy/container/api-server.env.example +++ b/deploy/container/api-server.env.example @@ -5,11 +5,10 @@ GENARRATIVE_ENV=container GENARRATIVE_API_HOST=0.0.0.0 GENARRATIVE_API_PORT=8082 -# 客户端埋点接收绑定的公开 origin,由 API Server 运行时读取;修改后重启服务。 -# 必须与客户端登录地址一致,不带 /api、路径或尾部斜杠;未配置/非法时上传接口返回 503。 -# 以下为 compose 默认宿主机入口;更改映射端口或接入域名时同步修改,不填容器内部地址。 -# dev 使用 https://dev.genarrative.world;release 使用 https://www.genarrative.world。 -GENARRATIVE_AGC_ANALYTICS_ORIGIN=http://127.0.0.1:18080 +# 官网客户端下载与客户端埋点共用部署渠道;修改后重启 API Server。 +# dev 对应 https://dev.genarrative.world;container + dev 额外允许 loopback 地址及可变映射端口。 +# release 对应 https://www.genarrative.world,且不接受 loopback 地址;其它渠道不接收埋点。 +GENARRATIVE_CLIENT_DOWNLOAD_CHANNEL=dev GENARRATIVE_API_LOG=info,tower_http=info GENARRATIVE_API_LISTEN_BACKLOG=1024 GENARRATIVE_API_WORKER_THREADS=4 diff --git a/deploy/env/api-server.env.example b/deploy/env/api-server.env.example index 3cd99970f..25ec4bbc4 100644 --- a/deploy/env/api-server.env.example +++ b/deploy/env/api-server.env.example @@ -4,10 +4,10 @@ GENARRATIVE_ENV=production GENARRATIVE_API_HOST=127.0.0.1 GENARRATIVE_API_PORT=8082 -# 客户端埋点接收绑定的公开 origin,由 API Server 运行时读取;修改后重启服务。 -# 必须与客户端登录地址一致,不带 /api、路径或尾部斜杠;未配置/非法时上传接口返回 503。 -# 以下为 release;dev 部署改为 https://dev.genarrative.world。 -GENARRATIVE_AGC_ANALYTICS_ORIGIN=https://www.genarrative.world +# 官网客户端下载与客户端埋点共用部署渠道;修改后重启 API Server。 +# release 对应 https://www.genarrative.world;dev 部署改为 dev,对应 https://dev.genarrative.world。 +# production 环境的埋点不接受 loopback 地址;其它渠道不接收埋点。 +GENARRATIVE_CLIENT_DOWNLOAD_CHANNEL=release GENARRATIVE_API_LOG=info,tower_http=info GENARRATIVE_API_LISTEN_BACKLOG=1024 GENARRATIVE_API_WORKER_THREADS=4 diff --git a/docs/README.md b/docs/README.md index f311df3de..8029e4852 100644 --- a/docs/README.md +++ b/docs/README.md @@ -42,6 +42,7 @@ - [DirectProject Codex 原始历史与异常恢复](<./technical/【技术方案】DirectProject Codex原始历史与异常恢复-2026-09-04.md>):原始 Responses item 持久化、线程注入与异常回合收尾。 - [DirectProject 对话历史单一事实源](./adr/【ADR】DirectProject对话历史单一事实源-2026-09-16.md):AGC 项目开发对话只以项目对话历史与运行态事件为真相源,聊天投影不落盘。 - [DirectProject 独立聊天容器与工作台钱包布局](./adr/【ADR】DirectProject独立聊天容器与工作台钱包布局-2026-09-18.md):DirectProject 与 Supervisor 等路径分容器,钱包入口由项目工作台布局独立承载。 +- [退役 AGC 项目对话斜杠命令](./adr/【ADR】退役AGC项目对话斜杠命令与终端swarm chat入口-2026-09-22.md):AGC 项目对话与终端 swarm chat 均不再解析斜杠命令,终端聊天入口一并退役;实现、测试、门禁与文档承诺全部删除,命令 id 与权限位作为项目策略词汇表保留。 - [引用候选由宿主注入](./adr/【ADR】引用候选由宿主注入-2026-09-22.md):引用输入区只接受宿主注入的引用 provider,素材选择面板独立成组件,附件芯片成为本轮附件唯一事实源。 - [GameAgent 对话工具调用卡片](./technical/【技术方案】GameAgent对话工具调用卡片-2026-09-14.md):把右侧对话里的执行命令 / 写文件投影成 Codex 风格可折叠卡片,含采集、独立历史文件、事件字段与回读契约。 - [DirectProject 客户端 Skill 与 MCP 扩展导入方案](./technical/【技术方案】DirectProject客户端Skill与MCP扩展导入方案-2026-08-31.md):客户端扩展导入、按独立 Skill/MCP 拆分、命名、启用和启动时注入边界。 diff --git a/docs/adr/【ADR】退役AGC项目对话斜杠命令与终端swarm chat入口-2026-09-22.md b/docs/adr/【ADR】退役AGC项目对话斜杠命令与终端swarm chat入口-2026-09-22.md new file mode 100644 index 000000000..296bef0d6 --- /dev/null +++ b/docs/adr/【ADR】退役AGC项目对话斜杠命令与终端swarm chat入口-2026-09-22.md @@ -0,0 +1,34 @@ +# 【ADR】退役AGC项目对话斜杠命令与终端swarm入口-2026-09-22 + +状态:已接受 + +## 背景 + +AGC(`apps/ai-game-creator-shell`)的项目对话曾把用户能力挂在「聊天输入 `/`」上:除真正带执行语义的 `/history` 外,还累积了几十个只生成后续草稿或只读摘要的命令(`/brief`、`/status`、`/read`、`/trace`、`/art`、`/export`、`/cover`、`/remember` 等)。无 GUI 的终端 swarm chat 入口 `--swarm-chat` 另有一套控制命令(`/help`、`/agents`、`/status`、`/history`、`/compact`、`/resume`、`/goal`、`/quit`)。 + +这些命令的实际状态是:正式对话面已经换成 DirectProject 单容器,斜杠命令列表不再有渲染入口,`/history` 之外没有任何现役调用方;但命令字面量仍分散在控制器分支、`chatPromptPolish` 的 `/` 前缀绕过、命令参数校验文案、`projectSummaryConstants` 的命令清单、只服务已退役摘要面板的 `project-summary/*Summaries.ts`、构建期门禁 `scripts/check-config.mjs`、无人调用的 Tauri 能力清单命令、`swarm_cli` 的终端输入解析与帮助输出,以及多份权威文档的承诺里。 + +保留它的代价持续存在:每次调整对话形态都要同步维护这套死词汇表和它的门禁,而且「命令」一词在 AGC 里同时指用户斜杠命令与项目权限命令 id(`GAME_CREATION_APP_COMMANDS`)两件事,术语歧义会直接误导后续改动。 + +## 决策 + +- 斜杠命令语义整体退役,按「从未存在」处理:不保留入口、不做兼容提示、不写 tombstone,实现、专属测试、构建期门禁条目与文档承诺一并删除,历史由 Git 保存。 +- 删除项:Direct 聊天的 `/history` 精确匹配分支与 `reloadHistory`;`chatPromptPolish` 的 `/` 前缀绕过;`chatCommandMetadata`、`projectSummaryConstants.chatCommandHelp`、`memoryCommands.parseRememberInput` 等命令清单与参数解析;只服务退役 Supervisor 摘要面板、零外部调用的 `project-summary/*Summaries.ts` 与 `agentTrace.ts`;草稿回填死链(前端 `agentPresentation.ts` 的草稿推导与 Rust `suggested_canvas_tool_call`);无人调用的 Tauri 命令 `get_game_creation_agent_capabilities` 与 `get_limited_local_commands`;钉住上述字符串的门禁条目与专属测试。 +- 终端 swarm chat 入口连带其命令层整体退役:`--swarm-chat`、`src-tauri/src/swarm_cli.rs` 与整个 `swarm_cli/` 目录(`/help`、`/agents`、`/status`、`/history`、`/compact`、`/resume`、`/goal`、`/quit` 的解析、帮助输出、turn 派发、观察器、报告与专属测试)一并删除;`SwarmChatFlow`、`SwarmTurnObservation`、`SwarmTurnOutcome::Quit`、`SwarmConfirmationResolution::Quit`、`SWARM_TURN_*_ERROR`、只服务终端命令的 `agent.compact` / `agent.resume` / `agent.run_status` 校验(`swarm_cli/input.rs` 内那份)与 `print_runtime_response_stream_status` 也随之消失;同名权限 id 在现役 Tauri 命令与 Runtime 生命周期上的门禁保持不动。只服务终端交互内核的 `agent/interaction.rs` 整层(`AgentInteractionAction`、tool registry、`game_creator_agent_uses_interaction_kernel`、`decide_game_creator_agent_interaction_turn_for_session_at`、`AgentInteractionProviderStreamSink`)同样删除;其上仅存的自然语言 steer 决策路径 `decide_game_creator_agent_runtime_steer_at` 在收尾复查后一并删除(见「影响」的复查收尾)。 +- 保留项(它们不是斜杠命令):命令 id 注册表 `GAME_CREATION_APP_COMMANDS` 与 `GameCreationAppPermission`(Rust 运行期项目权限策略的词汇表;App 前端只用 `GameCreationAppCommandDescriptor` 类型表达权限判定与审计日志粒度,数组本体由 Rust 策略路径与跨语言一致性门禁消费)、`needsInitializedChatProject` 与项目权限策略链路、Rust 侧路径与路由的 `/` 前缀校验、`--agent-*` CLI 控制命令(Goal、steer、cancel、retry、context compact、resume、状态查询)与 Tauri IPC 注册。 +- 应用内项目对话的输入只剩自然语言回合(外加 `@` 素材引用与附件);需要动作时由 Runtime 工具、确认卡和既有 CLI 控制命令承接,不由聊天文本解析控制词。 +- 需要显式控制时改用现有 `--agent-*` CLI 命令:手动压缩是 `--agent-context-compact`,恢复扫描是 `--agent-resume`,Goal 生命周期是 `--agent-goal-*`。 + +## 备选方案与取舍 + +1. **只删正式用户窗口的入口,保留解析层**:看似省事,但命令字面量与分支继续存在,新对话形态仍要绕过它们,正是本次要消除的持续维护成本。 +2. **保留 `/history` 作为唯一命令**:它确实是唯一有执行语义的入口,但保留一个精确匹配的 `/` 语法就要求保留前缀绕过判断、命令被发现与文档承诺的整套口径;DirectProject 的历史重读改由重新进入对话/重新订阅自然完成,不需要用户输入控制词。 +3. **加兼容层(识别到已知命令时给提示或忽略)**:等于把死词汇表永久固化在解析层,与「按从未存在处理」相反,且会长期占据用户可见面。 + +## 影响 + +- 术语收敛:AGC 里的「命令」此后指项目内部命令 id 与权限位,「斜杠命令」作为已退役说法不再出现在权威文档与代码注释中。 +- 应用内项目对话的可见行为不变:正式对话面本来就不渲染命令列表,`/history` 之外没有可执行路径;删除后唯一的用户可见差异是输入以 `/` 开头时按普通文本处理。终端侧不再有 swarm chat 入口,也没有任何斜杠命令面。 +- 删除范围包含构建期门禁条目,因此不得为退役概念新增守卫测试或断言残留字符串的 check 条目;防止概念回归依靠架构边界(没有解析层可写)而不是字符串钉桩。 +- 复查收尾(同一决定的后续提交):配置向导里指向已删 npm 脚本的 `test:chat` 调用与失去含义的 `--configure-only` 开关、App 内 `请先用 /project …` 用户文案、`interaction.json` 中只服务已删交互内核的 7 个 prompt 键、harness 里因调用方被删而零引用的死导出,以及过期注释一并清理;无前端调用方的 Tauri 命令 `start_game_creator_agent_runtime_task` 随本次清理删除(`main.rs` 注册与 `check-config.mjs` 条目同步移除);`steer_game_creator_agent_runtime_task` 同时是 `decide_game_creator_agent_runtime_steer_at` 的唯一非测试入口,删掉该命令后整条 LLM steer 判定链(`agent/interaction.rs`、`runtime.interrupt_for_steer_decision`、`agent.runtime.steer_decision` 持久记录、`steer_decision_*` prompt 键与只测该链的专属用例)一并退役。 +- `.agent/logs/command.log`、项目权限确认卡与 `commandRuns` 属于项目命令审计,继续保留,不受本次退役影响。 diff --git a/docs/project-memory/plans/【实施计划】AGC渠道安装身份隔离-2026-09-21.md b/docs/project-memory/plans/【实施计划】AGC渠道安装身份隔离-2026-09-21.md index 0b57d90e7..a8f468584 100644 --- a/docs/project-memory/plans/【实施计划】AGC渠道安装身份隔离-2026-09-21.md +++ b/docs/project-memory/plans/【实施计划】AGC渠道安装身份隔离-2026-09-21.md @@ -10,7 +10,7 @@ - 允许修改: - `apps/ai-game-creator-shell/scripts/channel-identity.mjs`(新增,渠道身份单点定义) - - `apps/ai-game-creator-shell/scripts/build-release.mjs`、`build-macos-ci.mjs`、`check-config.mjs`、`agent-swarm-test-chat.mjs` + - `apps/ai-game-creator-shell/scripts/build-release.mjs`、`build-macos-ci.mjs`、`check-config.mjs` - `apps/ai-game-creator-shell/src-tauri/src/main.rs`、`src-tauri/src/windows.rs`、`src-tauri/src/config.rs` - 对应测试:`build-release.test.mjs`、`prepare-macos-codex.test.mjs` - 文档:AGC 更新主规范、共享记忆与本计划对 diff --git a/docs/project-memory/plans/【实施计划】游戏分发阶段C-AGC发布资料AI生成-2026-09-23.md b/docs/project-memory/plans/【实施计划】游戏分发阶段C-AGC发布资料AI生成-2026-09-23.md new file mode 100644 index 000000000..b5856a60f --- /dev/null +++ b/docs/project-memory/plans/【实施计划】游戏分发阶段C-AGC发布资料AI生成-2026-09-23.md @@ -0,0 +1,47 @@ +# 实施计划:游戏分发阶段 C · AGC 发布资料 AI 生成 + +- 状态:`in_progress` +- 日期:`2026-09-23` +- 上游:`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md` 的“AGC 游戏分发与在线游玩合同” +- 里程碑:`docs/project-memory/plans/【里程碑】游戏分发目录详情与在线游玩-2026-09-18.md` 阶段 C + +## 1. 交付结果 + +AGC 发布面板完成三项收敛: + +1. 移除 ZIP 路径、文件数、体积和“只上传字节”等技术摘要。 +2. 打开面板时基于有界、脱敏的项目上下文免费生成一句话简介和七类白名单分类;失败保留本地兜底,不阻断发布,生成结果可编辑。 +3. 游戏封面支持基于项目上下文生成,复用现役图片生成与泥点扣费链路;生成结果登记为当前账号平台素材后自动作为 `coverAssetId`,不二次上传。 + +## 2. 实现边界 + +- 只改 AGC 发布面板、AGC 发布 service、api-server 内部发布资料建议路由、shared DTO、定向测试和文档。 +- 不改网页发布表单、游戏分发审核 API、发行包上传/审核状态机、SpacetimeDB schema 或 `/api/external/v1` OpenAPI。 +- 简介/分类生成不写用户泥点账本;封面生成继续由现役 `execute_billable_asset_operation_with_cost` 负责预扣、幂等、失败退款和结果登记。 +- 传给文本模型的上下文只包含项目名称、创作目标、任务标题/状态、素材 kind/相对路径、运行状态和最近编辑提示;不包含绝对路径、聊天记录、凭据、Token 或完整 manifest。 + +## 3. 实现步骤 + +1. `packages/shared` 与 `shared-contracts` 增加发布资料建议请求/响应 DTO,分类继续使用 `GAME_DISTRIBUTION_CATEGORIES`。 +2. api-server 增加 `POST /api/game-distribution/publish-metadata/suggestions`,经 Bearer 鉴权后使用内部文本模型生成严格 JSON;解析失败或模型不可用时返回本地确定性兜底,不触发钱包扣费。 +3. AGC `GameDistributionPublishPanel` 打开时调用建议接口;用户未修改字段时回填,用户已编辑或迟到响应不得覆盖,失败不影响发布。 +4. AGC 封面生成复用 `POST /api/editor/images/generations` 的 `publication-material`、`gpt-image-2`、`16:9`、`2K` 合同;按钮和确认弹窗从 `/api/editor/generation-pricing` 读取并显示具体泥点数。若响应进入队列,轮询 `/api/runtime/external-generation/jobs/{operationId}`,完成后直接使用 `assetObjectId`。 +5. 发布截图同批并行上传;成功缩略图悬浮显示“删除|预览”,失败缩略图内部显示错误并在下方保留删除按钮。失败项不参与发布,不阻断同批其它截图。 +6. 定向测试覆盖面板删除技术摘要、免费资料回填、封面价格/确认/生成/`coverAssetId` 直用、截图并行与单张失败跳过、服务端 parser/route 与输入边界。 + +## 4. 验收判据 + +- 发布面板不出现 `发行包摘要`、ZIP 相对路径或文件数/体积。 +- 建议接口成功时简介和分类自动回填;分类只可能是七类之一;接口失败时保留原创作目标或通用兜底。 +- 建议请求不会调用钱包、不会写 `asset_operation_consume` 或 LLM Router 额度账本。 +- 封面按钮和确认弹窗显示后端运行时定价对应的具体泥点数;确认后调用现役图片生成接口,生成结果直接成为发布 `coverAssetId`,没有第二次上传或素材身份分叉。 +- 同批截图并行上传;成功图悬浮显示删除/预览,失败图内部显示错误并有独立删除按钮;失败项不参与发布且不影响其它截图。 +- 生成失败、余额不足和队列失败均在面板内可见且可重试,不进入聊天历史。 +- 定向 vitest、api-server Rust 测试、AGC typecheck、`check:encoding`、`git diff --check` 通过。 + +## 5. 非目标 + +- 不做标题 AI 改写。 +- 不做网页发布页的自动生成。 +- 不新增发布草稿持久化、生成历史、重试队列或 SpacetimeDB 表。 +- 不改变封面上传入口和手动选择封面的能力。 diff --git a/docs/project-memory/plans/【里程碑】DirectProject聊天真相源收敛-2026-09-16.md b/docs/project-memory/plans/【里程碑】DirectProject聊天真相源收敛-2026-09-16.md index bb852bae7..0a1ebbe3d 100644 --- a/docs/project-memory/plans/【里程碑】DirectProject聊天真相源收敛-2026-09-16.md +++ b/docs/project-memory/plans/【里程碑】DirectProject聊天真相源收敛-2026-09-16.md @@ -42,7 +42,7 @@ AGC 项目开发对话的显示与恢复只依赖两项输入:**项目对话 - 未知 item 类型由 Rust 原样透传(只有类型与身份,Rust 侧留 TODO),当前由前端投影丢弃。 - 前端聊天卡片的工具形状是 `Omit`;`tool-calls.jsonl` 的持久化形状与 DirectRuntime 的写入保持不变。 - 「可显示」的判据取**前端回合反馈**:一次翻页操作连拉到「合并后聊天投影的回合数增加」为止。工具卡片与思考文本虽然能通过 `projectDirectThreadItem`,但可能整页落进已渲染回合的折叠「执行过程」,不构成用户可见反馈;口径只在 `directHistoryPaging.ts` 里实现一份,首屏与「显示更早」共用。 -- 首屏切片的**新端边界**只认 `subscribe` 回执里的 `lastCompletedItemId`(含该条):回执到达之前不读首屏,也不退化成「取文件尾」;锚点缺失(订阅不可用 / 失败 / 历史为空)时才按文件尾取尾屏,`/history` 手动重读保持按当前文件尾取尾屏的恢复语义。 +- 首屏切片的**新端边界**只认 `subscribe` 回执里的 `lastCompletedItemId`(含该条):回执到达之前不读首屏,也不退化成「取文件尾」;锚点缺失(订阅不可用 / 失败 / 历史为空)时才按文件尾取尾屏,手动重读保持按当前文件尾取尾屏的恢复语义。 ## 依赖与前置条件 diff --git a/docs/project-memory/plans/【里程碑】游戏分发目录详情与在线游玩-2026-09-18.md b/docs/project-memory/plans/【里程碑】游戏分发目录详情与在线游玩-2026-09-18.md index 4024b8e92..226855815 100644 --- a/docs/project-memory/plans/【里程碑】游戏分发目录详情与在线游玩-2026-09-18.md +++ b/docs/project-memory/plans/【里程碑】游戏分发目录详情与在线游玩-2026-09-18.md @@ -86,6 +86,8 @@ ## 阶段 C:双端发布与现代游戏体验 +- AGC 发布资料生成切片实施计划:`docs/project-memory/plans/【实施计划】游戏分发阶段C-AGC发布资料AI生成-2026-09-23.md`。 + ### 前置条件 - B 已验收;网页根入口、桌面/移动导航、暖色主题交互稿及首版设备范围已评审。 @@ -94,6 +96,8 @@ ### 行为与验收 - [ ] AGC 从已构建 dist 生成根入口为 `index.html` 的真实包,一次提交动作完成检查、资料确认、上传和送审;状态及失败原因与服务端回读一致。 +- [ ] AGC 发布面板隐藏发行包技术摘要;打开时基于有界、脱敏的项目上下文免费生成一句话简介与白名单分类,失败保留本地兜底且不阻断发布;作者始终可以直接编辑生成结果。 +- [ ] AGC 发布封面支持基于项目上下文生成,复用现役图片生成与泥点扣费链路;生成结果登记为当前账号平台素材后自动作为 `coverAssetId`,不二次上传。 - [ ] 网页可选 ZIP、提交封面和必需资料,进入相同上传/校验/审核流程;任一客户端可以查看同账号游戏状态,更新沿用相同 `gameId`。 - [ ] 上传中断、双击、登录失效、窗口关闭后恢复原操作;换账号不能恢复前账号私有状态;待审不能显示为已发布。 - [ ] 目录支持真实数据、关键词/分类/设备筛选、空/错/加载态;详情提供明确主动作,搜索与返回恢复上下文。 @@ -103,7 +107,7 @@ ### 证据要求 -- 自动化:AGC 打包与操作恢复定向 Rust 测试、两端客户端/组件/路由/状态测试及类型检查。 +- 自动化:AGC 打包与操作恢复定向 Rust 测试、两端客户端/组件/路由/状态测试及类型检查;发布资料免费生成必须验证不写入钱包账本,封面生成必须验证扣费幂等、失败退款与 `assetObjectId` 直用。 - 运行时:AGC 一次真实发布、网页一次真实 ZIP 上传,分别审核后从桌面和手机游玩;浏览器覆盖游戏模块、素材、音频、触屏及横竖屏。 - 边界:未构建/失效 dist、资料缺失、换账号、迟到响应、审核拒绝、非移动游戏及真实空态。 diff --git a/docs/project-memory/plans/【里程碑】退役AGC项目对话斜杠命令与终端swarm chat入口-2026-09-22.md b/docs/project-memory/plans/【里程碑】退役AGC项目对话斜杠命令与终端swarm chat入口-2026-09-22.md new file mode 100644 index 000000000..581077bf8 --- /dev/null +++ b/docs/project-memory/plans/【里程碑】退役AGC项目对话斜杠命令与终端swarm chat入口-2026-09-22.md @@ -0,0 +1,85 @@ +# 【里程碑】退役AGC项目对话斜杠命令与终端swarm chat入口-2026-09-22 + +状态:已完成 +父规范:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md` + +## 目标与判据 + +一句话目标:AGC 应用内不再存在任何斜杠命令语义,终端 swarm chat CLI 入口(`--swarm-chat`)连同其命令层整体退役,且不再残留只服务这两者的实现、测试、文案与构建期门禁;行为上等同于它们从未存在。 + +验收判据: +- AGC 应用内(`apps/ai-game-creator-shell/src/**`)除路径/路由字面量外,不存在形如 `'/xxx'` 的命令字面量,也不存在对 `/` 开头输入的命令分支。 +- 斜杠命令专属模块与其导出全部消失,`projectSummary` 桶文件只保留现役导出。 +- 权威文档不再承诺「聊天输入 `/`」能力,终端章节点也不再把控制命令列为入口。 +- 终端侧不再存在 swarm chat 入口与其命令层;`--agent-run`、`--agent-enqueue`、`--agent-steer`、`--agent-resume`、`--agent-context-compact`、`--preview-serve` 等运维类控制命令保持现役。 +- 依赖终端入口的真实 E2E 与脚本(`user-input`、`supervisor-autonomous-playable-lane-defense`、三个 supervisor-swarm 混合套件、`agc:test`、`agc:test:chat*`、`agc:chat`、`agc:swarm`)与交互式 CLI harness 管道一并消失。 +- 不新增任何守卫测试或 check 脚本条目。 + +## 范围 + +范围内(删除): +- `features/project-summary/projectSummaryConstants.ts` 的 `chatCommandHelp` 与全部命令清单常量。 +- `features/project-summary/chatCommandMetadata.ts`(斜杠命令参数校验文案)。 +- Direct 聊天的 `/history` 精确匹配分支与 `reloadHistory`。 +- `chatPromptPolish` 的 `/` 前缀绕过分支。 +- `memoryCommands.ts` 的 `parseRememberInput`(`/remember` 参数解析器)。 +- 只服务退役 Supervisor 摘要面板、零外部调用的 `project-summary/*Summaries.ts` 与 `agentTrace.ts`。 +- `/sync-canvas-project`、`/read `、`/trace` 的草稿回填死链(前端 `agentPresentation.ts` + Rust `suggested_canvas_tool_call`)。 +- 无人调用的 Tauri 命令 `get_game_creation_agent_capabilities`、`get_limited_local_commands`。 +- 钉住上述字符串的构建期门禁条目与专属测试。 +- 终端 swarm chat 入口与整个命令层:`cli.rs` 的 `SwarmChat` 变体、`--swarm-chat` 解析与派发、`src/swarm_cli.rs` 与 `src/swarm_cli/` 整个目录(输入解析与帮助输出、`/agents`、`/status` 打印器、`/goal` 引擎、`/compact`、`/resume`、`/quit` 退出分支、观察器、报告、turn 派发与等待)、`SwarmChatFlow`、`SwarmTurnObservation`、`SwarmTurnOutcome::Quit`、`SwarmConfirmationResolution::Quit`、`SWARM_TURN_*_ERROR`,以及 `swarm_cli/input.rs` 中那份只为终端命令存在的 `agent.compact` / `agent.resume` / `agent.run_status` 权限校验(同名权限 id 在现役 Tauri 命令与 Runtime 生命周期上的门禁保持不动)。 +- 只服务终端交互内核的 `agent/interaction.rs` 整层:`AgentInteractionAction`、tool registry、`game_creator_agent_uses_interaction_kernel`、`decide_game_creator_agent_interaction_turn_for_session_at`、`AgentInteractionProviderStreamSink`;其上的自然语言 steer 决策 LLM 路径 `decide_game_creator_agent_runtime_steer_at` 在收尾复查后一并删除,见「收尾清理」。 +- 只能由命令触达的打印器:`print_runtime_response_stream_status` 及其专属测试。 +- 依赖终端入口的脚本与套件:`scripts/agent-swarm-test-chat.mjs`、`scripts/agent-runtime-deterministic-playable-e2e.mjs`、`scripts/deterministic-lane-defense-provider.mjs`,套件 `user-input`、`supervisor-autonomous-playable-lane-defense`、`supervisor-swarm-autonomous-chat`、`supervisor-swarm-static-isolated-autonomous-chat`、`supervisor-swarm-collaboration-policy-mixed-recovery`,以及 harness 里的交互式 CLI 管道(`startInteractiveCli`、`writeInteractiveCliLine`、`waitForInteractiveCli*`、`closeInteractiveCli`、`activeInteractiveCliSessions`、`answerRemainingInteractiveQuestions` 等)与只服务这些套件的混合套件分支、状态字段、sentinel 与 npm 脚本。 同时清掉只被这些套件调用的 harness 残件:`collaboration-assertions.mjs` 的混合/静态隔离断言族(`supervisorSwarmMixed*`、`observeSupervisorSwarmStaticIsolatedProviderOverlap`)、`repair-recovery.mjs` 的静态隔离观测调用点、evidence 模板与校验里的 `mixed*` / `staticIsolated*` / `initialBatchRecovery*` 占位字段,以及 `self-test.mjs` 中对应的合成用例与汇总字段。 + +范围外(保留): +- 命令 id 注册表 `GAME_CREATION_APP_COMMANDS` 与 `GameCreationAppPermission`(项目权限策略词汇表;App 前端只用 `GameCreationAppCommandDescriptor` 类型表达权限判定与审计粒度,数组本体由 Rust 策略路径消费)。 +- `needsInitializedChatProject` 与项目权限策略链路。 +- Rust 侧路径/路由的 `/` 前缀校验。 +- `--agent-run`、`--agent-enqueue`、`--agent-task`、`--agent-steer`、`--agent-retry`、`--agent-cancel`、`--agent-confirm`、`--agent-resume`、`--agent-runtime-status`、`--agent-goal-*`、`--agent-context-compact`、`--runner-status`、`--runner-shutdown-if-idle`、`--llm-status`、`--preview-serve`、`--environment-check`、`--direct-codex-chat` 等运维与开发 CLI 控制命令。 +- 保留的真实 E2E 套件:`supervisor-swarm`、`supervisor-swarm-transient-retry`、`supervisor-swarm-final-reply-transient-retry`、`supervisor-swarm-tool-plan-handoff-runner-kill`、`goal-runtime`、`response-stream`、`web-search`、`context-compaction`、`scoped-agents`、`project-skill`、`parallel-read`、`steer-runner-kill`、`process-session`。 + +## 检查点 + +1. 斜杠语义层:Direct `/history`、润色绕过、`chatCommandHelp`/`chatCommandMetadata`、`parseRememberInput`、桶文件与门禁、专属测试。 +2. 死链与死模块:草稿回填链(前端 + Rust + 断言)、零调用摘要模块、`agentTrace.ts`。 +3. 无人调用的能力清单 Tauri 命令。 +4. 文档收口:AGC 主专题命令承诺、ADR、`decision-log.md`、`CONTEXT.md`。 +5. 终端 swarm chat 命令层与只服务它的权限门禁、打印器、专属测试。 +6. 终端 swarm chat 入口本体、交互内核、`--swarm-chat` 派发与其专属 Rust 测试。 +7. 依赖终端入口的 e2e 套件、deterministic wrapper/provider、npm 脚本、构建期门禁条目、harness 交互式 CLI 管道、混合套件分支,以及随之失效的混合/静态隔离断言族与 evidence 占位字段。 +8. 文档收口:终端入口退役后主实施计划、Runtime 文档、ADR、decision-log、pitfalls、里程碑计划的最终口径。 + +## 验证 + +- `npm run --workspace apps/ai-game-creator-shell typecheck`(含 `skill-pack:check` 与 `check-config.mjs`):通过。 +- 定向用例 `npx vitest run tests/appSurface.test.ts`:211 tests(202 passed / 9 skipped);`chatPromptPolish`、`rememberCommand`、`ChatMarkdownMessage`、`agentRuntimeModel` 等相关用例集通过。 +- `cargo check --tests`:0 error;删改文件无新增 `dead_code` 告警(按「父提交 vs 本次」引用数逐条比对告警标识符确认)。 +- `npm run check:encoding`、`npm run check:doc-index`、`git diff --check`:通过。 +- 真实 E2E harness 自检 `node scripts/agent-runtime-real-e2e.mjs --self-test`:`status: PASS`;harness 相对与具名 import 全部可解析。 +- 收尾清理(2026-09-23,LLM steer 判定链):`cargo check --tests` 0 error,告警与改动前基线一致(无新增 `dead_code`);`npm --workspace apps/ai-game-creator-shell run typecheck`、`npm run check:encoding`、`npm run check:doc-index`、`git diff --check` 通过;幂等复核 `steer_decision` / `interrupt_for_steer_decision` / `decide_game_creator_agent_runtime_steer_at` 在 `apps/`、`packages/` 源码中零命中。 + +## 文档收口 + +- `docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`:删除全部「聊天输入 `/`」能力条目,保留并改写其中的非命令事实;`/compact`、`/mcp`、`/goal`、`/resume` 的终端承诺改为对应 `--agent-*` 入口。 +- `docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md`:删除终端 swarm chat 入口整节,控制面统一记为 `--agent-*` CLI(`/goal` → `--agent-goal-*`、`/compact` → `--agent-context-compact`、`/mcp` → 开发配置面板与真实 E2E 核验);终端不再承担任何交互式聊天职责。 +- `docs/adr/【ADR】退役AGC项目对话斜杠命令与终端swarm chat入口-2026-09-22.md`:本次决策与影响边界。 +- `docs/project-memory/shared-memory/decision-log.md`:新增 2026-09-22 决策条目,并清理已被本次退役取代的历史命令条目。 +- `docs/project-memory/shared-memory/pitfalls.md`、`docs/project-memory/plans/【里程碑】DirectProject聊天真相源收敛-2026-09-16.md`:移除 `/history` 入口表述,改为「显式重新加载对话」。 +- `CONTEXT.md`:新增「项目对话输入」「项目命令 id」术语,把「斜杠命令」标为已退役说法;不再出现 `/history` 重读入口。 +- `docs/README.md`:登记新 ADR。 + +## 收尾清理 + +复查(2026-09-23)发现退役残留与随之产生的零引用代码,按同一口径清理: + +- 配置向导 `scripts/game-creator-config-wizard.mjs`:删除指向已删脚本的 `npm run test:chat` 调用、失去含义的 `--configure-only` 开关,以及只服务该分支的 `askYesNo` 与 `npmCommand`。 +- 用户文案:App 内 `请先用 /project 设置本地项目。` 改为 `请先打开本地项目。`。 +- 注释与 prompt:`chatPromptPolish` 头部注释的 `/` 命令表述、`harness/process.mjs` 与 `scripts/check-config.mjs` 中指向已退役入口的说明,以及 `interaction.json` 中只服务已删交互内核的 7 个键(`execute_description`、`resume_description`、`project_location_description`、`protocol`、`system`、`user`、`user_with_context`)。 +- harness 死导出:`runtime-state.mjs` 的 4 个 schema 常量、`assertions/runtime.mjs`、`harness/project.mjs`、`collaboration-policy.mjs`、`persistence.mjs` 中因调用方被删而零引用的函数,连同因此失去用途的 import。 +- 无前端调用方的 Tauri 命令 `start_game_creator_agent_runtime_task` 与 `steer_game_creator_agent_runtime_task` 全部删除(`main.rs` 注册与 `check-config.mjs` 条目同步移除)。删掉 steer 命令后其唯一非测试入口的整条 LLM steer 判定链一并退役:`agent/interaction.rs`(`runtime_steer_decision` 工具、判定请求构建、响应解析、`decide_game_creator_agent_runtime_steer_at`)、`runtime.interrupt_for_steer_decision` RPC 与派发分支、持久 decision 记录(`agent.runtime.steer_decision`)、`AgentRuntimeProviderInterrupt::applied_steer_cursor`、`AgentRuntimeSteerResult` 的 `assistantReply / interruptDecision / decisionReason` 字段、`steer_decision_*` prompt 键,以及只测该链条的 provider / runner / runtime_state 用例。 + +## 未做与边界 + +- 不为退役概念新增守卫测试或断言残留字符串的 check 条目。 +- 不为终端入口与已删套件补充替代实现、兼容别名或迁移提示。 diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 23eea23dc..265ec8690 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -24,6 +24,48 @@ - 未纳入本次:斜杠命令 `/` 解析、拖拽文本(drop)、附件 / 运行画面区域 / 文件路径 / URL / 剪贴板图片、复制侧 `text/plain` 形态调整、扩展安装卸载后的目录即时失效。 - 验证方式:`buildContentFromPastedText` 规则矩阵单测(含「显示文本再粘贴回来得到同一份 content」这条逆运算)、provider 的 `fuzzyLookup` / `lookup` 用例、输入区集成用例(真 Lexical `paste` 事件 → 芯片、未命中等价于默认粘贴、Skill 冷启动保持字面且敲过 `$` 后可解析);另跑 `npm run typecheck`、`npm run check:encoding`、`npm run check:doc-index`、`git diff --check`。 +## 2026-09-23 最近项目检查失败不进终态 + +- 背景:最近项目列表把一次性的目录检查失败当成终态——5s 超时被吞成 `null`,增量投影又把上一轮的 `null` 原样搬进下一轮,且没有重试或重查入口。AGC 一次 IPC 停顿之后,整张列表会永久停在「检查失败 + 待识别」,首页「最近项目」同时因 `canOpen` 过滤变空,只能重启客户端恢复(issue #490)。 +- 决策:单次检查失败先就地重试一次(300ms);失败结果不进新投影(失败项回到「检查中」并重新检查);一轮结束仍有**可重试**失败时按 15s / 45s / 120s 重跑整张列表,重跑上限 3 次,失败集合变化或整轮无失败即重置预算;重命名后的单条刷新复用同一套重试与有界重查。 +- 提权边界:Windows ACL 自动提权类失败(`DACL`、`权限`、`error 5`、`安全对象不属于当前用户`、`特权`、`1300`、`AGC ACL 提权修复未成功`)判定为不可重试——重试等于在用户刚点「否」后再弹一次 UAC(提权闸门只存在于单次 invoke 内,进程级没有冷却记忆)。这类项目在用户再次主动打开/新建项目或重命名刷新之前不再自动重试,也不驱动整表重查。 +- 验证:`apps/ai-game-creator-shell/tests/recentProjectsHook.test.tsx` 覆盖「单次失败就地重试」「失败不跨轮保留」「提权类失败不重试」(前两者在改前代码上必挂);`tests/appSurface/home.suite.ts` 的失败态改为等待最终状态;退避重查用一次性脚本验证持续失败后 15s 自动恢复(脚本未入库)。 + +## 2026-09-22 筛选控件选中态:类名收敛到 helper,视觉收敛到「实心填充 + 反白文字」 + +- 背景:`platform-category-chip` 的类名字符串此前在三个宿主各抄一份(共享筛选条 `PlatformResourceFilterBar`、资源画布筛选浮层 `ResourceFilterPanel`、模板库筛选区),「选中的筛选胶囊长什么样」随时会各自漂移;更严重的是选中态本身只用了 `--platform-cool-*` 这组低透明度暖色,实测选中/未选底色对比只有 1.09:1,用户反馈「选中和没选中的颜色看不出差别」。 +- 决策:① 类名口径收敛到 `packages/shared/src/components/platformCategoryChipModel.ts` 的 `getPlatformCategoryChipClassName(active)`(从 `@genarrative/shared/components` 导出),三处宿主统一改调它;② 选中态改为**实心品牌填充 + 反白文字**,语义色收在新的 `--platform-chip-idle-fill` / `--platform-chip-active-{fill,border,text,shadow}`(浅色皮肤深暖填充、深色皮肤亮靛蓝填充 + 深文字),`PlatformSegmentedTabs` 新增 `tone="accent"` 与 chip 共用这套色;③ `src/index.css`(平台 Web/平台 H5)里那份重复的 `--active` 规则同步改口径,避免覆盖共享样式把 Web 端打回旧样子。 +- 状态阶梯(同一份口径,三个状态不许互相冒充):静止 = 浅底 + 中性描边 + 常规文字;悬停 = 中性加描边 + 极淡暖底 + 深色文字(**品牌色只能属于「已选中」**,悬停用品牌色会让未选中的 chip 看起来已选中);按下 = 再压一层;选中 = 实心填充 + 反白文字,是唯一的强状态。运行时分段的 `accent` 未选中悬停同理(淡暖底 + 深文字)。 +- 焦点态同批收口:`--platform-input-focus-ring` 从 15% 透明度改成实心色(合成后 1.17:1 的环等于没有),筛选 chip / 分段项 / 排序按钮的焦点提示改用 `outline: 2px solid ; outline-offset: 2px`——不再用 `box-shadow` 画环,避免被选中态自己的投影盖掉。 +- 原因:二元状态必须靠**填充/明度**表达而不是色相微调;颜色只允许在 `packages/shared/src/theme.css` 的语义变量里出现,组件不再自己写颜色字面量。 +- 影响范围:`packages/shared/src/theme.css`、`packages/shared/src/components/{platformCategoryChipModel.ts,styles.css,PlatformSegmentedTabs.tsx,PlatformResourceFilterBar.tsx,index.ts}`、`src/index.css`、`apps/ai-game-creator-shell/src/view/{project-development/ResourceFilterPanel.tsx,template-library/index.tsx}`。 +- 验证方式:`apps/ai-game-creator-shell/tests/workbenchThemeContrast.test.ts` 按 WCAG 公式断言两套皮肤都满足「选中文字 ≥ 4.5:1(渐变两端)」且「选中填充 vs 未选底色 ≥ 3:1」;`platformCategoryChipModel.test.ts` 钉住选中类名分支;`PlatformResourceFilterBar.test.tsx` / `resourceFilterPanel.test.tsx` / `src/index.test.ts` 覆盖各宿主。真机 AGC 客户端截图实测两态填充对比 6.0:1、选中文字 4.8–6.0:1。 + +## 2026-09-22 退役 AGC 项目对话斜杠命令与终端 swarm chat 入口 + +- 背景:AGC 项目对话曾把大量能力挂在「聊天输入 `/`」上(`/history`、`/read`、`/help`、`/status`、`/trace`、`/export`、`/preview`、`/remember`、`/brief` 等),无 GUI 的终端 swarm chat 入口 `--swarm-chat` 又自带一套控制命令(`/help`、`/agents`、`/status`、`/history`、`/compact`、`/resume`、`/goal`、`/quit`)。两套入口都没有现役调用方,撤回成本却持续存在:命令字面量散落在前端命令分支、润色绕过、摘要模块、`swarm_cli` 终端输入解析、构建期门禁条目和文档承诺里,任何新对话形态都要额外维护这套死词汇表。 +- 决策:斜杠命令语义与终端 swarm chat 入口整体退役,按「从未存在」处理。应用侧删除 Direct 聊天的 `/history` 精确匹配分支与 `reloadHistory`、`chatPromptPolish` 的 `/` 前缀绕过、`chatCommandMetadata` / `chatCommandHelp` / `memoryCommands` 的命令清单与参数解析、只服务退役 Supervisor 摘要面板的 `project-summary/*Summaries.ts` 与 `agentTrace.ts`、草稿回填死链(前端 `agentPresentation.ts` + Rust `suggested_canvas_tool_call`)、无人调用的 Tauri 命令 `get_game_creation_agent_capabilities` / `get_limited_local_commands`,以及钉住这些字符串的构建期门禁条目与专属测试。终端侧连同入口一并删除:`--swarm-chat`、`src-tauri/src/swarm_cli.rs` 与整个 `swarm_cli/` 目录(命令解析与帮助输出、turn 派发、观察器、报告、专属测试)、`SwarmChatFlow`、`SwarmTurnObservation`、`SwarmTurnOutcome::Quit`、`SwarmConfirmationResolution::Quit`、`SWARM_TURN_*_ERROR`、只服务这些命令的 `agent.compact` / `agent.resume` / `agent.run_status` 权限门禁与 `print_runtime_response_stream_status` 打印器,以及只服务终端交互内核的 `agent/interaction.rs` 整层(`AgentInteractionAction`、tool registry、`game_creator_agent_uses_interaction_kernel`、`decide_game_creator_agent_interaction_turn_for_session_at`、`AgentInteractionProviderStreamSink`);该文件只保留自然语言 steer 决策路径 `decide_game_creator_agent_runtime_steer_at`。真实 E2E 的交互式 CLI 管道、`scripts/agent-swarm-test-chat.mjs`、`agentSwarmTestEntry.test.ts` 与 `agc:test:chat` / `agc:test:chat:manual` / `agc:chat` / `agc:swarm` 等 npm 脚本同步删除。 +- 保留项:命令 id 注册表 `GAME_CREATION_APP_COMMANDS` 与 `GameCreationAppPermission`(项目权限策略词汇表;App 前端只用 `GameCreationAppCommandDescriptor` 类型表达权限判定与审计粒度,数组本体由 Rust 策略路径消费)、`needsInitializedChatProject`,以及 `--agent-run` / `--agent-enqueue` / `--agent-steer` / `--agent-resume` / `--agent-context-compact` 等非聊天 CLI 控制命令与 Tauri IPC 注册。 +- 影响范围:`apps/ai-game-creator-shell/src/**`(Direct 聊天控制器、润色、`project-summary`、`project-workspace`)、`src-tauri/src/**`(`cli.rs`、`main.rs`、`swarm_cli` 整目录删除、`agent/interaction.rs` 收敛、命令注册、canvas 生成、provider / project 测试)、`apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/**`、`scripts/check-config.mjs`、root 与 App 的 `package.json` 脚本、`tests/**`,以及 AGC 主实施计划文档、Runtime V1.1 文档与 `CONTEXT.md` 术语。 +- 验证方式:`npx tsc -p apps/ai-game-creator-shell/tsconfig.json --noEmit`、`npm run --workspace apps/ai-game-creator-shell typecheck`(含 `check-config.mjs` 的脚本与门禁一致性)、`npx vitest run apps/ai-game-creator-shell/tests/appSurface.test.ts`、`cargo check --tests`(告警消息集与基线一致)、`npm run check:encoding`、`git diff --check`;保留的 e2e 套件为 `supervisor-swarm`、`-transient-retry`、`-final-reply-transient-retry`、`-tool-plan-handoff-runner-kill`、`goal-runtime`、`response-stream`、`web-search`、`context-compaction`、`scoped-agents`、`project-skill`、`parallel-read`、`steer-runner-kill`、`process-session`。 +- 关联文档:[【ADR】退役AGC项目对话斜杠命令与终端swarm chat入口-2026-09-22](../../adr/【ADR】退役AGC项目对话斜杠命令与终端swarm chat入口-2026-09-22.md)、[【里程碑】退役AGC项目对话斜杠命令与终端swarm chat入口-2026-09-22](../plans/【里程碑】退役AGC项目对话斜杠命令与终端swarm chat入口-2026-09-22.md)、`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。 + +## 2026-09-23 AGC 发布资料免费生成与封面泥点生成 + +- 背景:AGC 发布面板仍展示 ZIP 路径、文件数和体积,同时一句话简介只取创作目标、分类固定为“其他”,封面只能手选;这与发布页应隐藏技术信息、使用创作上下文降低填写成本的目标不一致。 +- 决策:发布面板移除发行包技术摘要。简介和分类由 AGC 调用平台内部免费文本模型生成,输入仅限有界、脱敏的项目名称、创作目标、任务状态、素材类型和最近编辑摘要;生成失败保留本地兜底,不扣用户泥点且不阻断发布。分类始终收敛到七类白名单。 +- 决策:封面支持基于项目上下文生成,复用现役编辑器图片生成接口和泥点扣费 wrapper;按钮与确认弹窗显示后端运行时定价对应的具体泥点数。服务端返回的 `assetObjectId` 直接作为 `coverAssetId`,禁止生成后再直传导致素材身份分叉。生成失败原因留在发布面板内。 +- 决策:发布截图同批并行上传;成功缩略图在鼠标移入时显示“删除|预览”,失败缩略图在图片内显示错误并保留独立删除按钮。单张失败只跳过该张,不阻断同批上传或发布。 +- 边界:网页发布表单、游戏分发审核 API、SpacetimeDB schema、外部 `/api/external/v1` OpenAPI 均不变;临时项目上下文不落库、不进聊天记录。 +- 验证:AGC 发布面板与发布 service 定向 vitest、api-server 发布资料 parser/route 测试、AGC 与 api-server 类型/编译检查、编码和 diff 检查。 + +## 2026-09-23 AGC 发布前先守可运行原型门禁 + +- 背景:客户端已经显示“首个可运行原型尚未完成,运行视图暂不可用”,但发布入口仍会先执行用户项目的 `build`,导致未完成原型也进入构建并在后续失败。 +- 决策:`project.export_package` 的发布专用导出链路先检查可玩入口;没有入口时,只有 `code-prototype` 已完成或存在运行中的预览才允许执行 `build`,否则直接返回“首个可运行原型尚未完成,暂不能发布”。发布阻断反馈使用独立提示弹窗,不写入 Direct 聊天记录;发布过程不再进入聊天确认卡,改为独立全屏进度弹窗;运行中遮罩覆盖整个工作区并阻止交互,失败留在弹窗内,成功后切换到发布资料面板。 +- 边界:已有可运行入口仍直接打包;原型已完成但缺构建产物时保留原有自动构建;缺失 `exports/README.md` 仍在导出前自动生成。 +- 验证:Rust `publish_export` 4/4、前端发布相关测试 16/16、AGC `tsc`、编码检查和 `git diff --check` 通过。 + ## 2026-09-22 Direct 埋点与业务持久化锁隔离 - Direct 采集身份和最新成果编号改由独立纯内存状态保存,初始化时从最终执行账本冻结项目与原 run 身份;成果采集、预览采集上下文和终态成果读取不再争用业务落盘锁。 @@ -70,7 +112,7 @@ - 当前合同唯一维护入口为[客户端本地埋点与主站入库契约](../../technical/【技术方案】客户端本地埋点与主站入库契约-2026-09-21.md),原始需求作为仓库内历史来源保存;后续里程碑规范与实施计划放在 `docs/project-memory/plans/`。 - 本地采集阶段已验收明文 JSONL 持久化;当前仍不做加密。一个项目对应一个目标,事件按业务节点采集,5 分钟封存,7 天或 20 MiB 清理;上传失败也受保留上限约束。 -- 当前上传实现已完成隔离环境验收,证据见同一主规范第 13 节:每 15 分钟上传匹配当前账号与平台的封存批次,新增一张客户端事件私有表、批次原子入库与幂等确认、成功清理及独立后台明细栏目;失败静默留待下周期重试。真实客户端文件、HTTP、数据库与后台查询已关联同一事件验证,浏览器列表/筛选/详情通过;未部署生产。保持原 12 类事件和原采集边界。上线须配置 `GENARRATIVE_AGC_ANALYTICS_ORIGIN`,按数据库、API/后台、客户端顺序发布。 +- 当前上传实现已完成隔离环境验收,证据见同一主规范第 13 节:每 15 分钟上传匹配当前账号与平台的封存批次,新增一张客户端事件私有表、批次原子入库与幂等确认、成功清理及独立后台明细栏目;失败静默留待下周期重试。真实客户端文件、HTTP、数据库与后台查询已关联同一事件验证,浏览器列表/筛选/详情通过;未部署生产。保持原 12 类事件和原采集边界。埋点接收复用 `GENARRATIVE_CLIENT_DOWNLOAD_CHANNEL` 的 dev/release 官方站点映射;仅 dev 渠道且 `GENARRATIVE_ENV` 为 development/test/container 时额外接受 loopback origin,无独立埋点环境变量。按数据库、API/后台、客户端顺序发布。 - 已按技术负责人授权开始实施:合同与本地队列、会话窗口与项目接入、策划阶段成果、首次提交及两类 Agent run 已实现并经独立审查;定向测试、生产编译和前序 GUI 启停证据统一见主规范第 12 节。不得宣称完整产品采集已上线。 ## 2026-09-22 引用输入区改为宿主注入引用 provider,选择器面板与输入区分离 @@ -306,7 +348,7 @@ Godot 编辑器操控复用既有 AGC 插件宿主、EditorAdapter、Runner 和 ## 2026-09-20 最近项目检查保持项目级隔离 - 背景:最近项目刷新会重新检查所有路径。若其中一个目录损坏、超时或不可读,清空整张状态表会让已确认正常的项目暂时全部显示“检查中”,用户只能移除坏项目后看到列表恢复。 -- 决策:最近项目状态按路径独立投影;刷新时保留仍在列表中的最后一次结果,只有新增或尚未检查的项目进入“检查中”。检查代次或列表成员变化后,迟到结果不得写回,单个项目的失败不能改变其它项目的可打开状态。 +- 决策:最近项目状态按路径独立投影;刷新时保留仍在列表中的最后一次**成功**结果,失败结果不进新投影并在本轮重新检查(见 2026-09-23 条目),只有新增或尚未检查的项目进入“检查中”。检查代次或列表成员变化后,迟到结果不得写回,单个项目的失败不能改变其它项目的可打开状态。 - 验证:`recentProjectsHook.test.tsx` 覆盖“新增慢/坏项目刷新时保留正常项目”;`recentProjectsModel.test.ts`、`unityProjectOpen.test.tsx` 与前端类型检查一并执行。 ## 2026-09-17 GameCreationApp 资源 kind 只保留一份词汇表:严格解析 + `app_log!` 留痕 @@ -438,7 +480,7 @@ Godot 编辑器操控复用既有 AGC 插件宿主、EditorAdapter、Runner 和 - 背景:ADR「首屏历史由 `subscribe` 返回的 `lastCompletedItemId` 锚定,再取最近切片」只落了一半。`DirectThreadManager` 是搬运层,内存里没有「已完成条目」的锚点,`subscribe` 一律返回 `last_completed_item_id: None`;`commands.rs` 的 `subscribe_direct_project_thread` 在为空时用 `read_direct_project_last_item_id_at` 从磁盘回填,所以线上回执里的值是真的(订阅那一刻文件里最后一条可显示条目的原始 item id)。前端侧:首屏一直在 `loadProjectConversation` 里用 `beforeItemId: null` 直接取文件尾一屏,`lastCompletedItemId` 自 `1b40f030e` 起不再被任何代码读取。 - 决策(锚点语义):首屏切片的新端(较新一侧)边界就是这个锚点,**含锚点条目本身**;切片命令新增 `throughItemId` 参数表达「取到这条为止」。比锚点更新的条目只从运行态事件来,历史切片与实时流因此不重叠(原来的文件尾读取会把订阅回执之后才完成的条目也拉进历史,与运行态事件同 id 重叠,只靠前端合并兜住)。 -- 决策(读取时机):订阅回执到达之前不读首屏,也不退化成「取文件尾」;锚点缺失(订阅不可用 / 失败 / 历史为空)时才按文件尾取尾屏。`/history` 手动重读保持「按当前文件尾取尾屏」的恢复语义,不锚定。 +- 决策(读取时机):订阅回执到达之前不读首屏,也不退化成「取文件尾」;锚点缺失(订阅不可用 / 失败 / 历史为空)时才按文件尾取尾屏。手动重读保持「按当前文件尾取尾屏」的恢复语义,不锚定。 - 决策(翻页不变):向后翻页仍用切片返回的 `firstItemId` 作 `beforeItemId`(不含锚点),`hasMore` 与连拉口径不变。 - 影响范围:`agent/direct_project_history.rs`(切片锚点 + `through_item_id` 参数)、`commands.rs`(`read_direct_project_history_slice` 命令参数)、AGC 前端首屏读取接线与测试骨架。**未改**:DirectRuntime 的 `turn-stream.jsonl` / `tool-calls.jsonl` 写入与进度事件、`list_game_creator_direct_active_turns`、SpacetimeDB 与 HTTP 契约。 - 验证方式(已跑):Rust 侧 `cargo test agent::direct_project_history`(22 passed,含「窗口取到锚点那条、排除比锚点更新的条目、`beforeItemId` 与 `throughItemId` 互斥报错」三类用例);前端 `npx vitest run .../directHistoryAnchorGate.test.ts`(10 passed)与 appSurface 的 `anchors the first history page at the subscribe receipt instead of the file tail`(全量 475 tests / 457 passed / 17 skipped;唯一失败 `edits the published runtime config without leaking API keys into chat` 与本次改动无关,stash 掉本次前端改动后同样变红);`tsc` / ESLint / prettier / `check:encoding` / `check:doc-index` / `git diff --check` 全绿。变异验证:闸门忽略「已消费」、首屏不等闸门两处改动各自让对应用例变红。 @@ -501,7 +543,7 @@ Godot 编辑器操控复用既有 AGC 插件宿主、EditorAdapter、Runner 和 - 决策(事件与退出):manifest 失效与 Runtime update relay 的接收端从单槽改为按 `event_sink_token` 去重的注册表并广播,发送失败只淘汰该接收端;GUI 退出先释放本窗口参与锁,仍有其它窗口时保留 Runner(`agent.runner.gui_exit.retained_for_other_windows`),最后一个窗口才请求关闭。Runner 启动失败时先按最新 endpoint 复用一次,避免两个窗口同时冷启动时的实例锁竞争被误报成启动失败。 - 边界:本机 GUI ↔ Runner 协议方法与参数不变,不引入多 Runner、不做跨 AppData 会话共享;项目级 `.agent/project.lock` 不变,多窗口仍不能并行写同一项目;平台登录态 generation 单调与 claim 失配失败关闭语义保持不变;混用新旧版本二进制访问同一 AppData 不属于支持场景。 - 验证:定向 Rust `runner::tests::gui_owner_*` 11 条与新增的参与锁多窗口 / 存活判定 / claim 采纳与轮换 / 同 claim 第二个窗口不清空登录态用例全部通过;真实 debug 二进制 Windows smoke 证明同一 AppData 两个 GUI 都完成 `startup.setup.complete`、只存在一个 `--agent-runner` 进程、关闭一个窗口后另一个窗口与 Runner 继续存活、最后一个窗口退出后 Runner 退出并删除 endpoint;`npm run check:encoding`、`npm run check:doc-index`、`git diff --check` 通过。 -- 未验证 / 已知环境问题:真实安装包双开需要重新构建发布后才能验证;`durable_provider_handoff_prevents_shutdown_even_when_corrupt`、`durable_provider_retry_prevents_shutdown_and_reopens_writes`、`runtime_interrupt_for_true_steer_decision_only_interrupts_older_provider_cursor` 三条用例在本机改动前的基线上即失败(Windows 安全对象 owner 校验与 Provider 请求重复),与本决策无关。 +- 未验证 / 已知环境问题:真实安装包双开需要重新构建发布后才能验证;`durable_provider_handoff_prevents_shutdown_even_when_corrupt`、`durable_provider_retry_prevents_shutdown_and_reopens_writes` 两条用例在本机改动前的基线上即失败(Windows 安全对象 owner 校验与 Provider 请求重复),与本决策无关。 - 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`(2026-09-16 节)、`docs/project-memory/plans/【里程碑】AGC同AppData多窗口共享Runner-2026-09-16.md`。 ## 2026-09-16 DirectProject 三维请求解除 Phaser 固定约束,由 Codex 自选技术栈 @@ -2421,7 +2463,7 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在 - 2026-07-12 安全边界:`project.verify` 的 script 最多 160 个字符,固定使用系统 script shell,并在解析和执行前拒绝项目级 `.npmrc`。Runtime context bundle 必须绑定 `projectId / agentId / taskId / sessionId / runId / source / task`,结尾换行计入 64 KiB 上限;恢复时还要校验 `nextLoopIndex`、context window、当前窗口已完成轮数、观察指纹、计划和 observation 数量。bundle 写入必须拒绝父目录符号链接,读取必须基于同一文件句柄限制到 64 KiB,并清洗项目路径及常见平台凭据;已观察动作只有在 observation 写入 context checkpoint 后才能删除 ledger,下一轮 planning 和跨重启恢复不得再被旧 ledger 抢占。 - 背景:开发用单 Agent 聊天已经能真实调用各 Agent 的 LLM 路由并持久化对话,但 Agent 仍主要表现为同步问答,用户无法明确投递一个任务让某个 Agent 独立运行,也无法同时启动多个 Agent 的工作。 -- 决策:在现有 `.agent/runtime` 和 `.agent/conversations` 基础上新增单 Agent 后台任务入口。Tauri 命令 `start_game_creator_agent_runtime_task` 立即写入该 Agent 的 runtime state/event/task history,追加用户任务到 `.agent/conversations/agents/.jsonl`,随后在 App 进程内启动 tokio task 执行最小 Agent loop:Agent 按轮输出 `thinkingSummary / plan / actions / response`,Runtime 按白名单和项目权限策略执行工具并记录 `action / observation` 事件,再把已有 observation 放回下一轮 prompt,让 Agent 修正计划、继续行动或用空 actions + response 收束;单 Agent Runtime 每 6 轮形成一个上下文压缩窗口,窗口有新的独立 observation 时压缩上下文并在同一 run 继续,最近 6 轮没有独立进展或相邻窗口重复时以 `failed / budget-exhausted` 和 `loop-budget-exhausted` 终止,不生成总结伪装完成。完成或失败后把 assistant 回复或错误追加回对话,并写入 `.agent/agent.db` 审计记录。工具箱包含只读工具 `memory.read`、`conversation.read`、`asset.list`、`project.index`、`project.diff`、`file.list`、`file.read`、`agent.run_status`,以及受策略保护的写/运行工具 `memory.write`、`file.write`、`command.run_limited`、`blackboard.write`、`agent.message` 和 `agent.delegate`;`memory.write` 可追加或覆盖本 Agent 私有记忆、项目长期/短期记忆或黑板,`file.write` 只能写项目内相对路径,`command.run_limited` 只接受 `game.static_smoke` 并复用本地静态自检安全边界,`blackboard.write` 追加共享黑板,`agent.message` 写目标 Agent 对话,`agent.delegate` 把任务投递到目标 Agent 的独立后台队列;策略拒绝时不执行工具并把 `blocked` observation 回给 Agent;策略要求确认时不执行工具,而是持久化精确待确认动作并暂停该 Agent 队列,待开发者确认或拒绝后在同一 run 续跑。每个 Agent 的任务历史落在 `.agent/runtime/tasks/.jsonl`,读 runtime 时按 `runId` 去重返回最近任务,任务视角状态使用 `pending / running / completed / failed`,Runtime state 增加 `nextStep`,UI 在 Runtime 面板和主 Agent 状态卡展示当前任务、动作、下一步与最近任务。不同 Agent 使用独立 `.agent/runtime/locks/.lock`,允许并行运行;同一 Agent 已有运行任务时,新任务会先进入该 Agent 的 pending 队列,当前 drain 持锁完成后串行继续下一条 pending。该能力仍不是独立 OS 进程或跨重启离线常驻 worker。 +- 决策:在现有 `.agent/runtime` 和 `.agent/conversations` 基础上新增单 Agent 后台任务入口。后台任务入口 `start_game_creator_agent_background_task_for_session_at` 立即写入该 Agent 的 runtime state/event/task history,追加用户任务到 `.agent/conversations/agents/.jsonl`,随后在 App 进程内启动 tokio task 执行最小 Agent loop:Agent 按轮输出 `thinkingSummary / plan / actions / response`,Runtime 按白名单和项目权限策略执行工具并记录 `action / observation` 事件,再把已有 observation 放回下一轮 prompt,让 Agent 修正计划、继续行动或用空 actions + response 收束;单 Agent Runtime 每 6 轮形成一个上下文压缩窗口,窗口有新的独立 observation 时压缩上下文并在同一 run 继续,最近 6 轮没有独立进展或相邻窗口重复时以 `failed / budget-exhausted` 和 `loop-budget-exhausted` 终止,不生成总结伪装完成。完成或失败后把 assistant 回复或错误追加回对话,并写入 `.agent/agent.db` 审计记录。工具箱包含只读工具 `memory.read`、`conversation.read`、`asset.list`、`project.index`、`project.diff`、`file.list`、`file.read`、`agent.run_status`,以及受策略保护的写/运行工具 `memory.write`、`file.write`、`command.run_limited`、`blackboard.write`、`agent.message` 和 `agent.delegate`;`memory.write` 可追加或覆盖本 Agent 私有记忆、项目长期/短期记忆或黑板,`file.write` 只能写项目内相对路径,`command.run_limited` 只接受 `game.static_smoke` 并复用本地静态自检安全边界,`blackboard.write` 追加共享黑板,`agent.message` 写目标 Agent 对话,`agent.delegate` 把任务投递到目标 Agent 的独立后台队列;策略拒绝时不执行工具并把 `blocked` observation 回给 Agent;策略要求确认时不执行工具,而是持久化精确待确认动作并暂停该 Agent 队列,待开发者确认或拒绝后在同一 run 续跑。每个 Agent 的任务历史落在 `.agent/runtime/tasks/.jsonl`,读 runtime 时按 `runId` 去重返回最近任务,任务视角状态使用 `pending / running / completed / failed`,Runtime state 增加 `nextStep`,UI 在 Runtime 面板和主 Agent 状态卡展示当前任务、动作、下一步与最近任务。不同 Agent 使用独立 `.agent/runtime/locks/.lock`,允许并行运行;同一 Agent 已有运行任务时,新任务会先进入该 Agent 的 pending 队列,当前 drain 持锁完成后串行继续下一条 pending。该能力仍不是独立 OS 进程或跨重启离线常驻 worker。 - 2026-07-10 补充:后台 Runtime 每次追加 `.agent/runtime/events/.jsonl` 后会通过 Tauri `game-creator-agent-runtime-update` 事件广播当前 `AgentRuntimeResult`;开发单 Agent 聊天页、项目内 Agent 对话弹窗和主窗口 Agent 状态列表都只把该事件作为实时 UI 通知并复用前端 runtime 归一化合并,事实源仍是 `.agent/runtime/agents`、`events` 和 `tasks` 文件。 - 2026-07-11 补充:开发单 Agent 聊天页保留整页纵向滚动,聊天消息区固定响应式高度并在内部滚动;Runtime 恢复确认区使用独立布局行,避免与 Runtime 详情或聊天内容重叠。Runtime 面板详情可折叠且折叠时不渲染详情 DOM,但状态标题与任务控制按钮继续保留;等待 LLM 时在消息区持续显示动态状态和进行中提示,连续流式 delta 合并到动画帧更新并跳过重复 Runtime state。OpenAI Chat SSE 会跳过空 `choices` 心跳 / 元数据事件,收集 usage-only 尾包、保留 finish reason 与上游 error message,收到 `[DONE]` 后立即结束;正文与 finish reason 已接收后出现尾包异常时保存已完成正文,不把整轮改写成失败。持久事件订阅失败时显示非致命错误,聊天事件监听不可用或首个文本片段前流式失败时降级普通回复并继续落盘。 - 2026-07-11 补充:为缩小单 Agent 与 Codex CLI 在代码任务上的差距,Runtime 工具箱新增 `project.search` 和 `file.patch`,并扩展 `file.read` 的按行分页。`project.search` 在项目内执行有界字面量检索,默认忽略大小写,返回相对路径、行号和匹配行,跳过 `.agent`、敏感配置、依赖和构建目录;权限继承 `file.read`。`file.read` 接受 `startLine / maxLines`,返回带行号的最多 240 行、8,000 字符上下文,允许 Agent 继续分页而不是只看到文件开头约 900 字符。`file.patch` 只做 `oldText -> newText` 精确替换,必须声明预期匹配数,匹配数不符时不写入;它继承 `file.write` 权限,复用项目写锁和 Runtime 动作账本,并追加不含代码正文的 `agent.runtime.file.patch` 审计记录。三者组成“搜索定位 -> 分段读取 -> 局部修改 -> 再次读取验证”的最小代码工作闭环,不开放任意 shell。 @@ -2439,7 +2481,7 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在 - 2026-07-12 补充,2026-07-27 更正:OpenAI Chat / Responses 的后台 Agent 工具 planning 改用唯一 `submit_agent_tool_plan` 原生 function tool,字符串 `tool_choice=required` 和 strict schema;只接受恰好一次同名调用,arguments 继续经过本地计划 schema、工具白名单和权限策略校验,错误函数、多调用或非法 arguments 进入原有两次格式修复预算且不产生副作用。本条原写「Anthropic 保留文本 JSON 回退;planning 非流式」,已由 2026-07-27「Anthropic 与流式统一使用 Provider 原生工具」取代——Anthropic 同样发送原生工具目录,planning 不再因协议强制非流式。每轮成功协议写 `agent.runtime.tool_plan.protocol`,修复审计记录 protocol、callId 和 functionName。 - 2026-07-10 补充:后台 Agent Runtime 的白名单工具继续扩到 `preview.start`,让 Agent 在完成写盘或静态自检后能按策略自行启动当前项目的 `127.0.0.1` 本地 HTTP 预览。该工具复用 `preview.start` 权限策略、项目写锁、共享 `PreviewRegistry`、manifest 预览状态、`.agent/logs/preview.log` 和 run trace 追加逻辑;写入 `.agent/agent.db` 的审计类型为 `agent.runtime.preview.start`。发给 LLM 的 observation 只包含 localhost URL 和端口,不包含用户项目绝对路径。 - 2026-07-10 补充:后台 Agent Runtime 的白名单工具继续扩到 `canvas.asset_generate`,让美术类 Agent 可在 loop 中自行请求生成首版美术素材。该工具读取 AppData / Tauri 配置中的 `editorApi`,复用 `canvas.asset_generate` 权限策略、项目写锁、External Editor API 生成和下载链路、manifest 资产登记以及 `canvas.asset_generate` 本地索引记录;另写 `agent.runtime.canvas.asset_generate` 记录到 `.agent/agent.db`,标明触发的 agent 与本地素材路径。API Key 不进入 prompt observation、manifest、agent.db 或日志;策略要求确认或拒绝时不会调用外部 API。 -- 补充:规范 Agent ID 统一使用 manifest taskId,例如 `art-asset-plan` 和 `code-prototype`;历史前端曾使用的 `group-role` 别名只在 Tauri command 层兼容并映射到规范 taskId。主窗口 Agent 状态列表通过 `read_game_creator_agent_runtimes` 批量读取 `.agent/runtime/agents/.json` 和最近任务,把每个 Agent 的 Runtime 状态、当前动作和最近 task 直接显示在状态卡片和 `/agents` 汇总里。 +- 补充:规范 Agent ID 统一使用 manifest taskId,例如 `art-asset-plan` 和 `code-prototype`;历史前端曾使用的 `group-role` 别名只在 Tauri command 层兼容并映射到规范 taskId。主窗口 Agent 状态列表通过 `read_game_creator_agent_runtimes` 批量读取 `.agent/runtime/agents/.json` 和最近任务,把每个 Agent 的 Runtime 状态、当前动作和最近 task 直接显示在状态卡片和 Agent 汇总里。 - 2026-07-10 补充:单 Agent 聊天和后台 planning prompt 统一注入本 Agent 的 Runtime 连续上下文,包括最近状态、runId、当前任务、计划、观察、最近回复、最近工具动作、最近事件、最近任务和工具策略摘要;上下文只按规范 taskId 读取本 Agent runtime,进入 prompt 前过滤密钥和本机绝对路径。新后台 run 启动时继承同 Agent 上次 `recentToolCalls` 和 `lastResponse`,让下一轮任务能基于前一轮真实行动证据继续推理,同时不串入其他 Agent 的 runtime。 - 2026-07-10 补充:后台 Agent Runtime 的白名单工具继续扩到 `file.list`,让 Agent 可先列出项目文件摘要或某个相对目录下的条目,再决定是否读取具体文件或继续行动。该工具复用 `file.list` 项目权限策略,策略要求确认或拒绝时不会枚举项目文件;observation 只包含项目相对路径、类型和大小,不读取文件内容、不返回项目绝对路径。 - 2026-07-10 补充:后台 Agent Runtime 的白名单工具继续扩到 `project.diff`,让 Agent 可基于已存在 checkpoint 观察本地项目新增、修改和删除摘要。该工具复用 `project.diff` 项目权限策略,策略要求确认或拒绝时不会执行 diff;observation 只包含 checkpoint id、三类计数和项目相对路径,不返回本机绝对路径或文件正文。 @@ -2468,18 +2510,18 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在 ## 2026-07-03 AI 游戏创作 App 本地试玩包导出只打包运行白名单 - 背景:AI 游戏创作 App 需要给普通用户提供首版本地试玩包,但不能把项目记忆、trace、日志、运行时配置或密钥类文件混入可分发 ZIP。 -- 决策:v1 新增 `/export` 聊天入口和 `project.export_package` 确认命令。导出前重新校验 `game/index.html` 是可试玩自包含 HTML;ZIP 只包含 `game/**`、`assets/**` 和 `exports/README.md`,输出到 `exports/playtest-package-*.zip`;导出拒绝符号链接和不安全条目路径,并写入 manifest `commandRuns`、`.agent/logs/command.log` 和 `.agent/agent.db`。 -- 补充:新增 `/exports` 只读聊天入口和 `project.export_list` 自动命令,用于列出当前项目 `exports/playtest-package-*.zip` 历史试玩包;该入口只读、不删除旧包、不做系统分享,给用户继续 `/export` 或显示目录的草稿。 -- 影响范围:`apps/ai-game-creator-shell` 的聊天命令、Tauri 本地项目能力、共享命令契约和 AI 游戏创作 App 实施计划。 +- 决策:v1 新增 `project.export_package` 确认命令。导出前重新校验 `game/index.html` 是可试玩自包含 HTML;ZIP 只包含 `game/**`、`assets/**` 和 `exports/README.md`,输出到 `exports/playtest-package-*.zip`;导出拒绝符号链接和不安全条目路径,并写入 manifest `commandRuns`、`.agent/logs/command.log` 和 `.agent/agent.db`。 +- 补充:新增 `project.export_list` 只读自动命令,用于列出当前项目 `exports/playtest-package-*.zip` 历史试玩包;该入口只读、不删除旧包、不做系统分享。 +- 影响范围:`apps/ai-game-creator-shell` 的 Tauri 本地项目能力、共享命令契约和 AI 游戏创作 App 实施计划。 - 验证方式:运行 AI 游戏创作壳主窗口 smoke、Tauri `export` 定向测试、共享契约测试、类型检查、编码检查和 `git diff --check`。 - 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。 ## 2026-07-01 AI 游戏创作 App v1 使用本地 JSONL 对话和派生 Agent 状态 - 背景:AI 游戏创作 App 已有 Godcoder 式本地工程护栏、项目黑板、角色私有记忆、manifest 和 run trace;新增结构化对话记录、agent 状态列表和单 agent 对话入口时,需要避免引入平行状态源或提前承诺后台 runner 能力。 -- 决策:v1 结构化对话记录统一使用本地 `.agent/conversations/` append-only JSONL。普通聊天写 `.agent/conversations/project.jsonl`;从 agent 状态列表进入单个 agent 后,用户消息、agent 回复、工具建议和错误只写对应 `.agent/conversations/agents/.jsonl`。Agent 状态列表从 `.agent/manifest.json` 的任务 / 角色清单和 `.agent/run.latest.json` / `.agent/runs/.json` 的 step、taskGraph、passPlans、lifecycleStatus 派生,并把 `taskGraph.tasks` 的任务状态与 active / carry-over / ready 编排标记显示在主窗口和单 agent 对话入口中;单 agent 最近证据里的安全相对输入 / 输出路径只填入 `/read ` 草稿,仍由用户发送并走既有 `file.read` / `agent.trace_read` 权限流。不新增独立状态数据库。项目黑板和角色私有记忆继续只保存稳定摘要,不承载原始对话流水。 +- 决策:v1 结构化对话记录统一使用本地 `.agent/conversations/` append-only JSONL。普通聊天写 `.agent/conversations/project.jsonl`;从 agent 状态列表进入单个 agent 后,用户消息、agent 回复、工具建议和错误只写对应 `.agent/conversations/agents/.jsonl`。Agent 状态列表从 `.agent/manifest.json` 的任务 / 角色清单和 `.agent/run.latest.json` / `.agent/runs/.json` 的 step、taskGraph、passPlans、lifecycleStatus 派生,并把 `taskGraph.tasks` 的任务状态与 active / carry-over / ready 编排标记显示在主窗口和单 agent 对话入口中;单 agent 最近证据里的安全相对输入 / 输出路径仍由既有 `file.read` / `agent.trace_read` 权限流处理。不新增独立状态数据库。项目黑板和角色私有记忆继续只保存稳定摘要,不承载原始对话流水。 - 补充:2026-07-08 起普通用户入口改为单窗口客户端首页;旧独立启动器 / 主窗口切换口径废止。首页发送需求或项目组新建项目时,先选择目录并在非空目录时二次确认,初始化成功后写最近项目并切到项目开发占位;取消或初始化失败则不切换视图、不写最近项目。最近工作区只保存在本机 WebView storage,可单项移除或清空,不进入项目文件或共享记忆;已初始化项目优先显示 manifest 项目名并保留路径副信息,`.agent/run.latest.json` 可读时显示最近 run 状态。最近项目路径缺失、不是目录、缺少可读 `.agent/manifest.json` 或检查失败时禁用打开,刷新只重新执行只读检查;“显示”只用系统文件管理器打开已确认存在的本地目录,未初始化但存在的目录也可显示,避免把历史路径误当新项目重建。 -- 补充:项目开发占位“显示目录”复用同一只读目录打开能力,只打开当前本地项目目录,不初始化项目、不写项目文件、不切换工作区;顶部只读显示 manifest 项目名、项目路径、最近 `.agent/run.latest.json` 的 run 状态摘要和当前预览状态,并通过“刷新状态”重新读取同一 trace,不新增状态数据库。最近项目资产入口只读展示 localPath、kind、mediaType 和 source.kind,点击仍走原 `file.read` 权限流;旁边的“读取命令”只填入 `/read ` 草稿,不直接读取文件或绕过权限。项目开发占位里的项目黑板和 Agent 状态快捷入口仍只填入聊天草稿,不直接读取 run 辅助文件、不写 `.agent/policy.json`、不调用 LLM。 +- 补充:项目开发占位“显示目录”复用同一只读目录打开能力,只打开当前本地项目目录,不初始化项目、不写项目文件、不切换工作区;顶部只读显示 manifest 项目名、项目路径、最近 `.agent/run.latest.json` 的 run 状态摘要和当前预览状态,并通过“刷新状态”重新读取同一 trace,不新增状态数据库。最近项目资产入口只读展示 localPath、kind、mediaType 和 source.kind,读取仍走原 `file.read` 权限流,不直接读取文件或绕过权限。 - 补充:首页、项目组和项目开发占位共用同一个运行时配置弹窗,配置只读写 Tauri 应用配置目录中的 `game-creator.config.json`,不写入项目文件或对话历史。 - 补充:项目组“打开”只进入已初始化且 `.agent/manifest.json` 可读的 AI 游戏项目;路径不存在、不是文件夹或只是普通文件夹时不切换到项目开发占位、不创建目录,用户需要创建或初始化时走“新建项目”。 - 影响范围:`apps/ai-game-creator-shell` 的主窗口 agent 状态列表、单 agent 对话入口、本地项目文件结构、共享契约和 AI 游戏创作 App 实施计划。 @@ -2489,7 +2531,7 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在 ## 2026-06-30 AI 游戏创作 App 使用客户端配置文件 - 背景:`apps/ai-game-creator-shell` 是客户端 App,不应通过 `.env` 或进程环境变量承载 LLM / 画板同步配置;旧口径会让本地 secrets、CLI wrapper 和桌面 App 启动逻辑混在一起。 -- 决策:仓库内 `apps/ai-game-creator-shell/game-creator.config.json` 只作为默认模板;发布 App 启动时在 Tauri 应用配置目录写入默认 `game-creator.config.json`,真实密钥和本机覆盖项都保存在该运行时配置文件中。主窗口提供“配置”面板读写该运行时 JSON;开发 CLI 无 AppHandle 时才回退读取仓库旁边的模板和 gitignored 本机覆盖文件。`llm.apiKey/baseUrl/model/apiKind/stream/requestTimeoutMs/maxRetries/retryBackoffMs` 驱动全局 LLM 路径,`agentLlm.` 可为 Planner、Generator 和角色 agent 单独覆盖 API Key、base URL、模型、API 类型和流式请求,空项继承全局配置;`editorApi.baseUrl/apiKey` 驱动画板项目同步;`/llm-status` 只展示全局和各 agent resolved 后的 baseUrl、model、apiKind、stream 和 API Key 是否存在,不显示密钥;`/llm-routes` 复用同一只读检查结果,按 agent 展示 resolved provider 路由、单独路由数量和缺口数量,不请求上游、不显示密钥、不写项目。生成游戏或平台美术遇到 LLM / editorApi 缺配置错误时,主窗口自动打开运行时配置弹窗,但错误消息仍只显示缺失项,不回显密钥值。 +- 决策:仓库内 `apps/ai-game-creator-shell/game-creator.config.json` 只作为默认模板;发布 App 启动时在 Tauri 应用配置目录写入默认 `game-creator.config.json`,真实密钥和本机覆盖项都保存在该运行时配置文件中。主窗口提供“配置”面板读写该运行时 JSON;开发 CLI 无 AppHandle 时才回退读取仓库旁边的模板和 gitignored 本机覆盖文件。`llm.apiKey/baseUrl/model/apiKind/stream/requestTimeoutMs/maxRetries/retryBackoffMs` 驱动全局 LLM 路径,`agentLlm.` 可为 Planner、Generator 和角色 agent 单独覆盖 API Key、base URL、模型、API 类型和流式请求,空项继承全局配置;`editorApi.baseUrl/apiKey` 驱动画板项目同步;主窗口运行时配置面板与生成入口只展示全局和各 agent resolved 后的 baseUrl、model、apiKind、stream 和 API Key 是否存在,不显示密钥、不请求上游、不写项目。生成游戏或平台美术遇到 LLM / editorApi 缺配置错误时,主窗口自动打开运行时配置弹窗,但错误消息仍只显示缺失项,不回显密钥值。 - 影响范围:AI 游戏创作 App 的 Tauri Rust 配置加载、主窗口配置面板、CLI wrapper、agent-run smoke、`check-config` 门禁、`.gitignore` 和实施计划文档。 - 验证方式:运行 `npm run ai-game-creator-shell:typecheck`、`cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml`、`npm run check:encoding` 和 `git diff --check`。 - 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。 @@ -2601,16 +2643,16 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在 ## 2026-06-26 AI 游戏创作 App 生成过程必须在聊天可见 - 背景:普通用户窗口只保留聊天入口,但如果生成确认后只显示“已生成草案”和本地产物路径,真实 LLM / Agent loop 会被误解成固定模板落盘。 -- 决策:`game.generate_draft` 保持正式用户窗口不展示开发面板,但必须通过聊天实时显示 Planner LLM、Orchestrator、6 组角色 brief、Generator LLM、Evaluator、ArtifactWriter 和自检进度;生成完成后普通聊天消息直接展示 `.agent/run.latest.json` 的 Run、LLM 对话、loop 轮次、active / carry-over 任务、编排轮次、最近步骤、建议命令和本地产物快照;没有同步建议命令时,首个安全产物只提供 `/read` 草稿,`/trace` 继续读取同一份完整证据。 +- 决策:`game.generate_draft` 保持正式用户窗口不展示开发面板,但必须通过聊天实时显示 Planner LLM、Orchestrator、6 组角色 brief、Generator LLM、Evaluator、ArtifactWriter 和自检进度;生成完成后普通聊天消息直接展示 `.agent/run.latest.json` 的 Run、LLM 对话、loop 轮次、active / carry-over 任务、编排轮次、最近步骤、建议命令和本地产物快照;产物证据仍从同一份 run trace 读取。 - 影响范围:`apps/ai-game-creator-shell/src/App.tsx`、`apps/ai-game-creator-shell/src-tauri/src/main.rs`、AI 游戏创作 App 聊天体验和实施计划文档。 - 验证方式:运行 `npm run ai-game-creator-shell:check`、`npm run check:encoding` 和 `git diff --check`。 - 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。 ## 2026-06-26 AI 游戏创作 App 增加显式质量评审 Gate -- 背景:AI 游戏创作 App 已有 Evaluator loop 和静态 smoke,但任务图、能力清单和 trace 中没有单独的质检 / 评审任务,用户无法从 `/tasks`、`/trace` 或 `/audit` 看出质量评审是明确环节。 +- 背景:AI 游戏创作 App 已有 Evaluator loop 和静态 smoke,但任务图、能力清单和 trace 中没有单独的质检 / 评审任务,用户无法从任务图、run trace 或审计摘要看出质量评审是明确环节。 - 决策:保持策划、美术、程序、数值、音乐、运营 6 个专业组不变,在程序组内新增 `quality-review` / `Review` 角色任务;Evaluator 的评审 step 绑定到该任务,依赖顺序为 `code-prototype -> quality-review -> preview-readiness -> preview-playtest -> publish-strategy -> publish-package`。`game.static_smoke` 只完成 `preview-readiness`,不代替质量评审。 -- 影响范围:AI 游戏创作 App 任务图、共享契约、Tauri trace / manifest 状态推导、聊天 `/capabilities` `/tasks` `/trace` `/audit` 摘要和实施计划文档。 +- 影响范围:AI 游戏创作 App 任务图、共享契约、Tauri trace / manifest 状态推导、任务图 / trace 摘要和实施计划文档。 - 验证方式:运行 `npm run ai-game-creator-shell:check`、`npm run check:encoding` 和 `git diff --check`。 - 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。 @@ -2628,7 +2670,7 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在 - 背景:AI 游戏创作 App 的 `game.generate_draft` 已接入 LLM,但单次请求仍不能体现 Planner / Generator / Evaluator 的协作闭环,也无法把评估反馈作为下一轮生成输入。 - 决策:v1 使用最小文件驱动 loop,不引入 LangChain、AutoGen、Microsoft Agent Framework 或 OpenAI Agents SDK sidecar。Planner 写 `.agent/spec.md`;每轮先调用策划、数值、美术、音乐、程序、运营 6 组下的 15 个角色 agent,角色 brief 写到 `.agent/passes/pass-N/groups//*.md`,再由 `GroupCoordinator` 汇总到 `.agent/passes/pass-N/groups/*.md`;Generator 读取 spec、`.agent/findings.md` 和 6 组汇总 brief 生成结构化游戏草案。LLM JSON 必须带 `handoffs` 数组并覆盖 `design`、`balance`、`art`、`audio`、`code`、`publishing` 6 个专业组;每轮再把这些结构化交接快照写到 `.agent/passes/pass-N/`。Evaluator 做本地静态验收并写 `.agent/findings.md`,最多 3 轮;返工轮必须把 findings 转成结构化 `repairRoutes`,记录每条问题命中的 taskIds 和 reason,再据此选择 activeTaskIds。每次运行另写 `.agent/run.latest.json` 和 `.agent/runs/.json`,记录 step、角色级 `toolCalls`、组汇总、专业组交接、输入输出路径、artifact 字节数与 `fnv1a64:` checksum;每个 step 带 phase、taskId、group 和 role,trace 顶层 `taskGraph` 记录 goal、readyTaskIds、activeTaskIds、carriedTaskIds、repairFocus、repairRoutes 和当前任务状态,`passPlans` 逐轮记录 mode、summary、activeTaskIds、carriedTaskIds、dependencyWaves、repairFocus 和 repairRoutes;latest 是当前指针,runs 目录保留历史 trace,作为开发窗口和后续工具调用 trace 的事实源,schema 由共享 TS/Rust 契约 `game-creator-agent-run.v1` 固定。最终产物写盘时追加 `ArtifactWriter / file.write.local_artifacts` step,随后自动跑白名单 `game.static_smoke`,检查 `game/index.html` 具备 canvas、canvas 渲染上下文、绘制调用、主循环、输入监听、明确目标、失败或胜利状态和重开路径,且不使用远程资源、`eval`、`new Function`、`localStorage`、`fetch`、`WebSocket` 或 `ServiceWorker`,再把 Playtest 工具调用写回 trace;通过后把 runId、状态、轮次、下一步、active / carry-over 任务和最终本地产物摘要追加到 `memory/session.md` 与 `memory/project.md`,让下一次 Planner / 角色 agent / Generator 从记忆输入直接看到上一轮稳定原型;后续 `preview.start` 会在已有 trace 上追加 Preview 工具调用和本地预览 URL。 -- 决策补充:普通用户聊天 `/trace` 读取同一份 `.agent/run.latest.json`,但摘要必须把 activeTaskIds、carriedTaskIds、repairRoutes 和 dependencyWaves 从内部 taskId 映射成专业组 / 角色 / 任务名,确保不打开开发窗口也能看出 6 组 agent、组内角色、返工路线和 carry-over 真实发生。 +- 决策补充:run trace 摘要读取同一份 `.agent/run.latest.json`,但摘要必须把 activeTaskIds、carriedTaskIds、repairRoutes 和 dependencyWaves 从内部 taskId 映射成专业组 / 角色 / 任务名,确保不打开开发窗口也能看出 6 组 agent、组内角色、返工路线和 carry-over 真实发生。 - 影响范围:`apps/ai-game-creator-shell/src-tauri/src/main.rs`、`packages/shared/src/contracts/gameCreationApp.ts`、`server-rs/crates/shared-contracts/src/game_creation_app.rs` 和 AI 游戏创作智能体 App 实施计划。 - 验证方式:运行 AI 游戏创作壳 Rust 测试、共享契约 TS/Rust 测试、壳 typecheck、编码检查和 `git diff --check`。 - 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。 @@ -2636,7 +2678,7 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在 ## 2026-06-24 AI 游戏创作 App 编排 v1 使用 ready-task 选择器 - 背景:AI 游戏创作 App 已有专业组任务拆分和依赖字段,但如果没有当前可执行任务选择器,“任务编排”只停留在静态清单,普通用户在聊天里也看不到下一步由哪组 agent 接手。 -- 决策:v1 编排先使用最小 ready-task 规则:只选择 `pending` 且所有依赖任务均为 `completed` 的任务;共享 TS/Rust 契约和 `platform-agent` 都提供同一语义的选择器,聊天 `/tasks` 只展示下一步可执行专业组,不新增独立编排面板或外部 agent 框架。 +- 决策:v1 编排先使用最小 ready-task 规则:只选择 `pending` 且所有依赖任务均为 `completed` 的任务;共享 TS/Rust 契约和 `platform-agent` 都提供同一语义的选择器,任务摘要只展示下一步可执行专业组,不新增独立编排面板或外部 agent 框架。 - 影响范围:`packages/shared/src/contracts/gameCreationApp.ts`、`server-rs/crates/shared-contracts/src/game_creation_app.rs`、`server-rs/crates/platform-agent/src/game_creation.rs`、`apps/ai-game-creator-shell/src/App.tsx` 和 AI 游戏创作智能体 App 实施计划。 - 验证方式:运行共享契约测试、`platform-agent` 与 `shared-contracts` 的 Rust 测试、AI 游戏创作壳 typecheck、`npm run check:encoding` 和 `git diff --check`。 - 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。 @@ -6254,88 +6296,19 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在 - 2026-06-25 调整:新增 `npm run ai-game-creator-shell:agent-run:smoke` 作为无密钥开发验证入口。脚本在本机启动 OpenAI-compatible 测试 provider,预置一个本地上传图片和一个本地上传音频,并复用真实 `--agent-run`、本地落盘、`game.static_smoke` 和本地 HTTP 预览;脚本会断言 provider 请求体包含图片与音频资产上下文、生成 HTML 引用 `/assets/...`、预览服务能用 `GET` 读取这些资产、用 `HEAD` 返回真实资源长度和对应 MIME、headless Chrome 打开预览后至少执行一帧游戏 JS,且通过确定性亮色探针采样证明 canvas 不是空白画布、第二轮重跑 Evaluator 命中任务及其下游影响任务,未受影响组 carry-over,再自动给 CLI 发送回车停止预览。该脚本仅验证 runtime,不作为产品生成 fallback。 - 2026-06-25 调整:新增根级 `npm run ai-game-creator-shell:check` 作为 v1 开发验收入口,串起壳 typecheck、`platform-agent` 编排测试、`shared-contracts` 契约测试、Tauri Rust 测试和无密钥本地 provider 端到端 smoke,避免测试口径散落成多条手工命令。 - 2026-06-25 调整:`scripts/check-native-shells.mjs` 的 AI 游戏创作项从单独 typecheck 升级为 `npm run ai-game-creator-shell:check`,让原生壳总门禁覆盖 agent loop、本地落盘、静态自检和本地 HTTP 预览 smoke。 -- 2026-06-25 调整,2026-06-30 更新:普通用户通过聊天输入 `/llm-status` 触发只读 `llm.config_check`,用于检查 LLM base_url、model 和 API Key 是否已从客户端配置读取;状态消息不得显示或保存 API Key。终端可用 `npm run ai-game-creator-shell:llm-status` 做同类配置自检,缺配置时以非零状态退出。发布 App 的真实密钥只放 Tauri 应用配置目录中的 `game-creator.config.json`;主窗口“配置”面板可读写该文件,但 API Key 不写入聊天、本地项目、trace 或 manifest。 - 2026-06-25 调整:`npm run ai-game-creator-shell:dev` 固定加载 `http://127.0.0.1:3080/`,Vite 继续 `strictPort` 与 Tauri `devUrl` 对齐。`beforeDevCommand` 改为先复用已经跑在 3080 且页面标题为 `AI 游戏创作` 的本 app Vite server,避免上次 Tauri 退出后遗留的同 app Vite 进程导致二次启动失败;如果 3080 是其它服务,仍直接失败并要求释放端口,不做端口漂移。 - 2026-06-25 调整:`preview.start` / `preview.stop` 必须追加 `.agent/logs/preview.log`,并把该日志列入 Preview trace step 的输出路径和 artifact 清单;这样 `preview-playtest` 任务声明的日志产物与实际本地 HTTP 预览行为一致。 - 2026-06-25 调整:AI 游戏创作 App v1 仍只维护一个全局本地 HTTP 预览实例;启动新项目预览替换旧预览时,必须 best-effort 把旧项目的 manifest preview 状态、`.agent/logs/preview.log` 和 run trace 记录为 stopped,避免旧项目状态残留 `running`。旧项目目录已删除时不阻断新预览启动。 -- 2026-06-25 调整,2026-07-18 替代:正式用户 App 的项目运行工作台承载当前授权项目的本地游戏预览,release / dev CSP 都只允许 `frame-src http://127.0.0.1:*`;`/preview`、`/run` 和生成完成后的用户侧路径启动 `127.0.0.1` HTTP preview 后直接切换客户端运行视图,不再调用系统外部浏览器。 -- 2026-06-25 调整:`project.create` 成功后的 durable 权限证据必须在聊天 `/project` 和开发窗口初始化两条入口统一写入 `.agent/logs/command.log`,避免同一能力因为入口不同导致 `/audit` 或开发排障证据不一致。 +- 2026-06-25 调整,2026-07-18 替代:正式用户 App 的项目运行工作台承载当前授权项目的本地游戏预览,release / dev CSP 都只允许 `frame-src http://127.0.0.1:*`;生成完成后的用户侧路径启动 `127.0.0.1` HTTP preview 后直接切换客户端运行视图,不再调用系统外部浏览器。 +- 2026-06-25 调整:`project.create` 成功后的 durable 权限证据必须在开发窗口初始化入口写入 `.agent/logs/command.log`,避免同一能力因为入口不同导致审计或开发排障证据不一致。 - 2026-06-25 调整:`.agent/run.latest.json` 和 `.agent/runs/.json` 必须记录 loop 的 `maxPasses` 与 `stopReason`,开发窗口直接展示该状态,避免只从 summary 文案推断 loop 是否跑满、通过、返工、写入产物或进入预览。本地 HTTP 预览的 `/` 映射到 `game/index.html`,路径解析必须 canonicalize 项目根目录和目标文件,只允许访问项目内 `game/` 与 `assets/`,拒绝 `memory/`、`.agent/`、`exports/`、`..`、反斜杠和符号链接越界;常见图片、音频、视频和 Web 资源必须返回对应 MIME。这样上传和画板回流资产能被生成游戏引用,但记忆、trace 和导出包不会被预览服务暴露。 -- 2026-06-26 调整,2026-07-03 更新:AI 游戏创作 App 借鉴 Harbour 的控制平面思想,但不搬 Harbour 后台。最近 run 在 `.agent/run.latest.json` 增加可选 `lifecycleStatus`,并通过 `/agent-status`、`/agent-kill`、`/agent-retry`、`/agent-resume [说明]` 控制本地生命周期,写入 `.agent/activity.jsonl`、`.agent/output.jsonl` 和 `.agent/context.bundle.json`;聊天里的状态 / 控制结果可填入 `/read .agent/output.jsonl` 草稿继续查看 run 输出,但不直接读取文件或绕过 `file.read` 策略。v1 的 kill/retry/resume 只更新本地状态和上下文包,不伪装成能中断已发出的上游 LLM 请求;后续引入独立 runner 后再把 `pending` 接入 claim。 -- 2026-07-03 调整:主窗口 Agent 状态栏新增“继续说明”,只把 `/agent-resume ` 填入聊天输入框,让用户补充说明后再走原确认流;策略快捷入口新增 project.index、asset.register、memory.write、preview.open、preview.stop、conversation.read 和 conversation.write 确认草稿,同样只填输入框,不直接写 `.agent/policy.json`。 +- 2026-06-26 调整,2026-07-03 更新:AI 游戏创作 App 借鉴 Harbour 的控制平面思想,但不搬 Harbour 后台。最近 run 在 `.agent/run.latest.json` 增加可选 `lifecycleStatus`,并通过 Runtime 状态控制动作维护本地生命周期,写入 `.agent/activity.jsonl`、`.agent/output.jsonl` 和 `.agent/context.bundle.json`。v1 的 kill/retry/resume 只更新本地状态和上下文包,不伪装成能中断已发出的上游 LLM 请求;后续引入独立 runner 后再把 `pending` 接入 claim。 - 2026-07-03 调整:主窗口 header 常驻项目摘要只从当前已加载的 manifest / trace 派生任务完成数、ready 数、资产来源分布和最近命令结果;未选择工作区时不显示,不为了摘要额外触发 Tauri 读取或写入,也不把任务、文件、run history 或预览开发面板搬进普通用户窗口。 -- 2026-07-03 调整:普通用户通过聊天输入 `/brief` 触发项目简报入口,只基于主窗口当前已加载的 manifest、最近 run trace、预览状态、资产数量和最近命令生成聊天内简报,并提供 `/next` 作为后续草稿;该入口不得触发 Tauri 读写、不得读取文件、不得启动或打开预览,也不得新增普通用户面板。 -- 2026-07-03 调整:普通用户通过聊天输入 `/goal` 查看创作目标,只基于当前 manifest.goal、最近 run goal 和 taskGraph.goal 汇总项目目标来源,并提供 `/agent-resume 细化目标:` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取 spec、上下文或 trace 文件,也不得新增普通用户目标面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/guide` 查看操作导引,只基于当前 manifest、最近 run trace、preview 和已加载命令状态判断未开始、需修复、可预览、可导出或已导出阶段,给出最多 3 个推荐命令和首选草稿;该入口不得触发 Tauri 读写、不得读取文件、不得启动 run、不得启动预览、不得写项目,也不得新增普通用户导引面板。`/guide` 只回答“下一步怎么操作”,不承接 `/brief` 的项目快照、`/mvp` 的最小范围或 `/plan` 的分工计划。 -- 2026-07-04 调整:普通用户通过聊天输入 `/progress` 查看项目进度,只基于当前 manifest、最近 run trace、preview、任务、素材和已加载命令状态汇总项目阶段、任务完成度、最近 run、预览、素材和交付进度,并提供 `/run`、`/review`、`/share`、`/test-plan`、`/todo`、`/trace` 或 `/guide` 草稿;该入口不得触发 Tauri 读写、不得读取文件、不得启动 run、不得启动预览、不得导出试玩包、不得写项目,也不得新增普通用户进度面板。`/progress` 只回答“当前走到哪了”,不承接 `/status` 的项目状态详情、`/ready` 的试玩门槛判断、`/groups` 的逐组进度或 `/next` 的长命令目录。 -- 2026-07-04 调整:普通用户通过聊天输入 `/spec` 查看创作规格包,只基于当前 manifest、最近 run trace、任务声明产物和 trace 输入 / 输出路径汇总 Planner 规格、玩法设计、数值表、美术清单、音频清单和发布说明状态,并提供 `/read .agent/spec.md` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取规格文件、不得启动预览、不得写项目,也不得新增普通用户规格面板。 -- 2026-07-03 调整:普通用户通过聊天输入 `/mvp` 查看本轮最小可玩范围,只基于当前 manifest、最近 run trace、preview、任务、资产和最近命令汇总 MVP 内、当前状态、试玩包状态和暂不做事项,并提供 `/review`、`/criteria`、`/trace`、`/run`、`/export`、`/exports` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取文件、不得启动或打开预览、不得导出试玩包,也不得新增普通用户 MVP 面板。 -- 2026-07-03 调整:普通用户通过聊天输入 `/pitch` 查看试玩定位与卖点,只基于当前 manifest、最近 run trace 和 preview 状态汇总试玩定位、一句话、核心乐趣、当前可演示状态、测试者讲解口径和暂不承诺事项,并提供 `/mvp`、`/review`、`/trace`、`/open-preview` 或 `/run` 草稿;该入口不得触发 Tauri 读写、不得读取文件、不得启动或打开预览、不得直接继续 run,也不得新增普通用户定位面板。该入口服务试玩讲解,不承接 `/listing` 的作品页包装。 -- 2026-07-04 调整:普通用户通过聊天输入 `/demo` 准备 30 秒试玩讲解稿,只基于当前 manifest、最近 run trace 和 preview 状态汇总开场、讲解顺序、口播稿、演示状态、最近试玩证据和收反馈口径,并提供 `/run`、`/open-preview`、`/trace`、`/review` 或 `/test-plan` 草稿;该入口不得触发 Tauri 读写、不得读取文件、不得启动或打开预览、不得导出试玩包、不得发布作品,也不得新增普通用户讲解面板。 -- 2026-07-03 调整:普通用户通过聊天输入 `/rules` 查看玩法操作与规则,只基于当前 manifest、最近 run trace.taskGraph、trace artifacts 和 steps 汇总玩法目标、操作 / 胜负 / 重开口径、设计与入口产物状态、相关任务和最近程序 / 试玩步骤,并提供 `/read game/game_design.md`、`/agent-resume 操作说明:...` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取设计文件、不得启动预览或继续 run,也不得新增普通用户规则面板。 -- 2026-07-03 调整:普通用户通过聊天输入 `/tutorial` 查看新手引导检查,只基于当前 manifest、最近 run trace、preview 和任务状态汇总首屏目标、首局 30 秒引导、原型证据、试玩任务、最近引导证据和补齐项,并提供 `/rules`、`/review`、`/agent-resume 新手引导:...`、`/open-preview` 或 `/run` 草稿;该入口不得触发 Tauri 读写、不得读取设计文件、不得启动或打开预览、不得直接继续 run,也不得新增普通用户引导面板。 -- 2026-07-03 调整:普通用户通过聊天输入 `/mobile` 查看移动试玩检查,只基于当前 manifest、最近 run trace、preview 和任务状态汇总移动试玩目标、键盘 / 触屏输入口径、原型证据、移动检查项、关联任务和最近移动相关步骤,并提供 `/rules`、`/review`、`/agent-resume 移动试玩:...`、`/open-preview` 或 `/run` 草稿;该入口不得触发 Tauri 读写、不得读取代码文件、不得启动或打开预览、不得直接继续 run,也不得新增普通用户移动适配面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/compatibility` 准备兼容性说明,只基于当前 manifest、最近 run trace、preview 和静态自检状态汇总推荐环境、输入兼容、不承诺范围、反馈口径和参考命令,并提供 `/run`、`/mobile`、`/review`、`/trace` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取文件、不得启动或打开预览、不得导出试玩包、不得上传云端、不得发布作品、不得写项目,也不得新增普通用户兼容性面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/accessibility` 查看可读性与无障碍检查,只基于当前 manifest、最近 run trace、preview 和任务状态汇总文字可读、颜色对比、按钮 / 状态命名、键盘等价、可见焦点、非颜色唯一反馈和静音可玩检查,并提供 `/rules`、`/review`、`/agent-resume 可读性与无障碍:...`、`/open-preview` 或 `/run` 草稿;该入口不得触发 Tauri 读写、不得读取代码或 trace 文件、不得启动或打开预览、不得直接继续 run,也不得新增普通用户无障碍面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/localization` 查看本地化与文案检查,只基于当前 manifest、最近 run trace、preview 和发布说明产物状态汇总默认语言、文案范围、关联任务、检查口径、暂不做事项和参考命令,并提供 `/read exports/README.md`、`/agent-resume 本地化与文案:...`、`/review` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取文件、不得启动或打开预览、不得导出试玩包、不得上传云端、不得发布作品、不得写项目,也不得新增普通用户本地化面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/performance` 查看性能与加载检查,只基于当前 manifest、最近 run trace、preview、资产数量和 trace artifact 摘要汇总入口自包含、首屏不空白、素材体积、主循环稳定、无远程依赖和预览启动检查,并提供 `/run-artifacts`、`/review`、`/open-preview`、`/run` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取产物或日志文件、不得启动或打开预览、不得直接继续 run,也不得新增普通用户性能面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/polish` 查看试玩前打磨清单,只基于当前 manifest、最近 run trace、preview、最近自检和资产数量汇总试玩前打磨范围、推荐检查顺序、关联任务和最近打磨相关步骤,并提供 `/agent-resume 打磨:...`、`/review`、`/feedback` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取文件、不得启动预览、不得导出试玩包、不得写项目,也不得新增普通用户打磨面板。 -- 2026-07-03 调整:普通用户通过聊天输入 `/credits` 查看素材署名与来源,只基于当前 manifest.assets 汇总素材数量、上传 / 生成 / 画板来源分布、来源清单和交付前需要确认的授权 / 模型 / 画板资源口径,并提供 `/assets` 草稿;该入口不得触发 Tauri 读写、不得刷新资产、不得读取素材清单、不得导出试玩包,也不得新增普通用户署名面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/blockers` 查看当前阻塞项,只基于当前 manifest、最近 run trace、preview、最近命令、ready / failed 任务、导出记录和资产概况汇总当前阻塞项,并提供 `/run`、`/export`、`/todo`、`/review`、`/trace`、`/tasks`、`/logs`、`/art` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取文件、不得启动预览、不得导出试玩包、不得写项目,也不得新增普通用户阻塞面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/ready` 查看试玩就绪度,只基于当前 manifest、最近 run trace、preview、最近自检、导出记录、ready / failed 任务和资产概况汇总可交付判断,并提供 `/run`、`/export`、`/todo`、`/review`、`/trace`、`/tasks`、`/art`、`/share` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取文件、不得启动预览、不得导出试玩包、不得写项目,也不得新增普通用户就绪度面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/evidence` 查看当前验证证据台账,只基于当前 manifest、最近 run trace、preview、最近命令、静态自检、导出记录、素材和最近试玩步骤汇总已有验证证据与缺口,并提供 `/run`、`/export`、`/art`、`/logs`、`/review`、`/next` 或 `/ready` 草稿;该入口不得触发 Tauri 读写、不得读取文件、不得启动或打开预览、不得导出试玩包、不得写项目,也不得新增普通用户证据面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/deps` 查看任务依赖链,只基于当前 manifest.tasks 和最近 run trace.taskGraph 汇总 active / carry / ready / 等待依赖、可执行任务与等待依赖,并提供 `/criteria`、`/todo`、`/tasks` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取任务文件、不得启动 run、不得修改项目,也不得新增普通用户依赖面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/revise` 准备下一轮改版说明草稿,只基于当前 manifest 和最近 run trace 汇总返工焦点、失败 / active / carry / ready 任务、最近评审 / 试玩步骤、预览和导出缺口,并填入 `/agent-resume 改版说明:...` 草稿;该入口不得触发 Tauri 读写、不得读取文件、不得继续 run、不得启动预览、不得导出试玩包、不得写项目,也不得新增普通用户改版面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/privacy` 查看隐私与导出边界,只基于当前 manifest、授权项目路径、最近 run trace、preview、资产来源和导出记录汇总 API Key、预览、本地试玩包、内部文件、素材来源和 trace 的隐私 / 交付边界,并提供 `/credits`、`/exports` 或 `/config` 草稿;该入口不得触发 Tauri 读写、不得读取文件、不得导出试玩包、不得启动预览、不得写项目,也不得新增普通用户隐私面板。 -- 2026-07-03 调整:普通用户通过聊天输入 `/risks` 查看当前项目风险,只基于主窗口当前已加载的 manifest、最近 run trace、预览状态、任务状态、资产来源和最近命令派生风险摘要,并提供首个风险处理草稿;该入口不得触发 Tauri 读写、不得读取文件、不得启动或打开预览,也不得新增普通用户面板。 -- 2026-07-03 调整:普通用户通过聊天输入 `/criteria` 查看当前任务验收标准,只基于当前 manifest.tasks 和最近 run trace.taskGraph 汇总 active、carry、ready、失败或待处理任务的验收条件和产物,并提供 `/tasks` 草稿;该入口不得触发 Tauri 读写、不得读取任务文件或 trace 文件,也不得新增普通用户验收面板。 -- 2026-07-03 调整:普通用户通过聊天输入 `/groups` 查看专业组进度,只基于当前 manifest.tasks 和最近 run trace.taskGraph / passPlans 汇总六个专业组的完成、active、carry、ready、失败数量和下一步任务,并提供 `/tasks` 草稿;该入口不得触发 Tauri 读写、不得读取任务文件或 trace 文件,也不得新增普通用户专业组面板。 -- 2026-07-03 调整:普通用户通过聊天输入 `/budget` 查看最近 run 预算,只基于当前最近 run trace 汇总轮次、工具调用、stopReason 和下一步建议,并提供 `/review`、`/publish`、`/trace` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取 trace 文件,也不得新增普通用户预算面板。 -- 2026-07-03 调整:普通用户通过聊天输入 `/qa` 查看质量检查清单,只基于当前 manifest、最近 run trace、最近命令和 preview 状态汇总 Evaluator、任务、静态自检、试玩和产物状态,并提供 `/review`、`/tasks`、`/trace`、`/playtest`、`/publish` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取 trace 或日志文件、不得启动或打开预览,也不得新增普通用户 QA 面板。 -- 2026-07-03 调整:普通用户通过聊天输入 `/changes` 查看最近生成变更,只基于当前 manifest、最近 run trace 的 artifacts / steps 和最近命令汇总可验产物、最近输出、当前资产和真实差异查看方向,并提供 `/read <首个可验产物>` 或 `/run-artifacts` 草稿;该入口不得触发 Tauri 读写、不得读取产物或日志文件、不得执行 checkpoint diff,也不得新增普通用户变更面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/todo` 查看下一轮小步清单,只基于当前 manifest 和最近 run trace 汇总失败、active、carry、ready 或待处理任务,并提供 `/tasks`、`/review` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取任务文件、不得启动 run、不得修改项目,也不得新增普通用户小步面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/plan` 查看下一轮分工计划,只基于当前 manifest 和最近 run trace 汇总协作顺序、各专业组接手任务、空档组和首个继续执行草稿,并提供 `/agent-resume 下一轮计划:...`、`/review` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取任务文件、不得启动 run、不得修改项目,也不得新增普通用户计划面板。 -- 2026-07-03 调整:普通用户通过聊天输入 `/review` 查看 Evaluator 评审状态,只基于主窗口当前已加载的最近 run trace 派生通过 / 需返工状态、返工焦点、返工路线和最近评审步骤,并提供 `/read .agent/findings.md` 或 `/agent-resume ` 草稿;该入口不得直接读取评审文件、不得触发 Tauri 读写,也不得新增普通用户评审面板。 -- 2026-07-03 调整:普通用户通过聊天输入 `/context` 查看生成上下文来源,只基于当前 manifest 和最近 run trace 列出项目对话、短期记忆、长期记忆、项目黑板、Agent 对话、Agent 私有记忆、manifest、最近 trace 和最近 LLM 输入路径,并提供 `/read` 或 `/memory blackboard` 草稿;该入口不得触发 Tauri 读写、不得读取上下文文件,也不得新增普通用户上下文面板。 -- 2026-07-03 调整:普通用户通过聊天输入 `/timeline` 查看项目活动时间线,只基于当前 manifest.commandRuns 和最近 run trace 汇总最近命令、日志读取草稿和最近 Agent 步骤,并提供 `/read`、`/trace` 或 `/history` 草稿;该入口不得触发 Tauri 读写、不得读取日志或 trace 文件,也不得新增普通用户时间线面板。 -- 2026-07-03 调整:普通用户通过聊天输入 `/playtest` 查看试玩状态,只基于主窗口当前已加载的 manifest、最近 run trace 和 preview 状态派生原型是否通过、预览是否运行、Playtest 任务状态、最近试玩步骤和预览日志读取命令,并提供 `/run`、`/open-preview`、`/trace` 或 `/review` 草稿;该入口不得触发 Tauri 读写、不得启动或打开预览、不得读取日志,也不得新增普通用户试玩面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/test-plan` 准备手动测试计划,只基于主窗口当前已加载的 manifest、最近 run trace 和 preview 状态汇总手动用例、关联 Preview / Playtest 任务和最近试玩证据,并提供 `/run`、`/open-preview`、`/trace`、`/review` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取文件、不得启动或打开预览、不得直接继续 run,也不得新增普通用户测试面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/audience` 查看首批试玩对象,只基于主窗口当前已加载的 manifest、最近 run trace、preview 和试玩任务状态汇总首批试玩人群、测试者规模、观察重点和暂不面向场景,并提供 `/run`、`/feedback`、`/review`、`/trace` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取文件、不得启动或打开预览、不得导出试玩包、不得直接继续 run,也不得新增普通用户对象面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/invite` 准备试玩邀请文案,只基于主窗口当前已加载的 manifest、最近 run trace 和 preview 状态汇总邀请对象、短文案、发送前检查和收反馈口径,并提供 `/run`、`/feedback`、`/review`、`/trace` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取文件、不得启动或打开预览、不得导出试玩包、不得写项目,也不得新增普通用户邀请面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/bug-report` 准备缺陷复现记录,只基于主窗口当前已加载的 manifest、最近 run trace 和 preview 状态汇总复现入口、最近试玩证据、记录模板、严重度口径和修复草稿,并提供 `/run`、`/agent-resume 缺陷修复:`、`/review`、`/trace` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取文件、不得启动或打开预览、不得导出试玩包、不得写项目,也不得新增普通用户缺陷面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/survey` 准备试玩问卷问题,只基于主窗口当前已加载的 manifest、最近 run trace 和 preview 状态汇总问卷使用场景、五个核心问题、记录格式和追踪方式,并提供 `/run`、`/invite`、`/review`、`/trace` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取文件、不得启动或打开预览、不得导出试玩包、不得写项目,也不得新增普通用户问卷面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/cover` 准备封面与缩略图检查,只基于主窗口当前已加载的 manifest、最近 run trace、preview 和资产状态汇总封面候选、用途尺寸、选择口径和补齐路径,并提供 `/run`、`/art`、`/listing`、`/review`、`/trace` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得截屏、不得裁剪、不得读取文件、不得启动或打开预览、不得导出试玩包、不得上传云端、不得发布作品、不得写项目,也不得新增普通用户封面面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/screenshots` 准备宣传截图清单,只基于主窗口当前已加载的 manifest、最近 run trace、preview 和资产状态汇总截图目标、拍摄顺序、命名建议和作品页搭配,并提供 `/run`、`/listing`、`/review`、`/trace` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得截屏、不得读取文件、不得启动或打开预览、不得导出试玩包、不得写项目,也不得新增普通用户截图面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/trailer` 准备试玩短视频脚本,只基于主窗口当前已加载的 manifest、最近 run trace、preview 和资产状态汇总 15 秒结构、镜头清单、口播节奏和录制提示,并提供 `/run`、`/share`、`/review`、`/trace` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得录屏、不得读取文件、不得启动或打开预览、不得导出试玩包、不得上传云端、不得写项目,也不得新增普通用户录屏面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/faq` 准备试玩常见问答,只基于主窗口当前已加载的 manifest、最近 run trace 和 preview 状态汇总试玩问答、回答口径、测试者提醒和交付搭配,并提供 `/run`、`/share`、`/review`、`/trace` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取文件、不得启动或打开预览、不得导出试玩包、不得上传云端、不得写项目,也不得新增普通用户 FAQ 面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/post` 准备社区发布文案,只基于主窗口当前已加载的 manifest、最近 run trace、preview 和资产状态汇总短文案、长文案结构、标签建议和 CTA,并提供 `/run`、`/store`、`/review`、`/trace` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得上传云端、不得发布作品、不得读取文件、不得启动或打开预览、不得导出试玩包、不得写项目,也不得新增普通用户社区发布面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/store` 准备上架资料清单,只基于主窗口当前已加载的 manifest、最近 run trace、preview、资产和发布说明状态汇总必备资料、首发范围、上架前检查和参考命令,并提供 `/run`、`/listing`、`/review`、`/trace`、`/read exports/README.md` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得上传云端、不得发布作品、不得读取文件、不得启动或打开预览、不得导出试玩包、不得写项目,也不得新增普通用户上架面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/media-kit` 准备媒体资料包清单,只基于主窗口当前已加载的 manifest、最近 run trace、preview、资产和发布说明状态汇总对外资料、素材缺口、组装顺序和参考命令,并提供 `/run`、`/screenshots`、`/review`、`/trace`、`/read exports/README.md` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得截屏、不得录屏、不得读取文件、不得启动或打开预览、不得导出试玩包、不得上传云端、不得发布作品、不得写项目,也不得新增普通用户媒体包面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/release-notes` 准备试玩更新说明,只基于主窗口当前已加载的 manifest、最近 run trace、preview、资产和发布说明状态汇总本轮变化、主要产物、玩家可见说明和已知限制,并提供 `/run`、`/media-kit`、`/review`、`/trace`、`/read exports/README.md` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取文件、不得启动或打开预览、不得导出试玩包、不得上传云端、不得发布作品、不得写项目,也不得新增普通用户更新说明面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/known-issues` 准备已知问题清单,只基于主窗口当前已加载的 manifest、最近 run trace、preview 和任务状态汇总已知问题、试玩限制、反馈入口和发送前检查,并提供 `/run`、`/share`、`/review`、`/trace` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取文件、不得启动或打开预览、不得导出试玩包、不得上传云端、不得发布作品、不得写项目,也不得新增普通用户已知问题面板。 -- 2026-07-03 调整:普通用户通过聊天输入 `/feedback` 准备试玩反馈和修改说明,只基于当前 manifest、最近 run trace 和 preview 状态列出反馈方向、反馈模板和参考命令,并提供 `/run`、`/agent-resume 试玩反馈:`、`/review` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取文件、不得启动或打开预览、不得直接继续 run,也不得新增普通用户反馈面板。 -- 2026-07-04 调整:普通用户通过聊天输入 `/retention` 准备首轮复玩/留存观察清单,只基于当前 manifest、最近 run trace、preview、最近试玩证据、素材数量、发布说明和导出状态汇总测试者样本、复玩信号、记录模板和暂不做事项,并提供 `/run`、`/feedback`、`/review`、`/trace` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取文件、不得启动或打开预览、不得导出试玩包、不得上传云端、不得发布作品、不得写项目,也不得新增普通用户留存面板;首版不做真实埋点、留存报表、用户画像、A/B 实验、排行榜或账号留存。 -- 2026-07-03 调整:普通用户通过聊天输入 `/listing` 准备作品页文案清单,只基于当前 manifest、最近 run trace、发布组任务和资产清单汇总标题、一句话卖点、标签口径、封面素材、发布说明和最近运营步骤,并提供 `/read exports/README.md`、`/review`、`/art`、`/publish` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得读取发布说明、不得上传云端、不得发布作品,也不得新增普通用户作品页面板。 -- 2026-07-03 调整:普通用户通过聊天输入 `/handoff` 生成当前项目交接摘要,只基于主窗口当前已加载的 manifest、授权项目路径、最近 run trace、Agent 状态和已加载 run 历史生成交接信息,并提供 `/next` 后续草稿;该入口不得触发 Tauri 读写、不得读取文件、不得启动或打开预览,也不得新增普通用户面板。 -- 2026-07-03 调整:普通用户通过聊天输入 `/runs` 查看已加载 Run 历史读取命令,只基于主窗口当前已加载的 latest trace 和最多 100 个历史 run 中已经载入的批次生成 `/trace` 或 `/read .agent/runs/...` 草稿;该入口不得额外触发 Tauri 读取、不得滚动加载更多历史、不得启动或打开预览,也不得新增普通用户面板。 -- 2026-07-03 调整:普通用户通过聊天输入 `/run-files` 查看 Agent 运行辅助文件读取命令,只列出 `.agent/output.jsonl`、`.agent/activity.jsonl` 和 `.agent/context.bundle.json` 对应 `/read` 草稿并提供首个草稿;该入口不得直接读取辅助文件、不得触发 Tauri 读写,也不得新增普通用户面板。 -- 2026-07-03 调整:`/llm-status` 读取到的 agent 级 LLM 配置状态可回填到主窗口 Agent 状态列表、聊天侧 `/agents` 汇总和单 Agent 对话头部,显示 provider 类型、模型、流式开关和 API Key 是否已读取;密钥本体仍不能进入聊天、状态列表、manifest、trace 或本地项目文件。 -- 2026-07-03 调整:开发窗口日志面板提供 `.agent/logs/command.log`、`.agent/logs/preview.log` 和 `.agent/logs/agent.log` 的只读查看入口,复用 `file.read` 授权策略;普通用户聊天输入 `/logs` 只列出这三个日志文件对应的 `/read ...` 草稿 / 命令并提供首个草稿,不直接读取日志,不新增普通用户日志面板,实际读取仍走聊天侧 `file.read`。 -- 2026-07-03 调整:单 Agent 对话面板允许用户把当前输入手动追加到该 agent 的 `memory/agents//.md` 私有记忆;写入复用 `memory.write` 项目策略、项目锁和 Tauri 本地目录能力,不把普通对话流水自动混入私有记忆;聊天侧 `/agent-conversations` 和 `/agent-memories` 只列出同一批 Agent 对话与私有记忆读取命令并提供首个 `/read` 草稿,不直接读取文件。 -- 2026-07-03 调整:普通用户通过聊天输入 `/art` 查看美术素材,只基于当前 manifest 盘点图片、视频和序列帧素材的数量、来源、画板接入状态和路径,并提供 `/generate-art 首版核心美术素材` 或 `/read assets/manifest.art.json` 草稿;该入口不得触发 Tauri 读写、平台生成、画板同步或新增普通用户美术面板。 -- 2026-07-03 调整:主窗口新增音效登记和画板音频导入快捷入口,只填入 `/asset-register assets/audio/sfx.wav audio audio/wav` 或 `/import-canvas-asset assets/audio/sfx.wav ` 草稿;聊天输入 `/audio` 只基于当前 manifest 盘点音频素材、来源和路径,并给出登记音效或读取 `assets/manifest.audio.json` 的草稿。音乐组仍复用现有资产登记 / 画板回流链路,不新增独立音频生成系统。 -- 2026-07-03 调整:普通用户通过聊天输入 `/balance` 查看数值与难度口径,只基于当前 manifest.tasks、最近 run trace.taskGraph、trace artifacts 和 steps 汇总数值组任务、验收口径、`game/balance.json` 状态和最近数值步骤,并提供 `/read game/balance.json` 或 `/agent-resume 数值调整:...` 草稿;该入口不得触发 Tauri 读写、不得读取数值表、不得启动预览或继续 run,也不得新增普通用户数值面板。 -- 2026-07-03 调整:主窗口新增常用生成产物读取入口,只把入口 HTML、设计、数值、美术清单、音频清单和发布说明对应的 `/read` 草稿填入聊天输入框;聊天命令 `/artifacts` 只列出同一组固定读取命令并提供首个读取草稿,`/run-artifacts` 只列出最近 trace 里的产物读取命令并提供首个 `/read` 草稿,`/logs` 只列出固定日志读取命令;实际读取仍走聊天侧 `file.read` 权限流,不直接读本地文件。 -- 2026-07-03 调整:普通用户通过聊天输入 `/share` 准备试玩交付清单,只基于当前 manifest、授权项目路径、最近 run trace、preview 状态和 manifest.commandRuns 汇总原型通过状态、本地预览、本地试玩包、测试者说明和反馈收集方向,并提供 `/export`、`/exports`、`/trace`、`/review` 或 `/next` 草稿;该入口不得触发 Tauri 读写、不得导出试玩包、不得列出历史包、不得上传云端、不得生成公开分享链接,也不得新增普通用户分享面板。 -- 2026-07-03 调整,2026-07-04 更新:普通用户通过聊天输入 `/next` 触发下一步建议入口,只基于主窗口当前已加载的 manifest、最近 run trace 和最近命令摘要生成聊天建议,列出 `/goal`、`/guide`、`/progress`、`/spec`、`/mvp`、`/pitch`、`/demo`、`/rules`、`/tutorial`、`/mobile`、`/compatibility`、`/accessibility`、`/localization`、`/performance`、`/polish`、`/blockers`、`/ready`、`/evidence`、`/deps`、`/revise`、`/privacy`、`/audience`、`/invite`、`/bug-report`、`/survey`、`/cover`、`/screenshots`、`/trailer`、`/faq`、`/post`、`/store`、`/media-kit`、`/release-notes`、`/known-issues`、`/tasks`、`/criteria`、`/groups`、`/balance`、`/budget`、`/qa`、`/changes`、`/plan`、`/todo`、`/trace`、`/review`、`/context`、`/timeline`、`/playtest`、`/test-plan`、`/feedback`、`/retention`、`/share`、`/listing`、`/run`、`/open-preview`、`/assets`、`/credits`、`/art`、`/audio`、`/publish`、`/artifacts`、`/run-artifacts`、`/passes`、`/run-files`、`/internals`、`/logs`、`/agent-resume ` 等安全命令草稿方向,并提供一个首选草稿;该命令不得直接执行 Tauri 读写、启动或打开预览、读取本地文件,也不得绕过原有命令确认和 `file.read` 权限流。 -- 2026-07-03 调整:普通用户通过聊天输入 `/publish` 生成发布准备清单,只基于主窗口当前已加载的 manifest、最近 run trace、预览状态、资产来源和最近命令摘要列出原型通过、预览、任务、资产、音频、包装说明和试玩包状态,并提供 `/run`、`/trace`、`/agent-resume ` 或 `/export` 草稿;该入口不得触发 Tauri 读写、不得启动或打开预览、不得读取文件,也不得新增普通用户发布面板。 -- 2026-07-03 调整:普通用户通过聊天输入 `/internals` 只列出 `.agent/manifest.json`、`.agent/run.latest.json`、`.agent/spec.md`、`.agent/findings.md`、`.agent/policy.json`、`.agent/project.index.json`、`.agent/agent.db` 和 `.agent/conversations/project.jsonl` 的 `/read` 草稿,并提供首个读取草稿;该入口不得直接读取内部文件、不得触发 Tauri 读写,也不得新增普通用户内部文件面板。 -- 2026-07-03 调整:普通用户通过聊天输入 `/passes` 只从当前已加载的最近 run trace artifacts 中筛选 `.agent/passes/` 轮次产物,列出 `/read` 草稿并提供首个读取草稿;该入口不得直接读取轮次文件、不得触发 Tauri 读写,也不得新增普通用户轮次面板。 +- 2026-07-03 调整:agent 级 LLM 配置状态可回填到主窗口 Agent 状态列表和单 Agent 对话头部,显示 provider 类型、模型、流式开关和 API Key 是否已读取;密钥本体仍不能进入聊天、状态列表、manifest、trace 或本地项目文件。 +- 2026-07-03 调整:开发窗口日志面板提供 `.agent/logs/command.log`、`.agent/logs/preview.log` 和 `.agent/logs/agent.log` 的只读查看入口,复用 `file.read` 授权策略;普通用户窗口不提供这组日志入口。 +- 2026-07-03 调整:单 Agent 对话面板允许用户把当前输入手动追加到该 agent 的 `memory/agents//.md` 私有记忆;写入复用 `memory.write` 项目策略、项目锁和 Tauri 本地目录能力,不把普通对话流水自动混入私有记忆。 +- 2026-07-03 调整:音乐组复用现有资产登记 / 画板回流链路,不新增独立音频生成系统。 - 2026-06-25 调整:本地 HTTP 预览静态 `HEAD` 必须返回与 `GET` 相同的真实 `Content-Length`,但不返回 body;浏览器、图片、音频和视频探测不能拿到 `Content-Length: 0` 的假响应。 -- 2026-06-25 调整:普通用户通过聊天输入 `/run` 触发待确认 `game.run_local`,确认后只能复用白名单 `game.static_smoke` 自检当前 `game/index.html`,通过后启动 `127.0.0.1` 本地 HTTP 预览。独立执行 `game.static_smoke` 时如果已有 `.agent/run.latest.json`,必须追加 `Playtest / game.static_smoke` trace step,避免“运行了代码但编排 trace 不可见”。 -- 2026-06-25 调整:普通用户通过聊天输入 `/trace` 触发只读 `agent.trace_read`,读取 `.agent/run.latest.json` 并在聊天里摘要 loop 轮次、stopReason、nextStep、active / carry-over 任务、repairRoutes、agent 建议命令和最近 step。trace 面板仍只在开发窗口展示,普通用户窗口不新增面板。 -- 2026-06-25 调整:普通用户通过聊天输入 `/import-canvas-export /绝对/画板素材.zip 画板项目ID` 触发待确认 `canvas.export_import`,读取现有 `/editor/canvas` 素材导出 ZIP。导入命令只读取用户指定 ZIP,写入当前本地项目 `assets/canvas-imports/`,基础护栏限制路径逃逸、文件数量和解压体积;导出包没有真实 resourceId 时,用 `canvas-export:` 作为可追踪 assetObjectId,不伪造后端画板资源行。 -- 普通用户通过聊天输入 `/sync-canvas-project 画板项目ID` 触发待确认 `canvas.project_sync`;普通模式用当前陶泥儿登录态读取 `/api/editor/projects/{projectId}` 并通过 `/api/assets/read-url` 换签,高级模式使用对应 External v1 路由。固定官方 origin、owner 和凭据均不写入 manifest、Agent DB、trace 或日志。 - `game.generate_draft` 在当前模式具备画板服务授权且美术组缺少 `canvas` 来源图片资产时,复用同一平台生成链路生成首版美术素材并下载到本地;普通模式使用登录态内部路由,高级模式使用 External v1。 - 美术组 `Asset` 和音乐组 `SFX` 在缺少对应 `canvas` 来源资产时建议同步;普通模式未登录或高级模式 Developer Key 缺失时只给出准确的能力不可用说明,不伪造生成结果。 - 2026-06-24 调整:同一本地项目多次 `game.generate_draft` 必须追加 `memory/session.md` 与 `memory/project.md`,不得覆盖历史对话和创作目标记录。 @@ -6346,7 +6319,7 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在 - 2026-07-10 调整:`.agent/policy.json` 支持 `agentPolicies`,用规范 Agent id 保存单个 Agent 的 `deniedCommands / confirmCommands`。Runtime 计算有效工具策略时把项目级策略和 Agent 级策略叠加,项目级策略继续对所有 Agent 生效,Agent 级策略只能进一步拒绝或要求确认,不能放宽项目级策略;拒绝优先于确认。主聊天新增 `/agent-policy-deny Agent 命令`、`/agent-policy-allow Agent 命令`、`/agent-policy-confirm Agent 命令` 和 `/agent-policy-auto Agent 命令`,继续通过 `project.policy_write` 确认卡写入策略。 - 2026-07-10 调整:后台 Agent 工具命中确认策略时不再当作 `blocked` observation 继续收尾,而是把当前 Runtime 写成 `status/phase = waiting-for-confirmation`,`waitingOn` 固定为等待开发者确认工具动作,`recentToolCalls`、事件流、任务记录和 `taskQueue.waitingForConfirmation` 都保留该事实;同一 Agent 的后台 drain 暂停,不继续消费后续 pending 任务。命中拒绝策略仍使用 `blocked` observation 交回 Agent 修正计划。 - 2026-07-10 调整:Agent Runtime 后台任务支持按 Agent / runId 取消和重试。取消先通过 `.agent/runtime/cancel//.json` 写入本地取消请求;pending 任务被取消后不会被 drain 消费,running 任务在原 worker 仍持锁时只投影为 `cancelling`,必须等当前 LLM 或工具调用返回后的检查点真正停下,才由持锁 worker 向任务 JSONL、事件流和 `agent.db` 追加 `cancelled` 审计,不再继续执行工具或保存最终 assistant 回复。`cancelling` 期间禁止重试;重试只能基于已有非 running / pending / waiting-for-confirmation / cancelling 任务创建新的 run,并继续走 `agent.resume` 自动权限和同一 Agent 队列锁。 -- 2026-07-10 调整:Agent Runtime 后台任务的 `runId` 是同一 Agent 任务历史的身份,不允许复用覆盖。`start_game_creator_agent_runtime_task`、`agent.delegate` 和 retry 进入后台队列前会读取该 Agent 全量 task JSONL 历史;若调用方传入的规范化 runId 已存在,Runtime 自动追加 `-dup--` 生成实际 runId。任务队列、delegate observation 和 `agent.db` 审计都必须使用实际 runId,避免 `latest_game_creator_agent_runtime_tasks` 按 runId 去重时折叠掉不同任务。 +- 2026-07-10 调整:Agent Runtime 后台任务的 `runId` 是同一 Agent 任务历史的身份,不允许复用覆盖。`start_game_creator_agent_background_task_for_session_at`、`agent.delegate` 和 retry 进入后台队列前会读取该 Agent 全量 task JSONL 历史;若调用方传入的规范化 runId 已存在,Runtime 自动追加 `-dup--` 生成实际 runId。任务队列、delegate observation 和 `agent.db` 审计都必须使用实际 runId,避免 `latest_game_creator_agent_runtime_tasks` 按 runId 去重时折叠掉不同任务。 - 2026-07-10 调整:Agent Runtime 的 `memory.write scope=agent` 只能写当前 Agent 自己的私有记忆。若 action 指定其他 `agentId / targetAgentId`,Runtime 返回 `blocked` observation,不写目标 Agent 私有记忆、不写 `agent.runtime.memory.write` 审计;跨 Agent 共享稳定结论必须走 `blackboard.write`,给单个 Agent 留上下文必须走 `agent.message`。 - 2026-07-10 调整:Agent Runtime 和本地对话使用 append-only JSONL 作为事实源时,进程内必须按目标文件路径串行追加整行。`.agent/agent.db`、`.agent/conversations/**/*.jsonl`、`.agent/runtime/events/*.jsonl`、`.agent/runtime/tasks/*.jsonl`、`.agent/activity.jsonl` 和 `.agent/output.jsonl` 统一走共享追加 helper,避免多个后台 Agent 并行完成时 JSON record 与换行交错。 - 2026-07-10 调整:Agent Runtime 待确认工具动作改用 durable `AgentRuntimePendingToolAction`。Runtime 将精确 `action` 输入、当前 task/run、loop 轮次、action 序号、计划、已有 observations 与后续 loop 所需上下文先做敏感内容和项目绝对路径校验,再通过临时文件替换原子写入 `.agent/runtime/pending-actions//.json`;公共 runtime state 的 `pendingToolAction` 只暴露 `actionId / actionFingerprint / tool / inputSummary / reason / requestedAt` 安全摘要,完整输入不进入公共状态。`actionFingerprint` 绑定工具名、完整输入 JSON 与实际执行使用的 task context;`actionId` 还绑定 run、loop、action 序号和 occurrence nonce,使同一 run 内输入相同的两次动作仍是两个不同发生。确认和拒绝都必须匹配 `runId + actionId`,Runtime 会重算指纹并与私有落盘动作及公共摘要交叉校验,不一致时失败关闭。确认通过后在同一 run 直接执行持久化的原 action,把真实 observation 接回后续 Agent loop,不创建新 run,也不让模型重复生成待确认动作;拒绝不执行工具,写入 `blocked` observation 后在同一 run 继续规划。待确认账本按 `pending-confirmation / approved / executing / observed-approved / observed-rejected` 迁移:重启时 `approved` 可恢复精确动作,已持久化 observation 可直接续 loop,`executing` 表示外部副作用结果未知,Runtime 必须进入 `failed / needs-reconciliation` 并禁止自动重放,开发者核对项目状态后只能先取消原任务。waiting run、完整待确认动作和安全摘要均已落盘,App 重启不会越过该 run 去启动后续任务;等待期间同 Agent 新任务只保持 `pending`,确认、拒绝或取消结束后再由同一 drain 串行排空。`.agent/runtime/` 是 Runtime 私有控制面,通用 `file.list / file.read / file.write / file.delete` 不得列出、读取、修改或删除;checkpoint/index/diff/restore 继续整体排除该目录。每 Agent 锁包含唯一 token,旧持有者析构时只删除自己的锁;Linux 上其他仍存活进程的锁不会因超过固定时长被抢占。确认、拒绝及工具 observation 分别写入 `agent.runtime.tool_confirmation.approved`、`agent.runtime.tool_confirmation.rejected` 和 `agent.runtime.tool_observation` 审计;pending 和 confirmation 文件只在 observation/终态可靠落盘后清理,失败清理会显式报错。 @@ -6369,29 +6342,15 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在 - 2026-07-10 调整:Agent Runtime V1 新增 `resume_game_creator_agent_runtime_tasks` 恢复入口。客户端读取项目 Runtime 时对每个项目路径最多自动尝试一次恢复;恢复命令必须通过 `agent.resume` 自动权限,默认需要确认或被拒绝时不会静默启动。恢复扫描 `.agent/runtime/tasks/.jsonl` 里的上一进程遗留 `running` 或 `pending` 任务,同一 Agent 同时存在二者时先重接遗留 `running`,再由既有 drain 串行继续 `pending`,并写 `agent.runtime.background_task.recovered` 审计记录。该能力只恢复本地 JSONL 队列到当前 App 进程,不是跨重启常驻 worker,也不承诺恢复已发出的上游 LLM 请求。 - 2026-07-10 调整:每个 Agent 新增独立持久化 Session 管理。legacy `agent-session-` 继续读写 `.agent/conversations/agents/.jsonl`;新 Session 写 `.agent/conversations/agents//sessions/.jsonl`,`.agent/runtime/sessions/.json` 原子保存 Session catalog 和 active Session。开发单 Agent 聊天页支持列表、创建、切换、归档和归档历史只读查看;归档不删除消息,运行中、排队中、等待确认、取消中或 `needs-reconciliation` 的 Session 不允许改变 active/归档。聊天、流式回调、后台 run、任务历史、事件历史和 prompt 连续上下文按启动时 `sessionId` 归属并过滤,`conversation.read` 和 self `agent.run_status` 通过 runId 使用同一 Session;恢复或处理待确认动作前校验 task、runtime state 和 pending action 的 Session 一致性。Runtime 的 OS 锁、FIFO 队列和恢复屏障仍属于 Agent,同一 Agent 不因多个 Session 获得并行执行能力。 - 2026-07-01 调整:AI 游戏创作 App 借鉴 Godcoder 的本地工程护栏,但只收敛到五项本地机制:`ArtifactWriter` 写入前 checkpoint、写入后 diff、用户确认 restore;进入 LLM 前过滤密钥和本机配置痕迹;`.agent/agent.db` 继续作为轻量 JSONL 项目索引,`/index` 额外刷新 `.agent/project.index.json`;同一项目写入通过 `.agent/project.lock` 串行化;`.agent/policy.json` 记录项目级命令拒绝 / 确认策略。v1 不引入通用 IDE 插件、云工作区、SQLite 或任意 shell 代理。 -- 2026-07-03 调整:主窗口最近 checkpoint 列表必须直接展示 checkpoint id、文件数、大小和创建时间,并提供直接对比、填入 `/diff`、确认回滚和填入 `/restore` 的轻量操作;回滚仍走 `project.restore` 确认卡,不在列表按钮中直接写项目文件。 -- 2026-06-24 调整:普通用户通过聊天输入 `/help` 发现可用内置命令;命令发现必须留在聊天消息里,不得因此暴露开发面板。 +- 2026-07-03 调整:主窗口最近 checkpoint 列表必须直接展示 checkpoint id、文件数、大小和创建时间,并提供直接对比和确认回滚的轻量操作;回滚仍走 `project.restore` 确认卡,不在列表按钮中直接写项目文件。 - 2026-06-24 调整:聊天区待确认命令的日志语义必须区分 `permission.pending`、`permission.confirm` 和 `permission.cancel`;待确认卡片必须展示本地写入目标路径,避免用户在不知道落盘位置时确认。 -- 2026-06-24 调整:普通用户通过聊天输入 `/status` 读取 `.agent/manifest.json` 的项目状态摘要,只在聊天消息里展示项目目录、任务状态、资产数量、预览状态和最近命令;不得为了状态查看暴露任务、文件或日志面板。 -- 2026-06-24 调整:普通用户通过聊天输入 `/files` 触发只读 `file.list`,只在聊天消息里展示本地项目文件摘要;不得把文件读写面板暴露到普通用户窗口。 -- 2026-06-24 调整,2026-07-03 更新:普通用户通过聊天输入 `/assets` 触发只读 `asset.list`,只在聊天消息里展示本地项目资产路径、类型和来源;资产列表消息可以填入首个资产的 `/read` 草稿,方便从聊天继续查看资产文本元数据,但仍不直接读取文件或绕过聊天命令;不得把资产面板暴露到普通用户窗口。 -- 2026-06-24 调整:普通用户通过聊天输入 `/read 本地相对路径` 触发只读 `file.read`,只在聊天消息里展示项目内文本文件并截断长文本;不得开放聊天里的文件写入或删除能力。 -- 2026-06-24 调整:普通用户通过聊天输入 `/tasks` 触发只读 `task.list`,只在聊天消息里展示专业组、角色、任务状态和产物交接;不得把任务面板暴露到普通用户窗口。 -- 2026-06-24 调整:普通用户只能通过聊天触发内置命令;当前 `/smoke` 映射到白名单 `command.run_limited game.static_smoke` 并走待确认卡片,不允许扩展成任意 shell 或自由命令解析。 -- 2026-06-24 调整:普通用户通过聊天输入 `/project /绝对路径` 触发 `project.create` 待确认命令,用于授权并初始化本地项目目录;相对路径不会生成待确认命令;不要把开发窗口项目路径输入框暴露到正式用户界面。 -- 2026-06-25 调整:普通用户侧所有会写入、运行、查看 / 打开预览或导入本地产物的命令必须先完成 `/project` 初始化,包括 `game.generate_draft`、`asset.upload`、`game.run_local`、`command.run_limited`、`preview.start`、`preview.status`、`preview.open`、`preview.stop`、`memory.write`、`memory.delete`、`canvas.project_sync`、`canvas.asset_import` 和 `canvas.export_import`;没有已授权本地项目时只提示设置项目,不得落到默认 `/tmp` 草稿目录。 +- 2026-06-25 调整:普通用户侧所有会写入、运行、查看 / 打开预览或导入本地产物的命令必须先完成项目初始化,包括 `game.generate_draft`、`asset.upload`、`game.run_local`、`command.run_limited`、`preview.start`、`preview.status`、`preview.open`、`preview.stop`、`memory.write`、`memory.delete`、`canvas.project_sync`、`canvas.asset_import` 和 `canvas.export_import`;没有已授权本地项目时只提示设置项目,不得落到默认 `/tmp` 草稿目录。 - 2026-06-24 调整,2026-06-30 更新:终端测试入口使用同一个 Tauri Rust 二进制的 `--agent-run <本地项目绝对路径> <创作需求>`,只复用现有 `game.generate_draft`、`game.static_smoke` 和本地 HTTP 预览链路,不另建第二套 agent runtime;发布 App 的 LLM 配置从 Tauri 应用配置目录读取,不写入仓库默认配置或项目文件。需要自动验证时可追加 `--no-wait`,生成预览 trace 后立即停止本地预览,避免命令卡在回车等待。 - 2026-07-04 调整,2026-07-08 更新:`apps/ai-game-creator-shell/src-tauri/src/main.rs` 拆成薄入口,继续只保留共享类型 / 常量、模块声明、CLI preflight、`tauri::Builder`、运行时配置初始化和 `invoke_handler` 清单;CLI 参数解析与终端运行输出放入 `cli.rs`,Tauri command 包装放入 `commands.rs`,运行时配置 / LLM 配置检查放入 `config.rs`,Agent loop 与生成编排放入 `agent.rs`,上传 / 画板 / 平台美术生成接入放入 `assets.rs`,本地项目文件、记忆、对话、权限、checkpoint、manifest 和通用路径工具放入 `project.rs`,本地 HTTP 预览 server、preview registry 和 preview Tauri command 放入 `preview.rs`,旧窗口 URL 与兼容 command 放入 `windows.rs`,Rust 单测放入 `tests.rs`。拆分不得改变 Tauri command 名、JSON 字段、`.agent/*` 路径、项目权限策略或错误语义。 - 2026-06-24 调整,2026-07-08 更新:AI 游戏创作 App 的 release 配置只登记一个普通用户窗口,登录后在同一 WebView 中进入首页、项目组和项目开发占位;开发专用单 Agent 对话、任务、文件、记忆、预览、日志和能力面板只能通过 Vite dev 的 `?dev/#dev` 分支或 debug 构建自动打开的 `developer` 开发窗口查看,不进入普通用户窗口。旧工作区窗口切换 command 只保留兼容,用户主流程不得调用它。 - 2026-06-24 调整,2026-07-18 更新:`check:native-shells` 必须静态守住 AI 游戏创作 App 的用户 / 开发边界:release 只保留一个普通用户窗口,用户侧预览只在项目运行工作台嵌入当前 `127.0.0.1` 游戏,且 Tauri 激活命令不得调用 opener;开发面板只能在 `devMode` 分支或 debug-only `developer` 窗口渲染,`developer` 窗口当前使用 `index.html?agent-chat` 并复用 `.agent/conversations/agents/.jsonl` 持久化单 Agent 对话;发布入口和普通用户窗口不得暴露 `Agent 聊天` 导航,也不得调用旧工作区窗口切换 command。 - 2026-07-10 调整:AI 游戏创作 App 的 Runtime 实时状态依赖 Tauri event listen。`src-tauri/capabilities/events.json` 必须覆盖 `client`、`developer`、`main`、`launcher`,只授予 `core:event:allow-listen` 与 `core:event:allow-unlisten`,不得向前端授予 emit;`check-config.mjs` 静态守住窗口和权限边界。Vite 开发服务器必须把仓库根目录加入 `server.fs.allow`,因为 App 直接加载 `packages/shared/src`;否则真实 WebView 会因共享源码 403 白屏,即使 TypeScript 检查仍通过。 - 2026-06-25 调整:`check:native-shells` 在 `ai-game-creator-shell:check` 之后必须追加 `ai-game-creator-shell:build -- --no-bundle`,让原生壳总门禁同时证明 AI 游戏创作独立 Tauri 壳能完成 release 编译,而不是只证明前端 / Rust 逻辑测试通过。 -- 2026-06-24 调整,2026-07-18 更新:普通用户通过聊天输入 `/preview` 触发待确认 `preview.start`,完成 `/project` 初始化后可通过 `/open-preview` 触发待确认 `preview.open` 并只激活当前已授权项目对应的 `127.0.0.1` 客户端运行视图,通过 `/preview-status` 查询当前项目预览,通过 `/preview-stop` 停止当前项目预览;用户工作台仅嵌入当前项目的 loopback 游戏,开发预览状态面板仍只在开发窗口可见,不能把 `preview.open` 扩展成任意 URL 打开能力,也不能展示或停止其它本地项目遗留的全局预览。 -- 2026-06-25 调整:`/preview-status` 虽然是只读命令,也必须写入 `preview.status` 命令日志并向聊天返回错误,不得因查询失败产生未捕获异常或无审计记录。 -- 2026-06-24 调整:普通用户通过聊天输入 `/memory [short]` 读取长期或短期记忆,通过 `/remember 内容` 待确认追加长期记忆,通过 `/forget-memory [short]` 待确认删除记忆;不得为了记忆查看或编辑暴露独立用户面板。 -- 2026-06-25 调整:`/remember` 支持可选 scope:`/remember short 内容` 追加短期记忆,`/remember long 内容` 或未写 scope 时追加长期记忆;仍统一走待确认 `memory.write`,不暴露独立用户面板。 -- 2026-06-24 调整:普通用户通过聊天输入 `/canvas 画板项目ID` 触发待确认 `canvas.project_open`,只打开本机 Genarrative 编辑器 `/editor/canvas?projectid=...`;不得把它扩展成远程站点或任意 URL 打开能力。 -- 2026-06-24 调整:普通用户通过聊天输入 `/import-canvas-asset 本地路径 画板项目ID 资源ID|object:资产对象ID [kind] [mediaType]` 触发待确认 `canvas.asset_import`,只登记项目目录内已有文件为 `canvas` 来源资产;只有 `assetObjectId` 时使用 `object:` 前缀,不伪造 resourceId;画板导出包回流使用 `/import-canvas-export /绝对/画板素材.zip 画板项目ID`。 - 验证方式:`npm run ai-game-creator-shell:typecheck`、`cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml`、`npm run test -- packages/shared/src/contracts/gameCreationApp.test.ts`、`cargo test -p shared-contracts game_creation_app --manifest-path server-rs/Cargo.toml`、`cargo test -p platform-agent --manifest-path server-rs/Cargo.toml`、`npm run check:encoding`、`git diff --check`。 ## 2026-06-30 唯一码和私有码按用户限兑一次 @@ -6429,7 +6388,7 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在 ## 2026-07-10 AI 游戏创作 Agent Runtime 执行边界 -- 决策:开发单 Agent 对话默认使用可执行 Runtime,输入区通过 `执行 / 聊天` 分段控件显式区分;`执行` 调用 `start_game_creator_agent_runtime_task` 并保留工具策略、确认、取消、排队和状态事件,`聊天` 才使用无工具流式回复,不再保留并列的“后台运行”按钮。消息区使用固定响应式网格行和内部滚动,并在 Runtime 非终态期间显示当前等待对象。Runtime 完成前必须先把 assistant 回复写入发起 Session,再写 completed 终态和广播;落盘失败只能进入 failed。前端收到匹配当前项目、Agent、Session 和 runId 的终态后自动重读对话,切换 Session 会清除当前等待投影,旧 run 事件不得覆盖新 Session。 +- 决策:开发单 Agent 对话默认使用可执行 Runtime,输入区通过 `执行 / 聊天` 分段控件显式区分;`执行` 走后台任务入口 `start_game_creator_agent_background_task_for_session_at` 并保留工具策略、确认、取消、排队和状态事件,`聊天` 才使用无工具流式回复,不再保留并列的“后台运行”按钮。消息区使用固定响应式网格行和内部滚动,并在 Runtime 非终态期间显示当前等待对象。Runtime 完成前必须先把 assistant 回复写入发起 Session,再写 completed 终态和广播;落盘失败只能进入 failed。前端收到匹配当前项目、Agent、Session 和 runId 的终态后自动重读对话,切换 Session 会清除当前等待投影,旧 run 事件不得覆盖新 Session。 - 2026-07-12 修正:Runtime 状态为空时也要保留其网格行位,消息区和输入区显式固定到第 5、6 行,禁止空 Runtime 容器通过 `display:none` 让长消息落入 `auto` 行并撑高页面;等待 LLM 期间消息区同步使用 `aria-busy` 暴露忙碌状态。消息区只在用户仍接近底部时自动跟随最新片段,用户向上查看历史后暂停跟随,切换会话、重新读取或主动发送时再恢复。 - 2026-07-12 修正:OpenAI-compatible 流式响应中 `choices` 为空数组或 `null` 的 usage / metadata 包不得再报缺少 `choices[0]`,必须跳过元数据并继续等待正文。首个 delta 前只有 `StreamUnavailable / EmptyResponse / Deserialize` 协议兼容错误允许由 Rust 单 Agent 流式入口回退一次非流式请求;上游状态、鉴权、额度、超时、连接和请求错误直接保留原错误,前端不得再次发起普通 LLM 请求。已收到正文和完成原因后继续保留完整流式回复,不能被尾部坏包覆盖。 - 2026-07-12 修正:Tauri 聊天事件监听被拒绝后,前端选择的普通回复入口必须固定调用 `client.run`,即使 Agent 路由保留 `stream=true` 也不得再内部发 SSE。pending action 的 project revision 快照改为绑定 planning 请求发出前的版本;`file.delete` 取得项目写锁后必须再次校验 revision / verification gate,公共 pending 摘要必须与私有 ledger 完整相等,confirm / reject 只在迁移状态可靠落盘后启动 continuation。manifest 和 pending ledger 禁止 truncate/remove 旧文件后再替换,统一使用同目录临时文件的原子替换及可恢复 backup。 @@ -6737,14 +6696,14 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在 - Finalization 生产闭集:四条 lifecycle 只允许固定 lifecycle 字段和统一 `schemaVersion / updatedAt` envelope,并绑定 `responseChars / conversationPath`;assistant 审计只允许 `recordType / agentId / sessionId / role / path / messageId / finalizationId`,两条 completed 审计只允许 `recordType / agentId / taskId / sessionId / runId / source / finalizationId / messageId / responseFingerprint / responseChars`,再加同一 envelope。匹配必须逐字核对 finalization/message、Agent/task/Session/run/source、Goal/plan 快照、response fingerprint/chars 和 conversation path,四阶段还必须核对 ordinal/previousStage 与 JSONL 物理顺序;任何额外生产字段都不能获得 finalization reservation。 - 公共投影边界:task、Goal/steer、委派任务、`project.verify` 命令和 Provider/Runtime error 正文只保留在对应私有执行事实中。event、Agent DB、receipt、activity、output 与报告统一只存身份、状态、SHA-256、字符/字节/条目计数和经 URL、项目根、其它绝对路径及凭据清洗的有界摘要;公共 task 固定不存正文,委派只存 `taskSha256 / taskChars`,verify 只存脚本安全标识、`expectedCommandSha256 / expectedCommandChars`、timeout 和结果计数,error 只存 kind/fingerprint/chars 或脱敏摘要。禁止保留 task/Goal/委派/命令/error 的正文、preview、head 或 tail;普通非 Goal 任务也不例外。 - 真实验收器:revision 2 marker/path/content 不再预埋首轮项目 fixture;两个 revision 都必须命中同一交付路径的真实 `file.write` 或 `project.patchset create` 待确认动作,edit 前最终 marker/文件必须不存在,revision 1 已完成步骤在 revision 2 和终态不可回退。Goal suite 使用带 sentinel 的专用 AppData,配置只以 hardlink 复用并在清理前核对 inode/hash;全部 CLI 固定指向专用 config dir,Runner 强杀绑定 endpoint、boot、实际二进制/argv 和 OS 启动指纹,endpoint 丢失只允许回收已认领的同指纹进程。CLI JSON 只接受精确 assigned 前缀,Goal completion evidence 按四项生产契约逐字核对,公共扫描同时包含完整正文和两个 marker,失败报告从现存 task/event/Agent DB/conversation 分面容错回收部分证据而不再全报 0。 -- 展示边界:开发 Agent UI 使用 `执行 / 聊天 / 目标` 三段模式,Goal 创建/编辑通过独立弹层完成,并展示状态、revision、完成标准和暂停/恢复/清理;纯聊天 CLI 提供对应 `/goal` 命令。正式用户 Project Supervisor 页面不暴露 Goal 管理控件。 +- 展示边界:开发 Agent UI 使用 `执行 / 聊天 / 目标` 三段模式,Goal 创建/编辑通过独立弹层完成,并展示状态、revision、完成标准和暂停/恢复/清理;纯聊天 CLI 通过 `--agent-goal-*` 入口管理同一 Goal。正式用户 Project Supervisor 页面不暴露 Goal 管理控件。 - 验收现状:确定性回归与 UI 覆盖不能替代真实 Provider 长链路。截至 2026-07-15 尚未记录 V1.18 真实 Provider PASS;最新现场仍在首轮 planning、零 plan/action 时由对端关闭长连接,Rust 25.2 秒短请求成功只能证明基础通道。恢复后必须用一次性项目完成 Goal edit、pause、Runner 强杀、重启保持 paused、显式同 run resume、唯一 assistant 和零旧动作重放的交叉取证。 ## 2026-07-15 Project Supervisor 纯聊天短入口 - 决策:无 GUI 开发聊天省略 `parentAgentId` 时固定进入 `project-supervisor`;新增 `npm run agc:chat -- --config-dir [--init] ` 作为总控入口。原 `agc:swarm` 和显式 `` 继续保留给专业父 Agent 调试,不改变既有调用兼容性。 - 边界:短入口只复用现有 Swarm CLI、External Runner、Supervisor active Session、conversation、黑板、记忆和 durable 委派协议,不新增 Agent、HTTP 服务、数据库或旁路 Provider 调用。 -- 验收:CLI 单测覆盖省略 ID 默认总控和显式 ID 兼容;真实入口 smoke 用一次性项目启动 `agc:chat`,终端显示 `project-supervisor`、创建空总控 Session,并在未发起 LLM 请求时通过 `/quit` 正常退出和清理。 +- 验收:CLI 单测覆盖省略 ID 默认总控和显式 ID 兼容;真实入口 smoke 用一次性项目启动 `agc:chat`,终端显示 `project-supervisor`、创建空总控 Session,并在未发起 LLM 请求时通过 EOF(Ctrl-D)正常退出和清理。 ## 2026-07-15 后台 Agent 最终回复使用真实增量流 @@ -6770,9 +6729,9 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在 - 边界:只压缩旧 Agent/legacy conversation 和当前 run 的旧 observation,保留最近精确 tail;Goal、任务、结构化计划、steer、pending action、project/repository revision、verification、process/join/delegate、receipt 和 finalization 身份保持规范事实,不进入摘要改写。 - 持久化:私有 `game-creator-runtime-context-compaction.v1` sidecar 绑定 Agent/Session、source prefix 指纹、可选 run、summary 指纹、预算与 usage;同源幂等,追加后 revision 单调,前缀漂移失败关闭。context bundle 只绑定压缩元数据,不复制 summary 正文。 - 请求安全:compaction 使用独立 Provider lifecycle、稳定 request slot、零工具和零 web search。未知 started 或 completed 后 sidecar 未提交均按 orphan barrier 进入 reconciliation,禁止自动重发;sidecar 已提交后恢复直接复用。 -- 入口:自动压缩只发生在 background planning 安全边界;开发 Agent UI 与 `agc:chat` / `agc:swarm` 提供 `/compact`,但 in-flight Provider、执行中工具、pending confirmation 或未收束 Runtime 时拒绝手动压缩。正式用户 Supervisor 页面不增加压缩控件。 +- 入口:自动压缩只发生在 background planning 安全边界;显式手动压缩入口为 `--agent-context-compact`,但 in-flight Provider、执行中工具、pending confirmation 或未收束 Runtime 时拒绝手动压缩。正式用户 Supervisor 页面不增加压缩控件。 - 验收:除配置、幂等、篡改、恢复和公共零正文回归外,真实套件必须完成至少 30 轮、两次压缩和一次 Runner 强杀,证明请求低于阈值、原身份不变、工具零重放、唯一 assistant 与早期约束可召回;此前不得宣称整体 PASS。 -- 语义修正:历史“每 6 轮形成上下文压缩窗口”的表述由本条取代;6 轮只形成进度 checkpoint 并执行停滞检测,不改写 observation。真正摘要只由 token 阈值或显式 `/compact` 触发。 +- 语义修正:历史“每 6 轮形成上下文压缩窗口”的表述由本条取代;6 轮只形成进度 checkpoint 并执行停滞检测,不改写 observation。真正摘要只由 token 阈值或显式手动压缩触发。 - 实现收口:显式用户约束由确定性保留层逐字钉住并继续做凭据/绝对路径脱敏;`runtime.compact` 单独使用 6 分钟 IPC 响应窗口,其他 Runner 方法仍为 10 秒;普通后台任务公共审计只保存 `taskChars + taskSha256`;终态旧 bundle 只有在完整身份、Goal、revision、verification、observation、sidecar、steer 校验通过后才可刷新 legacy plan 投影。 - 真实验收:2026-07-15 正式 `openai_chat / gpt-5.5` 路由的隔离 `context-compaction` suite PASS。30/30 轮、两次 compaction revision、一次 pidfd Runner 强杀恢复、早期约束召回和 29134/64000 最大估算输入均满足;30 个 tool-plan 与 2 个 compaction lifecycle 唯一闭合,fallback replay、重复 message/audit、工具重放和公共正文/summary/API Key/诱饵/项目路径/正式配置路径泄漏均为 0。首轮第 22 轮 Provider transport 终态按规则 FAIL 且零重放,新 disposable 项目完整重跑取得 PASS,全部一次性现场已按 sentinel 清理。 @@ -7161,7 +7120,7 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在 - 背景:`--swarm-chat` 曾在模型调用前用字符串包含判断选择 Chat / Execute / Resume,否定句、复合请求和未列入词表的工作请求都会误路由;busy Runtime 期间的裸聊天还可能绕过 Agent lane 并与原 run 交错写同一 Session。 - 决策:删除自然语言关键词分类和硬编码自然语言直答。Project Supervisor 与角色目录中 `role.id=director` 的六个部门负责人使用统一 interaction loop;自然语言回复与 `project_location / runtime_execute / runtime_resume` 都来自同一次 Provider turn 的直接文本或原生 function tool。叶子专业 Agent 保持合同执行者,不接入该外层决策能力。 - Canonical 输入:`runtime_execute` 不允许模型提交 task 参数,真正入队始终使用用户原始消息,避免模型改写时丢失否定、范围和验收条件。非原生 tool Provider 使用同构严格 JSON envelope 适配;模型只能提出 intention,不能选择 runId、越过权限或直接执行项目副作用。 -- 并发与 Runner:`SwarmChat` 恢复为 External Runner 写入口,启动前必须显式使用项目外 AppData。已有 active Goal 或 busy Runtime 时,新输入只进入同 run durable steer;空闲 direct reply 的 user / assistant 在 Agent Session lane 内成对落盘。`/resume` 是显式控制命令,不能再由“继续”等字符串特判。 +- 并发与 Runner:`SwarmChat` 恢复为 External Runner 写入口,启动前必须显式使用项目外 AppData。已有 active Goal 或 busy Runtime 时,新输入只进入同 run durable steer;空闲 direct reply 的 user / assistant 在 Agent Session lane 内成对落盘。恢复扫描只能由显式恢复入口(`--agent-resume`)触发,不能再由“继续”等字符串特判。 - 扩展边界:首版 interaction capability 由一个定义同时派生工具名、描述、schema 和 dispatch kind,作为后续统一 Tool Registry 的窄入口。现有 Runtime Store、Tool Host、Goal、delegation、sandbox、revision、verification、finalization 和 exactly-once 保持自研且不迁入 Prompt 或 Skill;本轮不引入 Pi Node sidecar,也不宣称已完成全量工具 registry、PromptSection 或 Cargo crate 拆分。 - Provider 兼容:真实 OpenAI-compatible smoke 发现部分网关会在纯文本回复中返回 `tool_calls: null`;`platform-llm` 将该字段按缺省空列表解析,并保留真实工具调用数组语义。 - 验证:interaction parser `7/7`、swarm CLI `39/39`、Runner/config 门禁回归和 `platform-llm` null-tool-calls 回归通过。隔离 AppData 的真实 Provider 连续验证了身份直接回复、否定执行的架构解释、模型选择 `project_location` 和模型选择 `runtime_execute`;执行轮产生 `[已投递]` 后以 `turn.report outcome=settled`、busy/pending/reconciliation 均为 `0` 收束。 @@ -8239,6 +8198,7 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在 - 时序:steer durable 入队后不先通知 Runner。`false` 或判定失败才调用只唤醒的 `runtime.steer`;`true` 直接调用 `runtime.interrupt_for_steer_decision`。后者必须读取已持久化判定,并只中断 `appliedSteerCursor < steer.sequence` 的旧 Provider;新规划 Provider、工具和外部副作用不可被误杀。 - 失败:判定调用、协议解析或持久化失败时公开回复“继续当前任务”,在下一安全边界应用 steer,绝不退化为默认中断。External Runner 与本地进程内执行保持同一语义。 - 关联:`docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md` V1.53,以及 `docs/project-memory/shared-memory/pitfalls.md` 的“Supervisor steer 不能只有内部排队事件”。 +- 2026-09-23 更新(已被「退役AGC项目对话斜杠命令与终端swarm chat入口」取代):`runtime.interrupt_for_steer_decision`、`steer_decision` LLM 判定链与 `steer_game_creator_agent_runtime_task` 命令已整体删除;steer 仍 durable 入队,但只保留 `runtime.steer` 唤醒路径,不再存在条件中断旧 Provider 的分支。 ## 2026-08-10 普通图片废弃 assetKind 的迁移与写入门禁 @@ -8462,7 +8422,7 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在 - 权限边界:开放的是 `regenerate / registered resources / playtest` 等产品语义,不是原始最高权限。`regenerate` 只由当前请求最新一条原始 User 消息授权并绑定客户端稳定 `clientTurnId`;模型参数、MCP 自动批准和缺失 clientTurnId 都失败关闭。授权输入先对完整原文做 Unicode NFKC 与撇号规范化,随后整串必须完整匹配审核过的独立立即执行指令,只允许句号/感叹号收尾;不得剥离引号、方括号或代码片段,动作前后也不得携带 brief、条件、否定、选择、确认、费用、延迟或其它文本。复杂风格需求先单独描述,再由下一条独立确认消息授权,不能用开放式 deny 词表推断付费同意。同一进程重复水合相同 stable turn 时,“回合仍在运行”只作为非终态占用提示,不得以该 turn 的稳定 assistant messageId 持久化并覆盖原执行结果。DirectProject 的 cwd、sandbox writable root 与文件批准根只允许 canonical 且非 symlink/reparse point 的真实 `game/`,canonical 项目根的原生 OS 路径字节和权威 manifest `projectId` 经域标签及独立长度前缀编码后共同绑定连接池与 thread 身份;项目根、`assets/`、`.agent/` 不可写,网络关闭,命令、MCP 扩权和额外权限批准全部拒绝。受控 `agc_tools` 只在客户端内部从同一真实 `game/` cwd 反查已校验的 canonical 项目根,不把项目根加入 Codex writable roots。Codex 不获得任意 Tauri invoke、Token/Key/Cookie;`resources` 也只投影稳定身份与相对路径,不返回 prompt、provider route、URL 或绝对路径。 - Direct 恢复 claim:同一 App 实例重复水合相同 stable turn 并收到“仍在运行”时,必须释放该 `projectPath + clientTurnId` 的恢复 claim,且不得写稳定 assistant 终态。后续显式刷新对话可按原身份重新读取或续跑;不新增无界自动重试。 - 严格图集崩溃收口:workflow 在严格图集调用前先持久化 `strictSpritesheetPending` 并冻结底层严格事务覆盖的九项旧合同身份;旧路径可精确冻结为缺失。Provider 完成结果先绑定原 retained stage ledger。恢复在同一项目锁内对账严格事务;只有新九项合同、规范图/背景图替换锚点与 retained spritesheet result 三者一致才补写 `completed`,旧九项合同才允许补偿。旧合同判定、写 `compensating`、恢复两项素材与登记、回读和清锚点必须在同一项目锁内,重启已有 `compensating` 也重新判定;第三种混合、漂移或 foreign result 状态进入 reconciliation。不能在主图集与四切片已整体提交后仍按两文件 rollback 制造混合包;若中断前阶段告警尚未进入 durable completed result,恢复结果追加“原阶段告警无法完整重放”的明确 warning,不静默清空。 -- Direct 对话恢复从新到旧扫描全部合法 User 回合,遇到较新已回答回合继续向前,不得丢失更早未回答回合。成功返回时 Rust 已先持久化 assistant,前端冗余 append 失败也不得重跑 Provider;普通错误终态的显式 append 失败后,恢复 claim 必须保持到 React fallback writer 对同一稳定 assistant messageId 的写入明确成功或失败,不能在 writer 尚在途时按旧 `/history` 快照重跑。fallback 成功后释放 claim;fallback 失败时跳过该 writer 的无界迟到重试并释放 claim,后续显式 `/history` 才可复用原稳定 `clientTurnId`。终态收敛后删除 claim,避免长会话无界增长。 +- Direct 对话恢复从新到旧扫描全部合法 User 回合,遇到较新已回答回合继续向前,不得丢失更早未回答回合。成功返回时 Rust 已先持久化 assistant,前端冗余 append 失败也不得重跑 Provider;普通错误终态的显式 append 失败后,恢复 claim 必须保持到 React fallback writer 对同一稳定 assistant messageId 的写入明确成功或失败,不能在 writer 尚在途时按旧会话快照重跑。fallback 成功后释放 claim;fallback 失败时跳过该 writer 的无界迟到重试并释放 claim,后续显式重新加载对话才可复用原稳定 `clientTurnId`。终态收敛后删除 claim,避免长会话无界增长。 - 正式资源提交结算遵守同一顺序:阶段三 commit 成功后先持久化 `asset-commit-settlement-pending`,恢复器幂等补齐 `asset-durable-committed` 公开投影与 staging revision,再发布私有终态;公开投影已经存在时不得重复增加草稿 revision。恢复必须把私有回执与阶段三 commit ledger、transaction journal、manifest 资产和事件 payload 的完整身份绑定,任一错配都保留 pending 并失败关闭。回归同时覆盖三个 durable write cut,以及私有回执、commit ledger、journal 错配。 ## 2026-08-24 AGC Direct 抠图语义工具 @@ -8586,7 +8546,7 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在 - 应用级日志持续写入 Tauri AppData 并滚动;报告系统完全忽略项目 `.agent/logs`、源码、prompt、配置、项目产物和截图。用户只可补充文字描述。 - 用户点击独立“报告问题”面板并确认后,批量提交当前进程事件和可取消的脱敏应用日志;失败只允许当前进程手动再次提交。 - 上传接口为登录态 `/api/error-reports`,后台新增 error-reports Tab、专用文件化诊断包、状态与受控下载;管理员查看/下载进入审计链路。 -- `/bug-report` 仅作为打开该面板的快捷入口,追加简短提示,不再生成包含项目、run 或截图口径的缺陷模板。 +- “报告问题”入口只打开该面板并追加简短提示,不再生成包含项目、run 或截图口径的缺陷模板。 - 2026-08-31 追加:事件 DTO 精简为 `eventId/fingerprint/source/message/stack/occurredAt/count`,提交请求携带 `submissionId` 做幂等。归档固定为 `events.jsonl`,服务端使用 `agc/error-reports/v1/{batchId}.zip` 私有 OSS key;元数据只保留 batch、用户、状态、大小、SHA-256 和 OSS key,事件正文/说明/日志从归档读取。OSS 不可用或上传失败时不写数据库,客户端可重新提交。 - 2026-09-01 追加:`application.log` 不再写结构化错误事件;Rust `app_log!` 和 WebView console 都写入普通文本 raw log,结构化事件仅保留在当前进程内,提交时才生成 ZIP 内的 `events.jsonl`。 - 2026-09-01 review 收口:错误报告修复详情请求竞态、下载 anchor 生命周期、客户端采集脱敏/指纹降级与 4xx 噪声、用户级幂等隔离、`agc` 私有 OSS 前缀越权、日志读取链接检查、ZIP 同名日志和元数据/归档清理一致性;同步在 `review.txt` 标注仍需产品/运维决定的架构项。 @@ -8650,7 +8610,7 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在 - 每个 Genarrative 用户在认证成功后都必须幂等准备独立 Router 账号:api-server 使用管理员 Token 创建随机密码普通用户,查询用户 ID,设置用户 `group=taonier`,登录、创建或复用固定标识 `agc_auto_generate` 的无限额度 Token(Token/API Key 使用 `default` 分组;发现旧 Token 为其它分组时先更新为 `default`)并签发 API Key。Router 账号用户名、随机密码、access token(如需)和 API Key 作为一个服务端加密 bundle 保存到 `llm_router_account.credential_ciphertext`,脱敏账号信息和 API Key 核心字段保存到 `llm_router_account`;客户端和普通用户永远不可见 Router Key。管理员 Token 仅存在 api-server 私有配置,不写入数据库或日志;Router 凭据只来源于这条正式账号流程。 - 该账号 provisioning 使用持久 saga 状态:远端注册、登录、token 或 Key 签发结果不确定时进入 `unknown` / `reconciliation_required`,禁止重复注册;远端 Key 已确定签发但本地 `llm_router_account` 写入失败时保持 `key_issued`,后续使用确定 key id 重试落库。Router 确定返回 401/403 时撤销当前 Key 并把账号状态置为 `retryable`,复用已保存的账号密码重新签发替代 Key。 - AGC 调用固定为客户端 access token -> api-server -> Router。计费读取账号 `used_quota`,每 50000 quota 扣 1 泥点,美元数值乘 10、不乘汇率。首次模型调用前以当前累计额度完整建立免追扣基线,之后调用前后同步;扣钱包、写 `llm_router_consume` 流水与推进已结算额度同事务完成。小数和余额不足未支付部分继续累计,失败或重复同步不推进已结算额度,不使用本地 WAL 或余数队列。完整合同见 `docs/technical/【技术方案】LLM累计额度结算-2026-09-05.md`。 -- AGC 状态面收口:Tauri `check_game_creator_llm_config`、`/llm-status` 与 `/llm-routes` 只返回账号凭据状态、官方路由锁定状态和运行参数;不序列化 Router 地址、模型、协议名或任何密钥/凭据字段,内部固定路由仅留在运行时配置与服务端代理中。 +- AGC 状态面收口:Tauri `check_game_creator_llm_config` 只返回账号凭据状态、官方路由锁定状态和运行参数;不序列化 Router 地址、模型、协议名或任何密钥/凭据字段,内部固定路由仅留在运行时配置与服务端代理中。 ## 2026-09-01 LLM Router provisioning 环境隔离与测试门禁 diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index d269c8c32..017035200 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -1,5 +1,17 @@ # 踩坑与排障记录 +## 最近项目一次失败会被钉成终态 + +- **现象**:AGC 卡住一次后,项目列表每一行都显示「检查失败 + 待识别」,首页「最近项目」变成「暂无最近项目」;现场在后端恢复后逐条复跑 `inspect_local_project_directory`(8 个项目)全部 0ms 成功,界面仍然全红(issue #490)。 +- **原因**:单次检查的 5s 超时被吞成 `null` 写入状态表,而刷新用的增量投影是 `next[path] = current[path] ?? null`,把失败结果原样搬进下一轮;effect 只依赖列表与刷新计数器,既没有重试也没有 focus/visibility 重查。于是一次抖动会让整张列表永久停在失败态,首页同时被 `canOpen` 过滤清空。 +- **处理**:失败就地重试一次(300ms);失败结果不进新投影;一轮仍有可重试失败时按 15s / 45s / 120s 重跑整表(上限 3 次,失败集合变化即重置预算)。提权/权限类失败(`DACL`、`权限`、`error 5`、`安全对象不属于当前用户`、`特权`、`1300`、`AGC ACL 提权修复未成功`)按不可重试处理,在用户主动打开/新建项目或重命名刷新之前跳过——否则「提权被拒 → 300ms 后重试」会自己驱动 UAC 反复弹窗。 +- **验证**:`apps/ai-game-creator-shell/tests/recentProjectsHook.test.tsx` 的三条用例(「单次失败就地重试」「失败不跨轮保留」「提权类失败不重试」),改前代码上前两条必挂;`tests/appSurface/home.suite.ts` 的失败态断言改为等待最终状态。 +- **关联**:`apps/ai-game-creator-shell/src/features/app-shell/useRecentProjects.ts`、`src-tauri/src/config.rs`。 + +## 策划 Agent 提示词中的相对路径不要当作内部实现删去 + +`project/...` 是 Agent 读写策划工作区的目标路径,`resources/...` 是查找内置分册、模板和例子的资源定位;即使阶段上下文也注入了同一产物路径,提示词里的路径仍是 Agent 需要的契约。清理宿主实现细节时不要误删这些相对路径,具体用法见[策划 Agent 路径说明](../../technical/【技术方案】策划Agent生产迁移与工作区浏览-2026-09-10.md#6-阶段与提示词注入)。 + ## AGC 素材直传的 OSS 权限必须同步到 Native shell 契约检查 - **现象**:Native shell CI 在 `check:native-shells:contract` 的 HTTP scope 检查失败,尚未准备 Rust 缓存;后续导出步骤正常退出但实际跳过,导致 master 缓存产物缺组、自动镜像刷新等待。 @@ -825,6 +837,7 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/` - 并发边界:steer 入队、Runner `runtime.steer` 通知都不得直接触发 Provider interrupt。Codex app-server 的判定使用独立节点,不能等待主节点 turn 锁;判定为 true 后也只能中断 `appliedSteerCursor < steer.sequence` 的旧 Provider 请求,已经消费该 steer 后启动的新请求不可被误杀。已经开始的工具和外部动作不强杀,完成 observation 后再消费 steer。 - 验证:真实 mock LLM 回归必须覆盖状态询问回复且 `interruptCurrentProvider=false`;持久重放只保留一条语义回复;steer 入队后旧 Provider 继续运行,判定为 true 后才中断;新规划 Provider 的 cursor 已包含该 steer 时即使旧判定为 true 也不能中断。前端同秒多条消息保持“用户补充 → 判断提示/语义回复”的关联顺序。 - 关联:`apps/ai-game-creator-shell/src-tauri/src/agent/interaction.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/steering.rs`、`apps/ai-game-creator-shell/src-tauri/src/runner/dispatch.rs`、`apps/ai-game-creator-shell/src/features/agent-runtime/model.ts`。 +- 2026-09-23 更新:`agent/interaction.rs` 与 `steer-decision` LLM 判定链已整体删除,本条中「判定 LLM / `interruptCurrentProvider` / 只中断旧 cursor」的实现细节仅作历史记录;现役语义是 steer durable 入队后由 `runtime.steer` 唤醒,并在下一安全边界应用。 ## Jenkins 异步备份不能用 nohup 脱离作业 @@ -4431,9 +4444,9 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/` - 现象:`npm run agc:test:chat` 在进入聊天前报“Agent Runner 版本与当前客户端不一致,但旧 Runner 仍有任务,暂不能重启”;正式客户端仍能看到自己的待确认或委派任务,重复执行测试也持续失败。 - 原因:Runner 复用身份同时绑定协议版本和当前可执行文件 SHA-256。`cargo run` 重新编译后的 debug 二进制与正在运行的 release Runner 指纹不同,而旧入口只隔离测试项目、仍把正式 AppData 直接传给 CLI,于是测试会向正式 endpoint 发升级探测。正式 Runner 有 pending action、Provider sidecar、进程会话或非终态队列时拒绝退出是正确的安全门禁,不能通过强退或放宽 idle 判定让测试通过。 -- 处理:正式 AppData 只作只读配置来源。每次人工测试在系统临时根创建 `0700` sentinel 隔离目录,只把主配置和可选 local overlay 私有复制为 `0600` 普通文件;不得复制 endpoint、lock、`.previous` 或其它状态。LLM 检查与 Swarm CLI 全部使用隔离目录。退出时通过内部 CLI 请求 `runner.shutdown_if_idle`,确认隔离 endpoint 消失后才删除配置;仍有任务或无法确认退出时同时保留测试项目和隔离配置并报告路径。正式 Runner 的 PID、bootId、端口和 executable fingerprint 必须保持不变。 +- 处理:正式 AppData 只作只读配置来源。每次人工测试在系统临时根创建 `0700` sentinel 隔离目录,只把主配置和可选 local overlay 私有复制为 `0600` 普通文件;不得复制 endpoint、lock、`.previous` 或其它状态。LLM 检查与端到端测试入口全部使用隔离目录。退出时通过内部 CLI 请求 `runner.shutdown_if_idle`,确认隔离 endpoint 消失后才删除配置;仍有任务或无法确认退出时同时保留测试项目和隔离配置并报告路径。正式 Runner 的 PID、bootId、端口和 executable fingerprint 必须保持不变。 - 验证:单元测试覆盖私有 inode、权限、local overlay、禁止复制 endpoint/lock/备份、符号链接拒绝、sentinel 清理和 endpoint 存在时拒绝删除;真实 smoke 使用隔离 AppData 启动并收束空闲 Runner,前后比较正式 endpoint 身份且确认正式 PID 存活,再检查本轮 `/tmp` 项目和隔离配置均已清理。 -- 关联:`apps/ai-game-creator-shell/scripts/agent-swarm-test-chat.mjs`、`apps/ai-game-creator-shell/tests/agentSwarmTestEntry.test.ts`、`apps/ai-game-creator-shell/src-tauri/src/runner/client.rs`、`apps/ai-game-creator-shell/src-tauri/src/cli.rs`。 +- 关联:`apps/ai-game-creator-shell/src-tauri/src/runner/client.rs`、`apps/ai-game-creator-shell/src-tauri/src/cli.rs`。 ## Swarm 队列 busy 不能直接当成 canonical run 可 steer @@ -4441,7 +4454,7 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/` - 原因:旧 `runtime_is_busy` 同时包含当前 state 和队列汇总,调用方看到 `task_queue.pending > 0` 后仍从 canonical state 反推 steer、失败扫描和 turn report 的 runId;取消 tombstone 还会让恢复扫描在处理 A 后无条件跳过 B。底层拒绝 terminal steer 和保留 A 的真实失败历史都是正确行为,不能通过放宽门禁或删除历史记录修复。 - 处理:保留 queue busy 用于 Runner 存活判断,另由 Runtime 协议层提供唯一 steerable 判定。start mutation 返回实际 `acceptedRunId`,CLI 以它建立不可变 turn baseline;失败、reconciliation、用户交互、收束和报告只观察该 run。canonical 已推进到后续 run 时从 task journal 读取目标 run 的最终记录。旧 cancelled canonical 若仍有 pending 且无 running,恢复扫描跳过旧 run 的 pending action 恢复,直接启动队首 pending。若输入与已落盘 pending task 及最后一条 user 消息相同,则只观察原 run。Goal 路径也必须核对同一 Agent、Session、runId、Run Profile 和 steerable 状态。连续 run 的回复必须按确定性 finalization message ID 过滤;历史 specialist 失败必须以 `(agentId, runId)` 为键读取完整 journal,不能让滞后的非失败 state 删除 journal 已记录的失败;报告计数也不能退回 `recent_tasks` 的 12 条窗口。 - 验证:构造 cancelled run A、保留 A cancel tombstone、pending run B 和单份已落盘用户消息,证明恢复后 B 进入 running 并完成且 conversation 不重复。另覆盖观察 B 时忽略 A 及 A 子任务失败、观察 A 时仍正常失败、B 完成后 canonical 已推进到 C 仍可从 journal 收束 B、`turn.report.parentRunId` 始终为 baseline,以及 expected Goal runId 不一致时不选中目标。 -- 关联:`apps/ai-game-creator-shell/src-tauri/src/swarm_cli/turn_dispatch.rs`、`apps/ai-game-creator-shell/src-tauri/src/swarm_cli/terminal_classification.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/steering.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/recovery_scan.rs`。 +- 关联:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/steering.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/recovery_scan.rs`。 ## 2026-07-25 autonomous-game-build 不能只检查 game/index.html 就宣称正式项目完成 @@ -4456,9 +4469,9 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/` ## 终端真实测试不能混用配置参数、stdin EOF 和持续预览 -- 现象:开发者第一次运行 `agc:test:chat` 时必须先打开 GUI 才能配置 Provider;无 TTY 的脚本可能在 stdin 立即 EOF 后零任务成功退出,或者任务已经完成却继续等待 preview 的 `Ctrl+C`,导致自动化看似卡死。若为图省事增加 `--api-key`,密钥还会进入 shell history 和进程列表。 +- 现象:开发者第一次运行真实 E2E 聊天测试时必须先打开 GUI 才能配置 Provider;无 TTY 的脚本可能在 stdin 立即 EOF 后零任务成功退出,或者任务已经完成却继续等待 preview 的 `Ctrl+C`,导致自动化看似卡死。若为图省事增加 `--api-key`,密钥还会进入 shell history 和进程列表。 - 原因:把首次配置、手工多轮聊天、单轮真实测试和持续试玩当成同一个交互生命周期;同时让 GUI 与 CLI 使用不同配置入口,或把 EOF 既解释为“提交当前需求”又解释为“没有输入”,会让退出语义随调用环境漂移。 -- 处理:GUI 与 `npm run agc:config` 共用系统 AppData `game-creator.config.json`,终端隐藏输入 API Key 并禁止 `--api-key`;更新时保留 `agentLlm`、`editorApi`、`mcpServers` 等其它配置,POSIX 权限维持目录 `0700` / 文件 `0600` 并原子替换。显式 `--config-dir` 必须以 `world.genarrative.ai-game-creator` 为独立叶目录,不能让向导对 `/tmp`、AppData 根或共享目录整体 chmod / 重建 DACL。隐藏输入调用 `stdin.resume()` 后必须记住原 pause 状态,在成功、取消、异常和 `SIGINT / SIGTERM / SIGHUP` 路径恢复 raw mode 并 `pause()`,信号恢复后重发;只移除 `data` listener 会让 `--configure-only`、配置检查失败或 Ctrl+C 保持活动 stdin。缺配置时仅 TTY 人工会话可询问进入向导,非 TTY 立即失败并提示配置命令。 +- 处理:GUI 与 `npm run agc:config` 共用系统 AppData `game-creator.config.json`,终端隐藏输入 API Key 并禁止 `--api-key`;更新时保留 `agentLlm`、`editorApi`、`mcpServers` 等其它配置,POSIX 权限维持目录 `0700` / 文件 `0600` 并原子替换。显式 `--config-dir` 必须以 `world.genarrative.ai-game-creator` 为独立叶目录,不能让向导对 `/tmp`、AppData 根或共享目录整体 chmod / 重建 DACL。隐藏输入调用 `stdin.resume()` 后必须记住原 pause 状态,在成功、取消、异常和 `SIGINT / SIGTERM / SIGHUP` 路径恢复 raw mode 并 `pause()`,信号恢复后重发;只移除 `data` listener 会让 配置检查失败或 Ctrl+C 保持活动 stdin。缺配置时仅 TTY 人工会话可询问进入向导,非 TTY 立即失败并提示配置命令。 - Windows 密钥复制:`mode: 0o600` 和 POSIX `chmod` 在 Windows 上不能代替 DACL。隔离 AppData 目录必须先设置仅当前用户、禁止继承的 DACL;目标配置文件先以空文件创建并收紧 DACL,之后才允许把 API Key 字节写入。先 `copyFile` 再依赖 Rust 只读检查或事后收紧会留下密钥暴露窗口,也可能因继承 ACL 不满足 Runtime 合同而在首次 `--llm-status` 失败。 - Windows PowerShell 参数:不要把 DACL 目标路径和目录标记直接追加在 `powershell.exe -Command