@geravant/sinain 1.8.0 → 1.10.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.
@@ -10,7 +10,7 @@ import { TranscriptionService } from "./audio/transcription.js";
10
10
  import { AgentLoop } from "./agent/loop.js";
11
11
  import { TraitEngine, loadTraitRoster } from "./agent/traits.js";
12
12
  import { shortAppName } from "./agent/context-window.js";
13
- import { Escalator, type SpawnContext } from "./escalation/escalator.js";
13
+ import { Escalator } from "./escalation/escalator.js";
14
14
  import { Recorder } from "./recorder.js";
15
15
  import { Tracer } from "./trace/tracer.js";
16
16
  import { TraceStore } from "./trace/trace-store.js";
@@ -18,6 +18,7 @@ import { FeedbackStore } from "./learning/feedback-store.js";
18
18
  import { SignalCollector } from "./learning/signal-collector.js";
19
19
  import { createAppServer } from "./server.js";
20
20
  import { Profiler } from "./profiler.js";
21
+ import { CostTracker } from "./cost/tracker.js";
21
22
  import type { SenseEvent, EscalationMode, FeedItem } from "./types.js";
22
23
  import { isDuplicateTranscript, bigramSimilarity } from "./util/dedup.js";
23
24
  import { log, warn, error } from "./log.js";
@@ -25,41 +26,6 @@ import { initPrivacy, levelFor, applyLevel } from "./privacy/index.js";
25
26
 
26
27
  const TAG = "core";
27
28
 
28
- /** Build context snapshot for user-initiated spawn tasks. */
29
- function buildSpawnContext(
30
- entry: { digest: string; context: { currentApp: string } },
31
- feedBuffer: FeedBuffer,
32
- senseBuffer: SenseBuffer,
33
- ): SpawnContext {
34
- const cutoff = Date.now() - 60_000;
35
-
36
- // Recent audio: last ~60s of transcripts
37
- const recentAudio = feedBuffer.queryByTime(cutoff)
38
- .filter(m => m.channel === "stream" && (m.text.startsWith("[🔊]") || m.text.startsWith("[🎙]")))
39
- .map(m => m.text)
40
- .join("\n")
41
- .slice(0, 2000);
42
-
43
- // Recent screen: last ~60s of deduped OCR text
44
- const screenEvents = senseBuffer.queryByTime(cutoff);
45
- const seenOcr = new Set<string>();
46
- const screenLines: string[] = [];
47
- for (const e of screenEvents) {
48
- if (e.ocr && !seenOcr.has(e.ocr)) {
49
- seenOcr.add(e.ocr);
50
- screenLines.push(`[${e.meta.app}] ${e.ocr}`);
51
- }
52
- }
53
- const recentScreen = screenLines.join("\n").slice(0, 3000);
54
-
55
- return {
56
- currentApp: entry.context.currentApp,
57
- digest: entry.digest,
58
- recentAudio: recentAudio || undefined,
59
- recentScreen: recentScreen || undefined,
60
- };
61
- }
62
-
63
29
  /** Resolve workspace path, expanding leading ~ to HOME. */
