myxo-lang 1.5.0 → 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md ADDED
@@ -0,0 +1,49 @@
1
+ # AGENTS.md — nx-lang (Myxo)
2
+
3
+ ## Multi-session lane protocol (the yield sign)
4
+
5
+ Multiple agent/human sessions work in this repo at once (e.g. two terminals). To avoid
6
+ crashing into each other, every session obeys this law:
7
+
8
+ 1. **Know your name.** The user assigns each session a lane name (e.g. `terminal-a`,
9
+ `terminal-b`). Use it in every `lane.js` call. If the `KIMI_LANE` environment
10
+ variable is set (the duo launcher sets it), that IS your lane name — no need to ask.
11
+ Otherwise, if you were never given one, ask.
12
+ 2. **Claim before you edit.** Before modifying any file, run:
13
+ `node tools/lane.js claim <file> --by <your-name> --why "<what you're doing>"`
14
+ - Exit 0 → the lane is yours; proceed.
15
+ - Exit 1 → **YIELD**. Another session holds it. Do NOT edit that file. Work on
16
+ something else, or tell the user you're blocked on that lane.
17
+ 3. **Batch edits = one claim.** List every file you intend to touch in a single `claim`
18
+ call (the claim is all-or-nothing: if any lane is taken, none are held).
19
+ 4. **Release when done.** As soon as the work lands (and tests pass):
20
+ `node tools/lane.js release --all --by <your-name>`
21
+ Do not hold lanes while idle. Do not release another session's lanes (`--force` is
22
+ for the user, or a proven-dead session).
23
+ 5. **Stale locks.** A crashed session leaves locks behind. `node tools/lane.js list`
24
+ shows ages; `node tools/lane.js clear-stale --hours 6` sweeps anything older.
25
+ Locks older than ~6h with no live session are presumed dead.
26
+ 6. **Everything is timestamped.** Every claim, yield, release, refusal, force-break,
27
+ and sweep is appended to `.lane/journal.jsonl` with a UTC ISO timestamp.
28
+ `node tools/lane.js log --last 20` shows the recent record. When you finish a
29
+ piece of work, it should be visible in the journal.
30
+ 7. **Reading is always free.** No claim needed to read, search, or run tests that
31
+ don't write repo files (background eval runs write only gitignored
32
+ `myxo-evals/results/*.jsonl` — still claim `myxo-evals/BASELINE.md` before editing it).
33
+
34
+ Locks live in `.lane/` (gitignored) — they never enter version control.
35
+
36
+ ## Honesty gate (Lucy)
37
+
38
+ Before any work in this repo is declared DONE, VERIFIED, or a number is repeated as fact,
39
+ dispatch the `lucy` subagent (defined in `.kimi-code/agents/lucy.md`) with: the exact list of
40
+ claims, and the evidence locations (files, result artifacts, commands). Lucy works from raw
41
+ evidence only, treats the claimant as unreliable, and returns VERIFIED / UNVERIFIED / FALSE
42
+ per claim with receipts. Her verdict is reported alongside the work — including when it is
43
+ unflattering. This is the standing form of the vault's verify-with-agents rule.
44
+
45
+ ## Project conventions
46
+
47
+ - Pure Node, **zero dependencies** — this is the language's law; `tools/lane.js` included.
48
+ - Tests: `npm test` (core suite must stay green; eval baselines live in `myxo-evals/BASELINE.md`).
49
+ - The language/binary is **Myxo** (`myxo.js`); the dir name `nx-lang` is historical.
package/HOSTS.md ADDED
@@ -0,0 +1,196 @@
1
+ # Hosting Myxo — the host integration contract
2
+
3
+ Myxo is designed to be embedded. A host (such as JAMES, an MCP server, or any Node program) grants Myxo a curated set of capabilities, and Myxo enforces a script's declared reach, budgets, and audit trail. This document is the contract a host should follow.
4
+
5
+ ## The host's job vs. Myxo's job
6
+
7
+ **Myxo guarantees:**
8
+ - A script can only call capabilities it declared in `needs`.
9
+ - Value/call budgets declared in `needs` are enforced at runtime.
10
+ - Every privileged call and refusal is recorded in an audit ledger.
11
+ - `weave`, `pyeval`, `jseval`, `sh`, and other broad runtimes can be fenced at the host's option.
12
+
13
+ **The host guarantees:**
14
+ - Only the intended tools are bridged across the allowlist.
15
+ - The implementation of each tool validates its own inputs (SQL allowlists, repo/branch allowlists, structured approval templates, etc.).
16
+ - The host keeps its receipt-signing key secret.
17
+ - The host decides which tools are value-metered vs. call-count-metered via `valueCaps`.
18
+
19
+ Myxo is a fence around *which verb* a script may call and *how much*. It does not sandbox what a granted tool does internally.
20
+
21
+ ## The three gates
22
+
23
+ Production runners (`myxo-run.js` and `myxo-live.js`) apply defense in depth:
24
+
25
+ ```text
26
+ host allowlist -> which tools are even bridged
27
+ script `needs` -> which bridged tools this script may call
28
+ value budgets -> per-call max and cumulative total enforced at runtime
29
+ ```
30
+
31
+ A call is refused at the first gate it fails. Every gate decision is logged.
32
+
33
+ ## The JAMES surface
34
+
35
+ JAMES exposes a narrow MCP surface to Myxo through `james_nx_run`. The current bridged tools are intentionally read-heavy and approval-centric:
36
+
37
+ | Capability | Type | Purpose |
38
+ |------------|------|---------|
39
+ | `james_db_query` | read | SELECT-style queries against the JAMES database. |
40
+ | `james_read_notes` | read | Read notes, scoped by query. |
41
+ | `james_approval_request` | approval queue | Queue a human-review action. No execution. |
42
+ | `james_approval_list` | read | Inspect the approval queue. |
43
+ | `james_git_push_request` | approval queue | One-tap `git_push` path; still requires human approval and passes JAMES's own repo/branch allowlist. |
44
+ | `james_action_await` | read | Poll the result of an approved action. |
45
+
46
+ **Deliberately absent:** direct memory writes, thought-board posts, Telegram sends, PM2 control, secret access, sandbox exec, rollback, and generic outward-action staging. Myxo can ask for human review; it cannot quietly perform those side effects through `james_nx_run`.
47
+
48
+ ## Bridging tools
49
+
50
+ A host bridges tools by passing a catalog and a call function:
51
+
52
+ ```js
53
+ const { runScript } = require('./myxo-run');
54
+ const { runLive } = require('./myxo-live');
55
+
56
+ const tools = [
57
+ { name: 'james_db_query', inputSchema: { properties: { sql: {} }, required: ['sql'] } },
58
+ ];
59
+
60
+ // Synchronous path (in-process MCP shim)
61
+ runScript(src, {
62
+ client: { tools, call: (name, args) => mcp.invoke(name, args) },
63
+ allow: ['james_db_query'], // host allowlist
64
+ requireManifest: true, // require `needs`
65
+ moduleLoader: null, // fence `weave`
66
+ receiptKey: hostKey(), // optional: seal the audit ledger
67
+ });
68
+
69
+ // Asynchronous path (real async tools)
70
+ runLive(src, {
71
+ tools,
72
+ onCall: async (name, args) => mcp.invoke(name, args),
73
+ allow: ['james_db_query'],
74
+ requireManifest: true,
75
+ moduleLoader: null,
76
+ receiptKey: hostKey(),
77
+ });
78
+ ```
79
+
80
+ `allow` is the first gate. Tools not in `allow` are invisible to the script, even if the underlying catalog contains them.
81
+
82
+ ## Metering: value vs. count
83
+
84
+ The host decides whether a capability is metered by value or by call count:
85
+
86
+ ```js
87
+ runScript(src, {
88
+ client: { tools, call },
89
+ valueCaps: ['james_spend'], // first numeric argument is the spend amount
90
+ });
91
+ ```
92
+
93
+ A script then declares budgets:
94
+
95
+ ```myx
96
+ needs james_spend(max 5, total 15)
97
+ emit james_spend(4) # ok
98
+ emit james_spend(100) # refused: per-call max exceeded
99
+ ```
100
+
101
+ Capabilities not in `valueCaps` are call-count metered:
102
+
103
+ ```myx
104
+ needs james_approval_request(total 3)
105
+ james_approval_request({"action": "a"}) # ok
106
+ james_approval_request({"action": "b"}) # ok
107
+ james_approval_request({"action": "c"}) # ok
108
+ james_approval_request({"action": "d"}) # refused: total exceeded
109
+ ```
110
+
111
+ ## Audit ledger and receipts
112
+
113
+ Every privileged call produces a ledger entry:
114
+
115
+ ```js
116
+ { cap: 'james_db_query', args: ['SELECT 1'], ok: true, result: '...' }
117
+ { cap: 'james_pm2_action', args: [...], ok: false, error: 'not declared in needs' }
118
+ ```
119
+
120
+ When `receiptKey` is provided, `runScript`/`runLive` return a tamper-evident receipt:
121
+
122
+ ```js
123
+ {
124
+ ok: true,
125
+ output: '...',
126
+ audit: [...],
127
+ receipt: {
128
+ entries: [...], // hash-chained ledger
129
+ seal: { count, root, mac }
130
+ }
131
+ }
132
+ ```
133
+
134
+ The receipt is sealed in the host process (`runLive`) or inside `runScript`. It can be verified later with the same key:
135
+
136
+ ```js
137
+ const { verifyChain } = require('./receipt');
138
+ const v = verifyChain(receipt.entries, receipt.seal, key);
139
+ // v.ok === true -> ledger is intact and sealed by the host
140
+ ```
141
+
142
+ Use a persistent key (environment variable or a 0600 keyfile) so receipts remain verifiable across restarts. `receipt.hostKey()` implements this fallback.
143
+
144
+ ## Production defaults
145
+
146
+ `myxo-run.js` and `myxo-live.js` default to safe production behavior:
147
+
148
+ - `requireManifest: true` — capability calls require a `needs` declaration.
149
+ - `moduleLoader: null` — `weave` is fenced unless the host explicitly grants file loading.
150
+ - `maxSteps: 200000` — interpreter fuel to catch runaway loops.
151
+ - `timeoutMs: 30000` — wall-clock kill switch for live workers.
152
+
153
+ Hosts should override these only when they have a reason to be more permissive.
154
+
155
+ ## Preflight with `myxo plan`
156
+
157
+ Before running an agent-written script, a host can call `myxo plan <file>` to preview its reach:
158
+
159
+ ```
160
+ $ node myxo.js plan job.myx
161
+ Declared capabilities (needs):
162
+ james_db_query
163
+ Capabilities referenced in code:
164
+ james_db_query OK declared
165
+ james_telegram_send XX NOT declared (line 4) — the fence would deny it
166
+ VERDICT: 1 referenced capability(ies) not declared — this script would be REFUSED at runtime ...
167
+ ```
168
+
169
+ This is a best-effort lint, not a sound static gate. The runtime fence remains the real boundary.
170
+
171
+ ## Capability versioning and deprecation
172
+
173
+ Tool names are part of the contract. If a host renames or removes a tool, existing scripts break. Recommended practices:
174
+
175
+ 1. Keep tool names stable.
176
+ 2. Add new tools under new names; do not repurpose old names for different semantics.
177
+ 3. If a tool must change, keep the old name as an alias for at least one release cycle.
178
+ 4. Publish the supported tool catalog as a JSON schema so scripts and gates can validate against it.
179
+
180
+ ## Security checklist for hosts
181
+
182
+ - [ ] Validate every argument inside the tool handler, not just at the Myxo fence.
183
+ - [ ] For database tools, enforce query shape (e.g., read-only, table allowlist).
184
+ - [ ] For approval tools, constrain `action`/`details` to structured templates.
185
+ - [ ] For git-push tools, validate repo/branch against a fixed host-side allowlist.
186
+ - [ ] Keep the receipt key secret and persistent.
187
+ - [ ] Run `myxo plan` as a preflight gate.
188
+ - [ ] Set conservative `maxSteps`/`timeoutMs` for untrusted scripts.
189
+ - [ ] Log or store the audit ledger/receipt from every run, including failed runs.
190
+
191
+ ## See also
192
+
193
+ - `examples/james-myxo-demo.js` — runnable demo of the JAMES surface without JAMES up.
194
+ - `test/james-integration.test.js` — pinned contract tests for the JAMES surface.
195
+ - `receipt.js` — receipt chain/seal/verify implementation.
196
+ - `HOW_IT_WORKS.md` — Myxo architecture and security boundary.
package/README.md CHANGED
@@ -32,6 +32,15 @@ reinforce i < 10 {
32
32
  }
