@staix/agent-hub 0.17.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 (109) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +4 -4
  3. package/docs/agent-notes/README.md +11 -0
  4. package/docs/agent-notes/adapters.md +10 -6
  5. package/docs/agent-notes/benchmarks.md +1 -0
  6. package/docs/agent-notes/daemon.md +6 -2
  7. package/docs/agent-notes/local-worker.md +3 -1
  8. package/docs/agent-notes/tasks.md +11 -2
  9. package/docs/agent-notes/tests.md +7 -3
  10. package/docs/dogfood.md +22 -2
  11. package/docs/memory.md +1 -1
  12. package/docs/operations.md +162 -35
  13. package/docs/profiles.md +36 -0
  14. package/docs/quickstart.md +15 -5
  15. package/docs/sdlc.md +29 -3
  16. package/docs/security.md +44 -13
  17. package/docs/smoke.md +3 -3
  18. package/docs/specs/2026-09-19-agent-hub-design.md +31 -10
  19. package/docs/verification/2026-10-25-0.18.0.md +76 -0
  20. package/docs/verification/2026-10-25-codex-review-live-verdict.md +35 -0
  21. package/docs/verification/2026-10-25-remote-docker-transport.md +56 -0
  22. package/docs/verification/2026-10-26-agy-1.4.7.md +69 -0
  23. package/docs/verification/2026-10-26-claude-implementer-2.1.310.md +69 -0
  24. package/docs/verification/2026-10-26-headless-claude-shell-context.md +62 -0
  25. package/docs/verification/2026-10-26-jcode-0.99.1.md +49 -0
  26. package/docs/verification/2026-10-26-kimi-232-reviewer-compat.md +53 -0
  27. package/docs/verification/2026-10-26-pi-140-compat.md +41 -0
  28. package/docs/verification/2026-10-26-qwen-0261-acp.md +60 -0
  29. package/docs/verification/2026-10-27-0.19.0.md +86 -0
  30. package/docs/verification/claude-permission-transport-native.py +187 -0
  31. package/docs/verification/claude-shell-wrapper-host.ts +20 -0
  32. package/docs/verification/claude-shell-wrapper-native.py +145 -0
  33. package/docs/verification/pi-140-model-free.ts +57 -0
  34. package/docs/verification/pi-140-native.mjs +21 -0
  35. package/docs/verification/profiles/YYYY-MM-DD-general.md +46 -0
  36. package/docs/verification/profiles/YYYY-MM-DD-research.md +61 -0
  37. package/docs/verification/profiles/YYYY-MM-DD-writing.md +46 -0
  38. package/docs/verified.json +24 -18
  39. package/package.json +2 -2
  40. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  41. package/plugins/agent-hub/server.js +364 -98
  42. package/src/adapters/agy/entry.ts +1 -1
  43. package/src/adapters/claude-channel.ts +7 -3
  44. package/src/adapters/claude-implementer.ts +5 -3
  45. package/src/adapters/compat.ts +32 -18
  46. package/src/adapters/conformance/fakes/pi-rpc.ts +1 -0
  47. package/src/adapters/definitions/builtin-metadata.ts +3 -3
  48. package/src/adapters/definitions/native-factories.ts +7 -2
  49. package/src/adapters/definitions/reviewer-variants.ts +2 -2
  50. package/src/adapters/local-worker.ts +5 -3
  51. package/src/adapters/pi.ts +39 -3
  52. package/src/adapters/qwen-dual.ts +25 -0
  53. package/src/adapters/registry-types.ts +6 -2
  54. package/src/adapters/registry.ts +1 -0
  55. package/src/cli/agents.ts +69 -7
  56. package/src/cli/claude-shell-wrapper.ts +54 -0
  57. package/src/cli/console-state.ts +9 -2
  58. package/src/cli/help.ts +29 -28
  59. package/src/cli/identity.ts +87 -6
  60. package/src/cli/main.ts +198 -71
  61. package/src/cli/output.ts +11 -2
  62. package/src/cli/sdlc-output.ts +242 -0
  63. package/src/cli/task-output.ts +212 -0
  64. package/src/cli/upgrade-output.ts +57 -0
  65. package/src/hub/agent-drift.ts +61 -0
  66. package/src/hub/board.ts +31 -2
  67. package/src/hub/check-runner.ts +385 -25
  68. package/src/hub/claude-shell-wrapper.ts +349 -0
  69. package/src/hub/conductor.ts +5 -0
  70. package/src/hub/config-apply.ts +16 -7
  71. package/src/hub/config-edit-host.ts +1 -1
  72. package/src/hub/config-edit.ts +2 -2
  73. package/src/hub/config-tier.ts +3 -3
  74. package/src/hub/daemon.ts +530 -87
  75. package/src/hub/dogfood-profile.ts +248 -0
  76. package/src/hub/dogfood-read.ts +77 -14
  77. package/src/hub/dogfood.ts +5 -1
  78. package/src/hub/events.ts +1 -1
  79. package/src/hub/host-executable.ts +21 -1
  80. package/src/hub/hub-tools.ts +2 -2
  81. package/src/hub/instances.ts +18 -2
  82. package/src/hub/lifecycle.ts +1 -1
  83. package/src/hub/operational-authority.ts +67 -0
  84. package/src/hub/operational-view.ts +137 -0
  85. package/src/hub/orphan-process.ts +69 -0
  86. package/src/hub/permission-mode.ts +1 -1
  87. package/src/hub/read-only-shell-proof.ts +81 -0
  88. package/src/hub/read-only-shell.ts +162 -0
  89. package/src/hub/review-scope.ts +13 -0
  90. package/src/hub/reviewer-pool-config.ts +3 -2
  91. package/src/hub/reviewer-pool.ts +3 -0
  92. package/src/hub/routing.ts +23 -6
  93. package/src/hub/sdlc-merge.ts +299 -0
  94. package/src/hub/sdlc-metrics.ts +6 -4
  95. package/src/hub/sdlc-mirror.ts +129 -0
  96. package/src/hub/sdlc.ts +58 -12
  97. package/src/hub/settings.ts +11 -5
  98. package/src/hub/snapshots.ts +60 -0
  99. package/src/hub/task-repository.ts +31 -5
  100. package/src/hub/task-shell-command.ts +85 -0
  101. package/src/hub/task-shell-default.ts +78 -0
  102. package/src/hub/task-shell-proof.ts +171 -0
  103. package/src/hub/task-shell-sandbox.ts +61 -0
  104. package/src/hub/tasks.ts +378 -27
  105. package/src/local/deny.ts +91 -11
  106. package/src/local/sandbox.ts +67 -28
  107. package/src/local/shell-environment.ts +10 -0
  108. package/src/local/tools.ts +17 -4
  109. package/src/ui/index.html +3 -1
