@staix/agent-hub 0.29.0 → 0.30.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 +12 -0
- package/README.md +5 -3
- package/docs/agent-notes/README.md +12 -0
- package/docs/agent-notes/adapters.md +2 -1
- package/docs/agent-notes/bus.md +1 -0
- package/docs/agent-notes/daemon.md +2 -1
- package/docs/agent-notes/tasks.md +4 -2
- package/docs/agent-notes/tests.md +2 -0
- package/docs/design/adapters.md +56 -0
- package/docs/design/agent-hub-architecture.md +12 -0
- package/docs/design/bus.md +8 -0
- package/docs/design/task-board.md +18 -0
- package/docs/dogfood.md +7 -4
- package/docs/operations.md +106 -11
- package/docs/quickstart.md +10 -3
- package/docs/sdlc.md +1 -1
- package/docs/security.md +28 -2
- package/docs/smoke.md +13 -2
- package/docs/specs/2026-10-12-office-view-design.md +86 -3
- package/docs/verification/2026-11-03-0.30.0.md +54 -0
- package/docs/verification/2026-11-03-agy-1.4.12-launcher-nonce.md +42 -0
- package/docs/verification/2026-11-03-agy-1.4.12.md +90 -0
- package/docs/verification/2026-11-03-agy-tui-close.md +49 -0
- package/docs/verification/2026-11-03-grok-1.0.62-t0.md +150 -0
- package/docs/verification/2026-11-03-pi-150-compat.md +40 -0
- package/docs/verification/2026-11-03-pi-title-api.md +26 -0
- package/docs/verification/2026-11-03-qwen-0265-acp.md +55 -0
- package/docs/verification/2026-11-03-qwen-0265-dual-output.md +40 -0
- package/docs/verification/dogfood/2026-11-03.md +3095 -0
- package/docs/verification/pi-150-model-free.ts +57 -0
- package/docs/verification/pi-150-native.mjs +21 -0
- package/docs/verification/pi-150-title-api.mjs +31 -0
- package/docs/verified.json +14 -14
- package/package.json +2 -2
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +193 -29
- package/src/adapters/compat.ts +10 -10
- package/src/adapters/definitions/builtin-metadata.ts +2 -2
- package/src/adapters/local-worker.ts +22 -6
- package/src/adapters/pi.ts +92 -10
- package/src/adapters/registry-types.ts +4 -0
- package/src/adapters/registry.ts +2 -0
- package/src/cli/autonomy.ts +66 -1
- package/src/cli/console-state.ts +6 -2
- package/src/cli/help.ts +2 -0
- package/src/cli/identity.ts +8 -1
- package/src/cli/main.ts +39 -0
- package/src/cli/worktree-tidy.ts +158 -0
- package/src/cli/worktrees-output.ts +43 -0
- package/src/hub/board.ts +28 -3
- package/src/hub/bus.ts +4 -4
- package/src/hub/conductor.ts +4 -3
- package/src/hub/config-apply.ts +2 -1
- package/src/hub/daemon.ts +237 -19
- package/src/hub/dogfood-read.ts +62 -9
- package/src/hub/hub-tools.ts +11 -5
- package/src/hub/manager.ts +9 -1
- package/src/hub/peer-doing.ts +33 -3
- package/src/hub/peers.ts +7 -0
- package/src/hub/settings.ts +3 -0
- package/src/hub/tasks.ts +30 -8
- package/src/hub/ui.ts +2 -0
- package/src/hub/worktree-prs.ts +100 -0
- package/src/hub/worktree-shelf.ts +246 -0
- package/src/memory/capture.ts +1 -1
- package/src/pi/extension.ts +43 -3
- package/src/ui/index.html +133 -10
- package/src/ui/office-model.ts +64 -8
- package/src/ui/office-space.ts +1 -0
- package/src/ui/plugin.ts +14 -3
|
@@ -305,13 +305,13 @@ var init_compat = __esm(() => {
|
|
|
305
305
|
COMPAT = {
|
|
306
306
|
pi: {
|
|
307
307
|
label: "Pi",
|
|
308
|
-
verified: ["1.4.0"],
|
|
308
|
+
verified: ["1.4.0", "1.5.0"],
|
|
309
309
|
gate: "contract",
|
|
310
310
|
probe: ["pi", "--version"],
|
|
311
311
|
config: "pi.cmd",
|
|
312
312
|
parse: (out) => plain(out.trim()),
|
|
313
|
-
trustNote: "The model-free extension load, RPC session identity and managed tool receipt path were checked on 1.4.0. Permissions remain host-owned; account-backed inference and another version's extension contract are unverified.",
|
|
314
|
-
receipt: "docs/verification/2026-
|
|
313
|
+
trustNote: "The model-free extension load, RPC session identity and managed tool receipt path were checked on 1.4.0 and 1.5.0. Permissions remain host-owned; account-backed inference and another version's extension contract are unverified.",
|
|
314
|
+
receipt: "docs/verification/2026-11-03-pi-150-compat.md"
|
|
315
315
|
},
|
|
316
316
|
"claude-implementer": {
|
|
317
317
|
label: "Claude headless implementer",
|
|
@@ -361,23 +361,23 @@ var init_compat = __esm(() => {
|
|
|
361
361
|
},
|
|
362
362
|
agy: {
|
|
363
363
|
label: "agy",
|
|
364
|
-
verified: ["1.4.2", "1.4.7"],
|
|
364
|
+
verified: ["1.4.2", "1.4.7", "1.4.12"],
|
|
365
365
|
line: { min: "1.4.0", below: "1.5.0" },
|
|
366
366
|
gate: "contract",
|
|
367
367
|
probe: ["agy", "--version"],
|
|
368
368
|
parse: (out) => plain(out.trim()),
|
|
369
|
-
trustNote: "Only 1.4.2 and 1.4.
|
|
370
|
-
receipt: "docs/verification/2026-
|
|
369
|
+
trustNote: "Only 1.4.2, 1.4.7 and 1.4.12 are T0-verified; the plugin MCP and hook contract of another version is unverified.",
|
|
370
|
+
receipt: "docs/verification/2026-11-03-agy-1.4.12-launcher-nonce.md"
|
|
371
371
|
},
|
|
372
372
|
qwen: {
|
|
373
373
|
label: "Qwen Code",
|
|
374
|
-
verified: ["0.25.0", "0.26.0", "0.26.1", "0.26.4"],
|
|
374
|
+
verified: ["0.25.0", "0.26.0", "0.26.1", "0.26.4", "0.26.5"],
|
|
375
375
|
gate: "contract",
|
|
376
376
|
probe: ["qwen", "--version"],
|
|
377
377
|
config: "qwen_cmd",
|
|
378
378
|
parse: (out) => plain(out.trim()),
|
|
379
|
-
trustNote: "Native ACP identity, mode ids and permission callbacks were re-checked without inference on 0.26.4; real model turns remain unverified. Another version may change those contracts.",
|
|
380
|
-
receipt: "docs/verification/2026-
|
|
379
|
+
trustNote: "Native ACP identity, mode ids and permission callbacks were re-checked without inference on 0.26.4 and 0.26.5; real model turns remain unverified. Another version may change those contracts.",
|
|
380
|
+
receipt: "docs/verification/2026-11-03-qwen-0265-acp.md"
|
|
381
381
|
},
|
|
382
382
|
"qwen-dual": {
|
|
383
383
|
label: "Qwen Code dual-output TUI",
|
|
@@ -7288,6 +7288,101 @@ var init_pi = __esm(() => {
|
|
|
7288
7288
|
currentReply;
|
|
7289
7289
|
sessionId = "";
|
|
7290
7290
|
sessionFile = "";
|
|
7291
|
+
observedTitle;
|
|
7292
|
+
titleRevision = -1;
|
|
7293
|
+
titleEpoch = 0;
|
|
7294
|
+
nativeTitleEpoch;
|
|
7295
|
+
get sessionTitle() {
|
|
7296
|
+
return this.acceptingTools && this.verifiedOwner() ? this.observedTitle : undefined;
|
|
7297
|
+
}
|
|
7298
|
+
get titleSession() {
|
|
7299
|
+
return this.nativeSession;
|
|
7300
|
+
}
|
|
7301
|
+
async setSessionTitle(title, onlyIfUnnamed, current) {
|
|
7302
|
+
const invalid = (error, mayHaveApplied = false) => ({ ok: false, reason: "invalid", error, ...mayHaveApplied ? { mayHaveApplied: true } : {} });
|
|
7303
|
+
if (typeof title !== "string" || !title.trim() || [...title].length > 80 || typeof onlyIfUnnamed !== "boolean")
|
|
7304
|
+
return invalid("Session title must contain 1-80 normalized characters and use a conditional-write flag; correct the title request and try again.");
|
|
7305
|
+
if (this.stopping || this.state === "offline")
|
|
7306
|
+
return invalid("Pi session is offline or stopped; start or reconnect Pi and check ahub status before naming it.");
|
|
7307
|
+
const session = this.sessionId, generation = this.budgetGeneration, owner = this.sessionGeneration, epoch = this.titleEpoch;
|
|
7308
|
+
if (!owner || !this.verifiedOwner())
|
|
7309
|
+
return invalid("Pi native session owner is unverified; reconnect the retained Pi session and check ahub status before naming it.");
|
|
7310
|
+
if (!this.acceptingTools)
|
|
7311
|
+
return invalid("Pi session is detached or running a personal turn; reconnect its current hub session and check ahub status before naming it.");
|
|
7312
|
+
const valid = () => {
|
|
7313
|
+
try {
|
|
7314
|
+
return this.acceptingTools && this.verifiedOwner() && this.sessionGeneration === owner && this.sessionId === session && this.budgetGeneration === generation && this.titleEpoch === epoch && current?.() !== false;
|
|
7315
|
+
} catch {
|
|
7316
|
+
return false;
|
|
7317
|
+
}
|
|
7318
|
+
};
|
|
7319
|
+
if (!valid())
|
|
7320
|
+
return invalid("Pi session, turn or title admission changed before dispatch; check ahub status before retrying in the current public session.");
|
|
7321
|
+
let dispatched = false;
|
|
7322
|
+
try {
|
|
7323
|
+
const result = await this.sendTui({ type: "set_session_title", title, onlyIfUnnamed, sessionId: session, generation, ...this.nativeTitleEpoch ? { titleEpoch: this.nativeTitleEpoch } : {} }, valid, () => {
|
|
7324
|
+
dispatched = true;
|
|
7325
|
+
});
|
|
7326
|
+
if (!valid())
|
|
7327
|
+
return invalid("Pi session, turn or title admission changed after dispatch; the title may have been applied; check ahub status before retrying.", true);
|
|
7328
|
+
if (result?.ok === false && result.reason === "invalid") {
|
|
7329
|
+
if (result.cause === "session")
|
|
7330
|
+
return invalid("Pi native session changed before the title write; check ahub status and reconnect the current session before retrying.");
|
|
7331
|
+
if (result.cause === "generation")
|
|
7332
|
+
return invalid("Pi native turn changed before the title write; wait for the current turn and check ahub status before retrying.");
|
|
7333
|
+
if (result.cause === "arguments")
|
|
7334
|
+
return invalid("Pi rejected invalid title command arguments; use a 1-80 character normalized title and retry in the current session.");
|
|
7335
|
+
if (result.cause === "native_unavailable")
|
|
7336
|
+
return invalid("Pi native title API is unavailable; restart Pi with the supported hub extension and check ahub status before retrying.");
|
|
7337
|
+
if (result.cause === "native_failed")
|
|
7338
|
+
return invalid("Pi native title API failed without confirming the write; the title may have been applied; check ahub status before retrying.", true);
|
|
7339
|
+
if (result.cause === "native_readback")
|
|
7340
|
+
return invalid("Pi native title readback did not confirm the write; the title may have been applied; check ahub status before retrying.", true);
|
|
7341
|
+
}
|
|
7342
|
+
if (result?.sessionId !== session || typeof result.sessionName !== "string" || this.nativeTitleEpoch !== undefined && result.titleEpoch !== this.nativeTitleEpoch)
|
|
7343
|
+
return invalid("Pi title acknowledgement did not confirm the current native slot; the title may have been applied; check ahub status before retrying.", true);
|
|
7344
|
+
this.observeTitle(result.sessionName, result.titleRevision, result.titleEpoch);
|
|
7345
|
+
if (result.ok === false && result.reason === "already_named")
|
|
7346
|
+
return { ok: false, reason: "already_named", error: "This session already has a title; keep it or ask the person to rename it with ahub title." };
|
|
7347
|
+
if (result.ok === true && result.sessionName === title)
|
|
7348
|
+
return { ok: true };
|
|
7349
|
+
return invalid("Pi native title write or readback was not confirmed; the title may have been applied; check ahub status before retrying.", true);
|
|
7350
|
+
} catch (error) {
|
|
7351
|
+
const timeout = error instanceof Error && error.message.includes("acknowledgement timed out");
|
|
7352
|
+
if (dispatched)
|
|
7353
|
+
return invalid(`Pi title ${timeout ? "acknowledgement timed out" : "bridge command failed after dispatch"}; the title may have been applied; check ahub status before retrying.`, true);
|
|
7354
|
+
return invalid(`Pi title ${timeout ? "command expired" : "admission changed or bridge failed"} before dispatch; check ahub status before retrying in the current session.`);
|
|
7355
|
+
}
|
|
7356
|
+
}
|
|
7357
|
+
observeTitle(value, revision, epoch) {
|
|
7358
|
+
if (this.nativeTitleEpoch !== undefined && epoch !== this.nativeTitleEpoch)
|
|
7359
|
+
return;
|
|
7360
|
+
if (typeof revision === "number" && Number.isSafeInteger(revision) && revision >= 0) {
|
|
7361
|
+
if (revision < this.titleRevision)
|
|
7362
|
+
return;
|
|
7363
|
+
this.titleRevision = revision;
|
|
7364
|
+
}
|
|
7365
|
+
if (value === undefined)
|
|
7366
|
+
value = "";
|
|
7367
|
+
if (typeof value !== "string")
|
|
7368
|
+
return;
|
|
7369
|
+
if (value)
|
|
7370
|
+
this.noteActivity();
|
|
7371
|
+
if (this.observedTitle !== value) {
|
|
7372
|
+
this.observedTitle = value;
|
|
7373
|
+
this.onDoing?.("title", value);
|
|
7374
|
+
}
|
|
7375
|
+
}
|
|
7376
|
+
observeSnapshotTitle(snapshot, before, beforeEpoch) {
|
|
7377
|
+
if (this.titleEpoch !== beforeEpoch || snapshot.titleEpoch !== undefined && snapshot.titleEpoch !== this.nativeTitleEpoch)
|
|
7378
|
+
return false;
|
|
7379
|
+
if (snapshot.titleRevision === undefined && this.titleRevision !== before)
|
|
7380
|
+
return false;
|
|
7381
|
+
if (typeof snapshot.titleRevision === "number" && snapshot.titleRevision < this.titleRevision)
|
|
7382
|
+
return false;
|
|
7383
|
+
this.observeTitle(snapshot.sessionName, snapshot.titleRevision, snapshot.titleEpoch ?? this.nativeTitleEpoch);
|
|
7384
|
+
return true;
|
|
7385
|
+
}
|
|
7291
7386
|
executionBudgetTimer;
|
|
7292
7387
|
executionAbort;
|
|
7293
7388
|
budgetStops = new Map;
|
|
@@ -7406,12 +7501,15 @@ var init_pi = __esm(() => {
|
|
|
7406
7501
|
throw new Error("Pi must settle before session capture");
|
|
7407
7502
|
if (this.sessionFile && existsSync7(this.sessionFile))
|
|
7408
7503
|
return this.recoveryMetadata();
|
|
7504
|
+
const titleRevisionBefore = this.titleRevision, titleEpochBefore = this.titleEpoch;
|
|
7409
7505
|
const snapshot = this.opts.mode === "headless" ? (await this.waitRpc("get_state", 15000)).data : await this.sendTui({ type: "get_session_state" });
|
|
7410
7506
|
if (this.state !== "idle" || this.activeTools || snapshot?.sessionId !== this.sessionId || snapshot?.sessionFile !== this.sessionFile)
|
|
7411
7507
|
throw new Error("Pi source session changed during capture");
|
|
7508
|
+
if (!this.observeSnapshotTitle(snapshot, titleRevisionBefore, titleEpochBefore))
|
|
7509
|
+
throw new Error("Pi source session title changed during capture; retry with a fresh native session snapshot");
|
|
7412
7510
|
const empty = this.opts.mode === "headless" ? snapshot.messageCount === 0 && snapshot.pendingMessageCount === 0 && snapshot.isStreaming === false && snapshot.isCompacting !== true && !snapshot.sessionName : snapshot.empty === true && snapshot.idle === true;
|
|
7413
7511
|
if (!empty || this.activityObserved || this.persistedOnce)
|
|
7414
|
-
throw new Error("Pi session history is not persisted; refusing to recreate it");
|
|
7512
|
+
throw new Error("Pi session history is not persisted; refusing to recreate it. Exchange one message or start a new session, then retry");
|
|
7415
7513
|
this.emptyResumeVerified = true;
|
|
7416
7514
|
const captured = this.recoveryMetadata();
|
|
7417
7515
|
if (typeof captured.sessionId === "string" && !captured.sessionFile)
|
|
@@ -7598,6 +7696,10 @@ var init_pi = __esm(() => {
|
|
|
7598
7696
|
await this.preflight();
|
|
7599
7697
|
this.terminalOwnerObservation = undefined;
|
|
7600
7698
|
this.stopping = false;
|
|
7699
|
+
this.observedTitle = undefined;
|
|
7700
|
+
this.titleRevision = -1;
|
|
7701
|
+
this.nativeTitleEpoch = undefined;
|
|
7702
|
+
this.onDoing?.("reset");
|
|
7601
7703
|
this.rosterDetached = false;
|
|
7602
7704
|
this.rosterUnownedRun = false;
|
|
7603
7705
|
this.requestedStopReason = undefined;
|
|
@@ -7698,6 +7800,7 @@ var init_pi = __esm(() => {
|
|
|
7698
7800
|
this.fail(new Error(`pi exited with ${signal ? `signal ${signal}` : `code ${code}`}`));
|
|
7699
7801
|
this.reportExit(exit);
|
|
7700
7802
|
});
|
|
7803
|
+
const titleRevisionBefore = this.titleRevision, titleEpochBefore = this.titleEpoch;
|
|
7701
7804
|
const state = await this.waitRpc("get_state", 15000);
|
|
7702
7805
|
if (state.success !== true || typeof state.data?.sessionId !== "string" || typeof state.data?.sessionFile !== "string" || !state.data.sessionId || !state.data.sessionFile)
|
|
7703
7806
|
throw new Error("Pi get_state did not prove session identity");
|
|
@@ -7718,6 +7821,7 @@ var init_pi = __esm(() => {
|
|
|
7718
7821
|
this.persistedOnce = existsSync7(this.sessionFile);
|
|
7719
7822
|
if (this.opts.sessionId && this.sessionId !== this.opts.sessionId || this.opts.sessionFile && this.sessionFile !== this.opts.sessionFile)
|
|
7720
7823
|
throw new Error("Pi startup session identity mismatch");
|
|
7824
|
+
this.observeSnapshotTitle(state.data, titleRevisionBefore, titleEpochBefore);
|
|
7721
7825
|
this.started = true;
|
|
7722
7826
|
this.setState("idle");
|
|
7723
7827
|
}
|
|
@@ -7807,11 +7911,13 @@ var init_pi = __esm(() => {
|
|
|
7807
7911
|
if (this.pendingPromptId)
|
|
7808
7912
|
throw new Error("Pi prompt admission is still pending; wait before reverifying the retained terminal");
|
|
7809
7913
|
const epoch = this.rosterEpoch, lifecycle = this.rosterLifecycleRevision, session = this.sessionId, file = this.sessionFile;
|
|
7914
|
+
const titleRevisionBefore = this.titleRevision, titleEpochBefore = this.titleEpoch;
|
|
7810
7915
|
const snapshot = await this.sendTui({ type: "get_session_state" });
|
|
7811
7916
|
if (epoch !== this.rosterEpoch || this.stopping || !this.verifiedOwner() || this.activeTools || snapshot?.sessionId !== session || snapshot?.sessionFile !== file || typeof snapshot?.idle !== "boolean")
|
|
7812
7917
|
throw new Error("Pi roster return could not verify the current native session");
|
|
7813
7918
|
if (lifecycle !== this.rosterLifecycleRevision)
|
|
7814
7919
|
throw new Error("Pi native activity changed during roster return; retry with a fresh session snapshot");
|
|
7920
|
+
this.observeSnapshotTitle(snapshot, titleRevisionBefore, titleEpochBefore);
|
|
7815
7921
|
this.rosterDetached = false;
|
|
7816
7922
|
this.hubPromptId = undefined;
|
|
7817
7923
|
this.hubPromptGeneration = undefined;
|
|
@@ -7962,12 +8068,14 @@ var init_pi = __esm(() => {
|
|
|
7962
8068
|
attempted = true;
|
|
7963
8069
|
if (deliveryId)
|
|
7964
8070
|
this.activeDeliveryIds.add(deliveryId);
|
|
8071
|
+
const message = [envs.some((env) => env.private || remoteEnvelope(env)) ? undefined : this.opts.titlePreface?.(), renderDigest(envs, true)].filter(Boolean).join(`
|
|
8072
|
+
`);
|
|
7965
8073
|
if (this.opts.mode === "tui") {
|
|
7966
|
-
const pending = this.sendTui({ type: "prompt", message
|
|
8074
|
+
const pending = this.sendTui({ type: "prompt", message });
|
|
7967
8075
|
promptId = this.pendingPromptId;
|
|
7968
8076
|
await pending;
|
|
7969
8077
|
} else
|
|
7970
|
-
await this.sendRpc({ type: "prompt", message
|
|
8078
|
+
await this.sendRpc({ type: "prompt", message });
|
|
7971
8079
|
if (promptId !== undefined && promptId !== this.pendingPromptId)
|
|
7972
8080
|
return;
|
|
7973
8081
|
if (this.fencedPrompt && this.rosterDetached) {
|
|
@@ -8144,6 +8252,10 @@ var init_pi = __esm(() => {
|
|
|
8144
8252
|
this.opts.log?.(`[${this.id}] Pi session identity mismatch`);
|
|
8145
8253
|
return;
|
|
8146
8254
|
}
|
|
8255
|
+
this.titleRevision = -1;
|
|
8256
|
+
this.titleEpoch++;
|
|
8257
|
+
this.nativeTitleEpoch = typeof event.titleEpoch === "string" && event.titleEpoch ? event.titleEpoch : undefined;
|
|
8258
|
+
this.observeTitle(event.sessionName, event.titleRevision, event.titleEpoch);
|
|
8147
8259
|
this.rememberTerminalOwner();
|
|
8148
8260
|
this.startOwnerMonitor();
|
|
8149
8261
|
if (this.opts.mode === "tui") {
|
|
@@ -8151,7 +8263,13 @@ var init_pi = __esm(() => {
|
|
|
8151
8263
|
this.setState("idle");
|
|
8152
8264
|
}
|
|
8153
8265
|
}
|
|
8266
|
+
if ((event.type === "session_title" || event.type === "agent_start" && Object.hasOwn(event, "sessionName")) && event.sessionId === this.sessionId && this.verifiedOwner())
|
|
8267
|
+
this.observeTitle(event.sessionName, event.titleRevision, event.titleEpoch);
|
|
8154
8268
|
if (event.type === "session_shutdown") {
|
|
8269
|
+
this.observedTitle = undefined;
|
|
8270
|
+
this.titleRevision = -1;
|
|
8271
|
+
this.nativeTitleEpoch = undefined;
|
|
8272
|
+
this.onDoing?.("reset");
|
|
8155
8273
|
const exit = this.exitMetadata("session_shutdown");
|
|
8156
8274
|
this.shutdownExit ??= exit;
|
|
8157
8275
|
this.stopping = true;
|
|
@@ -8291,9 +8409,19 @@ var init_pi = __esm(() => {
|
|
|
8291
8409
|
}
|
|
8292
8410
|
}
|
|
8293
8411
|
nextTuiCommand(timeout) {
|
|
8294
|
-
|
|
8295
|
-
|
|
8296
|
-
|
|
8412
|
+
let next = this.tuiQueue.shift();
|
|
8413
|
+
while (next) {
|
|
8414
|
+
const pending = this.tuiCommands.get(String(next.id));
|
|
8415
|
+
if (pending && pending.current?.() !== false) {
|
|
8416
|
+
pending.onDispatch?.();
|
|
8417
|
+
return Promise.resolve(next);
|
|
8418
|
+
}
|
|
8419
|
+
if (pending) {
|
|
8420
|
+
this.tuiCommands.delete(String(next.id));
|
|
8421
|
+
pending.reject(new Error("Pi session title admission changed before dispatch"));
|
|
8422
|
+
}
|
|
8423
|
+
next = this.tuiQueue.shift();
|
|
8424
|
+
}
|
|
8297
8425
|
return new Promise((resolvePromise) => {
|
|
8298
8426
|
const entry = { resolve: resolvePromise, timer: setTimeout(() => {
|
|
8299
8427
|
const i = this.tuiWaiters.indexOf(entry);
|
|
@@ -8304,7 +8432,9 @@ var init_pi = __esm(() => {
|
|
|
8304
8432
|
this.tuiWaiters.push(entry);
|
|
8305
8433
|
});
|
|
8306
8434
|
}
|
|
8307
|
-
sendTui(command) {
|
|
8435
|
+
sendTui(command, current, onDispatch) {
|
|
8436
|
+
if (current?.() === false)
|
|
8437
|
+
return Promise.reject(new Error("Pi session title admission changed before dispatch"));
|
|
8308
8438
|
const id = `ahub-${++this.seq}`;
|
|
8309
8439
|
const fenced = this.fencedPrompt && command.type === "prompt";
|
|
8310
8440
|
if (fenced) {
|
|
@@ -8319,7 +8449,7 @@ var init_pi = __esm(() => {
|
|
|
8319
8449
|
this.tuiQueue = this.tuiQueue.filter((queued) => queued.id !== id);
|
|
8320
8450
|
reject(new Error(`Pi TUI ${String(command.type)} acknowledgement timed out`));
|
|
8321
8451
|
}, 30000);
|
|
8322
|
-
this.tuiCommands.set(id, { command: wire, resolve: (value) => {
|
|
8452
|
+
this.tuiCommands.set(id, { command: wire, current, onDispatch, resolve: (value) => {
|
|
8323
8453
|
clearTimeout(timer);
|
|
8324
8454
|
resolvePromise(value);
|
|
8325
8455
|
}, reject: (error) => {
|
|
@@ -8329,6 +8459,7 @@ var init_pi = __esm(() => {
|
|
|
8329
8459
|
const waiter = this.tuiWaiters.shift();
|
|
8330
8460
|
if (waiter) {
|
|
8331
8461
|
clearTimeout(waiter.timer);
|
|
8462
|
+
onDispatch?.();
|
|
8332
8463
|
waiter.resolve(wire);
|
|
8333
8464
|
} else
|
|
8334
8465
|
this.tuiQueue.push(wire);
|
|
@@ -12502,7 +12633,7 @@ var package_default;
|
|
|
12502
12633
|
var init_package = __esm(() => {
|
|
12503
12634
|
package_default = {
|
|
12504
12635
|
name: "@staix/agent-hub",
|
|
12505
|
-
version: "0.
|
|
12636
|
+
version: "0.30.0",
|
|
12506
12637
|
description: "Native multi-agent hub: Claude Code, Codex, Kimi Code, Pi and local inference as peers in one project",
|
|
12507
12638
|
license: "MIT",
|
|
12508
12639
|
type: "module",
|
|
@@ -12519,7 +12650,7 @@ var init_package = __esm(() => {
|
|
|
12519
12650
|
bun: ">=1.4.2"
|
|
12520
12651
|
},
|
|
12521
12652
|
dependencies: {
|
|
12522
|
-
"@staix/agent-mem": "0.20.
|
|
12653
|
+
"@staix/agent-mem": "0.20.1"
|
|
12523
12654
|
},
|
|
12524
12655
|
devDependencies: {
|
|
12525
12656
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
@@ -12746,7 +12877,7 @@ function profileTaskTools(profile = "dev", stages, criteria = false) {
|
|
|
12746
12877
|
if (customProfile && tool.name === "hub_task_propose")
|
|
12747
12878
|
properties.class = { type: "string", enum: [...new Set([...Object.keys(data.classes), ...PROFILE_CLASS_NAMES.filter((v) => projectProfile("dev").classes[v])])] };
|
|
12748
12879
|
if (customProfile && properties.plan && tool.name !== "hub_task_accept" && tool.name !== "hub_experiment_record")
|
|
12749
|
-
properties.plan = { ...plan, properties: { ...Object.fromEntries(data.plan_keys.map((key) => [key, plan.properties[key] ?? list(key === "sections" ? "document heading anchors, such as report.md#methods" : key === "sources" ? "
|
|
12880
|
+
properties.plan = { ...plan, properties: { ...Object.fromEntries(data.plan_keys.map((key) => [key, plan.properties[key] ?? list(key === "sections" ? "document heading anchors, such as report.md#methods" : key === "sources" ? "registered src:<16 hex> source ids; list available ids with ahub source list" : "short claim ids to verify")])) } };
|
|
12750
12881
|
if (customProfile && tool.name === "hub_task_done" && data.name !== "dev") {
|
|
12751
12882
|
const summaryProp = properties.summary;
|
|
12752
12883
|
properties.summary = {
|
|
@@ -12756,7 +12887,7 @@ function profileTaskTools(profile = "dev", stages, criteria = false) {
|
|
|
12756
12887
|
}
|
|
12757
12888
|
if (customProfile && tool.name === "hub_backlog_split") {
|
|
12758
12889
|
const parts = properties.parts;
|
|
12759
|
-
const selectedPlan = { ...plan, properties: { ...Object.fromEntries(data.plan_keys.map((key) => [key, plan.properties[key] ?? list(key === "sections" ? "document heading anchors, such as report.md#methods" : key === "sources" ? "
|
|
12890
|
+
const selectedPlan = { ...plan, properties: { ...Object.fromEntries(data.plan_keys.map((key) => [key, plan.properties[key] ?? list(key === "sections" ? "document heading anchors, such as report.md#methods" : key === "sources" ? "registered src:<16 hex> source ids; list available ids with ahub source list" : "short claim ids to verify")])) } };
|
|
12760
12891
|
properties.parts = { ...parts, items: { ...parts.items, properties: { ...parts.items.properties, plan: selectedPlan } } };
|
|
12761
12892
|
}
|
|
12762
12893
|
if (criteria && properties.plan && tool.name !== "hub_experiment_record") {
|
|
@@ -12784,7 +12915,7 @@ var IMPLEMENTER_HUB_TOOL_NAMES, IMPLEMENTER_PERMISSION_TOOL, ACTIONABLE_MESSAGE_
|
|
|
12784
12915
|
name,
|
|
12785
12916
|
description,
|
|
12786
12917
|
inputSchema: { type: "object", properties: name === "hub_review" || name === "hub_task_propose" ? { ...properties, request_id: toolWriteRequestId } : properties, required, additionalProperties: false }
|
|
12787
|
-
}), STUDY_TOOLS, HANDOFF_TOOLS, BOUNDED_TASK_TOOLS, CAPABILITY_TOOLS, TASK_TOOLS, PLANNER_REVIEW_TOOLS, TASK_TOOL_NAMES, CONDUCTOR_TOOLS, CONDUCTOR_TOOL_NAMES, READ_ONLY_PROJECT_TOOLS, ROLE_TEXT, DEFAULT_ROLES;
|
|
12918
|
+
}), STUDY_TOOLS, HANDOFF_TOOLS, BOUNDED_TASK_TOOLS, CAPABILITY_TOOLS, TASK_TOOLS, PLANNER_REVIEW_TOOLS, SESSION_TITLE_TOOL, TASK_TOOL_NAMES, CONDUCTOR_TOOLS, CONDUCTOR_TOOL_NAMES, READ_ONLY_PROJECT_TOOLS, ROLE_TEXT, DEFAULT_ROLES;
|
|
12788
12919
|
var init_hub_tools = __esm(() => {
|
|
12789
12920
|
init_test_integrity();
|
|
12790
12921
|
init_sdlc();
|
|
@@ -12847,7 +12978,7 @@ var init_hub_tools = __esm(() => {
|
|
|
12847
12978
|
},
|
|
12848
12979
|
additionalProperties: false
|
|
12849
12980
|
};
|
|
12850
|
-
acceptPlan = { ...plan, properties: { ...plan.properties, sections: list("document heading anchors, such as report.md#methods"), sources: list("
|
|
12981
|
+
acceptPlan = { ...plan, properties: { ...plan.properties, sections: list("document heading anchors, such as report.md#methods"), sources: list("registered src:<16 hex> source ids; list available ids with ahub source list"), claims: list("short claim ids to verify") } };
|
|
12851
12982
|
STUDY_TOOLS = new Set(["hub_study_draft", "hub_study_revise", "hub_study_show", "hub_experiment_show", "hub_experiment_record", "hub_study_handoff"]);
|
|
12852
12983
|
HANDOFF_TOOLS = new Set(["hub_task_handoff", "hub_task_handoffs"]);
|
|
12853
12984
|
BOUNDED_TASK_TOOLS = new Set(["hub_task_spec", "hub_check_start", "hub_check_result", "hub_check_cancel"]);
|
|
@@ -12916,10 +13047,11 @@ var init_hub_tools = __esm(() => {
|
|
|
12916
13047
|
tool2("hub_plan_review", "Record a planner review against the task planning revision and host-read issue edit time. Configured planner or person only; never accepts caller issue timestamps.", { id: id2, revision: { type: "integer", minimum: 0 }, reason: { ...str2, maxLength: 1000 }, ...costPlanProperties }, ["id", "revision", "reason"]),
|
|
12917
13048
|
tool2("hub_sdlc_costs", "Read bounded review and CI cost metrics for linked merged PRs. Planner/person only; metadata only, no board writes.", { repo: str2 }),
|
|
12918
13049
|
tool2("hub_sdlc_metrics", "Read SDLC metrics from board history: time from acceptance to plan, first review and merge, first-pass approval and rework rounds, first-pass CI and same-head reruns, stale-head re-reviews, stalls, and the control band tiers when sdlc.toml sets [bands]. Planner/person only; read-only, no model and no GitHub issue.", { since: str2 }),
|
|
12919
|
-
tool2("hub_backlog_return_untouched", "Return accepted but untouched work to the backlog with its history and planning retained. Refuses execution/branch/commit evidence; planner or person only.", { id: id2, revision: { type: "integer", minimum: 0 }, reason: { ...str2, maxLength: 1000 } }, ["id", "revision", "reason"])
|
|
13050
|
+
tool2("hub_backlog_return_untouched", "Return accepted but untouched work to the backlog with its history and planning retained. Optional fields.reserved changes the reservation atomically before capacity is released; null clears it. Refuses execution/branch/commit evidence; planner or person only, and a person's reservation requires the person.", { id: id2, revision: { type: "integer", minimum: 0 }, reason: { ...str2, maxLength: 1000 }, fields: { type: "object", properties: { reserved: { type: ["string", "null"], minLength: 1, maxLength: 32 } }, additionalProperties: false } }, ["id", "revision", "reason"])
|
|
12920
13051
|
];
|
|
12921
13052
|
TASK_TOOLS.push(...PLANNER_REVIEW_TOOLS);
|
|
12922
|
-
|
|
13053
|
+
SESSION_TITLE_TOOL = tool2("hub_set_title", "Name your own session once the task is clear. Only an unnamed session accepts the first agent name; the person may replace it. Private contexts refuse the write. No task, branch or instance is renamed.", { title: { type: "string", description: "1-80 characters after doingText normalization (one line, controls removed, surrounding whitespace trimmed)" } }, ["title"]);
|
|
13054
|
+
TASK_TOOL_NAMES = new Set([...TASK_TOOLS.map((t) => t.name), SESSION_TITLE_TOOL.name]);
|
|
12923
13055
|
CONDUCTOR_TOOLS = [
|
|
12924
13056
|
tool2("hub_status", "Inspect public team state, holds, quota windows, task counts and pending approval peer/tool/age. Only the configured conductor may use this tool.", {}),
|
|
12925
13057
|
tool2("hub_task_show", "Read a task's public view and history, its done summary and check result included. PII tasks remain stubs. Only the conductor, the configured planner and the task's current owner and reviewer may use this tool.", { id: id2 }, ["id"]),
|
|
@@ -13030,6 +13162,32 @@ var init_local_worker = __esm(() => {
|
|
|
13030
13162
|
deliveryOnly = true;
|
|
13031
13163
|
hubOwned = true;
|
|
13032
13164
|
history = [];
|
|
13165
|
+
title = "";
|
|
13166
|
+
get sessionTitle() {
|
|
13167
|
+
return this.title;
|
|
13168
|
+
}
|
|
13169
|
+
get titleSession() {
|
|
13170
|
+
return this.sessionId;
|
|
13171
|
+
}
|
|
13172
|
+
async setSessionTitle(title, onlyIfUnnamed, current) {
|
|
13173
|
+
if (typeof title !== "string" || !title.trim() || [...title].length > 80 || typeof onlyIfUnnamed !== "boolean")
|
|
13174
|
+
return { ok: false, reason: "invalid", error: "Session title must contain 1-80 normalized characters and use a conditional-write flag; correct the title request and try again." };
|
|
13175
|
+
if (this.state === "offline")
|
|
13176
|
+
return { ok: false, reason: "invalid", error: "Local worker session is offline; start the peer and check ahub status before naming it." };
|
|
13177
|
+
let admitted = false;
|
|
13178
|
+
try {
|
|
13179
|
+
admitted = current?.() !== false;
|
|
13180
|
+
} catch {}
|
|
13181
|
+
if (!admitted)
|
|
13182
|
+
return { ok: false, reason: "invalid", error: "Session title admission changed before the write; check ahub status and retry only in the current public session." };
|
|
13183
|
+
if (onlyIfUnnamed && this.title)
|
|
13184
|
+
return { ok: false, reason: "already_named", error: "This session already has a title; keep it or ask the person to rename it with ahub title." };
|
|
13185
|
+
this.title = title;
|
|
13186
|
+
try {
|
|
13187
|
+
this.onDoing?.("title", title);
|
|
13188
|
+
} catch {}
|
|
13189
|
+
return { ok: true };
|
|
13190
|
+
}
|
|
13033
13191
|
sessionId = `agent-hub-local-${randomUUID7()}`;
|
|
13034
13192
|
routes;
|
|
13035
13193
|
warnedLegacyRoutes = new Set;
|
|
@@ -13060,7 +13218,7 @@ var init_local_worker = __esm(() => {
|
|
|
13060
13218
|
throw new Error("route belongs to an ended turn");
|
|
13061
13219
|
if (signal.aborted)
|
|
13062
13220
|
throw new Error("turn cancelled before model request");
|
|
13063
|
-
const tools = judge ? undefined : [...this.toolSchemas(), ...this.opts.taskTool ? [...profileTaskTools(this.opts.profile, this.opts.sdlcStages, this.opts.sdlcCriteria).filter((tool) => tool.name !== "hub_delivery_retry"), ...CONDUCTOR_TOOLS].map(asFunction) : []];
|
|
13221
|
+
const tools = judge ? undefined : [...this.toolSchemas(), ...this.opts.taskTool ? [...profileTaskTools(this.opts.profile, this.opts.sdlcStages, this.opts.sdlcCriteria).filter((tool) => tool.name !== "hub_delivery_retry"), ...CONDUCTOR_TOOLS].concat(SESSION_TITLE_TOOL).map(asFunction) : []];
|
|
13064
13222
|
const result = await this.opts.omni.chat({ model, messages, ...tools ? { tools } : {}, ...judge ? { max_tokens: maxTokens ?? 2048 } : {} }, { signal, ...pii ? { onCampusOnly: true } : {} });
|
|
13065
13223
|
this.recordUsage(result, model, task);
|
|
13066
13224
|
if (generation !== this.turn)
|
|
@@ -13113,7 +13271,8 @@ var init_local_worker = __esm(() => {
|
|
|
13113
13271
|
this.setState("busy");
|
|
13114
13272
|
if (deliveryId)
|
|
13115
13273
|
this.delivery({ id: deliveryId, state: "accepted" });
|
|
13116
|
-
const msgs = [{ role: "user", content: renderDigest(envs, true)
|
|
13274
|
+
const msgs = [{ role: "user", content: [envs.some((env) => env.private || remoteEnvelope(env)) ? undefined : this.opts.titlePreface?.(), renderDigest(envs, true)].filter(Boolean).join(`
|
|
13275
|
+
`) }];
|
|
13117
13276
|
const progress = { sideEffects: 0, last: "" };
|
|
13118
13277
|
const requestedPolicy = this.opts.turnPolicy?.(envs);
|
|
13119
13278
|
const policy = envs.some((env) => env.private) ? { ...requestedPolicy, pii: true } : requestedPolicy;
|
|
@@ -13250,7 +13409,7 @@ var init_local_worker = __esm(() => {
|
|
|
13250
13409
|
this.abort?.abort();
|
|
13251
13410
|
throw new ApprovalWaitStop(this.approvalStopReason);
|
|
13252
13411
|
}
|
|
13253
|
-
if (!remoteTurn)
|
|
13412
|
+
if (!remoteTurn && name !== "hub_set_title")
|
|
13254
13413
|
this.observeTool(call, output, policy, `${this.sessionId}.${turn}.${step}`);
|
|
13255
13414
|
if (name !== "hub_send" && name !== "doc_read" && name !== "web_fetch" && !TASK_TOOL_NAMES.has(name) && !CONDUCTOR_TOOL_NAMES.has(name))
|
|
13256
13415
|
capture?.observe({ tool: call.function.name, args: call.function.arguments, output, id: call.id, sourceId: `${this.sessionId}.${turn}.${step}.${captureInvocation++}`, paths: touchedPaths(call.function.name, safeParse(call.function.arguments)) });
|
|
@@ -13385,7 +13544,7 @@ var init_local_worker = __esm(() => {
|
|
|
13385
13544
|
}
|
|
13386
13545
|
const fixedModel = policy?.fixedModel ?? this.opts.fixedModel;
|
|
13387
13546
|
const task = policy?.task === undefined ? undefined : Number(policy.task);
|
|
13388
|
-
const tools = [...this.toolSchemas(), ...this.opts.taskTool ? [...profileTaskTools(this.opts.profile, this.opts.sdlcStages, this.opts.sdlcCriteria).filter((tool) => tool.name !== "hub_delivery_retry"), ...CONDUCTOR_TOOLS].map(asFunction) : []];
|
|
13547
|
+
const tools = [...this.toolSchemas(), ...this.opts.taskTool ? [...profileTaskTools(this.opts.profile, this.opts.sdlcStages, this.opts.sdlcCriteria).filter((tool) => tool.name !== "hub_delivery_retry"), ...CONDUCTOR_TOOLS].concat(SESSION_TITLE_TOOL).map(asFunction) : []];
|
|
13389
13548
|
const signal = this.abort.signal;
|
|
13390
13549
|
const messages = [{ role: "system", content: system(this.opts.cwd, this.opts.preamble) }, ...this.history, ...turnMsgs];
|
|
13391
13550
|
const routed = await this.hubCall(route, messages, policy, signal);
|
|
@@ -15912,6 +16071,8 @@ var init_builtin_metadata = __esm(() => {
|
|
|
15912
16071
|
projection: { roleRank: 4, startRank: 0, helpRank: 6, plannerEligible: false },
|
|
15913
16072
|
api: 1,
|
|
15914
16073
|
kind: "pi",
|
|
16074
|
+
title: "hub",
|
|
16075
|
+
titlePersistence: "native",
|
|
15915
16076
|
enabledSwitchStartOnly: true,
|
|
15916
16077
|
displayName: "Pi",
|
|
15917
16078
|
family: "pi",
|
|
@@ -15936,6 +16097,7 @@ var init_builtin_metadata = __esm(() => {
|
|
|
15936
16097
|
projection: { roleRank: 3, helpRank: 7, plannerEligible: false },
|
|
15937
16098
|
api: 1,
|
|
15938
16099
|
kind: "local",
|
|
16100
|
+
title: "hub",
|
|
15939
16101
|
displayName: "Local worker",
|
|
15940
16102
|
family: "local",
|
|
15941
16103
|
transport: "hub-native",
|
|
@@ -16543,6 +16705,8 @@ function assertNativeCapabilityScope(definition) {
|
|
|
16543
16705
|
return;
|
|
16544
16706
|
const declared = (block, key) => block !== undefined && (key in block);
|
|
16545
16707
|
const restricted = [
|
|
16708
|
+
["title", definition.title !== undefined],
|
|
16709
|
+
["titlePersistence", definition.titlePersistence !== undefined],
|
|
16546
16710
|
["start.acpCommand", declared(definition.start, "acpCommand")],
|
|
16547
16711
|
["attach.admission", declared(definition.attach, "admission")],
|
|
16548
16712
|
["budget.resetSoftTokensOnResume", declared(definition.budget, "resetSoftTokensOnResume")],
|
package/src/adapters/compat.ts
CHANGED
|
@@ -29,11 +29,11 @@ export interface CompatPolicy {
|
|
|
29
29
|
|
|
30
30
|
const plain = (text: string) => text.length <= 64 && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(text) ? text : undefined;
|
|
31
31
|
export const COMPAT = {
|
|
32
|
-
//
|
|
33
|
-
pi: { label: "Pi", verified: ["1.4.0"], gate: "contract", probe: ["pi", "--version"], config: "pi.cmd",
|
|
32
|
+
// Receipts: docs/verification/2026-10-26-pi-140-compat.md (1.4.0); docs/verification/2026-11-03-pi-150-compat.md (1.5.0)
|
|
33
|
+
pi: { label: "Pi", verified: ["1.4.0", "1.5.0"], gate: "contract", probe: ["pi", "--version"], config: "pi.cmd",
|
|
34
34
|
parse: out => plain(out.trim()),
|
|
35
|
-
trustNote: "The model-free extension load, RPC session identity and managed tool receipt path were checked on 1.4.0. Permissions remain host-owned; account-backed inference and another version's extension contract are unverified.",
|
|
36
|
-
receipt: "docs/verification/2026-
|
|
35
|
+
trustNote: "The model-free extension load, RPC session identity and managed tool receipt path were checked on 1.4.0 and 1.5.0. Permissions remain host-owned; account-backed inference and another version's extension contract are unverified.",
|
|
36
|
+
receipt: "docs/verification/2026-11-03-pi-150-compat.md" },
|
|
37
37
|
// Transport receipts: docs/verification/2026-10-23-headless-claude-permissions.md; 2.1.310: docs/verification/2026-10-26-claude-implementer-2.1.310.md; 2.1.314 (transport and wrapper): docs/verification/2026-10-31-claude-implementer-2.1.314.md; 2.1.316: docs/verification/2026-10-31-claude-implementer-2.1.316.md; host-wrapper receipt below repeats with them.
|
|
38
38
|
"claude-implementer": { label: "Claude headless implementer", verified: ["2.1.309", "2.1.310", "2.1.314", "2.1.316"], gate: "contract", probe: ["claude", "--version"],
|
|
39
39
|
parse: out => plain(out.trim().replace(/ \(Claude Code\)$/, "")),
|
|
@@ -53,14 +53,14 @@ export const COMPAT = {
|
|
|
53
53
|
parse: out => { try { const v = JSON.parse(out).semver; return typeof v === "string" ? plain(v) : undefined; } catch { return undefined; } },
|
|
54
54
|
trustNote: "The ACP, serve and MCP contract was verified on 0.98.1 and 0.99.1 and steering on 0.99.0 and 0.99.1; an untested version may change them.",
|
|
55
55
|
receipt: "docs/verification/2026-10-26-jcode-0.99.1.md" },
|
|
56
|
-
agy: { label: "agy", verified: ["1.4.2", "1.4.7"], line: { min: "1.4.0", below: "1.5.0" }, gate: "contract", probe: ["agy", "--version"],
|
|
56
|
+
agy: { label: "agy", verified: ["1.4.2", "1.4.7", "1.4.12"], line: { min: "1.4.0", below: "1.5.0" }, gate: "contract", probe: ["agy", "--version"],
|
|
57
57
|
parse: out => plain(out.trim()),
|
|
58
|
-
trustNote: "Only 1.4.2 and 1.4.
|
|
59
|
-
receipt: "docs/verification/2026-
|
|
60
|
-
qwen: { label: "Qwen Code", verified: ["0.25.0", "0.26.0", "0.26.1", "0.26.4"], gate: "contract", probe: ["qwen", "--version"], config: "qwen_cmd",
|
|
58
|
+
trustNote: "Only 1.4.2, 1.4.7 and 1.4.12 are T0-verified; the plugin MCP and hook contract of another version is unverified.",
|
|
59
|
+
receipt: "docs/verification/2026-11-03-agy-1.4.12-launcher-nonce.md" },
|
|
60
|
+
qwen: { label: "Qwen Code", verified: ["0.25.0", "0.26.0", "0.26.1", "0.26.4", "0.26.5"], gate: "contract", probe: ["qwen", "--version"], config: "qwen_cmd",
|
|
61
61
|
parse: out => plain(out.trim()),
|
|
62
|
-
trustNote: "Native ACP identity, mode ids and permission callbacks were re-checked without inference on 0.26.4; real model turns remain unverified. Another version may change those contracts.",
|
|
63
|
-
receipt: "docs/verification/2026-
|
|
62
|
+
trustNote: "Native ACP identity, mode ids and permission callbacks were re-checked without inference on 0.26.4 and 0.26.5; real model turns remain unverified. Another version may change those contracts.",
|
|
63
|
+
receipt: "docs/verification/2026-11-03-qwen-0265-acp.md" },
|
|
64
64
|
// Receipt: docs/verification/2026-10-25-qwen-dual-output-t0.md. The dual-output contract (input file, event file, can_use_tool control requests) is its own entry:
|
|
65
65
|
// the ACP receipts of the headless `qwen` entry say nothing about it, so that entry is not widened and this one carries no `line`.
|
|
66
66
|
"qwen-dual": { label: "Qwen Code dual-output TUI", verified: ["0.26.1"], gate: "contract", probe: ["qwen", "--version"], config: "qwen_cmd",
|
|
@@ -119,7 +119,7 @@ export const NATIVE_BUILTIN_DEFINITIONS_WITHOUT_QWEN: readonly AdapterDefinition
|
|
|
119
119
|
},
|
|
120
120
|
{
|
|
121
121
|
projection: { roleRank: 4, startRank: 0, helpRank: 6, plannerEligible: false },
|
|
122
|
-
api: 1, kind: "pi", enabledSwitchStartOnly: true, displayName: "Pi", family: "pi", transport: "hub-native",
|
|
122
|
+
api: 1, kind: "pi", title: "hub", titlePersistence: "native", enabledSwitchStartOnly: true, displayName: "Pi", family: "pi", transport: "hub-native",
|
|
123
123
|
roles: implementerRoles, defaultOnRoster: false, trust: { onPrem: false, hubSandboxed: true },
|
|
124
124
|
start: { create: createPi, exclusiveStartRefusal: "Pi start is in progress; inspect status before retrying", refreshTools: true, explicitHeadlessMode: true, daemonPort: "pi-rpc", forms: dualForms, defaultForm: "tui", conductorStartable: true, upStart: { allowed: true }, autoStart: "pi.auto_start", conductorStartArgs: piConductorStartArgs, hubSwitches: [], ownedFlags: [] },
|
|
125
125
|
identity: { mcp: "env" }, instances: { mode: "headless" },
|
|
@@ -132,7 +132,7 @@ export const NATIVE_BUILTIN_DEFINITIONS_WITHOUT_QWEN: readonly AdapterDefinition
|
|
|
132
132
|
},
|
|
133
133
|
{
|
|
134
134
|
projection: { roleRank: 3, helpRank: 7, plannerEligible: false },
|
|
135
|
-
api: 1, kind: "local", displayName: "Local worker", family: "local", transport: "hub-native",
|
|
135
|
+
api: 1, kind: "local", title: "hub", displayName: "Local worker", family: "local", transport: "hub-native",
|
|
136
136
|
roles: implementerRoles, defaultOnRoster: true, trust: { onPrem: true, hubSandboxed: true },
|
|
137
137
|
start: { create: createLocal, reconfigureWhileAttached: true, refreshTools: true, daemonPort: "local-worker", forms: headlessForms, defaultForm: "headless", conductorStartable: true, upStart: { allowed: true }, hubSwitches: [], ownedFlags: [] },
|
|
138
138
|
identity: { mcp: "env" }, instances: { mode: "headless" },
|
|
@@ -5,8 +5,8 @@ import type { StaySwitchPolicy } from "../models/route/stage.ts";
|
|
|
5
5
|
import type { ToolObservation } from "../models/route/signals.ts";
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
7
|
import { MAX_HOP, remoteEnvelope, renderDigest, replyAudience, replyParent, STANDING_INSTRUCTION, USER, type Envelope, type EnvelopeOpts, type PeerId } from "../hub/envelope.ts";
|
|
8
|
-
import { CONDUCTOR_TOOL_NAMES, CONDUCTOR_TOOLS, TASK_TOOL_NAMES, TASK_TOOLS, profileTaskTools } from "../hub/hub-tools.ts";
|
|
9
|
-
import { BasePeer } from "../hub/peers.ts";
|
|
8
|
+
import { CONDUCTOR_TOOL_NAMES, CONDUCTOR_TOOLS, TASK_TOOL_NAMES, TASK_TOOLS, profileTaskTools, SESSION_TITLE_TOOL } from "../hub/hub-tools.ts";
|
|
9
|
+
import { BasePeer, type SessionTitleResult } from "../hub/peers.ts";
|
|
10
10
|
import type { PublishResult } from "../hub/bus.ts";
|
|
11
11
|
import { profile, proxyEnv, type SandboxNetwork } from "../local/sandbox.ts";
|
|
12
12
|
import type { DocReadPort } from "../local/doc-read.ts";
|
|
@@ -22,6 +22,8 @@ export interface LocalOptions {
|
|
|
22
22
|
profile?: import("../hub/profiles.ts").ProfileSelection;
|
|
23
23
|
sdlcStages?: readonly string[];
|
|
24
24
|
sdlcCriteria?: boolean;
|
|
25
|
+
/** Host-owned conditional instruction, sampled once per delivered turn. */
|
|
26
|
+
titlePreface?: () => string | undefined;
|
|
25
27
|
cwd: string;
|
|
26
28
|
omni: OmniRoute;
|
|
27
29
|
/** In-process hub route id; legacy routes use the fixed model. */
|
|
@@ -82,6 +84,20 @@ export class LocalPeer extends BasePeer {
|
|
|
82
84
|
readonly deliveryOnly: boolean = true;
|
|
83
85
|
readonly hubOwned: boolean = true;
|
|
84
86
|
private readonly history: ChatMessage[] = [];
|
|
87
|
+
private title = "";
|
|
88
|
+
get sessionTitle(): string { return this.title; }
|
|
89
|
+
get titleSession(): string { return this.sessionId; }
|
|
90
|
+
async setSessionTitle(title: string, onlyIfUnnamed: boolean, current?: () => boolean): Promise<SessionTitleResult> {
|
|
91
|
+
if (typeof title !== "string" || !title.trim() || [...title].length > 80 || typeof onlyIfUnnamed !== "boolean") return { ok: false, reason: "invalid", error: "Session title must contain 1-80 normalized characters and use a conditional-write flag; correct the title request and try again." };
|
|
92
|
+
if (this.state === "offline") return { ok: false, reason: "invalid", error: "Local worker session is offline; start the peer and check ahub status before naming it." };
|
|
93
|
+
let admitted = false;
|
|
94
|
+
try { admitted = current?.() !== false; } catch { /* host admission failure is a refusal */ }
|
|
95
|
+
if (!admitted) return { ok: false, reason: "invalid", error: "Session title admission changed before the write; check ahub status and retry only in the current public session." };
|
|
96
|
+
if (onlyIfUnnamed && this.title) return { ok: false, reason: "already_named", error: "This session already has a title; keep it or ask the person to rename it with ahub title." };
|
|
97
|
+
this.title = title;
|
|
98
|
+
try { this.onDoing?.("title", title); } catch { /* observation failure cannot undo the saved local title */ }
|
|
99
|
+
return { ok: true };
|
|
100
|
+
}
|
|
85
101
|
private readonly sessionId = `agent-hub-local-${randomUUID()}`;
|
|
86
102
|
private readonly routes: HubRouteRuntime;
|
|
87
103
|
private readonly warnedLegacyRoutes = new Set<string>();
|
|
@@ -113,7 +129,7 @@ export class LocalPeer extends BasePeer {
|
|
|
113
129
|
await this.requireBudget(envs, "model_calls");
|
|
114
130
|
if (generation !== this.turn) throw new Error("route belongs to an ended turn");
|
|
115
131
|
if (signal.aborted) throw new Error("turn cancelled before model request");
|
|
116
|
-
const tools = judge ? undefined : [...this.toolSchemas(), ...(this.opts.taskTool ? [...profileTaskTools(this.opts.profile, this.opts.sdlcStages, this.opts.sdlcCriteria).filter(tool => tool.name !== "hub_delivery_retry"), ...CONDUCTOR_TOOLS].map(asFunction) : [])];
|
|
132
|
+
const tools = judge ? undefined : [...this.toolSchemas(), ...(this.opts.taskTool ? [...profileTaskTools(this.opts.profile, this.opts.sdlcStages, this.opts.sdlcCriteria).filter(tool => tool.name !== "hub_delivery_retry"), ...CONDUCTOR_TOOLS].concat(SESSION_TITLE_TOOL).map(asFunction) : [])];
|
|
117
133
|
const result = await this.opts.omni.chat({ model, messages, ...(tools ? { tools } : {}), ...(judge ? { max_tokens: maxTokens ?? 2048 } : {}) }, { signal, ...(pii ? { onCampusOnly: true } : {}) });
|
|
118
134
|
this.recordUsage(result, model, task);
|
|
119
135
|
if (generation !== this.turn) throw new Error("route belongs to an ended turn");
|
|
@@ -165,7 +181,7 @@ export class LocalPeer extends BasePeer {
|
|
|
165
181
|
if (deliveryId) this.delivery({ id: deliveryId, state: "accepted" });
|
|
166
182
|
// The turn works on its own message list and joins the history only as a whole, so a failed or aborted turn can
|
|
167
183
|
// never leave a tool call without its result (strict servers reject that history forever after).
|
|
168
|
-
const msgs: ChatMessage[] = [{ role: "user", content: renderDigest(envs, true) }];
|
|
184
|
+
const msgs: ChatMessage[] = [{ role: "user", content: [envs.some(env => env.private || remoteEnvelope(env)) ? undefined : this.opts.titlePreface?.(), renderDigest(envs, true)].filter(Boolean).join("\n") }];
|
|
169
185
|
const progress = { sideEffects: 0, last: "" };
|
|
170
186
|
const requestedPolicy = this.opts.turnPolicy?.(envs);
|
|
171
187
|
const policy = envs.some(env => env.private) ? { ...requestedPolicy, pii: true } : requestedPolicy;
|
|
@@ -288,7 +304,7 @@ export class LocalPeer extends BasePeer {
|
|
|
288
304
|
const result: ChatMessage = { role: "tool", tool_call_id: call.id, content: output, is_error: toolResultFailed(name, output) };
|
|
289
305
|
msgs.push(result); results.push(result);
|
|
290
306
|
if (this.approvalStopReason) { this.abort?.abort(); throw new ApprovalWaitStop(this.approvalStopReason); }
|
|
291
|
-
if (!remoteTurn) this.observeTool(call, output, policy, `${this.sessionId}.${turn}.${step}`);
|
|
307
|
+
if (!remoteTurn && name !== "hub_set_title") this.observeTool(call, output, policy, `${this.sessionId}.${turn}.${step}`);
|
|
292
308
|
// Hub tool arguments/results are not capture evidence; Tasks owns its screened memory writes.
|
|
293
309
|
// doc_read text is converted document content (a converter can surface hidden text), never memory evidence: not captured, whatever the turn.
|
|
294
310
|
// web_fetch text is a third party's page and its URL is the model's own request: neither is memory evidence either (#515b).
|
|
@@ -401,7 +417,7 @@ export class LocalPeer extends BasePeer {
|
|
|
401
417
|
}
|
|
402
418
|
const fixedModel = policy?.fixedModel ?? this.opts.fixedModel;
|
|
403
419
|
const task = policy?.task === undefined ? undefined : Number(policy.task);
|
|
404
|
-
const tools = [...this.toolSchemas(), ...(this.opts.taskTool ? [...profileTaskTools(this.opts.profile, this.opts.sdlcStages, this.opts.sdlcCriteria).filter(tool => tool.name !== "hub_delivery_retry"), ...CONDUCTOR_TOOLS].map(asFunction) : [])];
|
|
420
|
+
const tools = [...this.toolSchemas(), ...(this.opts.taskTool ? [...profileTaskTools(this.opts.profile, this.opts.sdlcStages, this.opts.sdlcCriteria).filter(tool => tool.name !== "hub_delivery_retry"), ...CONDUCTOR_TOOLS].concat(SESSION_TITLE_TOOL).map(asFunction) : [])];
|
|
405
421
|
const signal = this.abort!.signal;
|
|
406
422
|
const messages: ChatMessage[] = [{ role: "system", content: system(this.opts.cwd, this.opts.preamble) }, ...this.history, ...turnMsgs];
|
|
407
423
|
const routed = await this.hubCall(route, messages, policy, signal);
|