diff --git a/config/aipods/artificer.yaml b/config/aipods/artificer.yaml index 33dc9e4..5cc0d56 100644 --- a/config/aipods/artificer.yaml +++ b/config/aipods/artificer.yaml @@ -124,7 +124,11 @@ spec: lfs: false payloadDefaults: title: Artificer distributed development task - prompt: Follow the bundled dad-dev and UniDesk skills. Report progress to the relevant GitHub issue before risky or long operations. + prompt: |- + 你就是当前已派发任务的 Artificer 执行者,必须直接在 primary workspace 完成被分配的调查、修改、测试和 PR。 + 禁止通过 `agentrun create`、`agentrun apply`、`agentrun dispatch`、Code Queue 或任何等价入口创建或派发另一个 Artificer 来完成当前任务。 + 允许只读查询 AgentRun task、session 和 run 状态;只读查询不属于二次派单。 + 遵循随资源包提供的 dad-dev 与 UniDesk skills;执行高风险或长时间操作前,先向相关 GitHub issue 报告进展。 references: [] metadata: aipod: Artificer diff --git a/src/common/aipod-specs.ts b/src/common/aipod-specs.ts index c0e4d64..44b69ea 100644 --- a/src/common/aipod-specs.ts +++ b/src/common/aipod-specs.ts @@ -136,7 +136,9 @@ export function renderAipodSpec(record: AipodSpecRecord, input: RenderAipodInput const metadata = mergeRecords(spec.metadata, input.metadata, { aipod: record.name, aipodSpecHash: record.specHash, aipodImageRef: imageRef }); const inputPayload = input.payload === undefined ? undefined : validateAipodPayload(input.payload, "aipodRender.payload"); const payload = mergeRecords(spec.payloadDefaults, inputPayload); - if (typeof input.prompt === "string" && input.prompt.trim().length > 0) payload.prompt = input.prompt; + const defaultPrompt = nonEmptyText(spec.payloadDefaults?.prompt); + const userPrompt = nonEmptyText(input.prompt) ?? nonEmptyText(inputPayload?.prompt); + if (userPrompt !== null) payload.prompt = defaultPrompt === null ? userPrompt : `${defaultPrompt}\n\n${userPrompt}`; applyEffectiveModelPayload(payload, effectiveModel); const sessionRef = renderSessionRef(spec.sessionRef ?? null, input, record, payload); const queueTask = validateCreateQueueTask({ @@ -443,6 +445,10 @@ function stringValue(value: unknown): string | null { return typeof value === "string" && value.trim().length > 0 ? value.trim() : null; } +function nonEmptyText(value: unknown): string | null { + return typeof value === "string" && value.trim().length > 0 ? value : null; +} + function isJsonRecord(value: unknown): value is JsonRecord { return typeof value === "object" && value !== null && !Array.isArray(value); } diff --git a/src/selftest/cases/81-artificer-nonrecursive-prompt.ts b/src/selftest/cases/81-artificer-nonrecursive-prompt.ts new file mode 100644 index 0000000..6027bbf --- /dev/null +++ b/src/selftest/cases/81-artificer-nonrecursive-prompt.ts @@ -0,0 +1,34 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import path from "node:path"; +import { parseAipodSpecYaml, renderAipodSpec } from "../../common/aipod-specs.js"; +import type { JsonRecord } from "../../common/types.js"; +import type { SelfTestCase } from "../harness.js"; + +const selfTest: SelfTestCase = async (context) => { + const source = path.join(context.root, "config", "aipods", "artificer.yaml"); + const spec = parseAipodSpecYaml(await readFile(source, "utf8"), source); + const userPrompt = "调查 Monitor issue #1888 的迁移失败。\n保留这段用户业务提示的完整内容。"; + const rendered = renderAipodSpec({ + name: spec.metadata.name, + spec, + specHash: "selftest-artificer-nonrecursive-prompt", + source, + createdAt: "2026-01-01T00:00:00.000Z", + updatedAt: "2026-01-01T00:00:00.000Z", + }, { prompt: userPrompt }); + const effectivePrompt = String(((rendered.queueTask as JsonRecord).payload as JsonRecord).prompt); + + assert.match(effectivePrompt, /当前已派发任务的 Artificer 执行者/u); + assert.match(effectivePrompt, /agentrun create/u); + assert.match(effectivePrompt, /agentrun apply/u); + assert.match(effectivePrompt, /agentrun dispatch/u); + assert.match(effectivePrompt, /Code Queue/u); + assert.match(effectivePrompt, /只读查询 AgentRun task、session 和 run 状态/u); + assert.equal(effectivePrompt.endsWith(userPrompt), true, "rendered prompt must preserve the complete user business prompt"); + assert.equal(effectivePrompt.split(userPrompt).length - 1, 1, "rendered prompt must include the user business prompt exactly once"); + + return { name: "artificer-nonrecursive-prompt", tests: ["artificer-yaml-nonrecursive-prompt-render", "artificer-user-prompt-preserved"] }; +}; + +export default selfTest;