Compare commits
56 Commits
0290feccc1
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| b22a15cc5d | |||
| b8f359f518 | |||
| 3a4c5a6f23 | |||
| 7d7fbe0881 | |||
| 3b6bf53829 | |||
| 5c2a928522 | |||
| cfb1fb01d2 | |||
| f32acc54a1 | |||
| 18a73b4fe0 | |||
| 36275b0307 | |||
| 81c558dcdf | |||
| 15a5aaba8c | |||
| 0705804fc6 | |||
| 104164048c | |||
| 34a95373a9 | |||
| 41d52d94d0 | |||
| f0fe2d8ed8 | |||
| 71398e93eb | |||
| 3cce601aa8 | |||
| 3e0163c2c7 | |||
| 81243704ea | |||
| 79bc3fb94d | |||
| d42882446f | |||
| b2c3a82bf4 | |||
| 162d489556 | |||
| dd6f9bd23e | |||
| 51d833a64a | |||
| 902196ba0b | |||
| ab2dc009a6 | |||
| bde09f7ea8 | |||
| e28af77f18 | |||
| d14d1ab8ae | |||
| e509dba4b9 | |||
| aed138cec3 | |||
| ae4a5c0c93 | |||
| c42170552a | |||
| ef85055cc7 | |||
| eabc63df9f | |||
| d985968acd | |||
| 191456cd7e | |||
| 91dc0242be | |||
| 56648e29f4 | |||
| 2cc31a1381 | |||
| a88dac5510 | |||
| 18b289dff8 | |||
| 34b26a4fde | |||
| b234ef2439 | |||
| a2a7b4ca08 | |||
| c9cc874e53 | |||
| b0b9a25fb3 | |||
| 4c55f135fb | |||
| fc7bc2fef8 | |||
| 3a65052641 | |||
| ef8848c0e2 | |||
| 0397dbf7b1 | |||
| 699be93918 |
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -7,3 +7,6 @@
|
|||||||
[submodule "projects/passepartout-contrib"]
|
[submodule "projects/passepartout-contrib"]
|
||||||
path = projects/passepartout-contrib
|
path = projects/passepartout-contrib
|
||||||
url = ssh://git@10.10.10.201:2222/amr/opencortex-contrib.git
|
url = ssh://git@10.10.10.201:2222/amr/opencortex-contrib.git
|
||||||
|
[submodule "projects/cl-tty"]
|
||||||
|
path = projects/cl-tty
|
||||||
|
url = ssh://git@10.10.10.201:2222/amr/cl-tty.git
|
||||||
|
|||||||
11
.opencode/commands/check-parens.md
Normal file
11
.opencode/commands/check-parens.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
description: Check paren balance in lisp blocks of .org files
|
||||||
|
---
|
||||||
|
|
||||||
|
Run `projects/check-parens/check-parens` on the given .org files to verify all
|
||||||
|
`#+begin_src lisp` blocks have balanced parentheses. Uses SBCL's reader for
|
||||||
|
100% accuracy — no false positives from string literals or character literals.
|
||||||
|
|
||||||
|
Exit 0 if all blocks balanced, 1 if any issues found.
|
||||||
|
|
||||||
|
Usage: /check-parens <file.org> [<file.org> ...]
|
||||||
15
.opencode/commands/check-tangle.md
Normal file
15
.opencode/commands/check-tangle.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
description: Tangle an org file and compile the result
|
||||||
|
---
|
||||||
|
|
||||||
|
Tangle an .org file to .lisp then compile with SBCL. Reports the first
|
||||||
|
compile error with line numbers. Exit 0 = clean compile, exit 1 = error.
|
||||||
|
|
||||||
|
Prepares code for commit by ensuring the tangled .lisp file is syntactically
|
||||||
|
valid. Catches missing symbols, undefined functions, and type errors before
|
||||||
|
they reach the running daemon.
|
||||||
|
|
||||||
|
Usage: /check-tangle <file.org>
|
||||||
|
|
||||||
|
Example:
|
||||||
|
/check-tangle projects/passepartout/org/channel-tui-main.org
|
||||||
17
.opencode/commands/repl-block.md
Normal file
17
.opencode/commands/repl-block.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
description: Send a lisp block from an org file to the REPL
|
||||||
|
---
|
||||||
|
|
||||||
|
Extract a `#+begin_src lisp` block from an .org file and pipe it to the
|
||||||
|
running daemon REPL. Identify the block by function name or index.
|
||||||
|
|
||||||
|
The `--package` flag wraps the block in `(in-package ...)` so it evaluates
|
||||||
|
in the right namespace — essential when the block references symbols from
|
||||||
|
a specific package without the package prefix.
|
||||||
|
|
||||||
|
Usage: /repl-block <file.org> --function <name>
|
||||||
|
/repl-block <file.org> --function <name> --package <pkg>
|
||||||
|
/repl-block <file.org> --block <number>
|
||||||
|
|
||||||
|
Example:
|
||||||
|
/repl-block projects/passepartout/org/channel-tui-view.org --function view-status --package :passepartout.channel-tui
|
||||||
@@ -8,5 +8,7 @@ in `projects/`. See `projects/AGENTS.md` for the general development workflow
|
|||||||
|
|
||||||
| Project | Description | Runtime |
|
| Project | Description | Runtime |
|
||||||
|---------|-------------|---------|
|
|---------|-------------|---------|
|
||||||
| passepartout | Neurosymbolic agent | `passepartout daemon` |
|
| passepartout | Probabilistic-Deterministic Lisp Machine | `passepartout daemon` |
|
||||||
| cl-tui | Reusable terminal UI framework | `sbcl` + `(ql:quickload :cl-tui)` |
|
| cl-tty | Reusable Common Lisp Terminal UI Framework | `sbcl` + `(ql:quickload :cl-tty)` |
|
||||||
|
| stoa | Body/environment — editor, browser, shell, infrastructure (post-v1.0.0) | ROADMAP in `projects/stoa/docs/` |
|
||||||
|
| agora | Decentralized social protocol — sovereign identity, communication, contracts | Protocol spec in `projects/agora/docs/` |
|
||||||
|
|||||||
33
notes/naming-convention.org
Normal file
33
notes/naming-convention.org
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
#+TITLE: Naming Convention — Logos, Stoa, Agora, and Identity
|
||||||
|
#+FILETAGS: :notes:architecture:naming:
|
||||||
|
|
||||||
|
* Project Architecture
|
||||||
|
|
||||||
|
| Name | Greek | Layer | Role |
|
||||||
|
|------|-------|-------|------|
|
||||||
|
| Logos | Λόγος | Mind | The memex, org files — recorded discourse itself |
|
||||||
|
| Passepartout | — | Agent | The neurosymbolic agent that traverses all three layers |
|
||||||
|
| Stoa | Στοά | Body | Environment — editor, browser, shell, infrastructure (post-v1.0.0) |
|
||||||
|
| Agora | Ἀγορά | Society | Decentralized identity, communication, contracts, PDS |
|
||||||
|
|
||||||
|
cl-tty is the first harvested library of the Stoa cannibalization pipeline — a pure-CL terminal I/O library, independent of any project.
|
||||||
|
|
||||||
|
* Versioning Convention
|
||||||
|
|
||||||
|
All projects follow strict semver: X.Y.Z where:
|
||||||
|
- **X** — major release (architecture-breaking changes)
|
||||||
|
- **Y** — minor release (new features, backwards-compatible)
|
||||||
|
- **Z** — patch release (bug fixes only, no new features)
|
||||||
|
|
||||||
|
Feature releases increment Y. Bugfix and hardening releases increment Z.
|
||||||
|
No feature release ships without its prerequisite hardening releases resolved.
|
||||||
|
|
||||||
|
* Identity Architecture
|
||||||
|
|
||||||
|
| Name | Greek | Layer | Role |
|
||||||
|
|------|-------|-------|------|
|
||||||
|
| Psyche | Ψυχή | Root | Master Key — offline seed, never exposed to network |
|
||||||
|
| Persona | — | Identity | Active DID — signs, owns, contracts on the network |
|
||||||
|
| Profile / Eidolon | εἴδωλον | Face | Public-facing shadow-image projected by the Persona |
|
||||||
|
|
||||||
|
Hierarchy: Psyche → Persona → Profile (emanation, not composition).
|
||||||
@@ -1,5 +1,42 @@
|
|||||||
# AGENTS.md
|
# AGENTS.md
|
||||||
|
|
||||||
|
## Subagent iteration workflow
|
||||||
|
|
||||||
|
When delegating work to subagents, follow this iteration loop:
|
||||||
|
|
||||||
|
1. **Specify** — define the task clearly: what files to change, what the
|
||||||
|
output should look like, what constraints apply (read-only vs. build)
|
||||||
|
2. **Receive** — review the subagent's output for correctness, edge cases,
|
||||||
|
style, and consistency with the existing codebase
|
||||||
|
3. **Critique** — send back specific issues: "Block 3 has an off-by-one
|
||||||
|
error", "The README doesn't explain the --package flag", etc.
|
||||||
|
4. **Iterate** — repeat steps 2-3 until zero issues remain. Do not accept
|
||||||
|
work that still has bugs, documentation gaps, or untested edge cases
|
||||||
|
5. **Accept** — only when you cannot find any more issues. **DONE means:**
|
||||||
|
- All unit tests pass (`(fiveam:run-all-tests)` or equivalent)
|
||||||
|
- Integration tests pass (if applicable)
|
||||||
|
- The project runs and the changed feature works end-to-end (start the runtime,
|
||||||
|
exercise the feature, verify output)
|
||||||
|
- No regressions introduced (pre-existing tests still pass, unchanged features
|
||||||
|
still work)
|
||||||
|
- `check-parens` reports zero issues on modified org files
|
||||||
|
- If the project has a runnable entry point (daemon, TUI, demo), verify it
|
||||||
|
starts and the change is visible
|
||||||
|
- **Test with the user's command, not a substitute.** If the user runs
|
||||||
|
`passepartout tui`, test with `passepartout tui`. Loading a stale fasl
|
||||||
|
or bypassing the normal entry point will miss compilation errors that
|
||||||
|
only appear on a fresh build.
|
||||||
|
|
||||||
|
Key principles:
|
||||||
|
- The subagent implements, the architect reviews. Do not switch roles.
|
||||||
|
- When testing reveals a bug, the architect writes the spec and deploys a
|
||||||
|
subagent to fix it. Do not fix bugs discovered during testing yourself.
|
||||||
|
- Every review pass must find fewer issues than the previous pass
|
||||||
|
- If a subagent introduces new bugs while fixing reported ones, reject
|
||||||
|
- Zero issues means zero: no style nits, no missing edge cases, no
|
||||||
|
unclear prose, no dead code
|
||||||
|
- This applies to code, documentation, tests, and config files equally
|
||||||
|
|
||||||
## Development Cycle (every change)
|
## Development Cycle (every change)
|
||||||
|
|
||||||
0. **Start the runtime** — boot the Lisp image that loads your project.
|
0. **Start the runtime** — boot the Lisp image that loads your project.
|
||||||
@@ -71,7 +108,19 @@
|
|||||||
modified .org files has balanced parentheses. Use your project's equivalent
|
modified .org files has balanced parentheses. Use your project's equivalent
|
||||||
function or the SBCL fallback below.
|
function or the SBCL fallback below.
|
||||||
|
|
||||||
10. **Commit on the branch** — include the RED and GREEN test output recorded
|
10. **Plan the test before running it.** When testing a runnable entry point
|
||||||
|
(daemon, TUI, demo), write down three things first:
|
||||||
|
- **Environment** — use `tmux`, not a PTY shell. tmux provides real
|
||||||
|
`FD-STREAM`s identical to a user's terminal. A plain PTY or subprocess
|
||||||
|
may inherit `SYNONYM-STREAM`s and miss bugs.
|
||||||
|
- **Command** — the exact command the user runs (`passepartout tui`),
|
||||||
|
not a substitute (`load "file.fasl"`, `compile-file`, etc.)
|
||||||
|
- **Pass/fail criteria** — specific checks written before the test runs:
|
||||||
|
"check for COMPILE-FILE-ERROR in output", "check for NOT is not of
|
||||||
|
type SB-SYS:FD-STREAM", "check Goodbye after /quit", "check daemon
|
||||||
|
responds to repl command". Do not accept "it seems to work" as a pass.
|
||||||
|
|
||||||
|
11. **Commit on the branch** — include the RED and GREEN test output recorded
|
||||||
in the .org file as part of the commit message evidence:
|
in the .org file as part of the commit message evidence:
|
||||||
```
|
```
|
||||||
git add org/ lisp/ docs/
|
git add org/ lisp/ docs/
|
||||||
@@ -125,13 +174,72 @@
|
|||||||
goes to main. If it spans sessions or might be abandoned for a better
|
goes to main. If it spans sessions or might be abandoned for a better
|
||||||
approach, it gets a branch.
|
approach, it gets a branch.
|
||||||
|
|
||||||
|
## Tools
|
||||||
|
|
||||||
|
All tools are standalone scripts in `projects/<tool>/`, committed to the memex.
|
||||||
|
They survive upgrades, reinstalls, and new clones.
|
||||||
|
|
||||||
|
| Tool | What | Usage |
|
||||||
|
|------|------|-------|
|
||||||
|
| `check-parens` | Validate paren balance in org lisp blocks (uses SBCL reader) | `projects/check-parens/check-parens org/file.org` |
|
||||||
|
| `repl-block` | Extract and send an org lisp block to the daemon REPL | `projects/repl-block/repl-block org/file.org --function foo` |
|
||||||
|
| `check-tangle` | Tangle an org file and compile the result in SBCL | `projects/check-tangle/check-tangle org/file.org` |
|
||||||
|
| `repl` | Send lisp code to the running daemon | `projects/repl-tool/repl "(+ 1 2)"` |
|
||||||
|
| `tangle` | Tangle an org file via Emacs batch | `projects/tangle-tool/tangle org/file.org` |
|
||||||
|
| `org-eval` | Evaluate an org src block via Emacs | `projects/org-eval-tool/org-eval org/file.org` |
|
||||||
|
| `verify-repl` | Compliance: check defuns have REPL-VERIFIED comments | `projects/verify-repl-tool/verify-repl org/` |
|
||||||
|
|
||||||
|
## Package reference
|
||||||
|
|
||||||
|
When sending code to the REPL, use the correct `(in-package ...)` form first.
|
||||||
|
|
||||||
|
### Passepartout
|
||||||
|
|
||||||
|
| Package | When to use |
|
||||||
|
|---------|-------------|
|
||||||
|
| `:passepartout` | Core system — skills, gates, memory, dispatcher |
|
||||||
|
| `:passepartout.channel-tui` | TUI — event handlers, view, state |
|
||||||
|
| `<project>-tests` | Test suites — run with `(fiveam:run (intern ...))` |
|
||||||
|
|
||||||
|
### cl-tty
|
||||||
|
|
||||||
|
| Package | When to use |
|
||||||
|
|---------|-------------|
|
||||||
|
| `:cl-tty.backend` | Backend protocol — terminal init, draw-text, read-event |
|
||||||
|
| `:cl-tty.rendering` | Framebuffer — make-framebuffer, flush-framebuffer |
|
||||||
|
| `:cl-tty.input` | Input — key-event, defkeymap, dispatch-key-event, text-input, textarea |
|
||||||
|
| `:cl-tty.layout` | Layout — vbox, hbox, spacer, layout-calculate |
|
||||||
|
| `:cl-tty.box` | Components — box, text, word-wrap, char-width, dirty-mixin, render protocol |
|
||||||
|
| `:cl-tty.theme` | Theme — theme class, define-preset, load-preset, theme-color |
|
||||||
|
| `:cl-tty.dialog` | Dialog — dialog stack, select, select-dialog, alert-dialog, toast |
|
||||||
|
| `:cl-tty.slot` | Slot/plugin system |
|
||||||
|
| `:cl-tty.markdown` | Markdown — parse-blocks, parse-inline, highlight-code, render-md |
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Before working on CL projects in this monorepo:
|
||||||
|
|
||||||
|
- **Quicklisp local-projects** — ensure `~/quicklisp/local-projects/` is a symlink
|
||||||
|
to `~/memex/projects/`. This eliminates stale copies and ensures cl-tty,
|
||||||
|
passepartout, and other projects are loaded from the same location being edited:
|
||||||
|
```
|
||||||
|
ln -sf ~/memex/projects/cl-tty ~/quicklisp/local-projects/cl-tty
|
||||||
|
```
|
||||||
|
|
||||||
|
- **No .lisp files** — projects contain only `.org` source files. `.lisp` files
|
||||||
|
are generated by tangling and are never edited directly. After cloning or
|
||||||
|
pulling, tangle all org files before loading:
|
||||||
|
```
|
||||||
|
cd projects/passepartout && for f in org/*.org; do projects/tangle-tool/tangle "$f"; done
|
||||||
|
```
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
Tangle a single file:
|
Tangle a single file:
|
||||||
emacs --batch --eval "(progn (require 'org) (find-file \"org/FILE.org\") (org-babel-tangle) (kill-buffer))"
|
projects/tangle-tool/tangle org/FILE.org
|
||||||
|
|
||||||
Validate structural integrity (org/ source files only):
|
Or:
|
||||||
emacs --batch -Q --eval '(progn (find-file "org/FILE.org") (check-parens) (kill-buffer))'
|
emacs --batch --eval "(progn (require 'org) (find-file \"org/FILE.org\") (org-babel-tangle) (kill-buffer))"
|
||||||
|
|
||||||
Run tests (from REPL):
|
Run tests (from REPL):
|
||||||
(fiveam:run (intern "SUITE-NAME" :project-TESTS))
|
(fiveam:run (intern "SUITE-NAME" :project-TESTS))
|
||||||
@@ -139,7 +247,6 @@ Run tests (from REPL):
|
|||||||
|
|
||||||
Run tests (SBCL fallback — only when the runtime cannot start):
|
Run tests (SBCL fallback — only when the runtime cannot start):
|
||||||
sbcl --noinform \
|
sbcl --noinform \
|
||||||
--eval '(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))' \
|
|
||||||
--eval '(ql:quickload :your-project :silent t)' \
|
--eval '(ql:quickload :your-project :silent t)' \
|
||||||
--eval '(load "lisp/FILE.lisp")' \
|
--eval '(load "lisp/FILE.lisp")' \
|
||||||
--eval '(fiveam:run (intern "SUITE-NAME" :project-TESTS))' --quit
|
--eval '(fiveam:run (intern "SUITE-NAME" :project-TESTS))' --quit
|
||||||
@@ -167,6 +274,8 @@ when the runtime itself cannot start.
|
|||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
|
|
||||||
|
- After copying code from the REPL to the .org file, run ~../check-parens/check-parens <file.org>~ to verify all Lisp blocks have balanced parentheses. This catches mismatched parens before tangling:
|
||||||
|
~projects/check-parens/check-parens org/channel-tui-view.org~
|
||||||
- .org is source of truth; .lisp is generated — never edit .lisp directly
|
- .org is source of truth; .lisp is generated — never edit .lisp directly
|
||||||
- Every code change starts with a contract and a failing test
|
- Every code change starts with a contract and a failing test
|
||||||
- Prove RED before writing implementation
|
- Prove RED before writing implementation
|
||||||
@@ -178,3 +287,42 @@ when the runtime itself cannot start.
|
|||||||
- **YOU MAY NOT** push a version tag (e.g., `v0.5.0`), create a GitHub release,
|
- **YOU MAY NOT** push a version tag (e.g., `v0.5.0`), create a GitHub release,
|
||||||
or run `git push` that triggers CI/CD version workflows without explicit
|
or run `git push` that triggers CI/CD version workflows without explicit
|
||||||
permission. Ask first.
|
permission. Ask first.
|
||||||
|
|
||||||
|
## Library/Application Boundary (passepartout + cl-tty)
|
||||||
|
|
||||||
|
The line between cl-tty and passepartout must be clear. Violations produce
|
||||||
|
duplicated code, inconsistent UX, and maintenance burden across both projects.
|
||||||
|
|
||||||
|
**The rule: before writing any UI code in passepartout, check cl-tty first.**
|
||||||
|
If the abstraction already exists in cl-tty, use it. If it doesn't exist, add it
|
||||||
|
to cl-tty first, then consume it in passepartout.
|
||||||
|
|
||||||
|
| This belongs in **cl-tty** (library) | This belongs in **passepartout** (application) |
|
||||||
|
|---------------------------------------|------------------------------------------------|
|
||||||
|
| Widget rendering, layout computation, dirty tracking | Which widgets to show and when |
|
||||||
|
| Theme system (presets, role→hex resolution, load/switch) | Theme choice and application-specific semantic keys |
|
||||||
|
| Markdown parser, renderer, syntax highlighter | What markdown content to render |
|
||||||
|
| Input handling (keymaps, text-input, textarea, event dispatch) | What keybindings to register and what actions they trigger |
|
||||||
|
| Dialog, select, toast rendering and keyboard navigation | Dialog content and on-submit/on-select callbacks |
|
||||||
|
| Word-wrap, char-width, cursor rendering | Cursor position and text content |
|
||||||
|
| Box, scrollbox, tabbar, panel containers | Data displayed inside containers |
|
||||||
|
|
||||||
|
**Concrete checklist before writing any UI function in a passepartout .org file:**
|
||||||
|
1. Run `(do-external-symbols (s :cl-tty.box) ...)` or check this table
|
||||||
|
2. If the function exists in cl-tty, call it — do not reimplement
|
||||||
|
3. If the function doesn't exist but belongs in a library (generic widget, theme
|
||||||
|
mechanism, layout primitive, input handler), add it to cl-tty first
|
||||||
|
4. If the function is passepartout-specific business logic (command dispatch,
|
||||||
|
daemon communication, gate trace data model), write it in passepartout
|
||||||
|
|
||||||
|
Common violations found in the codebase (do not repeat):
|
||||||
|
- Writing a parallel theme system (`*tui-theme*`, `*tui-theme-presets*`,
|
||||||
|
`theme-switch`) — use `cl-tty.theme` instead
|
||||||
|
- Writing a custom word-wrap — use `cl-tty.box:word-wrap` instead
|
||||||
|
- Writing a custom markdown parser or syntax highlighter — use
|
||||||
|
`cl-tty.markdown` instead
|
||||||
|
- Bypassing `select-handle-key` with manual dialog key dispatch — use
|
||||||
|
`select-handle-key` and `render-dialog` instead
|
||||||
|
- Hardcoding inline hex colors — use `(theme-color :role)` instead
|
||||||
|
- Manual framebuffer coordinate arithmetic — use `vbox`/`hbox`/`spacer` layout
|
||||||
|
from `cl-tty.layout` instead
|
||||||
|
|||||||
55
projects/check-parens/README.org
Normal file
55
projects/check-parens/README.org
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
#+TITLE: check-parens
|
||||||
|
#+FILETAGS: :tool:lisp:org:
|
||||||
|
|
||||||
|
Standalone parentheseis checker for Lisp source blocks in Org files.
|
||||||
|
|
||||||
|
Scans all ~#+begin_src lisp … #+end_src~ blocks in an Org file, uses
|
||||||
|
SBCL's reader to validate each block. 100% accurate — no false positives
|
||||||
|
from string literals or character literals (e.g. ~#\)~).
|
||||||
|
|
||||||
|
== Limitations
|
||||||
|
|
||||||
|
Each ~#+begin_src lisp~ block is checked independently. Some literate
|
||||||
|
files intentionally split a single form (e.g. a ~defpackage~ with many
|
||||||
|
symbols) across multiple blocks with prose between them. In such cases
|
||||||
|
no individual block is self-contained and check-parens will report
|
||||||
|
false positives.
|
||||||
|
|
||||||
|
To verify these files, tangle and compile the resulting .lisp instead:
|
||||||
|
check-tangle <file.org>
|
||||||
|
|
||||||
|
== Usage
|
||||||
|
|
||||||
|
#+begin_src shell
|
||||||
|
check-parens <file.org> [<file.org> ...]
|
||||||
|
check-parens -v <file.org>
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
Exit 0 if all blocks are balanced and terminated, 1 otherwise.
|
||||||
|
|
||||||
|
== Output
|
||||||
|
|
||||||
|
~file.org: OK~ — all blocks balanced
|
||||||
|
~file.org: Block at line 27: +2 (missing 2 closes) — near …~
|
||||||
|
~file.org: Block at line 103: unterminated — no matching #+end_src~
|
||||||
|
|
||||||
|
The ~-v~ flag prints the full block content for each issue.
|
||||||
|
|
||||||
|
== Integration
|
||||||
|
|
||||||
|
Pre-commit hook:
|
||||||
|
|
||||||
|
#+begin_src shell
|
||||||
|
cat > .git/hooks/pre-commit <<'HOOK'
|
||||||
|
#!/bin/sh
|
||||||
|
for f in $(git diff --cached --name-only --diff-filter=ACM | grep '\.org$'); do
|
||||||
|
projects/check-parens/check-parens "$f" || exit 1
|
||||||
|
done
|
||||||
|
HOOK
|
||||||
|
chmod +x .git/hooks/pre-commit
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
== Dependencies
|
||||||
|
|
||||||
|
Python 3 + SBCL (for the reader-based validation).
|
||||||
|
SBCL must be at `/usr/bin/sbcl` (the default path).
|
||||||
206
projects/check-parens/check-parens
Executable file
206
projects/check-parens/check-parens
Executable file
@@ -0,0 +1,206 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Check paren balance in #+begin_src lisp blocks of .org files.
|
||||||
|
|
||||||
|
Uses SBCL's actual reader for 100% accuracy.
|
||||||
|
|
||||||
|
Usage: check-parens <file.org> [<file.org> ...]
|
||||||
|
check-parens -v <file.org>
|
||||||
|
|
||||||
|
Exit 0 if all blocks balanced and terminated, 1 otherwise.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
|
def check_file(path, verbose):
|
||||||
|
lines = read_lines(path)
|
||||||
|
if lines is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
blocks = extract_blocks(lines)
|
||||||
|
ok = True
|
||||||
|
|
||||||
|
for start, body in blocks:
|
||||||
|
if not body:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if is_reader_error(body):
|
||||||
|
print(f"{path}: Block at line {start}: {is_reader_error(body)}")
|
||||||
|
if verbose:
|
||||||
|
for line in body:
|
||||||
|
print(f" | {line}")
|
||||||
|
ok = False
|
||||||
|
|
||||||
|
return ok
|
||||||
|
|
||||||
|
|
||||||
|
def read_lines(path):
|
||||||
|
try:
|
||||||
|
with open(path, encoding="utf-8") as f:
|
||||||
|
return f.readlines()
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(f"{path}: file not found", file=sys.stderr)
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"{path}: error reading file — {e}", file=sys.stderr)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
LISP_BEGIN = re.compile(r"#\+begin_src\s+lisp\b", re.IGNORECASE)
|
||||||
|
END_SRC = re.compile(r"#\+end_src\b", re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_blocks(lines):
|
||||||
|
blocks = []
|
||||||
|
start = None
|
||||||
|
buf = None
|
||||||
|
|
||||||
|
for i, line in enumerate(lines, start=1):
|
||||||
|
if start is None:
|
||||||
|
if LISP_BEGIN.match(line.lstrip()):
|
||||||
|
start = i
|
||||||
|
buf = []
|
||||||
|
else:
|
||||||
|
if END_SRC.match(line.lstrip()):
|
||||||
|
blocks.append((start, buf))
|
||||||
|
start = None
|
||||||
|
buf = None
|
||||||
|
else:
|
||||||
|
buf.append(line.rstrip("\n"))
|
||||||
|
|
||||||
|
if start is not None:
|
||||||
|
blocks.append((start, buf))
|
||||||
|
|
||||||
|
return blocks
|
||||||
|
|
||||||
|
|
||||||
|
SBCL = "/usr/bin/sbcl"
|
||||||
|
CHECKER_LISP = "/tmp/check-parens-reader.lisp"
|
||||||
|
|
||||||
|
# One-time setup: write the checker lisp module
|
||||||
|
CHECKER_SRC = r"""(in-package :cl-user)
|
||||||
|
(defpackage :cp-check (:use :cl))
|
||||||
|
(in-package :cp-check)
|
||||||
|
(defun read-file (path)
|
||||||
|
(with-open-file (s path :external-format :utf-8)
|
||||||
|
(let ((buf (make-string (file-length s))))
|
||||||
|
(read-sequence buf s)
|
||||||
|
buf)))
|
||||||
|
(defun check (path)
|
||||||
|
(handler-case
|
||||||
|
(let* ((str (read-file path))
|
||||||
|
(end (length str))
|
||||||
|
(pos 0))
|
||||||
|
(loop
|
||||||
|
(multiple-value-bind (form new-pos)
|
||||||
|
(read-from-string str nil nil :start pos)
|
||||||
|
(when (null form)
|
||||||
|
(return :OK))
|
||||||
|
(setf pos new-pos))))
|
||||||
|
(sb-int:simple-reader-package-error (c)
|
||||||
|
(declare (ignore c))
|
||||||
|
:PACKAGE-ERROR)
|
||||||
|
(sb-int:simple-reader-error (c)
|
||||||
|
(format nil "READER-ERROR: ~a" c))
|
||||||
|
(end-of-file (c)
|
||||||
|
(format nil "EOF: ~a" c))
|
||||||
|
(error (c)
|
||||||
|
(declare (ignore c))
|
||||||
|
:OTHER-ERROR)))
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not os.path.exists(CHECKER_LISP):
|
||||||
|
with open(CHECKER_LISP, "w") as f:
|
||||||
|
f.write(CHECKER_SRC)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_result(output):
|
||||||
|
"""Parse SBCL output to determine if there's a paren error."""
|
||||||
|
output = output.strip()
|
||||||
|
if not output:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# SBCL may print warnings before the result on separate lines.
|
||||||
|
# Find the last non-style-warning line that contains our result token.
|
||||||
|
for line in reversed(output.split("\n")):
|
||||||
|
line = line.strip()
|
||||||
|
if line.startswith(";") or line.startswith("#<"):
|
||||||
|
continue
|
||||||
|
if line == ":OK" or line.endswith(":OK"):
|
||||||
|
return None
|
||||||
|
if line.startswith(":PACKAGE-ERROR") or line.endswith(":PACKAGE-ERROR"):
|
||||||
|
return None
|
||||||
|
if line.startswith(":OTHER-ERROR") or line.endswith(":OTHER-ERROR"):
|
||||||
|
return "unbalanced parentheses (unknown error)"
|
||||||
|
if "READER-ERROR:" in line:
|
||||||
|
msg = line.split("READER-ERROR:", 1)[1].strip()
|
||||||
|
if "unmatched close parenthesis" in msg:
|
||||||
|
return "unbalanced (extra close parenthesis)"
|
||||||
|
return f"unbalanced ({msg[:60]})"
|
||||||
|
if "EOF:" in line:
|
||||||
|
return "unbalanced (missing close parenthesis)"
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def is_reader_error(code_lines):
|
||||||
|
"""Feed code to SBCL's reader via temp file. Returns error string or None."""
|
||||||
|
code = "\n".join(code_lines)
|
||||||
|
if not code.strip():
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not os.path.exists(SBCL):
|
||||||
|
return f"SBCL not found at {SBCL}"
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".lisp", delete=False) as f:
|
||||||
|
f.write(code)
|
||||||
|
temp_path = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Use --no-userinit and --disable-debugger to suppress all interactive output
|
||||||
|
result = subprocess.run(
|
||||||
|
[SBCL, "--noinform", "--no-userinit", "--disable-debugger",
|
||||||
|
"--quit", "--load", CHECKER_LISP,
|
||||||
|
"--eval", f'(print (cp-check::check "{temp_path}"))'],
|
||||||
|
capture_output=True, text=True, timeout=10
|
||||||
|
)
|
||||||
|
return parse_result(result.stdout)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return "TIMEOUT (sbcl hung)"
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
os.unlink(temp_path)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
verbose = False
|
||||||
|
files = []
|
||||||
|
for arg in sys.argv[1:]:
|
||||||
|
if arg == "-v" or arg == "--verbose":
|
||||||
|
verbose = True
|
||||||
|
elif arg.startswith("-"):
|
||||||
|
print(f"Usage: {sys.argv[0]} [-v] <file.org> [...]", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
else:
|
||||||
|
files.append(arg)
|
||||||
|
|
||||||
|
if not files:
|
||||||
|
print(f"Usage: {sys.argv[0]} [-v] <file.org> [...]", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
all_ok = True
|
||||||
|
for path in files:
|
||||||
|
if not check_file(path, verbose):
|
||||||
|
all_ok = False
|
||||||
|
|
||||||
|
return 0 if all_ok else 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
22
projects/check-tangle/README.org
Normal file
22
projects/check-tangle/README.org
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
#+TITLE: check-tangle
|
||||||
|
|
||||||
|
Tangle an .org file and compile the resulting .lisp with SBCL in one step.
|
||||||
|
|
||||||
|
== Usage
|
||||||
|
|
||||||
|
#+begin_src shell
|
||||||
|
check-tangle org/file.org
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
Exit 0 if compilation succeeds, 1 if tangling or compilation fails.
|
||||||
|
|
||||||
|
== What it checks
|
||||||
|
|
||||||
|
1. Reads the ~:tangle~ header from the org file to find the target .lisp path
|
||||||
|
2. Runs ~tangle~ to generate the .lisp from the .org source
|
||||||
|
3. Runs ~sbcl compile-file~ on the result
|
||||||
|
4. Reports the first compile error if any
|
||||||
|
|
||||||
|
== Requires
|
||||||
|
|
||||||
|
Emacs (for tangling), SBCL (for compilation).
|
||||||
75
projects/check-tangle/check-tangle
Executable file
75
projects/check-tangle/check-tangle
Executable file
@@ -0,0 +1,75 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Tangle an .org file and compile the resulting .lisp with SBCL.
|
||||||
|
# Reports the first compile error with line numbers.
|
||||||
|
#
|
||||||
|
# Usage: check-tangle <file.org>
|
||||||
|
# Exit 0 if compile succeeds, 1 if tangled or compilation fails
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [ $# -ne 1 ]; then
|
||||||
|
echo "Usage: check-tangle <file.org>" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
ORG_FILE="$1"
|
||||||
|
|
||||||
|
if [ ! -f "$ORG_FILE" ]; then
|
||||||
|
echo "Error: File not found: $ORG_FILE" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Determine the tangle target from the org file
|
||||||
|
TANGLE=$(grep -m1 'header-args.*:tangle' "$ORG_FILE" | sed 's/.*:tangle //' | head -1 || true)
|
||||||
|
if [ -z "$TANGLE" ]; then
|
||||||
|
echo "SKIP: $ORG_FILE — no :tangle header" >&2
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Resolve relative tangle path
|
||||||
|
ORG_DIR=$(dirname "$ORG_FILE")
|
||||||
|
if [ "$TANGLE" = "${TANGLE#/}" ]; then
|
||||||
|
# Relative path
|
||||||
|
LISP_FILE=$(cd "$ORG_DIR" && realpath -m "$TANGLE" 2>/dev/null || echo "$ORG_DIR/$TANGLE")
|
||||||
|
else
|
||||||
|
# Absolute path
|
||||||
|
LISP_FILE="$TANGLE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Tangling: $ORG_FILE → $LISP_FILE" >&2
|
||||||
|
|
||||||
|
# Prefer the memex's own tangle tool, then fall back to PATH
|
||||||
|
if [ -x "projects/tangle-tool/tangle" ]; then
|
||||||
|
TANGLE_CMD="projects/tangle-tool/tangle"
|
||||||
|
elif command -v tangle &>/dev/null; then
|
||||||
|
TANGLE_CMD="tangle"
|
||||||
|
else
|
||||||
|
TANGLE_CMD="/home/user/.opencode/bin/tangle"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! "$TANGLE_CMD" "$ORG_FILE"; then
|
||||||
|
echo "FAIL: Tangling $ORG_FILE failed" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$LISP_FILE" ]; then
|
||||||
|
echo "SKIP: Tangle target $LISP_FILE not found after tangling" >&2
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Compiling: $LISP_FILE" >&2
|
||||||
|
|
||||||
|
# Compile with SBCL, capturing errors
|
||||||
|
OUTPUT=$(sbcl --noinform --no-userinit --disable-debugger --quit \
|
||||||
|
--eval "(handler-case
|
||||||
|
(progn (compile-file \"$LISP_FILE\") (print :OK))
|
||||||
|
(error (c) (print c) (sb-ext:exit :code 1)))" 2>&1 || true)
|
||||||
|
|
||||||
|
if echo "$OUTPUT" | grep -q ":OK"; then
|
||||||
|
echo "OK: $ORG_FILE compiles cleanly" >&2
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "FAIL: $ORG_FILE — compilation error" >&2
|
||||||
|
echo "$OUTPUT"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
1
projects/cl-tty
Submodule
1
projects/cl-tty
Submodule
Submodule projects/cl-tty added at 94df17a7b9
17
projects/org-eval-tool/README.org
Normal file
17
projects/org-eval-tool/README.org
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
#+TITLE: org-eval
|
||||||
|
|
||||||
|
Evaluate Lisp source blocks in an .org file via Emacs batch mode.
|
||||||
|
|
||||||
|
== Usage
|
||||||
|
|
||||||
|
#+begin_src shell
|
||||||
|
org-eval org/file.org # evaluate all blocks
|
||||||
|
org-eval org/file.org 3 # evaluate block at index 3 (0-based)
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
Useful for quick REPL evaluation without starting the daemon — Emacs
|
||||||
|
tangles and evaluates the block inline. Results are printed to stdout.
|
||||||
|
|
||||||
|
== Dependencies
|
||||||
|
|
||||||
|
Emacs with org-mode (org-babel).
|
||||||
53
projects/org-eval-tool/org-eval
Executable file
53
projects/org-eval-tool/org-eval
Executable file
@@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Evaluate an org src block using Emacs batch mode
|
||||||
|
# Usage: org-eval <org-file> [block-index]
|
||||||
|
# block-index is 1-based (first block = 1).
|
||||||
|
# If omitted, evaluates ALL blocks.
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ $# -lt 1 ]; then
|
||||||
|
echo "Usage: org-eval <org-file> [block-index]"
|
||||||
|
echo " Evaluates src blocks in the org file"
|
||||||
|
echo " block-index is 1-based (first block = 1)"
|
||||||
|
echo " If omitted, evaluates ALL blocks"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ORG_FILE="$1"
|
||||||
|
BLOCK_INDEX="${2:-}"
|
||||||
|
|
||||||
|
if [ ! -f "$ORG_FILE" ]; then
|
||||||
|
echo "Error: File not found: $ORG_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Evaluating: $ORG_FILE"
|
||||||
|
|
||||||
|
if [ -n "$BLOCK_INDEX" ]; then
|
||||||
|
# 1-based indexing: subtract 1 for Emacs' dotimes (0-based)
|
||||||
|
if [ "$BLOCK_INDEX" -lt 1 ]; then
|
||||||
|
echo "Error: block-index must be 1 or greater" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
SKIP=$((BLOCK_INDEX - 1))
|
||||||
|
# Evaluate specific block
|
||||||
|
emacs --batch \
|
||||||
|
--load org \
|
||||||
|
--eval "(setq org-confirm-babel-evaluate nil)" \
|
||||||
|
--eval "(with-current-buffer (find-file-noselect \"$ORG_FILE\") \
|
||||||
|
(goto-char (point-min)) \
|
||||||
|
(dotimes (_ $SKIP) \
|
||||||
|
(org-babel-next-src-block)) \
|
||||||
|
(if (org-in-src-block-p t) \
|
||||||
|
(org-babel-execute-src-block) \
|
||||||
|
(error \"Block $BLOCK_INDEX not found\")))"
|
||||||
|
else
|
||||||
|
# Evaluate all blocks
|
||||||
|
emacs --batch \
|
||||||
|
--load org \
|
||||||
|
--eval "(setq org-confirm-babel-evaluate nil)" \
|
||||||
|
--eval "(org-babel-execute-buffer)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Done."
|
||||||
Submodule projects/passepartout updated: 96628d00e9...a0694d6489
28
projects/repl-block/README.org
Normal file
28
projects/repl-block/README.org
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#+TITLE: repl-block
|
||||||
|
|
||||||
|
Extract a ~#+begin_src lisp~ block from an .org file and output it to stdout,
|
||||||
|
ready to pipe to ~repl~ or inspect.
|
||||||
|
|
||||||
|
== Usage
|
||||||
|
|
||||||
|
#+begin_src shell
|
||||||
|
repl-block org/file.org --function foo | repl
|
||||||
|
repl-block org/file.org --block 3
|
||||||
|
repl-block org/file.org --function foo --package :my-package
|
||||||
|
repl-block org/file.org # list all blocks
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
== Identifying blocks
|
||||||
|
|
||||||
|
By function name (--function): scans all blocks for (defun <name> ...) and
|
||||||
|
outputs the containing block. By index (--block): 1-based position in file.
|
||||||
|
|
||||||
|
The --package flag prepends an ~(in-package ...)~ form so the REPL evaluates
|
||||||
|
in the right namespace.
|
||||||
|
|
||||||
|
Combine with ~repl~ to send a block directly to the daemon:
|
||||||
|
repl-block org/channel-tui-view.org --function view-status --package :passepartout.channel-tui | repl
|
||||||
|
|
||||||
|
== Requires
|
||||||
|
|
||||||
|
Python 3. No dependencies.
|
||||||
108
projects/repl-block/repl-block
Executable file
108
projects/repl-block/repl-block
Executable file
@@ -0,0 +1,108 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Extract a #+begin_src lisp block from an .org file and print it.
|
||||||
|
|
||||||
|
Identify the block by:
|
||||||
|
- index: --block 3 (1-based, counting all #+begin_src lisp blocks)
|
||||||
|
- function: --function view-status (finds a defun/demacro matching the name)
|
||||||
|
|
||||||
|
Output is the block content between begin and end markers, sans markers.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
repl-block org/file.org --function foo | repl
|
||||||
|
repl-block org/file.org --block 3
|
||||||
|
repl-block org/file.org --function foo --package :my-package (adds in-package prefix)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
|
||||||
|
LISP_BEGIN = re.compile(r"#\+begin_src\s+lisp\b", re.IGNORECASE)
|
||||||
|
END_SRC = re.compile(r"#\+end_src\b", re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_blocks(lines):
|
||||||
|
blocks = []
|
||||||
|
start = None
|
||||||
|
buf = None
|
||||||
|
for i, line in enumerate(lines, start=1):
|
||||||
|
if start is None:
|
||||||
|
if LISP_BEGIN.match(line.lstrip()):
|
||||||
|
start = i
|
||||||
|
buf = []
|
||||||
|
else:
|
||||||
|
if END_SRC.match(line.lstrip()):
|
||||||
|
blocks.append((start, buf))
|
||||||
|
start = None
|
||||||
|
buf = None
|
||||||
|
else:
|
||||||
|
buf.append(line.rstrip("\n"))
|
||||||
|
return blocks
|
||||||
|
|
||||||
|
|
||||||
|
def find_by_function(blocks, name):
|
||||||
|
for line_no, body in blocks:
|
||||||
|
for bline in body:
|
||||||
|
if re.match(rf"\(def(un|macro|method|var|parameter|class|struct|package)\s+{re.escape(name)}\b", bline):
|
||||||
|
return line_no, body
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def find_by_index(blocks, idx):
|
||||||
|
if 1 <= idx <= len(blocks):
|
||||||
|
return blocks[idx - 1]
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Extract lisp blocks from org files")
|
||||||
|
parser.add_argument("file", help=".org file to extract from")
|
||||||
|
parser.add_argument("--block", type=int, default=None, help="Block number (1-based)")
|
||||||
|
parser.add_argument("--function", type=str, default=None, help="Function name to find")
|
||||||
|
parser.add_argument("--package", type=str, default=None, help="(in-package ...) prefix")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(args.file, encoding="utf-8") as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(f"File not found: {args.file}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error reading {args.file}: {e}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
blocks = extract_blocks(lines)
|
||||||
|
|
||||||
|
if args.function:
|
||||||
|
line_no, body = find_by_function(blocks, args.function)
|
||||||
|
if body is None:
|
||||||
|
print(f"No block found containing function '{args.function}'", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
elif args.block:
|
||||||
|
line_no, body = find_by_index(blocks, args.block)
|
||||||
|
if body is None:
|
||||||
|
print(f"Block {args.block} not found (file has {len(blocks)} blocks)", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
else:
|
||||||
|
# Print listing to stderr so piping still works
|
||||||
|
for idx, (line_no, body) in enumerate(blocks, 1):
|
||||||
|
first = (body or [""])[0][:60]
|
||||||
|
print(f" {idx}: line {line_no}: {first}", file=sys.stderr)
|
||||||
|
print(f"\n{len(blocks)} total blocks", file=sys.stderr)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if args.package:
|
||||||
|
pkg = args.package
|
||||||
|
if not pkg.startswith(":"):
|
||||||
|
pkg = f":{pkg}"
|
||||||
|
print(f"(in-package {pkg})")
|
||||||
|
print()
|
||||||
|
|
||||||
|
for line in body:
|
||||||
|
print(line)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
27
projects/repl-tool/README.org
Normal file
27
projects/repl-tool/README.org
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
#+TITLE: repl
|
||||||
|
|
||||||
|
Send a Lisp form to the running Passepartout daemon REPL (port 9105).
|
||||||
|
Prints the response from the daemon.
|
||||||
|
|
||||||
|
== Usage
|
||||||
|
|
||||||
|
#+begin_src shell
|
||||||
|
repl "(+ 1 2)"
|
||||||
|
echo "(+ 1 2)" | repl
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
== Environment
|
||||||
|
|
||||||
|
- ~PASSEPARTOUT_HOST~ — daemon host (default 127.0.0.1)
|
||||||
|
- ~PASSEPARTOUT_PORT~ — daemon port (default 9105)
|
||||||
|
- ~PASSEPARTOUT_REPL_TIMEOUT~ — timeout seconds (default 10)
|
||||||
|
|
||||||
|
== Protocol
|
||||||
|
|
||||||
|
Connects via framed TCP (6-byte hex length prefix). Sends a
|
||||||
|
~:repl-eval~ sensor event and reads the response. Expects the
|
||||||
|
daemon to be running (~passepartout daemon~).
|
||||||
|
|
||||||
|
== Dependencies
|
||||||
|
|
||||||
|
Perl (stdlib only).
|
||||||
78
projects/repl-tool/repl
Executable file
78
projects/repl-tool/repl
Executable file
@@ -0,0 +1,78 @@
|
|||||||
|
#!/usr/bin/env perl
|
||||||
|
# repl — evaluate Lisp forms against the running Passepartout daemon
|
||||||
|
# Usage: repl <lisp-form>
|
||||||
|
# or: echo '<lisp-form>' | repl
|
||||||
|
#
|
||||||
|
# Connects to the daemon on 127.0.0.1:9105, sends a repl-eval request
|
||||||
|
# via the framed TCP protocol, reads and prints the response.
|
||||||
|
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
use IO::Socket::INET;
|
||||||
|
|
||||||
|
my $HOST = $ENV{PASSEPARTOUT_HOST} || "127.0.0.1";
|
||||||
|
my $PORT = $ENV{PASSEPARTOUT_PORT} || "9105";
|
||||||
|
my $TIMEOUT = $ENV{PASSEPARTOUT_REPL_TIMEOUT} || 10;
|
||||||
|
|
||||||
|
my $expr = join(" ", @ARGV);
|
||||||
|
if (!$expr) {
|
||||||
|
$expr = do { local $/; <STDIN> };
|
||||||
|
}
|
||||||
|
chomp($expr);
|
||||||
|
# Quote the expression for embedding in a Lisp string
|
||||||
|
$expr =~ s/\\/\\\\/g; # backslash → doubled
|
||||||
|
$expr =~ s/"/\\"/g; # " → \"
|
||||||
|
if (!$expr) {
|
||||||
|
die "Usage: repl <lisp-form>\n or: echo '(+ 1 2)' | repl\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
my $sock = IO::Socket::INET->new(
|
||||||
|
PeerHost => $HOST,
|
||||||
|
PeerPort => $PORT,
|
||||||
|
Proto => "tcp",
|
||||||
|
Timeout => $TIMEOUT
|
||||||
|
) or die "Cannot connect to $HOST:$PORT: $!\n";
|
||||||
|
|
||||||
|
sub read_frame {
|
||||||
|
my ($sock) = @_;
|
||||||
|
my $hex;
|
||||||
|
$sock->read($hex, 6) or return undef;
|
||||||
|
my $len = hex($hex);
|
||||||
|
my $content;
|
||||||
|
$sock->read($content, $len);
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub write_frame {
|
||||||
|
my ($sock, $content) = @_;
|
||||||
|
my $len = sprintf("%06X", length($content));
|
||||||
|
$sock->send($len . $content);
|
||||||
|
}
|
||||||
|
|
||||||
|
# Read handshake (discard)
|
||||||
|
my $handshake = read_frame($sock);
|
||||||
|
|
||||||
|
# Build framed message
|
||||||
|
my $msg = '(:TYPE :EVENT :PAYLOAD (:SENSOR :repl-eval :CODE "' . $expr . '"))';
|
||||||
|
write_frame($sock, $msg);
|
||||||
|
|
||||||
|
# Read response
|
||||||
|
my $response = read_frame($sock);
|
||||||
|
if (defined $response) {
|
||||||
|
if ($response eq "") {
|
||||||
|
print STDERR "Daemon returned empty response\n";
|
||||||
|
exit 1;
|
||||||
|
}
|
||||||
|
if ($response =~ /:VALUE "([^"]*)"/s) {
|
||||||
|
print "$1\n";
|
||||||
|
} elsif ($response =~ /:message "([^"]*)"/s) {
|
||||||
|
print STDERR "$1\n";
|
||||||
|
exit 1;
|
||||||
|
} else {
|
||||||
|
print "Response: $response\n";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
print STDERR "No response from daemon (is it running on $HOST:$PORT?)\n";
|
||||||
|
exit 1;
|
||||||
|
}
|
||||||
|
$sock->close();
|
||||||
199
projects/stoa/docs/ROADMAP.org
Normal file
199
projects/stoa/docs/ROADMAP.org
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
#+TITLE: Stoa Roadmap — The Porch
|
||||||
|
#+STARTUP: content
|
||||||
|
#+FILETAGS: :docs:roadmap:stoa:
|
||||||
|
|
||||||
|
* The Porch
|
||||||
|
|
||||||
|
Stoa (Στοά) is the body/environment layer of the triad:
|
||||||
|
|
||||||
|
| Logos | The mind — recorded discourse (memex + agent) |
|
||||||
|
| Stoa | The porch — editor, browser, shell, infrastructure |
|
||||||
|
| Agora | The society — identity, communication, contracts |
|
||||||
|
|
||||||
|
The name comes from the Stoa Poikile (Painted Porch) in ancient Athens,
|
||||||
|
where Zeno taught Stoic philosophy. The porch was not the philosophy
|
||||||
|
itself — it was the environment that made discourse possible. Stoa is
|
||||||
|
the same: not the agent, not the network, but the infrastructure that
|
||||||
|
hosts both.
|
||||||
|
|
||||||
|
cl-tty (projects/cl-tty/) is retroactively the first harvested library
|
||||||
|
in the Stoa pipeline — a pure-CL terminal I/O library that replaced
|
||||||
|
ncurses/croatoan. Future libraries will follow the same pattern:
|
||||||
|
identify a C/FFI dependency, write a pure CL replacement, use it.
|
||||||
|
|
||||||
|
* v2.0.0: Lisp Machine Emergence
|
||||||
|
|
||||||
|
v2.0.0 is where Passepartout stops being a daemon with clients and becomes the environment. The agent's cognitive loop, the user's editor, the user's shell, and the user's browser run in the same Common Lisp image. The Dispatcher gate stack verifies every action regardless of who initiated it — user or agent. The distinction between "tool" and "self" dissolves.
|
||||||
|
|
||||||
|
*Why this version matters for UX parity.* v0.4.0 through v1.0.0 give Passepartout four interaction surfaces (TUI, messaging apps, Emacs, voice). v2.0.0 inverts the problem: instead of building more clients, it builds a platform where the agent's environment and the user's environment are the same process, separated not by a sandbox but by the Dispatcher gate stack. The editor IS the agent's prompt. The shell IS the agent's actuator. The browser IS the agent's web research tool. There are no clients — there is one Lisp image, one address space, one Org-mode file system.
|
||||||
|
|
||||||
|
*Architectural principle: Browser inside Lisp, not Lisp inside browser.* Lisp is the parent process. It owns the window, the memory, and the input loop. The rendering engine (WebKit/Blink) is a library that paints pixels inside a Lisp buffer. The user can redefine functions while browsing without restarting. Keybinding lookups happen in microseconds (SBCL machine code) — the browser cannot "steal" shortcuts.
|
||||||
|
|
||||||
|
** Qt/QML via EQL5 — the rendering surface
|
||||||
|
|
||||||
|
- Qt/QML (via EQL5) is the UI framework. EQL5 exposes the full Qt C++ API from Common Lisp. QML is declarative — it matches Lisp's generation model.
|
||||||
|
- Desktop: native look and feel on Linux, macOS, and Windows.
|
||||||
|
- Mobile: Qt runs natively on iOS and Android. Android uses F-Droid for the unrestricted version and Play Store for sandboxed. iOS uses Guideline 4.7 ("Educational/Developer Tool" loophole, no JIT compilation).
|
||||||
|
- Safety Bridge for mobile: Lisp code can manipulate browser/files but cannot touch hardware (GPS, camera, contacts) without standard permission pop-ups.
|
||||||
|
- The minibuffer: a universal command line at the bottom of the screen. Not an Emacs modeline. Not a VS Code command palette. A single command surface for every action — edit files, navigate web, run Lisp expressions, invoke agent commands. ~M-x~ for everything.
|
||||||
|
|
||||||
|
*** Lish — the Common Lisp editor
|
||||||
|
|
||||||
|
Not elisp. Not Emacs. A multi-threaded Common Lisp editor rendered via Qt/QML. The complete system prompt lives in an Org buffer — the agent's identity, its skill registry, its memory, and its reasoning are visible and editable as Org text. The user modifies the agent's prompt and the agent reflects the change immediately — the prompt is a file in memory, not a hidden string in a config.
|
||||||
|
|
||||||
|
Org-babel for interactive evaluation: source blocks in Org files are executable. The user evaluates a ~#+begin_src lisp~ block and the result appears inline. The agent evaluates blocks to verify code before writing. The REPL is not a separate window — it is the Org buffer in which the agent and user both work.
|
||||||
|
|
||||||
|
The editor and the agent share the same Lisp image. The editor is not a client that connects to a daemon — it IS the daemon process. The TUI from v0.x is the editor's rendering surface.
|
||||||
|
|
||||||
|
*** Nyxt — the Common Lisp browser (three erosion stages)
|
||||||
|
|
||||||
|
The browser is not a one-time feature. It is a multi-year erosion of the rendering stack toward pure Lisp:
|
||||||
|
|
||||||
|
*Stage 1 — Qt + WebKit.* Qt provides window management and native widgets. WebKit renders web content inside a Lisp buffer. Network requests via dexador (pure Lisp). HTML parsed via Plump (pure Lisp). Layout via Yoga (C-based Flexbox, wrapped via FFI). JavaScript via embedded QuickJS. This stage delivers a working browser in months, not years.
|
||||||
|
|
||||||
|
*Stage 2 — S-expression DOM.* Lisp builds its own DOM representation as native S-expressions. WebKit is reduced to pixel painting only — it receives rendered layouts from Lisp, not raw HTML. The agent can traverse and manipulate the DOM as Lisp data structures without serialization. This makes web content natively queryable and modifiable by the agent's cognitive loop.
|
||||||
|
|
||||||
|
*Stage 3 — Pure Lisp layout.* WebKit turned off entirely. Lisp-native layout engine (12-18 months of focused development). CSS subset sufficient for the modern web's 95% use case. JavaScript via QuickJS remains for interactive content. The browser is now a Lisp application that happens to speak HTTP, not a web engine wrapped in a Lisp process.
|
||||||
|
|
||||||
|
*** Lish — the Lisp shell
|
||||||
|
|
||||||
|
Bash is a text-stream protocol. Passepartout speaks plists. The Lish shell replaces text streams with structured data — every command returns a plist, not a byte stream. Pipe becomes function composition. Scripts become Lisp functions that operate on memory objects directly.
|
||||||
|
|
||||||
|
The agent and the user share the same shell. The user types ~(list-todos :tag "@urgent")~. The agent proposes ~(shell "npm run build")~. The Dispatcher verifies both. The shell is not a separate process — it is a REPL connected to the same Lisp image as the agent's cognitive loop.
|
||||||
|
|
||||||
|
Org-mode buffers become the file system. The user's memex (~/memex/) is browsable as a tree of Org headlines. File operations (read, write, list, search) operate on Org AST nodes, not byte streams. A "directory listing" is a tree of headlines. A "file read" is a subtree rendered as text.
|
||||||
|
|
||||||
|
Bash remains available as a backend for running external commands, but it is not the primary interface.
|
||||||
|
|
||||||
|
*** Emacs migration — three phases
|
||||||
|
|
||||||
|
The Emacs bridge (v0.4.0) is Phase I. The deep integration is three phases, not one:
|
||||||
|
|
||||||
|
*Phase I — Parasite (v0.4.0).* Emacs is a client. The elisp TCP bridge sends text and receives responses. The agent does not control Emacs. Emacs users get a native chat experience alongside the TUI.
|
||||||
|
|
||||||
|
*Phase II — Interpreter (v2.0.0).* An ELisp compatibility layer runs inside Passepartout's Common Lisp image. Key Emacs packages (Org-mode, Magit) run natively without an Emacs process. The compatibility layer does not aim for 100% coverage — it targets the packages the agent's workflows depend on.
|
||||||
|
|
||||||
|
*Phase III — Successor (v2.0.0 and beyond).* Native Common Lisp implementations of Org-mode workflows and Git integration read/write the same file formats. Total independence from Emacs. Emacs users who prefer Emacs keep the bridge. New users get the native experience.
|
||||||
|
|
||||||
|
*** Strategic timeline
|
||||||
|
|
||||||
|
v0.4.0 Emacs bridge (Phase I Parasite) → v1.0.0 Neurosymbolic Maturity → v2.0.0 Lish editor + Nyxt browser (Stage 1) + Emacs Phase II/III + mobile. The Qt/QML surface enables gradual erosion of the rendering stack without rewriting the application logic. The three-phase Emacs migration ensures Lisp users are never abandoned — the bridge works from day one, the native experience grows under it.
|
||||||
|
|
||||||
|
* v3.0.0+: Cannibalization — Eat Your Dependencies
|
||||||
|
|
||||||
|
v3.0.0 begins the erosion of external dependencies — the system that was bootstrapped on Qt, WebKit, C runtime, and Linux starts replacing them piece by piece with native Lisp components. This is the realization of the Lisp Machine: not built from scratch, but arrived at through gradual replacement of a working system.
|
||||||
|
|
||||||
|
** v3.0.0: Single-Process Convergence
|
||||||
|
- TCP bridge between daemon and EQL5 client becomes an internal function call
|
||||||
|
- One SBCL image: daemon + editor + shell + browser share one address space
|
||||||
|
- The wire protocol becomes nil — all communication is plist exchange in memory
|
||||||
|
|
||||||
|
** v3.1.0: Lisp-Native Layout Engine
|
||||||
|
- Replace QML layout with Lisp layout (Yoga FFI as intermediate step)
|
||||||
|
- CLOS-based widget tree with computed dirty regions
|
||||||
|
- Diff-based redisplay: only changed cells re-render
|
||||||
|
|
||||||
|
** v3.2.0: Browser Stage 2 — S-Expression DOM
|
||||||
|
- Lisp builds its own DOM as native s-expressions
|
||||||
|
- WebKit reduced to pixel painting only
|
||||||
|
- Agent traverses and manipulates DOM as Lisp data without serialization
|
||||||
|
|
||||||
|
** v3.3.0: Browser Stage 3 — Pure Lisp Browser
|
||||||
|
- Lisp-native layout engine handles CSS subset
|
||||||
|
- JavaScript via QuickJS remains
|
||||||
|
- WebKit turned off entirely
|
||||||
|
- The browser is now a Lisp application
|
||||||
|
|
||||||
|
** v3.4.0+: Qt/QML Erosion
|
||||||
|
- Replace QML components with Lisp-native widgets (one at a time)
|
||||||
|
- Window management via Lisp-native X11/Wayland bindings
|
||||||
|
- Font rendering via HarfBuzz FFI → Lisp replacement
|
||||||
|
- Event loop: Qt's → SBCL's native thread scheduler
|
||||||
|
- Each replacement is verified by the eval harness; the system remains usable at every step
|
||||||
|
|
||||||
|
** v3.6.0: Stage0 Lisp Bootstrap
|
||||||
|
- 500-byte hex bootstrap → self-hosting Lisp
|
||||||
|
- Replace Linux bootloader
|
||||||
|
- The Lisp machine runs on bare metal
|
||||||
|
|
||||||
|
* v4.0.0: Native Inference
|
||||||
|
|
||||||
|
LLM inference moves in-process. No external servers. No API keys required for inference.
|
||||||
|
|
||||||
|
*Lisp as Sovereign Governor, not as Math Engine.* The weights themselves are not stored as Lisp objects — this would waste 50% memory on type tags and destroy cache locality through pointer-chasing. Instead, the entire tensor is tagged as a single Lisp object (~macro-tag~). The Lisp image holds a pointer to optimized flat binary (GPU-friendly, FPGA-compatible). The tag is checked once. After that, all math happens in the optimized backend.
|
||||||
|
|
||||||
|
** Native inference (FFI binding to llama.cpp)
|
||||||
|
|
||||||
|
- FFI binding to llama.cpp via CFFI: load GGUF models, run inference, manage KV cache. Single SBCL image, zero process boundaries. The agent and the model share memory.
|
||||||
|
- Speculative safety: the Dispatcher gate stack intercepts token generation in real time. A token that would produce a blocked action is preemptively suppressed before generation. No external inference API supports this.
|
||||||
|
- Foveal-peripheral compute: the model skips pruned context nodes during attention computation. External APIs compute full attention regardless of what you send. In-process inference makes the sparse-tree rendering pay off at the compute level, not just the token level.
|
||||||
|
|
||||||
|
** Live surgery on cognition
|
||||||
|
|
||||||
|
With in-process inference, the agent's internal state becomes inspectable:
|
||||||
|
|
||||||
|
- Pause inference mid-stream. Inspect hidden states and activations as Lisp variables.
|
||||||
|
- Modify a vector, change a sampling parameter, resume.
|
||||||
|
- Detect when the agent is likely to hallucinate by comparing current activation patterns against historical baselines.
|
||||||
|
- The REPL becomes a surgical instrument for the agent's own cognition — not just for verifying code, but for inspecting and correcting the neural process that generates it.
|
||||||
|
|
||||||
|
** DSL-compiled model architectures
|
||||||
|
|
||||||
|
Model architectures are described as Lisp DSL:
|
||||||
|
|
||||||
|
- ~(defmodel passepartout-reasoning :type 'transformer :heads 32 :dim 4096 :layers 32)~
|
||||||
|
- The DSL compiles to machine code for the target backend (GPU via CUDA, FPGA via VexRiscv, CPU via llama.cpp).
|
||||||
|
- Python interprets at runtime. Lisp compiles once. Model architecture changes are treated the same as code changes — edited, verified, hot-reloaded.
|
||||||
|
|
||||||
|
* v5.0.0: Hardware — Tagged Lisp Architecture
|
||||||
|
|
||||||
|
The Lisp machine becomes physical. RISC-V with tagged architecture, hardware-enforced type checking, and FPGA prototype for the symbolic core.
|
||||||
|
|
||||||
|
*Not a from-scratch processor.* Use RISC-V as the skeleton, add custom Lisp extensions. RISC-V provides the carrier architecture (standard instruction set, existing toolchain, LLVM support). Lisp extensions provide tagged computation (type checking in hardware, parallel garbage collection, S-expression traversal as atomic operations).
|
||||||
|
|
||||||
|
** The macro-tag approach
|
||||||
|
|
||||||
|
- Top 4–8 bits of every memory word = Type Tag. Hardware checks tags in parallel with ALU operations. Trap on type mismatch.
|
||||||
|
- A tensor (70B weights) is one macro-tagged Lisp object — a pointer to flat binary. The tag is checked once. Math happens at native speed. This replaces "weights as sexps" (which wastes 50% memory on per-weight tags and destroys cache locality).
|
||||||
|
- Custom instructions: TADD (tagged add), LISP.CAR, LISP.CDR — Lisp primitives as single-cycle hardware operations.
|
||||||
|
|
||||||
|
** Phase migration: Host → Co-processor → Self-hosted
|
||||||
|
|
||||||
|
1. *Parasitic.* Lisp card (FPGA) is a PCIe co-processor. Host CPU (Intel/AMD, Linux/Windows) handles "dirty" I/O — networking, display, file systems. Lisp card handles tagged computation and the agent's cognitive loop. If Lisp crashes, host survives. Reset card, reload. Memory mapping: the card can see the host's memory. The Lisp environment reaches out and inspects data.
|
||||||
|
|
||||||
|
2. *Functional Hijacking.* Lisp UI runs on the card, displays through the PC's GPU. The agent indexes Linux files into Lisp objects. The host becomes an I/O server for the Lisp card.
|
||||||
|
|
||||||
|
3. *Driver Cannibalization.* Point the agent at C drivers. Ask it to generate native Lisp drivers for the hardware the card controls directly. PCIe Passthrough for direct hardware access.
|
||||||
|
|
||||||
|
4. *Self-Hosting.* Replace the Linux bootloader with Stage0 Lisp (a bootstrap from 500 bytes of hex to a self-hosting Lisp). Cut the umbilical cord. The Lisp machine runs on bare metal.
|
||||||
|
|
||||||
|
** Concrete prototyping milestones
|
||||||
|
|
||||||
|
| Stage | Hardware | Cost | What it delivers |
|
||||||
|
|-------+----------+------+-----------------|
|
||||||
|
| TinyTapeout | Custom silicon (130nm) | ~$500–1,000 | 8-bit tagged toy processor with Lisp primitives |
|
||||||
|
| Shuttle | Multi-project wafer | ~$10,000–20,000 | Tagged RISC-V core at 100–300MHz |
|
||||||
|
| FPGA | Terasic DE10-Nano / Xilinx KCU105 | ~$200–500 | VexRiscv with custom Lisp extensions, PCIe card form factor |
|
||||||
|
| Industrial | Commercial foundry (5nm) | ~$10M–100M+ | Competes with modern CPUs on tagged workloads |
|
||||||
|
|
||||||
|
Start at TinyTapeout. Validate the tagged architecture works. Move to FPGA. Validate at speed. Only then consider silicon.
|
||||||
|
|
||||||
|
** Garbage collection in hardware
|
||||||
|
|
||||||
|
Dedicated bus master (Scavenger) runs background garbage collection while the main CPU executes code. No "GC pause." The scavenger traverses the heap in parallel with computation, freeing unreachable objects without stopping the agent.
|
||||||
|
|
||||||
|
** Persistent single-address-space memory
|
||||||
|
|
||||||
|
NVRAM for the entire heap. Turn on the machine — state is exactly where you left it. No "booting." No "loading memory from disk." The agent's Merkle-tree memory, skill registry, knowledge graph, and induced functions survive restarts as a contiguous hardware state.
|
||||||
|
|
||||||
|
** Why this is not "Lisp inside browser"
|
||||||
|
|
||||||
|
Most Lisp-on-hardware attempts fail because they try to compete with Intel on raw math. That's the wrong axis. The tagged architecture doesn't need to beat a GPU at matrix multiplication. It needs to beat a CPU at symbolic computation — graph traversal, constraint solving, theorem proving, garbage collection. These are the v3.0.0 symbolic engine's workload. Hardware that makes them single-cycle is the differentiator, not hardware that runs matrix math faster.
|
||||||
|
|
||||||
|
* v6.0.0: True Agency
|
||||||
|
|
||||||
|
World models, temporal reasoning, goal persistence across restarts.
|
||||||
|
|
||||||
|
- World models: Predictive models of user behavior, project dynamics, system state.
|
||||||
|
- Temporal reasoning: Scheduling, deadlines, elapsed duration awareness.
|
||||||
|
- Goal persistence: Goals survive restarts. Long-term projects in memory-objects.
|
||||||
16
projects/tangle-tool/README.org
Normal file
16
projects/tangle-tool/README.org
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#+TITLE: tangle
|
||||||
|
|
||||||
|
Generate .lisp files from .org sources via Emacs org-babel-tangle.
|
||||||
|
|
||||||
|
== Usage
|
||||||
|
|
||||||
|
#+begin_src shell
|
||||||
|
tangle org/file.org
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
Requires the org file to have ~#+PROPERTY: header-args:lisp :tangle target.lisp~
|
||||||
|
or per-block ~:tangle~ headers. Files without a ~:tangle~ header are skipped.
|
||||||
|
|
||||||
|
== Dependencies
|
||||||
|
|
||||||
|
Emacs with org-mode (ob-tangle).
|
||||||
27
projects/tangle-tool/tangle
Executable file
27
projects/tangle-tool/tangle
Executable file
@@ -0,0 +1,27 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Tangle an org file using Emacs batch mode
|
||||||
|
# Usage: tangle <org-file>
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ $# -ne 1 ]; then
|
||||||
|
echo "Usage: tangle <org-file>"
|
||||||
|
echo " Tangles all src blocks with :tangle directives to their targets"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ORG_FILE="$1"
|
||||||
|
|
||||||
|
if [ ! -f "$ORG_FILE" ]; then
|
||||||
|
echo "Error: File not found: $ORG_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Tangling: $ORG_FILE"
|
||||||
|
|
||||||
|
emacs --batch \
|
||||||
|
--load org \
|
||||||
|
--eval "(setq org-confirm-babel-evaluate nil)" \
|
||||||
|
--eval "(org-babel-tangle-file \"$ORG_FILE\")"
|
||||||
|
|
||||||
|
echo "Done."
|
||||||
30
projects/verify-repl-tool/README.org
Normal file
30
projects/verify-repl-tool/README.org
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
#+TITLE: verify-repl
|
||||||
|
|
||||||
|
Compliance checker for literate programming discipline. Scans .org files
|
||||||
|
for violations of the REPL-first and one-per-block rules.
|
||||||
|
|
||||||
|
== Usage
|
||||||
|
|
||||||
|
#+begin_src shell
|
||||||
|
verify-repl org/
|
||||||
|
verify-repl projects/passepartout/org/
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
== Checks performed
|
||||||
|
|
||||||
|
1. **REPL-first** — every ~(defun|defmacro|defvar|defstruct|defmethod|defclass)~
|
||||||
|
block must have ~;; REPL-VERIFIED: <timestamp>~ on the line above
|
||||||
|
~#+begin_src lisp~. Any definition block without this comment is a violation.
|
||||||
|
2. **One-per-block** — each ~#+begin_src lisp~ block must contain exactly one
|
||||||
|
top-level form. Multiple defuns in the same block are a violation.
|
||||||
|
3. **Prose-before-code** — each code block must be preceded by an Org headline
|
||||||
|
(the prose explanation). Blocks without a preceding headline are a violation.
|
||||||
|
|
||||||
|
== Exempt files
|
||||||
|
|
||||||
|
Some core infrastructure files are blacklisted (package definitions, setup),
|
||||||
|
defined in the script's ~BLACKLIST~ array. Extend as needed.
|
||||||
|
|
||||||
|
== Dependencies
|
||||||
|
|
||||||
|
Bash, grep.
|
||||||
149
projects/verify-repl-tool/verify-repl
Executable file
149
projects/verify-repl-tool/verify-repl
Executable file
@@ -0,0 +1,149 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# verify-repl — compliance checker for the OpenCode Engineering Discipline
|
||||||
|
#
|
||||||
|
# Usage: verify-repl <org-directory>
|
||||||
|
# Scans all .org files in the given directory for violations of:
|
||||||
|
# 1. REPL-First: every (defun|defmacro|defvar|defparameter|defstruct|defmethod|defclass)
|
||||||
|
# block must have ";; REPL-VERIFIED:" on the line above #+begin_src lisp
|
||||||
|
# 2. One-per-block: each #+begin_src lisp block must contain exactly one top-level form
|
||||||
|
# 3. Prose-before-code: each code block must be preceded by an Org headline
|
||||||
|
#
|
||||||
|
# Returns 0 if all checks pass, 1 if violations found.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ORG_DIR="${1:-}"
|
||||||
|
if [ -z "$ORG_DIR" ] || [ ! -d "$ORG_DIR" ]; then
|
||||||
|
echo "Usage: verify-repl <org-directory>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
VIOLATIONS=0
|
||||||
|
FILES_CHECKED=0
|
||||||
|
|
||||||
|
# Blacklist: files exempt from REPL verification (core infrastructure, tests)
|
||||||
|
# Override with $VERIFY_REPL_EXCLUDE (space-separated filenames).
|
||||||
|
# Example: VERIFY_REPL_EXCLUDE="setup.org package.lisp" verify-repl org/
|
||||||
|
DEFAULT_BLACKLIST=(
|
||||||
|
"core-defpackage.org"
|
||||||
|
"core-manifest.org"
|
||||||
|
"core-skills.org"
|
||||||
|
"core-communication.org"
|
||||||
|
"package.lisp"
|
||||||
|
"setup.org"
|
||||||
|
)
|
||||||
|
|
||||||
|
if [ -n "${VERIFY_REPL_EXCLUDE:-}" ]; then
|
||||||
|
IFS=' ' read -ra BLACKLIST <<< "$VERIFY_REPL_EXCLUDE"
|
||||||
|
else
|
||||||
|
BLACKLIST=("${DEFAULT_BLACKLIST[@]}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
is_blacklisted() {
|
||||||
|
local fname
|
||||||
|
fname=$(basename "$1")
|
||||||
|
for bl in "${BLACKLIST[@]}"; do
|
||||||
|
[ "$fname" = "$bl" ] && return 0
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
check_file() {
|
||||||
|
local file="$1"
|
||||||
|
local fname
|
||||||
|
fname=$(basename "$file")
|
||||||
|
local in_block=0
|
||||||
|
local block_start=0
|
||||||
|
local prev_line=""
|
||||||
|
local prev_was_headline=0
|
||||||
|
local block_content=""
|
||||||
|
local line_no=0
|
||||||
|
local has_repl_verify=0
|
||||||
|
local def_count=0
|
||||||
|
local violations_in_file=0
|
||||||
|
|
||||||
|
is_blacklisted "$file" && return 0
|
||||||
|
|
||||||
|
FILES_CHECKED=$((FILES_CHECKED + 1))
|
||||||
|
|
||||||
|
while IFS= read -r line || [ -n "$line" ]; do
|
||||||
|
line_no=$((line_no + 1))
|
||||||
|
local trimmed="${line#"${line%%[![:space:]]*}"}"
|
||||||
|
|
||||||
|
# Track headlines
|
||||||
|
if echo "$trimmed" | grep -qE '^\*+[[:space:]]'; then
|
||||||
|
prev_was_headline=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Enter code block
|
||||||
|
if echo "$trimmed" | grep -qE '^#\+begin_src[[:space:]]+lisp'; then
|
||||||
|
in_block=1
|
||||||
|
block_start=$line_no
|
||||||
|
block_content=""
|
||||||
|
def_count=0
|
||||||
|
has_repl_verify=0
|
||||||
|
# Check for REPL-VERIFIED comment on previous line(s)
|
||||||
|
# Matches ";; REPL-VERIFIED:" or ";; REPL-VERIFIED:" etc.
|
||||||
|
if echo "$prev_line" | grep -qE ';; +REPL[-_]VERIFIED:'; then
|
||||||
|
has_repl_verify=1
|
||||||
|
fi
|
||||||
|
# Check for prose requirement: was there a headline before this block?
|
||||||
|
if [ "$prev_was_headline" -eq 0 ]; then
|
||||||
|
echo " $fname:$block_start: PROSE-BEFORE-CODE: no Org headline before code block"
|
||||||
|
violations_in_file=$((violations_in_file + 1))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Inside code block: collect content
|
||||||
|
if [ "$in_block" -eq 1 ]; then
|
||||||
|
if echo "$trimmed" | grep -qE '^#\+end_src'; then
|
||||||
|
in_block=0
|
||||||
|
# Check: REPL verification for definition blocks
|
||||||
|
if [ "$def_count" -gt 0 ] && [ "$has_repl_verify" -eq 0 ]; then
|
||||||
|
echo " $fname:$block_start: REPL-FIRST: $(if [ "$def_count" -gt 1 ]; then echo "$def_count definitions"; else echo "defun/defmacro"; fi) without REPL-VERIFIED comment"
|
||||||
|
violations_in_file=$((violations_in_file + 1))
|
||||||
|
fi
|
||||||
|
# Check: one-per-block
|
||||||
|
if [ "$def_count" -gt 1 ]; then
|
||||||
|
echo " $fname:$block_start: ONE-PER-BLOCK: $def_count definitions in a single block (must be exactly 1)"
|
||||||
|
violations_in_file=$((violations_in_file + 1))
|
||||||
|
fi
|
||||||
|
prev_was_headline=0
|
||||||
|
block_content=""
|
||||||
|
else
|
||||||
|
# Count definitions in block
|
||||||
|
if echo "$trimmed" | grep -qE '^\((defun|defmacro|defvar|defparameter|defstruct|defmethod|defclass)[[:space:](]'; then
|
||||||
|
def_count=$((def_count + 1))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
prev_line="$line"
|
||||||
|
done < "$file"
|
||||||
|
|
||||||
|
if [ "$violations_in_file" -gt 0 ]; then
|
||||||
|
echo " ✗ $fname: $violations_in_file violation(s)"
|
||||||
|
VIOLATIONS=$((VIOLATIONS + violations_in_file))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "=== REPL Compliance Check ==="
|
||||||
|
echo "Directory: $ORG_DIR"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
for file in "$ORG_DIR"/*.org; do
|
||||||
|
[ -f "$file" ] || continue
|
||||||
|
check_file "$file"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Files checked: $FILES_CHECKED"
|
||||||
|
echo "Violations: $VIOLATIONS"
|
||||||
|
|
||||||
|
if [ "$VIOLATIONS" -eq 0 ]; then
|
||||||
|
echo "Status: ✓ PASS — all checks satisfied"
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "Status: ✗ FAIL — fix violations before committing"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
Reference in New Issue
Block a user