@stackmemoryai/stackmemory 1.10.5 → 1.12.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.
Files changed (92) hide show
  1. package/README.md +104 -23
  2. package/dist/src/cli/claude-sm.js +266 -84
  3. package/dist/src/cli/codex-sm.js +185 -33
  4. package/dist/src/cli/commands/bench.js +209 -2
  5. package/dist/src/cli/commands/cache.js +126 -0
  6. package/dist/src/cli/commands/daemon.js +41 -0
  7. package/dist/src/cli/commands/handoff.js +40 -9
  8. package/dist/src/cli/commands/onboard.js +70 -3
  9. package/dist/src/cli/commands/optimize.js +117 -0
  10. package/dist/src/cli/commands/orchestrate.js +230 -5
  11. package/dist/src/cli/commands/orchestrator.js +312 -24
  12. package/dist/src/cli/commands/pack.js +322 -0
  13. package/dist/src/cli/commands/search.js +40 -1
  14. package/dist/src/cli/commands/setup.js +177 -7
  15. package/dist/src/cli/commands/skills.js +10 -1
  16. package/dist/src/cli/commands/state.js +265 -0
  17. package/dist/src/cli/gemini-sm.js +19 -29
  18. package/dist/src/cli/index.js +90 -29
  19. package/dist/src/cli/opencode-sm.js +38 -21
  20. package/dist/src/cli/utils/determinism-watcher.js +66 -0
  21. package/dist/src/cli/utils/real-cli-bin.js +44 -0
  22. package/dist/src/core/cache/content-cache.js +238 -0
  23. package/dist/src/core/cache/index.js +11 -0
  24. package/dist/src/core/cache/token-estimator.js +16 -0
  25. package/dist/src/core/context/frame-database.js +38 -30
  26. package/dist/src/core/cross-search/cross-project-search.js +269 -0
  27. package/dist/src/core/{merge → cross-search}/index.js +6 -4
  28. package/dist/src/core/database/sqlite-adapter.js +0 -83
  29. package/dist/src/core/extensions/provider-adapter.js +5 -0
  30. package/dist/src/core/models/model-router.js +22 -2
  31. package/dist/src/core/monitoring/logger.js +2 -1
  32. package/dist/src/core/optimization/trace-optimizer.js +413 -0
  33. package/dist/src/core/provenance/confidence-scorer.js +128 -0
  34. package/dist/src/core/provenance/index.js +40 -0
  35. package/dist/src/core/provenance/provenance-store.js +194 -0
  36. package/dist/src/core/provenance/types.js +82 -0
  37. package/dist/src/core/session/project-handoff.js +64 -0
  38. package/dist/src/core/session/session-manager.js +28 -0
  39. package/dist/src/core/shared-state/canonical-store.js +564 -0
  40. package/dist/src/core/skill-packs/index.js +18 -0
  41. package/dist/src/core/skill-packs/parser.js +42 -0
  42. package/dist/src/core/skill-packs/registry.js +224 -0
  43. package/dist/src/core/skill-packs/types.js +66 -0
  44. package/dist/src/core/trace/trace-event-store.js +282 -0
  45. package/dist/src/core/trace/trace-event.js +4 -0
  46. package/dist/src/daemon/daemon-config.js +7 -0
  47. package/dist/src/daemon/services/github-service.js +126 -0
  48. package/dist/src/daemon/unified-daemon.js +30 -0
  49. package/dist/src/features/sweep/pty-wrapper.js +13 -5
  50. package/dist/src/hooks/schemas.js +2 -0
  51. package/dist/src/integrations/claude-code/subagent-client.js +89 -0
  52. package/dist/src/integrations/github/pr-state.js +158 -0
  53. package/dist/src/integrations/linear/client.js +4 -1
  54. package/dist/src/integrations/mcp/handlers/cross-search-handlers.js +188 -0
  55. package/dist/src/integrations/mcp/handlers/index.js +40 -59
  56. package/dist/src/integrations/mcp/server.js +425 -311
  57. package/dist/src/integrations/mcp/tool-alias-registry.js +370 -0
  58. package/dist/src/integrations/mcp/tool-definitions.js +98 -229
  59. package/dist/src/integrations/ralph/context/stackmemory-context-loader.js +3 -40
  60. package/dist/src/integrations/ralph/learning/pattern-learner.js +1 -20
  61. package/dist/src/integrations/ralph/swarm/swarm-coordinator.js +0 -2
  62. package/dist/src/mcp/stackmemory-mcp-server.js +315 -0
  63. package/dist/src/orchestrators/multimodal/determinism.js +243 -0
  64. package/dist/src/orchestrators/multimodal/harness.js +147 -77
  65. package/dist/src/orchestrators/multimodal/providers.js +44 -3
  66. package/dist/src/utils/hook-installer.js +0 -8
  67. package/package.json +10 -1
  68. package/packs/coding/python-fastapi/instructions.md +60 -0
  69. package/packs/coding/python-fastapi/pack.yaml +28 -0
  70. package/packs/coding/typescript-react/instructions.md +47 -0
  71. package/packs/coding/typescript-react/pack.yaml +28 -0
  72. package/packs/core/commands/capture.md +32 -0
  73. package/packs/core/commands/learn.md +73 -0
  74. package/packs/core/commands/next.md +36 -0
  75. package/packs/core/commands/restart.md +58 -0
  76. package/packs/core/commands/restore.md +29 -0
  77. package/packs/core/commands/start.md +57 -0
  78. package/packs/core/commands/stop.md +65 -0
  79. package/packs/core/commands/summary.md +40 -0
  80. package/packs/core/manifest.json +24 -0
  81. package/packs/ops/decision-recovery/instructions.md +65 -0
  82. package/packs/ops/decision-recovery/pack.yaml +89 -0
  83. package/dist/src/cli/commands/team.js +0 -168
  84. package/dist/src/core/context/shared-context-layer.js +0 -620
  85. package/dist/src/core/context/stack-merge-resolver.js +0 -748
  86. package/dist/src/core/merge/conflict-detector.js +0 -430
  87. package/dist/src/core/merge/resolution-engine.js +0 -557
  88. package/dist/src/core/merge/stack-diff.js +0 -531
  89. package/dist/src/core/merge/unified-merge-resolver.js +0 -302
  90. package/dist/src/integrations/mcp/handlers/cord-handlers.js +0 -397
  91. package/dist/src/integrations/mcp/handlers/team-handlers.js +0 -211
  92. /package/dist/src/core/{merge → cache}/types.js +0 -0
