feat: 为 Artificer 增加中文任务标题

This commit is contained in:
Codex
2026-07-15 16:18:52 +02:00
parent 94a242f8d1
commit 967f031f0c
13 changed files with 188 additions and 27 deletions
+10 -4
View File
@@ -11,18 +11,19 @@ description: UniDesk AgentRun-backed Code Queue CLI — Skill(cli-spec)。legacy
```bash
bun scripts/cli.ts agentrun get tasks --limit 20
bun scripts/cli.ts agentrun describe task/<taskId>
bun scripts/cli.ts agentrun describe "修复 HWLAB 的 cicd"
bun scripts/cli.ts agentrun describe task/<taskId> --input -o json
bun scripts/cli.ts agentrun describe aipodspec/<name>
bun scripts/cli.ts agentrun events run/<runId> --after-seq 0 --expect 10 --timeout 120s --limit 20
bun scripts/cli.ts agentrun events "修复 HWLAB 的 cicd" --after-seq 0 --expect 10 --timeout 120s --limit 20
bun scripts/cli.ts agentrun events run/<runId> --detail-seq <seq> -o json
bun scripts/cli.ts agentrun logs session/<sessionId> --tail 120
bun scripts/cli.ts agentrun result run/<runId> --command <commandId>
bun scripts/cli.ts agentrun logs "修复 HWLAB 的 cicd" --tail 120
bun scripts/cli.ts agentrun result "修复 HWLAB 的 cicd"
bun scripts/cli.ts agentrun get attempts --task <taskId>
bun scripts/cli.ts agentrun retry task/<taskId> --idempotency-key <key> --reason <text> --dry-run
bun scripts/cli.ts agentrun send <session> --prompt-stdin
bun scripts/cli.ts agentrun create task \
--aipod Artificer \
--title "修复 HWLAB 的 cicd" \
--target <Target> \
--target-workspace <absolute-task-worktree> \
--repo <owner/repo> \
@@ -43,6 +44,11 @@ AipodSpec/Artificer、task manifest 和 queue 渐进披露见 [references/resour
## P0 边界
- Artificer `create task` 必须显式提供唯一中文短标题:
- 标题为单行 2 到 80 字符,并至少包含一个中文汉字;
- `HWLAB``CI/CD` 等专有名和空格可以保留;
- `describe/events/logs/result` 优先使用标题的完全匹配查询;
- 标题重复时 CLI 必须报歧义,不得猜测;内部 ID 仅用于深度下钻和机器身份。
- AgentRun 与调用方之间的契约版本、源码/镜像版本和 `commitId` 对齐只能产生非阻塞 warning;可安全归一化的请求继续 admission/dispatch,不得因版本或契约漂移阻止用户任务、session、run、command 或 runner。
- 新写入口不要用 `codex submit/steer/resume`;这些 legacy mutation 已冻结。
- Artificer 的 runner primary workspace 与任务 Target 必须分开判定:
@@ -18,6 +18,7 @@
```bash
bun scripts/cli.ts agentrun create task \
--aipod Artificer \
--title "修复 HWLAB 的 cicd" \
--target <Target> \
--target-workspace <absolute-task-worktree> \
--repo <owner/repo> \
@@ -26,6 +27,10 @@ bun scripts/cli.ts agentrun create task \
--prompt-stdin
```
- `--title` 为必填中文短标题,允许英文专有名和空格;
- 后续 `describe/events/logs/result` 直接传入标题字符串;
- 标题按规范化后的完全匹配解析,重复标题必须改用内部 ID 处理歧义。
- 默认直接执行 live 单命令;不要把 `--dry-run` 作为派单前置步骤;
- live 必须先经 owning YAML 选中的 Target `trans` 重入并完成只读 preflight
- preflight 必须验证:
+2
View File
@@ -38,6 +38,8 @@ description: UniDesk 主代理调度子代理的必读技能。用户提到子
- 审查证据只保留 issue/PR/commit、验证摘要和必要下钻链接,不复制无界日志或构造额外实证体系;
- 只有直接保护真实业务资源、不可逆操作或明确损害风险时才允许最小 guard,且只覆盖实际风险窗口并在风险解除后立即释放;
- Artificer 的 `create task` 外层入口:
- 必须使用与子 issue / MDTODO 语义一致的唯一中文短标题;
- 标题至少包含一个中文汉字,专有名和空格可以保留;
- 必须按 owning YAML 选中 Target
- 必须通过 `trans <Target>:<unideskWorkspace>` 重入目标节点;
- 预检、创建、自动派发和观察都在重入后完成;
+9
View File
@@ -254,6 +254,7 @@ UniDesk 指挥官新任务入口固定使用 `bun scripts/cli.ts agentrun get|de
```bash
bun scripts/cli.ts agentrun create task \
--aipod Artificer \
--title "修复 HWLAB 的 cicd" \
--target <Target> \
--target-workspace <absolute-task-worktree> \
--repo <owner/repo> \
@@ -262,6 +263,14 @@ bun scripts/cli.ts agentrun create task \
--prompt-stdin
```
Artificer 标题契约:
- `create task` 必须显式提供单行中文短标题;
- 标题长度为 2 到 80 字符,至少包含一个中文汉字;
- `HWLAB``CI/CD` 等专有名和空格可以保留;
- `describe/events/logs/result` 使用标题完全匹配并解析最新执行资源;
- 标题重复时返回歧义错误,内部 ID 继续作为不可变机器身份。
- 默认直接执行 live 单命令,不把 `--dry-run` 作为派单前置步骤;
- live 先经 Target `trans` 重入并执行 source/worktree/Aipod/SecretRef 只读预检;
- 预检通过后在同一命令内 submit 并自动 dispatch
@@ -259,6 +259,7 @@ function options(output: AgentRunOutputMode): AgentRunResourceOptions {
dryRun: false,
file: null,
aipod: null,
title: null,
idempotencyKey: null,
promptStdin: false,
node: null,
+10 -4
View File
@@ -221,6 +221,7 @@ describe("AgentRun render-only REST client config", () => {
describe("Artificer Target one-command dispatch", () => {
test("requires trans re-entry before Target filesystem preflight and derives a stable idempotency key", () => {
const request = targetTaskRequestFromArgs([
"--title", "修复 SelfMedia 的 OCR 规划器",
"--target", "NC01",
"--target-workspace", "/root/.worktrees/selfmedia/r6-ocr-visual-planner",
"--repo", "pikainc/selfmedia",
@@ -269,6 +270,7 @@ describe("Artificer Target one-command dispatch", () => {
test("scopes post-task create replay to one complete logical operation", () => {
const base = {
title: "修复 UniDesk 的 Artificer 派单",
target: "NC01",
targetRoute: "NC01:/root/unidesk/.worktree/base",
projectId: "pikasTech/unidesk",
@@ -1642,6 +1644,7 @@ describe("AgentRun YAML tool credential binding", () => {
test("preserves explicit credential subsets across Aipod admission and rejects unknown identities or conflicts", async () => {
const renderBodies: Record<string, any>[] = [];
const submittedBodies: Record<string, any>[] = [];
const taskTitle = "验证 Artificer 工具凭据";
let credentialMode: "all" | "subset" | "conflict" | "unknown" = "all";
const renderedToolCredentials = (): Record<string, unknown>[] => {
const credentials = [
@@ -1679,6 +1682,9 @@ describe("AgentRun YAML tool credential binding", () => {
return credentials;
};
const renderedTask = () => ({
title: taskTitle,
metadata: { title: taskTitle },
sessionRef: { metadata: { title: taskTitle } },
tenantId: "unidesk",
projectId: "pikasTech/unidesk",
queue: "commander",
@@ -1726,7 +1732,7 @@ describe("AgentRun YAML tool credential binding", () => {
process.env.AGENTRUN_CLIENT_CONFIG = tempConfigPath;
await Bun.write(tempConfigPath, agentRunMinimalClientYaml.replace("http://agentrun.example.local:8080", server.url.href.replace(/\/$/u, "")));
const accepted = await runAgentRunCommand(null, ["create", "task", "--aipod", "Artificer", "--prompt", "fixture", "--model", "gpt-explicit", "--reasoning-effort", "high", "-o", "json"]);
const accepted = await runAgentRunCommand(null, ["create", "task", "--aipod", "Artificer", "--title", taskTitle, "--prompt", "fixture", "--model", "gpt-explicit", "--reasoning-effort", "high", "-o", "json"]);
expect(accepted.ok).toBe(true);
expect(renderBodies).toHaveLength(1);
expect(submittedBodies).toHaveLength(1);
@@ -1737,20 +1743,20 @@ describe("AgentRun YAML tool credential binding", () => {
expect(submittedBodies[0]).toEqual(renderedTask());
credentialMode = "subset";
const subsetAccepted = await runAgentRunCommand(null, ["create", "task", "--aipod", "Artificer", "--prompt", "fixture", "-o", "json"]);
const subsetAccepted = await runAgentRunCommand(null, ["create", "task", "--aipod", "Artificer", "--title", taskTitle, "--prompt", "fixture", "-o", "json"]);
expect(subsetAccepted.ok).toBe(true);
expect(renderBodies).toHaveLength(2);
expect(submittedBodies).toHaveLength(2);
expect(submittedBodies[1]).toEqual(renderedTask());
credentialMode = "conflict";
const rejected = await runAgentRunCommand(null, ["create", "task", "--aipod", "Artificer", "--prompt", "fixture", "-o", "json"]);
const rejected = await runAgentRunCommand(null, ["create", "task", "--aipod", "Artificer", "--title", taskTitle, "--prompt", "fixture", "-o", "json"]);
expect(rejected.ok).toBe(false);
expect(submittedBodies).toHaveLength(2);
expect(renderedTextOf(rejected)).toContain("github/github-ssh keys conflict");
credentialMode = "unknown";
const unknownRejected = await runAgentRunCommand(null, ["create", "task", "--aipod", "Artificer", "--prompt", "fixture", "-o", "json"]);
const unknownRejected = await runAgentRunCommand(null, ["create", "task", "--aipod", "Artificer", "--title", taskTitle, "--prompt", "fixture", "-o", "json"]);
expect(unknownRejected.ok).toBe(false);
expect(submittedBodies).toHaveLength(2);
expect(renderedTextOf(unknownRejected)).toContain("artifact-store/publish is not declared by YAML lane");
+8 -8
View File
@@ -57,19 +57,19 @@ export function agentRunHelp(): unknown {
"bun scripts/cli.ts agentrun describe run/<runId> --node NC01 --lane nc01-v02",
"bun scripts/cli.ts agentrun get tasks -o wide",
"bun scripts/cli.ts agentrun get tasks -o json",
"bun scripts/cli.ts agentrun describe task/<taskId>",
"bun scripts/cli.ts agentrun describe \"修复 HWLAB 的 cicd\"",
"bun scripts/cli.ts agentrun describe task/<taskId> --input -o json",
"bun scripts/cli.ts agentrun events run/<runId> --after-seq 0 --expect 10 --timeout 120s --limit 100",
"bun scripts/cli.ts agentrun events \"修复 HWLAB 的 cicd\" --after-seq 0 --expect 10 --timeout 120s --limit 100",
"bun scripts/cli.ts agentrun events run/<runId> --detail-seq <seq>",
"bun scripts/cli.ts agentrun logs session/<sessionId> --tail 100",
"bun scripts/cli.ts agentrun result run/<runId> --command <commandId>",
"bun scripts/cli.ts agentrun logs \"修复 HWLAB 的 cicd\" --tail 100",
"bun scripts/cli.ts agentrun result \"修复 HWLAB 的 cicd\"",
"bun scripts/cli.ts agentrun ack task/<taskId> --reader-id cli",
"bun scripts/cli.ts agentrun cancel task/<taskId> --reason <text> --dry-run",
"bun scripts/cli.ts agentrun retry task/<taskId> --idempotency-key <key> --reason <text> --dry-run",
"bun scripts/cli.ts agentrun get attempts --task <taskId> --limit 20",
"bun scripts/cli.ts agentrun dispatch task/<taskId>",
"bun scripts/cli.ts agentrun create task --aipod Artificer --prompt-stdin --idempotency-key <key>",
"bun scripts/cli.ts agentrun create task --aipod Artificer --target <Target> --target-workspace <absolute-task-worktree> --repo <owner/repo> --ref <branch> --mdtodo-id <R6.3> --prompt-stdin [--dry-run]",
"bun scripts/cli.ts agentrun create task --aipod Artificer --title \"修复 HWLAB 的 cicd\" --prompt-stdin --idempotency-key <key>",
"bun scripts/cli.ts agentrun create task --aipod Artificer --title \"修复 HWLAB 的 cicd\" --target <Target> --target-workspace <absolute-task-worktree> --repo <owner/repo> --ref <branch> --mdtodo-id <R6.3> --prompt-stdin [--dry-run]",
"bun scripts/cli.ts agentrun apply -f - --dry-run",
"bun scripts/cli.ts agentrun send session/<sessionId> --aipod Artificer --prompt-stdin",
"bun scripts/cli.ts agentrun explain task",
@@ -318,7 +318,7 @@ export function agentRunHelpText(args: string[]): string {
return "Usage: bun scripts/cli.ts agentrun dispatch task/<taskId>";
}
if (verb === "create") {
return "Usage: bun scripts/cli.ts agentrun create task --aipod Artificer --target <Target> --target-workspace <task-worktree> --repo <owner/repo> --ref <branch> --mdtodo-id <R6.3> --prompt-stdin [--idempotency-key <key>] [--dry-run]";
return "Usage: bun scripts/cli.ts agentrun create task --aipod Artificer --title <中文短标题> --target <Target> --target-workspace <task-worktree> --repo <owner/repo> --ref <branch> --mdtodo-id <R6.3> --prompt-stdin [--idempotency-key <key>] [--dry-run]";
}
if (verb === "apply") {
return "Usage: bun scripts/cli.ts agentrun apply -f task.yaml|json|- [--dry-run]\nTask manifests use kind: Task and spec: <AgentRun task payload>.";
@@ -413,7 +413,7 @@ export function agentRunHelpText(args: string[]): string {
" bun scripts/cli.ts agentrun logs session/<sessionId> --tail 100",
" bun scripts/cli.ts agentrun retry task/<taskId> --idempotency-key <key> --reason <text> --dry-run",
" bun scripts/cli.ts agentrun get attempts --task <taskId> --limit 20",
" bun scripts/cli.ts agentrun create task --aipod Artificer --target <Target> --target-workspace <task-worktree> --repo <owner/repo> --ref <branch> --mdtodo-id <R6.3> --prompt-stdin",
" bun scripts/cli.ts agentrun create task --aipod Artificer --title \"修复 HWLAB 的 cicd\" --target <Target> --target-workspace <task-worktree> --repo <owner/repo> --ref <branch> --mdtodo-id <R6.3> --prompt-stdin",
"",
"Machine/debug output:",
" -o json|yaml emits a stable UniDesk resource object without the global JSON envelope.",
+1
View File
@@ -528,6 +528,7 @@ export function renderTargetTaskMutationLines(targetTask: Record<string, unknown
return [
"",
"TargetTaskPreflight:",
` Title: ${displayValue(targetTask.title)}`,
` MDTODO: ${displayValue(targetTask.mdtodoId)} project=${displayValue(targetTask.projectId)}`,
` Target: ${displayValue(targetTask.targetRoute)}`,
` Source: ${displayValue(targetTask.repository)} ref=${displayValue(targetTask.ref)}`,
+76 -6
View File
@@ -43,6 +43,7 @@ import { AgentRunRestError, renderAgentRunRestError, resolveAgentRunRestTarget,
import { renderSessionSendError } from "./session-send-render";
import { runAgentRunResourceThroughTarget } from "./target-execution";
import { runnerJobObservationScript, type RunnerJobObservationIdentity } from "./runner-observation";
import { isChineseTaskTitle, validateChineseTaskTitle } from "./target-task";
import { capture, captureJsonPayload, compactCapture, nonNegativeIntegerOrNull, record, stringOrNull } from "./utils";
export function agentRunGetKindHelp(kindRaw: string): string {
@@ -123,6 +124,9 @@ function validateResourceActionBeforeTarget(
if (verb === "get" && parseResourceKind(action) === "attempt" && options.taskId === null) {
throw new AgentRunRestError("validation-failed", "get attempts requires --task <taskId>");
}
if (verb === "create" && parseResourceKind(action) === "task" && options.aipod === "Artificer") {
validateChineseTaskTitle(options.title);
}
}
function resourceErrorOutputOptions(args: string[]): AgentRunResourceOptions {
@@ -187,6 +191,7 @@ export function parseResourceOptions(args: string[]): AgentRunResourceOptions {
dryRun: false,
file: null,
aipod: null,
title: null,
idempotencyKey: null,
promptStdin: false,
node: null,
@@ -198,7 +203,7 @@ export function parseResourceOptions(args: string[]): AgentRunResourceOptions {
mdtodoId: null,
passthroughArgs: [],
};
const valueFlags = new Set(["-o", "--output", "--limit", "--cursor", "--queue", "--state", "--reader-id", "--task", "--task-id", "--run", "--run-id", "--command", "--command-id", "--session", "--session-id", "--after-seq", "--expect", "--timeout", "--detail-seq", "--tail", "--reason", "-f", "--file", "--filename", "--aipod", "--idempotency-key", "--node", "--lane", "--target", "--target-workspace", "--repo", "--ref", "--mdtodo-id"]);
const valueFlags = new Set(["-o", "--output", "--limit", "--cursor", "--queue", "--state", "--reader-id", "--task", "--task-id", "--run", "--run-id", "--command", "--command-id", "--session", "--session-id", "--after-seq", "--expect", "--timeout", "--detail-seq", "--tail", "--reason", "-f", "--file", "--filename", "--aipod", "--title", "--idempotency-key", "--node", "--lane", "--target", "--target-workspace", "--repo", "--ref", "--mdtodo-id"]);
const booleanFlags = new Set(["--full", "--raw", "--debug", "--input", "--unread", "--dry-run", "--full-text", "--prompt-stdin", "--stdin"]);
for (let index = 0; index < args.length; index += 1) {
const arg = args[index] ?? "";
@@ -341,6 +346,7 @@ export function applyResourceOption(options: AgentRunResourceOptions, flag: stri
else if (flag === "--reason") options.reason = requiredValue(value, flag);
else if (flag === "-f" || flag === "--file" || flag === "--filename") options.file = requiredValue(value, flag);
else if (flag === "--aipod") options.aipod = requiredValue(value, flag);
else if (flag === "--title") options.title = requiredValue(value, flag);
else if (flag === "--idempotency-key") options.idempotencyKey = requiredValue(value, flag);
else if (flag === "--node") options.node = requiredValue(value, flag);
else if (flag === "--lane") options.lane = requiredValue(value, flag);
@@ -458,7 +464,8 @@ export async function resourceRetry(config: UniDeskConfig | null, command: strin
}
export async function resourceDescribe(config: UniDeskConfig | null, command: string, action: string | undefined, args: string[], options: AgentRunResourceOptions): Promise<RenderedCliResult> {
const ref = parseResourceRef(action, args);
const titleContext = await resolveTaskTitleContext(config, action);
const ref = titleContext === null ? parseResourceRef(action, args) : { kind: "task", name: titleContext.taskId } as const;
if (options.taskInput && ref.kind !== "task") throw new AgentRunRestError("validation-failed", "describe --input supports task/<taskId> only");
if (ref.kind === "task") {
const result = await runAgentRunRestCommand(config, "queue", ["show", ref.name, ...(options.full ? ["--full"] : [])]);
@@ -619,8 +626,11 @@ function runnerJobObservationIdentity(managerResource: Record<string, unknown>,
}
export async function resourceEvents(config: UniDeskConfig | null, command: string, action: string | undefined, args: string[], options: AgentRunResourceOptions): Promise<RenderedCliResult> {
const ref = parseResourceRef(action, args, "run");
const runId = ref.kind === "run" ? ref.name : options.runId ?? requiredContext("events", "--run <runId>");
const titleContext = await resolveTaskTitleContext(config, action);
const ref = titleContext === null ? parseResourceRef(action, args, "run") : null;
const runId = titleContext === null
? ref?.kind === "run" ? ref.name : options.runId ?? requiredContext("events", "--run <runId>")
: requiredTitleResource(titleContext, "runId", "events");
if (options.eventDetailSeq !== null) {
const detailSeq = options.eventDetailSeq;
const result = await runAgentRunRestCommand(config, "runs", ["events", runId, "--after-seq", String(detailSeq - 1), "--limit", "1"]);
@@ -699,8 +709,11 @@ export async function resourceEvents(config: UniDeskConfig | null, command: stri
}
export async function resourceLogs(config: UniDeskConfig | null, command: string, action: string | undefined, args: string[], options: AgentRunResourceOptions): Promise<RenderedCliResult> {
const ref = parseResourceRef(action, args, "session");
if (ref.kind !== "session") throw new Error("logs currently requires session/<sessionId>");
const titleContext = await resolveTaskTitleContext(config, action);
const ref = titleContext === null
? parseResourceRef(action, args, "session")
: { kind: "session", name: requiredTitleResource(titleContext, "sessionId", "logs") } as const;
if (ref.kind !== "session") throw new Error("logs currently requires session/<sessionId> or an exact Chinese task title");
const effectiveLimit = options.tail ?? options.limit;
const logArgs = ["output", ref.name, "--limit", String(effectiveLimit)];
if (options.afterSeq !== null) logArgs.push("--after-seq", String(options.afterSeq));
@@ -753,6 +766,13 @@ export async function resourceLogsTailResult(config: UniDeskConfig | null, sessi
}
export async function resourceResult(config: UniDeskConfig | null, command: string, action: string | undefined, args: string[], options: AgentRunResourceOptions): Promise<RenderedCliResult> {
const titleContext = await resolveTaskTitleContext(config, action);
if (titleContext !== null) {
const runId = requiredTitleResource(titleContext, "runId", "result");
const commandId = requiredTitleResource(titleContext, "commandId", "result");
const result = await runAgentRunRestCommand(config, "runs", ["result", runId, "--command-id", commandId]);
return renderResultSummary(command, result, options, { kind: "run", name: runId });
}
const ref = parseResourceRef(action, args);
if (ref.kind === "run") {
const commandId = options.commandId ?? requiredContext("run result", "--command <commandId>");
@@ -767,6 +787,56 @@ export async function resourceResult(config: UniDeskConfig | null, command: stri
throw new Error("result supports run/<runId> or command/<commandId>");
}
interface AgentRunTaskTitleContext {
title: string;
taskId: string;
runId: string | null;
commandId: string | null;
sessionId: string | null;
}
async function resolveTaskTitleContext(config: UniDeskConfig | null, action: string | undefined): Promise<AgentRunTaskTitleContext | null> {
if (!isChineseTaskTitle(action)) return null;
const title = validateChineseTaskTitle(action);
const result = await runAgentRunRestCommand(config, "queue", ["list", "--limit", "500"]);
const tasks = arrayRecords(record(innerData(result)).items ?? innerData(result));
const matches = tasks.filter((task) => normalizeStoredTaskTitle(task) === title);
if (matches.length === 0) {
throw new AgentRunRestError("not-found", `未找到中文标题完全匹配“${title}”的 Artificer 任务。`);
}
if (matches.length > 1) {
throw new AgentRunRestError("validation-failed", `中文标题“${title}”匹配到多个任务,不能猜测。`, {
details: {
title,
candidates: matches.map((task) => ({ id: stringOrNull(task.id), state: stringOrNull(task.state ?? task.status) })),
valuesPrinted: false,
},
});
}
const task = matches[0] ?? {};
const attempt = record(task.latestAttempt);
const taskId = stringOrNull(task.id);
if (taskId === null) throw new AgentRunRestError("schema-mismatch", `中文标题“${title}”匹配的任务缺少 id。`);
return {
title,
taskId,
runId: stringOrNull(attempt.runId ?? task.runId),
commandId: stringOrNull(attempt.commandId ?? task.commandId),
sessionId: stringOrNull(attempt.sessionId ?? record(task.sessionRef).sessionId ?? task.sessionId),
};
}
function normalizeStoredTaskTitle(task: Record<string, unknown>): string | null {
const title = stringOrNull(task.title ?? record(task.metadata).title ?? record(record(task.sessionRef).metadata).title);
return title === null ? null : title.trim().replace(/\s+/gu, " ");
}
function requiredTitleResource(context: AgentRunTaskTitleContext, key: "runId" | "commandId" | "sessionId", operation: string): string {
const value = context[key];
if (value !== null) return value;
throw new AgentRunRestError("not-found", `中文标题“${context.title}”对应任务尚无可用于 ${operation}${key}`);
}
export async function resourceAck(config: UniDeskConfig | null, command: string, action: string | undefined, args: string[], options: AgentRunResourceOptions): Promise<RenderedCliResult> {
const ref = parseResourceRef(action, args, "task");
const result = ref.kind === "task"
+9
View File
@@ -50,6 +50,7 @@ import {
targetTaskDisclosure,
targetTaskPrompt,
targetTaskRequestFromArgs,
validateChineseTaskTitle,
type AgentRunTargetTaskPreflight,
} from "./target-task";
import { capture, captureJsonPayload, compactCapture, nonNegativeIntegerOrNull, progressEvent, record, shQuote, sleep, stringOrNull } from "./utils";
@@ -599,8 +600,10 @@ export async function runAgentRunAipodSpecsRest(action: string | undefined, id:
export async function submitQueueTaskRest(args: string[]): Promise<Record<string, unknown>> {
const aipod = agentRunOption(args, "aipod") ?? agentRunOption(args, "aipod-spec");
if (aipod) {
const taskTitle = aipod === "Artificer" ? validateChineseTaskTitle(agentRunOption(args, "title")) : null;
const targetTask = targetTaskPreflightFromArgs(args, aipod);
const renderInput = await aipodRenderInputFromArgs(args, targetTask === null ? 2 : args.length);
if (taskTitle !== null) renderInput.title = taskTitle;
let targetTaskOperation: Record<string, unknown> | null = null;
let canonicalIdempotencyKey: string | null = null;
if (targetTask !== null) {
@@ -625,6 +628,12 @@ export async function submitQueueTaskRest(args: string[]): Promise<Record<string
const rendered = await agentRunRestRequest("agentrun aipod-specs render", "POST", `/api/v1/aipod-specs/${encodeURIComponent(aipod)}/render`, renderInput);
const body = normalizeAipodRenderedQueueTask(record(record(innerData(rendered)).queueTask), args, aipod);
if (Object.keys(body).length === 0) throw new AgentRunRestError("schema-mismatch", `aipod-spec ${aipod} render did not return queueTask`);
if (taskTitle !== null) {
body.title = taskTitle;
body.metadata = { ...record(body.metadata), title: taskTitle };
const sessionRef = record(body.sessionRef);
body.sessionRef = { ...sessionRef, metadata: { ...record(sessionRef.metadata), title: taskTitle } };
}
assertLegalAipodWorkspaceRef(body, aipod);
if (targetTask !== null && canonicalIdempotencyKey !== null) {
body.idempotencyKey = canonicalIdempotencyKey;
+13 -4
View File
@@ -70,6 +70,7 @@ function compactTargetTaskIdentity(
function compactTargetTaskSummary(targetTask: Record<string, unknown>): Record<string, unknown> {
const workspace = asRecord(targetTask.workspace);
return {
title: boundedText(targetTask.title, 80),
mdtodoId: boundedText(targetTask.mdtodoId, 80),
target: boundedText(targetTask.target, 120),
targetWorkspace: boundedText(targetTask.targetWorkspace, 360),
@@ -182,6 +183,8 @@ function targetTaskIdentityNext(
const commandId = asString(identity.commandId);
const runnerJobId = asString(identity.runnerJobId);
const sessionId = asString(identity.sessionId);
const title = asString(targetTask.title);
const selector = title === null ? null : shellQuote(title);
const target = asString(targetTask.target) ?? asString(aipodBinding.node);
const lane = asString(aipodBinding.lane);
const suffix = [
@@ -191,24 +194,30 @@ function targetTaskIdentityNext(
const scoped = (command: string): string => suffix.length === 0 ? command : `${command} ${suffix}`;
return {
...(taskId === null ? {} : {
task: scoped(`bun scripts/cli.ts agentrun describe task/${taskId}`),
task: scoped(`bun scripts/cli.ts agentrun describe ${selector ?? `task/${taskId}`}`),
attempts: scoped(`bun scripts/cli.ts agentrun get attempts --task ${taskId}`),
}),
...(runId === null ? {} : {
events: scoped(`bun scripts/cli.ts agentrun events run/${runId} --after-seq 0 --limit 20`),
events: scoped(`bun scripts/cli.ts agentrun events ${selector ?? `run/${runId}`} --after-seq 0 --limit 20`),
}),
...(runId === null || commandId === null ? {} : {
result: scoped(`bun scripts/cli.ts agentrun result run/${runId} --command ${commandId}`),
result: scoped(selector === null
? `bun scripts/cli.ts agentrun result run/${runId} --command ${commandId}`
: `bun scripts/cli.ts agentrun result ${selector}`),
}),
...(runnerJobId === null || runId === null ? {} : {
runner: scoped(`bun scripts/cli.ts agentrun describe runnerjob/${runnerJobId} --run ${runId}`),
}),
...(sessionId === null ? {} : {
logs: scoped(`bun scripts/cli.ts agentrun logs session/${sessionId} --tail 120`),
logs: scoped(`bun scripts/cli.ts agentrun logs ${selector ?? `session/${sessionId}`} --tail 120`),
}),
};
}
function shellQuote(value: string): string {
return `'${value.replace(/'/gu, `'"'"'`)}'`;
}
function innerData(value: Record<string, unknown>): unknown {
return value.data ?? value;
}
+43 -1
View File
@@ -3,6 +3,7 @@ import { realpathSync, statSync } from "node:fs";
import { spawnSync } from "node:child_process";
export interface AgentRunTargetTaskRequest {
readonly title: string;
readonly target: string;
readonly targetWorkspace: string;
readonly repository: string;
@@ -43,6 +44,7 @@ export interface AgentRunTargetTaskOperationEvidence {
readonly tenantId: string;
readonly projectId: string;
readonly aipod: string;
readonly title: string;
readonly mdtodoId: string;
readonly target: string;
readonly repository: string;
@@ -68,6 +70,7 @@ export class AgentRunTargetTaskPreflightError extends Error {
export function targetTaskRequestFromArgs(args: string[]): AgentRunTargetTaskRequest | null {
const raw = {
title: option(args, "title"),
target: option(args, "target"),
targetWorkspace: option(args, "target-workspace"),
repository: option(args, "repo"),
@@ -80,11 +83,12 @@ export function targetTaskRequestFromArgs(args: string[]): AgentRunTargetTaskReq
if (missing.length > 0) {
throw new AgentRunTargetTaskPreflightError(
"target-context",
`Artificer Target 派单缺少 ${missing.join(", ")};请同时提供 --target、--target-workspace、--repo、--ref 和 --mdtodo-id。`,
`Artificer Target 派单缺少 ${missing.join(", ")};请同时提供 --title、--target、--target-workspace、--repo、--ref 和 --mdtodo-id。`,
{ missing },
);
}
return {
title: validateChineseTaskTitle(raw.title),
target: validateTarget(raw.target as string),
targetWorkspace: validateTargetWorkspace(raw.targetWorkspace as string),
repository: normalizeRepository(raw.repository as string),
@@ -209,6 +213,7 @@ export function preflightTargetTask(request: AgentRunTargetTaskRequest, markerEn
export function targetTaskPrompt(preflight: AgentRunTargetTaskPreflight, userPrompt: string | null): string {
const context = [
"任务目标上下文(由 UniDesk YAML-first CLI 在创建 task 前经 Target trans 只读核验):",
`- 中文任务标题: ${preflight.title}`,
`- MDTODO ID: ${preflight.mdtodoId}`,
`- Target: ${preflight.target}`,
`- targetWorkspace: ${preflight.targetWorkspace}`,
@@ -229,6 +234,7 @@ export function targetTaskPrompt(preflight: AgentRunTargetTaskPreflight, userPro
export function targetTaskDisclosure(preflight: AgentRunTargetTaskPreflight): Record<string, unknown> {
return {
title: preflight.title,
mdtodoId: preflight.mdtodoId,
target: preflight.target,
targetWorkspace: preflight.targetWorkspace,
@@ -260,6 +266,7 @@ export function targetTaskOperationEvidence(
tenantId: operation.tenantId,
projectId: operation.projectId,
aipod: operation.aipod,
title: preflight.title,
mdtodoId: preflight.mdtodoId,
target: preflight.target,
repository: preflight.repository,
@@ -311,6 +318,41 @@ export function targetTaskIdempotencyKey(
return resolveTargetTaskIdempotency(preflight, operation, null).idempotencyKey;
}
export function validateChineseTaskTitle(raw: string | null): string {
if (raw === null || raw.trim().length === 0) {
throw new AgentRunTargetTaskPreflightError(
"task-title",
"Artificer 创建任务必须提供 --title <中文短标题>,例如 --title \"修复 HWLAB 的 cicd\"。",
{ reason: "missing-chinese-task-title" },
);
}
if (/\r|\n|[\u0000-\u001f\u007f]/u.test(raw)) {
throw new AgentRunTargetTaskPreflightError("task-title", "Artificer 任务标题必须是单行短字符串。", {
reason: "invalid-task-title-control-character",
});
}
const title = raw.trim().replace(/\s+/gu, " ");
const length = Array.from(title).length;
if (length < 2 || length > 80) {
throw new AgentRunTargetTaskPreflightError("task-title", "Artificer 任务标题长度必须为 2 到 80 个字符。", {
reason: "invalid-task-title-length",
length,
});
}
if (!isChineseTaskTitle(title)) {
throw new AgentRunTargetTaskPreflightError(
"task-title",
"Artificer 任务标题必须至少包含一个中文汉字;HWLAB、CI/CD 等专有名和空格可以保留。",
{ reason: "missing-han-character" },
);
}
return title;
}
export function isChineseTaskTitle(value: string | undefined): value is string {
return typeof value === "string" && /\p{Script=Han}/u.test(value);
}
function git(request: AgentRunTargetTaskRequest, args: string[], stage: string): string {
const result = spawnSync("git", args, {
cwd: request.targetWorkspace,
+1
View File
@@ -85,6 +85,7 @@ export interface AgentRunResourceOptions {
dryRun: boolean;
file: string | null;
aipod: string | null;
title: string | null;
idempotencyKey: string | null;
promptStdin: boolean;
node: string | null;