@stackmemoryai/stackmemory 1.10.5 → 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 +107 -24
- 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/claude-sm.js +266 -84
- package/dist/src/cli/codex-sm.js +225 -33
- package/dist/src/cli/commands/bench.js +209 -2
- package/dist/src/cli/commands/brain.js +206 -0
- package/dist/src/cli/commands/cache.js +126 -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/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/operator.js +127 -0
- package/dist/src/cli/commands/optimize.js +117 -0
- package/dist/src/cli/commands/orchestrate.js +232 -5
- package/dist/src/cli/commands/orchestrator.js +315 -26
- package/dist/src/cli/commands/pack.js +322 -0
- 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/search.js +40 -1
- package/dist/src/cli/commands/setup.js +178 -11
- package/dist/src/cli/commands/skills.js +10 -1
- package/dist/src/cli/commands/state.js +265 -0
- 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/gemini-sm.js +19 -29
- package/dist/src/cli/hermes-sm.js +224 -0
- package/dist/src/cli/index.js +105 -39
- 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 +116 -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/content-cache.js +238 -0
- package/dist/src/{integrations/diffmem → core/cache}/index.js +5 -5
- package/dist/src/core/cache/token-estimator.js +39 -0
- package/dist/src/core/config/feature-flags.js +2 -6
- package/dist/src/core/context/frame-database.js +79 -27
- package/dist/src/core/context/recursive-context-manager.js +1 -1
- package/dist/src/core/context/rehydration.js +2 -1
- package/dist/src/core/cross-search/cross-project-search.js +269 -0
- package/dist/src/core/cross-search/index.js +10 -0
- package/dist/src/core/database/sqlite-adapter.js +14 -84
- package/dist/src/core/extensions/provider-adapter.js +5 -0
- package/dist/src/core/models/model-router.js +54 -2
- package/dist/src/core/models/provider-pricing.js +58 -4
- package/dist/src/core/monitoring/logger.js +2 -1
- package/dist/src/core/optimization/trace-optimizer.js +413 -0
- 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/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/retrieval/llm-context-retrieval.js +5 -4
- package/dist/src/core/retrieval/unified-context-assembler.js +11 -66
- 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 +79 -0
- 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/trace/trace-event-store.js +282 -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 +52 -0
- package/dist/src/daemon/services/desire-path-service.js +566 -0
- package/dist/src/daemon/services/github-service.js +126 -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 +58 -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/sweep/pty-wrapper.js +13 -5
- 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/features/web/client/hooks/use-socket.js +12 -0
- package/dist/src/{core/merge/index.js → features/web/client/lib/utils.js} +8 -4
- package/dist/src/features/web/client/next-env.d.js +4 -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 +6 -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 +307 -11
- package/dist/src/integrations/claude-code/task-coordinator.js +2 -1
- package/dist/src/integrations/github/pr-state.js +158 -0
- package/dist/src/integrations/linear/client.js +4 -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 +40 -84
- package/dist/src/integrations/mcp/server.js +542 -641
- package/dist/src/integrations/mcp/tool-alias-registry.js +297 -0
- package/dist/src/integrations/mcp/tool-definitions.js +152 -682
- package/dist/src/mcp/stackmemory-mcp-server.js +571 -231
- package/dist/src/orchestrators/multimodal/determinism.js +244 -0
- package/dist/src/orchestrators/multimodal/harness.js +149 -78
- package/dist/src/orchestrators/multimodal/providers.js +44 -3
- package/dist/src/skills/recursive-agent-orchestrator.js +2 -4
- package/dist/src/utils/hook-installer.js +0 -8
- 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 +27 -9
- 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/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/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/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/cord-handlers.js +0 -397
- 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/mcp/handlers/team-handlers.js +0 -211
- 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 -391
- package/dist/src/integrations/ralph/index.js +0 -17
- package/dist/src/integrations/ralph/learning/pattern-learner.js +0 -435
- 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 -1007
- package/dist/src/integrations/ralph/visualization/ralph-debugger.js +0 -635
- package/scripts/ralph-loop-implementation.js +0 -404
- /package/dist/src/core/{merge → cache}/types.js +0 -0
- /package/dist/src/{integrations/diffmem/types.js → core/storage/cloud-sync-types.js} +0 -0
- /package/dist/src/{integrations/greptile/types.js → core/trace/trace-event.js} +0 -0
- /package/dist/src/{integrations/ralph → features/operator}/types.js +0 -0
|
@@ -0,0 +1,227 @@
|
|
|
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 RATE_LIMIT_PATTERNS = [
|
|
6
|
+
/usage limits?/i,
|
|
7
|
+
/rate limit/i,
|
|
8
|
+
/too many requests/i,
|
|
9
|
+
/\b429\b/,
|
|
10
|
+
/over(?:loaded|capacity)/i,
|
|
11
|
+
/try again (?:in|after)/i,
|
|
12
|
+
/exceeded.*quota/i,
|
|
13
|
+
/max plan.*limit/i,
|
|
14
|
+
/you've reached/i,
|
|
15
|
+
/please wait/i
|
|
16
|
+
];
|
|
17
|
+
const PERMISSION_PATTERNS = [
|
|
18
|
+
/Do you want to proceed\?/i,
|
|
19
|
+
/Allow\s+(?:this|once|always)/i,
|
|
20
|
+
/\([Yy]\/[Nn]\)/,
|
|
21
|
+
/\([Yy]es\/[Nn]o\)/,
|
|
22
|
+
/Press Enter to allow/i,
|
|
23
|
+
/Allow .* to run/i,
|
|
24
|
+
/Do you want to allow/i,
|
|
25
|
+
/approve.*tool/i,
|
|
26
|
+
/Would you like to/i
|
|
27
|
+
];
|
|
28
|
+
const ERROR_PATTERNS = [
|
|
29
|
+
/^Error:/m,
|
|
30
|
+
/\bFATAL\b/,
|
|
31
|
+
/\bpanic:/,
|
|
32
|
+
/Traceback \(most recent/,
|
|
33
|
+
/Unhandled.*exception/i,
|
|
34
|
+
/cannot.*connect/i,
|
|
35
|
+
/\bECONNREFUSED\b/,
|
|
36
|
+
/\bENOENT\b/,
|
|
37
|
+
/SIGKILL/,
|
|
38
|
+
/out of memory/i
|
|
39
|
+
];
|
|
40
|
+
const ERROR_ANTI_PATTERNS = [
|
|
41
|
+
/Reading file/i,
|
|
42
|
+
/Searching/i,
|
|
43
|
+
/grep.*Error/i,
|
|
44
|
+
/Grep.*pattern/i,
|
|
45
|
+
/cat.*Error/i,
|
|
46
|
+
/```/
|
|
47
|
+
];
|
|
48
|
+
const SESSION_ENDED_PATTERNS = [
|
|
49
|
+
/Process exited/i,
|
|
50
|
+
/Connection closed/i,
|
|
51
|
+
/Session ended/i,
|
|
52
|
+
/claude.*exited/i
|
|
53
|
+
];
|
|
54
|
+
const IDLE_PATTERNS = [
|
|
55
|
+
/^>\s*$/m,
|
|
56
|
+
/What would you like/i,
|
|
57
|
+
/How can I help/i,
|
|
58
|
+
/What do you want/i
|
|
59
|
+
];
|
|
60
|
+
const WORKING_PATTERNS = [
|
|
61
|
+
/Reading file/i,
|
|
62
|
+
/Writing to/i,
|
|
63
|
+
/Searching/i,
|
|
64
|
+
/Running/i,
|
|
65
|
+
/Executing/i,
|
|
66
|
+
/Analyzing/i,
|
|
67
|
+
/Creating/i,
|
|
68
|
+
/Editing/i,
|
|
69
|
+
/\.\.\./,
|
|
70
|
+
/Glob|Grep|Read|Write|Edit|Bash/
|
|
71
|
+
];
|
|
72
|
+
const COMPLETION_PATTERNS = [
|
|
73
|
+
/TASK COMPLETE/,
|
|
74
|
+
/completed successfully/i,
|
|
75
|
+
/all tasks? (?:are )?(?:done|completed)/i
|
|
76
|
+
];
|
|
77
|
+
const BLOCKED_PATTERN = /TASK BLOCKED:\s*(.+)/;
|
|
78
|
+
function tailLines(content, n) {
|
|
79
|
+
const lines = content.split("\n");
|
|
80
|
+
return lines.slice(-n).join("\n");
|
|
81
|
+
}
|
|
82
|
+
function looksLikeShellPrompt(tail) {
|
|
83
|
+
const last = tail.split("\n").filter(Boolean).pop() ?? "";
|
|
84
|
+
return /(?:\$\s*$|%\s*$|bash-\d|zsh|❯\s*$)/.test(last) && !IDLE_PATTERNS.some((p) => p.test(last));
|
|
85
|
+
}
|
|
86
|
+
function detectState(screenContent, previousState, lastChangeTimestamp, stuckTimeoutMs) {
|
|
87
|
+
const tail = tailLines(screenContent, 50);
|
|
88
|
+
const recentTail = tailLines(screenContent, 10);
|
|
89
|
+
if (screenContent === "") {
|
|
90
|
+
return {
|
|
91
|
+
state: "SESSION_ENDED",
|
|
92
|
+
confidence: "high",
|
|
93
|
+
detail: "empty screen buffer"
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
if (SESSION_ENDED_PATTERNS.some((p) => p.test(tail))) {
|
|
97
|
+
return {
|
|
98
|
+
state: "SESSION_ENDED",
|
|
99
|
+
confidence: "high",
|
|
100
|
+
detail: "exit pattern matched"
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
if (looksLikeShellPrompt(recentTail)) {
|
|
104
|
+
return {
|
|
105
|
+
state: "SESSION_ENDED",
|
|
106
|
+
confidence: "medium",
|
|
107
|
+
detail: "shell prompt detected"
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
for (const pattern of RATE_LIMIT_PATTERNS) {
|
|
111
|
+
if (pattern.test(recentTail)) {
|
|
112
|
+
return {
|
|
113
|
+
state: "RATE_LIMITED",
|
|
114
|
+
confidence: "high",
|
|
115
|
+
detail: pattern.source
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
for (const pattern of PERMISSION_PATTERNS) {
|
|
120
|
+
if (pattern.test(recentTail)) {
|
|
121
|
+
return {
|
|
122
|
+
state: "PERMISSION_PROMPT",
|
|
123
|
+
confidence: "high",
|
|
124
|
+
detail: pattern.source
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const hasAntiPattern = ERROR_ANTI_PATTERNS.some((p) => p.test(recentTail));
|
|
129
|
+
if (!hasAntiPattern) {
|
|
130
|
+
for (const pattern of ERROR_PATTERNS) {
|
|
131
|
+
if (pattern.test(recentTail)) {
|
|
132
|
+
return { state: "ERROR", confidence: "medium", detail: pattern.source };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (IDLE_PATTERNS.some((p) => p.test(recentTail))) {
|
|
137
|
+
return { state: "IDLE", confidence: "high" };
|
|
138
|
+
}
|
|
139
|
+
if (WORKING_PATTERNS.some((p) => p.test(recentTail))) {
|
|
140
|
+
return { state: "WORKING", confidence: "high" };
|
|
141
|
+
}
|
|
142
|
+
const elapsed = Date.now() - lastChangeTimestamp;
|
|
143
|
+
if (elapsed > stuckTimeoutMs && previousState === "WORKING") {
|
|
144
|
+
return {
|
|
145
|
+
state: "STUCK",
|
|
146
|
+
confidence: "high",
|
|
147
|
+
detail: `no change for ${Math.round(elapsed / 1e3)}s`
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return { state: "UNKNOWN", confidence: "low" };
|
|
151
|
+
}
|
|
152
|
+
function decideAction(detection, checkpoint, nextTask) {
|
|
153
|
+
const { state } = detection;
|
|
154
|
+
switch (state) {
|
|
155
|
+
case "PERMISSION_PROMPT":
|
|
156
|
+
return { type: "AUTO_APPROVE" };
|
|
157
|
+
case "RATE_LIMITED": {
|
|
158
|
+
const hits = checkpoint.consecutiveRateLimitHits + 1;
|
|
159
|
+
const backoff = Math.min(6e4 * Math.pow(2, hits - 1), 9e5);
|
|
160
|
+
return { type: "BACKOFF", durationMs: backoff };
|
|
161
|
+
}
|
|
162
|
+
case "SESSION_ENDED": {
|
|
163
|
+
if (checkpoint.consecutiveRestarts >= 10) {
|
|
164
|
+
return {
|
|
165
|
+
type: "LOG_ERROR",
|
|
166
|
+
error: "max consecutive restarts exceeded \u2014 stopping"
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
return { type: "RESTART_SESSION" };
|
|
170
|
+
}
|
|
171
|
+
case "STUCK":
|
|
172
|
+
if (checkpoint.currentTaskId) {
|
|
173
|
+
if (checkpoint.nudgeCount < 2) {
|
|
174
|
+
return {
|
|
175
|
+
type: "NUDGE",
|
|
176
|
+
message: "You appear stuck. Try a different approach or run the tests to check progress."
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
type: "MARK_BLOCKED",
|
|
181
|
+
taskId: checkpoint.currentTaskId,
|
|
182
|
+
reason: detection.detail ?? "stuck \u2014 no output after nudges"
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
return { type: "KILL_AND_RESTART" };
|
|
186
|
+
case "ERROR":
|
|
187
|
+
return { type: "LOG_ERROR", error: detection.detail ?? "unknown error" };
|
|
188
|
+
case "WORKING":
|
|
189
|
+
return { type: "NOOP" };
|
|
190
|
+
case "IDLE": {
|
|
191
|
+
if (checkpoint.currentTaskId) {
|
|
192
|
+
return { type: "WAIT", durationMs: 2e3 };
|
|
193
|
+
}
|
|
194
|
+
if (nextTask) {
|
|
195
|
+
return { type: "INJECT_TASK", task: nextTask };
|
|
196
|
+
}
|
|
197
|
+
return { type: "NOOP" };
|
|
198
|
+
}
|
|
199
|
+
case "COMPLETE":
|
|
200
|
+
if (checkpoint.currentTaskId) {
|
|
201
|
+
return { type: "MARK_COMPLETE", taskId: checkpoint.currentTaskId };
|
|
202
|
+
}
|
|
203
|
+
return { type: "NOOP" };
|
|
204
|
+
default:
|
|
205
|
+
return { type: "WAIT", durationMs: 2e3 };
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function detectCompletion(screenContent) {
|
|
209
|
+
const tail = tailLines(screenContent, 15);
|
|
210
|
+
if (COMPLETION_PATTERNS.some((p) => p.test(tail))) {
|
|
211
|
+
return { completed: true, blocked: false };
|
|
212
|
+
}
|
|
213
|
+
const blockedMatch = BLOCKED_PATTERN.exec(tail);
|
|
214
|
+
if (blockedMatch?.[1]) {
|
|
215
|
+
return {
|
|
216
|
+
completed: false,
|
|
217
|
+
blocked: true,
|
|
218
|
+
blockedReason: blockedMatch[1].trim()
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
return { completed: false, blocked: false };
|
|
222
|
+
}
|
|
223
|
+
export {
|
|
224
|
+
decideAction,
|
|
225
|
+
detectCompletion,
|
|
226
|
+
detectState
|
|
227
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
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 } from "fs";
|
|
6
|
+
import {
|
|
7
|
+
parseMasterTasks,
|
|
8
|
+
getNextTask,
|
|
9
|
+
updateTaskInFile
|
|
10
|
+
} from "../../core/tasks/md-task-parser.js";
|
|
11
|
+
class TaskQueue {
|
|
12
|
+
constructor(filePath) {
|
|
13
|
+
this.filePath = filePath;
|
|
14
|
+
}
|
|
15
|
+
/** Get the highest-priority actionable task */
|
|
16
|
+
dequeue() {
|
|
17
|
+
const content = readFileSync(this.filePath, "utf-8");
|
|
18
|
+
const tasks = parseMasterTasks(content);
|
|
19
|
+
return getNextTask(tasks);
|
|
20
|
+
}
|
|
21
|
+
/** Mark a task as active with @operator owner */
|
|
22
|
+
markActive(taskId) {
|
|
23
|
+
updateTaskInFile(this.filePath, taskId, {
|
|
24
|
+
status: "active",
|
|
25
|
+
owner: "@operator"
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/** Mark a task as done */
|
|
29
|
+
markDone(taskId, notes) {
|
|
30
|
+
const updates = { status: "done" };
|
|
31
|
+
if (notes) updates.notes = notes;
|
|
32
|
+
updateTaskInFile(this.filePath, taskId, updates);
|
|
33
|
+
}
|
|
34
|
+
/** Mark a task as blocked with reason */
|
|
35
|
+
markBlocked(taskId, reason) {
|
|
36
|
+
updateTaskInFile(this.filePath, taskId, {
|
|
37
|
+
status: "blocked",
|
|
38
|
+
notes: reason
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/** Count of remaining actionable tasks */
|
|
42
|
+
remaining() {
|
|
43
|
+
const content = readFileSync(this.filePath, "utf-8");
|
|
44
|
+
const tasks = parseMasterTasks(content);
|
|
45
|
+
return tasks.filter((t) => t.status === "todo" || t.status === "active").length;
|
|
46
|
+
}
|
|
47
|
+
/** True when no more actionable tasks */
|
|
48
|
+
isEmpty() {
|
|
49
|
+
return this.remaining() === 0;
|
|
50
|
+
}
|
|
51
|
+
/** Format a task into a Claude-friendly prompt with completion sentinels */
|
|
52
|
+
buildTaskPrompt(task) {
|
|
53
|
+
const parts = [
|
|
54
|
+
`# Task: ${task.task}`,
|
|
55
|
+
"",
|
|
56
|
+
`Priority: ${task.priority}`,
|
|
57
|
+
`ID: ${task.id}`
|
|
58
|
+
];
|
|
59
|
+
if (task.notes) {
|
|
60
|
+
parts.push(`Notes: ${task.notes}`);
|
|
61
|
+
}
|
|
62
|
+
parts.push(
|
|
63
|
+
"",
|
|
64
|
+
"## Instructions",
|
|
65
|
+
"- Complete this task fully before moving on",
|
|
66
|
+
"- Run tests, lint, and build to verify your work",
|
|
67
|
+
"- Commit your changes with a descriptive message",
|
|
68
|
+
"",
|
|
69
|
+
"## Completion",
|
|
70
|
+
"When you have fully completed this task, output exactly:",
|
|
71
|
+
"TASK COMPLETE",
|
|
72
|
+
"",
|
|
73
|
+
"If you are blocked and cannot proceed, output exactly:",
|
|
74
|
+
"TASK BLOCKED: <reason>"
|
|
75
|
+
);
|
|
76
|
+
return parts.join("\n");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export {
|
|
80
|
+
TaskQueue
|
|
81
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
PortalServer,
|
|
7
|
+
resolveConfig,
|
|
8
|
+
ensureToken,
|
|
9
|
+
readStatus,
|
|
10
|
+
stopRunning,
|
|
11
|
+
getPortalDir
|
|
12
|
+
} from "./server.js";
|
|
13
|
+
import { renderPortalPage } from "./ui.js";
|
|
14
|
+
import {
|
|
15
|
+
DEFAULT_PORTAL_CONFIG
|
|
16
|
+
} from "./types.js";
|
|
17
|
+
export {
|
|
18
|
+
DEFAULT_PORTAL_CONFIG,
|
|
19
|
+
PortalServer,
|
|
20
|
+
ensureToken,
|
|
21
|
+
getPortalDir,
|
|
22
|
+
readStatus,
|
|
23
|
+
renderPortalPage,
|
|
24
|
+
resolveConfig,
|
|
25
|
+
stopRunning
|
|
26
|
+
};
|
|
@@ -0,0 +1,240 @@
|
|
|
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 express from "express";
|
|
6
|
+
import { createServer } from "http";
|
|
7
|
+
import { Server as SocketServer } from "socket.io";
|
|
8
|
+
import { execFileSync } from "child_process";
|
|
9
|
+
import {
|
|
10
|
+
existsSync,
|
|
11
|
+
mkdirSync,
|
|
12
|
+
readFileSync,
|
|
13
|
+
writeFileSync,
|
|
14
|
+
unlinkSync
|
|
15
|
+
} from "fs";
|
|
16
|
+
import { join } from "path";
|
|
17
|
+
import { randomBytes } from "crypto";
|
|
18
|
+
import { renderPortalPage } from "./ui.js";
|
|
19
|
+
import {
|
|
20
|
+
DEFAULT_PORTAL_CONFIG
|
|
21
|
+
} from "./types.js";
|
|
22
|
+
const HOME = process.env["HOME"] || "/tmp";
|
|
23
|
+
function getPortalDir() {
|
|
24
|
+
const dir = process.env["PORTAL_STATE_DIR"] || join(HOME, ".stackmemory", "portal");
|
|
25
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
26
|
+
return dir;
|
|
27
|
+
}
|
|
28
|
+
function tokenPath() {
|
|
29
|
+
return join(getPortalDir(), "token");
|
|
30
|
+
}
|
|
31
|
+
function pidPath() {
|
|
32
|
+
return join(getPortalDir(), "portal.json");
|
|
33
|
+
}
|
|
34
|
+
function ensureToken() {
|
|
35
|
+
const p = tokenPath();
|
|
36
|
+
if (existsSync(p)) {
|
|
37
|
+
const t = readFileSync(p, "utf-8").trim();
|
|
38
|
+
if (t) return t;
|
|
39
|
+
}
|
|
40
|
+
const token = randomBytes(24).toString("hex");
|
|
41
|
+
writeFileSync(p, token + "\n", { mode: 384 });
|
|
42
|
+
return token;
|
|
43
|
+
}
|
|
44
|
+
function tmuxAvailable() {
|
|
45
|
+
try {
|
|
46
|
+
execFileSync("tmux", ["-V"], { stdio: "ignore" });
|
|
47
|
+
return true;
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function resolveConfig(overrides = {}) {
|
|
53
|
+
const token = overrides.token ?? (overrides.noAuth ? "" : ensureToken());
|
|
54
|
+
return {
|
|
55
|
+
...DEFAULT_PORTAL_CONFIG,
|
|
56
|
+
cwd: process.cwd(),
|
|
57
|
+
token,
|
|
58
|
+
...overrides
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
class PortalServer {
|
|
62
|
+
config;
|
|
63
|
+
httpServer = null;
|
|
64
|
+
io = null;
|
|
65
|
+
ptys = /* @__PURE__ */ new Set();
|
|
66
|
+
constructor(config = {}) {
|
|
67
|
+
this.config = resolveConfig(config);
|
|
68
|
+
}
|
|
69
|
+
getConfig() {
|
|
70
|
+
return this.config;
|
|
71
|
+
}
|
|
72
|
+
authOk(token) {
|
|
73
|
+
if (this.config.noAuth) return true;
|
|
74
|
+
return typeof token === "string" && token === this.config.token;
|
|
75
|
+
}
|
|
76
|
+
async start() {
|
|
77
|
+
if (!tmuxAvailable()) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
"tmux is not installed. Install it first (e.g. `apt install tmux` / `brew install tmux`)."
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
const app = express();
|
|
83
|
+
const page = renderPortalPage({ session: this.config.session });
|
|
84
|
+
app.get("/", (req, res) => {
|
|
85
|
+
if (!this.config.noAuth && !this.authOk(req.query["token"])) {
|
|
86
|
+
res.status(401).type("text/plain").send("Unauthorized: missing or invalid ?token");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
res.type("html").send(page);
|
|
90
|
+
});
|
|
91
|
+
app.get("/healthz", (_req, res) => {
|
|
92
|
+
res.json({ ok: true, session: this.config.session });
|
|
93
|
+
});
|
|
94
|
+
const httpServer = createServer(app);
|
|
95
|
+
const io = new SocketServer(httpServer, { cors: { origin: true } });
|
|
96
|
+
io.use((socket, next) => {
|
|
97
|
+
const token = socket.handshake.auth?.token ?? socket.handshake.query["token"];
|
|
98
|
+
if (this.authOk(token)) return next();
|
|
99
|
+
next(new Error("Invalid access token"));
|
|
100
|
+
});
|
|
101
|
+
io.on("connection", (socket) => {
|
|
102
|
+
void this.attachSession(socket);
|
|
103
|
+
});
|
|
104
|
+
this.httpServer = httpServer;
|
|
105
|
+
this.io = io;
|
|
106
|
+
await new Promise((resolve, reject) => {
|
|
107
|
+
httpServer.once("error", reject);
|
|
108
|
+
httpServer.listen(this.config.port, this.config.host, () => resolve());
|
|
109
|
+
});
|
|
110
|
+
const status = {
|
|
111
|
+
running: true,
|
|
112
|
+
pid: process.pid,
|
|
113
|
+
port: this.config.port,
|
|
114
|
+
host: this.config.host,
|
|
115
|
+
session: this.config.session,
|
|
116
|
+
startedAt: Date.now()
|
|
117
|
+
};
|
|
118
|
+
writeFileSync(pidPath(), JSON.stringify(status, null, 2));
|
|
119
|
+
return status;
|
|
120
|
+
}
|
|
121
|
+
async attachSession(socket) {
|
|
122
|
+
let pty;
|
|
123
|
+
try {
|
|
124
|
+
pty = await import("node-pty");
|
|
125
|
+
} catch {
|
|
126
|
+
socket.emit(
|
|
127
|
+
"portal:error",
|
|
128
|
+
"node-pty is not installed on the server. Run: npm install node-pty"
|
|
129
|
+
);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const proc = pty.spawn(
|
|
133
|
+
"tmux",
|
|
134
|
+
["new-session", "-A", "-s", this.config.session, this.config.command],
|
|
135
|
+
{
|
|
136
|
+
name: "xterm-256color",
|
|
137
|
+
cols: 80,
|
|
138
|
+
rows: 24,
|
|
139
|
+
cwd: this.config.cwd,
|
|
140
|
+
env: { ...process.env, TERM: "xterm-256color" }
|
|
141
|
+
}
|
|
142
|
+
);
|
|
143
|
+
this.ptys.add(proc);
|
|
144
|
+
proc.onData((data) => socket.emit("output", data));
|
|
145
|
+
proc.onExit(() => {
|
|
146
|
+
this.ptys.delete(proc);
|
|
147
|
+
socket.emit(
|
|
148
|
+
"output",
|
|
149
|
+
"\r\n\x1B[33m[portal] session detached]\x1B[0m\r\n"
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
socket.on("input", (data) => {
|
|
153
|
+
if (typeof data === "string") proc.write(data);
|
|
154
|
+
});
|
|
155
|
+
socket.on("resize", (size) => {
|
|
156
|
+
const s = size;
|
|
157
|
+
if (s && Number.isFinite(s.cols) && Number.isFinite(s.rows)) {
|
|
158
|
+
try {
|
|
159
|
+
proc.resize(
|
|
160
|
+
Math.max(2, s.cols),
|
|
161
|
+
Math.max(2, s.rows)
|
|
162
|
+
);
|
|
163
|
+
} catch {
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
socket.on("disconnect", () => {
|
|
168
|
+
this.ptys.delete(proc);
|
|
169
|
+
try {
|
|
170
|
+
proc.kill();
|
|
171
|
+
} catch {
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
async stop() {
|
|
176
|
+
for (const p of this.ptys) {
|
|
177
|
+
try {
|
|
178
|
+
p.kill();
|
|
179
|
+
} catch {
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
this.ptys.clear();
|
|
183
|
+
if (this.io) {
|
|
184
|
+
this.io.close();
|
|
185
|
+
this.io = null;
|
|
186
|
+
}
|
|
187
|
+
await new Promise((resolve) => {
|
|
188
|
+
if (!this.httpServer) return resolve();
|
|
189
|
+
this.httpServer.close(() => resolve());
|
|
190
|
+
});
|
|
191
|
+
this.httpServer = null;
|
|
192
|
+
clearPidFile();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function readStatus() {
|
|
196
|
+
const p = pidPath();
|
|
197
|
+
if (!existsSync(p)) return { running: false };
|
|
198
|
+
try {
|
|
199
|
+
const status = JSON.parse(readFileSync(p, "utf-8"));
|
|
200
|
+
if (status.pid && !isProcessAlive(status.pid)) {
|
|
201
|
+
clearPidFile();
|
|
202
|
+
return { running: false };
|
|
203
|
+
}
|
|
204
|
+
return status;
|
|
205
|
+
} catch {
|
|
206
|
+
return { running: false };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function stopRunning() {
|
|
210
|
+
const status = readStatus();
|
|
211
|
+
if (!status.running || !status.pid) return false;
|
|
212
|
+
try {
|
|
213
|
+
process.kill(status.pid, "SIGTERM");
|
|
214
|
+
} catch {
|
|
215
|
+
}
|
|
216
|
+
clearPidFile();
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
function clearPidFile() {
|
|
220
|
+
try {
|
|
221
|
+
if (existsSync(pidPath())) unlinkSync(pidPath());
|
|
222
|
+
} catch {
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function isProcessAlive(pid) {
|
|
226
|
+
try {
|
|
227
|
+
process.kill(pid, 0);
|
|
228
|
+
return true;
|
|
229
|
+
} catch {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
export {
|
|
234
|
+
PortalServer,
|
|
235
|
+
ensureToken,
|
|
236
|
+
getPortalDir,
|
|
237
|
+
readStatus,
|
|
238
|
+
resolveConfig,
|
|
239
|
+
stopRunning
|
|
240
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
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_PORTAL_CONFIG = {
|
|
6
|
+
port: 7799,
|
|
7
|
+
host: "0.0.0.0",
|
|
8
|
+
session: "claude",
|
|
9
|
+
command: "claude",
|
|
10
|
+
noAuth: false
|
|
11
|
+
};
|
|
12
|
+
export {
|
|
13
|
+
DEFAULT_PORTAL_CONFIG
|
|
14
|
+
};
|