@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.
- package/LICENSE +131 -64
- package/README.md +3 -1
- package/bin/claude-sm +16 -1
- package/bin/claude-smd +16 -1
- package/bin/codex-smd +16 -1
- package/bin/gemini-sm +16 -1
- package/bin/hermes-sm +21 -0
- package/bin/hermes-smd +21 -0
- package/bin/opencode-sm +16 -1
- package/dist/src/cli/codex-sm.js +51 -11
- package/dist/src/cli/commands/brain.js +206 -0
- package/dist/src/cli/commands/company-os.js +184 -0
- package/dist/src/cli/commands/context.js +5 -0
- package/dist/src/cli/commands/operator.js +127 -0
- package/dist/src/cli/commands/orchestrate.js +2 -0
- package/dist/src/cli/commands/orchestrator.js +3 -2
- package/dist/src/cli/commands/patterns.js +254 -0
- package/dist/src/cli/commands/portal.js +161 -0
- package/dist/src/cli/commands/scaffold.js +92 -0
- package/dist/src/cli/commands/setup.js +1 -4
- package/dist/src/cli/commands/sync.js +253 -0
- package/dist/src/cli/commands/tasks.js +130 -1
- package/dist/src/cli/commands/vision.js +221 -0
- package/dist/src/cli/hermes-sm.js +224 -0
- package/dist/src/cli/index.js +15 -10
- package/dist/src/cli/utils/real-cli-bin.js +72 -0
- package/dist/src/core/brain/brain-store.js +187 -0
- package/dist/src/core/brain/brain-sync.js +193 -0
- package/dist/src/core/brain/index.js +78 -0
- package/dist/src/core/brain/types.js +10 -0
- package/dist/src/core/cache/token-estimator.js +24 -1
- package/dist/src/core/config/feature-flags.js +2 -6
- package/dist/src/core/context/frame-database.js +44 -0
- package/dist/src/core/context/recursive-context-manager.js +1 -1
- package/dist/src/core/context/rehydration.js +2 -1
- package/dist/src/core/database/sqlite-adapter.js +14 -1
- package/dist/src/core/models/model-router.js +33 -1
- package/dist/src/core/models/provider-pricing.js +58 -4
- package/dist/src/core/patterns/index.js +22 -0
- package/dist/src/core/patterns/pattern-applier.js +39 -0
- package/dist/src/core/patterns/pattern-observer.js +157 -0
- package/dist/src/core/patterns/pattern-store.js +259 -0
- package/dist/src/core/patterns/types.js +19 -0
- package/dist/src/core/retrieval/llm-context-retrieval.js +5 -4
- package/dist/src/core/retrieval/unified-context-assembler.js +11 -66
- package/dist/src/core/skill-packs/types.js +14 -1
- package/dist/src/core/storage/cloud-sync-manager.js +116 -0
- package/dist/src/core/storage/cloud-sync.js +574 -0
- package/dist/src/core/storage/two-tier-storage.js +5 -1
- package/dist/src/core/tasks/master-tasks-template.js +43 -0
- package/dist/src/core/tasks/md-task-parser.js +138 -0
- package/dist/src/core/vision/index.js +27 -0
- package/dist/src/core/vision/signals.js +79 -0
- package/dist/src/core/vision/types.js +22 -0
- package/dist/src/core/vision/vision-file.js +146 -0
- package/dist/src/core/vision/vision-loop.js +220 -0
- package/dist/src/core/wiki/wiki-compiler.js +103 -1
- package/dist/src/daemon/daemon-config.js +45 -0
- package/dist/src/daemon/services/desire-path-service.js +566 -0
- package/dist/src/daemon/services/research-stream-service.js +320 -0
- package/dist/src/daemon/services/telemetry-service.js +192 -0
- package/dist/src/daemon/unified-daemon.js +28 -1
- package/dist/src/features/browser/cli-browser-agent.js +417 -0
- package/dist/src/features/browser/stagehand-workflows.js +578 -0
- package/dist/src/features/operator/adapter-factory.js +62 -0
- package/dist/src/features/operator/browser-adapter.js +109 -0
- package/dist/src/features/operator/desktop-adapter.js +125 -0
- package/dist/src/features/operator/index.js +39 -0
- package/dist/src/features/operator/llm-decision.js +137 -0
- package/dist/src/features/operator/operator-logger.js +92 -0
- package/dist/src/features/operator/overnight-runner.js +327 -0
- package/dist/src/features/operator/screen-adapter.js +91 -0
- package/dist/src/features/operator/session-manager.js +127 -0
- package/dist/src/features/operator/state-machine.js +227 -0
- package/dist/src/features/operator/task-queue.js +81 -0
- package/dist/src/features/portal/index.js +26 -0
- package/dist/src/features/portal/server.js +240 -0
- package/dist/src/features/portal/types.js +14 -0
- package/dist/src/features/portal/ui.js +195 -0
- package/dist/src/features/tasks/task-aware-context.js +2 -1
- package/dist/src/features/tui/simple-monitor.js +0 -23
- package/dist/src/features/tui/swarm-monitor.js +8 -66
- package/dist/src/{integrations/diffmem/index.js → features/web/client/hooks/use-socket.js} +6 -5
- package/dist/src/features/web/client/lib/utils.js +12 -0
- package/dist/src/features/web/client/stores/session-store.js +12 -0
- package/dist/src/features/web/server/gcp-billing.js +76 -0
- package/dist/src/features/web/server/index.js +10 -0
- package/dist/src/features/web/server/spend-calculator.js +228 -0
- package/dist/src/hooks/schemas.js +4 -1
- package/dist/src/integrations/anthropic/client.js +3 -2
- package/dist/src/integrations/claude-code/agent-bridge.js +0 -3
- package/dist/src/integrations/claude-code/subagent-client.js +218 -11
- package/dist/src/integrations/claude-code/task-coordinator.js +2 -1
- package/dist/src/integrations/linear/webhook-retry.js +196 -0
- package/dist/src/integrations/linear/webhook-server.js +18 -22
- package/dist/src/integrations/mcp/handlers/cloud-sync-handlers.js +101 -0
- package/dist/src/integrations/mcp/handlers/index.js +27 -52
- package/dist/src/integrations/mcp/server.js +122 -335
- package/dist/src/integrations/mcp/tool-alias-registry.js +0 -73
- package/dist/src/integrations/mcp/tool-definitions.js +111 -510
- package/dist/src/mcp/stackmemory-mcp-server.js +404 -379
- package/dist/src/orchestrators/multimodal/determinism.js +2 -1
- package/dist/src/orchestrators/multimodal/harness.js +2 -1
- package/dist/src/skills/recursive-agent-orchestrator.js +2 -4
- package/dist/src/utils/process-cleanup.js +1 -7
- package/docs/README.md +42 -0
- package/docs/guides/README_INSTALL.md +208 -0
- package/package.json +18 -9
- package/scripts/claude-code-wrapper.sh +11 -0
- package/scripts/claude-sm-setup.sh +12 -1
- package/scripts/codex-wrapper.sh +11 -0
- package/scripts/git-hooks/branch-context-manager.sh +11 -0
- package/scripts/git-hooks/post-checkout-stackmemory.sh +11 -0
- package/scripts/git-hooks/post-commit-stackmemory.sh +11 -0
- package/scripts/git-hooks/pre-commit-stackmemory.sh +11 -0
- package/scripts/hooks/cleanup-shell.sh +12 -1
- package/scripts/hooks/task-complete.sh +12 -1
- package/scripts/install-code-execution-hooks.sh +12 -1
- package/scripts/install-sweep-hook.sh +12 -0
- package/scripts/install.sh +11 -0
- package/scripts/opencode-wrapper.sh +11 -0
- package/scripts/portal/cloud-init.yaml +69 -0
- package/scripts/portal/setup.sh +69 -0
- package/scripts/portal/stackmemory-portal.service +34 -0
- package/scripts/setup-claude-integration.sh +12 -1
- package/scripts/smoke-init-db.sh +23 -0
- package/scripts/stackmemory-daemon.sh +11 -0
- package/scripts/verify-dist.cjs +11 -4
- package/dist/src/cli/commands/ralph.js +0 -1053
- package/dist/src/hooks/diffmem-hooks.js +0 -376
- package/dist/src/integrations/diffmem/client.js +0 -208
- package/dist/src/integrations/diffmem/config.js +0 -14
- package/dist/src/integrations/greptile/client.js +0 -101
- package/dist/src/integrations/greptile/config.js +0 -14
- package/dist/src/integrations/greptile/index.js +0 -11
- package/dist/src/integrations/mcp/handlers/cross-search-handlers.js +0 -188
- package/dist/src/integrations/mcp/handlers/diffmem-handlers.js +0 -455
- package/dist/src/integrations/mcp/handlers/greptile-handlers.js +0 -456
- package/dist/src/integrations/mcp/handlers/provider-handlers.js +0 -227
- package/dist/src/integrations/ralph/bridge/ralph-stackmemory-bridge.js +0 -863
- package/dist/src/integrations/ralph/context/context-budget-manager.js +0 -308
- package/dist/src/integrations/ralph/context/stackmemory-context-loader.js +0 -354
- package/dist/src/integrations/ralph/index.js +0 -17
- package/dist/src/integrations/ralph/learning/pattern-learner.js +0 -416
- package/dist/src/integrations/ralph/lifecycle/iteration-lifecycle.js +0 -448
- package/dist/src/integrations/ralph/loopmax.js +0 -488
- package/dist/src/integrations/ralph/monitoring/swarm-dashboard.js +0 -293
- package/dist/src/integrations/ralph/monitoring/swarm-registry.js +0 -107
- package/dist/src/integrations/ralph/orchestration/multi-loop-orchestrator.js +0 -508
- package/dist/src/integrations/ralph/patterns/compounding-engineering-pattern.js +0 -407
- package/dist/src/integrations/ralph/patterns/extended-coherence-sessions.js +0 -495
- package/dist/src/integrations/ralph/patterns/oracle-worker-pattern.js +0 -387
- package/dist/src/integrations/ralph/performance/performance-optimizer.js +0 -357
- package/dist/src/integrations/ralph/recovery/crash-recovery.js +0 -461
- package/dist/src/integrations/ralph/state/state-reconciler.js +0 -420
- package/dist/src/integrations/ralph/swarm/git-workflow-manager.js +0 -444
- package/dist/src/integrations/ralph/swarm/swarm-coordinator.js +0 -1005
- package/dist/src/integrations/ralph/visualization/ralph-debugger.js +0 -635
- package/scripts/ralph-loop-implementation.js +0 -404
- /package/dist/src/{integrations/diffmem/types.js → core/storage/cloud-sync-types.js} +0 -0
- /package/dist/src/{integrations/greptile → features/operator}/types.js +0 -0
- /package/dist/src/{integrations/ralph/types.js → features/web/client/next-env.d.js} +0 -0
|
@@ -0,0 +1,138 @@
|
|
|
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 { readFileSync, writeFileSync } from "fs";
|
|
6
|
+
const HEADER_RE = /^\|\s*id\s*\|/i;
|
|
7
|
+
const SEPARATOR_RE = /^\|[\s-|]+\|$/;
|
|
8
|
+
const PRIORITIES = ["P0", "P1", "P2", "P3"];
|
|
9
|
+
const STATUSES = ["todo", "active", "done", "blocked", "cut"];
|
|
10
|
+
const SYNCS = ["local", "linear", "gh"];
|
|
11
|
+
function parseMasterTasks(content) {
|
|
12
|
+
const lines = content.split("\n");
|
|
13
|
+
const tasks = [];
|
|
14
|
+
let inTable = false;
|
|
15
|
+
for (const line of lines) {
|
|
16
|
+
const trimmed = line.trim();
|
|
17
|
+
if (!trimmed.startsWith("|")) {
|
|
18
|
+
if (inTable) break;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (HEADER_RE.test(trimmed)) {
|
|
22
|
+
inTable = true;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (SEPARATOR_RE.test(trimmed)) continue;
|
|
26
|
+
if (!inTable) continue;
|
|
27
|
+
const cells = trimmed.split("|").slice(1, -1).map((c) => c.trim());
|
|
28
|
+
if (cells.length < 8) continue;
|
|
29
|
+
const [id, priority, status, owner, sync, task, branchPr, notes] = cells;
|
|
30
|
+
if (!id || !PRIORITIES.includes(priority)) continue;
|
|
31
|
+
tasks.push({
|
|
32
|
+
id,
|
|
33
|
+
priority,
|
|
34
|
+
status: STATUSES.includes(status) ? status : "todo",
|
|
35
|
+
owner: owner || "@me",
|
|
36
|
+
sync: SYNCS.includes(sync) ? sync : "local",
|
|
37
|
+
task: task || "",
|
|
38
|
+
branchPr: branchPr || "",
|
|
39
|
+
notes: notes || ""
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return tasks;
|
|
43
|
+
}
|
|
44
|
+
function serializeTaskRows(tasks) {
|
|
45
|
+
return tasks.map(
|
|
46
|
+
(t) => `| ${t.id} | ${t.priority} | ${t.status} | ${t.owner} | ${t.sync} | ${t.task} | ${t.branchPr} | ${t.notes} |`
|
|
47
|
+
).join("\n");
|
|
48
|
+
}
|
|
49
|
+
function updateTaskInFile(filePath, taskId, updates) {
|
|
50
|
+
const content = readFileSync(filePath, "utf-8");
|
|
51
|
+
const lines = content.split("\n");
|
|
52
|
+
let found = false;
|
|
53
|
+
const updated = lines.map((line) => {
|
|
54
|
+
const trimmed = line.trim();
|
|
55
|
+
if (!trimmed.startsWith("|") || HEADER_RE.test(trimmed) || SEPARATOR_RE.test(trimmed)) {
|
|
56
|
+
return line;
|
|
57
|
+
}
|
|
58
|
+
const cells = trimmed.split("|").slice(1, -1).map((c) => c.trim());
|
|
59
|
+
if (cells.length < 8 || cells[0] !== taskId) return line;
|
|
60
|
+
found = true;
|
|
61
|
+
const task = {
|
|
62
|
+
id: cells[0],
|
|
63
|
+
priority: updates.priority ?? cells[1],
|
|
64
|
+
status: updates.status ?? cells[2],
|
|
65
|
+
owner: updates.owner ?? cells[3],
|
|
66
|
+
sync: updates.sync ?? cells[4],
|
|
67
|
+
task: updates.task ?? cells[5],
|
|
68
|
+
branchPr: updates.branchPr ?? cells[6],
|
|
69
|
+
notes: updates.notes ?? cells[7]
|
|
70
|
+
};
|
|
71
|
+
return `| ${task.id} | ${task.priority} | ${task.status} | ${task.owner} | ${task.sync} | ${task.task} | ${task.branchPr} | ${task.notes} |`;
|
|
72
|
+
});
|
|
73
|
+
if (!found) throw new Error(`Task ${taskId} not found in ${filePath}`);
|
|
74
|
+
writeFileSync(filePath, updated.join("\n"), "utf-8");
|
|
75
|
+
}
|
|
76
|
+
function addTaskToFile(filePath, task) {
|
|
77
|
+
const content = readFileSync(filePath, "utf-8");
|
|
78
|
+
const existing = parseMasterTasks(content);
|
|
79
|
+
const maxNum = existing.reduce((max, t) => {
|
|
80
|
+
const n = parseInt(t.id.replace(/^T/, ""), 10);
|
|
81
|
+
return isNaN(n) ? max : Math.max(max, n);
|
|
82
|
+
}, 0);
|
|
83
|
+
const id = `T${String(maxNum + 1).padStart(2, "0")}`;
|
|
84
|
+
const newRow = `| ${id} | ${task.priority} | ${task.status} | ${task.owner} | ${task.sync} | ${task.task} | ${task.branchPr} | ${task.notes} |`;
|
|
85
|
+
const lines = content.split("\n");
|
|
86
|
+
let insertIdx = -1;
|
|
87
|
+
let inTable = false;
|
|
88
|
+
for (let i = 0; i < lines.length; i++) {
|
|
89
|
+
const trimmed = lines[i].trim();
|
|
90
|
+
if (HEADER_RE.test(trimmed)) {
|
|
91
|
+
inTable = true;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (inTable && trimmed.startsWith("|") && !SEPARATOR_RE.test(trimmed)) {
|
|
95
|
+
insertIdx = i;
|
|
96
|
+
}
|
|
97
|
+
if (inTable && !trimmed.startsWith("|") && trimmed !== "") {
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (insertIdx === -1) {
|
|
102
|
+
for (let i = 0; i < lines.length; i++) {
|
|
103
|
+
if (SEPARATOR_RE.test(lines[i].trim())) {
|
|
104
|
+
insertIdx = i;
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
lines.splice(insertIdx + 1, 0, newRow);
|
|
110
|
+
writeFileSync(filePath, lines.join("\n"), "utf-8");
|
|
111
|
+
return id;
|
|
112
|
+
}
|
|
113
|
+
function getNextTask(tasks) {
|
|
114
|
+
const actionable = tasks.filter(
|
|
115
|
+
(t) => t.status === "todo" || t.status === "active"
|
|
116
|
+
);
|
|
117
|
+
if (actionable.length === 0) return void 0;
|
|
118
|
+
const ownerRank = {
|
|
119
|
+
"@agent": 0,
|
|
120
|
+
"@me": 1,
|
|
121
|
+
"@defer": 2
|
|
122
|
+
};
|
|
123
|
+
actionable.sort((a, b) => {
|
|
124
|
+
const pDiff = PRIORITIES.indexOf(a.priority) - PRIORITIES.indexOf(b.priority);
|
|
125
|
+
if (pDiff !== 0) return pDiff;
|
|
126
|
+
const aRank = ownerRank[a.owner] ?? 1;
|
|
127
|
+
const bRank = ownerRank[b.owner] ?? 1;
|
|
128
|
+
return aRank - bRank;
|
|
129
|
+
});
|
|
130
|
+
return actionable[0];
|
|
131
|
+
}
|
|
132
|
+
export {
|
|
133
|
+
addTaskToFile,
|
|
134
|
+
getNextTask,
|
|
135
|
+
parseMasterTasks,
|
|
136
|
+
serializeTaskRows,
|
|
137
|
+
updateTaskInFile
|
|
138
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
export * from "./types.js";
|
|
6
|
+
import {
|
|
7
|
+
parseVision,
|
|
8
|
+
loadVision,
|
|
9
|
+
setObjectiveDone,
|
|
10
|
+
scaffoldVision,
|
|
11
|
+
objectiveId,
|
|
12
|
+
VISION_TEMPLATE
|
|
13
|
+
} from "./vision-file.js";
|
|
14
|
+
import { SignalInbox } from "./signals.js";
|
|
15
|
+
import {
|
|
16
|
+
VisionLoop
|
|
17
|
+
} from "./vision-loop.js";
|
|
18
|
+
export {
|
|
19
|
+
SignalInbox,
|
|
20
|
+
VISION_TEMPLATE,
|
|
21
|
+
VisionLoop,
|
|
22
|
+
loadVision,
|
|
23
|
+
objectiveId,
|
|
24
|
+
parseVision,
|
|
25
|
+
scaffoldVision,
|
|
26
|
+
setObjectiveDone
|
|
27
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
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 {
|
|
6
|
+
existsSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
appendFileSync
|
|
11
|
+
} from "fs";
|
|
12
|
+
import { dirname } from "path";
|
|
13
|
+
import { randomUUID } from "crypto";
|
|
14
|
+
class SignalInbox {
|
|
15
|
+
constructor(path) {
|
|
16
|
+
this.path = path;
|
|
17
|
+
}
|
|
18
|
+
ensureDir() {
|
|
19
|
+
const dir = dirname(this.path);
|
|
20
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
add(input) {
|
|
23
|
+
this.ensureDir();
|
|
24
|
+
const signal = {
|
|
25
|
+
id: randomUUID(),
|
|
26
|
+
source: input.source ?? "manual",
|
|
27
|
+
severity: input.severity ?? "medium",
|
|
28
|
+
text: input.text,
|
|
29
|
+
createdAt: Date.now(),
|
|
30
|
+
...input.refs ? { refs: input.refs } : {}
|
|
31
|
+
};
|
|
32
|
+
appendFileSync(this.path, JSON.stringify(signal) + "\n");
|
|
33
|
+
return signal;
|
|
34
|
+
}
|
|
35
|
+
all() {
|
|
36
|
+
if (!existsSync(this.path)) return [];
|
|
37
|
+
return readFileSync(this.path, "utf-8").split(/\r?\n/).filter(Boolean).map((l) => {
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(l);
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}).filter((s) => !!s);
|
|
44
|
+
}
|
|
45
|
+
/** Unresolved signals, most severe + oldest first. */
|
|
46
|
+
pending() {
|
|
47
|
+
const rank = {
|
|
48
|
+
low: 1,
|
|
49
|
+
medium: 2,
|
|
50
|
+
high: 3,
|
|
51
|
+
critical: 4
|
|
52
|
+
};
|
|
53
|
+
return this.all().filter((s) => !s.resolvedAt).sort(
|
|
54
|
+
(a, b) => rank[b.severity] - rank[a.severity] || a.createdAt - b.createdAt
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
/** Rewrite the file marking a signal resolved (compacts the log). */
|
|
58
|
+
resolve(id) {
|
|
59
|
+
const signals = this.all();
|
|
60
|
+
let changed = false;
|
|
61
|
+
for (const s of signals) {
|
|
62
|
+
if (s.id === id && !s.resolvedAt) {
|
|
63
|
+
s.resolvedAt = Date.now();
|
|
64
|
+
changed = true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (changed) {
|
|
68
|
+
this.ensureDir();
|
|
69
|
+
writeFileSync(
|
|
70
|
+
this.path,
|
|
71
|
+
signals.map((s) => JSON.stringify(s)).join("\n") + "\n"
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
return changed;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export {
|
|
78
|
+
SignalInbox
|
|
79
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
const DEFAULT_LIMITS = {
|
|
6
|
+
maxIterations: 10,
|
|
7
|
+
maxIterationsPerDay: 50,
|
|
8
|
+
maxConsecutiveFailures: 3,
|
|
9
|
+
tickIntervalSec: 60,
|
|
10
|
+
requireApproval: false,
|
|
11
|
+
stopWhenComplete: true
|
|
12
|
+
};
|
|
13
|
+
const SEVERITY_RANK = {
|
|
14
|
+
low: 1,
|
|
15
|
+
medium: 2,
|
|
16
|
+
high: 3,
|
|
17
|
+
critical: 4
|
|
18
|
+
};
|
|
19
|
+
export {
|
|
20
|
+
DEFAULT_LIMITS,
|
|
21
|
+
SEVERITY_RANK
|
|
22
|
+
};
|
|
@@ -0,0 +1,146 @@
|
|
|
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 { existsSync, readFileSync, writeFileSync } from "fs";
|
|
6
|
+
import { createHash } from "crypto";
|
|
7
|
+
import {
|
|
8
|
+
DEFAULT_LIMITS
|
|
9
|
+
} from "./types.js";
|
|
10
|
+
function objectiveId(text) {
|
|
11
|
+
return createHash("sha1").update(text.trim()).digest("hex").slice(0, 10);
|
|
12
|
+
}
|
|
13
|
+
function splitSections(text) {
|
|
14
|
+
const lines = text.split(/\r?\n/);
|
|
15
|
+
const sections = /* @__PURE__ */ new Map();
|
|
16
|
+
const preamble = [];
|
|
17
|
+
let current = null;
|
|
18
|
+
for (const line of lines) {
|
|
19
|
+
const h2 = line.match(/^##\s+(.+?)\s*$/);
|
|
20
|
+
if (h2?.[1]) {
|
|
21
|
+
current = { body: [] };
|
|
22
|
+
sections.set(h2[1].toLowerCase(), current);
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (/^#\s+/.test(line)) continue;
|
|
26
|
+
if (current) {
|
|
27
|
+
current.body.push(line);
|
|
28
|
+
} else {
|
|
29
|
+
preamble.push(line);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { preamble, sections };
|
|
33
|
+
}
|
|
34
|
+
function bulletLines(body) {
|
|
35
|
+
return body.map((l) => l.match(/^\s*[-*]\s+(.*\S)\s*$/)?.[1]?.trim()).filter((s) => !!s).filter((s) => !/^\[[ xX]\]/.test(s));
|
|
36
|
+
}
|
|
37
|
+
function parseObjectives(body) {
|
|
38
|
+
const objectives = [];
|
|
39
|
+
for (const line of body) {
|
|
40
|
+
const m = line.match(/^\s*[-*]\s+\[([ xX])\]\s+(.*\S)\s*$/);
|
|
41
|
+
if (!m?.[2]) continue;
|
|
42
|
+
const text = m[2].trim();
|
|
43
|
+
objectives.push({
|
|
44
|
+
id: objectiveId(text),
|
|
45
|
+
text,
|
|
46
|
+
done: (m[1] ?? "").toLowerCase() === "x"
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return objectives;
|
|
50
|
+
}
|
|
51
|
+
function parseLimits(body) {
|
|
52
|
+
const limits = { ...DEFAULT_LIMITS };
|
|
53
|
+
for (const line of body) {
|
|
54
|
+
const m = line.match(/^\s*([a-zA-Z]+)\s*:\s*(.+?)\s*$/);
|
|
55
|
+
if (!m?.[1] || m[2] === void 0) continue;
|
|
56
|
+
const key = m[1];
|
|
57
|
+
const raw = m[2];
|
|
58
|
+
if (!(key in limits)) continue;
|
|
59
|
+
if (key === "requireApproval" || key === "stopWhenComplete") {
|
|
60
|
+
limits[key] = /^(true|yes|1)$/i.test(raw);
|
|
61
|
+
} else {
|
|
62
|
+
const n = parseInt(raw, 10);
|
|
63
|
+
if (Number.isFinite(n)) limits[key] = n;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return limits;
|
|
67
|
+
}
|
|
68
|
+
function parseVision(text) {
|
|
69
|
+
const { preamble, sections } = splitSections(text);
|
|
70
|
+
const body = (name) => sections.get(name)?.body ?? [];
|
|
71
|
+
const mission = preamble.map((l) => l.trim()).filter(Boolean).join(" ").trim();
|
|
72
|
+
return {
|
|
73
|
+
mission,
|
|
74
|
+
guardrails: bulletLines(body("guardrails")),
|
|
75
|
+
scope: bulletLines(body("scope")),
|
|
76
|
+
objectives: parseObjectives(body("objectives")),
|
|
77
|
+
limits: sections.has("limits") ? parseLimits(body("limits")) : { ...DEFAULT_LIMITS }
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function loadVision(path) {
|
|
81
|
+
if (!existsSync(path)) return null;
|
|
82
|
+
return parseVision(readFileSync(path, "utf-8"));
|
|
83
|
+
}
|
|
84
|
+
function setObjectiveDone(path, objId, done) {
|
|
85
|
+
if (!existsSync(path)) return false;
|
|
86
|
+
const lines = readFileSync(path, "utf-8").split(/\r?\n/);
|
|
87
|
+
let changed = false;
|
|
88
|
+
for (let i = 0; i < lines.length; i++) {
|
|
89
|
+
const line = lines[i];
|
|
90
|
+
if (line === void 0) continue;
|
|
91
|
+
const m = line.match(/^(\s*[-*]\s+)\[([ xX])\]\s+(.*\S)\s*$/);
|
|
92
|
+
if (!m?.[3]) continue;
|
|
93
|
+
if (objectiveId(m[3].trim()) === objId) {
|
|
94
|
+
lines[i] = `${m[1] ?? ""}[${done ? "x" : " "}] ${m[3].trim()}`;
|
|
95
|
+
changed = true;
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (changed) writeFileSync(path, lines.join("\n"));
|
|
100
|
+
return changed;
|
|
101
|
+
}
|
|
102
|
+
const VISION_TEMPLATE = `# Vision
|
|
103
|
+
|
|
104
|
+
State the single north-star mission this autonomous loop serves. Keep it to a
|
|
105
|
+
sentence or two \u2014 concrete enough to judge whether a piece of work belongs.
|
|
106
|
+
|
|
107
|
+
## Guardrails
|
|
108
|
+
|
|
109
|
+
- Stay within the scope below; do not touch anything outside it.
|
|
110
|
+
- Never modify secrets, production credentials, or deploy/publish.
|
|
111
|
+
- Open a PR for review; never merge to the default branch autonomously.
|
|
112
|
+
- If an objective is ambiguous or risky, stop and ask a human.
|
|
113
|
+
|
|
114
|
+
## Scope
|
|
115
|
+
|
|
116
|
+
- src/**
|
|
117
|
+
- docs/**
|
|
118
|
+
|
|
119
|
+
## Objectives
|
|
120
|
+
|
|
121
|
+
- [ ] First concrete objective the loop should pursue
|
|
122
|
+
- [ ] Second objective
|
|
123
|
+
- [ ] Third objective
|
|
124
|
+
|
|
125
|
+
## Limits
|
|
126
|
+
|
|
127
|
+
maxIterations: 10
|
|
128
|
+
maxIterationsPerDay: 50
|
|
129
|
+
maxConsecutiveFailures: 3
|
|
130
|
+
tickIntervalSec: 60
|
|
131
|
+
requireApproval: false
|
|
132
|
+
stopWhenComplete: true
|
|
133
|
+
`;
|
|
134
|
+
function scaffoldVision(path, force = false) {
|
|
135
|
+
if (existsSync(path) && !force) return false;
|
|
136
|
+
writeFileSync(path, VISION_TEMPLATE);
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
export {
|
|
140
|
+
VISION_TEMPLATE,
|
|
141
|
+
loadVision,
|
|
142
|
+
objectiveId,
|
|
143
|
+
parseVision,
|
|
144
|
+
scaffoldVision,
|
|
145
|
+
setObjectiveDone
|
|
146
|
+
};
|
|
@@ -0,0 +1,220 @@
|
|
|
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 { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
6
|
+
import { dirname } from "path";
|
|
7
|
+
import { loadVision, setObjectiveDone } from "./vision-file.js";
|
|
8
|
+
import { SignalInbox } from "./signals.js";
|
|
9
|
+
import {
|
|
10
|
+
SEVERITY_RANK
|
|
11
|
+
} from "./types.js";
|
|
12
|
+
function today() {
|
|
13
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
14
|
+
}
|
|
15
|
+
const realSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
16
|
+
class VisionLoop {
|
|
17
|
+
opts;
|
|
18
|
+
inbox;
|
|
19
|
+
constructor(options) {
|
|
20
|
+
this.opts = { sleep: realSleep, ...options };
|
|
21
|
+
this.inbox = new SignalInbox(options.signalsPath);
|
|
22
|
+
}
|
|
23
|
+
readState() {
|
|
24
|
+
const base = {
|
|
25
|
+
day: today(),
|
|
26
|
+
iterationsToday: 0,
|
|
27
|
+
consecutiveFailures: 0,
|
|
28
|
+
lastTickAt: 0
|
|
29
|
+
};
|
|
30
|
+
if (!existsSync(this.opts.statePath)) return base;
|
|
31
|
+
try {
|
|
32
|
+
const s = JSON.parse(
|
|
33
|
+
readFileSync(this.opts.statePath, "utf-8")
|
|
34
|
+
);
|
|
35
|
+
if (s.day !== today()) return { ...s, day: today(), iterationsToday: 0 };
|
|
36
|
+
return s;
|
|
37
|
+
} catch {
|
|
38
|
+
return base;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
writeState(s) {
|
|
42
|
+
const dir = dirname(this.opts.statePath);
|
|
43
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
44
|
+
writeFileSync(this.opts.statePath, JSON.stringify(s, null, 2));
|
|
45
|
+
}
|
|
46
|
+
/** Pick the next unit of work: pending signals outrank pending objectives. */
|
|
47
|
+
selectCandidate(vision) {
|
|
48
|
+
const signal = this.inbox.pending()[0];
|
|
49
|
+
if (signal) {
|
|
50
|
+
return {
|
|
51
|
+
kind: "signal",
|
|
52
|
+
id: signal.id,
|
|
53
|
+
text: signal.text,
|
|
54
|
+
priority: 100 + SEVERITY_RANK[signal.severity],
|
|
55
|
+
refs: signal.refs ?? []
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const idx = vision.objectives.findIndex((o2) => !o2.done);
|
|
59
|
+
const o = idx >= 0 ? vision.objectives[idx] : void 0;
|
|
60
|
+
if (o) {
|
|
61
|
+
return {
|
|
62
|
+
kind: "objective",
|
|
63
|
+
id: o.id,
|
|
64
|
+
text: o.text,
|
|
65
|
+
priority: 50 - idx,
|
|
66
|
+
refs: []
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
checkGuardrails(state, vision, iterationThisRun) {
|
|
72
|
+
const l = vision.limits;
|
|
73
|
+
if (iterationThisRun >= l.maxIterations) {
|
|
74
|
+
return {
|
|
75
|
+
ok: false,
|
|
76
|
+
reason: `reached maxIterations (${l.maxIterations}) for this run`
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (state.iterationsToday >= l.maxIterationsPerDay) {
|
|
80
|
+
return {
|
|
81
|
+
ok: false,
|
|
82
|
+
reason: `reached maxIterationsPerDay (${l.maxIterationsPerDay})`
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (state.consecutiveFailures >= l.maxConsecutiveFailures) {
|
|
86
|
+
return {
|
|
87
|
+
ok: false,
|
|
88
|
+
reason: `circuit breaker: ${state.consecutiveFailures} consecutive failures (limit ${l.maxConsecutiveFailures})`
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return { ok: true };
|
|
92
|
+
}
|
|
93
|
+
/** Has the brain already concluded this exact piece of work? */
|
|
94
|
+
priorConclusion(text) {
|
|
95
|
+
const hits = this.brainRecall(text);
|
|
96
|
+
const match = hits.find(
|
|
97
|
+
(e) => e.title.trim() === text.trim() && e.conclusion.trim().length > 0
|
|
98
|
+
);
|
|
99
|
+
return match?.conclusion;
|
|
100
|
+
}
|
|
101
|
+
brainRecall(text) {
|
|
102
|
+
return this.opts.brain.recall({ text, limit: 5 });
|
|
103
|
+
}
|
|
104
|
+
async tick(iterationThisRun, dryRun = false) {
|
|
105
|
+
const vision = loadVision(this.opts.visionPath);
|
|
106
|
+
if (!vision) {
|
|
107
|
+
return {
|
|
108
|
+
candidate: null,
|
|
109
|
+
guardrail: { ok: false, reason: "no VISION.md found" },
|
|
110
|
+
skippedAsKnown: false,
|
|
111
|
+
delegated: false
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const state = this.readState();
|
|
115
|
+
const guardrail = this.checkGuardrails(state, vision, iterationThisRun);
|
|
116
|
+
if (!guardrail.ok) {
|
|
117
|
+
return {
|
|
118
|
+
candidate: null,
|
|
119
|
+
guardrail,
|
|
120
|
+
skippedAsKnown: false,
|
|
121
|
+
delegated: false
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const candidate = this.selectCandidate(vision);
|
|
125
|
+
if (!candidate) {
|
|
126
|
+
return {
|
|
127
|
+
candidate: null,
|
|
128
|
+
guardrail: { ok: true },
|
|
129
|
+
skippedAsKnown: false,
|
|
130
|
+
delegated: false
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const prior = this.priorConclusion(candidate.text);
|
|
134
|
+
if (prior) {
|
|
135
|
+
if (candidate.kind === "objective") {
|
|
136
|
+
setObjectiveDone(this.opts.visionPath, candidate.id, true);
|
|
137
|
+
} else {
|
|
138
|
+
this.inbox.resolve(candidate.id);
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
candidate,
|
|
142
|
+
guardrail: { ok: true },
|
|
143
|
+
skippedAsKnown: true,
|
|
144
|
+
priorConclusion: prior,
|
|
145
|
+
delegated: false
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
if (dryRun || vision.limits.requireApproval) {
|
|
149
|
+
return {
|
|
150
|
+
candidate,
|
|
151
|
+
guardrail: { ok: true },
|
|
152
|
+
skippedAsKnown: false,
|
|
153
|
+
delegated: false
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
const outcome = await this.opts.delegate(candidate, vision);
|
|
157
|
+
this.opts.brain.record({
|
|
158
|
+
title: candidate.text,
|
|
159
|
+
summary: `Vision loop handled a ${candidate.kind} toward: ${vision.mission}`,
|
|
160
|
+
conclusion: outcome.conclusion,
|
|
161
|
+
kind: "experiment",
|
|
162
|
+
agent: "vision",
|
|
163
|
+
tags: ["vision", candidate.kind, outcome.success ? "success" : "failure"],
|
|
164
|
+
refs: [...candidate.refs, ...outcome.refs ?? []],
|
|
165
|
+
confidence: outcome.success ? 0.8 : 0.4
|
|
166
|
+
});
|
|
167
|
+
const next = this.readState();
|
|
168
|
+
if (outcome.success) {
|
|
169
|
+
next.iterationsToday += 1;
|
|
170
|
+
next.consecutiveFailures = 0;
|
|
171
|
+
if (candidate.kind === "objective") {
|
|
172
|
+
setObjectiveDone(this.opts.visionPath, candidate.id, true);
|
|
173
|
+
} else {
|
|
174
|
+
this.inbox.resolve(candidate.id);
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
next.consecutiveFailures += 1;
|
|
178
|
+
}
|
|
179
|
+
next.lastTickAt = Date.now();
|
|
180
|
+
this.writeState(next);
|
|
181
|
+
return {
|
|
182
|
+
candidate,
|
|
183
|
+
guardrail: { ok: true },
|
|
184
|
+
skippedAsKnown: false,
|
|
185
|
+
delegated: true,
|
|
186
|
+
outcome
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
/** Run ticks until a guardrail stops the loop or there's nothing left. */
|
|
190
|
+
async run(opts = {}) {
|
|
191
|
+
const vision = loadVision(this.opts.visionPath);
|
|
192
|
+
const max = opts.maxIterations ?? vision?.limits.maxIterations ?? 1;
|
|
193
|
+
const tickInterval = (vision?.limits.tickIntervalSec ?? 60) * 1e3;
|
|
194
|
+
const decisions = [];
|
|
195
|
+
let delegated = 0;
|
|
196
|
+
let skipped = 0;
|
|
197
|
+
let stopped = "completed run";
|
|
198
|
+
for (let i = 0; i < max; i++) {
|
|
199
|
+
const d = await this.tick(i, opts.dryRun);
|
|
200
|
+
decisions.push(d);
|
|
201
|
+
if (!d.guardrail.ok) {
|
|
202
|
+
stopped = d.guardrail.reason ?? "guardrail stop";
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
if (!d.candidate) {
|
|
206
|
+
stopped = "no work remaining";
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
if (d.skippedAsKnown) skipped++;
|
|
210
|
+
if (d.delegated) delegated++;
|
|
211
|
+
if (!opts.dryRun && i < max - 1) {
|
|
212
|
+
await this.opts.sleep(tickInterval);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return { decisions, stopped, delegated, skipped };
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
export {
|
|
219
|
+
VisionLoop
|
|
220
|
+
};
|