@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
package/dist/src/cli/codex-sm.js
CHANGED
|
@@ -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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
177
|
-
const output = execSync(cmd, {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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("
|
|
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 (
|
|
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
|
+
};
|