@nowcrew/daemon 0.6.88-beta.1 → 0.6.88-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstat, readFile, realpath } from "node:fs/promises";
|
|
3
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
4
|
+
const MAX_REPORT_BYTES = 32 * 1024 * 1024;
|
|
5
|
+
export function reportDeliveryContext(executionId, runDir, facts) {
|
|
6
|
+
return {
|
|
7
|
+
version: 1,
|
|
8
|
+
provider: facts.provider,
|
|
9
|
+
executionId,
|
|
10
|
+
receiptFile: join(runDir, `.qa-report-delivery-${createHash("sha256").update(executionId).digest("hex")}.json`),
|
|
11
|
+
...(facts.ossUploadScript === undefined ? {} : { script: facts.ossUploadScript }),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function inside(root, file) {
|
|
15
|
+
const part = relative(root, file);
|
|
16
|
+
return part !== ".." && !part.startsWith("../") && !part.startsWith("..\\") && !isAbsolute(part);
|
|
17
|
+
}
|
|
18
|
+
export async function verifiedReportReceipt(input) {
|
|
19
|
+
if (!input.receiptFile || !input.answer)
|
|
20
|
+
return false;
|
|
21
|
+
try {
|
|
22
|
+
const stat = await lstat(input.receiptFile);
|
|
23
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 64 * 1024)
|
|
24
|
+
return false;
|
|
25
|
+
const receipt = JSON.parse(await readFile(input.receiptFile, "utf8"));
|
|
26
|
+
if (!receipt || typeof receipt !== "object")
|
|
27
|
+
return false;
|
|
28
|
+
const r = receipt;
|
|
29
|
+
if (r.version !== 1 || r.executionId !== input.executionId || r.evidenceComplete !== true
|
|
30
|
+
|| !Array.isArray(r.runIds) || r.runIds.length === 0 || r.runIds.length > 100
|
|
31
|
+
|| r.runIds.some(id => typeof id !== "string" || !id || id.length > 256)
|
|
32
|
+
|| typeof r.file !== "string" || !isAbsolute(r.file) || !r.file.endsWith(".html")
|
|
33
|
+
|| typeof r.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(r.sha256)
|
|
34
|
+
|| typeof r.url !== "string")
|
|
35
|
+
return false;
|
|
36
|
+
const url = new URL(r.url);
|
|
37
|
+
if (url.protocol !== "https:" || url.hostname !== "ai-claw-in.nowcoder.com"
|
|
38
|
+
|| !url.pathname.endsWith(".html") || url.username || url.password || url.search || url.hash)
|
|
39
|
+
return false;
|
|
40
|
+
const answerUrls = input.answer.match(/https:\/\/ai-claw-in\.nowcoder\.com\/[^\s<>"')\]]+/g) ?? [];
|
|
41
|
+
if (!answerUrls.includes(url.href))
|
|
42
|
+
return false;
|
|
43
|
+
const file = await realpath(r.file);
|
|
44
|
+
const roots = await Promise.all([input.root, input.qaReportRoot ?? join(input.root, "qa-report-server-v2")]
|
|
45
|
+
.map(root => realpath(resolve(root)).catch(() => null)));
|
|
46
|
+
if (!roots.some(root => root !== null && inside(root, file)))
|
|
47
|
+
return false;
|
|
48
|
+
const fileStat = await lstat(file);
|
|
49
|
+
if (!fileStat.isFile() || fileStat.size > MAX_REPORT_BYTES)
|
|
50
|
+
return false;
|
|
51
|
+
const expectedHash = createHash("sha256").update(await readFile(file)).digest("hex");
|
|
52
|
+
if (expectedHash !== r.sha256)
|
|
53
|
+
return false;
|
|
54
|
+
const response = await fetch(url.href, { redirect: "error", signal: AbortSignal.timeout(15_000) });
|
|
55
|
+
if (!response.ok || !/^text\/html(?:;|$)/i.test(response.headers.get("content-type") ?? "")
|
|
56
|
+
|| /attachment/i.test(response.headers.get("content-disposition") ?? "") || !response.body) {
|
|
57
|
+
await response.body?.cancel();
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
const reader = response.body.getReader();
|
|
61
|
+
const hash = createHash("sha256");
|
|
62
|
+
let bytes = 0;
|
|
63
|
+
try {
|
|
64
|
+
while (true) {
|
|
65
|
+
const chunk = await reader.read();
|
|
66
|
+
if (chunk.done)
|
|
67
|
+
break;
|
|
68
|
+
bytes += chunk.value.byteLength;
|
|
69
|
+
if (bytes > MAX_REPORT_BYTES)
|
|
70
|
+
return false;
|
|
71
|
+
hash.update(chunk.value);
|
|
72
|
+
}
|
|
73
|
+
return bytes === fileStat.size && hash.digest("hex") === expectedHash;
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
await reader.cancel().catch(() => undefined);
|
|
77
|
+
reader.releaseLock();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -4,6 +4,7 @@ import { extname, join, relative, resolve, sep } from "node:path";
|
|
|
4
4
|
import { promisify } from "node:util";
|
|
5
5
|
import { cloudReportUploadFacts } from "./cloud-report-upload.js";
|
|
6
6
|
import { dslog } from "./slog.js";
|
|
7
|
+
import { verifiedReportReceipt } from "./cloud-report-receipt.js";
|
|
7
8
|
const execFileP = promisify(execFile);
|
|
8
9
|
const REPORT_FILENAMES = new Set(["report.md", "result.json"]);
|
|
9
10
|
const MAX_DEPTH = 8;
|
|
@@ -166,6 +167,14 @@ export async function deliverCloudReports(input) {
|
|
|
166
167
|
if (!input.facts.enabled || input.facts.ossUploadScript === undefined || input.before === null) {
|
|
167
168
|
return { finalText: input.finalText, externalAnswer: input.externalAnswer };
|
|
168
169
|
}
|
|
170
|
+
if (input.captureFinal && await verifiedReportReceipt({
|
|
171
|
+
...input, answer: input.externalAnswer ?? input.finalText,
|
|
172
|
+
})) {
|
|
173
|
+
dslog("cloud_report_upload.already_delivered", "本次执行的 HTML 交付收据及内容已验证,跳过重复上传", {
|
|
174
|
+
execution_id: input.executionId, agent_handle: input.agentHandle,
|
|
175
|
+
});
|
|
176
|
+
return { finalText: input.finalText, externalAnswer: input.externalAnswer };
|
|
177
|
+
}
|
|
169
178
|
try {
|
|
170
179
|
const result = await (input.upload ?? uploadCloudReports)({
|
|
171
180
|
script: input.facts.ossUploadScript,
|
package/dist/local-executor.js
CHANGED
|
@@ -12,6 +12,7 @@ import { spawnKimi, KIMI_EFFORT_LEVELS } from "./runtimes/kimi.js";
|
|
|
12
12
|
import { applyDeepSeekHarnessMachineEnv, applyProviderEnv, providerFingerprint, } from "./provider-env.js";
|
|
13
13
|
import { augmentedPath } from "./runtime-path.js";
|
|
14
14
|
import { deliverCloudReports, prepareCloudReportExecution } from "./cloud-report-uploader.js";
|
|
15
|
+
import { reportDeliveryContext } from "./cloud-report-receipt.js";
|
|
15
16
|
import { extractFinalText, extractRunMeta, normalizeEvent, parseLine, } from "./normalize.js";
|
|
16
17
|
import { readSession, writeSession, pickResumeId, isNearBudget } from "./session.js";
|
|
17
18
|
import { providerErrorFromExit } from "./provider-error.js";
|
|
@@ -34,7 +35,8 @@ import { diffMemoryPruneNotes, evaluateMemoryPrunePostcondition, inspectMemoryPr
|
|
|
34
35
|
import { createLocalMemoryTelemetry, logLocalMemoryContextPrepareFailure, logLocalMemoryDiagnosticsFailure, } from "./local-memory-telemetry.js";
|
|
35
36
|
import { CodexStartupStageParser } from "./codex-startup-stage.js";
|
|
36
37
|
import { formatRuntimeStartupError } from "./runtime-startup-error.js";
|
|
37
|
-
import { machinePackageManagerEnv
|
|
38
|
+
import { machinePackageManagerEnv } from "./package-manager-env.js";
|
|
39
|
+
import { logRuntimeEnvironmentContract } from "./runtime-environment-telemetry.js";
|
|
38
40
|
import { sanitizeEnvVars } from "./runtime-env.js";
|
|
39
41
|
import { commitLocalMemoryResultAfterExit, initializeLocalMemoryV2ForWorkspace, isLocalMemoryV2Eligible, localMemoryResultPath as makeLocalMemoryResultPath, } from "./local-memory/runtime.js";
|
|
40
42
|
import { prepareLocalMemoryPrompt } from "./local-memory/prompt.js";
|
|
@@ -260,6 +262,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
260
262
|
};
|
|
261
263
|
const cloudReportPreparation = await prepareCloudReportExecution({ env: inheritedEnv, ...reportRoots, executionId: input.executionId, agentHandle: input.handle });
|
|
262
264
|
const { facts: cloudReportUpload, snapshot: reportSnapshot } = cloudReportPreparation;
|
|
265
|
+
const reportContext = reportDeliveryContext(input.executionId, workspace.runDir, cloudReportUpload);
|
|
263
266
|
for (const key of Object.keys(inheritedEnv)) {
|
|
264
267
|
if (key.startsWith("CREW_AGENT_MEMORY_") || ["CREW_CLOUD_PROVIDER", "CREW_OSS_UPLOAD_SCRIPT", "CREW_OSS_REPORT_UPLOAD"].includes(key))
|
|
265
268
|
delete inheritedEnv[key];
|
|
@@ -485,6 +488,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
485
488
|
// Expose only the training root; ability-specific workspace layout stays
|
|
486
489
|
// owned by the training package rather than the NowWork daemon.
|
|
487
490
|
QA_RUNTIME_ROOT: join(crewHome, "training"),
|
|
491
|
+
QA_REPORT_DELIVERY_CONTEXT: JSON.stringify(reportContext),
|
|
488
492
|
CREW_TASK_DIR: executionWorkspace.runDir,
|
|
489
493
|
CREW_TASK_LOG: executionWorkspace.workLogPath,
|
|
490
494
|
...(input.wakeMessageId ? { CREW_WAKE_MESSAGE_ID: input.wakeMessageId } : {}),
|
|
@@ -504,30 +508,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
504
508
|
const childEnv = runtime.name === "deepseek-harness"
|
|
505
509
|
? applyDeepSeekHarnessMachineEnv(providerEnv, inheritedEnv)
|
|
506
510
|
: providerEnv;
|
|
507
|
-
|
|
508
|
-
execution_id: input.executionId,
|
|
509
|
-
agent_handle: input.handle,
|
|
510
|
-
runtime: runtime.name,
|
|
511
|
-
qa_runtime_root: childEnv.QA_RUNTIME_ROOT ?? null,
|
|
512
|
-
qa_runtime_root_present: childEnv.QA_RUNTIME_ROOT !== undefined,
|
|
513
|
-
inherited_qa_runtime_root_present: inheritedEnv.QA_RUNTIME_ROOT !== undefined,
|
|
514
|
-
...packageManagerEnvFacts(childEnv),
|
|
515
|
-
training_root: join(crewHome, "training"),
|
|
516
|
-
cloud_provider: cloudReportUpload.provider,
|
|
517
|
-
oss_report_upload_enabled: cloudReportUpload.enabled,
|
|
518
|
-
project_context_present: input.projectContext !== undefined,
|
|
519
|
-
project_ids: JSON.stringify(input.projectContext === undefined
|
|
520
|
-
? []
|
|
521
|
-
: [
|
|
522
|
-
...(input.projectContext.primary === undefined
|
|
523
|
-
? []
|
|
524
|
-
: [input.projectContext.primary.projectId]),
|
|
525
|
-
...input.projectContext.secondary.map((project) => project.projectId),
|
|
526
|
-
]),
|
|
527
|
-
ability_release_present: input.abilityRelease !== undefined,
|
|
528
|
-
ability_release_id: input.abilityRelease?.releaseId,
|
|
529
|
-
ability_root_commit: input.abilityRelease?.rootCommit,
|
|
530
|
-
});
|
|
511
|
+
logRuntimeEnvironmentContract(input, childEnv, inheritedEnv, crewHome, cloudReportUpload);
|
|
531
512
|
const warn = dependencies.warnRuntime ?? warnRuntimeLocally;
|
|
532
513
|
const launchRuntime = dependencies.launchRuntime ?? (async (launchReq) => {
|
|
533
514
|
if (launchReq.runtime === "codex" && launchReq.codexCollaborationTools === true) {
|
|
@@ -936,22 +917,27 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
936
917
|
localMemoryTelemetry?.markRuntimeExited(exitCode);
|
|
937
918
|
memoryPruneRuntimeExitCode = exitCode;
|
|
938
919
|
if (localMemoryResultPath !== null) {
|
|
920
|
+
memoryPruneFailurePhase = "memory_commit";
|
|
939
921
|
const committed = await commitLocalMemoryResultAfterExit({
|
|
940
922
|
resultPath: localMemoryResultPath,
|
|
941
923
|
agentDir: executionWorkspace.dir,
|
|
942
924
|
executionId: input.executionId,
|
|
943
925
|
pruneTraceId: memoryPruneTraceId,
|
|
944
926
|
exitCode,
|
|
927
|
+
workLogPath: executionWorkspace.workLogPath,
|
|
945
928
|
});
|
|
946
|
-
if (committed.status === "failed") {
|
|
947
|
-
|
|
948
|
-
|
|
929
|
+
if (committed.status === "failed" || committed.status === "missing") {
|
|
930
|
+
const reason = committed.error ?? "local_memory_result_missing";
|
|
931
|
+
dslog("local_memory.commit_failed", "结构化本地记忆提交或清理未完成,保留可用恢复材料", {
|
|
932
|
+
level: "ERROR",
|
|
949
933
|
execution_id: input.executionId,
|
|
950
934
|
agent_handle: input.handle,
|
|
951
|
-
error_code:
|
|
935
|
+
error_code: reason,
|
|
952
936
|
result_path: localMemoryResultPath,
|
|
953
937
|
});
|
|
938
|
+
throw new Error(`local_memory_commit_failed: ${reason}`);
|
|
954
939
|
}
|
|
940
|
+
memoryPruneFailurePhase = "runtime_finalize";
|
|
955
941
|
}
|
|
956
942
|
const errorTail = [
|
|
957
943
|
stderrTail.trim(),
|
|
@@ -991,6 +977,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
991
977
|
const cloudReportResult = exitCode === 0
|
|
992
978
|
? await deliverCloudReports({
|
|
993
979
|
facts: cloudReportUpload, ...reportRoots, before: reportSnapshot, finalText,
|
|
980
|
+
receiptFile: reportContext.receiptFile,
|
|
994
981
|
externalAnswer: externalOutput.lastClosedBlock
|
|
995
982
|
?? (finalText === null ? null : extractExternalAnswer(finalText)),
|
|
996
983
|
captureFinal: input.captureFinal, executionId: input.executionId, agentHandle: input.handle,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { lstat, mkdir, open, readFile, realpath, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
5
|
import { applyLocalMemoryResult, parseLocalMemoryResult } from "./write.js";
|
|
5
6
|
export function isLocalMemoryV2Eligible(input) {
|
|
6
7
|
// The allowlist is the rollout boundary. The marker and seed/pre-existing
|
|
@@ -54,10 +55,21 @@ export function renderLocalMemoryResultInstructions(resultPath) {
|
|
|
54
55
|
将一个合法 JSON 对象写入以下 daemon 提供的文件,写入完成后再结束本轮:
|
|
55
56
|
${resultPath}
|
|
56
57
|
|
|
57
|
-
JSON
|
|
58
|
+
outcome 只允许 no_change、proposed、conflict、failed。新增、更新、替代记忆或任何 statusChanges 都必须使用 proposed;no_change 的 changes/statusChanges 必须都为空。conflict/failed 表示未完成,不能当作提交成功。自然语言“已修正”不是 JSON 枚举值,不能写成 corrected。
|
|
59
|
+
|
|
60
|
+
新增记忆的完整合法示例(内容须替换为本任务实际候选):
|
|
61
|
+
\`\`\`json
|
|
62
|
+
{"schemaVersion":1,"purpose":"memory_prune","baseCatalogRevision":0,"outcome":"proposed","changes":[{"operation":"create","memoryId":null,"baseRevision":null,"supersedesMemoryId":null,"title":"发布验收约定","kind":"procedural","summary":"发布前必须验证回滚步骤。","triggers":["发布","回滚"],"priority":"medium","detailCategory":"project-rules","detailBody":"发布前必须验证回滚步骤,并将验证结果写入发布报告。","reviewAfter":null}],"statusChanges":[]}
|
|
63
|
+
\`\`\`
|
|
64
|
+
|
|
65
|
+
没有需要变更的记忆时:
|
|
66
|
+
\`\`\`json
|
|
58
67
|
{"schemaVersion":1,"purpose":"memory_prune","baseCatalogRevision":0,"outcome":"no_change","changes":[],"statusChanges":[]}
|
|
59
|
-
|
|
68
|
+
\`\`\`
|
|
69
|
+
|
|
70
|
+
每个 changes 项须保留示例中的全部字段。operation=create|update|supersede;kind=procedural|semantic|episodic|preference;priority=high|medium|low;detailCategory=preferences|project-rules|testing|debugging|git|incidents|lessons。create 的 memoryId/baseRevision/supersedesMemoryId 必须为 null;update 按现有索引填写 memoryId/baseRevision,supersedesMemoryId 为 null;supersede 填 supersedesMemoryId/baseRevision,memoryId 为 null。
|
|
60
71
|
只提交可复用、与本次任务明确相关的少量候选;不要写入凭证、token、隐私或内部控制标记。daemon 会校验、去重、限制 MEMORY.md 大小,超出索引容量的候选会保存在 pending 归档中;后续相关任务会按需召回,索引腾出空间后可自动提升。
|
|
72
|
+
写入候选文件不等于已经记住,只有 daemon 校验并提交成功后记忆才生效。本结构化轮次覆盖前文提前清理工作日志的要求:保留 $CREW_TASK_LOG,不要清空或删除;daemon 将在提交成功后清理,失败时保留日志和候选用于排查或重试。
|
|
61
73
|
`;
|
|
62
74
|
}
|
|
63
75
|
/** Prevent ordinary V2 turns from racing the daemon-managed memory writer. */
|
|
@@ -81,16 +93,52 @@ export async function commitLocalMemoryResultAfterExit(input) {
|
|
|
81
93
|
}
|
|
82
94
|
try {
|
|
83
95
|
const result = parseLocalMemoryResult(raw);
|
|
96
|
+
if (result.outcome === "conflict" || result.outcome === "failed") {
|
|
97
|
+
return { status: "failed", error: `local_memory_result_${result.outcome}` };
|
|
98
|
+
}
|
|
84
99
|
await applyLocalMemoryResult({
|
|
85
100
|
agentDir: input.agentDir,
|
|
86
101
|
result,
|
|
87
102
|
executionId: input.executionId,
|
|
88
103
|
pruneTraceId: input.pruneTraceId,
|
|
89
104
|
});
|
|
90
|
-
await rm(input.resultPath, { force: true });
|
|
91
|
-
return { status: "applied" };
|
|
92
105
|
}
|
|
93
106
|
catch (error) {
|
|
94
107
|
return { status: "failed", error: error instanceof Error ? error.message : "apply_failed" };
|
|
95
108
|
}
|
|
109
|
+
if (input.workLogPath !== undefined) {
|
|
110
|
+
try {
|
|
111
|
+
const relativeLogPath = relative(resolve(input.agentDir), resolve(input.workLogPath));
|
|
112
|
+
if (relativeLogPath === "" || relativeLogPath === ".." || relativeLogPath.startsWith(`..${sep}`)
|
|
113
|
+
|| isAbsolute(relativeLogPath))
|
|
114
|
+
throw new Error("local_memory_work_log_path_invalid");
|
|
115
|
+
const expectedLogPath = join(await realpath(input.agentDir), relativeLogPath);
|
|
116
|
+
const before = await lstat(input.workLogPath);
|
|
117
|
+
if (!before.isFile() || before.isSymbolicLink())
|
|
118
|
+
throw new Error("local_memory_work_log_path_invalid");
|
|
119
|
+
if (await realpath(input.workLogPath) !== expectedLogPath)
|
|
120
|
+
throw new Error("local_memory_work_log_path_invalid");
|
|
121
|
+
const handle = await open(input.workLogPath, constants.O_WRONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
|
|
122
|
+
try {
|
|
123
|
+
const opened = await handle.stat();
|
|
124
|
+
if (!opened.isFile() || opened.dev !== before.dev || opened.ino !== before.ino) {
|
|
125
|
+
throw new Error("local_memory_work_log_path_changed");
|
|
126
|
+
}
|
|
127
|
+
await handle.truncate(0);
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
await handle.close();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return { status: "failed", error: "local_memory_work_log_cleanup_failed" };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
await rm(input.resultPath, { force: true });
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return { status: "failed", error: "local_memory_result_cleanup_failed" };
|
|
142
|
+
}
|
|
143
|
+
return { status: "applied" };
|
|
96
144
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { packageManagerEnvFacts } from "./package-manager-env.js";
|
|
3
|
+
import { dslog } from "./slog.js";
|
|
4
|
+
export function logRuntimeEnvironmentContract(input, childEnv, inheritedEnv, crewHome, cloudReportUpload) {
|
|
5
|
+
dslog("runtime.environment_contract", "Runtime 子进程环境契约", {
|
|
6
|
+
execution_id: input.executionId,
|
|
7
|
+
agent_handle: input.handle,
|
|
8
|
+
runtime: input.runtime.name,
|
|
9
|
+
qa_runtime_root: childEnv.QA_RUNTIME_ROOT ?? null,
|
|
10
|
+
qa_runtime_root_present: childEnv.QA_RUNTIME_ROOT !== undefined,
|
|
11
|
+
inherited_qa_runtime_root_present: inheritedEnv.QA_RUNTIME_ROOT !== undefined,
|
|
12
|
+
...packageManagerEnvFacts(childEnv),
|
|
13
|
+
training_root: join(crewHome, "training"),
|
|
14
|
+
cloud_provider: cloudReportUpload.provider,
|
|
15
|
+
oss_report_upload_enabled: cloudReportUpload.enabled,
|
|
16
|
+
project_context_present: input.projectContext !== undefined,
|
|
17
|
+
project_ids: JSON.stringify(input.projectContext === undefined
|
|
18
|
+
? []
|
|
19
|
+
: [
|
|
20
|
+
...(input.projectContext.primary === undefined ? [] : [input.projectContext.primary.projectId]),
|
|
21
|
+
...input.projectContext.secondary.map((project) => project.projectId),
|
|
22
|
+
]),
|
|
23
|
+
ability_release_present: input.abilityRelease !== undefined,
|
|
24
|
+
ability_release_id: input.abilityRelease?.releaseId,
|
|
25
|
+
ability_root_commit: input.abilityRelease?.rootCommit,
|
|
26
|
+
});
|
|
27
|
+
}
|