@staix/agent-hub 0.18.0 → 0.19.0

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.
Files changed (73) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +4 -4
  3. package/docs/agent-notes/README.md +6 -0
  4. package/docs/agent-notes/adapters.md +10 -8
  5. package/docs/agent-notes/daemon.md +5 -2
  6. package/docs/agent-notes/local-worker.md +2 -1
  7. package/docs/agent-notes/tasks.md +4 -1
  8. package/docs/agent-notes/tests.md +6 -3
  9. package/docs/dogfood.md +9 -3
  10. package/docs/memory.md +1 -1
  11. package/docs/operations.md +66 -28
  12. package/docs/quickstart.md +9 -5
  13. package/docs/security.md +22 -13
  14. package/docs/smoke.md +3 -3
  15. package/docs/specs/2026-09-19-agent-hub-design.md +16 -5
  16. package/docs/verification/2026-10-25-remote-docker-transport.md +10 -0
  17. package/docs/verification/2026-10-26-agy-1.4.7.md +69 -0
  18. package/docs/verification/2026-10-26-claude-implementer-2.1.310.md +69 -0
  19. package/docs/verification/2026-10-26-headless-claude-shell-context.md +62 -0
  20. package/docs/verification/2026-10-26-jcode-0.99.1.md +49 -0
  21. package/docs/verification/2026-10-26-kimi-232-reviewer-compat.md +53 -0
  22. package/docs/verification/2026-10-26-pi-140-compat.md +41 -0
  23. package/docs/verification/2026-10-26-qwen-0261-acp.md +60 -0
  24. package/docs/verification/2026-10-27-0.19.0.md +86 -0
  25. package/docs/verification/claude-permission-transport-native.py +187 -0
  26. package/docs/verification/claude-shell-wrapper-host.ts +20 -0
  27. package/docs/verification/claude-shell-wrapper-native.py +145 -0
  28. package/docs/verification/pi-140-model-free.ts +57 -0
  29. package/docs/verification/pi-140-native.mjs +21 -0
  30. package/docs/verified.json +14 -14
  31. package/package.json +2 -2
  32. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  33. package/plugins/agent-hub/server.js +273 -68
  34. package/src/adapters/agy/entry.ts +1 -1
  35. package/src/adapters/claude-channel.ts +7 -3
  36. package/src/adapters/claude-implementer.ts +5 -3
  37. package/src/adapters/compat.ts +32 -18
  38. package/src/adapters/conformance/fakes/pi-rpc.ts +1 -0
  39. package/src/adapters/definitions/builtin-metadata.ts +2 -2
  40. package/src/adapters/definitions/native-factories.ts +7 -2
  41. package/src/adapters/definitions/reviewer-variants.ts +2 -2
  42. package/src/adapters/pi.ts +39 -3
  43. package/src/adapters/qwen-dual.ts +25 -0
  44. package/src/adapters/registry-types.ts +6 -2
  45. package/src/adapters/registry.ts +1 -0
  46. package/src/cli/agents.ts +69 -7
  47. package/src/cli/claude-shell-wrapper.ts +54 -0
  48. package/src/cli/main.ts +30 -24
  49. package/src/cli/output.ts +4 -0
  50. package/src/hub/agent-drift.ts +61 -0
  51. package/src/hub/board.ts +14 -2
  52. package/src/hub/check-runner.ts +148 -18
  53. package/src/hub/claude-shell-wrapper.ts +349 -0
  54. package/src/hub/config-edit.ts +2 -2
  55. package/src/hub/config-tier.ts +2 -2
  56. package/src/hub/daemon.ts +197 -35
  57. package/src/hub/host-executable.ts +20 -1
  58. package/src/hub/instances.ts +18 -2
  59. package/src/hub/lifecycle.ts +1 -1
  60. package/src/hub/operational-authority.ts +7 -0
  61. package/src/hub/operational-view.ts +6 -10
  62. package/src/hub/read-only-shell-proof.ts +7 -6
  63. package/src/hub/review-scope.ts +13 -0
  64. package/src/hub/sdlc-metrics.ts +6 -4
  65. package/src/hub/task-shell-command.ts +85 -0
  66. package/src/hub/task-shell-default.ts +78 -0
  67. package/src/hub/task-shell-proof.ts +171 -0
  68. package/src/hub/task-shell-sandbox.ts +61 -0
  69. package/src/hub/tasks.ts +139 -13
  70. package/src/local/deny.ts +91 -11
  71. package/src/local/sandbox.ts +67 -28
  72. package/src/local/shell-environment.ts +10 -0
  73. package/src/ui/index.html +1 -0
