Skip to content

Commit 6b45c80

Browse files
jonathanpeterwuStackMemory Bot (CLI)
andauthored
feat(hooks): token optimization — dedup, auto-route, prewarm, script-suggest (#14)
* feat(teleop): add native voice control prototype * feat(cli): add scaffold command for Company OS folder structure stackmemory scaffold creates company/, wiki/, skills/, clients/, raw/, and .stackmemory/config.yml. Enables local context management with file-based skill rot detection and tenant isolation. * feat(daemon): add opt-out telemetry service New DaemonTelemetryService collects anonymous usage snapshots: - Daemon health (uptime, context saves, memory triggers, errors) - Session counts (total heartbeats, active now) - Skill audit entries, handoff counts - No PII — instance ID is random hex Runs daily (default 24h interval, first at boot+30s). Stores rolling 90-snapshot history in ~/.stackmemory/telemetry.json. Opt out: STACKMEMORY_TELEMETRY=0 or telemetry.enabled: false in config. * feat(hooks): add self-healing daemon health check for SessionStart * feat(daemon): add desire-path detector — auto-discover workflows, suggest skills Three-component system in DaemonDesirePathService: 1. ActionStreamLogger — PostToolUse hook captures tool:target pairs to ~/.stackmemory/desire-paths/action-stream.jsonl (no data/content) 2. PatternDetector — sliding window extracts repeated sequences, filters by min 3 occurrences across 2+ sessions, scores by freq×sessions 3. SkillSuggester — generates skill.md files from top patterns with inputs/outputs inferred from sequence endpoints - 10MB JSONL rotation, 10K entry scan cap for performance - Opt out: STACKMEMORY_DESIRE_PATHS=0 or desirePaths.enabled: false - Scans every 6h, first at boot+2m - Suggestions written to ~/.stackmemory/desire-paths/suggestions/ - 3 adversarial review rounds: fixed separator injection, added scan cap, improved skill naming with target directory context * fix(desire-paths): adaptive backoff — hourly when active, exponential to 12h when idle * feat(cli): add hermes-sm wrapper with StackMemory integration - Auto-starts daemon on session boot - Writes session heartbeats for telemetry tracking - Restores handoff context from previous sessions - Sets STACKMEMORY_SESSION env for desire-path hook - Determinism watcher + tracing - bin/hermes-sm and bin/hermes-smd registered in package.json * feat(desire-paths): auto-promote skills above 0.8 confidence + 5 sessions * feat(daemon): add research stream scanner for market signal detection * fix(test): replace bun:test import with vitest in desire-path-service test * feat(tokens): replace char/4 heuristic with js-tiktoken (cl100k_base) Centralizes token estimation across 14 files through src/core/cache/token-estimator.ts and packages/sdk/src/token-estimator.ts. Lazy-loads cl100k_base encoder with char/4 fallback if WASM fails. Also ports context-budget hook to codex-sm exit handler for compact/restart nudges matching Claude Code behavior. * feat(skill-packs): add content licenses (CC-BY-4.0) to registry metadata Skills are often prompt text (content) not code — content licenses like CC-BY-4.0 fit better than MIT for these. Adds KnownLicenseSchema enum with both code (MIT, Apache-2.0, ISC, BSD) and content (CC-BY-4.0, CC-BY-SA-4.0, CC0-1.0) licenses while keeping the field open for custom SPDX identifiers. * feat(tasks): add local-first master-tasks.md task management Markdown table parser + CLI commands + MCP tools for local-first task steering. Tasks live in master-tasks.md, optionally sync to Linear/GH. - Parser: parse/serialize/update/add/getNext for pipe-delimited md tables - CLI: stackmemory tasks init/md list/md next/md add/md update - MCP: get_next_master_task, update_master_task, create_master_task - 19 tests covering parse, round-trip, priority sorting, file ops * feat(hooks): token optimization — dedup escalation, auto-route, prewarm, script-suggest - dedup-reads: escalate to [STOP] at 5+ reads (was soft-only at 3+) - desire-path-hook: auto-route Bash→Glob/Read/Grep with inline suggestions - prewarm-tools: SessionStart hook emits top deferred tool pre-fetch hint - script-suggest: detects multi-tool patterns matching existing scripts * feat(bench): add hook benchmark script + baseline report Replays 7,589 action-stream entries through hook logic. Result: 324K token savings projected (22% waste reduction). --------- Co-authored-by: StackMemory Bot (CLI) <bot@stackmemory.ai>
1 parent 8d59fb0 commit 6b45c80

56 files changed

Lines changed: 4717 additions & 109 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/teleop-native/Package.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// swift-tools-version: 6.0
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "TeleopNative",
7+
platforms: [
8+
.iOS(.v17),
9+
.macOS(.v14),
10+
],
11+
products: [
12+
.library(name: "TeleopUI", targets: ["TeleopUI"]),
13+
.executable(name: "TeleopMac", targets: ["TeleopMac"]),
14+
],
15+
targets: [
16+
.target(name: "TeleopUI"),
17+
.executableTarget(
18+
name: "TeleopMac",
19+
dependencies: ["TeleopUI"]
20+
),
21+
]
22+
)

