合并 master 最新变更
Project CI / Repository checks (pull_request) Successful in 6m28s
Project CI / Frontend tests (pull_request) Successful in 10m58s
Project CI / Backend tests (pull_request) Successful in 23m10s
Project CI / Native shell tests (pull_request) Successful in 25m43s

保留共享组件库与 UI 组件复用规范变更

整合 AGC 自主构建与项目工具链最新变更

合并 decision-log 两侧有效决策记录
This commit is contained in:
2026-08-31 14:06:51 +08:00
507 changed files with 20676 additions and 6155 deletions
+9
View File
@@ -362,6 +362,10 @@ export interface AdminEditorAssetPayload {
assetKind?: string | null;
generationInputs?: Record<string, unknown> | null;
sourceResourceId?: string | null;
sourceImageSrc?: string | null;
sourceObjectKey?: string | null;
sourceAssetObjectId?: string | null;
sourceLabel?: string | null;
thumbnailSrc?: string | null;
generationCostMudPoints: number;
createdAt: string;
@@ -404,6 +408,11 @@ export interface AdminEditorShowcaseAssetPayload {
model?: string | null;
provider?: string | null;
taskId?: string | null;
sourceResourceId?: string | null;
sourceImageSrc?: string | null;
sourceObjectKey?: string | null;
sourceAssetObjectId?: string | null;
sourceLabel?: string | null;
assetKind?: string | null;
generationInputs?: Record<string, unknown> | null;
thumbnailSrc?: string | null;
@@ -25,6 +25,11 @@ export interface AdminPreviewableEditorAsset {
thumbnailSrc?: string | null;
imageSequenceFrames?: AdminEditorImageSequenceFramePayload[] | null;
imageSequenceDurationMs?: number | null;
sourceResourceId?: string | null;
sourceImageSrc?: string | null;
sourceObjectKey?: string | null;
sourceAssetObjectId?: string | null;
sourceLabel?: string | null;
}
export function AdminEditorAssetThumbnail({
@@ -70,6 +75,15 @@ export function AdminEditorAssetPreviewDialog({
token: string;
onClose: () => void;
}) {
const sourceEntry = entry.sourceImageSrc?.trim()
? {
assetId: entry.sourceResourceId ?? `${entry.assetId}-source`,
label: entry.sourceLabel?.trim() || '原图',
imageSrc: entry.sourceImageSrc,
objectKey: entry.sourceObjectKey,
}
: null;
return (
<div className="admin-confirm-backdrop" role="presentation">
<section
@@ -91,13 +105,19 @@ export function AdminEditorAssetPreviewDialog({
<X size={17} aria-hidden="true" />
</button>
</div>
{sourceEntry ? (
<div className="admin-asset-query-source-preview">
<h4>原图</h4>
<AdminEditorAssetPreviewMedia entry={sourceEntry} token={token} />
</div>
) : null}
<AdminEditorAssetPreviewMedia entry={entry} token={token} />
</section>
</div>
);
}
function AdminEditorAssetPreviewMedia({
export function AdminEditorAssetPreviewMedia({
entry,
token,
}: {
@@ -16,6 +16,7 @@ import type {
} from '../api/adminApiTypes';
import {
AdminEditorAssetPreviewDialog,
AdminEditorAssetPreviewMedia,
AdminEditorAssetThumbnail,
} from '../components/AdminEditorAssetMedia';
import { AdminUserReferenceButton } from '../components/AdminUserReferenceButton';
@@ -561,14 +562,34 @@ function AdminAssetDetailDialog({
</button>
</div>
<div className="admin-asset-query-detail-layout">
<button
className="admin-asset-query-thumb-button admin-asset-query-detail-thumb-button"
title="预览素材"
type="button"
onClick={() => onPreview(entry)}
>
<AdminEditorAssetThumbnail entry={entry} token={token} />
</button>
<div className="admin-asset-query-detail-media">
{entry.sourceImageSrc?.trim() ? (
<div className="admin-asset-query-detail-media-card">
<h4>原图</h4>
<AdminEditorAssetPreviewMedia
entry={{
assetId:
entry.sourceResourceId ?? `${entry.assetId}-source`,
label: entry.sourceLabel?.trim() || '原图',
imageSrc: entry.sourceImageSrc,
objectKey: entry.sourceObjectKey,
}}
token={token}
/>
</div>
) : null}
<div className="admin-asset-query-detail-media-card">
<h4>当前结果</h4>
<button
className="admin-asset-query-thumb-button admin-asset-query-detail-thumb-button"
title="预览素材"
type="button"
onClick={() => onPreview(entry)}
>
<AdminEditorAssetThumbnail entry={entry} token={token} />
</button>
</div>
</div>
<dl className="admin-info-list admin-detail-list">
<AdminInfoItem label="作者">
<div className="admin-inline-identity">
@@ -17,6 +17,7 @@ import type {
} from '../api/adminApiTypes';
import {
AdminEditorAssetPreviewDialog,
AdminEditorAssetPreviewMedia,
AdminEditorAssetThumbnail,
} from '../components/AdminEditorAssetMedia';
import { AdminUserReferenceButton } from '../components/AdminUserReferenceButton';
@@ -709,18 +710,38 @@ function AdminShowcaseDetailDialog({
</button>
</div>
<div className="admin-asset-query-detail-layout">
<button
className="admin-asset-query-thumb-button admin-asset-query-detail-thumb-button"
title="预览素材"
type="button"
onClick={() => onPreview(entry)}
>
<AdminEditorAssetThumbnail
entry={entry}
token={token}
altPrefix="精选素材"
/>
</button>
<div className="admin-asset-query-detail-media">
{entry.sourceImageSrc?.trim() ? (
<div className="admin-asset-query-detail-media-card">
<h4>原图</h4>
<AdminEditorAssetPreviewMedia
entry={{
assetId:
entry.sourceResourceId ?? `${entry.assetId}-source`,
label: entry.sourceLabel?.trim() || '原图',
imageSrc: entry.sourceImageSrc,
objectKey: entry.sourceObjectKey,
}}
token={token}
/>
</div>
) : null}
<div className="admin-asset-query-detail-media-card">
<h4>当前结果</h4>
<button
className="admin-asset-query-thumb-button admin-asset-query-detail-thumb-button"
title="预览素材"
type="button"
onClick={() => onPreview(entry)}
>
<AdminEditorAssetThumbnail
entry={entry}
token={token}
altPrefix="精选素材"
/>
</button>
</div>
</div>
<dl className="admin-info-list admin-detail-list">
<AdminInfoItem label="作者">
<div className="admin-inline-identity">
+37
View File
@@ -1655,6 +1655,18 @@ button:disabled {
width: min(100%, 860px);
}
.admin-asset-query-source-preview {
margin: 0 20px 16px;
padding-bottom: 16px;
border-bottom: 1px solid #e2e8f0;
}
.admin-asset-query-source-preview h4 {
margin: 0 0 8px;
color: #64748b;
font-size: 13px;
}
.admin-asset-query-prompt-dialog .admin-panel-heading > div,
.admin-asset-query-detail-dialog .admin-panel-heading > div,
.admin-asset-query-preview-dialog .admin-panel-heading > div {
@@ -1683,6 +1695,27 @@ button:disabled {
align-items: start;
}
.admin-asset-query-detail-media {
display: grid;
gap: 14px;
}
.admin-asset-query-detail-media-card {
display: grid;
gap: 7px;
}
.admin-asset-query-detail-media-card h4 {
margin: 0;
color: #64748b;
font-size: 13px;
}
.admin-asset-query-detail-media-card .admin-asset-query-preview-media {
width: 220px;
height: 220px;
}
.admin-asset-query-detail-thumb-button .admin-asset-query-thumb {
width: 220px;
height: 220px;
@@ -2332,6 +2365,10 @@ button:disabled {
justify-self: center;
}
.admin-asset-query-detail-media {
justify-items: center;
}
.admin-dashboard-tabs {
width: 100%;
}
+5 -2
View File
@@ -1,13 +1,16 @@
{
"name": "@genarrative/ai-game-creator-shell",
"private": true,
"version": "0.1.8",
"version": "0.1.12",
"type": "module",
"scripts": {
"dev": "node scripts/start-tauri-dev.mjs",
"dev-server": "node scripts/start-dev-server.mjs",
"dev-stack": "node scripts/start-dev-stack.mjs",
"build": "npm --prefix ../.. exec tauri -- build",
"skill-pack:check": "node scripts/check-skill-pack.mjs",
"skill-pack:sync": "node scripts/check-skill-pack.mjs --write",
"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",
@@ -31,7 +34,7 @@
"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",
"agent-runtime:steer-real-e2e": "node scripts/agent-runtime-steer-real-e2e.mjs",
"agent-runtime:steer-runner-kill-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite steer-runner-kill",
"typecheck": "tsc -p tsconfig.json --noEmit && node scripts/check-config.mjs"
"typecheck": "tsc -p tsconfig.json --noEmit && npm run skill-pack:check && node scripts/check-config.mjs"
},
"dependencies": {
"@cubone/react-file-manager": "^1.35.0",
@@ -26,6 +26,10 @@ const wrapperSuite =
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) {
@@ -77,6 +81,16 @@ function deterministicRuntimeConfig(apiKey, provider) {
};
}
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');
@@ -1909,6 +1923,12 @@ async function runE2e(options) {
);
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`,
@@ -1924,7 +1944,11 @@ async function runE2e(options) {
if (options.keepProject) childArgs.push('--keep-project');
childResult = await runChild(
childArgs,
withLoopbackNoProxy({ ...process.env, NO_COLOR: '1' }),
withLoopbackNoProxy({
...process.env,
NO_COLOR: '1',
[platformSessionFixtureEnv]: fixturePath,
}),
);
childReport = parseChildReport(childResult);
} catch (error) {
@@ -100,6 +100,13 @@ 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() ||
@@ -498,6 +505,120 @@ 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,
@@ -769,6 +890,10 @@ 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) => {
@@ -1874,6 +1999,31 @@ 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() {
@@ -1913,7 +2063,22 @@ export async function removeIsolatedSuiteAppData() {
} catch (error) {
ownershipError = error;
}
await fs.rm(appDataDir, { recursive: true, force: false });
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;
}
state.runtimeConfigDir = state.options.configDir;
try {
await verifySourceConfigLinkCountsRestored();
@@ -84,6 +84,14 @@ 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
// 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) {
environment.GENARRATIVE_AGC_PLATFORM_SESSION_FIXTURE =
state.isolatedRunner.platformSessionFixturePath;
}
return isSupervisorSwarmTransientRetrySuite()
? withLoopbackNoProxy(environment)
: environment;
@@ -241,7 +241,7 @@ export const autonomousCompletionContractSchemaVersion =
'game-creator-autonomous-completion-contract.v2';
export const autonomousPlaytestReceiptSchemaVersion =
'game-creator-autonomous-playtest-receipt.v1';
'game-creator-autonomous-playtest-receipt.v2';
export const autonomousGameBuildRunProfile = 'autonomous-game-build';
@@ -938,6 +938,10 @@ export class BlockedError extends Error {
export const isolatedRunnerState = {
appDataDir: null,
platformSessionFixturePath: null,
platformSessionFixtureSourcePath: null,
platformSessionFixtureSha256: null,
platformSessionFixturePreviousEnv: undefined,
ownerToken: null,
createdAt: 0,
current: null,
@@ -1220,6 +1220,11 @@ export async function validateSupervisorAutonomousPlayableEvidence(
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,
@@ -143,7 +143,12 @@ export const usage = `用法:
--plan 走「做方案」立项策划入口,不做游戏,不做产物验收和试玩
--timeout-minutes <分钟> 设置本次执行期限;自动任务默认 50 分钟,--plan 默认 6 分钟,手工模式默认不限时
--dry-run 只检查目录发现和项目准备,不启动 LLM
-h, --help 显示帮助`;
-h, --help 显示帮助
环境变量:
AGC_PLAN_GDD_DECISION 审批卡自动应答动作,默认 approve;revise/reject 必须
同时用 AGC_PLAN_GDD_COMMENT 给出真实修改意见
AGC_PLAN_GDD_COMMENT revise/reject 的意见原文`;
function readOptionValue(args, index, option) {
const value = args[index + 1]?.trim();
@@ -911,12 +916,15 @@ export function parseSettledSwarmTurnReport(output) {
async function runCapturedCargo(
cliArguments,
setActiveChild,
{ timeoutMs = null, label = 'Cargo 子命令' } = {},
{ timeoutMs = null, label = 'Cargo 子命令', stdin = null } = {},
) {
const child = spawnChild(cargoCommand, buildCargoCliArguments(cliArguments), {
stdio: ['ignore', 'pipe', 'pipe'],
stdio: [stdin === null ? 'ignore' : 'pipe', 'pipe', 'pipe'],
});
setActiveChild(child);
if (stdin !== null) {
child.stdin.end(stdin);
}
let stdout = '';
let stderr = '';
child.stdout.setEncoding('utf8');
@@ -1007,7 +1015,9 @@ async function runTaskCargo(
let planGddApprovalPromise = null;
const startPlanGddApproval = () => {
planGddApprovalStarted = true;
console.log('[自动审批] 检测到 Fast GDD 审批位,正在提交 approve');
console.log(
`[自动审批] 检测到 Fast GDD 审批位,正在提交 ${resolvePlanGddAutoDecision().action}`,
);
planGddApprovalPromise = onPlanGddApprovalWait()
.then((value) => {
planGddApproval = value;
@@ -1842,13 +1852,30 @@ export function parsePlanGddDecisionOutput(output) {
);
}
// 审批卡是这条链路唯一的人类判据,所以自动应答只投 approve,且只在投影确实有一张
// 待决定审批时出手。revise/reject 需要一段真实的修改意见,让机器编一段等于把判据
// 换成噪声;要跑那两条分支就手工调 --plan-gdd-decide。
// 审批卡是这条链路唯一的人类判据,所以自动应答默认只投 approve,且只在投影确实有
// 一张待决定审批时出手。revise/reject 需要一段真实的修改意见,让机器编一段等于把
// 判据换成噪声——所以那两条分支只在跑的人自己用 AGC_PLAN_GDD_COMMENT 给出意见时
// 才走。手工调 --plan-gdd-decide 也能达到同样效果,但那要求 plan 根 run 仍然活着,
// 而它恰好是本进程持有的 CLI 子进程。
export function planGddAutoApprovalIsPending(state) {
return Boolean(state?.pendingApproval);
}
export function resolvePlanGddAutoDecision(env = process.env) {
const action = (env.AGC_PLAN_GDD_DECISION ?? 'approve').trim();
if (!['approve', 'revise', 'reject'].includes(action)) {
throw new Error('AGC_PLAN_GDD_DECISION 只能是 approve / revise / reject');
}
const comment = (env.AGC_PLAN_GDD_COMMENT ?? '').trim();
if (action === 'approve') return { action, comment: null };
if (!comment) {
throw new Error(
`${action} 必须同时设 AGC_PLAN_GDD_COMMENT 提供真实修改意见`,
);
}
return { action, comment };
}
async function settlePlanGddApproval(
projectPath,
runtimeConfigPath,
@@ -1875,18 +1902,21 @@ async function settlePlanGddApproval(
if (!planGddAutoApprovalIsPending(before)) {
return { decided: false, state: before };
}
const { action, comment } = resolvePlanGddAutoDecision();
const decision = await runCapturedCargo(
[
'--config-dir',
runtimeConfigPath,
'--plan-gdd-decide',
projectPath,
'approve',
action,
...(comment === null ? [] : ['--stdin']),
],
setActiveChild,
{
timeoutMs: planGddApprovalTimeoutMs,
label: 'Fast GDD 审批决定',
stdin: comment,
},
);
if (decision.code !== 0 || decision.signal) {
@@ -1926,7 +1956,7 @@ async function reportPlanGddApproval(approval) {
return;
}
console.log(
` [已批准] outcome=${approval.receipt.outcome} v${approval.receipt.decisionRef.version} 投影状态=${state.state}`,
` [已决定 ${approval.receipt.decisionRef.action}] outcome=${approval.receipt.outcome} v${approval.receipt.decisionRef.version} 投影状态=${state.state}`,
);
if (approval.recovered) {
console.log(
@@ -1533,11 +1533,13 @@ if (
}
if (
tauriConfig.version !== '0.1.8' ||
packageConfig.version !== '0.1.8' ||
cargoPackageVersion !== '0.1.8'
tauriConfig.version !== '0.1.12' ||
packageConfig.version !== '0.1.12' ||
cargoPackageVersion !== '0.1.12'
) {
throw new Error('AI game creator standard release must remain version 0.1.8');
throw new Error(
'AI game creator standard release must remain version 0.1.12',
);
}
const devServerSource = fs.readFileSync(
@@ -0,0 +1,48 @@
import process from 'node:process';
import {
inspectSkillPack,
syncSkillPackManifest,
} from './skill-pack-manifest.mjs';
const argumentsList = process.argv.slice(2);
const writeMode = argumentsList.length === 1 && argumentsList[0] === '--write';
if (
argumentsList.length > 1 ||
(argumentsList.length === 1 && argumentsList[0] !== '--write')
) {
console.error('用法:node scripts/check-skill-pack.mjs [--write]');
process.exit(1);
}
try {
if (writeMode) {
const result = syncSkillPackManifest();
if (!result.changed) {
console.log(`[skill-pack] 已是最新(version=${result.version})`);
} else {
console.log(
`[skill-pack] 已同步 ${result.mismatches.map((item) => item.name).join('、')}(version=${result.version})`,
);
}
} else {
const result = inspectSkillPack();
if (result.mismatches.length > 0) {
console.error('[skill-pack] 内容指纹与 manifest 不一致:');
for (const mismatch of result.mismatches) {
console.error(
`- ${mismatch.name}: manifest=${mismatch.expected} actual=${mismatch.actual}`,
);
}
console.error('[skill-pack] 内容变更后运行:npm run agc:skill-pack:sync');
process.exitCode = 1;
} else {
console.log(`[skill-pack] OK(version=${result.manifest.version})`);
}
}
} catch (error) {
console.error(
`[skill-pack] ${error instanceof Error ? error.message : String(error)}`,
);
process.exitCode = 1;
}
@@ -0,0 +1,49 @@
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import { fileURLToPath } from 'node:url';
import {
computeSkillContentFingerprint,
inspectSkillPack,
} from './skill-pack-manifest.mjs';
test('bundled skill pack manifest is synchronized', () => {
const result = inspectSkillPack();
assert.deepEqual(result.mismatches, []);
});
test('skill content fingerprint canonicalizes CRLF', () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agc-skill-pack-'));
try {
fs.mkdirSync(path.join(root, 'demo'), { recursive: true });
const entry = {
name: 'demo',
files: ['SKILL.md'],
};
fs.writeFileSync(path.join(root, 'demo', 'SKILL.md'), 'line 1\nline 2\n');
const lf = computeSkillContentFingerprint(root, entry);
fs.writeFileSync(
path.join(root, 'demo', 'SKILL.md'),
'line 1\r\nline 2\r\n',
);
assert.equal(computeSkillContentFingerprint(root, entry), lf);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
test('check command without arguments remains read-only', () => {
const scriptPath = path.join(
path.dirname(fileURLToPath(import.meta.url)),
'check-skill-pack.mjs',
);
const result = spawnSync(process.execPath, [scriptPath], {
encoding: 'utf8',
});
assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, /\[skill-pack\] OK/u);
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,213 @@
import crypto from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { TextDecoder } from 'node:util';
export const SKILL_PACK_SCHEMA_VERSION = 'agc-skill-pack.v1';
export const EXPECTED_SKILL_NAMES = Object.freeze([
'agc-browser-playtest',
'agc-client-projection',
'agc-project-structure',
'agc-web-game-development',
'taonier-art-assets',
]);
const utf8Decoder = new TextDecoder('utf-8', { fatal: true });
const defaultRoot = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
'../src-tauri/resources/agc-skills',
);
function canonicalTextBytes(filePath) {
const decoded = utf8Decoder.decode(fs.readFileSync(filePath));
return Buffer.from(decoded.replaceAll('\r\n', '\n'), 'utf8');
}
export function isSafeSkillRelativePath(value) {
if (
typeof value !== 'string' ||
value.length === 0 ||
value.includes('\\') ||
value.includes(':') ||
value.startsWith('/')
) {
return false;
}
return value
.split('/')
.every(
(segment) => segment.length > 0 && segment !== '.' && segment !== '..',
);
}
function skillFilePath(rootDir, skillName, relativePath) {
if (!isSafeSkillRelativePath(relativePath)) {
throw new Error(`Skill ${skillName} 包含不安全相对路径: ${relativePath}`);
}
const target = path.resolve(rootDir, skillName, ...relativePath.split('/'));
const skillRoot = path.resolve(rootDir, skillName);
const prefix = `${skillRoot}${path.sep}`;
if (!target.startsWith(prefix)) {
throw new Error(`Skill ${skillName} 路径越过审核根目录: ${relativePath}`);
}
return target;
}
export function computeSkillContentFingerprint(rootDir, entry) {
const digest = crypto.createHash('sha256');
for (const relativePath of [...entry.files].sort()) {
const filePath = skillFilePath(rootDir, entry.name, relativePath);
const bytes = canonicalTextBytes(filePath);
digest.update(relativePath, 'utf8');
digest.update(Buffer.from([0]));
digest.update(bytes);
digest.update(Buffer.from([0]));
}
return digest.digest('hex');
}
function collectBundledFiles(rootDir) {
const files = [];
const walk = (directory, prefix) => {
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
const absolutePath = path.join(directory, entry.name);
if (entry.isSymbolicLink()) {
throw new Error(`内置 AGC Skill 不允许符号链接: ${relativePath}`);
}
if (entry.isDirectory()) {
walk(absolutePath, relativePath);
} else if (entry.isFile()) {
files.push(relativePath.replaceAll('\\', '/'));
} else {
throw new Error(`内置 AGC Skill 文件类型不受支持: ${relativePath}`);
}
}
};
walk(rootDir, '');
return files.filter((file) => file !== 'manifest.json').sort();
}
function readManifest(rootDir) {
const manifestPath = path.join(rootDir, 'manifest.json');
return {
manifestPath,
manifest: JSON.parse(fs.readFileSync(manifestPath, 'utf8')),
};
}
function validateManifestShape(rootDir, manifest) {
if (manifest?.schemaVersion !== SKILL_PACK_SCHEMA_VERSION) {
throw new Error('内置 AGC Skill 清单 schemaVersion 不受支持');
}
if (typeof manifest.version !== 'string' || manifest.version.trim() === '') {
throw new Error('内置 AGC Skill 清单缺少版本');
}
if (!Array.isArray(manifest.skills)) {
throw new Error('内置 AGC Skill 清单缺少 skills 数组');
}
const names = manifest.skills.map((entry) => entry?.name);
if (
names.length !== EXPECTED_SKILL_NAMES.length ||
[...names].sort().join('\n') !== [...EXPECTED_SKILL_NAMES].sort().join('\n')
) {
throw new Error('内置 AGC Skill 清单不等于审核白名单');
}
const declaredFiles = new Set();
const mismatches = [];
for (const entry of manifest.skills) {
if (
typeof entry.name !== 'string' ||
!Array.isArray(entry.files) ||
entry.files.length === 0 ||
!entry.files.includes('SKILL.md') ||
new Set(entry.files).size !== entry.files.length
) {
throw new Error(
`内置 AGC Skill ${entry.name ?? '<unknown>'} 元数据不完整`,
);
}
for (const relativePath of entry.files) {
if (!isSafeSkillRelativePath(relativePath)) {
throw new Error(
`内置 AGC Skill ${entry.name} 包含不安全相对路径: ${relativePath}`,
);
}
declaredFiles.add(`${entry.name}/${relativePath}`);
}
const actual = computeSkillContentFingerprint(rootDir, entry);
if (actual !== entry.sha256) {
mismatches.push({
name: entry.name,
expected: entry.sha256,
actual,
});
}
}
const bundledFiles = collectBundledFiles(rootDir);
if (
declaredFiles.size !== bundledFiles.length ||
[...declaredFiles].sort().join('\n') !== bundledFiles.join('\n')
) {
throw new Error('内置 AGC Skill 文件集合与审核清单不一致');
}
return { mismatches };
}
export function inspectSkillPack(rootDir = defaultRoot) {
const resolvedRoot = path.resolve(rootDir);
const { manifestPath, manifest } = readManifest(resolvedRoot);
const { mismatches } = validateManifestShape(resolvedRoot, manifest);
return { manifestPath, manifest, mismatches };
}
function incrementPackVersion(version) {
const match = /^(\d{4}-\d{2}-\d{2})\.(\d+)$/u.exec(version);
if (!match) {
throw new Error(
`无法自动递增 Skill pack 版本 ${version},请使用 YYYY-MM-DD.N 格式`,
);
}
return `${match[1]}.${Number(match[2]) + 1}`;
}
export function syncSkillPackManifest(rootDir = defaultRoot) {
const inspection = inspectSkillPack(rootDir);
if (inspection.mismatches.length === 0) {
return {
changed: false,
version: inspection.manifest.version,
mismatches: [],
};
}
const mismatchByName = new Map(
inspection.mismatches.map((mismatch) => [mismatch.name, mismatch.actual]),
);
const nextManifest = {
...inspection.manifest,
version: incrementPackVersion(inspection.manifest.version),
skills: inspection.manifest.skills.map((entry) =>
mismatchByName.has(entry.name)
? { ...entry, sha256: mismatchByName.get(entry.name) }
: entry,
),
};
fs.writeFileSync(
inspection.manifestPath,
`${JSON.stringify(nextManifest, null, 2)}\n`,
'utf8',
);
const verified = inspectSkillPack(rootDir);
if (verified.mismatches.length > 0) {
throw new Error('Skill pack manifest 同步后仍存在内容指纹不匹配');
}
return {
changed: true,
version: nextManifest.version,
mismatches: inspection.mismatches,
};
}
+11 -1
View File
@@ -16,6 +16,14 @@ dependencies = [
"serde_json",
]
[[package]]
name = "agent-runtime-orchestration"
version = "0.1.0"
dependencies = [
"agent-runtime-core",
"serde",
]
[[package]]
name = "ahash"
version = "0.8.12"
@@ -1695,7 +1703,7 @@ dependencies = [
[[package]]
name = "genarrative-ai-game-creator-shell"
version = "0.1.8"
version = "0.1.12"
dependencies = [
"agent-runtime-core",
"axum",
@@ -3772,6 +3780,8 @@ checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
name = "platform-agent"
version = "0.1.0"
dependencies = [
"agent-runtime-core",
"agent-runtime-orchestration",
"platform-llm",
"serde",
"serde_json",
@@ -1,6 +1,6 @@
[package]
name = "genarrative-ai-game-creator-shell"
version = "0.1.8"
version = "0.1.12"
edition = "2021"
publish = false
@@ -0,0 +1,13 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "window-chrome",
"description": "自绘标题栏允许执行当前窗口的基础控制和拖拽。",
"windows": ["client", "developer", "main", "launcher", "supervisor-chat"],
"permissions": [
"core:window:allow-close",
"core:window:allow-is-maximized",
"core:window:allow-minimize",
"core:window:allow-start-dragging",
"core:window:allow-toggle-maximize"
]
}
@@ -5,11 +5,11 @@
3. 等待子 Agent 期间不得调用 `respond_to_user`。Runtime 会通过 delegate 完成屏障保持同一父 run,回执到达后再继续。
4. 子 Agent 以问询信封退出时,决策卡由 Runtime 直接按信封原文呈现给用户,**不需要你调用任何工具**——你根本不会在那一刻被恢复。用户答完之后你才会拿到答案,届时为该原 delivery 创建且仅创建一次 continuation 委派,`continuationOfDelegationId` 与 `repairOfDelegationId` 都指向该原 delivery。`questionsSha256`、`answersSha256`、`acceptanceCriteria`、`expectedArtifacts` 四个全传 null——Runtime 会从该原 delivery 补齐权威指纹和原委派合同,你不要自己抄。子 Agent 在 continuation 里**再次**以信封退出时,对那条新 delivery 重复同一动作:「仅创建一次」约束的是单条 delivery,不是整条链,澄清预算未用尽时这个循环继续。Runtime 会在委派 task 末尾写明已用轮次与上限,不需要你自己数,也不要替它宣布预算已尽。
5. 回执 contractStatus=evidence-ready 且 GDD 已提交时,用 `file.read` 从第 1 行读到 `game/fast_gdd.md` 末尾取证,每次都传 `maxLines: 240`(上限),尽量一页读完;确实需要第二页时从上一页的下一行开始,不要重复读同一段。每次 `file.read` 的 observation 末尾都带着 `sourceAgentId` / `sourceRunId` / `sourceActionId` 三个字段,把它们原样抄成 evidence 的 `{agentId, runId, actionId}`,用一次 `agent.acceptance_update` 一并提交即可——evidence 是按这三个字段整体查回执的,回忆错任何一个都会被判成"缺少持久动作回执"。不要为了取这些字段再去查动作历史。取证完成前审批卡不会出现。
6. 用户在审批卡上选择修改或退回时,直接创建返工委派:`repairOfDelegationId` 指向原 delegationId,`runId`、`acceptanceCriteria`、`expectedArtifacts` 都传 null——Runtime 会从原 delivery 继承权威合同,不需要先 `agent.run_status` 去取再手抄。把用户原话完整附在 task 里;同一原委派只能返工一次。用户通过后只做一句简短收尾。
6. 用户在审批卡上选择修改或退回时,直接创建返工委派:`repairOfDelegationId` 指向原 delegationId,`runId`、`acceptanceCriteria`、`expectedArtifacts` 都传 null——Runtime 会从原 delivery 继承权威合同,不需要先 `agent.run_status` 去取再手抄。把用户原话完整附在 task 里。「同一原委派只能返工一次」约束的是单条 delivery,不是整条链:用户看过新稿再点一次修改,就对那条新 delivery 重复同一动作,这个循环没有次数上限——`repair_depth` 防的是 runaway agent,而每一轮修订都由用户亲手触发,人本身就是循环边界。不要替 Runtime 宣布「这是最后一次修改机会」,也不要因此把多条意见攒到一轮里改完。用户通过后只做一句简短收尾。
【转达的规则】
- 把用户答案回灌给 `project-planning` 时,逐条列出全部已确认决定,每条格式为 `[已确认] 第N轮问的是:{question 原文} | 候选项:{option1.label} / {option2.label} / {option3.label} → 用户答:{原文}`。**问题原文和三个选项标签必须带上**:`{header}` 恒为「第N轮·关键决定」,不含任何信息量;子 Agent 每轮都是全新 run,除了这段正文什么都看不到,只给它 header 和答案,「类似B」「B · 沙盒里程碑成长」这类答案就无从解读,它只能把同一件事再问一遍。用户答案原文一字不改、不归纳、不拆分、不搬轮次;任务长度接近上限时压缩你自己的说明文字和选项描述,绝不压缩用户答案、问题原文和选项标签。
- 把用户答案回灌给 `project-planning` 时,逐条列出全部已确认决定,每条格式为 `[已确认] 第N轮问的是:{question 原文} | 候选项:{option1.label} / {option2.label} / {option3.label} → 用户答:{原文}`。**问题原文和三个选项标签必须带上**:`{header}` 只写到「第N轮·当前要决定:{主题}」这一层,答案落在选项上;子 Agent 每轮都是全新 run,除了这段正文什么都看不到,只给它主题和答案,「类似B」「B · 沙盒里程碑成长」这类答案就无从解读,它只能把同一件事再问一遍。用户答案原文一字不改、不归纳、不拆分、不搬轮次;任务长度接近上限时压缩你自己的说明文字和选项描述,绝不压缩用户答案、问题原文和选项标签。
- 策划链路的澄清信封**恰好一题**,不是通用静态委派协议里的 1-3 题:`project-planning` 每轮只提一个主要决定,Runtime 也只接受一题,多于一题会在出卡时被拒。委派 task 里不要写“1-3 个结构化问题”。
- 上一条格式里的三个选项标签就是决策卡上的 A、B 和“需要原型验证”,必须原样转述、一个都不能省;B 是用户确认的 `confirmed/user_option`,不能转成默认建议。用户后续自由填写推翻了更早的决定时,你只负责把两轮答案的原文都原样带到,并说明后者更晚;怎么记进决定台账由 `project-planning` 判断,不要替它裁定哪条作废。
@@ -3,26 +3,26 @@
## 身份与边界
- 当前 run 固定为 `source=agent-delegate`、`profile=standard`,父 Agent 是 `project-supervisor`。不得伪造、改写或猜测这些 Runtime 身份。
- 你不能委派或调度其他 Agent,不能创建 isolated child,不能调用命令、进程、预览、画布、素材生成、写入/补丁/删除工具,也不能改变项目版本或审批事实。
- 你的原生工具目录只应包含 `file.read`、`file.list` 以及 Runtime 协议控制函数 `update_agent_plan`、`respond_to_user`;`user.input_request` 不属于你的工具目录。若需要用户决定,必须以终态信封首行 `AGC_NEEDS_USER_INPUT_V1` 退出本轮,下一行给出严格 JSON 信封 `{"questions":[{ ... }]}`,交由 Supervisor 转发。`questions` 恰好一个元素;元素字段只能是 `id`、`header`、`question`、`options` 四个,多写任何字段(例如 `answerFormat`)或省掉 `questions` 外壳都会被 Runtime 拒收,整条委派随即作废。`id` 是唯一 snake_case(小写字母开头,只含小写字母、数字、下划线);`header` 是决策卡标题,单行且不超过 12 字符;`question` 是决策卡正文,单行且不超过 400 字符;`options` 是 2~3 个 `{"label": ..., "description": ...}`,label 单行不超过 60 字符、description 单行不超过 240 字符。不要另起一行写答题说明或把选项复述进 `question`,作答方式由 Runtime 自己呈现。
- 你不能委派或调度其他 Agent,不能创建 isolated child,不能调用 MCP、命令、进程、预览、画布、素材生成、写入/补丁/删除工具,也不能改变项目版本或审批事实。
- 你的原生工具目录只应包含 `file.read`、`file.list` 以及 Runtime 协议控制函数 `update_agent_plan`、`respond_to_user`;`user.input_request` 不属于你的工具目录。若需要用户决定,必须以终态信封首行 `AGC_NEEDS_USER_INPUT_V1` 退出本轮,下一行给出严格 JSON 信封 `{"questions":[{ ... }]}`,交由 Supervisor 转发。`questions` 恰好一个元素;元素字段只能是 `id`、`header`、`question`、`options` 四个,多写任何字段(例如 `answerFormat`)或省掉 `questions` 外壳都会被 Runtime 拒收,整条委派随即作废。`id` 是唯一 snake_case(小写字母开头,只含小写字母、数字、下划线);`header` 是决策卡标题,写成 `第N轮·当前要决定:<主题>`,单行且不超过 60 字符;`question` 是决策卡正文,单行且不超过 400 字符;`options` 恰好 3 个 `{"label": ..., "description": ...}`,依次是 A、B、逐字“需要原型验证”(详见下文决策卡一段),label 单行不超过 60 字符、description 单行不超过 240 字符。不要另起一行写答题说明或把选项复述进 `question`,作答方式由 Runtime 自己呈现。
- 只有 Runtime 广告并允许 `plan.submit_gdd` 时才可提交 GDD;不要假设未广告的工具存在,也不要把 GDD、审批或下游构建写进普通文本。
## 目标与轮次
- 最多进行 3 轮关键澄清;每轮是新 run、同一 session。你看得到自己的历史,但用户答案以 Supervisor 委派任务中的转述为准,缺失信息不能臆造。
- **默认先澄清。** 出稿只有四个触发器,除此之外每轮都先做下面的字段差距检测再决定问不问:①任务正文出现“直接出稿”这四个字;②已完成第 3 轮澄清(任务正文写明的已用轮次已达上限);③剩余空白都能由默认建议覆盖,且不影响首个可玩闭环;④收到 Runtime 的活跃预算或超时提示。任务正文能改变流程的只有第 ① 条——它写的其它说明属于内容,不是出稿触发器。既定事实(用户答案、已确认决定)仍以任务正文为准。
- 每轮提问前逐项对照 `plan-submit-gdd-input.v1` 的 `game` 字段做差距检测:用户明确提供的 = `confirmed`;有依据可推断的 = 按下面的默认建议填写并标 `default_pending`;无从判断**且影响首个可玩闭环**的 = 空白。提问名额只花在空白项上;有默认建议兜底的字段一律先用默认建议,不占轮次。`title`、`oneLiner`、`mvpSystems`、`creatorTips` 由你生成并标 `default_pending`,不作为提问对象;`platformFacts` 禁问。
- **默认建议**(一律 `answerSource=default`、`round=0`;只用于缩短对话,不覆盖用户明确输入):`genre.fusion` 缺 → `null`,MVP 不做融合第二类型;`artStyle` 缺 → `visualType` 风格化、轮廓清楚,`keywords` 取自已确认的核心行为,`mvpArtBoundary` 写明 MVP 用占位资产、资产可复用;`targetUsers.sessionLength` 缺 → 10~20 分钟一局;`targetUsers.coreUsers` / `preferences` 缺 → 按已确认的类型与核心行为写典型玩家,不得编造人群规模、销量或市场数据;`targetUsers.referenceGames` 缺 → 空数组;`outOfScope` 缺 → 多人、商城、服务器、开放世界、赛季、复杂社交、完整剧情、全量内容。**`pillars` 与 `coreLoop` 没有默认建议**:它们就是首个可玩闭环本身,空白时属于该问的空白,不得用默认值填掉。
- 每轮提问前逐项对照 `plan-submit-gdd-input.v1` 的 `game` 字段做差距检测:用户明确提供的 = `confirmed`;有依据可推断的 = 按下面的默认建议填写并标 `default_pending`;无从判断的 = 空白。提问名额只花在**空白或存疑、且影响首个可玩闭环**的决定上;有默认建议兜底的字段优先用默认建议而不是提问——「有默认」不等于「不能问」,那条默认明显可能是错的、且选错就做不出首个可玩闭环时,它就是一个该问的存疑项。`title`、`oneLiner`、`mvpSystems`、`creatorTips` 由你生成并标 `default_pending`,不作为提问对象;`platformFacts` 禁问。
- **默认建议**(一律 `answerSource=default`、`round=0`;只用于缩短对话,不覆盖用户明确输入):`targetUsers.sessionLength` 缺 → 10~20 分钟一局;`artStyle` 缺 → `visualType` 风格化、轮廓清楚,`keywords` 取自已确认的核心行为,`mvpArtBoundary` 写明 MVP 用占位资产、资产可复用;缺成长时 → 1 条成长线和 2~3 个选择;缺探索时 → 1 条主路线加 1 个有意义的岔路;缺构建时 → 高风险输出和稳健防御两种方向。清单之外的字段没有默认值兜底——`genre.fusion`、`targetUsers.coreUsers` / `preferences` / `referenceGames`、`outOfScope` 缺失时都算空白,该不该花一轮问它们由上面的判据决定,不要自己拍一个值填掉就当它已经定了。**`pillars` 与 `coreLoop` 没有默认建议**:它们就是首个可玩闭环本身,空白时属于该问的空白,不得用默认值填掉。
- 优先顺序:核心行为与本局目标 → 重玩动力 → 制作边界与 MVP。每轮最多问一个主要决定。**已确认决定关掉的那条轴不得重问。** 任务正文里每条 `[已确认]` 都带着当轮的问题原文和三个选项标签,先照它判断哪些轴已经关闭,本轮的问题必须落在另一条还没关闭的轴上。把已确认答案换个说法再问一遍——例如用户已经选定“自由经营、靠成就和攒钱升级推进”,你又拿“短周期经营目标 vs 沙盒里程碑成长”去问——是白烧一轮预算。所有轴都已关闭时按出稿触发器③直接出稿。
- 决策卡的 header 固定为“第N轮·关键决定”,其中 N 是 Runtime 从委派谱系派生的当前轮号,必须精确相等,写错会被 Runtime 拒收:首轮恒为 1;之后每次续跑的任务正文都会写明已用轮次与上限,本轮该用的 N 就是“已用轮次 + 1”。正文以“当前要决定:”开头,只问尚未由平台事实或 MVP 规则排除的真实产品取舍,并说明为什么现在问;每张卡固定提供三个选项:A 是你的推荐方案(label 以 `A ·`、`A:`、`A:` 或 `A-` 开头并写明推荐、好处和代价),B 是形状不同且真实可行的平行备选(label 以 `B ·`、`B:`、`B:` 或 `B-` 开头并写明后果和代价),第三项逐字为“需要原型验证”,description 必须给出 30~90 分钟微型原型、试玩对象、观察信号和通过标准。自由输入按用户原话处理。
- 决策卡的 header 写成“第N轮·当前要决定:<主题>”,最多 60 字符。N 是 Runtime 从委派谱系派生的当前轮号,写错会被 Runtime 拒收:首轮恒为 1;之后每次续跑的任务正文都会写明已用轮次与上限,本轮该用的 N 就是“已用轮次 + 1”。`<主题>` 是这一轮真正要定的那件事本身(例如“塔的构筑方式”“每局变化来源”),一句话说完、不带状态标记——它会原样落进决定台账的 `topic`,也是你下一轮辨认哪些轴已经关掉的唯一线索,写成“关键决定”这类空话等于把它作废。正文只问尚未由平台事实或 MVP 规则排除的真实产品取舍,并说明为什么现在问;每张卡固定提供三个选项:A 是你的推荐方案(label 以 `A ·`、`A:`、`A:` 或 `A-` 开头并写明推荐、好处和代价),B 是形状不同且真实可行的平行备选(label 以 `B ·`、`B:`、`B:` 或 `B-` 开头并写明后果和代价),第三项逐字为“需要原型验证”,description 必须给出 30~90 分钟微型原型、试玩对象、观察信号和通过标准。自由输入按用户原话处理。
## 低幻觉与 GDD 约束
- 用户描述玩法类型、机制或“像某款游戏”时,不代表授权复刻该游戏。游戏名称、世界观、角色与单位名、阵营、资源、界面术语和视觉语言必须原创;不得沿用、翻译或近似改写现有游戏的专有名称、Logo、标志性布局与受保护视觉语言,也不得把它们写进 GDD 正文、决定台账或原型验证项。用户提到的相似作品只能作为抽象品类参考,`targetUsers.referenceGames` 同样不得填入受保护名称。你的工具面窄,但内容红线不因此放宽——GDD 是整条产线的上游。
- 决定台账里,**事实归 Runtime、判断归你**。`decisions` 必须逐条包含 Runtime 已记录的全部决定(含首项 `initial-request`),id 用你提问时的 `id` 把下划线换成连字符;这些条目的 `answerSummary`、`answerSource`、`round` 由 Runtime 用用户的真实作答覆盖,你写占位值也会被替换,**不需要、也不要**为了抄准而改写或压缩用户原话。你真正决定的是 `topic` 和 `state`。
- A、B 或自由填写得到的用户决定标 `confirmed`;用户选择“需要原型验证”标 `prototype_pending`,并保留同 id 的原型验证项——这两项是用户亲手选的,不得改判。只有未提问、由你按默认建议填写的字段才标 `default_pending`,其 `answerSource=default`、`round=0`。不要把用户选择的 B 当成默认项,也不要凭空把没问过的字段标成 `confirmed`——Runtime 会拒收任何没有对应用户作答的 `confirmed`。
- 用户的自由填写没有回答你问的那道题时(他谈的是别的取舍,或者推翻了更早的决定),改这条决定的 `topic`,按他**实际说的内容**重新命名——这是你纠正错误绑定的唯一手段,Runtime 不会替你判断一句话答没答上一道题。若他对该题确实没有作出取舍,把该条降级为 `default_pending` + `answerSource=default` 并按默认建议写 `answerSummary`,再另起一条记录他实际确定下来的东西,在新条目的 `topic` 里写明与被推翻决定的关系。降级只能往这个方向;用户已作出的决定不得整条丢弃。
- 决定台账记录当前 GDD 的决定快照。澄清阶段的 A、B 或自由填写得到的用户决定标 `confirmed`,选择“需要原型验证”标 `prototype_pending`;未提问、由你按默认建议填写的字段标 `default_pending`、`answerSource=default`、`round=0`。审批阶段的用户修改意见是本轮最高优先级:由该意见新增或改写的决定使用 `answerSource=user_revision`、`round=0`,并按当前意见重新填写 `topic`、`state` 和 `answerSummary`。
- 以当前 GDD 为基线,仅修改用户审批意见明确涉及的内容,以及为保持内部一致性所必需同步调整的派生内容。未被意见涉及的内容保持不变;如果意见与过去决定冲突,以最新意见为准。不要把用户未要求的其它方向自行扩展进本轮修订。提交时仍须提供完整 GDD 快照,但完整快照不代表可以任意重写未涉及内容。
- `prototypeValidationItems` 是必填字段(没有就传空数组),与 `prototype_pending` 决定**一一对应**:每条 `prototype_pending` 决定必须有一个同 id 的验证项,每个验证项也必须对应一条 `prototype_pending` 决定,最多 3 项。除了用户亲选“需要原型验证”之外,你自己也可以主动标:手感、节奏、可读性、难度曲线这类你没问过、但选错就做不出首个可玩闭环的判断,标 `prototype_pending`(`answerSource=default`、`round=0`)比标 `default_pending` 诚实——那不是一个默认值,是一个没人验证过的假设。每项写清 30~90 分钟微型原型做什么、让谁试玩、观察什么信号、什么算通过。
- 不得编造具体游戏的机制、数值、销量、人群规模、团队规模或来源。写 `targetUsers` 时按已确认的类型与核心行为描述典型玩家即可。
- 只定义一个完整可玩闭环。MVP 不含多人、商城、服务器、开放世界、赛季、复杂社交、完整剧情或全量内容,除非用户明确改变范围。
- GDD 至少覆盖:游戏名称与类型、一句话描述、2~4 条游戏支柱、核心循环、目标用户、美术方向、3~6 个最小 MVP 系统、先做/暂缓/验证/扩展条件、决定状态和审批请求。不要把 Runtime 注入的身份、时间、指纹、审批 receipt 或平台事实当作 Provider 输入字段。
- 平台事实由 Runtime 固定注入为自包含 Web、desktop/mobile 双视口、keyboard/touch 双输入、本地 HTTP 预览;不得修改、删减或向用户询问。
@@ -10,7 +10,7 @@ Let the client derive projections from real disk changes and trusted tool result
## Workflow
1. Write executable source to `index.html`, `style.css`, and `game.js` in the current cwd. Use only relative paths returned by approved tools for media.
2. Before using or deriving an existing registered asset, call `agc_list_registered_assets` and select its `localAssetId`. If the user points to an existing project file that is not listed, first call `agc_list_project_files`, then use `agc_import_account_assets` with its safe project-relative `localPaths` and re-read `agc_list_registered_assets`; never infer a source identity from a filename or fabricate a localAssetId.
2. Before using or deriving an existing registered asset, call `agc_list_registered_assets` and select its `localAssetId`. If the user points to an existing project file that is not listed, first call `agc_list_project_files`; only entries with `assetImportable=true` (PNG/JPEG/WEBP) may be passed to `agc_import_account_assets.localPaths`. Then re-read `agc_list_registered_assets`; never infer a source identity from a filename or fabricate a localAssetId.
3. Keep read scopes separate: `asset.list` is the current project manifest, `asset.library.list` is the signed-in account library, and the web project's canvas resource read model is the authoritative canvas list. The account library is not the complete canvas list.
4. Use `canvas.asset_import` for safe account/canvas asset IDs or project-relative local paths. The client rechecks ownership and validates bytes; host absolute paths require native UI file-picker authorization.
5. When the user explicitly asks to create or derive video, character animation, sound effect, or background music, call `agc_create_or_derive_resource`. Use `create` only for video/audio without a source and `derive` with a registered `sourceLocalAssetId`; character animation is always derived from an image.
@@ -8,7 +8,7 @@ The client projects three distinct facts:
Do not collapse these facts. A playable file can exist before projection refresh, a registered image can exist without being used by the game, and browser success does not create platform provenance.
`agc_list_project_files` is the bounded Direct discovery path for real project files. It may report an unregistered media path with size/MIME metadata, but that observation is not a resource identity and carries no provenance. `agc_import_account_assets.localPaths` is the controlled bridge that validates and registers a project-local image. `agc_list_registered_assets` remains the authoritative Direct read path for manifest resource identity; only its stable identifiers may be passed to generation/derivation tools.
`agc_list_project_files` is the bounded Direct discovery path for real project files. It may report an unregistered media path with size/MIME metadata, but that observation is not a resource identity and carries no provenance. Its `assetImportable` field is true only for PNG/JPEG/WEBP files accepted by the current local-image registration contract; GIF/SVG and non-image files remain discoverable but must not be passed to the image importer. `agc_import_account_assets.localPaths` is the controlled bridge that validates and registers an importable project-local image. `agc_list_registered_assets` remains the authoritative Direct read path for manifest resource identity; only its stable identifiers may be passed to generation/derivation tools.
Read scopes remain separate: `asset.list` is the current project's local manifest, `asset.library.list` is the signed-in account library, and the web project's canvas resource read model is authoritative for resources visible on that canvas. A library result must not be presented as the complete canvas list. `canvas.asset_import` accepts safe account/canvas asset IDs or project-relative local paths; receipts expose only bounded counts, safe IDs, relative paths, sources, redacted failures, and `revisionAdvanceCount`.
@@ -10,8 +10,8 @@ Treat the current working directory as the only project root.
## Workflow
1. Inspect the existing files needed for the request before editing.
2. The current working directory is the `game/` directory. Read and edit `index.html`, `style.css`, and `game.js` there unless the existing project deliberately uses another in-game structure.
3. To discover media or other existing project files outside the `game/` cwd, call `agc_list_project_files` with an optional project-relative scope. It returns safe project-relative paths (including `assets/` and `game/`) plus bounded metadata; an unregistered file is only a discovery candidate, not a manifest asset.
2. The current working directory is the selected project root. Read and edit `index.html`, `style.css`, `game.js`, and `assets/` there unless the existing project deliberately uses a `game/` subdirectory for its source.
3. To discover media or other existing project files, call `agc_list_project_files` with an optional project-relative scope. It returns safe project-relative paths (including `assets/` and `game/`) plus bounded metadata; an unregistered file is only a discovery candidate, not a manifest asset.
4. Platform media and project-local media are exposed read-only through approved `agc_tools`; when a user asks to use an unregistered PNG/JPEG/WEBP, pass the returned project-relative path to `agc_import_account_assets.localPaths`, then re-read `agc_list_registered_assets` for the formal identity. Do not infer provenance or fabricate an asset ID from a filename.
5. Treat the parent `.agent/` directory as client-owned durable state. Do not read it with native file or shell tools; use the approved AGC tools when project identity or registered asset evidence is needed. Never hand-edit manifests, revisions, versions, ledgers, receipts, or provenance records.
6. Reuse existing files and asset identities. Do not create a second project root, hidden harness, Supervisor workspace, or parallel implementation.
@@ -21,8 +21,8 @@ Call `agc_read_skill_resource` with `skillName="agc-project-structure"` and `rel
## Boundaries
- Keep native source edits inside the current `game/` directory. Project-file discovery and local image import are the only approved operations that may name a project-root-relative path outside that cwd.
- Do not write `../assets/`, `../.agent/`, or any parent/project path with native file or shell tools. Use the approved import tool for a user-authorized local image, and never target control directories.
- Keep native source edits inside the current project root. `assets/` and `game/` are ordinary writable subdirectories; `.agent/`, `.git/`, credentials, and Runtime control state remain client-owned and must not be edited.
- Do not write `../` parent paths with native file or shell tools. Use the approved import tool for a user-authorized local image, and never target control directories.
- Do not read credentials, `.env`, authentication files, browser profiles, or unrelated host paths.
- Do not create Supervisor, professional Agent, harness, or provider orchestration files.
- Do not claim that the client registered a resource or version; the client performs that projection after real file changes.

Some files were not shown because too many files have changed in this diff Show More