@integrity-labs/agt-cli 0.15.4 → 0.15.7
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/dist/bin/agt.js +381 -52
- package/dist/bin/agt.js.map +1 -1
- package/dist/{chunk-IEVDKEIT.js → chunk-S3SFU5IM.js} +1 -1
- package/dist/{chunk-IEVDKEIT.js.map → chunk-S3SFU5IM.js.map} +1 -1
- package/dist/{chunk-WDF7NJ2F.js → chunk-WIW5FIRY.js} +2 -1
- package/dist/{chunk-WDF7NJ2F.js.map → chunk-WIW5FIRY.js.map} +1 -1
- package/dist/lib/manager-worker.js +100 -34
- package/dist/lib/manager-worker.js.map +1 -1
- package/dist/{persistent-session-ALP5DGFI.js → persistent-session-Q4X2KRS6.js} +2 -2
- package/package.json +4 -1
- /package/dist/{persistent-session-ALP5DGFI.js.map → persistent-session-Q4X2KRS6.js.map} +0 -0
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
resolveChannels,
|
|
23
23
|
resolveDmTarget,
|
|
24
24
|
wrapScheduledTaskPrompt
|
|
25
|
-
} from "../chunk-
|
|
25
|
+
} from "../chunk-WIW5FIRY.js";
|
|
26
26
|
import {
|
|
27
27
|
findTaskByTemplate,
|
|
28
28
|
getProjectDir,
|
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
startPersistentSession,
|
|
43
43
|
stopAllSessionsAndWait,
|
|
44
44
|
stopPersistentSession
|
|
45
|
-
} from "../chunk-
|
|
45
|
+
} from "../chunk-S3SFU5IM.js";
|
|
46
46
|
|
|
47
47
|
// src/lib/manager-worker.ts
|
|
48
48
|
import { createHash } from "crypto";
|
|
@@ -1069,8 +1069,8 @@ function startRealtimeKanban(config2) {
|
|
|
1069
1069
|
filter: filterStr
|
|
1070
1070
|
}, (payload) => {
|
|
1071
1071
|
const item = payload.new;
|
|
1072
|
-
if (item.status === "
|
|
1073
|
-
log2(`[realtime] New kanban item in '
|
|
1072
|
+
if (item.status === "todo") {
|
|
1073
|
+
log2(`[realtime] New kanban item in 'todo': item_id=${item.id} agent=${item.agent_id}`);
|
|
1074
1074
|
onTodayItem(item);
|
|
1075
1075
|
}
|
|
1076
1076
|
}).on("postgres_changes", {
|
|
@@ -1081,8 +1081,8 @@ function startRealtimeKanban(config2) {
|
|
|
1081
1081
|
}, (payload) => {
|
|
1082
1082
|
const item = payload.new;
|
|
1083
1083
|
const old = payload.old;
|
|
1084
|
-
if (item.status === "
|
|
1085
|
-
log2(`[realtime] Kanban item moved to '
|
|
1084
|
+
if (item.status === "todo" && old.status !== "todo") {
|
|
1085
|
+
log2(`[realtime] Kanban item moved to 'todo': item_id=${item.id} agent=${item.agent_id}`);
|
|
1086
1086
|
onTodayItem(item);
|
|
1087
1087
|
}
|
|
1088
1088
|
if (onCompletion && (item.status === "done" || item.status === "failed") && old.status !== item.status) {
|
|
@@ -1753,19 +1753,19 @@ The following skills are installed in \`.claude/skills/\`. Claude Code auto-acti
|
|
|
1753
1753
|
|
|
1754
1754
|
${body}
|
|
1755
1755
|
|
|
1756
|
-
## Updating
|
|
1756
|
+
## Updating Integrations (ENG-4341)
|
|
1757
1757
|
|
|
1758
|
-
|
|
1758
|
+
Integration skills under \`.claude/skills/integration-*/SKILL.md\` are **read-only** and managed by the platform. They are derived from the integration's database row plus any per-agent context overrides, and are re-rendered every time the manager polls or a context change is broadcast over Supabase Realtime.
|
|
1759
1759
|
|
|
1760
|
-
**Never edit \`.claude/skills/
|
|
1760
|
+
**Never edit \`.claude/skills/integration-*/SKILL.md\` files directly.** If you do, your edit will be silently overwritten on the next manager refresh, AND it won't propagate to other agents using the same integration.
|
|
1761
1761
|
|
|
1762
|
-
To change
|
|
1762
|
+
To change an integration's behavior (add a rule, update a default, tell the integration not to do something), call the **\`plugin.improve\`** MCP tool with the user's request. The tool calls the platform API, which uses an LLM to translate the request into a structured update of the integration's typed context fields and/or freeform overrides text. You'll get a diff back to show the user; on confirmation, call the tool again with \`auto_apply: true\` to apply.
|
|
1763
1763
|
|
|
1764
1764
|
Examples of when to use \`plugin.improve\`:
|
|
1765
|
-
- *"Update the Coding
|
|
1766
|
-
- *"Add a rule to the Coding
|
|
1767
|
-
- *"Tell the Knowledge Base
|
|
1768
|
-
- *"The Slack
|
|
1765
|
+
- *"Update the Coding integration so we always use trunk instead of main"*
|
|
1766
|
+
- *"Add a rule to the Coding integration: never merge directly to main, always open a PR"*
|
|
1767
|
+
- *"Tell the Knowledge Base integration not to return results below 70% relevance"*
|
|
1768
|
+
- *"The Slack integration should post incidents to #oncall, not #general"*
|
|
1769
1769
|
${SKILLS_INDEX_END}`;
|
|
1770
1770
|
const current = rfs(claudeMdPath, "utf-8");
|
|
1771
1771
|
let next;
|
|
@@ -2035,7 +2035,7 @@ async function pollCycle() {
|
|
|
2035
2035
|
}
|
|
2036
2036
|
try {
|
|
2037
2037
|
const { detectHostSecurity } = await import("../host-security-6PDFG7F5.js");
|
|
2038
|
-
const { collectDiagnostics } = await import("../persistent-session-
|
|
2038
|
+
const { collectDiagnostics } = await import("../persistent-session-Q4X2KRS6.js");
|
|
2039
2039
|
const diagCodeNames = [...persistentSessionAgents];
|
|
2040
2040
|
const agentDiagnostics = diagCodeNames.length > 0 ? collectDiagnostics(diagCodeNames) : void 0;
|
|
2041
2041
|
let tailscaleHostname;
|
|
@@ -2897,7 +2897,7 @@ async function processAgent(agent, agentStates) {
|
|
|
2897
2897
|
);
|
|
2898
2898
|
for (const [pluginSlug, scopes] of pluginGroups) {
|
|
2899
2899
|
try {
|
|
2900
|
-
const pluginSkillId = `
|
|
2900
|
+
const pluginSkillId = `integration-${pluginSlug}`.replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
2901
2901
|
currentPluginSkillIds.add(pluginSkillId);
|
|
2902
2902
|
const ctx = contextBySlug.get(pluginSlug);
|
|
2903
2903
|
const renderedScopes = scopes.map((s) => ({
|
|
@@ -2943,7 +2943,9 @@ async function processAgent(agent, agentStates) {
|
|
|
2943
2943
|
for (const dir of existingDirs) {
|
|
2944
2944
|
try {
|
|
2945
2945
|
for (const entry of readdirSync2(dir)) {
|
|
2946
|
-
if (entry.startsWith("plugin-")
|
|
2946
|
+
if (entry.startsWith("plugin-") || entry.startsWith("integration-")) {
|
|
2947
|
+
discoveredEntries.add(entry);
|
|
2948
|
+
}
|
|
2947
2949
|
}
|
|
2948
2950
|
} catch {
|
|
2949
2951
|
}
|
|
@@ -2959,7 +2961,7 @@ async function processAgent(agent, agentStates) {
|
|
|
2959
2961
|
};
|
|
2960
2962
|
for (const entry of discoveredEntries) {
|
|
2961
2963
|
if (!currentPluginSkillIds.has(entry)) {
|
|
2962
|
-
removeSkillFolder(entry, "orphaned
|
|
2964
|
+
removeSkillFolder(entry, "orphaned skill folder");
|
|
2963
2965
|
}
|
|
2964
2966
|
}
|
|
2965
2967
|
} catch (err) {
|
|
@@ -3139,7 +3141,7 @@ async function processAgent(agent, agentStates) {
|
|
|
3139
3141
|
} else if (agentFw === "claude-code") {
|
|
3140
3142
|
if (sessionMode === "persistent") {
|
|
3141
3143
|
if (isSessionHealthy(agent.code_name)) {
|
|
3142
|
-
const todayItem = boardItems.find((b) => b.status === "
|
|
3144
|
+
const todayItem = boardItems.find((b) => b.status === "todo");
|
|
3143
3145
|
const taskHint = todayItem ? ` Top item: "${todayItem.title}" (priority ${todayItem.priority}).` : "";
|
|
3144
3146
|
injectMessage(agent.code_name, "task", `New work triggered. Check your kanban board with kanban_list and pick up the next item.${taskHint}`, {
|
|
3145
3147
|
task_name: "kanban-work-trigger"
|
|
@@ -3465,7 +3467,7 @@ async function syncAndCheckClaudeScheduler(agent, tasks, boardItems, refreshData
|
|
|
3465
3467
|
prompt = boardPrefix + prompt;
|
|
3466
3468
|
}
|
|
3467
3469
|
if (KANBAN_WORK_TEMPLATES.has(task.templateId)) {
|
|
3468
|
-
const todayItem = boardItems.find((b) => b.status === "
|
|
3470
|
+
const todayItem = boardItems.find((b) => b.status === "todo");
|
|
3469
3471
|
if (todayItem) {
|
|
3470
3472
|
try {
|
|
3471
3473
|
await api.post("/host/kanban", {
|
|
@@ -3487,9 +3489,42 @@ async function syncAndCheckClaudeScheduler(agent, tasks, boardItems, refreshData
|
|
|
3487
3489
|
});
|
|
3488
3490
|
}
|
|
3489
3491
|
}
|
|
3492
|
+
async function startRun(opts) {
|
|
3493
|
+
try {
|
|
3494
|
+
const res = await api.post(
|
|
3495
|
+
"/host/runs/start",
|
|
3496
|
+
opts
|
|
3497
|
+
);
|
|
3498
|
+
return { run_id: res.run_id ?? null, kanban_item_id: res.kanban_item_id ?? null };
|
|
3499
|
+
} catch (err) {
|
|
3500
|
+
const errText = err instanceof Error ? err.message : String(err);
|
|
3501
|
+
const errId = createHash("sha256").update(errText).digest("hex").slice(0, 12);
|
|
3502
|
+
log(`[runs] start failed for agent_id=${opts.agent_id} source_type=${opts.source_type} error_id=${errId}`);
|
|
3503
|
+
return { run_id: null, kanban_item_id: null };
|
|
3504
|
+
}
|
|
3505
|
+
}
|
|
3506
|
+
async function finishRun(runId, outcome, options = {}) {
|
|
3507
|
+
try {
|
|
3508
|
+
await api.post("/host/runs/finish", {
|
|
3509
|
+
run_id: runId,
|
|
3510
|
+
outcome,
|
|
3511
|
+
outcome_message: options.outcomeMessage,
|
|
3512
|
+
metadata: options.metadata,
|
|
3513
|
+
complete_kanban_item_id: options.completeKanbanItemId ?? void 0,
|
|
3514
|
+
result: options.result
|
|
3515
|
+
});
|
|
3516
|
+
} catch (err) {
|
|
3517
|
+
const errText = err instanceof Error ? err.message : String(err);
|
|
3518
|
+
const errId = createHash("sha256").update(errText).digest("hex").slice(0, 12);
|
|
3519
|
+
log(`[runs] finish failed for run_id=${runId} outcome=${outcome} error_id=${errId}`);
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3490
3522
|
async function executeAndProcessClaudeTask(codeName, agentId, task, prompt) {
|
|
3491
3523
|
const projectDir = getProjectDir(codeName);
|
|
3492
3524
|
const mcpConfigPath = join2(projectDir, ".mcp.json");
|
|
3525
|
+
let runId = null;
|
|
3526
|
+
let kanbanItemId = null;
|
|
3527
|
+
let taskResult;
|
|
3493
3528
|
sanitizeMcpJson(mcpConfigPath, requireHost());
|
|
3494
3529
|
prompt = wrapScheduledTaskPrompt(prompt);
|
|
3495
3530
|
try {
|
|
@@ -3536,6 +3571,16 @@ async function executeAndProcessClaudeTask(codeName, agentId, task, prompt) {
|
|
|
3536
3571
|
log(`[claude-scheduler] Skipping task '${task.name}' for '${codeName}' \u2014 auth resolve failed: ${err.message}`);
|
|
3537
3572
|
return;
|
|
3538
3573
|
}
|
|
3574
|
+
const startResult = await startRun({
|
|
3575
|
+
agent_id: agentId,
|
|
3576
|
+
source_type: "scheduled_task",
|
|
3577
|
+
source_ref: task.taskId,
|
|
3578
|
+
metadata: { template_id: task.templateId, name: task.name },
|
|
3579
|
+
materialize_kanban: { title: task.name, priority: 2 }
|
|
3580
|
+
});
|
|
3581
|
+
runId = startResult.run_id;
|
|
3582
|
+
kanbanItemId = startResult.kanban_item_id;
|
|
3583
|
+
if (runId) childEnv["AGT_RUN_ID"] = runId;
|
|
3539
3584
|
const { stdout, stderr } = await execFilePromiseLong(resolveClaudeBinary(), claudeArgs, {
|
|
3540
3585
|
cwd: projectDir,
|
|
3541
3586
|
timeout: 3e5,
|
|
@@ -3546,6 +3591,7 @@ async function executeAndProcessClaudeTask(codeName, agentId, task, prompt) {
|
|
|
3546
3591
|
log(`[claude-scheduler] Task '${task.name}' stderr for '${codeName}': ${stderr.slice(0, 500)}`);
|
|
3547
3592
|
}
|
|
3548
3593
|
const output = stdout.trim();
|
|
3594
|
+
taskResult = output.slice(0, 4e3) || void 0;
|
|
3549
3595
|
log(`[claude-scheduler] Task '${task.name}' completed for '${codeName}' (${output.length} chars): ${output.slice(0, 300)}`);
|
|
3550
3596
|
await processClaudeTaskResult(codeName, agentId, task.templateId, output, {
|
|
3551
3597
|
mode: task.deliveryMode,
|
|
@@ -3553,6 +3599,13 @@ async function executeAndProcessClaudeTask(codeName, agentId, task, prompt) {
|
|
|
3553
3599
|
to: task.deliveryTo,
|
|
3554
3600
|
taskId: task.taskId
|
|
3555
3601
|
});
|
|
3602
|
+
if (runId) {
|
|
3603
|
+
await finishRun(runId, "completed", {
|
|
3604
|
+
metadata: { output_length: output.length },
|
|
3605
|
+
completeKanbanItemId: kanbanItemId,
|
|
3606
|
+
result: taskResult
|
|
3607
|
+
});
|
|
3608
|
+
}
|
|
3556
3609
|
const updated = markTaskFired(codeName, task.taskId, "ok");
|
|
3557
3610
|
claudeSchedulerStates.set(codeName, updated);
|
|
3558
3611
|
if (task.scheduleKind === "at") {
|
|
@@ -3565,13 +3618,25 @@ async function executeAndProcessClaudeTask(codeName, agentId, task, prompt) {
|
|
|
3565
3618
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
3566
3619
|
log(`[claude-scheduler] Task '${task.name}' failed for '${codeName}': ${errMsg}`);
|
|
3567
3620
|
if (err instanceof ChildProcessError) {
|
|
3568
|
-
|
|
3569
|
-
|
|
3621
|
+
const errStdout = err.stdout.trim();
|
|
3622
|
+
if (errStdout) {
|
|
3623
|
+
taskResult = errStdout.slice(0, 4e3) || taskResult;
|
|
3624
|
+
log(`[claude-scheduler] Task '${task.name}' stdout for '${codeName}': ${errStdout.slice(0, 1e3)}`);
|
|
3570
3625
|
}
|
|
3571
3626
|
if (err.stderr.trim()) {
|
|
3572
3627
|
log(`[claude-scheduler] Task '${task.name}' stderr for '${codeName}': ${err.stderr.trim().slice(0, 1e3)}`);
|
|
3573
3628
|
}
|
|
3574
3629
|
}
|
|
3630
|
+
if (runId) {
|
|
3631
|
+
try {
|
|
3632
|
+
await finishRun(runId, "failed", {
|
|
3633
|
+
outcomeMessage: errMsg,
|
|
3634
|
+
completeKanbanItemId: kanbanItemId,
|
|
3635
|
+
result: taskResult
|
|
3636
|
+
});
|
|
3637
|
+
} catch {
|
|
3638
|
+
}
|
|
3639
|
+
}
|
|
3575
3640
|
const updated = markTaskFired(codeName, task.taskId, "error");
|
|
3576
3641
|
claudeSchedulerStates.set(codeName, updated);
|
|
3577
3642
|
}
|
|
@@ -3819,7 +3884,7 @@ async function ensurePersistentSession(agent, tasks, boardItems, refreshData) {
|
|
|
3819
3884
|
prompt = boardPrefix + prompt;
|
|
3820
3885
|
}
|
|
3821
3886
|
if (KANBAN_WORK_TEMPLATES.has(task.templateId)) {
|
|
3822
|
-
const todayItem = boardItems.find((b) => b.status === "
|
|
3887
|
+
const todayItem = boardItems.find((b) => b.status === "todo");
|
|
3823
3888
|
if (todayItem) {
|
|
3824
3889
|
try {
|
|
3825
3890
|
await api.post("/host/kanban", {
|
|
@@ -4214,7 +4279,7 @@ var TASK_UPDATE_TEMPLATES = /* @__PURE__ */ new Set(["hourly-status", "task-upda
|
|
|
4214
4279
|
var PLAN_TEMPLATES = /* @__PURE__ */ new Set(["morning-plan"]);
|
|
4215
4280
|
var KANBAN_WORK_TEMPLATES = /* @__PURE__ */ new Set(["kanban-work"]);
|
|
4216
4281
|
var BOARD_INJECT_TEMPLATES = /* @__PURE__ */ new Set(["morning-plan", "task-update", "hourly-status", "end-of-day-summary", "kanban-work"]);
|
|
4217
|
-
var ACTIONABLE_STATUSES = /* @__PURE__ */ new Set(["backlog", "
|
|
4282
|
+
var ACTIONABLE_STATUSES = /* @__PURE__ */ new Set(["backlog", "todo", "in_progress"]);
|
|
4218
4283
|
function hasActionableItems(items) {
|
|
4219
4284
|
return items.some((item) => ACTIONABLE_STATUSES.has(item.status));
|
|
4220
4285
|
}
|
|
@@ -4347,8 +4412,8 @@ function parseStandupSummary(summary) {
|
|
|
4347
4412
|
if (lower.includes("yesterday") || lower.includes("accomplished")) {
|
|
4348
4413
|
currentSection = "yesterday";
|
|
4349
4414
|
continue;
|
|
4350
|
-
} else if (lower.includes("
|
|
4351
|
-
currentSection = "
|
|
4415
|
+
} else if (lower.includes("todo") || lower.includes("working on")) {
|
|
4416
|
+
currentSection = "todo";
|
|
4352
4417
|
continue;
|
|
4353
4418
|
} else if (lower.includes("blocker")) {
|
|
4354
4419
|
currentSection = "blockers";
|
|
@@ -4360,7 +4425,7 @@ function parseStandupSummary(summary) {
|
|
|
4360
4425
|
case "yesterday":
|
|
4361
4426
|
yesterday += (yesterday ? "\n" : "") + trimmed;
|
|
4362
4427
|
break;
|
|
4363
|
-
case "
|
|
4428
|
+
case "todo":
|
|
4364
4429
|
today += (today ? "\n" : "") + trimmed;
|
|
4365
4430
|
break;
|
|
4366
4431
|
case "blockers":
|
|
@@ -4403,7 +4468,7 @@ function parsePlanItems(summary) {
|
|
|
4403
4468
|
title,
|
|
4404
4469
|
priority,
|
|
4405
4470
|
estimated_minutes: estimatedMinutes,
|
|
4406
|
-
status: "
|
|
4471
|
+
status: "todo"
|
|
4407
4472
|
};
|
|
4408
4473
|
} else if (currentItem && trimmed && !trimmed.match(/^(?:PLAN|---)/i)) {
|
|
4409
4474
|
const descLine = sanitizeKanbanString(trimmed, MAX_KANBAN_NOTES_LENGTH);
|
|
@@ -4413,7 +4478,7 @@ function parsePlanItems(summary) {
|
|
|
4413
4478
|
if (currentItem) items.push(currentItem);
|
|
4414
4479
|
return items;
|
|
4415
4480
|
}
|
|
4416
|
-
var VALID_KANBAN_STATUSES = /* @__PURE__ */ new Set(["backlog", "
|
|
4481
|
+
var VALID_KANBAN_STATUSES = /* @__PURE__ */ new Set(["backlog", "todo", "in_progress", "done"]);
|
|
4417
4482
|
var MAX_KANBAN_TITLE_LENGTH = 500;
|
|
4418
4483
|
var MAX_KANBAN_NOTES_LENGTH = 2e3;
|
|
4419
4484
|
function sanitizeKanbanString(value, maxLen) {
|
|
@@ -4459,9 +4524,10 @@ function parseKanbanUpdates(summary) {
|
|
|
4459
4524
|
const lines = kanbanSection.split("\n");
|
|
4460
4525
|
for (const line of lines) {
|
|
4461
4526
|
const trimmed = line.trim();
|
|
4462
|
-
const match = trimmed.match(/^[-*•]\s*"([^"]+)":\s*(backlog|today|in_progress|done)(?:\s*\((.+)\))?/i);
|
|
4527
|
+
const match = trimmed.match(/^[-*•]\s*"([^"]+)":\s*(backlog|todo|today|in_progress|done)(?:\s*\((.+)\))?/i);
|
|
4463
4528
|
if (match) {
|
|
4464
|
-
const
|
|
4529
|
+
const rawStatus = match[2].toLowerCase();
|
|
4530
|
+
const status = rawStatus === "today" ? "todo" : rawStatus;
|
|
4465
4531
|
if (!VALID_KANBAN_STATUSES.has(status)) continue;
|
|
4466
4532
|
const title = sanitizeKanbanString(match[1], MAX_KANBAN_TITLE_LENGTH);
|
|
4467
4533
|
if (!title) continue;
|
|
@@ -4503,7 +4569,7 @@ function formatBoardForPrompt(items, template) {
|
|
|
4503
4569
|
const lines = [];
|
|
4504
4570
|
if (template === "morning-plan") {
|
|
4505
4571
|
lines.push("=== CURRENT BOARD ===");
|
|
4506
|
-
for (const [status, label] of [["backlog", "BACKLOG (carry-over)"], ["
|
|
4572
|
+
for (const [status, label] of [["backlog", "BACKLOG (carry-over)"], ["todo", "TO DO"], ["in_progress", "IN PROGRESS"]]) {
|
|
4507
4573
|
const statusItems = grouped[status];
|
|
4508
4574
|
if (statusItems && statusItems.length > 0) {
|
|
4509
4575
|
lines.push(`${label}:`);
|
|
@@ -4515,13 +4581,13 @@ function formatBoardForPrompt(items, template) {
|
|
|
4515
4581
|
lines.push("=====================");
|
|
4516
4582
|
lines.push("");
|
|
4517
4583
|
lines.push("Create today's plan. You may:");
|
|
4518
|
-
lines.push('- Move backlog items to "
|
|
4584
|
+
lines.push('- Move backlog items to "todo"');
|
|
4519
4585
|
lines.push("- Add new items you've identified");
|
|
4520
4586
|
lines.push("- Reprioritise existing items");
|
|
4521
4587
|
lines.push("");
|
|
4522
4588
|
} else {
|
|
4523
4589
|
lines.push("=== YOUR KANBAN BOARD ===");
|
|
4524
|
-
for (const [status, label] of [["
|
|
4590
|
+
for (const [status, label] of [["todo", "TO DO"], ["in_progress", "IN PROGRESS"], ["backlog", "BACKLOG"]]) {
|
|
4525
4591
|
const statusItems = grouped[status];
|
|
4526
4592
|
if (statusItems && statusItems.length > 0) {
|
|
4527
4593
|
lines.push(`${label}:`);
|