@gethmy/mcp 2.23.0 → 2.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -20,7 +20,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
20
20
  // src/config.ts
21
21
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
22
22
  import { homedir } from "node:os";
23
- import { join } from "node:path";
23
+ import { dirname, join, parse, resolve } from "node:path";
24
24
  function getConfigDir() {
25
25
  return join(homedir(), ".harmony-mcp");
26
26
  }
@@ -30,6 +30,22 @@ function getConfigPath() {
30
30
  function getLocalConfigPath(cwd) {
31
31
  return join(cwd || process.cwd(), LOCAL_CONFIG_FILENAME);
32
32
  }
33
+ function findLocalConfigPath(cwd) {
34
+ const home = resolve(homedir());
35
+ let dir = resolve(cwd || process.cwd());
36
+ const { root } = parse(dir);
37
+ for (;; ) {
38
+ if (dir !== home && dir !== root) {
39
+ const candidate = join(dir, LOCAL_CONFIG_FILENAME);
40
+ if (existsSync(candidate))
41
+ return candidate;
42
+ }
43
+ const parent = dirname(dir);
44
+ if (parent === dir)
45
+ return null;
46
+ dir = parent;
47
+ }
48
+ }
33
49
  function emptyConfig() {
34
50
  return {
35
51
  apiKey: null,
@@ -81,8 +97,8 @@ function saveConfig(config) {
81
97
  });
82
98
  }
83
99
  function loadLocalConfig(cwd) {
84
- const localConfigPath = getLocalConfigPath(cwd);
85
- if (!existsSync(localConfigPath)) {
100
+ const localConfigPath = findLocalConfigPath(cwd);
101
+ if (localConfigPath === null) {
86
102
  return null;
87
103
  }
88
104
  try {
@@ -97,7 +113,7 @@ function loadLocalConfig(cwd) {
97
113
  }
98
114
  }
99
115
  function saveLocalConfig(config, cwd) {
100
- const localConfigPath = getLocalConfigPath(cwd);
116
+ const localConfigPath = findLocalConfigPath(cwd) ?? getLocalConfigPath(cwd);
101
117
  const existingConfig = loadLocalConfig(cwd) || {
102
118
  workspaceId: null,
103
119
  projectId: null
@@ -111,7 +127,7 @@ function saveLocalConfig(config, cwd) {
111
127
  writeFileSync(localConfigPath, JSON.stringify(cleanConfig, null, 2));
112
128
  }
113
129
  function hasLocalConfig(cwd) {
114
- return existsSync(getLocalConfigPath(cwd));
130
+ return findLocalConfigPath(cwd) !== null;
115
131
  }
116
132
  function getActiveCredential() {
117
133
  const config = loadConfig();
@@ -133,33 +149,69 @@ function getUserEmail() {
133
149
  const config = loadConfig();
134
150
  return config.userEmail;
135
151
  }
136
- function setActiveWorkspace(workspaceId, options) {
137
- if (options?.local) {
138
- saveLocalConfig({ workspaceId }, options.cwd);
139
- } else {
140
- saveConfig({ activeWorkspaceId: workspaceId });
152
+ function setActiveContext(context, options) {
153
+ if (options?.global) {
154
+ saveConfig({
155
+ activeWorkspaceId: context.workspaceId,
156
+ activeProjectId: context.projectId
157
+ });
158
+ return;
141
159
  }
142
- }
143
- function setActiveProject(projectId, options) {
144
- if (options?.local) {
145
- saveLocalConfig({ projectId }, options.cwd);
160
+ const localPath = findLocalConfigPath(options?.cwd);
161
+ if (options?.local || localPath !== null) {
162
+ saveLocalConfig({ workspaceId: context.workspaceId, projectId: context.projectId }, options?.cwd);
146
163
  } else {
147
- saveConfig({ activeProjectId: projectId });
164
+ saveConfig({
165
+ activeWorkspaceId: context.workspaceId,
166
+ activeProjectId: context.projectId
167
+ });
148
168
  }
149
169
  }
150
- function getActiveWorkspaceId(cwd) {
170
+ function setActiveWorkspace(workspaceId, options) {
171
+ const currentWorkspaceId = getActiveWorkspaceId(options?.cwd);
172
+ const keepProject = currentWorkspaceId === workspaceId;
173
+ setActiveContext({
174
+ workspaceId,
175
+ projectId: keepProject ? getActiveProjectId(options?.cwd) : null
176
+ }, options);
177
+ }
178
+ function readActiveContext(cwd) {
151
179
  const localConfig = loadLocalConfig(cwd);
152
- if (localConfig?.workspaceId) {
153
- return localConfig.workspaceId;
180
+ if (localConfig) {
181
+ return {
182
+ workspaceId: localConfig.workspaceId ?? null,
183
+ projectId: localConfig.projectId ?? null
184
+ };
154
185
  }
155
- return loadConfig().activeWorkspaceId;
186
+ const globalConfig = loadConfig();
187
+ return {
188
+ workspaceId: globalConfig.activeWorkspaceId,
189
+ projectId: globalConfig.activeProjectId
190
+ };
191
+ }
192
+ function getActiveWorkspaceId(cwd) {
193
+ return readActiveContext(cwd).workspaceId;
156
194
  }
157
195
  function getActiveProjectId(cwd) {
158
- const localConfig = loadLocalConfig(cwd);
159
- if (localConfig?.projectId) {
160
- return localConfig.projectId;
196
+ return readActiveContext(cwd).projectId;
197
+ }
198
+ function getActiveContext(cwd) {
199
+ return describeActiveContext({
200
+ projectId: getActiveProjectId(cwd),
201
+ workspaceId: getActiveWorkspaceId(cwd)
202
+ });
203
+ }
204
+ function describeActiveContext(context) {
205
+ const { projectId, workspaceId } = context;
206
+ if (projectId && !workspaceId) {
207
+ return {
208
+ projectId,
209
+ workspaceId,
210
+ consistent: false,
211
+ note: `An active project (${projectId}) is set with no active workspace, so ` + "workspace-scoped tools cannot resolve one from it. Re-set it with " + "harmony_set_project_context, or pass workspaceId explicitly."
212
+ };
161
213
  }
162
- return loadConfig().activeProjectId;
214
+ return { projectId, workspaceId, consistent: true, note: null };
163
215
  }
164
216
  function isConfigured() {
165
217
  const config = loadConfig();
@@ -302,15 +354,7 @@ ${lines.join(`
302
354
  `)}`;
303
355
  }
304
356
  function generatePrompt(options) {
305
- const {
306
- card,
307
- column,
308
- variant,
309
- customConstraints,
310
- memories,
311
- assembledContext,
312
- assemblyId
313
- } = options;
357
+ const { card, column, variant, customConstraints, memories, assemblyId } = options;
314
358
  const contextOpts = {
315
359
  includeTitle: true,
316
360
  includeDescription: true,
@@ -388,10 +432,7 @@ ${card.description}`);
388
432
  roleFraming.outputSuggestions.forEach((s) => {
389
433
  sections.push(`- ${s}`);
390
434
  });
391
- if (assembledContext) {
392
- sections.push(`
393
- ${assembledContext}`);
394
- } else if (memories && memories.length > 0) {
435
+ if (memories && memories.length > 0) {
395
436
  sections.push(`
396
437
  ## Relevant Memories`);
397
438
  sections.push(`*${memories.length} memories recalled from knowledge graph:*`);
@@ -402,7 +443,7 @@ ${assembledContext}`);
402
443
  sections.push(memory.content);
403
444
  }
404
445
  }
405
- const oneThingLine = synthesizeOneThing(card, subtasks, links, assembledContext);
446
+ const oneThingLine = synthesizeOneThing(card, subtasks, links);
406
447
  if (oneThingLine) {
407
448
  sections.push(`
408
449
  ## Recommended Next Step
@@ -429,7 +470,7 @@ ${customConstraints}`);
429
470
  *Card #${card.short_id} | Generated for ${variant} mode*`);
430
471
  const prompt = sections.join(`
431
472
  `);
432
- const memoryCount = assembledContext ? (assembledContext.match(/^### /gm) || []).length : memories?.length || 0;
473
+ const memoryCount = memories?.length ?? 0;
433
474
  return {
434
475
  prompt,
435
476
  variant,
@@ -450,40 +491,7 @@ ${customConstraints}`);
450
491
  version: PROMPT_TEMPLATE_VERSION
451
492
  };
452
493
  }
453
- function extractSessionInsights(assembledContext) {
454
- const result = {
455
- lastSessionStatus: null,
456
- lastSessionTask: null,
457
- lastSessionProgress: null,
458
- blockers: [],
459
- procedureNextStep: null
460
- };
461
- const sessionMatches = assembledContext.match(/### Session:.*?\n([\s\S]*?)(?=\n###|\n## |\n---|\n\*Assembly|$)/g);
462
- if (sessionMatches && sessionMatches.length > 0) {
463
- const latest = sessionMatches[0];
464
- if (/Completed work on/i.test(latest)) {
465
- result.lastSessionStatus = "completed";
466
- } else if (/Paused work on|status:\s*paused/i.test(latest)) {
467
- result.lastSessionStatus = "paused";
468
- }
469
- const taskMatch = latest.match(/Final task:\s*(.+)/);
470
- if (taskMatch)
471
- result.lastSessionTask = taskMatch[1].trim();
472
- const progressMatch = latest.match(/Progress:\s*(\d+)%/);
473
- if (progressMatch)
474
- result.lastSessionProgress = parseInt(progressMatch[1], 10);
475
- }
476
- const blockerMatches = assembledContext.match(/(?:blocker|blocked by|blocking):\s*(.+)/gi);
477
- if (blockerMatches) {
478
- result.blockers = blockerMatches.map((m) => m.replace(/(?:blocker|blocked by|blocking):\s*/i, "").trim());
479
- }
480
- const stepMatches = assembledContext.match(/^\d+\.\s+(?!.*\*\*\[key step\]\*\*.*✓)(.+?)(?:\s*\*\*\[key step\]\*\*)?$/gm);
481
- if (stepMatches && stepMatches.length > 0) {
482
- result.procedureNextStep = stepMatches[0].replace(/^\d+\.\s+/, "").replace(/\s*\*\*\[key step\]\*\*.*$/, "").trim();
483
- }
484
- return result;
485
- }
486
- function synthesizeOneThing(card, subtasks, links, assembledContext) {
494
+ function synthesizeOneThing(card, subtasks, links) {
487
495
  if (card.done)
488
496
  return null;
489
497
  const blockers = links.filter((l) => l.display_type === "is_blocked_by" && l.direction === "incoming");
@@ -491,14 +499,6 @@ function synthesizeOneThing(card, subtasks, links, assembledContext) {
491
499
  const blocker = blockers[0];
492
500
  return `Unblock first: resolve #${blocker.target_card.short_id} "${blocker.target_card.title}" which is blocking this card.`;
493
501
  }
494
- const session = assembledContext ? extractSessionInsights(assembledContext) : null;
495
- if (session?.blockers && session.blockers.length > 0) {
496
- return `Resolve blocker: ${session.blockers[0]}`;
497
- }
498
- if (session?.lastSessionStatus === "paused" && session.lastSessionTask) {
499
- const progress = session.lastSessionProgress ? ` (was ${session.lastSessionProgress}% complete)` : "";
500
- return `Resume previous session${progress}: "${session.lastSessionTask}".`;
501
- }
502
502
  if (subtasks.length > 0) {
503
503
  const completed = subtasks.filter((s) => s.completed).length;
504
504
  if (completed === subtasks.length) {
@@ -509,12 +509,6 @@ function synthesizeOneThing(card, subtasks, links, assembledContext) {
509
509
  return `Work on next subtask: "${nextSubtask.title}" (${completed}/${subtasks.length} done).`;
510
510
  }
511
511
  }
512
- if (session?.procedureNextStep) {
513
- return `Follow procedure: ${session.procedureNextStep}`;
514
- }
515
- if (session?.lastSessionStatus === "completed" && session.lastSessionTask) {
516
- return `Previous session completed ("${session.lastSessionTask}"). Review results and continue with remaining work.`;
517
- }
518
512
  if (card.due_date && (card.priority === "urgent" || card.priority === "high")) {
519
513
  return `High-priority task with deadline ${card.due_date}. Start implementation immediately.`;
520
514
  }
@@ -878,7 +872,7 @@ function lockPath() {
878
872
  return join3(getConfigDir(), LOCK_FILENAME);
879
873
  }
880
874
  function sleep(ms) {
881
- return new Promise((resolve) => setTimeout(resolve, ms));
875
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
882
876
  }
883
877
  async function withRefreshLock(fn) {
884
878
  const path = lockPath();
@@ -1386,6 +1380,9 @@ import {
1386
1380
  ReadResourceRequestSchema
1387
1381
  } from "@modelcontextprotocol/sdk/types.js";
1388
1382
  import { z } from "zod";
1383
+
1384
+ // src/api-client.ts
1385
+ import { randomUUID as randomUUID2 } from "node:crypto";
1389
1386
  // ../harmony-shared/dist/agentStaleness.js
1390
1387
  var AGENT_HEARTBEAT_LIVENESS_MS = 5 * 60 * 1000;
1391
1388
  var AGENT_MILESTONE_LIVENESS_MS = 30 * 60 * 1000;
@@ -1507,12 +1504,97 @@ var TIMINGS = {
1507
1504
  QUERY_STALE_TIME: 1000 * 60 * 5,
1508
1505
  QUERY_GC_TIME: 1000 * 60 * 60 * 24
1509
1506
  };
1507
+ // ../harmony-shared/dist/declaredGateMetrics.js
1508
+ function declaredGateMetricsFromAgents(agents) {
1509
+ const names = new Set;
1510
+ let known = false;
1511
+ for (const agent of agents) {
1512
+ const declared = agent.declared_gate_metrics;
1513
+ if (!Array.isArray(declared))
1514
+ continue;
1515
+ known = true;
1516
+ for (const name of declared) {
1517
+ if (typeof name === "string" && name.trim())
1518
+ names.add(name.trim());
1519
+ }
1520
+ }
1521
+ return { names, known };
1522
+ }
1523
+ // ../harmony-shared/dist/gateConfigError.js
1524
+ var GATE_CONFIG_ERROR_KEY = "configError";
1525
+ var GATE_CONFIG_ERROR_MARK = Object.freeze({ [GATE_CONFIG_ERROR_KEY]: true });
1510
1526
  // ../harmony-shared/dist/playbookStage.js
1527
+ var DEFAULT_LOOP_MAX_ITERATIONS = 5;
1528
+ function normalizeLoopDef(raw) {
1529
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw))
1530
+ return null;
1531
+ const obj = raw;
1532
+ if (obj.mode !== "converge" && obj.mode !== "fanout")
1533
+ return null;
1534
+ const mode = obj.mode;
1535
+ const rawMax = obj.max_iterations;
1536
+ const maxInt = typeof rawMax === "number" && Number.isFinite(rawMax) && rawMax >= 1 ? Math.floor(rawMax) : DEFAULT_LOOP_MAX_ITERATIONS;
1537
+ const exitGate = obj.exit_gate && typeof obj.exit_gate === "object" && !Array.isArray(obj.exit_gate) ? obj.exit_gate : null;
1538
+ const def = { mode, max_iterations: maxInt };
1539
+ if (exitGate)
1540
+ def.exit_gate = exitGate;
1541
+ if (obj.item_source && typeof obj.item_source === "object" && !Array.isArray(obj.item_source)) {
1542
+ def.item_source = obj.item_source;
1543
+ }
1544
+ if (typeof obj.concurrency === "number" && obj.concurrency >= 1) {
1545
+ def.concurrency = Math.floor(obj.concurrency);
1546
+ }
1547
+ if (obj.on_item_fail === "continue" || obj.on_item_fail === "halt") {
1548
+ def.on_item_fail = obj.on_item_fail;
1549
+ }
1550
+ return def;
1551
+ }
1552
+ function readStageDefs(def) {
1553
+ if (def.steps_version !== 2)
1554
+ return [];
1555
+ return Array.isArray(def.steps) ? def.steps : [];
1556
+ }
1511
1557
  var STAGE_DAEMON_OWNED_TOOLS = [
1512
1558
  "mcp__harmony__harmony_end_agent_session",
1513
1559
  "mcp__harmony__harmony_start_agent_session",
1514
1560
  "mcp__harmony__harmony_move_card"
1515
1561
  ];
1562
+ function customGateMetric(gate) {
1563
+ if (gate === null || typeof gate !== "object" || Array.isArray(gate)) {
1564
+ return null;
1565
+ }
1566
+ const record = gate;
1567
+ if (record.kind !== "custom")
1568
+ return null;
1569
+ if (record.pendingEngine === true)
1570
+ return null;
1571
+ const metric = typeof record.metric === "string" ? record.metric.trim() : "";
1572
+ return metric ? metric : null;
1573
+ }
1574
+ function referencedGateMetrics(def) {
1575
+ const out = [];
1576
+ for (const stage of readStageDefs(def)) {
1577
+ if (!stage || typeof stage !== "object")
1578
+ continue;
1579
+ const stageId = typeof stage.id === "string" ? stage.id : "";
1580
+ const stageName = typeof stage.name === "string" ? stage.name : stageId;
1581
+ const gateMetric = customGateMetric(stage.gate);
1582
+ if (gateMetric) {
1583
+ out.push({ stageId, stageName, metric: gateMetric, source: "gate" });
1584
+ }
1585
+ const loop = normalizeLoopDef(stage.loop);
1586
+ const loopMetric = loop?.exit_gate ? customGateMetric(loop.exit_gate) : null;
1587
+ if (loopMetric) {
1588
+ out.push({
1589
+ stageId,
1590
+ stageName,
1591
+ metric: loopMetric,
1592
+ source: "loop_exit_gate"
1593
+ });
1594
+ }
1595
+ }
1596
+ return out;
1597
+ }
1516
1598
  // ../harmony-shared/dist/reviewTools.js
1517
1599
  var REVIEW_DISALLOWED_TOOLS = [
1518
1600
  ...STAGE_DAEMON_OWNED_TOOLS,
@@ -1546,7 +1628,20 @@ function getRetryDelay(attempt) {
1546
1628
  const delay = Math.min(RETRY_CONFIG.baseDelayMs * 2 ** attempt, RETRY_CONFIG.maxDelayMs);
1547
1629
  return Math.round(delay + delay * 0.25 * (Math.random() * 2 - 1));
1548
1630
  }
1549
- var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1631
+ var sleep2 = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
1632
+ function buildMemoryQuery(title, description) {
1633
+ const DESCRIPTION_CAP = 600;
1634
+ const trimmedTitle = title.trim();
1635
+ const trimmedBody = (description ?? "").trim();
1636
+ if (!trimmedTitle && !trimmedBody)
1637
+ return "";
1638
+ if (!trimmedTitle)
1639
+ return trimmedBody.slice(0, DESCRIPTION_CAP);
1640
+ if (!trimmedBody)
1641
+ return trimmedTitle;
1642
+ return `${trimmedTitle}
1643
+ ${trimmedBody.slice(0, DESCRIPTION_CAP)}`;
1644
+ }
1550
1645
 
1551
1646
  class Semaphore {
1552
1647
  permits;
@@ -1559,7 +1654,7 @@ class Semaphore {
1559
1654
  this.permits--;
1560
1655
  return;
1561
1656
  }
1562
- return new Promise((resolve) => this.queue.push(resolve));
1657
+ return new Promise((resolve2) => this.queue.push(resolve2));
1563
1658
  }
1564
1659
  release() {
1565
1660
  const next = this.queue.shift();
@@ -2017,6 +2112,14 @@ class HarmonyApiClient {
2017
2112
  params.set("sinceSeq", String(sinceSeq));
2018
2113
  return this.request("GET", `/cards/${cardId}/agent-messages?${params.toString()}`);
2019
2114
  }
2115
+ async postBudgetDecision(cardId, data) {
2116
+ return this.request("POST", `/cards/${cardId}/budget-decisions`, data);
2117
+ }
2118
+ async getBudgetDecisions(cardId, sinceIso) {
2119
+ const params = new URLSearchParams;
2120
+ params.set("sinceIso", sinceIso);
2121
+ return this.request("GET", `/cards/${cardId}/budget-decisions?${params.toString()}`);
2122
+ }
2020
2123
  async updateAgentProgress(cardId, data) {
2021
2124
  return this.request("POST", `/cards/${cardId}/agent-context`, data);
2022
2125
  }
@@ -2062,6 +2165,8 @@ class HarmonyApiClient {
2062
2165
  params.set("offset", String(options.offset));
2063
2166
  if (options.include_superseded)
2064
2167
  params.set("include_superseded", "true");
2168
+ if (options.consumer)
2169
+ params.set("consumer", options.consumer);
2065
2170
  if (options.include_episodes)
2066
2171
  params.set("include_episodes", "true");
2067
2172
  return this.request("GET", `/memory/entities?${params.toString()}`);
@@ -2117,6 +2222,12 @@ class HarmonyApiClient {
2117
2222
  if (options.topK !== undefined) {
2118
2223
  entities = entities.slice(0, options.topK);
2119
2224
  }
2225
+ if (options.consumer) {
2226
+ const deliveredIds = entities.map((e) => e.id).filter((id) => typeof id === "string");
2227
+ if (deliveredIds.length > 0) {
2228
+ this.batchTouchMemoryEntities(deliveredIds, options.consumer).catch(() => {});
2229
+ }
2230
+ }
2120
2231
  return { entities };
2121
2232
  }
2122
2233
  async deleteMemoryEntity(entityId) {
@@ -2125,9 +2236,10 @@ class HarmonyApiClient {
2125
2236
  async touchMemoryEntity(entityId) {
2126
2237
  return this.request("POST", `/memory/entities/${entityId}/touch`);
2127
2238
  }
2128
- async batchTouchMemoryEntities(entityIds) {
2239
+ async batchTouchMemoryEntities(entityIds, consumer) {
2129
2240
  return this.request("POST", "/memory/entities/batch-touch", {
2130
- entity_ids: entityIds
2241
+ entity_ids: entityIds,
2242
+ ...consumer ? { consumer } : {}
2131
2243
  });
2132
2244
  }
2133
2245
  async createMemoryRelation(data) {
@@ -2153,8 +2265,12 @@ class HarmonyApiClient {
2153
2265
  params.append("tags", tag);
2154
2266
  if (options?.include_superseded)
2155
2267
  params.set("include_superseded", "true");
2268
+ if (options?.consumer)
2269
+ params.set("consumer", options.consumer);
2156
2270
  if (options?.include_episodes)
2157
2271
  params.set("include_episodes", "true");
2272
+ if (options?.assembly_id)
2273
+ params.set("assembly_id", options.assembly_id);
2158
2274
  return this.request("GET", `/memory/search?${params.toString()}`);
2159
2275
  }
2160
2276
  async getVaultIndex(options) {
@@ -2166,6 +2282,8 @@ class HarmonyApiClient {
2166
2282
  params.set("type", options.type);
2167
2283
  if (options.limit !== undefined)
2168
2284
  params.set("limit", String(options.limit));
2285
+ if (options.consumer)
2286
+ params.set("consumer", options.consumer);
2169
2287
  if (options.include_episodes)
2170
2288
  params.set("include_episodes", "true");
2171
2289
  return this.request("GET", `/memory/index?${params.toString()}`);
@@ -2179,6 +2297,8 @@ class HarmonyApiClient {
2179
2297
  params.set("type", options.type);
2180
2298
  if (options.limit !== undefined)
2181
2299
  params.set("limit", String(options.limit));
2300
+ if (options.consumer)
2301
+ params.set("consumer", options.consumer);
2182
2302
  if (options.include_episodes)
2183
2303
  params.set("include_episodes", "true");
2184
2304
  return this.requestRaw("GET", `/memory/index?${params.toString()}`, undefined, {
@@ -2238,6 +2358,8 @@ class HarmonyApiClient {
2238
2358
  params.set("type", options.type);
2239
2359
  if (options?.limit !== undefined)
2240
2360
  params.set("limit", String(options.limit));
2361
+ if (options?.consumer)
2362
+ params.set("consumer", options.consumer);
2241
2363
  if (options?.include_episodes)
2242
2364
  params.set("include_episodes", "true");
2243
2365
  return this.requestRaw("GET", `/memory/search?${params.toString()}`, undefined, {
@@ -2343,14 +2465,15 @@ class HarmonyApiClient {
2343
2465
  } catch {}
2344
2466
  }
2345
2467
  const variant = options.variant || "execute";
2346
- const assembledContextStr = undefined;
2347
- const assemblyId = undefined;
2468
+ const assemblyId = randomUUID2();
2348
2469
  let memories;
2349
2470
  try {
2350
2471
  if (options.workspaceId && cardData.title) {
2351
- const memoryResult = await this.searchMemoryEntities(options.workspaceId, cardData.title, {
2472
+ const memoryResult = await this.searchMemoryEntities(options.workspaceId, buildMemoryQuery(cardData.title, cardData.description), {
2352
2473
  project_id: options.projectId,
2353
- limit: 5
2474
+ limit: 5,
2475
+ consumer: "agent-prompt",
2476
+ assembly_id: assemblyId
2354
2477
  });
2355
2478
  if (memoryResult.entities?.length > 0) {
2356
2479
  memories = memoryResult.entities.map((e) => ({
@@ -2374,7 +2497,6 @@ class HarmonyApiClient {
2374
2497
  contextOptions: options.contextOptions,
2375
2498
  customConstraints: options.customConstraints,
2376
2499
  memories,
2377
- assembledContext: assembledContextStr,
2378
2500
  assemblyId
2379
2501
  });
2380
2502
  try {
@@ -2705,7 +2827,7 @@ async function autoExpandGraph(client3, entityId, title, content, _tags, workspa
2705
2827
  });
2706
2828
  candidates = entities.filter((e) => e.id !== entityId && (e.confidence ?? 1) >= 0.4).slice(0, maxRelations);
2707
2829
  if (candidates.length === 0) {
2708
- await new Promise((resolve) => setTimeout(resolve, 2000));
2830
+ await new Promise((resolve2) => setTimeout(resolve2, 2000));
2709
2831
  const retry = await client3.searchMemoryEntities(workspaceId, query, {
2710
2832
  project_id: projectId,
2711
2833
  limit: 20
@@ -3288,6 +3410,34 @@ async function onboardNewUser(params) {
3288
3410
  };
3289
3411
  }
3290
3412
 
3413
+ // src/playbook-metric-warnings.ts
3414
+ function playbookMetricWarnings(agents, steps) {
3415
+ if (!Array.isArray(steps))
3416
+ return [];
3417
+ const declared = declaredGateMetricsFromAgents(agents);
3418
+ if (!declared.known)
3419
+ return [];
3420
+ const warnings = [];
3421
+ const seen = new Set;
3422
+ for (const ref of referencedGateMetrics({ steps, steps_version: 2 })) {
3423
+ if (declared.names.has(ref.metric) || seen.has(ref.metric))
3424
+ continue;
3425
+ seen.add(ref.metric);
3426
+ warnings.push(`Gate metric "${ref.metric}" (stage "${ref.stageName}") is not declared by any agent in this workspace — a stage run gating on it will hold until a daemon declares it under agent.playbooks.metrics.${ref.metric}.`);
3427
+ }
3428
+ return warnings;
3429
+ }
3430
+ async function collectPlaybookMetricWarnings(client3, workspaceId, steps) {
3431
+ if (!workspaceId || !Array.isArray(steps))
3432
+ return [];
3433
+ try {
3434
+ const { agents } = await client3.listWorkspaceAgents(workspaceId);
3435
+ return playbookMetricWarnings(agents, steps);
3436
+ } catch {
3437
+ return [];
3438
+ }
3439
+ }
3440
+
3291
3441
  // src/skills.ts
3292
3442
  import {
3293
3443
  existsSync as existsSync4,
@@ -3297,7 +3447,7 @@ import {
3297
3447
  writeFileSync as writeFileSync3
3298
3448
  } from "node:fs";
3299
3449
  import { homedir as homedir3 } from "node:os";
3300
- import { dirname, join as join5 } from "node:path";
3450
+ import { dirname as dirname2, join as join5 } from "node:path";
3301
3451
  init_config();
3302
3452
 
3303
3453
  // src/hmy-config.ts
@@ -3444,7 +3594,7 @@ function stripSkillPreamble(content) {
3444
3594
  `;
3445
3595
  }
3446
3596
  function atomicWrite(filePath, content) {
3447
- const dir = dirname(filePath);
3597
+ const dir = dirname2(filePath);
3448
3598
  if (!existsSync4(dir))
3449
3599
  mkdirSync3(dir, { recursive: true });
3450
3600
  const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}`;
@@ -3535,10 +3685,10 @@ async function refreshSkills(opts = {}) {
3535
3685
  continue;
3536
3686
  let siblingPath;
3537
3687
  if (samplePath.endsWith("SKILL.md")) {
3538
- const parentDir = dirname(dirname(samplePath));
3688
+ const parentDir = dirname2(dirname2(samplePath));
3539
3689
  siblingPath = `${parentDir}/${name}/SKILL.md`;
3540
3690
  } else {
3541
- const parentDir = dirname(samplePath);
3691
+ const parentDir = dirname2(samplePath);
3542
3692
  siblingPath = `${parentDir}/${name}.md`;
3543
3693
  }
3544
3694
  if (existsSync4(siblingPath)) {
@@ -4332,7 +4482,7 @@ var TOOLS = {
4332
4482
  }
4333
4483
  },
4334
4484
  harmony_classify_card: {
4335
- description: "Classify a card with the LLM classifier: sets `intent` (plan/think/implement/review), `complexity_score` (0-10), `model_tier` (simple/advanced/research), stamps `classified_at`, and applies the type label (feature/bug/idea). Call right after creating a card to classify it in-flow. Idempotent; never touches the user-owned `model_override`.",
4485
+ description: "DEPRECATED run sizing now happens at daemon pickup and is run-scoped, so nothing reads `model_tier`, `intent` or `complexity_score` any more; this tool still writes them, but only the type label has an effect. Prefer letting card creation apply the type label. Sets `intent` (plan/think/implement/review), `complexity_score` (0-10), `model_tier` (simple/advanced/research), stamps `classified_at`, and applies the type label (feature/bug/idea). Idempotent; never touches the user-owned `model_override`.",
4336
4486
  inputSchema: {
4337
4487
  type: "object",
4338
4488
  properties: {
@@ -4586,11 +4736,15 @@ var TOOLS = {
4586
4736
  }
4587
4737
  },
4588
4738
  harmony_set_project_context: {
4589
- description: "Set the active project context for subsequent operations",
4739
+ description: "Set the active project context for subsequent operations. The project's workspace is set with it, so the two can never point at different places; pass workspaceId to skip the lookup.",
4590
4740
  inputSchema: {
4591
4741
  type: "object",
4592
4742
  properties: {
4593
- projectId: { type: "string" }
4743
+ projectId: { type: "string" },
4744
+ workspaceId: {
4745
+ type: "string",
4746
+ description: "The workspace this project belongs to. Optional — looked up when omitted."
4747
+ }
4594
4748
  },
4595
4749
  required: ["projectId"]
4596
4750
  }
@@ -5394,13 +5548,26 @@ var TOOLS = {
5394
5548
  type: "array",
5395
5549
  description: "The playbook's ordered stage objects.",
5396
5550
  items: { type: "object" }
5551
+ },
5552
+ triggerType: {
5553
+ type: "string",
5554
+ enum: ["manual", "auto"],
5555
+ description: "'manual' (default) — the playbook is applied by a person. 'auto' — it claims matching cards itself, and requires autoBind."
5556
+ },
5557
+ autoBind: {
5558
+ type: "object",
5559
+ description: "Auto-bind rule: {priority?: number, mode?: 'all'|'any', when: [{path, op, value}]}. Conditions are evaluated against the card's labels (lowercased), intent, complexity_score and priority with the gate operators eq/neq/gte/gt/lte/lt/contains/exists; 'contains' on labels is membership. Stored even while triggerType is 'manual', so a rule can be armed later without re-authoring it."
5560
+ },
5561
+ catalogId: {
5562
+ type: "string",
5563
+ description: "Slug of the built-in template this came from (provenance only; never used to match)."
5397
5564
  }
5398
5565
  },
5399
5566
  required: ["name"]
5400
5567
  }
5401
5568
  },
5402
5569
  harmony_update_playbook: {
5403
- description: "Update a playbook's name, description, steps/stages, enabled flag, or lifecycle state ('active'|'deprecated').",
5570
+ description: "Update a playbook's name, description, steps/stages, enabled flag, lifecycle state ('active'|'deprecated'), or its auto-bind rule and arming.",
5404
5571
  inputSchema: {
5405
5572
  type: "object",
5406
5573
  properties: {
@@ -5423,6 +5590,15 @@ var TOOLS = {
5423
5590
  type: "string",
5424
5591
  enum: ["active", "deprecated"],
5425
5592
  description: "Lifecycle state"
5593
+ },
5594
+ triggerType: {
5595
+ type: "string",
5596
+ enum: ["manual", "auto"],
5597
+ description: "Arm ('auto') or disarm ('manual') automatic application. Arming requires a rule to be present or supplied in the same call."
5598
+ },
5599
+ autoBind: {
5600
+ type: "object",
5601
+ description: "Replace the auto-bind rule: {priority?, mode?: 'all'|'any', when: [{path, op, value}]}. Pass null to remove it."
5426
5602
  }
5427
5603
  },
5428
5604
  required: ["playbookId"]
@@ -5887,7 +6063,10 @@ async function handleToolCall(name, args, deps) {
5887
6063
  const where = resolved.project.workspaceName ? `project "${resolved.project.name ?? resolved.project.id}" (workspace "${resolved.project.workspaceName}")` : `project "${resolved.project.name ?? resolved.project.id}"`;
5888
6064
  const established = activeProjectId == null;
5889
6065
  if (established) {
5890
- deps.setActiveProject(resolved.project.id);
6066
+ deps.setActiveContext({
6067
+ projectId: resolved.project.id,
6068
+ workspaceId: resolved.project.workspaceId
6069
+ });
5891
6070
  }
5892
6071
  return {
5893
6072
  success: true,
@@ -6326,15 +6505,57 @@ ${options}
6326
6505
  }
6327
6506
  case "harmony_set_project_context": {
6328
6507
  const projectId = z.string().uuid().parse(args.projectId);
6329
- deps.setActiveProject(projectId);
6330
- return { success: true, activeProjectId: projectId };
6508
+ const explicitWorkspaceId = args.workspaceId ? z.string().uuid().parse(args.workspaceId) : null;
6509
+ let owningWorkspaceId = explicitWorkspaceId;
6510
+ if (!owningWorkspaceId) {
6511
+ try {
6512
+ const { workspaces } = await client3.listWorkspaces();
6513
+ for (const workspace of workspaces) {
6514
+ if (!workspace?.id)
6515
+ continue;
6516
+ const { projects } = await client3.listProjects(workspace.id);
6517
+ if (projects.some((p) => p?.id === projectId)) {
6518
+ owningWorkspaceId = workspace.id;
6519
+ break;
6520
+ }
6521
+ }
6522
+ } catch (error) {
6523
+ const reason = error instanceof Error ? error.message : String(error);
6524
+ return {
6525
+ success: false,
6526
+ activeProjectId: deps.getActiveProjectId(),
6527
+ activeWorkspaceId: deps.getActiveWorkspaceId(),
6528
+ note: `Could not resolve this project's workspace (${reason}), so the ` + `active context was left unchanged rather than half-written. ` + `Retry, or pass workspaceId explicitly to skip the lookup.`
6529
+ };
6530
+ }
6531
+ if (!owningWorkspaceId) {
6532
+ return {
6533
+ success: false,
6534
+ activeProjectId: deps.getActiveProjectId(),
6535
+ activeWorkspaceId: deps.getActiveWorkspaceId(),
6536
+ note: `Project ${projectId} is not in any workspace this connection ` + `can reach, so the active context was left unchanged. Check ` + `harmony_list_projects, pass workspaceId explicitly, or ` + `reconnect with /mcp if it lives in another workspace.`
6537
+ };
6538
+ }
6539
+ }
6540
+ deps.setActiveContext({ projectId, workspaceId: owningWorkspaceId });
6541
+ return {
6542
+ success: true,
6543
+ activeProjectId: projectId,
6544
+ activeWorkspaceId: owningWorkspaceId
6545
+ };
6331
6546
  }
6332
6547
  case "harmony_get_context": {
6548
+ const report = describeActiveContext({
6549
+ projectId: deps.getActiveProjectId(),
6550
+ workspaceId: deps.getActiveWorkspaceId()
6551
+ });
6333
6552
  return {
6334
6553
  success: true,
6335
6554
  context: {
6336
- activeWorkspaceId: deps.getActiveWorkspaceId(),
6337
- activeProjectId: deps.getActiveProjectId()
6555
+ activeWorkspaceId: report.workspaceId,
6556
+ activeProjectId: report.projectId,
6557
+ consistent: report.consistent,
6558
+ ...report.note ? { note: report.note } : {}
6338
6559
  }
6339
6560
  };
6340
6561
  }
@@ -6774,7 +6995,7 @@ ${options}
6774
6995
  if (trimmed.length > 0) {
6775
6996
  const touchIds = trimmed.map(({ entity }) => entity?.id).filter((id) => typeof id === "string");
6776
6997
  if (touchIds.length > 0) {
6777
- client3.batchTouchMemoryEntities(touchIds).catch(() => {});
6998
+ client3.batchTouchMemoryEntities(touchIds, "mcp-tool").catch(() => {});
6778
6999
  }
6779
7000
  }
6780
7001
  let sessionEntities = [];
@@ -7183,9 +7404,17 @@ ${options}
7183
7404
  workspaceId,
7184
7405
  name: name2,
7185
7406
  description: args.description,
7186
- steps: args.steps
7407
+ steps: args.steps,
7408
+ triggerType: args.triggerType,
7409
+ autoBind: args.autoBind,
7410
+ catalogId: args.catalogId
7187
7411
  });
7188
- return { success: true, playbook: result.playbook };
7412
+ const warnings = await collectPlaybookMetricWarnings(client3, workspaceId, args.steps);
7413
+ return {
7414
+ success: true,
7415
+ playbook: result.playbook,
7416
+ ...warnings.length > 0 ? { warnings } : {}
7417
+ };
7189
7418
  }
7190
7419
  case "harmony_update_playbook": {
7191
7420
  const playbookId = z.string().uuid().parse(args.playbookId);
@@ -7194,9 +7423,16 @@ ${options}
7194
7423
  description: args.description,
7195
7424
  steps: args.steps,
7196
7425
  enabled: args.enabled,
7197
- state: args.state
7426
+ state: args.state,
7427
+ triggerType: args.triggerType,
7428
+ ..."autoBind" in args ? { autoBind: args.autoBind } : {}
7198
7429
  });
7199
- return { success: true, playbook: result.playbook };
7430
+ const warnings = await collectPlaybookMetricWarnings(client3, result.playbook?.workspace_id, args.steps);
7431
+ return {
7432
+ success: true,
7433
+ playbook: result.playbook,
7434
+ ...warnings.length > 0 ? { warnings } : {}
7435
+ };
7200
7436
  }
7201
7437
  case "harmony_save_card_as_playbook":
7202
7438
  return deprecatedRemovedToolResult("harmony_save_card_as_playbook");
@@ -7271,8 +7507,10 @@ ${options}
7271
7507
  apiUrl: deps.getApiUrl()
7272
7508
  });
7273
7509
  deps.saveConfig({ apiKey: result.apiKey.rawKey });
7274
- deps.setActiveWorkspace(result.workspace.id);
7275
- deps.setActiveProject(result.project.id);
7510
+ deps.setActiveContext({
7511
+ projectId: result.project.id,
7512
+ workspaceId: result.workspace.id
7513
+ });
7276
7514
  deps.resetClient();
7277
7515
  return {
7278
7516
  success: true,
@@ -7294,7 +7532,7 @@ function createConfigDeps() {
7294
7532
  isConfigured,
7295
7533
  getActiveProjectId: () => getActiveProjectId(),
7296
7534
  getActiveWorkspaceId: () => getActiveWorkspaceId(),
7297
- setActiveProject: (id) => setActiveProject(id),
7535
+ setActiveContext: (context) => setActiveContext(context),
7298
7536
  setActiveWorkspace: (id) => setActiveWorkspace(id),
7299
7537
  getApiUrl,
7300
7538
  getMemoryDir: () => getMemoryDir(),
@@ -7373,7 +7611,7 @@ import {
7373
7611
  unlinkSync
7374
7612
  } from "node:fs";
7375
7613
  import { homedir as homedir6 } from "node:os";
7376
- import { dirname as dirname3, join as join8 } from "node:path";
7614
+ import { dirname as dirname4, join as join8 } from "node:path";
7377
7615
  import * as p4 from "@clack/prompts";
7378
7616
  init_config();
7379
7617
  init_oauth_login();
@@ -7445,7 +7683,7 @@ async function confirmOrDefault(assumeYes, opts) {
7445
7683
 
7446
7684
  // src/tui/docs.ts
7447
7685
  import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync5, statSync as statSync2 } from "node:fs";
7448
- import { isAbsolute, join as join7, resolve, sep as sep2 } from "node:path";
7686
+ import { isAbsolute, join as join7, resolve as resolve2, sep as sep2 } from "node:path";
7449
7687
  import * as p2 from "@clack/prompts";
7450
7688
 
7451
7689
  // src/tui/theme.ts
@@ -7869,7 +8107,7 @@ function verifyDocs(cwd) {
7869
8107
  const agentsMd = readText(join7(cwd, "AGENTS.md"));
7870
8108
  const pkg = readJson(join7(cwd, "package.json"));
7871
8109
  const pkgScripts = pkg && typeof pkg.scripts === "object" && pkg.scripts !== null ? pkg.scripts : {};
7872
- const projectRoot = resolve(cwd);
8110
+ const projectRoot = resolve2(cwd);
7873
8111
  if (claudeMd) {
7874
8112
  const importedFiles = [];
7875
8113
  for (const line of claudeMd.split(`
@@ -7886,7 +8124,7 @@ function verifyDocs(cwd) {
7886
8124
  });
7887
8125
  continue;
7888
8126
  }
7889
- const resolvedPath = resolve(projectRoot, refPath);
8127
+ const resolvedPath = resolve2(projectRoot, refPath);
7890
8128
  if (resolvedPath !== projectRoot && !resolvedPath.startsWith(projectRoot + sep2)) {
7891
8129
  issues.push({
7892
8130
  severity: "error",
@@ -8071,13 +8309,13 @@ function checkBacktickPaths(content, file, cwd, issues) {
8071
8309
  const pathRe = /`((?:src\/|packages\/|apps\/|supabase\/|docs\/)[^`]+)`/g;
8072
8310
  let match;
8073
8311
  const checked = new Set;
8074
- const root = resolve(cwd);
8312
+ const root = resolve2(cwd);
8075
8313
  while ((match = pathRe.exec(content)) !== null) {
8076
8314
  const refPath = match[1].replace(/\/$/, "");
8077
8315
  if (checked.has(refPath))
8078
8316
  continue;
8079
8317
  checked.add(refPath);
8080
- const resolvedRef = resolve(root, refPath);
8318
+ const resolvedRef = resolve2(root, refPath);
8081
8319
  if (resolvedRef !== root && !resolvedRef.startsWith(root + sep2))
8082
8320
  continue;
8083
8321
  if (!existsSync6(resolvedRef)) {
@@ -8158,7 +8396,7 @@ import {
8158
8396
  writeFileSync as writeFileSync4
8159
8397
  } from "node:fs";
8160
8398
  import { homedir as homedir5 } from "node:os";
8161
- import { dirname as dirname2 } from "node:path";
8399
+ import { dirname as dirname3 } from "node:path";
8162
8400
  import * as p3 from "@clack/prompts";
8163
8401
  function ensureDir(dirPath) {
8164
8402
  if (!existsSync7(dirPath)) {
@@ -8171,7 +8409,7 @@ function writeFile(filePath, content, options = {}) {
8171
8409
  return { path: filePath, action: "skip" };
8172
8410
  }
8173
8411
  try {
8174
- ensureDir(dirname2(filePath));
8412
+ ensureDir(dirname3(filePath));
8175
8413
  const defaultMode = filePath.includes(".harmony-mcp") ? 384 : 420;
8176
8414
  const mode = options.mode ?? defaultMode;
8177
8415
  writeFileSync4(filePath, content, { mode });
@@ -8191,7 +8429,7 @@ function mergeJsonFile(filePath, updates, options = {}) {
8191
8429
  const exists = existsSync7(filePath);
8192
8430
  if (!exists) {
8193
8431
  try {
8194
- ensureDir(dirname2(filePath));
8432
+ ensureDir(dirname3(filePath));
8195
8433
  writeFileSync4(filePath, JSON.stringify(updates, null, 2), {
8196
8434
  mode: 420
8197
8435
  });
@@ -8241,7 +8479,7 @@ function appendToToml(filePath, section, content, options = {}) {
8241
8479
  const exists = existsSync7(filePath);
8242
8480
  if (!exists) {
8243
8481
  try {
8244
- ensureDir(dirname2(filePath));
8482
+ ensureDir(dirname3(filePath));
8245
8483
  writeFileSync4(filePath, content, { mode: 420 });
8246
8484
  return { path: filePath, action: "create" };
8247
8485
  } catch (error) {
@@ -8295,7 +8533,7 @@ async function writeFilesWithProgress(files, options = {}) {
8295
8533
  });
8296
8534
  }
8297
8535
  results.push(result);
8298
- await new Promise((resolve2) => setTimeout(resolve2, 50));
8536
+ await new Promise((resolve3) => setTimeout(resolve3, 50));
8299
8537
  }
8300
8538
  spinner2.stop("Files written");
8301
8539
  for (const result of results) {
@@ -8395,7 +8633,7 @@ async function registerMcpServer() {
8395
8633
  async function writeMcpConfigFallback(home) {
8396
8634
  const { readFileSync: readFileSync7, writeFileSync: writeFileSync5, mkdirSync: mkdirSync6, existsSync: existsSync9 } = await import("node:fs");
8397
8635
  const settingsPath = join8(home, ".claude", "settings.json");
8398
- const settingsDir = dirname3(settingsPath);
8636
+ const settingsDir = dirname4(settingsPath);
8399
8637
  if (!existsSync9(settingsDir)) {
8400
8638
  mkdirSync6(settingsDir, { recursive: true });
8401
8639
  }
@@ -8414,7 +8652,7 @@ async function writeMcpConfigFallback(home) {
8414
8652
  async function allowlistHarmonyTools(home, allowAll) {
8415
8653
  const { readFileSync: readFileSync7, writeFileSync: writeFileSync5, mkdirSync: mkdirSync6, existsSync: existsSync9 } = await import("node:fs");
8416
8654
  const settingsPath = join8(home, ".claude", "settings.json");
8417
- const settingsDir = dirname3(settingsPath);
8655
+ const settingsDir = dirname4(settingsPath);
8418
8656
  if (!existsSync9(settingsDir)) {
8419
8657
  mkdirSync6(settingsDir, { recursive: true });
8420
8658
  }
@@ -8946,8 +9184,10 @@ ${colors.dim(url)}`);
8946
9184
  createdNewAccount = true;
8947
9185
  needsApiKey = true;
8948
9186
  saveConfig({ apiKey, userEmail, apiUrl: API_URL });
8949
- setActiveWorkspace(selectedWorkspaceIdFromSignup);
8950
- setActiveProject(selectedProjectIdFromSignup);
9187
+ setActiveContext({
9188
+ workspaceId: selectedWorkspaceIdFromSignup,
9189
+ projectId: selectedProjectIdFromSignup
9190
+ });
8951
9191
  p4.log.success("Workspace and board created");
8952
9192
  } catch (error) {
8953
9193
  spinner4.stop(colors.error("Account creation failed"));
@@ -9279,7 +9519,7 @@ Specify the workspace with --workspace <id>, or select one below.`);
9279
9519
  if (allSymlinks.length > 0) {
9280
9520
  for (const symlink of allSymlinks) {
9281
9521
  try {
9282
- const linkDir = dirname3(symlink.link);
9522
+ const linkDir = dirname4(symlink.link);
9283
9523
  if (!existsSync8(linkDir)) {
9284
9524
  mkdirSync5(linkDir, { recursive: true });
9285
9525
  }
@@ -9346,10 +9586,12 @@ Specify the workspace with --workspace <id>, or select one below.`);
9346
9586
  localConfig.projectId = selectedProjectId;
9347
9587
  saveLocalConfig(localConfig, cwd);
9348
9588
  console.log(` ${colors.success("✓")} ${colors.dim(formatPath(getLocalConfigPath(cwd), home))} ${colors.dim("(created)")}`);
9349
- if (selectedWorkspaceId)
9350
- setActiveWorkspace(selectedWorkspaceId);
9351
- if (selectedProjectId)
9352
- setActiveProject(selectedProjectId);
9589
+ if (selectedWorkspaceId || selectedProjectId) {
9590
+ setActiveContext({
9591
+ workspaceId: selectedWorkspaceId ?? null,
9592
+ projectId: selectedProjectId ?? null
9593
+ }, { global: true });
9594
+ }
9353
9595
  }
9354
9596
  console.log("");
9355
9597
  p4.outro(colors.success("Setup complete!"));
@@ -9448,7 +9690,7 @@ Skills:`);
9448
9690
  console.log(`
9449
9691
  Context:`);
9450
9692
  if (hasLocal) {
9451
- console.log(` Local config: ${getLocalConfigPath()}`);
9693
+ console.log(` Local config: ${findLocalConfigPath()}`);
9452
9694
  console.log(` Workspace: ${localConfig?.workspaceId || "(not set)"}`);
9453
9695
  console.log(` Project: ${localConfig?.projectId || "(not set)"}`);
9454
9696
  }
@@ -9457,12 +9699,18 @@ Context:`);
9457
9699
  console.log(` Project: ${globalConfig.activeProjectId || "(not set)"}`);
9458
9700
  const effectiveWorkspace = getActiveWorkspaceId();
9459
9701
  const effectiveProject = getActiveProjectId();
9460
- const wsSource = localConfig?.workspaceId ? "local" : globalConfig.activeWorkspaceId ? "global" : "";
9461
- const projSource = localConfig?.projectId ? "local" : globalConfig.activeProjectId ? "global" : "";
9702
+ const contextSource = hasLocal ? "local" : "global";
9703
+ const wsSource = effectiveWorkspace ? contextSource : "";
9704
+ const projSource = effectiveProject ? contextSource : "";
9462
9705
  console.log(`
9463
9706
  Active (effective):`);
9464
9707
  console.log(` Workspace: ${effectiveWorkspace || "(not set)"}${wsSource ? ` ← ${wsSource}` : ""}`);
9465
9708
  console.log(` Project: ${effectiveProject || "(not set)"}${projSource ? ` ← ${projSource}` : ""}`);
9709
+ const report = getActiveContext();
9710
+ if (!report.consistent && report.note) {
9711
+ console.log(`
9712
+ ⚠ ${report.note}`);
9713
+ }
9466
9714
  } else {
9467
9715
  console.log(`Status: Not configured
9468
9716
  `);