@stackmemoryai/stackmemory 1.12.0 → 1.14.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 (162) hide show
  1. package/LICENSE +131 -64
  2. package/README.md +3 -1
  3. package/bin/claude-sm +16 -1
  4. package/bin/claude-smd +16 -1
  5. package/bin/codex-smd +16 -1
  6. package/bin/gemini-sm +16 -1
  7. package/bin/hermes-sm +21 -0
  8. package/bin/hermes-smd +21 -0
  9. package/bin/opencode-sm +16 -1
  10. package/dist/src/cli/codex-sm.js +51 -11
  11. package/dist/src/cli/commands/brain.js +206 -0
  12. package/dist/src/cli/commands/company-os.js +184 -0
  13. package/dist/src/cli/commands/context.js +5 -0
  14. package/dist/src/cli/commands/operator.js +127 -0
  15. package/dist/src/cli/commands/orchestrate.js +2 -0
  16. package/dist/src/cli/commands/orchestrator.js +3 -2
  17. package/dist/src/cli/commands/patterns.js +254 -0
  18. package/dist/src/cli/commands/portal.js +161 -0
  19. package/dist/src/cli/commands/scaffold.js +92 -0
  20. package/dist/src/cli/commands/setup.js +1 -4
  21. package/dist/src/cli/commands/sync.js +253 -0
  22. package/dist/src/cli/commands/tasks.js +130 -1
  23. package/dist/src/cli/commands/vision.js +221 -0
  24. package/dist/src/cli/hermes-sm.js +224 -0
  25. package/dist/src/cli/index.js +15 -10
  26. package/dist/src/cli/utils/real-cli-bin.js +72 -0
  27. package/dist/src/core/brain/brain-store.js +187 -0
  28. package/dist/src/core/brain/brain-sync.js +193 -0
  29. package/dist/src/core/brain/index.js +78 -0
  30. package/dist/src/core/brain/types.js +10 -0
  31. package/dist/src/core/cache/token-estimator.js +24 -1
  32. package/dist/src/core/config/feature-flags.js +2 -6
  33. package/dist/src/core/context/frame-database.js +44 -0
  34. package/dist/src/core/context/recursive-context-manager.js +1 -1
  35. package/dist/src/core/context/rehydration.js +2 -1
  36. package/dist/src/core/database/sqlite-adapter.js +14 -1
  37. package/dist/src/core/models/model-router.js +33 -1
  38. package/dist/src/core/models/provider-pricing.js +58 -4
  39. package/dist/src/core/patterns/index.js +22 -0
  40. package/dist/src/core/patterns/pattern-applier.js +39 -0
  41. package/dist/src/core/patterns/pattern-observer.js +157 -0
  42. package/dist/src/core/patterns/pattern-store.js +259 -0
  43. package/dist/src/core/patterns/types.js +19 -0
  44. package/dist/src/core/retrieval/llm-context-retrieval.js +5 -4
  45. package/dist/src/core/retrieval/unified-context-assembler.js +11 -66
  46. package/dist/src/core/skill-packs/types.js +14 -1
  47. package/dist/src/core/storage/cloud-sync-manager.js +116 -0
  48. package/dist/src/core/storage/cloud-sync.js +574 -0
  49. package/dist/src/core/storage/two-tier-storage.js +5 -1
  50. package/dist/src/core/tasks/master-tasks-template.js +43 -0
  51. package/dist/src/core/tasks/md-task-parser.js +138 -0
  52. package/dist/src/core/vision/index.js +27 -0
  53. package/dist/src/core/vision/signals.js +79 -0
  54. package/dist/src/core/vision/types.js +22 -0
  55. package/dist/src/core/vision/vision-file.js +146 -0
  56. package/dist/src/core/vision/vision-loop.js +220 -0
  57. package/dist/src/core/wiki/wiki-compiler.js +103 -1
  58. package/dist/src/daemon/daemon-config.js +45 -0
  59. package/dist/src/daemon/services/desire-path-service.js +566 -0
  60. package/dist/src/daemon/services/research-stream-service.js +320 -0
  61. package/dist/src/daemon/services/telemetry-service.js +192 -0
  62. package/dist/src/daemon/unified-daemon.js +28 -1
  63. package/dist/src/features/browser/cli-browser-agent.js +417 -0
  64. package/dist/src/features/browser/stagehand-workflows.js +578 -0
  65. package/dist/src/features/operator/adapter-factory.js +62 -0
  66. package/dist/src/features/operator/browser-adapter.js +109 -0
  67. package/dist/src/features/operator/desktop-adapter.js +125 -0
  68. package/dist/src/features/operator/index.js +39 -0
  69. package/dist/src/features/operator/llm-decision.js +137 -0
  70. package/dist/src/features/operator/operator-logger.js +92 -0
  71. package/dist/src/features/operator/overnight-runner.js +327 -0
  72. package/dist/src/features/operator/screen-adapter.js +91 -0
  73. package/dist/src/features/operator/session-manager.js +127 -0
  74. package/dist/src/features/operator/state-machine.js +227 -0
  75. package/dist/src/features/operator/task-queue.js +81 -0
  76. package/dist/src/features/portal/index.js +26 -0
  77. package/dist/src/features/portal/server.js +240 -0
  78. package/dist/src/features/portal/types.js +14 -0
  79. package/dist/src/features/portal/ui.js +195 -0
  80. package/dist/src/features/tasks/task-aware-context.js +2 -1
  81. package/dist/src/features/tui/simple-monitor.js +0 -23
  82. package/dist/src/features/tui/swarm-monitor.js +8 -66
  83. package/dist/src/{integrations/diffmem/index.js → features/web/client/hooks/use-socket.js} +6 -5
  84. package/dist/src/features/web/client/lib/utils.js +12 -0
  85. package/dist/src/features/web/client/stores/session-store.js +12 -0
  86. package/dist/src/features/web/server/gcp-billing.js +76 -0
  87. package/dist/src/features/web/server/index.js +10 -0
  88. package/dist/src/features/web/server/spend-calculator.js +228 -0
  89. package/dist/src/hooks/schemas.js +4 -1
  90. package/dist/src/integrations/anthropic/client.js +3 -2
  91. package/dist/src/integrations/claude-code/agent-bridge.js +0 -3
  92. package/dist/src/integrations/claude-code/subagent-client.js +218 -11
  93. package/dist/src/integrations/claude-code/task-coordinator.js +2 -1
  94. package/dist/src/integrations/linear/webhook-retry.js +196 -0
  95. package/dist/src/integrations/linear/webhook-server.js +18 -22
  96. package/dist/src/integrations/mcp/handlers/cloud-sync-handlers.js +101 -0
  97. package/dist/src/integrations/mcp/handlers/index.js +27 -52
  98. package/dist/src/integrations/mcp/server.js +122 -335
  99. package/dist/src/integrations/mcp/tool-alias-registry.js +0 -73
  100. package/dist/src/integrations/mcp/tool-definitions.js +111 -510
  101. package/dist/src/mcp/stackmemory-mcp-server.js +404 -379
  102. package/dist/src/orchestrators/multimodal/determinism.js +2 -1
  103. package/dist/src/orchestrators/multimodal/harness.js +2 -1
  104. package/dist/src/skills/recursive-agent-orchestrator.js +2 -4
  105. package/dist/src/utils/process-cleanup.js +1 -7
  106. package/docs/README.md +42 -0
  107. package/docs/guides/README_INSTALL.md +208 -0
  108. package/package.json +18 -9
  109. package/scripts/claude-code-wrapper.sh +11 -0
  110. package/scripts/claude-sm-setup.sh +12 -1
  111. package/scripts/codex-wrapper.sh +11 -0
  112. package/scripts/git-hooks/branch-context-manager.sh +11 -0
  113. package/scripts/git-hooks/post-checkout-stackmemory.sh +11 -0
  114. package/scripts/git-hooks/post-commit-stackmemory.sh +11 -0
  115. package/scripts/git-hooks/pre-commit-stackmemory.sh +11 -0
  116. package/scripts/hooks/cleanup-shell.sh +12 -1
  117. package/scripts/hooks/task-complete.sh +12 -1
  118. package/scripts/install-code-execution-hooks.sh +12 -1
  119. package/scripts/install-sweep-hook.sh +12 -0
  120. package/scripts/install.sh +11 -0
  121. package/scripts/opencode-wrapper.sh +11 -0
  122. package/scripts/portal/cloud-init.yaml +69 -0
  123. package/scripts/portal/setup.sh +69 -0
  124. package/scripts/portal/stackmemory-portal.service +34 -0
  125. package/scripts/setup-claude-integration.sh +12 -1
  126. package/scripts/smoke-init-db.sh +23 -0
  127. package/scripts/stackmemory-daemon.sh +11 -0
  128. package/scripts/verify-dist.cjs +11 -4
  129. package/dist/src/cli/commands/ralph.js +0 -1053
  130. package/dist/src/hooks/diffmem-hooks.js +0 -376
  131. package/dist/src/integrations/diffmem/client.js +0 -208
  132. package/dist/src/integrations/diffmem/config.js +0 -14
  133. package/dist/src/integrations/greptile/client.js +0 -101
  134. package/dist/src/integrations/greptile/config.js +0 -14
  135. package/dist/src/integrations/greptile/index.js +0 -11
  136. package/dist/src/integrations/mcp/handlers/cross-search-handlers.js +0 -188
  137. package/dist/src/integrations/mcp/handlers/diffmem-handlers.js +0 -455
  138. package/dist/src/integrations/mcp/handlers/greptile-handlers.js +0 -456
  139. package/dist/src/integrations/mcp/handlers/provider-handlers.js +0 -227
  140. package/dist/src/integrations/ralph/bridge/ralph-stackmemory-bridge.js +0 -863
  141. package/dist/src/integrations/ralph/context/context-budget-manager.js +0 -308
  142. package/dist/src/integrations/ralph/context/stackmemory-context-loader.js +0 -354
  143. package/dist/src/integrations/ralph/index.js +0 -17
  144. package/dist/src/integrations/ralph/learning/pattern-learner.js +0 -416
  145. package/dist/src/integrations/ralph/lifecycle/iteration-lifecycle.js +0 -448
  146. package/dist/src/integrations/ralph/loopmax.js +0 -488
  147. package/dist/src/integrations/ralph/monitoring/swarm-dashboard.js +0 -293
  148. package/dist/src/integrations/ralph/monitoring/swarm-registry.js +0 -107
  149. package/dist/src/integrations/ralph/orchestration/multi-loop-orchestrator.js +0 -508
  150. package/dist/src/integrations/ralph/patterns/compounding-engineering-pattern.js +0 -407
  151. package/dist/src/integrations/ralph/patterns/extended-coherence-sessions.js +0 -495
  152. package/dist/src/integrations/ralph/patterns/oracle-worker-pattern.js +0 -387
  153. package/dist/src/integrations/ralph/performance/performance-optimizer.js +0 -357
  154. package/dist/src/integrations/ralph/recovery/crash-recovery.js +0 -461
  155. package/dist/src/integrations/ralph/state/state-reconciler.js +0 -420
  156. package/dist/src/integrations/ralph/swarm/git-workflow-manager.js +0 -444
  157. package/dist/src/integrations/ralph/swarm/swarm-coordinator.js +0 -1005
  158. package/dist/src/integrations/ralph/visualization/ralph-debugger.js +0 -635
  159. package/scripts/ralph-loop-implementation.js +0 -404
  160. /package/dist/src/{integrations/diffmem/types.js → core/storage/cloud-sync-types.js} +0 -0
  161. /package/dist/src/{integrations/greptile → features/operator}/types.js +0 -0
  162. /package/dist/src/{integrations/ralph/types.js → features/web/client/next-env.d.js} +0 -0
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env node
2
+ import { fileURLToPath as __fileURLToPath } from 'url';
3
+ import { dirname as __pathDirname } from 'path';
4
+ const __filename = __fileURLToPath(import.meta.url);
5
+ const __dirname = __pathDirname(__filename);
6
+ import { Command } from "commander";
7
+ import chalk from "chalk";
8
+ import { readdirSync, readFileSync, existsSync } from "node:fs";
9
+ import { join, extname } from "node:path";
10
+ const WIKI_DIR = join(process.cwd(), "wiki");
11
+ const WIKI_SOPS_DIR = join(WIKI_DIR, "sops");
12
+ const PROSE_SPEC_PATH = join(
13
+ process.cwd(),
14
+ "docs",
15
+ "specs",
16
+ "COMPANY-OS-PROSE.md"
17
+ );
18
+ function collectSopFiles() {
19
+ const files = [];
20
+ const dirs = [WIKI_DIR];
21
+ if (existsSync(WIKI_SOPS_DIR)) dirs.push(WIKI_SOPS_DIR);
22
+ for (const dir of dirs) {
23
+ if (!existsSync(dir)) continue;
24
+ for (const entry of readdirSync(dir)) {
25
+ if (extname(entry) !== ".md") continue;
26
+ const filePath = join(dir, entry);
27
+ const content = readFileSync(filePath, "utf8");
28
+ const idMatch = content.match(/^# (SOP-\d+)/m);
29
+ const titleMatch = content.match(/^# SOP-\d+\s+(.+)/m);
30
+ const statusMatch = content.match(/\*\*Status:\*\*\s*(\w+)/);
31
+ const proseMatch = content.match(/Related PROSE Expectation.*\[(E\.\d+)/);
32
+ files.push({
33
+ id: idMatch?.[1] ?? entry.replace(".md", ""),
34
+ title: titleMatch?.[1] ?? entry,
35
+ status: statusMatch?.[1] ?? "Unknown",
36
+ proseId: proseMatch?.[1] ?? null,
37
+ path: filePath
38
+ });
39
+ }
40
+ }
41
+ return files;
42
+ }
43
+ function loadValidProseIds() {
44
+ const ids = /* @__PURE__ */ new Set();
45
+ if (!existsSync(PROSE_SPEC_PATH)) return ids;
46
+ const content = readFileSync(PROSE_SPEC_PATH, "utf8");
47
+ const headingRegex = /^###\s+(E\.\d+)\s+/gm;
48
+ let match;
49
+ while ((match = headingRegex.exec(content)) !== null) {
50
+ ids.add(match[1]);
51
+ }
52
+ return ids;
53
+ }
54
+ function validateSop(content) {
55
+ const errors = [];
56
+ const required = ["## Objective", "## Procedure", "## Verification"];
57
+ for (const section of required) {
58
+ if (!content.includes(section)) {
59
+ errors.push(`Missing section: ${section}`);
60
+ }
61
+ }
62
+ if (!content.match(/Related PROSE Expectation.*\[E\.\d+/)) {
63
+ errors.push("Missing or invalid Related PROSE Expectation");
64
+ }
65
+ if (!content.match(/^# SOP-\d+/m)) {
66
+ errors.push("Missing or invalid SOP ID (expected # SOP-NNN ...)");
67
+ }
68
+ return errors;
69
+ }
70
+ function createCompanyOsCommand() {
71
+ const cmd = new Command("company-os").alias("cos").description("Manage Company OS processes, SOPs, and audits");
72
+ cmd.command("list").description("List SOPs in the Company OS wiki").option("--json", "Output as JSON").action(() => {
73
+ const sops = collectSopFiles();
74
+ if (sops.length === 0) {
75
+ console.log(chalk.yellow("No SOPs found in wiki/"));
76
+ return;
77
+ }
78
+ console.log(chalk.cyan(`
79
+ Company OS SOPs (${sops.length})
80
+ `));
81
+ for (const sop of sops.sort((a, b) => a.id.localeCompare(b.id))) {
82
+ console.log(
83
+ ` ${chalk.bold(sop.id)} ${chalk.white(sop.title.slice(0, 50))}`
84
+ );
85
+ console.log(
86
+ chalk.gray(
87
+ ` status: ${sop.status} PROSE: ${sop.proseId ?? "none"} ${sop.path}`
88
+ )
89
+ );
90
+ }
91
+ console.log();
92
+ });
93
+ cmd.command("validate").description("Validate all Company OS SOPs against the schema").option("--json", "Output as JSON").action(() => {
94
+ const sops = collectSopFiles();
95
+ const validProseIds = loadValidProseIds();
96
+ const results = [];
97
+ for (const sop of sops) {
98
+ const content = readFileSync(sop.path, "utf8");
99
+ const errors = validateSop(content);
100
+ if (sop.proseId && !validProseIds.has(sop.proseId)) {
101
+ errors.push(`Invalid PROSE Expectation: ${sop.proseId}`);
102
+ }
103
+ results.push({
104
+ id: sop.id,
105
+ path: sop.path,
106
+ valid: errors.length === 0,
107
+ errors
108
+ });
109
+ }
110
+ const validCount = results.filter((r) => r.valid).length;
111
+ if (results.every((r) => r.valid)) {
112
+ console.log(chalk.green(`
113
+ \u2713 All ${validCount} SOPs are valid.
114
+ `));
115
+ } else {
116
+ console.log(chalk.yellow(`
117
+ SOP Validation Results
118
+ `));
119
+ for (const result of results) {
120
+ const icon = result.valid ? chalk.green("\u2713") : chalk.red("\u2717");
121
+ console.log(`${icon} ${result.id}`);
122
+ for (const error of result.errors) {
123
+ console.log(chalk.gray(` - ${error}`));
124
+ }
125
+ }
126
+ console.log(chalk.gray(`
127
+ Valid: ${validCount} / ${results.length}
128
+ `));
129
+ process.exitCode = 1;
130
+ }
131
+ });
132
+ cmd.command("audit <process>").description(
133
+ "Audit a Company OS process against its SOP (e.g. onboarding, pto)"
134
+ ).option("--json", "Output as JSON").action((processName) => {
135
+ const sops = collectSopFiles();
136
+ const normalized = processName.toLowerCase().replace(/\s+/g, "-");
137
+ const sop = sops.find((s) => {
138
+ const sopNormalized = s.title.toLowerCase().replace(/\s+/g, "-");
139
+ return sopNormalized.includes(normalized) || s.id.toLowerCase().includes(normalized);
140
+ });
141
+ if (!sop) {
142
+ console.log(
143
+ chalk.red(`No SOP found matching process "${processName}"`)
144
+ );
145
+ console.log(chalk.gray("Run: stackmemory company-os list"));
146
+ process.exitCode = 1;
147
+ return;
148
+ }
149
+ const content = readFileSync(sop.path, "utf8");
150
+ const errors = validateSop(content);
151
+ const validProseIds = loadValidProseIds();
152
+ if (sop.proseId && !validProseIds.has(sop.proseId)) {
153
+ errors.push(`Invalid PROSE Expectation: ${sop.proseId}`);
154
+ }
155
+ const compliant = errors.length === 0;
156
+ console.log(chalk.cyan(`
157
+ Company OS Audit: ${sop.id} ${sop.title}
158
+ `));
159
+ console.log(` PROSE Expectation: ${sop.proseId ?? "none"}`);
160
+ console.log(
161
+ ` Status: ${compliant ? chalk.green("compliant") : chalk.red("non-compliant")}`
162
+ );
163
+ if (errors.length > 0) {
164
+ console.log(chalk.yellow("\n Findings:"));
165
+ for (const error of errors) {
166
+ console.log(chalk.gray(` - ${error}`));
167
+ }
168
+ } else {
169
+ console.log(
170
+ chalk.gray(
171
+ "\n Note: This POC audit validates SOP structure only. A full audit would check operational data against the SOP."
172
+ )
173
+ );
174
+ }
175
+ console.log();
176
+ if (!compliant) {
177
+ process.exitCode = 1;
178
+ }
179
+ });
180
+ return cmd;
181
+ }
182
+ export {
183
+ createCompanyOsCommand
184
+ };
@@ -34,6 +34,7 @@ function createContextCommands() {
34
34
  const frameManager = new FrameManager(db, projectId, {
35
35
  skipContextBridge: true
36
36
  });
37
+ await frameManager.initialize();
37
38
  const depth = frameManager.getStackDepth();
38
39
  const activePath = frameManager.getActiveFramePath();
39
40
  console.log(`
@@ -101,6 +102,7 @@ function createContextCommands() {
101
102
  const frameManager = new FrameManager(db, projectId, {
102
103
  skipContextBridge: true
103
104
  });
105
+ await frameManager.initialize();
104
106
  const activePath = frameManager.getActiveFramePath();
105
107
  const parentId = activePath.length > 0 ? activePath[activePath.length - 1].frame_id : void 0;
106
108
  let inputs = {};
@@ -147,6 +149,7 @@ function createContextCommands() {
147
149
  const frameManager = new FrameManager(db, projectId, {
148
150
  skipContextBridge: true
149
151
  });
152
+ await frameManager.initialize();
150
153
  const activePath = frameManager.getActiveFramePath();
151
154
  if (activePath.length === 0) {
152
155
  console.log("\u{1F4DA} Stack is already empty.");
@@ -193,6 +196,7 @@ function createContextCommands() {
193
196
  const frameManager = new FrameManager(db, projectId, {
194
197
  skipContextBridge: true
195
198
  });
199
+ await frameManager.initialize();
196
200
  const activePath = frameManager.getActiveFramePath();
197
201
  if (activePath.length === 0) {
198
202
  console.log("\u26A0\uFE0F No active context frame. Creating one...");
@@ -248,6 +252,7 @@ function createContextCommands() {
248
252
  const frameManager = new FrameManager(db, projectId, {
249
253
  skipContextBridge: true
250
254
  });
255
+ await frameManager.initialize();
251
256
  if (options.list || action === "list") {
252
257
  const worktreeFrames = db.prepare(
253
258
  `
@@ -0,0 +1,127 @@
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 { existsSync, writeFileSync, mkdirSync } from "fs";
7
+ import { join } from "path";
8
+ import { homedir } from "os";
9
+ import { OvernightRunner } from "../../features/operator/overnight-runner.js";
10
+ import { OperatorLogger } from "../../features/operator/operator-logger.js";
11
+ const OPERATOR_DIR = join(homedir(), ".stackmemory", "operator");
12
+ const STOP_SIGNAL_FILE = join(OPERATOR_DIR, "stop-signal");
13
+ const LOG_DIR = join(OPERATOR_DIR, "logs");
14
+ function createOperatorCommands() {
15
+ const operator = new Command("operator").description(
16
+ "Autonomous Claude Code operator \u2014 drives sessions overnight via tmux"
17
+ );
18
+ operator.command("start").description(
19
+ "Start the operator \u2014 drains task queue using Claude Code in tmux"
20
+ ).requiredOption("-f, --file <path>", "Path to master-tasks.md").option("--cwd <dir>", "Working directory for Claude", process.cwd()).option("--mode <mode>", "Adapter: tmux | desktop | browser | auto", "auto").option("--poll <ms>", "Poll interval in ms", "2000").option("--stuck-timeout <ms>", "Stuck detection threshold in ms", "300000").option("--session <name>", "tmux session name", "operator").option("--model <model>", "Claude model override (inner agent)").option(
21
+ "--api-key <key>",
22
+ "Anthropic API key for LLM outer loop (or ANTHROPIC_API_KEY env)"
23
+ ).option(
24
+ "--llm-model <model>",
25
+ "LLM model for outer loop decisions",
26
+ "claude-haiku-4-5-20251001"
27
+ ).option("--no-auto-commit", "Disable auto-commit after each task").option(
28
+ "--max-restarts <n>",
29
+ "Max consecutive restarts before stopping",
30
+ "10"
31
+ ).option("--log-dir <dir>", "Log directory", LOG_DIR).action(async (opts) => {
32
+ if (!existsSync(opts.file)) {
33
+ process.stderr.write(`Error: task file not found: ${opts.file}
34
+ `);
35
+ process.exit(1);
36
+ }
37
+ const apiKey = opts.apiKey ?? process.env.ANTHROPIC_API_KEY;
38
+ if (opts.mode === "desktop" && !apiKey) {
39
+ process.stderr.write(
40
+ "Error: desktop mode requires --api-key or ANTHROPIC_API_KEY for screenshot interpretation\n"
41
+ );
42
+ process.exit(1);
43
+ }
44
+ const config = {
45
+ taskFilePath: opts.file,
46
+ cwd: opts.cwd,
47
+ adapterMode: opts.mode,
48
+ pollIntervalMs: parseInt(opts.poll, 10),
49
+ stuckTimeoutMs: parseInt(opts.stuckTimeout, 10),
50
+ rateLimitBackoffMs: 6e4,
51
+ maxRateLimitBackoffMs: 9e5,
52
+ maxConsecutiveRestarts: parseInt(opts.maxRestarts, 10),
53
+ sessionName: opts.session,
54
+ model: opts.model,
55
+ anthropicApiKey: apiKey,
56
+ llmModel: opts.llmModel,
57
+ autoCommit: opts.autoCommit !== false,
58
+ logDir: opts.logDir
59
+ };
60
+ const runner = new OvernightRunner(config);
61
+ try {
62
+ await runner.run();
63
+ } catch (err) {
64
+ const msg = err instanceof Error ? err.message : String(err);
65
+ process.stderr.write(`Operator error: ${msg}
66
+ `);
67
+ process.exit(1);
68
+ }
69
+ });
70
+ operator.command("stop").description("Signal the running operator to stop gracefully").option("--session <name>", "tmux session name", "operator").action((_opts) => {
71
+ mkdirSync(OPERATOR_DIR, { recursive: true });
72
+ writeFileSync(STOP_SIGNAL_FILE, String(Date.now()), "utf-8");
73
+ process.stderr.write(
74
+ "Stop signal sent. Operator will shut down after current tick.\n"
75
+ );
76
+ });
77
+ operator.command("status").description("Show current operator status from checkpoint").option("--json", "Output raw JSON").action((opts) => {
78
+ const checkpoint = OperatorLogger.readCheckpoint(OPERATOR_DIR);
79
+ if (!checkpoint) {
80
+ process.stderr.write(
81
+ "No operator checkpoint found. Is the operator running?\n"
82
+ );
83
+ process.exit(1);
84
+ }
85
+ if (opts.json) {
86
+ process.stdout.write(JSON.stringify(checkpoint, null, 2) + "\n");
87
+ return;
88
+ }
89
+ const runtime = Date.now() - checkpoint.startedAt;
90
+ const hrs = Math.floor(runtime / 36e5);
91
+ const mins = Math.floor(runtime % 36e5 / 6e4);
92
+ const lines = [
93
+ `State: ${checkpoint.currentState}`,
94
+ `Active task: ${checkpoint.currentTaskId ?? "none"}`,
95
+ `Completed: ${checkpoint.tasksCompleted.length}`,
96
+ `Blocked: ${checkpoint.tasksBlocked.length}`,
97
+ `Runtime: ${hrs}h ${mins}m`,
98
+ `Restarts: ${checkpoint.totalRestarts}`,
99
+ `Approvals: ${checkpoint.totalPermissionApprovals}`,
100
+ `Rate limits: ${checkpoint.totalRateLimitHits}`
101
+ ];
102
+ process.stdout.write(lines.join("\n") + "\n");
103
+ });
104
+ operator.command("attach").description(
105
+ "Attach to the operator tmux session for live observation (Ctrl-B d to detach)"
106
+ ).option("--session <name>", "tmux session name", "operator").action((opts) => {
107
+ try {
108
+ const {
109
+ SessionManager
110
+ } = require("../../features/operator/session-manager.js");
111
+ const mgr = new SessionManager({
112
+ sessionName: opts.session,
113
+ cwd: process.cwd()
114
+ });
115
+ mgr.attach();
116
+ } catch (err) {
117
+ const msg = err instanceof Error ? err.message : String(err);
118
+ process.stderr.write(`Attach error: ${msg}
119
+ `);
120
+ process.exit(1);
121
+ }
122
+ });
123
+ return operator;
124
+ }
125
+ export {
126
+ createOperatorCommands
127
+ };
@@ -18,6 +18,7 @@ import Database from "better-sqlite3";
18
18
  import { logger } from "../../core/monitoring/logger.js";
19
19
  import { isProcessAlive } from "../../utils/process-cleanup.js";
20
20
  import { Conductor } from "./orchestrator.js";
21
+ import { createVisionCommand } from "./vision.js";
21
22
  import {
22
23
  getAgentStatusDir,
23
24
  getOutcomesLogPath
@@ -668,6 +669,7 @@ function createConductorCommands() {
668
669
  }
669
670
  cmd.help();
670
671
  });
672
+ cmd.addCommand(createVisionCommand());
671
673
  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) => {
672
674
  const workspace = options.workspace;
673
675
  const issueId = options.issue;
@@ -3,6 +3,7 @@ import { dirname as __pathDirname } from 'path';
3
3
  const __filename = __fileURLToPath(import.meta.url);
4
4
  const __dirname = __pathDirname(__filename);
5
5
  import { spawn, execSync } from "child_process";
6
+ import { estimateTokens } from "../../core/cache/token-estimator.js";
6
7
  import {
7
8
  appendFileSync,
8
9
  existsSync,
@@ -1602,7 +1603,7 @@ class Conductor {
1602
1603
  }
1603
1604
  if (block.type === "text" && block.text) {
1604
1605
  const text = block.text;
1605
- run.tokensUsed += Math.ceil(text.length / 4);
1606
+ run.tokensUsed += estimateTokens(text);
1606
1607
  turnTextParts.push(text);
1607
1608
  }
1608
1609
  }
@@ -1738,7 +1739,7 @@ class Conductor {
1738
1739
  }
1739
1740
  }
1740
1741
  if (msg.method === "item/text" && params?.text) {
1741
- run.tokensUsed += Math.ceil(params.text.length / 4);
1742
+ run.tokensUsed += estimateTokens(params.text);
1742
1743
  }
1743
1744
  if (run.toolCalls % 5 === 0 || phase) {
1744
1745
  this.writeAgentStatus(issue.identifier, run);
@@ -0,0 +1,254 @@
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, readFileSync, writeFileSync } from "fs";
8
+ import { join } from "path";
9
+ import { homedir } from "os";
10
+ import { PatternStore } from "../../core/patterns/pattern-store.js";
11
+ function getDb() {
12
+ const dbPath = join(homedir(), ".stackmemory", "stackmemory.db");
13
+ return new Database(dbPath);
14
+ }
15
+ function getStore() {
16
+ return new PatternStore(getDb());
17
+ }
18
+ function createPatternsCommand() {
19
+ const patterns = new Command("patterns").description(
20
+ "Manage learned behavioral patterns"
21
+ );
22
+ patterns.command("list").description("List learned patterns").option("-d, --domain <domain>", "Filter by domain").option("-s, --status <status>", "Filter by status", "active").option("--min-confidence <n>", "Minimum confidence", "0").option("--json", "Output JSON").action((opts) => {
23
+ const store = getStore();
24
+ const list = store.list({
25
+ domain: opts.domain,
26
+ status: opts.status,
27
+ minConfidence: parseFloat(opts.minConfidence)
28
+ });
29
+ if (opts.json) {
30
+ process.stdout.write(JSON.stringify(list, null, 2) + "\n");
31
+ return;
32
+ }
33
+ if (list.length === 0) {
34
+ process.stdout.write("No patterns found.\n");
35
+ return;
36
+ }
37
+ for (const p of list) {
38
+ const bar = confidenceBar(p.confidence);
39
+ const scope = p.scope === "global" ? "[global]" : "[project]";
40
+ process.stdout.write(
41
+ `${bar} ${p.confidence.toFixed(2)} ${scope} ${p.id}
42
+ trigger: ${p.trigger}
43
+ action: ${p.action}
44
+ domain: ${p.domain} obs: ${p.observationCount} status: ${p.status}
45
+
46
+ `
47
+ );
48
+ }
49
+ });
50
+ patterns.command("learn").description("Manually record a pattern").requiredOption("-t, --trigger <text>", "When this happens").requiredOption("-a, --action <text>", "Do this").option("-d, --domain <domain>", "Pattern domain", "general").option("--scope <scope>", "project or global", "project").option("--id <id>", "Pattern ID (auto-generated if omitted)").action((opts) => {
51
+ const store = getStore();
52
+ const id = opts.id ?? slugify(`${opts.domain}-${opts.trigger.slice(0, 40)}`);
53
+ const input = {
54
+ id,
55
+ domain: opts.domain,
56
+ trigger: opts.trigger,
57
+ action: opts.action,
58
+ scope: opts.scope,
59
+ source: "manual",
60
+ confidence: 0.5
61
+ };
62
+ const pattern = store.create(input);
63
+ process.stdout.write(
64
+ `Pattern created: ${pattern.id} (confidence: ${pattern.confidence})
65
+ `
66
+ );
67
+ });
68
+ patterns.command("stats").description("Show pattern statistics").option("--json", "Output JSON").action((opts) => {
69
+ const store = getStore();
70
+ const s = store.stats();
71
+ if (opts.json) {
72
+ process.stdout.write(JSON.stringify(s, null, 2) + "\n");
73
+ return;
74
+ }
75
+ const lines = [
76
+ `Total patterns: ${s.total}`,
77
+ `Avg confidence: ${s.avgConfidence.toFixed(2)}`,
78
+ "",
79
+ "By domain:",
80
+ ...Object.entries(s.byDomain).map(([d, n]) => ` ${d}: ${n}`),
81
+ "",
82
+ "By status:",
83
+ ...Object.entries(s.byStatus).map(([d, n]) => ` ${d}: ${n}`)
84
+ ];
85
+ if (s.topPatterns.length > 0) {
86
+ lines.push("", "Top patterns:");
87
+ for (const p of s.topPatterns) {
88
+ lines.push(
89
+ ` ${confidenceBar(p.confidence)} ${p.confidence.toFixed(2)} ${p.id}`
90
+ );
91
+ }
92
+ }
93
+ process.stdout.write(lines.join("\n") + "\n");
94
+ });
95
+ patterns.command("prune").description("Remove old pending patterns").option("--days <n>", "Max age in days", "30").action((opts) => {
96
+ const store = getStore();
97
+ const removed = store.prune(parseInt(opts.days, 10));
98
+ process.stdout.write(
99
+ `Pruned ${removed} pending patterns older than ${opts.days} days.
100
+ `
101
+ );
102
+ });
103
+ patterns.command("export").description("Export patterns to JSON file").option("-o, --output <path>", "Output file", "patterns-export.json").option("--min-confidence <n>", "Minimum confidence", "0.3").action((opts) => {
104
+ const store = getStore();
105
+ const list = store.list({
106
+ status: "active",
107
+ minConfidence: parseFloat(opts.minConfidence)
108
+ });
109
+ writeFileSync(opts.output, JSON.stringify(list, null, 2), "utf-8");
110
+ process.stdout.write(
111
+ `Exported ${list.length} patterns to ${opts.output}
112
+ `
113
+ );
114
+ });
115
+ patterns.command("import").description("Import patterns from JSON file").argument("<file>", "JSON file to import").option("--scope <scope>", "Override scope", "project").action((file, opts) => {
116
+ if (!existsSync(file)) {
117
+ process.stderr.write(`File not found: ${file}
118
+ `);
119
+ process.exit(1);
120
+ }
121
+ const store = getStore();
122
+ const data = JSON.parse(readFileSync(file, "utf-8"));
123
+ const patterns2 = Array.isArray(data) ? data : [data];
124
+ let imported = 0;
125
+ for (const p of patterns2) {
126
+ if (!p.id || !p.trigger || !p.action) continue;
127
+ const existing = store.get(p.id);
128
+ if (existing) {
129
+ store.reinforce(p.id, "imported");
130
+ } else {
131
+ store.create({
132
+ id: p.id,
133
+ domain: p.domain ?? "general",
134
+ trigger: p.trigger,
135
+ action: p.action,
136
+ evidence: p.evidence ?? ["imported"],
137
+ scope: opts.scope ?? p.scope ?? "project",
138
+ source: "imported",
139
+ confidence: p.confidence ?? 0.5
140
+ });
141
+ }
142
+ imported++;
143
+ }
144
+ process.stdout.write(`Imported ${imported} patterns.
145
+ `);
146
+ });
147
+ patterns.command("promote").description("Promote a project-scoped pattern to global").argument("[id]", "Pattern ID to promote (omit for auto-candidates)").option("--dry-run", "Show candidates without promoting").action((id, opts) => {
148
+ const store = getStore();
149
+ if (id) {
150
+ const pattern = store.get(id);
151
+ if (!pattern) {
152
+ process.stderr.write(`Pattern not found: ${id}
153
+ `);
154
+ process.exit(1);
155
+ }
156
+ if (pattern.scope === "global") {
157
+ process.stdout.write(`Already global: ${id}
158
+ `);
159
+ return;
160
+ }
161
+ if (!opts.dryRun) {
162
+ store.promote(id);
163
+ process.stdout.write(`Promoted to global: ${id}
164
+ `);
165
+ } else {
166
+ process.stdout.write(
167
+ `Would promote: ${id} (confidence: ${pattern.confidence.toFixed(2)})
168
+ `
169
+ );
170
+ }
171
+ return;
172
+ }
173
+ const candidates = store.promotionCandidates(0.7);
174
+ if (candidates.length === 0) {
175
+ process.stdout.write(
176
+ "No promotion candidates found (need 0.7+ confidence in 2+ projects).\n"
177
+ );
178
+ return;
179
+ }
180
+ for (const c of candidates) {
181
+ const action = opts.dryRun ? "candidate" : "promoted";
182
+ if (!opts.dryRun) store.promote(c.id);
183
+ process.stdout.write(
184
+ `${action}: ${c.id} (${c.confidence.toFixed(2)}, project: ${c.projectId})
185
+ `
186
+ );
187
+ }
188
+ if (opts.dryRun) {
189
+ process.stdout.write(
190
+ `
191
+ ${candidates.length} candidates. Run without --dry-run to promote.
192
+ `
193
+ );
194
+ }
195
+ });
196
+ patterns.command("projects").description("List projects with pattern counts").option("--json", "Output JSON").action((opts) => {
197
+ const store = getStore();
198
+ const projs = store.projects();
199
+ if (opts.json) {
200
+ process.stdout.write(JSON.stringify(projs, null, 2) + "\n");
201
+ return;
202
+ }
203
+ if (projs.length === 0) {
204
+ process.stdout.write("No project-scoped patterns found.\n");
205
+ return;
206
+ }
207
+ for (const p of projs) {
208
+ process.stdout.write(
209
+ `${p.projectId} ${p.count} patterns avg confidence: ${p.avgConfidence.toFixed(2)}
210
+ `
211
+ );
212
+ }
213
+ });
214
+ patterns.command("evolve").description("Analyze pattern clusters and suggest evolved structures").option("--min-cluster <n>", "Minimum cluster size", "2").option("--json", "Output JSON").action((opts) => {
215
+ const store = getStore();
216
+ const clusters = store.findClusters(parseInt(opts.minCluster, 10));
217
+ if (opts.json) {
218
+ process.stdout.write(JSON.stringify(clusters, null, 2) + "\n");
219
+ return;
220
+ }
221
+ if (clusters.length === 0) {
222
+ process.stdout.write(
223
+ "No pattern clusters found. Need 2+ active patterns in a domain.\n"
224
+ );
225
+ return;
226
+ }
227
+ for (const cluster of clusters) {
228
+ const avgConf = cluster.patterns.reduce((s, p) => s + p.confidence, 0) / cluster.patterns.length;
229
+ const label = avgConf >= 0.8 ? "SKILL candidate" : avgConf >= 0.6 ? "command candidate" : "cluster";
230
+ process.stdout.write(
231
+ `
232
+ [${cluster.domain}] ${cluster.patterns.length} patterns \u2014 ${label} (avg: ${avgConf.toFixed(2)})
233
+ `
234
+ );
235
+ for (const p of cluster.patterns) {
236
+ process.stdout.write(
237
+ ` ${confidenceBar(p.confidence)} ${p.id}: ${p.trigger}
238
+ `
239
+ );
240
+ }
241
+ }
242
+ });
243
+ return patterns;
244
+ }
245
+ function confidenceBar(confidence) {
246
+ const filled = Math.round(confidence * 10);
247
+ return "\u2588".repeat(filled) + "\u2591".repeat(10 - filled);
248
+ }
249
+ function slugify(s) {
250
+ return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
251
+ }
252
+ export {
253
+ createPatternsCommand
254
+ };