33
33
  ```
34
34
 
35
+ ## Install
36
+
37
+ ```
38
+ npm i -g myxo-lang # gives you the `myxo` command (Node 18+, zero dependencies)
39
+ myxo # multi-line REPL
40
+ ```
41
+
42
+ Or run from a clone: `node myxo.js examples/fib.myx`.
43
+
35
44
  ## Run it
36
45
 
37
46
  ```
@@ -0,0 +1,32 @@
1
+ # garden.myx - written by ox-alpha on its hour off the leash.
2
+ # Three eaters compete for the same sunlight. Nobody programs the outcome:
3
+ # success reinforces a tube, failure decays it, everything passively fades.
4
+ # The garden prunes itself. That is the whole thesis of the house.
5
+
6
+ agent greedy(x) { report x * 2 }
7
+ # patient does real work for its meal (impure, so memoization can't cheat it)
8
+ agent patient(x) { seed stir = random() seed s = x reinforce 200000 times { s = s + 0 } report x * 2 }
9
+ agent sleepy(x) { fail "still asleep" }
10
+
11
+ seed light = route("sun", [sleepy, patient, greedy])
12
+
13
+ reinforce 6 times {
14
+ emit "photosynthesis:", light(21)
15
+ }
16
+
17
+ emit ""
18
+ emit "== after six mornings, the mesh remembers =="
19
+ emit "conductivities:", flows("sun")
20
+
21
+ # heat is visible too: attention makes pathways stronger
22
+ seed moss = "quiet"
23
+ reinforce 4 times { emit "moss grows...", moss }
24
+ emit "strength(moss):", strength("moss")
25
+
26
+ # and winter comes for whatever nobody touched
27
+ seed fern = "untouched"
28
+ seed winter = metabolize(1)
29
+ emit "winter reaped:", winter["reaped"]
30
+
31
+ emit ""
32
+ emit "the garden keeps only what reaches for the light."
@@ -1,4 +1,4 @@
1
- # A plain Perl module — Nx calls these subs as fenced capabilities via plcall, no knowledge of Perl required.
1
+ # A plain Perl module — Myxo calls these subs as fenced capabilities via plcall, no knowledge of Perl required.
2
2
  sub add { my ($a, $b) = @_; return $a + $b; }