apps/teleop-native/README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Teleop Native
2+
3+
Native SwiftUI prototype for low-latency voice teleoperation on macOS and iOS.
4+
5+
## OpenUI Result Summary
6+
7+
- First screen is the control surface, not a landing page.
8+
- One primary animated voice button drives the interaction.
9+
- Visible text is limited to a tiny transient agent cue.
10+
- Telemetry is icon-first: signal, power, and motion are visual gauges.
11+
- Voice/chat owns intent. The button starts, interrupts, resumes, or reconnects.
12+
- WebRTC/media plumbing is intentionally a future integration layer behind the current `TeleopSession` state object.
13+
14+
## Architecture Target
15+
16+
`Mobile or macOS app -> WebRTC session -> global relay -> transceiver -> realtime agent -> teleop gateway -> device`
17+
18+
The app should keep hard safety controls local to the device gateway. The model can interpret intent, but stop limits and degraded-network behavior should not depend on a cloud round trip.
19+
20+
## Run Mac Prototype
21+
22+
```bash
23+
cd apps/teleop-native
24+
swift run TeleopMac
25+
```
26+
27+
## iOS
28+
29+
Open `apps/teleop-native/Package.swift` in Xcode and use `TeleopUI` from a new iOS app target. The shared SwiftUI view is `TeleopHomeView`.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import SwiftUI
2+
import TeleopUI
3+
4+
@main
5+
struct TeleopMacApp: App {
6+
var body: some Scene {
7+
WindowGroup {
8+
TeleopHomeView()
9+
.frame(minWidth: 360, minHeight: 620)
10+
}
11+
.windowStyle(.hiddenTitleBar)
12+
}
13+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import SwiftUI
2+
3+
public struct TeleopHomeView: View {
4+
@StateObject private var session = TeleopSession()
5+
6+
public init() {}
7+
8+
public var body: some View {
9+
ZStack {
10+
TeleopBackground(state: session.state)
11+
12+
VStack(spacing: 26) {
13+
TelemetryStrip(sample: session.telemetry, state: session.state)
14+
.padding(.top, 26)
15+
16+
Spacer(minLength: 20)
17+
18+
TeleopPulseButton(state: session.state) {
19+
session.pressPrimary()
20+
}
21+
22+
if !session.agentSummary.isEmpty {
23+
AgentCue(text: session.agentSummary)
24+
.transition(.opacity.combined(with: .scale(scale: 0.96)))
25+
}
26+
27+
Spacer(minLength: 32)
28+
}
29+
.padding(.horizontal, 24)
30+
.padding(.bottom, 20)
31+
}
32+
.accessibilityElement(children: .contain)
33+
.onDisappear {
34+
session.stop()
35+
}
36+
}
37+
}
38+
39+
private struct TeleopBackground: View {
40+
let state: TeleopConnectionState
41+
42+
var body: some View {
43+
LinearGradient(
44+
colors: backgroundColors,
45+
startPoint: .topLeading,
46+
endPoint: .bottomTrailing
47+
)
48+
.ignoresSafeArea()
49+
.animation(.easeInOut(duration: 0.35), value: state)
50+
}
51+
52+
private var backgroundColors: [Color] {
53+
switch state {
54+
case .idle:
55+
return [Color(red: 0.05, green: 0.06, blue: 0.07), Color(red: 0.0, green: 0.09, blue: 0.08)]
56+
case .connecting:
57+
return [Color(red: 0.03, green: 0.07, blue: 0.10), Color(red: 0.0, green: 0.18, blue: 0.16)]
58+
case .live:
59+
return [Color(red: 0.02, green: 0.11, blue: 0.08), Color(red: 0.10, green: 0.16, blue: 0.07)]
60+
case .thinking:
61+
return [Color(red: 0.10, green: 0.09, blue: 0.02), Color(red: 0.16, green: 0.11, blue: 0.02)]
62+
case .fault:
63+
return [Color(red: 0.12, green: 0.03, blue: 0.04), Color(red: 0.04, green: 0.02, blue: 0.03)]
64+
}
65+
}
66+
}
67+
68+
private struct TelemetryStrip: View {
69+
let sample: TelemetrySample
70+
let state: TeleopConnectionState
71+
72+
var body: some View {
73+
HStack(spacing: 18) {
74+
GaugeGlyph(
75+
systemName: "point.3.connected.trianglepath.dotted",
76+
value: signalValue,
77+
tint: .mint
78+
)
79+
GaugeGlyph(systemName: "bolt.fill", value: sample.battery, tint: .yellow)
80+
GaugeGlyph(systemName: "waveform.path.ecg", value: motionValue, tint: .cyan)
81+
}
82+
.opacity(state == .idle ? 0.38 : 1)
83+
.animation(.easeInOut(duration: 0.25), value: state)
84+
.accessibilityLabel("Telemetry")
85+
}
86+
87+
private var signalValue: Double {
88+
state == .idle ? 0.12 : sample.signal
89+
}
90+
91+
private var motionValue: Double {
92+
state == .idle ? 0.08 : (sample.motion + 1.0) / 2.0
93+
}
94+
}
95+
96+
private struct GaugeGlyph: View {
97+
let systemName: String
98+
let value: Double
99+
let tint: Color
100+
101+
var body: some View {
102+
ZStack {
103+
Circle()
104+
.stroke(.white.opacity(0.12), lineWidth: 4)
105+
Circle()
106+
.trim(from: 0, to: max(0.05, min(1.0, value)))
107+
.stroke(tint, style: StrokeStyle(lineWidth: 4, lineCap: .round))
108+
.rotationEffect(.degrees(-90))
109+
Image(systemName: systemName)
110+
.font(.system(size: 17, weight: .semibold))
111+
.foregroundStyle(.white.opacity(0.86))
112+
}
113+
.frame(width: 48, height: 48)
114+
.animation(.smooth(duration: 0.18), value: value)
115+
}
116+
}
117+
118+
private struct AgentCue: View {
119+
let text: String
120+
121+
var body: some View {
122+
Text(text)
123+
.font(.system(size: 13, weight: .medium, design: .rounded))
124+
.foregroundStyle(.white.opacity(0.82))
125+
.padding(.horizontal, 14)
126+
.padding(.vertical, 8)
127+
.background(.white.opacity(0.10), in: Capsule())
128+
.accessibilityLabel(text)
129+
}
130+
}
131+
132+
#Preview {
133+
TeleopHomeView()
134+
.frame(width: 390, height: 740)
135+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import SwiftUI
2+
3+
struct TeleopPulseButton: View {
4+
let state: TeleopConnectionState
5+
let action: () -> Void
6+
7+
@State private var pulse = false
8+
9+
var body: some View {
10+
Button(action: action) {
11+
ZStack {
12+
ForEach(0..<3, id: \.self) { index in
13+
Circle()
14+
.stroke(ringColor.opacity(0.30), lineWidth: 2)
15+
.frame(width: 170 + CGFloat(index * 42), height: 170 + CGFloat(index * 42))
16+
.scaleEffect(pulse ? 1.12 : 0.86)
17+
.opacity(pulse ? 0.0 : 0.55)
18+
.animation(
19+
.easeOut(duration: 1.45)
20+
.repeatForever(autoreverses: false)
21+
.delay(Double(index) * 0.18),
22+
value: pulse
23+
)
24+
}
25+
26+
Circle()
27+
.fill(buttonFill)
28+
.frame(width: 168, height: 168)
29+
.shadow(color: ringColor.opacity(0.34), radius: 30, y: 16)
30+
31+
Image(systemName: iconName)
32+
.font(.system(size: 58, weight: .semibold))
33+
.foregroundStyle(.white)
34+
.symbolEffect(.pulse, options: .repeating, value: isAnimated)
35+
}
36+
.frame(width: 280, height: 280)
37+
}
38+
.buttonStyle(.plain)
39+
.accessibilityLabel(accessibilityLabel)
40+
.onAppear {
41+
pulse = true
42+
}
43+
.onChange(of: state) { _, newState in
44+
pulse = newState != .idle
45+
}
46+
}
47+
48+
private var iconName: String {
49+
switch state {
50+
case .idle:
51+
return "mic.fill"
52+
case .connecting:
53+
return "antenna.radiowaves.left.and.right"
54+
case .live:
55+
return "waveform"
56+
case .thinking:
57+
return "hand.raised.fill"
58+
case .fault:
59+
return "exclamationmark.triangle.fill"
60+
}
61+
}
62+
63+
private var accessibilityLabel: String {
64+
switch state {
65+
case .idle:
66+
return "Start voice teleoperation"
67+
case .connecting:
68+
return "Cancel connection"
69+
case .live:
70+
return "Interrupt or stop motion"
71+
case .thinking:
72+
return "Resume live control"
73+
case .fault:
74+
return "Reconnect"
75+
}
76+
}
77+
78+
private var buttonFill: LinearGradient {
79+
LinearGradient(
80+
colors: [ringColor.opacity(0.95), ringColor.opacity(0.48)],
81+
startPoint: .topLeading,
82+
endPoint: .bottomTrailing
83+
)
84+
}
85+
86+
private var ringColor: Color {
87+
switch state {
88+
case .idle:
89+
return .teal
90+
case .connecting:
91+
return .cyan
92+
case .live:
93+
return .green
94+
case .thinking:
95+
return .orange
96+
case .fault:
97+
return .red
98+
}
99+
}
100+
101+
private var isAnimated: Bool {
102+
state == .connecting || state == .live || state == .thinking
103+
}
104+
}

0 commit comments

Comments
 (0)