diff --git a/README.md b/README.md
index f5ed84a..2eb753e 100644
--- a/README.md
+++ b/README.md
@@ -75,6 +75,15 @@ bounded effect
## Applications Built With HowlFrame
+* **[HowlBoard](https://github.com/howlcipher/howlboard)** — the canonical
+ HowlFrame reference application and human-facing AI engineering operations
+ interface. A full-stack mission-control console for governed autonomous work:
+ `http_server` compiled to standalone bytecode, `web_app` compiled through the
+ JavaScript backend, native-store persistence, and no hand-written server or
+ client code. Live at . Its
+ [dogfooding journal](https://github.com/howlcipher/howlboard/blob/main/docs/dogfooding.md)
+ is the most detailed record of where this language helps and where it gets in
+ the way.
* [Status API](apps/status_api/README.md) — Proves HTTP serving, deterministic routing, and environment inspection.
* [Log Analyzer](apps/log_analyzer/README.md) — Proves file parsing, deterministic string logic, and graceful capability denial.
* [KV CLI](apps/kv_cli/README.md) — Proves in-memory store functionality and sequential deterministic state.
@@ -281,7 +290,20 @@ go run howlframe.go -o build examples/wasm_math.howl
### Reference Application
-[HowlFrame Repo Analyst](examples/repo_analyst/README.md) is a deterministic, five-module application that compiles to HowlFrame bytecode and analyzes repositories without generated Go or JavaScript. Its dogfooding tests prove both the unchanged default instruction ceiling and a larger finite budget explicitly authorized by the trusted runner.
+[HowlBoard](https://github.com/howlcipher/howlboard) is the canonical reference
+application: a full-stack mission-control interface for governed autonomous
+engineering work, with both tiers written in HowlFrame. It is the largest
+program built on this toolchain and the one that most exercises it — the
+`store_keys` primitive, heterogeneous dict records, fail-closed HTTP handlers,
+and several `web_app` codegen fixes all exist because building it required
+them.
+
+[HowlFrame Repo Analyst](examples/repo_analyst/README.md) remains the reference
+for the in-repository bytecode path: a deterministic, five-module application
+that compiles to HowlFrame bytecode and analyzes repositories without generated
+Go or JavaScript. Its dogfooding tests prove both the unchanged default
+instruction ceiling and a larger finite budget explicitly authorized by the
+trusted runner.
## Common Language Features
diff --git a/apps/task_api/DEVELOPMENT_NOTES.md b/apps/task_api/DEVELOPMENT_NOTES.md
index 5a86f68..5e219b7 100644
--- a/apps/task_api/DEVELOPMENT_NOTES.md
+++ b/apps/task_api/DEVELOPMENT_NOTES.md
@@ -4,6 +4,36 @@ Phase 3 dogfooding: a genuine stateful HTTP CRUD service, running as
standalone bytecode, exercised across multiple independent HTTP requests
against one long-lived server process.
+## Corrections (recorded during the HowlBoard dogfood pass)
+
+Three claims below were accurate when written and are no longer true. They are
+left in place for the record, with the correction stated here rather than
+silently edited into the original text.
+
+* **"No opcode exposes method, query string, headers, or path segments" — partly
+ stale.** `OpHttpReqMethod` exists and `(req_method req)` returns the HTTP
+ method; HowlBoard uses it for OPTIONS preflight in every route. Query strings,
+ headers and path segments remain genuinely unavailable, and `req_header` is
+ documented in the app-development skill despite having no opcode.
+
+* **"`defun` return-type annotations compile but crash at runtime" and "there is
+ currently no working path to a `defun` that returns a dict" — stale.** The
+ supported form is the `type_hint` annotation, not a positional type symbol:
+ `(defun make_rec (id) (type_hint return "dict") ...)` compiles and returns a
+ dict that `map_get` consumes correctly at the call site. `type_hint` is
+ compiled away as a pure annotation. HowlBoard's backend and interface are both
+ decomposed into dict-returning helpers on this basis.
+
+* **"An unhandled panic inside a route handler is silently swallowed as a
+ successful response" — fixed.** A handler that fails before writing now
+ returns 500 carrying the structured `VMError` JSON with its code preserved,
+ logged to the VM error stream rather than process stdout. See
+ `TestHTTPHandlerFailuresFailClosed`.
+
+Separately, the note that `store_keys`-style enumeration is "adequate friction,
+not a blocker" no longer applies: `store_keys` exists, returns sorted keys, and
+removes the `next_id` scan workaround entirely.
+
## What worked well
* **State sharing across HTTP requests.** Every `route` handler runs in a
diff --git a/apps/task_api/task_api_test.go b/apps/task_api/task_api_test.go
index 2f461cf..94fd526 100644
--- a/apps/task_api/task_api_test.go
+++ b/apps/task_api/task_api_test.go
@@ -399,13 +399,19 @@ func TestTaskAPICapabilities(t *testing.T) {
t.Run("network only starts but store access is denied", func(t *testing.T) {
startServer(t, srcDir, bcPath, "network")
- // Documented runtime finding: an unhandled capability-denied panic
- // inside a route handler is swallowed by the VM's own recover, and
- // the client observes a 200 with an empty body rather than an
- // error status. This subtest asserts that real, current behavior.
+ // This subtest previously asserted the opposite: that a
+ // capability-denied panic inside a route handler was swallowed and the
+ // client saw a 200 with an empty body. That made the platform's core
+ // safety mechanism indistinguishable from success at the client, and
+ // the VM now fails closed instead, surfacing the structured VMError
+ // with its code intact. See TestHTTPHandlerFailuresFailClosed in
+ // internal/vm for the unit-level regression test.
status, data := doRaw(t, "POST", "/tasks/create", `{"title":"x"}`)
- if status != 200 || len(data) != 0 {
- t.Errorf("expected silent 200/empty-body on capability-denied store access (documented runtime finding), got %d body=%q", status, data)
+ if status != 500 {
+ t.Errorf("expected 500 on capability-denied store access, got %d body=%q", status, data)
+ }
+ if !strings.Contains(string(data), "CAPABILITY_DENIED") {
+ t.Errorf("denial response does not carry its code: %q", data)
}
})
diff --git a/change_log.md b/change_log.md
index 7c67cea..ed6e36b 100644
--- a/change_log.md
+++ b/change_log.md
@@ -3,6 +3,16 @@
## Unreleased
### Added
+* `time_now` in the JavaScript backend. It was supported by the bytecode VM and
+ the Go backend but rejected as an unknown statement for `web_app` programs, so
+ a browser interface had no way to read the clock and render relative times.
+* `store_keys` construct and `STORE_KEYS` bytecode instruction, returning every
+ record key in a native store as a sorted list. Go randomizes map iteration, so
+ enumeration is sorted to keep listing deterministic. Every prior HowlFrame
+ application (`kv_cli`, `todo_cli`, `task_api`, HowlBoard) had to maintain a
+ parallel index record that could silently diverge from the records it indexed;
+ `store_keys` removes that workaround. Requires the `database` capability, and
+ `filesystem` additionally for `file://` stores.
* Runner-sealed, bounded negative map-state provenance for the internal direct
HFIR experiment. It records completed map mutations and reads by backing-map
identity, proves never-present versus effectively deleted keys, and fails
@@ -65,6 +75,13 @@
diverge silently.
### Changed
+* Dict values may now mix types. Dicts are the language's record literal, and the
+ VM and native store both carry `map[string]any`, so a record combining strings,
+ ints, lists, and nested dicts already executed correctly; only the analyzer
+ rejected it. Heterogeneous dict literals and `map_set` writes now widen the
+ element type to `any` through the existing `join` helper instead of reporting
+ `dict value N has type X, want Y`. Key checks, target-kind checks, and list
+ element homogeneity are unchanged.
* Documented the existing standalone HTTP JSON request composition
(`parse_json ... req.body` with `try_let`), its bounded scope, and the
@@ -104,6 +121,26 @@
are classified separately and keep compiling unchanged.
### Fixed
+* `for` over an expression no longer silently miscompiles in the JavaScript and
+ Go backends. Both read the iterable's raw node value, which is empty for
+ anything but a bound symbol, so `(for m (map_get d "missions") ...)` emitted
+ `for (let m of )` and `for _, m := range {` - invalid output produced with no
+ diagnostic, in a toolchain whose contract is to fail closed.
+* `on_event` now terminates its statement. Automatic semicolon insertion does
+ not apply before `(`, so any following top-level statement was parsed as a
+ call of the `addEventListener` result.
+* A `web_app`'s top-level statements are wrapped in an async IIFE. They routinely
+ contain awaited calls, and a classic `