@@ -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);
@@ -0,0 +1,46 @@
1
+ # General profile weekly verification receipt
2
+
3
+ Template only. No pilot result or verdict has been recorded.
4
+
5
+ - Week ending: YYYY-MM-DD
6
+ - Profile id: general
7
+ - Window bounds: unknown
8
+
9
+ ## Recorded profile JSON
10
+
11
+ Run ahub report --dogfood --since 7d --json once at the review boundary. Replace the null below with only the emitted profile subsection. Preserve numbers and null values. Include no project/person name, task title, path, raw log or model text; use anonymous identifiers where necessary.
12
+
13
+ null
14
+
15
+ ## Verification verdicts
16
+
17
+ Choose exactly one listed verdict for each claim after inspecting recorded evidence. Unknown data alone cannot establish support. These placeholders are not verdicts.
18
+
19
+ - writing-live-example
20
+ - Verdict: [supported | partly supported | not supported]
21
+ - Evidence task/event/receipt ids: []
22
+ - Remaining unknowns: unknown
23
+ - general-live-example
24
+ - Verdict: [supported | partly supported | not supported]
25
+ - Evidence task/event/receipt ids: []
26
+ - Remaining unknowns: unknown
27
+ - research-live-example
28
+ - Verdict: [supported | partly supported | not supported]
29
+ - Evidence task/event/receipt ids: []
30
+ - Remaining unknowns: unknown
31
+ - native-model-ingestion
32
+ - Verdict: [supported | partly supported | not supported]
33
+ - Evidence task/event/receipt ids: []
34
+ - Remaining unknowns: unknown
35
+ - context-retention
36
+ - Verdict: [supported | partly supported | not supported]
37
+ - Evidence task/event/receipt ids: []
38
+ - Remaining unknowns: unknown
39
+ - native-account-research
40
+ - Verdict: [supported | partly supported | not supported]
41
+ - Evidence task/event/receipt ids: []
42
+ - Remaining unknowns: unknown
43
+ - research-measurement
44
+ - Verdict: [supported | partly supported | not supported]
45
+ - Evidence task/event/receipt ids: []
46
+ - Remaining unknowns: unknown
@@ -0,0 +1,61 @@
1
+ # Research profile weekly verification receipt
2
+
3
+ Template only. No pilot result or verdict has been recorded.
4
+
5
+ - Week ending: YYYY-MM-DD
6
+ - Profile id: research
7
+ - Window bounds: unknown
8
+
9
+ ## Recorded profile JSON
10
+
11
+ Run ahub report --dogfood --since 7d --json once at the review boundary. Replace the null below with only the emitted profile subsection. Preserve numbers and null values. Include no project/person name, task title, path, raw log or model text; use anonymous identifiers where necessary.
12
+
13
+ null
14
+
15
+ ## Research measurements
16
+
17
+ Copy only emitted counts and anonymous task/claim slots. These placeholders remain unknown until recorded evidence supports a value.
18
+
19
+ - Current-plan declared sources and claims per task: unknown
20
+ - Claims reaching a person verdict: unknown
21
+ - Claim slots with model label, person verdict and agreement: unknown
22
+ - Agreement counts by supported/unsupported/unclear label: unknown
23
+ - Recorded task-check outcomes by class alias: unknown
24
+ - Proven checks.fact_check selection and pass/fail by alias: unknown
25
+ - Missing check keys per run: unknown
26
+ - Section-overlap notices and confirmed section edits: unknown
27
+
28
+ Task-check history alone does not prove checks.fact_check ran. A model label or task approval alone does not prove a person verdict or agreement. Preserve every emitted null.
29
+
30
+ ## Verification verdicts
31
+
32
+ Choose exactly one listed verdict for each claim after inspecting recorded evidence. Unknown data alone cannot establish support. These placeholders are not verdicts.
33
+
34
+ - writing-live-example
35
+ - Verdict: [supported | partly supported | not supported]
36
+ - Evidence task/event/receipt ids: []
37
+ - Remaining unknowns: unknown
38
+ - general-live-example
39
+ - Verdict: [supported | partly supported | not supported]
40
+ - Evidence task/event/receipt ids: []
41
+ - Remaining unknowns: unknown
42
+ - research-live-example
43
+ - Verdict: [supported | partly supported | not supported]
44
+ - Evidence task/event/receipt ids: []
45
+ - Remaining unknowns: unknown
46
+ - native-model-ingestion
47
+ - Verdict: [supported | partly supported | not supported]
48
+ - Evidence task/event/receipt ids: []
49
+ - Remaining unknowns: unknown
50
+ - context-retention
51
+ - Verdict: [supported | partly supported | not supported]
52
+ - Evidence task/event/receipt ids: []
53
+ - Remaining unknowns: unknown
54
+ - native-account-research
55
+ - Verdict: [supported | partly supported | not supported]
56
+ - Evidence task/event/receipt ids: []
57
+ - Remaining unknowns: unknown
58
+ - research-measurement
59
+ - Verdict: [supported | partly supported | not supported]
60
+ - Evidence task/event/receipt ids: []
61
+ - Remaining unknowns: unknown
@@ -0,0 +1,46 @@
1
+ # Writing profile weekly verification receipt
2
+
3
+ Template only. No pilot result or verdict has been recorded.
4
+
5
+ - Week ending: YYYY-MM-DD
6
+ - Profile id: writing
7
+ - Window bounds: unknown
8
+
9
+ ## Recorded profile JSON
10
+
11
+ Run ahub report --dogfood --since 7d --json once at the review boundary. Replace the null below with only the emitted profile subsection. Preserve numbers and null values. Include no project/person name, task title, path, raw log or model text; use anonymous identifiers where necessary.
12
+
13
+ null
14
+
15
+ ## Verification verdicts
16
+
17
+ Choose exactly one listed verdict for each claim after inspecting recorded evidence. Unknown data alone cannot establish support. These placeholders are not verdicts.
18
+
19
+ - writing-live-example
20
+ - Verdict: [supported | partly supported | not supported]
21
+ - Evidence task/event/receipt ids: []
22
+ - Remaining unknowns: unknown
23
+ - general-live-example
24
+ - Verdict: [supported | partly supported | not supported]
25
+ - Evidence task/event/receipt ids: []
26
+ - Remaining unknowns: unknown
27
+ - research-live-example
28
+ - Verdict: [supported | partly supported | not supported]
29
+ - Evidence task/event/receipt ids: []
30
+ - Remaining unknowns: unknown
31
+ - native-model-ingestion
32
+ - Verdict: [supported | partly supported | not supported]
33
+ - Evidence task/event/receipt ids: []
34
+ - Remaining unknowns: unknown
35
+ - context-retention
36
+ - Verdict: [supported | partly supported | not supported]
37
+ - Evidence task/event/receipt ids: []
38
+ - Remaining unknowns: unknown
39
+ - native-account-research
40
+ - Verdict: [supported | partly supported | not supported]
41
+ - Evidence task/event/receipt ids: []
42
+ - Remaining unknowns: unknown
43
+ - research-measurement
44
+ - Verdict: [supported | partly supported | not supported]
45
+ - Evidence task/event/receipt ids: []
46
+ - Remaining unknowns: unknown
@@ -2,7 +2,7 @@
2
2
  "schemaVersion": 1,
