@stackmemoryai/stackmemory 1.3.2 → 1.4.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.
@@ -3,14 +3,15 @@ import { dirname as __pathDirname } from 'path';
3
3
  const __filename = __fileURLToPath(import.meta.url);
4
4
  const __dirname = __pathDirname(__filename);
5
5
  import { Command } from "commander";
6
+ import { execSync } from "child_process";
6
7
  import { existsSync, mkdirSync, writeFileSync } from "fs";
7
8
  import { join } from "path";
8
9
  import { homedir, tmpdir } from "os";
9
10
  import Database from "better-sqlite3";
10
11
  import { logger } from "../../core/monitoring/logger.js";
11
- import { SymphonyOrchestrator } from "./symphony-orchestrator.js";
12
+ import { Conductor } from "./orchestrator.js";
12
13
  function getGlobalStorePath() {
13
- const dir = join(homedir(), ".stackmemory", "symphony");
14
+ const dir = join(homedir(), ".stackmemory", "conductor");
14
15
  if (!existsSync(dir)) {
15
16
  mkdirSync(dir, { recursive: true });
16
17
  }
@@ -42,9 +43,9 @@ function getGlobalDb() {
42
43
  `);
43
44
  return db;
44
45
  }
45
- function createSymphonyCommands() {
46
- const cmd = new Command("symphony");
47
- cmd.description("Symphony orchestrator integration commands");
46
+ function createConductorCommands() {
47
+ const cmd = new Command("conductor");
48
+ cmd.description("Conductor \u2014 autonomous agent orchestration via Linear");
48
49
  cmd.command("capture").description("Capture workspace context after an agent run").requiredOption("--issue <id>", "Issue identifier (e.g., STA-476)").option("--workspace <path>", "Workspace directory", process.cwd()).option("--attempt <n>", "Attempt number", "1").action(async (options) => {
49
50
  const workspace = options.workspace;
50
51
  const issueId = options.issue;
@@ -54,16 +55,20 @@ function createSymphonyCommands() {
54
55
  let framesJson = "[]";
55
56
  let anchorsJson = "[]";
56
57
  let eventsJson = "[]";
58
+ let frameCount = 0;
59
+ let anchorCount = 0;
57
60
  if (existsSync(dbPath)) {
58
61
  try {
59
62
  const db = new Database(dbPath, { readonly: true });
60
63
  const frames = db.prepare(
61
64
  "SELECT frame_id, name, type, digest_text, created_at FROM frames ORDER BY created_at DESC LIMIT 20"
62
65
  ).all();
66
+ frameCount = frames.length;
63
67
  framesJson = JSON.stringify(frames);
64
68
  const anchors = db.prepare(
65
69
  "SELECT anchor_id, type, text, priority FROM anchors WHERE type IN ('DECISION', 'FACT', 'CONSTRAINT', 'RISK') ORDER BY priority DESC LIMIT 30"
66
70
  ).all();
71
+ anchorCount = anchors.length;
67
72
  anchorsJson = JSON.stringify(anchors);
68
73
  const events = db.prepare(
69
74
  "SELECT event_type, payload, ts FROM events ORDER BY ts DESC LIMIT 50"
@@ -80,7 +85,6 @@ function createSymphonyCommands() {
80
85
  }
81
86
  let metadata = { workspace, attempt };
82
87
  try {
83
- const { execSync } = await import("child_process");
84
88
  const branch = execSync("git rev-parse --abbrev-ref HEAD", {
85
89
  cwd: workspace,
86
90
  encoding: "utf8",
@@ -113,8 +117,6 @@ function createSymphonyCommands() {
113
117
  JSON.stringify(metadata)
114
118
  );
115
119
  globalDb.close();
116
- const frameCount = JSON.parse(framesJson).length;
117
- const anchorCount = JSON.parse(anchorsJson).length;
118
120
  console.log(
119
121
  `Captured ${frameCount} frames, ${anchorCount} anchors for ${issueId} (attempt ${attempt})`
120
122
  );
@@ -124,7 +126,7 @@ function createSymphonyCommands() {
124
126
  const issueId = options.issue;
125
127
  const globalDbPath = join(getGlobalStorePath(), "context.db");
126
128
  if (!existsSync(globalDbPath)) {
127
- console.log("No prior Symphony context found");
129
+ console.log("No prior orchestrator context found");
128
130
  return;
129
131
  }
130
132
  const globalDb = getGlobalDb();
@@ -186,7 +188,7 @@ function createSymphonyCommands() {
186
188
  if (!existsSync(restoreDir)) {
187
189
  mkdirSync(restoreDir, { recursive: true });
188
190
  }
189
- const restorePath = join(restoreDir, "symphony-context.md");
191
+ const restorePath = join(restoreDir, "conductor-context.md");
190
192
  writeFileSync(restorePath, lines.join("\n"));
191
193
  globalDb.close();
192
194
  console.log(
@@ -227,10 +229,10 @@ function createSymphonyCommands() {
227
229
  `Archived ${frames.length} frames, ${anchors.length} anchors for ${issueId}`
228
230
  );
229
231
  });
230
- cmd.command("search").description("Search across all Symphony issue contexts").argument("<query>", "Search query").option("--limit <n>", "Max results", "10").action(async (query, options) => {
232
+ cmd.command("search").description("Search across all orchestrator issue contexts").argument("<query>", "Search query").option("--limit <n>", "Max results", "10").action(async (query, options) => {
231
233
  const globalDbPath = join(getGlobalStorePath(), "context.db");
232
234
  if (!existsSync(globalDbPath)) {
233
- console.log("No Symphony context database found");
235
+ console.log("No orchestrator context database found");
234
236
  return;
235
237
  }
236
238
  const limit = parseInt(options.limit, 10);
@@ -261,7 +263,7 @@ function createSymphonyCommands() {
261
263
  }
262
264
  globalDb.close();
263
265
  });
264
- cmd.command("start").description("Start the Symphony orchestrator daemon").option("--team <id>", "Linear team ID").option(
266
+ cmd.command("start").description("Start the orchestrator daemon").option("--team <id>", "Linear team ID").option(
265
267
  "--states <states>",
266
268
  "Comma-separated issue states to pick up",
267
269
  "Todo"
@@ -274,23 +276,23 @@ function createSymphonyCommands() {
274
276
  "State name for completed review",
275
277
  "In Review"
276
278
  ).option("--poll <ms>", "Polling interval in milliseconds", "30000").option("--concurrency <n>", "Max concurrent agents", "3").option("--workspace-root <path>", "Workspace root directory").option("--repo <path>", "Git repo root for worktrees", process.cwd()).option("--branch <name>", "Base branch for worktrees", "main").option("--retries <n>", "Max retries per issue", "1").option("--turn-timeout <ms>", "Agent turn timeout in ms", "3600000").action(async (options) => {
277
- const orchestrator = new SymphonyOrchestrator({
279
+ const conductor = new Conductor({
278
280
  teamId: options.team,
279
281
  activeStates: options.states.split(",").map((s) => s.trim()),
280
282
  inProgressState: options.inProgress,
281
283
  inReviewState: options.inReview,
282
284
  pollIntervalMs: parseInt(options.poll, 10),
283
285
  maxConcurrent: parseInt(options.concurrency, 10),
284
- workspaceRoot: options.workspaceRoot || join(tmpdir(), "symphony_workspaces"),
286
+ workspaceRoot: options.workspaceRoot || join(tmpdir(), "conductor_workspaces"),
285
287
  repoRoot: options.repo,
286
288
  baseBranch: options.branch,
287
289
  maxRetries: parseInt(options.retries, 10),
288
290
  turnTimeoutMs: parseInt(options.turnTimeout, 10)
289
291
  });
290
- await orchestrator.start();
292
+ await conductor.start();
291
293
  });
292
294
  return cmd;
293
295
  }
294
296
  export {
295
- createSymphonyCommands
297
+ createConductorCommands
296
298
  };
@@ -11,6 +11,11 @@ import {
11
11
  LinearClient
12
12
  } from "../../integrations/linear/client.js";
13
13
  import { LinearAuthManager } from "../../integrations/linear/auth.js";
14
+ import {
15
+ PreflightChecker
16
+ } from "../../core/worktree/preflight.js";
17
+ import { ContextCapture } from "../../core/worktree/capture.js";
18
+ import { extractKeywords } from "../../core/utils/text.js";
14
19
  const DEFAULT_CONFIG = {
15
20
  activeStates: ["Todo"],
16
21
  terminalStates: ["Done", "Cancelled", "Canceled", "Closed"],
@@ -18,7 +23,7 @@ const DEFAULT_CONFIG = {
18
23
  inReviewState: "In Review",
19
24
  pollIntervalMs: 3e4,
20
25
  maxConcurrent: 3,
21
- workspaceRoot: join(tmpdir(), "symphony_workspaces"),
26
+ workspaceRoot: join(tmpdir(), "conductor_workspaces"),
22
27
  repoRoot: process.cwd(),
23
28
  baseBranch: "main",
24
29
  appServerPath: join(
@@ -27,14 +32,14 @@ const DEFAULT_CONFIG = {
27
32
  "..",
28
33
  "..",
29
34
  "scripts",
30
- "symphony",
35
+ "conductor",
31
36
  "claude-app-server.cjs"
32
37
  ),
33
38
  turnTimeoutMs: 36e5,
34
39
  maxRetries: 1,
35
40
  hookTimeoutMs: 6e4
36
41
  };
37
- class SymphonyOrchestrator {
42
+ class Conductor {
38
43
  config;
39
44
  client = null;
40
45
  running = /* @__PURE__ */ new Map();
@@ -47,8 +52,16 @@ class SymphonyOrchestrator {
47
52
  completeCount = 0;
48
53
  stopping = false;
49
54
  stateCache = /* @__PURE__ */ new Map();
55
+ activeStatesLower;
56
+ terminalStatesLower;
50
57
  constructor(config = {}) {
51
58
  this.config = { ...DEFAULT_CONFIG, ...config };
59
+ this.activeStatesLower = this.config.activeStates.map(
60
+ (s) => s.trim().toLowerCase()
61
+ );
62
+ this.terminalStatesLower = this.config.terminalStates.map(
63
+ (s) => s.trim().toLowerCase()
64
+ );
52
65
  }
53
66
  /**
54
67
  * Start the orchestrator loop.
@@ -77,14 +90,14 @@ class SymphonyOrchestrator {
77
90
  }
78
91
  this.client = await this.createLinearClient();
79
92
  await this.cacheWorkflowStates();
80
- logger.info("Symphony orchestrator started", {
93
+ logger.info("Orchestrator started", {
81
94
  activeStates: this.config.activeStates,
82
95
  maxConcurrent: this.config.maxConcurrent,
83
96
  pollIntervalMs: this.config.pollIntervalMs,
84
97
  workspaceRoot: this.config.workspaceRoot
85
98
  });
86
99
  console.log(
87
- `Symphony started \u2014 polling every ${this.config.pollIntervalMs / 1e3}s, max ${this.config.maxConcurrent} concurrent`
100
+ `Orchestrator started \u2014 polling every ${this.config.pollIntervalMs / 1e3}s, max ${this.config.maxConcurrent} concurrent`
88
101
  );
89
102
  const shutdown = () => this.stop();
90
103
  process.on("SIGINT", shutdown);
@@ -103,8 +116,8 @@ class SymphonyOrchestrator {
103
116
  async stop() {
104
117
  if (this.stopping) return;
105
118
  this.stopping = true;
106
- console.log("\nSymphony stopping...");
107
- logger.info("Symphony orchestrator stopping", {
119
+ console.log("\nOrchestrator stopping...");
120
+ logger.info("Orchestrator stopping", {
108
121
  runningCount: this.running.size
109
122
  });
110
123
  if (this.pollTimer) {
@@ -132,7 +145,7 @@ class SymphonyOrchestrator {
132
145
  this.running.clear();
133
146
  this.claimed.clear();
134
147
  console.log(
135
- `Symphony stopped. Completed: ${this.completeCount}, Failed: ${this.failCount}`
148
+ `Orchestrator stopped. Completed: ${this.completeCount}, Failed: ${this.failCount}`
136
149
  );
137
150
  }
138
151
  /**
@@ -185,10 +198,12 @@ class SymphonyOrchestrator {
185
198
  (issue) => !this.claimed.has(issue.id) && !this.completed.has(issue.id)
186
199
  );
187
200
  if (eligible.length === 0) return;
188
- const toDispatch = eligible.sort((a, b) => (a.priority || 4) - (b.priority || 4)).slice(0, available);
201
+ const sorted = eligible.sort((a, b) => (a.priority || 4) - (b.priority || 4)).slice(0, available);
202
+ const toDispatch = this.preflightFilter(sorted);
189
203
  logger.info("Dispatching issues", {
190
204
  count: toDispatch.length,
191
- identifiers: toDispatch.map((i) => i.identifier)
205
+ identifiers: toDispatch.map((i) => i.identifier),
206
+ skipped: sorted.length - toDispatch.length
192
207
  });
193
208
  for (const issue of toDispatch) {
194
209
  this.dispatch(issue).catch((err) => {
@@ -206,17 +221,81 @@ class SymphonyOrchestrator {
206
221
  teamId: this.config.teamId,
207
222
  limit: 50
208
223
  });
209
- const activeStatesLower = this.config.activeStates.map(
210
- (s) => s.trim().toLowerCase()
211
- );
212
224
  for (const issue of issues) {
213
225
  const stateName = issue.state.name.trim().toLowerCase();
214
- if (activeStatesLower.includes(stateName)) {
226
+ if (this.activeStatesLower.includes(stateName)) {
215
227
  allCandidates.push(issue);
216
228
  }
217
229
  }
218
230
  return allCandidates;
219
231
  }
232
+ // ── Pre-flight ──
233
+ /**
234
+ * Filter candidates against running issues using file overlap prediction.
235
+ * Returns only issues that are parallel-safe with currently running work.
236
+ */
237
+ preflightFilter(candidates) {
238
+ if (candidates.length === 0 || this.running.size === 0) {
239
+ return candidates;
240
+ }
241
+ try {
242
+ const checker = new PreflightChecker(this.config.repoRoot);
243
+ const runningFileSets = [];
244
+ const runningNames = [];
245
+ for (const run of this.running.values()) {
246
+ const task = {
247
+ name: run.issue.identifier,
248
+ description: run.issue.title,
249
+ keywords: this.extractIssueKeywords(run.issue)
250
+ };
251
+ runningFileSets.push(checker.predictFiles(task));
252
+ runningNames.push(run.issue.identifier);
253
+ }
254
+ const safe = [];
255
+ for (const candidate of candidates) {
256
+ const candidateTask = {
257
+ name: candidate.identifier,
258
+ description: candidate.title,
259
+ keywords: this.extractIssueKeywords(candidate)
260
+ };
261
+ const candidateFiles = checker.predictFiles(candidateTask);
262
+ const conflictFiles = [];
263
+ const conflictTasks = [];
264
+ for (let i = 0; i < runningFileSets.length; i++) {
265
+ const shared = [...candidateFiles].filter(
266
+ (f) => runningFileSets[i].has(f)
267
+ );
268
+ if (shared.length > 0) {
269
+ conflictFiles.push(...shared);
270
+ conflictTasks.push(runningNames[i]);
271
+ }
272
+ }
273
+ if (conflictFiles.length > 0) {
274
+ const uniqueFiles = [...new Set(conflictFiles)].slice(0, 3);
275
+ logger.info("Preflight: skipping conflicting issue", {
276
+ identifier: candidate.identifier,
277
+ conflictsWith: conflictTasks,
278
+ files: uniqueFiles
279
+ });
280
+ console.log(
281
+ `[${candidate.identifier}] Deferred \u2014 file overlap with running work (${uniqueFiles.join(", ")})`
282
+ );
283
+ } else {
284
+ safe.push(candidate);
285
+ }
286
+ }
287
+ return safe;
288
+ } catch (err) {
289
+ logger.warn("Preflight check failed, dispatching all", {
290
+ error: err.message
291
+ });
292
+ return candidates;
293
+ }
294
+ }
295
+ extractIssueKeywords(issue) {
296
+ const labelText = issue.labels.map((l) => l.name).join(" ");
297
+ return extractKeywords(`${issue.title} ${labelText}`, { maxCount: 8 });
298
+ }
220
299
  // ── Dispatch ──
221
300
  async dispatch(issue) {
222
301
  const issueId = issue.id;
@@ -242,6 +321,7 @@ class SymphonyOrchestrator {
242
321
  run.status = "completed";
243
322
  this.completeCount++;
244
323
  await this.runHook("after-run", workspacePath, issue, run.attempt);
324
+ this.takeSnapshot(workspacePath, issue);
245
325
  await this.transitionIssue(issue, this.config.inReviewState);
246
326
  console.log(`[${issue.identifier}] Completed successfully`);
247
327
  } catch (err) {
@@ -314,7 +394,7 @@ class SymphonyOrchestrator {
314
394
  });
315
395
  return wsPath;
316
396
  }
317
- const branchName = `symphony/${wsKey}`;
397
+ const branchName = `conductor/${wsKey}`;
318
398
  try {
319
399
  execSync("git fetch origin", {
320
400
  cwd: this.config.repoRoot,
@@ -398,8 +478,7 @@ class SymphonyOrchestrator {
398
478
  run.process = proc;
399
479
  let stderr = "";
400
480
  let turnCompleted = false;
401
- let timer;
402
- timer = setTimeout(() => {
481
+ const timer = setTimeout(() => {
403
482
  if (!turnCompleted) {
404
483
  logger.warn("Agent turn timeout", {
405
484
  identifier: issue.identifier,
@@ -485,14 +564,16 @@ class SymphonyOrchestrator {
485
564
  });
486
565
  }
487
566
  handleAgentMessage(msg, issue, _run) {
567
+ const params = msg.params;
488
568
  if (msg.method === "item/commandExecution/started") {
489
569
  logger.debug("Agent tool use", {
490
570
  identifier: issue.identifier,
491
- tool: msg.params?.tool
571
+ tool: params?.tool
492
572
  });
493
573
  }
494
574
  if (msg.method === "turn/completed") {
495
- const output = msg.params?.result?.output;
575
+ const result = params?.result;
576
+ const output = result?.output;
496
577
  if (Array.isArray(output)) {
497
578
  const text = output.filter((b) => b.type === "text").map((b) => b.text).join("\n");
498
579
  if (text) {
@@ -521,7 +602,7 @@ class SymphonyOrchestrator {
521
602
  if (attempt > 1) {
522
603
  lines.push(
523
604
  "",
524
- `This is attempt ${attempt}. Check .stackmemory/symphony-context.md for context from prior attempts.`
605
+ `This is attempt ${attempt}. Check .stackmemory/conductor-context.md for context from prior attempts.`
525
606
  );
526
607
  }
527
608
  lines.push(
@@ -538,12 +619,32 @@ class SymphonyOrchestrator {
538
619
  );
539
620
  return lines.join("\n");
540
621
  }
622
+ // ── Snapshot ──
623
+ takeSnapshot(workspacePath, issue) {
624
+ try {
625
+ const capture = new ContextCapture(workspacePath);
626
+ const result = capture.capture({
627
+ task: `${issue.identifier}: ${issue.title}`
628
+ });
629
+ logger.info("Snapshot captured", {
630
+ identifier: issue.identifier,
631
+ filesChanged: result.filesChanged.length,
632
+ filesCreated: result.filesCreated.length,
633
+ commits: result.commits.length
634
+ });
635
+ } catch (err) {
636
+ logger.warn("Snapshot capture failed", {
637
+ identifier: issue.identifier,
638
+ error: err.message
639
+ });
640
+ }
641
+ }
541
642
  // ── Hooks ──
542
643
  async runHook(hookName, workspacePath, issue, attempt) {
543
644
  const hookPath = join(
544
645
  this.config.repoRoot,
545
646
  "scripts",
546
- "symphony",
647
+ "conductor",
547
648
  `${hookName}.sh`
548
649
  );
549
650
  if (!existsSync(hookPath)) {
@@ -622,16 +723,12 @@ class SymphonyOrchestrator {
622
723
  // ── Reconciliation ──
623
724
  async reconcile() {
624
725
  if (!this.client || this.running.size === 0) return;
625
- const terminalLower = this.config.terminalStates.map(
626
- (s) => s.trim().toLowerCase()
627
- );
628
726
  for (const [issueId, run] of this.running) {
629
727
  try {
630
- const issues = await this.client.getIssues({ limit: 1 });
631
- const fresh = issues.find((i) => i.id === issueId);
728
+ const fresh = await this.client.getIssue(issueId);
632
729
  if (!fresh) continue;
633
730
  const currentState = fresh.state.name.trim().toLowerCase();
634
- if (terminalLower.includes(currentState)) {
731
+ if (this.terminalStatesLower.includes(currentState)) {
635
732
  logger.info(
636
733
  "Issue moved to terminal state externally, stopping agent",
637
734
  {
@@ -672,5 +769,5 @@ class SymphonyOrchestrator {
672
769
  }
673
770
  }
674
771
  export {
675
- SymphonyOrchestrator
772
+ Conductor
676
773
  };
@@ -0,0 +1,87 @@
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 { Command } from "commander";
6
+ import chalk from "chalk";
7
+ import {
8
+ PreflightChecker
9
+ } from "../../core/worktree/preflight.js";
10
+ function createPreflightCommand() {
11
+ const cmd = new Command("preflight").alias("pf").description("Check file overlap before running parallel tasks").argument("<tasks...>", "Task descriptions (quoted strings)").option(
12
+ "-k, --keywords <keywords>",
13
+ "Comma-separated keywords per task (task1:kw1,kw2;task2:kw3)"
14
+ ).option(
15
+ "-f, --files <files>",
16
+ "Comma-separated files per task (task1:file1,file2;task2:file3)"
17
+ ).option("--json", "Output as JSON").action((taskArgs, options) => {
18
+ const checker = new PreflightChecker();
19
+ const tasks = taskArgs.map((desc, i) => {
20
+ const task = {
21
+ name: `task-${i + 1}`,
22
+ description: desc
23
+ };
24
+ if (options.keywords) {
25
+ const kwParts = options.keywords.split(";");
26
+ if (kwParts[i]) {
27
+ const [, kws] = kwParts[i].split(":");
28
+ if (kws) task.keywords = kws.split(",");
29
+ }
30
+ }
31
+ if (options.files) {
32
+ const fileParts = options.files.split(";");
33
+ if (fileParts[i]) {
34
+ const [, fs] = fileParts[i].split(":");
35
+ if (fs) task.files = fs.split(",");
36
+ }
37
+ }
38
+ if (desc.length <= 40) {
39
+ task.name = desc;
40
+ }
41
+ return task;
42
+ });
43
+ const result = checker.check(tasks);
44
+ if (options.json) {
45
+ console.log(JSON.stringify(result, null, 2));
46
+ return;
47
+ }
48
+ console.log(chalk.cyan("\nPre-flight Check\n"));
49
+ console.log(chalk.gray(`Tasks: ${tasks.length}`));
50
+ console.log(chalk.gray(`Overlaps: ${result.allOverlaps.length}
51
+ `));
52
+ if (result.parallelSafe.length === 1 && result.allOverlaps.length === 0) {
53
+ console.log(chalk.green("All tasks are parallel-safe.\n"));
54
+ for (const task of result.parallelSafe[0]) {
55
+ console.log(chalk.green(` + ${task.name}`));
56
+ }
57
+ } else {
58
+ for (let i = 0; i < result.parallelSafe.length; i++) {
59
+ const group = result.parallelSafe[i];
60
+ console.log(chalk.cyan(`Group ${i + 1} (parallel-safe):`));
61
+ for (const task of group) {
62
+ console.log(chalk.green(` + ${task.name}`));
63
+ }
64
+ }
65
+ }
66
+ if (result.sequential.length > 0) {
67
+ console.log(chalk.yellow("\nSequential (file overlaps detected):"));
68
+ for (const entry of result.sequential) {
69
+ console.log(
70
+ chalk.yellow(` "${entry.task.name}" -> after "${entry.after}"`)
71
+ );
72
+ for (const overlap of entry.overlaps.slice(0, 5)) {
73
+ const conf = Math.round(overlap.confidence * 100);
74
+ console.log(
75
+ chalk.gray(` ${overlap.file} (${overlap.source}, ${conf}%)`)
76
+ );
77
+ }
78
+ }
79
+ }
80
+ console.log(chalk.gray(`
81
+ ${result.summary}`));
82
+ });
83
+ return cmd;
84
+ }
85
+ export {
86
+ createPreflightCommand
87
+ };
@@ -0,0 +1,89 @@
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 { Command } from "commander";
6
+ import chalk from "chalk";
7
+ import { ContextCapture } from "../../core/worktree/capture.js";
8
+ function createSnapshotCommand() {
9
+ const cmd = new Command("snapshot").alias("snap").description("Point-in-time snapshot of work (what changed and why)");
10
+ cmd.command("save").alias("s").description("Save a snapshot of current branch state").option("-t, --task <name>", "Task name or description").option(
11
+ "-b, --base <branch>",
12
+ "Base branch to diff against (default: auto-detect)"
13
+ ).option(
14
+ "-d, --decision <decisions...>",
15
+ "Key decisions made during this task"
16
+ ).option("--json", "Output as JSON").action((options) => {
17
+ const capture = new ContextCapture();
18
+ const result = capture.capture({
19
+ task: options.task,
20
+ baseBranch: options.base,
21
+ decisions: options.decision
22
+ });
23
+ if (options.json) {
24
+ console.log(JSON.stringify(result, null, 2));
25
+ return;
26
+ }
27
+ console.log(chalk.green("\nSnapshot saved.\n"));
28
+ console.log(chalk.gray(` Branch: ${result.branch}`));
29
+ console.log(chalk.gray(` Base: ${result.baseBranch}`));
30
+ console.log(chalk.gray(` Changed: ${result.filesChanged.length} files`));
31
+ console.log(chalk.gray(` Created: ${result.filesCreated.length} files`));
32
+ console.log(chalk.gray(` Deleted: ${result.filesDeleted.length} files`));
33
+ console.log(chalk.gray(` Commits: ${result.commits.length}`));
34
+ if (result.decisions.length > 0) {
35
+ console.log(chalk.cyan("\n Decisions:"));
36
+ result.decisions.forEach((d) => console.log(chalk.gray(` - ${d}`)));
37
+ }
38
+ if (result.duration) {
39
+ console.log(chalk.gray(` Duration: ${result.duration}`));
40
+ }
41
+ console.log(chalk.gray(`
42
+ Saved: ${result.id}`));
43
+ });
44
+ cmd.command("list").alias("ls").description("List recent snapshots").option("-n, --limit <n>", "Number of captures to show", "10").option("--json", "Output as JSON").action((options) => {
45
+ const capture = new ContextCapture();
46
+ const captures = capture.list(parseInt(options.limit));
47
+ if (captures.length === 0) {
48
+ console.log(chalk.yellow("No snapshots found."));
49
+ return;
50
+ }
51
+ if (options.json) {
52
+ console.log(JSON.stringify(captures, null, 2));
53
+ return;
54
+ }
55
+ console.log(chalk.cyan(`
56
+ Recent Snapshots (${captures.length}):
57
+ `));
58
+ for (const cap of captures) {
59
+ const date = new Date(cap.timestamp).toLocaleDateString();
60
+ const files = cap.filesChanged.length + cap.filesCreated.length;
61
+ console.log(
62
+ chalk.gray(
63
+ ` ${date} ${cap.branch.padEnd(30)} ${files} files ${cap.commits.length} commits`
64
+ )
65
+ );
66
+ }
67
+ });
68
+ cmd.command("show [branch]").description("Show snapshot details (latest or by branch)").option("--json", "Output as JSON").action((branch, options) => {
69
+ const capturer = new ContextCapture();
70
+ const result = capturer.getLatest(branch);
71
+ if (!result) {
72
+ console.log(
73
+ chalk.yellow(
74
+ branch ? `No snapshot found for branch: ${branch}` : "No snapshots found."
75
+ )
76
+ );
77
+ return;
78
+ }
79
+ if (options.json) {
80
+ console.log(JSON.stringify(result, null, 2));
81
+ return;
82
+ }
83
+ console.log(capturer.format(result));
84
+ });
85
+ return cmd;
86
+ }
87
+ export {
88
+ createSnapshotCommand
89
+ };
@@ -57,7 +57,9 @@ import { createBenchCommand } from "./commands/bench.js";
57
57
  import { createDigestCommands } from "./commands/digest.js";
58
58
  import { createTeamCommands } from "./commands/team.js";
59
59
  import { createDesiresCommands } from "./commands/desires.js";
60
- import { createSymphonyCommands } from "./commands/symphony.js";
60
+ import { createConductorCommands } from "./commands/orchestrate.js";
61
+ import { createPreflightCommand } from "./commands/preflight.js";
62
+ import { createSnapshotCommand } from "./commands/snapshot.js";
61
63
  import chalk from "chalk";
62
64
  import * as fs from "fs";
63
65
  import * as path from "path";
@@ -508,7 +510,9 @@ program.addCommand(createBenchCommand());
508
510
  program.addCommand(createDigestCommands());
509
511
  program.addCommand(createTeamCommands());
510
512
  program.addCommand(createDesiresCommands());
511
- program.addCommand(createSymphonyCommands());
513
+ program.addCommand(createConductorCommands());
514
+ program.addCommand(createPreflightCommand());
515
+ program.addCommand(createSnapshotCommand());
512
516
  registerSetupCommands(program);
513
517
  program.command("mm-spike").description(
514
518
  "Run multi-agent planning/implementation spike (planner/implementer/critic)"
@@ -6,6 +6,7 @@ import {
6
6
  DEFAULT_RETRIEVAL_CONFIG
7
7
  } from "./types.js";
8
8
  import { logger } from "../monitoring/logger.js";
9
+ import { extractKeywords as extractKeywordsShared } from "../utils/text.js";
9
10
  class CompressedSummaryGenerator {
10
11
  db;
11
12
  frameManager;
@@ -570,20 +571,7 @@ class CompressedSummaryGenerator {
570
571
  }
571
572
  }
572
573
  extractKeywords(text) {
573
- const stopWords = /* @__PURE__ */ new Set([
574
- "the",
575
- "a",
576
- "an",
577
- "and",
578
- "or",
579
- "but",
580
- "in",
581
- "on",
582
- "at",
583
- "to",
584
- "for"
585
- ]);
586
- return text.toLowerCase().split(/\W+/).filter((word) => word.length > 2 && !stopWords.has(word));
574
+ return extractKeywordsShared(text, { maxCount: 20 });
587
575
  }
588
576
  /**
589
577
  * Clear the cache
@@ -0,0 +1,18 @@
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 { readdirSync, unlinkSync } from "fs";
6
+ import { join } from "path";
7
+ function pruneOldFiles(dir, ext, maxKeep) {
8
+ try {
9
+ const files = readdirSync(dir).filter((f) => f.endsWith(ext)).sort().reverse();
10
+ for (const old of files.slice(maxKeep)) {
11
+ unlinkSync(join(dir, old));
12
+ }
13
+ } catch {
14
+ }
15
+ }
16
+ export {
17
+ pruneOldFiles
18
+ };