@staix/agent-hub 0.15.0 → 0.16.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.
- package/CHANGELOG.md +25 -0
- package/README.md +12 -8
- package/docs/agent-notes/README.md +4 -0
- package/docs/agent-notes/adapters.md +6 -3
- package/docs/agent-notes/budget.md +4 -2
- package/docs/agent-notes/daemon.md +9 -3
- package/docs/agent-notes/local-worker.md +2 -2
- package/docs/agent-notes/tasks.md +13 -1
- package/docs/agent-notes/tests.md +3 -0
- package/docs/dogfood.md +13 -4
- package/docs/events.md +1 -1
- package/docs/memory.md +78 -10
- package/docs/operations.md +116 -30
- package/docs/quickstart.md +8 -4
- package/docs/security.md +80 -18
- package/docs/smoke.md +7 -3
- package/docs/specs/2026-09-19-agent-hub-design.md +84 -8
- package/docs/verification/2026-10-24-always-ask-adapters.md +52 -0
- package/docs/verification/2026-10-24-amem-runtime.md +69 -0
- package/docs/verification/2026-10-24-codex-0164-reviewer-isolation.md +55 -0
- package/docs/verification/2026-10-24-delivery-holds-ac8.md +264 -0
- package/docs/verification/2026-10-24-hardcoded-values.md +2108 -0
- package/docs/verification/2026-10-25-0.16.0.md +82 -0
- package/docs/verified.json +26 -17
- package/package.json +2 -2
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +2085 -1702
- package/src/adapters/acp.ts +5 -2
- package/src/adapters/codex-appserver.ts +69 -1
- package/src/adapters/compat.ts +2 -2
- package/src/adapters/definitions/builtin-metadata.ts +3 -3
- package/src/adapters/local-worker.ts +5 -4
- package/src/adapters/registry-types.ts +1 -1
- package/src/cli/console-state.ts +20 -2
- package/src/cli/console-symbols.ts +3 -2
- package/src/cli/console-usage.ts +5 -2
- package/src/cli/console.ts +5 -1
- package/src/cli/help.ts +7 -5
- package/src/cli/identity.ts +3 -1
- package/src/cli/main.ts +44 -10
- package/src/cli/memory.ts +119 -7
- package/src/cli/output.ts +12 -4
- package/src/hub/action-class.ts +211 -17
- package/src/hub/action-enforce.ts +13 -5
- package/src/hub/action-policy.ts +19 -8
- package/src/hub/ask.ts +16 -4
- package/src/hub/board.ts +86 -31
- package/src/hub/budget-notes.ts +65 -0
- package/src/hub/budget.ts +42 -10
- package/src/hub/bus.ts +1 -1
- package/src/hub/code-facts.ts +56 -16
- package/src/hub/conductor.ts +5 -2
- package/src/hub/config-apply.ts +7 -0
- package/src/hub/control-client.ts +1 -1
- package/src/hub/daemon.ts +250 -49
- package/src/hub/envelope.ts +1 -1
- package/src/hub/events.ts +2 -2
- package/src/hub/hub-tools.ts +3 -3
- package/src/hub/kimi-usage.ts +12 -0
- package/src/hub/lifecycle.ts +10 -5
- package/src/hub/memory-code-head.ts +44 -0
- package/src/hub/memory-code-overlay.ts +119 -0
- package/src/hub/memory-plan-guard.ts +10 -1
- package/src/hub/memory-stale-anchors.ts +159 -0
- package/src/hub/memory-symbols.ts +74 -0
- package/src/hub/planning.ts +10 -4
- package/src/hub/read-tools.ts +21 -3
- package/src/hub/sdlc.ts +34 -0
- package/src/hub/settings.ts +17 -2
- package/src/hub/settled-roles.ts +15 -3
- package/src/hub/task-sweep.ts +25 -3
- package/src/hub/tasks.ts +353 -64
- package/src/hub/token-window.ts +15 -2
- package/src/hub/window-kind.ts +13 -0
- package/src/local/tools.ts +5 -5
- package/src/memory/brief.ts +22 -12
- package/src/memory/capture.ts +26 -40
- package/src/memory/client.ts +137 -65
- package/src/memory/recall.ts +13 -22
- package/src/ui/index.html +100 -18
package/src/hub/code-facts.ts
CHANGED
|
@@ -10,9 +10,12 @@ import { releaseTrackedClean } from "./release-worktree.ts";
|
|
|
10
10
|
|
|
11
11
|
export interface CodeSymbol { name: string; kind: string; path: string; line: number; endLine?: number; ambiguous: boolean; change: "unknown" }
|
|
12
12
|
export type CodeFactResult<T> = { state: "known"; value: T } | { state: "unknown" };
|
|
13
|
-
export interface CodeLocation { repo: string; head: string; private?: boolean }
|
|
13
|
+
export interface CodeLocation { repo: string; head: string; private?: boolean; priority?: "optional" }
|
|
14
|
+
export const OPTIONAL_CODE_UNKNOWN_TTL_MS = 5000;
|
|
14
15
|
const unknown = (): {state:"unknown"} => ({state:"unknown"});
|
|
15
16
|
const SHA = /^[a-f0-9]{40}$/;
|
|
17
|
+
/** These host anchor spellings have no indexed-name lookup attestation in ripwire 0.6.7. */
|
|
18
|
+
export const unindexedSymbolSyntax = (name: string): boolean => /#|:|->/.test(name);
|
|
16
19
|
// Keep the owned group leader alive after the CLI exits, until the host has stopped every group member.
|
|
17
20
|
const LAUNCHER = String.raw`
|
|
18
21
|
const {writeSync,closeSync}=require("node:fs");
|
|
@@ -76,6 +79,9 @@ function changed(raw:string,head:string,base:string,cwd:string): CodeSymbol[] {
|
|
|
76
79
|
/** Sole optional ripwire boundary. No model/config-selected command, no raw output in results or errors. */
|
|
77
80
|
export class CodeFacts {
|
|
78
81
|
private readonly cache=new Map<string,Promise<CodeFactResult<unknown>>>();
|
|
82
|
+
private readonly optionalUnknown=new Map<string,number>();
|
|
83
|
+
private priorityWaiting=0;
|
|
84
|
+
private active?:{id:string;optional:boolean;cancelled:boolean;done:Promise<void>};
|
|
79
85
|
private busy=false;
|
|
80
86
|
private closing=false;
|
|
81
87
|
private pending?:Promise<CodeFactResult<unknown>>;
|
|
@@ -83,13 +89,13 @@ export class CodeFacts {
|
|
|
83
89
|
private failedChild?:ReturnType<typeof spawn>;
|
|
84
90
|
private retained?:{tree:{cwd:string;close:()=>void};child:ReturnType<typeof spawn>};
|
|
85
91
|
async close():Promise<void> {
|
|
86
|
-
this.closing=true;this.cancelActive?.();await this.pending;
|
|
92
|
+
this.closing=true;this.cancelActive?.();await (this.active?.done ?? this.pending);
|
|
87
93
|
if(this.retained){
|
|
88
94
|
try{await stopOwnedProcess(this.retained.child,{group:true,termMs:50,killMs:500});this.retained.tree.close();this.retained=undefined;}
|
|
89
95
|
catch{throw Error("code facts cleanup unconfirmed");}
|
|
90
96
|
}
|
|
91
97
|
}
|
|
92
|
-
constructor(private readonly options:{env?:NodeJS.ProcessEnv;timeoutMs?:number}={}) {}
|
|
98
|
+
constructor(private readonly options:{env?:NodeJS.ProcessEnv;timeoutMs?:number;now?:()=>number}={}) {}
|
|
93
99
|
private git(repo:string,args:string[]) {
|
|
94
100
|
return hubGitSync(["-C",repo,...args],{encoding:"utf8",env:hubGitEnv(childEnv(),{repositoryIsolation:true}),timeout:5000,maxBuffer:1024*1024});
|
|
95
101
|
}
|
|
@@ -107,7 +113,7 @@ export class CodeFacts {
|
|
|
107
113
|
return env;
|
|
108
114
|
}
|
|
109
115
|
private async run(bin:string,args:string[],cwd:string):Promise<{code:number;out:string}> {
|
|
110
|
-
if(this.closing)return {code:1,out:""};
|
|
116
|
+
if(this.closing || this.active?.cancelled)return {code:1,out:""};
|
|
111
117
|
return new Promise(resolve=>{
|
|
112
118
|
let child:ReturnType<typeof spawn>;
|
|
113
119
|
try{child=spawn(process.execPath,["-e",LAUNCHER,"--",bin,...args],{cwd,env:this.nativeEnv(cwd),detached:true,stdio:["ignore","pipe","ignore","pipe"]});}catch{resolve({code:1,out:""});return;}
|
|
@@ -164,29 +170,58 @@ export class CodeFacts {
|
|
|
164
170
|
return{cwd,close};
|
|
165
171
|
}catch{close();throw Error();}
|
|
166
172
|
}
|
|
167
|
-
private query<T>(at:CodeLocation,key:string,read:(cwd:string,bin:string)=>Promise<T>):Promise<CodeFactResult<T>> {
|
|
168
|
-
if(this.closing || this.retained || at.private || !SHA.test(at.head))return
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if(this.
|
|
172
|
-
|
|
173
|
+
private async query<T>(at:CodeLocation,key:string,read:(cwd:string,bin:string)=>Promise<T>):Promise<CodeFactResult<T>> {
|
|
174
|
+
if(this.closing || this.retained || at.private || !SHA.test(at.head))return unknown();
|
|
175
|
+
const optional=at.priority==="optional";
|
|
176
|
+
// A read-time graph poll must never take the next slot from review/completion work.
|
|
177
|
+
if(optional && (this.priorityWaiting>0 || this.active && !this.active.optional))return unknown();
|
|
178
|
+
let repo:string;try{repo=realPath(at.repo);}catch{return unknown();}
|
|
179
|
+
const id=JSON.stringify([repo,at.head,key]),hit=this.cache.get(id);
|
|
180
|
+
if(hit && !(this.active?.optional && !optional && this.active.id===id))return structuredClone(await hit) as CodeFactResult<T>;
|
|
181
|
+
if(!optional && this.active?.optional) {
|
|
182
|
+
const interrupted=this.active;
|
|
183
|
+
this.priorityWaiting++;
|
|
184
|
+
interrupted.cancelled=true;this.cache.delete(interrupted.id);this.cancelActive?.();
|
|
185
|
+
try{await interrupted.done;}finally{this.priorityWaiting--;}
|
|
186
|
+
return this.query(at,key,read);
|
|
187
|
+
}
|
|
188
|
+
if(optional) {
|
|
189
|
+
const now=(this.options.now ?? performance.now.bind(performance))(),until=this.optionalUnknown.get(id);
|
|
190
|
+
if(until!==undefined && until>now)return unknown();
|
|
191
|
+
this.optionalUnknown.delete(id);
|
|
192
|
+
}
|
|
193
|
+
if(this.busy)return unknown(); // Preserve default-vs-default contention semantics.
|
|
194
|
+
let finished!:()=>void;
|
|
195
|
+
const active={id,optional,cancelled:false,done:new Promise<void>(resolve=>{finished=resolve;})};
|
|
196
|
+
this.active=active;this.busy=true;this.failedChild=undefined;
|
|
173
197
|
const job=(async()=>{
|
|
174
198
|
let tree:ReturnType<CodeFacts["tree"]>|undefined;let answer:CodeFactResult<T>=unknown();
|
|
175
199
|
try{
|
|
176
200
|
const found=Bun.which("ripwire",{PATH:(this.options.env??process.env).PATH});if(!found)throw Error();const bin=realPath(found);if(within(repo,bin))throw Error();
|
|
177
201
|
tree=this.tree(repo,at.head);
|
|
178
|
-
const version=await this.run(bin,["--version"],tree.cwd);if(version.code!==0 || !/^ripwire 0\.6\.7(?:\s|$)/.test(version.out))throw Error();
|
|
202
|
+
const version=await this.run(bin,["--version"],tree.cwd);if(active.cancelled || version.code!==0 || !/^ripwire 0\.6\.7(?:\s|$)/.test(version.out))throw Error();
|
|
179
203
|
const value=await read(tree.cwd,bin);
|
|
180
|
-
if(this.value(tree.cwd,["rev-parse","HEAD"])!==at.head || !releaseTrackedClean(tree.cwd,args=>{const r=this.git(tree!.cwd,args);return{code:r.status??1,stdout:r.stdout};}))throw Error();
|
|
204
|
+
if(active.cancelled || this.value(tree.cwd,["rev-parse","HEAD"])!==at.head || !releaseTrackedClean(tree.cwd,args=>{const r=this.git(tree!.cwd,args);return{code:r.status??1,stdout:r.stdout};}))throw Error();
|
|
181
205
|
answer={state:"known",value};
|
|
182
206
|
}catch{/* Optional derived evidence is unknown, never a review refusal. */}
|
|
183
|
-
finally{
|
|
207
|
+
finally{
|
|
208
|
+
if(tree && this.failedChild){this.retained={tree,child:this.failedChild};answer=unknown();}else try{tree?.close();}catch{answer=unknown();}
|
|
209
|
+
this.busy=false;if(this.active===active)this.active=undefined;finished();
|
|
210
|
+
}
|
|
184
211
|
return answer;
|
|
185
212
|
})();
|
|
186
213
|
this.pending=job;this.cache.set(id,job);
|
|
187
214
|
if(this.cache.size>128)this.cache.delete(this.cache.keys().next().value!);
|
|
188
|
-
void job.then(answer=>{
|
|
189
|
-
|
|
215
|
+
void job.then(answer=>{
|
|
216
|
+
if(answer.state==="unknown") {
|
|
217
|
+
if(this.cache.get(id)===job)this.cache.delete(id);
|
|
218
|
+
if(optional && !this.closing) {
|
|
219
|
+
this.optionalUnknown.set(id,(this.options.now ?? performance.now.bind(performance))()+OPTIONAL_CODE_UNKNOWN_TTL_MS);
|
|
220
|
+
if(this.optionalUnknown.size>128)this.optionalUnknown.delete(this.optionalUnknown.keys().next().value!);
|
|
221
|
+
}
|
|
222
|
+
} else this.optionalUnknown.delete(id);
|
|
223
|
+
});
|
|
224
|
+
return structuredClone(await job);
|
|
190
225
|
}
|
|
191
226
|
changedSymbols(repo:string,base:string,head:string,options:Omit<CodeLocation,"repo"|"head">={}):Promise<CodeFactResult<CodeSymbol[]>> {
|
|
192
227
|
if(!SHA.test(base))return Promise.resolve(unknown());
|
|
@@ -205,12 +240,17 @@ export class CodeFacts {
|
|
|
205
240
|
if(v.of!==symbol || !Number.isSafeInteger(v.defs)||v.defs<1 || !Number.isSafeInteger(v.count)||v.count<0 || !Array.isArray(v[kind])||v.count!==v[kind].length || v.count>MAX_SYMBOLS || realPath(v.root)!==realPath(cwd) || ![0,1,false,true].includes(v.counts_floor)
|
|
206
241
|
|| !Number.isSafeInteger(v.hop_tested)||v.hop_tested<0||!Number.isSafeInteger(v.hop_untested)||v.hop_untested<0
|
|
207
242
|
|| !Number.isSafeInteger(v.graph_ambiguous)||v.graph_ambiguous<0||!Number.isSafeInteger(v.graph_unresolved)||v.graph_unresolved<0)throw Error();
|
|
243
|
+
if(v.defs!==1 || v.via!==undefined && v.via!=="resolved")throw Error(); // A name-only or multiply-defined seed is unknown, never a known empty neighbourhood.
|
|
208
244
|
return {defs:v.defs,countsFloor:!!v.counts_floor,items:v[kind].map((s:Record<string,unknown>)=>({...cleanSymbol(s.n,s.t,s.p,s.via),ambiguous:s.via==="name" || v.defs>1 || v.graph_ambiguous>0}))};
|
|
209
245
|
}
|
|
210
246
|
exists(repo:string,head:string,symbol:string,options:Omit<CodeLocation,"repo"|"head">={}):Promise<CodeFactResult<{exists:boolean;ambiguous:boolean}>> {
|
|
211
247
|
if(!text(symbol))return Promise.resolve(unknown());
|
|
212
248
|
return this.query({...options,repo,head},`exists:${symbol}`,async(cwd,bin)=>{const r=await this.run(bin,[cwd,`--callers=${symbol}`,"--no-cache"],cwd);
|
|
213
|
-
if(r.code!==0){const n=xml(r.out);if(n.tag==="callers" && n.attrs.schema==="ripwire.callers/v1" && n.attrs.of===symbol && n.attrs.found==="0")
|
|
249
|
+
if(r.code!==0){const n=xml(r.out);if(n.tag==="callers" && n.attrs.schema==="ripwire.callers/v1" && n.attrs.of===symbol && n.attrs.found==="0") {
|
|
250
|
+
// ponytail: ripwire 0.6.7 can miss a qualified name whose bare method exists. Require a CLI/schema qualified-lookup attestation before trusting its absence.
|
|
251
|
+
if(symbol.includes(".") || unindexedSymbolSyntax(symbol))throw Error();
|
|
252
|
+
return{exists:false,ambiguous:false};
|
|
253
|
+
}throw Error();}
|
|
214
254
|
const n=xml(r.out);if(n.tag!=="callers" || n.attrs.schema!=="ripwire.callers/v1" || n.attrs.of!==symbol || !integer(n.attrs.defs)||Number(n.attrs.defs)<1)throw Error();return{exists:true,ambiguous:n.attrs.via==="name" || Number(n.attrs.defs)>1};});
|
|
215
255
|
}
|
|
216
256
|
neighbours(repo:string,head:string,symbol:string,depth=1,options:Omit<CodeLocation,"repo"|"head">={}):Promise<CodeFactResult<{callers:CodeSymbol[];callees:CodeSymbol[];hiddenAmbiguous:number;countsFloor:boolean}>> {
|
package/src/hub/conductor.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type { StartMode } from "./start-mode.ts";
|
|
|
9
9
|
import type { HubEvent } from "./events.ts";
|
|
10
10
|
import type { SupervisionFeed } from "./supervision.ts";
|
|
11
11
|
import type { Budget } from "./budget.ts";
|
|
12
|
+
import { windowKind, type WindowKind } from "./window-kind.ts";
|
|
12
13
|
import { HUB_DB_BUSY_TIMEOUT_MS } from "./db-timeout.ts";
|
|
13
14
|
import { supportsHeadlessInstances } from "./instances.ts";
|
|
14
15
|
|
|
@@ -17,9 +18,11 @@ export function conductorProgressSink(record: (event: HubEvent) => void, feed: P
|
|
|
17
18
|
return event => { record(event); if (event.type === "stuck") feed.milestone(event.task, "stuck", { reason: event.category }); };
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
/** What `Budget.status` returns, with the fields added after it optional so an older shape still projects. */
|
|
22
|
+
type PublicBudgetStatus = Record<string, { windows: (Omit<ReturnType<Budget["status"]>[string]["windows"][number], "kind" | "superseded"> & { kind?: WindowKind; superseded?: boolean })[]; paused?: ReturnType<Budget["status"]>[string]["paused"] }>;
|
|
23
|
+
export function publicPeerBudget(status: PublicBudgetStatus, nameable: (text: string) => boolean): Record<string, unknown> {
|
|
21
24
|
return Object.fromEntries(Object.entries(status).map(([peer, value]) => [peer, {
|
|
22
|
-
windows: value.windows.map(window => ({ id: window.id, used: window.used, at: window.at, stale: window.stale,
|
|
25
|
+
windows: value.windows.map(window => ({ id: window.id, kind: windowKind(window), used: window.used, at: window.at, stale: window.stale, ...(window.superseded ? { superseded: true } : {}),
|
|
23
26
|
...(window.resetsAt === undefined ? {} : { resetsAt: window.resetsAt }),
|
|
24
27
|
...(window.windowMins === undefined ? {} : { windowMins: window.windowMins }),
|
|
25
28
|
source: nameable(window.source) ? window.source : "[quota source withheld]",
|
package/src/hub/config-apply.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { projectionConfig } from "./text-projection.ts";
|
|
|
17
17
|
import { validateInstances, type InstancesConfig } from "./instances.ts";
|
|
18
18
|
import { peerRosterConfig, staleInstanceKey } from "./roster.ts";
|
|
19
19
|
import { CHARACTER_IDS } from "../pet/characters.ts";
|
|
20
|
+
import { checkSettingValue, settingDef } from "./settings.ts";
|
|
20
21
|
import { petPalette } from "../pet/config.ts";
|
|
21
22
|
|
|
22
23
|
export type ApplyClass = "live" | "next use" | "restart";
|
|
@@ -189,6 +190,12 @@ export function validateRawConfig(raw: unknown, declarations?: InstancesConfig,
|
|
|
189
190
|
const instances = validateInstances(value.instances ?? declarations, catalog);
|
|
190
191
|
block({ ...schema,
|
|
191
192
|
instances: v => { validateInstances(v, catalog); },
|
|
193
|
+
planner: v => {
|
|
194
|
+
if (v === null) fail("config.planner", "takes a peer id or a nonempty list");
|
|
195
|
+
const def = settingDef("planner", instances, catalog);
|
|
196
|
+
if (typeof def === "string") return fail("config.planner", def);
|
|
197
|
+
checkSettingValue(def, v);
|
|
198
|
+
},
|
|
192
199
|
permission_modes: v => { permissionDefaults(v, instances, catalog); },
|
|
193
200
|
roster: v => { peerRosterConfig(v, instances, catalog, inertModuleRoster); },
|
|
194
201
|
peers: v => { peerStartConfig(v, instances, catalog); },
|
|
@@ -9,7 +9,7 @@ export function stateDirFor(cwd: string): string {
|
|
|
9
9
|
|
|
10
10
|
/** Control WS wire version. 2 = `deliver` carries `envs` (digests); 3 = `tools` role and task messages; 4 = budget messages and `hub_checkpoint`; 5 = `ask`; 6 = console-only `ui` session bootstrap; 8 = controlled recovery; 9 = Pi bridge metadata; 10 = durable delivery receipts; 11 = queue hold diagnostics; 12 = generation-bound channel settlement and execution budgets; 13 = turn-free facts (`facts` with tool, session and transcript binding, `silenced`) and per-recipient send results (issue #108). 14 = native context-window status and session-bound context checkpoint request ids (issue #185). 15 = conductor tools and permission deadlines/closed events (issues #190, #194). 16 = a Claude session without channel evidence: hello `channel: false` and `inbox` (issue #205). The plugin is installed apart from the daemon, so they can drift. */
|
|
11
11
|
export const PROTOCOL = 16;
|
|
12
|
-
export const DAEMON_FEATURES = ["external_adapter_modules", "configured_acp", "native_hook_launch", "task_cancel", "recovery_deferred_hub", "peer_instances", "role_stages", "release_trains", "reviewer_pool", "native_tui_launch", "terminal_owner", "cli_audit_delegated"] as const;
|
|
12
|
+
export const DAEMON_FEATURES = ["external_adapter_modules", "configured_acp", "native_hook_launch", "task_cancel", "recovery_deferred_hub", "peer_instances", "role_stages", "task_planners", "release_trains", "reviewer_pool", "native_tui_launch", "terminal_owner", "cli_audit_delegated"] as const;
|
|
13
13
|
/** Protocols a current coordinator may authenticate while upgrading a running source. */
|
|
14
14
|
export const RECOVERY_SOURCE_PROTOCOLS = [9, 10, 11, 12, 13, 14, 15, PROTOCOL] as const;
|
|
15
15
|
|