SLOPSHOPPER

ailang-lens

Live view of AILANG as it is written: after each edit of a .ail file, a pane shows the module's functions, their types and effect rows (pure or !{IO,FS,...})…

newpaneguardcommandstatusprocess
★ 34v0.1.0Apache-2.0updated 2026-10-07sunholo-data/ailang/tools/claude-mods/ailang-lens
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · ailang-lens
│ ┃ AILANG lens ✕ › fix the failing auth test and add an audit log call │ ┃ No AILANG edited yet. │ ┃ Edit a .ail file, or /ail-lens ⏺ Read(src/auth.ts) │ ┃ path/to/file.ail ⎿ Read 6 lines │ ⏺ Update(src/auth.ts) │ ⎿ Added 2 lines, removed 1 line │ ⏺ Bash(bun test) │ ⎿ 3 pass, 1 fail │ │ ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ › /ail-lens │ ⎿ ailang-lens: AILANG lens opened. │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · AILANG lens
No AILANG edited yet. Edit a .ail file, or /ail-lens path/to/file.ail
README

<img src="docs/static/img/ailang-logo.svg" alt="AILANG Logo" width="128" height="128">

AILANG: The Deterministic Language for AI Coders

<!-- EXAMPLES_STATUS_START --> Examples License

Example status <!-- EXAMPLES_STATUS_END -->

Reliability Security Maintainability CodeQL OpenSSF Scorecard OpenSSF Best Practices

Third-party verification. AILANG is written autonomously by AI agents via its own coordinator. The badges above are independent static-analysis and supply-chain scores — not self-reported. The benchmark dashboard publishes the live correctness signal across current benchmark and model cohorts.

AILANG is a purely functional, effect-typed language designed as a deterministic execution substrate for AI-generated code. Every construct has deterministic semantics that can be reflected, verified, and serialized.

Documentation | Examples | Live Demos | Vision | Benchmarks


Quick Start

AILANG is designed to be used by AI coding agents. The AILANG Bootstrap packages the current language guidance, reusable skills, and local MCP tools for Claude Code and OpenAI Codex.

Install the AILANG CLI first:

curl -fsSL https://ailang.sunholo.com/install.sh | bash
ailang --version

The agent integrations call the local ailang executable, so it must be available on PATH.

With Claude Code

/plugin marketplace add sunholo-data/ailang_bootstrap
/plugin install ailang@sunholo-data/ailang_bootstrap

With OpenAI Codex

AILANG Bootstrap also ships a Codex plugin manifest. Add its stable marketplace:

# Required by the plugin's local MCP server on first launch
node --version
npm --version

codex plugin marketplace add sunholo-data/ailang_bootstrap --ref stable
codex plugin add ailang@ailang-marketplace
codex plugin list

Start a new Codex session after installation. Alternatively, after adding the marketplace, launch codex, enter /plugins, and install ailang from the AILANG marketplace. The plugin can also be installed from the Plugins directory in the Codex desktop app. Codex plugins are not currently available in the IDE extension.

Codex reads this repository's AGENTS.md automatically, whether or not the plugin is installed. The plugin adds the reusable AILANG skills and MCP tools; AGENTS.md supplies repository-specific workflows and guardrails.

What the Agent Package Provides

  • CLI integration - Skills and tools use the separately installed ailang executable
  • MCP tools - ailang_prompt, ailang_check, ailang_run, ailang_builtins, ailang_eval
  • Agent skills - AILANG authoring, debugging, messaging, design-doc, and sprint workflows
  • Teaching guidance - Current syntax rules via repository guidance, skills, and ailang prompt

The packaging is intentionally native to each host:

CapabilityClaude CodeCodex
Persistent guidanceCLAUDE.mdAGENTS.md
Reusable workflowsPlugin skillsPlugin skills
Local AILANG MCP toolsYesYes
Host-native commandsClaude slash commandsSkills and MCP tools
AILANG Bootstrap hooksClaude hooksNot currently packaged

Once installed, ask the agent to write AILANG code. All agents should run ailang prompt before authoring .ail files; it is the installed CLI's source for current syntax and idioms.

See ailang_bootstrap for details.

The installer detects your OS and architecture automatically. To pin a release:

curl -fsSL https://ailang.sunholo.com/install.sh | VERSION=v0.33.1 bash
# macOS (Apple Silicon)
curl -L https://github.com/sunholo-data/ailang/releases/latest/download/darwin.arm64.ailang.tar.gz | tar -xz
sudo mv ailang /usr/local/bin/

# macOS (Intel)
curl -L https://github.com/sunholo-data/ailang/releases/latest/download/darwin.x64.ailang.tar.gz | tar -xz
sudo mv ailang /usr/local/bin/

# Linux
curl -L https://github.com/sunholo-data/ailang/releases/latest/download/linux.x64.ailang.tar.gz | tar -xz
sudo mv ailang /usr/local/bin/