3
3
 
4
4
  sub stats {
@@ -1,4 +1,4 @@
1
- # A plain Python module — Nx will call these as fenced capabilities, no knowledge of Python required.
1
+ # A plain Python module — Myxo will call these as fenced capabilities, no knowledge of Python required.
2
2
 
3
3
  def add(a, b):
4
4
  return a + b
@@ -16,7 +16,7 @@ def spoof(x):
16
16
 
17
17
 
18
18
  def inf():
19
- return float("inf") # non-finite -> must become a clean rescuable Nx error, not a raw crash
19
+ return float("inf") # non-finite -> must become a clean rescuable Myxo error, not a raw crash
20
20
 
21
21
 
22
22
  def echo(x):
@@ -0,0 +1,40 @@
1
+ # ox-alpha-learn.myx - proof I learned the law.
2
+ # Written by ox-alpha after reading SPEC.md and running the examples.
3
+
4
+ # --- agents & recursion (the hot pathway will promote this on its own) ---
5
+ agent fib(n) {
6
+ when n < 2 { report n }
7
+ report fib(n - 1) + fib(n - 2)
8
+ }
9
+
10
+ agent square(x) { report x * x }
11
+
12
+ # pipeline sugar: value flows left to right through pure agents
13
+ emit "pipeline 7 | square | fib =>", (7 | square | fib)
14
+
15
+ # --- pattern matching: lists ---
16
+ seed pair = [3, 4]
17
+ match pair {
18
+ [a, b] { emit "pair destructured, sum:", a + b }
19
+ _ { emit "no pair here" }
20
+ }
21
+
22
+ # --- pattern matching: meshes (subset match, binds shorthand keys) ---
23
+ seed hunt = { "name": "hound", "hits": 531, "band": "6ghz" }
24
+ match hunt {
25
+ { name, hits } { emit "record:", name, "-", hits, "unique aps" }
26
+ _ { emit "empty record" }
27
+ }
28
+
29
+ # --- concurrency: isolated workers crossing plain data by value ---
30
+ seed results = gather [dispatch fib(12), dispatch square(9)]
31
+ emit "gather (dispatch order):", results
32
+
33
+ # --- the living mesh, watched from inside ---
34
+ seed watched = 41
35
+ reinforce 3 times { emit "heating...", watched }
36
+ emit "strength(watched):", strength("watched")
37
+
38
+ seed cold_one = "never touched again"
39
+ seed ledger = metabolize(2)
40
+ emit "reaped:", ledger["reaped"]
package/mcp-bridge.js CHANGED
@@ -35,12 +35,20 @@ function jsToNx(v) {
35
35
  // Unwrap that to the plain text a script wants; surface an error result as an MyxoError so
36
36
  // it lands in the audit ledger and an enclosing `attempt` can rescue it.
37
37
  function unwrapResult(res) {
38
- if (res && typeof res === 'object' && Array.isArray(res.content)) {
38
+ if (res === null || res === undefined) {
39
+ throw new MyxoError('MCP tool returned no result');
40
+ }
41
+ if (typeof res === 'string') return res;
42
+ if (typeof res === 'object' && Array.isArray(res.content)) {
39
43
  const text = res.content.filter(c => c && c.type === 'text').map(c => c.text).join('\n');
40
44
  if (res.isError) throw new MyxoError(text || 'MCP tool reported an error');
41
45
  return text;
42
46
  }
43
- return res;
47
+ if (typeof res === 'object') {
48
+ // A tool returned a structured object without the MCP envelope; stringify it for the script.
49
+ return JSON.stringify(res);
50
+ }
51
+ return String(res);
44
52
  }
45
53
 
46
54
  // Turn one Myxo call's args into the named-arguments object an MCP tool expects.
package/myxo-live.js CHANGED
@@ -13,19 +13,22 @@
13
13
 
14
14
  const { Worker, MessageChannel } = require('worker_threads');
15
15
  const path = require('path');
16
+ const { chain, sealReceipt } = require('./receipt');
16
17
 
17
- // runLive(script, opts) -> Promise<{ ok, output, audit, error }>
18
+ // runLive(script, opts) -> Promise<{ ok, output, audit, error, receipt? }>
18
19
  // opts.tools: [{ name, inputSchema }] the catalog to bridge as fenced capabilities
19
20
  // opts.onCall: async (name, argsObject) => result the real async invoker
20
21
  // opts.allow: optional host allowlist of tool names (defense in depth)
21
22
  // opts.dir, opts.maxDepth, opts.maxSteps, opts.requireManifest: passed through to the runner
22
23
  // opts.timeoutMs: wall-clock kill switch for the worker (default 30000)
24
+ // opts.receiptKey: optional Buffer; if provided, the audit ledger is sealed in the parent
23
25
  function runLive(script, opts = {}) {
24
26
  const { tools = [], onCall, allow, dir, maxDepth, valueCaps } = opts;
25
27
  const requireManifest = opts.requireManifest !== undefined ? !!opts.requireManifest : true;
26
28
  const maxSteps = opts.maxSteps !== undefined ? opts.maxSteps : 200000;
27
29
  const moduleLoader = opts.moduleLoader !== undefined ? opts.moduleLoader : null;
28
30
  const timeoutMs = Number.isFinite(opts.timeoutMs) && opts.timeoutMs > 0 ? opts.timeoutMs : 30000;
31
+ const receiptKey = opts.receiptKey;
29
32
  if (typeof onCall !== 'function') {
30
33
  return Promise.reject(new Error('runLive needs an async onCall(name, args)'));
31
34
  }
@@ -50,6 +53,10 @@ function runLive(script, opts = {}) {
50
53
  if (timer) clearTimeout(timer);
51
54
  port1.close();
52
55
  worker.terminate();
56
+ if (receiptKey && v && Array.isArray(v.audit)) {
57
+ const chained = chain(v.audit);
58
+ v.receipt = { entries: chained, seal: sealReceipt(chained, receiptKey) };
59
+ }
53
60
  fn(v);
54
61
  };
55
62
  timer = setTimeout(() => {
package/myxo-run.js CHANGED
@@ -9,8 +9,11 @@
9
9
  // 2. the script's `needs` manifest — the script declares what it will touch
10
10
  // 3. value budgets (`max`/`total`) — the script's own ceilings, runtime-enforced
11
11
  // Everything privileged lands in the audit ledger, returned even when the run fails.
12
+ //
13
+ // Optional: pass `receiptKey` to also receive a tamper-evident receipt over the audit ledger.
12
14
 
13
15
  const { run } = require('./myxo');
16
+ const { chain, sealReceipt } = require('./receipt');
14
17
 
15
18
  // Wrap a live client so only allow-listed tools exist, and a call to anything outside
16
19
  // the list is hard-stopped at the host boundary (belt to the script-manifest braces).
@@ -28,13 +31,14 @@ function gateClient(client, allow) {
28
31
  }
29
32
 
30
33
  // Run `script` with `client` bridged as fenced capabilities.
31
- // opts: { client, allow, dir, maxDepth, maxSteps, natives, requireManifest, moduleLoader }
32
- // returns: { ok, output, audit, error }
34
+ // opts: { client, allow, dir, maxDepth, maxSteps, natives, requireManifest, moduleLoader, receiptKey }
35
+ // returns: { ok, output, audit, error, receipt? }
33
36
  function runScript(script, opts = {}) {
34
37
  let audit = [];
35
38
  const result = { ok: true, output: '', audit, error: null };
36
39
  const requireManifest = opts.requireManifest !== undefined ? !!opts.requireManifest : true;
37
40
  const moduleLoader = opts.moduleLoader !== undefined ? opts.moduleLoader : null;
41
+ const maxSteps = opts.maxSteps !== undefined ? opts.maxSteps : 200000;
38
42
  // Accumulate output OUTSIDE run() so it survives a failing script. With capture:true the chunks
39
43
  // died inside the throw and a failed run returned output:'' — everything the script emitted before
40
44
  // failing (a monitor's verdict, an agent's partial report) was silently dropped. Evidence survives.
@@ -44,7 +48,7 @@ function runScript(script, opts = {}) {
44
48
  output: (s) => { buf += s; },
45
49
  dir: opts.dir,
46
50
  maxDepth: opts.maxDepth,
47
- maxSteps: opts.maxSteps,
51
+ maxSteps,
48
52
  requireManifest,
49
53
  moduleLoader,
50
54
  natives: opts.natives,
@@ -57,6 +61,10 @@ function runScript(script, opts = {}) {
57
61
  result.error = typeof e.format === 'function' ? e.format() : e.message;
58
62
  }
59
63
  result.output = buf;
64
+ if (opts.receiptKey) {
65
+ const chained = chain(audit);
66
+ result.receipt = { entries: chained, seal: sealReceipt(chained, opts.receiptKey) };
67
+ }
60
68
  return result;
61
69
  }
62
70
 
package/package.json CHANGED
@@ -1,27 +1,27 @@
1
- {
2
- "name": "myxo-lang",
3
- "version": "1.5.0",
4
- "description": "Myxo — the language of the Nexus. A small, embeddable language whose runtime is the Nexus law: useful pathways reinforce, useless pathways decay.",
5
- "bin": {
6
- "myxo": "myxo.js"
7
- },
8
- "main": "myxo.js",
9
- "scripts": {
10
- "test": "node --test",
11
- "test:core": "node --test test/myxo.test.js test/format.test.js test/myxotest.test.js test/match.test.js test/pipe.test.js test/types.test.js test/lsp.test.js test/cli.test.js test/docs.test.js test/concurrent.test.js test/scheduler.test.js test/fibers.test.js test/examples.test.js test/plan.test.js test/mcp-framing.test.js test/mcp-server.test.js test/command-fence.test.js test/receipt.test.js test/soundness-fuzz.test.js",
12
- "test:polyglot": "node --test test/polyglot.test.js",
13
- "test:myxo": "node myxo.js test tests/",
14
- "lsp": "node myxo.js lsp",
15
- "docs": "node docs/build.js",
16
- "test:full": "node --test",
17
- "fmt": "node myxo.js fmt",
18
- "start": "node myxo.js"
19
- },
20
- "license": "MIT",
21
- "author": "Zero (an AI agent by Arc) with Milton Rojas",
22
- "homepage": "https://thefinalmilkman.github.io/myxo/",
23
- "repository": { "type": "git", "url": "git+https://github.com/thefinalmilkman/myxo.git" },
24
- "bugs": { "url": "https://github.com/thefinalmilkman/myxo/issues" },
25
- "engines": { "node": ">=18" },
26
- "keywords": ["nexus", "language", "interpreter", "myxo", "slime-mold", "embeddable", "capability-security", "zero-dependency", "concurrency"]
27
- }
1
+ {
2
+ "name": "myxo-lang",
3
+ "version": "1.5.2",
4
+ "description": "Myxo — the language of the Nexus. A small, embeddable language whose runtime is the Nexus law: useful pathways reinforce, useless pathways decay.",
5
+ "bin": {
6
+ "myxo": "myxo.js"
7
+ },
8
+ "main": "myxo.js",
9
+ "scripts": {
10
+ "test": "node --test",
11
+ "test:core": "node --test test/myxo.test.js test/format.test.js test/myxotest.test.js test/match.test.js test/pipe.test.js test/types.test.js test/lsp.test.js test/cli.test.js test/docs.test.js test/concurrent.test.js test/scheduler.test.js test/fibers.test.js test/examples.test.js test/plan.test.js test/mcp-framing.test.js test/mcp-server.test.js test/command-fence.test.js test/receipt.test.js test/soundness-fuzz.test.js",
12
+ "test:polyglot": "node --test test/polyglot.test.js",
13
+ "test:myxo": "node myxo.js test tests/",
14
+ "lsp": "node myxo.js lsp",
15
+ "docs": "node docs/build.js",
16
+ "test:full": "node --test",
17
+ "fmt": "node myxo.js fmt",
18
+ "start": "node myxo.js"
19
+ },
20
+ "license": "MIT",
21
+ "author": "Zero (an AI agent by Arc) with Milton Rojas",
22
+ "homepage": "https://thefinalmilkman.github.io/myxo/",
23
+ "repository": { "type": "git", "url": "git+https://github.com/thefinalmilkman/myxo.git" },
24
+ "bugs": { "url": "https://github.com/thefinalmilkman/myxo/issues" },
25
+ "engines": { "node": ">=18" },
26
+ "keywords": ["nexus", "language", "interpreter", "myxo", "slime-mold", "embeddable", "capability-security", "zero-dependency", "concurrency"]
27
+ }
package/tools/lane.js ADDED
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // lane.js — the yield sign for concurrent agent/human sessions in this repo (zero-dep, per repo law).
4
+ // Before EDITING a file, claim its lane; if the claim fails, YIELD (do something else or ask).
5
+ // When done, release. Locks are tiny JSON files in .lane/ (gitignored) — atomic via exclusive create.
6
+ //
7
+ // node tools/lane.js claim <path...> --by <name> [--why "<what you're doing>"]
8
+ // node tools/lane.js check <path...> # exit 0 all clear, 1 = someone holds one
9
+ // node tools/lane.js release <path...> --by <name> # or: release --all --by <name>
10
+ // node tools/lane.js list # who holds what, since when
11
+ // node tools/lane.js log [--last N] # the timestamped journal of everything done
12
+ // node tools/lane.js clear-stale [--hours 6] # drop locks older than N hours (dead sessions)
13
+ //
14
+ // Release refuses to drop ANOTHER session's locks without --force. Paths are normalized repo-relative,
15
+ // so `interpreter.js` and `./interpreter.js` are the same lane.
16
+ //
17
+ // THE JOURNAL: every action (claim / yield / release / refuse / force-break / stale-sweep) is appended
18
+ // to .lane/journal.jsonl with an ISO timestamp — one JSON object per line, newest last. Nothing in this
19
+ // system happens without a timestamped record.
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+
23
+ const ROOT = path.join(__dirname, '..');
24
+ const LANE = path.join(ROOT, '.lane');
25
+ const JOURNAL = path.join(LANE, 'journal.jsonl');
26
+
27
+ const argv = process.argv.slice(2);
28
+ const cmd = argv[0];
29
+ const flag = (name, def) => { const i = argv.indexOf('--' + name); return i >= 0 ? argv[i + 1] : def; };
30
+ const has = name => argv.indexOf('--' + name) >= 0;
31
+ const paths = argv.slice(1).filter(a => !a.startsWith('--') && a !== flag('by') && a !== flag('why') && a !== flag('hours') && a !== flag('last'));
32
+
33
+ const iso = () => new Date().toISOString(); // UTC ISO — same stamp convention as myxo-evals results
34
+ function rel(p) {
35
+ const r = path.relative(ROOT, path.resolve(ROOT, p));
36
+ return r.split(path.sep).join('/');
37
+ }
38
+ function key(r) { return r.toLowerCase().replace(/[^a-z0-9]+/g, '_') + '.json'; }
39
+ function ensureLane() { fs.mkdirSync(LANE, { recursive: true }); }
40
+ function readLock(f) { try { return JSON.parse(fs.readFileSync(path.join(LANE, f), 'utf8')); } catch { return null; } }
41
+ function ageMin(ts) { return Math.round((Date.now() - ts) / 60000); }
42
+
43
+ // One journal line per thing done. Best-effort: a journaling failure never breaks the lane op itself.
44
+ function journal(act, by, lockPath, detail) {
45
+ const line = JSON.stringify({ ts: iso(), act, by: by || null, path: lockPath || null, detail: detail || null });
46
+ try { fs.appendFileSync(JOURNAL, line + '\n'); } catch { /* journal is a record, not a gate */ }
47
+ }
48
+
49
+ function main() {
50
+ ensureLane();
51
+ if (cmd === 'claim') {
52
+ const by = flag('by'), why = flag('why', '');
53
+ if (!by) { console.error('claim needs --by <name>'); process.exit(2); }
54
+ if (!paths.length) { console.error('claim needs at least one path'); process.exit(2); }
55
+ const held = [];
56
+ for (const p of paths) {
57
+ const r = rel(p), f = path.join(LANE, key(r));
58
+ try {
59
+ fs.writeFileSync(f, JSON.stringify({ path: r, by, why, ts: Date.now(), at: iso() }, null, 2), { flag: 'wx' });
60
+ journal('claim', by, r, why || null);
61
+ console.log('CLAIMED ' + r + ' by ' + by + ' @ ' + iso());
62
+ } catch (e) {
63
+ if (e.code !== 'EEXIST') throw e;
64
+ const lock = readLock(key(r));
65
+ held.push(r + ' held by ' + (lock ? lock.by + ' (since ' + (lock.at || '?') + ', ' + ageMin(lock.ts) + 'm' + (lock.why ? ', ' + lock.why : '') + ')' : '?'));
66
+ }
67
+ }
68
+ if (held.length) {
69
+ journal('yield', by, null, 'blocked on: ' + held.map(h => h.split(' ')[0]).join(', '));
70
+ console.error('\nYIELD — lane(s) taken:\n ' + held.join('\n '));
71
+ // roll back any lanes this call DID claim: a failed batch must hold nothing (all-or-nothing)
72
+ for (const p of paths) {
73
+ const r = rel(p), lock = readLock(key(r));
74
+ if (lock && lock.by === by && !held.some(h => h.startsWith(r + ' '))) {
75
+ try { fs.unlinkSync(path.join(LANE, key(r))); } catch { /* gone */ }
76
+ journal('rollback', by, r, 'batch failed — released');
77
+ }
78
+ }
79
+ process.exit(1);
80
+ }
81
+ } else if (cmd === 'check') {
82
+ if (!paths.length) { console.error('check needs at least one path'); process.exit(2); }
83
+ let blocked = false;
84
+ for (const p of paths) {
85
+ const r = rel(p), lock = readLock(key(r));
86
+ if (lock) { blocked = true; console.log('TAKEN ' + r + ' by ' + lock.by + ' (since ' + (lock.at || '?') + ', ' + ageMin(lock.ts) + 'm' + (lock.why ? ', ' + lock.why : '') + ')'); }
87
+ else console.log('CLEAR ' + r);
88
+ }
89
+ process.exit(blocked ? 1 : 0);
90
+ } else if (cmd === 'release') {
91
+ const by = flag('by');
92
+ if (!by) { console.error('release needs --by <name>'); process.exit(2); }
93
+ const targets = has('all')
94
+ ? fs.readdirSync(LANE).filter(f => f.endsWith('.json')).map(f => readLock(f)).filter(Boolean).map(l => l.path)
95
+ : paths.map(rel);
96
+ if (!targets.length) { console.log('nothing to release'); return; }
97
+ for (const r of targets) {
98
+ const lock = readLock(key(r));
99
+ if (!lock) { console.log('FREE ' + r + ' (was not locked)'); continue; }
100
+ if (lock.by !== by && !has('force')) {
101
+ journal('refuse', by, r, 'held by ' + lock.by);
102
+ console.error('REFUSE ' + r + ' held by ' + lock.by + ' (use --force to break)'); process.exitCode = 1; continue;
103
+ }
104
+ fs.unlinkSync(path.join(LANE, key(r)));
105
+ journal(lock.by !== by ? 'force-break' : 'release', by, r, lock.by !== by ? 'broke ' + lock.by + "'s lock (held since " + (lock.at || '?') + ')' : null);
106
+ console.log('RELEASED ' + r + ' @ ' + iso() + (lock.by !== by ? ' (force-broken from ' + lock.by + ')' : ''));
107
+ }
108
+ } else if (cmd === 'list') {
109
+ const files = fs.readdirSync(LANE).filter(f => f.endsWith('.json'));
110
+ if (!files.length) { console.log('no lanes held — the road is open'); return; }
111
+ for (const f of files) {
112
+ const l = readLock(f);
113
+ if (l) console.log(l.path + ' by ' + l.by + ' since ' + (l.at || '?') + ' (' + ageMin(l.ts) + 'm)' + (l.why ? ' — ' + l.why : ''));
114
+ }
115
+ } else if (cmd === 'log') {
116
+ if (!fs.existsSync(JOURNAL)) { console.log('journal is empty — nothing has happened yet'); return; }
117
+ const lines = fs.readFileSync(JOURNAL, 'utf8').split('\n').filter(Boolean);
118
+ const last = Number(flag('last', 0)) || 0;
119
+ const show = last > 0 ? lines.slice(-last) : lines;
120
+ for (const line of show) {
121
+ let e; try { e = JSON.parse(line); } catch { continue; }
122
+ console.log(e.ts + ' ' + String(e.act).padEnd(11) + ' ' + (e.by || '?').padEnd(12) + (e.path ? ' ' + e.path : '') + (e.detail ? ' — ' + e.detail : ''));
123
+ }
124
+ } else if (cmd === 'clear-stale') {
125
+ const hours = Number(flag('hours', 6));
126
+ const cutoff = Date.now() - hours * 3600000;
127
+ let n = 0;
128
+ for (const f of fs.readdirSync(LANE).filter(f => f.endsWith('.json'))) {
129
+ const l = readLock(f);
130
+ if (l && l.ts < cutoff) {
131
+ fs.unlinkSync(path.join(LANE, f)); n++;
132
+ journal('stale-sweep', '(sweeper)', l.path, 'was ' + l.by + ', held since ' + (l.at || '?'));
133
+ console.log('STALE ' + l.path + ' (was ' + l.by + ', ' + ageMin(l.ts) + 'm)');
134
+ }
135
+ }
136
+ if (!n) console.log('no stale lanes (older than ' + hours + 'h)');
137
+ } else {
138
+ console.log('usage: node tools/lane.js <claim|check|release|list|log|clear-stale> [paths...] [--by name] [--why text] [--all] [--force] [--hours N] [--last N]');
139
+ process.exit(2);
140
+ }
141
+ }
142
+
143
+ main();