3
3
  "documents": {
4
4
  "README.md": {
5
- "verifiedAgainst": "66db760595ad8bec4d844e4094e8b9db45d38e7a",
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": "66db760595ad8bec4d844e4094e8b9db45d38e7a",
14
+ "verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
15
15
  "paths": [
16
16
  "src/",
17
17
  "templates/"
18
18
  ]
19
19
  },
20
20
  "docs/operations.md": {
21
- "verifiedAgainst": "66db760595ad8bec4d844e4094e8b9db45d38e7a",
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": "66db760595ad8bec4d844e4094e8b9db45d38e7a",
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": "66db760595ad8bec4d844e4094e8b9db45d38e7a",
46
+ "verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
47
47
  "paths": [
48
48
  "src/adapters/",
49
49
  "src/pi/",
@@ -71,11 +71,14 @@
71
71
  "src/hub/roster.ts",
72
72
  "src/hub/instances.ts",
73
73
  "src/hub/action-enforce.ts",
74
- "src/hub/permission-payload.ts"
74
+ "src/hub/permission-payload.ts",
75
+ "src/hub/read-only-shell.ts",
76
+ "src/hub/read-only-shell-proof.ts",
77
+ "src/hub/operational-authority.ts"
75
78
  ]
76
79
  },
77
80
  "docs/agent-notes/benchmarks.md": {
78
- "verifiedAgainst": "66db760595ad8bec4d844e4094e8b9db45d38e7a",
81
+ "verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
79
82
  "paths": [
80
83
  "scripts/benchmarks/",
81
84
  "test/benchmarks/",
@@ -87,7 +90,7 @@
87
90
  ]
88
91
  },
89
92
  "docs/agent-notes/budget.md": {
90
- "verifiedAgainst": "66db760595ad8bec4d844e4094e8b9db45d38e7a",
93
+ "verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
91
94
  "paths": [
92
95
  "src/hub/budget.ts",
93
96
  "src/cli/statusline-tee.ts",
@@ -111,7 +114,7 @@
111
114
  ]
112
115
  },
113
116
  "docs/agent-notes/bus.md": {
114
- "verifiedAgainst": "66db760595ad8bec4d844e4094e8b9db45d38e7a",
117
+ "verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
115
118
  "paths": [
116
119
  "src/hub/bus.ts",
117
120
  "src/hub/envelope.ts",
@@ -126,7 +129,7 @@
126
129
  ]
127
130
  },
128
131
  "docs/agent-notes/daemon.md": {
129
- "verifiedAgainst": "66db760595ad8bec4d844e4094e8b9db45d38e7a",
132
+ "verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
130
133
  "paths": [
131
134
  "src/hub/daemon.ts",
132
135
  "src/hub/control-client.ts",
@@ -199,7 +202,7 @@
199
202
  ]
200
203
  },
201
204
  "docs/agent-notes/local-worker.md": {
202
- "verifiedAgainst": "66db760595ad8bec4d844e4094e8b9db45d38e7a",
205
+ "verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
203
206
  "paths": [
204
207
  "src/adapters/local-worker.ts",
205
208
  "src/local/",
@@ -211,7 +214,7 @@
211
214
  ]
212
215
  },
213
216
  "docs/agent-notes/models.md": {
214
- "verifiedAgainst": "66db760595ad8bec4d844e4094e8b9db45d38e7a",
217
+ "verifiedAgainst": "af22ee0dd9c6a1f4a5c5fc3f10fbfe4875350c1e",
215
218
  "paths": [
216
219
  "src/models/",
217
220
  "src/hub/inference.ts",
@@ -219,7 +222,7 @@
219
222
  ]
220
223
  },
221
224
  "docs/agent-notes/tasks.md": {
222
- "verifiedAgainst": "66db760595ad8bec4d844e4094e8b9db45d38e7a",
225
+ "verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
223
226
  "paths": [
224
227
  "src/hub/tasks.ts",
225
228
  "src/hub/board.ts",
@@ -251,11 +254,13 @@
251
254
  "src/hub/spec-read.ts",
252
255
  "src/hub/workflow.ts",
253
256
  "src/hub/deliverables.ts",
254
- "src/hub/deliverable-files.ts"
257
+ "src/hub/deliverable-files.ts",
258
+ "src/hub/sdlc-merge.ts",
259
+ "src/hub/sdlc-mirror.ts"
255
260
  ]
256
261
  },
257
262
  "docs/agent-notes/tests.md": {
258
- "verifiedAgainst": "66db760595ad8bec4d844e4094e8b9db45d38e7a",
263
+ "verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
259
264
  "paths": [
260
265
  "test/",
261
266
  "scripts/check.sh",
@@ -274,11 +279,12 @@
274
279
  "scripts/changed-tests.mjs",
275
280
  "scripts/check-closing-refs.mjs",
276
281
  ".github/workflows/closing-refs.yml",
277
- "scripts/lib/ts-ast.ts"
282
+ "scripts/lib/ts-ast.ts",
283
+ "test/cli-json-guard.test.ts"
278
284
  ]
279
285
  },
280
286
  "docs/agent-notes/README.md": {
281
- "verifiedAgainst": "66db760595ad8bec4d844e4094e8b9db45d38e7a",
287
+ "verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
282
288
  "paths": [
283
289
  "src/cli/console-state.ts",
284
290
  "src/cli/attention.ts",
@@ -287,7 +293,7 @@
287
293
  ]
288
294
  },
289
295
  "src/cli/help.ts": {
290
- "verifiedAgainst": "66db760595ad8bec4d844e4094e8b9db45d38e7a",
296
+ "verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
291
297
  "paths": [
292
298
  "src/cli/main.ts",
293
299
  "src/cli/launch.ts",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@staix/agent-hub",
3
- "version": "0.17.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.7.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.17.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",