# From source
git clone https://github.com/sunholo-data/ailang.git
cd ailang && make install

# Verify
ailang --version

For complete setup instructions, see the Getting Started Guide.

Hello World

module examples/hello_world

import std/io (println)

export func main() -> () ! {IO} {
  println("Hello, World!")
}
ailang run --caps IO examples/hello_world.ail
# Output: Hello, World!

Interactive REPL

ailang repl

λ> 1 + 2
3 :: Int

λ> let double = \x. x * 2 in double(21)
42 :: Int

λ> :type \x. x + x
\x. x + x :: ∀α. Num α ⇒ α → α

λ> :quit

Key Features

  • Pure functional - Lambda calculus, closures, pattern matching, ADTs
  • Type inference - Hindley-Milner with row polymorphism
  • Effect system - Capability-based security (IO, FS, Net, Clock, AI)
  • Deterministic - Replayable execution, structured traces
  • AI-first - Designed for machine reasoning, not human convenience
  • LSP - ailang lsp --stdio ships in the binary: diagnostics, hover types, go-to-def, references, document symbols. One-command VS Code install: ailang editor install vscode (guide)

Learn more: Why AILANG? | No Loops Design | Go Interop | Stability Promise (1.x)


Development

make install    # Build and install
make test       # Run all tests
make repl       # Start REPL
make lint       # Run linter

Guides:


Project Structure

ailang/
├── cmd/ailang/     # CLI
├── internal/       # Compiler (lexer, parser, types, eval, effects)
├── std/            # Standard library (std/io, std/fs, std/json, std/zip, std/xml, etc.)
├── examples/       # Example and reference programs
├── docs/           # Documentation website source
└── design_docs/    # Design documents

License

Apache 2.0 - See LICENSE

AILANG draws inspiration from Haskell, OCaml, Rust, and Idris/Agda.


For AI agents: Deterministic functional language with Hindley-Milner type inference, algebraic effects, and explicit effect tracking. Read AGENTS.md, then use ailang prompt for current language guidance.