@@ -0,0 +1,126 @@
1
+ import { fileURLToPath as __fileURLToPath } from 'url';
2
+ import { dirname as __pathDirname } from 'path';
3
+ const __filename = __fileURLToPath(import.meta.url);
4
+ const __dirname = __pathDirname(__filename);
5
+ import { existsSync } from "fs";
6
+ import { join } from "path";
7
+ import { homedir } from "os";
8
+ import { refreshCurrentRepoPullRequestState } from "../../integrations/github/pr-state.js";
9
+ import { canonicalStateStore } from "../../core/shared-state/canonical-store.js";
10
+ class DaemonGitHubService {
11
+ config;
12
+ state;
13
+ intervalId;
14
+ isRunning = false;
15
+ onLog;
16
+ constructor(config, onLog) {
17
+ this.config = config;
18
+ this.onLog = onLog;
19
+ this.state = {
20
+ lastSyncTime: 0,
21
+ syncCount: 0,
22
+ errors: []
23
+ };
24
+ }
25
+ async start() {
26
+ if (this.isRunning || !this.config.enabled) {
27
+ return;
28
+ }
29
+ if (!this.isGitHubConfigured()) {
30
+ this.onLog("WARN", "GitHub CLI not configured, skipping github service");
31
+ return;
32
+ }
33
+ this.isRunning = true;
34
+ const intervalMs = this.config.interval * 60 * 1e3;
35
+ this.onLog("INFO", "GitHub service started", {
36
+ interval: this.config.interval
37
+ });
38
+ await this.performSync();
39
+ this.intervalId = setInterval(async () => {
40
+ await this.performSync();
41
+ }, intervalMs);
42
+ }
43
+ stop() {
44
+ if (this.intervalId) {
45
+ clearInterval(this.intervalId);
46
+ this.intervalId = void 0;
47
+ }
48
+ this.isRunning = false;
49
+ this.onLog("INFO", "GitHub service stopped");
50
+ }
51
+ getState() {
52
+ return {
53
+ ...this.state,
54
+ nextSyncTime: this.isRunning ? this.state.lastSyncTime + this.config.interval * 60 * 1e3 : void 0
55
+ };
56
+ }
57
+ async forceSync() {
58
+ await this.performSync();
59
+ }
60
+ async performSync() {
61
+ if (!this.isRunning) return;
62
+ try {
63
+ const projectRoots = await this.getProjectRoots();
64
+ this.state.lastProjectsScanned = projectRoots.length;
65
+ if (projectRoots.length === 0) {
66
+ this.onLog("DEBUG", "No active project roots found for GitHub sync");
67
+ return;
68
+ }
69
+ let synced = false;
70
+ for (const projectRoot of projectRoots) {
71
+ const projection = await refreshCurrentRepoPullRequestState(projectRoot);
72
+ if (!projection) {
73
+ this.onLog("DEBUG", "No GitHub PR projection available", {
74
+ projectRoot
75
+ });
76
+ continue;
77
+ }
78
+ synced = true;
79
+ this.state.syncCount++;
80
+ this.state.lastSyncTime = Date.now();
81
+ this.state.lastProjectionState = projection.state;
82
+ this.onLog("INFO", "GitHub PR projection refreshed", {
83
+ projectRoot,
84
+ repo: projection.repo,
85
+ branch: projection.branch,
86
+ prNumber: projection.prNumber,
87
+ state: projection.state
88
+ });
89
+ }
90
+ if (!synced) {
91
+ this.state.lastSyncTime = Date.now();
92
+ }
93
+ } catch (err) {
94
+ const errorMsg = err instanceof Error ? err.message : String(err);
95
+ this.state.errors.push(errorMsg);
96
+ this.onLog("ERROR", "GitHub sync failed", { error: errorMsg });
97
+ if (this.state.errors.length > 10) {
98
+ this.state.errors = this.state.errors.slice(-10);
99
+ }
100
+ }
101
+ }
102
+ isGitHubConfigured() {
103
+ try {
104
+ return existsSync(join(homedir(), ".config", "gh", "hosts.yml"));
105
+ } catch {
106
+ return false;
107
+ }
108
+ }
109
+ async getProjectRoots() {
110
+ const roots = /* @__PURE__ */ new Set();
111
+ const activeProjectPaths = await canonicalStateStore.listActiveProjectPaths();
112
+ for (const projectPath of activeProjectPaths) {
113
+ if (existsSync(join(projectPath, ".git"))) {
114
+ roots.add(projectPath);
115
+ }
116
+ }
117
+ const cwd = process.cwd();
118
+ if (existsSync(join(cwd, ".git"))) {
119
+ roots.add(cwd);
120
+ }
121
+ return Array.from(roots).sort();
122
+ }
123
+ }
124
+ export {
125
+ DaemonGitHubService
126
+ };
@@ -18,6 +18,7 @@ import {
18
18
  } from "./daemon-config.js";
