Guildhall Lanterns: reply urgency frames and Calm smithy presentation.

Run a team of AI coding agents on your own machines, and stay in charge without babysitting them.
You tell a Director what you want in plain English. It plans the work, hands each piece to an agent working in its own copy of your repository, has the result reviewed, and only interrupts you when it needs a decision: accept this plan, answer this question, merge this change. Everything is written down as it happens, so closing a terminal or restarting your machine never loses work.
Status: early, not ready for real work. Guildhall builds and its tests pass in CI on Linux, Windows and macOS, but no agent runtime or platform is supported for real work yet. The conversational Director is built and exercised in CI against stand-ins, but has not yet run a real task; see What works today. What you can also try today is driving the same steps by hand with the
guildhallcommands; see Try it today.
Coding agents are good at doing one task while you watch. Guildhall is for when you want several of them working while you don't:
These are the kinds of jobs Guildhall is designed for:
You will talk to your Director, and never need to type a command to give work, answer a decision or approve a merge. This is the standing rule, recorded in plain English to the Director. The session below is illustrative, not a transcript of a real task: the conversational Director is built, but live acceptance of this experience is still pending.
You: Add a health check endpoint to the app repository.
Director: Created task t-2 in
app. I'll research and plan it, then bring you the plan to accept.(later)
You: What needs me?
Director: One decision: the plan for t-2 is ready. It adds
/healthzand a test. Accept it?You: Yes, but keep the migration behind a flag.
Director: Recorded. Building t-2 now; I'll come back when it has been reviewed.
(later)
Director: t-2 passed review. Ready to merge?
You: Merge it. And stop the build on t-3; I've changed my mind.
Behind the scenes each task follows a workflow; the default one is research, plan, your approval, build, independent review, then delivery under the repository's merge policy. You can change it per repository, for example to review changes yourself instead of an agent. See configuration and workflows.
git.claude, subject to the skill-policy compatibility requirements) for the minimal Claude adapter, gh for publishing and merging pull requests on GitHub, ssh for Steward rollouts, and Herdr, a terminal multiplexer, for showing agents in tabs.guildhall doctor tells you which of git, ssh, claude and herdr it can find; it does not check for Go or gh.
git clone https://github.com/ndrwl/guildhall.git
cd guildhall
go build -o guildhall ./cmd/guildhall
./guildhall version
./guildhall doctor
$ ./guildhall doctor
os: linux
arch: amd64
guildhall: v0.0.0-20261020184157-8090cc664d0a
go: go1.27.2
...
A missing tool is reported, not treated as an error. Prebuilt binaries for Linux, Windows and macOS, with a SHA256SUMS file, will be published for version tags; side-by-side install and rollback is planned (release upgrades).
You can also run the steps the Director runs yourself. A Director's copy is a directory holding its config.toml, its store director.store and its state/ directory. The quickest look needs nothing but the binary: create an empty store with store init, which refuses anything already at that path, and inspect it.
mkdir -p demo/state
./guildhall store init demo/director.store
./guildhall task list --copy demo --json
./guildhall decide list --copy demo
./guildhall store doctor --copy demo
Every command that takes a store accepts --copy demo in place of --store demo/director.store, --config demo/config.toml and --state demo/state, or reads the copy from the environment variable GUILDHALL_COPY, which must be an absolute path; see copies.
To go further (create a task in your own repository, launch an agent for a stage, answer its decisions, send messages, preview a Steward rollout), follow the walkthrough. It needs a small Director config naming your repository; see configuration and workflows.
| Status | |
|---|---|
| Creating, inspecting and timing tasks; answering decisions; local messages | Built, driven by hand with commands |
| Advancing a task stage by stage and launching an agent for each stage | Built, with a fake agent for tests and demos and a minimal Claude adapter whose live acceptance is pending |
Publishing a task's pull request, waiting for its CI checks and merging it through gh | Built, exercised in CI against a stand-in gh; live GitHub behaviour is not claimed |
| Steward rollouts of shared settings over SSH | Built, preview and apply |
The conversational Director (guildhall director start) | Built in its own Herdr tab or the foreground, exercised in CI against a stand-in Claude Code and a stand-in terminal; pause, resume, stop, handoff notes, reload and the automatic handoff triggers are built; see tab acceptance status |
| Running stages that the Director itself performs, and background watching of running agents | Built: the Director finishes its own stages with step finish, and each attempt's launcher advances its task when the attempt ends and reports a stalled worker |
| Docker isolation for agents | Planned |
| Any runtime or platform supported for real work | Not yet: each needs to pass live acceptance first |
The commands are the Director's tools; you can also call them yourself to script, inspect or drive work by hand. guildhall help lists them, and each links to its own page.
guildhall version and guildhall doctor: version, platform and which tools are installed.guildhall task new|show|list|timeline|files|finding rule|review: create and inspect tasks; see tasks.guildhall task start and guildhall step done: advance a task through its workflow; see running a task.guildhall step retry: start the next attempt of a step after a failed or stopped one; see running a task.guildhall decide list|show|answer: decisions waiting for you; see decisions.guildhall director status --json: everything in flight and what needs the Director or you, in one JSON document; see the Director.guildhall director start and guildhall director ready: run a Director session in its own Herdr tab or your terminal, and let a recovering session start new work; see the Director.guildhall director pause, guildhall director resume and guildhall director stop: hold new work while running attempts finish, lift the hold, and end the session and its attempts; see the Director.guildhall director handoff, guildhall director reload, guildhall director turn-started and guildhall director turn-ended: record a handoff note for the next session, restart the session at a quiet point or the end of the turn, and the runtime's turn-start and end-of-turn hooks; see the Director.guildhall director context-report: the Director's status-line command, display-only with --lanterns; see Lanterns and legacy context reporting.guildhall artisan start|stop|status|resume: control one agent attempt; guildhall fake-artisan and guildhall claude-artisan are the fake and Claude wrappers; see Artisan commands and adapters.guildhall messages send|list|read|ack: informational messages between the Director and its agents; see messaging.guildhall quality judge|judged|agreement: judge a delivered task's self-review in a separate session, take its judgement, and measure the judge against your audits; see the quality rubric.guildhall workflow approve|approvals: approve workflow changes; see workflow-edit authority.guildhall steward rollout: preview and apply shared settings to enrolled machines; see Steward rollouts.guildhall render <request.json>: render an agent prompt with its skills; see prompt rendering.guildhall store init <path> and guildhall store doctor <path>: create an empty store file, or check one without changing it; see the store.guildhall help: list the commands.See CONTRIBUTING.md for local checks and how changes are reviewed, docs/review.md for the review standard, and docs/contracts.md for how contracts, interfaces, tests and packages are organised.
hooks/register.js 85 lines1import {urgency,smithy,raster} from '../lib/presentation.mjs';
2
3// Engine operations are kept here. Geometry and urgency classification are pure.
4// Each module lifetime owns its state; the preference survives Director sessions.
5let activated,loading,preference,calm=false,ticker,tick=0,theme='light';
6 const sites=new Map();
7 const key='guildhall-lanterns-smithy';
8 const active=$=>activated??= $.env.get('CLAUDE_CODE_ENABLE_FUNCTION_HOOKS').then(v=>v==='1',()=>false);
9 function timer($) {
10 if(!calm||ticker)return;
11 ticker=$.clock.every(220,async()=>{
12 if(!calm||sites.size===0)return;
13 tick++;
14 for(const [requestId,columns] of sites) {
15 try {
16 const packed=raster(smithy(columns,tick),columns,theme);
17 const result=await $.ui.blit({requestId,key,columns,...packed});
18 if(result.deny!==undefined)sites.delete(requestId);
19 } catch {sites.delete(requestId)}
20 }
21 });
22 }
23 async function load($) {
24 if(!loading)loading=(async()=>{
25 const copy=await $.env.get('GUILDHALL_COPY');
26 preference=copy?copy+'/director/lanterns-calm':undefined;
27 try {calm=!!preference&&await $.fs.exists(preference)&&(await $.fs.read(preference)).trim()==='on'}catch {calm=false}
28 try {theme=(await $.config.list()).find(r=>r.key==='theme')?.value??'light'}catch {}
29 timer($);
30 })();
31 await loading;
32 }
33export function register(on) {
34 activated=undefined;loading=undefined;preference=undefined;calm=false;ticker=undefined;tick=0;theme='light';sites.clear();
35 on('session.start',async($,e,next)=>{
36 if(!await active($))return next(e);
37 await load($);
38 if(preference)await $.command.register({name:'calm',description:'Toggle Lanterns Calm tool presentation and smithy chimney.'});
39 return next(e);
40 });
41 on('command.run',{command:'calm'},async($,e,next)=>{
42 if(!await active($))return next(e);
43 await load($);
44 if(!preference)return next(e);
45 try {await $.fs.write(preference,calm?'off\n':'on\n')}catch {
46 $.ui.toast('Calm unchanged: could not save the Director preference.');return {};
47 }
48 calm=!calm;
49 if(!calm){sites.clear();ticker?.cancel();ticker=undefined}else timer($);
50 $.ui.invalidate('ui.render');$.ui.toast(calm?'Calm on':'Calm off');return {};
51 });
52 on('config.set',{key:'theme'},async($,e,next)=>{
53 if(!await active($))return next(e);
54 const result=await next(e);
55 if(result.deny===undefined){theme=result.value;$.ui.invalidate('ui.render')}
56 return result;
57 });
58 on('ui.render',{component:'AssistantMessage'},async($,e,next)=>{
59 const original=await next(e);
60 if(!await active($)||!e.props?.text)return original;
61 try {
62 const {Box}=$.ui.resolve(e);
63 return Box({borderStyle:'round',borderColor:urgency(e.props.text),flexDirection:'column',paddingX:1,children:original});
64 }catch {return original}
65 });
66 for(const component of ['ToolUse','ToolResult','ToolGroup'])on('ui.render',{component},async($,e,next)=>{
67 if(!await active($))return next(e);
68 await load($);
69 if(!calm)return next(e);
70 const {Box}=$.ui.resolve(e);return Box({display:'none'});
71 });
72 on('ui.render',{component:'Spinner'},async($,e,next)=>{
73 if(!await active($))return next(e);
74 await load($);
75 if(!calm||e.surface!=='terminal'){sites.delete(e.requestId);return next(e)}
76 const columns=Math.max(1,Math.min(512,Math.floor(e.viewport?.columns??80)-2));
77 try {
78 const {Box,Raster}=$.ui.resolve(e);
79 const packed=raster(smithy(columns,tick),columns,theme);
80 sites.set(e.requestId,columns);
81 return Box({flexDirection:'column',children:Raster({key,columns,...packed})});
82 }catch {sites.delete(e.requestId);return next(e)}
83 });
84}
85lib/presentation.mjs 43 lines1// Pure geometry and encoding: no engine, filesystem or process dependencies.
2export function urgency(text) {
3 return ({'🔴':'red','🟡':'yellow','🟢':'green'})[Array.from(String(text ?? '').trimStart()).slice(0,1).join('')] ?? 'gray';
4}
5
6export function smithy(columns, tick) {
7 const width=Math.max(1,Math.min(512,Math.floor(columns)));
8 const rows=Array.from({length:5},()=>Array(width).fill(' '));
9 const x=Math.max(0,Math.floor((width-7)/2));
10 const put=(r,c,text)=>{for(const ch of text){if(c>=0&&c<width)rows[r][c]=ch;c++}};
11 put(2,x+2,'│'); put(3,x,'▁▟███▙▁');
12 put(4,x+3,Math.floor(tick/4)%2===0?'▄':'▁');
13 const phase=tick%4;
14 put(phase===0?1:0,x+2+phase,['○','○','◌','·'][phase]);
15 return rows.map(row=>row.join(''));
16}
17
18function base64(bytes) {
19 const alphabet='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
20 let out='';
21 for(let i=0;i<bytes.length;i+=3) {
22 const word=(bytes[i]<<16)|((bytes[i+1]??0)<<8)|(bytes[i+2]??0);
23 out+=alphabet[(word>>>18)&63]+alphabet[(word>>>12)&63]+(i+1<bytes.length?alphabet[(word>>>6)&63]:'=')+(i+2<bytes.length?alphabet[word&63]:'=');
24 }
25 return out;
26}
27
28export function raster(frame,columns,theme) {
29 const bytes=new Uint8Array(columns*frame.length*12);
30 const view=new DataView(bytes.buffer);
31 const ambient=typeof theme==='string'&&theme.startsWith('dark')?0x93a5ff:0x5769f7;
32 for(let row=0;row<frame.length;row++) {
33 const glyphs=Array.from(frame[row]);
34 for(let col=0;col<columns;col++) {
35 const ch=glyphs[col]??' ';const offset=(row*columns+col)*12;
36 view.setUint32(offset,ch.codePointAt(0),true);
37 view.setUint32(offset+4,row===4?0xd77757:ambient,true);
38 view.setUint32(offset+8,0x01000000,true);
39 }
40 }
41 return {rows:frame.length,cells:base64(bytes)};
42}
43