@brawnen/agent-harness-cli 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +224 -0
- package/README.zh-CN.md +232 -0
- package/bin/agent-harness.js +6 -0
- package/package.json +46 -0
- package/src/commands/audit.js +110 -0
- package/src/commands/delivery.js +497 -0
- package/src/commands/docs.js +251 -0
- package/src/commands/gate.js +236 -0
- package/src/commands/init.js +711 -0
- package/src/commands/report.js +272 -0
- package/src/commands/state.js +274 -0
- package/src/commands/status.js +493 -0
- package/src/commands/task.js +316 -0
- package/src/commands/verify.js +173 -0
- package/src/index.js +101 -0
- package/src/lib/audit-store.js +80 -0
- package/src/lib/delivery-policy.js +219 -0
- package/src/lib/output-policy.js +266 -0
- package/src/lib/project-config.js +235 -0
- package/src/lib/runtime-paths.js +46 -0
- package/src/lib/state-store.js +510 -0
- package/src/lib/task-core.js +490 -0
- package/src/lib/workflow-policy.js +307 -0
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { runtimePath } from "./runtime-paths.js";
|
|
6
|
+
|
|
7
|
+
const SCHEMA_VERSION = "0.3";
|
|
8
|
+
const LOCK_WAIT_MS = 25;
|
|
9
|
+
const LOCK_TIMEOUT_MS = 5000;
|
|
10
|
+
const LOCK_STALE_MS = 30000;
|
|
11
|
+
|
|
12
|
+
const VALID_PHASES = ["intake", "clarify", "plan", "execute", "verify", "report", "close"];
|
|
13
|
+
const VALID_STATES = [
|
|
14
|
+
"draft",
|
|
15
|
+
"needs_clarification",
|
|
16
|
+
"planned",
|
|
17
|
+
"in_progress",
|
|
18
|
+
"blocked",
|
|
19
|
+
"verifying",
|
|
20
|
+
"done",
|
|
21
|
+
"failed",
|
|
22
|
+
"suspended"
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const VALID_TRANSITIONS = {
|
|
26
|
+
draft: ["needs_clarification", "planned", "failed", "suspended"],
|
|
27
|
+
needs_clarification: ["draft", "planned", "failed", "suspended"],
|
|
28
|
+
planned: ["in_progress", "needs_clarification", "suspended"],
|
|
29
|
+
in_progress: ["blocked", "verifying", "failed", "suspended"],
|
|
30
|
+
blocked: ["in_progress", "needs_clarification", "failed", "suspended"],
|
|
31
|
+
verifying: ["done", "failed", "in_progress", "suspended"],
|
|
32
|
+
done: [],
|
|
33
|
+
failed: ["draft"],
|
|
34
|
+
suspended: ["draft", "needs_clarification", "planned", "in_progress", "blocked", "verifying"]
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const SCOPE_PLACEHOLDER = "待澄清作用范围";
|
|
38
|
+
const ACCEPTANCE_PLACEHOLDER = "待澄清完成标准";
|
|
39
|
+
const VALID_RISK_LEVELS = new Set(["low", "medium", "high"]);
|
|
40
|
+
|
|
41
|
+
export function initTaskState(cwd, { taskDraft, taskId, workflowDecision = null }) {
|
|
42
|
+
return withStateWriteLock(cwd, () => {
|
|
43
|
+
const resolvedTaskId = taskId ?? generateTaskId(taskDraft);
|
|
44
|
+
const now = new Date().toISOString();
|
|
45
|
+
const state = {
|
|
46
|
+
schema_version: SCHEMA_VERSION,
|
|
47
|
+
task_id: resolvedTaskId,
|
|
48
|
+
current_phase: "intake",
|
|
49
|
+
current_state: taskDraft?.derived?.state ?? "draft",
|
|
50
|
+
task_draft: taskDraft,
|
|
51
|
+
confirmed_contract: null,
|
|
52
|
+
workflow_decision: workflowDecision,
|
|
53
|
+
evidence: [],
|
|
54
|
+
open_questions: Array.isArray(taskDraft?.open_questions) ? taskDraft.open_questions : [],
|
|
55
|
+
override_history: [],
|
|
56
|
+
created_at: now,
|
|
57
|
+
updated_at: now
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
persistTaskState(cwd, resolvedTaskId, state);
|
|
61
|
+
updateIndex(cwd, resolvedTaskId, state, { setActive: true });
|
|
62
|
+
return state;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function confirmTaskContract(cwd, taskId, contractInput = {}) {
|
|
67
|
+
const state = requireTaskState(cwd, taskId);
|
|
68
|
+
const confirmedContract = buildConfirmedContract(state, contractInput);
|
|
69
|
+
const changes = {
|
|
70
|
+
confirmed_contract: confirmedContract,
|
|
71
|
+
open_questions: []
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
if (["draft", "needs_clarification"].includes(state.current_state)) {
|
|
75
|
+
changes.current_state = "planned";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (["intake", "clarify"].includes(state.current_phase)) {
|
|
79
|
+
changes.current_phase = "plan";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return updateTaskState(cwd, taskId, changes);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function appendTaskEvidence(cwd, taskId, evidence) {
|
|
86
|
+
if (!taskId) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const normalizedEvidence = evidence && typeof evidence === "object" ? evidence : null;
|
|
91
|
+
if (!normalizedEvidence) {
|
|
92
|
+
throw new Error("缺少 evidence 对象");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return updateTaskState(cwd, taskId, {
|
|
96
|
+
evidence: [normalizedEvidence]
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function appendTaskOverride(cwd, taskId, entry) {
|
|
101
|
+
if (!taskId) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const normalizedEntry = entry && typeof entry === "object" ? entry : null;
|
|
106
|
+
if (!normalizedEntry) {
|
|
107
|
+
throw new Error("缺少 override entry 对象");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return updateTaskState(cwd, taskId, {
|
|
111
|
+
override_history: [normalizedEntry]
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function getTaskState(cwd, taskId) {
|
|
116
|
+
const statePath = taskFilePath(cwd, taskId);
|
|
117
|
+
if (!fs.existsSync(statePath)) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return readJson(statePath);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function requireTaskState(cwd, taskId) {
|
|
125
|
+
const taskState = getTaskState(cwd, taskId);
|
|
126
|
+
if (!taskState) {
|
|
127
|
+
throw new Error(`任务不存在: ${taskId}`);
|
|
128
|
+
}
|
|
129
|
+
return taskState;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function updateTaskState(cwd, taskId, changes) {
|
|
133
|
+
return withStateWriteLock(cwd, () => {
|
|
134
|
+
const state = requireTaskState(cwd, taskId);
|
|
135
|
+
|
|
136
|
+
if (Object.hasOwn(changes, "current_phase")) {
|
|
137
|
+
validatePhase(changes.current_phase);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (Object.hasOwn(changes, "current_state")) {
|
|
141
|
+
validateState(changes.current_state);
|
|
142
|
+
validateTransition(state.current_state, changes.current_state);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for (const [key, value] of Object.entries(changes)) {
|
|
146
|
+
if (key === "evidence") {
|
|
147
|
+
state.evidence = [...(state.evidence ?? []), ...toArray(value)];
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (key === "override_history") {
|
|
152
|
+
state.override_history = [...(state.override_history ?? []), ...toArray(value)];
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (key === "open_questions") {
|
|
157
|
+
state.open_questions = toArray(value);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
state[key] = value;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
state.updated_at = new Date().toISOString();
|
|
165
|
+
persistTaskState(cwd, taskId, state);
|
|
166
|
+
updateIndex(cwd, taskId, state);
|
|
167
|
+
return state;
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function loadStateIndex(cwd) {
|
|
172
|
+
const indexPath = stateIndexPath(cwd);
|
|
173
|
+
if (!fs.existsSync(indexPath)) {
|
|
174
|
+
return defaultIndex();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return readJson(indexPath);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function getActiveTask(cwd) {
|
|
181
|
+
const activeTaskId = resolveActiveTaskId(cwd);
|
|
182
|
+
if (!activeTaskId) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
return getTaskState(cwd, activeTaskId);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function resolveActiveTaskId(cwd) {
|
|
189
|
+
const index = loadStateIndex(cwd);
|
|
190
|
+
return index.active_task_id ?? null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function resolveTaskId(cwd, explicitTaskId) {
|
|
194
|
+
if (explicitTaskId) {
|
|
195
|
+
return explicitTaskId;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const activeTaskId = resolveActiveTaskId(cwd);
|
|
199
|
+
if (!activeTaskId) {
|
|
200
|
+
throw new Error("未指定 --task-id,且当前项目没有 active task");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return activeTaskId;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function setActiveTaskId(cwd, taskId) {
|
|
207
|
+
return withStateWriteLock(cwd, () => {
|
|
208
|
+
const index = loadStateIndex(cwd);
|
|
209
|
+
index.active_task_id = taskId ?? null;
|
|
210
|
+
saveStateIndex(cwd, index);
|
|
211
|
+
return index;
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function persistTaskState(cwd, taskId, state) {
|
|
216
|
+
ensureDirectory(tasksDirPath(cwd));
|
|
217
|
+
atomicWriteFile(taskFilePath(cwd, taskId), `${JSON.stringify(state, null, 2)}\n`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function updateIndex(cwd, taskId, state, { setActive = false } = {}) {
|
|
221
|
+
const index = loadStateIndex(cwd);
|
|
222
|
+
const entry = {
|
|
223
|
+
task_id: taskId,
|
|
224
|
+
intent: state?.confirmed_contract?.intent ?? state?.task_draft?.intent ?? "unknown",
|
|
225
|
+
goal_summary: state?.confirmed_contract?.goal ?? state?.task_draft?.goal ?? "",
|
|
226
|
+
current_state: state.current_state,
|
|
227
|
+
updated_at: state.updated_at
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const existingIndex = index.tasks.findIndex((item) => item.task_id === taskId);
|
|
231
|
+
if (existingIndex >= 0) {
|
|
232
|
+
index.tasks[existingIndex] = entry;
|
|
233
|
+
} else {
|
|
234
|
+
index.tasks.push(entry);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (setActive) {
|
|
238
|
+
index.active_task_id = taskId;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
saveStateIndex(cwd, index);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function saveStateIndex(cwd, index) {
|
|
245
|
+
ensureDirectory(stateDirPath(cwd));
|
|
246
|
+
const nextIndex = {
|
|
247
|
+
schema_version: SCHEMA_VERSION,
|
|
248
|
+
active_task_id: index.active_task_id ?? null,
|
|
249
|
+
tasks: Array.isArray(index.tasks) ? index.tasks : []
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
atomicWriteFile(stateIndexPath(cwd), `${JSON.stringify(nextIndex, null, 2)}\n`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function taskFilePath(cwd, taskId) {
|
|
256
|
+
return path.join(tasksDirPath(cwd), `${taskId}.json`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function stateIndexPath(cwd) {
|
|
260
|
+
return path.join(stateDirPath(cwd), "index.json");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function defaultIndex() {
|
|
264
|
+
return {
|
|
265
|
+
schema_version: SCHEMA_VERSION,
|
|
266
|
+
active_task_id: null,
|
|
267
|
+
tasks: []
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function buildConfirmedContract(state, contractInput) {
|
|
272
|
+
const draft = normalizeObject(state.task_draft);
|
|
273
|
+
const existing = normalizeObject(state.confirmed_contract);
|
|
274
|
+
const intent = pickString(contractInput.intent, existing.intent, draft.intent);
|
|
275
|
+
const goal = pickString(contractInput.goal, existing.goal, draft.goal);
|
|
276
|
+
const scope = sanitizeScopeLikeArray(pickArray(contractInput.scope, existing.scope, draft.scope), SCOPE_PLACEHOLDER);
|
|
277
|
+
const acceptance = sanitizeScopeLikeArray(
|
|
278
|
+
pickArray(contractInput.acceptance, existing.acceptance, draft.acceptance),
|
|
279
|
+
ACCEPTANCE_PLACEHOLDER
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
if (!intent || intent === "unknown") {
|
|
283
|
+
throw new Error("闭合 confirmed_contract 失败:缺少明确 intent");
|
|
284
|
+
}
|
|
285
|
+
if (!goal) {
|
|
286
|
+
throw new Error("闭合 confirmed_contract 失败:缺少明确 goal");
|
|
287
|
+
}
|
|
288
|
+
if (scope.length === 0) {
|
|
289
|
+
throw new Error("闭合 confirmed_contract 失败:缺少明确 scope");
|
|
290
|
+
}
|
|
291
|
+
if (acceptance.length === 0) {
|
|
292
|
+
throw new Error("闭合 confirmed_contract 失败:缺少明确 acceptance");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const riskLevel = pickRiskLevel(contractInput.riskLevel, existing.risk_level, draft?.derived?.risk_level);
|
|
296
|
+
const contract = {
|
|
297
|
+
intent,
|
|
298
|
+
goal,
|
|
299
|
+
scope,
|
|
300
|
+
acceptance,
|
|
301
|
+
title: pickNullableString(contractInput.title, existing.title, draft.title),
|
|
302
|
+
constraints: pickArray(contractInput.constraints, existing.constraints, draft.constraints),
|
|
303
|
+
verification: pickArray(contractInput.verification, existing.verification),
|
|
304
|
+
context_refs: pickArray(contractInput.contextRefs, existing.context_refs, draft.context_refs),
|
|
305
|
+
id: pickString(contractInput.id, existing.id, state.task_id),
|
|
306
|
+
mode: pickString(contractInput.mode, existing.mode, draft.mode),
|
|
307
|
+
evidence_required: pickArray(contractInput.evidenceRequired, existing.evidence_required)
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
if (riskLevel) {
|
|
311
|
+
contract.risk_level = riskLevel;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return compactObject(contract);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function validatePhase(phase) {
|
|
318
|
+
if (!VALID_PHASES.includes(phase)) {
|
|
319
|
+
throw new Error(`无效的 phase: ${phase}`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function validateState(state) {
|
|
324
|
+
if (!VALID_STATES.includes(state)) {
|
|
325
|
+
throw new Error(`无效的 state: ${state}`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function validateTransition(from, to) {
|
|
330
|
+
if (from === to) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const allowed = VALID_TRANSITIONS[from] ?? [];
|
|
335
|
+
if (!allowed.includes(to)) {
|
|
336
|
+
throw new Error(`非法状态迁移: ${from} -> ${to}。${from} 允许迁移到: ${allowed.join(", ")}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function generateTaskId(taskDraft) {
|
|
341
|
+
const intent = taskDraft?.intent ?? "task";
|
|
342
|
+
const goal = String(taskDraft?.goal ?? "")
|
|
343
|
+
.replace(/[^a-zA-Z0-9\u4e00-\u9fff]/g, " ")
|
|
344
|
+
.trim()
|
|
345
|
+
.split(/\s+/)
|
|
346
|
+
.slice(0, 3)
|
|
347
|
+
.join("-");
|
|
348
|
+
const slug = (goal || "unnamed").toLowerCase().replace(/[^a-z0-9-]/g, "") || "unnamed";
|
|
349
|
+
return `${intent}-${slug}-${crypto.randomBytes(3).toString("hex")}`;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function readJson(filePath) {
|
|
353
|
+
try {
|
|
354
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
355
|
+
} catch {
|
|
356
|
+
throw new Error(`JSON 解析失败: ${filePath}`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function ensureDirectory(directoryPath) {
|
|
361
|
+
fs.mkdirSync(directoryPath, { recursive: true });
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function atomicWriteFile(filePath, content) {
|
|
365
|
+
const directory = path.dirname(filePath);
|
|
366
|
+
ensureDirectory(directory);
|
|
367
|
+
const tempPath = path.join(
|
|
368
|
+
directory,
|
|
369
|
+
`.${path.basename(filePath)}.${process.pid}.${Date.now()}.${crypto.randomBytes(3).toString("hex")}.tmp`
|
|
370
|
+
);
|
|
371
|
+
fs.writeFileSync(tempPath, content, "utf8");
|
|
372
|
+
fs.renameSync(tempPath, filePath);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function withStateWriteLock(cwd, callback) {
|
|
376
|
+
const lockPath = stateLockDirPath(cwd);
|
|
377
|
+
const release = acquireStateWriteLock(lockPath);
|
|
378
|
+
try {
|
|
379
|
+
return callback();
|
|
380
|
+
} finally {
|
|
381
|
+
release();
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function acquireStateWriteLock(lockPath) {
|
|
386
|
+
ensureDirectory(path.dirname(lockPath));
|
|
387
|
+
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
388
|
+
|
|
389
|
+
while (true) {
|
|
390
|
+
try {
|
|
391
|
+
fs.mkdirSync(lockPath);
|
|
392
|
+
fs.writeFileSync(
|
|
393
|
+
path.join(lockPath, "owner.json"),
|
|
394
|
+
`${JSON.stringify({ pid: process.pid, acquired_at: new Date().toISOString() }, null, 2)}\n`,
|
|
395
|
+
"utf8"
|
|
396
|
+
);
|
|
397
|
+
return () => {
|
|
398
|
+
fs.rmSync(lockPath, { recursive: true, force: true });
|
|
399
|
+
};
|
|
400
|
+
} catch (error) {
|
|
401
|
+
if (error?.code !== "EEXIST") {
|
|
402
|
+
throw error;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
cleanupStaleLock(lockPath);
|
|
406
|
+
|
|
407
|
+
if (Date.now() >= deadline) {
|
|
408
|
+
throw new Error(`获取 state 写锁超时: ${lockPath}`);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
sleep(LOCK_WAIT_MS);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function cleanupStaleLock(lockPath) {
|
|
417
|
+
try {
|
|
418
|
+
const stats = fs.statSync(lockPath);
|
|
419
|
+
if (Date.now() - stats.mtimeMs > LOCK_STALE_MS) {
|
|
420
|
+
fs.rmSync(lockPath, { recursive: true, force: true });
|
|
421
|
+
}
|
|
422
|
+
} catch {
|
|
423
|
+
// lock 已被其他进程释放,无需处理
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function sleep(ms) {
|
|
428
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function toArray(value) {
|
|
432
|
+
return Array.isArray(value) ? value : [value];
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function normalizeObject(value) {
|
|
436
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function pickString(...values) {
|
|
440
|
+
for (const value of values) {
|
|
441
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
442
|
+
return value.trim();
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function pickNullableString(...values) {
|
|
449
|
+
for (const value of values) {
|
|
450
|
+
if (value === null) {
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
if (typeof value === "string") {
|
|
454
|
+
return value.trim();
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function pickArray(...values) {
|
|
461
|
+
for (const value of values) {
|
|
462
|
+
const normalized = normalizeArray(value);
|
|
463
|
+
if (normalized.length > 0) {
|
|
464
|
+
return normalized;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return [];
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function normalizeArray(value) {
|
|
471
|
+
if (Array.isArray(value)) {
|
|
472
|
+
return value.map((item) => String(item).trim()).filter(Boolean);
|
|
473
|
+
}
|
|
474
|
+
if (value == null) {
|
|
475
|
+
return [];
|
|
476
|
+
}
|
|
477
|
+
const normalized = String(value).trim();
|
|
478
|
+
return normalized ? [normalized] : [];
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function sanitizeScopeLikeArray(values, placeholder) {
|
|
482
|
+
return normalizeArray(values).filter((item) => item !== placeholder);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function pickRiskLevel(...values) {
|
|
486
|
+
for (const value of values) {
|
|
487
|
+
if (typeof value === "string" && VALID_RISK_LEVELS.has(value.trim())) {
|
|
488
|
+
return value.trim();
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function compactObject(value) {
|
|
495
|
+
return Object.fromEntries(
|
|
496
|
+
Object.entries(value).filter(([, item]) => item !== undefined && item !== null)
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function stateDirPath(cwd) {
|
|
501
|
+
return runtimePath(cwd, "state");
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function tasksDirPath(cwd) {
|
|
505
|
+
return runtimePath(cwd, "state", "tasks");
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function stateLockDirPath(cwd) {
|
|
509
|
+
return runtimePath(cwd, "state", ".write-lock");
|
|
510
|
+
}
|