metheus-governance-mcp-cli 0.2.269 → 0.2.271

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.
@@ -212,6 +212,67 @@ function readOutputFile(filePath) {
212
212
  return fs.readFileSync(filePath, "utf8");
213
213
  }
214
214
 
215
+ export function looksLikeCodexCliSessionTranscript(rawText = "") {
216
+ const text = String(rawText || "").trim();
217
+ if (!text) {
218
+ return false;
219
+ }
220
+ return /OpenAI Codex v|session id:|^workdir:|^model:|^provider:|^approval:|^sandbox:|^reasoning effort:|^--------|^user$|^codex$|mcp:.*\b(starting|ready)\b/i.test(text);
221
+ }
222
+
223
+ function summarizeCodexCliFailure({
224
+ status = 0,
225
+ stdoutText = "",
226
+ stderrText = "",
227
+ spawnError = null,
228
+ }) {
229
+ const genericMessage = status
230
+ ? `Codex CLI exited before returning a final reply (status ${status})`
231
+ : "Codex CLI exited before returning a final reply";
232
+ if (spawnError) {
233
+ const spawnMessage = String(spawnError?.message || spawnError || "").trim();
234
+ return spawnMessage
235
+ ? `${genericMessage}: ${spawnMessage}`
236
+ : genericMessage;
237
+ }
238
+ const candidateLines = `${String(stderrText || "")}\n${String(stdoutText || "")}`
239
+ .split(/\r?\n/)
240
+ .map((line) => String(line || "").trim())
241
+ .filter(Boolean)
242
+ .filter((line) => !looksLikeCodexCliSessionTranscript(line))
243
+ .filter((line) => /(error|failed|timeout|timed out|denied|missing|unavailable|not found|capacity)/i.test(line))
244
+ .slice(0, 2);
245
+ return candidateLines.length
246
+ ? `${genericMessage}: ${candidateLines.join(" | ")}`
247
+ : genericMessage;
248
+ }
249
+
250
+ export function resolveCodexRawTextProcessResult({
251
+ outputText = "",
252
+ stdoutText = "",
253
+ stderrText = "",
254
+ status = 0,
255
+ spawnError = null,
256
+ }) {
257
+ const normalizedOutputText = String(outputText || "").trim();
258
+ if (normalizedOutputText && !looksLikeCodexCliSessionTranscript(normalizedOutputText)) {
259
+ return normalizedOutputText;
260
+ }
261
+ const normalizedStdoutText = String(stdoutText || "").trim();
262
+ if (normalizedStdoutText && !looksLikeCodexCliSessionTranscript(normalizedStdoutText)) {
263
+ return normalizedStdoutText;
264
+ }
265
+ if (spawnError || status !== 0) {
266
+ throw new Error(summarizeCodexCliFailure({
267
+ status,
268
+ stdoutText: normalizedStdoutText,
269
+ stderrText,
270
+ spawnError,
271
+ }));
272
+ }
273
+ throw new Error("Codex CLI did not return a usable final reply");
274
+ }
275
+
215
276
  function tryParseEmbeddedJsonObject(text) {
216
277
  const raw = String(text || "").trim();
217
278
  if (!raw) return null;
@@ -686,13 +747,13 @@ function runCodexRawText({ promptText, workspaceDir, model, permissionMode, reas
686
747
  maxBuffer: 8 * 1024 * 1024,
687
748
  },
688
749
  );
689
- if (result.error) {
690
- throw new Error(String(result.error?.message || result.error));
691
- }
692
- if (result.status !== 0) {
693
- throw new Error(String(result.stderr || result.stdout || `codex exited with status ${result.status}`));
694
- }
695
- return readOutputFile(outputPath) || String(result.stdout || "");
750
+ return resolveCodexRawTextProcessResult({
751
+ outputText: readOutputFile(outputPath),
752
+ stdoutText: String(result.stdout || ""),
753
+ stderrText: String(result.stderr || ""),
754
+ status: Number.isInteger(result.status) ? result.status : 0,
755
+ spawnError: result.error || null,
756
+ });
696
757
  } finally {
697
758
  if (fs.existsSync(outputPath)) {
698
759
  fs.rmSync(outputPath, { force: true });
@@ -254,12 +254,24 @@ function normalizePendingSelectionOptions(rawOptions) {
254
254
  };
255
255
  }
256
256
 
257
+ function resolveArchiveRecordEventTime(record) {
258
+ const normalizedRecord = safeObject(record);
259
+ const parsedArchive = safeObject(normalizedRecord.parsedArchive);
260
+ return firstNonEmptyString([
261
+ normalizedRecord.sourceOccurredAt,
262
+ parsedArchive.occurredAt,
263
+ parsedArchive.occurred_at,
264
+ normalizedRecord.createdAt,
265
+ normalizedRecord.updatedAt,
266
+ ]);
267
+ }
268
+
257
269
  function isArchiveRecordWithinPendingAgeLimit(record, rawOptions) {
258
270
  const options = normalizePendingSelectionOptions(rawOptions);
259
271
  if (!(options.maxPendingAgeMs > 0)) {
260
272
  return true;
261
273
  }
262
- const recordTime = Date.parse(firstNonEmptyString([record?.createdAt, record?.updatedAt]));
274
+ const recordTime = Date.parse(resolveArchiveRecordEventTime(record));
263
275
  if (!Number.isFinite(recordTime)) {
264
276
  return true;
265
277
  }
@@ -411,8 +423,8 @@ function contextRelatednessScore(record, selectedRecord, rawOptions) {
411
423
  }
412
424
 
413
425
  export function compareArchiveCommentRecords(left, right) {
414
- const leftTime = firstNonEmptyString([left.createdAt, left.updatedAt]);
415
- const rightTime = firstNonEmptyString([right.createdAt, right.updatedAt]);
426
+ const leftTime = resolveArchiveRecordEventTime(left);
427
+ const rightTime = resolveArchiveRecordEventTime(right);
416
428
  if (leftTime && rightTime && leftTime !== rightTime) {
417
429
  return leftTime < rightTime ? -1 : 1;
418
430
  }
@@ -521,13 +533,18 @@ export function buildRunnerRouteStateFromComment(record, patch = {}) {
521
533
  export function normalizeArchiveCommentRecord(rawComment, parseArchivedChatComment) {
522
534
  const comment = safeObject(rawComment);
523
535
  const body = String(comment.body || "").trim();
536
+ const parsedArchive = typeof parseArchivedChatComment === "function" ? parseArchivedChatComment(body) : null;
524
537
  return {
525
538
  id: String(comment.id || "").trim(),
526
539
  body,
527
540
  createdAt: firstNonEmptyString([comment.created_at, comment.createdAt, comment.updated_at, comment.updatedAt]),
528
541
  updatedAt: firstNonEmptyString([comment.updated_at, comment.updatedAt]),
529
542
  authorUserID: firstNonEmptyString([comment.author_user_id, comment.authorUserId, comment.created_by]),
530
- parsedArchive: typeof parseArchivedChatComment === "function" ? parseArchivedChatComment(body) : null,
543
+ sourceOccurredAt: firstNonEmptyString([
544
+ safeObject(parsedArchive).occurredAt,
545
+ safeObject(parsedArchive).occurred_at,
546
+ ]),
547
+ parsedArchive,
531
548
  };
532
549
  }
533
550
 
@@ -51,6 +51,48 @@ function detectDirectedManagedReplyTarget({
51
51
  return "";
52
52
  }
53
53
 
54
+ const DIRECTED_MANAGED_REPLY_ENGLISH_ACTION_PATTERN = "\\b(?:say|tell|greet|reply|answer|introduce|mention)\\b";
55
+ const DIRECTED_MANAGED_REPLY_KOREAN_ACTION_PATTERN = "(?:\\uC778\\uC0AC|\\uB9D0|\\uC804\\uB2EC|\\uB2F5|\\uC18C\\uAC1C|\\uC5B8\\uAE09)";
56
+ const DIRECTED_MANAGED_REPLY_ACTION_PATTERN = `(?:${DIRECTED_MANAGED_REPLY_ENGLISH_ACTION_PATTERN}|${DIRECTED_MANAGED_REPLY_KOREAN_ACTION_PATTERN})`;
57
+ const DIRECTED_MANAGED_REPLY_POSTPOSITION_PATTERN = "(?:\\uC5D0\\uAC8C|\\uD55C\\uD14C|\\uAED8|\\uBCF4\\uACE0)";
58
+
59
+ function detectDirectedManagedReplyTargetV2({
60
+ text,
61
+ currentBotSelector = "",
62
+ managedMentions = [],
63
+ }) {
64
+ const normalizedText = String(text || "").trim();
65
+ const currentSelector = String(currentBotSelector || "").trim().toLowerCase();
66
+ if (!normalizedText || !currentSelector) {
67
+ return "";
68
+ }
69
+ const instructionPattern = new RegExp(DIRECTED_MANAGED_REPLY_ACTION_PATTERN, "iu");
70
+ if (!instructionPattern.test(normalizedText)) {
71
+ return "";
72
+ }
73
+ const candidates = ensureArray(managedMentions)
74
+ .map((item) => String(item || "").trim().toLowerCase())
75
+ .filter((item) => item && item !== currentSelector);
76
+ for (const selector of candidates) {
77
+ const escapedSelector = escapeRegexText(String(selector || "").replace(/^@+/, ""));
78
+ const explicitTargetPattern = new RegExp(
79
+ `(?:@${escapedSelector}\\s*(?:${DIRECTED_MANAGED_REPLY_POSTPOSITION_PATTERN})?|(?:to|for)\\s+@${escapedSelector}\\b)`,
80
+ "iu",
81
+ );
82
+ if (explicitTargetPattern.test(normalizedText)) {
83
+ return selector;
84
+ }
85
+ const instructionWindowPattern = new RegExp(
86
+ `@${escapedSelector}(?:\\s*(?:${DIRECTED_MANAGED_REPLY_POSTPOSITION_PATTERN}))?(?:[^@\\n]{0,32}?)${DIRECTED_MANAGED_REPLY_ACTION_PATTERN}`,
87
+ "iu",
88
+ );
89
+ if (instructionWindowPattern.test(normalizedText)) {
90
+ return selector;
91
+ }
92
+ }
93
+ return "";
94
+ }
95
+
54
96
  export async function resolveHumanIntentContext({
55
97
  selectedRecord,
56
98
  normalizedRoute,
@@ -118,7 +160,7 @@ export async function resolveHumanIntentContext({
118
160
  runnerHumanIntentPromises.set(cacheKey, promise);
119
161
  }
120
162
  let humanIntent = await runnerHumanIntentPromises.get(cacheKey);
121
- const directedReplyTargetSelector = detectDirectedManagedReplyTarget({
163
+ const directedReplyTargetSelector = detectDirectedManagedReplyTargetV2({
122
164
  text: parsed.body,
123
165
  currentBotSelector,
124
166
  managedMentions,
@@ -85,6 +85,9 @@ import {
85
85
  import {
86
86
  finalizeRunnerRequestRootReferenceRecorderState,
87
87
  } from "./runner-recorder-request-root-reference-finalization-handoff.mjs";
88
+ import {
89
+ resolveCodexRawTextProcessResult,
90
+ } from "./local-ai-adapters.mjs";
88
91
  import {
89
92
  buildRunnerInheritedRootReferenceRecorderResult,
90
93
  buildRunnerRootThreadRecorderFailure,
@@ -737,11 +740,11 @@ export async function runSelftestRunnerScenarios(push, deps) {
737
740
  );
738
741
  }
739
742
 
740
- try {
741
- const pendingSelection = selectPendingArchiveComments(
742
- [
743
- {
744
- id: "dup-comment-1",
743
+ try {
744
+ const pendingSelection = selectPendingArchiveComments(
745
+ [
746
+ {
747
+ id: "dup-comment-1",
745
748
  createdAt: "2026-03-18T00:00:01.000Z",
746
749
  updatedAt: "2026-03-18T00:00:01.000Z",
747
750
  parsedArchive: { kind: "telegram_message", chatID: "-1001", messageID: 353, body: "@RyoAI_bot first copy" },
@@ -767,14 +770,83 @@ export async function runSelftestRunnerScenarios(push, deps) {
767
770
  } catch (err) {
768
771
  push(
769
772
  "runner_pending_selection_ignores_duplicate_archived_inbound_message_ids",
770
- false,
771
- String(err?.message || err),
772
- );
773
- }
774
-
773
+ false,
774
+ String(err?.message || err),
775
+ );
776
+ }
777
+
778
+ try {
779
+ const nowMs = Date.parse("2026-03-31T12:00:00.000Z");
780
+ const pendingSelection = selectPendingArchiveComments(
781
+ [
782
+ {
783
+ id: "cursor-comment",
784
+ createdAt: "2026-03-31T11:55:00.000Z",
785
+ updatedAt: "2026-03-31T11:55:00.000Z",
786
+ sourceOccurredAt: "2026-03-31T09:55:00.000Z",
787
+ parsedArchive: {
788
+ kind: "telegram_message",
789
+ chatID: "-1001",
790
+ messageID: 1145,
791
+ occurredAt: "2026-03-31T09:55:00.000Z",
792
+ body: "@RyoAI_bot 기준 댓글",
793
+ },
794
+ },
795
+ {
796
+ id: "stale-source-comment",
797
+ createdAt: "2026-03-31T11:59:58.000Z",
798
+ updatedAt: "2026-03-31T11:59:58.000Z",
799
+ sourceOccurredAt: "2026-03-31T10:00:00.000Z",
800
+ parsedArchive: {
801
+ kind: "telegram_message",
802
+ chatID: "-1001",
803
+ messageID: 325,
804
+ occurredAt: "2026-03-31T10:00:00.000Z",
805
+ body: "@RyoAI_bot 오래된 하이",
806
+ },
807
+ },
808
+ {
809
+ id: "fresh-source-comment",
810
+ createdAt: "2026-03-31T11:59:59.000Z",
811
+ updatedAt: "2026-03-31T11:59:59.000Z",
812
+ sourceOccurredAt: "2026-03-31T11:57:00.000Z",
813
+ parsedArchive: {
814
+ kind: "telegram_message",
815
+ chatID: "-1001",
816
+ messageID: 1146,
817
+ occurredAt: "2026-03-31T11:57:00.000Z",
818
+ body: "@RyoAI_bot 최신 하이",
819
+ },
820
+ },
821
+ ],
822
+ {
823
+ last_processed_comment_id: "cursor-comment",
824
+ },
825
+ "start",
826
+ (record) => record,
827
+ {
828
+ maxPendingAgeMs: 15 * 60 * 1000,
829
+ nowMs,
830
+ },
831
+ );
832
+ push(
833
+ "runner_pending_selection_uses_source_occurrence_time_for_stale_filtering",
834
+ pendingSelection.pending.length === 1
835
+ && String(pendingSelection.pending[0]?.id || "") === "fresh-source-comment"
836
+ && ensureArray(pendingSelection.staleSkipped).some((record) => String(record?.id || "") === "stale-source-comment"),
837
+ `pending=${pendingSelection.pending.map((item) => item.id).join(",") || "(none)"} stale=${ensureArray(pendingSelection.staleSkipped).map((item) => item.id).join(",") || "(none)"}`,
838
+ );
839
+ } catch (err) {
840
+ push(
841
+ "runner_pending_selection_uses_source_occurrence_time_for_stale_filtering",
842
+ false,
843
+ String(err?.message || err),
844
+ );
845
+ }
846
+
775
847
  try {
776
848
  const duplicateComments = [
777
- {
849
+ {
778
850
  id: "dup-comment-earliest",
779
851
  createdAt: "2026-03-18T00:00:01.000Z",
780
852
  updatedAt: "2026-03-18T00:00:01.000Z",
@@ -3222,15 +3294,76 @@ export async function runSelftestRunnerScenarios(push, deps) {
3222
3294
  && aliasHumanIntentContext?.reusedPersistedContract === true,
3223
3295
  `managedMentions=${JSON.stringify(ensureArray(aliasHumanIntentContext?.managedMentions))} reused=${String(aliasHumanIntentContext?.reusedPersistedContract)}`,
3224
3296
  );
3225
- } catch (err) {
3226
- push(
3227
- "runner_human_intent_context_ignores_raw_alias_selector_for_managed_mentions",
3228
- false,
3229
- String(err?.message || err),
3230
- );
3231
- }
3232
-
3233
- const mentionOverridesReplyForUnmentionedBot = evaluateTelegramRunnerTrigger(
3297
+ } catch (err) {
3298
+ push(
3299
+ "runner_human_intent_context_ignores_raw_alias_selector_for_managed_mentions",
3300
+ false,
3301
+ String(err?.message || err),
3302
+ );
3303
+ }
3304
+
3305
+ try {
3306
+ const replyTargetIntentContext = await resolveHumanIntentContext({
3307
+ selectedRecord: {
3308
+ id: "comment-directed-reply-target-korean",
3309
+ parsedArchive: {
3310
+ kind: "telegram_message",
3311
+ body: "@RyoAI_bot 너가 @SangHoon01_bot 인사 시켜봐",
3312
+ senderIsBot: false,
3313
+ },
3314
+ },
3315
+ normalizedRoute: {
3316
+ name: "telegram-monitor-ryoai-bot-2",
3317
+ },
3318
+ bot: {
3319
+ username: "ryoai_bot",
3320
+ name: "RyoAI_bot",
3321
+ },
3322
+ executionPlan: {},
3323
+ deps: {
3324
+ resolveConversationPeerBots: () => [
3325
+ { id: "bot-self-1", name: "RyoAI_bot" },
3326
+ { id: "bot-peer-1", name: "SangHoon01_bot" },
3327
+ ],
3328
+ },
3329
+ intentDeps: {
3330
+ normalizeMentionSelector: normalizeSelftestMentionSelector,
3331
+ buildConversationPeerMap: (_bot, _route, runtimeDeps) => new Map(
3332
+ ensureArray(runtimeDeps?.resolveConversationPeerBots?.() || []).map((item) => {
3333
+ const selector = normalizeSelftestMentionSelector(item?.name || item?.username);
3334
+ return [selector, item];
3335
+ }).filter(([selector]) => selector),
3336
+ ),
3337
+ extractOrderedMentionSelectors: (text) => Array.from(String(text || "").matchAll(/@([A-Za-z0-9_]+)/g)).map((match) => normalizeSelftestMentionSelector(match[1] || "")),
3338
+ uniqueOrdered: normalizeSelftestConversationSelectorList,
3339
+ buildHumanIntentFromPersistedRunnerRequest: () => null,
3340
+ buildRunnerHumanIntentCacheKey: () => "directed-reply-target-korean",
3341
+ runnerHumanIntentPromises: new Map(),
3342
+ analyzeHumanConversationIntentWithContractResolver: async () => ({
3343
+ intentMode: "single_bot",
3344
+ replyExpectation: "actionable",
3345
+ intentType: "actionable_request",
3346
+ }),
3347
+ scheduleRunnerHumanIntentCacheCleanup: () => {},
3348
+ isCompleteHumanIntentContract: () => true,
3349
+ normalizeHumanIntentType: (value, fallback = "") => String(value || "").trim() || fallback,
3350
+ },
3351
+ });
3352
+ push(
3353
+ "runner_human_intent_context_detects_korean_directed_reply_target",
3354
+ String(replyTargetIntentContext?.humanIntent?.replyTargetBotSelector || "") === "sanghoon01_bot"
3355
+ && JSON.stringify(ensureArray(replyTargetIntentContext?.managedMentions || [])) === JSON.stringify(["ryoai_bot", "sanghoon01_bot"]),
3356
+ `reply_target=${String(replyTargetIntentContext?.humanIntent?.replyTargetBotSelector || "(none)")} managed=${JSON.stringify(ensureArray(replyTargetIntentContext?.managedMentions || []))}`,
3357
+ );
3358
+ } catch (err) {
3359
+ push(
3360
+ "runner_human_intent_context_detects_korean_directed_reply_target",
3361
+ false,
3362
+ String(err?.message || err),
3363
+ );
3364
+ }
3365
+
3366
+ const mentionOverridesReplyForUnmentionedBot = evaluateTelegramRunnerTrigger(
3234
3367
  {
3235
3368
  id: "comment-2b",
3236
3369
  parsedArchive: {
@@ -18701,6 +18834,48 @@ export async function runSelftestRunnerScenarios(push, deps) {
18701
18834
  push("runner_selected_record_terminal_outcome_stays_within_module_4_boundary", false, String(err?.message || err));
18702
18835
  }
18703
18836
 
18837
+ try {
18838
+ const replyText = resolveCodexRawTextProcessResult({
18839
+ outputText: "{\"reply\":\"하이\"}",
18840
+ stdoutText: "",
18841
+ stderrText: "OpenAI Codex v0.114.0\nsession id: test-session\nmcp: metheus ready",
18842
+ status: 1,
18843
+ spawnError: null,
18844
+ });
18845
+ push(
18846
+ "codex_nonzero_exit_uses_final_output_file_when_available",
18847
+ String(replyText || "").trim() === "{\"reply\":\"하이\"}",
18848
+ `reply=${String(replyText || "(none)")}`,
18849
+ );
18850
+ } catch (err) {
18851
+ push("codex_nonzero_exit_uses_final_output_file_when_available", false, String(err?.message || err));
18852
+ }
18853
+
18854
+ try {
18855
+ let errorMessage = "";
18856
+ try {
18857
+ resolveCodexRawTextProcessResult({
18858
+ outputText: "",
18859
+ stdoutText: "",
18860
+ stderrText: "OpenAI Codex v0.114.0\nworkdir: C:\\LUSH KOREA CLI\nsession id: test-session\nmcp: metheus starting\nmcp: metheus ready",
18861
+ status: 1,
18862
+ spawnError: null,
18863
+ });
18864
+ } catch (err) {
18865
+ errorMessage = String(err?.message || err);
18866
+ }
18867
+ push(
18868
+ "codex_failure_summary_does_not_leak_session_transcript",
18869
+ /Codex CLI exited before returning a final reply/i.test(errorMessage)
18870
+ && !/OpenAI Codex v/i.test(errorMessage)
18871
+ && !/session id:/i.test(errorMessage)
18872
+ && !/mcp:/i.test(errorMessage),
18873
+ errorMessage || "(no error message)",
18874
+ );
18875
+ } catch (err) {
18876
+ push("codex_failure_summary_does_not_leak_session_transcript", false, String(err?.message || err));
18877
+ }
18878
+
18704
18879
  try {
18705
18880
  const prepared = await prepareRunnerSelectedRecordContractContext({
18706
18881
  selectedRecord: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metheus-governance-mcp-cli",
3
- "version": "0.2.269",
3
+ "version": "0.2.271",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [