Compare commits

..

1 Commits

Author SHA1 Message Date
bigmerge 6f3d692784 Add peripheral — own-core, consent-gated I/O organ
El SDK CI - dev / build-and-test (pull_request) Waiting to run
939-line Swift I/O organ (mic/camera capture, speaker playback via
AVFoundation/CoreAudio), own-core LPC voice synthesis/imitation,
consent-gating, and full-duplex barge-in conversation — closing the
hear -> understand -> speak loop entirely on-device.

.gitignore in this dir already excludes bin/ (build output), out/
(captured media), and .consent.json/.resume.json (local runtime state),
so only src + README + .gitignore are committed here.
2026-08-15 14:28:14 -05:00
5 changed files with 1027 additions and 625 deletions
+8
View File
@@ -0,0 +1,8 @@
# Build + runtime artifacts — never committed.
bin/
# Captured media (camera frames, mic audio) and syntheses. Raw streams stay
# LOCAL and never egress — including into git.
out/
# Runtime consent + resume state (local, per-machine).
.consent.json
.resume.json
+80
View File
@@ -0,0 +1,80 @@
# peripheral — Neuron's I/O organ (own-core, local, consent-gated)
The interface made physical. Two afferent senses in, one efferent voice out —
all reached the way the agentic surface reaches any tool.
```
MIC (hear) afferent device -> capture -> descriptor -> ingest -> geometry
CAMERA (see) afferent device -> capture -> descriptor -> ingest -> scene-geometry
SPEAKER(speak) efferent render WAV -> PLAY ALOUD out the speaker
```
Closes the conversational loop: **hear (mic) -> understand (engram) -> speak (speaker)**.
## Rails
- **Own-core.** macOS-native only: AVFoundation (camera/mic), CoreAudio voice-
processing (AEC), afplay (speaker), ImageIO/CoreGraphics (frames), hand-rolled
DSP (WAV, LPC, formant synthesis). No cloud, no heavy deps.
- **Local-only.** Raw streams are written to `out/` and never egress. `.gitignore`
keeps captured media out of git.
- **Consent-gated (two locks).** A Neuron-level grant (`grant`/`revoke`) *and* the
OS TCC permission. Sensitive senses (camera/mic) fail closed without both.
- **Disclosed.** Every device touch prints a `[peripheral]` line on stderr.
## Build
```
swiftc -O -o bin/periph src/periph.swift \
-framework AVFoundation -framework CoreMedia -framework Foundation \
-framework CoreGraphics -framework ImageIO -framework CoreImage
```
## Commands
```
periph grant|revoke <camera|mic> # Neuron-level consent
periph status
periph speak <file.wav> # SPEAK ALOUD (efferent)
periph tone <out.wav> [hz] [sec] # own-core WAV synth
periph listen <sec> <out.wav> # MIC capture (afferent), 16k mono
periph see <out.jpg> # CAMERA one frame (afferent)
periph feat-audio <wav> | feat-image <jpg> # capture -> compact descriptor
periph ingest-audio|ingest-image <file> <engramURL> # descriptor -> engram node (geometry)
periph voiceprint <voice.wav> # extract F0 + formants F1-F5
periph imitate <voice.wav> <out.wav> # speak back in that voice (LPC resynthesis)
periph hear-imitate <sec> <out.wav> # MIC -> signature -> imitate -> SPEAK ALOUD
periph converse <manifest.json> [--authority F] [--barge-at S[:backchannel|:bargein]] [--resume] [--live-mic]
```
## The afferent metabolism
A capture is never shipped raw. It becomes a **compact descriptor** — the afferent
twin of the music instrument-signature:
- audio -> `[seconds, sr, ch, rms, peak, zcr, centroid, F0]` (~2400-6000x smaller)
- image -> `[w, h, meanRGB, brightness, 3x3 luminance grid]` (~400000x smaller)
- voice -> `[F0, F1..F5, bandwidths]` (11 numbers)
That descriptor is what the ingest organ (engram `POST /api/nodes`) turns into an
embedded node = geometry.
## Voice by imitation
`voiceprint`/`imitate` are own-core LPC (autocorrelation + Levinson-Durbin, order
16 @ 16 kHz), formant extraction from the LPC spectral envelope, and source-filter
resynthesis (glottal impulse train at F0 through the all-pole formant filter). A
voice is grabbed by ear as ~a dozen numbers and spoken back — **no training, no
stolen voice.** Measured fidelity on real speech: resynthesized formants match the
source within 2-3%. The full phoneme->formant path for *novel* sentences is the
speech faculty's seam (`elp` audio surface profile); this engine provides the
formant synthesis primitive it renders through.
## Interruptibility (native turn-taking)
`converse` plays the utterance as an ordered, salience-tagged **meaning-plan**
while the mic listens (full-duplex, AEC on so it never barges in on its own voice):
- **barge-in**: user speech -> pause on the spot (sample-accurate), not "finish the buffer."
- **yield-or-hold**: a decision grounded in the current segment's salience + progress
+ the interrupter's authority — YIELD (stop) or HOLD ("hang on, let me finish").
- **backchannel** ("mm-hm"): brief/low -> keep going, resume seamlessly.
- **resumable**: on yield the remaining plan persists (`.resume.json`); `--resume`
picks the thread back up ("as I was saying").
Live full-duplex uses `--live-mic` (OS AEC). Injected `--barge-at` drives the
decision loop deterministically for testing.
```
```
+939
View File
@@ -0,0 +1,939 @@
// periph.swift Neuron's PERIPHERAL I/O organ (own-core, LOCAL, CONSENT-GATED).
//
// The interface made physical:
// MIC (hear) = afferent : device -> capture -> [ingest -> geometry]
// CAMERA (see) = afferent : device -> capture -> [ingest -> scene-geometry]
// SPEAKER(speak) = efferent : [render WAV] -> PLAY ALOUD out the speaker
//
// Rails: own-core (AVFoundation / CoreAudio / afplay all ship with macOS),
// no cloud, no heavy deps, raw streams stay LOCAL and never egress,
// every device access is CONSENT-GATED and DISCLOSED.
//
// Full-duplex CONVERSE mode implements native interruptibility: while the
// speaker plays the utterance (a persistent, segmented meaning-plan), the mic
// listens; on user speech it interrupts instantly, then DECIDES yield-or-hold
// grounded in the salience of what it is mid-saying, and can RESUME the thread.
//
// Build: swiftc -O -o peripheral/bin/periph peripheral/src/periph.swift \
// -framework AVFoundation -framework CoreMedia -framework Foundation
import Foundation
import AVFoundation
import CoreMedia
import CoreGraphics
import ImageIO
import CoreImage
// ----------------------------------------------------------------------------
// Disclosure every peripheral touch is announced on stderr. Nothing is silent.
// ----------------------------------------------------------------------------
func disclose(_ msg: String) {
FileHandle.standardError.write(" [peripheral] \(msg)\n".data(using: .utf8)!)
}
func emit(_ obj: [String: Any]) { // machine-readable event on stdout (JSON line)
if let d = try? JSONSerialization.data(withJSONObject: obj),
let s = String(data: d, encoding: .utf8) {
print(s)
}
}
func die(_ msg: String) -> Never {
disclose("ERROR: \(msg)")
emit(["ok": false, "error": msg])
exit(1)
}
// ----------------------------------------------------------------------------
// Consent store Neuron's OWN gate, on top of the OS (TCC) gate. Two locks on
// the sensitive senses. Persisted locally next to the binary's organ dir.
// ----------------------------------------------------------------------------
struct Consent {
static let path: String = {
let dir = ProcessInfo.processInfo.environment["PERIPH_HOME"]
?? FileManager.default.currentDirectoryPath + "/peripheral"
return dir + "/.consent.json"
}()
static func load() -> [String: Bool] {
guard let d = FileManager.default.contents(atPath: path),
let o = try? JSONSerialization.jsonObject(with: d) as? [String: Bool]
else { return ["camera": false, "mic": false] }
return o
}
static func save(_ g: [String: Bool]) {
let d = try! JSONSerialization.data(withJSONObject: g, options: [.prettyPrinted])
try? d.write(to: URL(fileURLWithPath: path))
}
// Neuron-level gate. Sensitive senses (camera/mic) require an explicit grant.
static func require(_ device: String) {
let g = load()
if g[device] != true {
die("CONSENT DENIED for '\(device)'. The user has not granted this sense. " +
"Run: periph grant \(device) (raw streams stay local, never egress).")
}
disclose("consent OK (Neuron-level) for '\(device)' — local only, never egresses.")
}
}
// ----------------------------------------------------------------------------
// OS (TCC) permission the second lock. AVFoundation prompts the user the first
// time; if denied, we fail cleanly rather than hang.
// ----------------------------------------------------------------------------
func requireOSAccess(_ media: AVMediaType, _ label: String) {
let status = AVCaptureDevice.authorizationStatus(for: media)
switch status {
case .authorized:
disclose("consent OK (OS/TCC) for \(label).")
return
case .notDetermined:
disclose("requesting OS permission for \(label) (first use) — user must grant...")
let sem = DispatchSemaphore(value: 0)
var ok = false
AVCaptureDevice.requestAccess(for: media) { granted in ok = granted; sem.signal() }
_ = sem.wait(timeout: .now() + 30)
if !ok { die("OS permission for \(label) was not granted.") }
disclose("consent OK (OS/TCC) for \(label).")
case .denied, .restricted:
die("OS permission for \(label) is DENIED in System Settings > Privacy. " +
"Grant it to the controlling terminal/app, then retry.")
@unknown default:
die("unknown OS permission state for \(label).")
}
}
// ----------------------------------------------------------------------------
// Own-core WAV writer (16-bit PCM). No library proves we own the medium.
// ----------------------------------------------------------------------------
func writeWav(_ url: URL, samples: [Int16], sampleRate: Int, channels: Int = 1) {
var data = Data()
func u32(_ v: UInt32) { var x = v.littleEndian; data.append(Data(bytes: &x, count: 4)) }
func u16(_ v: UInt16) { var x = v.littleEndian; data.append(Data(bytes: &x, count: 2)) }
let bytesPerSample = 2
let dataBytes = samples.count * bytesPerSample
let byteRate = sampleRate * channels * bytesPerSample
data.append("RIFF".data(using: .ascii)!); u32(UInt32(36 + dataBytes))
data.append("WAVE".data(using: .ascii)!)
data.append("fmt ".data(using: .ascii)!); u32(16); u16(1); u16(UInt16(channels))
u32(UInt32(sampleRate)); u32(UInt32(byteRate))
u16(UInt16(channels * bytesPerSample)); u16(16)
data.append("data".data(using: .ascii)!); u32(UInt32(dataBytes))
for s in samples { var x = s.littleEndian; data.append(Data(bytes: &x, count: 2)) }
try? data.write(to: url)
}
// Read a WAV's basic geometry (own-core header parse). Walks chunks to find
// 'fmt ' and 'data' robust to JUNK/FLLR padding chunks (AVAudioRecorder emits them).
func wavInfo(_ path: String) -> (sampleRate: Int, channels: Int, bits: Int, frames: Int)? {
guard let d = FileManager.default.contents(atPath: path), d.count > 44 else { return nil }
func rd16(_ o: Int) -> Int { Int(d[o]) | (Int(d[o+1]) << 8) }
func rd32(_ o: Int) -> Int { Int(d[o]) | (Int(d[o+1])<<8) | (Int(d[o+2])<<16) | (Int(d[o+3])<<24) }
var channels = 0, sampleRate = 0, bits = 0, dataSize = 0
var o = 12
while o + 8 <= d.count {
let id = String(bytes: d[o..<o+4], encoding: .ascii) ?? ""
let sz = rd32(o+4)
if id == "fmt " && o + 24 <= d.count {
channels = rd16(o+10); sampleRate = rd32(o+12); bits = rd16(o+22)
} else if id == "data" {
dataSize = min(sz, d.count - (o+8))
}
o += 8 + sz + (sz & 1)
}
let frames = (channels > 0 && bits > 0) ? dataSize / (channels * bits/8) : 0
return (sampleRate, channels, bits, frames)
}
// ----------------------------------------------------------------------------
// SPEAKER (efferent) play a WAV ALOUD. Own-core: afplay ships with macOS.
// ----------------------------------------------------------------------------
func speak(_ wavPath: String) {
guard FileManager.default.fileExists(atPath: wavPath) else { die("no such file: \(wavPath)") }
disclose("SPEAKER: playing '\(wavPath)' ALOUD out the local speaker (efferent).")
let p = Process()
p.executableURL = URL(fileURLWithPath: "/usr/bin/afplay")
p.arguments = [wavPath]
try? p.run(); p.waitUntilExit()
let ok = p.terminationStatus == 0
disclose(ok ? "SPEAKER: done — Neuron spoke aloud." : "SPEAKER: afplay failed.")
if let i = wavInfo(wavPath) {
emit(["ok": ok, "op": "speak", "file": wavPath, "played_aloud": ok,
"sample_rate": i.sampleRate, "channels": i.channels,
"seconds": Double(i.frames)/Double(max(i.sampleRate,1))])
} else {
emit(["ok": ok, "op": "speak", "file": wavPath, "played_aloud": ok])
}
}
// ----------------------------------------------------------------------------
// MIC (afferent) capture N seconds -> 16k mono 16-bit WAV (formant-ready).
// ----------------------------------------------------------------------------
func listen(seconds: Double, out: String) {
Consent.require("mic")
requireOSAccess(.audio, "microphone")
disclose("MIC: capturing \(seconds)s -> '\(out)' (16 kHz mono, LOCAL, never egresses).")
let url = URL(fileURLWithPath: out)
let settings: [String: Any] = [
AVFormatIDKey: kAudioFormatLinearPCM,
AVSampleRateKey: 16000.0,
AVNumberOfChannelsKey: 1,
AVLinearPCMBitDepthKey: 16,
AVLinearPCMIsFloatKey: false,
AVLinearPCMIsBigEndianKey: false,
]
guard let rec = try? AVAudioRecorder(url: url, settings: settings) else {
die("could not open the microphone recorder.")
}
rec.record()
Thread.sleep(forTimeInterval: seconds)
rec.stop()
// let the file flush
Thread.sleep(forTimeInterval: 0.1)
if let i = wavInfo(out) {
disclose("MIC: captured \(i.frames) frames @ \(i.sampleRate)Hz — ready to hand to the ingest organ.")
emit(["ok": true, "op": "listen", "file": out, "sample_rate": i.sampleRate,
"channels": i.channels, "frames": i.frames,
"seconds": Double(i.frames)/Double(max(i.sampleRate,1)),
"next": "ingest -> phonetic/voice geometry"])
} else {
die("mic capture produced no readable WAV.")
}
}
// ----------------------------------------------------------------------------
// CAMERA (afferent) capture ONE frame -> JPEG on disk.
// ----------------------------------------------------------------------------
// Grab one video frame via AVCaptureVideoDataOutput (CLI-safe; no KVO/photo classes).
final class FrameGrabber: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate {
let sem = DispatchSemaphore(value: 0)
var cgImage: CGImage?
var seen = 0
let cictx = CIContext(options: nil)
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer,
from connection: AVCaptureConnection) {
seen += 1
if cgImage != nil || seen < 5 { return } // let exposure settle a few frames
guard let pb = CMSampleBufferGetImageBuffer(sampleBuffer) else { return }
let ci = CIImage(cvPixelBuffer: pb)
cgImage = cictx.createCGImage(ci, from: ci.extent)
sem.signal()
}
}
func see(out: String) {
Consent.require("camera")
requireOSAccess(.video, "camera")
disclose("CAMERA: capturing one frame -> '\(out)' (LOCAL, never egresses).")
let session = AVCaptureSession()
session.sessionPreset = .photo
guard let device = AVCaptureDevice.default(for: .video),
let input = try? AVCaptureDeviceInput(device: device),
session.canAddInput(input) else { die("no camera device available.") }
session.addInput(input)
let output = AVCaptureVideoDataOutput()
output.alwaysDiscardsLateVideoFrames = true
let grabber = FrameGrabber()
output.setSampleBufferDelegate(grabber, queue: DispatchQueue(label: "periph.cam"))
guard session.canAddOutput(output) else { die("cannot add video output.") }
session.addOutput(output)
session.startRunning()
if grabber.sem.wait(timeout: .now() + 10) == .timedOut { session.stopRunning(); die("camera capture timed out.") }
session.stopRunning()
guard let cg = grabber.cgImage,
let dst = CGImageDestinationCreateWithURL(URL(fileURLWithPath: out) as CFURL,
"public.jpeg" as CFString, 1, nil)
else { die("camera returned no frame.") }
CGImageDestinationAddImage(dst, cg, nil)
guard CGImageDestinationFinalize(dst) else { die("could not write JPEG.") }
let bytes = ((try? FileManager.default.attributesOfItem(atPath: out))?[.size] as? Int) ?? 0
disclose("CAMERA: wrote \(cg.width)x\(cg.height) frame (\(bytes) bytes) — ready for scene-geometry ingest.")
emit(["ok": true, "op": "see", "file": out, "width": cg.width, "height": cg.height,
"bytes": bytes, "next": "ingest -> scene-geometry"])
}
// ============================================================================
// FEAT the afferent METABOLISM: a raw capture becomes a COMPACT descriptor
// (a few dozen numbers), the mirror of the efferent signature. This is what
// gets handed to the ingest organ as geometry NOT the raw stream. Own-core.
// ============================================================================
// Read all 16-bit PCM samples from a WAV (own-core).
func readWavSamples(_ path: String) -> (samples: [Double], sr: Int, ch: Int)? {
guard let d = FileManager.default.contents(atPath: path), d.count > 44 else { return nil }
func rd16(_ o: Int) -> Int { Int(d[o]) | (Int(d[o+1]) << 8) }
func rd32(_ o: Int) -> Int { Int(d[o]) | (Int(d[o+1])<<8) | (Int(d[o+2])<<16) | (Int(d[o+3])<<24) }
var ch = 0, sr = 0, bits = 0
var o = 12
while o + 8 <= d.count {
let id = String(bytes: d[o..<o+4], encoding: .ascii) ?? ""
let sz = rd32(o+4)
if id == "fmt " && o + 24 <= d.count { ch = rd16(o+10); sr = rd32(o+12); bits = rd16(o+22) }
if id == "data" {
guard bits == 16, ch > 0 else { return nil }
var samples = [Double](); let start = o + 8
let end = min(start + sz, d.count - 1)
var i = start
while i + 1 < end {
var v = Int(rd16(i)); if v >= 32768 { v -= 65536 }
samples.append(Double(v) / 32768.0)
i += 2 * ch // take channel 0 if stereo
}
return (samples, sr, ch)
}
o += 8 + sz + (sz & 1)
}
return nil
}
// Audio descriptor = compact sound/voice signature (energy, ZCR, centroid, F0).
// The seed for phonetic geometry + the hear->imitate voice-signature.
func computeAudio(_ path: String) -> (content: String, vector: [Double], extra: [String: Any]) {
guard let (s, sr, ch) = readWavSamples(path), !s.isEmpty else { die("cannot read PCM from \(path)") }
let n = s.count
let seconds = Double(n) / Double(sr)
var sumsq = 0.0, peak = 0.0, zc = 0.0
for i in 0..<n {
sumsq += s[i]*s[i]; peak = max(peak, abs(s[i]))
if i > 0 && (s[i-1] < 0) != (s[i] < 0) { zc += 1 }
}
let rms = (sumsq / Double(n)).squareRoot()
let zcr = zc / Double(n) * Double(sr) // ~2*dominant freq for tonal
// Spectral centroid via a coarse DFT on a mid window (own-core).
let W = min(2048, n); let off = max(0, (n - W)/2)
var num = 0.0, den = 0.0
let bins = 64
for k in 1..<bins {
let f = Double(k) * Double(sr) / Double(2*bins)
var re = 0.0, im = 0.0
for j in 0..<W {
let ang = -2*Double.pi*Double(k)*Double(j)/Double(2*bins)
re += s[off+j]*cos(ang); im += s[off+j]*sin(ang)
}
let mag = (re*re+im*im).squareRoot()
num += f*mag; den += mag
}
let centroid = den > 0 ? num/den : 0
// F0 via autocorrelation (voice pitch) over plausible speech range 70-400 Hz.
var bestLag = 0; var bestCorr = 0.0
let lagMin = sr/400, lagMax = min(sr/70, n-1)
if lagMax > lagMin {
for lag in lagMin...lagMax {
var c = 0.0
var i = 0; while i + lag < min(n, off+W) { c += s[off+i]*s[off+i+lag]; i += 1 }
if c > bestCorr { bestCorr = c; bestLag = lag }
}
}
let f0 = bestLag > 0 ? Double(sr)/Double(bestLag) : 0
let vector: [Double] = [seconds, Double(sr), Double(ch), rms, peak, zcr, centroid, f0]
let content = String(format:
"Heard sound (afferent, mic): %.2fs at %dHz. RMS energy %.3f, peak %.3f, " +
"zero-crossing rate %.0fHz, spectral centroid %.0fHz, estimated voice pitch F0 %.0fHz. " +
"Compact voice/sound signature (%d numbers) — phonetic geometry + hear-to-imitate seed.",
seconds, sr, rms, peak, zcr, centroid, f0, vector.count)
disclose("FEAT(audio): \(vector.count)-number signature vs \(n) raw samples (~\(n/max(vector.count,1))x compression).")
return (content, vector, ["f0_hz": f0, "centroid_hz": centroid, "zcr_hz": zcr,
"rms": rms, "seconds": seconds, "raw_samples": n])
}
func featAudio(_ path: String) {
let r = computeAudio(path)
var out: [String: Any] = ["ok": true, "op": "feat-audio", "file": path,
"vector": r.vector, "content": r.content,
"ingest": ["node_type": "Observation", "tier": "Episodic", "content": r.content]]
r.extra.forEach { out[$0] = $1 }
emit(out)
}
// Image descriptor = compact scene-geometry (dims, brightness, region grid).
func computeImage(_ path: String) -> (content: String, vector: [Double], extra: [String: Any]) {
guard let src = CGImageSourceCreateWithURL(URL(fileURLWithPath: path) as CFURL, nil),
let img = CGImageSourceCreateImageAtIndex(src, 0, nil) else { die("cannot decode image \(path)") }
let w = img.width, h = img.height
let cs = CGColorSpaceCreateDeviceRGB()
let bpr = w * 4
var buf = [UInt8](repeating: 0, count: h * bpr)
guard let ctx = CGContext(data: &buf, width: w, height: h, bitsPerComponent: 8,
bytesPerRow: bpr, space: cs,
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) else {
die("cannot rasterize image")
}
ctx.draw(img, in: CGRect(x: 0, y: 0, width: w, height: h))
// 3x3 region average luminance + overall average color.
var rAvg = 0.0, gAvg = 0.0, bAvg = 0.0
var grid = [Double](repeating: 0, count: 9); var gridN = [Int](repeating: 0, count: 9)
let step = max(1, (w*h)/40000) // subsample for speed
var count = 0; var idx = 0
while idx < w*h {
let x = idx % w, y = idx / w
let p = y*bpr + x*4
let r = Double(buf[p]), g = Double(buf[p+1]), b = Double(buf[p+2])
rAvg += r; gAvg += g; bAvg += b; count += 1
let cell = (min(2, y*3/h))*3 + min(2, x*3/w)
grid[cell] += 0.299*r + 0.587*g + 0.114*b; gridN[cell] += 1
idx += step
}
if count == 0 { die("no pixels sampled") }
rAvg /= Double(count); gAvg /= Double(count); bAvg /= Double(count)
for i in 0..<9 { grid[i] = gridN[i] > 0 ? grid[i]/Double(gridN[i]) : 0 }
let bright = (0.299*rAvg + 0.587*gAvg + 0.114*bAvg)/255.0
let vector = [Double(w), Double(h), rAvg/255, gAvg/255, bAvg/255, bright] + grid.map { $0/255 }
let content = String(format:
"Saw scene (afferent, camera): %dx%d frame. Mean color rgb(%.0f,%.0f,%.0f), " +
"brightness %.2f. 3x3 luminance grid [%.0f %.0f %.0f / %.0f %.0f %.0f / %.0f %.0f %.0f]. " +
"Compact scene-geometry (%d numbers) vs %d pixel-channels.",
w, h, rAvg, gAvg, bAvg, bright,
grid[0],grid[1],grid[2],grid[3],grid[4],grid[5],grid[6],grid[7],grid[8],
vector.count, w*h*3)
disclose("FEAT(image): \(vector.count)-number scene-geometry vs \(w*h*3) pixel-channels (~\(w*h*3/max(vector.count,1))x).")
return (content, vector, ["width": w, "height": h, "brightness": bright])
}
func featImage(_ path: String) {
let r = computeImage(path)
var out: [String: Any] = ["ok": true, "op": "feat-image", "file": path,
"vector": r.vector, "content": r.content,
"ingest": ["node_type": "Observation", "tier": "Episodic", "content": r.content]]
r.extra.forEach { out[$0] = $1 }
emit(out)
}
// The afferent WIRE hand a capture's descriptor to the ingest organ (engram),
// where it becomes an embedded node = GEOMETRY. Own-core URLSession POST.
// LOCAL only: point at a local engram; raw stream never leaves the machine.
func postNode(engramURL: String, content: String, label: String, tags: [String]) -> String? {
guard let url = URL(string: engramURL + "/api/nodes") else { return nil }
let body: [String: Any] = ["content": content, "node_type": "Observation",
"label": label, "tier": "Episodic",
"salience": 0.7, "importance": 0.6, "confidence": 0.9,
"tags": tags]
var req = URLRequest(url: url); req.httpMethod = "POST"
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
req.httpBody = try? JSONSerialization.data(withJSONObject: body)
let sem = DispatchSemaphore(value: 0); var out: String?
URLSession.shared.dataTask(with: req) { data, _, _ in
if let d = data { out = String(data: d, encoding: .utf8) }
sem.signal()
}.resume()
_ = sem.wait(timeout: .now() + 15)
return out
}
func ingest(_ path: String, kind: String, engramURL: String) {
let r = kind == "audio" ? computeAudio(path) : computeImage(path)
let label = kind == "audio" ? "heard:mic" : "saw:camera"
disclose("INGEST: handing \(kind) descriptor to the ingest organ at \(engramURL) (LOCAL) -> geometry.")
guard let resp = postNode(engramURL: engramURL, content: r.content, label: label,
tags: ["peripheral", kind == "audio" ? "afferent-mic" : "afferent-camera"]) else {
die("ingest POST failed (no local engram at \(engramURL)?)")
}
// pull the node id out of the response (own-core, tolerant)
var nodeId = ""
if let d = resp.data(using: .utf8),
let o = try? JSONSerialization.jsonObject(with: d) as? [String: Any] {
nodeId = (o["id"] as? String) ?? (o["node_id"] as? String) ?? ""
}
disclose("INGEST: landed as node \(nodeId.isEmpty ? "(see response)" : nodeId) — the capture is now geometry in the engram.")
emit(["ok": !nodeId.isEmpty, "op": "ingest-\(kind)", "file": path,
"node_id": nodeId, "engram_response": resp, "content": r.content,
"vector": r.vector])
}
// ============================================================================
// VOICE BY IMITATION hear a voice, grab its compact SIGNATURE (pitch +
// formants F1-F5 via LPC), and speak back in that voice by source-filter
// resynthesis. Own-core DSP (physics), no training, no stolen voice. The
// afferent twin of the music instrument-signature: a voice = a few dozen
// numbers, not a corpus.
// ============================================================================
func hamming(_ x: [Double]) -> [Double] {
let n = x.count; if n < 2 { return x }
return (0..<n).map { x[$0] * (0.54 - 0.46*cos(2*Double.pi*Double($0)/Double(n-1))) }
}
func autocorr(_ x: [Double], _ p: Int) -> [Double] {
var r = [Double](repeating: 0, count: p+1)
for lag in 0...p { var s = 0.0; var i = lag; while i < x.count { s += x[i]*x[i-lag]; i += 1 }; r[lag] = s }
return r
}
// Levinson-Durbin -> LPC coeffs a[0..p] (A(z)=1+sum a[k]z^-k) and residual energy.
func levinson(_ r: [Double], _ p: Int) -> (a: [Double], err: Double) {
var a = [Double](repeating: 0, count: p+1); a[0] = 1
var err = r[0]
if err <= 0 { return (a, 0) }
for i in 1...p {
var acc = r[i]
if i > 1 { for j in 1..<i { acc += a[j]*r[i-j] } }
let k = -acc/err
var na = a; na[i] = k
if i > 1 { for j in 1..<i { na[j] = a[j] + k*a[i-j] } }
a = na; err *= (1 - k*k)
if err <= 0 { break }
}
return (a, err)
}
// Formant peaks from the LPC all-pole spectral envelope.
func formants(_ a: [Double], sr: Int) -> [(f: Double, bw: Double)] {
let p = a.count - 1
let steps = 512
var mag = [Double](repeating: 0, count: steps)
for s in 0..<steps {
let w = Double.pi * Double(s) / Double(steps) // 0..pi -> 0..sr/2
var re = 0.0, im = 0.0
for k in 0...p { re += a[k]*cos(w*Double(k)); im -= a[k]*sin(w*Double(k)) }
mag[s] = 1.0 / max((re*re+im*im).squareRoot(), 1e-9)
}
var peaks: [(f: Double, bw: Double)] = []
for s in 1..<(steps-1) where mag[s] > mag[s-1] && mag[s] >= mag[s+1] {
let f = Double(s) * Double(sr) / 2 / Double(steps)
if f > 150 && f < 5200 {
// crude bandwidth: width where magnitude falls to peak/sqrt(2)
let thr = mag[s]/1.4142
var lo = s; while lo > 0 && mag[lo] > thr { lo -= 1 }
var hi = s; while hi < steps-1 && mag[hi] > thr { hi += 1 }
let bw = Double(hi-lo) * Double(sr) / 2 / Double(steps)
peaks.append((f, bw))
}
}
return Array(peaks.prefix(5))
}
func pitchOf(_ frame: [Double], sr: Int) -> Double {
let n = frame.count
let lagMin = sr/400, lagMax = min(sr/70, n-1)
if lagMax <= lagMin { return 0 }
var r0 = 0.0; for v in frame { r0 += v*v }
if r0 < 1e-5 { return 0 }
var bestLag = 0; var best = 0.0
for lag in lagMin...lagMax { var c = 0.0; var i = lag; while i < n { c += frame[i]*frame[i-lag]; i += 1 }; if c > best { best = c; bestLag = lag } }
return (best / r0 > 0.30 && bestLag > 0) ? Double(sr)/Double(bestLag) : 0 // voiced?
}
let LPC_ORDER = 16
let FRAME = 400 // 25ms @16k
let HOP = 160 // 10ms
// Extract Will's voice-signature: averaged F0 + formants over voiced frames.
func voiceprint(_ path: String) -> (f0: Double, f0lo: Double, f0hi: Double, formants: [(Double,Double)], content: String) {
guard let (x, sr, _) = readWavSamples(path), x.count > FRAME else { die("cannot read speech from \(path)") }
var f0s: [Double] = []
var fbank: [[Double]] = [[],[],[],[],[]]
var bbank: [[Double]] = [[],[],[],[],[]]
var pos = 0
while pos + FRAME <= x.count {
let raw = Array(x[pos..<pos+FRAME])
let f0 = pitchOf(raw, sr: sr)
if f0 > 0 { // voiced frame only
f0s.append(f0)
let r = autocorr(hamming(raw), LPC_ORDER)
if r[0] > 1e-6 {
let (a, _) = levinson(r, LPC_ORDER)
let fs = formants(a, sr: sr)
for (i, fm) in fs.enumerated() where i < 5 { fbank[i].append(fm.f); bbank[i].append(fm.bw) }
}
}
pos += HOP
}
func med(_ v: [Double]) -> Double { v.isEmpty ? 0 : v.sorted()[v.count/2] }
let f0med = med(f0s)
let f0lo = f0s.isEmpty ? 0 : f0s.sorted().first!
let f0hi = f0s.isEmpty ? 0 : f0s.sorted().last!
var forms: [(Double,Double)] = []
for i in 0..<5 where !fbank[i].isEmpty { forms.append((med(fbank[i]), med(bbank[i]))) }
let fstr = forms.map { String(format:"%.0f", $0.0) }.joined(separator: "/")
let content = String(format:
"Voice-signature (afferent, heard a voice): pitch F0 %.0fHz (range %.0f-%.0fHz), " +
"formants F1-F5 = %@ Hz. Compact voiceprint (%d numbers) — grabbed by ear for imitation, not trained.",
f0med, f0lo, f0hi, fstr, 1 + forms.count*2)
return (f0med, f0lo, f0hi, forms, content)
}
// IMITATE: LPC analysis-resynthesis. Reconstruct the heard voice from its
// per-frame filter model + pitch the voice rebuilt from its signature.
func imitate(inPath: String, outPath: String) {
guard let (x, sr, _) = readWavSamples(inPath), x.count > FRAME else { die("cannot read speech from \(inPath)") }
var out = [Double](repeating: 0, count: x.count)
var state = [Double](repeating: 0, count: LPC_ORDER) // past outputs
var phase = 0.0
var lastF0 = 0.0
var pos = 0
while pos + FRAME <= x.count {
let raw = Array(x[pos..<pos+FRAME])
let r = autocorr(hamming(raw), LPC_ORDER)
let f0 = pitchOf(raw, sr: sr)
if r[0] < 1e-7 { pos += HOP; continue }
let (a, err) = levinson(r, LPC_ORDER)
let gain = max(err, 0).squareRoot()
let useF0 = f0 > 0 ? f0 : (lastF0 > 0 ? lastF0 : 0)
lastF0 = f0
for i in 0..<HOP {
let idx = pos + i; if idx >= x.count { break }
var e = 0.0
if useF0 > 0 { // voiced: glottal impulse train
phase += useF0/Double(sr)
if phase >= 1.0 { phase -= 1.0; e = sqrt(Double(sr)/useF0) } // energy-normalized impulse
} else { // unvoiced: noise
e = Double.random(in: -1...1)
}
var y = gain * e
for k in 1...LPC_ORDER { y -= a[k]*state[k-1] }
for k in stride(from: LPC_ORDER-1, through: 1, by: -1) { state[k] = state[k-1] }
state[0] = y
out[idx] = y
}
pos += HOP
}
// normalize to peak 0.9
let peak = out.map { abs($0) }.max() ?? 1
let scale = peak > 1e-9 ? 0.9/peak : 1
let samples = out.map { Int16(max(-32767, min(32767, $0*scale*32767))) }
writeWav(URL(fileURLWithPath: outPath), samples: samples, sampleRate: sr)
let vp = voiceprint(inPath)
disclose(String(format: "IMITATE: rebuilt the voice from its signature (F0 %.0fHz, formants %@) -> %@",
vp.f0, vp.formants.map{String(format:"%.0f",$0.0)}.joined(separator:"/"), outPath))
emit(["ok": true, "op": "imitate", "in": inPath, "out": outPath,
"f0_hz": vp.f0, "f0_range": [vp.f0lo, vp.f0hi],
"formants_hz": vp.formants.map { $0.0 },
"method": "LPC analysis-resynthesis (own-core, no training, no stolen voice)"])
}
// ============================================================================
// CONVERSE (full-duplex) the interruptible conversational loop.
// The utterance is a persistent, ordered meaning-plan of SEGMENTS, each with
// a salience. The speaker plays them; the mic listens concurrently. On user
// speech: pause INSTANTLY, classify (backchannel vs barge-in), then DECIDE
// yield-or-hold from the salience of the current segment + the social read.
// Yielded utterances persist their remaining plan so Neuron can RESUME.
// ============================================================================
struct Segment { let file: String; let salience: Double; let text: String }
enum Decision { case backchannelContinue, hold, yield }
// The yield-or-hold DECISION grounded, contextual. Not a fixed rule.
func decide(currentSalience: Double, progress: Double,
interrupterAuthority: Double, isBackchannel: Bool) -> Decision {
if isBackchannel { return .backchannelContinue } // "mm-hm" => keep going
// Holding the floor is justified when what I'm saying matters AND I'm nearly
// done (cheap to finish) AND the interrupter isn't high-priority.
let holdScore = currentSalience * 0.6 + progress * 0.4
if holdScore >= 0.6 && interrupterAuthority < 0.8 { return .hold }
return .yield // default: be polite, let them in
}
final class Conversation {
let engine = AVAudioEngine()
let player = AVAudioPlayerNode()
var micLive = false
// VAD state (shared with the audio tap thread)
let lock = NSLock()
var micRMS: Float = 0
var speechFrames = 0 // consecutive above-threshold frames
var onsetHandled = false
let resumePath: String
init(resumePath: String) { self.resumePath = resumePath }
// Try to bring the mic up as a live VAD. Returns false if unavailable/denied.
func startMic() -> Bool {
let status = AVCaptureDevice.authorizationStatus(for: .audio)
if Consent.load()["mic"] != true || status != .authorized {
disclose("CONVERSE: live mic not available (consent/OS) — using injected barge events for the proof.")
return false
}
let input = engine.inputNode
// Acoustic echo cancellation: the OS voice-processing unit subtracts our
// own speaker output from the mic so Neuron does NOT hear itself and
// barge in on its own voice. This is what makes real-room barge-in work.
do { try input.setVoiceProcessingEnabled(true); disclose("CONVERSE: AEC on (echo-cancelled mic — won't self-interrupt).") }
catch { disclose("CONVERSE: AEC unavailable (\(error)); raising VAD floor instead.") }
let fmt = input.inputFormat(forBus: 0)
if fmt.sampleRate == 0 { return false }
input.installTap(onBus: 0, bufferSize: 1024, format: fmt) { [weak self] buf, _ in
guard let self = self, let ch = buf.floatChannelData?[0] else { return }
let n = Int(buf.frameLength)
var sum: Float = 0
for i in 0..<n { let v = ch[i]; sum += v*v }
let rms = n > 0 ? (sum / Float(n)).squareRoot() : 0
self.lock.lock(); self.micRMS = rms; self.lock.unlock()
}
micLive = true
disclose("CONVERSE: full-duplex — mic listening WHILE speaking (barge-in armed).")
return true
}
func run(_ segs: [Segment], interrupterAuthority: Double,
injectBargeAt: Double?, injectKind: String, startIndex: Int, liveMic: Bool) {
engine.attach(player)
let firstFmt = (try? AVAudioFile(forReading: URL(fileURLWithPath: segs[startIndex].file)))?.processingFormat
?? AVAudioFormat(standardFormatWithSampleRate: 16000, channels: 1)!
engine.connect(player, to: engine.mainMixerNode, format: firstFmt)
if liveMic { _ = startMic() }
else { disclose("CONVERSE: deterministic mode (live mic off) — barge events \(injectBargeAt != nil ? "injected" : "none").") }
do { try engine.start() } catch { die("audio engine failed to start: \(error)") }
player.play()
let injectDeadline = injectBargeAt.map { Date().addingTimeInterval($0) }
var injectedFired = false
var idx = startIndex
segmentLoop: while idx < segs.count {
let seg = segs[idx]
guard let f = try? AVAudioFile(forReading: URL(fileURLWithPath: seg.file)) else {
disclose("CONVERSE: missing segment '\(seg.file)', skipping."); idx += 1; continue
}
let dur = Double(f.length) / f.processingFormat.sampleRate
disclose(String(format: "CONVERSE: speaking segment %d/%d (salience %.2f) — \"%@\"",
idx+1, segs.count, seg.salience, seg.text))
emit(["op": "converse", "event": "speaking", "segment": idx,
"salience": seg.salience, "text": seg.text])
let done = DispatchSemaphore(value: 0)
// .dataPlayedBack: completion fires only after the audio has actually
// played OUT the DAC (not merely been consumed) so the tail is never
// clipped and playback always runs the FULL file length.
player.scheduleFile(f, at: nil, completionCallbackType: .dataPlayedBack) { _ in done.signal() }
player.play()
// Monitor this segment: poll VAD / injected event until it finishes.
let segStart = Date()
while done.wait(timeout: .now() + 0.02) == .timedOut {
let elapsed = Date().timeIntervalSince(segStart)
let progress = min(elapsed / max(dur, 0.001), 1.0)
// --- detect an onset (live mic OR injected) ---
var onset = false
if micLive {
lock.lock(); let rms = micRMS; lock.unlock()
if rms > 0.02 { speechFrames += 1 } else { speechFrames = 0 }
if speechFrames >= 3 && !onsetHandled { onset = true } // ~60ms of voice
}
if let dl = injectDeadline, !injectedFired, Date() >= dl, !onsetHandled { onset = true; injectedFired = true }
if onset {
onsetHandled = true
// (1) BARGE-IN: pause INSTANTLY, on the spot.
player.pause()
let tBarge = Date().timeIntervalSince(segStart)
disclose(String(format: "CONVERSE: << user speech at %.2fs into segment %d — PAUSED instantly >>", tBarge, idx+1))
emit(["op": "converse", "event": "barge_in", "segment": idx,
"at_seconds": tBarge, "progress": progress])
// (2) classify backchannel vs real barge-in
let isBackchannel = classifyBackchannel(injected: injectDeadline != nil,
kind: injectKind)
let d = decide(currentSalience: seg.salience, progress: progress,
interrupterAuthority: interrupterAuthority,
isBackchannel: isBackchannel)
switch d {
case .backchannelContinue:
disclose("CONVERSE: read as BACKCHANNEL (\"mm-hm\") — keep going, resume seamlessly.")
emit(["op": "converse", "event": "backchannel_continue", "segment": idx])
onsetHandled = false; speechFrames = 0
player.play() // seamless resume
case .hold:
disclose("CONVERSE: HOLD the floor — \"hang on, let me finish this thought.\" (high salience, nearly done)")
emit(["op": "converse", "event": "hold_floor", "segment": idx,
"salience": seg.salience, "progress": progress])
onsetHandled = false; speechFrames = 0
player.play() // finish the segment, THEN yield
// after this segment completes we yield the remainder
_ = done.wait(timeout: .now() + dur + 1.0)
persistResume(segs: segs, from: idx + 1, reason: "held-then-yield")
finish(); return
case .yield:
disclose("CONVERSE: YIELD — stop, let them in. Remembering where I was (resumable).")
player.stop()
persistResume(segs: segs, from: idx, reason: "yield")
emit(["op": "converse", "event": "yield", "interrupted_segment": idx,
"resume_from": idx])
finish(); return
}
}
}
emit(["op": "converse", "event": "segment_done", "segment": idx])
idx += 1
}
// whole utterance completed uninterrupted
clearResume()
disclose("CONVERSE: utterance complete (uninterrupted).")
emit(["ok": true, "op": "converse", "event": "complete", "segments": segs.count])
finish()
}
// A backchannel is brief/low. Injected kind lets us prove both paths headlessly;
// the live path would measure post-onset duration & energy.
func classifyBackchannel(injected: Bool, kind: String) -> Bool {
if injected { return kind == "backchannel" }
// live: sample ~250ms after onset; if speech already died away, it was a backchannel
Thread.sleep(forTimeInterval: 0.25)
lock.lock(); let rms = micRMS; lock.unlock()
return rms < 0.015
}
func persistResume(segs: [Segment], from: Int, reason: String) {
let remaining = segs[from...].map { ["file": $0.file, "salience": $0.salience, "text": $0.text] as [String: Any] }
let state: [String: Any] = ["resume_from": from, "reason": reason,
"remaining": remaining, "ts": Date().timeIntervalSince1970]
if let d = try? JSONSerialization.data(withJSONObject: state, options: [.prettyPrinted]) {
try? d.write(to: URL(fileURLWithPath: resumePath))
}
disclose("CONVERSE: meaning-plan persisted (\(remaining.count) segments remain) — Neuron can resume the thread.")
}
func clearResume() { try? FileManager.default.removeItem(atPath: resumePath) }
func finish() { player.stop(); if micLive { engine.inputNode.removeTap(onBus: 0) }; engine.stop() }
}
// ----------------------------------------------------------------------------
// CLI
// ----------------------------------------------------------------------------
func loadManifest(_ path: String) -> (segs: [Segment], utterance: String) {
guard let d = FileManager.default.contents(atPath: path),
let o = try? JSONSerialization.jsonObject(with: d) as? [String: Any],
let arr = o["segments"] as? [[String: Any]] else { die("bad manifest: \(path)") }
let segs = arr.map { Segment(file: $0["file"] as? String ?? "",
salience: ($0["salience"] as? NSNumber)?.doubleValue ?? 0.5,
text: $0["text"] as? String ?? "") }
return (segs, o["utterance"] as? String ?? "")
}
let args = CommandLine.arguments
guard args.count >= 2 else {
print("""
periph — Neuron peripheral I/O (own-core, local, consent-gated)
grant <camera|mic> grant a sensitive sense (Neuron-level consent)
revoke <camera|mic> revoke it
status show consent state
speak <file.wav> SPEAK ALOUD (efferent) via the speaker
tone <out.wav> [hz] [sec] own-core synth a test WAV (no deps)
listen <sec> <out.wav> MIC capture (afferent) 16k mono
see <out.jpg> CAMERA one frame (afferent)
feat-audio <file.wav> extract compact voice/sound signature (for ingest)
feat-image <file.jpg> extract compact scene-geometry (for ingest)
ingest-audio <file.wav> <engramURL> capture -> descriptor -> engram node (geometry)
ingest-image <file.jpg> <engramURL> capture -> descriptor -> engram node (geometry)
voiceprint <voice.wav> extract voice-signature (F0 + formants F1-F5)
imitate <voice.wav> <out.wav> speak back in that voice (LPC analysis-resynthesis)
hear-imitate <sec> <out.wav> MIC -> extract signature -> imitate -> SPEAK ALOUD
wav-info <file.wav> print WAV geometry
converse <manifest.json> [--authority F] [--barge-at S[:backchannel|:bargein]] [--resume]
full-duplex interruptible utterance
""")
exit(0)
}
switch args[1] {
case "grant":
guard args.count >= 3 else { die("grant needs a device") }
var g = Consent.load(); g[args[2]] = true; Consent.save(g)
disclose("granted '\(args[2])' — the user consents; raw stream stays local, never egresses.")
emit(["ok": true, "op": "grant", "device": args[2], "consent": g])
case "revoke":
guard args.count >= 3 else { die("revoke needs a device") }
var g = Consent.load(); g[args[2]] = false; Consent.save(g)
emit(["ok": true, "op": "revoke", "device": args[2], "consent": g])
case "status":
emit(["ok": true, "op": "status", "consent": Consent.load()])
case "speak":
guard args.count >= 3 else { die("speak needs a wav") }
speak(args[2])
case "tone":
guard args.count >= 3 else { die("tone needs an out path") }
let hz = args.count >= 4 ? Double(args[3]) ?? 220 : 220
let sec = args.count >= 5 ? Double(args[4]) ?? 1.0 : 1.0
let sr = 16000
var s = [Int16](); s.reserveCapacity(Int(Double(sr)*sec))
for i in 0..<Int(Double(sr)*sec) {
let t = Double(i)/Double(sr)
let env = min(1.0, min(t*20, (sec - t)*20)) // gentle attack/release
s.append(Int16(env * 0.3 * 32767 * sin(2*Double.pi*hz*t)))
}
writeWav(URL(fileURLWithPath: args[2]), samples: s, sampleRate: sr)
disclose("tone: wrote own-core \(sec)s @ \(hz)Hz WAV to \(args[2]).")
emit(["ok": true, "op": "tone", "file": args[2], "hz": hz, "seconds": sec])
case "listen":
guard args.count >= 4 else { die("listen needs <sec> <out.wav>") }
listen(seconds: Double(args[2]) ?? 3.0, out: args[3])
case "see":
guard args.count >= 3 else { die("see needs an out path") }
see(out: args[2])
case "feat-audio":
guard args.count >= 3 else { die("feat-audio needs a wav") }
featAudio(args[2])
case "feat-image":
guard args.count >= 3 else { die("feat-image needs an image") }
featImage(args[2])
case "ingest-audio":
guard args.count >= 4 else { die("ingest-audio needs <wav> <engramURL>") }
ingest(args[2], kind: "audio", engramURL: args[3])
case "ingest-image":
guard args.count >= 4 else { die("ingest-image needs <image> <engramURL>") }
ingest(args[2], kind: "image", engramURL: args[3])
case "voiceprint":
guard args.count >= 3 else { die("voiceprint needs a wav") }
let vp = voiceprint(args[2])
disclose("VOICEPRINT: \(vp.content)")
emit(["ok": true, "op": "voiceprint", "file": args[2], "f0_hz": vp.f0,
"f0_range": [vp.f0lo, vp.f0hi], "formants_hz": vp.formants.map { $0.0 },
"bandwidths_hz": vp.formants.map { $0.1 }, "content": vp.content,
"ingest": ["node_type": "Observation", "tier": "Episodic", "content": vp.content]])
case "imitate":
guard args.count >= 4 else { die("imitate needs <voice.wav> <out.wav>") }
imitate(inPath: args[2], outPath: args[3])
case "hear-imitate":
guard args.count >= 4 else { die("hear-imitate needs <sec> <out.wav>") }
let secs = Double(args[2]) ?? 4.0
let outp = args[3]
let capp = outp.replacingOccurrences(of: ".wav", with: "") + ".heard.wav"
disclose("HEAR-IMITATE: open the ear, listen \(secs)s, grab the voice, speak it back.")
listen(seconds: secs, out: capp) // afferent: hear the voice
imitate(inPath: capp, outPath: outp) // extract signature + resynthesize
speak(outp) // efferent: speak back ALOUD in that voice
case "wav-info":
guard args.count >= 3, let i = wavInfo(args[2]) else { die("wav-info needs a readable wav") }
disclose("WAV \(args[2]): \(i.sampleRate)Hz \(i.channels)ch \(i.bits)bit \(i.frames) frames")
emit(["ok": true, "op": "wav-info", "sample_rate": i.sampleRate, "channels": i.channels,
"bits": i.bits, "frames": i.frames,
"seconds": Double(i.frames)/Double(max(i.sampleRate,1))])
case "converse":
guard args.count >= 3 else { die("converse needs a manifest") }
let (segs, utter) = loadManifest(args[2])
var authority = 0.5
var bargeAt: Double? = nil
var bargeKind = "bargein"
var resume = false
var liveMic = false
var i = 3
while i < args.count {
switch args[i] {
case "--authority": if i+1 < args.count { authority = Double(args[i+1]) ?? 0.5; i += 1 }
case "--barge-at":
if i+1 < args.count {
let parts = args[i+1].split(separator: ":")
bargeAt = Double(parts[0]) ?? nil
if parts.count > 1 { bargeKind = String(parts[1]) }
i += 1
}
case "--resume": resume = true
case "--live-mic": liveMic = true
default: break
}
i += 1
}
let resumePath = (ProcessInfo.processInfo.environment["PERIPH_HOME"]
?? FileManager.default.currentDirectoryPath + "/peripheral") + "/.resume.json"
var startIndex = 0
var runSegs = segs
if resume, let d = FileManager.default.contents(atPath: resumePath),
let o = try? JSONSerialization.jsonObject(with: d) as? [String: Any],
let rem = o["remaining"] as? [[String: Any]] {
runSegs = rem.map { Segment(file: $0["file"] as? String ?? "",
salience: ($0["salience"] as? NSNumber)?.doubleValue ?? 0.5,
text: $0["text"] as? String ?? "") }
startIndex = 0
disclose("CONVERSE: resuming — \"as I was saying...\" (\(runSegs.count) segments left).")
emit(["op": "converse", "event": "resume", "remaining": runSegs.count])
}
if runSegs.isEmpty { die("no segments to speak") }
disclose("CONVERSE: utterance = \"\(utter)\" (\(runSegs.count) segments).")
let convo = Conversation(resumePath: resumePath)
convo.run(runSegs, interrupterAuthority: authority,
injectBargeAt: bargeAt, injectKind: bargeKind, startIndex: startIndex, liveMic: liveMic)
default:
die("unknown command: \(args[1])")
}
-120
View File
@@ -1,120 +0,0 @@
# sandbox — the Neuron STACK sandbox
**Work on a whole stack at once, not one repo at a time.** `sandbox` assembles every
constituent repo of a named stack into **one combined worktree workspace**, wired so
they build and run **together**, on an isolated clean base — then tears it all down
cleanly. The live soul/engram (`:7770` / `:8742`) are never touched.
It is the multi-repo sibling of [`nsbx`](./README.md): where `nsbx dev` stands up
**one** repo's worktree + an isolated engram, `sandbox` stands up **every** repo of a
stack as sibling git worktrees under a single workspace.
```bash
export PATH="$PWD:$PATH" # or symlink `sandbox` onto your PATH
sandbox neuron-stack tim # el + neuron soul + NeuronUI, assembled together
cd ~/Development/neuron-technologies/stack-worktrees/neuron-stack-tim
source .stack-env # EL_REPO + PATH now point at the SANDBOX el
./build.sh # engram compiles, soul compiles, UI present & buildable
sandbox down neuron-stack tim # remove every worktree; live untouched
```
## The two profiles
### `el-stack` — the whole EL kit
The compiler + language + framework + tooling are **all one repo** (`foundation/el`:
`lang/` = elc/elb + runtime, `engram/src/server.el`, `elp/` = NLG, `ui/` = the **el-ui
framework**, plus `ql`, `ide`, `epm`, `arbor`, `tools`). Its downstream SDK consumers
come along so a change to `elc` can be proven end-to-end across the kit.
| repo | required | role |
|------|----------|------|
| `foundation/el` | ✓ | elc + elb compiler, el_runtime, engram source, **el-ui framework**, elp/ql/ide/epm tooling |
| `engram-language` | | language-faculty reference POC (Python) — being ported into `el/elp` |
| `foundation/forge` | | downstream SDK consumer — `make build` |
| `foundation/dharma` | | downstream SDK consumer — CGI provenance registry |
`build.sh` proves it: `elc` compiles a real stack source and `cc` links it against the
runtime into a native binary (elc + runtime build together), and — if present — `forge`
builds against the freshly-assembled SDK.
### `neuron-stack` — the full product
Substrate + soul + UI. **Engram is not a separate repo** — its source lives inside
`foundation/el`.
| repo | required | role |
|------|----------|------|
| `foundation/el` | ✓ | substrate: elc + el_runtime + engram source + the `elp` NLG the soul imports |
| `neuron` | ✓ | the soul (`:7770`) + engram build; `soul.el` imports `../foundation/el/elp/src/elp.el` |
| `products/NeuronUI` | ✓ | the app/UI (Kotlin/Compose desktop client; bundles the soul binary) |
| `products/web` | | marketing site + interactive soul-demo |
`build.sh` proves it: **engram** builds (`elc engram/src/server.el``cc … el_runtime.c`
→ native binary), the **soul** compiles with its cross-repo `../foundation/el` import
resolving to the *sandbox* el, and the **UI** is present with its build entry.
## Why it works — mirrored-layout wiring
The repos reference each other by **relative sibling paths** (e.g. the soul imports
`../foundation/el/elp/src/elp.el`). So `sandbox` lays every worktree out at its **natural
relative path** inside the workspace:
```
stack-worktrees/neuron-stack-tim/
├── foundation/el/ ← worktree of foundation/el (the sandbox el)
├── neuron/ ← worktree of neuron
└── products/NeuronUI/ ← worktree of products/NeuronUI
```
From `neuron/`, `../foundation/el` resolves to `…/neuron-stack-tim/foundation/el` — the
**sandbox** copy, never the live tree. No symlinks, no path rewriting: the layout *is*
the wiring. `.stack-env` additionally pins `EL_REPO` and prepends the sandbox `elc`/`elb`
to `PATH`.
## Commands
| command | does |
|---------|------|
| `sandbox el-stack <name> [--minimal]` | assemble the EL kit (`--minimal` = required repos only) |
| `sandbox neuron-stack <name> [--minimal]` | assemble the full product |
| `sandbox build <profile> <name>` | run the workspace's combined `build.sh` |
| `sandbox status <profile> <name>` | per-repo head + clean/dirty |
| `sandbox list` | list assembled workspaces |
| `sandbox down <profile> <name> [--delete-branch]` | remove every worktree + drop the workspace (branch kept unless `--delete-branch`) |
Flags: `--minimal` (required repos only), `--branch B` (branch name; default
`sandbox/<profile>-<name>`), `--base REF` (fork point; default each repo's committed
HEAD).
## Rails (always)
- **Clean base** — worktrees fork off each repo's **committed HEAD**; the dirty state of
the live checkout is deliberately *not* carried in.
- **Persistent** — the workspace lives under `NSBX_STACK_ROOT` (default
`~/Development/neuron-technologies/stack-worktrees`), **never `/tmp`** (ablated on
compaction).
- **Never touches live** — `sandbox` only does `git worktree` + offline `cc`. It never
binds `:8742`/`:7770`, never `launchctl`, never `pkill`. Bringing up an **isolated
engram** is delegated, opt-in, to `nsbx` (which guards the live store and refuses the
live ports).
- **Idempotent & safe** — refuses to clobber an existing workspace; a failed assembly
rolls back its partial worktrees; teardown removes worktrees through their origin repo
and prunes.
- **Own-the-core** — pure bash + `git worktree`. No new dependencies.
## Env knobs
`NSBX_STACK_ROOT` (workspace root), `NEURON_DEV_ROOT` (the dir holding all the peer
repos, default `~/Development/neuron-technologies`).
## Isolated engram for `neuron-stack` (opt-in)
`sandbox` gets the code building together; to run the soul against an **isolated** engram
(never live), delegate to `nsbx` from inside the workspace:
```bash
source .stack-env
nsbx create $STACK_NAME --source "$EL_REPO" # clone live store onto a non-default port
nsbx up $STACK_NAME
nsbx status $STACK_NAME # prints the isolated engram URL
```
-505
View File
@@ -1,505 +0,0 @@
#!/usr/bin/env bash
# sandbox — the Neuron STACK sandbox: assemble a WHOLE stack of repos into ONE
# combined worktree workspace, wired so they build/run TOGETHER, on an isolated
# clean base — so an agent (or Will) can work on the full stack at once instead of
# one repo at a time.
#
# It is the multi-repo generalisation of `nsbx` (this same directory): where
# `nsbx dev` stands up ONE repo's worktree + an isolated engram, `sandbox` stands
# up EVERY constituent repo of a named stack as sibling git worktrees under a
# single workspace, mirroring their on-disk relative layout so the cross-repo
# `../foundation/el` imports resolve to the SANDBOX copy — never the live tree.
#
# sandbox el-stack <name> # elc compiler + EL language + framework + tooling (+ consumers)
# sandbox neuron-stack <name> # runtime/soul + engram + app/UI (the full product)
# sandbox list # list assembled stack workspaces
# sandbox status <profile> <name> # inspect one
# sandbox build <profile> <name> # run the workspace's combined build.sh
# sandbox down <profile> <name> # tear down: remove every worktree, drop the workspace
#
# RAILS (always):
# * worktrees fork off each repo's COMMITTED HEAD -> a clean, reproducible base
# (the dirty state of the live checkout is deliberately NOT carried in).
# * the workspace lives at a PERSISTENT path (never /tmp — ablated on compaction).
# * NEVER touches the live soul/engram (:7770 / :8742). It only creates git
# worktrees + a build script; bringing up an isolated engram is delegated,
# opt-in, to `nsbx` (which already guards the live store & ports).
# * idempotent & safe: refuses to clobber an existing workspace; teardown removes
# worktrees through their origin repo and prunes — branches are kept by default.
# * own-the-core: pure bash + git worktree. No new dependencies.
set -uo pipefail
# ---------------------------------------------------------------- constants ----
# Root that holds all the peer repos (neuron, foundation/el, products/*, ...).
DEV_ROOT="${NEURON_DEV_ROOT:-$HOME/Development/neuron-technologies}"
# Where assembled stack workspaces live (persistent; sibling to el-worktrees/).
STACK_ROOT="${NSBX_STACK_ROOT:-$DEV_ROOT/stack-worktrees}"
EL_REPO_REL="foundation/el"
LIVE_ENGRAM_PORT=8742 # live engram — sandbox must never bind it
LIVE_SOUL_PORT=7770 # live soul — sandbox must never bind it
# nsbx (single-repo isolated-engram tool) lives next to this script.
NSBX="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)/nsbx"
C_RED=$'\033[31m'; C_GRN=$'\033[32m'; C_YEL=$'\033[33m'; C_CYN=$'\033[36m'; C_DIM=$'\033[2m'; C_BLD=$'\033[1m'; C_0=$'\033[0m'
# ---------------------------------------------------------------- helpers ------
die(){ printf '%serror:%s %s\n' "$C_RED" "$C_0" "$*" >&2; exit 1; }
log(){ printf '%s==>%s %s\n' "$C_BLD" "$C_0" "$*" >&2; }
info(){ printf ' %s\n' "$*" >&2; }
ok(){ printf ' %s%s%s\n' "$C_GRN" "$*" "$C_0" >&2; }
warn(){ printf ' %s%s%s\n' "$C_YEL" "$*" "$C_0" >&2; }
need(){ command -v "$1" >/dev/null 2>&1 || die "missing dependency: $1"; }
# ---------------------------------------------------------------- profiles -----
# profile_repos <profile> : emit one line per constituent repo:
# <relpath-under-DEV_ROOT> | <required|optional> | <role>
# The relpath is preserved INSIDE the workspace, so all cross-repo `../foundation/el`
# references resolve to the sandbox copy automatically (mirrored-layout wiring).
profile_repos(){
case "$1" in
el-stack)
# The compiler+language+framework+tooling are all ONE repo (foundation/el).
# Its downstream SDK consumers (forge, dharma) + the language-faculty POC come
# along so a change to elc can be proven end-to-end across the kit.
cat <<'EOF'
foundation/el | required | elc + elb compiler, el_runtime, engram source, el-ui framework, elp/ql/ide/epm tooling
engram-language | optional | language-faculty reference POC (Python) — ported into el/elp
foundation/forge | optional | downstream SDK consumer — `make build` (imprint forge CLI)
foundation/dharma | optional | downstream SDK consumer — CGI provenance registry
EOF
;;
neuron-stack)
# The full product: substrate (el) + soul + UI. Engram is NOT a separate repo
# (its source lives in foundation/el/engram/src/server.el).
cat <<'EOF'
foundation/el | required | substrate: elc + el_runtime + engram source + elp NLG the soul imports
neuron | required | the soul (:7770) + engram build; soul.el imports ../foundation/el/elp/src/elp.el
products/NeuronUI | required | the app/UI (Kotlin/Compose desktop client; bundles the soul binary)
products/web | optional | marketing site + interactive soul-demo
EOF
;;
*) return 1;;
esac
}
is_profile(){ profile_repos "$1" >/dev/null 2>&1; }
ws_dir(){ printf '%s/%s-%s' "$STACK_ROOT" "$1" "$2"; } # <root>/<profile>-<name>
ws_branch(){ printf 'sandbox/%s-%s' "$1" "$2"; } # branch name used in each repo
manifest(){ printf '%s/.stack-manifest.json' "$1"; } # <ws>/.stack-manifest.json
# ================================================================ up ===========
cmd_up(){
local profile="$1"; shift
local name="" branch="" base_override="" minimal=0
[ $# -gt 0 ] && [ "${1#-}" = "$1" ] && { name="$1"; shift; } || die "usage: sandbox $profile <name> [--minimal] [--branch B] [--base REF]"
while [ $# -gt 0 ]; do case "$1" in
--minimal) minimal=1; shift;;
--branch) branch="$2"; shift 2;;
--base) base_override="$2"; shift 2;;
*) die "unknown flag: $1";;
esac; done
need git
is_profile "$profile" || die "unknown profile: $profile (try: el-stack | neuron-stack)"
local ws; ws="$(ws_dir "$profile" "$name")"
[ -n "$branch" ] || branch="$(ws_branch "$profile" "$name")"
# -------- pre-flight (fail before creating anything) --------
case "$ws" in /tmp/*|/private/tmp/*|/var/tmp/*)
die "refusing workspace under a temp dir ($ws) — temp dirs are ablated on compaction; set NSBX_STACK_ROOT to a persistent path";;
esac
[ -e "$ws" ] && die "workspace already exists: $ws (sandbox down $profile $name first)"
# resolve + validate every repo, and pick a base sha per repo, BEFORE touching disk
local -a rels roles bases origins wts
local line rel role_extra role req origin base wt
while IFS= read -r line; do
[ -z "${line// }" ] && continue
rel="$(printf '%s' "$line" | cut -d'|' -f1 | xargs)"
req="$(printf '%s' "$line" | cut -d'|' -f2 | xargs)"
role="$(printf '%s' "$line" | cut -d'|' -f3- | sed 's/^ *//')"
[ "$minimal" -eq 1 ] && [ "$req" = "optional" ] && continue
origin="$DEV_ROOT/$rel"
git -C "$origin" rev-parse --git-dir >/dev/null 2>&1 || {
[ "$req" = "required" ] && die "required repo missing or not a git repo: $origin"
warn "skipping optional repo (missing): $rel"; continue; }
if [ -n "$base_override" ]; then base="$base_override"; else base="$(git -C "$origin" rev-parse HEAD)"; fi
wt="$ws/$rel"
[ -e "$wt" ] && die "target worktree path already exists: $wt"
rels+=("$rel"); roles+=("$role"); origins+=("$origin"); bases+=("$base"); wts+=("$wt")
done < <(profile_repos "$profile")
[ "${#rels[@]}" -gt 0 ] || die "no repos resolved for profile $profile"
log "assembling '$profile' workspace '$name'"
info "workspace: $ws"
info "branch: $branch (created in each repo, off its committed HEAD)"
mkdir -p "$ws"
# -------- create a worktree per repo (mirrored relpath layout) --------
local i n="${#rels[@]}"
SB_DONE_WTS=(); SB_DONE_ORIGINS=()
for ((i=0; i<n; i++)); do
rel="${rels[$i]}"; origin="${origins[$i]}"; base="${bases[$i]}"; wt="${wts[$i]}"
mkdir -p "$(dirname "$wt")"
local gerr
if git -C "$origin" show-ref --verify --quiet "refs/heads/$branch"; then
gerr="$(git -C "$origin" worktree add "$wt" "$branch" 2>&1)" \
|| { _rollback; die "git worktree add failed for $rel (existing branch $branch):"$'\n'" $gerr"; }
else
gerr="$(git -C "$origin" worktree add -b "$branch" "$wt" "$base" 2>&1)" \
|| { _rollback; die "git worktree add -b $branch failed for $rel (base $base):"$'\n'" $gerr"; }
fi
SB_DONE_WTS+=("$wt"); SB_DONE_ORIGINS+=("$origin")
ok "worktree: $rel -> ${wt#$ws/} (branch $branch @ ${base:0:9})"
done
local el_ws="$ws/$EL_REPO_REL"
_write_env "$ws" "$profile" "$name" "$branch" "$el_ws"
_write_manifest "$ws" "$profile" "$name" "$branch"
_write_build "$ws" "$profile" "$el_ws"
_write_readme "$ws" "$profile" "$name" "$branch" "$el_ws"
# -------- summary --------
echo >&2
printf '%s STACK WORKSPACE READY — %s / %s%s\n' "$C_BLD" "$profile" "$name" "$C_0" >&2
printf ' %-11s %s\n' "workspace" "$ws" >&2
printf ' %-11s %s\n' "branch" "$branch (in each repo)" >&2
printf ' %-11s %s\n' "repos" "$n worktrees, mirrored layout" >&2
echo >&2
info "get in: cd $ws && source .stack-env"
info "build all: sandbox build $profile $name # (or: cd $ws && ./build.sh)"
if [ "$profile" = "neuron-stack" ]; then
info "isolated engram (opt-in, via nsbx):"
info " nsbx create $profile-$name --source $el_ws && nsbx up $profile-$name"
fi
info "tear down: sandbox down $profile $name # removes all worktrees; branches kept"
}
# _rollback : remove any worktrees already created this run (globals set by cmd_up)
SB_DONE_WTS=(); SB_DONE_ORIGINS=()
_rollback(){
local j
[ "${#SB_DONE_WTS[@]}" -gt 0 ] && warn "rolling back ${#SB_DONE_WTS[@]} partial worktree(s)"
for ((j=${#SB_DONE_WTS[@]}-1; j>=0; j--)); do
git -C "${SB_DONE_ORIGINS[$j]}" worktree remove --force "${SB_DONE_WTS[$j]}" 2>/dev/null || rm -rf "${SB_DONE_WTS[$j]}"
git -C "${SB_DONE_ORIGINS[$j]}" worktree prune 2>/dev/null || true
done
}
# ---------------------------------------------------------------- writers ------
_write_env(){
local ws="$1" profile="$2" name="$3" branch="$4" el_ws="$5"
local elc_dir="$el_ws/lang/dist/platform"
cat > "$ws/.stack-env" <<ENV
# stack env for '$profile/$name' — SOURCE this to work the whole stack together.
# Pins EL_REPO + PATH at the SANDBOX copy of foundation/el, so elc/elb/runtime and
# every cross-repo ../foundation/el import resolve INSIDE this workspace.
# The live mind (:$LIVE_ENGRAM_PORT engram / :$LIVE_SOUL_PORT soul) is deliberately NOT referenced.
export STACK_NAME="$profile-$name"
export STACK_PROFILE="$profile"
export STACK_ROOT_WS="$ws"
export EL_REPO="$el_ws"
export PATH="$elc_dir:\$PATH" # elc, elb (darwin/linux prebuilt) from the sandbox el
ENV
if [ "$profile" = "neuron-stack" ]; then
cat >> "$ws/.stack-env" <<ENV
export NEURON_REPO="$ws/neuron"
export NEURONUI_REPO="$ws/products/NeuronUI"
# engram/soul are NOT bound here — bring up an ISOLATED engram via nsbx when needed
# (nsbx guards the live store & refuses ports :$LIVE_ENGRAM_PORT/:$LIVE_SOUL_PORT):
# nsbx create $profile-$name --source \$EL_REPO && nsbx up $profile-$name
# nsbx status $profile-$name # prints the isolated engram URL to point the soul at
ENV
fi
# direnv convenience
[ -e "$ws/.envrc" ] || printf 'source_env .stack-env 2>/dev/null || source .stack-env\n' > "$ws/.envrc"
}
_write_manifest(){
local ws="$1" profile="$2" name="$3" branch="$4"
# emit worktree records from git's own worktree list, filtered to this workspace
python3 - "$ws" "$profile" "$name" "$branch" "$DEV_ROOT" <<'PY'
import json, os, subprocess, sys
ws, profile, name, branch, dev = sys.argv[1:6]
repos = []
for rel in sorted(os.listdir(ws)) if False else []:
pass
# discover worktrees by walking one level of relpaths we created
def git(root, *a):
return subprocess.run(["git","-C",root,*a], capture_output=True, text=True).stdout.strip()
for dirpath, dirnames, filenames in os.walk(ws):
if ".git" in filenames or ".git" in dirnames:
rel = os.path.relpath(dirpath, ws)
toplevel = git(dirpath, "rev-parse", "--show-toplevel")
common = git(dirpath, "rev-parse", "--git-common-dir")
origin = os.path.realpath(os.path.join(common, ".."))
head = git(dirpath, "rev-parse", "HEAD")
repos.append({"rel": rel, "worktree": dirpath, "origin": origin,
"branch": branch, "head": head})
dirnames[:] = [] # don't descend into a repo
repos.sort(key=lambda r: r["rel"])
json.dump({"profile": profile, "name": name, "branch": branch,
"workspace": ws, "repos": repos},
open(os.path.join(ws, ".stack-manifest.json"), "w"), indent=2)
PY
}
_write_build(){
local ws="$1" profile="$2" el_ws="$3"
cat > "$ws/build.sh" <<'BUILD'
#!/usr/bin/env bash
# build.sh — build the assembled stack together, in dependency order.
# Generated by `sandbox`. Run from the workspace root (it sources .stack-env).
set -uo pipefail
cd "$(dirname "$0")"; source ./.stack-env
say(){ printf '\033[1m==>\033[0m %s\n' "$*"; }
ok(){ printf ' \033[32m%s\033[0m\n' "$*"; }
bad(){ printf ' \033[31m%s\033[0m\n' "$*"; }
# locate an elc that runs on THIS machine (darwin-arm64 / linux-amd64), from the sandbox el
find_elc(){
local d="$EL_REPO/lang/dist/platform"
case "$(uname -s)-$(uname -m)" in
Darwin-arm64) echo "$d/elc-darwin-arm64";;
Linux-x86_64) echo "$d/elc-linux-amd64";;
*) echo "$d/elc";;
esac
}
ELC="$(find_elc)"; [ -x "$ELC" ] || ELC="$EL_REPO/lang/dist/platform/elc"
say "elc: $ELC"
[ -x "$ELC" ] && ok "$("$ELC" 2>&1 | head -1 || echo present)" || { bad "elc not executable"; exit 1; }
# canonical runtime C to link (CI-published release copy; ~8 copies exist in-tree)
RT="$EL_REPO/lang/releases/v1.0.0-20260501"
[ -f "$RT/el_runtime.c" ] || RT="$EL_REPO/lang/el-compiler/runtime"
[ -f "$RT/el_runtime.c" ] && ok "el_runtime: $RT/el_runtime.c" || bad "no el_runtime.c found under $EL_REPO/lang"
BUILD
if [ "$profile" = "el-stack" ]; then
cat >> "$ws/build.sh" <<'BUILD'
# ---- EL STACK: prove elc + the el stuff (incl. the el-ui framework) build together ----
say "el-ui framework present: $EL_REPO/ui"
[ -d "$EL_REPO/ui" ] && ok "framework dir present ($(ls "$EL_REPO/ui" | tr '\n' ' '))" || bad "no ui/ dir"
# end-to-end compiler proof: elc compiles a real, substantial stack source to C,
# then cc links it against the runtime -> a working native binary.
B="$(mktemp -d)"
say "elc end-to-end: compile engram/src/server.el and link a native binary"
if "$ELC" "$EL_REPO/engram/src/server.el" > "$B/x.c" 2>"$B/elc.err"; then
ok "elc -> C ($(wc -c <"$B/x.c" | tr -d ' ') bytes)"
if cc -std=c11 -O2 -w -I "$RT" -o "$B/x" "$B/x.c" "$RT/el_runtime.c" -lcurl -lpthread -lm 2>"$B/cc.err"; then
ok "cc link ok -> native binary $(ls -lh "$B/x" | awk '{print $5}') (elc + runtime build together)"
else
bad "cc link failed:"; grep -i 'error:' "$B/cc.err" | sort -u | head | sed 's/^/ /'
fi
else
bad "elc compile failed:"; sed 's/^/ /' "$B/elc.err" | head
fi
# optional downstream consumer: forge builds on the SDK (make build) — proves the
# freshly-assembled el SDK still compiles a real downstream repo.
FORGE="$STACK_ROOT_WS/foundation/forge"
if [ -f "$FORGE/Makefile" ]; then
say "downstream consumer: foundation/forge (make build)"
( cd "$FORGE" && EL_REPO="$EL_REPO" PATH="$EL_REPO/lang/dist/platform:$PATH" make build ) \
&& ok "forge built against the sandbox SDK" || bad "forge build failed (see above)"
fi
say "el-stack build complete"
BUILD
else
cat >> "$ws/build.sh" <<'BUILD'
# ---- 1) engram (elc engram/src/server.el -> cc engram.c el_runtime.c), from the sandbox el ----
say "build engram from $EL_REPO/engram/src/server.el"
B="$(mktemp -d)"
if "$ELC" "$EL_REPO/engram/src/server.el" > "$B/engram.c" 2>"$B/elc.err"; then
ok "elc -> engram.c ($(wc -c <"$B/engram.c" | tr -d ' ') bytes)"
if cc -std=c11 -O2 -w -I "$RT" -o "$B/engram" \
"$B/engram.c" "$RT/el_runtime.c" -lcurl -lpthread -lm 2>"$B/cc.err"; then
ok "engram binary built: $(ls -lh "$B/engram" | awk '{print $5}')"
else
bad "engram cc link failed:"; grep -i 'error:' "$B/cc.err" | sort -u | head | sed 's/^/ /'
fi
else
bad "engram elc transpile failed:"; sed 's/^/ /' "$B/elc.err"
fi
# ---- 2) soul (imports ../foundation/el/elp/src/elp.el — resolves to SANDBOX el) ----
say "soul present + cross-repo import resolves inside the sandbox"
[ -f "$NEURON_REPO/soul.el" ] && ok "neuron/soul.el present" || bad "no soul.el"
if [ -f "$EL_REPO/elp/src/elp.el" ]; then
ok "../foundation/el/elp/src/elp.el resolves -> $EL_REPO/elp/src/elp.el (sandbox copy)"
else
bad "elp NLG source missing under sandbox el"
fi
# soul is a heavy single-TU compile; prove elc parses it rather than a full link
if "$ELC" "$NEURON_REPO/soul.el" > "$B/soul.c" 2>"$B/soul.err"; then
ok "elc compiled soul.el -> $(wc -c <"$B/soul.c" | tr -d ' ') bytes of C (cross-repo imports resolved)"
else
bad "soul.el elc compile failed:"; sed 's/^/ /' "$B/soul.err" | head
fi
# ---- 3) UI (present + buildable; gradle/JDK21 is heavy so we don't run it here) ----
say "app/UI present + buildable"
if [ -f "$NEURONUI_REPO/build.sh" ] || [ -f "$NEURONUI_REPO/gradlew" ]; then
ok "NeuronUI build entry present (./build.sh / ./gradlew — needs JDK21; run: cd $NEURONUI_REPO && ./gradlew run)"
else
bad "no NeuronUI build entry"
fi
say "neuron-stack build complete (engram compiled, soul compiled, UI present & buildable)"
BUILD
fi
chmod +x "$ws/build.sh"
}
_write_readme(){
local ws="$1" profile="$2" name="$3" branch="$4" el_ws="$5"
cat > "$ws/README.md" <<MD
# $profile / $name — combined stack workspace
Assembled by \`sandbox\`. Every constituent repo is a **git worktree** on branch
\`$branch\`, forked off its origin repo's committed HEAD, laid out at its natural
relative path so cross-repo \`../foundation/el\` imports resolve **inside this
workspace** (the sandbox el), never the live tree.
## Get in
\`\`\`bash
cd $ws
source .stack-env # EL_REPO + PATH now point at the sandbox el
./build.sh # build the stack together (or: sandbox build $profile $name)
\`\`\`
## Layout
__STACK_LAYOUT__
## Isolation
- Worktrees only; the live soul/engram (:$LIVE_SOUL_PORT / :$LIVE_ENGRAM_PORT) are never touched.
- To run against an **isolated engram**, delegate to \`nsbx\` (guards the live store/ports):
\`\`\`bash
nsbx create $profile-$name --source \$EL_REPO && nsbx up $profile-$name
\`\`\`
## Tear down
\`\`\`bash
sandbox down $profile $name # remove every worktree; branch '$branch' kept
sandbox down $profile $name --delete-branch
\`\`\`
MD
# fill the layout list from the manifest without embedding backticks in the heredoc
python3 - "$ws" <<'PY'
import json, os, sys
ws = sys.argv[1]
d = json.load(open(os.path.join(ws, ".stack-manifest.json")))
lines = ["- `%s` <- worktree of %s" % (r["rel"], r["origin"]) for r in d["repos"]]
p = os.path.join(ws, "README.md")
txt = open(p).read().replace("__STACK_LAYOUT__", "\n".join(lines))
open(p, "w").write(txt)
PY
}
# ================================================================ down =========
cmd_down(){
local profile="$1" name="$2"; shift 2 || true
local del_branch=0
while [ $# -gt 0 ]; do case "$1" in
--delete-branch) del_branch=1; shift;;
*) die "unknown flag: $1";;
esac; done
need git
local ws; ws="$(ws_dir "$profile" "$name")"
[ -d "$ws" ] || die "no such workspace: $ws"
local mf; mf="$(manifest "$ws")"
[ -f "$mf" ] || die "no manifest in $ws (refusing to guess); remove it by hand if intended"
local branch; branch="$(python3 -c "import json;print(json.load(open('$mf'))['branch'])")"
log "tearing down '$profile/$name' ($ws)"
# remove each worktree through its origin repo
python3 -c "import json;[print(r['origin']+'\t'+r['worktree']) for r in json.load(open('$mf'))['repos']]" \
| while IFS=$'\t' read -r origin wt; do
if [ -d "$wt" ]; then
git -C "$origin" worktree remove --force "$wt" 2>/dev/null || rm -rf "$wt"
git -C "$origin" worktree prune 2>/dev/null || true
ok "removed worktree: ${wt#$ws/}"
fi
if [ "$del_branch" -eq 1 ]; then
git -C "$origin" branch -D "$branch" 2>/dev/null && ok "deleted branch $branch in ${origin#$DEV_ROOT/}" || true
fi
done
# drop the (now worktree-free) workspace tree
rm -rf "$ws"
ok "workspace removed: $ws"
[ "$del_branch" -eq 1 ] || info "branch '$branch' kept in each repo (use --delete-branch to drop)"
ok "down '$profile/$name' complete (live untouched)"
}
# ================================================================ build ========
cmd_build(){
local profile="$1" name="$2"; local ws; ws="$(ws_dir "$profile" "$name")"
[ -x "$ws/build.sh" ] || die "no build.sh in $ws (is it assembled? sandbox $profile $name)"
exec "$ws/build.sh"
}
# ================================================================ list/status ==
cmd_list(){
[ -d "$STACK_ROOT" ] || { info "no stack workspaces (root $STACK_ROOT absent)"; return 0; }
local mf found=0
for mf in "$STACK_ROOT"/*/.stack-manifest.json; do
[ -f "$mf" ] || continue; found=1
python3 -c "import json;d=json.load(open('$mf'));print(' %-22s %-8s %2d repos branch=%s'%(d['profile']+'/'+d['name'],'',len(d['repos']),d['branch']))" 2>/dev/null
done
[ "$found" -eq 1 ] || info "no assembled stack workspaces under $STACK_ROOT"
}
cmd_status(){
local profile="$1" name="$2"; local ws; ws="$(ws_dir "$profile" "$name")"
local mf; mf="$(manifest "$ws")"; [ -f "$mf" ] || die "no such workspace: $ws"
log "stack '$profile/$name'"; info "workspace: $ws"
python3 - "$mf" <<'PY'
import json,sys,subprocess
d=json.load(open(sys.argv[1]))
print(f" branch: {d['branch']}")
for r in d['repos']:
st=subprocess.run(["git","-C",r["worktree"],"status","--porcelain"],capture_output=True,text=True).stdout
n=len([l for l in st.splitlines() if l.strip()])
print(f" {r['rel']:<20} {r['head'][:9]} {'clean' if n==0 else str(n)+' changed'}")
PY
}
# ================================================================ usage/main ===
usage(){ cat >&2 <<EOF
${C_BLD}sandbox${C_0} — assemble a WHOLE Neuron stack into one combined worktree workspace,
wired to build together on an isolated clean base. Multi-repo sibling of ${C_BLD}nsbx${C_0}.
${C_CYN}sandbox el-stack <name>${C_0} [--minimal] elc + EL language + el-ui framework + tooling (+ SDK consumers)
${C_CYN}sandbox neuron-stack <name>${C_0} [--minimal] runtime/soul + engram + app/UI (the full product)
${C_CYN}sandbox build <profile> <name>${C_0} build the assembled stack together (runs its build.sh)
${C_CYN}sandbox status <profile> <name>${C_0} inspect one workspace
${C_CYN}sandbox list${C_0} list assembled workspaces
${C_CYN}sandbox down <profile> <name>${C_0} [--delete-branch] tear down (remove worktrees; branch kept)
Flags: --minimal only the required repos --branch B branch name --base REF fork point
Env: NSBX_STACK_ROOT (workspace root, default \$DEV_ROOT/stack-worktrees) NEURON_DEV_ROOT
Each constituent repo becomes a git worktree at its natural relpath inside the
workspace, so cross-repo ../foundation/el imports resolve to the SANDBOX el. The
live soul/engram (:$LIVE_SOUL_PORT / :$LIVE_ENGRAM_PORT) are never touched; isolated-engram
bring-up is delegated to nsbx.
EOF
}
main(){
local cmd="${1:-}"; shift || true
case "$cmd" in
el-stack|neuron-stack) cmd_up "$cmd" "$@";;
up) [ $# -ge 1 ] || die "usage: sandbox up <profile> <name>"; local p="$1"; shift; cmd_up "$p" "$@";;
down) [ $# -ge 2 ] || die "usage: sandbox down <profile> <name>"; cmd_down "$@";;
build) [ $# -ge 2 ] || die "usage: sandbox build <profile> <name>"; cmd_build "$@";;
status) [ $# -ge 2 ] || die "usage: sandbox status <profile> <name>"; cmd_status "$@";;
list|ls) cmd_list "$@";;
""|-h|--help|help) usage;;
*) die "unknown command: $cmd (try: sandbox help)";;
esac
}
main "$@"