@@ -0,0 +1,145 @@
1
+ # Model-free native receipt. Usage: python3 this.py OUTPUT --bun /absolute/bun --claude /absolute/claude [--claude ...]
2
+ import argparse, hashlib, http.server, json, os, pathlib, select, shlex, signal, subprocess, sys, threading
3
+ parser=argparse.ArgumentParser(); parser.add_argument('output'); parser.add_argument('--bun',required=True); parser.add_argument('--claude',action='append',required=True)
4
+ args=parser.parse_args(); ROOT=pathlib.Path(args.output).resolve(); ROOT.mkdir(mode=0o700,parents=False,exist_ok=False)
5
+ HOST=pathlib.Path(__file__).with_name('claude-shell-wrapper-host.ts').resolve()
6
+ MCP="import sys,json,os,time\nfor line in sys.stdin:\n try: q=json.loads(line)\n except: continue\n method=q.get('method'); rid=q.get('id')\n if rid is None: continue\n if method=='initialize': r={'protocolVersion':'2024-11-05','capabilities':{'tools':{}},'serverInfo':{'name':'fixture','version':'1'}}\n elif method=='tools/list': r={'tools':[{'name':'approve','description':'Person approval fixture','inputSchema':{'type':'object','properties':{'tool_name':{'type':'string'},'input':{'type':'object'},'tool_use_id':{'type':'string'}},'required':['tool_name','input','tool_use_id'],'additionalProperties':False}}]}\n elif method=='tools/call':\n mode=os.environ['CASE']; args=q.get('params',{}).get('arguments',{})\n if mode=='timeout': time.sleep(60)\n if mode=='approval-timeout': time.sleep(.1)\n if mode=='disconnect': sys.exit(1)\n if mode=='tool-error':\n print(json.dumps({'jsonrpc':'2.0','id':rid,'error':{'code':-32000,'message':'fixture hub unavailable'}}),flush=True); continue\n if mode in ('deny','hook-failure','approval-timeout'): value={'behavior':'deny','message':'no person answered' if mode=='approval-timeout' else 'fixture denied'}\n else:\n updated=args.get('input',{})\n if mode=='normalized' and updated.get('description') in ('probe1','probe2'): updated={'command':os.environ['NORMALIZED'],'description':'host-bound fixture'}\n value={'behavior':'allow','updatedInput':updated}\n with open(os.environ['RECEIPT'],'a') as f: print(json.dumps({'arguments':args,'response':value}),file=f)\n r={'content':[{'type':'text','text':json.dumps(value)}]}\n else: r={}\n print(json.dumps({'jsonrpc':'2.0','id':rid,'result':r}),flush=True)\n"
7
+ (ROOT/'mcp.py').write_text(MCP)
8
+ class Handler(http.server.BaseHTTPRequestHandler):
9
+ def log_message(self,*a): pass
10
+ def do_POST(self):
11
+ q=json.loads(self.rfile.read(int(self.headers['Content-Length'])))
12
+ state=self.server.state
13
+ state['requests']+=1
14
+ if 'count_tokens' in self.path:
15
+ b=json.dumps({'input_tokens':100}).encode(); self.send_response(200);self.end_headers();self.wfile.write(b);return
16
+ msgs=q.get('messages',[])
17
+ with open(state['record'],'a') as f:f.write(json.dumps({'path':self.path,'keys':list(q),'tools':[x.get('name') for x in q.get('tools',[])],'messages':msgs})+'\n')
18
+ count=sum(1 for m in msgs if isinstance(m.get('content'),list) for x in m['content'] if isinstance(x,dict) and x.get('type')=='tool_result'); seen=count>=len(state['commands'])
19
+ content=[{'type':'text','text':'Fixture finished.'}] if seen else [{'type':'tool_use','id':'toolu_probe_'+str(count),'name':'Bash','input':{'command':state['commands'][count],'description':'probe'+str(count)}}]
20
+ result={'id':'msg_probe_'+str(count),'type':'message','role':'assistant','model':'claude-sonnet-4-5','content':content,'stop_reason':'end_turn' if seen else 'tool_use','stop_sequence':None,'usage':{'input_tokens':100,'output_tokens':10}}
21
+ if not q.get('stream'):
22
+ self.send_response(200);self.send_header('Content-Type','application/json');self.end_headers();self.wfile.write(json.dumps(result).encode());return
23
+ self.send_response(200);self.send_header('Content-Type','text/event-stream');self.end_headers()
24
+ start=dict(result);start.update(content=[],stop_reason=None,usage={'input_tokens':100,'output_tokens':0})
25
+ events=[('message_start',{'type':'message_start','message':start})]
26
+ for i,c in enumerate(content):
27
+ block=dict(c)
28
+ if c['type']=='tool_use': block['input']={}
29
+ else: block['text']=''
30
+ events.append(('content_block_start',{'type':'content_block_start','index':i,'content_block':block}))
31
+ delta={'type':'input_json_delta','partial_json':json.dumps(c['input'])} if c['type']=='tool_use' else {'type':'text_delta','text':c['text']}
32
+ events.extend([('content_block_delta',{'type':'content_block_delta','index':i,'delta':delta}),('content_block_stop',{'type':'content_block_stop','index':i})])
33
+ events.extend([('message_delta',{'type':'message_delta','delta':{'stop_reason':result['stop_reason'],'stop_sequence':None},'usage':{'output_tokens':10}}),('message_stop',{'type':'message_stop'})])
34
+ try:
35
+ for e,v in events:self.wfile.write(('event: '+e+'\ndata: '+json.dumps(v)+'\n\n').encode());self.wfile.flush()
36
+ except BrokenPipeError:pass
37
+
38
+ def stop(process):
39
+ if process is None: return True
40
+ try:
41
+ if process.stdin: process.stdin.close()
42
+ try: process.wait(timeout=25)
43
+ except subprocess.TimeoutExpired:
44
+ if process.poll() is None:
45
+ try: os.killpg(process.pid,signal.SIGKILL)
46
+ except ProcessLookupError: pass
47
+ process.wait(timeout=5)
48
+ return process.returncode==0
49
+ except (OSError,subprocess.TimeoutExpired):return False
50
+
51
+ def empty_group(pid):
52
+ try:table=subprocess.run(['/bin/ps','-axo','pgid=,stat='],env={'PATH':'/usr/bin:/bin','LC_ALL':'C','TZ':'UTC'},capture_output=True,text=True,timeout=3)
53
+ except (OSError,subprocess.TimeoutExpired):return False
54
+ if table.returncode or len(table.stdout)>1024*1024:return False
55
+ parsed=[line.split() for line in table.stdout.splitlines() if line.strip()]
56
+ return bool(parsed) and all(len(row)==2 for row in parsed) and not any(row[0]==str(pid) and not row[1].startswith('Z') for row in parsed)
57
+
58
+ def rows(file):
59
+ return [json.loads(line) for line in file.read_text().splitlines() if line]
60
+
61
+ def sha(file):
62
+ digest=hashlib.sha256()
63
+ with file.open('rb') as stream:
64
+ for block in iter(lambda:stream.read(1024*1024),b''):digest.update(block)
65
+ return digest.hexdigest()
66
+
67
+ results=[]
68
+ for raw_binary in args.claude:
69
+ binary=pathlib.Path(raw_binary).resolve(); identity=ROOT/('identity-'+binary.name); identity.mkdir()
70
+ version=subprocess.run([str(binary),'--version'],env={'HOME':str(identity),'CLAUDE_CONFIG_DIR':str(identity/'config'),'PATH':'/usr/bin:/bin'},capture_output=True,text=True,timeout=10)
71
+ assert version.returncode==0 and version.stdout.strip().split()[0] in ('2.1.309','2.1.310'),(version.returncode,version.stdout)
72
+ label=version.stdout.strip().split()[0]; digest=sha(binary)
73
+ for mode in ('context','normalized'):
74
+ d=ROOT/(label+'-'+mode); d.mkdir(); home=d/'home';home.mkdir(); work=d/"work 'quoted'";work.mkdir();(work/'sub').mkdir()
75
+ work=work.resolve(); host_bin=d/"host bin's";host_bin.mkdir(); executable=host_bin/'fixture';counter=d/'effects'
76
+ program='#!/usr/bin/python3\nimport json,os,pathlib,sys\np=pathlib.Path('+repr(str(counter))+');p.write_text(str(int(p.read_text())+1) if p.exists() else "1")\nprint(json.dumps({"marker":"HOST_EXECUTABLE","cwd":os.getcwd(),"executable":os.path.realpath(sys.argv[0]),"nativeVars":{k:os.environ.get(k) for k in ["GIT_DIR","RIPGREP_CONFIG_PATH"]},"peer":os.environ.get("AGENTHUB_PEER_ID"),"term":os.environ.get("TERM"),"home":os.environ.get("HOME"),"path":os.environ.get("PATH"),"tmpdir":os.environ.get("TMPDIR")}))\n'
77
+ executable.write_text(program);executable.chmod(0o700)
78
+ helper=None;srv=None;native=None
79
+ result={'version':version.stdout.strip(),'binarySha256':digest,'case':mode}
80
+ try:
81
+ rewritten=''
82
+ if mode=='normalized':
83
+ fixture=d/'host.json';fixture.write_text(json.dumps({'root':str(work),'executable':str(executable)}))
84
+ helper=subprocess.Popen([str(pathlib.Path(args.bun).resolve()),str(HOST),str(fixture)],env={'HOME':str(home),'PATH':'/opt/homebrew/bin:/usr/bin:/bin','LANG':'C','TERM':'dumb'},stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=open(d/'host.stderr','w'),text=True,start_new_session=True)
85
+ assert select.select([helper.stdout],[],[],10)[0],'host fixture handshake timeout'
86
+ reply=json.loads(helper.stdout.readline());rewritten=reply['command'];assert isinstance(rewritten,str) and rewritten
87
+ commands=['cd sub && pwd','pwd']+(['pwd'] if mode=='normalized' else [])
88
+ srv=http.server.ThreadingHTTPServer(('127.0.0.1',0),Handler);srv.state={'requests':0,'record':str(d/'api.jsonl'),'commands':commands}
89
+ threading.Thread(target=srv.serve_forever,daemon=True).start()
90
+ config={'mcpServers':{'fixture':{'command':'/usr/bin/python3','args':[str(ROOT/'mcp.py')],'env':{'CASE':mode,'RECEIPT':str(d/'mcp.jsonl'),'NORMALIZED':rewritten}}}}
91
+ (d/'mcp.json').write_text(json.dumps(config));(d/'settings.json').write_text(json.dumps({'permissions':{'ask':['Bash']}}))
92
+ native_env={'HOME':str(home),'CLAUDE_CONFIG_DIR':str(home/'config'),'PATH':'/usr/bin:/bin','ANTHROPIC_BASE_URL':'http://127.0.0.1:'+str(srv.server_port),'ANTHROPIC_API_KEY':'synthetic-probe-not-a-credential','CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC':'1','DISABLE_AUTOUPDATER':'1','GIT_DIR':str(d/'poison-git'),'RIPGREP_CONFIG_PATH':str(d/'poison-rg')}
93
+ argv=[str(binary),'-p','--output-format','stream-json','--verbose','--strict-mcp-config','--mcp-config',str(d/'mcp.json'),'--setting-sources','','--settings',str(d/'settings.json'),'--permission-mode','default','--permission-prompts','host','--permission-prompt-tool','mcp__fixture__approve','--model','claude-sonnet-4-5','Run the fixture commands.']
94
+ native=subprocess.Popen(argv,cwd=work,env=native_env,stdout=open(d/'stdout','w'),stderr=open(d/'stderr','w'),start_new_session=True)
95
+ native.wait(timeout=25);assert native.returncode==0,(native.returncode,(d/'stderr').read_text())
96
+ assert (d/'stderr').read_text()==''
97
+ calls=rows(d/'mcp.jsonl');assert len(calls)==len(commands),calls
98
+ frames={}
99
+ for frame in rows(d/'stdout'):
100
+ if frame.get('type')!='user':continue
101
+ for block in frame.get('message',{}).get('content',[]):
102
+ if block.get('type')=='tool_result':
103
+ ident=block['tool_use_id'];assert ident not in frames;frames[ident]=(frame.get('tool_use_result'),block)
104
+ assert set(frames)=={'toolu_probe_'+str(i) for i in range(len(commands))},frames
105
+ for i,call in enumerate(calls):
106
+ assert call['arguments']=={'tool_name':'Bash','input':{'command':commands[i],'description':'probe'+str(i)},'tool_use_id':'toolu_probe_'+str(i)},call
107
+ expected=call['arguments']['input'] if i==0 or mode=='context' else {'command':rewritten,'description':'host-bound fixture'}
108
+ assert call['response']=={'behavior':'allow','updatedInput':expected},call
109
+ def output(i):
110
+ tool,block=frames['toolu_probe_'+str(i)];assert isinstance(tool,dict) and block.get('is_error') is False and tool.get('stderr')=='' and tool.get('interrupted') is False,(tool,block)
111
+ assert block.get('content')==tool['stdout'];return tool['stdout']
112
+ assert output(0).strip()==str(work/'sub')
113
+ if mode=='context':
114
+ assert output(1).strip()==str(work/'sub');result['persistentCwd']=True
115
+ else:
116
+ observed=json.loads(output(1));assert observed['marker']=='HOST_EXECUTABLE' and observed['cwd']==str(work) and observed['executable']==str(executable.resolve()),observed
117
+ assert observed['nativeVars']=={'GIT_DIR':None,'RIPGREP_CONFIG_PATH':None},observed
118
+ assert observed['peer']=='claude-fixture' and observed['term']=='dumb' and observed['home']==str(home) and observed['path']=='/opt/homebrew/bin:/usr/bin:/bin',observed
119
+ assert observed['tmpdir'] and 'ahub-shell-' in observed['tmpdir'] and str(d) not in observed['tmpdir'],observed
120
+ replay_tool,replay=frames['toolu_probe_2'];assert replay.get('is_error') is True and 'host shell execution refused' in json.dumps(replay_tool),frames['toolu_probe_2']
121
+ assert counter.read_text()=='1',counter.read_text()
122
+ result.update(rewrite=True,priorCd=True,environmentIsolation=True,replayRefused=True,effects=1,quotedPaths=True)
123
+ result.update(passed=True,nativeExit=0,requests=srv.state['requests'],permissionCalls=len(calls))
124
+ except Exception as error:result.update(passed=False,error=repr(error))
125
+ finally:
126
+ if native is not None:
127
+ # Signal only while this unreaped direct child still owns its pid, never a group id after successful wait.
128
+ if native.poll() is None:
129
+ try:os.killpg(native.pid,signal.SIGKILL)
130
+ except ProcessLookupError:pass
131
+ native.wait(timeout=5)
132
+ import time
133
+ deadline=time.monotonic()+1
134
+ while not empty_group(native.pid) and time.monotonic()<deadline:time.sleep(.02)
135
+ result['nativeGroupEmpty']=empty_group(native.pid)
136
+ if not result['nativeGroupEmpty']:result.update(passed=False,cleanup='native group not established empty; no unrelated process signalled')
137
+ result['hostStopped']=stop(helper)
138
+ if not result['hostStopped']:result.update(passed=False,cleanup='host wrapper cleanup failed')
139
+ if srv:srv.shutdown()
140
+ result['binaryUnchanged']=sha(binary)==digest
141
+ if not result['binaryUnchanged']:result.update(passed=False,error='native binary changed during probe')
142
+ results.append(result);print(json.dumps(result),flush=True)
143
+ receipt={'cases':results,'certified':all(row['passed'] for row in results),'account':False,'inference':False}
144
+ (ROOT/'receipt.json').write_text(json.dumps(receipt,indent=2));print(json.dumps({'receipt':str(ROOT/'receipt.json'),'certified':receipt['certified']}),flush=True)
145
+ raise SystemExit(0 if receipt['certified'] else 1)
@@ -0,0 +1,57 @@
1
+ import {mkdtempSync, mkdirSync, readFileSync} from 'node:fs';
2
+ import {join, resolve} from 'node:path';
3
+ import {homedir} from 'node:os';
4
+ import {fileURLToPath} from 'node:url';
5
+ import assert from 'node:assert/strict';
6
+ const launcher=resolve(process.argv[2] ?? join(homedir(),'.pi/agent/bin/pi'));
7
+ const packageRoot=resolve(process.argv[3] ?? join(homedir(),'.pi/agent/install/releases/1.4.0/node_modules/@earendil-works/pi-coding-agent'));
8
+ const extension=fileURLToPath(new URL('../../src/pi/extension.ts',import.meta.url));
9
+ const driver=fileURLToPath(new URL('./pi-140-native.mjs',import.meta.url));
10
+ import {PiToolReceipts} from '../../src/pi/tool-receipts.ts';
11
+ const home=mkdtempSync('/tmp/ahub-505-native-home.'); mkdirSync(join(home,'agent'));
12
+ const pkg=JSON.parse(readFileSync(join(packageRoot,'package.json'),'utf8'));
13
+ assert.equal(pkg.name,'@earendil-works/pi-coding-agent');assert.equal(pkg.version,'1.4.0');
14
+ const version=Bun.spawn([launcher,'--version'],{cwd:home,env:{HOME:home,PATH:process.env.PATH!,PI_CODING_AGENT_DIR:join(home,'agent')},stdin:'ignore',stdout:'pipe',stderr:'pipe'});
15
+ const versionTimer=setTimeout(()=>version.kill(),10000);
16
+ const [versionCode,versionOut,versionErr]=await Promise.all([version.exited,new Response(version.stdout).text(),new Response(version.stderr).text()]);clearTimeout(versionTimer);
17
+ assert.equal(versionCode,0);assert.equal(versionOut.trim(),'1.4.0');assert.equal(versionErr,'');
18
+ const ledger=new PiToolReceipts(join(home,'hub.db'));
19
+ let effects=0, providers=0; const events:any[]=[];
20
+ const bridge=Bun.serve({hostname:'127.0.0.1',port:0,async fetch(req){
21
+ const url=new URL(req.url); if(req.headers.get('authorization')!=='Bearer fixture')return new Response('forbidden',{status:403});
22
+ if(url.pathname==='/commands') {await Bun.sleep(25);return Response.json({command:null});}
23
+ const body=await req.json() as any;
24
+ if(url.pathname==='/event'){events.push(body.type);return Response.json({ok:true});}
25
+ if(url.pathname==='/budget')return Response.json({decisions:[]});
26
+ if(url.pathname==='/tool') {
27
+ const text=await ledger.execute(body.sessionId,body.toolCallId,body.name,body.args,async()=>{
28
+ if(body.args.value==='deny')return 'error: fixture permission denied';
29
+ effects++;return 'fixture success';
30
+ });
31
+ return Response.json({text,failed:text.startsWith('error:')});
32
+ }
33
+ providers++;return new Response('provider forbidden',{status:500});
34
+ }});
35
+ const env={HOME:home,PATH:process.env.PATH!,PI_CODING_AGENT_DIR:join(home,'agent'),HUB_EXTENSION:extension,PI_COMPAT_PACKAGE:packageRoot,
36
+ AGENTHUB_PI_BRIDGE_URL:`http://127.0.0.1:${bridge.port}`,AGENTHUB_PI_BRIDGE_TOKEN:'fixture',AGENTHUB_PI_OWNER_TOKEN:'fixture',
37
+ AGENTHUB_PI_TOOLS:JSON.stringify([{name:'compat_probe',description:'model-free fixture',parameters:{type:'object',properties:{value:{type:'string'}},required:['value']}}])};
38
+ const child=Bun.spawn(['node',driver],{cwd:home,env,stdin:'ignore',stdout:'pipe',stderr:'pipe'});
39
+ const timer=setTimeout(()=>child.kill(),10000);
40
+ const [code,stdout,stderr]=await Promise.all([child.exited,new Response(child.stdout).text(),new Response(child.stderr).text()]);clearTimeout(timer);
41
+ console.log(JSON.stringify({code,stdout,stderr,effects,providers,events,isolatedHome:home}));
42
+ const native=Bun.spawn([launcher,'--mode','rpc','--provider','agent-hub-local','--model','fixture','--no-builtin-tools','--no-skills','--no-prompt-templates','--no-extensions','--extension',env.HUB_EXTENSION,'--session-dir',join(home,'native-sessions')],{
43
+ cwd:home,env:{...env,AGENTHUB_PI_RELAY_URL:`http://127.0.0.1:${bridge.port}/v1`,AGENTHUB_PI_RELAY_TOKEN:'fixture',AGENTHUB_PI_MODELS:JSON.stringify([{id:'fixture',name:'fixture'}])},stdin:'pipe',stdout:'pipe',stderr:'pipe'});
44
+ native.stdin.write(JSON.stringify({id:'probe',type:'get_state'})+'\n');
45
+ const nativeTimer=setTimeout(()=>native.kill(),10000);
46
+ let wire='';for await(const chunk of native.stdout){wire+=new TextDecoder().decode(chunk);if(wire.includes('"command":"get_state"')){native.kill();break;}}
47
+ await native.exited;clearTimeout(nativeTimer);const nativeErr=await new Response(native.stderr).text();
48
+ console.log(JSON.stringify({nativeRpc:wire,nativeStderr:nativeErr,providers,events}));
49
+ bridge.stop(true);await ledger.close();
50
+ assert.equal(code,0);assert.equal(stderr,'');
51
+ const result=JSON.parse(stdout);
52
+ assert.equal(result.extension,'loaded');assert.equal(result.first.isError,false);assert.deepEqual(result.first,result.replay);assert.equal(result.denied.isError,true);
53
+ assert.equal(effects,1);assert.equal(providers,0);assert.equal(nativeErr,'');
54
+ const state=wire.trim().split('\n').map(line=>JSON.parse(line)).find(item=>item.command==='get_state');
55
+ assert.equal(state?.success,true);assert.equal(state.data.messageCount,0);assert.ok(state.data.sessionId);assert.ok(state.data.sessionFile);
56
+ assert.ok(events.includes('session_start'));assert.ok(events.includes('session_shutdown'));
57
+ console.log('pi 1.4.0 model-free receipt: PASS');
@@ -0,0 +1,21 @@
1
+ import { pathToFileURL } from 'node:url';
2
+ const { createAgentSession, DefaultResourceLoader, SessionManager, SettingsManager } = await import(pathToFileURL(`${process.env.PI_COMPAT_PACKAGE}/dist/index.js`).href);
3
+ const cwd = process.env.HOME;
4
+ const settingsManager = SettingsManager.create(cwd, process.env.PI_CODING_AGENT_DIR);
5
+ const loader = new DefaultResourceLoader({cwd, agentDir:process.env.PI_CODING_AGENT_DIR, settingsManager,
6
+ noExtensions:true, additionalExtensionPaths:[process.env.HUB_EXTENSION], noSkills:true, noPromptTemplates:true, noThemes:true});
7
+ await loader.reload();
8
+ const {session, extensionsResult} = await createAgentSession({cwd, agentDir:process.env.PI_CODING_AGENT_DIR,
9
+ resourceLoader:loader, settingsManager, sessionManager:SessionManager.create(cwd, `${cwd}/sessions`), noTools:'builtin'});
10
+ if (extensionsResult.errors.length) throw new Error(JSON.stringify(extensionsResult.errors));
11
+ await session.bindExtensions({mode:'rpc'});
12
+ const tool = session.extensionRunner.getAllRegisteredTools().find(t=>t.definition.name==='compat_probe');
13
+ if (!tool) throw new Error('hub extension tool not registered');
14
+ const invoke = async (id, args) => tool.definition.execute(id, args, new AbortController().signal);
15
+ const first = await invoke('same-call', {value:'fixture'});
16
+ const replay = await invoke('same-call', {value:'fixture'});
17
+ const denied = await invoke('denied-call', {value:'deny'});
18
+ console.log(JSON.stringify({extension:'loaded',first,replay,denied,tools:session.getActiveToolNames()}));
19
+ await session.extensionRunner.emit({type:'session_shutdown'});
20
+ session.dispose();
21
+ process.exit(0);
@@ -2,7 +2,7 @@
2
2
  "schemaVersion": 1,