19
19
  import { DaemonContextService } from "./services/context-service.js";
20
20
  import { DaemonLinearService } from "./services/linear-service.js";
21
+ import { DaemonGitHubService } from "./services/github-service.js";
21
22
  import { DaemonMaintenanceService } from "./services/maintenance-service.js";
22
23
  import { DaemonMemoryService } from "./services/memory-service.js";
23
24
  class UnifiedDaemon {
@@ -25,6 +26,7 @@ class UnifiedDaemon {
25
26
  paths;
26
27
  contextService;
27
28
  linearService;
29
+ githubService;
28
30
  maintenanceService;
29
31
  memoryService;
30
32
  heartbeatInterval;
@@ -41,6 +43,10 @@ class UnifiedDaemon {
41
43
  this.config.linear,
42
44
  (level, msg, data) => this.log(level, "linear", msg, data)
43
45
  );
46
+ this.githubService = new DaemonGitHubService(
47
+ this.config.github,
48
+ (level, msg, data) => this.log(level, "github", msg, data)
49
+ );
44
50
  this.maintenanceService = new DaemonMaintenanceService(
45
51
  this.config.maintenance,
46
52
  (level, msg, data) => this.log(level, "maintenance", msg, data)
@@ -101,6 +107,7 @@ class UnifiedDaemon {
101
107
  updateStatus() {
102
108
  const maintenanceState = this.maintenanceService.getState();
103
109
  const memoryState = this.memoryService.getState();
110
+ const githubState = this.githubService.getState();
104
111
  const status = {
105
112
  running: true,
106
113
  pid: process.pid,
@@ -117,6 +124,12 @@ class UnifiedDaemon {
117
124
  lastRun: this.linearService.getState().lastSyncTime || void 0,
118
125
  syncCount: this.linearService.getState().syncCount
119
126
  },
127
+ github: {
128
+ enabled: this.config.github.enabled,
129
+ lastRun: githubState.lastSyncTime || void 0,
130
+ syncCount: githubState.syncCount,
131
+ lastProjectionState: githubState.lastProjectionState
132
+ },
120
133
  maintenance: {
121
134
  enabled: this.config.maintenance.enabled,
122
135
  lastRun: maintenanceState.lastRunTime || void 0,
@@ -139,6 +152,7 @@ class UnifiedDaemon {
139
152
  errors: [
140
153
  ...this.contextService.getState().errors.slice(-5),
141
154
  ...this.linearService.getState().errors.slice(-5),
155
+ ...githubState.errors.slice(-5),
142
156
  ...maintenanceState.errors.slice(-5),
143
157
  ...memoryState.errors.slice(-5)
144
158
  ]
@@ -190,6 +204,11 @@ class UnifiedDaemon {
190
204
  enabled: false,
191
205
  syncCount: this.linearService.getState().syncCount
192
206
  },
207
+ github: {
208
+ enabled: false,
209
+ syncCount: this.githubService.getState().syncCount,
210
+ lastProjectionState: this.githubService.getState().lastProjectionState
211
+ },
193
212
  maintenance: {
194
213
  enabled: false,
195
214
  staleFramesCleaned: this.maintenanceService.getState().staleFramesCleaned,
@@ -213,6 +232,7 @@ class UnifiedDaemon {
213
232
  uptime: Date.now() - this.startTime,
214
233
  contextSaves: this.contextService.getState().saveCount,
215
234
  linearSyncs: this.linearService.getState().syncCount,
235
+ githubSyncs: this.githubService.getState().syncCount,
216
236
  maintenanceRuns: this.maintenanceService.getState().ftsRebuilds,
217
237
  memoryTriggers: this.memoryService.getState().triggerCount
218
238
  });
@@ -222,6 +242,7 @@ class UnifiedDaemon {
222
242
  }
223
243
  this.contextService.stop();
224
244
  this.linearService.stop();
245
+ this.githubService.stop();
225
246
  this.maintenanceService.stop();
226
247
  this.memoryService.stop();
227
248
  this.cleanup();
@@ -241,6 +262,7 @@ class UnifiedDaemon {
241
262
  config: {
242
263
  context: this.config.context.enabled,
243
264
  linear: this.config.linear.enabled,
265
+ github: this.config.github.enabled,
244
266
  maintenance: this.config.maintenance.enabled,
245
267
  memory: this.config.memory.enabled,
246
268
  fileWatch: this.config.fileWatch.enabled
@@ -248,6 +270,7 @@ class UnifiedDaemon {
248
270
  });
249
271
  this.contextService.start();
250
272
  await this.linearService.start();
273
+ await this.githubService.start();
251
274
  this.maintenanceService.start();
252
275
  this.memoryService.start();
253
276
  this.heartbeatInterval = setInterval(() => {
@@ -258,6 +281,7 @@ class UnifiedDaemon {
258
281
  getStatus() {
259
282
  const maintenanceState = this.maintenanceService.getState();
260
283
  const memoryState = this.memoryService.getState();
284
+ const githubState = this.githubService.getState();
261
285
  return {
262
286
  running: !this.isShuttingDown,
263
287
  pid: process.pid,
@@ -274,6 +298,12 @@ class UnifiedDaemon {
274
298
  lastRun: this.linearService.getState().lastSyncTime || void 0,
275
299
  syncCount: this.linearService.getState().syncCount
276
300
  },
301
+ github: {
302
+ enabled: this.config.github.enabled,
303
+ lastRun: githubState.lastSyncTime || void 0,
304
+ syncCount: githubState.syncCount,
305
+ lastProjectionState: githubState.lastProjectionState
306
+ },
277
307
  maintenance: {
278
308
  enabled: this.config.maintenance.enabled,
279
309
  lastRun: maintenanceState.lastRunTime || void 0,
@@ -30,7 +30,9 @@ class PtyWrapper {
30
30
  claudeBin: config.claudeBin || this.findClaude(),
31
31
  claudeArgs: config.claudeArgs || [],
32
32
  stateFile: config.stateFile || getSweepPath("sweep-state.json"),
33
- initialInput: config.initialInput || ""
33
+ initialInput: config.initialInput || "",
34
+ onExit: config.onExit || (() => void 0),
35
+ onSignal: config.onSignal || (() => void 0)
34
36
  };
35
37
  this.stateWatcher = new SweepStateWatcher(this.config.stateFile);
36
38
  this.statusBar = new StatusBar();
@@ -115,8 +117,9 @@ class PtyWrapper {
115
117
  this.ptyProcess?.resize(newCols, newRows - 1);
116
118
  this.statusBar.resize(newRows, newCols);
117
119
  });
118
- this.ptyProcess.onExit(({ exitCode }) => {
120
+ this.ptyProcess.onExit(async ({ exitCode }) => {
119
121
  this.cleanup();
122
+ await this.config.onExit(exitCode);
120
123
  if (process.env["LINEAR_API_KEY"]) {
121
124
  try {
122
125
  execSync("stackmemory linear sync", {
@@ -128,12 +131,17 @@ class PtyWrapper {
128
131
  }
129
132
  process.exit(exitCode);
130
133
  });
131
- const onSignal = () => {
134
+ const onSignal = async (signal) => {
132
135
  this.cleanup();
136
+ await this.config.onSignal(signal);
133
137
  process.exit(0);
134
138
  };
135
- process.on("SIGINT", onSignal);
136
- process.on("SIGTERM", onSignal);
139
+ process.on("SIGINT", () => {
140
+ void onSignal("SIGINT");
141
+ });
142
+ process.on("SIGTERM", () => {
143
+ void onSignal("SIGTERM");
144
+ });
137
145
  }
138
146
  acceptPrediction() {
139
147
  if (!this.currentPrediction || !this.ptyProcess) return;
@@ -19,6 +19,7 @@ const ModelProviderSchema = z.enum([
19
19
  "cerebras",
20
20
  "deepinfra",
21
21
  "openrouter",
22
+ "moonshot",
22
23
  "anthropic-batch",
23
24
  "custom"
24
25
  ]);
@@ -59,6 +60,7 @@ const ModelRouterConfigSchema = z.object({
59
60
  cerebras: ModelConfigSchema.optional(),
60
61
  deepinfra: ModelConfigSchema.optional(),
61
62
  openrouter: ModelConfigSchema.optional(),
63
+ moonshot: ModelConfigSchema.optional(),
62
64
  "anthropic-batch": ModelConfigSchema.optional(),
63
65
  custom: ModelConfigSchema.optional()
64
66
  }).optional().default({}),
@@ -17,6 +17,17 @@ import {
17
17
  createProvider
18
18
  } from "../../core/extensions/provider-adapter.js";
19
19
  import { AnthropicBatchClient } from "../anthropic/batch-client.js";
20
+ const QUOTA_ERROR_PATTERNS = [
21
+ /rate.?limit/i,
22
+ /quota.?exceeded/i,
23
+ /too many requests/i,
24
+ /429/,
25
+ /capacity/i,
26
+ /billing/i,
27
+ /usage.?limit/i,
28
+ /plan.?limit/i,
29
+ /max.*requests/i
30
+ ];
20
31
  class ClaudeCodeSubagentClient {
21
32
  tempDir;
22
33
  mockMode;
@@ -108,6 +119,12 @@ class ClaudeCodeSubagentClient {
108
119
  tokens: result.usage.inputTokens + result.usage.outputTokens
109
120
  };
110
121
  } catch (error) {
122
+ if (optimal.provider === "anthropic" && this.isQuotaError(error.message)) {
123
+ logger.warn("Anthropic API quota hit, overflowing to Kimi", {
124
+ error: error.message
125
+ });
126
+ return this.executeKimiOverflow(request, startTime, subagentId);
127
+ }
111
128
  logger.warn(`Direct API failed for ${optimal.provider}, falling back`, {
112
129
  error: error.message
113
130
  });
@@ -176,6 +193,13 @@ Context (JSON): ${JSON.stringify(request.context)}`;
176
193
  tokens: this.estimateTokens(fullPrompt + result.text)
177
194
  };
178
195
  } catch (error) {
196
+ if (this.isQuotaError(error.message)) {
197
+ logger.warn("Claude quota/rate limit hit, overflowing to Kimi", {
198
+ subagentId,
199
+ error: error.message
200
+ });
201
+ return this.executeKimiOverflow(request, startTime, subagentId);
202
+ }
179
203
  logger.error(`Subagent CLI execution failed: ${request.type}`, {
180
204
  error,
181
205
  subagentId
@@ -210,6 +234,71 @@ Context (JSON): ${JSON.stringify(request.context)}`;
210
234
  }
211
235
  });
212
236
  }
237
+ /**
238
+ * Check if an error message indicates quota/rate limit exhaustion
239
+ */
240
+ isQuotaError(message) {
241
+ return QUOTA_ERROR_PATTERNS.some((pattern) => pattern.test(message));
242
+ }
243
+ /**
244
+ * Execute via Kimi/Moonshot API as overflow when Claude quota is exhausted.
245
+ * Uses OpenAI-compatible API at api.moonshot.ai/v1.
246
+ */
247
+ async executeKimiOverflow(request, startTime, subagentId) {
248
+ const apiKey = process.env["MOONSHOT_API_KEY"] || "";
249
+ if (!apiKey) {
250
+ logger.warn("No MOONSHOT_API_KEY set, cannot overflow to Kimi");
251
+ return {
252
+ success: false,
253
+ result: null,
254
+ error: "Claude quota exceeded and no MOONSHOT_API_KEY configured",
255
+ duration: Date.now() - startTime,
256
+ subagentType: request.type
257
+ };
258
+ }
259
+ try {
260
+ const adapter = createProvider("moonshot", {
261
+ apiKey,
262
+ baseUrl: "https://api.moonshot.ai/v1"
263
+ });
264
+ const prompt = this.buildSubagentPrompt(request);
265
+ const result = await adapter.complete(
266
+ [{ role: "user", content: prompt }],
267
+ { model: "kimi-k2.6", maxTokens: 8192 }
268
+ );
269
+ const text = result.content.filter((c) => c.type === "text").map((c) => c.text).join("");
270
+ let parsed;
271
+ try {
272
+ parsed = JSON.parse(text);
273
+ } catch {
274
+ parsed = { rawOutput: text };
275
+ }
276
+ logger.info("Kimi overflow completed", {
277
+ subagentId,
278
+ tokens: result.usage.inputTokens + result.usage.outputTokens
279
+ });
280
+ return {
281
+ success: true,
282
+ result: parsed,
283
+ output: text,
284
+ duration: Date.now() - startTime,
285
+ subagentType: request.type,
286
+ tokens: result.usage.inputTokens + result.usage.outputTokens
287
+ };
288
+ } catch (kimiError) {
289
+ logger.error("Kimi overflow also failed", {
290
+ subagentId,
291
+ error: kimiError.message
292
+ });
293
+ return {
294
+ success: false,
295
+ result: null,
296
+ error: `Claude quota exceeded, Kimi fallback failed: ${kimiError.message}`,
297
+ duration: Date.now() - startTime,
298
+ subagentType: request.type
299
+ };
300
+ }
301
+ }
213
302
  /**
214
303
  * Build subagent prompt based on type
215
304
  */
@@ -0,0 +1,158 @@
1
+ import { fileURLToPath as __fileURLToPath } from 'url';
2
+ import { dirname as __pathDirname } from 'path';
3
+ const __filename = __fileURLToPath(import.meta.url);
4
+ const __dirname = __pathDirname(__filename);
5
+ import { execFileSync } from "child_process";
6
+ import {
7
+ canonicalStateStore
8
+ } from "../../core/shared-state/canonical-store.js";
9
+ import { projectIdFromIdentifier } from "../../core/shared-state/canonical-store.js";
10
+ function runGit(args, cwd) {
11
+ return execFileSync("git", args, {
12
+ cwd,
13
+ encoding: "utf8",
14
+ stdio: ["ignore", "pipe", "pipe"]
15
+ }).trim();
16
+ }
17
+ function runGh(args, cwd) {
18
+ return execFileSync("gh", args, {
19
+ cwd,
20
+ encoding: "utf8",
21
+ stdio: ["ignore", "pipe", "pipe"]
22
+ }).trim();
23
+ }
24
+ function normalizeRemoteToRepo(remote) {
25
+ const cleaned = remote.replace(/\.git$/, "").trim();
26
+ if (cleaned.startsWith("git@github.com:")) {
27
+ return cleaned.replace("git@github.com:", "");
28
+ }
29
+ if (cleaned.startsWith("https://github.com/")) {
30
+ return cleaned.replace("https://github.com/", "");
31
+ }
32
+ if (cleaned.startsWith("http://github.com/")) {
33
+ return cleaned.replace("http://github.com/", "");
34
+ }
35
+ throw new Error(`Unsupported GitHub remote: ${remote}`);
36
+ }
37
+ function summarizeStatusCheckRollup(rollup) {
38
+ if (!rollup || rollup.length === 0) {
39
+ return void 0;
40
+ }
41
+ const states = rollup.map((item) => item.conclusion || item.status || item.state).filter(Boolean);
42
+ if (states.length === 0) {
43
+ return void 0;
44
+ }
45
+ if (states.every((state) => state === "SUCCESS")) {
46
+ return "SUCCESS";
47
+ }
48
+ if (states.some((state) => state === "FAILURE" || state === "ERROR")) {
49
+ return "FAILURE";
50
+ }
51
+ if (states.some(
52
+ (state) => state === "PENDING" || state === "IN_PROGRESS" || state === "EXPECTED"
53
+ )) {
54
+ return "PENDING";
55
+ }
56
+ return states[0];
57
+ }
58
+ function getCurrentRepoGitHubInfo(cwd = process.cwd()) {
59
+ try {
60
+ const projectPath = runGit(["rev-parse", "--show-toplevel"], cwd);
61
+ const branch = runGit(["rev-parse", "--abbrev-ref", "HEAD"], projectPath);
62
+ const remote = runGit(
63
+ ["config", "--get", "remote.origin.url"],
64
+ projectPath
65
+ );
66
+ const repo = normalizeRemoteToRepo(remote);
67
+ return {
68
+ repo,
69
+ branch,
70
+ projectPath,
71
+ projectId: projectIdFromIdentifier(remote)
72
+ };
73
+ } catch {
74
+ return null;
75
+ }
76
+ }
77
+ async function refreshCurrentRepoPullRequestState(cwd = process.cwd()) {
78
+ const info = getCurrentRepoGitHubInfo(cwd);
79
+ if (!info) {
80
+ return null;
81
+ }
82
+ try {
83
+ const output = runGh(
84
+ [
85
+ "pr",
86
+ "view",
87
+ "--repo",
88
+ info.repo,
89
+ "--json",
90
+ [
91
+ "number",
92
+ "title",
93
+ "state",
94
+ "isDraft",
95
+ "url",
96
+ "baseRefName",
97
+ "headRefName",
98
+ "headRefOid",
99
+ "mergedAt",
100
+ "updatedAt",
101
+ "reviewDecision",
102
+ "statusCheckRollup"
103
+ ].join(",")
104
+ ],
105
+ info.projectPath
106
+ );
107
+ const parsed = JSON.parse(output);
108
+ const projection = {
109
+ repo: info.repo,
110
+ branch: info.branch,
111
+ projectId: info.projectId,
112
+ projectPath: info.projectPath,
113
+ prNumber: parsed.number,
114
+ title: parsed.title,
115
+ state: parsed.mergedAt && parsed.state === "MERGED" ? "MERGED" : parsed.state,
116
+ isDraft: parsed.isDraft,
117
+ url: parsed.url,
118
+ baseRefName: parsed.baseRefName,
119
+ headRefName: parsed.headRefName,
120
+ headRefOid: parsed.headRefOid,
121
+ mergedAt: parsed.mergedAt || void 0,
122
+ updatedAt: parsed.updatedAt,
123
+ reviewDecision: parsed.reviewDecision || void 0,
124
+ statusCheckRollup: summarizeStatusCheckRollup(parsed.statusCheckRollup),
125
+ lastSyncedAt: Date.now()
126
+ };
127
+ await canonicalStateStore.saveGitHubPullRequest(projection);
128
+ if (projection.state === "MERGED" || projection.state === "CLOSED") {
129
+ await canonicalStateStore.releaseClaims({
130
+ projectId: info.projectId,
131
+ projectPath: info.projectPath,
132
+ branch: info.branch,
133
+ reason: `github_pr_${projection.state.toLowerCase()}`
134
+ });
135
+ }
136
+ await canonicalStateStore.appendEvent({
137
+ type: "github_pr_refreshed",
138
+ tool: "stackmemory",
139
+ projectId: info.projectId,
140
+ projectPath: info.projectPath,
141
+ branch: info.branch,
142
+ payload: {
143
+ repo: info.repo,
144
+ prNumber: projection.prNumber,
145
+ state: projection.state,
146
+ reviewDecision: projection.reviewDecision,
147
+ statusCheckRollup: projection.statusCheckRollup
148
+ }
149
+ });
150
+ return projection;
151
+ } catch {
152
+ return null;
153
+ }
154
+ }
155
+ export {
156
+ getCurrentRepoGitHubInfo,
157
+ refreshCurrentRepoPullRequestState
158
+ };
@@ -496,7 +496,10 @@ class LinearClient {
496
496
  filter: Object.keys(filter).length > 0 ? filter : void 0,
497
497
  first: options?.limit || 50
498
498
  });
499
- return result.issues.nodes;
499
+ return result.issues.nodes.map((issue) => ({
500
+ ...issue,
501
+ labels: Array.isArray(issue.labels) ? issue.labels : issue.labels?.nodes || []
502
+ }));
500
503
  }
501
504
  /**
502
505
  * Assign an issue to a user