64
30
  function resolveWorkspace(): string {
65
31
  const raw = process.env.SINAIN_WORKSPACE || `${process.env.HOME}/.openclaw/workspace`;
@@ -93,6 +59,10 @@ async function main() {
93
59
  // ── Initialize overlay WS handler ──
94
60
  const wsHandler = new WsHandler();
95
61
 
62
+ // ── Initialize cost tracker ──
63
+ const costTracker = new CostTracker((snapshot) => wsHandler.broadcastCost(snapshot, config.costDisplayEnabled));
64
+ costTracker.startPeriodicLog(60_000);
65
+
96
66
  // ── Initialize tracing ──
97
67
  const tracer = config.traceEnabled ? new Tracer() : null;
98
68
  const traceStore = config.traceEnabled ? new TraceStore(config.traceDir) : null;
@@ -100,9 +70,6 @@ async function main() {
100
70
  // ── Initialize recorder ──
101
71
  const recorder = new Recorder();
102
72
 
103
- // ── Spawn context cache — updated every agent tick for user-initiated spawns ──
104
- let lastSpawnContext: SpawnContext | null = null;
105
-
106
73
  // ── Initialize profiler ──
107
74
  const profiler = new Profiler();
108
75
 
@@ -146,11 +113,35 @@ async function main() {
146
113
  getRecorderStatus: () => recorder.getStatus(),
147
114
  profiler,
148
115
  onAnalysis: (entry, contextWindow) => {
149
- // Handle recorder commands (recording start/stop still works)
150
- recorder.handleCommand(entry.record);
116
+ // Handle recorder commands
117
+ const stopResult = recorder.handleCommand(entry.record);
118
+
119
+ // Dispatch task via subagent spawn
120
+ if (entry.task || stopResult) {
121
+ let task: string;
122
+ let label: string | undefined;
123
+
124
+ if (stopResult && stopResult.segments > 0 && entry.task) {
125
+ // Recording stopped with explicit task instruction
126
+ task = `${entry.task}\n\n[Recording: "${stopResult.title}", ${stopResult.durationS}s]\n${stopResult.transcript}`;
127
+ label = stopResult.title;
128
+ } else if (stopResult && stopResult.segments > 0) {
129
+ // Recording stopped without explicit task — default to cleanup/summarize
130
+ task = `Clean up and summarize this recording transcript:\n\n[Recording: "${stopResult.title}", ${stopResult.durationS}s]\n${stopResult.transcript}`;
131
+ label = stopResult.title;
132
+ } else if (entry.task) {
133
+ // Standalone task without recording
134
+ task = entry.task;
135
+ } else {
136
+ task = "";
137
+ }
151
138
 
152
- // Cache context for user-initiated spawn commands (Shift+Enter)
153
- lastSpawnContext = buildSpawnContext(entry, feedBuffer, senseBuffer);
139
+ if (task) {
140
+ escalator.dispatchSpawnTask(task, label).catch(err => {
141
+ error(TAG, "spawn task dispatch error:", err);
142
+ });
143
+ }
144
+ }
154
145
 
155
146
  // Escalation continues as normal
156
147
  escalator.onAgentAnalysis(entry, contextWindow);
@@ -184,6 +175,7 @@ async function main() {
184
175
  return null;
185
176
  },
186
177
  feedbackStore: feedbackStore ?? undefined,
178
+ costTracker,
187
179
  });
188
180
 
189
181
  // ── Wire learning signal collector (needs agentLoop) ──
@@ -211,6 +203,7 @@ async function main() {
211
203
  systemAudioPipeline.setProfiler(profiler);
212
204
  if (micPipeline) micPipeline.setProfiler(profiler);
213
205
  transcription.setProfiler(profiler);
206
+ transcription.setCostTracker(costTracker);
214
207
 
215
208
  // Wire: audio chunks → transcription (both pipelines share the same transcription service)
216
209
  systemAudioPipeline.on("chunk", (chunk) => {
@@ -322,6 +315,7 @@ async function main() {
322
315
  senseBuffer,
323
316
  wsHandler,
324
317
  profiler,
318
+ costTracker,
325
319
  feedbackStore: feedbackStore ?? undefined,
326
320
  isScreenActive: () => screenActive,
327
321
 
@@ -447,7 +441,7 @@ async function main() {
447
441
 
448
442
  // Spawn background agent task (from HUD Shift+Enter or bare agent POST /spawn)
449
443
  onSpawnCommand: (text: string) => {
450
- escalator.dispatchSpawnTask(text, text.slice(0, 64), lastSpawnContext ?? undefined).catch((err) => {
444
+ escalator.dispatchSpawnTask(text, "user-command").catch((err) => {
451
445
  log("srv", `spawn via HTTP failed: ${err}`);
452
446
  });
453
447
  },
@@ -475,7 +469,7 @@ async function main() {
475
469
  agentLoop.onNewContext(true);
476
470
  },
477
471
  onSpawnCommand: (text) => {
478
- escalator.dispatchSpawnTask(text, text.slice(0, 64), lastSpawnContext ?? undefined).catch((err) => {
472
+ escalator.dispatchSpawnTask(text, "user-command").catch((err) => {
479
473
  log("cmd", `spawn command failed: ${err}`);
480
474
  wsHandler.broadcast(`\u26a0 Spawn failed: ${String(err).slice(0, 100)}`, "normal");
481
475
  });
@@ -551,12 +545,14 @@ async function main() {
551
545
  log(TAG, ` agent: ${config.agentConfig.enabled ? "enabled" : "disabled"}`);
552
546
  log(TAG, ` escal: ${config.escalationConfig.mode}`);
553
547
  log(TAG, ` traits: ${config.traitConfig.enabled ? "enabled" : "disabled"} (${traitRoster.length} traits)`);
548
+ log(TAG, ` cost: display=${config.costDisplayEnabled ? "on" : "off"} (always logged)`);
554
549
 
555
550
  // ── Graceful shutdown ──
556
551
  const shutdown = async (signal: string) => {
557
552
  log(TAG, `${signal} received, shutting down...`);
558
553
  clearInterval(bufferGaugeTimer);
559
554
  if (feedbackSummaryTimer) clearInterval(feedbackSummaryTimer);
555
+ costTracker.stop();
560
556
  profiler.stop();
561
557
  recorder.forceStop(); // Stop any active recording
562
558
  agentLoop.stop();
@@ -1,8 +1,10 @@
1
+ import { execFile } from "node:child_process";
1
2
  import type { InboundMessage } from "../types.js";
2
3
  import type { WsHandler } from "./ws-handler.js";
3
4
  import type { AudioPipeline } from "../audio/pipeline.js";
4
5
  import type { CoreConfig } from "../types.js";
5
6
  import { WebSocket } from "ws";
7
+ import { loadedEnvPath } from "../config.js";
6
8
  import { log } from "../log.js";
7
9
 
8
10
  const TAG = "cmd";
@@ -151,6 +153,16 @@ function handleCommand(action: string, deps: CommandDeps): void {
151
153
  log(TAG, `traits toggled ${nowEnabled ? "ON" : "OFF"}`);
152
154
  break;
153
155
  }
156
+ case "open_settings": {
157
+ const envPath = loadedEnvPath || `${process.env.HOME || process.env.USERPROFILE}/.sinain/.env`;
158
+ const cmd = process.platform === "win32" ? "notepad" : "open";
159
+ const args = process.platform === "win32" ? [envPath] : ["-t", envPath];
160
+ execFile(cmd, args, (err) => {
161
+ if (err) log(TAG, `open_settings failed: ${err.message}`);
162
+ });
163
+ log(TAG, `open_settings: ${envPath}`);
164
+ break;
165
+ }
154
166
  default:
155
167
  log(TAG, `unhandled command: ${action}`);
156
168
  }
@@ -7,6 +7,8 @@ import type {
7
7
  FeedMessage,
8
8
  StatusMessage,
9
9
  SpawnTaskMessage,
10
+ CostMessage,
11
+ CostSnapshot,
10
12
  Priority,
11
13
  FeedChannel,
12
14
  } from "../types.js";
@@ -39,6 +41,7 @@ export class WsHandler {
39
41
  };
40
42
  private replayBuffer: FeedMessage[] = [];
41
43
  private spawnTaskBuffer: Map<string, SpawnTaskMessage> = new Map();
44
+ private latestCostMsg: CostMessage | null = null;
42
45
 
43
46
  constructor() {
44
47
  this.startHeartbeat();
@@ -89,6 +92,16 @@ export class WsHandler {
89
92
  log(TAG, `replayed ${this.spawnTaskBuffer.size} spawn tasks to new client`);
90
93
  }
91
94
 
95
+ // Replay current cost snapshot (or send zeroed snapshot so client resets)
96
+ this.sendTo(ws, this.latestCostMsg ?? {
97
+ type: "cost" as const,
98
+ totalCost: 0,
99
+ costBySource: {},
100
+ callCount: 0,
101
+ startedAt: Date.now(),
102
+ displayEnabled: false,
103
+ });
104
+
92
105
  ws.on("message", (raw) => {
93
106
  try {
94
107
  const data = JSON.parse(raw.toString()) as InboundMessage;
@@ -158,6 +171,20 @@ export class WsHandler {
158
171
  this.broadcastMessage(msg);
159
172
  }
160
173
 
174
+ /** Broadcast cost snapshot to all connected overlays. */
175
+ broadcastCost(snapshot: CostSnapshot, displayEnabled: boolean): void {
176
+ const msg: CostMessage = {
177
+ type: "cost",
178
+ totalCost: snapshot.totalCost,
179
+ costBySource: snapshot.costBySource,
180
+ callCount: snapshot.callCount,
181
+ startedAt: snapshot.startedAt,
182
+ displayEnabled,
183
+ };
184
+ this.latestCostMsg = msg;
185
+ this.broadcastMessage(msg);
186
+ }
187
+
161
188
  /** Update internal state and broadcast. */
162
189
  updateState(partial: Partial<BridgeState>): void {
163
190
  Object.assign(this.state, partial);
@@ -2,6 +2,7 @@ import { createServer, type IncomingMessage, type ServerResponse } from "node:ht
2
2
  import { WebSocketServer, WebSocket } from "ws";
3
3
  import type { CoreConfig, SenseEvent } from "./types.js";
4
4
  import type { Profiler } from "./profiler.js";
5
+ import type { CostTracker } from "./cost/tracker.js";
5
6
  import type { FeedbackStore } from "./learning/feedback-store.js";
6
7
  import { FeedBuffer } from "./buffers/feed-buffer.js";
7
8
  import { SenseBuffer, type SemanticSenseEvent, type TextDelta } from "./buffers/sense-buffer.js";
@@ -20,6 +21,7 @@ export interface ServerDeps {
20
21
  senseBuffer: SenseBuffer;
21
22
  wsHandler: WsHandler;
22
23
  profiler?: Profiler;
24
+ costTracker?: CostTracker;
23
25
  onSenseEvent: (event: SenseEvent) => void;
24
26
  onSenseDelta?: (data: { app: string; activity: string; changes: TextDelta[]; priority?: string; ts: number }) => void;
25
27
  onFeedPost: (text: string, priority: string) => void;
@@ -65,6 +67,8 @@ function readBody(req: IncomingMessage, maxBytes: number): Promise<string> {
65
67
  export function createAppServer(deps: ServerDeps) {
66
68
  const { config, feedBuffer, senseBuffer, wsHandler } = deps;
67
69
  let senseInBytes = 0;
70
+ const seenVisionCostIds = new Set<string>();
71
+ const visionCostCleanup = setInterval(() => seenVisionCostIds.clear(), 60_000);
68
72
 
69
73
  const httpServer = createServer(async (req: IncomingMessage, res: ServerResponse) => {
70
74
  res.setHeader("Access-Control-Allow-Origin", "*");
@@ -91,6 +95,24 @@ export function createAppServer(deps: ServerDeps) {
91
95
  senseInBytes += Buffer.byteLength(body);
92
96
  deps.profiler?.gauge("network.senseInBytes", senseInBytes);
93
97
  const data = JSON.parse(body);
98
+
99
+ // Record vision API cost before dedup — the call is already billed.
100
+ // Dedup by cost_id to avoid double-counting on sender retries.
101
+ const vc = data.vision_cost;
102
+ if (vc && typeof vc.cost === "number" && vc.cost > 0) {
103
+ const costId = vc.cost_id as string | undefined;
104
+ if (!costId || !seenVisionCostIds.has(costId)) {
105
+ if (costId) seenVisionCostIds.add(costId);
106
+ deps.costTracker?.record({
107
+ source: "vision",
108
+ model: vc.model || "unknown",
109
+ cost: vc.cost,
110
+ tokensIn: vc.tokens_in || 0,
111
+ tokensOut: vc.tokens_out || 0,
112
+ ts: Date.now(),
113
+ });
114
+ }
115
+ }
94
116
  if (!data.type || data.ts === undefined) {
95
117
  res.writeHead(400);
96
118
  res.end(JSON.stringify({ ok: false, error: "missing type or ts" }));
@@ -309,6 +331,24 @@ export function createAppServer(deps: ServerDeps) {
309
331
  return;
310
332
  }
311
333
 
334
+ // ── /setup/status ── (used by overlay onboarding wizard)
335
+ if (req.method === "GET" && url.pathname === "/setup/status") {
336
+ const health = deps.getHealthPayload();
337
+ res.end(JSON.stringify({
338
+ ok: true,
339
+ setup: {
340
+ openrouterKey: !!config.transcriptionConfig.openrouterApiKey,
341
+ gatewayConfigured: !!config.openclawConfig.gatewayToken,
342
+ gatewayConnected: !!(health as Record<string, any>)?.escalation?.gatewayConnected,
343
+ audioActive: (health as Record<string, any>)?.audio?.state === "active" || (health as Record<string, any>)?.audioPipeline?.state === "running",
344
+ screenActive: deps.isScreenActive(),
345
+ transcriptionBackend: config.transcriptionConfig.backend,
346
+ escalationMode: config.escalationConfig.mode,
347
+ },
348
+ }));
349
+ return;
350
+ }
351
+
312
352
  // ── /user/command ──
313
353
  if (req.method === "POST" && url.pathname === "/user/command") {
314
354
  const body = await readBody(req, 4096);
@@ -504,6 +544,7 @@ export function createAppServer(deps: ServerDeps) {
504
544
  });
505
545
  },
506
546
  async destroy(): Promise<void> {
547
+ clearInterval(visionCostCleanup);
507
548
  wsHandler.destroy();
508
549
  wss.close();
509
550
  await new Promise<void>((resolve) => httpServer.close(() => resolve()));
@@ -78,7 +78,36 @@ export interface SpawnCommandMessage {
78
78
  text: string;
79
79
  }
80
80
 
81
- export type OutboundMessage = FeedMessage | StatusMessage | PingMessage | SpawnTaskMessage;
81
+ /** Cost update broadcast to overlay. */
82
+ export interface CostMessage {
83
+ type: "cost";
84
+ totalCost: number;
85
+ costBySource: Record<string, number>;
86
+ callCount: number;
87
+ startedAt: number;
88
+ displayEnabled: boolean;
89
+ }
90
+
91
+ /** Entry recorded by CostTracker for each LLM call. */
92
+ export interface CostEntry {
93
+ source: "analyzer" | "transcription" | "vision";
94
+ model: string;
95
+ cost: number;
96
+ tokensIn: number;
97
+ tokensOut: number;
98
+ ts: number;
99
+ }
100
+
101
+ /** Snapshot of accumulated cost state. */
102
+ export interface CostSnapshot {
103
+ totalCost: number;
104
+ costBySource: Record<string, number>;
105
+ costByModel: Record<string, number>;
106
+ callCount: number;
107
+ startedAt: number;
108
+ }
109
+
110
+ export type OutboundMessage = FeedMessage | StatusMessage | PingMessage | SpawnTaskMessage | CostMessage;
82
111
  export type InboundMessage = UserMessage | CommandMessage | PongMessage | ProfilingMessage | UserCommandMessage | SpawnCommandMessage;
83
112
 
84
113
  /** Abstraction for user commands (text now, voice later). */
@@ -218,28 +247,31 @@ export interface StopResult {
218
247
  export type EscalationMode = "off" | "selective" | "focus" | "rich";
219
248
  export type ContextRichness = "lean" | "standard" | "rich";
220
249
 
221
- export interface AgentConfig {
250
+ export type AnalysisProvider = "openrouter" | "ollama";
251
+
252
+ export interface AnalysisConfig {
222
253
  enabled: boolean;
254
+ provider: AnalysisProvider;
223
255
  model: string;
224
256
  visionModel: string;
225
- visionEnabled: boolean;
226
- localVisionEnabled: boolean;
227
- localVisionModel: string;
228
- localVisionUrl: string;
229
- localVisionTimeout: number;
230
- openrouterApiKey: string;
257
+ endpoint: string;
258
+ apiKey: string;
231
259
  maxTokens: number;
232
260
  temperature: number;
261
+ fallbackModels: string[];
262
+ timeout: number;
263
+ // Loop timing
233
264
  pushToFeed: boolean;
234
265
  debounceMs: number;
235
266
  maxIntervalMs: number;
236
267
  cooldownMs: number;
237
268
  maxAgeMs: number;
238
- fallbackModels: string[];
239
- /** Maximum entries to keep in agent history buffer (default: 50) */
240
269
  historyLimit: number;
241
270
  }
242
271
 
272
+ /** @deprecated Use AnalysisConfig */
273
+ export type AgentConfig = AnalysisConfig;
274
+
243
275
  export interface AgentResult {
244
276
  hud: string;
245
277
  digest: string;
@@ -253,6 +285,8 @@ export interface AgentResult {
253
285
  voice?: string;
254
286
  voice_stat?: number;
255
287
  voice_confidence?: number;
288
+ /** Actual USD cost returned by OpenRouter (undefined if not available). */
289
+ cost?: number;
256
290
  }
257
291
 
258
292
  export interface AgentEntry extends AgentResult {
@@ -437,11 +471,12 @@ export interface CoreConfig {
437
471
  micConfig: AudioPipelineConfig;
438
472
  micEnabled: boolean;
439
473
  transcriptionConfig: TranscriptionConfig;
440
- agentConfig: AgentConfig;
474
+ agentConfig: AnalysisConfig;
441
475
  escalationConfig: EscalationConfig;
442
476
  openclawConfig: OpenClawConfig;
443
477
  situationMdPath: string;
444
478
  traceEnabled: boolean;
479
+ costDisplayEnabled: boolean;
445
480
  traceDir: string;
446
481
  learningConfig: LearningConfig;
447
482
  traitConfig: TraitConfig;
@@ -43,7 +43,6 @@ function writeDistillState(workspaceDir: string, state: DistillState): void {
43
43
 
44
44
  export type HeartbeatResult = {
45
45
  status: string;
46
- gitBackup: string | null;
47
46
  signals: unknown[];
48
47
  recommendedAction: { action: string; task: string | null; confidence: number };
49
48
  output: unknown | null;
@@ -95,7 +94,6 @@ export class CurationEngine {
95
94
  const workspaceDir = this.store.getWorkspaceDir();
96
95
  const result: HeartbeatResult = {
97
96
  status: "ok",
98
- gitBackup: null,
99
97
  signals: [],
100
98
  recommendedAction: { action: "skip", task: null, confidence: 0 },
101
99
  output: null,
@@ -131,20 +129,6 @@ export class CurationEngine {
131
129
  const latencyMs: Record<string, number> = {};
132
130
  const heartbeatStart = Date.now();
133
131
 
134
- // 1. Git backup (30s timeout)
135
- try {
136
- const t0 = Date.now();
137
- const gitOut = await this.runScript(
138
- ["bash", "sinain-memory/git_backup.sh"],
139
- { timeoutMs: 30_000, cwd: workspaceDir },
140
- );
141
- latencyMs.gitBackup = Date.now() - t0;
142
- result.gitBackup = gitOut.stdout.trim() || "nothing to commit";
143
- } catch (err) {
144
- this.logger.warn(`sinain-hud: git backup error: ${String(err)}`);
145
- result.gitBackup = `error: ${String(err)}`;
146
- }
147
-
148
132
  // Current time string for memory scripts
149
133
  const hbTz = this.config.userTimezone;
150
134
  const currentTimeStr = new Date().toLocaleString("en-GB", {
@@ -291,7 +275,6 @@ export class CurationEngine {
291
275
  output: result.output,
292
276
  skipped: result.skipped,
293
277
  skipReason: result.skipReason,
294
- gitBackup: result.gitBackup,
295
278
  latencyMs,
296
279
  totalLatencyMs,
297
280
  };
@@ -59,4 +59,4 @@ SINAIN_BACKUP_REPO=<git-url> npx sinain
59
59
  - Token printed at end (or visible in Brev dashboard → Gateway Token)
60
60
  - Mac side: `./setup-nemoclaw.sh` → 5 prompts → overlay starts
61
61
 
62
- Memory is git-backed via `git_backup.sh` on every heartbeat tick. New instances restore instantly via `SINAIN_BACKUP_REPO`.
62
+ Memory is backed up via knowledge snapshots to `~/.sinain/knowledge-snapshots/`. New instances restore instantly via `SINAIN_BACKUP_REPO`.
@@ -317,23 +317,7 @@ server.tool(
317
317
  const results: string[] = [];
318
318
  const now = new Date().toISOString();
319
319
 
320
- // Step 1: git_backup.sh
321
- const gitBackupPath = resolve(SCRIPTS_DIR, "git_backup.sh");
322
- if (existsSync(gitBackupPath)) {
323
- try {
324
- const out = await new Promise<string>((res, rej) => {
325
- execFile("bash", [gitBackupPath, MEMORY_DIR], { timeout: 30_000 }, (err, stdout, stderr) => {
326
- if (err) rej(new Error(`git_backup failed: ${err.message}\n${stderr}`));
327
- else res(stdout);
328
- });
329
- });
330
- results.push(`[git_backup] ${out.trim() || "OK"}`);
331
- } catch (err: any) {
332
- results.push(`[git_backup] FAILED: ${err.message}`);
333
- }
334
- }
335
-
336
- // Step 2: signal_analyzer.py
320
+ // Step 1: signal_analyzer.py
337
321
  try {
338
322
  const out = await runScript([
339
323
  resolve(SCRIPTS_DIR, "signal_analyzer.py"),
@@ -346,7 +330,7 @@ server.tool(
346
330
  results.push(`[signal_analyzer] FAILED: ${err.message}`);
347
331
  }
348
332
 
349
- // Step 3: insight_synthesizer.py
333
+ // Step 2: insight_synthesizer.py
350
334
  try {
351
335
  const out = await runScript([
352
336
  resolve(SCRIPTS_DIR, "insight_synthesizer.py"),
@@ -358,7 +342,7 @@ server.tool(
358
342
  results.push(`[insight_synthesizer] FAILED: ${err.message}`);
359
343
  }
360
344
 
361
- // Step 4: memory_miner.py
345
+ // Step 3: memory_miner.py
362
346
  try {
363
347
  const out = await runScript([
364
348
  resolve(SCRIPTS_DIR, "memory_miner.py"),
@@ -369,7 +353,7 @@ server.tool(
369
353
  results.push(`[memory_miner] FAILED: ${err.message}`);
370
354
  }
371
355
 
372
- // Step 5: playbook_curator.py
356
+ // Step 4: playbook_curator.py
373
357
  try {
374
358
  const out = await runScript([
375
359
  resolve(SCRIPTS_DIR, "playbook_curator.py"),
@@ -1,19 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Phase 1: Git backup — commit and push any uncommitted changes in the workspace.
3
- # Runs from the workspace root. Exits 0 on success or nothing to commit, 1 on push failure.
4
-
5
- set -euo pipefail
6
-
7
- changes=$(git status --porcelain 2>/dev/null || true)
8
-
9
- if [ -z "$changes" ]; then
10
- echo "nothing to commit"
11
- exit 0
12
- fi
13
-
14
- git add -A
15
- git commit -m "auto: heartbeat $(date -u +%Y-%m-%dT%H:%M:%SZ)"
16
- git push origin main
17
-
18
- # Output the commit hash
19
- git rev-parse --short HEAD