Load present shells from a skill-pack catalog and list leftover creation modules.

Design-flow now sees leftover catalog skills and aesthetics-card commitments; reply-format injects present-shells/, and world-simulator prompts add random-range plus a tighter module set.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-07 01:21:10 +08:00
co-authored by Cursor
parent 770a8fdcdc
commit 3c751988de
59 changed files with 2793 additions and 1638 deletions
+2 -2
View File
@@ -440,7 +440,7 @@ export function buildFocus(
actorType: "user",
actorLabel: "你",
action: "点选要做的节点",
detail: "点可进入的节点即确认并开始;虚线原型点一下增殖再进去。要改排在底栏写意见再发",
detail: "点图上可进入的节点即开始;要看技能池点上方「技能」。虚线原型点一下增殖。要改排在底栏写意见再发",
};
}
@@ -485,7 +485,7 @@ export function buildFocus(
actorType: "user",
actorLabel: "你",
action: "点选要做的节点",
detail: "点可进入的节点即确认并开始;虚线原型点一下增殖再进去。要改排在底栏写意见再发",
detail: "点图上可进入的节点即开始;要看技能池点上方「技能」。虚线原型点一下增殖。要改排在底栏写意见再发",
};
}
const copy = reviewComposerCopy(art?.workerId, {
+2 -2
View File
@@ -2472,7 +2472,7 @@ export class SessionManager {
});
} else if (reason?.kind === "pick_creation_step") {
hints.push(
"点可进入的节点即确认并开始。虚线原型点一下会增殖一条实例再进去。要改排在底栏写意见再发。",
"点图上可进入的节点即开始。要看技能池点上方「技能」。虚线原型点一下会增殖一条实例再进去。要改排在底栏写意见再发。",
);
actions.push({ type: "replan", label: "发送" });
actions.push({
@@ -2494,7 +2494,7 @@ export class SessionManager {
});
if (art?.workerId === DESIGN_FLOW_WORKER_ID) {
hints.push(
"点可进入的节点即确认并开始。虚线原型点一下会增殖一条实例再进去。要改排在底栏写意见再发。",
"点图上可进入的节点即开始。要看技能池点上方「技能」。虚线原型点一下会增殖一条实例再进去。要改排在底栏写意见再发。",
);
actions.push({
type: "send_message",
+99 -34
View File
@@ -269,11 +269,21 @@ export type CreationFlowSpawnView = {
blockedReason?: string;
};
/** 目录里尚未编入本局 DAG、仍可追加的技能 */
export type CreationFlowAvailableModule = {
name: string;
declaration?: string;
repeatable?: boolean;
closer?: boolean;
};
export type CreationFlowUserView = {
brief?: string;
status?: CreationFlowStatus;
/** 规划层建议追加的原型(尚未在 steps 里) */
suggestions?: CreationFlowPrototypeSuggestion[];
/** 技能目录里尚未编入本局、仍可要求追加 */
availableModules?: CreationFlowAvailableModule[];
steps: Array<{
order: number;
id: string;
@@ -1244,12 +1254,14 @@ function slugFromName(name: string): string {
生成规则: "generation-rules",
具体实例: "concrete-instances",
拓扑图谱: "topology",
回复呈现: "status-bar",
设计状态栏: "status-bar",
设计监控栏: "status-bar",
变量设计与更新规则: "variable-design",
变量控制上下文: "variable-context",
设计回复格式: "reply-format",
正文组成: "reply-format",
随机范围整理: "random-range",
游玩拓扑: "worker-spec",
"Worker 规格": "worker-spec", // 旧称,等同游玩拓扑
细化终稿: "refine",
@@ -1318,7 +1330,7 @@ export function formatModuleCatalogForAgent(catalog: ModuleCatalog): string {
"按需选用,勿默认全选;步骤名只能从这里选;标〔可反复〕的可多次编入;标〔收口〕的是终节点:排在细化终稿之后作最后一步;尚未选定开场前,用户要补前序节点(如 NPC)时插在收口之前,勿以「收口已排入」拒绝追加;选定后才结束创作并保存;标〔程序步〕的确认编排后直接执行(不抛默认问题、不经同意并开始),产物仍验收。",
"标〔先验产物〕(生成规则、具体实例):编排层只排 role=prototype 槽位 + suggestion;禁止在原型写 params、禁止预排 instance。「写什么」在用户点原型增殖后的实例步内钉。",
"其它有「编排参数」的步骤:确认开干前写齐必填 params;缺参时用 askUser 选项+其它。",
"选型依据是下方「何时用 / 何时不用 / 边界」(来自各能力 meta)。",
"选型依据是下方「何时用 / 何时不用 / 边界」(来自各能力 meta)。对照用户表述与已验收产物判定:该排才排;可跳过或条件含糊则不排。不要用执行细则补脑。",
"〔可反复〕再编入新 id = 彻底新建一条。已完成节点要改:禁止排 mode=revise 新步;用户会在图上点该节点重进。禁止把改旧稿写成又一条 fresh。",
"不要把能力执行全文塞进本步;执行由 design-step 注入。",
lines.join("\n"),
@@ -1534,15 +1546,14 @@ export function formatRecipeCatalogForAgent(catalog: RecipeCatalog): string {
return `【可选配方】(须由用户手动选择)\n${lines.join("\n")}`;
}
/** 注入 design-flow:用户已选配方(方法论 + 近期起点) */
/** 注入 design-flow:用户已选配方(方法论 + 开局起点) */
export function formatSelectedRecipeForAgent(detail: RecipeDetail): string {
const lines: string[] = [
`【用户已选配方 · ${detail.name}】`,
"这是用户手动选定的设计方法,不是锁死流水线。",
"产出**增量工作流计划(DAG)**:只排近期要做的步骤;已验收步保留,可追加同技能多次调用。",
"按用户表述与配方方法论增删改未验收步骤与依赖;步骤名只能从【能力】选。",
"这是用户手动选定的设计方法。步骤名从【能力】选。",
"对照美学卡上用户会在意的部分与配方方法论,增删改未验收步骤与依赖;已验收步保留。",
"能力「何时用 / 何时不用」以【能力 · 可选工序】为准;本配方不重复罗列各能力调用条件。",
"禁止改选其它配方;若用户要换配方,须等用户重新选定后再编排。",
"用户要换配方:等用户在界面重新选定后再编排。",
];
if (detail.declaration) lines.push(`简介:${detail.declaration}`);
if (detail.when) lines.push(`适用:${detail.when}`);
@@ -1571,12 +1582,12 @@ export function formatSelectedRecipeForAgent(detail: RecipeDetail): string {
null,
2,
);
lines.push("建议近期 steps(增量起点,可改;勿一次排完全程):");
lines.push("开局 steps(配方预置起点,可按美学卡上的承诺增删):");
lines.push("```json");
lines.push(stepsJson);
lines.push("```");
} else {
lines.push("建议 steps:(待作者完善 recipe.yaml;可从【能力】自行编排近期 horizon)");
lines.push("开局 steps:(recipe.yaml 尚未写 steps;可从【能力】按美学卡上的承诺编排)");
}
return lines.join("\n");
}
@@ -1592,7 +1603,6 @@ function formatRecipeFieldBlock(value: string | string[]): string {
export function formatDesignFlowContentBlocks(params: {
selectedRecipe?: RecipeDetail | null;
modules?: ModuleCatalog | null;
missingSelection?: boolean;
flow?: CreationFlow | null;
acceptedStepIds?: readonly string[];
filledArtifactTags?: readonly string[];
@@ -1605,15 +1615,7 @@ export function formatDesignFlowContentBlocks(params: {
filledArtifactTags: params.filledArtifactTags,
}),
];
if (params.missingSelection) {
blocks.push(
[
"【配方】用户尚未手动选择。",
"禁止自行猜测或替用户选定配方。",
"请 askUser 请用户从可用配方中选择,或等待用户在界面选定后再编排。",
].join("\n"),
);
} else if (params.selectedRecipe) {
if (params.selectedRecipe) {
blocks.push(formatSelectedRecipeForAgent(params.selectedRecipe));
}
if (params.modules) {
@@ -1623,8 +1625,8 @@ export function formatDesignFlowContentBlocks(params: {
}
/**
* 编排器必须看见的进度:哪些步已做完、草案里已有什么、哪些能力才允许再编入。
* 裸 JSON id 列表不够;此块由程序钉死。
* 编排 LLM 必须看见的现场进度:已完成、草案已有、原型槽位、尚未编入。
* 裸 JSON id 列表不够;此块由程序按现场拼装。
*/
export function formatFlowProgressForAgent(params: {
flow?: CreationFlow | null;
@@ -1655,8 +1657,8 @@ export function formatFlowProgressForAgent(params: {
repeatable: boolean,
) => {
const flag = repeatable
? "〔可反复:mode=fresh 再追加 = 彻底新建〕"
: "〔不可反复:禁止再排新建,也禁止排 mode=revise〕";
? "〔可反复:再追加 = 彻底新建;改旧的由用户点该实例重进〕"
: "〔一次性:这一面已覆盖;要改则用户点该节点重进〕";
const idBit = id ? `(id: ${id})` : "";
doneLines.push(`- ${name}${idBit} · ${why} ${flag}`);
doneNames.add(name);
@@ -1702,7 +1704,7 @@ export function formatFlowProgressForAgent(params: {
if (isPrototypeStep(step)) continue;
if (doneIds.has(step.id) || isStepAccepted(step, accepted)) continue;
draftLines.push(
`- ${step.name}(id: ${step.id}) · 已在草案,保留原 id;不要当作新规划再写一遍`,
`- ${step.name}(id: ${step.id}) · 已在草案,保留原 id`,
);
}
}
@@ -1719,7 +1721,7 @@ export function formatFlowProgressForAgent(params: {
? ` · 建议:${step.suggestion.trim()}`
: "";
prototypeLines.push(
`- ${step.name}(id: ${step.id},role=prototype)${sug} · 保留;用户在图上点增殖,禁止预排实例或写 params`,
`- ${step.name}(id: ${step.id},role=prototype)${sug} · 保留;用户点此槽增殖 instance`,
);
}
}
@@ -1729,12 +1731,10 @@ export function formatFlowProgressForAgent(params: {
);
const lines = [
"【流程进度】(程序钉死,必须遵守)",
"你只排 DAG 骨架:节点 + depends_on +(repeatable 能力)原型槽位 + suggestions。不决定用户先跑哪个。",
"〔可反复〕须写 role=prototype,可带 suggestion(建议生成什么);禁止预排 instance、禁止在原型上写 params。「写什么」在实例步 design-step 里钉。",
"已完成的非反复技能:禁止再排新建,禁止排 mode=revise。改已完成节点由用户在图上点选重进,不要在 DAG 里加修订步。",
"要改已有实例:用户点该实例重进。点原型增殖 = 全新 instance(role=instance,from=原型 id)。",
"草案里已有的步骤:原样保留,不要重排一遍开局。",
"【流程进度】程序按现场列出:已完成、草案已有、原型槽位、尚未编入。编排以这份清单为准。",
"已完成 = 覆盖标记(原 id 留在 steps);草案已有 = 保留原 id,在其上追加或改未验收依赖;尚未编入 = 新 name 只从这里选。",
"〔可反复〕在 steps 里写一条 role=prototype,带 suggestion(点名这局在意的对象)。「写什么」在用户点原型后的实例步里钉。",
"一次性技能已覆盖的那一面:要改则用户点该节点重进。要改已有实例:用户点该实例重进。点原型增殖 = 全新 instance(role=instance,from=原型 id)。",
];
const closerPending =
catalog && flow
@@ -1747,7 +1747,7 @@ export function formatFlowProgressForAgent(params: {
lines.push(
`草案已有〔收口〕${closerPending
.map((s) => `「${s.name}」`)
.join("、")}但尚未选定。用户要求补节点(如 NPC、生成规则、具体实例)时:把新步插在收口之前,保持收口为最后一步,并将 status 改回 open。禁止以「收口已排入」为由拒绝追加。`,
.join("、")}但尚未选定。用户要补节点时:把新步插在收口之前,保持收口为最后一步,并将 status 改回 open。收口已排入仍可在它前面加步。`,
);
}
if (doneLines.length) {
@@ -1755,14 +1755,14 @@ export function formatFlowProgressForAgent(params: {
} else {
lines.push(
"",
"已完成:尚无已验收步骤。若草案已有开局步,那是配方预置起点,不是你新规划的。",
"已完成:尚无已验收步骤。草案里的开局步是配方预置起点,保留原 id。",
);
}
if (draftLines.length) {
lines.push("", "草案已有、尚未验收(保留,勿重排):", ...draftLines);
lines.push("", "草案已有、尚未验收(保留原 id):", ...draftLines);
}
if (prototypeLines.length) {
lines.push("", "原型槽位(保留,勿改成 instance):", ...prototypeLines);
lines.push("", "原型槽位(保留;用户点增殖):", ...prototypeLines);
}
if (suggestionLines.length) {
lines.push(
@@ -1771,6 +1771,33 @@ export function formatFlowProgressForAgent(params: {
...suggestionLines,
);
}
const leftover = listCallableCatalogModules({
flow,
catalog,
acceptedStepIds: accepted,
filledArtifactTags: params.filledArtifactTags,
});
if (leftover.length) {
lines.push(
"",
"尚未编入、仍可调用(新 name 只从这里选):",
...leftover.map((m) => {
const flags = [
m.repeatable ? "〔可反复〕" : "",
m.closer ? "〔收口〕" : "",
]
.filter(Boolean)
.join("");
const decl = m.declaration?.trim() ? `:${m.declaration.trim()}` : "";
return `- ${m.name}${flags}${decl}`;
}),
);
} else {
lines.push(
"",
"尚未编入、仍可调用:无。用户只能点图上已有节点,或对〔可反复〕原型增殖。",
);
}
lines.push(
"",
repeatableList.length
@@ -2071,6 +2098,37 @@ export function deriveFlowStepTitle(params: {
return raw;
}
/** 目录里尚未编入本局、且非反复技能也未有产物的条目——用户仍可要求追加。 */
export function listCallableCatalogModules(params: {
flow?: CreationFlow | null;
catalog?: ModuleCatalog | null;
acceptedStepIds?: readonly string[];
filledArtifactTags?: readonly string[];
}): CreationFlowAvailableModule[] {
const catalog = params.catalog ?? null;
if (!catalog?.modules.length) return [];
const scheduled = new Set((params.flow?.steps ?? []).map((s) => s.name));
const filled = new Set(params.filledArtifactTags ?? []);
const accepted = new Set(
(params.acceptedStepIds ?? []).map((id) => id.trim()).filter(Boolean),
);
const out: CreationFlowAvailableModule[] = [];
for (const mod of catalog.modules) {
if (scheduled.has(mod.name)) continue;
if (!mod.repeatable) {
if (mod.artifact && filled.has(mod.artifact)) continue;
if (accepted.has(mod.name)) continue;
}
out.push({
name: mod.name,
...(mod.declaration ? { declaration: mod.declaration } : {}),
...(mod.repeatable ? { repeatable: true } : {}),
...(mod.closer ? { closer: true } : {}),
});
}
return out;
}
export function formatCreationFlowForUser(
flow: CreationFlow,
catalog?: ModuleCatalog | null,
@@ -2092,10 +2150,17 @@ export function formatCreationFlowForUser(
);
const filled = new Set(filledArtifactTags);
const byId = new Map(normalized.steps.map((s) => [s.id, s] as const));
const availableModules = listCallableCatalogModules({
flow: normalized,
catalog,
acceptedStepIds,
filledArtifactTags,
});
return {
brief: normalized.brief,
status: normalized.status,
...(pendingSuggestions.length ? { suggestions: pendingSuggestions } : {}),
...(availableModules.length ? { availableModules } : {}),
steps: normalized.steps.map((s, i) => {
const revise = isReviseStep(s);
const proto = isPrototypeStep(s);
+13 -1
View File
@@ -503,7 +503,6 @@ export async function loadWorkerSkillWithContext(
const blocks = formatDesignFlowContentBlocks({
selectedRecipe,
modules,
missingSelection: !selectedRecipe,
flow: parseCreationFlow(opts?.flowRaw),
acceptedStepIds: opts?.acceptedStepNames,
filledArtifactTags: opts?.filledArtifactTags,
@@ -551,6 +550,19 @@ export async function loadWorkerSkillWithContext(
const { formatPlayAgentRosterForPrompt } = await import("./play-slots.js");
rosterBlock = `${formatPlayAgentRosterForPrompt()}\n\n`;
}
if (binding.module.id === "reply-format") {
const {
loadPresentShellCatalog,
formatPresentShellsForPrompt,
} = await import("./present-shell-catalog.js");
const shells = await loadPresentShellCatalog(
skill.skillPackRoot,
skillsRoot,
);
if (shells.length) {
rosterBlock = `${formatPresentShellsForPrompt(shells)}\n\n`;
}
}
modulePromptBlock = `${rosterBlock}${paramsBlock}\n\n## 【本步方法 · ${binding.module.name}】\n\n${binding.modulePrompt.trim()}${openingNote}${inheritNote}`;
const depTags = inheritTag
? binding.depTags.filter((tag) => tag !== inheritTag)
+2 -1
View File
@@ -192,7 +192,8 @@ export function stripPresentSourceFences(raw: string): string {
}
export const PLAY_VISIBLE_BODY_INSTRUCTION = [
"用户可见正文(present.v1 的 blocks.body,或纯 Markdown 主读)默认 **500~2000 字**(按汉字计)。",
"用户可见正文(present.v1 的 blocks.body,或纯 Markdown 主读)默认 **1000~2000 字**(按汉字计);以「设计.监控栏/回复呈现」已钉字数为准。",
"采用完整长自然段叙述,不要单句成段。",
"不要写成几句气泡短信就结束,除非用户明确要求极短。",
"禁止把 tag 名(如「输出.用户展示」)、压缩摘要、过程日志写进正文。",
].join("\n");
+116
View File
@@ -0,0 +1,116 @@
/**
* 游玩呈现壳目录:介绍与壳规格写在 present-shells/{id}/prompt.md。
* 正文组成执行时注入【可选呈现壳】,不在技能 prompt 里写死清单。
*/
import { readFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { parse as parseYaml } from "yaml";
import {
getModuleSection,
parseModulePromptSections,
} from "./creation-flow.js";
const DEFAULT_SKILLS_ROOT = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
"../../skills",
);
export const PRESENT_SHELL_CATALOG_FILENAME = "present-shells/catalog.yaml";
export type PresentShellCatalogEntry = {
id: string;
name: string;
intro: string;
shell: string;
};
export function parsePresentShellCatalog(
raw: string,
): Array<{ id: string; name: string }> {
let doc: unknown;
try {
doc = parseYaml(raw);
} catch {
return [];
}
if (!doc || typeof doc !== "object" || Array.isArray(doc)) return [];
const shellsRaw = (doc as Record<string, unknown>).shells;
if (!Array.isArray(shellsRaw)) return [];
const out: Array<{ id: string; name: string }> = [];
for (const item of shellsRaw) {
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
const row = item as Record<string, unknown>;
const id = typeof row.id === "string" ? row.id.trim() : "";
const name = typeof row.name === "string" ? row.name.trim() : "";
if (!id || !name) continue;
out.push({ id, name });
}
return out;
}
export async function loadPresentShellCatalog(
skillPackRoot: string,
skillsRoot = DEFAULT_SKILLS_ROOT,
): Promise<PresentShellCatalogEntry[]> {
const catalogPath = path.join(
skillsRoot,
skillPackRoot,
PRESENT_SHELL_CATALOG_FILENAME,
);
let raw: string;
try {
raw = await readFile(catalogPath, "utf8");
} catch {
return [];
}
const index = parsePresentShellCatalog(raw);
const out: PresentShellCatalogEntry[] = [];
for (const entry of index) {
const promptPath = path.join(
skillsRoot,
skillPackRoot,
"present-shells",
entry.id,
"prompt.md",
);
let promptMd = "";
try {
promptMd = await readFile(promptPath, "utf8");
} catch {
continue;
}
const sections = parseModulePromptSections(promptMd);
const intro = getModuleSection(sections, "intro") ?? "";
const shell = getModuleSection(sections, "shell") ?? "";
if (!intro && !shell) continue;
out.push({
id: entry.id,
name: entry.name,
intro,
shell,
});
}
return out;
}
export function formatPresentShellsForPrompt(
shells: readonly PresentShellCatalogEntry[],
): string {
const lines = [
"## 【可选呈现壳】程序从仓库动态装载",
"",
"只从下列 id 里选一个并做微调。禁止自造目录没有的壳,禁止从零发明布局。介绍与壳规格以各壳自己的文档为准。",
"",
];
for (const s of shells) {
lines.push(`### \`${s.id}\` · ${s.name}`, "");
if (s.intro) {
lines.push("介绍:", "", s.intro, "");
}
if (s.shell) {
lines.push("壳:", "", "```", s.shell, "```", "");
}
}
return lines.join("\n").trim();
}