3
3
  "documents": {
4
4
  "README.md": {
5
- "verifiedAgainst": "af22ee0dd9c6a1f4a5c5fc3f10fbfe4875350c1e",
5
+ "verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
6
6
  "paths": [
7
7
  "package.json",
8
8
  "src/",
@@ -11,14 +11,14 @@
11
11
  ]
12
12
  },
13
13
  "docs/security.md": {
14
- "verifiedAgainst": "af22ee0dd9c6a1f4a5c5fc3f10fbfe4875350c1e",
14
+ "verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
15
15
  "paths": [
16
16
  "src/",
17
17
  "templates/"
18
18
  ]
19
19
  },
20
20
  "docs/operations.md": {
21
- "verifiedAgainst": "af22ee0dd9c6a1f4a5c5fc3f10fbfe4875350c1e",
21
+ "verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
22
22
  "paths": [
23
23
  "package.json",
24
24
  "src/",
@@ -35,7 +35,7 @@
35
35
  ]
36
36
  },
37
37
  "docs/quickstart.md": {
38
- "verifiedAgainst": "af22ee0dd9c6a1f4a5c5fc3f10fbfe4875350c1e",
38
+ "verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
39
39
  "paths": [
40
40
  "package.json",
41
41
  "src/",
@@ -43,7 +43,7 @@
43
43
  ]
44
44
  },
45
45
  "docs/agent-notes/adapters.md": {
46
- "verifiedAgainst": "af22ee0dd9c6a1f4a5c5fc3f10fbfe4875350c1e",
46
+ "verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
47
47
  "paths": [
48
48
  "src/adapters/",
49
49
  "src/pi/",
@@ -78,7 +78,7 @@
78
78
  ]
79
79
  },
80
80
  "docs/agent-notes/benchmarks.md": {
81
- "verifiedAgainst": "af22ee0dd9c6a1f4a5c5fc3f10fbfe4875350c1e",
81
+ "verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
82
82
  "paths": [
83
83
  "scripts/benchmarks/",
84
84
  "test/benchmarks/",
@@ -90,7 +90,7 @@
90
90
  ]
91
91
  },
92
92
  "docs/agent-notes/budget.md": {
93
- "verifiedAgainst": "af22ee0dd9c6a1f4a5c5fc3f10fbfe4875350c1e",
93
+ "verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
94
94
  "paths": [
95
95
  "src/hub/budget.ts",
96
96
  "src/cli/statusline-tee.ts",
@@ -114,7 +114,7 @@
114
114
  ]
115
115
  },
116
116
  "docs/agent-notes/bus.md": {
117
- "verifiedAgainst": "af22ee0dd9c6a1f4a5c5fc3f10fbfe4875350c1e",
117
+ "verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
118
118
  "paths": [
119
119
  "src/hub/bus.ts",
120
120
  "src/hub/envelope.ts",
@@ -129,7 +129,7 @@
129
129
  ]
130
130
  },
131
131
  "docs/agent-notes/daemon.md": {
132
- "verifiedAgainst": "af22ee0dd9c6a1f4a5c5fc3f10fbfe4875350c1e",
132
+ "verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
133
133
  "paths": [
134
134
  "src/hub/daemon.ts",
135
135
  "src/hub/control-client.ts",
@@ -202,7 +202,7 @@
202
202
  ]
203
203
  },
204
204
  "docs/agent-notes/local-worker.md": {
205
- "verifiedAgainst": "af22ee0dd9c6a1f4a5c5fc3f10fbfe4875350c1e",
205
+ "verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
206
206
  "paths": [
207
207
  "src/adapters/local-worker.ts",
208
208
  "src/local/",
@@ -222,7 +222,7 @@
222
222
  ]
223
223
  },
224
224
  "docs/agent-notes/tasks.md": {
225
- "verifiedAgainst": "af22ee0dd9c6a1f4a5c5fc3f10fbfe4875350c1e",
225
+ "verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
226
226
  "paths": [
227
227
  "src/hub/tasks.ts",
228
228
  "src/hub/board.ts",
@@ -260,7 +260,7 @@
260
260
  ]
261
261
  },
262
262
  "docs/agent-notes/tests.md": {
263
- "verifiedAgainst": "af22ee0dd9c6a1f4a5c5fc3f10fbfe4875350c1e",
263
+ "verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
264
264
  "paths": [
265
265
  "test/",
266
266
  "scripts/check.sh",
@@ -284,7 +284,7 @@
284
284
  ]
285
285
  },
286
286
  "docs/agent-notes/README.md": {
287
- "verifiedAgainst": "af22ee0dd9c6a1f4a5c5fc3f10fbfe4875350c1e",
287
+ "verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
288
288
  "paths": [
289
289
  "src/cli/console-state.ts",
290
290
  "src/cli/attention.ts",
@@ -293,7 +293,7 @@
293
293
  ]
294
294
  },
295
295
  "src/cli/help.ts": {
296
- "verifiedAgainst": "af22ee0dd9c6a1f4a5c5fc3f10fbfe4875350c1e",
296
+ "verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
297
297
  "paths": [
298
298
  "src/cli/main.ts",
299
299
  "src/cli/launch.ts",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@staix/agent-hub",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "description": "Native multi-agent hub: Claude Code, Codex, Kimi Code, Pi and local inference as peers in one project",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -17,7 +17,7 @@
17
17
  "bun": ">=1.4.2"
18
18
  },
19
19
  "dependencies": {
20
- "@staix/agent-mem": "0.8.0"
20
+ "@staix/agent-mem": "0.10.0"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@modelcontextprotocol/sdk": "^1.27.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-hub",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "description": "Channel between Claude Code and the agent-hub daemon: peer messages from Codex, Kimi and the local worker arrive as channel events; hub_send replies.",
5
5
  "author": {
6
6
  "name": "Young Joon Lee",