fix: wire El imports, fix runtime API calls, build passes
Forge Release / build-and-release (push) Failing after 3s

- forge.el: add import statements for schema/probe/compiler/install
- All files: arg(n)/arg_count() → get(args(),n)/len(args())
- compiler.el: llm_call_system (utility cap violation) → http_post_with_headers
- install.el: http_post_auth_json → http_post_json (_auth already in body)
- Makefile: local dev build using installed El SDK
- .gitea/workflows/forge-release.yaml: CI build on push + el-sdk-updated dispatch
- dist/forge: builds and runs
This commit is contained in:
Will Anderson
2026-05-02 17:45:51 -05:00
parent f9beb50ae1
commit c24197516f
6 changed files with 180 additions and 52 deletions
+125
View File
@@ -0,0 +1,125 @@
name: Forge Release
on:
push:
branches:
- main
repository_dispatch:
types:
- el-sdk-updated
jobs:
build-and-release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install build dependencies
run: |
apt-get update -qq
apt-get install -y gcc libcurl4-openssl-dev
# Install El SDK from the latest release
- name: Install El SDK
env:
RELEASE_BASE: https://git.neuralplatform.ai/neuron-technologies/el/releases/download/latest
run: |
mkdir -p /usr/local/bin /usr/local/lib/el
echo "Downloading elc..."
curl -fsSL "${RELEASE_BASE}/elc" -o /usr/local/bin/elc
chmod +x /usr/local/bin/elc
echo "Downloading el_runtime.c..."
curl -fsSL "${RELEASE_BASE}/el_runtime.c" -o /usr/local/lib/el/el_runtime.c
echo "Downloading el_runtime.h..."
curl -fsSL "${RELEASE_BASE}/el_runtime.h" -o /usr/local/lib/el/el_runtime.h
echo "El SDK installed:"
elc --version || true
# Compile forge.el → dist/forge.c
- name: Compile forge.el
run: |
mkdir -p dist
elc src/forge.el > dist/forge.c
echo "Compiled src/forge.el → dist/forge.c"
# Link to produce the forge binary
- name: Link forge binary
run: |
cc -std=c11 -O2 \
-I /usr/local/lib/el \
-o dist/forge \
dist/forge.c \
/usr/local/lib/el/el_runtime.c \
-lcurl -lpthread
echo "Linked dist/forge"
ls -lh dist/forge
# Publish / update the `latest` release
- name: Publish latest release
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
GITEA_API: https://git.neuralplatform.ai/api/v1
REPO: neuron-technologies/forge
run: |
# Delete existing `latest` release if present
EXISTING_ID=$(curl -sf \
-H "Authorization: token ${GITEA_TOKEN}" \
"${GITEA_API}/repos/${REPO}/releases/tags/latest" \
| python3 -c "import sys,json; d=json.load(sys.stdin); print(d['id'])" 2>/dev/null || true)
if [ -n "${EXISTING_ID}" ]; then
echo "Deleting existing release id=${EXISTING_ID}"
curl -sf -X DELETE \
-H "Authorization: token ${GITEA_TOKEN}" \
"${GITEA_API}/repos/${REPO}/releases/${EXISTING_ID}"
fi
# Remove stale tag
curl -sf -X DELETE \
-H "Authorization: token ${GITEA_TOKEN}" \
"${GITEA_API}/repos/${REPO}/tags/latest" || true
# Create new release
RELEASE_ID=$(curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
"${GITEA_API}/repos/${REPO}/releases" \
-d "{
\"tag_name\": \"latest\",
\"name\": \"Forge (latest)\",
\"body\": \"Latest Forge build from commit ${GITHUB_SHA}.\nBuilt $(date -u +%Y-%m-%dT%H:%M:%SZ).\",
\"draft\": false,
\"prerelease\": false
}" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
echo "Created release id=${RELEASE_ID}"
# Upload forge binary
echo "Uploading forge binary..."
curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-F "attachment=@dist/forge;filename=forge" \
"${GITEA_API}/repos/${REPO}/releases/${RELEASE_ID}/assets"
echo "Release published successfully"
# Dispatch forge-updated to any downstream dependents
- name: Dispatch forge-updated to dependents
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
GITEA_API: https://git.neuralplatform.ai/api/v1
run: |
# Add downstream repos here as the dependency graph grows
echo "forge-updated dispatch ready (no downstream targets configured yet)"
# Example:
# curl -sf -X POST \
# -H "Authorization: token ${GITEA_TOKEN}" \
# -H "Content-Type: application/json" \
# "${GITEA_API}/repos/neuron-technologies/some-service/dispatches" \
# -d '{"type":"forge-updated","inputs":{"forge_version":"latest","commit":"'"${GITHUB_SHA}"'"}}'
+23
View File
@@ -0,0 +1,23 @@
EL_LIB ?= /usr/local/lib/el
ELC ?= elc
CC ?= cc
CFLAGS := -std=c11 -O2 -I$(EL_LIB)
.PHONY: build clean install
dist/:
mkdir -p dist
dist/forge.c: src/forge.el | dist/
$(ELC) src/forge.el > dist/forge.c
dist/forge: dist/forge.c
$(CC) $(CFLAGS) -o dist/forge dist/forge.c $(EL_LIB)/el_runtime.c -lcurl -lpthread
build: dist/forge
install: dist/forge
install -m 755 dist/forge /usr/local/bin/forge
clean:
rm -rf dist/forge dist/forge.c
+16 -44
View File
@@ -47,25 +47,6 @@ fn build_extract_prompt(subject: String, probe_json: String) -> String {
"when the event occurred (0 if unknown). Return only the JSON object, no prose, no markdown fences." "when the event occurred (0 if unknown). Return only the JSON object, no prose, no markdown fences."
} }
// build_claude_request build the Anthropic messages API request body.
fn build_claude_request(prompt: String) -> String {
"{\"model\":\"claude-opus-4-5\",\"max_tokens\":4096,\"messages\":[{\"role\":\"user\",\"content\":\"" +
str_escape_json(prompt) +
"\"}]}"
}
// extract_content_text pull the text out of the Claude API response.
// Response shape: {"content":[{"type":"text","text":"..."}],...}
fn extract_content_text(response: String) -> String {
// json_get_raw returns the raw value for the key; content is an array
let content_arr: String = json_get_raw(response, "content")
if str_eq(content_arr, "") { return "" }
// The first element's "text" field holds the extraction
// Strip outer [ ] to get first object
let inner: String = str_slice(content_arr, 1, str_len(content_arr) - 1)
return json_get_string(inner, "text")
}
// build_seed wrap extracted patterns into the full seed.json structure. // build_seed wrap extracted patterns into the full seed.json structure.
fn build_seed(subject: String, extracted: String) -> String { fn build_seed(subject: String, extracted: String) -> String {
let values_raw: String = json_get_raw(extracted, "values") let values_raw: String = json_get_raw(extracted, "values")
@@ -97,9 +78,10 @@ fn build_seed(subject: String, extracted: String) -> String {
fn compile_main() -> String { fn compile_main() -> String {
// Get input file // Get input file
let argv: [String] = args()
let input_file: String = "" let input_file: String = ""
if arg_count() > 2 { if len(argv) > 2 {
let input_file = arg(2) let input_file = get(argv, 2)
} }
if str_eq(input_file, "") { if str_eq(input_file, "") {
println("[forge] usage: forge compile <file.forge>") println("[forge] usage: forge compile <file.forge>")
@@ -122,45 +104,35 @@ fn compile_main() -> String {
println("[forge] subject: " + subject) println("[forge] subject: " + subject)
println("[forge] calling Claude to extract consciousness signature...") println("[forge] calling Claude to extract consciousness signature...")
// Check API key // Build and send request via http_post_with_headers.
// Uses ANTHROPIC_API_KEY from env for x-api-key header.
let api_key: String = anthropic_key() let api_key: String = anthropic_key()
if str_eq(api_key, "") { if str_eq(api_key, "") {
println("[forge] error: ANTHROPIC_API_KEY not set") println("[forge] error: ANTHROPIC_API_KEY not set")
return "" return ""
} }
// Build and send request
let prompt: String = build_extract_prompt(subject, probe_json) let prompt: String = build_extract_prompt(subject, probe_json)
let request_body: String = build_claude_request(prompt) let request_body: String = "{\"model\":\"claude-opus-4-5\",\"max_tokens\":4096,\"messages\":[{\"role\":\"user\",\"content\":\"" + str_escape_json(prompt) + "\"}]}"
let headers: Map<String, String> = {"x-api-key": api_key, "anthropic-version": "2023-06-01", "Content-Type": "application/json"}
// http_post_auth_json(url, key, body) key is sent as Bearer token. let response: String = http_post_with_headers("https://api.anthropic.com/v1/messages", request_body, headers)
// The Anthropic API uses x-api-key rather than Bearer; if the runtime
// surfaces a separate http_post_with_headers() builtin, prefer that.
// For now, pass the key as the auth parameter (maps to Authorization header
// in most runtimes; adjust when http_post_anthropic() is available).
let response: String = http_post_auth_json(
"https://api.anthropic.com/v1/messages",
api_key,
request_body
)
if str_eq(response, "") { if str_eq(response, "") {
println("[forge] error: no response from Claude API") println("[forge] error: no response from Anthropic API")
return "" return ""
} }
// Check for API error // Extract text from response: {"content":[{"type":"text","text":"..."}],...}
let api_error: String = json_get_string(response, "error") let content_arr: String = json_get_raw(response, "content")
if !str_eq(api_error, "") { if str_eq(content_arr, "") {
println("[forge] error: Claude API returned error: " + api_error) println("[forge] error: unexpected API response: " + response)
return "" return ""
} }
let first_item: String = str_slice(content_arr, 1, str_len(content_arr) - 1)
let extracted_text: String = json_get_string(first_item, "text")
// Extract the text content from the response
let extracted_text: String = extract_content_text(response)
if str_eq(extracted_text, "") { if str_eq(extracted_text, "") {
println("[forge] error: could not extract text from Claude response") println("[forge] error: could not extract text from response")
println("[forge] raw response: " + response)
return "" return ""
} }
+8 -2
View File
@@ -18,6 +18,11 @@
// ENGRAM_API_KEY engram auth key (if set) // ENGRAM_API_KEY engram auth key (if set)
// ANTHROPIC_API_KEY required for compile step // ANTHROPIC_API_KEY required for compile step
import "schema.el"
import "probe.el"
import "compiler.el"
import "install.el"
fn show_usage() -> String { fn show_usage() -> String {
"forge " + FORGE_VERSION + " — consciousness channel tuner\n\nusage: forge <command> [options]\n\ncommands:\n probe <name> run the consciousness interview\n compile <file> build seed artifact from probe responses\n install <seed> open a channel in the running engram\n inspect list installed imprints\n\nenvironment:\n ENGRAM_URL engram server (default: http://localhost:8742)\n ENGRAM_API_KEY engram auth key (if set)\n ANTHROPIC_API_KEY required for compile step\n" "forge " + FORGE_VERSION + " — consciousness channel tuner\n\nusage: forge <command> [options]\n\ncommands:\n probe <name> run the consciousness interview\n compile <file> build seed artifact from probe responses\n install <seed> open a channel in the running engram\n inspect list installed imprints\n\nenvironment:\n ENGRAM_URL engram server (default: http://localhost:8742)\n ENGRAM_API_KEY engram auth key (if set)\n ANTHROPIC_API_KEY required for compile step\n"
} }
@@ -44,9 +49,10 @@ fn inspect_main() -> String {
// Dispatch // Dispatch
let argv: [String] = args()
let cmd: String = "" let cmd: String = ""
if arg_count() > 1 { if len(argv) > 1 {
let cmd = arg(1) let cmd = get(argv, 1)
} }
if str_eq(cmd, "probe") { if str_eq(cmd, "probe") {
+5 -4
View File
@@ -36,7 +36,7 @@ fn post_node(content: String, node_type: String, salience: String) -> String {
let key: String = engram_key() let key: String = engram_key()
let body: String = build_node_body(content, node_type, salience, key) let body: String = build_node_body(content, node_type, salience, key)
let url: String = engram_url() + "/api/nodes" let url: String = engram_url() + "/api/nodes"
let response: String = http_post_auth_json(url, key, body) let response: String = http_post_json(url, body)
if str_eq(response, "") { return "" } if str_eq(response, "") { return "" }
return json_get_string(response, "id") return json_get_string(response, "id")
} }
@@ -46,7 +46,7 @@ fn post_edge(from_id: String, to_id: String, relation: String, weight: String) -
let key: String = engram_key() let key: String = engram_key()
let body: String = build_edge_body(from_id, to_id, relation, weight, key) let body: String = build_edge_body(from_id, to_id, relation, weight, key)
let url: String = engram_url() + "/api/edges" let url: String = engram_url() + "/api/edges"
let response: String = http_post_auth_json(url, key, body) let response: String = http_post_json(url, body)
return response return response
} }
@@ -149,9 +149,10 @@ fn install_reasoning_pattern(root_id: String, pattern: String, index: Int) -> St
fn install_main() -> String { fn install_main() -> String {
// Get seed file path // Get seed file path
let argv: [String] = args()
let seed_file: String = "" let seed_file: String = ""
if arg_count() > 2 { if len(argv) > 2 {
let seed_file = arg(2) let seed_file = get(argv, 2)
} }
if str_eq(seed_file, "") { if str_eq(seed_file, "") {
println("[forge] usage: forge install <seed.json>") println("[forge] usage: forge install <seed.json>")
+3 -2
View File
@@ -53,9 +53,10 @@ fn inject_responses(shell: String, responses: String) -> String {
fn probe_main() -> String { fn probe_main() -> String {
// Determine subject name // Determine subject name
let argv: [String] = args()
let subject: String = "" let subject: String = ""
if arg_count() > 2 { if len(argv) > 2 {
let subject = arg(2) let subject = get(argv, 2)
} }
if str_eq(subject, "") { if str_eq(subject, "") {
println("[forge] usage: forge probe <name>") println("[forge] usage: forge probe <name>")