fix: 移除 AgentRun worktree Git 状态门禁

This commit is contained in:
Codex
2026-07-16 11:50:16 +02:00
parent 5e0010590e
commit 69c3787598
8 changed files with 125 additions and 198 deletions
+2 -1
View File
@@ -76,7 +76,8 @@ AipodSpec/Artificer、task manifest 和 queue 渐进披露见 [references/resour
- 新写入口不要用 `codex submit/steer/resume`;这些 legacy mutation 已冻结。
- Artificer 的 runner primary workspace 与任务 Target 必须分开判定:
- runner primary workspace 只装配默认 UniDesk resource bundle、`tools/trans`、skill 和轻量准备;
- 待修改源码只来自派单上下文中的 `targetWorkspace`,且该路径必须是任务专属 worktree
- 待修改源码只来自派单上下文中的显式 `targetWorkspace`
- admission 只校验绝对路径、存在性和目录类型,不读取 Git 状态,也不因 dirty、ahead、behind 或已有 diff 产生 warning/blocker
- 现场探测、Git、编辑、验证和运行面观察必须使用 `trans <Target>:<targetWorkspace>`
- 不得把 runner 本地结果、默认 bundle 的 Git 状态或容器内同名路径当作 Target 验收;
- `unidesk-trans` required skill、runner tools bundle、`unidesk-ssh` SecretRef 和 endpoint presence 缺一项即归为装配失败;
@@ -32,21 +32,22 @@ bun scripts/cli.ts agentrun create task \
- 标题按规范化后的完全匹配解析,重复标题必须改用内部 ID 处理歧义。
- 默认直接执行 live 单命令;不要把 `--dry-run` 作为派单前置步骤;
- live 必须先经 owning YAML 选中的 Target `trans` 重入并完成只读 preflight
- live 必须先经 owning YAML 选中的 Target `trans` 重入并完成最小路径 preflight
- preflight 必须验证:
- task worktree 是 Git 根目录且 clean
- origin 与 `--repo` 一致
- current branch 与 `--ref` 一致;
- HEAD 与 origin 远端 ref commit 一致
- `targetWorkspace` 已显式提供为绝对路径
- Target 上的该路径存在且为目录
- preflight 禁止读取 task worktree Git 状态:
- dirty、ahead、behind、detached 和已有 diff 不产生 warning 或 blocker
- `repo``ref` 只保留为显式任务身份;
- preflight 输出必须披露:
- Target route、repo/ref、projectId、HEAD/remote 关系
- Target route、repo/ref、projectId`gitStateInspected=false`
- 合法的 `workspaceRef.kind/path`
- 从 AipodSpec 继承的默认 resource bundle
- session identity
- provider/tool SecretRef 的对象、key 与 presence
- preflight 失败固定 `mutation=false`,不得创建 task
- preflight 通过后在同一命令内 submit 并自动 dispatch,不要求用户再运行第二条命令;
- 未显式传 `--idempotency-key` 时,由 Target/repo/ref/MDTODO/verified commit 派生稳定键;
- 未显式传 `--idempotency-key` 时,由 Target/repo/ref/targetWorkspace/MDTODO 和完整请求 payload 派生稳定键;
- 幂等重试遇到已有 running 或 terminal task 时,返回既有 attempt/run/session,不重复 dispatch
- 最终输出必须包含 task、attempt、run、command、runner 和 session identity。
- Target live 的默认 human、`-o json``-o yaml` 使用同一份有界投影:
@@ -60,7 +61,7 @@ Artificer 内部执行边界:
- primary workspace 只承载默认 UniDesk bundle、`tools/trans` 和 required skills
- 任务源码操作全部使用 `trans <Target>:<targetWorkspace>`
- `targetWorkspace` 必须是任务专属 worktree
- `targetWorkspace` 是用户显式指定的任务工作目录
- runner 本地结果不得替代 Target 原入口证据;
- `unidesk-trans`、runner tools bundle、`unidesk-ssh` SecretRef 和 endpoint presence 必须同时存在;
- Secret 值不得进入 prompt、task、日志或报告。
@@ -287,3 +287,7 @@
### R5.1 [in_progress]
泛化改进 PaC consumer 首次 CI/CD bootstrap 与排错:source-artifact 在 PR 前验证流水线源码入口,debug-step 一次返回失败 task/step/exit code/有界日志,closeout --wait 持续观察并精确关联 source、GitOps、Argo 与 runtime digesthistory 提示单 consumer 快路径;由 [GitHub issue #2307](https://github.com/pikasTech/unidesk/issues/2307) 跟踪,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R5.1_Task_Report.md)。
### R5.2 [in_progress]
解决 [UniDesk #2310](https://github.com/pikasTech/unidesk/issues/2310):彻底废弃 AgentRun 对 task worktree 的 Git 状态校验,使 dirty、ahead、behind 和已有 diff 均不阻断 create/apply/dispatch;只保留显式 targetWorkspace、绝对路径和目录存在性校验,不读取 Git 状态形成 warning 或 blocker,不修改生产运行面,提交独立 PR 且不自行合并,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R5.2_Task_Report.md)。
+6 -7
View File
@@ -273,21 +273,20 @@ Artificer 标题契约:
- 默认直接执行 live 单命令,不把 `--dry-run` 作为派单前置步骤;
- 未显式传 `--lane` 时,空选择或只传 YAML 默认 node 都使用 `config/agentrun.yaml#controlPlane.default`
- live 先经 Target `trans` 重入并执行 source/worktree/Aipod/SecretRef 只读预检
- live 先经 Target `trans` 重入,校验显式工作区路径与 Aipod/SecretRef
- 预检通过后在同一命令内 submit 并自动 dispatch
- 幂等重试不得重复创建 attempt 或重复 dispatch
- UniDesk 不实现 AgentRun queue 协议,也不把 task double-write 回旧 Code Queue。
Artificer Target preflight 必须在 task 创建前完成:
- `targetWorkspace` 是任务专属 Git worktree 根目录;
- worktree 必须 clean
- origin 与 `--repo` 一致
- current branch 与 `--ref` 一致
- HEAD 与 origin 远端 ref commit 一致;
- `targetWorkspace` 必须显式提供为 Target 上的绝对路径,且路径存在并为目录;
- `repo``ref` 只作为用户显式提供的任务身份,不用于读取或判断工作区 Git 状态
- dirty、ahead、behind、detached 和已有 diff 均不产生 warning 或 blocker
- create、apply 和 dispatch 不得读取 task worktree Git 状态形成 admission 结论
- AipodSpec 渲染出的 `workspaceRef` 只允许 `kind/path`
- 默认 UniDesk resource bundle 保持继承,不把私有目标 repo 改成 runner primary bundle
- 输出包含 projectId、session identitySecretRef presence 和 HEAD/remote 关系
- 输出包含 projectId、显式 Target 上下文、session identitySecretRef presence
- 失败固定 `mutation=false`,且不创建 task
- 成功返回 task、attempt、run、command、runner 和 session identity。
+93 -31
View File
@@ -1,4 +1,7 @@
import { readFileSync, rmSync } from "node:fs";
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { spawnSync } from "node:child_process";
import { describe, expect, test } from "bun:test";
import {
parseAgentRunClientConfigYaml,
@@ -37,6 +40,12 @@ function renderedContentTypeOf(value: unknown): string {
return contentType;
}
function gitFixture(cwd: string, args: string[]): string {
const result = spawnSync("git", args, { cwd, encoding: "utf8" });
if (result.status !== 0) throw new Error(String(result.stderr || result.stdout));
return String(result.stdout).trim();
}
const agentRunClientYaml = [
"version: 1",
"kind: AgentRunConfig",
@@ -219,13 +228,44 @@ 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", () => {
test("keeps only explicit absolute path and directory existence admission", () => {
expect(() => targetTaskRequestFromArgs([
"--title", "检查 AgentRun 工作区路径",
"--target", "NC01",
"--target-workspace", "relative/workspace",
"--repo", "pikasTech/unidesk",
"--ref", "fix/path-check",
"--mdtodo-id", "R5.2",
])).toThrow("--target-workspace 必须是 Target 上的绝对路径");
const request = targetTaskRequestFromArgs([
"--title", "检查 AgentRun 工作区路径",
"--target", "NC01",
"--target-workspace", "/definitely/missing/agentrun-workspace",
"--repo", "pikasTech/unidesk",
"--ref", "fix/path-check",
"--mdtodo-id", "R5.2",
]);
if (request === null) throw new Error("expected Target request");
const markerEnv = "UNIDESK_AGENTRUN_TARGET_EXECUTED_PATH_TEST";
const previous = process.env[markerEnv];
process.env[markerEnv] = request.target;
try {
expect(() => preflightTargetTask(request, markerEnv)).toThrow("Target 工作区不存在或不是目录");
} finally {
if (previous === undefined) delete process.env[markerEnv];
else process.env[markerEnv] = previous;
}
});
test("requires trans re-entry but admits dirty, ahead, behind, and mismatched Git state", () => {
const workspace = mkdtempSync(join(tmpdir(), "unidesk-agentrun-target-"));
const request = targetTaskRequestFromArgs([
"--title", "修复 SelfMedia 的 OCR 规划器",
"--target", "NC01",
"--target-workspace", "/root/.worktrees/selfmedia/r6-ocr-visual-planner",
"--target-workspace", workspace,
"--repo", "pikainc/selfmedia",
"--ref", "feat/r6-ocr-visual-planner",
"--ref", "declared/ref",
"--mdtodo-id", "R6.3",
]);
expect(request).not.toBeNull();
@@ -235,25 +275,53 @@ describe("Artificer Target one-command dispatch", () => {
delete process.env[markerEnv];
try {
expect(() => preflightTargetTask(request, markerEnv)).toThrow("只能在外层 trans 重入 NC01 后执行");
gitFixture(workspace, ["init", "-b", "actual/ref"]);
gitFixture(workspace, ["config", "user.email", "test@example.com"]);
gitFixture(workspace, ["config", "user.name", "AgentRun Test"]);
writeFileSync(join(workspace, "tracked.txt"), "base\n", "utf8");
gitFixture(workspace, ["add", "tracked.txt"]);
gitFixture(workspace, ["commit", "-m", "base"]);
const baseCommit = gitFixture(workspace, ["rev-parse", "HEAD"]);
const baseTree = gitFixture(workspace, ["rev-parse", "HEAD^{tree}"]);
writeFileSync(join(workspace, "tracked.txt"), "local\n", "utf8");
gitFixture(workspace, ["commit", "-am", "local ahead"]);
const remoteCommit = gitFixture(workspace, ["commit-tree", baseTree, "-p", baseCommit, "-m", "remote behind"]);
gitFixture(workspace, ["update-ref", "refs/remotes/origin/declared/ref", remoteCommit]);
gitFixture(workspace, ["remote", "add", "origin", "https://example.invalid/wrong/repository.git"]);
writeFileSync(join(workspace, "tracked.txt"), "dirty diff\n", "utf8");
expect(gitFixture(workspace, ["rev-list", "--left-right", "--count", "HEAD...refs/remotes/origin/declared/ref"]))
.toMatch(/^1\s+1$/u);
expect(gitFixture(workspace, ["status", "--porcelain"])).not.toBe("");
process.env[markerEnv] = request.target;
const preflight = preflightTargetTask(request, markerEnv);
expect(preflight).toMatchObject({
targetWorkspace: workspace,
workspaceRoot: workspace,
repository: "pikainc/selfmedia",
ref: "declared/ref",
sourceAuthority: {
kind: "explicit-target-context",
gitStateInspected: false,
valuesPrinted: false,
},
});
expect(preflight).not.toHaveProperty("verifiedCommit");
expect(preflight).not.toHaveProperty("clean");
} finally {
if (previous === undefined) delete process.env[markerEnv];
else process.env[markerEnv] = previous;
rmSync(workspace, { recursive: true, force: true });
}
const preflight = {
...request,
targetRoute: `${request.target}:${request.targetWorkspace}`,
projectId: request.repository,
workspaceRoot: request.targetWorkspace,
currentBranch: request.ref,
headCommit: "a".repeat(40),
clean: true,
originRepository: request.repository,
verifiedCommit: "a".repeat(40),
matchedRemoteRef: `refs/heads/${request.ref}`,
refRelation: "branch-and-head-match-remote-ref",
sourceAuthority: {
kind: "target-workspace-origin",
credentialScope: "target-owned-git-credential",
kind: "explicit-target-context",
gitStateInspected: false,
valuesPrinted: false,
},
} satisfies AgentRunTargetTaskPreflight;
@@ -275,16 +343,9 @@ describe("Artificer Target one-command dispatch", () => {
targetRoute: "NC01:/root/unidesk/.worktree/base",
projectId: "pikasTech/unidesk",
workspaceRoot: "/root/unidesk/.worktree/base",
currentBranch: "fix/base",
headCommit: "a".repeat(40),
clean: true,
originRepository: "pikasTech/unidesk",
verifiedCommit: "a".repeat(40),
matchedRemoteRef: "refs/heads/fix/base",
refRelation: "branch-and-head-match-remote-ref",
sourceAuthority: {
kind: "target-workspace-origin",
credentialScope: "target-owned-git-credential",
kind: "explicit-target-context",
gitStateInspected: false,
valuesPrinted: false,
},
} as const;
@@ -300,8 +361,6 @@ describe("Artificer Target one-command dispatch", () => {
...fixture,
targetRoute: `NC01:${fixture.targetWorkspace}`,
workspaceRoot: fixture.targetWorkspace,
currentBranch: fixture.ref,
matchedRemoteRef: `refs/heads/${fixture.ref}`,
},
{ aipod: "Artificer", tenantId: "default", projectId: fixture.repository, prompt: fixture.prompt },
));
@@ -312,8 +371,6 @@ describe("Artificer Target one-command dispatch", () => {
...fixtures[0],
targetRoute: `NC01:${fixtures[0].targetWorkspace}`,
workspaceRoot: fixtures[0].targetWorkspace,
currentBranch: fixtures[0].ref,
matchedRemoteRef: `refs/heads/${fixtures[0].ref}`,
} satisfies AgentRunTargetTaskPreflight;
const operation = { aipod: "Artificer", tenantId: "default", projectId: replay.repository, prompt: fixtures[0].prompt };
const evidence = targetTaskOperationEvidence(replay, operation);
@@ -497,12 +554,9 @@ describe("Artificer Target one-command dispatch", () => {
projectId: "pikasTech/unidesk",
repository: "pikasTech/unidesk",
ref: "fix/artificer-compact-output",
verifiedCommit: "a".repeat(40),
refRelation: "branch-and-head-match-remote-ref",
workspace: {
branch: "fix/artificer-compact-output",
headCommit: "a".repeat(40),
clean: true,
root: "/root/.worktrees/unidesk/artificer-compact-output",
gitStateInspected: false,
},
prompt: hugePrompt,
},
@@ -564,6 +618,9 @@ describe("Artificer Target one-command dispatch", () => {
}
expect(text).toContain("R6.4");
expect(text).toContain("dispatched");
expect(text).toContain("gitStateInspected");
expect(text).not.toContain("verifiedCommit");
expect(text).not.toContain("refRelation");
}
const jsonText = renderedTextOf(renderMutationSummary(
@@ -592,6 +649,11 @@ describe("Artificer Target one-command dispatch", () => {
});
expect((json.autoDispatch as Record<string, unknown>).decision).toBe("dispatched");
expect((json.targetTask as Record<string, unknown>).mdtodoId).toBe("R6.4");
expect((json.targetTask as Record<string, unknown>).workspace).toEqual({
root: "/root/.worktrees/unidesk/artificer-compact-output",
gitStateInspected: false,
valuesPrinted: false,
});
expect(jsonCredentials).toMatchObject({ totalCount: 2, allPresent: true });
expect((jsonCredentials.provider as Record<string, unknown>).presentCount).toBe(1);
expect((jsonCredentials.tool as Record<string, unknown>).presentCount).toBe(1);
+1 -2
View File
@@ -532,8 +532,7 @@ export function renderTargetTaskMutationLines(targetTask: Record<string, unknown
` MDTODO: ${displayValue(targetTask.mdtodoId)} project=${displayValue(targetTask.projectId)}`,
` Target: ${displayValue(targetTask.targetRoute)}`,
` Source: ${displayValue(targetTask.repository)} ref=${displayValue(targetTask.ref)}`,
` Commit: HEAD=${displayValue(workspace.headCommit)} remote=${displayValue(targetTask.verifiedCommit)}`,
` Worktree: branch=${displayValue(workspace.branch)} clean=${displayValue(workspace.clean)} relation=${displayValue(targetTask.refRelation)}`,
` Workspace: root=${displayValue(workspace.root)} gitStateInspected=${displayValue(workspace.gitStateInspected)}`,
];
}
+2 -5
View File
@@ -78,12 +78,9 @@ function compactTargetTaskSummary(targetTask: Record<string, unknown>): Record<s
projectId: boundedText(targetTask.projectId, 240),
repository: boundedText(targetTask.repository, 240),
ref: boundedText(targetTask.ref, 240),
verifiedCommit: boundedText(targetTask.verifiedCommit, 160),
refRelation: boundedText(targetTask.refRelation, 120),
workspace: {
branch: boundedText(workspace.branch, 240),
headCommit: boundedText(workspace.headCommit, 160),
clean: booleanOrNull(workspace.clean),
root: boundedText(workspace.root, 360),
gitStateInspected: booleanOrNull(workspace.gitStateInspected),
valuesPrinted: false,
},
valuesPrinted: false,
+8 -144
View File
@@ -1,6 +1,5 @@
import { createHash } from "node:crypto";
import { realpathSync, statSync } from "node:fs";
import { spawnSync } from "node:child_process";
export interface AgentRunTargetTaskRequest {
readonly title: string;
@@ -15,16 +14,9 @@ export interface AgentRunTargetTaskPreflight extends AgentRunTargetTaskRequest {
readonly targetRoute: string;
readonly projectId: string;
readonly workspaceRoot: string;
readonly currentBranch: string;
readonly headCommit: string;
readonly clean: true;
readonly originRepository: string;
readonly verifiedCommit: string;
readonly matchedRemoteRef: string;
readonly refRelation: "branch-and-head-match-remote-ref";
readonly sourceAuthority: {
readonly kind: "target-workspace-origin";
readonly credentialScope: "target-owned-git-credential";
readonly kind: "explicit-target-context";
readonly gitStateInspected: false;
readonly valuesPrinted: false;
};
}
@@ -50,7 +42,6 @@ export interface AgentRunTargetTaskOperationEvidence {
readonly repository: string;
readonly ref: string;
readonly targetWorkspace: string;
readonly verifiedCommit: string;
};
readonly reason: "canonical-logical-operation";
readonly valuesPrinted: false;
@@ -117,94 +108,14 @@ export function preflightTargetTask(request: AgentRunTargetTaskRequest, markerEn
);
}
const gitRoot = git(request, ["rev-parse", "--show-toplevel"], "workspace-git-root").trim();
let observedRoot: string;
try {
observedRoot = realpathSync(gitRoot);
} catch {
throw new AgentRunTargetTaskPreflightError(
"workspace-git-root",
`Target Git 返回了无效工作区根目录:${gitRoot}`,
{ targetWorkspace: request.targetWorkspace },
);
}
if (observedRoot !== workspaceRoot) {
throw new AgentRunTargetTaskPreflightError(
"workspace-git-root",
`--target-workspace 必须指向 Git 工作区根目录;当前根目录是 ${gitRoot}`,
{ targetWorkspace: request.targetWorkspace, observedWorkspaceRoot: gitRoot },
);
}
const originUrl = git(request, ["remote", "get-url", "origin"], "origin").trim();
const originRepository = repositoryFromRemote(originUrl);
if (originRepository === null || originRepository.toLowerCase() !== request.repository.toLowerCase()) {
throw new AgentRunTargetTaskPreflightError(
"origin",
`Target 工作区 origin 与 --repo 不一致;期望 ${request.repository}`,
{
expectedRepository: request.repository,
observedRepository: originRepository,
observedRemoteFingerprint: sha256(originUrl),
},
);
}
const status = git(request, ["status", "--porcelain=v1", "--untracked-files=normal"], "workspace-status");
const dirtyEntryCount = status.split(/\r?\n/u).filter(Boolean).length;
if (dirtyEntryCount > 0) {
throw new AgentRunTargetTaskPreflightError(
"workspace-status",
"Target 固定工作区存在未提交修改;请先保存并按语义提交并行改动,再重新派单。",
{ targetWorkspace: request.targetWorkspace, dirtyEntryCount },
);
}
const currentBranch = git(request, ["branch", "--show-current"], "workspace-branch").trim() || "(detached)";
const headCommit = git(request, ["rev-parse", "HEAD"], "workspace-head").trim().toLowerCase();
const remote = git(request, ["ls-remote", "--exit-code", "origin", request.ref], "remote-ref");
const refs = remote.split(/\r?\n/u)
.map((line) => line.trim().split(/\s+/u))
.filter((parts) => /^[0-9a-f]{40}$/u.test(parts[0] ?? "") && (parts[1] ?? "").length > 0)
.map((parts) => ({ commit: parts[0] as string, ref: parts[1] as string }));
const selected = refs.find((item) => item.ref === `refs/heads/${request.ref}`) ?? refs[0];
if (selected === undefined) {
throw new AgentRunTargetTaskPreflightError(
"remote-ref",
`Target 工作区 origin 中找不到 --ref ${request.ref}`,
{ repository: request.repository, ref: request.ref },
);
}
if (currentBranch !== request.ref || headCommit !== selected.commit.toLowerCase()) {
throw new AgentRunTargetTaskPreflightError(
"workspace-ref-relation",
"Target 任务 worktree 必须检出 --ref,且 HEAD 必须等于 origin 远端提交;请先完成分支切换、提交和推送。",
{
targetWorkspace: request.targetWorkspace,
currentBranch,
expectedBranch: request.ref,
headCommit,
remoteCommit: selected.commit,
matchedRemoteRef: selected.ref,
},
);
}
return {
...request,
targetRoute: `${request.target}:${request.targetWorkspace}`,
projectId: request.repository,
workspaceRoot,
currentBranch,
headCommit,
clean: true,
originRepository,
verifiedCommit: selected.commit,
matchedRemoteRef: selected.ref,
refRelation: "branch-and-head-match-remote-ref",
sourceAuthority: {
kind: "target-workspace-origin",
credentialScope: "target-owned-git-credential",
kind: "explicit-target-context",
gitStateInspected: false,
valuesPrinted: false,
},
};
@@ -212,7 +123,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 只读核验):",
"任务目标上下文(由用户显式提供,并由 UniDesk CLI 在 Target trans 重入后校验目录存在):",
`- 中文任务标题: ${preflight.title}`,
`- MDTODO ID: ${preflight.mdtodoId}`,
`- Target: ${preflight.target}`,
@@ -220,9 +131,7 @@ export function targetTaskPrompt(preflight: AgentRunTargetTaskPreflight, userPro
`- targetRoute: ${preflight.targetRoute}`,
`- repository: ${preflight.repository}`,
`- ref: ${preflight.ref}`,
`- verifiedCommit: ${preflight.verifiedCommit}`,
`- refRelation: ${preflight.refRelation}`,
"- sourceAuthority: target-workspace-origin(凭据值不进入 task、prompt 或日志)",
"- sourceAuthority: explicit-target-context(不读取 Git 状态,不把 dirty/ahead/behind/diff 作为门禁或警告)",
"",
"执行边界:",
`- 目标源码的探测、Git、编辑、验证和运行面观察全部使用 \`trans ${preflight.target}:${preflight.targetWorkspace} <operation>\` 或对应 k3s route。`,
@@ -242,14 +151,10 @@ export function targetTaskDisclosure(preflight: AgentRunTargetTaskPreflight): Re
repository: preflight.repository,
ref: preflight.ref,
projectId: preflight.projectId,
verifiedCommit: preflight.verifiedCommit,
matchedRemoteRef: preflight.matchedRemoteRef,
refRelation: preflight.refRelation,
workspace: {
root: preflight.workspaceRoot,
branch: preflight.currentBranch,
headCommit: preflight.headCommit,
clean: preflight.clean,
gitStateInspected: false,
valuesPrinted: false,
},
sourceAuthority: preflight.sourceAuthority,
mutation: false,
@@ -272,7 +177,6 @@ export function targetTaskOperationEvidence(
repository: preflight.repository,
ref: preflight.ref,
targetWorkspace: preflight.targetWorkspace,
verifiedCommit: preflight.verifiedCommit,
};
const fingerprint = sha256Hex(JSON.stringify({ version: "v1", scope, payloadHash }));
return {
@@ -353,33 +257,6 @@ 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,
encoding: "utf8",
timeout: 15_000,
env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
maxBuffer: 1024 * 1024,
});
if (result.error !== undefined || result.status !== 0) {
const stderr = String(result.stderr ?? "").trim().split(/\r?\n/u)[0] ?? "";
throw new AgentRunTargetTaskPreflightError(
stage,
`Target Git 只读预检失败(${stage});请先在 ${request.target}:${request.targetWorkspace} 修复 origin/ref/凭据可达性。`,
{
target: request.target,
targetWorkspace: request.targetWorkspace,
repository: request.repository,
ref: request.ref,
exitCode: result.status,
timedOut: result.error?.name === "ETIMEDOUT",
stderrSummary: redactGitError(stderr),
},
);
}
return String(result.stdout ?? "");
}
function option(args: string[], name: string): string | null {
const flag = `--${name}`;
for (let index = 0; index < args.length; index += 1) {
@@ -437,19 +314,6 @@ function validateMdtodoId(value: string): string {
return value;
}
function repositoryFromRemote(value: string): string | null {
const normalized = value.replace(/\.git$/u, "");
const match = normalized.match(/^(?:git@github\.com:|https:\/\/github\.com\/)([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)$/u);
return match?.[1] ?? null;
}
function redactGitError(value: string): string {
return value
.replace(/https?:\/\/[^@\s]+@/gu, "https://[redacted]@")
.replace(/(token|password|authorization)[=:][^\s]+/giu, "$1=[redacted]")
.slice(0, 240);
}
function sha256(value: string): string {
return `sha256:${sha256Hex(value)}`;
}