@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.
Files changed (95) hide show
  1. package/README.md +104 -23
  2. package/dist/src/cli/claude-sm.js +266 -84
  3. package/dist/src/cli/codex-sm.js +185 -33
  4. package/dist/src/cli/commands/bench.js +209 -2
  5. package/dist/src/cli/commands/cache.js +126 -0
  6. package/dist/src/cli/commands/daemon.js +41 -0
  7. package/dist/src/cli/commands/handoff.js +40 -9
  8. package/dist/src/cli/commands/onboard.js +70 -3
  9. package/dist/src/cli/commands/optimize.js +117 -0
  10. package/dist/src/cli/commands/orchestrate.js +230 -5
  11. package/dist/src/cli/commands/orchestrator.js +312 -24
  12. package/dist/src/cli/commands/pack.js +322 -0
  13. package/dist/src/cli/commands/search.js +40 -1
  14. package/dist/src/cli/commands/setup.js +177 -7
  15. package/dist/src/cli/commands/skills.js +10 -1
  16. package/dist/src/cli/commands/state.js +265 -0
  17. package/dist/src/cli/commands/wiki.js +33 -0
  18. package/dist/src/cli/gemini-sm.js +19 -29
  19. package/dist/src/cli/index.js +90 -29
  20. package/dist/src/cli/opencode-sm.js +38 -21
  21. package/dist/src/cli/utils/determinism-watcher.js +66 -0
  22. package/dist/src/cli/utils/real-cli-bin.js +44 -0
  23. package/dist/src/core/cache/content-cache.js +238 -0
  24. package/dist/src/core/cache/index.js +11 -0
  25. package/dist/src/core/cache/token-estimator.js +16 -0
  26. package/dist/src/core/context/frame-database.js +38 -30
  27. package/dist/src/core/cross-search/cross-project-search.js +269 -0
  28. package/dist/src/core/{merge → cross-search}/index.js +6 -4
  29. package/dist/src/core/database/sqlite-adapter.js +0 -83
  30. package/dist/src/core/extensions/provider-adapter.js +5 -0
  31. package/dist/src/core/models/model-router.js +22 -2
  32. package/dist/src/core/monitoring/logger.js +2 -1
  33. package/dist/src/core/optimization/trace-optimizer.js +413 -0
  34. package/dist/src/core/provenance/confidence-scorer.js +128 -0
  35. package/dist/src/core/provenance/index.js +40 -0
  36. package/dist/src/core/provenance/provenance-store.js +194 -0
  37. package/dist/src/core/provenance/types.js +82 -0
  38. package/dist/src/core/session/project-handoff.js +64 -0
  39. package/dist/src/core/session/session-manager.js +28 -0
  40. package/dist/src/core/shared-state/canonical-store.js +564 -0
  41. package/dist/src/core/skill-packs/index.js +18 -0
  42. package/dist/src/core/skill-packs/parser.js +42 -0
  43. package/dist/src/core/skill-packs/registry.js +224 -0
  44. package/dist/src/core/skill-packs/types.js +66 -0
  45. package/dist/src/core/trace/trace-event-store.js +282 -0
  46. package/dist/src/core/trace/trace-event.js +4 -0
  47. package/dist/src/core/wiki/wiki-compiler.js +219 -0
  48. package/dist/src/daemon/daemon-config.js +7 -0
  49. package/dist/src/daemon/services/github-service.js +126 -0
  50. package/dist/src/daemon/unified-daemon.js +30 -0
  51. package/dist/src/features/sweep/pty-wrapper.js +13 -5
  52. package/dist/src/hooks/schemas.js +2 -0
  53. package/dist/src/integrations/claude-code/subagent-client.js +89 -0
  54. package/dist/src/integrations/github/pr-state.js +158 -0
  55. package/dist/src/integrations/linear/client.js +4 -1
  56. package/dist/src/integrations/mcp/handlers/cross-search-handlers.js +188 -0
  57. package/dist/src/integrations/mcp/handlers/index.js +40 -59
  58. package/dist/src/integrations/mcp/server.js +425 -311
  59. package/dist/src/integrations/mcp/tool-alias-registry.js +370 -0
  60. package/dist/src/integrations/mcp/tool-definitions.js +98 -229
  61. package/dist/src/integrations/ralph/context/stackmemory-context-loader.js +3 -40
  62. package/dist/src/integrations/ralph/learning/pattern-learner.js +1 -20
  63. package/dist/src/integrations/ralph/swarm/swarm-coordinator.js +0 -2
  64. package/dist/src/mcp/stackmemory-mcp-server.js +315 -0
  65. package/dist/src/orchestrators/multimodal/determinism.js +243 -0
  66. package/dist/src/orchestrators/multimodal/harness.js +147 -77
  67. package/dist/src/orchestrators/multimodal/providers.js +44 -3
  68. package/dist/src/utils/hook-installer.js +8 -8
  69. package/package.json +10 -1
  70. package/packs/coding/python-fastapi/instructions.md +60 -0
  71. package/packs/coding/python-fastapi/pack.yaml +28 -0
  72. package/packs/coding/typescript-react/instructions.md +47 -0
  73. package/packs/coding/typescript-react/pack.yaml +28 -0
  74. package/packs/core/commands/capture.md +32 -0
  75. package/packs/core/commands/learn.md +73 -0
  76. package/packs/core/commands/next.md +36 -0
  77. package/packs/core/commands/restart.md +58 -0
  78. package/packs/core/commands/restore.md +29 -0
  79. package/packs/core/commands/start.md +57 -0
  80. package/packs/core/commands/stop.md +65 -0
  81. package/packs/core/commands/summary.md +40 -0
  82. package/packs/core/manifest.json +24 -0
  83. package/packs/ops/decision-recovery/instructions.md +65 -0
  84. package/packs/ops/decision-recovery/pack.yaml +89 -0
  85. package/templates/claude-hooks/doc-ingest.js +76 -0
  86. package/dist/src/cli/commands/team.js +0 -168
  87. package/dist/src/core/context/shared-context-layer.js +0 -620
  88. package/dist/src/core/context/stack-merge-resolver.js +0 -748
  89. package/dist/src/core/merge/conflict-detector.js +0 -430
  90. package/dist/src/core/merge/resolution-engine.js +0 -557
  91. package/dist/src/core/merge/stack-diff.js +0 -531
  92. package/dist/src/core/merge/unified-merge-resolver.js +0 -302
  93. package/dist/src/integrations/mcp/handlers/cord-handlers.js +0 -397
  94. package/dist/src/integrations/mcp/handlers/team-handlers.js +0 -211
  95. /package/dist/src/core/{merge → cache}/types.js +0 -0
