@stackmemoryai/stackmemory 1.10.4 → 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.
- package/README.md +104 -23
- package/dist/src/cli/claude-sm.js +266 -84
- package/dist/src/cli/codex-sm.js +185 -33
- package/dist/src/cli/commands/bench.js +209 -2
- package/dist/src/cli/commands/cache.js +126 -0
- package/dist/src/cli/commands/daemon.js +41 -0
- package/dist/src/cli/commands/handoff.js +40 -9
- package/dist/src/cli/commands/onboard.js +70 -3
- package/dist/src/cli/commands/optimize.js +117 -0
- package/dist/src/cli/commands/orchestrate.js +230 -5
- package/dist/src/cli/commands/orchestrator.js +312 -24
- package/dist/src/cli/commands/pack.js +322 -0
- package/dist/src/cli/commands/search.js +40 -1
- package/dist/src/cli/commands/setup.js +177 -7
- package/dist/src/cli/commands/skills.js +10 -1
- package/dist/src/cli/commands/state.js +265 -0
- package/dist/src/cli/commands/wiki.js +33 -0
- package/dist/src/cli/gemini-sm.js +19 -29
- package/dist/src/cli/index.js +90 -29
- package/dist/src/cli/opencode-sm.js +38 -21
- package/dist/src/cli/utils/determinism-watcher.js +66 -0
- package/dist/src/cli/utils/real-cli-bin.js +44 -0
- package/dist/src/core/cache/content-cache.js +238 -0
- package/dist/src/core/cache/index.js +11 -0
- package/dist/src/core/cache/token-estimator.js +16 -0
- package/dist/src/core/context/frame-database.js +38 -30
- package/dist/src/core/cross-search/cross-project-search.js +269 -0
- package/dist/src/core/{merge → cross-search}/index.js +6 -4
- package/dist/src/core/database/sqlite-adapter.js +0 -83
- package/dist/src/core/extensions/provider-adapter.js +5 -0
- package/dist/src/core/models/model-router.js +22 -2
- package/dist/src/core/monitoring/logger.js +2 -1
- package/dist/src/core/optimization/trace-optimizer.js +413 -0
- package/dist/src/core/provenance/confidence-scorer.js +128 -0
- package/dist/src/core/provenance/index.js +40 -0
- package/dist/src/core/provenance/provenance-store.js +194 -0
- package/dist/src/core/provenance/types.js +82 -0
- package/dist/src/core/session/project-handoff.js +64 -0
- package/dist/src/core/session/session-manager.js +28 -0
- package/dist/src/core/shared-state/canonical-store.js +564 -0
- package/dist/src/core/skill-packs/index.js +18 -0
- package/dist/src/core/skill-packs/parser.js +42 -0
- package/dist/src/core/skill-packs/registry.js +224 -0
- package/dist/src/core/skill-packs/types.js +66 -0
- package/dist/src/core/trace/trace-event-store.js +282 -0
- package/dist/src/core/trace/trace-event.js +4 -0
- package/dist/src/core/wiki/wiki-compiler.js +219 -0
- package/dist/src/daemon/daemon-config.js +7 -0
- package/dist/src/daemon/services/github-service.js +126 -0
- package/dist/src/daemon/unified-daemon.js +30 -0
- package/dist/src/features/sweep/pty-wrapper.js +13 -5
- package/dist/src/hooks/schemas.js +2 -0
- package/dist/src/integrations/claude-code/subagent-client.js +89 -0
- package/dist/src/integrations/github/pr-state.js +158 -0
- package/dist/src/integrations/linear/client.js +4 -1
- package/dist/src/integrations/mcp/handlers/cross-search-handlers.js +188 -0
- package/dist/src/integrations/mcp/handlers/index.js +40 -59
- package/dist/src/integrations/mcp/server.js +425 -311
- package/dist/src/integrations/mcp/tool-alias-registry.js +370 -0
- package/dist/src/integrations/mcp/tool-definitions.js +98 -229
- package/dist/src/integrations/ralph/context/stackmemory-context-loader.js +3 -40
- package/dist/src/integrations/ralph/learning/pattern-learner.js +1 -20
- package/dist/src/integrations/ralph/swarm/swarm-coordinator.js +0 -2
- package/dist/src/mcp/stackmemory-mcp-server.js +315 -0
- package/dist/src/orchestrators/multimodal/determinism.js +243 -0
- package/dist/src/orchestrators/multimodal/harness.js +147 -77
- package/dist/src/orchestrators/multimodal/providers.js +44 -3
- package/dist/src/utils/hook-installer.js +8 -8
- package/package.json +10 -1
- package/packs/coding/python-fastapi/instructions.md +60 -0
- package/packs/coding/python-fastapi/pack.yaml +28 -0
- package/packs/coding/typescript-react/instructions.md +47 -0
- package/packs/coding/typescript-react/pack.yaml +28 -0
- package/packs/core/commands/capture.md +32 -0
- package/packs/core/commands/learn.md +73 -0
- package/packs/core/commands/next.md +36 -0
- package/packs/core/commands/restart.md +58 -0
- package/packs/core/commands/restore.md +29 -0
- package/packs/core/commands/start.md +57 -0
- package/packs/core/commands/stop.md +65 -0
- package/packs/core/commands/summary.md +40 -0
- package/packs/core/manifest.json +24 -0
- package/packs/ops/decision-recovery/instructions.md +65 -0
- package/packs/ops/decision-recovery/pack.yaml +89 -0
- package/templates/claude-hooks/doc-ingest.js +76 -0
- package/dist/src/cli/commands/team.js +0 -168
- package/dist/src/core/context/shared-context-layer.js +0 -620
- package/dist/src/core/context/stack-merge-resolver.js +0 -748
- package/dist/src/core/merge/conflict-detector.js +0 -430
- package/dist/src/core/merge/resolution-engine.js +0 -557
- package/dist/src/core/merge/stack-diff.js +0 -531
- package/dist/src/core/merge/unified-merge-resolver.js +0 -302
- package/dist/src/integrations/mcp/handlers/cord-handlers.js +0 -397
- package/dist/src/integrations/mcp/handlers/team-handlers.js +0 -211
- /package/dist/src/core/{merge → cache}/types.js +0 -0
|
@@ -107,6 +107,7 @@ The daemon provides:
|
|
|
107
107
|
const services = [];
|
|
108
108
|
if (newStatus.services.context.enabled) services.push("context");
|
|
109
109
|
if (newStatus.services.linear.enabled) services.push("linear");
|
|
110
|
+
if (newStatus.services.github?.enabled) services.push("github");
|
|
110
111
|
if (newStatus.services.maintenance?.enabled)
|
|
111
112
|
services.push("maintenance");
|
|
112
113
|
if (newStatus.services.memory?.enabled) services.push("memory");
|
|
@@ -232,6 +233,25 @@ The daemon provides:
|
|
|
232
233
|
console.log(chalk.gray(` Syncs: ${lin.syncCount}`));
|
|
233
234
|
}
|
|
234
235
|
}
|
|
236
|
+
const gh = status.services.github;
|
|
237
|
+
if (gh) {
|
|
238
|
+
console.log(
|
|
239
|
+
` GitHub: ${gh.enabled ? chalk.green("Enabled") : chalk.gray("Disabled")}`
|
|
240
|
+
);
|
|
241
|
+
if (gh.enabled) {
|
|
242
|
+
console.log(
|
|
243
|
+
chalk.gray(` Interval: ${config.github.interval} min`)
|
|
244
|
+
);
|
|
245
|
+
if (gh.syncCount) {
|
|
246
|
+
console.log(chalk.gray(` Refreshes: ${gh.syncCount}`));
|
|
247
|
+
}
|
|
248
|
+
if (gh.lastProjectionState) {
|
|
249
|
+
console.log(
|
|
250
|
+
chalk.gray(` Last PR state: ${gh.lastProjectionState}`)
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
235
255
|
const maint = status.services.maintenance;
|
|
236
256
|
if (maint) {
|
|
237
257
|
console.log(
|
|
@@ -605,6 +625,22 @@ function getServiceHealthChecks(status, config) {
|
|
|
605
625
|
detail: "Disabled"
|
|
606
626
|
});
|
|
607
627
|
}
|
|
628
|
+
const gh = status.services.github;
|
|
629
|
+
if (gh?.enabled) {
|
|
630
|
+
const intervalMs = config.github.interval * 6e4;
|
|
631
|
+
const overdue = gh.lastRun ? Date.now() - gh.lastRun > intervalMs * 2 : false;
|
|
632
|
+
checks.push({
|
|
633
|
+
name: "GitHub Service",
|
|
634
|
+
status: overdue ? "warn" : "ok",
|
|
635
|
+
detail: gh.lastRun ? `Last refresh: ${formatTimeAgo(gh.lastRun)} | Refreshes: ${gh.syncCount ?? 0}${gh.lastProjectionState ? ` | Last PR state: ${gh.lastProjectionState}` : ""}` : `Enabled (interval: ${config.github.interval}m) | No refreshes yet`
|
|
636
|
+
});
|
|
637
|
+
} else {
|
|
638
|
+
checks.push({
|
|
639
|
+
name: "GitHub Service",
|
|
640
|
+
status: "ok",
|
|
641
|
+
detail: "Disabled"
|
|
642
|
+
});
|
|
643
|
+
}
|
|
608
644
|
const maint = status.services.maintenance;
|
|
609
645
|
if (maint?.enabled) {
|
|
610
646
|
const intervalMs = config.maintenance.interval * 6e4;
|
|
@@ -701,6 +737,11 @@ function buildHealthReport(status, config, paths) {
|
|
|
701
737
|
enabled: status.services.linear.enabled,
|
|
702
738
|
lastRun: status.services.linear.lastRun
|
|
703
739
|
},
|
|
740
|
+
{
|
|
741
|
+
key: "github",
|
|
742
|
+
enabled: status.services.github?.enabled ?? false,
|
|
743
|
+
lastRun: status.services.github?.lastRun
|
|
744
|
+
},
|
|
704
745
|
{
|
|
705
746
|
key: "maintenance",
|
|
706
747
|
enabled: status.services.maintenance?.enabled ?? false,
|
|
@@ -20,6 +20,10 @@ import { FrameManager } from "../../core/context/index.js";
|
|
|
20
20
|
import { LinearTaskManager } from "../../features/tasks/linear-task-manager.js";
|
|
21
21
|
import { logger } from "../../core/monitoring/logger.js";
|
|
22
22
|
import { EnhancedHandoffGenerator } from "../../core/session/handoff.js";
|
|
23
|
+
import {
|
|
24
|
+
getProjectHandoffPaths,
|
|
25
|
+
loadProjectHandoff
|
|
26
|
+
} from "../../core/session/project-handoff.js";
|
|
23
27
|
const countTokens = (text) => Math.ceil(text.length / 3.5);
|
|
24
28
|
const MAX_HANDOFF_VERSIONS = 10;
|
|
25
29
|
function saveVersionedHandoff(projectRoot, branch, content) {
|
|
@@ -273,7 +277,7 @@ Generated by stackmemory capture at ${timestamp}
|
|
|
273
277
|
if (!existsSync(stackmemoryDir)) {
|
|
274
278
|
mkdirSync(stackmemoryDir, { recursive: true });
|
|
275
279
|
}
|
|
276
|
-
const handoffPath =
|
|
280
|
+
const { handoffPath, metadataPath } = getProjectHandoffPaths(projectRoot);
|
|
277
281
|
writeFileSync(handoffPath, handoffPrompt);
|
|
278
282
|
let branch = "unknown";
|
|
279
283
|
try {
|
|
@@ -289,6 +293,18 @@ Generated by stackmemory capture at ${timestamp}
|
|
|
289
293
|
branch,
|
|
290
294
|
handoffPrompt
|
|
291
295
|
);
|
|
296
|
+
writeFileSync(
|
|
297
|
+
metadataPath,
|
|
298
|
+
JSON.stringify(
|
|
299
|
+
{
|
|
300
|
+
branch,
|
|
301
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
302
|
+
projectRoot
|
|
303
|
+
},
|
|
304
|
+
null,
|
|
305
|
+
2
|
|
306
|
+
)
|
|
307
|
+
);
|
|
292
308
|
console.log(
|
|
293
309
|
`Versioned: ${versionedPath.split("/").slice(-2).join("/")}`
|
|
294
310
|
);
|
|
@@ -331,26 +347,41 @@ Generated by stackmemory capture at ${timestamp}
|
|
|
331
347
|
}
|
|
332
348
|
function createRestoreCommand() {
|
|
333
349
|
const cmd = new Command("restore");
|
|
334
|
-
cmd.description("Restore context from last handoff").option("--no-copy", "Do not copy prompt to clipboard").action(async (options) => {
|
|
350
|
+
cmd.description("Restore context from last handoff").option("--no-copy", "Do not copy prompt to clipboard").option("--force", "Restore even if the handoff branch does not match").action(async (options) => {
|
|
335
351
|
try {
|
|
336
352
|
const projectRoot = process.cwd();
|
|
337
|
-
const handoffPath = join(
|
|
338
|
-
projectRoot,
|
|
339
|
-
".stackmemory",
|
|
340
|
-
"last-handoff.md"
|
|
341
|
-
);
|
|
342
353
|
const metaPath = join(
|
|
343
354
|
process.env["HOME"] || "~",
|
|
344
355
|
".stackmemory",
|
|
345
356
|
"handoffs",
|
|
346
357
|
"last-handoff-meta.json"
|
|
347
358
|
);
|
|
348
|
-
|
|
359
|
+
const currentBranch = (() => {
|
|
360
|
+
try {
|
|
361
|
+
return execSync("git rev-parse --abbrev-ref HEAD", {
|
|
362
|
+
encoding: "utf-8",
|
|
363
|
+
cwd: projectRoot,
|
|
364
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
365
|
+
}).trim();
|
|
366
|
+
} catch {
|
|
367
|
+
return void 0;
|
|
368
|
+
}
|
|
369
|
+
})();
|
|
370
|
+
const handoff = loadProjectHandoff(projectRoot, currentBranch);
|
|
371
|
+
if (!handoff) {
|
|
349
372
|
console.log("\u274C No handoff found in this project");
|
|
350
373
|
console.log('\u{1F4A1} Run "stackmemory capture" to create one');
|
|
351
374
|
return;
|
|
352
375
|
}
|
|
353
|
-
|
|
376
|
+
if (!handoff.compatible && !options.force) {
|
|
377
|
+
console.log("\u26A0\uFE0F Skipping stale handoff");
|
|
378
|
+
console.log(` ${handoff.mismatchReason}`);
|
|
379
|
+
console.log(
|
|
380
|
+
' Run "stackmemory restore --force" to inspect it anyway'
|
|
381
|
+
);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
const handoffPrompt = handoff.content;
|
|
354
385
|
console.log("\n" + "=".repeat(60));
|
|
355
386
|
console.log("\u{1F4CB} RESTORED HANDOFF");
|
|
356
387
|
console.log("=".repeat(60));
|
|
@@ -383,6 +383,67 @@ DATABASE_URL=${config.databaseUrl}
|
|
|
383
383
|
console.log(chalk.yellow(" \u26A0 Could not write hosted DB env file"));
|
|
384
384
|
}
|
|
385
385
|
}
|
|
386
|
+
{
|
|
387
|
+
const claudeCommandsDir = join(homedir(), ".claude", "commands");
|
|
388
|
+
const packsDir = join(__dirname, "..", "..", "..", "packs");
|
|
389
|
+
const coreManifest = join(packsDir, "core", "manifest.json");
|
|
390
|
+
if (existsSync(coreManifest)) {
|
|
391
|
+
const { installCommands } = await inquirer.prompt([
|
|
392
|
+
{
|
|
393
|
+
type: "confirm",
|
|
394
|
+
name: "installCommands",
|
|
395
|
+
message: "Install session commands for Claude Code? (/start, /stop, /restart, /next)",
|
|
396
|
+
default: true
|
|
397
|
+
}
|
|
398
|
+
]);
|
|
399
|
+
if (installCommands) {
|
|
400
|
+
console.log(chalk.gray("Installing command pack: core..."));
|
|
401
|
+
try {
|
|
402
|
+
execFileSync(
|
|
403
|
+
process.execPath,
|
|
404
|
+
[process.argv[1], "setup-commands", "--force"],
|
|
405
|
+
{ stdio: "pipe" }
|
|
406
|
+
);
|
|
407
|
+
console.log(
|
|
408
|
+
chalk.green(" \u2713 Installed /start, /stop, /restart, /next")
|
|
409
|
+
);
|
|
410
|
+
} catch {
|
|
411
|
+
try {
|
|
412
|
+
if (!existsSync(claudeCommandsDir)) {
|
|
413
|
+
mkdirSync(claudeCommandsDir, { recursive: true });
|
|
414
|
+
}
|
|
415
|
+
const manifest = JSON.parse(
|
|
416
|
+
(await import("fs")).readFileSync(coreManifest, "utf8")
|
|
417
|
+
);
|
|
418
|
+
const allCmds = [
|
|
419
|
+
...manifest.commands?.public || [],
|
|
420
|
+
...manifest.commands?.internal || []
|
|
421
|
+
];
|
|
422
|
+
let count = 0;
|
|
423
|
+
for (const cmd of allCmds) {
|
|
424
|
+
const src = join(packsDir, "core", cmd.file);
|
|
425
|
+
const dst = join(claudeCommandsDir, `${cmd.name}.md`);
|
|
426
|
+
if (existsSync(src) && !existsSync(dst)) {
|
|
427
|
+
execFileSync("ln", ["-s", src, dst]);
|
|
428
|
+
count++;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (count > 0) {
|
|
432
|
+
console.log(
|
|
433
|
+
chalk.green(` \u2713 Installed ${count} command(s)`)
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
} catch {
|
|
437
|
+
console.log(
|
|
438
|
+
chalk.yellow(
|
|
439
|
+
" \u26A0 Could not install commands. Run: stackmemory setup-commands"
|
|
440
|
+
)
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
386
447
|
const binPath = "/usr/local/bin/claude-sm";
|
|
387
448
|
const sourcePath = join(configPath, "bin", "stackmemory");
|
|
388
449
|
try {
|
|
@@ -531,12 +592,18 @@ function showNextSteps(config) {
|
|
|
531
592
|
)
|
|
532
593
|
);
|
|
533
594
|
}
|
|
534
|
-
console.log("3.
|
|
595
|
+
console.log("3. Install session commands for Claude Code:");
|
|
596
|
+
console.log(
|
|
597
|
+
chalk.gray(
|
|
598
|
+
" stackmemory setup-commands # Installs /start, /stop, /restart, /next\n"
|
|
599
|
+
)
|
|
600
|
+
);
|
|
601
|
+
console.log("4. Use with Claude:");
|
|
535
602
|
console.log(chalk.gray(" claude-sm # Or use stackmemory directly\n"));
|
|
536
|
-
console.log("
|
|
603
|
+
console.log("5. Use with Codex:");
|
|
537
604
|
console.log(chalk.gray(" codex-sm # Codex + StackMemory integration\n"));
|
|
538
605
|
if (config.enableLinear) {
|
|
539
|
-
console.log("
|
|
606
|
+
console.log("6. Sync with Linear:");
|
|
540
607
|
console.log(chalk.gray(" stackmemory linear sync\n"));
|
|
541
608
|
}
|
|
542
609
|
console.log("For more help:");
|
|
@@ -0,0 +1,117 @@
|
|
|
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 Database from "better-sqlite3";
|
|
7
|
+
import { existsSync } from "fs";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
import chalk from "chalk";
|
|
10
|
+
import { TraceStore } from "../../core/trace/trace-store.js";
|
|
11
|
+
import { TraceOptimizer } from "../../core/optimization/trace-optimizer.js";
|
|
12
|
+
function createOptimizeCommand() {
|
|
13
|
+
const optimize = new Command("optimize").description(
|
|
14
|
+
"Offline optimizers for harnesses, traces, and prompts"
|
|
15
|
+
);
|
|
16
|
+
optimize.command("traces").description(
|
|
17
|
+
"Analyze stored traces and generate HALO-like offline optimizer recommendations"
|
|
18
|
+
).option("-d, --days <n>", "Only analyze traces from the last N days", "30").option(
|
|
19
|
+
"-m, --min-occurrences <n>",
|
|
20
|
+
"Minimum repeated occurrences before surfacing a pattern",
|
|
21
|
+
"2"
|
|
22
|
+
).option("--json", "Print machine-readable JSON").option(
|
|
23
|
+
"--no-write",
|
|
24
|
+
"Do not persist report files under .stackmemory/build"
|
|
25
|
+
).action(async (options) => {
|
|
26
|
+
const projectRoot = process.cwd();
|
|
27
|
+
const dbPath = join(projectRoot, ".stackmemory", "context.db");
|
|
28
|
+
if (!existsSync(dbPath)) {
|
|
29
|
+
console.log(
|
|
30
|
+
chalk.red("StackMemory not initialized in this directory.")
|
|
31
|
+
);
|
|
32
|
+
console.log(chalk.gray('Run "stackmemory init" first.'));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const db = new Database(dbPath);
|
|
36
|
+
try {
|
|
37
|
+
const traceStore = new TraceStore(db);
|
|
38
|
+
const optimizer = new TraceOptimizer(traceStore);
|
|
39
|
+
const report = optimizer.analyze({
|
|
40
|
+
lookbackDays: parseInt(options.days, 10) || 30,
|
|
41
|
+
minOccurrences: parseInt(options.minOccurrences, 10) || 2
|
|
42
|
+
});
|
|
43
|
+
const persisted = options.write ? optimizer.persistReport(projectRoot, report) : null;
|
|
44
|
+
if (options.json) {
|
|
45
|
+
console.log(
|
|
46
|
+
JSON.stringify(
|
|
47
|
+
{
|
|
48
|
+
...report,
|
|
49
|
+
persisted
|
|
50
|
+
},
|
|
51
|
+
null,
|
|
52
|
+
2
|
|
53
|
+
)
|
|
54
|
+
);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
console.log(chalk.blue("\nTrace Optimizer Report\n"));
|
|
58
|
+
console.log(`Lookback window: ${report.lookbackDays} day(s)`);
|
|
59
|
+
console.log(`Traces analyzed: ${report.totalTracesAnalyzed}`);
|
|
60
|
+
console.log(`Traces with errors: ${report.tracesWithErrors}`);
|
|
61
|
+
console.log(`Causal traces: ${report.causalTraces}`);
|
|
62
|
+
console.log(
|
|
63
|
+
`Average tools/trace: ${report.averageToolsPerTrace.toFixed(2)}`
|
|
64
|
+
);
|
|
65
|
+
console.log(
|
|
66
|
+
`Average trace score: ${report.averageTraceScore.toFixed(2)}`
|
|
67
|
+
);
|
|
68
|
+
if (report.recommendations.length === 0) {
|
|
69
|
+
console.log(
|
|
70
|
+
chalk.yellow(
|
|
71
|
+
"\nNo repeated patterns crossed the threshold. Lower --min-occurrences or collect more traces."
|
|
72
|
+
)
|
|
73
|
+
);
|
|
74
|
+
} else {
|
|
75
|
+
console.log(chalk.blue("\nRecommendations:\n"));
|
|
76
|
+
for (const recommendation of report.recommendations) {
|
|
77
|
+
const badge = recommendation.priority === "high" ? chalk.red("[high]") : chalk.yellow("[medium]");
|
|
78
|
+
console.log(
|
|
79
|
+
`${badge} ${chalk.white(recommendation.title)} (${recommendation.confidence.toFixed(2)} confidence)`
|
|
80
|
+
);
|
|
81
|
+
console.log(` ${recommendation.summary}`);
|
|
82
|
+
console.log(` Targets: ${recommendation.targetAreas.join(", ")}`);
|
|
83
|
+
console.log(` Actions: ${recommendation.actions.join(" | ")}`);
|
|
84
|
+
console.log(
|
|
85
|
+
` Validate: ${recommendation.validations.join(" | ")}`
|
|
86
|
+
);
|
|
87
|
+
console.log("");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (report.clusters.length > 0) {
|
|
91
|
+
console.log(chalk.blue("Detected clusters:\n"));
|
|
92
|
+
for (const cluster of report.clusters) {
|
|
93
|
+
console.log(
|
|
94
|
+
`- ${cluster.label} (${cluster.occurrences} traces, ${cluster.kind})`
|
|
95
|
+
);
|
|
96
|
+
if (cluster.toolPatterns.length > 0) {
|
|
97
|
+
console.log(` Tools: ${cluster.toolPatterns.join(", ")}`);
|
|
98
|
+
}
|
|
99
|
+
if (cluster.sampleSummaries.length > 0) {
|
|
100
|
+
console.log(` Examples: ${cluster.sampleSummaries.join(" | ")}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
console.log("");
|
|
104
|
+
}
|
|
105
|
+
if (persisted) {
|
|
106
|
+
console.log(chalk.gray(`Saved JSON: ${persisted.jsonPath}`));
|
|
107
|
+
console.log(chalk.gray(`Saved Markdown: ${persisted.markdownPath}`));
|
|
108
|
+
}
|
|
109
|
+
} finally {
|
|
110
|
+
db.close();
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
return optimize;
|
|
114
|
+
}
|
|
115
|
+
export {
|
|
116
|
+
createOptimizeCommand
|
|
117
|
+
};
|
|
@@ -189,13 +189,13 @@ function progressBar(pct, width) {
|
|
|
189
189
|
return `${col}${"\u2501".repeat(filled)}${c.d}${"\u254C".repeat(empty)}${c.r}`;
|
|
190
190
|
}
|
|
191
191
|
function scanAgentStatuses() {
|
|
192
|
-
const
|
|
193
|
-
if (!existsSync(
|
|
194
|
-
const entries = readdirSync(
|
|
192
|
+
const agentsDir = join(homedir(), ".stackmemory", "conductor", "agents");
|
|
193
|
+
if (!existsSync(agentsDir)) return [];
|
|
194
|
+
const entries = readdirSync(agentsDir, { withFileTypes: true });
|
|
195
195
|
const statuses = [];
|
|
196
196
|
for (const entry of entries) {
|
|
197
197
|
if (!entry.isDirectory()) continue;
|
|
198
|
-
const statusPath = join(
|
|
198
|
+
const statusPath = join(agentsDir, entry.name, "status.json");
|
|
199
199
|
if (!existsSync(statusPath)) continue;
|
|
200
200
|
try {
|
|
201
201
|
const data = JSON.parse(readFileSync(statusPath, "utf-8"));
|
|
@@ -953,6 +953,7 @@ function createConductorCommands() {
|
|
|
953
953
|
console.log("");
|
|
954
954
|
});
|
|
955
955
|
cmd.command("finalize").description("Clean up completed/dead agents that conductor missed").option("--dry-run", "Show what would be done without doing it", false).action(async (options) => {
|
|
956
|
+
const agentsDir = join(homedir(), ".stackmemory", "conductor", "agents");
|
|
956
957
|
const statuses = scanAgentStatuses();
|
|
957
958
|
const needsFinalize = statuses.map((s) => ({ ...s, ...enrichStatus(s) })).filter((s) => {
|
|
958
959
|
return !s.alive || s.elapsed > STALE_FINALIZE_THRESHOLD_MS;
|
|
@@ -1038,6 +1039,221 @@ function createConductorCommands() {
|
|
|
1038
1039
|
);
|
|
1039
1040
|
}
|
|
1040
1041
|
});
|
|
1042
|
+
const isMerged = (repo, branch, lane) => {
|
|
1043
|
+
try {
|
|
1044
|
+
execSync(`git merge-base --is-ancestor "${branch}" "${lane}"`, {
|
|
1045
|
+
cwd: repo,
|
|
1046
|
+
stdio: "pipe",
|
|
1047
|
+
timeout: 5e3
|
|
1048
|
+
});
|
|
1049
|
+
return true;
|
|
1050
|
+
} catch (err) {
|
|
1051
|
+
const status = err.status;
|
|
1052
|
+
if (status !== 1) throw err;
|
|
1053
|
+
}
|
|
1054
|
+
try {
|
|
1055
|
+
const out = execSync(`git cherry "${lane}" "${branch}"`, {
|
|
1056
|
+
cwd: repo,
|
|
1057
|
+
encoding: "utf-8",
|
|
1058
|
+
timeout: 1e4
|
|
1059
|
+
}).trim();
|
|
1060
|
+
if (!out) return true;
|
|
1061
|
+
const lines = out.split("\n").filter(Boolean);
|
|
1062
|
+
return lines.every((l) => l.startsWith("-"));
|
|
1063
|
+
} catch {
|
|
1064
|
+
return false;
|
|
1065
|
+
}
|
|
1066
|
+
};
|
|
1067
|
+
const bucketLaneBranches = (repo, lane) => {
|
|
1068
|
+
let all = [];
|
|
1069
|
+
try {
|
|
1070
|
+
all = execSync(`git branch --list 'worktree-agent-*'`, {
|
|
1071
|
+
cwd: repo,
|
|
1072
|
+
encoding: "utf-8",
|
|
1073
|
+
timeout: 1e4
|
|
1074
|
+
}).split("\n").map((s) => s.trim().replace(/^[*+]\s*/, "")).filter(Boolean);
|
|
1075
|
+
} catch {
|
|
1076
|
+
}
|
|
1077
|
+
const merged = [];
|
|
1078
|
+
const unmerged = [];
|
|
1079
|
+
for (const b of all) {
|
|
1080
|
+
(isMerged(repo, b, lane) ? merged : unmerged).push(b);
|
|
1081
|
+
}
|
|
1082
|
+
return { merged, unmerged };
|
|
1083
|
+
};
|
|
1084
|
+
const getWorktreeCleanliness = (repo, wtPath) => {
|
|
1085
|
+
try {
|
|
1086
|
+
const out = execSync(`git -C "${wtPath}" status --short`, {
|
|
1087
|
+
cwd: repo,
|
|
1088
|
+
encoding: "utf-8",
|
|
1089
|
+
timeout: 5e3
|
|
1090
|
+
});
|
|
1091
|
+
return out.trim().length > 0 ? "dirty" : "clean";
|
|
1092
|
+
} catch {
|
|
1093
|
+
return "unknown";
|
|
1094
|
+
}
|
|
1095
|
+
};
|
|
1096
|
+
const laneCmd = cmd.command("lane").description(
|
|
1097
|
+
"Inspect or clean up worktree-agent-* branches against a lane"
|
|
1098
|
+
);
|
|
1099
|
+
laneCmd.command("status").description(
|
|
1100
|
+
"List worktree-agent-* branches and their merge state vs the lane"
|
|
1101
|
+
).option(
|
|
1102
|
+
"--lane <branch>",
|
|
1103
|
+
"Lane branch to compare against (default: current branch)"
|
|
1104
|
+
).option("--repo <path>", "Git repo root", process.cwd()).action((options) => {
|
|
1105
|
+
const repo = options.repo;
|
|
1106
|
+
const lane = options.lane || execSync("git branch --show-current", {
|
|
1107
|
+
cwd: repo,
|
|
1108
|
+
encoding: "utf-8"
|
|
1109
|
+
}).trim();
|
|
1110
|
+
if (!lane) {
|
|
1111
|
+
console.error(
|
|
1112
|
+
`${c.red}Could not resolve lane branch.${c.r} Pass --lane.`
|
|
1113
|
+
);
|
|
1114
|
+
process.exit(1);
|
|
1115
|
+
}
|
|
1116
|
+
const { merged, unmerged } = bucketLaneBranches(repo, lane);
|
|
1117
|
+
console.log(`
|
|
1118
|
+
${c.b}Lane:${c.r} ${c.cyan}${lane}${c.r}
|
|
1119
|
+
`);
|
|
1120
|
+
if (merged.length) {
|
|
1121
|
+
console.log(` ${c.green}merged (${merged.length}):${c.r}`);
|
|
1122
|
+
for (const b of merged) console.log(` ${c.gray}\u2713${c.r} ${b}`);
|
|
1123
|
+
}
|
|
1124
|
+
if (unmerged.length) {
|
|
1125
|
+
console.log(`
|
|
1126
|
+
${c.orange}unmerged (${unmerged.length}):${c.r}`);
|
|
1127
|
+
for (const b of unmerged) console.log(` ${c.orange}\u2022${c.r} ${b}`);
|
|
1128
|
+
}
|
|
1129
|
+
if (!merged.length && !unmerged.length) {
|
|
1130
|
+
console.log(` ${c.gray}No worktree-agent-* branches found.${c.r}`);
|
|
1131
|
+
}
|
|
1132
|
+
});
|
|
1133
|
+
laneCmd.command("cleanup").description(
|
|
1134
|
+
"Remove worktree-agent-* branches and worktrees already merged into the lane"
|
|
1135
|
+
).option(
|
|
1136
|
+
"--lane <branch>",
|
|
1137
|
+
"Lane branch to compare against (default: current branch)"
|
|
1138
|
+
).option("--repo <path>", "Git repo root", process.cwd()).option("--dry-run", "Show what would be removed without doing it", false).option(
|
|
1139
|
+
"--force",
|
|
1140
|
+
"Remove worktrees even if they have uncommitted changes",
|
|
1141
|
+
false
|
|
1142
|
+
).action((options) => {
|
|
1143
|
+
const repo = options.repo;
|
|
1144
|
+
const lane = options.lane || execSync("git branch --show-current", {
|
|
1145
|
+
cwd: repo,
|
|
1146
|
+
encoding: "utf-8"
|
|
1147
|
+
}).trim();
|
|
1148
|
+
if (!lane) {
|
|
1149
|
+
console.error(
|
|
1150
|
+
`${c.red}Could not resolve lane branch.${c.r} Pass --lane.`
|
|
1151
|
+
);
|
|
1152
|
+
process.exit(1);
|
|
1153
|
+
}
|
|
1154
|
+
const { merged } = bucketLaneBranches(repo, lane);
|
|
1155
|
+
if (merged.length === 0) {
|
|
1156
|
+
console.log(
|
|
1157
|
+
`${c.green}Nothing to clean up for lane ${c.cyan}${lane}${c.r}.`
|
|
1158
|
+
);
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
const worktreeMap = /* @__PURE__ */ new Map();
|
|
1162
|
+
try {
|
|
1163
|
+
const wt = execSync("git worktree list --porcelain", {
|
|
1164
|
+
cwd: repo,
|
|
1165
|
+
encoding: "utf-8",
|
|
1166
|
+
timeout: 1e4
|
|
1167
|
+
});
|
|
1168
|
+
let currentPath = "";
|
|
1169
|
+
for (const line of wt.split("\n")) {
|
|
1170
|
+
if (line.startsWith("worktree ")) {
|
|
1171
|
+
currentPath = line.slice("worktree ".length).trim();
|
|
1172
|
+
} else if (line.startsWith("branch ")) {
|
|
1173
|
+
const ref = line.slice("branch ".length).trim();
|
|
1174
|
+
const b = ref.replace(/^refs\/heads\//, "");
|
|
1175
|
+
if (currentPath) worktreeMap.set(b, currentPath);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
} catch {
|
|
1179
|
+
}
|
|
1180
|
+
console.log(
|
|
1181
|
+
`
|
|
1182
|
+
${c.b}Lane:${c.r} ${c.cyan}${lane}${c.r} ${c.d}(${merged.length} merged branches)${c.r}
|
|
1183
|
+
`
|
|
1184
|
+
);
|
|
1185
|
+
let skippedDirty = 0;
|
|
1186
|
+
let skippedUnknown = 0;
|
|
1187
|
+
for (const branch of merged) {
|
|
1188
|
+
const wtPath = worktreeMap.get(branch);
|
|
1189
|
+
const cleanliness = wtPath ? getWorktreeCleanliness(repo, wtPath) : "unknown";
|
|
1190
|
+
const dirty = cleanliness === "dirty";
|
|
1191
|
+
if (dirty && !options.force) {
|
|
1192
|
+
skippedDirty++;
|
|
1193
|
+
console.log(
|
|
1194
|
+
` ${c.orange}\u26A0${c.r} ${c.orange}dirty${c.r} ${wtPath} ${c.d}(skipping ${branch} \u2014 pass --force to remove)${c.r}`
|
|
1195
|
+
);
|
|
1196
|
+
continue;
|
|
1197
|
+
}
|
|
1198
|
+
if (wtPath && cleanliness === "unknown" && !options.force) {
|
|
1199
|
+
skippedUnknown++;
|
|
1200
|
+
console.log(
|
|
1201
|
+
` ${c.orange}\u26A0${c.r} ${c.orange}unknown${c.r} ${wtPath} ${c.d}(skipping ${branch} \u2014 could not verify clean state; pass --force to remove)${c.r}`
|
|
1202
|
+
);
|
|
1203
|
+
continue;
|
|
1204
|
+
}
|
|
1205
|
+
const dirtyTag = dirty ? ` ${c.orange}[dirty]${c.r}` : "";
|
|
1206
|
+
const unknownTag = cleanliness === "unknown" ? ` ${c.orange}[unknown]${c.r}` : "";
|
|
1207
|
+
const action = wtPath ? `${c.gray}worktree remove${c.r} ${wtPath}${dirtyTag}${unknownTag} + ${c.gray}branch -D${c.r} ${branch}` : `${c.gray}branch -D${c.r} ${branch}`;
|
|
1208
|
+
console.log(` ${c.green}\u2713${c.r} ${action}`);
|
|
1209
|
+
if (options.dryRun) continue;
|
|
1210
|
+
if (wtPath) {
|
|
1211
|
+
try {
|
|
1212
|
+
execSync(`git worktree remove "${wtPath}" --force`, {
|
|
1213
|
+
cwd: repo,
|
|
1214
|
+
stdio: "pipe",
|
|
1215
|
+
timeout: 15e3
|
|
1216
|
+
});
|
|
1217
|
+
} catch (err) {
|
|
1218
|
+
console.log(
|
|
1219
|
+
` ${c.red}worktree remove failed:${c.r} ${err.message}`
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
try {
|
|
1224
|
+
execSync(`git branch -D "${branch}"`, {
|
|
1225
|
+
cwd: repo,
|
|
1226
|
+
stdio: "pipe",
|
|
1227
|
+
timeout: 5e3
|
|
1228
|
+
});
|
|
1229
|
+
} catch (err) {
|
|
1230
|
+
console.log(
|
|
1231
|
+
` ${c.red}branch -D failed:${c.r} ${err.message}`
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
if (!options.dryRun) {
|
|
1236
|
+
try {
|
|
1237
|
+
execSync("git worktree prune", {
|
|
1238
|
+
cwd: repo,
|
|
1239
|
+
stdio: "pipe",
|
|
1240
|
+
timeout: 1e4
|
|
1241
|
+
});
|
|
1242
|
+
} catch {
|
|
1243
|
+
}
|
|
1244
|
+
const skipNote = skippedDirty ? ` ${c.orange}(${skippedDirty} dirty skipped \u2014 pass --force)${c.r}` : "";
|
|
1245
|
+
const unknownNote = skippedUnknown ? ` ${c.orange}(${skippedUnknown} unknown skipped \u2014 pass --force)${c.r}` : "";
|
|
1246
|
+
console.log(`
|
|
1247
|
+
${c.green}Done.${c.r}${skipNote}${unknownNote}`);
|
|
1248
|
+
} else {
|
|
1249
|
+
const skipNote = skippedDirty ? ` ${c.orange}(${skippedDirty} dirty would be skipped \u2014 pass --force to include)${c.r}` : "";
|
|
1250
|
+
const unknownNote = skippedUnknown ? ` ${c.orange}(${skippedUnknown} unknown would be skipped \u2014 pass --force to include)${c.r}` : "";
|
|
1251
|
+
console.log(
|
|
1252
|
+
`
|
|
1253
|
+
${c.d}Dry run \u2014 no changes made. Remove --dry-run to execute.${c.r}${skipNote}${unknownNote}`
|
|
1254
|
+
);
|
|
1255
|
+
}
|
|
1256
|
+
});
|
|
1041
1257
|
cmd.command("logs").description("Tail agent output log").argument("<issue-id>", "Issue identifier (e.g., STA-499)").option("-f, --follow", "Follow the log (tail -f)", false).option("-n, --lines <n>", "Number of lines to show", "50").action(async (issueId, options) => {
|
|
1042
1258
|
const logPath = join(getAgentStatusDir(issueId), "output.log");
|
|
1043
1259
|
if (!existsSync(logPath)) {
|
|
@@ -1521,6 +1737,13 @@ function createConductorCommands() {
|
|
|
1521
1737
|
).option(
|
|
1522
1738
|
"--no-pr",
|
|
1523
1739
|
"Disable automatic GitHub PR creation after agent success"
|
|
1740
|
+
).option(
|
|
1741
|
+
"--workspace-mode <mode>",
|
|
1742
|
+
'Workspace mode: "auto" (detect GitButler), "gitbutler", or "worktree"',
|
|
1743
|
+
"auto"
|
|
1744
|
+
).option(
|
|
1745
|
+
"--lane <branch>",
|
|
1746
|
+
"Optional lane branch. When set, conductor uses git worktrees rooted at the lane as worktree-agent-<id> and suppresses PRs (lane is human-curated)."
|
|
1524
1747
|
).action(async (options) => {
|
|
1525
1748
|
ensureDefaultPromptTemplate();
|
|
1526
1749
|
const conductor = new Conductor({
|
|
@@ -1537,7 +1760,9 @@ function createConductorCommands() {
|
|
|
1537
1760
|
turnTimeoutMs: parseInt(options.turnTimeout, 10),
|
|
1538
1761
|
agentMode: options.mode === "adapter" ? "adapter" : "cli",
|
|
1539
1762
|
model: options.model,
|
|
1540
|
-
autoPR: options.pr
|
|
1763
|
+
autoPR: options.lane ? false : options.pr,
|
|
1764
|
+
workspaceMode: options.workspaceMode,
|
|
1765
|
+
laneBranch: options.lane
|
|
1541
1766
|
});
|
|
1542
1767
|
await conductor.start();
|
|
1543
1768
|
});
|