metheus-governance-mcp-cli 0.2.170 → 0.2.172
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/cli.mjs +1308 -24
- package/lib/local-project-dispatch.mjs +50 -0
- package/lib/proxy-gateway-request.mjs +3 -1
- package/lib/proxy-tool-helpers.mjs +41 -0
- package/lib/runner-data.mjs +35 -0
- package/lib/runner-execution.mjs +14 -0
- package/lib/runner-helpers.mjs +1 -0
- package/lib/runner-orchestration.mjs +109 -44
- package/lib/runner-trigger.mjs +11 -1
- package/lib/selftest-runner-scenarios.mjs +131 -51
- package/lib/selftest-telegram-e2e.mjs +1 -1
- package/package.json +1 -1
package/cli.mjs
CHANGED
|
@@ -80,6 +80,7 @@ import {
|
|
|
80
80
|
appendWorkitemListHints as appendWorkitemListHintsImpl,
|
|
81
81
|
injectCtxpackPreflightToken as injectCtxpackPreflightTokenImpl,
|
|
82
82
|
injectCtxpackUpdateDefaults as injectCtxpackUpdateDefaultsImpl,
|
|
83
|
+
injectWorkitemBotActorDefaults as injectWorkitemBotActorDefaultsImpl,
|
|
83
84
|
maybeAutoSyncCtxpackForCall as maybeAutoSyncCtxpackForCallImpl,
|
|
84
85
|
maybeAutoSyncCtxpackForSessionRequest as maybeAutoSyncCtxpackForSessionRequestImpl,
|
|
85
86
|
stripLocalOnlyToolArgs as stripLocalOnlyToolArgsImpl,
|
|
@@ -142,6 +143,7 @@ import {
|
|
|
142
143
|
linkWorkItemEvidence as linkWorkItemEvidenceImpl,
|
|
143
144
|
listProjectChatDestinations as listProjectChatDestinationsImpl,
|
|
144
145
|
listProjectContextItems as listProjectContextItemsImpl,
|
|
146
|
+
listProjectCtxpackFiles as listProjectCtxpackFilesImpl,
|
|
145
147
|
listThreadComments as listThreadCommentsImpl,
|
|
146
148
|
listUserBotsForRunner as listUserBotsForRunnerImpl,
|
|
147
149
|
selectProjectChatDestination as selectProjectChatDestinationImpl,
|
|
@@ -187,9 +189,13 @@ const BOT_RUNNER_CONFIG_RELATIVE_PATH = path.join(".metheus", "bot-runner.json")
|
|
|
187
189
|
const BOT_RUNNER_WORKSPACE_REGISTRY_RELATIVE_PATH = path.join(".metheus", "project-workspaces.json");
|
|
188
190
|
const BOT_RUNNER_STATE_RELATIVE_PATH = path.join(".metheus", "bot-runner-state.json");
|
|
189
191
|
const BOT_RUNNER_PROCESSES_RELATIVE_PATH = path.join(".metheus", "bot-runner-processes.json");
|
|
192
|
+
const BOT_RUNNER_LOGS_RELATIVE_DIR = path.join(".metheus", "runner-logs");
|
|
190
193
|
const BOT_RUNNER_LOCKS_RELATIVE_DIR = path.join(".metheus", "bot-runner-locks");
|
|
191
194
|
const BOT_RUNNER_CONFIG_VERSION = 2;
|
|
192
195
|
const BOT_RUNNER_ROUTE_LOCK_STALE_MS = 30 * 60 * 1000;
|
|
196
|
+
const BOT_RUNNER_ACTIVE_EXECUTION_STUCK_WARNING_MS = 20 * 60 * 1000;
|
|
197
|
+
const BOT_RUNNER_ACTIVE_EXECUTION_HEARTBEAT_INTERVAL_MS = 5000;
|
|
198
|
+
const BOT_RUNNER_ACTIVE_EXECUTION_HEARTBEAT_STALE_MS = 60 * 1000;
|
|
193
199
|
const BOT_RUNNER_DEFAULT_CONCURRENCY = 2;
|
|
194
200
|
const BOT_RUNNER_MAX_CONCURRENCY = 8;
|
|
195
201
|
const TELEGRAM_ROOT_LEGACY_ENV_RELATIVE_PATH = path.join(".metheus", "telegram.env");
|
|
@@ -220,6 +226,7 @@ const SELF_UPDATE_STATE_RELATIVE_PATH = path.join(".metheus", "governance-mcp-cl
|
|
|
220
226
|
const CTXPACK_CACHE_RELATIVE_DIR = path.join(".metheus", "ctxpack-cache");
|
|
221
227
|
const CTXPACK_META_FILENAME = ".metheus_ctxpack_sync.json";
|
|
222
228
|
const CTXPACK_UPDATE_TOOL_NAMES = ["ctxpack.update"];
|
|
229
|
+
const WORKITEM_MUTATION_TOOL_NAMES = ["workitem.push", "workitem.update", "workitem.transition"];
|
|
223
230
|
const CLI_META = loadCLIMeta();
|
|
224
231
|
const CLI_NAME = CLI_META.name || "metheus-governance-mcp-cli";
|
|
225
232
|
const CLI_VERSION = CLI_META.version || "0.0.0";
|
|
@@ -295,14 +302,14 @@ function printUsage() {
|
|
|
295
302
|
` ${cmd} runner list [--json <true|false>]`,
|
|
296
303
|
` ${cmd} runner route list [--json <true|false>]`,
|
|
297
304
|
` ${cmd} runner route add [--project-id <uuid>] [--provider <telegram|slack|kakaotalk>] [--role <monitor|review|worker|approval>] [--bot-name <server_name> | --bot-id <uuid>] [--destination-label <label> | --destination-id <uuid>] [--poll-interval-ms <n>] [--enabled <true|false>]`,
|
|
298
|
-
` ${cmd} runner project up [--project-id <uuid>] [--provider <telegram>] [--destination-label <label> | --destination-id <uuid>] [--bot-name <server_name> | --bot-id <uuid>] [--role <monitor|review|worker|approval> | --roles <csv>] [--apply <true|false>] [--start <true|false>] [--start-detached <true|false>] [--dry-run-delivery <true|false>] [--concurrency <n>] [--json <true|false>]`,
|
|
305
|
+
` ${cmd} runner project up [--project-id <uuid>] [--provider <telegram>] [--destination-label <label> | --destination-id <uuid>] [--bot-name <server_name> | --bot-id <uuid>] [--role <monitor|review|worker|approval> | --roles <csv>] [--apply <true|false>] [--start <true|false>] [--start-detached <true|false>] [--tui <true|false>] [--log-file <path>] [--dry-run-delivery <true|false>] [--concurrency <n>] [--json <true|false>]`,
|
|
299
306
|
` ${cmd} runner artifact scan [--workspace-dir <path>] [--file-limit <n>] [--json <true|false>]`,
|
|
300
307
|
` ${cmd} runner route edit [--route-name <name> | --bot-name <server_name> | --bot-id <uuid>]`,
|
|
301
308
|
` ${cmd} runner route remove [--route-name <name> | --bot-name <server_name> | --bot-id <uuid>]`,
|
|
302
309
|
` ${cmd} runner show [--route-name <name> | --bot-name <server_name> | --bot-id <uuid>] [--json <true|false>]`,
|
|
303
310
|
` ${cmd} runner once [--route-name <name> | --bot-name <server_name when one enabled route matches> | --bot-id <uuid when one enabled route matches>] [--project-id <uuid>] [--provider <telegram|slack|kakaotalk>] [--role <monitor|review|worker|approval>] [--role-profile <name>] [--mentions-only <true|false>] [--reply-to-bot-messages <true|false>] [--direct-messages <true|false>] [--ignore-edited-messages <true|false>] [--dry-run-delivery <true|false>] [--context-comments <n>] [--archive-replies <true|false>]`,
|
|
304
|
-
` ${cmd} runner start [--route-name <name> | --bot-name <server_name when one enabled route matches> | --bot-id <uuid when one enabled route matches>] [--project-id <uuid>] [--provider <telegram|slack|kakaotalk>] [--role <monitor|review|worker|approval>] [--role-profile <name>] [--mentions-only <true|false>] [--reply-to-bot-messages <true|false>] [--direct-messages <true|false>] [--ignore-edited-messages <true|false>] [--dry-run-delivery <true|false>] [--poll-interval-ms <n>] [--concurrency <n>] [--context-comments <n>] [--archive-replies <true|false>]`,
|
|
305
|
-
` ${cmd} runner start-detached [--route-name <name> | --bot-name <server_name when one enabled route matches> | --bot-id <uuid when one enabled route matches>] [--project-id <uuid>] [--provider <telegram|slack|kakaotalk>] [--role <monitor|review|worker|approval>] [--role-profile <name>] [--mentions-only <true|false>] [--reply-to-bot-messages <true|false>] [--direct-messages <true|false>] [--ignore-edited-messages <true|false>] [--dry-run-delivery <true|false>] [--poll-interval-ms <n>] [--concurrency <n>] [--context-comments <n>] [--archive-replies <true|false>] [--json <true|false>]`,
|
|
311
|
+
` ${cmd} runner start [--route-name <name> | --bot-name <server_name when one enabled route matches> | --bot-id <uuid when one enabled route matches>] [--project-id <uuid>] [--provider <telegram|slack|kakaotalk>] [--role <monitor|review|worker|approval>] [--role-profile <name>] [--mentions-only <true|false>] [--reply-to-bot-messages <true|false>] [--direct-messages <true|false>] [--ignore-edited-messages <true|false>] [--tui <true|false>] [--log-file <path>] [--dry-run-delivery <true|false>] [--poll-interval-ms <n>] [--concurrency <n>] [--context-comments <n>] [--archive-replies <true|false>]`,
|
|
312
|
+
` ${cmd} runner start-detached [--route-name <name> | --bot-name <server_name when one enabled route matches> | --bot-id <uuid when one enabled route matches>] [--project-id <uuid>] [--provider <telegram|slack|kakaotalk>] [--role <monitor|review|worker|approval>] [--role-profile <name>] [--mentions-only <true|false>] [--reply-to-bot-messages <true|false>] [--direct-messages <true|false>] [--ignore-edited-messages <true|false>] [--log-file <path>] [--dry-run-delivery <true|false>] [--poll-interval-ms <n>] [--concurrency <n>] [--context-comments <n>] [--archive-replies <true|false>] [--json <true|false>]`,
|
|
306
313
|
` ${cmd} runner status [--launch-id <id> | --project-id <uuid> | --route-name <name>] [--json <true|false>]`,
|
|
307
314
|
` ${cmd} runner stop [--launch-id <id> | --project-id <uuid> | --route-name <name>] [--all <true|false>] [--json <true|false>]`,
|
|
308
315
|
` ${cmd} ctxpack pull [--project-id <uuid>] [--base-url <url>] [--workspace-dir <path|auto>] [--paths <csv>] [--timeout-seconds <n>]`,
|
|
@@ -784,6 +791,92 @@ function botRunnerProcessesFilePath() {
|
|
|
784
791
|
return resolveHomeFilePath(BOT_RUNNER_PROCESSES_RELATIVE_PATH);
|
|
785
792
|
}
|
|
786
793
|
|
|
794
|
+
function botRunnerLogsDirPath() {
|
|
795
|
+
return resolveHomeFilePath(BOT_RUNNER_LOGS_RELATIVE_DIR);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function sanitizeRunnerLogSlug(rawValue, fallback = "runner") {
|
|
799
|
+
const normalized = String(rawValue || "")
|
|
800
|
+
.trim()
|
|
801
|
+
.toLowerCase()
|
|
802
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
803
|
+
.replace(/^-+|-+$/g, "");
|
|
804
|
+
return normalized || fallback;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function buildDefaultRunnerLogFilePath(routes, sourceLabel = "runner start", startedAt = new Date()) {
|
|
808
|
+
const timestamp = [
|
|
809
|
+
startedAt.getFullYear(),
|
|
810
|
+
String(startedAt.getMonth() + 1).padStart(2, "0"),
|
|
811
|
+
String(startedAt.getDate()).padStart(2, "0"),
|
|
812
|
+
"T",
|
|
813
|
+
String(startedAt.getHours()).padStart(2, "0"),
|
|
814
|
+
String(startedAt.getMinutes()).padStart(2, "0"),
|
|
815
|
+
String(startedAt.getSeconds()).padStart(2, "0"),
|
|
816
|
+
].join("");
|
|
817
|
+
const sourceSlug = sanitizeRunnerLogSlug(sourceLabel, "runner-start");
|
|
818
|
+
const routeSlug = sanitizeRunnerLogSlug(
|
|
819
|
+
ensureArray(routes)
|
|
820
|
+
.map((route) => normalizeRunnerRoute(route).name || "")
|
|
821
|
+
.filter(Boolean)
|
|
822
|
+
.slice(0, 2)
|
|
823
|
+
.join("-"),
|
|
824
|
+
"routes",
|
|
825
|
+
);
|
|
826
|
+
const dayDir = [
|
|
827
|
+
startedAt.getFullYear(),
|
|
828
|
+
String(startedAt.getMonth() + 1).padStart(2, "0"),
|
|
829
|
+
String(startedAt.getDate()).padStart(2, "0"),
|
|
830
|
+
].join("-");
|
|
831
|
+
return path.join(botRunnerLogsDirPath(), dayDir, `${sourceSlug}-${routeSlug}-${timestamp}.jsonl`);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function resolveRunnerLogFilePath(rawValue, routes, sourceLabel = "runner start") {
|
|
835
|
+
const text = String(rawValue || "").trim();
|
|
836
|
+
if (["false", "none", "off", "disable"].includes(text.toLowerCase())) {
|
|
837
|
+
return "";
|
|
838
|
+
}
|
|
839
|
+
const filePath = text
|
|
840
|
+
? path.resolve(text)
|
|
841
|
+
: buildDefaultRunnerLogFilePath(routes, sourceLabel);
|
|
842
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
843
|
+
return filePath;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function createRunnerStartLogger({ routes, flags, sourceLabel = "runner start", jsonMode = false }) {
|
|
847
|
+
const filePath = resolveRunnerLogFilePath(flags["log-file"], routes, sourceLabel);
|
|
848
|
+
if (!filePath) {
|
|
849
|
+
return null;
|
|
850
|
+
}
|
|
851
|
+
const append = (kind, payload = {}) => {
|
|
852
|
+
try {
|
|
853
|
+
fs.appendFileSync(
|
|
854
|
+
filePath,
|
|
855
|
+
`${JSON.stringify({
|
|
856
|
+
ts: new Date().toISOString(),
|
|
857
|
+
source: sourceLabel,
|
|
858
|
+
kind: String(kind || "").trim() || "event",
|
|
859
|
+
...safeObject(payload),
|
|
860
|
+
})}\n`,
|
|
861
|
+
"utf8",
|
|
862
|
+
);
|
|
863
|
+
} catch {
|
|
864
|
+
// best-effort logging only
|
|
865
|
+
}
|
|
866
|
+
};
|
|
867
|
+
append("runner_start", {
|
|
868
|
+
json_mode: jsonMode === true,
|
|
869
|
+
route_names: ensureArray(routes).map((route) => normalizeRunnerRoute(route).name || runnerRouteKey(normalizeRunnerRoute(route))),
|
|
870
|
+
});
|
|
871
|
+
return {
|
|
872
|
+
filePath,
|
|
873
|
+
append,
|
|
874
|
+
close(payload = {}) {
|
|
875
|
+
append("runner_stop", payload);
|
|
876
|
+
},
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
|
|
787
880
|
function normalizeBotProvider(rawValue) {
|
|
788
881
|
return normalizeBotProviderName(rawValue, "telegram");
|
|
789
882
|
}
|
|
@@ -1953,6 +2046,7 @@ function normalizeRunnerProcessLaunchEntry(rawEntry) {
|
|
|
1953
2046
|
project_ids: ensureArray(entry.project_ids || entry.projectIds).map((item) => String(item || "").trim()).filter(Boolean),
|
|
1954
2047
|
provider: normalizeBotProvider(entry.provider),
|
|
1955
2048
|
destination_labels: ensureArray(entry.destination_labels || entry.destinationLabels).map((item) => String(item || "").trim()).filter(Boolean),
|
|
2049
|
+
log_file: String(entry.log_file || entry.logFile || "").trim(),
|
|
1956
2050
|
created_by_pid: intFromRawAllowZero(entry.created_by_pid || entry.createdByPid, 0) || undefined,
|
|
1957
2051
|
source_command: String(entry.source_command || entry.sourceCommand || "").trim(),
|
|
1958
2052
|
});
|
|
@@ -2827,6 +2921,10 @@ async function listProjectContextItems(params) {
|
|
|
2827
2921
|
return listProjectContextItemsImpl(params, buildRunnerDataDeps());
|
|
2828
2922
|
}
|
|
2829
2923
|
|
|
2924
|
+
async function listProjectCtxpackFiles(params) {
|
|
2925
|
+
return listProjectCtxpackFilesImpl(params, buildRunnerDataDeps());
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2830
2928
|
async function createProjectContextItem(params) {
|
|
2831
2929
|
return createProjectContextItemImpl(params, buildRunnerDataDeps());
|
|
2832
2930
|
}
|
|
@@ -3568,6 +3666,327 @@ function buildRunnerDeliveryDeps() {
|
|
|
3568
3666
|
};
|
|
3569
3667
|
}
|
|
3570
3668
|
|
|
3669
|
+
function normalizeComparableProjectText(value) {
|
|
3670
|
+
return String(value || "")
|
|
3671
|
+
.trim()
|
|
3672
|
+
.toLowerCase()
|
|
3673
|
+
.replace(/[\\/_-]+/g, " ")
|
|
3674
|
+
.replace(/\s+/g, " ");
|
|
3675
|
+
}
|
|
3676
|
+
|
|
3677
|
+
function extractArtifactQueryTerms(messageText) {
|
|
3678
|
+
const text = String(messageText || "").trim();
|
|
3679
|
+
const exactNames = Array.from(
|
|
3680
|
+
new Set(
|
|
3681
|
+
ensureArray(text.match(/[A-Za-z0-9._-]+\.(?:md|txt|json|ya?ml|ts|tsx|js|jsx|py|go|java|cs|html|css|pdf)\b/gi))
|
|
3682
|
+
.map((item) => String(item || "").trim().toLowerCase())
|
|
3683
|
+
.filter(Boolean),
|
|
3684
|
+
),
|
|
3685
|
+
);
|
|
3686
|
+
const tokens = Array.from(
|
|
3687
|
+
new Set(
|
|
3688
|
+
normalizeComparableProjectText(text)
|
|
3689
|
+
.split(/[^a-z0-9.]+/i)
|
|
3690
|
+
.map((item) => item.trim())
|
|
3691
|
+
.filter((item) => item.length >= 4 && !["where", "path", "file", "project", "guide", "current"].includes(item)),
|
|
3692
|
+
),
|
|
3693
|
+
);
|
|
3694
|
+
return {
|
|
3695
|
+
exactNames,
|
|
3696
|
+
tokens,
|
|
3697
|
+
};
|
|
3698
|
+
}
|
|
3699
|
+
|
|
3700
|
+
function scoreProjectFileMatch(filePath, queryTerms) {
|
|
3701
|
+
const normalizedPath = String(filePath || "").trim().toLowerCase();
|
|
3702
|
+
if (!normalizedPath) return 0;
|
|
3703
|
+
const baseName = path.posix.basename(normalizedPath);
|
|
3704
|
+
let score = 0;
|
|
3705
|
+
for (const exactName of ensureArray(queryTerms.exactNames)) {
|
|
3706
|
+
if (!exactName) continue;
|
|
3707
|
+
if (baseName === exactName) score += 1000;
|
|
3708
|
+
else if (normalizedPath.includes(exactName)) score += 600;
|
|
3709
|
+
}
|
|
3710
|
+
const comparablePath = normalizeComparableProjectText(normalizedPath);
|
|
3711
|
+
for (const token of ensureArray(queryTerms.tokens)) {
|
|
3712
|
+
if (!token) continue;
|
|
3713
|
+
if (baseName.includes(token)) score += 120;
|
|
3714
|
+
else if (comparablePath.includes(token)) score += 45;
|
|
3715
|
+
}
|
|
3716
|
+
return score;
|
|
3717
|
+
}
|
|
3718
|
+
|
|
3719
|
+
async function locateProjectFilesForQuery({
|
|
3720
|
+
siteBaseURL,
|
|
3721
|
+
projectID,
|
|
3722
|
+
token,
|
|
3723
|
+
timeoutSeconds,
|
|
3724
|
+
query,
|
|
3725
|
+
limit = 5,
|
|
3726
|
+
}) {
|
|
3727
|
+
const queryTerms = extractArtifactQueryTerms(query);
|
|
3728
|
+
const files = ensureArray(await listProjectCtxpackFiles({
|
|
3729
|
+
siteBaseURL,
|
|
3730
|
+
projectID,
|
|
3731
|
+
token,
|
|
3732
|
+
timeoutSeconds,
|
|
3733
|
+
}));
|
|
3734
|
+
const matches = files
|
|
3735
|
+
.map((file) => ({
|
|
3736
|
+
...safeObject(file),
|
|
3737
|
+
score: scoreProjectFileMatch(String(file.path || ""), queryTerms),
|
|
3738
|
+
}))
|
|
3739
|
+
.filter((file) => file.score > 0)
|
|
3740
|
+
.sort((left, right) => {
|
|
3741
|
+
if (right.score !== left.score) return right.score - left.score;
|
|
3742
|
+
return String(left.path || "").localeCompare(String(right.path || ""));
|
|
3743
|
+
})
|
|
3744
|
+
.slice(0, Math.max(1, intFromRaw(limit, 5)));
|
|
3745
|
+
return {
|
|
3746
|
+
query: String(query || "").trim(),
|
|
3747
|
+
exact_names: queryTerms.exactNames,
|
|
3748
|
+
tokens: queryTerms.tokens,
|
|
3749
|
+
count: matches.length,
|
|
3750
|
+
matches: matches.map((file) => ({
|
|
3751
|
+
path: String(file.path || "").trim(),
|
|
3752
|
+
doc_type: String(file.docType || "").trim(),
|
|
3753
|
+
generated: file.generated === true,
|
|
3754
|
+
sha256: String(file.sha256 || "").trim(),
|
|
3755
|
+
size_bytes: intFromRawAllowZero(file.sizeBytes, 0),
|
|
3756
|
+
score: intFromRawAllowZero(file.score, 0),
|
|
3757
|
+
})),
|
|
3758
|
+
};
|
|
3759
|
+
}
|
|
3760
|
+
|
|
3761
|
+
function buildProjectFileLocateText(payload) {
|
|
3762
|
+
const query = String(safeObject(payload).query || "").trim() || "the requested file";
|
|
3763
|
+
const matches = ensureArray(safeObject(payload).matches);
|
|
3764
|
+
if (!matches.length) {
|
|
3765
|
+
return `현재 프로젝트 ctxpack에서 "${query}"와 일치하는 파일 경로를 찾지 못했습니다.`;
|
|
3766
|
+
}
|
|
3767
|
+
if (matches.length === 1) {
|
|
3768
|
+
const match = safeObject(matches[0]);
|
|
3769
|
+
return `현재 확인되는 "${query}" 경로는 "${String(match.path || "").trim()}" 입니다.`;
|
|
3770
|
+
}
|
|
3771
|
+
return [
|
|
3772
|
+
`현재 프로젝트 ctxpack에서 "${query}"와 일치하는 파일 경로는 ${matches.length}개입니다:`,
|
|
3773
|
+
...matches.map((match) => `- ${String(safeObject(match).path || "").trim()}`),
|
|
3774
|
+
].join("\n");
|
|
3775
|
+
}
|
|
3776
|
+
|
|
3777
|
+
function resolveProjectWorkspaceBindingSummary(projectID, workspaceDir = "") {
|
|
3778
|
+
const normalizedProjectID = String(projectID || "").trim();
|
|
3779
|
+
const registry = loadBotRunnerWorkspaceRegistry({ persistIfNeeded: false });
|
|
3780
|
+
const mapping = normalizeBotRunnerProjectMapping(
|
|
3781
|
+
normalizedProjectID,
|
|
3782
|
+
safeObject(registry.projectMappings)[normalizedProjectID],
|
|
3783
|
+
);
|
|
3784
|
+
const boundWorkspaceDir = String(mapping.workspaceDir || "").trim();
|
|
3785
|
+
const effectiveWorkspaceDir = boundWorkspaceDir
|
|
3786
|
+
|| String(resolveStableProjectWorkspaceCandidate(normalizedProjectID, workspaceDir) || "").trim();
|
|
3787
|
+
const warnings = [];
|
|
3788
|
+
const requestedWorkspaceDir = sanitizeWorkspaceCandidate(workspaceDir);
|
|
3789
|
+
if (requestedWorkspaceDir && boundWorkspaceDir && requestedWorkspaceDir !== boundWorkspaceDir) {
|
|
3790
|
+
warnings.push(`request workspace_dir differs from registry binding: ${requestedWorkspaceDir}`);
|
|
3791
|
+
}
|
|
3792
|
+
if (effectiveWorkspaceDir && !fs.existsSync(effectiveWorkspaceDir)) {
|
|
3793
|
+
warnings.push(`workspace_dir does not exist: ${effectiveWorkspaceDir}`);
|
|
3794
|
+
}
|
|
3795
|
+
const workspaceMeta = effectiveWorkspaceDir ? loadWorkspaceMeta(effectiveWorkspaceDir) : {};
|
|
3796
|
+
return {
|
|
3797
|
+
ok: Boolean(boundWorkspaceDir),
|
|
3798
|
+
project_id: normalizedProjectID,
|
|
3799
|
+
workspace_dir: effectiveWorkspaceDir,
|
|
3800
|
+
workspace_source: boundWorkspaceDir
|
|
3801
|
+
? `project-workspaces.json:${String(mapping.source || "unknown").trim() || "unknown"}`
|
|
3802
|
+
: "",
|
|
3803
|
+
workspace_registry_file: String(registry.filePath || botRunnerWorkspaceRegistryFilePath()).trim(),
|
|
3804
|
+
workspace_meta_file: effectiveWorkspaceDir ? findSyncMetaFile(effectiveWorkspaceDir) : "",
|
|
3805
|
+
ctxpack_key: buildCtxpackKeyFromMeta(workspaceMeta),
|
|
3806
|
+
warnings,
|
|
3807
|
+
};
|
|
3808
|
+
}
|
|
3809
|
+
|
|
3810
|
+
function listProjectRunnerRoleBindings({
|
|
3811
|
+
projectID,
|
|
3812
|
+
provider = "",
|
|
3813
|
+
destinationID = "",
|
|
3814
|
+
destinationLabel = "",
|
|
3815
|
+
}) {
|
|
3816
|
+
const normalizedProjectID = String(projectID || "").trim();
|
|
3817
|
+
const normalizedProvider = normalizeBotProvider(provider || "");
|
|
3818
|
+
const normalizedDestinationID = String(destinationID || "").trim();
|
|
3819
|
+
const normalizedDestinationLabel = String(destinationLabel || "").trim().toLowerCase();
|
|
3820
|
+
const runnerConfig = loadBotRunnerConfig({ persistIfNeeded: true });
|
|
3821
|
+
const telegramEntries = ensureArray(readTelegramEnvState().entries);
|
|
3822
|
+
return ensureArray(runnerConfig.routes)
|
|
3823
|
+
.map((routeRaw) => normalizeRunnerRoute(routeRaw))
|
|
3824
|
+
.filter((route) => route.enabled)
|
|
3825
|
+
.filter((route) => route.projectID === normalizedProjectID)
|
|
3826
|
+
.filter((route) => !normalizedProvider || route.provider === normalizedProvider)
|
|
3827
|
+
.filter((route) => !normalizedDestinationID || route.destinationID === normalizedDestinationID)
|
|
3828
|
+
.filter((route) => !normalizedDestinationLabel || String(route.destinationLabel || "").trim().toLowerCase() === normalizedDestinationLabel)
|
|
3829
|
+
.map((route) => {
|
|
3830
|
+
const diagnostics = collectRunnerRouteDiagnostics(route, runnerConfig);
|
|
3831
|
+
const matchedTelegramEntry = findRunnerTelegramEntryForRoute(route, telegramEntries);
|
|
3832
|
+
const botName = firstNonEmptyString([
|
|
3833
|
+
route.botName,
|
|
3834
|
+
matchedTelegramEntry?.serverBotName,
|
|
3835
|
+
]);
|
|
3836
|
+
return {
|
|
3837
|
+
route_name: String(route.name || runnerRouteKey(route)).trim(),
|
|
3838
|
+
project_id: String(route.projectID || "").trim(),
|
|
3839
|
+
provider: String(route.provider || "").trim(),
|
|
3840
|
+
destination_id: String(route.destinationID || "").trim(),
|
|
3841
|
+
destination_label: String(route.destinationLabel || "").trim(),
|
|
3842
|
+
bot_id: String(route.botID || "").trim(),
|
|
3843
|
+
bot_name: botName,
|
|
3844
|
+
role: String(route.role || "").trim(),
|
|
3845
|
+
role_profile_name: String(diagnostics.roleProfileName || route.roleProfile || "").trim(),
|
|
3846
|
+
client: String(diagnostics.roleProfile?.client || "").trim(),
|
|
3847
|
+
model: String(diagnostics.roleProfile?.model || "").trim(),
|
|
3848
|
+
permission_mode: String(diagnostics.roleProfile?.permissionMode || "").trim(),
|
|
3849
|
+
reasoning_effort: String(diagnostics.roleProfile?.reasoningEffort || "").trim(),
|
|
3850
|
+
};
|
|
3851
|
+
})
|
|
3852
|
+
.sort((left, right) => String(left.bot_name || left.route_name).localeCompare(String(right.bot_name || right.route_name)));
|
|
3853
|
+
}
|
|
3854
|
+
|
|
3855
|
+
function buildProjectBotRolesPayload({
|
|
3856
|
+
projectID,
|
|
3857
|
+
provider = "",
|
|
3858
|
+
destinationID = "",
|
|
3859
|
+
destinationLabel = "",
|
|
3860
|
+
}) {
|
|
3861
|
+
const bindings = listProjectRunnerRoleBindings({
|
|
3862
|
+
projectID,
|
|
3863
|
+
provider,
|
|
3864
|
+
destinationID,
|
|
3865
|
+
destinationLabel,
|
|
3866
|
+
});
|
|
3867
|
+
return {
|
|
3868
|
+
ok: bindings.length > 0,
|
|
3869
|
+
project_id: String(projectID || "").trim(),
|
|
3870
|
+
provider: normalizeBotProvider(provider || "") || "",
|
|
3871
|
+
destination_id: String(destinationID || "").trim(),
|
|
3872
|
+
destination_label: String(destinationLabel || "").trim(),
|
|
3873
|
+
count: bindings.length,
|
|
3874
|
+
bindings,
|
|
3875
|
+
};
|
|
3876
|
+
}
|
|
3877
|
+
|
|
3878
|
+
function buildProjectBotRolesText(payload) {
|
|
3879
|
+
const bindings = ensureArray(safeObject(payload).bindings);
|
|
3880
|
+
if (!bindings.length) {
|
|
3881
|
+
return "현재 프로젝트의 활성 runner 설정에서 확인되는 봇 역할이 없습니다.";
|
|
3882
|
+
}
|
|
3883
|
+
return [
|
|
3884
|
+
"현재 프로젝트의 활성 runner 설정 기준 역할은 아래와 같습니다.",
|
|
3885
|
+
...bindings.map((binding) => {
|
|
3886
|
+
const current = safeObject(binding);
|
|
3887
|
+
const botName = String(current.bot_name || current.route_name || "").trim() || "(unknown bot)";
|
|
3888
|
+
const routeRole = String(current.role || "").trim() || "-";
|
|
3889
|
+
const roleProfile = String(current.role_profile_name || "").trim() || "-";
|
|
3890
|
+
const client = String(current.client || "").trim() || "-";
|
|
3891
|
+
const model = String(current.model || "").trim() || "-";
|
|
3892
|
+
return `- ${botName}: role=${routeRole}, profile=${roleProfile}, client=${client}, model=${model}`;
|
|
3893
|
+
}),
|
|
3894
|
+
].join("\n");
|
|
3895
|
+
}
|
|
3896
|
+
|
|
3897
|
+
async function resolveInformationalQueryReply({
|
|
3898
|
+
intentType,
|
|
3899
|
+
route,
|
|
3900
|
+
routeState,
|
|
3901
|
+
selectedRecord,
|
|
3902
|
+
runtime,
|
|
3903
|
+
executionPlan,
|
|
3904
|
+
}) {
|
|
3905
|
+
const normalizedIntentType = String(intentType || "").trim();
|
|
3906
|
+
const messageText = String(safeObject(selectedRecord?.parsedArchive).body || "").trim();
|
|
3907
|
+
if (normalizedIntentType === "small_talk") {
|
|
3908
|
+
return {
|
|
3909
|
+
handled: true,
|
|
3910
|
+
reply: "안녕하세요. 현재 프로젝트 기준으로 필요한 정보를 바로 확인해드릴게요.",
|
|
3911
|
+
source: "small_talk",
|
|
3912
|
+
};
|
|
3913
|
+
}
|
|
3914
|
+
if (normalizedIntentType === "workspace_query") {
|
|
3915
|
+
const workspace = resolveProjectWorkspaceBindingSummary(route?.projectID, executionPlan?.workspaceDir || route?.workspaceDir || "");
|
|
3916
|
+
const workspaceDir = String(workspace.workspace_dir || "").trim();
|
|
3917
|
+
const reply = workspaceDir
|
|
3918
|
+
? `현재 이 프로젝트에 바인딩된 로컬 작업 폴더는 "${workspaceDir}" 입니다.`
|
|
3919
|
+
: "현재 이 프로젝트는 project-workspaces.json에 로컬 작업 폴더가 바인딩되어 있지 않습니다.";
|
|
3920
|
+
return {
|
|
3921
|
+
handled: true,
|
|
3922
|
+
reply,
|
|
3923
|
+
source: "project.workspace",
|
|
3924
|
+
lookup: workspace,
|
|
3925
|
+
};
|
|
3926
|
+
}
|
|
3927
|
+
if (normalizedIntentType === "bot_role_query") {
|
|
3928
|
+
const payload = buildProjectBotRolesPayload({
|
|
3929
|
+
projectID: route?.projectID,
|
|
3930
|
+
provider: route?.provider,
|
|
3931
|
+
destinationID: route?.destinationID,
|
|
3932
|
+
destinationLabel: route?.destinationLabel,
|
|
3933
|
+
});
|
|
3934
|
+
return {
|
|
3935
|
+
handled: true,
|
|
3936
|
+
reply: buildProjectBotRolesText(payload),
|
|
3937
|
+
source: "project.bot_roles",
|
|
3938
|
+
lookup: payload,
|
|
3939
|
+
};
|
|
3940
|
+
}
|
|
3941
|
+
if (normalizedIntentType === "status_query") {
|
|
3942
|
+
const activeExecution = resolveRunnerActiveExecutionState(routeState);
|
|
3943
|
+
if (activeExecution.active) {
|
|
3944
|
+
const startedAt = String(activeExecution.startedAt || "").trim();
|
|
3945
|
+
const sourceMessage = intFromRawAllowZero(activeExecution.sourceMessageID, 0);
|
|
3946
|
+
const stuckSuffix = activeExecution.stuck ? " 현재 실행이 오래 지속되어 확인이 필요합니다." : "";
|
|
3947
|
+
return {
|
|
3948
|
+
handled: true,
|
|
3949
|
+
reply: `현재 이 runner는 작업 중입니다.${sourceMessage > 0 ? ` source_message_id=${sourceMessage}` : ""}${startedAt ? ` started_at=${startedAt}` : ""}.${stuckSuffix}`.replace(/\s+\./g, "."),
|
|
3950
|
+
source: "runner.status",
|
|
3951
|
+
lookup: activeExecution,
|
|
3952
|
+
};
|
|
3953
|
+
}
|
|
3954
|
+
const lastAction = String(safeObject(routeState).last_action || "").trim();
|
|
3955
|
+
const lastReason = String(safeObject(routeState).last_reason || "").trim();
|
|
3956
|
+
const lastIntentType = String(safeObject(routeState).last_intent_type || "").trim();
|
|
3957
|
+
const reply = lastAction
|
|
3958
|
+
? `현재 실행 중인 작업은 없습니다. 마지막 상태는 "${lastAction}"${lastIntentType ? ` (intent=${lastIntentType})` : ""}${lastReason ? `, detail=${lastReason}` : ""} 입니다.`
|
|
3959
|
+
: "현재 실행 중인 작업은 없습니다. 아직 기록된 최근 작업 상태도 없습니다.";
|
|
3960
|
+
return {
|
|
3961
|
+
handled: true,
|
|
3962
|
+
reply,
|
|
3963
|
+
source: "runner.status",
|
|
3964
|
+
lookup: {
|
|
3965
|
+
active: false,
|
|
3966
|
+
last_action: lastAction,
|
|
3967
|
+
last_reason: lastReason,
|
|
3968
|
+
last_intent_type: lastIntentType,
|
|
3969
|
+
},
|
|
3970
|
+
};
|
|
3971
|
+
}
|
|
3972
|
+
if (normalizedIntentType === "artifact_location_query") {
|
|
3973
|
+
const payload = await locateProjectFilesForQuery({
|
|
3974
|
+
siteBaseURL: runtime.baseURL,
|
|
3975
|
+
projectID: route?.projectID,
|
|
3976
|
+
token: runtime.token,
|
|
3977
|
+
timeoutSeconds: runtime.timeoutSeconds,
|
|
3978
|
+
query: messageText,
|
|
3979
|
+
});
|
|
3980
|
+
return {
|
|
3981
|
+
handled: true,
|
|
3982
|
+
reply: buildProjectFileLocateText(payload),
|
|
3983
|
+
source: "project.file.locate",
|
|
3984
|
+
lookup: payload,
|
|
3985
|
+
};
|
|
3986
|
+
}
|
|
3987
|
+
return null;
|
|
3988
|
+
}
|
|
3989
|
+
|
|
3571
3990
|
function buildRunnerExecutionDeps() {
|
|
3572
3991
|
return {
|
|
3573
3992
|
analyzeHumanConversationIntentWithAI,
|
|
@@ -3594,8 +4013,10 @@ function buildRunnerExecutionDeps() {
|
|
|
3594
4013
|
createWorkItemThread,
|
|
3595
4014
|
linkWorkItemEvidence,
|
|
3596
4015
|
listProjectContextItems,
|
|
4016
|
+
listProjectCtxpackFiles,
|
|
3597
4017
|
createProjectContextItem,
|
|
3598
4018
|
replaceProjectCtxpackFiles,
|
|
4019
|
+
resolveInformationalQueryReply,
|
|
3599
4020
|
};
|
|
3600
4021
|
}
|
|
3601
4022
|
|
|
@@ -3650,23 +4071,52 @@ function emptyRunnerActiveExecutionPatch() {
|
|
|
3650
4071
|
active_comment_created_at: "",
|
|
3651
4072
|
active_source_message_id: undefined,
|
|
3652
4073
|
active_started_at: "",
|
|
4074
|
+
active_heartbeat_at: "",
|
|
3653
4075
|
active_runner_pid: undefined,
|
|
3654
4076
|
active_execution_token: "",
|
|
3655
4077
|
};
|
|
3656
4078
|
}
|
|
3657
4079
|
|
|
3658
4080
|
function buildRunnerActiveExecutionPatch(selectedRecord) {
|
|
4081
|
+
const nowISO = new Date().toISOString();
|
|
3659
4082
|
const parsed = safeObject(selectedRecord?.parsedArchive);
|
|
3660
4083
|
return {
|
|
3661
4084
|
active_comment_id: String(selectedRecord?.id || "").trim(),
|
|
3662
4085
|
active_comment_created_at: firstNonEmptyString([selectedRecord?.createdAt, selectedRecord?.updatedAt]),
|
|
3663
4086
|
active_source_message_id: intFromRawAllowZero(parsed.messageID, 0) || undefined,
|
|
3664
|
-
active_started_at:
|
|
4087
|
+
active_started_at: nowISO,
|
|
4088
|
+
active_heartbeat_at: nowISO,
|
|
3665
4089
|
active_runner_pid: process.pid,
|
|
3666
4090
|
active_execution_token: `${Date.now()}-${process.pid}-${String(selectedRecord?.id || "").trim()}`,
|
|
3667
4091
|
};
|
|
3668
4092
|
}
|
|
3669
4093
|
|
|
4094
|
+
function startRunnerExecutionHeartbeat(routeKey, selectedRecord, intervalMs = BOT_RUNNER_ACTIVE_EXECUTION_HEARTBEAT_INTERVAL_MS) {
|
|
4095
|
+
let stopped = false;
|
|
4096
|
+
const tick = () => {
|
|
4097
|
+
if (stopped) return;
|
|
4098
|
+
const currentState = safeObject(loadBotRunnerState().routes[routeKey]);
|
|
4099
|
+
if (String(currentState.active_comment_id || "").trim() !== String(selectedRecord?.id || "").trim()) {
|
|
4100
|
+
return;
|
|
4101
|
+
}
|
|
4102
|
+
saveRunnerRouteState(routeKey, {
|
|
4103
|
+
...currentState,
|
|
4104
|
+
active_heartbeat_at: new Date().toISOString(),
|
|
4105
|
+
});
|
|
4106
|
+
};
|
|
4107
|
+
tick();
|
|
4108
|
+
const timer = setInterval(tick, Math.max(1000, intervalMs));
|
|
4109
|
+
if (typeof timer.unref === "function") {
|
|
4110
|
+
timer.unref();
|
|
4111
|
+
}
|
|
4112
|
+
return {
|
|
4113
|
+
stop() {
|
|
4114
|
+
stopped = true;
|
|
4115
|
+
clearInterval(timer);
|
|
4116
|
+
},
|
|
4117
|
+
};
|
|
4118
|
+
}
|
|
4119
|
+
|
|
3670
4120
|
function resolveRunnerActiveExecutionState(routeState) {
|
|
3671
4121
|
const state = safeObject(routeState);
|
|
3672
4122
|
const activeCommentID = String(state.active_comment_id || "").trim();
|
|
@@ -3674,19 +4124,45 @@ function resolveRunnerActiveExecutionState(routeState) {
|
|
|
3674
4124
|
return {
|
|
3675
4125
|
active: false,
|
|
3676
4126
|
stale: false,
|
|
4127
|
+
stuck: false,
|
|
3677
4128
|
commentID: "",
|
|
3678
4129
|
sourceMessageID: 0,
|
|
3679
4130
|
startedAt: "",
|
|
4131
|
+
ageMs: 0,
|
|
4132
|
+
ageSeconds: 0,
|
|
4133
|
+
warning: "",
|
|
3680
4134
|
};
|
|
3681
4135
|
}
|
|
3682
4136
|
const activeRunnerPID = intFromRawAllowZero(state.active_runner_pid, 0);
|
|
3683
4137
|
const staleByProcess = activeRunnerPID > 0 && !isProcessAlive(activeRunnerPID);
|
|
4138
|
+
const startedAt = String(state.active_started_at || "").trim();
|
|
4139
|
+
const startedAtMs = Date.parse(startedAt);
|
|
4140
|
+
const ageMs = Number.isFinite(startedAtMs)
|
|
4141
|
+
? Math.max(0, Date.now() - startedAtMs)
|
|
4142
|
+
: 0;
|
|
4143
|
+
const ageSeconds = ageMs > 0 ? Math.floor(ageMs / 1000) : 0;
|
|
4144
|
+
const heartbeatAt = String(state.active_heartbeat_at || "").trim();
|
|
4145
|
+
const heartbeatAtMs = Date.parse(heartbeatAt);
|
|
4146
|
+
const heartbeatAgeMs = Number.isFinite(heartbeatAtMs)
|
|
4147
|
+
? Math.max(0, Date.now() - heartbeatAtMs)
|
|
4148
|
+
: ageMs;
|
|
4149
|
+
const staleByHeartbeat = !staleByProcess && heartbeatAgeMs >= BOT_RUNNER_ACTIVE_EXECUTION_HEARTBEAT_STALE_MS;
|
|
4150
|
+
const stale = staleByProcess || staleByHeartbeat;
|
|
4151
|
+
const stuck = !stale && ageMs >= BOT_RUNNER_ACTIVE_EXECUTION_STUCK_WARNING_MS;
|
|
3684
4152
|
return {
|
|
3685
|
-
active: !
|
|
3686
|
-
stale
|
|
4153
|
+
active: !stale,
|
|
4154
|
+
stale,
|
|
4155
|
+
stuck,
|
|
3687
4156
|
commentID: activeCommentID,
|
|
3688
4157
|
sourceMessageID: intFromRawAllowZero(state.active_source_message_id, 0),
|
|
3689
|
-
startedAt
|
|
4158
|
+
startedAt,
|
|
4159
|
+
ageMs,
|
|
4160
|
+
ageSeconds,
|
|
4161
|
+
warning: staleByHeartbeat
|
|
4162
|
+
? "active execution heartbeat expired; clearing busy state"
|
|
4163
|
+
: stuck
|
|
4164
|
+
? "active execution is taking longer than expected"
|
|
4165
|
+
: "",
|
|
3690
4166
|
};
|
|
3691
4167
|
}
|
|
3692
4168
|
|
|
@@ -4895,7 +5371,14 @@ async function runRunnerProjectUp(flags) {
|
|
|
4895
5371
|
await runRunnerStartDetachedResolvedRoutes(matchingRoutes, startFlags, "runner project up");
|
|
4896
5372
|
return;
|
|
4897
5373
|
}
|
|
4898
|
-
await runRunnerStartResolvedRoutes(matchingRoutes, startFlags
|
|
5374
|
+
await runRunnerStartResolvedRoutes(matchingRoutes, startFlags, {
|
|
5375
|
+
sourceLabel: "runner project up",
|
|
5376
|
+
bootstrapEvent: {
|
|
5377
|
+
route_name: "runner project up",
|
|
5378
|
+
outcome: "prepared",
|
|
5379
|
+
detail: `prepared ${matchingRoutes.length} enabled route(s) for ${summaryPayload.destination_label || summaryPayload.destination_id || summaryPayload.project_id || "project"}`,
|
|
5380
|
+
},
|
|
5381
|
+
});
|
|
4899
5382
|
}
|
|
4900
5383
|
|
|
4901
5384
|
function canStartRunnerDespiteProjectUpApplyFailure({ applyRequested, applyResult, matchingRoutes }) {
|
|
@@ -5115,6 +5598,7 @@ function buildRunnerShowPayload(route, flags = {}) {
|
|
|
5115
5598
|
const runnerConfig = loadBotRunnerConfig({ persistIfNeeded: true });
|
|
5116
5599
|
const runnerState = loadBotRunnerState();
|
|
5117
5600
|
const routeState = safeObject(runnerState.routes[runnerRouteKey(normalizedRoute)]);
|
|
5601
|
+
const activeExecutionState = resolveRunnerActiveExecutionState(routeState);
|
|
5118
5602
|
const diagnostics = collectRunnerRouteDiagnostics(normalizedRoute, runnerConfig);
|
|
5119
5603
|
const telegramState = readTelegramEnvState();
|
|
5120
5604
|
const telegramEntries = ensureArray(telegramState.entries);
|
|
@@ -5177,8 +5661,21 @@ function buildRunnerShowPayload(route, flags = {}) {
|
|
|
5177
5661
|
artifact_errors: ensureArray(routeState.last_artifact_errors).map((item) => String(item || "").trim()).filter(Boolean),
|
|
5178
5662
|
boundary_violations: ensureArray(routeState.last_boundary_violations).map((item) => safeObject(item)),
|
|
5179
5663
|
},
|
|
5664
|
+
active_execution: {
|
|
5665
|
+
active: activeExecutionState.active === true,
|
|
5666
|
+
stale: activeExecutionState.stale === true,
|
|
5667
|
+
stuck: activeExecutionState.stuck === true,
|
|
5668
|
+
comment_id: String(activeExecutionState.commentID || "").trim(),
|
|
5669
|
+
source_message_id: intFromRawAllowZero(activeExecutionState.sourceMessageID, 0),
|
|
5670
|
+
started_at: String(activeExecutionState.startedAt || "").trim(),
|
|
5671
|
+
age_seconds: intFromRawAllowZero(activeExecutionState.ageSeconds, 0),
|
|
5672
|
+
warning: String(activeExecutionState.warning || "").trim(),
|
|
5673
|
+
},
|
|
5180
5674
|
route_selection_note: "Routes are the executable unit. Use --route-name in production. --bot-name and --bot-id are convenience selectors that resolve one enabled route only when the match is unique.",
|
|
5181
|
-
warnings:
|
|
5675
|
+
warnings: [
|
|
5676
|
+
...ensureArray(diagnostics.warnings),
|
|
5677
|
+
...(activeExecutionState.stuck ? [activeExecutionState.warning] : []),
|
|
5678
|
+
],
|
|
5182
5679
|
errors: diagnostics.errors,
|
|
5183
5680
|
};
|
|
5184
5681
|
}
|
|
@@ -5234,6 +5731,15 @@ async function runRunnerShow(flags) {
|
|
|
5234
5731
|
` artifact_paths: ${payload.last_run.artifact_paths.length ? payload.last_run.artifact_paths.join(", ") : "-"}`,
|
|
5235
5732
|
` artifact_errors: ${payload.last_run.artifact_errors.length ? payload.last_run.artifact_errors.join(" | ") : "-"}`,
|
|
5236
5733
|
` boundary_violations: ${payload.last_run.boundary_violations.length ? payload.last_run.boundary_violations.map((item) => String(item.detail || item.path || JSON.stringify(item))).join(" | ") : "-"}`,
|
|
5734
|
+
" active_execution:",
|
|
5735
|
+
` active: ${payload.active_execution.active ? "true" : "false"}`,
|
|
5736
|
+
` stale: ${payload.active_execution.stale ? "true" : "false"}`,
|
|
5737
|
+
` stuck: ${payload.active_execution.stuck ? "true" : "false"}`,
|
|
5738
|
+
` comment_id: ${payload.active_execution.comment_id || "-"}`,
|
|
5739
|
+
` source_message_id: ${payload.active_execution.source_message_id || "-"}`,
|
|
5740
|
+
` started_at: ${payload.active_execution.started_at || "-"}`,
|
|
5741
|
+
` age_seconds: ${payload.active_execution.age_seconds || 0}`,
|
|
5742
|
+
` warning: ${payload.active_execution.warning || "-"}`,
|
|
5237
5743
|
payload.warnings.length ? ` warnings: ${payload.warnings.join("; ")}` : " warnings: -",
|
|
5238
5744
|
payload.errors.length ? ` errors: ${payload.errors.join("; ")}` : " errors: -",
|
|
5239
5745
|
].join("\n") + "\n",
|
|
@@ -5323,7 +5829,7 @@ async function waitForRunnerPIDFile(pidFilePath, timeoutMs = 8000) {
|
|
|
5323
5829
|
throw new Error(`detached runner launch did not publish a child process id: ${pidFilePath}`);
|
|
5324
5830
|
}
|
|
5325
5831
|
|
|
5326
|
-
function buildRunnerDetachedLaunchRecord(childPID, routes, flags, sourceCommand) {
|
|
5832
|
+
function buildRunnerDetachedLaunchRecord(childPID, routes, flags, sourceCommand, logFilePath = "") {
|
|
5327
5833
|
const normalizedRoutes = ensureArray(routes).map((route) => normalizeRunnerRoute(route));
|
|
5328
5834
|
const cliPath = fileURLToPath(import.meta.url);
|
|
5329
5835
|
return normalizeRunnerProcessLaunchEntry({
|
|
@@ -5339,6 +5845,7 @@ function buildRunnerDetachedLaunchRecord(childPID, routes, flags, sourceCommand)
|
|
|
5339
5845
|
project_ids: Array.from(new Set(normalizedRoutes.map((route) => String(route.projectID || "").trim()).filter(Boolean))),
|
|
5340
5846
|
provider: firstNonEmptyString(normalizedRoutes.map((route) => route.provider)),
|
|
5341
5847
|
destination_labels: Array.from(new Set(normalizedRoutes.map((route) => String(route.destinationLabel || route.destinationID || "").trim()).filter(Boolean))),
|
|
5848
|
+
log_file: String(logFilePath || "").trim(),
|
|
5342
5849
|
created_by_pid: process.pid,
|
|
5343
5850
|
source_command: sourceCommand,
|
|
5344
5851
|
});
|
|
@@ -5355,7 +5862,12 @@ function findExistingDetachedRunnerLaunch(registry, routes) {
|
|
|
5355
5862
|
|
|
5356
5863
|
async function launchDetachedRunnerProcess(flags, routes, sourceCommand) {
|
|
5357
5864
|
const cliPath = fileURLToPath(import.meta.url);
|
|
5358
|
-
const
|
|
5865
|
+
const detachedLogFilePath = resolveRunnerLogFilePath(flags["log-file"], routes, sourceCommand);
|
|
5866
|
+
const startFlags = {
|
|
5867
|
+
...safeObject(flags),
|
|
5868
|
+
...(detachedLogFilePath ? { "log-file": detachedLogFilePath } : {}),
|
|
5869
|
+
};
|
|
5870
|
+
const startArgs = [cliPath, "--no-update", "runner", "start", ...serializeCLIFlags(startFlags, {
|
|
5359
5871
|
omit: ["json", "start", "start-detached", "detached"],
|
|
5360
5872
|
})];
|
|
5361
5873
|
if (process.platform === "win32") {
|
|
@@ -5387,7 +5899,7 @@ async function launchDetachedRunnerProcess(flags, routes, sourceCommand) {
|
|
|
5387
5899
|
if (childPID <= 0) {
|
|
5388
5900
|
throw new Error("detached runner launch did not return a child process id");
|
|
5389
5901
|
}
|
|
5390
|
-
return buildRunnerDetachedLaunchRecord(childPID, routes,
|
|
5902
|
+
return buildRunnerDetachedLaunchRecord(childPID, routes, startFlags, sourceCommand, detachedLogFilePath);
|
|
5391
5903
|
}
|
|
5392
5904
|
const launchTempDir = fs.mkdtempSync(path.join(os.tmpdir(), "metheus-runner-launch-"));
|
|
5393
5905
|
const scriptPath = path.join(launchTempDir, "start-runner.sh");
|
|
@@ -5417,7 +5929,7 @@ async function launchDetachedRunnerProcess(flags, routes, sourceCommand) {
|
|
|
5417
5929
|
throw new Error(String(launched.stderr || launched.stdout || "failed to launch detached runner in Terminal.app").trim());
|
|
5418
5930
|
}
|
|
5419
5931
|
const childPID = await waitForRunnerPIDFile(pidFilePath);
|
|
5420
|
-
return buildRunnerDetachedLaunchRecord(childPID, routes,
|
|
5932
|
+
return buildRunnerDetachedLaunchRecord(childPID, routes, startFlags, sourceCommand, detachedLogFilePath);
|
|
5421
5933
|
}
|
|
5422
5934
|
const terminalLauncher = resolveLinuxDetachedTerminalLauncher(scriptPath);
|
|
5423
5935
|
if (!terminalLauncher) {
|
|
@@ -5433,7 +5945,7 @@ async function launchDetachedRunnerProcess(flags, routes, sourceCommand) {
|
|
|
5433
5945
|
throw new Error("detached terminal launcher did not return a process id");
|
|
5434
5946
|
}
|
|
5435
5947
|
const runnerPID = await waitForRunnerPIDFile(pidFilePath);
|
|
5436
|
-
return buildRunnerDetachedLaunchRecord(runnerPID, routes,
|
|
5948
|
+
return buildRunnerDetachedLaunchRecord(runnerPID, routes, startFlags, sourceCommand, detachedLogFilePath);
|
|
5437
5949
|
} finally {
|
|
5438
5950
|
try {
|
|
5439
5951
|
fs.rmSync(launchTempDir, { recursive: true, force: true });
|
|
@@ -5515,6 +6027,7 @@ async function runRunnerStatus(flags) {
|
|
|
5515
6027
|
` project_ids: ${launch.project_ids.join(", ") || "-"}`,
|
|
5516
6028
|
` route_names: ${launch.route_names.join(", ") || "-"}`,
|
|
5517
6029
|
` destination_labels: ${launch.destination_labels.join(", ") || "-"}`,
|
|
6030
|
+
` log_file: ${launch.log_file || "-"}`,
|
|
5518
6031
|
` command: ${launch.command || "-"}`,
|
|
5519
6032
|
].join("\n") + "\n",
|
|
5520
6033
|
);
|
|
@@ -5551,7 +6064,7 @@ async function runRunnerStartDetachedResolvedRoutes(routes, flags, sourceCommand
|
|
|
5551
6064
|
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
5552
6065
|
return payload;
|
|
5553
6066
|
}
|
|
5554
|
-
process.stdout.write(`Detached runner already running: launch_id=${existing.launch_id} pid=${existing.pid}\n`);
|
|
6067
|
+
process.stdout.write(`Detached runner already running: launch_id=${existing.launch_id} pid=${existing.pid}${existing.log_file ? ` log_file=${existing.log_file}` : ""}\n`);
|
|
5555
6068
|
return payload;
|
|
5556
6069
|
}
|
|
5557
6070
|
const launch = launchDetachedRunnerProcess(flags, routes, sourceCommand);
|
|
@@ -5582,6 +6095,7 @@ async function runRunnerStartDetachedResolvedRoutes(routes, flags, sourceCommand
|
|
|
5582
6095
|
`project_ids: ${launch.project_ids.join(", ") || "-"}`,
|
|
5583
6096
|
`route_names: ${launch.route_names.join(", ") || "-"}`,
|
|
5584
6097
|
`destination_labels: ${launch.destination_labels.join(", ") || "-"}`,
|
|
6098
|
+
`log_file: ${launch.log_file || "-"}`,
|
|
5585
6099
|
`command: ${launch.command}`,
|
|
5586
6100
|
].join("\n") + "\n",
|
|
5587
6101
|
);
|
|
@@ -5677,8 +6191,311 @@ async function runRunnerArtifactScan(flags) {
|
|
|
5677
6191
|
}
|
|
5678
6192
|
}
|
|
5679
6193
|
|
|
5680
|
-
|
|
6194
|
+
function truncateRunnerTUIText(rawValue, width) {
|
|
6195
|
+
const text = String(rawValue || "").replace(/\s+/g, " ").trim();
|
|
6196
|
+
const maxWidth = Math.max(4, intFromRaw(width, 0));
|
|
6197
|
+
if (!maxWidth || text.length <= maxWidth) {
|
|
6198
|
+
return text;
|
|
6199
|
+
}
|
|
6200
|
+
if (maxWidth <= 3) {
|
|
6201
|
+
return text.slice(0, maxWidth);
|
|
6202
|
+
}
|
|
6203
|
+
return `${text.slice(0, maxWidth - 3)}...`;
|
|
6204
|
+
}
|
|
6205
|
+
|
|
6206
|
+
function normalizeRunnerTUIPhase(rawValue) {
|
|
6207
|
+
const value = String(rawValue || "").trim().toLowerCase();
|
|
6208
|
+
return value || "waiting";
|
|
6209
|
+
}
|
|
6210
|
+
|
|
6211
|
+
function colorizeRunnerTUIPhase(phase, width, useColor) {
|
|
6212
|
+
const rendered = bootstrapPadRight(String(phase || "-").toUpperCase(), width);
|
|
6213
|
+
if (!useColor) return rendered;
|
|
6214
|
+
const colorCode = phase === "replied" || phase === "idle"
|
|
6215
|
+
? "32"
|
|
6216
|
+
: phase === "ai_running" || phase === "delivering" || phase === "context_suggesting"
|
|
6217
|
+
? "36"
|
|
6218
|
+
: phase === "polling" || phase === "busy" || phase === "waiting" || phase === "primed"
|
|
6219
|
+
? "33"
|
|
6220
|
+
: phase === "skipped"
|
|
6221
|
+
? "35"
|
|
6222
|
+
: "31";
|
|
6223
|
+
return bootstrapColorText(rendered, colorCode);
|
|
6224
|
+
}
|
|
6225
|
+
|
|
6226
|
+
function mapRunnerTUIPhaseFromOutcome(outcome) {
|
|
6227
|
+
const value = String(outcome || "").trim().toLowerCase();
|
|
6228
|
+
if (!value) return "waiting";
|
|
6229
|
+
if (value === "accepted") return "ai_running";
|
|
6230
|
+
if (value === "dry_run") return "replied";
|
|
6231
|
+
if (value === "policy_violation" || value === "execution_failed") return "error";
|
|
6232
|
+
return value;
|
|
6233
|
+
}
|
|
6234
|
+
|
|
6235
|
+
function formatRunnerTUIAge(now, startedAtMs) {
|
|
6236
|
+
const ageMs = Math.max(0, Number(now) - Number(startedAtMs || 0));
|
|
6237
|
+
if (!(ageMs > 0)) return "-";
|
|
6238
|
+
const ageSeconds = Math.floor(ageMs / 1000);
|
|
6239
|
+
if (ageSeconds < 60) return `${ageSeconds}s`;
|
|
6240
|
+
const minutes = Math.floor(ageSeconds / 60);
|
|
6241
|
+
const seconds = ageSeconds % 60;
|
|
6242
|
+
if (minutes < 60) return `${minutes}m${String(seconds).padStart(2, "0")}`;
|
|
6243
|
+
const hours = Math.floor(minutes / 60);
|
|
6244
|
+
const remMinutes = minutes % 60;
|
|
6245
|
+
return `${hours}h${String(remMinutes).padStart(2, "0")}`;
|
|
6246
|
+
}
|
|
6247
|
+
|
|
6248
|
+
function summarizeRunnerTUIPhases(routes = []) {
|
|
6249
|
+
const counts = {
|
|
6250
|
+
waiting: 0,
|
|
6251
|
+
polling: 0,
|
|
6252
|
+
running: 0,
|
|
6253
|
+
busy: 0,
|
|
6254
|
+
replied: 0,
|
|
6255
|
+
error: 0,
|
|
6256
|
+
};
|
|
6257
|
+
let warningCount = 0;
|
|
6258
|
+
for (const route of ensureArray(routes)) {
|
|
6259
|
+
const phase = normalizeRunnerTUIPhase(route.phase);
|
|
6260
|
+
if (phase === "waiting") counts.waiting += 1;
|
|
6261
|
+
else if (phase === "polling") counts.polling += 1;
|
|
6262
|
+
else if (["ai_running", "delivering", "context_suggesting"].includes(phase)) counts.running += 1;
|
|
6263
|
+
else if (phase === "busy") counts.busy += 1;
|
|
6264
|
+
else if (["replied", "idle", "skipped", "dry_run"].includes(phase)) counts.replied += 1;
|
|
6265
|
+
else if (["error", "blocked", "locked", "execution_failed", "policy_violation"].includes(phase)) counts.error += 1;
|
|
6266
|
+
else counts.waiting += 1;
|
|
6267
|
+
if (String(route.warning || route.last_error || "").trim()) {
|
|
6268
|
+
warningCount += 1;
|
|
6269
|
+
}
|
|
6270
|
+
}
|
|
6271
|
+
return { counts, warningCount };
|
|
6272
|
+
}
|
|
6273
|
+
|
|
6274
|
+
function buildRunnerTUIFrame({ routes = [], recentEvents = [], concurrency = 1, now = Date.now(), stopping = false, useColor = false, sourceLabel = "runner start", logFilePath = "" }) {
|
|
6275
|
+
const lines = [];
|
|
6276
|
+
const activeCount = ensureArray(routes).filter((item) => ["polling", "ai_running", "delivering", "context_suggesting", "busy"].includes(normalizeRunnerTUIPhase(item.phase))).length;
|
|
6277
|
+
const phaseSummary = summarizeRunnerTUIPhases(routes);
|
|
6278
|
+
const header = `METHEUS RUNNER ${stopping ? "STOPPING" : "LIVE"} | source=${String(sourceLabel || "runner start").trim()} | routes=${routes.length} active=${activeCount} concurrency=${concurrency} time=${new Date(now).toLocaleTimeString("en-US", { hour12: false })}`;
|
|
6279
|
+
lines.push(useColor ? bootstrapColorText(header, stopping ? "33" : "36") : header);
|
|
6280
|
+
if (String(logFilePath || "").trim()) {
|
|
6281
|
+
lines.push(`Log: ${truncateRunnerTUIText(String(logFilePath || "").trim(), 112)}`);
|
|
6282
|
+
}
|
|
6283
|
+
lines.push(`Summary: waiting=${phaseSummary.counts.waiting} polling=${phaseSummary.counts.polling} running=${phaseSummary.counts.running} busy=${phaseSummary.counts.busy} replied=${phaseSummary.counts.replied} error=${phaseSummary.counts.error} warnings=${phaseSummary.warningCount}`);
|
|
6284
|
+
lines.push("Ctrl+C to stop. Foreground runner state is shown below.");
|
|
6285
|
+
lines.push("");
|
|
6286
|
+
lines.push(
|
|
6287
|
+
[
|
|
6288
|
+
bootstrapPadRight("Route", 24),
|
|
6289
|
+
bootstrapPadRight("Phase", 18),
|
|
6290
|
+
bootstrapPadRight("Age", 7),
|
|
6291
|
+
bootstrapPadRight("AI", 20),
|
|
6292
|
+
bootstrapPadRight("Intent", 18),
|
|
6293
|
+
bootstrapPadRight("Msg", 8),
|
|
6294
|
+
bootstrapPadRight("Next", 8),
|
|
6295
|
+
bootstrapPadRight("Warn", 6),
|
|
6296
|
+
].join(" "),
|
|
6297
|
+
);
|
|
6298
|
+
lines.push("-".repeat(120));
|
|
6299
|
+
for (const route of ensureArray(routes)) {
|
|
6300
|
+
const phase = normalizeRunnerTUIPhase(route.phase);
|
|
6301
|
+
const aiLabel = truncateRunnerTUIText(
|
|
6302
|
+
route.client
|
|
6303
|
+
? `${String(route.client || "").trim()}/${String(route.model || "").trim() || "-"}`
|
|
6304
|
+
: "-",
|
|
6305
|
+
20,
|
|
6306
|
+
);
|
|
6307
|
+
const ageLabel = formatRunnerTUIAge(now, route.phase_started_at || route.updated_at || now);
|
|
6308
|
+
const nextSeconds = Number.isFinite(Number(route.next_run_at)) && Number(route.next_run_at) > now
|
|
6309
|
+
? `${Math.max(0, Math.ceil((Number(route.next_run_at) - now) / 1000))}s`
|
|
6310
|
+
: phase === "ai_running" || phase === "delivering" || phase === "context_suggesting"
|
|
6311
|
+
? "run"
|
|
6312
|
+
: "-";
|
|
6313
|
+
const warningLabel = String(route.warning || route.last_error || "").trim() ? "yes" : "-";
|
|
6314
|
+
lines.push(
|
|
6315
|
+
[
|
|
6316
|
+
bootstrapPadRight(truncateRunnerTUIText(route.route_name, 24), 24),
|
|
6317
|
+
colorizeRunnerTUIPhase(phase, 18, useColor),
|
|
6318
|
+
bootstrapPadRight(ageLabel, 7),
|
|
6319
|
+
bootstrapPadRight(aiLabel, 20),
|
|
6320
|
+
bootstrapPadRight(truncateRunnerTUIText(route.intent_type || "-", 18), 18),
|
|
6321
|
+
bootstrapPadRight(String(route.source_message_id || "-"), 8),
|
|
6322
|
+
bootstrapPadRight(nextSeconds, 8),
|
|
6323
|
+
bootstrapPadRight(warningLabel, 6),
|
|
6324
|
+
].join(" "),
|
|
6325
|
+
);
|
|
6326
|
+
const detailParts = [
|
|
6327
|
+
String(route.detail || "").trim(),
|
|
6328
|
+
String(route.context_suggestion_status || "").trim()
|
|
6329
|
+
? `context=${String(route.context_suggestion_status || "").trim()}`
|
|
6330
|
+
: "",
|
|
6331
|
+
String(route.warning || "").trim()
|
|
6332
|
+
? `warning=${String(route.warning || "").trim()}`
|
|
6333
|
+
: "",
|
|
6334
|
+
String(route.last_error || "").trim()
|
|
6335
|
+
? `error=${String(route.last_error || "").trim()}`
|
|
6336
|
+
: "",
|
|
6337
|
+
].filter(Boolean);
|
|
6338
|
+
lines.push(` ${truncateRunnerTUIText(detailParts.join(" | ") || "-", 116)}`);
|
|
6339
|
+
}
|
|
6340
|
+
lines.push("");
|
|
6341
|
+
lines.push(useColor ? bootstrapColorText("Recent Events", "35") : "Recent Events");
|
|
6342
|
+
if (!ensureArray(recentEvents).length) {
|
|
6343
|
+
lines.push(" (none yet)");
|
|
6344
|
+
} else {
|
|
6345
|
+
for (const event of ensureArray(recentEvents).slice(0, 8)) {
|
|
6346
|
+
lines.push(
|
|
6347
|
+
` [${String(event.time || "").trim() || "--:--:--"}] ${truncateRunnerTUIText(event.route_name, 24)} ${bootstrapPadRight(String(event.outcome || "-").trim(), 12)} ${truncateRunnerTUIText(event.detail || "-", 74)}`,
|
|
6348
|
+
);
|
|
6349
|
+
}
|
|
6350
|
+
}
|
|
6351
|
+
return lines.join("\n");
|
|
6352
|
+
}
|
|
6353
|
+
|
|
6354
|
+
function createRunnerStartTUI({ routes, flags, jsonMode, concurrency, sourceLabel = "runner start", bootstrapEvent = null, logFilePath = "" }) {
|
|
6355
|
+
const enabled = !jsonMode && boolFromRaw(flags.tui, Boolean(process.stdout?.isTTY));
|
|
6356
|
+
if (!enabled || !process.stdout?.isTTY) {
|
|
6357
|
+
return null;
|
|
6358
|
+
}
|
|
6359
|
+
const useColor = bootstrapSupportsANSIColors();
|
|
6360
|
+
const routeStates = new Map();
|
|
6361
|
+
const recentEvents = [];
|
|
6362
|
+
for (const route of ensureArray(routes)) {
|
|
6363
|
+
const normalizedRoute = normalizeRunnerRoute(route);
|
|
6364
|
+
const routeKey = runnerRouteKey(normalizedRoute);
|
|
6365
|
+
routeStates.set(routeKey, {
|
|
6366
|
+
route_key: routeKey,
|
|
6367
|
+
route_name: normalizedRoute.name,
|
|
6368
|
+
phase: "waiting",
|
|
6369
|
+
detail: "awaiting next poll",
|
|
6370
|
+
client: "",
|
|
6371
|
+
model: "",
|
|
6372
|
+
intent_type: "",
|
|
6373
|
+
source_message_id: "",
|
|
6374
|
+
next_run_at: 0,
|
|
6375
|
+
context_suggestion_status: "",
|
|
6376
|
+
warning: "",
|
|
6377
|
+
last_error: "",
|
|
6378
|
+
phase_started_at: Date.now(),
|
|
6379
|
+
updated_at: Date.now(),
|
|
6380
|
+
});
|
|
6381
|
+
}
|
|
6382
|
+
if (bootstrapEvent && typeof bootstrapEvent === "object") {
|
|
6383
|
+
recentEvents.unshift({
|
|
6384
|
+
time: new Date().toLocaleTimeString("en-US", { hour12: false }),
|
|
6385
|
+
route_name: String(bootstrapEvent.route_name || sourceLabel || "runner").trim(),
|
|
6386
|
+
outcome: String(bootstrapEvent.outcome || "prepared").trim(),
|
|
6387
|
+
detail: String(bootstrapEvent.detail || "").trim(),
|
|
6388
|
+
});
|
|
6389
|
+
}
|
|
6390
|
+
let intervalHandle = null;
|
|
6391
|
+
let disposed = false;
|
|
6392
|
+
|
|
6393
|
+
const render = (stopping = false) => {
|
|
6394
|
+
if (disposed) return;
|
|
6395
|
+
const frame = buildRunnerTUIFrame({
|
|
6396
|
+
routes: Array.from(routeStates.values()),
|
|
6397
|
+
recentEvents,
|
|
6398
|
+
concurrency,
|
|
6399
|
+
now: Date.now(),
|
|
6400
|
+
stopping,
|
|
6401
|
+
useColor,
|
|
6402
|
+
sourceLabel,
|
|
6403
|
+
logFilePath,
|
|
6404
|
+
});
|
|
6405
|
+
process.stdout.write(`\u001b[?25l\u001b[2J\u001b[H${frame}`);
|
|
6406
|
+
};
|
|
6407
|
+
|
|
6408
|
+
const setRouteState = (routeKey, patch = {}) => {
|
|
6409
|
+
const current = safeObject(routeStates.get(routeKey));
|
|
6410
|
+
if (!current.route_key) return;
|
|
6411
|
+
const nextPhase = normalizeRunnerTUIPhase(patch.phase || current.phase);
|
|
6412
|
+
const phaseChanged = nextPhase !== normalizeRunnerTUIPhase(current.phase);
|
|
6413
|
+
routeStates.set(routeKey, {
|
|
6414
|
+
...current,
|
|
6415
|
+
...safeObject(patch),
|
|
6416
|
+
phase: nextPhase,
|
|
6417
|
+
phase_started_at: phaseChanged ? Date.now() : Number(current.phase_started_at || Date.now()),
|
|
6418
|
+
updated_at: Date.now(),
|
|
6419
|
+
});
|
|
6420
|
+
render(false);
|
|
6421
|
+
};
|
|
6422
|
+
|
|
6423
|
+
const recordResult = (result, routeState = null) => {
|
|
6424
|
+
const routeKey = String(result?.route_key || "").trim();
|
|
6425
|
+
if (routeKey && routeStates.has(routeKey)) {
|
|
6426
|
+
const current = safeObject(routeStates.get(routeKey));
|
|
6427
|
+
setRouteState(routeKey, {
|
|
6428
|
+
phase: mapRunnerTUIPhaseFromOutcome(result?.outcome),
|
|
6429
|
+
detail: String(result?.detail || "").trim(),
|
|
6430
|
+
intent_type: String(routeState?.last_intent_type || current.intent_type || "").trim(),
|
|
6431
|
+
source_message_id: intFromRawAllowZero(routeState?.last_source_message_id, intFromRawAllowZero(current.source_message_id, 0)) || current.source_message_id,
|
|
6432
|
+
context_suggestion_status: String(result?.context_suggestion_status || routeState?.last_context_suggestion_status || current.context_suggestion_status || "").trim(),
|
|
6433
|
+
warning: String(safeObject(routeState).active_execution_warning || result?.warning || "").trim(),
|
|
6434
|
+
last_error: String(routeState?.last_error || "").trim(),
|
|
6435
|
+
});
|
|
6436
|
+
}
|
|
6437
|
+
if (!result || ["idle", "busy"].includes(String(result.outcome || "").trim().toLowerCase())) {
|
|
6438
|
+
return;
|
|
6439
|
+
}
|
|
6440
|
+
recentEvents.unshift({
|
|
6441
|
+
time: new Date().toLocaleTimeString("en-US", { hour12: false }),
|
|
6442
|
+
route_name: String(result.route_name || result.route_key || "").trim(),
|
|
6443
|
+
outcome: String(result.outcome || "").trim(),
|
|
6444
|
+
detail: String(result.detail || "").trim(),
|
|
6445
|
+
});
|
|
6446
|
+
if (recentEvents.length > 8) {
|
|
6447
|
+
recentEvents.length = 8;
|
|
6448
|
+
}
|
|
6449
|
+
render(false);
|
|
6450
|
+
};
|
|
6451
|
+
|
|
6452
|
+
const reportExecutionStage = ({ routeKey, executionPlan, selectedRecord, phase, detail, warning = "", contextSuggestionStatus = "", intentType = "" }) => {
|
|
6453
|
+
const normalizedRouteKey = String(routeKey || "").trim();
|
|
6454
|
+
if (!normalizedRouteKey || !routeStates.has(normalizedRouteKey)) return;
|
|
6455
|
+
setRouteState(normalizedRouteKey, {
|
|
6456
|
+
phase: normalizeRunnerTUIPhase(phase),
|
|
6457
|
+
detail: String(detail || "").trim(),
|
|
6458
|
+
client: String(safeObject(executionPlan).roleProfile?.client || "").trim(),
|
|
6459
|
+
model: String(safeObject(executionPlan).roleProfile?.model || "").trim(),
|
|
6460
|
+
source_message_id: intFromRawAllowZero(safeObject(selectedRecord).parsedArchive?.messageID, 0),
|
|
6461
|
+
intent_type: String(intentType || safeObject(executionPlan).intentType || "").trim(),
|
|
6462
|
+
warning: String(warning || "").trim(),
|
|
6463
|
+
context_suggestion_status: String(contextSuggestionStatus || "").trim(),
|
|
6464
|
+
});
|
|
6465
|
+
};
|
|
6466
|
+
|
|
6467
|
+
const updateNextRunAt = (routeKey, nextRunAt) => {
|
|
6468
|
+
const normalizedRouteKey = String(routeKey || "").trim();
|
|
6469
|
+
if (!normalizedRouteKey || !routeStates.has(normalizedRouteKey)) return;
|
|
6470
|
+
setRouteState(normalizedRouteKey, { next_run_at: Number(nextRunAt) || 0 });
|
|
6471
|
+
};
|
|
6472
|
+
|
|
6473
|
+
return {
|
|
6474
|
+
start() {
|
|
6475
|
+
render(false);
|
|
6476
|
+
intervalHandle = setInterval(() => render(false), 1000);
|
|
6477
|
+
},
|
|
6478
|
+
stop() {
|
|
6479
|
+
if (intervalHandle) clearInterval(intervalHandle);
|
|
6480
|
+
render(true);
|
|
6481
|
+
disposed = true;
|
|
6482
|
+
process.stdout.write("\u001b[?25h\n");
|
|
6483
|
+
},
|
|
6484
|
+
setRouteState,
|
|
6485
|
+
recordResult,
|
|
6486
|
+
reportExecutionStage,
|
|
6487
|
+
updateNextRunAt,
|
|
6488
|
+
};
|
|
6489
|
+
}
|
|
6490
|
+
|
|
6491
|
+
async function runRunnerStartResolvedRoutes(routes, flags, options = {}) {
|
|
5681
6492
|
const jsonMode = boolFromRaw(flags.json, false);
|
|
6493
|
+
const sourceLabel = String(safeObject(options).sourceLabel || "runner start").trim() || "runner start";
|
|
6494
|
+
const runnerLogger = createRunnerStartLogger({ routes, flags, sourceLabel, jsonMode });
|
|
6495
|
+
const tui = createRunnerStartTUI({ routes, flags, jsonMode, concurrency: intFromRaw(
|
|
6496
|
+
flags.concurrency ?? process.env.METHEUS_RUNNER_CONCURRENCY,
|
|
6497
|
+
routes.length > 1 ? Math.min(BOT_RUNNER_DEFAULT_CONCURRENCY, routes.length) : 1,
|
|
6498
|
+
), sourceLabel, bootstrapEvent: safeObject(options).bootstrapEvent || null, logFilePath: String(runnerLogger?.filePath || "").trim() });
|
|
5682
6499
|
const schedules = new Map();
|
|
5683
6500
|
const inFlightExecutions = new Map();
|
|
5684
6501
|
const requestedConcurrency = intFromRaw(
|
|
@@ -5695,6 +6512,10 @@ async function runRunnerStartResolvedRoutes(routes, flags) {
|
|
|
5695
6512
|
process.on("SIGTERM", stop);
|
|
5696
6513
|
|
|
5697
6514
|
try {
|
|
6515
|
+
if (!tui && runnerLogger?.filePath && !jsonMode) {
|
|
6516
|
+
process.stdout.write(`Runner log file: ${runnerLogger.filePath}\n`);
|
|
6517
|
+
}
|
|
6518
|
+
tui?.start();
|
|
5698
6519
|
while (!stopRequested) {
|
|
5699
6520
|
const now = Date.now();
|
|
5700
6521
|
const dueRoutes = [];
|
|
@@ -5715,10 +6536,38 @@ async function runRunnerStartResolvedRoutes(routes, flags) {
|
|
|
5715
6536
|
await runTasksWithConcurrencyLimit(dueRouteGroups, concurrency, async (routeGroup) => {
|
|
5716
6537
|
for (const normalizedRoute of ensureArray(routeGroup)) {
|
|
5717
6538
|
const routeKey = runnerRouteKey(normalizedRoute);
|
|
6539
|
+
tui?.setRouteState(routeKey, {
|
|
6540
|
+
phase: "polling",
|
|
6541
|
+
detail: "checking inbound messages and archive thread",
|
|
6542
|
+
});
|
|
6543
|
+
runnerLogger?.append("route_poll", {
|
|
6544
|
+
route_key: routeKey,
|
|
6545
|
+
route_name: normalizedRoute.name,
|
|
6546
|
+
logical_signature: runnerRouteLogicalSignature(normalizedRoute),
|
|
6547
|
+
});
|
|
5718
6548
|
try {
|
|
5719
6549
|
const result = await processRunnerRouteOnce(normalizedRoute, runtime, "start", { deferExecution: true });
|
|
5720
6550
|
const deferredExecution = safeObject(result.deferred_execution);
|
|
5721
6551
|
if (deferredExecution.routeKey) {
|
|
6552
|
+
tui?.reportExecutionStage({
|
|
6553
|
+
routeKey: deferredExecution.routeKey,
|
|
6554
|
+
executionPlan: deferredExecution.executionPlan,
|
|
6555
|
+
selectedRecord: deferredExecution.selectedRecord,
|
|
6556
|
+
phase: "ai_running",
|
|
6557
|
+
detail: `running local AI client for comment ${String(deferredExecution.selectedRecord?.id || "").trim() || "-"}`,
|
|
6558
|
+
});
|
|
6559
|
+
runnerLogger?.append("execution_accepted", {
|
|
6560
|
+
route_key: deferredExecution.routeKey,
|
|
6561
|
+
route_name: deferredExecution.normalizedRoute?.name,
|
|
6562
|
+
comment_id: String(deferredExecution.selectedRecord?.id || "").trim(),
|
|
6563
|
+
source_message_id: intFromRawAllowZero(deferredExecution.selectedRecord?.parsedArchive?.messageID, 0),
|
|
6564
|
+
execution_mode: String(deferredExecution.executionPlan?.mode || "").trim(),
|
|
6565
|
+
role_profile: String(deferredExecution.executionPlan?.roleProfileName || "").trim(),
|
|
6566
|
+
});
|
|
6567
|
+
const executionHeartbeat = startRunnerExecutionHeartbeat(
|
|
6568
|
+
deferredExecution.routeKey,
|
|
6569
|
+
deferredExecution.selectedRecord,
|
|
6570
|
+
);
|
|
5722
6571
|
const executionPromise = (async () => {
|
|
5723
6572
|
try {
|
|
5724
6573
|
const processed = await processRunnerSelectedRecord({
|
|
@@ -5743,6 +6592,24 @@ async function runRunnerStartResolvedRoutes(routes, flags) {
|
|
|
5743
6592
|
buildRunnerDeliveryDeps,
|
|
5744
6593
|
buildRunnerRuntimeDeps,
|
|
5745
6594
|
resolveConversationPeerBots: resolveRunnerConversationPeers,
|
|
6595
|
+
reportRunnerStage: (stage) => {
|
|
6596
|
+
runnerLogger?.append("execution_stage", {
|
|
6597
|
+
route_key: deferredExecution.routeKey,
|
|
6598
|
+
route_name: deferredExecution.normalizedRoute?.name,
|
|
6599
|
+
comment_id: String(deferredExecution.selectedRecord?.id || "").trim(),
|
|
6600
|
+
source_message_id: intFromRawAllowZero(deferredExecution.selectedRecord?.parsedArchive?.messageID, 0),
|
|
6601
|
+
phase: String(safeObject(stage).phase || "").trim(),
|
|
6602
|
+
detail: String(safeObject(stage).detail || "").trim(),
|
|
6603
|
+
intent_type: String(safeObject(stage).intentType || "").trim(),
|
|
6604
|
+
context_suggestion_status: String(safeObject(stage).contextSuggestionStatus || "").trim(),
|
|
6605
|
+
});
|
|
6606
|
+
tui?.reportExecutionStage({
|
|
6607
|
+
routeKey: deferredExecution.routeKey,
|
|
6608
|
+
executionPlan: deferredExecution.executionPlan,
|
|
6609
|
+
selectedRecord: deferredExecution.selectedRecord,
|
|
6610
|
+
...safeObject(stage),
|
|
6611
|
+
});
|
|
6612
|
+
},
|
|
5746
6613
|
},
|
|
5747
6614
|
});
|
|
5748
6615
|
if (processed.kind === "skipped") {
|
|
@@ -5786,20 +6653,68 @@ async function runRunnerStartResolvedRoutes(routes, flags) {
|
|
|
5786
6653
|
comment_id: deferredExecution.selectedRecord.id,
|
|
5787
6654
|
};
|
|
5788
6655
|
} finally {
|
|
6656
|
+
executionHeartbeat.stop();
|
|
5789
6657
|
inFlightExecutions.delete(deferredExecution.routeKey);
|
|
5790
6658
|
schedules.set(deferredExecution.routeKey, Date.now() + 250);
|
|
6659
|
+
tui?.updateNextRunAt(deferredExecution.routeKey, Date.now() + 250);
|
|
5791
6660
|
}
|
|
5792
6661
|
})();
|
|
5793
6662
|
inFlightExecutions.set(routeKey, executionPromise);
|
|
5794
6663
|
executionPromise.then((finalResult) => {
|
|
5795
|
-
|
|
6664
|
+
const latestRouteState = safeObject(loadBotRunnerState().routes[deferredExecution.routeKey]);
|
|
6665
|
+
runnerLogger?.append("execution_result", {
|
|
6666
|
+
route_key: String(finalResult?.route_key || deferredExecution.routeKey).trim(),
|
|
6667
|
+
route_name: String(finalResult?.route_name || deferredExecution.normalizedRoute?.name || "").trim(),
|
|
6668
|
+
outcome: String(finalResult?.outcome || "").trim(),
|
|
6669
|
+
detail: String(finalResult?.detail || "").trim(),
|
|
6670
|
+
comment_id: String(finalResult?.comment_id || deferredExecution.selectedRecord?.id || "").trim(),
|
|
6671
|
+
source_message_id: intFromRawAllowZero(latestRouteState?.last_source_message_id, intFromRawAllowZero(deferredExecution.selectedRecord?.parsedArchive?.messageID, 0)),
|
|
6672
|
+
intent_type: String(latestRouteState?.last_intent_type || "").trim(),
|
|
6673
|
+
context_suggestion_status: String(finalResult?.context_suggestion_status || latestRouteState?.last_context_suggestion_status || "").trim(),
|
|
6674
|
+
});
|
|
6675
|
+
if (tui) {
|
|
6676
|
+
tui.recordResult(finalResult, latestRouteState);
|
|
6677
|
+
} else {
|
|
6678
|
+
printRunnerResult("start", finalResult, jsonMode);
|
|
6679
|
+
}
|
|
5796
6680
|
});
|
|
5797
|
-
|
|
6681
|
+
const acceptedResult = {
|
|
5798
6682
|
...result,
|
|
5799
6683
|
deferred_execution: undefined,
|
|
5800
|
-
}
|
|
6684
|
+
};
|
|
6685
|
+
runnerLogger?.append("route_result", {
|
|
6686
|
+
route_key: String(acceptedResult?.route_key || routeKey).trim(),
|
|
6687
|
+
route_name: String(acceptedResult?.route_name || normalizedRoute.name || "").trim(),
|
|
6688
|
+
outcome: String(acceptedResult?.outcome || "").trim(),
|
|
6689
|
+
detail: String(acceptedResult?.detail || "").trim(),
|
|
6690
|
+
});
|
|
6691
|
+
if (tui) {
|
|
6692
|
+
tui.recordResult(acceptedResult);
|
|
6693
|
+
} else {
|
|
6694
|
+
printRunnerResult("start", acceptedResult, jsonMode);
|
|
6695
|
+
}
|
|
5801
6696
|
} else if (result.outcome !== "busy") {
|
|
5802
|
-
|
|
6697
|
+
runnerLogger?.append("route_result", {
|
|
6698
|
+
route_key: String(result?.route_key || routeKey).trim(),
|
|
6699
|
+
route_name: String(result?.route_name || normalizedRoute.name || "").trim(),
|
|
6700
|
+
outcome: String(result?.outcome || "").trim(),
|
|
6701
|
+
detail: String(result?.detail || "").trim(),
|
|
6702
|
+
comment_id: String(result?.comment_id || "").trim(),
|
|
6703
|
+
});
|
|
6704
|
+
if (tui) {
|
|
6705
|
+
tui.recordResult(result);
|
|
6706
|
+
} else {
|
|
6707
|
+
printRunnerResult("start", result, jsonMode);
|
|
6708
|
+
}
|
|
6709
|
+
} else {
|
|
6710
|
+
runnerLogger?.append("route_result", {
|
|
6711
|
+
route_key: String(result?.route_key || routeKey).trim(),
|
|
6712
|
+
route_name: String(result?.route_name || normalizedRoute.name || "").trim(),
|
|
6713
|
+
outcome: "busy",
|
|
6714
|
+
detail: String(result?.detail || "").trim(),
|
|
6715
|
+
comment_id: String(result?.comment_id || "").trim(),
|
|
6716
|
+
});
|
|
6717
|
+
tui?.recordResult(result);
|
|
5803
6718
|
}
|
|
5804
6719
|
} catch (err) {
|
|
5805
6720
|
const errorText = String(err?.message || err);
|
|
@@ -5816,9 +6731,26 @@ async function runRunnerStartResolvedRoutes(routes, flags) {
|
|
|
5816
6731
|
outcome: fatalArchiveBootstrapError ? "blocked" : "error",
|
|
5817
6732
|
detail: errorText,
|
|
5818
6733
|
};
|
|
5819
|
-
|
|
6734
|
+
runnerLogger?.append("route_result", {
|
|
6735
|
+
route_key: routeKey,
|
|
6736
|
+
route_name: normalizedRoute.name,
|
|
6737
|
+
outcome: String(result.outcome || "error").trim(),
|
|
6738
|
+
detail: errorText,
|
|
6739
|
+
});
|
|
6740
|
+
if (tui) {
|
|
6741
|
+
tui.recordResult(result);
|
|
6742
|
+
} else {
|
|
6743
|
+
printRunnerResult("start", result, jsonMode);
|
|
6744
|
+
}
|
|
5820
6745
|
} finally {
|
|
5821
6746
|
const routeState = safeObject(loadBotRunnerState().routes[routeKey]);
|
|
6747
|
+
const activeExecutionState = resolveRunnerActiveExecutionState(routeState);
|
|
6748
|
+
tui?.setRouteState(routeKey, {
|
|
6749
|
+
intent_type: String(routeState.last_intent_type || "").trim(),
|
|
6750
|
+
source_message_id: intFromRawAllowZero(routeState.last_source_message_id, 0),
|
|
6751
|
+
warning: String(activeExecutionState.warning || "").trim(),
|
|
6752
|
+
last_error: String(routeState.last_error || "").trim(),
|
|
6753
|
+
});
|
|
5822
6754
|
const lastErrorText = String(routeState.last_error || "").trim();
|
|
5823
6755
|
const isFatalArchiveBootstrapError = lastErrorText.includes("Archive thread is missing")
|
|
5824
6756
|
&& lastErrorText.includes("write access is denied");
|
|
@@ -5826,6 +6758,7 @@ async function runRunnerStartResolvedRoutes(routes, flags) {
|
|
|
5826
6758
|
? 60000
|
|
5827
6759
|
: Math.max(1000, normalizedRoute.pollIntervalMs));
|
|
5828
6760
|
schedules.set(routeKey, nextRunAt);
|
|
6761
|
+
tui?.updateNextRunAt(routeKey, nextRunAt);
|
|
5829
6762
|
}
|
|
5830
6763
|
}
|
|
5831
6764
|
});
|
|
@@ -5845,12 +6778,14 @@ async function runRunnerStartResolvedRoutes(routes, flags) {
|
|
|
5845
6778
|
} finally {
|
|
5846
6779
|
process.off("SIGINT", stop);
|
|
5847
6780
|
process.off("SIGTERM", stop);
|
|
6781
|
+
tui?.stop();
|
|
6782
|
+
runnerLogger?.close({ stop_requested: stopRequested === true });
|
|
5848
6783
|
}
|
|
5849
6784
|
}
|
|
5850
6785
|
|
|
5851
6786
|
async function runRunnerStart(flags) {
|
|
5852
6787
|
const routes = resolveRunnerRoutes(flags, "start");
|
|
5853
|
-
await runRunnerStartResolvedRoutes(routes, flags);
|
|
6788
|
+
await runRunnerStartResolvedRoutes(routes, flags, { sourceLabel: "runner start" });
|
|
5854
6789
|
}
|
|
5855
6790
|
|
|
5856
6791
|
async function runRunner(argv) {
|
|
@@ -7493,6 +8428,10 @@ function buildLocalProjectDispatchDeps() {
|
|
|
7493
8428
|
startDetachedRunnerWithFlags: (flags, sourceCommand) => startDetachedRunnerWithFlags(flags, sourceCommand),
|
|
7494
8429
|
buildRunnerDetachedStatusPayload: (flags) => buildRunnerDetachedStatusPayload(flags),
|
|
7495
8430
|
stopDetachedRunnerWithFlags: (flags) => stopDetachedRunnerWithFlags(flags),
|
|
8431
|
+
buildProjectBotRolesPayload: (params) => buildProjectBotRolesPayload(params),
|
|
8432
|
+
buildProjectBotRolesText,
|
|
8433
|
+
locateProjectFilesForQuery: (params) => locateProjectFilesForQuery(params),
|
|
8434
|
+
buildProjectFileLocateText,
|
|
7496
8435
|
loadBotRunnerWorkspaceRegistry: (options) => loadBotRunnerWorkspaceRegistry(options),
|
|
7497
8436
|
botRunnerWorkspaceRegistryFilePath,
|
|
7498
8437
|
sanitizeWorkspaceCandidate,
|
|
@@ -7527,6 +8466,7 @@ function buildProxyToolHelperDeps() {
|
|
|
7527
8466
|
parseToolEnvelopeFromRPCResult: (resultObj) =>
|
|
7528
8467
|
parseToolEnvelopeFromRPCResult(resultObj, buildGatewayTransportDeps()),
|
|
7529
8468
|
ctxpackUpdateToolNames: CTXPACK_UPDATE_TOOL_NAMES,
|
|
8469
|
+
workitemMutationToolNames: WORKITEM_MUTATION_TOOL_NAMES,
|
|
7530
8470
|
localProjectToolNames: LOCAL_PROJECT_TOOL_NAMES,
|
|
7531
8471
|
localCtxpackMergeToolNames: LOCAL_CTXPACK_MERGE_TOOL_NAMES,
|
|
7532
8472
|
autoCtxpackSyncTracker,
|
|
@@ -7552,6 +8492,7 @@ function buildProxyResponsePipelineDeps() {
|
|
|
7552
8492
|
function buildProxyGatewayRequestDeps() {
|
|
7553
8493
|
return {
|
|
7554
8494
|
injectCtxpackUpdateDefaults,
|
|
8495
|
+
injectWorkitemBotActorDefaults,
|
|
7555
8496
|
injectCtxpackPreflightToken,
|
|
7556
8497
|
stripLocalOnlyToolArgs,
|
|
7557
8498
|
postJSON,
|
|
@@ -8027,6 +8968,8 @@ const LOCAL_PROJECT_TOOL_NAMES = [
|
|
|
8027
8968
|
"project.get",
|
|
8028
8969
|
"project.workspace",
|
|
8029
8970
|
"project.workspace.bind",
|
|
8971
|
+
"project.bot_roles",
|
|
8972
|
+
"project.file.locate",
|
|
8030
8973
|
"project.context.list",
|
|
8031
8974
|
"project.context.get",
|
|
8032
8975
|
"project.context.active",
|
|
@@ -8104,6 +9047,56 @@ function buildProjectWorkspaceBindInputSchema() {
|
|
|
8104
9047
|
};
|
|
8105
9048
|
}
|
|
8106
9049
|
|
|
9050
|
+
function buildProjectBotRolesInputSchema() {
|
|
9051
|
+
return {
|
|
9052
|
+
type: "object",
|
|
9053
|
+
properties: {
|
|
9054
|
+
project_id: {
|
|
9055
|
+
type: "string",
|
|
9056
|
+
description: "Project UUID.",
|
|
9057
|
+
},
|
|
9058
|
+
provider: {
|
|
9059
|
+
type: "string",
|
|
9060
|
+
enum: ["telegram", "slack", "kakaotalk"],
|
|
9061
|
+
description: "Optional provider filter.",
|
|
9062
|
+
},
|
|
9063
|
+
destination_id: {
|
|
9064
|
+
type: "string",
|
|
9065
|
+
description: "Optional destination UUID filter.",
|
|
9066
|
+
},
|
|
9067
|
+
destination_label: {
|
|
9068
|
+
type: "string",
|
|
9069
|
+
description: "Optional destination label filter.",
|
|
9070
|
+
},
|
|
9071
|
+
},
|
|
9072
|
+
additionalProperties: false,
|
|
9073
|
+
};
|
|
9074
|
+
}
|
|
9075
|
+
|
|
9076
|
+
function buildProjectFileLocateInputSchema() {
|
|
9077
|
+
return {
|
|
9078
|
+
type: "object",
|
|
9079
|
+
properties: {
|
|
9080
|
+
project_id: {
|
|
9081
|
+
type: "string",
|
|
9082
|
+
description: "Project UUID.",
|
|
9083
|
+
},
|
|
9084
|
+
query: {
|
|
9085
|
+
type: "string",
|
|
9086
|
+
description: "Filename, path fragment, or natural-language file location query.",
|
|
9087
|
+
},
|
|
9088
|
+
limit: {
|
|
9089
|
+
type: "integer",
|
|
9090
|
+
minimum: 1,
|
|
9091
|
+
maximum: 20,
|
|
9092
|
+
description: "Maximum matching file paths to return. Default: 5.",
|
|
9093
|
+
},
|
|
9094
|
+
},
|
|
9095
|
+
required: ["project_id", "query"],
|
|
9096
|
+
additionalProperties: false,
|
|
9097
|
+
};
|
|
9098
|
+
}
|
|
9099
|
+
|
|
8107
9100
|
function buildProjectContextListInputSchema() {
|
|
8108
9101
|
return {
|
|
8109
9102
|
type: "object",
|
|
@@ -8417,6 +9410,18 @@ function buildLocalToolSpecs() {
|
|
|
8417
9410
|
"Bind a project_id to a local workspace_dir in project-workspaces.json so local runner and AI execution use the correct project folder.",
|
|
8418
9411
|
inputSchema: buildProjectWorkspaceBindInputSchema(),
|
|
8419
9412
|
},
|
|
9413
|
+
{
|
|
9414
|
+
name: "project.bot_roles",
|
|
9415
|
+
description:
|
|
9416
|
+
"List the currently enabled local runner bot roles for a project and destination. Use this to answer role/ownership questions from current route config, not memory.",
|
|
9417
|
+
inputSchema: buildProjectBotRolesInputSchema(),
|
|
9418
|
+
},
|
|
9419
|
+
{
|
|
9420
|
+
name: "project.file.locate",
|
|
9421
|
+
description:
|
|
9422
|
+
"Locate project ctxpack file paths that match a filename or natural-language query. Use this for file/path questions.",
|
|
9423
|
+
inputSchema: buildProjectFileLocateInputSchema(),
|
|
9424
|
+
},
|
|
8420
9425
|
{
|
|
8421
9426
|
name: "project.context.list",
|
|
8422
9427
|
description:
|
|
@@ -9116,6 +10121,16 @@ function injectCtxpackUpdateDefaults(requestObj, toolName, workspaceDir) {
|
|
|
9116
10121
|
);
|
|
9117
10122
|
}
|
|
9118
10123
|
|
|
10124
|
+
function injectWorkitemBotActorDefaults(requestObj, toolName) {
|
|
10125
|
+
return injectWorkitemBotActorDefaultsImpl(
|
|
10126
|
+
{
|
|
10127
|
+
requestObj,
|
|
10128
|
+
toolName,
|
|
10129
|
+
},
|
|
10130
|
+
buildProxyToolHelperDeps(),
|
|
10131
|
+
);
|
|
10132
|
+
}
|
|
10133
|
+
|
|
9119
10134
|
async function injectCtxpackPreflightToken(requestObj, toolName, toolArgs, args, token, workspaceDir) {
|
|
9120
10135
|
return injectCtxpackPreflightTokenImpl(
|
|
9121
10136
|
{
|
|
@@ -10135,6 +11150,42 @@ TELEGRAM_BOT_REVIEW_TOKEN=review-token
|
|
|
10135
11150
|
push("local_project_context_suggest_tool_dispatches", false, String(err?.message || err));
|
|
10136
11151
|
}
|
|
10137
11152
|
|
|
11153
|
+
const previousRunnerBotID = process.env.METHEUS_RUNNER_BOT_ID;
|
|
11154
|
+
const previousRunnerBotName = process.env.METHEUS_RUNNER_BOT_NAME;
|
|
11155
|
+
try {
|
|
11156
|
+
process.env.METHEUS_RUNNER_BOT_ID = "bot-selftest-123";
|
|
11157
|
+
process.env.METHEUS_RUNNER_BOT_NAME = "RyoAI_bot";
|
|
11158
|
+
const injected = injectWorkitemBotActorDefaults(
|
|
11159
|
+
{
|
|
11160
|
+
jsonrpc: "2.0",
|
|
11161
|
+
id: 51,
|
|
11162
|
+
method: "tools/call",
|
|
11163
|
+
params: {
|
|
11164
|
+
name: "workitem.transition",
|
|
11165
|
+
arguments: {
|
|
11166
|
+
work_item_id: "w1",
|
|
11167
|
+
status: "doing",
|
|
11168
|
+
},
|
|
11169
|
+
},
|
|
11170
|
+
},
|
|
11171
|
+
"workitem.transition",
|
|
11172
|
+
);
|
|
11173
|
+
const injectedArgs = safeObject(safeObject(injected).params).arguments || {};
|
|
11174
|
+
push(
|
|
11175
|
+
"workitem_bot_actor_defaults_injected",
|
|
11176
|
+
String(injectedArgs.actor_bot_id || "").trim() === "bot-selftest-123"
|
|
11177
|
+
&& String(injectedArgs.actor_bot_name || "").trim() === "RyoAI_bot",
|
|
11178
|
+
`actor_bot_id=${String(injectedArgs.actor_bot_id || "").trim() || "(none)"} actor_bot_name=${String(injectedArgs.actor_bot_name || "").trim() || "(none)"}`,
|
|
11179
|
+
);
|
|
11180
|
+
} catch (err) {
|
|
11181
|
+
push("workitem_bot_actor_defaults_injected", false, String(err?.message || err));
|
|
11182
|
+
} finally {
|
|
11183
|
+
if (previousRunnerBotID === undefined) delete process.env.METHEUS_RUNNER_BOT_ID;
|
|
11184
|
+
else process.env.METHEUS_RUNNER_BOT_ID = previousRunnerBotID;
|
|
11185
|
+
if (previousRunnerBotName === undefined) delete process.env.METHEUS_RUNNER_BOT_NAME;
|
|
11186
|
+
else process.env.METHEUS_RUNNER_BOT_NAME = previousRunnerBotName;
|
|
11187
|
+
}
|
|
11188
|
+
|
|
10138
11189
|
try {
|
|
10139
11190
|
const activeResponse = await handleLocalProjectToolDispatchImpl(
|
|
10140
11191
|
{
|
|
@@ -10268,6 +11319,118 @@ TELEGRAM_BOT_REVIEW_TOKEN=review-token
|
|
|
10268
11319
|
push("local_project_context_activate_tool_dispatches", false, String(err?.message || err));
|
|
10269
11320
|
}
|
|
10270
11321
|
|
|
11322
|
+
try {
|
|
11323
|
+
const botRolesResponse = await handleLocalProjectToolDispatchImpl(
|
|
11324
|
+
{
|
|
11325
|
+
requestObj: {
|
|
11326
|
+
jsonrpc: "2.0",
|
|
11327
|
+
id: 7.1,
|
|
11328
|
+
method: "tools/call",
|
|
11329
|
+
params: {
|
|
11330
|
+
name: "project.bot_roles",
|
|
11331
|
+
arguments: {
|
|
11332
|
+
project_id: selftestProjectID,
|
|
11333
|
+
destination_label: "Selftest Room",
|
|
11334
|
+
},
|
|
11335
|
+
},
|
|
11336
|
+
},
|
|
11337
|
+
toolName: "project.bot_roles",
|
|
11338
|
+
toolArgs: {
|
|
11339
|
+
project_id: selftestProjectID,
|
|
11340
|
+
destination_label: "Selftest Room",
|
|
11341
|
+
},
|
|
11342
|
+
args: {
|
|
11343
|
+
baseURL: DEFAULT_SITE_URL,
|
|
11344
|
+
timeoutSeconds: 30,
|
|
11345
|
+
},
|
|
11346
|
+
token: "selftest-token",
|
|
11347
|
+
workspaceDir: "",
|
|
11348
|
+
workspaceSignalTrusted: true,
|
|
11349
|
+
},
|
|
11350
|
+
{
|
|
11351
|
+
...buildLocalProjectDispatchDeps(),
|
|
11352
|
+
buildProjectBotRolesPayload: () => ({
|
|
11353
|
+
project_id: selftestProjectID,
|
|
11354
|
+
provider: "telegram",
|
|
11355
|
+
destination_label: "Selftest Room",
|
|
11356
|
+
count: 2,
|
|
11357
|
+
routes: [
|
|
11358
|
+
{ route_name: "telegram-monitor-ryoai-bot", bot_name: "RyoAI_bot", role: "monitor" },
|
|
11359
|
+
{ route_name: "telegram-monitor-ryoai2-bot", bot_name: "RyoAI2_bot", role: "review" },
|
|
11360
|
+
],
|
|
11361
|
+
}),
|
|
11362
|
+
buildProjectBotRolesText: () => "RyoAI_bot=monitor, RyoAI2_bot=review",
|
|
11363
|
+
},
|
|
11364
|
+
);
|
|
11365
|
+
const botRolesStructured = safeObject(safeObject(botRolesResponse).result)?.structuredContent || {};
|
|
11366
|
+
push(
|
|
11367
|
+
"local_project_bot_roles_tool_dispatches",
|
|
11368
|
+
String(botRolesStructured.project_id || "").trim() === selftestProjectID
|
|
11369
|
+
&& Number(botRolesStructured.count || 0) === 2
|
|
11370
|
+
&& ensureArray(botRolesStructured.routes).length === 2,
|
|
11371
|
+
`count=${String(botRolesStructured.count || 0)} routes=${ensureArray(botRolesStructured.routes).map((item) => String(safeObject(item).route_name || "")).join(",") || "(none)"}`,
|
|
11372
|
+
);
|
|
11373
|
+
} catch (err) {
|
|
11374
|
+
push("local_project_bot_roles_tool_dispatches", false, String(err?.message || err));
|
|
11375
|
+
}
|
|
11376
|
+
|
|
11377
|
+
try {
|
|
11378
|
+
const fileLocateResponse = await handleLocalProjectToolDispatchImpl(
|
|
11379
|
+
{
|
|
11380
|
+
requestObj: {
|
|
11381
|
+
jsonrpc: "2.0",
|
|
11382
|
+
id: 7.2,
|
|
11383
|
+
method: "tools/call",
|
|
11384
|
+
params: {
|
|
11385
|
+
name: "project.file.locate",
|
|
11386
|
+
arguments: {
|
|
11387
|
+
project_id: selftestProjectID,
|
|
11388
|
+
query: "project-operating-guide",
|
|
11389
|
+
},
|
|
11390
|
+
},
|
|
11391
|
+
},
|
|
11392
|
+
toolName: "project.file.locate",
|
|
11393
|
+
toolArgs: {
|
|
11394
|
+
project_id: selftestProjectID,
|
|
11395
|
+
query: "project-operating-guide",
|
|
11396
|
+
},
|
|
11397
|
+
args: {
|
|
11398
|
+
baseURL: DEFAULT_SITE_URL,
|
|
11399
|
+
timeoutSeconds: 30,
|
|
11400
|
+
},
|
|
11401
|
+
token: "selftest-token",
|
|
11402
|
+
workspaceDir: "",
|
|
11403
|
+
workspaceSignalTrusted: true,
|
|
11404
|
+
},
|
|
11405
|
+
{
|
|
11406
|
+
...buildLocalProjectDispatchDeps(),
|
|
11407
|
+
locateProjectFilesForQuery: async () => ({
|
|
11408
|
+
query: "project-operating-guide",
|
|
11409
|
+
count: 1,
|
|
11410
|
+
matches: [
|
|
11411
|
+
{
|
|
11412
|
+
path: ".metheus/project-guides/project-operating-guide.md",
|
|
11413
|
+
doc_type: "Guide",
|
|
11414
|
+
generated: true,
|
|
11415
|
+
},
|
|
11416
|
+
],
|
|
11417
|
+
}),
|
|
11418
|
+
buildProjectFileLocateText: () => ".metheus/project-guides/project-operating-guide.md",
|
|
11419
|
+
},
|
|
11420
|
+
);
|
|
11421
|
+
const fileLocateStructured = safeObject(safeObject(fileLocateResponse).result)?.structuredContent || {};
|
|
11422
|
+
push(
|
|
11423
|
+
"local_project_file_locate_tool_dispatches",
|
|
11424
|
+
fileLocateStructured.ok === true
|
|
11425
|
+
&& String(fileLocateStructured.project_id || "").trim() === selftestProjectID
|
|
11426
|
+
&& Number(fileLocateStructured.count || 0) === 1
|
|
11427
|
+
&& String(safeObject(ensureArray(fileLocateStructured.matches)[0]).path || "").trim() === ".metheus/project-guides/project-operating-guide.md",
|
|
11428
|
+
`count=${String(fileLocateStructured.count || 0)} path=${String(safeObject(ensureArray(fileLocateStructured.matches)[0]).path || "").trim() || "(none)"}`,
|
|
11429
|
+
);
|
|
11430
|
+
} catch (err) {
|
|
11431
|
+
push("local_project_file_locate_tool_dispatches", false, String(err?.message || err));
|
|
11432
|
+
}
|
|
11433
|
+
|
|
10271
11434
|
let detachedRunnerRegistryTempHome = "";
|
|
10272
11435
|
const previousDetachedRunnerUserProfile = process.env.USERPROFILE;
|
|
10273
11436
|
const previousDetachedRunnerHome = process.env.HOME;
|
|
@@ -10336,6 +11499,41 @@ TELEGRAM_BOT_REVIEW_TOKEN=review-token
|
|
|
10336
11499
|
push("detached_runner_cli_args_strip_control_flags", false, String(err?.message || err));
|
|
10337
11500
|
}
|
|
10338
11501
|
|
|
11502
|
+
try {
|
|
11503
|
+
const defaultLogFilePath = buildDefaultRunnerLogFilePath(
|
|
11504
|
+
[{ name: "telegram-monitor-selftest" }],
|
|
11505
|
+
"runner start",
|
|
11506
|
+
new Date("2026-03-19T07:45:00Z"),
|
|
11507
|
+
);
|
|
11508
|
+
push(
|
|
11509
|
+
"runner_default_log_file_path_uses_runner_logs_directory",
|
|
11510
|
+
defaultLogFilePath.includes(path.join(".metheus", "runner-logs"))
|
|
11511
|
+
&& defaultLogFilePath.endsWith(".jsonl")
|
|
11512
|
+
&& defaultLogFilePath.includes("runner-start")
|
|
11513
|
+
&& defaultLogFilePath.includes("telegram-monitor-selftest"),
|
|
11514
|
+
defaultLogFilePath,
|
|
11515
|
+
);
|
|
11516
|
+
} catch (err) {
|
|
11517
|
+
push("runner_default_log_file_path_uses_runner_logs_directory", false, String(err?.message || err));
|
|
11518
|
+
}
|
|
11519
|
+
|
|
11520
|
+
try {
|
|
11521
|
+
const detachedLaunch = buildRunnerDetachedLaunchRecord(
|
|
11522
|
+
4321,
|
|
11523
|
+
[{ name: "telegram-monitor-selftest", projectID: selftestProjectID, provider: "telegram", destinationLabel: "Selftest Room" }],
|
|
11524
|
+
{},
|
|
11525
|
+
"runner start-detached",
|
|
11526
|
+
"C:\\logs\\runner-selftest.jsonl",
|
|
11527
|
+
);
|
|
11528
|
+
push(
|
|
11529
|
+
"detached_runner_launch_record_persists_log_file",
|
|
11530
|
+
String(detachedLaunch.log_file || "").trim() === "C:\\logs\\runner-selftest.jsonl",
|
|
11531
|
+
`log_file=${String(detachedLaunch.log_file || "").trim() || "(none)"}`,
|
|
11532
|
+
);
|
|
11533
|
+
} catch (err) {
|
|
11534
|
+
push("detached_runner_launch_record_persists_log_file", false, String(err?.message || err));
|
|
11535
|
+
}
|
|
11536
|
+
|
|
10339
11537
|
let detachedRunnerPosixTempDir = "";
|
|
10340
11538
|
try {
|
|
10341
11539
|
detachedRunnerPosixTempDir = fs.mkdtempSync(path.join(os.tmpdir(), "metheus-detached-script-selftest-"));
|
|
@@ -10503,6 +11701,7 @@ TELEGRAM_BOT_REVIEW_TOKEN=review-token
|
|
|
10503
11701
|
resolved_destination: { destination_label: "Selftest Room" },
|
|
10504
11702
|
workspace_mapping: { workspace_dir: "C:\\selftest-workspace", workspace_source: "project-workspaces.json:manual_override" },
|
|
10505
11703
|
execution_profile: { permission_mode: "read_only" },
|
|
11704
|
+
active_execution: { active: true, stuck: true, source_message_id: 128, warning: "active execution is taking longer than expected" },
|
|
10506
11705
|
last_run: { intent_type: "status_query" },
|
|
10507
11706
|
warnings: [],
|
|
10508
11707
|
errors: [],
|
|
@@ -10514,13 +11713,53 @@ TELEGRAM_BOT_REVIEW_TOKEN=review-token
|
|
|
10514
11713
|
"local_runner_show_tool_dispatches",
|
|
10515
11714
|
showStructured.ok === true
|
|
10516
11715
|
&& String(showStructured.route_name || "").trim() === "telegram-monitor-selftest"
|
|
10517
|
-
&& String(safeObject(showStructured.last_run).intent_type || "").trim() === "status_query"
|
|
10518
|
-
|
|
11716
|
+
&& String(safeObject(showStructured.last_run).intent_type || "").trim() === "status_query"
|
|
11717
|
+
&& safeObject(showStructured.active_execution).stuck === true,
|
|
11718
|
+
`route=${String(showStructured.route_name || "").trim() || "(none)"} workspace=${String(safeObject(showStructured.workspace_mapping).workspace_dir || "").trim() || "-"} intent=${String(safeObject(showStructured.last_run).intent_type || "").trim() || "-"} stuck=${String(safeObject(showStructured.active_execution).stuck || false)}`,
|
|
10519
11719
|
);
|
|
10520
11720
|
} catch (err) {
|
|
10521
11721
|
push("local_runner_show_tool_dispatches", false, String(err?.message || err));
|
|
10522
11722
|
}
|
|
10523
11723
|
|
|
11724
|
+
try {
|
|
11725
|
+
const heartbeatFreshState = resolveRunnerActiveExecutionState({
|
|
11726
|
+
active_comment_id: "comment-live",
|
|
11727
|
+
active_source_message_id: 128,
|
|
11728
|
+
active_started_at: new Date(Date.now() - 15_000).toISOString(),
|
|
11729
|
+
active_heartbeat_at: new Date(Date.now() - 2_000).toISOString(),
|
|
11730
|
+
active_runner_pid: process.pid,
|
|
11731
|
+
});
|
|
11732
|
+
push(
|
|
11733
|
+
"runner_active_execution_heartbeat_keeps_fresh_live_execution_active",
|
|
11734
|
+
heartbeatFreshState.active === true
|
|
11735
|
+
&& heartbeatFreshState.stale === false
|
|
11736
|
+
&& heartbeatFreshState.stuck === false
|
|
11737
|
+
&& String(heartbeatFreshState.warning || "").trim() === "",
|
|
11738
|
+
`active=${String(heartbeatFreshState.active)} stale=${String(heartbeatFreshState.stale)} stuck=${String(heartbeatFreshState.stuck)} warning=${String(heartbeatFreshState.warning || "").trim() || "-"}`,
|
|
11739
|
+
);
|
|
11740
|
+
} catch (err) {
|
|
11741
|
+
push("runner_active_execution_heartbeat_keeps_fresh_live_execution_active", false, String(err?.message || err));
|
|
11742
|
+
}
|
|
11743
|
+
|
|
11744
|
+
try {
|
|
11745
|
+
const heartbeatExpiredState = resolveRunnerActiveExecutionState({
|
|
11746
|
+
active_comment_id: "comment-stale",
|
|
11747
|
+
active_source_message_id: 129,
|
|
11748
|
+
active_started_at: new Date(Date.now() - (BOT_RUNNER_ACTIVE_EXECUTION_HEARTBEAT_STALE_MS + 20_000)).toISOString(),
|
|
11749
|
+
active_heartbeat_at: new Date(Date.now() - (BOT_RUNNER_ACTIVE_EXECUTION_HEARTBEAT_STALE_MS + 5_000)).toISOString(),
|
|
11750
|
+
active_runner_pid: process.pid,
|
|
11751
|
+
});
|
|
11752
|
+
push(
|
|
11753
|
+
"runner_active_execution_heartbeat_marks_live_execution_stale_when_expired",
|
|
11754
|
+
heartbeatExpiredState.active === false
|
|
11755
|
+
&& heartbeatExpiredState.stale === true
|
|
11756
|
+
&& String(heartbeatExpiredState.warning || "").trim() === "active execution heartbeat expired; clearing busy state",
|
|
11757
|
+
`active=${String(heartbeatExpiredState.active)} stale=${String(heartbeatExpiredState.stale)} warning=${String(heartbeatExpiredState.warning || "").trim() || "-"}`,
|
|
11758
|
+
);
|
|
11759
|
+
} catch (err) {
|
|
11760
|
+
push("runner_active_execution_heartbeat_marks_live_execution_stale_when_expired", false, String(err?.message || err));
|
|
11761
|
+
}
|
|
11762
|
+
|
|
10524
11763
|
try {
|
|
10525
11764
|
const startResponse = await handleLocalProjectToolDispatchImpl(
|
|
10526
11765
|
{
|
|
@@ -10678,6 +11917,51 @@ TELEGRAM_BOT_REVIEW_TOKEN=review-token
|
|
|
10678
11917
|
push("local_runner_stop_tool_returns_stopped_entries", false, String(err?.message || err));
|
|
10679
11918
|
}
|
|
10680
11919
|
|
|
11920
|
+
try {
|
|
11921
|
+
const frame = buildRunnerTUIFrame({
|
|
11922
|
+
routes: [
|
|
11923
|
+
{
|
|
11924
|
+
route_name: "telegram-monitor-selftest",
|
|
11925
|
+
phase: "ai_running",
|
|
11926
|
+
client: "gpt",
|
|
11927
|
+
model: "gpt-5.4",
|
|
11928
|
+
intent_type: "status_query",
|
|
11929
|
+
source_message_id: 128,
|
|
11930
|
+
next_run_at: Date.now() + 5000,
|
|
11931
|
+
detail: "running local AI execution",
|
|
11932
|
+
warning: "active execution is taking longer than expected",
|
|
11933
|
+
phase_started_at: Date.now() - 15000,
|
|
11934
|
+
},
|
|
11935
|
+
],
|
|
11936
|
+
recentEvents: [
|
|
11937
|
+
{
|
|
11938
|
+
time: "12:34:56",
|
|
11939
|
+
route_name: "telegram-monitor-selftest",
|
|
11940
|
+
outcome: "accepted",
|
|
11941
|
+
detail: "accepted comment 128 for background execution",
|
|
11942
|
+
},
|
|
11943
|
+
],
|
|
11944
|
+
concurrency: 1,
|
|
11945
|
+
now: Date.now(),
|
|
11946
|
+
useColor: false,
|
|
11947
|
+
logFilePath: "C:\\logs\\runner-selftest.jsonl",
|
|
11948
|
+
});
|
|
11949
|
+
push(
|
|
11950
|
+
"runner_tui_frame_renders_route_statuses",
|
|
11951
|
+
frame.includes("METHEUS RUNNER LIVE")
|
|
11952
|
+
&& frame.includes("Log: C:\\logs\\runner-selftest.jsonl")
|
|
11953
|
+
&& frame.includes("Summary: waiting=0 polling=0 running=1 busy=0 replied=0 error=0 warnings=1")
|
|
11954
|
+
&& frame.includes("telegram-monitor-self...")
|
|
11955
|
+
&& frame.includes("AI_RUNNING")
|
|
11956
|
+
&& frame.includes("gpt/gpt-5.4")
|
|
11957
|
+
&& frame.includes("status_query")
|
|
11958
|
+
&& frame.includes("warning=active execution is taking longer than expected"),
|
|
11959
|
+
frame.replace(/\r?\n/g, " | "),
|
|
11960
|
+
);
|
|
11961
|
+
} catch (err) {
|
|
11962
|
+
push("runner_tui_frame_renders_route_statuses", false, String(err?.message || err));
|
|
11963
|
+
}
|
|
11964
|
+
|
|
10681
11965
|
await runSelftestBotCommands(push, {
|
|
10682
11966
|
cliPath: fileURLToPath(import.meta.url),
|
|
10683
11967
|
parseSimpleEnvText,
|