metheus-governance-mcp-cli 0.2.148 → 0.2.152

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 CHANGED
@@ -16,6 +16,7 @@ import {
16
16
  auditDirectHumanReplyWithAI,
17
17
  normalizeExecutionArtifacts,
18
18
  planRoleExecutionWithAI,
19
+ repairRoleExecutionPlanWithAI,
19
20
  resolveLocalAIExecutionModel,
20
21
  resolveGeminiReasoningConfig,
21
22
  suggestLocalAIModelDisplayName,
@@ -129,8 +130,12 @@ import {
129
130
  printRunnerResult,
130
131
  } from "./lib/runner-helpers.mjs";
131
132
  import {
133
+ createProjectEvidence as createProjectEvidenceImpl,
134
+ createProjectWorkItem as createProjectWorkItemImpl,
132
135
  createThreadComment as createThreadCommentImpl,
136
+ createWorkItemThread as createWorkItemThreadImpl,
133
137
  discoverArchiveThreadForDestination as discoverArchiveThreadForDestinationImpl,
138
+ linkWorkItemEvidence as linkWorkItemEvidenceImpl,
134
139
  listProjectChatDestinations as listProjectChatDestinationsImpl,
135
140
  listThreadComments as listThreadCommentsImpl,
136
141
  listUserBotsForRunner as listUserBotsForRunnerImpl,
@@ -1359,7 +1364,6 @@ function loadBotRunnerWorkspaceRegistry(options = {}) {
1359
1364
  }
1360
1365
 
1361
1366
  function saveBotRunnerConfig(nextConfig, filePath = botRunnerConfigFilePath()) {
1362
- saveBotRunnerWorkspaceRegistry(safeObject(nextConfig).projectMappings);
1363
1367
  const payload = serializeBotRunnerConfig(nextConfig);
1364
1368
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
1365
1369
  fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
@@ -1520,6 +1524,9 @@ function loadBotRunnerConfig(options = {}) {
1520
1524
  normalized.projectMappings = mergedProjectMappings;
1521
1525
  normalized.workspaceRegistryFilePath = workspaceRegistry.filePath;
1522
1526
  if (persistIfNeeded && (normalized.migrated || !fs.existsSync(filePath) || registryChanged || legacyProjectMappingsPresent)) {
1527
+ if (registryChanged || legacyProjectMappingsPresent) {
1528
+ saveBotRunnerWorkspaceRegistry(mergedProjectMappings, workspaceRegistry.filePath);
1529
+ }
1523
1530
  saveBotRunnerConfig(normalized, filePath);
1524
1531
  normalized.migrated = false;
1525
1532
  }
@@ -1533,6 +1540,42 @@ function loadBotRunnerConfig(options = {}) {
1533
1540
  }
1534
1541
  }
1535
1542
 
1543
+ function normalizeProjectWorkspaceMappingSource(rawSource) {
1544
+ return String(rawSource || "").trim().toLowerCase();
1545
+ }
1546
+
1547
+ function isAutomaticProjectWorkspaceMappingSource(rawSource) {
1548
+ const source = normalizeProjectWorkspaceMappingSource(rawSource);
1549
+ return [
1550
+ "ctxpack_sync",
1551
+ "ctxpack_sync_current",
1552
+ "ctxpack_sync_write",
1553
+ "project_tool_workspace_signal",
1554
+ ].includes(source);
1555
+ }
1556
+
1557
+ function resolveStableProjectWorkspaceCandidate(projectID, workspaceDir) {
1558
+ const normalizedWorkspaceDir = sanitizeWorkspaceCandidate(workspaceDir);
1559
+ if (!isUUID(projectID)) {
1560
+ return normalizedWorkspaceDir;
1561
+ }
1562
+ const config = loadBotRunnerConfig({ persistIfNeeded: true });
1563
+ const existing = normalizeBotRunnerProjectMapping(projectID, config.projectMappings?.[projectID]);
1564
+ if (!existing.workspaceDir) {
1565
+ return normalizedWorkspaceDir;
1566
+ }
1567
+ if (!normalizedWorkspaceDir) {
1568
+ return existing.workspaceDir;
1569
+ }
1570
+ if (
1571
+ isSameOrChildPath(normalizedWorkspaceDir, existing.workspaceDir)
1572
+ || isSameOrChildPath(existing.workspaceDir, normalizedWorkspaceDir)
1573
+ ) {
1574
+ return existing.workspaceDir;
1575
+ }
1576
+ return existing.workspaceDir;
1577
+ }
1578
+
1536
1579
  function rememberProjectWorkspaceMapping({ projectID, workspaceDir, source }) {
1537
1580
  if (!isUUID(projectID)) {
1538
1581
  return { ok: false, updated: false, reason: "invalid project_id" };
@@ -1546,6 +1589,7 @@ function rememberProjectWorkspaceMapping({ projectID, workspaceDir, source }) {
1546
1589
  }
1547
1590
  const config = loadBotRunnerConfig({ persistIfNeeded: true });
1548
1591
  const existing = normalizeBotRunnerProjectMapping(projectID, config.projectMappings?.[projectID]);
1592
+ const normalizedSource = String(source || existing.source || "ctxpack_sync").trim() || "ctxpack_sync";
1549
1593
  if (existing.workspaceDir && isSameOrChildPath(normalizedWorkspaceDir, existing.workspaceDir)) {
1550
1594
  return {
1551
1595
  ok: true,
@@ -1570,24 +1614,38 @@ function rememberProjectWorkspaceMapping({ projectID, workspaceDir, source }) {
1570
1614
  workspaceDir: normalizedWorkspaceDir,
1571
1615
  };
1572
1616
  }
1573
- const nextConfig = {
1574
- ...config,
1575
- projectMappings: {
1576
- ...safeObject(config.projectMappings),
1577
- [projectID]: {
1578
- projectID,
1579
- workspaceDir: normalizedWorkspaceDir,
1580
- source: String(source || existing.source || "ctxpack_sync").trim() || "ctxpack_sync",
1581
- updatedAt: new Date().toISOString(),
1582
- },
1617
+ if (
1618
+ existing.workspaceDir
1619
+ && !isSameOrChildPath(existing.workspaceDir, normalizedWorkspaceDir)
1620
+ && !isSameOrChildPath(normalizedWorkspaceDir, existing.workspaceDir)
1621
+ && isAutomaticProjectWorkspaceMappingSource(normalizedSource)
1622
+ ) {
1623
+ return {
1624
+ ok: true,
1625
+ updated: false,
1626
+ filePath: config.filePath,
1627
+ workspaceRegistryFilePath: config.workspaceRegistryFilePath,
1628
+ projectID,
1629
+ workspaceDir: existing.workspaceDir,
1630
+ reason: "existing workspace mapping preserved over unrelated automatic signal",
1631
+ };
1632
+ }
1633
+ const nextProjectMappings = {
1634
+ ...safeObject(config.projectMappings),
1635
+ [projectID]: {
1636
+ projectID,
1637
+ workspaceDir: normalizedWorkspaceDir,
1638
+ source: normalizedSource,
1639
+ updatedAt: new Date().toISOString(),
1583
1640
  },
1584
1641
  };
1585
- const filePath = saveBotRunnerConfig(nextConfig, config.filePath);
1642
+ const workspaceRegistryFilePath = config.workspaceRegistryFilePath || botRunnerWorkspaceRegistryFilePath();
1643
+ saveBotRunnerWorkspaceRegistry(nextProjectMappings, workspaceRegistryFilePath);
1586
1644
  return {
1587
1645
  ok: true,
1588
1646
  updated: true,
1589
- filePath,
1590
- workspaceRegistryFilePath: nextConfig.workspaceRegistryFilePath || botRunnerWorkspaceRegistryFilePath(),
1647
+ filePath: config.filePath,
1648
+ workspaceRegistryFilePath,
1591
1649
  projectID,
1592
1650
  workspaceDir: normalizedWorkspaceDir,
1593
1651
  };
@@ -1808,6 +1866,7 @@ function loadBotRunnerState() {
1808
1866
  return {
1809
1867
  filePath,
1810
1868
  routes: {},
1869
+ sharedInboxes: {},
1811
1870
  migrated: false,
1812
1871
  migratedKeys: [],
1813
1872
  remainingAnonymousKeys: [],
@@ -1817,11 +1876,15 @@ function loadBotRunnerState() {
1817
1876
  const runnerConfig = loadBotRunnerConfig({ persistIfNeeded: true });
1818
1877
  const migratedState = migrateBotRunnerStateRoutes(safeObject(parsed?.routes), runnerConfig);
1819
1878
  if (migratedState.changed) {
1820
- saveBotRunnerState({ routes: migratedState.routes });
1879
+ saveBotRunnerState({
1880
+ routes: migratedState.routes,
1881
+ sharedInboxes: safeObject(parsed?.shared_inboxes || parsed?.sharedInboxes),
1882
+ });
1821
1883
  }
1822
1884
  return {
1823
1885
  filePath,
1824
1886
  routes: migratedState.routes,
1887
+ sharedInboxes: safeObject(parsed?.shared_inboxes || parsed?.sharedInboxes),
1825
1888
  migrated: migratedState.changed,
1826
1889
  migratedKeys: migratedState.migratedKeys,
1827
1890
  remainingAnonymousKeys: migratedState.remainingAnonymousKeys,
@@ -1830,6 +1893,7 @@ function loadBotRunnerState() {
1830
1893
  return {
1831
1894
  filePath,
1832
1895
  routes: {},
1896
+ sharedInboxes: {},
1833
1897
  migrated: false,
1834
1898
  migratedKeys: [],
1835
1899
  remainingAnonymousKeys: [],
@@ -1839,10 +1903,15 @@ function loadBotRunnerState() {
1839
1903
 
1840
1904
  function saveBotRunnerState(nextState) {
1841
1905
  const filePath = botRunnerStateFilePath();
1906
+ let current = {};
1907
+ try {
1908
+ current = safeObject(tryJsonParse(fs.readFileSync(filePath, "utf8")));
1909
+ } catch {}
1842
1910
  const payload = {
1843
1911
  version: 1,
1844
1912
  updated_at: new Date().toISOString(),
1845
- routes: safeObject(nextState?.routes),
1913
+ routes: safeObject(nextState?.routes ?? current.routes),
1914
+ shared_inboxes: safeObject(nextState?.sharedInboxes ?? nextState?.shared_inboxes ?? current.shared_inboxes ?? current.sharedInboxes),
1846
1915
  };
1847
1916
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
1848
1917
  fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
@@ -2978,6 +3047,235 @@ async function postJSONWithAuthHeaders(urlText, timeoutSeconds, token, payload,
2978
3047
  });
2979
3048
  }
2980
3049
 
3050
+ async function putJSONWithAuthHeaders(urlText, timeoutSeconds, token, payload, extraHeaders = {}) {
3051
+ return new Promise((resolve, reject) => {
3052
+ const url = new URL(urlText);
3053
+ const body = Buffer.from(JSON.stringify(payload));
3054
+ const transport = url.protocol === "http:" ? http : https;
3055
+ const req = transport.request(
3056
+ {
3057
+ protocol: url.protocol,
3058
+ hostname: url.hostname,
3059
+ port: url.port || (url.protocol === "http:" ? 80 : 443),
3060
+ path: `${url.pathname}${url.search}`,
3061
+ method: "PUT",
3062
+ headers: {
3063
+ "content-type": "application/json",
3064
+ accept: "application/json",
3065
+ authorization: `Bearer ${token}`,
3066
+ "content-length": String(body.length),
3067
+ ...extraHeaders,
3068
+ },
3069
+ timeout: Math.max(3, timeoutSeconds) * 1000,
3070
+ },
3071
+ (res) => {
3072
+ const chunks = [];
3073
+ res.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
3074
+ res.on("end", () => {
3075
+ const text = Buffer.concat(chunks).toString("utf8");
3076
+ const statusCode = Number(res.statusCode || 0);
3077
+ if (statusCode >= 200 && statusCode < 300) {
3078
+ resolve(text.trim());
3079
+ return;
3080
+ }
3081
+ const err = new Error(text.trim() || `http ${statusCode}`);
3082
+ err.statusCode = statusCode;
3083
+ err.responseBody = text;
3084
+ reject(err);
3085
+ });
3086
+ },
3087
+ );
3088
+ req.on("timeout", () => {
3089
+ req.destroy(new Error("http timeout"));
3090
+ });
3091
+ req.on("error", reject);
3092
+ req.write(body);
3093
+ req.end();
3094
+ });
3095
+ }
3096
+
3097
+ function inferCtxpackDocTypeFromPath(relativePath, artifactKind = "", existingDocType = "") {
3098
+ const explicit = String(existingDocType || "").trim().toLowerCase();
3099
+ if (explicit) {
3100
+ return explicit;
3101
+ }
3102
+ const normalizedPath = sanitizeCtxpackRelativePath(relativePath).toLowerCase();
3103
+ const normalizedKind = String(artifactKind || "").trim().toLowerCase();
3104
+ if (!normalizedPath) {
3105
+ return normalizedKind || "guide";
3106
+ }
3107
+ if (normalizedPath === "ctxpack.yaml") return "manifest";
3108
+ if (normalizedPath === "readme.md") return "readme";
3109
+ if (normalizedPath === "agenda.md" || normalizedPath.endsWith("/agenda.md")) return "agenda";
3110
+ if (normalizedPath.startsWith("repo_map/")) return "repo_map";
3111
+ if (normalizedPath.startsWith("evals/")) return "eval_rules";
3112
+ if (normalizedPath.includes("invariant")) return "invariants";
3113
+ if (normalizedPath.startsWith("rules/")) return "rule";
3114
+ if (normalizedPath.includes("architecture")) return "architecture";
3115
+ if (normalizedPath.includes("glossary")) return "glossary";
3116
+ if (normalizedPath.includes("style")) return "style";
3117
+ if (normalizedPath.includes("error") || normalizedPath.includes("logging")) return "errors_logging";
3118
+ if (normalizedPath.includes("playbook") && normalizedPath.includes("debug")) return "playbook_debugging";
3119
+ if (normalizedPath.includes("playbook") && normalizedPath.includes("release")) return "playbook_release";
3120
+ if (normalizedPath.includes("playbook") && normalizedPath.includes("migration")) return "playbook_migrations";
3121
+ if (normalizedKind === "spec") return "architecture";
3122
+ if (normalizedKind === "plan") return "guide";
3123
+ if (normalizedKind === "doc") return "guide";
3124
+ return normalizedPath.endsWith(".yaml") || normalizedPath.endsWith(".yml") ? "manifest" : "guide";
3125
+ }
3126
+
3127
+ function shouldIgnoreCtxpackArtifactPath(relativePath) {
3128
+ const normalizedPath = sanitizeCtxpackRelativePath(relativePath).toLowerCase();
3129
+ if (!normalizedPath) return true;
3130
+ return (
3131
+ normalizedPath === CTXPACK_META_FILENAME.toLowerCase()
3132
+ || normalizedPath.startsWith(".metheus/")
3133
+ || normalizedPath.startsWith(".claude/")
3134
+ || normalizedPath.startsWith(".git/")
3135
+ || normalizedPath.startsWith("node_modules/")
3136
+ );
3137
+ }
3138
+
3139
+ function writeWorkspaceCtxpackMeta({
3140
+ siteBaseURL,
3141
+ projectID,
3142
+ workspaceDir,
3143
+ ctxpackResponse,
3144
+ }) {
3145
+ const resolvedWorkspaceDir = resolveWorkspaceDir(workspaceDir);
3146
+ if (!resolvedWorkspaceDir) {
3147
+ return;
3148
+ }
3149
+ const files = normalizeCtxpackFiles(safeObject(ctxpackResponse).files);
3150
+ const payload = {
3151
+ project_id: projectID,
3152
+ ctxpack_id: String(ctxpackResponse?.ctxpack_id || "").trim(),
3153
+ version_id: String(ctxpackResponse?.version_id || "").trim(),
3154
+ version: String(ctxpackResponse?.version || "").trim(),
3155
+ status: String(ctxpackResponse?.status || "").trim() || "draft",
3156
+ files_count: files.length,
3157
+ files: files.map((item) => ({
3158
+ path: item.path,
3159
+ doc_type: item.docType || "",
3160
+ })),
3161
+ source: `${String(siteBaseURL || "").replace(/\/+$/, "")}/api/v1/projects/${encodeURIComponent(projectID)}/ctxpack`,
3162
+ synced_at: new Date().toISOString(),
3163
+ };
3164
+ fs.writeFileSync(
3165
+ path.join(resolvedWorkspaceDir, CTXPACK_META_FILENAME),
3166
+ `${JSON.stringify(payload, null, 2)}\n`,
3167
+ "utf8",
3168
+ );
3169
+ }
3170
+
3171
+ async function replaceProjectCtxpackFiles({
3172
+ siteBaseURL,
3173
+ token,
3174
+ timeoutSeconds,
3175
+ actorUserID,
3176
+ projectID,
3177
+ workspaceDir,
3178
+ artifacts = [],
3179
+ }) {
3180
+ const resolvedWorkspaceDir = resolveStableProjectWorkspaceCandidate(projectID, workspaceDir) || resolveWorkspaceDir(workspaceDir);
3181
+ if (!resolvedWorkspaceDir) {
3182
+ throw new Error("workspace_dir is required to update project ctxpack");
3183
+ }
3184
+ if (!isUUID(projectID)) {
3185
+ throw new Error("invalid project_id for ctxpack update");
3186
+ }
3187
+ const ctxpackURL = `${siteBaseURL}/api/v1/projects/${encodeURIComponent(projectID)}/ctxpack`;
3188
+ const currentCtxpack = safeObject(await getJSONWithAuthHeaders(
3189
+ ctxpackURL,
3190
+ timeoutSeconds,
3191
+ token,
3192
+ { "X-Actor-User-Id": actorUserID },
3193
+ ));
3194
+ const versionID = firstNonEmptyString([
3195
+ currentCtxpack.version_id,
3196
+ loadWorkspaceMeta(resolvedWorkspaceDir).version_id,
3197
+ loadWorkspaceMeta(resolvedWorkspaceDir).base_version_id,
3198
+ ]);
3199
+ if (!versionID) {
3200
+ throw new Error("ctxpack version_id is missing; run ctxpack pull or refresh project summary first");
3201
+ }
3202
+ const baselineFiles = normalizeCtxpackFiles(currentCtxpack.files);
3203
+ const fileMap = new Map();
3204
+ baselineFiles.forEach((file) => {
3205
+ fileMap.set(file.path, {
3206
+ path: file.path,
3207
+ doc_type: inferCtxpackDocTypeFromPath(file.path, "", file.docType),
3208
+ content: String(file.content || ""),
3209
+ is_generated: false,
3210
+ });
3211
+ });
3212
+ const changedPaths = [];
3213
+ const skippedPaths = [];
3214
+ for (const artifactRaw of ensureArray(artifacts)) {
3215
+ const artifact = safeObject(artifactRaw);
3216
+ const relativePath = sanitizeCtxpackRelativePath(firstNonEmptyString([artifact.relativePath, artifact.path]));
3217
+ if (!relativePath) {
3218
+ continue;
3219
+ }
3220
+ if (shouldIgnoreCtxpackArtifactPath(relativePath)) {
3221
+ skippedPaths.push(relativePath);
3222
+ continue;
3223
+ }
3224
+ const operation = String(artifact.operation || "").trim().toLowerCase() || "update";
3225
+ changedPaths.push(relativePath);
3226
+ if (operation === "delete") {
3227
+ fileMap.delete(relativePath);
3228
+ continue;
3229
+ }
3230
+ const absolutePath = path.join(resolvedWorkspaceDir, relativePath);
3231
+ if (!fs.existsSync(absolutePath)) {
3232
+ throw new Error(`ctxpack source artifact is missing from workspace: ${absolutePath}`);
3233
+ }
3234
+ const content = fs.readFileSync(absolutePath, "utf8");
3235
+ const existingDocType = String(safeObject(fileMap.get(relativePath)).doc_type || "").trim();
3236
+ fileMap.set(relativePath, {
3237
+ path: relativePath,
3238
+ doc_type: inferCtxpackDocTypeFromPath(relativePath, artifact.kind, existingDocType),
3239
+ content,
3240
+ is_generated: true,
3241
+ });
3242
+ }
3243
+ const files = Array.from(fileMap.values())
3244
+ .filter((item) => item.path && !shouldIgnoreCtxpackArtifactPath(item.path))
3245
+ .sort((left, right) => left.path.localeCompare(right.path));
3246
+ if (!files.length) {
3247
+ throw new Error("ctxpack update produced no pushable ctxpack files");
3248
+ }
3249
+ const responseText = await putJSONWithAuthHeaders(
3250
+ ctxpackURL,
3251
+ timeoutSeconds,
3252
+ token,
3253
+ {
3254
+ version_id: versionID,
3255
+ files,
3256
+ },
3257
+ {
3258
+ "X-Actor-User-Id": actorUserID,
3259
+ },
3260
+ );
3261
+ const parsed = safeObject(parseJSONText(responseText));
3262
+ if (!Object.keys(parsed).length) {
3263
+ throw new Error("invalid json response from ctxpack update");
3264
+ }
3265
+ writeWorkspaceCtxpackMeta({
3266
+ siteBaseURL,
3267
+ projectID,
3268
+ workspaceDir: resolvedWorkspaceDir,
3269
+ ctxpackResponse: parsed,
3270
+ });
3271
+ return {
3272
+ ...parsed,
3273
+ changed_paths: uniqueOrdered(changedPaths),
3274
+ skipped_paths: uniqueOrdered(skippedPaths),
3275
+ pushed_file_count: files.length,
3276
+ };
3277
+ }
3278
+
2981
3279
  async function listThreadComments(params) {
2982
3280
  return listThreadCommentsImpl(params, buildRunnerDataDeps());
2983
3281
  }
@@ -2986,6 +3284,22 @@ async function createThreadComment(params) {
2986
3284
  return createThreadCommentImpl(params, buildRunnerDataDeps());
2987
3285
  }
2988
3286
 
3287
+ async function createProjectWorkItem(params) {
3288
+ return createProjectWorkItemImpl(params, buildRunnerDataDeps());
3289
+ }
3290
+
3291
+ async function createProjectEvidence(params) {
3292
+ return createProjectEvidenceImpl(params, buildRunnerDataDeps());
3293
+ }
3294
+
3295
+ async function createWorkItemThread(params) {
3296
+ return createWorkItemThreadImpl(params, buildRunnerDataDeps());
3297
+ }
3298
+
3299
+ async function linkWorkItemEvidence(params) {
3300
+ return linkWorkItemEvidenceImpl(params, buildRunnerDataDeps());
3301
+ }
3302
+
2989
3303
  function buildRunnerDeliveryDeps() {
2990
3304
  return {
2991
3305
  listProjectChatDestinations,
@@ -3005,6 +3319,7 @@ function buildRunnerExecutionDeps() {
3005
3319
  auditRoleExecutionPlanWithAI,
3006
3320
  auditDirectHumanReplyWithAI,
3007
3321
  planRoleExecutionWithAI,
3322
+ repairRoleExecutionPlanWithAI,
3008
3323
  normalizeRunnerRoleProfileName,
3009
3324
  normalizeRunnerRoleProfile,
3010
3325
  normalizeBotRunnerProjectMapping,
@@ -3019,12 +3334,19 @@ function buildRunnerExecutionDeps() {
3019
3334
  resolveRunnerExecutionPlanForRole,
3020
3335
  validateWorkspaceArtifacts,
3021
3336
  tryJsonParse,
3337
+ createProjectEvidence,
3338
+ createProjectWorkItem,
3339
+ createWorkItemThread,
3340
+ linkWorkItemEvidence,
3341
+ replaceProjectCtxpackFiles,
3022
3342
  };
3023
3343
  }
3024
3344
 
3025
3345
  function buildRunnerRuntimeDeps() {
3026
3346
  return {
3027
3347
  loadProviderEnvConfig,
3348
+ loadBotRunnerState,
3349
+ saveBotRunnerState,
3028
3350
  getTelegramWebhookInfo,
3029
3351
  deleteTelegramWebhook,
3030
3352
  saveRunnerRouteState,
@@ -7015,7 +7337,7 @@ function syncCtxpackToLocalCache({
7015
7337
  const versionID = String(ctxpack?.version_id || ctxpack?.current_version_id || "").trim();
7016
7338
  const status = String(ctxpack?.status || "").trim() || "draft";
7017
7339
  const files = normalizeCtxpackFiles(ctxpack?.files);
7018
- const resolvedWorkspaceDir = resolveWorkspaceDir(workspaceDir);
7340
+ const resolvedWorkspaceDir = resolveStableProjectWorkspaceCandidate(projectID, workspaceDir) || resolveWorkspaceDir(workspaceDir);
7019
7341
  const isHomeFallback = isHomeWorkspaceRoot(resolvedWorkspaceDir) && !allowHomeWorkspaceRoot();
7020
7342
  const cacheDir = path.join(
7021
7343
  isHomeFallback ? homeCtxpackCacheRootDir() : ctxpackCacheRootDir(resolvedWorkspaceDir),
@@ -7070,13 +7392,6 @@ function syncCtxpackToLocalCache({
7070
7392
  } catch {
7071
7393
  // Best-effort metadata refresh for workspace-root auto-detection.
7072
7394
  }
7073
- if (!isHomeFallback) {
7074
- rememberProjectWorkspaceMapping({
7075
- projectID,
7076
- workspaceDir: resolvedWorkspaceDir,
7077
- source: "ctxpack_sync_current",
7078
- });
7079
- }
7080
7395
  return {
7081
7396
  sync_status: isHomeFallback ? "home_fallback" : "current",
7082
7397
  sync_message: isHomeFallback
@@ -7100,14 +7415,6 @@ function syncCtxpackToLocalCache({
7100
7415
 
7101
7416
  fs.writeFileSync(metaPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
7102
7417
  fs.writeFileSync(workspaceMetaPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
7103
- if (!isHomeFallback) {
7104
- rememberProjectWorkspaceMapping({
7105
- projectID,
7106
- workspaceDir: resolvedWorkspaceDir,
7107
- source: "ctxpack_sync_write",
7108
- });
7109
- }
7110
-
7111
7418
  return {
7112
7419
  sync_status: isHomeFallback ? "home_fallback" : (previousMeta ? "updated" : "downloaded"),
7113
7420
  sync_message: isHomeFallback
@@ -8083,6 +8390,8 @@ TELEGRAM_BOT_REVIEW_TOKEN=review-token
8083
8390
  performLocalBotDelivery,
8084
8391
  buildRunnerDeliveryDeps,
8085
8392
  parseJSONText,
8393
+ archiveLocalTelegramMessagesForRoute,
8394
+ buildRunnerRuntimeDeps,
8086
8395
  });
8087
8396
 
8088
8397
  await runSelftestWorkspaceEnvScenarios(push, {
@@ -8193,14 +8502,14 @@ TELEGRAM_BOT_REVIEW_TOKEN=review-token
8193
8502
  safeObject(runnerConfigAfterProjectTool.projectMappings)?.[selftestProjectID]?.workspaceDir || "";
8194
8503
  const projectToolWorkspaceSignalOk =
8195
8504
  String(safeObject(response).error || "").trim() === ""
8196
- && normalizedPathForCompare(storedProjectToolWorkspace) === normalizedPathForCompare(projectToolWorkspaceDir);
8505
+ && !String(storedProjectToolWorkspace || "").trim();
8197
8506
  push(
8198
- "project_summary_workspace_signal_updates_runner_mapping",
8507
+ "project_summary_workspace_signal_does_not_update_runner_mapping",
8199
8508
  projectToolWorkspaceSignalOk,
8200
8509
  `workspace=${storedProjectToolWorkspace || "(none)"} source=${String(safeObject(runnerConfigAfterProjectTool.projectMappings)?.[selftestProjectID]?.source || "(none)")}`,
8201
8510
  );
8202
8511
  } catch (err) {
8203
- push("project_summary_workspace_signal_updates_runner_mapping", false, String(err?.message || err));
8512
+ push("project_summary_workspace_signal_does_not_update_runner_mapping", false, String(err?.message || err));
8204
8513
  } finally {
8205
8514
  if (previousProjectToolUserProfile === undefined) delete process.env.USERPROFILE;
8206
8515
  else process.env.USERPROFILE = previousProjectToolUserProfile;
@@ -8215,6 +8524,81 @@ TELEGRAM_BOT_REVIEW_TOKEN=review-token
8215
8524
  }
8216
8525
  }
8217
8526
 
8527
+ let projectToolWorkspaceDriftTempHome = "";
8528
+ const previousProjectToolDriftUserProfile = process.env.USERPROFILE;
8529
+ const previousProjectToolDriftHome = process.env.HOME;
8530
+ try {
8531
+ projectToolWorkspaceDriftTempHome = fs.mkdtempSync(path.join(os.tmpdir(), "metheus-project-tool-drift-selftest-"));
8532
+ const stableWorkspaceDir = path.join(projectToolWorkspaceDriftTempHome, "stable-workspace");
8533
+ const unrelatedWorkspaceDir = path.join(projectToolWorkspaceDriftTempHome, "unrelated-workspace");
8534
+ fs.mkdirSync(stableWorkspaceDir, { recursive: true });
8535
+ fs.mkdirSync(unrelatedWorkspaceDir, { recursive: true });
8536
+ process.env.USERPROFILE = projectToolWorkspaceDriftTempHome;
8537
+ process.env.HOME = projectToolWorkspaceDriftTempHome;
8538
+ rememberProjectWorkspaceMapping({
8539
+ projectID: selftestProjectID,
8540
+ workspaceDir: stableWorkspaceDir,
8541
+ source: "manual_override",
8542
+ });
8543
+ await handleLocalProjectToolDispatchImpl(
8544
+ {
8545
+ requestObj: {
8546
+ jsonrpc: "2.0",
8547
+ id: 2,
8548
+ method: "tools/call",
8549
+ params: {
8550
+ name: "project.summary",
8551
+ arguments: {
8552
+ project_id: selftestProjectID,
8553
+ },
8554
+ },
8555
+ },
8556
+ toolName: "project.summary",
8557
+ toolArgs: {
8558
+ project_id: selftestProjectID,
8559
+ },
8560
+ args: {
8561
+ baseURL: DEFAULT_SITE_URL,
8562
+ timeoutSeconds: 30,
8563
+ },
8564
+ token: "selftest-token",
8565
+ workspaceDir: unrelatedWorkspaceDir,
8566
+ workspaceSignalTrusted: true,
8567
+ },
8568
+ {
8569
+ ...buildLocalProjectDispatchDeps(),
8570
+ loadProjectSummaryForTool: async () => ({
8571
+ project_id: selftestProjectID,
8572
+ name: "Selftest Project",
8573
+ access: "granted",
8574
+ }),
8575
+ buildProjectSummaryText: (summary) => `Project: ${String(summary?.name || "").trim()}`,
8576
+ },
8577
+ );
8578
+ const runnerConfigAfterProjectToolDrift = loadBotRunnerConfig({ persistIfNeeded: true });
8579
+ const storedWorkspaceAfterDrift =
8580
+ safeObject(runnerConfigAfterProjectToolDrift.projectMappings)?.[selftestProjectID]?.workspaceDir || "";
8581
+ push(
8582
+ "project_summary_workspace_signal_preserves_existing_manual_mapping",
8583
+ normalizedPathForCompare(storedWorkspaceAfterDrift) === normalizedPathForCompare(stableWorkspaceDir),
8584
+ `stored=${storedWorkspaceAfterDrift || "(none)"} stable=${stableWorkspaceDir}`,
8585
+ );
8586
+ } catch (err) {
8587
+ push("project_summary_workspace_signal_preserves_existing_manual_mapping", false, String(err?.message || err));
8588
+ } finally {
8589
+ if (previousProjectToolDriftUserProfile === undefined) delete process.env.USERPROFILE;
8590
+ else process.env.USERPROFILE = previousProjectToolDriftUserProfile;
8591
+ if (previousProjectToolDriftHome === undefined) delete process.env.HOME;
8592
+ else process.env.HOME = previousProjectToolDriftHome;
8593
+ if (projectToolWorkspaceDriftTempHome) {
8594
+ try {
8595
+ fs.rmSync(projectToolWorkspaceDriftTempHome, { recursive: true, force: true });
8596
+ } catch {
8597
+ // ignore selftest cleanup error
8598
+ }
8599
+ }
8600
+ }
8601
+
8218
8602
  await runSelftestBotCommands(push, {
8219
8603
  cliPath: fileURLToPath(import.meta.url),
8220
8604
  parseSimpleEnvText,