Source 2 files
hooks/register.tsx 290 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { LensFunc, LensModule } from '../types'
5
6const PANE = 'ailang-lens'
7const modules = atom({ plugin: 'ailang-lens', key: 'modules' } as const, [])
8
9// First JSON object in a stream that may carry warnings before it.
10function parseJson(text: string): any {
11  const at = text.indexOf('{')
12  if (at < 0) return undefined
13  try {
14    return JSON.parse(text.slice(at))
15  } catch {
16    return undefined
17  }
18}
19
20function typeName(t: unknown): string {
21  if (typeof t === 'string') return t
22  if (t && typeof t === 'object' && 'name' in t) return String((t as { name: unknown }).name)
23  return JSON.stringify(t)
24}
25
26// "type error in m (decl 0): type unification failed at [... at m.ail:3:8]: cannot unify ..."
27// → "3:8 cannot unify ..."
28function shortError(message: string): string {
29  const where = message.match(/\.ail:(\d+:\d+)/)?.[1]
30  const tail = message.includes(']: ')
31    ? message.slice(message.lastIndexOf(']: ') + 3)
32    : message.replace(/^type error in \S+ \(decl \d+\):\s*/, '')
33  return where ? `${where}  ${tail}` : message
34}
35
36async function exists($: EngineInterface, path: string): Promise<boolean> {
37  try {
38    await $.fs.stat(path)
39    return true
40  } catch {
41    return false
42  }
43}
44
45// ailang names a module by its path from the working directory (MOD010), so
46// run it from the project root: the nearest ailang.toml, else .git, else the
47// file's own folder.
48async function projectRoot($: EngineInterface, file: string): Promise<string> {
49  const dirs: string[] = []
50  for (let dir = file.slice(0, file.lastIndexOf('/')); dir; dir = dir.slice(0, dir.lastIndexOf('/'))) {
51    dirs.push(dir)
52  }
53  for (const marker of ['ailang.toml', '.git']) {
54    for (const dir of dirs) {
55      if (await exists($, `${dir}/${marker}`)) return dir
56    }
57  }
58  return dirs[0] ?? '.'
59}
60
61async function mtime($: EngineInterface, file: string): Promise<number> {
62  try {
63    return (await $.fs.stat(file)).mtimeMs
64  } catch {
65    return 0
66  }
67}
68
69/** Pure: the files a module imports by relative path (`import ./data/stars`). */
70export function relativeImports(file: string, source: string): string[] {
71  const dir = file.slice(0, file.lastIndexOf('/'))
72  return [...source.matchAll(/^\s*import\s+(\.{1,2}\/[\w./-]+)/gm)].map(m => {
73    const parts = `${dir}/${m[1]}.ail`.split('/')
74    const out: string[] = []
75    for (const part of parts) {
76      if (part === '..') out.pop()
77      else if (part !== '.') out.push(part)
78    }
79    return out.join('/')
80  })
81}
82
83// A module's result goes stale when the module, its package's manifest or
84// lock, or a module it imports by relative path changes: a package-qualified
85// import resolves through ailang.toml's [exports], so a manifest edit can
86// flip a check without the module itself changing.
87async function stampOf($: EngineInterface, file: string, root: string): Promise<number> {
88  let source = ''
89  try {
90    source = await $.fs.read(file)
91  } catch {
92    // gone: the stamp drops to 0 and the next check reports it
93  }
94  const paths = [file, `${root}/ailang.toml`, `${root}/ailang.lock`, ...relativeImports(file, source)]
95  return Math.max(...(await Promise.all(paths.map(path => mtime($, path)))))
96}
97
98async function analyse($: EngineInterface, file: string, previous?: LensModule): Promise<LensModule> {
99  const started = Date.now()
100  const cwd = file.startsWith('/') ? await projectRoot($, file) : undefined
101  const root = cwd ?? '.'
102  const stamp = await stampOf($, file, root)
103  const rel = cwd ? file.slice(cwd.length + 1) : file
104  const [iface, check] = await Promise.all([
105    $.process.run(['ailang', 'iface', rel], { cwd, timeoutMs: 20_000 }),
106    $.process.run(['ailang', 'check', '--json', '--quiet', rel], { cwd, timeoutMs: 60_000 }),
107  ])
108  const shape = iface.exitCode === 0 ? parseJson(iface.stdout) : undefined
109  const report = parseJson(check.stdout)
110
111  const errors: string[] = report?.errors?.length
112    ? report.errors.map((err: { message?: string }) => shortError(err.message ?? 'error'))
113    : check.exitCode === 0
114      ? []
115      : [shortError((check.stderr || check.stdout).trim().split('\n').pop() ?? 'check failed')]
116
117  // A broken edit keeps the last good shape on screen, beside the errors.
118  const funcs: LensFunc[] = shape
119    ? (shape.funcs ?? []).map((f: LensFunc) => ({ name: f.name, type: f.type, effects: f.effects ?? [] }))
120    : (previous?.funcs ?? [])
121
122  return {
123    file,
124    module: shape?.module ?? previous?.module ?? (file.split('/').pop() ?? file).replace(/\.ail$/, ''),
125    types: shape ? (shape.types ?? []).map(typeName) : (previous?.types ?? []),
126    funcs,
127    passed: errors.length === 0 && check.exitCode === 0,
128    errors,
129    ms: Date.now() - started,
130    root,
131    stamp,
132  }
133}
134
135async function refresh($: EngineInterface, file: string): Promise<LensModule> {
136  const known = await read($, modules)
137  const lens = await analyse($, file, known.find(m => m.file === file))
138  await update($, modules, list => [lens, ...list.filter(m => m.file !== file)].slice(0, 8))
139
140  const effects = [...new Set(lens.funcs.flatMap(f => f.effects))].sort()
141  $.ui.status(
142    lens.passed
143      ? `ail ✓ ${lens.module} · ${lens.funcs.length} fn · ${effects.length ? `!{${effects.join(',')}}` : 'pure'}`
144      : `ail ✗ ${lens.module} · ${lens.errors.length} error${lens.errors.length === 1 ? '' : 's'}`,
145  )
146  return lens
147}
148
149// Edit/Write are seen directly; anything else that touches a module, its
150// package manifest or a relative import (Bash, another session, the person's
151// editor) is caught by its stamp after a Bash call and at the end of each turn.
152async function refreshStale($: EngineInterface): Promise<void> {
153  for (const known of await read($, modules)) {
154    if ((await stampOf($, known.file, known.root)) !== known.stamp) await refresh($, known.file)
155  }
156}
157
158// An edited .ail or ailang.toml can change how its package's other modules
159// check (exports, a sibling's signature), so they are re-checked with it.
160async function refreshPackage($: EngineInterface, edited: string): Promise<void> {
161  for (const known of await read($, modules)) {
162    if (known.file !== edited && known.root !== '.' && edited.startsWith(`${known.root}/`)) await refresh($, known.file)
163  }
164}
165
166export const register: Register = on => {
167  on('session.start', async ($, e, next) => {
168    await $.command.register({
169      name: 'ail-lens',
170      description: 'Open the AILANG lens, or analyse a .ail file into it',
171      argumentHint: '[file.ail]',
172    })
173    return next(e)
174  })
175
176  on('command.run', { command: 'ail-lens' }, async ($, e) => {
177    const arg = e.args.trim()
178    if (!arg) {
179      await refreshStale($)
180      await $.ui.open({ id: PANE, title: 'AILANG lens' })
181      return { text: 'AILANG lens opened.' }
182    }
183    if (!arg.endsWith('.ail')) return { text: `ail-lens: ${arg} is not a .ail file.` }
184
185    // A relative path is the person's, typed against the session's folder.
186    const file = arg.startsWith('/') ? arg : `${await $.session.cwd()}/${arg.replace(/^\.\//, '')}`
187    if (!(await exists($, file))) return { text: `ail-lens: ${file} not found.` }
188
189    const lens = await refresh($, file)
190    await $.ui.open({ id: PANE, title: 'AILANG lens' })
191    return { text: `AILANG lens: ${lens.module} ${lens.passed ? '✓' : `✗ ${lens.errors.length} error(s)`}` }
192  })
193
194  // Every successful edit of a .ail file re-reads its interface and type-checks it.
195  on('tool.call', async ($, e, next) => {
196    const ran = await next(e)
197    if (e.tool === 'Bash') {
198      await refreshStale($).catch(() => undefined)
199      return ran
200    }
201    const file = (e.tool === 'Edit' || e.tool === 'Write') ? e.file_path : undefined
202    const isModule = file?.endsWith('.ail') === true
203    const isManifest = file?.endsWith('/ailang.toml') === true
204    if (!file || !(isModule || isManifest) || ran.deny !== undefined || ran.isError) return ran
205
206    try {
207      if (isModule) {
208        await refresh($, file)
209        void $.ui.open({ id: PANE, title: 'AILANG lens' })
210      }
211      await refreshPackage($, file)
212    } catch (err) {
213      $.ui.status(`ail lens: ${String(err).slice(0, 60)}`)
214    }
215    return ran
216  })
217
218  on('turn.complete', async ($, e, next) => {
219    const done = await next(e)
220    await refreshStale($).catch(() => undefined)
221    return done
222  })
223
224  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
225    const { Box, Text } = $.ui.resolve(e)
226    const [current, ...older] = await read($, modules)
227
228    if (!current) {
229      return (
230        <Box flexDirection="column">
231          <Text dimColor>No AILANG edited yet.</Text>
232          <Text dimColor>Edit a .ail file, or /ail-lens path/to/file.ail</Text>
233        </Box>
234      )
235    }
236
237    const width = Math.max(...current.funcs.map(f => f.name.length), 4)
238    return (
239      <Box flexDirection="column">
240        <Text>
241          <Text bold>{current.module}</Text>
242          <Text color={current.passed ? 'green' : 'red'}>{current.passed ? '  ✓ checks' : '  ✗ fails'}</Text>
243          <Text dimColor>{`  ${current.ms}ms`}</Text>
244        </Text>
245
246        {current.types.length > 0 && (
247          <Text>
248            <Text dimColor>types </Text>
249            <Text color="magenta">{current.types.join('  ')}</Text>
250          </Text>
251        )}
252
253        <Box flexDirection="column" marginTop={1}>
254          {current.funcs.map(f => (
255            <Box flexDirection="column">
256              <Text>
257                <Text bold>{f.name.padEnd(width)}</Text>
258                <Text color={f.effects.length ? 'yellow' : 'green'}>
259                  {f.effects.length ? `  !{${f.effects.join(',')}}` : '  pure'}
260                </Text>
261              </Text>
262              <Text dimColor wrap="truncate-end">{`  ${f.type}`}</Text>
263            </Box>
264          ))}
265        </Box>
266
267        {current.errors.length > 0 && (
268          <Box flexDirection="column" marginTop={1}>
269            {current.errors.map(err => (
270              <Text color="red" wrap="wrap">{`✗ ${err}`}</Text>
271            ))}
272            {current.funcs.length > 0 && <Text dimColor>(signatures above are the last good shape)</Text>}
273          </Box>
274        )}
275
276        {older.length > 0 && (
277          <Box flexDirection="column" marginTop={1}>
278            <Text dimColor>recent</Text>
279            {older.map(m => (
280              <Text dimColor>
281                {`${m.passed ? '✓' : '✗'} ${m.module} · ${m.funcs.length} fn`}
282              </Text>
283            ))}
284          </Box>
285        )}
286      </Box>
287    )
288  })
289}
290
types/index.d.ts 22 lines
1export type LensFunc = { name: string; type: string; effects: string[] }
2
3export type LensModule = {
4  file: string
5  module: string
6  types: string[]
7  funcs: LensFunc[]
8  passed: boolean
9  errors: string[]
10  ms: number
11  /** Package root the checks ran from (nearest ailang.toml, else .git). */
12  root: string
13  /** Newest mtime of the module, its package manifest/lock and its ./ imports. */
14  stamp: number
15}
16
17declare module 'claude-code' {
18  interface PluginState {
19    'ailang-lens': { modules: LensModule[] }
20  }
21}
22