SLOPSHOPPER

lanterns

Guildhall Lanterns: reply urgency frames and Calm smithy presentation.

newspinnerrowscommandtoasttimer
v0.1.0MITupdated 2026-10-26ndrwl/guildhall/internal/lanterns/plugin
A shopper browsing a rack in a slop shop
README

guildhall

CI Go version License

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 guildhall commands; see Try it today.

Why Guildhall

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:

  • One conversation, many agents. You talk to a Director, not to each agent. It keeps track of every task and agent for you.
  • You make the calls. Plans and questions wait for your answer, and so do merges unless you set a repository to merge automatically. Waiting costs no model tokens. Agents never merge on their own say-so.
  • Your rules hold. Each repository has its own merge policy and workflow, for example "a human reviews every change here", and a workflow cannot run arbitrary commands.
  • Nothing gets lost. Task state lives in a file on disk, not in an agent's chat history or a terminal tab.
  • Your machines, your agents. It runs on hardware you own and is built to work with agent tools such as Claude Code, so you can switch runtimes without losing work.

What you could use it for

These are the kinds of jobs Guildhall is designed for:

  • Hand off a backlog. "Add a health check endpoint, fix the flaky login test, and update the README." Each becomes its own task; independent tasks run in parallel.
  • Plan first, then build. Each task is researched and planned, and you accept the plan before any code is written.
  • Reviewed changes, not raw diffs. A second agent reviews each change against the plan you accepted before it comes to you to merge.
  • Several repositories, several machines. Run a Director for one or more repositories on each machine, and keep shared settings in step with an optional Steward.

How you will use it

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 /healthz and 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.

Requirements

  • Go 1.27 or later, to build Guildhall (no release has been published yet).
  • git.
  • To try the task commands: a git checkout of a repository to work on.
  • Optional, depending on what you try: Claude Code (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.

Install

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).

Try it today

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.

What works today

Status
Creating, inspecting and timing tasks; answering decisions; local messagesBuilt, driven by hand with commands
Advancing a task stage by stage and launching an agent for each stageBuilt, 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 ghBuilt, exercised in CI against a stand-in gh; live GitHub behaviour is not claimed
Steward rollouts of shared settings over SSHBuilt, 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 agentsBuilt: 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 agentsPlanned
Any runtime or platform supported for real workNot yet: each needs to pass live acceptance first

The commands

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.

Learn more

Contributing

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.

Source 2 files
hooks/register.js 85 lines
1import {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}
85
lib/presentation.mjs 43 lines
1// 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