@@ -11,9 +11,22 @@ import { program } from "commander";
11
11
  import { v4 as uuidv4 } from "uuid";
12
12
  import chalk from "chalk";
13
13
  import { initializeTracing, trace } from "../core/trace/index.js";
14
+ import { resolveRealCliBin } from "./utils/real-cli-bin.js";
15
+ import {
16
+ startDeterminismWatcher,
17
+ stopDeterminismWatcher
18
+ } from "./utils/determinism-watcher.js";
19
+ import {
20
+ canonicalStateStore,
21
+ projectIdFromIdentifier
22
+ } from "../core/shared-state/canonical-store.js";
14
23
  class CodexSM {
15
24
  config;
16
25
  stackmemoryPath;
26
+ sessionId;
27
+ ownsSession;
28
+ sessionEnded;
29
+ determinismWatcher;
17
30
  constructor() {
18
31
  this.config = {
19
32
  instanceId: this.generateInstanceId(),
@@ -23,6 +36,10 @@ class CodexSM {
23
36
  verboseTracing: false
24
37
  };
25
38
  this.stackmemoryPath = this.findStackMemory();
39
+ this.sessionId = process.env["STACKMEMORY_SESSION"] || uuidv4();
40
+ this.ownsSession = !process.env["STACKMEMORY_SESSION"];
41
+ this.sessionEnded = false;
42
+ this.determinismWatcher = null;
26
43
  }
27
44
  getRepoRoot() {
28
45
  try {
@@ -87,6 +104,20 @@ class CodexSM {
87
104
  return "main";
88
105
  }
89
106
  }
107
+ getProjectId() {
108
+ const root = this.getRepoRoot() || process.cwd();
109
+ try {
110
+ const remote = execSync("git config --get remote.origin.url", {
111
+ cwd: root,
112
+ encoding: "utf8"
113
+ }).trim();
114
+ if (remote) {
115
+ return projectIdFromIdentifier(remote);
116
+ }
117
+ } catch {
118
+ }
119
+ return projectIdFromIdentifier(root);
120
+ }
90
121
  hasUncommittedChanges() {
91
122
  try {
92
123
  const status = execSync("git status --porcelain", { encoding: "utf8" });
@@ -96,24 +127,26 @@ class CodexSM {
96
127
  }
97
128
  }
98
129
  resolveCodexBin() {
99
- if (this.config.codexBin && this.config.codexBin.trim()) {
100
- return this.config.codexBin.trim();
101
- }
102
- const envBin = process.env["CODEX_BIN"];
103
- if (envBin && envBin.trim()) {
104
- return envBin.trim();
105
- }
106
- try {
107
- execSync("which codex", { stdio: "ignore" });
108
- return "codex";
109
- } catch {
110
- }
111
- try {
112
- execSync("which codex-cli", { stdio: "ignore" });
113
- return "codex-cli";
114
- } catch {
115
- }
116
- return null;
130
+ return resolveRealCliBin({
131
+ explicitBin: this.config.codexBin,
132
+ envBin: process.env["CODEX_BIN"],
133
+ preferredPaths: [
134
+ path.join(
135
+ os.homedir(),
136
+ ".nvm",
137
+ "versions",
138
+ "node",
139
+ "v22.22.0",
140
+ "bin",
141
+ "codex"
142
+ ),
143
+ "/usr/local/bin/codex",
144
+ "/opt/homebrew/bin/codex",
145
+ "/usr/local/bin/codex-cli",
146
+ "/opt/homebrew/bin/codex-cli"
147
+ ],
148
+ pathCommands: ["codex", "codex-cli"]
149
+ });
117
150
  }
118
151
  setupWorktree() {
119
152
  if (!this.config.useWorktree || !this.isGitRepo()) return null;
@@ -173,18 +206,14 @@ class CodexSM {
173
206
  if (!this.config.contextEnabled) return;
174
207
  try {
175
208
  console.log(chalk.blue("\u{1F4DA} Loading previous context..."));
176
- const cmd = `${this.stackmemoryPath} context list --limit 5 --format json`;
177
- const output = execSync(cmd, { encoding: "utf8" });
178
- const contexts = JSON.parse(output);
179
- if (Array.isArray(contexts) && contexts.length > 0) {
180
- console.log(chalk.gray("Recent context loaded:"));
181
- contexts.forEach(
182
- (ctx) => {
183
- console.log(
184
- chalk.gray(` - ${ctx.message} (${ctx.metadata?.timestamp})`)
185
- );
186
- }
187
- );
209
+ const cmd = `${this.stackmemoryPath} context show`;
210
+ const output = execSync(cmd, {
211
+ encoding: "utf8",
212
+ stdio: ["pipe", "pipe", "pipe"]
213
+ });
214
+ const lines = output.trim().split("\n").filter((line) => line.trim());
215
+ if (lines.length > 3) {
216
+ console.log(chalk.gray("Context stack loaded"));
188
217
  }
189
218
  } catch {
190
219
  }
@@ -197,6 +226,117 @@ class CodexSM {
197
226
  );
198
227
  }
199
228
  }
229
+ async publishSessionStart() {
230
+ const projectPath = process.cwd();
231
+ const projectId = this.getProjectId();
232
+ const branch = this.isGitRepo() ? this.getCurrentBranch() : void 0;
233
+ await canonicalStateStore.upsertSession({
234
+ sessionId: this.sessionId,
235
+ tool: "codex",
236
+ projectId,
237
+ projectPath,
238
+ branch,
239
+ instanceId: this.config.instanceId,
240
+ metadata: {
241
+ task: this.config.task
242
+ }
243
+ });
244
+ await canonicalStateStore.upsertInstance({
245
+ instanceId: this.config.instanceId,
246
+ tool: "codex",
247
+ sessionId: this.sessionId,
248
+ projectId,
249
+ projectPath,
250
+ branch,
251
+ worktreePath: this.config.worktreePath,
252
+ pid: process.pid,
253
+ status: "active",
254
+ metadata: {
255
+ task: this.config.task
256
+ }
257
+ });
258
+ await canonicalStateStore.appendEvent({
259
+ type: "session_start",
260
+ tool: "codex",
261
+ sessionId: this.sessionId,
262
+ instanceId: this.config.instanceId,
263
+ projectId,
264
+ projectPath,
265
+ branch,
266
+ payload: {
267
+ task: this.config.task,
268
+ worktreePath: this.config.worktreePath
269
+ }
270
+ });
271
+ const claimResult = await canonicalStateStore.claimPaths({
272
+ tool: "codex",
273
+ sessionId: this.sessionId,
274
+ instanceId: this.config.instanceId,
275
+ projectId,
276
+ projectPath,
277
+ branch,
278
+ paths: [],
279
+ metadata: {
280
+ task: this.config.task,
281
+ scope: "branch"
282
+ }
283
+ });
284
+ if (claimResult.conflicts.length > 0) {
285
+ console.log(chalk.yellow("\u26A0\uFE0F Shared state conflict detected"));
286
+ for (const conflict of claimResult.conflicts.slice(0, 3)) {
287
+ console.log(
288
+ chalk.gray(
289
+ ` Claim ${conflict.claimId.slice(0, 8)} already owns ${conflict.branch || "overlapping work"}`
290
+ )
291
+ );
292
+ }
293
+ }
294
+ }
295
+ async publishSessionEnd(eventType, payload = {}) {
296
+ if (this.sessionEnded) {
297
+ return;
298
+ }
299
+ this.sessionEnded = true;
300
+ const projectPath = process.cwd();
301
+ const projectId = this.getProjectId();
302
+ const branch = this.isGitRepo() ? this.getCurrentBranch() : void 0;
303
+ await canonicalStateStore.appendEvent({
304
+ type: eventType,
305
+ tool: "codex",
306
+ sessionId: this.sessionId,
307
+ instanceId: this.config.instanceId,
308
+ projectId,
309
+ projectPath,
310
+ branch,
311
+ payload
312
+ });
313
+ await canonicalStateStore.releaseClaims({
314
+ instanceId: this.config.instanceId,
315
+ reason: eventType
316
+ });
317
+ await canonicalStateStore.endInstance(this.config.instanceId);
318
+ if (this.ownsSession) {
319
+ await canonicalStateStore.endSession(this.sessionId);
320
+ }
321
+ }
322
+ startDeterminismWatcher() {
323
+ this.determinismWatcher = startDeterminismWatcher({
324
+ stackmemoryBin: this.stackmemoryPath,
325
+ cwd: process.cwd(),
326
+ task: this.config.task,
327
+ instanceId: this.config.instanceId,
328
+ sessionId: this.sessionId,
329
+ tool: "codex"
330
+ });
331
+ if (this.determinismWatcher) {
332
+ const modeLabel = this.determinismWatcher.mode === "targeted" ? "targeted" : "repo-root fallback";
333
+ console.log(chalk.gray(`\u{1F9EA} Determinism: ${modeLabel}`));
334
+ }
335
+ }
336
+ stopDeterminismWatcher() {
337
+ stopDeterminismWatcher(this.determinismWatcher);
338
+ this.determinismWatcher = null;
339
+ }
200
340
  async run(args) {
201
341
  const codexArgs = [];
202
342
  let i = 0;
@@ -293,9 +433,13 @@ class CodexSM {
293
433
  }
294
434
  this.loadContext();
295
435
  process.env["CODEX_INSTANCE_ID"] = this.config.instanceId;
436
+ process.env["STACKMEMORY_SESSION"] = this.sessionId;
296
437
  if (this.config.worktreePath)
297
438
  process.env["CODEX_WORKTREE_PATH"] = this.config.worktreePath;
439
+ await this.publishSessionStart();
440
+ this.startDeterminismWatcher();
298
441
  console.log(chalk.gray(`\u{1F916} Instance ID: ${this.config.instanceId}`));
442
+ console.log(chalk.gray(`\u{1F9E0} Session ID: ${this.sessionId.slice(0, 8)}`));
299
443
  console.log(chalk.gray(`\u{1F4C1} Working in: ${process.cwd()}`));
300
444
  console.log();
301
445
  console.log(chalk.gray("Starting Codex..."));
@@ -334,11 +478,15 @@ class CodexSM {
334
478
  }
335
479
  process.exit(1);
336
480
  });
337
- child.on("exit", (code) => {
481
+ child.on("exit", async (code) => {
482
+ this.stopDeterminismWatcher();
338
483
  this.saveContext("Codex session ended", {
339
484
  action: "session_end",
340
485
  exitCode: code
341
486
  });
487
+ await this.publishSessionEnd("session_end", {
488
+ exitCode: code
489
+ });
342
490
  try {
343
491
  execSync("stackmemory linear sync", {
344
492
  stdio: "ignore",
@@ -361,16 +509,20 @@ class CodexSM {
361
509
  }
362
510
  process.exit(code || 0);
363
511
  });
364
- process.on("SIGINT", () => {
512
+ process.on("SIGINT", async () => {
513
+ this.stopDeterminismWatcher();
365
514
  this.saveContext("Codex session interrupted", {
366
515
  action: "session_interrupt"
367
516
  });
517
+ await this.publishSessionEnd("session_interrupt");
368
518
  child.kill("SIGINT");
369
519
  });
370
- process.on("SIGTERM", () => {
520
+ process.on("SIGTERM", async () => {
521
+ this.stopDeterminismWatcher();
371
522
  this.saveContext("Codex session terminated", {
372
523
  action: "session_terminate"
373
524
  });
525
+ await this.publishSessionEnd("session_terminate");
374
526
  child.kill("SIGTERM");
375
527
  });
376
528
  }
@@ -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 { Command } from "commander";
6
+ import chalk from "chalk";
6
7
  import { existsSync, readFileSync, readdirSync } from "fs";
7
8
  import { join } from "path";
8
9
  import {
@@ -13,6 +14,13 @@ import {
13
14
  import {
14
15
  feedbackLoops
15
16
  } from "../../core/monitoring/feedback-loops.js";
17
+ import {
18
+ DETERMINISM_WATCH_IGNORE,
19
+ getDeterminismWatchTargets,
20
+ persistDeterminismReport,
21
+ readLatestDeterminismReport,
22
+ runDeterminismSmoke
23
+ } from "../../orchestrators/multimodal/determinism.js";
16
24
  function loadRunMetrics(projectRoot) {
17
25
  const metricsFile = join(
18
26
  projectRoot,
@@ -45,6 +53,56 @@ function loadSpikeAudits(projectRoot) {
45
53
  }
46
54
  }).filter(Boolean);
47
55
  }
56
+ function printDeterminismReport(task, requestedRuns, report) {
57
+ console.log("\nHarness Determinism Smoke");
58
+ console.log("\u2550".repeat(60));
59
+ console.log(`Task: ${task}`);
60
+ console.log(`Runs: ${report.runs}`);
61
+ console.log(`Determinism score: ${report.score.toFixed(2)}/100`);
62
+ console.log("\nDimension Scores:");
63
+ for (const dimension of report.dimensions) {
64
+ console.log(
65
+ ` ${dimension.name.padEnd(14)} ${dimension.score.toFixed(2).padStart(6)}/100 ${dimension.details}`
66
+ );
67
+ }
68
+ if (report.recommendations.length > 0) {
69
+ console.log("\nRecommended Tightening:");
70
+ for (const recommendation of report.recommendations) {
71
+ console.log(` - ${recommendation}`);
72
+ }
73
+ } else {
74
+ console.log("\nNo drift detected in deterministic fixture mode.");
75
+ }
76
+ const sample = report.snapshots[0];
77
+ if (sample) {
78
+ console.log("\nReference Snapshot:");
79
+ console.log(` resultHash: ${sample.resultHash.slice(0, 16)}`);
80
+ console.log(` planHash: ${sample.planHash.slice(0, 16)}`);
81
+ console.log(` critiqueHash: ${sample.critiqueHash.slice(0, 16)}`);
82
+ console.log(` commandsHash: ${sample.commandsHash.slice(0, 16)}`);
83
+ console.log(` iterations: ${sample.iterations}`);
84
+ console.log(` contextTokens: ${sample.contextTokens}`);
85
+ }
86
+ if (report.runs !== requestedRuns) {
87
+ console.log(
88
+ `
89
+ Note: requested ${requestedRuns} runs, completed ${report.runs}.`
90
+ );
91
+ }
92
+ console.log("");
93
+ }
94
+ function printStoredDeterminismReport(stored) {
95
+ console.log("\nCached Determinism Result");
96
+ console.log("\u2550".repeat(60));
97
+ console.log(`Task: ${stored.task}`);
98
+ console.log(`Trigger: ${stored.trigger}`);
99
+ console.log(`Timestamp: ${stored.timestamp}`);
100
+ console.log(`Determinism score: ${stored.report.score.toFixed(2)}/100`);
101
+ if (stored.changedPaths.length > 0) {
102
+ console.log(`Changed paths: ${stored.changedPaths.join(", ")}`);
103
+ }
104
+ console.log("");
105
+ }
48
106
  function createBenchCommand() {
49
107
  const bench = new Command("bench").description(
50
108
  "Harness benchmarks \u2014 compare local runs against SWE-bench baselines"
@@ -197,11 +255,160 @@ Recent Spike Audits (${audits.length}):`);
197
255
  }
198
256
  console.log("");
199
257
  });
200
- bench.command("loops").description("Show feedback loop configuration, status, and recent events").option("--json", "Output as JSON", false).action((options) => {
258
+ bench.command("determinism").description(
259
+ "Run deterministic fixture smoke checks for the multimodal harness"
260
+ ).option(
261
+ "-t, --task <desc>",
262
+ "Task description to run through the harness",
263
+ "Add a small auth guard"
264
+ ).option("--runs <n>", "Number of repeated runs", "5").option(
265
+ "--planner-model <name>",
266
+ "Planner model label to include in the run config",
267
+ "claude-sonnet-4-20250514"
268
+ ).option(
269
+ "--reviewer-model <name>",
270
+ "Reviewer model label to include in the run config",
271
+ "claude-sonnet-4-20250514"
272
+ ).option("--implementer <name>", "codex|claude", "codex").option("--max-iters <n>", "Retry loop iterations", "2").option(
273
+ "--watch",
274
+ "Watch harness-critical files and rerun on changes",
275
+ false
276
+ ).option(
277
+ "--debounce-ms <n>",
278
+ "Debounce window for write completion in watch mode",
279
+ "3000"
280
+ ).option("--latest", "Show the latest cached determinism result", false).option("--json", "Output as JSON", false).action(async function() {
281
+ const command = this;
282
+ const options = command.opts();
283
+ const json = Boolean(options.json || command.parent?.opts().json);
284
+ const projectRoot = process.cwd();
285
+ const runs = Math.max(1, parseInt(options.runs, 10) || 5);
286
+ const debounceMs = Math.max(
287
+ 250,
288
+ parseInt(options.debounceMs, 10) || 3e3
289
+ );
290
+ if (options.latest) {
291
+ const stored = readLatestDeterminismReport(projectRoot);
292
+ if (!stored) {
293
+ console.error("No cached determinism result found.");
294
+ process.exitCode = 1;
295
+ return;
296
+ }
297
+ if (json) {
298
+ console.log(JSON.stringify(stored, null, 2));
299
+ return;
300
+ }
301
+ printStoredDeterminismReport(stored);
302
+ return;
303
+ }
304
+ const runCheck = async (trigger, changedPaths = []) => {
305
+ const report = await runDeterminismSmoke(
306
+ {
307
+ task: options.task,
308
+ repoPath: projectRoot
309
+ },
310
+ {
311
+ runs,
312
+ plannerModel: options.plannerModel,
313
+ reviewerModel: options.reviewerModel,
314
+ implementer: options.implementer,
315
+ maxIters: parseInt(options.maxIters, 10) || 2
316
+ }
317
+ );
318
+ const stored = persistDeterminismReport(projectRoot, report, {
319
+ task: options.task,
320
+ trigger,
321
+ changedPaths
322
+ });
323
+ if (json) {
324
+ console.log(JSON.stringify(stored, null, 2));
325
+ } else {
326
+ printDeterminismReport(options.task, runs, report);
327
+ }
328
+ return stored;
329
+ };
330
+ if (options.watch) {
331
+ const chokidar = await import("chokidar");
332
+ const watchTargets = getDeterminismWatchTargets(projectRoot);
333
+ const watchPatterns = watchTargets.map(
334
+ (pattern) => join(projectRoot, pattern)
335
+ );
336
+ const watcher = chokidar.watch(watchPatterns, {
337
+ ignoreInitial: true,
338
+ ignored: DETERMINISM_WATCH_IGNORE.map(
339
+ (pattern) => join(projectRoot, pattern)
340
+ ),
341
+ awaitWriteFinish: {
342
+ stabilityThreshold: debounceMs,
343
+ pollInterval: 100
344
+ }
345
+ });
346
+ let running = false;
347
+ let rerunRequested = false;
348
+ const pendingPaths = /* @__PURE__ */ new Set();
349
+ const maybeRun = async (trigger) => {
350
+ if (running) {
351
+ rerunRequested = true;
352
+ return;
353
+ }
354
+ running = true;
355
+ const changedPaths = Array.from(pendingPaths).sort();
356
+ pendingPaths.clear();
357
+ try {
358
+ await runCheck(trigger, changedPaths);
359
+ } finally {
360
+ running = false;
361
+ if (rerunRequested) {
362
+ rerunRequested = false;
363
+ await maybeRun("watch:debounced-rerun");
364
+ }
365
+ }
366
+ };
367
+ const onFileEvent = async (trigger, filePath) => {
368
+ const relativePath = filePath.startsWith(projectRoot) ? filePath.slice(projectRoot.length + 1) : filePath;
369
+ pendingPaths.add(relativePath);
370
+ if (!json) {
371
+ console.log(
372
+ chalk.gray(`determinism watcher: ${trigger} ${relativePath}`)
373
+ );
374
+ }
375
+ await maybeRun(`watch:${trigger}`);
376
+ };
377
+ watcher.on("all", async (eventName, filePath) => {
378
+ if (eventName !== "add" && eventName !== "change") {
379
+ return;
380
+ }
381
+ await onFileEvent(eventName, filePath);
382
+ });
383
+ if (!json) {
384
+ console.log("\nHarness Determinism Watch");
385
+ console.log("\u2550".repeat(60));
386
+ console.log(`Task: ${options.task}`);
387
+ console.log(`Watching: ${watchTargets.join(", ")}`);
388
+ console.log(`Debounce: ${debounceMs}ms`);
389
+ console.log(chalk.gray("Press Ctrl+C to stop.\n"));
390
+ }
391
+ await runCheck("watch:initial");
392
+ await new Promise((resolve) => {
393
+ const stop = () => {
394
+ void watcher.close();
395
+ resolve();
396
+ };
397
+ process.once("SIGINT", stop);
398
+ process.once("SIGTERM", stop);
399
+ });
400
+ return;
401
+ }
402
+ await runCheck("manual");
403
+ });
404
+ bench.command("loops").description("Show feedback loop configuration, status, and recent events").option("--json", "Output as JSON", false).action(function() {
405
+ const command = this;
406
+ const options = command.opts();
407
+ const json = Boolean(options.json || command.parent?.opts().json);
201
408
  const config = feedbackLoops.getConfig();
202
409
  const stats = feedbackLoops.getStats();
203
410
  const history = feedbackLoops.getHistory(void 0, 20);
204
- if (options.json) {
411
+ if (json) {
205
412
  console.log(JSON.stringify({ config, stats, history }, null, 2));
206
413
  return;
207
414
  }
@@ -0,0 +1,126 @@
1
+ import { fileURLToPath as __fileURLToPath } from 'url';
2
+ import { dirname as __pathDirname } from 'path';
3
+ const __filename = __fileURLToPath(import.meta.url);
4
+ const __dirname = __pathDirname(__filename);
5
+ import { Command } from "commander";
6
+ import chalk from "chalk";
7
+ import * as path from "path";
8
+ import { existsSync } from "fs";
9
+ import Database from "better-sqlite3";
10
+ import { ContentCache } from "../../core/cache/index.js";
11
+ function findProjectDbPath() {
12
+ let dir = process.cwd();
13
+ while (dir !== "/") {
14
+ const dbPath = path.join(dir, ".stackmemory", "context.db");
15
+ if (existsSync(dbPath)) return dbPath;
16
+ dir = path.dirname(dir);
17
+ }
18
+ const home = process.env["HOME"] || "/tmp";
19
+ const homePath = path.join(home, ".stackmemory", "context.db");
20
+ if (existsSync(homePath)) return homePath;
21
+ return void 0;
22
+ }
23
+ function getProjectDb() {
24
+ const dbPath = findProjectDbPath();
25
+ if (!dbPath) return void 0;
26
+ return new Database(dbPath);
27
+ }
28
+ function createCacheCommand() {
29
+ const cmd = new Command("cache").description(
30
+ "Content-hash cache \u2014 view token savings and manage cached content"
31
+ );
32
+ cmd.command("stats").description("Show cache statistics and token savings").option("--json", "Output as JSON").action((options) => {
33
+ const db = getProjectDb();
34
+ if (!db) {
35
+ console.log(chalk.yellow("No project database found"));
36
+ return;
37
+ }
38
+ try {
39
+ const cache = new ContentCache(db);
40
+ const stats = cache.getStats();
41
+ if (options.json) {
42
+ console.log(JSON.stringify(stats, null, 2));
43
+ return;
44
+ }
45
+ console.log(chalk.bold("Content Cache Statistics\n"));
46
+ console.log(` Entries: ${stats.totalEntries.toLocaleString()}`);
47
+ console.log(
48
+ ` Tokens cached: ${stats.totalTokensCached.toLocaleString()}`
49
+ );
50
+ console.log(
51
+ ` Tokens saved: ${chalk.green(stats.totalTokensSaved.toLocaleString())}`
52
+ );
53
+ console.log(` Hit rate: ${(stats.hitRate * 100).toFixed(1)}%`);
54
+ if (stats.topSources.length > 0) {
55
+ console.log(chalk.bold("\n Top sources by tokens saved:"));
56
+ for (const src of stats.topSources.slice(0, 5)) {
57
+ console.log(
58
+ ` ${chalk.dim(src.source)}: ${src.tokensSaved.toLocaleString()} tokens`
59
+ );
60
+ }
61
+ }
62
+ const costSaved = stats.totalTokensSaved / 1e6 * 3;
63
+ if (costSaved > 0.01) {
64
+ console.log(
65
+ `
66
+ Est. cost saved: ${chalk.green("$" + costSaved.toFixed(2))}`
67
+ );
68
+ }
69
+ } finally {
70
+ db.close();
71
+ }
72
+ });
73
+ cmd.command("clear").description("Clear the content cache").option("--confirm", "Skip confirmation prompt").action((options) => {
74
+ if (!options.confirm) {
75
+ console.log(
76
+ chalk.yellow(
77
+ "This will clear all cached content. Run with --confirm to proceed."
78
+ )
79
+ );
80
+ return;
81
+ }
82
+ const db = getProjectDb();
83
+ if (!db) {
84
+ console.log(chalk.yellow("No project database found"));
85
+ return;
86
+ }
87
+ try {
88
+ const cache = new ContentCache(db);
89
+ cache.clear();
90
+ console.log(chalk.green("Cache cleared"));
91
+ } finally {
92
+ db.close();
93
+ }
94
+ });
95
+ cmd.command("search <query>").description("Search cached content by keyword").option("--limit <n>", "Max results", "10").action((query, options) => {
96
+ const db = getProjectDb();
97
+ if (!db) {
98
+ console.log(chalk.yellow("No project database found"));
99
+ return;
100
+ }
101
+ try {
102
+ const cache = new ContentCache(db);
103
+ const results = cache.search(query, parseInt(options.limit));
104
+ if (results.length === 0) {
105
+ console.log(chalk.dim(`No cached content matching "${query}"`));
106
+ return;
107
+ }
108
+ console.log(chalk.bold(`${results.length} result(s):
109
+ `));
110
+ for (const entry of results) {
111
+ const preview = entry.content.slice(0, 120).replace(/\n/g, " ");
112
+ console.log(
113
+ ` ${chalk.dim(entry.hash.slice(0, 8))} ${chalk.dim(`(${entry.tokenCount} tokens, ${entry.hitCount} hits)`)}`
114
+ );
115
+ console.log(` ${preview}${entry.content.length > 120 ? "..." : ""}`);
116
+ console.log();
117
+ }
118
+ } finally {
119
+ db.close();
120
+ }
121
+ });
122
+ return cmd;
123
+ }
124
+ export {
125
+ createCacheCommand
126
+ };