Merge remote-tracking branch 'origin/master' into docs/pj2026-02-terminal-spec

This commit is contained in:
AgentRun Codex
2026-07-13 07:17:00 +00:00
6 changed files with 142 additions and 24 deletions
+5 -3
View File
@@ -15,6 +15,7 @@ description: UniDesk 主代理调度子代理的必读技能。用户提到子
- 发现衍生问题时,只能创建独立 issue 或记录待办,不得扩大当前 prompt、PR、合并范围或验收前置;
- 现有机制阻碍原始目标时,先寻找既有架构内的最小实现路径;确需架构扩展时,必须向用户说明与原始目标的关系并取得明确授权。
- 执行型和调研型委派默认优先使用 AgentRun `Artificer`
- Artificer 的 `create task``apply``dispatch` 必须以下文“子 issue + MDTODO”登记为 fail-closed 前置;禁止先创建 task 或派单再补登记;
- 未显式选模时,不由主代理注入客户端模型默认值;使用 owning AipodSpec 的默认模型,当前为 `gpt-5.6-sol`,默认 reasoning effort 为 `medium`
- Artificer 的 API credential
- 只允许由 UniDesk owning YAML 将 `/root/.codex/auth.json.pika``/root/.codex/config.toml.pika` 投影为 `gpt-pika` SecretRef
@@ -37,9 +38,10 @@ description: UniDesk 主代理调度子代理的必读技能。用户提到子
- 纯文档交付可以按 `$git-spec` 的稳定分支快路径由主代理直接 commit/push,不要求临时 branch、`.worktree` 或 PR;发现本地分叉、并行改动、分支保护或运行面影响时必须保留状态并回到隔离交付。
- 正式 GitHub issue/PR/comment/merge 仍走 `$unidesk-gh`;子代理可以提交 PR 和写调查结论,主代理负责 review/preflight/merge,除非用户明确授权某个子代理自上线自验证。
- 主代理和子代理必须通过 issue/PR/comment 链接传递可复用上下文、调查结论、证据链接和下一步边界;派发前先读既有评论,prompt 中只引用链接和增量任务,不复述长结论,避免不同子代理重复调查同一事实。
- 主代理派发执行型子代理前必须完成两项登记
- 主代理派发任何执行型子代理前必须完成以下登记;对 Artificer,全部步骤必须早于 AgentRun `create task``apply``dispatch`
- 创建子 issue,把主要任务、接续上下文、目标分支/worktree、范围、禁止项和验收入口写入正文;
- 使用 `$mdtodo-edit` 将子 issue 链接登记到对应泛化问题域的 MDTODO ITEM 或 SUBITEM;不得为单个 issue 创建窄范围 MDTODO FILE
- 使用 `$mdtodo-edit` 对应泛化问题域的 MDTODO ITEM 或 SUBITEM 中登记子 issue 链接,并把任务标记为进行中;不得为单个 issue 创建窄范围 MDTODO FILE
- 任一登记缺失或写入失败时不得创建 AgentRun task;派单后补写 MDTODO 不计为合规登记。
- 子代理 prompt 只给子 issue 链接和极短边界,不塞大段任务正文。每个执行型子代理维护自己的子 issue 和评论作为接续上下文;主 issue 评论区只由主代理写入主线 anchor、阶段汇总、调度决策和最终 closeout。
- 子代理不得把过程日志、单步证据、长调查和 post-task 反馈直接堆到主 issue 评论区,只能在主 issue 需要可见时由主代理引用子 issue/PR/comment 链接。
- 主代理跟踪子代理进度时优先读取子 issue 评论区、关联 PR 更新和 bounded issue/PR 状态;不要为了普通进度查询主动 `send_input interrupt` 打断子代理主线。只有子 issue/PR 长时间无更新且只读 worktree 也无推进时,才进入问询、关闭和重开流程。
@@ -70,7 +72,7 @@ description: UniDesk 主代理调度子代理的必读技能。用户提到子
- CI/CD、rollout、PipelineRun、Argo closeout`$unidesk-cicd`
- WebProbe、Workbench、浏览器复测:`$unidesk-webdev`
- 子代理完成后的反馈收口和反馈 issue 判定:`$post-task`
- 需要 MDTODO 记录和执行-审查循环时再`$mdtodo-loop`;不要把普通 GitHub PR 并行工作强行塞进 MDTODO
- Artificer 和其他执行型子代理派单前的任务登记必须使用 `$mdtodo-edit`;只有用户明确要求多轮审查循环修复时才使`$mdtodo-loop`
## 何时读取 reference
@@ -1,4 +1,4 @@
# GitHub Subagent Workflow
# GitHub 子代理工作流
本 reference 记录 UniDesk 主代理调度子代理、并结合 GitHub issue/PR 的稳定工作流。它约束的是协作方式,不替代 `$unidesk-gh` 的具体命令参数,也不替代各业务 skill 的验收标准。
@@ -35,6 +35,11 @@
## GitHub Issue 协作
- 大任务先有 GitHub issue 或在既有 issue 中补并行计划:列出子任务、负责人/子代理、目标分支、预期 PR、验证入口、依赖关系和哪些任务可并行。
- Artificer 派单采用 fail-closed 登记顺序:
- 先创建子 issue,冻结目标分支、工作区、范围、禁止项和验收入口;
- 再使用 `$mdtodo-edit` 在泛化问题域 MDTODO 中创建或更新 ITEM/SUBITEM、登记子 issue 链接并标记进行中;
- 只有上述写入成功后,才允许 AgentRun `create task``apply``dispatch`
- 缺少 MDTODO FILE、任务项、进行中状态或子 issue 链接时不得派单,派单后补写不算合规。
- 主 issue 评论区由主代理独占维护:只写主线 anchor、阶段汇总、调度决策、已采纳结论、下一批边界和最终 closeout。子代理不得直接在主 issue 评论区堆过程、日志、单步证据或 post-task 反馈;需要让主线可见时,由主代理在主 issue 引用子 issue/PR/comment 链接。
- 主代理派发执行型子代理前必须先创建子 issue,不能把主要任务正文直接塞进 subagent prompt。子 issue 标题应能反映父 issue、运行面/模块和子任务;正文必须引用父 issue、目标分支/worktree、允许范围、禁止范围、验收入口、模型/思考等级选择理由和当前接续链接。子代理的调查、单步证据、阻塞、post-task 和后续接力评论都写在自己的子 issue 或关联 PR 中。
- 子代理 prompt 只传子 issue 链接、模型要求和“不写父 issue 评论区”等极短边界;不要在 prompt 里复述长任务、历史结论、日志或完整证据。主代理通过观察子 issue 评论区跟踪进度。
@@ -75,14 +80,15 @@ Prompt 至少包含以下字段,按任务裁剪:
1. 建立计划:列出可并行任务、串行依赖和每个子代理交付物。
2. 为每个执行型子代理先创建子 issue,把任务正文写进子 issue。
3. 同时派发低耦合子任务;prompt 只引用子 issue 链接和极短边界;共享契约先派一个基线任务
4. 轮询子代理结果:子 issue comment、PR、验证摘要、阻塞
5. 对每个 PR 做架构 review、bounded diff、preflight 和必要本地验证
6. 按依赖顺序合并;合并后同步目标 worktree
7. 触发受控 CI/CD 或让明确授权的子代理上线;主代理核对 closeout
8. 用原入口复测;把剩余问题拆到新 issue 或追加既有 issue
9. 对每个已完成或已纠偏完成的子代理发送 post-task 收口要求,读取其已判断 feedback;主代理只挑选适合工程化的项转正式 FEATURE/BUG issue,并优先派回原子代理执行
10. 主代理 post-task:长期参考、清理已吸收 worktree;不代替子代理做 feedback 池去重
3. 使用 `$mdtodo-edit` 把子 issue 登记到泛化问题域 ITEM/SUBITEM 并标记进行中;登记失败时停止派单
4. 同时派发低耦合子任务;Artificer 的 `create task``apply``dispatch` 均只能发生在 MDTODO 登记之后;共享契约先派一个基线任务
5. 轮询子代理结果:子 issue comment、PR、验证摘要、阻塞
6. 对每个 PR 做架构 review、bounded diff、preflight 和必要本地验证
7. 按依赖顺序合并;合并后同步目标 worktree
8. 触发受控 CI/CD 或让明确授权的子代理上线;主代理核对 closeout
9. 用原入口复测;把剩余问题拆到新 issue 或追加既有 issue
10. 对每个已完成或已纠偏完成的子代理发送 post-task 收口要求,读取其已判断 feedback;主代理只挑选适合工程化的项转正式 FEATURE/BUG issue,并优先派回原子代理执行
11. 主代理 post-task:长期参考、清理已吸收 worktree;不代替子代理做 feedback 池去重。
## 无响应接续
@@ -77,11 +77,11 @@
## R6 [in_progress]
让 Artificer 派单成为 Target-aware 的单命令流程:主代理先用 MDTODO 跟踪,再通过 trans 在 YAML 选择的 Target 上执行 create/dispatchCLI 只需 repo、ref 与 prompt 即自动渲染合法 workspaceRef、resourceBundleRef、session、projectId、bundle 继承和 SecretRef,提交前 dry-run 阻止必然失败任务;Artificer owning YAML prompt 与 unidesk-subagent skill 固化 Target/trans 及所有子代理先建 MDTODO 的规则,并以私有 pikainc/selfmedia 在 NC01 真实验收,完成任务后将详细报告写入[任务报告](./details/artificer-runtime-capabilities/R6_Task_Report.md)。
让 Artificer 派单成为 Target-aware 的单命令流程:所有子代理派单前先用 MDTODO 跟踪;主代理通过 trans 在 YAML 选择的 Target 上执行 create/dispatchArtificer 接单后内部也通过 trans 到 Target 的任务 worktree 执行,从而复用当前 NC01 的 Git、工具和运行面受控权限。CLI 只需 target、target-workspace、repo、ref 与 prompt,即在创建前通过同一 trans 核对工作区、分支和源码身份,自动渲染合法 session、workspaceRef 与 Aipod 资源包,不让 schema-invalid 或私库 fetch 错误在建 task 后才暴露;owning YAML prompt 与 unidesk-subagent 固化规则,并以 /root/.worktrees/selfmedia/r6-ocr-visual-planner 真实验收,完成任务后将详细报告写入[任务报告](./details/artificer-runtime-capabilities/R6_Task_Report.md)。
### R6.1 [in_progress]
由原生子代理在最新远端基线的独立 worktree 调查并实现易用派单入口,覆盖 repo/ref/Target/trans、合法资源投影、提交前校验、私有仓库 YAML SecretRef 与 Artificer owning promptunidesk-subagent 固定规则;对应原生任务 artificer_dispatch_ux,完成任务后将详细报告写入[任务报告](./details/artificer-runtime-capabilities/R6.1_Task_Report.md)。
由原生子代理在最新远端基线的独立 worktree 实现易用派单入口:外层在 YAML Target 上通过 trans 派单,Artificer 内部通过 trans 到 NC01 目标工作区复用主机权限;覆盖 target/target-workspace/repo/ref、创建前源码身份预检、合法 session/workspaceRef/Aipod 投影、owning prompt,以及 unidesk-subagent 的“所有子代理先建 MDTODO”固定规则;对应原生任务 artificer_dispatch_ux,完成任务后将详细报告写入[任务报告](./details/artificer-runtime-capabilities/R6.1_Task_Report.md)。
### R6.2
@@ -89,4 +89,4 @@
### R6.3
上线后通过 trans 在 NC01 使用新单命令入口派发 pikainc/selfmedia feat/r6-ocr-visual-planner,确认 task 直接 running、session 可续跑且不出现 schema-invalid 或 repository-not-found,完成任务后将详细报告写入[任务报告](./details/artificer-runtime-capabilities/R6.3_Task_Report.md)。
上线后通过 trans 在 NC01 使用新单命令入口,以任务 worktree /root/.worktrees/selfmedia/r6-ocr-visual-planner、仓库 pikainc/selfmedia 和 ref feat/r6-ocr-visual-planner 派发 OCR/视觉规划,确认 task 直接 running、Artificer 内部继续用 trans、session 可续跑且不出现 schema-invalid 或 repository-not-found,完成任务后将详细报告写入[任务报告](./details/artificer-runtime-capabilities/R6.3_Task_Report.md)。
@@ -94,6 +94,10 @@ function missing(reason, sourceCommit = null) {
function isAdmissionDefault(path, value) {
const normalized = path.map((segment) => typeof segment === "number" ? "*" : segment).join(".");
const emptyRecord = value !== null && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0;
if ([
"spec.workspaces.*.volumeClaimTemplate.metadata",
"spec.workspaces.*.volumeClaimTemplate.status",
].includes(normalized)) return emptyRecord;
const atTaskSpec = (suffix) => [
`tasks.*.taskSpec.${suffix}`,
`spec.tasks.*.taskSpec.${suffix}`,
@@ -110,6 +114,8 @@ function isAdmissionDefault(path, value) {
export function canonicalizePacPipelineSpec(value, path = []) {
if (Array.isArray(value)) return value.map((item, index) => canonicalizePacPipelineSpec(item, [...path, index]));
const duration = canonicalTektonPipelineTimeout(path, value);
if (duration !== null) return duration;
if (value === null || typeof value !== "object") return value;
const output = {};
for (const key of Object.keys(value).sort()) {
@@ -121,6 +127,38 @@ export function canonicalizePacPipelineSpec(value, path = []) {
return output;
}
function canonicalTektonPipelineTimeout(path, value) {
if (path.join(".") !== "spec.timeouts.pipeline" || typeof value !== "string") return null;
const matchSign = /^([+-]?)(.*)$/u.exec(value);
if (matchSign === null) return null;
const source = matchSign[2] ?? "";
const units = {
h: 3_600_000_000_000n,
m: 60_000_000_000n,
s: 1_000_000_000n,
ms: 1_000_000n,
us: 1_000n,
"µs": 1_000n,
"μs": 1_000n,
ns: 1n,
};
const segment = /([0-9]+)(ns|us|µs|μs|ms|s|m|h)/uy;
let offset = 0;
let nanoseconds = 0n;
let count = 0;
while (offset < source.length) {
segment.lastIndex = offset;
const matched = segment.exec(source);
if (matched === null) return null;
nanoseconds += BigInt(matched[1] ?? "0") * (units[matched[2] ?? ""] ?? 0n);
offset = segment.lastIndex;
count += 1;
}
if (count === 0) return null;
if (matchSign[1] === "-") nanoseconds = -nanoseconds;
return `tekton-duration-ns:${nanoseconds}`;
}
export function canonicalPacPipelineSha256(value) {
return `sha256:${createHash("sha256").update(JSON.stringify(canonicalizePacPipelineSpec(value))).digest("hex")}`;
}
@@ -331,6 +331,31 @@ describe("Tekton admission canonicalization", () => {
expect(canonicalizePacPipelineSpec(negative)).toEqual(negative);
expect(nativeCanonicalize(negative)).toEqual(negative);
});
test("normalizes equivalent PipelineRun timeout spellings without hiding different values", () => {
const seconds = { spec: { timeouts: { pipeline: "600s" } } };
const admitted = { spec: { timeouts: { pipeline: "10m0s" } } };
const different = { spec: { timeouts: { pipeline: "601s" } } };
const unrelated = { timeouts: { pipeline: "600s" } };
expect(canonicalizePacPipelineSpec(seconds)).toEqual(canonicalizePacPipelineSpec(admitted));
expect(nativeCanonicalize(seconds)).toEqual(nativeCanonicalize(admitted));
expect(canonicalSha256(seconds)).toBe(canonicalSha256(admitted));
expect(nativeCanonicalSha256(admitted)).toBe(canonicalSha256(seconds));
expect(firstPacSourceArtifactDrift(seconds, admitted)).toBeNull();
expect(firstPacSourceArtifactDrift(seconds, different)?.path).toBe("$.spec.timeouts.pipeline");
expect(canonicalizePacPipelineSpec(unrelated)).toEqual(unrelated);
expect(nativeCanonicalize(unrelated)).toEqual(unrelated);
});
test("removes only empty workspace claim metadata and status added by admission", () => {
const desired = { spec: { workspaces: [{ name: "source", volumeClaimTemplate: { spec: { accessModes: ["ReadWriteOnce"] } } }] } };
const admitted = { spec: { workspaces: [{ name: "source", volumeClaimTemplate: { metadata: {}, spec: { accessModes: ["ReadWriteOnce"] }, status: {} } }] } };
const labeled = { spec: { workspaces: [{ name: "source", volumeClaimTemplate: { metadata: { labels: { owner: "test" } }, spec: { accessModes: ["ReadWriteOnce"] } } }] } };
expect(canonicalizePacPipelineSpec(admitted)).toEqual(canonicalizePacPipelineSpec(desired));
expect(nativeCanonicalize(admitted)).toEqual(nativeCanonicalize(desired));
expect(firstPacSourceArtifactDrift(desired, admitted)).toBeNull();
expect(firstPacSourceArtifactDrift(desired, labeled)?.path).toBe("$.spec.workspaces[0].volumeClaimTemplate.metadata");
});
});
describe("source artifact serialization", () => {
@@ -636,6 +661,9 @@ describe("runtime source-commit selection", () => {
expect(sourceArtifactRuntimeAlignment(aligned, exact, sourceCommit)).toBe("aligned");
expect(sourceArtifactRuntimeAlignment(drifted, exact, sourceCommit)).toBe("drifted");
expect(sourceArtifactRuntimeAlignment(aligned, { ...exact, clean: false }, sourceCommit)).toBe("unavailable");
const embeddedOnly = { ...structuredClone(aligned), live: { ...aligned.live, status: "missing" as const, provenanceAligned: false } };
expect(sourceArtifactRuntimeAlignment(embeddedOnly, exact, sourceCommit, "embedded-pipeline-spec")).toBe("aligned");
expect(sourceArtifactRuntimeAlignment(embeddedOnly, exact, sourceCommit, "remote-pipeline-annotation")).toBe("missing");
expect(JSON.stringify([aligned, drifted])).not.toContain("pending");
});
@@ -284,7 +284,7 @@ export async function runPacSourceArtifact(
})
: null;
const sourceOk = source.aligned && source.provenanceAligned;
const runtimeAlignment = sourceArtifactRuntimeAlignment(runtime, sourceWorktreeState, options.sourceCommit);
const runtimeAlignment = sourceArtifactRuntimeAlignment(runtime, sourceWorktreeState, options.sourceCommit, binding.consumer.sourceArtifact.mode);
const runtimeOk = runtimeAlignment === "aligned";
const ok = options.action === "check"
? sourceOk
@@ -326,6 +326,9 @@ export async function runPacSourceArtifact(
liveExportAllowed: false,
sourceWorktreeExplicit: true,
canonicalAdmissionDefaultsRemoved: [
"PipelineRun.spec.timeouts.pipeline=<semantic Go duration>",
"workspaces[].volumeClaimTemplate.metadata={}",
"workspaces[].volumeClaimTemplate.status={}",
"tasks[].taskSpec.metadata={}",
"tasks[].taskSpec.spec=null",
"tasks[].taskSpec.params[].type=string",
@@ -359,13 +362,14 @@ export function sourceArtifactRuntimeAlignment(
runtime: PacSourceArtifactRuntimeObservation | null,
worktree: SourceWorktreeState,
sourceCommit: string | null,
mode: PacSourceArtifactMode = "remote-pipeline-annotation",
): PacSourceArtifactRuntimeAlignment {
if (runtime === null) return "not-requested";
if (sourceCommit !== null && (!worktree.clean || worktree.matchesSourceCommit !== true)) return "unavailable";
if (runtime.live.status === "unavailable" || runtime.embedded.status === "unavailable") return "unavailable";
if (runtime.live.status === "missing" || runtime.embedded.status === "missing") return "missing";
if (runtime.live.status === "drifted" || runtime.embedded.status === "drifted") return "drifted";
if (!runtime.live.provenanceAligned || !runtime.embedded.provenanceAligned) return "drifted";
const required = mode === "embedded-pipeline-spec" ? [runtime.embedded] : [runtime.live, runtime.embedded];
if (required.some((item) => item.status === "unavailable")) return "unavailable";
if (required.some((item) => item.status === "missing")) return "missing";
if (required.some((item) => item.status === "drifted" || !item.provenanceAligned)) return "drifted";
return "aligned";
}
@@ -386,14 +390,16 @@ function sourceArtifactNext(
if (!worktree.clean || (options.sourceCommit !== null && worktree.matchesSourceCommit !== true)) {
return "Create a clean worktree at the exact GitHub merge commit, then rerun status or verify-runtime with that full source commit.";
}
if (runtime.live.status === "unavailable") return "Repair the owning control-plane runtime observer or RBAC, then rerun status before verification.";
if (runtime.live.status === "missing") return "Apply the owning YAML control plane, confirm the live Pipeline exists, then rerun verify-runtime.";
if (runtime.live.status === "drifted" || !runtime.live.provenanceAligned) return "Apply the owning YAML control plane so the live Pipeline spec and provenance align, then rerun verify-runtime.";
if (binding.consumer.sourceArtifact.mode === "remote-pipeline-annotation" && runtime.live.status === "unavailable") return "Repair the owning control-plane runtime observer or RBAC, then rerun status before verification.";
if (binding.consumer.sourceArtifact.mode === "remote-pipeline-annotation" && runtime.live.status === "missing") return "Apply the owning YAML control plane, confirm the live Pipeline exists, then rerun verify-runtime.";
if (binding.consumer.sourceArtifact.mode === "remote-pipeline-annotation" && (runtime.live.status === "drifted" || !runtime.live.provenanceAligned)) return "Apply the owning YAML control plane so the live Pipeline spec and provenance align, then rerun verify-runtime.";
if (runtime.embedded.status === "unavailable") return "Inspect the exact-commit PaC PipelineRun identity and observer access, then rerun verify-runtime.";
if (runtime.embedded.status === "missing") return "Wait for or repair the exact-commit PaC PipelineRun; do not validate a prefix-selected run.";
if (runtime.embedded.status === "drifted" || !runtime.embedded.provenanceAligned) return "Regenerate and merge the consumer source artifact, then verify the new exact-commit PipelineRun.";
return runtimeAlignment === "aligned"
? "Exact source, live Pipeline, embedded PipelineRun, and provenance are aligned; continue consumer closeout."
? binding.consumer.sourceArtifact.mode === "embedded-pipeline-spec"
? "Exact source, embedded PipelineRun, and provenance are aligned; continue consumer closeout."
: "Exact source, live Pipeline, embedded PipelineRun, and provenance are aligned; continue consumer closeout."
: "Resolve the reported runtime layer and rerun verify-runtime.";
}
@@ -428,6 +434,8 @@ export function renderPacSourceArtifactResult(result: Record<string, unknown>):
export function canonicalizePacPipelineSpec(value: unknown, path: readonly (string | number)[] = []): unknown {
if (Array.isArray(value)) return value.map((item, index) => canonicalizePacPipelineSpec(item, [...path, index]));
const duration = canonicalTektonPipelineTimeout(path, value);
if (duration !== null) return duration;
if (!isRecord(value)) return value;
const output: Record<string, unknown> = {};
for (const key of Object.keys(value).sort()) {
@@ -439,6 +447,38 @@ export function canonicalizePacPipelineSpec(value: unknown, path: readonly (stri
return output;
}
function canonicalTektonPipelineTimeout(path: readonly (string | number)[], value: unknown): string | null {
if (path.join(".") !== "spec.timeouts.pipeline" || typeof value !== "string") return null;
const matchSign = /^([+-]?)(.*)$/u.exec(value);
if (matchSign === null) return null;
const source = matchSign[2] ?? "";
const units: Readonly<Record<string, bigint>> = {
h: 3_600_000_000_000n,
m: 60_000_000_000n,
s: 1_000_000_000n,
ms: 1_000_000n,
us: 1_000n,
"µs": 1_000n,
"μs": 1_000n,
ns: 1n,
};
const segment = /([0-9]+)(ns|us|µs|μs|ms|s|m|h)/uy;
let offset = 0;
let nanoseconds = 0n;
let count = 0;
while (offset < source.length) {
segment.lastIndex = offset;
const matched = segment.exec(source);
if (matched === null) return null;
nanoseconds += BigInt(matched[1] ?? "0") * (units[matched[2] ?? ""] ?? 0n);
offset = segment.lastIndex;
count += 1;
}
if (count === 0) return null;
if (matchSign[1] === "-") nanoseconds = -nanoseconds;
return `tekton-duration-ns:${nanoseconds}`;
}
export function canonicalSha256(value: unknown): string {
return `sha256:${createHash("sha256").update(JSON.stringify(canonicalizePacPipelineSpec(value))).digest("hex")}`;
}
@@ -1027,6 +1067,10 @@ function firstCanonicalDrift(expected: unknown, actual: unknown, path: string):
function isProvenTektonAdmissionDefault(path: readonly (string | number)[], value: unknown): boolean {
const normalized = path.map((segment) => typeof segment === "number" ? "*" : segment).join(".");
const emptyRecord = isRecord(value) && Object.keys(value).length === 0;
if ([
"spec.workspaces.*.volumeClaimTemplate.metadata",
"spec.workspaces.*.volumeClaimTemplate.status",
].includes(normalized)) return emptyRecord;
const atTaskSpec = (suffix: string): boolean => [
`tasks.*.taskSpec.${suffix}`,
`spec.tasks.*.taskSpec.${suffix}`,