@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
|
@@ -18,6 +18,10 @@ import {
|
|
|
18
18
|
import { FrameManager } from "../core/context/index.js";
|
|
19
19
|
import { AgentTaskManager } from "../agents/core/agent-task-manager.js";
|
|
20
20
|
import { logger } from "../core/monitoring/logger.js";
|
|
21
|
+
import { ContentCache } from "../core/cache/content-cache.js";
|
|
22
|
+
import { getSkillPackRegistry } from "../core/skill-packs/index.js";
|
|
23
|
+
import { ProvenanceStore } from "../core/provenance/provenance-store.js";
|
|
24
|
+
import { scoreConfidence } from "../core/provenance/confidence-scorer.js";
|
|
21
25
|
const PROJECT_ROOT = process.env["STACKMEMORY_PROJECT"] || process.cwd();
|
|
22
26
|
const stackmemoryDir = join(PROJECT_ROOT, ".stackmemory");
|
|
23
27
|
if (!existsSync(stackmemoryDir)) {
|
|
@@ -27,6 +31,11 @@ const db = new Database(join(stackmemoryDir, "cache.db"));
|
|
|
27
31
|
const taskStore = new LinearTaskManager(PROJECT_ROOT, db);
|
|
28
32
|
const frameManager = new FrameManager(db, PROJECT_ROOT, void 0);
|
|
29
33
|
const agentTaskManager = new AgentTaskManager(taskStore, frameManager);
|
|
34
|
+
const contentCacheDb = new Database(join(stackmemoryDir, "content-cache.db"));
|
|
35
|
+
const contentCache = new ContentCache(contentCacheDb);
|
|
36
|
+
const provenanceDb = new Database(join(stackmemoryDir, "provenance.db"));
|
|
37
|
+
const provenanceStore = new ProvenanceStore(provenanceDb);
|
|
38
|
+
const packRegistry = getSkillPackRegistry();
|
|
30
39
|
let _claudeSessionId = null;
|
|
31
40
|
let claudeFrameId = null;
|
|
32
41
|
const TOOLS = [
|
|
@@ -182,6 +191,137 @@ const TOOLS = [
|
|
|
182
191
|
},
|
|
183
192
|
required: ["sessionId"]
|
|
184
193
|
}
|
|
194
|
+
},
|
|
195
|
+
// ── Content Cache ───────────────────────────────────────────────────
|
|
196
|
+
{
|
|
197
|
+
name: "cache_lookup",
|
|
198
|
+
description: "Check if content has been seen before. Returns cache hit/miss and token savings.",
|
|
199
|
+
inputSchema: {
|
|
200
|
+
type: "object",
|
|
201
|
+
properties: {
|
|
202
|
+
content: { type: "string", description: "Content to check/cache" },
|
|
203
|
+
source: {
|
|
204
|
+
type: "string",
|
|
205
|
+
description: 'Where this content came from (e.g. "file:src/index.ts")'
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
required: ["content"]
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
name: "cache_stats",
|
|
213
|
+
description: "Get content cache statistics: total entries, tokens cached, tokens saved, hit rate.",
|
|
214
|
+
inputSchema: {
|
|
215
|
+
type: "object",
|
|
216
|
+
properties: {}
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
// ── Skill Packs ─────────────────────────────────────────────────────
|
|
220
|
+
{
|
|
221
|
+
name: "pack_list",
|
|
222
|
+
description: "List installed skill packs, optionally filtered by namespace.",
|
|
223
|
+
inputSchema: {
|
|
224
|
+
type: "object",
|
|
225
|
+
properties: {
|
|
226
|
+
namespace: {
|
|
227
|
+
type: "string",
|
|
228
|
+
description: 'Filter by namespace (e.g. "coding", "ops")'
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
name: "pack_search",
|
|
235
|
+
description: "Search installed skill packs by keyword.",
|
|
236
|
+
inputSchema: {
|
|
237
|
+
type: "object",
|
|
238
|
+
properties: {
|
|
239
|
+
query: { type: "string", description: "Search keyword" }
|
|
240
|
+
},
|
|
241
|
+
required: ["query"]
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
name: "pack_get",
|
|
246
|
+
description: "Get full details of a skill pack including instructions and MCP tools.",
|
|
247
|
+
inputSchema: {
|
|
248
|
+
type: "object",
|
|
249
|
+
properties: {
|
|
250
|
+
name: {
|
|
251
|
+
type: "string",
|
|
252
|
+
description: 'Pack name (e.g. "coding/typescript-react")'
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
required: ["name"]
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
// ── Provenance ──────────────────────────────────────────────────────
|
|
259
|
+
{
|
|
260
|
+
name: "record_trace",
|
|
261
|
+
description: "Record a provenance-tracked trace event with actor, operation, and source lineage.",
|
|
262
|
+
inputSchema: {
|
|
263
|
+
type: "object",
|
|
264
|
+
properties: {
|
|
265
|
+
traceId: { type: "string", description: "Unique trace ID" },
|
|
266
|
+
sessionId: { type: "string", description: "Session ID" },
|
|
267
|
+
tenantId: { type: "string", description: "Tenant ID" },
|
|
268
|
+
operation: {
|
|
269
|
+
type: "string",
|
|
270
|
+
description: 'What happened (e.g. "query", "decision", "edit")'
|
|
271
|
+
},
|
|
272
|
+
host: {
|
|
273
|
+
type: "string",
|
|
274
|
+
description: 'Agent host (e.g. "claude-code", "cursor")'
|
|
275
|
+
},
|
|
276
|
+
inputs: { type: "object", description: "Operation inputs" },
|
|
277
|
+
outputs: { type: "object", description: "Operation outputs" },
|
|
278
|
+
tokensIn: { type: "number", description: "Input tokens" },
|
|
279
|
+
tokensOut: { type: "number", description: "Output tokens" },
|
|
280
|
+
costUsd: { type: "number", description: "Cost in USD" },
|
|
281
|
+
parentTraceId: { type: "string", description: "Parent trace ID" },
|
|
282
|
+
score: { type: "number", description: "Numeric evaluation score" },
|
|
283
|
+
feedback: {
|
|
284
|
+
type: "string",
|
|
285
|
+
description: "Textual feedback for optimization"
|
|
286
|
+
},
|
|
287
|
+
confidence: {
|
|
288
|
+
type: "number",
|
|
289
|
+
description: "Confidence level (0-1)",
|
|
290
|
+
minimum: 0,
|
|
291
|
+
maximum: 1
|
|
292
|
+
},
|
|
293
|
+
sources: {
|
|
294
|
+
type: "array",
|
|
295
|
+
items: {
|
|
296
|
+
type: "object",
|
|
297
|
+
properties: {
|
|
298
|
+
system: { type: "string" },
|
|
299
|
+
externalId: { type: "string" },
|
|
300
|
+
url: { type: "string" }
|
|
301
|
+
},
|
|
302
|
+
required: ["system", "externalId"]
|
|
303
|
+
},
|
|
304
|
+
description: "Source references for provenance"
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
required: ["traceId", "sessionId", "tenantId", "operation"]
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
name: "score_confidence",
|
|
312
|
+
description: "Score text for decision confidence. Returns confidence (0-1), signals, and classification (accept/review/discard).",
|
|
313
|
+
inputSchema: {
|
|
314
|
+
type: "object",
|
|
315
|
+
properties: {
|
|
316
|
+
text: { type: "string", description: "Text to score" },
|
|
317
|
+
actor: { type: "string", description: "Who said it (boosts score)" },
|
|
318
|
+
replyCount: {
|
|
319
|
+
type: "number",
|
|
320
|
+
description: "Thread reply count (boosts if >2)"
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
required: ["text"]
|
|
324
|
+
}
|
|
185
325
|
}
|
|
186
326
|
];
|
|
187
327
|
const server = new Server(
|
|
@@ -500,6 +640,179 @@ Ready for next action.`
|
|
|
500
640
|
]
|
|
501
641
|
};
|
|
502
642
|
}
|
|
643
|
+
// ── Content Cache handlers ──────────────────────────────────────
|
|
644
|
+
case "cache_lookup": {
|
|
645
|
+
const { content, source } = args;
|
|
646
|
+
const result = contentCache.lookup(content, source ?? "mcp");
|
|
647
|
+
if (!result.hit) {
|
|
648
|
+
contentCache.put(content, source ?? "mcp");
|
|
649
|
+
}
|
|
650
|
+
return {
|
|
651
|
+
content: [
|
|
652
|
+
{
|
|
653
|
+
type: "text",
|
|
654
|
+
text: result.hit ? `Cache HIT (hash: ${result.hash.slice(0, 12)}...). Tokens saved: ${result.tokensSaved}. Total hits: ${result.entry?.hitCount ?? 0}.` : `Cache MISS (hash: ${result.hash.slice(0, 12)}...). Content cached for future dedup.`
|
|
655
|
+
}
|
|
656
|
+
]
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
case "cache_stats": {
|
|
660
|
+
const stats = contentCache.getStats();
|
|
661
|
+
return {
|
|
662
|
+
content: [
|
|
663
|
+
{
|
|
664
|
+
type: "text",
|
|
665
|
+
text: `Content Cache Stats:
|
|
666
|
+
Entries: ${stats.totalEntries}
|
|
667
|
+
Tokens cached: ${stats.totalTokensCached}
|
|
668
|
+
Tokens saved: ${stats.totalTokensSaved}
|
|
669
|
+
Hit rate: ${(stats.hitRate * 100).toFixed(1)}%
|
|
670
|
+
Top sources: ${stats.topSources.map((s) => `${s.source} (${s.tokensSaved} saved)`).join(", ") || "none"}`
|
|
671
|
+
}
|
|
672
|
+
]
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
// ── Skill Pack handlers ─────────────────────────────────────────
|
|
676
|
+
case "pack_list": {
|
|
677
|
+
const { namespace } = args;
|
|
678
|
+
const packs = packRegistry.list(namespace ? { namespace } : void 0);
|
|
679
|
+
if (packs.length === 0) {
|
|
680
|
+
return {
|
|
681
|
+
content: [{ type: "text", text: "No packs installed." }]
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
const list = packs.map((p) => {
|
|
685
|
+
const tools = p.manifest.mcp?.tools?.length ?? 0;
|
|
686
|
+
return `- ${p.manifest.name} v${p.manifest.version} (${tools} tools) \u2014 ${p.manifest.description}`;
|
|
687
|
+
}).join("\n");
|
|
688
|
+
return {
|
|
689
|
+
content: [
|
|
690
|
+
{
|
|
691
|
+
type: "text",
|
|
692
|
+
text: `${packs.length} pack(s) installed:
|
|
693
|
+
${list}`
|
|
694
|
+
}
|
|
695
|
+
]
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
case "pack_search": {
|
|
699
|
+
const { query } = args;
|
|
700
|
+
const results = packRegistry.search(query);
|
|
701
|
+
if (results.length === 0) {
|
|
702
|
+
return {
|
|
703
|
+
content: [{ type: "text", text: `No packs matching "${query}".` }]
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
const list = results.map(
|
|
707
|
+
(p) => `- ${p.manifest.name} v${p.manifest.version} \u2014 ${p.manifest.description}`
|
|
708
|
+
).join("\n");
|
|
709
|
+
return {
|
|
710
|
+
content: [
|
|
711
|
+
{
|
|
712
|
+
type: "text",
|
|
713
|
+
text: `${results.length} result(s) for "${query}":
|
|
714
|
+
${list}`
|
|
715
|
+
}
|
|
716
|
+
]
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
case "pack_get": {
|
|
720
|
+
const { name: packName } = args;
|
|
721
|
+
const pack = packRegistry.get(packName);
|
|
722
|
+
if (!pack) {
|
|
723
|
+
return {
|
|
724
|
+
content: [{ type: "text", text: `Pack "${packName}" not found.` }]
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
const m = pack.manifest;
|
|
728
|
+
const tools = m.mcp?.tools?.map((t) => ` - ${t.name}: ${t.description}`).join("\n");
|
|
729
|
+
const examples = m.examples?.map((e) => ` Q: ${e.input}
|
|
730
|
+
A: ${e.output}`).join("\n\n");
|
|
731
|
+
return {
|
|
732
|
+
content: [
|
|
733
|
+
{
|
|
734
|
+
type: "text",
|
|
735
|
+
text: [
|
|
736
|
+
`${m.name} v${m.version}`,
|
|
737
|
+
m.description,
|
|
738
|
+
`Author: ${m.author} | License: ${m.license}`,
|
|
739
|
+
`Runtime: ${m.runtime?.type ?? "local"}`,
|
|
740
|
+
tools ? `
|
|
741
|
+
MCP Tools:
|
|
742
|
+
${tools}` : "",
|
|
743
|
+
examples ? `
|
|
744
|
+
Examples:
|
|
745
|
+
${examples}` : "",
|
|
746
|
+
pack.instructions ? `
|
|
747
|
+
Instructions:
|
|
748
|
+
${pack.instructions}` : ""
|
|
749
|
+
].filter(Boolean).join("\n")
|
|
750
|
+
}
|
|
751
|
+
]
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
// ── Provenance handlers ─────────────────────────────────────────
|
|
755
|
+
case "record_trace": {
|
|
756
|
+
const a = args;
|
|
757
|
+
const event = {
|
|
758
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
759
|
+
traceId: a["traceId"],
|
|
760
|
+
sessionId: a["sessionId"],
|
|
761
|
+
tenantId: a["tenantId"],
|
|
762
|
+
operation: a["operation"],
|
|
763
|
+
actor: {
|
|
764
|
+
host: a["host"] || "unknown",
|
|
765
|
+
agent: "mcp",
|
|
766
|
+
user: "unknown"
|
|
767
|
+
},
|
|
768
|
+
inputs: a["inputs"] ?? null,
|
|
769
|
+
outputs: a["outputs"] ?? null,
|
|
770
|
+
tokensIn: a["tokensIn"] || 0,
|
|
771
|
+
tokensOut: a["tokensOut"] || 0,
|
|
772
|
+
costUsd: a["costUsd"] || 0,
|
|
773
|
+
provenance: {
|
|
774
|
+
sources: (a["sources"] || []).map((s) => ({
|
|
775
|
+
system: s["system"] ?? "",
|
|
776
|
+
externalId: s["externalId"] ?? "",
|
|
777
|
+
url: s["url"],
|
|
778
|
+
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
779
|
+
})),
|
|
780
|
+
derivation: [],
|
|
781
|
+
confidence: a["confidence"] || 0
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
if (a["parentTraceId"]) {
|
|
785
|
+
event.parentTraceId = a["parentTraceId"];
|
|
786
|
+
}
|
|
787
|
+
if (a["score"] !== void 0) {
|
|
788
|
+
event.score = a["score"];
|
|
789
|
+
}
|
|
790
|
+
if (a["feedback"]) {
|
|
791
|
+
event.feedback = a["feedback"];
|
|
792
|
+
}
|
|
793
|
+
provenanceStore.record(event);
|
|
794
|
+
return {
|
|
795
|
+
content: [
|
|
796
|
+
{
|
|
797
|
+
type: "text",
|
|
798
|
+
text: `Trace recorded: ${event.traceId} (${event.operation}, confidence: ${event.provenance.confidence})`
|
|
799
|
+
}
|
|
800
|
+
]
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
case "score_confidence": {
|
|
804
|
+
const { text, actor, replyCount } = args;
|
|
805
|
+
const result = scoreConfidence(text, { actor, replyCount });
|
|
806
|
+
return {
|
|
807
|
+
content: [
|
|
808
|
+
{
|
|
809
|
+
type: "text",
|
|
810
|
+
text: `Confidence: ${result.confidence.toFixed(2)} (${result.classification})
|
|
811
|
+
Signals: ${JSON.stringify(result.signals)}`
|
|
812
|
+
}
|
|
813
|
+
]
|
|
814
|
+
};
|
|
815
|
+
}
|
|
503
816
|
default:
|
|
504
817
|
throw new Error(`Unknown tool: ${name}`);
|
|
505
818
|
}
|
|
@@ -542,6 +855,8 @@ process.on("SIGINT", async () => {
|
|
|
542
855
|
}
|
|
543
856
|
}
|
|
544
857
|
db.close();
|
|
858
|
+
contentCacheDb.close();
|
|
859
|
+
provenanceDb.close();
|
|
545
860
|
process.exit(0);
|
|
546
861
|
});
|
|
547
862
|
main().catch((error) => {
|
|
@@ -0,0 +1,243 @@
|
|
|
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 { createHash } from "crypto";
|
|
6
|
+
import {
|
|
7
|
+
appendFileSync,
|
|
8
|
+
existsSync,
|
|
9
|
+
mkdirSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
writeFileSync
|
|
12
|
+
} from "fs";
|
|
13
|
+
import { join } from "path";
|
|
14
|
+
import { compactPlan } from "./utils.js";
|
|
15
|
+
import { runSpike } from "./harness.js";
|
|
16
|
+
const DETERMINISM_WATCH_PATTERNS = [
|
|
17
|
+
"src/orchestrators/multimodal",
|
|
18
|
+
"src/cli/commands/bench.ts",
|
|
19
|
+
"src/cli/index.ts",
|
|
20
|
+
"src/core/monitoring/logger.ts"
|
|
21
|
+
];
|
|
22
|
+
const DETERMINISM_WATCH_IGNORE = [
|
|
23
|
+
".git/**",
|
|
24
|
+
"node_modules/**",
|
|
25
|
+
"dist/**",
|
|
26
|
+
"build/**",
|
|
27
|
+
".next/**",
|
|
28
|
+
".turbo/**",
|
|
29
|
+
"coverage/**",
|
|
30
|
+
".stackmemory/**"
|
|
31
|
+
];
|
|
32
|
+
function stableStringify(value) {
|
|
33
|
+
return JSON.stringify(canonicalize(value));
|
|
34
|
+
}
|
|
35
|
+
function canonicalize(value) {
|
|
36
|
+
if (Array.isArray(value)) {
|
|
37
|
+
return value.map((item) => canonicalize(item));
|
|
38
|
+
}
|
|
39
|
+
if (value && typeof value === "object") {
|
|
40
|
+
const entries = Object.entries(value).sort(
|
|
41
|
+
([a], [b]) => a.localeCompare(b)
|
|
42
|
+
);
|
|
43
|
+
return Object.fromEntries(
|
|
44
|
+
entries.map(([key, entryValue]) => [key, canonicalize(entryValue)])
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
function hashValue(value) {
|
|
50
|
+
return createHash("sha256").update(stableStringify(value)).digest("hex");
|
|
51
|
+
}
|
|
52
|
+
function modeAgreement(values) {
|
|
53
|
+
if (values.length === 0) return 1;
|
|
54
|
+
const counts = /* @__PURE__ */ new Map();
|
|
55
|
+
for (const value of values) {
|
|
56
|
+
counts.set(value, (counts.get(value) || 0) + 1);
|
|
57
|
+
}
|
|
58
|
+
const maxCount = Math.max(...counts.values());
|
|
59
|
+
return maxCount / values.length;
|
|
60
|
+
}
|
|
61
|
+
function normalizeResult(result) {
|
|
62
|
+
return {
|
|
63
|
+
plan: compactPlan(result.plan),
|
|
64
|
+
critique: canonicalize(result.critique),
|
|
65
|
+
implementation: {
|
|
66
|
+
success: result.implementation.success,
|
|
67
|
+
summary: result.implementation.summary,
|
|
68
|
+
commands: [...result.implementation.commands || []]
|
|
69
|
+
},
|
|
70
|
+
iterations: (result.iterations || []).map((iteration) => ({
|
|
71
|
+
command: iteration.command,
|
|
72
|
+
ok: iteration.ok,
|
|
73
|
+
critique: canonicalize(iteration.critique),
|
|
74
|
+
outputPreviewHash: hashValue(iteration.outputPreview)
|
|
75
|
+
}))
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function estimateContextTokens(result) {
|
|
79
|
+
const normalized = normalizeResult(result);
|
|
80
|
+
return Math.ceil(stableStringify(normalized).length / 4);
|
|
81
|
+
}
|
|
82
|
+
function toSnapshot(result, index) {
|
|
83
|
+
const normalized = normalizeResult(result);
|
|
84
|
+
return {
|
|
85
|
+
index,
|
|
86
|
+
approved: result.critique.approved,
|
|
87
|
+
iterations: (result.iterations || []).length,
|
|
88
|
+
planHash: hashValue(compactPlan(result.plan)),
|
|
89
|
+
critiqueHash: hashValue(canonicalize(result.critique)),
|
|
90
|
+
commandsHash: hashValue(result.implementation.commands || []),
|
|
91
|
+
resultHash: hashValue(normalized),
|
|
92
|
+
contextTokens: estimateContextTokens(result)
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function computeNumericStability(values) {
|
|
96
|
+
if (values.length <= 1) return 1;
|
|
97
|
+
const min = Math.min(...values);
|
|
98
|
+
const max = Math.max(...values);
|
|
99
|
+
if (max === min) return 1;
|
|
100
|
+
return Math.max(0, 1 - (max - min) / Math.max(max, 1));
|
|
101
|
+
}
|
|
102
|
+
function scoreReport(snapshots) {
|
|
103
|
+
const dimensions = [
|
|
104
|
+
{
|
|
105
|
+
name: "result",
|
|
106
|
+
weight: 40,
|
|
107
|
+
score: modeAgreement(snapshots.map((item) => item.resultHash)) * 100,
|
|
108
|
+
details: "Full normalized result hash agreement"
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: "plan",
|
|
112
|
+
weight: 20,
|
|
113
|
+
score: modeAgreement(snapshots.map((item) => item.planHash)) * 100,
|
|
114
|
+
details: "Plan structure hash agreement"
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: "critique",
|
|
118
|
+
weight: 15,
|
|
119
|
+
score: modeAgreement(snapshots.map((item) => item.critiqueHash)) * 100,
|
|
120
|
+
details: "Critique hash agreement"
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: "commands",
|
|
124
|
+
weight: 10,
|
|
125
|
+
score: modeAgreement(snapshots.map((item) => item.commandsHash)) * 100,
|
|
126
|
+
details: "Implementer command sequence agreement"
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: "iterations",
|
|
130
|
+
weight: 10,
|
|
131
|
+
score: modeAgreement(snapshots.map((item) => item.iterations)) * 100,
|
|
132
|
+
details: "Retry-count agreement"
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: "context_tokens",
|
|
136
|
+
weight: 5,
|
|
137
|
+
score: computeNumericStability(snapshots.map((item) => item.contextTokens)) * 100,
|
|
138
|
+
details: "Token-footprint stability"
|
|
139
|
+
}
|
|
140
|
+
];
|
|
141
|
+
const weightedScore = dimensions.reduce((sum, dimension) => {
|
|
142
|
+
return sum + dimension.score * dimension.weight;
|
|
143
|
+
}, 0);
|
|
144
|
+
const totalWeight = dimensions.reduce(
|
|
145
|
+
(sum, dimension) => sum + dimension.weight,
|
|
146
|
+
0
|
|
147
|
+
);
|
|
148
|
+
const score = totalWeight > 0 ? weightedScore / totalWeight : 0;
|
|
149
|
+
const recommendations = [];
|
|
150
|
+
if (dimensions[0].score < 100) {
|
|
151
|
+
recommendations.push(
|
|
152
|
+
"Pin planner/critic outputs behind deterministic fixtures or replay traces."
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
if (dimensions[1].score < 100) {
|
|
156
|
+
recommendations.push(
|
|
157
|
+
"Canonicalize plan generation further and remove any model-dependent fields from smoke checks."
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
if (dimensions[4].score < 100) {
|
|
161
|
+
recommendations.push(
|
|
162
|
+
"Tighten retry rules so the same failure mode produces the same iteration count."
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
if (dimensions[5].score < 100) {
|
|
166
|
+
recommendations.push(
|
|
167
|
+
"Reduce context assembly drift by sorting symbols and fixing token accounting."
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
runs: snapshots.length,
|
|
172
|
+
score: Math.round(score * 100) / 100,
|
|
173
|
+
snapshots,
|
|
174
|
+
dimensions,
|
|
175
|
+
recommendations
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
async function runDeterminismSmoke(input, options = {}) {
|
|
179
|
+
const runs = Math.max(1, options.runs ?? 5);
|
|
180
|
+
const snapshots = [];
|
|
181
|
+
for (let index = 0; index < runs; index++) {
|
|
182
|
+
const result = await runSpike(input, {
|
|
183
|
+
...options,
|
|
184
|
+
dryRun: options.dryRun ?? true,
|
|
185
|
+
deterministicFixture: options.deterministicFixture ?? true,
|
|
186
|
+
persistAudit: false,
|
|
187
|
+
record: false,
|
|
188
|
+
recordFrame: false
|
|
189
|
+
});
|
|
190
|
+
snapshots.push(toSnapshot(result, index + 1));
|
|
191
|
+
}
|
|
192
|
+
return scoreReport(snapshots);
|
|
193
|
+
}
|
|
194
|
+
function getDeterminismWatchTargets(repoPath) {
|
|
195
|
+
const existingTargets = DETERMINISM_WATCH_PATTERNS.filter(
|
|
196
|
+
(target) => existsSync(join(repoPath, target))
|
|
197
|
+
);
|
|
198
|
+
if (existingTargets.length > 0) {
|
|
199
|
+
return existingTargets;
|
|
200
|
+
}
|
|
201
|
+
return ["."];
|
|
202
|
+
}
|
|
203
|
+
function getDeterminismDir(repoPath) {
|
|
204
|
+
return join(repoPath, ".stackmemory", "determinism");
|
|
205
|
+
}
|
|
206
|
+
function persistDeterminismReport(repoPath, report, meta) {
|
|
207
|
+
const dir = getDeterminismDir(repoPath);
|
|
208
|
+
mkdirSync(dir, { recursive: true });
|
|
209
|
+
const stored = {
|
|
210
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
211
|
+
task: meta.task,
|
|
212
|
+
trigger: meta.trigger,
|
|
213
|
+
changedPaths: meta.changedPaths || [],
|
|
214
|
+
report
|
|
215
|
+
};
|
|
216
|
+
writeFileSync(
|
|
217
|
+
join(dir, "latest.json"),
|
|
218
|
+
JSON.stringify(stored, null, 2) + "\n"
|
|
219
|
+
);
|
|
220
|
+
appendFileSync(join(dir, "history.jsonl"), JSON.stringify(stored) + "\n");
|
|
221
|
+
return stored;
|
|
222
|
+
}
|
|
223
|
+
function readLatestDeterminismReport(repoPath) {
|
|
224
|
+
const latestPath = join(getDeterminismDir(repoPath), "latest.json");
|
|
225
|
+
if (!existsSync(latestPath)) {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
return JSON.parse(
|
|
230
|
+
readFileSync(latestPath, "utf8")
|
|
231
|
+
);
|
|
232
|
+
} catch {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
export {
|
|
237
|
+
DETERMINISM_WATCH_IGNORE,
|
|
238
|
+
DETERMINISM_WATCH_PATTERNS,
|
|
239
|
+
getDeterminismWatchTargets,
|
|
240
|
+
persistDeterminismReport,
|
|
241
|
+
readLatestDeterminismReport,
|
|
242
|
+
runDeterminismSmoke
|
|
243
|
+
};
|