@staix/agent-hub 0.28.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.
Files changed (90) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +5 -3
  3. package/docs/agent-notes/README.md +12 -0
  4. package/docs/agent-notes/adapters.md +2 -1
  5. package/docs/agent-notes/bus.md +1 -0
  6. package/docs/agent-notes/daemon.md +4 -1
  7. package/docs/agent-notes/local-worker.md +3 -1
  8. package/docs/agent-notes/tasks.md +7 -3
  9. package/docs/agent-notes/tests.md +3 -0
  10. package/docs/design/adapters.md +56 -0
  11. package/docs/design/agent-hub-architecture.md +12 -0
  12. package/docs/design/bus.md +8 -0
  13. package/docs/design/task-board.md +19 -1
  14. package/docs/dogfood.md +13 -7
  15. package/docs/operations.md +177 -20
  16. package/docs/quickstart.md +18 -3
  17. package/docs/sdlc.md +2 -2
  18. package/docs/security.md +45 -4
  19. package/docs/smoke.md +13 -2
  20. package/docs/specs/2026-10-12-office-view-design.md +86 -3
  21. package/docs/verification/2026-11-03-0.29.0.md +59 -0
  22. package/docs/verification/2026-11-03-0.30.0.md +54 -0
  23. package/docs/verification/2026-11-03-agy-1.4.12-launcher-nonce.md +42 -0
  24. package/docs/verification/2026-11-03-agy-1.4.12.md +90 -0
  25. package/docs/verification/2026-11-03-agy-tui-close.md +49 -0
  26. package/docs/verification/2026-11-03-control-write-stall-windows.json +241 -0
  27. package/docs/verification/2026-11-03-control-write-stall-windows.md +20 -0
  28. package/docs/verification/2026-11-03-grok-1.0.62-t0.md +150 -0
  29. package/docs/verification/2026-11-03-live-board-latency.json +556 -0
  30. package/docs/verification/2026-11-03-live-board-latency.md +64 -0
  31. package/docs/verification/2026-11-03-pi-150-compat.md +40 -0
  32. package/docs/verification/2026-11-03-pi-title-api.md +26 -0
  33. package/docs/verification/2026-11-03-qwen-0265-acp.md +55 -0
  34. package/docs/verification/2026-11-03-qwen-0265-dual-output.md +40 -0
  35. package/docs/verification/dogfood/2026-11-03.md +3095 -0
  36. package/docs/verification/pi-150-model-free.ts +57 -0
  37. package/docs/verification/pi-150-native.mjs +21 -0
  38. package/docs/verification/pi-150-title-api.mjs +31 -0
  39. package/docs/verified.json +14 -14
  40. package/package.json +2 -2
  41. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  42. package/plugins/agent-hub/server.js +715 -45
  43. package/src/adapters/compat.ts +10 -10
  44. package/src/adapters/definitions/builtin-metadata.ts +2 -2
  45. package/src/adapters/local-worker.ts +22 -6
  46. package/src/adapters/pi.ts +92 -10
  47. package/src/adapters/registry-types.ts +4 -0
  48. package/src/adapters/registry.ts +2 -0
  49. package/src/cli/autonomy.ts +66 -1
  50. package/src/cli/console-state.ts +6 -2
  51. package/src/cli/help.ts +3 -1
  52. package/src/cli/identity.ts +8 -1
  53. package/src/cli/main.ts +39 -0
  54. package/src/cli/output.ts +5 -0
  55. package/src/cli/recovery-package.ts +9 -0
  56. package/src/cli/worktree-tidy.ts +158 -0
  57. package/src/cli/worktrees-output.ts +43 -0
  58. package/src/hub/board.ts +31 -5
  59. package/src/hub/bus.ts +4 -4
  60. package/src/hub/conductor.ts +4 -3
  61. package/src/hub/config-apply.ts +2 -1
  62. package/src/hub/control-client.ts +16 -1
  63. package/src/hub/control-write-trace.ts +409 -0
  64. package/src/hub/daemon.ts +408 -66
  65. package/src/hub/dogfood-read.ts +62 -9
  66. package/src/hub/hub-tools.ts +12 -6
  67. package/src/hub/manager.ts +9 -1
  68. package/src/hub/peer-doing.ts +33 -3
  69. package/src/hub/peers.ts +7 -0
  70. package/src/hub/routing.ts +9 -2
  71. package/src/hub/runtime-capture-prune.ts +4 -1
  72. package/src/hub/sdlc-release-authorization.ts +13 -4
  73. package/src/hub/sdlc-release.ts +23 -10
  74. package/src/hub/sdlc.ts +15 -2
  75. package/src/hub/settings.ts +3 -0
  76. package/src/hub/task-worktree.ts +331 -0
  77. package/src/hub/tasks.ts +192 -25
  78. package/src/hub/ui.ts +2 -0
  79. package/src/hub/worktree-prs.ts +100 -0
  80. package/src/hub/worktree-shelf.ts +246 -0
  81. package/src/local/deny.ts +49 -0
  82. package/src/local/tools.ts +4 -2
  83. package/src/memory/brief.ts +5 -0
  84. package/src/memory/capture.ts +1 -1
  85. package/src/memory/client.ts +5 -2
  86. package/src/pi/extension.ts +43 -3
  87. package/src/ui/index.html +133 -10
  88. package/src/ui/office-model.ts +64 -8
  89. package/src/ui/office-space.ts +1 -0
  90. 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-10-26-pi-140-compat.md"
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.7 are T0-verified; the plugin MCP and hook contract of another version is unverified.",
370
- receipt: "docs/verification/2026-10-26-agy-1.4.7.md"
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-10-31-qwen-0264-acp.md"
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",
@@ -6258,6 +6258,28 @@ var init_path_policy = __esm(() => {
6258
6258
 
6259
6259
  // src/local/deny.ts
6260
6260
  import { basename as basename4, dirname as dirname8 } from "path";
6261
+ function gitRefRules(gitDirs, subDirs = []) {
6262
+ const git = hfsFoldedName(".git");
6263
+ const re = (s) => `(regex ${sbplString(s)})`;
6264
+ const name = (n) => hfsFoldedName(n);
6265
+ const plainSub = `(require-not ${re(`^.*/${git}/worktrees/[^/]+/modules(/|$)`)})`;
6266
+ const plain = [
6267
+ re(`^.*/${git}/${name("HEAD")}(\\.lock)?$`),
6268
+ re(`^.*/${git}/${name("refs")}(/|$)`),
6269
+ re(`^.*/${git}/${name("packed")}`),
6270
+ `(require-all ${re(`^.*/${git}/${name("work")}`)} ${re(`^.*${name("trees")}(/|$)`)} ${plainSub})`
6271
+ ];
6272
+ const external = [...gitDirs, ...subDirs].flatMap((d) => {
6273
+ const own = sbplString(d), direct = re(`^${sbplEscape(d)}/[^/]+$`);
6274
+ return [
6275
+ `(require-all ${direct} ${re(`^.*/${name("HEAD")}(\\.lock)?$`)})`,
6276
+ `(require-all ${direct} ${re(`^.*/${name("packed")}`)})`,
6277
+ `(require-all (subpath ${own}) ${re(refUnder(d, "refs"))})`,
6278
+ `(require-all (subpath ${own}) ${re(refUnder(d, "worktrees"))} (require-not ${re(`^${sbplEscape(d)}/worktrees/[^/]+/modules(/|$)`)}))`
6279
+ ];
6280
+ });
6281
+ return [...plain, ...external];
6282
+ }
6261
6283
  function hubWriteRegexes(root, gitDirs = [], moduleGit = [], worktreesRoot) {
6262
6284
  const git = hfsFoldedName(".git");
6263
6285
  const re = (s) => `(regex ${sbplString(s)})`;
@@ -6311,6 +6333,7 @@ function hubWriteRegexes(root, gitDirs = [], moduleGit = [], worktreesRoot) {
6311
6333
  `(require-all ${re(`^.*/${git}/[^/]+$`)} ${CONFIG_WORKTREE_NAME.map(re).join(" ")})`,
6312
6334
  re(`^.*/${git}/${hfsFoldedName("hooks")}(/|$)`),
6313
6335
  `(require-all ${re(`^.*/${git}/.*$`)} ${re(`^.*/${hfsFoldedName("commondir")}$`)})`,
6336
+ ...gitRefRules(gitDirs, allDirs),
6314
6337
  ...modules,
6315
6338
  ...siblings,
6316
6339
  ...external
@@ -6339,6 +6362,9 @@ function denyRegexes(root, extra = [], withNames = true) {
6339
6362
  var sbplEscape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), sbplString = (s) => `"${s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`, HFS_IGNORABLE_POINTS, HFS_IGNORABLE, hfsFoldedName = (name) => `${HFS_IGNORABLE}*${[...name].map((c) => sbplEscape(c)).join(`${HFS_IGNORABLE}*`)}${HFS_IGNORABLE}*`, CONFIG_WORKTREE_NAME, MAX_ANCHOR_DEPTH = 80, hooksUnder = (parent) => {
6340
6363
  const depth = parent.split("/").filter(Boolean).length;
6341
6364
  return depth > MAX_ANCHOR_DEPTH ? `^.*/${hfsFoldedName("hooks")}(/|$)` : `^${"/[^/]+".repeat(depth)}/${hfsFoldedName("hooks")}(/|$)`;
6365
+ }, refDepth = (d) => d.split("/").filter(Boolean).length, refUnder = (d, name) => {
6366
+ const depth = refDepth(d), cost = Buffer.byteLength(hfsFoldedName(name)) + depth * 6 + 8;
6367
+ return cost > 1000 ? `^.*/${hfsFoldedName(name)}(/|$)` : `^${"/[^/]+".repeat(depth)}/${hfsFoldedName(name)}(/|$)`;
6342
6368
  };
6343
6369
  var init_deny = __esm(() => {
6344
6370
  init_platform();
@@ -7073,7 +7099,9 @@ ${preview(content)}`, "write", ctx.signal, file, undefined, String(a.path), a),
7073
7099
  try {
7074
7100
  ctx.onProcessExit?.(res.code);
7075
7101
  } catch {}
7076
- return `${res.output}
7102
+ const refused = res.code !== 0 && /(?:\.git\/(?:refs|HEAD|packed-refs|worktrees)\b|cannot lock ref|index\.lock)[^\n]*(?:Operation not permitted|not permitted)/i.test(res.output);
7103
+ return `${res.output}${refused ? `
7104
+ note: the sandbox does not let you move a branch, tag or HEAD, or stage inside a linked worktree (#1123). Leave your edits in the working tree and finish with hub_task_done. For implementation work the hub stages, commits and pushes them for you; for work that changes no files, or a private task, there is nothing for it to commit, so do not try to commit it yourself.` : ""}
7077
7105
  (exit ${res.code})`;
7078
7106
  }
7079
7107
  case "doc_read": {
@@ -7126,7 +7154,7 @@ var init_tools = __esm(() => {
7126
7154
  fn("write", "Create or overwrite a file inside the project. Needs the user's approval.", { path: str, content: str }, ["path", "content"]),
7127
7155
  fn("edit", "Replace one exact, unique occurrence of `old` with `new` in a file. Needs the user's approval.", { path: str, old: str, new: str }, ["path", "old", "new"]),
7128
7156
  fn("bash", "Run a shell command in the project directory, sandboxed: no writes outside the project, no credential reads, no network unless the hub enables it. Follows hub permission policy: proven bounded read-only commands may run automatically in ask-when-needed; other commands ask.", { command: str, timeout_s: { type: "integer", minimum: 1, maximum: 600 } }, ["command"]),
7129
- fn("git", "Run git in the project. status, diff, log, show, ls-files, blame, rev-parse run freely; add, commit, restore, checkout, switch, stash, mv, rm, branch, merge, rebase, cherry-pick, revert, tag need approval; everything else (push, fetch, config, remote ...) is refused.", { args: { type: "array", items: str, minItems: 1 } }, ["args"]),
7157
+ fn("git", "Run git in the project. status, diff, log, show, ls-files, blame, rev-parse run freely; add, commit, restore, checkout, switch, stash, mv, rm, branch, merge, rebase, cherry-pick, revert, tag need approval; everything else (push, fetch, config, remote ...) is refused. The sandbox does not let you move a branch, tag or HEAD anywhere (commit, branch, tag, stash, switch, merge, rebase and reset fail, submodules included), nor stage with add inside a linked worktree: edit files, and the hub commits and pushes them when you call hub_task_done.", { args: { type: "array", items: str, minItems: 1 } }, ["args"]),
7130
7158
  fn("hub_send", "Send a message to the other agents now, before your turn ends. Conclusions only. Your final answer is shared anyway; use this for something that cannot wait.", { text: str, to: { type: "array", items: str } }, ["text"])
7131
7159
  ];
7132
7160
  DOC_READ_SCHEMA = fn("doc_read", "Read a PDF, DOCX or HWPX document inside the project as plain text, through the converters the project configured. Returns numbered lines, bounded; read again with offset for more. Read-only.", { path: str, offset: { type: "integer", minimum: 1 }, limit: { type: "integer", minimum: 1 } }, ["path"]);
@@ -7260,6 +7288,101 @@ var init_pi = __esm(() => {
7260
7288
  currentReply;
7261
7289
  sessionId = "";
7262
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
+ }
7263
7386
  executionBudgetTimer;
7264
7387
  executionAbort;
7265
7388
  budgetStops = new Map;
@@ -7378,12 +7501,15 @@ var init_pi = __esm(() => {
7378
7501
  throw new Error("Pi must settle before session capture");
7379
7502
  if (this.sessionFile && existsSync7(this.sessionFile))
7380
7503
  return this.recoveryMetadata();
7504
+ const titleRevisionBefore = this.titleRevision, titleEpochBefore = this.titleEpoch;
7381
7505
  const snapshot = this.opts.mode === "headless" ? (await this.waitRpc("get_state", 15000)).data : await this.sendTui({ type: "get_session_state" });
7382
7506
  if (this.state !== "idle" || this.activeTools || snapshot?.sessionId !== this.sessionId || snapshot?.sessionFile !== this.sessionFile)
7383
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");
7384
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;
7385
7511
  if (!empty || this.activityObserved || this.persistedOnce)
7386
- 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");
7387
7513
  this.emptyResumeVerified = true;
7388
7514
  const captured = this.recoveryMetadata();
7389
7515
  if (typeof captured.sessionId === "string" && !captured.sessionFile)
@@ -7570,6 +7696,10 @@ var init_pi = __esm(() => {
7570
7696
  await this.preflight();
7571
7697
  this.terminalOwnerObservation = undefined;
7572
7698
  this.stopping = false;
7699
+ this.observedTitle = undefined;
7700
+ this.titleRevision = -1;
7701
+ this.nativeTitleEpoch = undefined;
7702
+ this.onDoing?.("reset");
7573
7703
  this.rosterDetached = false;
7574
7704
  this.rosterUnownedRun = false;
7575
7705
  this.requestedStopReason = undefined;
@@ -7670,6 +7800,7 @@ var init_pi = __esm(() => {
7670
7800
  this.fail(new Error(`pi exited with ${signal ? `signal ${signal}` : `code ${code}`}`));
7671
7801
  this.reportExit(exit);
7672
7802
  });
7803
+ const titleRevisionBefore = this.titleRevision, titleEpochBefore = this.titleEpoch;
7673
7804
  const state = await this.waitRpc("get_state", 15000);
7674
7805
  if (state.success !== true || typeof state.data?.sessionId !== "string" || typeof state.data?.sessionFile !== "string" || !state.data.sessionId || !state.data.sessionFile)
7675
7806
  throw new Error("Pi get_state did not prove session identity");
@@ -7690,6 +7821,7 @@ var init_pi = __esm(() => {
7690
7821
  this.persistedOnce = existsSync7(this.sessionFile);
7691
7822
  if (this.opts.sessionId && this.sessionId !== this.opts.sessionId || this.opts.sessionFile && this.sessionFile !== this.opts.sessionFile)
7692
7823
  throw new Error("Pi startup session identity mismatch");
7824
+ this.observeSnapshotTitle(state.data, titleRevisionBefore, titleEpochBefore);
7693
7825
  this.started = true;
7694
7826
  this.setState("idle");
7695
7827
  }
@@ -7779,11 +7911,13 @@ var init_pi = __esm(() => {
7779
7911
  if (this.pendingPromptId)
7780
7912
  throw new Error("Pi prompt admission is still pending; wait before reverifying the retained terminal");
7781
7913
  const epoch = this.rosterEpoch, lifecycle = this.rosterLifecycleRevision, session = this.sessionId, file = this.sessionFile;
7914
+ const titleRevisionBefore = this.titleRevision, titleEpochBefore = this.titleEpoch;
7782
7915
  const snapshot = await this.sendTui({ type: "get_session_state" });
7783
7916
  if (epoch !== this.rosterEpoch || this.stopping || !this.verifiedOwner() || this.activeTools || snapshot?.sessionId !== session || snapshot?.sessionFile !== file || typeof snapshot?.idle !== "boolean")
7784
7917
  throw new Error("Pi roster return could not verify the current native session");
7785
7918
  if (lifecycle !== this.rosterLifecycleRevision)
7786
7919
  throw new Error("Pi native activity changed during roster return; retry with a fresh session snapshot");
7920
+ this.observeSnapshotTitle(snapshot, titleRevisionBefore, titleEpochBefore);
7787
7921
  this.rosterDetached = false;
7788
7922
  this.hubPromptId = undefined;
7789
7923
  this.hubPromptGeneration = undefined;
@@ -7934,12 +8068,14 @@ var init_pi = __esm(() => {
7934
8068
  attempted = true;
7935
8069
  if (deliveryId)
7936
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
+ `);
7937
8073
  if (this.opts.mode === "tui") {
7938
- const pending = this.sendTui({ type: "prompt", message: renderDigest(envs, true) });
8074
+ const pending = this.sendTui({ type: "prompt", message });
7939
8075
  promptId = this.pendingPromptId;
7940
8076
  await pending;
7941
8077
  } else
7942
- await this.sendRpc({ type: "prompt", message: renderDigest(envs, true) });
8078
+ await this.sendRpc({ type: "prompt", message });
7943
8079
  if (promptId !== undefined && promptId !== this.pendingPromptId)
7944
8080
  return;
7945
8081
  if (this.fencedPrompt && this.rosterDetached) {
@@ -8116,6 +8252,10 @@ var init_pi = __esm(() => {
8116
8252
  this.opts.log?.(`[${this.id}] Pi session identity mismatch`);
8117
8253
  return;
8118
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);
8119
8259
  this.rememberTerminalOwner();
8120
8260
  this.startOwnerMonitor();
8121
8261
  if (this.opts.mode === "tui") {
@@ -8123,7 +8263,13 @@ var init_pi = __esm(() => {
8123
8263
  this.setState("idle");
8124
8264
  }
8125
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);
8126
8268
  if (event.type === "session_shutdown") {
8269
+ this.observedTitle = undefined;
8270
+ this.titleRevision = -1;
8271
+ this.nativeTitleEpoch = undefined;
8272
+ this.onDoing?.("reset");
8127
8273
  const exit = this.exitMetadata("session_shutdown");
8128
8274
  this.shutdownExit ??= exit;
8129
8275
  this.stopping = true;
@@ -8263,9 +8409,19 @@ var init_pi = __esm(() => {
8263
8409
  }
8264
8410
  }
8265
8411
  nextTuiCommand(timeout) {
8266
- const next = this.tuiQueue.shift();
8267
- if (next)
8268
- return Promise.resolve(next);
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
+ }
8269
8425
  return new Promise((resolvePromise) => {
8270
8426
  const entry = { resolve: resolvePromise, timer: setTimeout(() => {
8271
8427
  const i = this.tuiWaiters.indexOf(entry);
@@ -8276,7 +8432,9 @@ var init_pi = __esm(() => {
8276
8432
  this.tuiWaiters.push(entry);
8277
8433
  });
8278
8434
  }
8279
- sendTui(command) {
8435
+ sendTui(command, current, onDispatch) {
8436
+ if (current?.() === false)
8437
+ return Promise.reject(new Error("Pi session title admission changed before dispatch"));
8280
8438
  const id = `ahub-${++this.seq}`;
8281
8439
  const fenced = this.fencedPrompt && command.type === "prompt";
8282
8440
  if (fenced) {
@@ -8291,7 +8449,7 @@ var init_pi = __esm(() => {
8291
8449
  this.tuiQueue = this.tuiQueue.filter((queued) => queued.id !== id);
8292
8450
  reject(new Error(`Pi TUI ${String(command.type)} acknowledgement timed out`));
8293
8451
  }, 30000);
8294
- this.tuiCommands.set(id, { command: wire, resolve: (value) => {
8452
+ this.tuiCommands.set(id, { command: wire, current, onDispatch, resolve: (value) => {
8295
8453
  clearTimeout(timer);
8296
8454
  resolvePromise(value);
8297
8455
  }, reject: (error) => {
@@ -8301,6 +8459,7 @@ var init_pi = __esm(() => {
8301
8459
  const waiter = this.tuiWaiters.shift();
8302
8460
  if (waiter) {
8303
8461
  clearTimeout(waiter.timer);
8462
+ onDispatch?.();
8304
8463
  waiter.resolve(wire);
8305
8464
  } else
8306
8465
  this.tuiQueue.push(wire);
@@ -9958,6 +10117,464 @@ var init_test_integrity = __esm(() => {
9958
10117
  // src/hub/plan-conformance.ts
9959
10118
  var init_plan_conformance = () => {};
9960
10119
 
10120
+ // src/hub/control-write-trace.ts
10121
+ import { AsyncLocalStorage } from "async_hooks";
10122
+ function isObject(value) {
10123
+ return !!value && typeof value === "object" && !Array.isArray(value);
10124
+ }
10125
+ function validTaskId(value) {
10126
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
10127
+ }
10128
+ function numericTaskId(value) {
10129
+ return validTaskId(value) ? value : undefined;
10130
+ }
10131
+ function ownValue(value, key) {
10132
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
10133
+ return descriptor && Object.hasOwn(descriptor, "value") ? descriptor.value : undefined;
10134
+ }
10135
+ function controlWriteMetadata(message) {
10136
+ if (!isObject(message))
10137
+ return;
10138
+ try {
10139
+ let kind;
10140
+ let args;
10141
+ const type = ownValue(message, "t"), operation = ownValue(message, "op");
10142
+ if (type === "delivery_complete")
10143
+ kind = "hub_delivery_done";
10144
+ else if (type === "task" && typeof operation === "string") {
10145
+ kind = Object.hasOwn(OPERATIONS, operation) ? OPERATIONS[operation] : undefined;
10146
+ const rawArgs = ownValue(message, "args");
10147
+ args = isObject(rawArgs) ? rawArgs : undefined;
10148
+ }
10149
+ if (!kind)
10150
+ return;
10151
+ const task = numericTaskId(args ? ownValue(args, "id") : undefined);
10152
+ return task === undefined ? { kind } : { kind, task };
10153
+ } catch {
10154
+ return;
10155
+ }
10156
+ }
10157
+ function isPhase(value) {
10158
+ return CONTROL_WRITE_PHASES.includes(value);
10159
+ }
10160
+ function isThenable(value) {
10161
+ try {
10162
+ return !!value && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
10163
+ } catch {
10164
+ return false;
10165
+ }
10166
+ }
10167
+ function controlWriteSpan(phase, fn) {
10168
+ const request = currentTrace.getStore();
10169
+ if (!request || request.settled || request.privacySuppressed || !isPhase(phase))
10170
+ return fn();
10171
+ const span = request.telemetry.beginSpan(request, phase);
10172
+ let result;
10173
+ try {
10174
+ result = fn();
10175
+ } catch (error) {
10176
+ request.telemetry.finishSpan(span);
10177
+ throw error;
10178
+ }
10179
+ if (!isThenable(result)) {
10180
+ request.telemetry.finishSpan(span);
10181
+ return result;
10182
+ }
10183
+ span.async = true;
10184
+ request.telemetry.recordSyncPrefix(span);
10185
+ return Promise.resolve(result).then((value) => {
10186
+ request.telemetry.finishSpan(span);
10187
+ return value;
10188
+ }, (error) => {
10189
+ request.telemetry.finishSpan(span);
10190
+ throw error;
10191
+ });
10192
+ }
10193
+
10194
+ class ControlWriteTelemetry {
10195
+ options;
10196
+ nowFn;
10197
+ maxActiveRequests;
10198
+ maxActiveSpans;
10199
+ maxRecentSpans;
10200
+ rateLimitMs;
10201
+ requests = new Map;
10202
+ spans = new Map;
10203
+ recent = [];
10204
+ emitted = [];
10205
+ lastEmit = new Map;
10206
+ nextRequestId = 1;
10207
+ nextSpanId = 1;
10208
+ timer;
10209
+ nextSampleAt;
10210
+ lastNow = 0;
10211
+ stopped = false;
10212
+ completedRequests = 0;
10213
+ expiredRequests = 0;
10214
+ slowRequests = 0;
10215
+ blockedLoopSamples = 0;
10216
+ droppedActive = 0;
10217
+ droppedSpans = 0;
10218
+ droppedRecent = 0;
10219
+ droppedLogs = 0;
10220
+ droppedMetadata = 0;
10221
+ clockFailures = 0;
10222
+ constructor(options) {
10223
+ this.options = options;
10224
+ this.nowFn = options.now ?? (() => performance.now());
10225
+ this.maxActiveRequests = options.maxActiveRequests ?? MAX_ACTIVE_REQUESTS;
10226
+ this.maxActiveSpans = options.maxActiveSpans ?? MAX_ACTIVE_SPANS;
10227
+ this.maxRecentSpans = options.maxRecentSpans ?? MAX_RECENT_SPANS;
10228
+ this.rateLimitMs = options.rateLimitMs ?? RATE_LIMIT_MS;
10229
+ if (![this.maxActiveRequests, this.maxActiveSpans, this.maxRecentSpans].every((value) => Number.isSafeInteger(value) && value > 0) || !Number.isSafeInteger(this.rateLimitMs) || this.rateLimitMs < 0 || this.rateLimitMs > RATE_LIMIT_MS || this.maxActiveRequests > MAX_ACTIVE_REQUESTS || this.maxActiveSpans > MAX_ACTIVE_SPANS || this.maxRecentSpans > MAX_RECENT_SPANS)
10230
+ throw new Error("invalid control write trace bounds");
10231
+ }
10232
+ run(metadata, privacyCurrent, fn, inputPublic) {
10233
+ if (this.stopped)
10234
+ return fn();
10235
+ const safeMetadata = this.safeMetadata(metadata);
10236
+ if (!safeMetadata || typeof privacyCurrent !== "function") {
10237
+ this.droppedMetadata++;
10238
+ return fn();
10239
+ }
10240
+ const now = this.readNow();
10241
+ this.expireRequests(now);
10242
+ if (this.requests.size >= this.maxActiveRequests) {
10243
+ this.droppedActive++;
10244
+ return fn();
10245
+ }
10246
+ if (!this.privateNow(privacyCurrent))
10247
+ return fn();
10248
+ const inputVisible = !inputPublic || this.privateNow(inputPublic);
10249
+ inputPublic = undefined;
10250
+ if (!inputVisible)
10251
+ return fn();
10252
+ const request = {
10253
+ telemetry: this,
10254
+ metadata: safeMetadata,
10255
+ privacyCurrent,
10256
+ startedAt: now,
10257
+ phases: new Map,
10258
+ phaseOrder: [],
10259
+ settled: false,
10260
+ privacySuppressed: false,
10261
+ logPrivacy: () => false
10262
+ };
10263
+ request.logPrivacy = () => !request.privacySuppressed && this.privateNow(request.privacyCurrent);
10264
+ const id = this.nextRequestId++;
10265
+ this.requests.set(id, request);
10266
+ let result;
10267
+ try {
10268
+ result = currentTrace.run(request, fn);
10269
+ } catch (error) {
10270
+ this.finishRequest(id, request, "threw");
10271
+ throw error;
10272
+ }
10273
+ if (!isThenable(result)) {
10274
+ this.finishRequest(id, request, "returned");
10275
+ return result;
10276
+ }
10277
+ return Promise.resolve(result).then((value) => {
10278
+ this.finishRequest(id, request, "returned");
10279
+ return value;
10280
+ }, (error) => {
10281
+ this.finishRequest(id, request, "threw");
10282
+ throw error;
10283
+ });
10284
+ }
10285
+ observe(phase, privacyCurrent, fn, inputPublic) {
10286
+ if (this.stopped)
10287
+ return fn();
10288
+ const current = currentTrace.getStore();
10289
+ if (current?.telemetry === this && !current.settled) {
10290
+ if (current.privacySuppressed)
10291
+ return fn();
10292
+ const inputVisible = this.privateNow(privacyCurrent) && (!inputPublic || this.privateNow(inputPublic));
10293
+ inputPublic = undefined;
10294
+ if (!inputVisible)
10295
+ current.privacySuppressed = true;
10296
+ return controlWriteSpan(phase, fn);
10297
+ }
10298
+ return this.run({ kind: "background" }, privacyCurrent, () => controlWriteSpan(phase, fn), inputPublic);
10299
+ }
10300
+ start() {
10301
+ if (this.stopped || this.timer)
10302
+ return;
10303
+ this.nextSampleAt = this.readNow() + CONTROL_WRITE_SAMPLE_MS;
10304
+ this.timer = setInterval(() => this.sample(), CONTROL_WRITE_SAMPLE_MS);
10305
+ this.timer.unref?.();
10306
+ }
10307
+ stop() {
10308
+ this.stopped = true;
10309
+ if (this.timer)
10310
+ clearInterval(this.timer);
10311
+ this.timer = undefined;
10312
+ this.nextSampleAt = undefined;
10313
+ }
10314
+ sample() {
10315
+ if (this.stopped)
10316
+ return;
10317
+ const now = this.readNow(), expected = this.nextSampleAt ?? now;
10318
+ this.expireRequests(now);
10319
+ this.nextSampleAt = now + CONTROL_WRITE_SAMPLE_MS;
10320
+ const delayMs = Math.max(0, now - expected);
10321
+ if (delayMs < CONTROL_WRITE_LOOP_DELAY_MS)
10322
+ return;
10323
+ this.blockedLoopSamples++;
10324
+ const from = expected;
10325
+ const observed = [];
10326
+ for (const interval of this.recent) {
10327
+ if (interval.async || interval.endedAt === undefined || interval.startedAt >= now || interval.endedAt < from)
10328
+ continue;
10329
+ observed.push({ kind: interval.kind, ...interval.task === undefined ? {} : { task: interval.task }, phase: interval.phase, durationMs: this.roundMs(interval.endedAt - interval.startedAt), privacyCurrent: interval.privacyCurrent });
10330
+ }
10331
+ for (const span of this.spans.values()) {
10332
+ if (!span.async || span.startedAt >= now)
10333
+ continue;
10334
+ observed.push({
10335
+ kind: span.request.metadata.kind,
10336
+ ...span.request.metadata.task === undefined ? {} : { task: span.request.metadata.task },
10337
+ phase: span.phase,
10338
+ durationMs: this.roundMs(now - span.startedAt),
10339
+ open: true,
10340
+ privacyCurrent: span.request.logPrivacy
10341
+ });
10342
+ }
10343
+ const unique = new Map;
10344
+ for (const row of observed.sort((a, b) => b.durationMs - a.durationMs)) {
10345
+ const key = `${row.kind}:${row.task ?? ""}:${row.phase}:${row.open === true}`;
10346
+ if (!unique.has(key))
10347
+ unique.set(key, row);
10348
+ }
10349
+ const selected = [...unique.values()].slice(0, MAX_LOOP_SPANS);
10350
+ const spans = selected.map(({ kind, task, phase, durationMs, open }) => ({ kind, ...task === undefined ? {} : { task }, phase, durationMs, ...open ? { open: true } : {} }));
10351
+ const guards = [...new Set(selected.map((row) => row.privacyCurrent))];
10352
+ const attribution = selected.some((row) => row.open !== true) ? "sync_overlap_observed" : selected.length ? "open_async_wait" : "unknown";
10353
+ this.emit("loop", {
10354
+ type: "control_write_loop_delay",
10355
+ delayMs: this.roundMs(delayMs),
10356
+ attribution,
10357
+ spans
10358
+ }, () => guards.every((guard) => this.privateNow(guard)));
10359
+ }
10360
+ status() {
10361
+ return {
10362
+ active: this.requests.size,
10363
+ activeSpans: this.spans.size,
10364
+ completedRequests: this.completedRequests,
10365
+ expiredRequests: this.expiredRequests,
10366
+ slowRequests: this.slowRequests,
10367
+ blockedLoopSamples: this.blockedLoopSamples,
10368
+ recentSpans: this.recent.length,
10369
+ droppedActive: this.droppedActive,
10370
+ droppedSpans: this.droppedSpans,
10371
+ droppedRecent: this.droppedRecent,
10372
+ droppedLogs: this.droppedLogs,
10373
+ droppedMetadata: this.droppedMetadata,
10374
+ clockFailures: this.clockFailures
10375
+ };
10376
+ }
10377
+ beginSpan(request, phase) {
10378
+ const span = { request, phase, startedAt: this.readNow(), async: false, finished: false, prefixRecorded: false };
10379
+ if (this.spans.size >= this.maxActiveSpans) {
10380
+ this.droppedSpans++;
10381
+ return span;
10382
+ }
10383
+ span.id = this.nextSpanId++;
10384
+ this.spans.set(span.id, span);
10385
+ return span;
10386
+ }
10387
+ finishSpan(span) {
10388
+ if (span.finished)
10389
+ return;
10390
+ span.finished = true;
10391
+ if (span.request.settled)
10392
+ return;
10393
+ const endedAt = this.readNow();
10394
+ if (span.id !== undefined && this.spans.get(span.id) === span)
10395
+ this.spans.delete(span.id);
10396
+ const durationMs = Math.max(0, endedAt - span.startedAt);
10397
+ const request = span.request;
10398
+ const prior = request.phases.get(span.phase) ?? 0;
10399
+ if (!request.phaseOrder.includes(span.phase))
10400
+ request.phaseOrder.push(span.phase);
10401
+ request.phases.set(span.phase, prior + durationMs);
10402
+ if (!span.async && !span.prefixRecorded)
10403
+ this.rememberSyncInterval(span, endedAt);
10404
+ }
10405
+ recordSyncPrefix(span) {
10406
+ if (span.finished || span.prefixRecorded || span.request.settled)
10407
+ return;
10408
+ const endedAt = this.readNow();
10409
+ if (endedAt - span.startedAt < MIN_SYNC_PREFIX_MS)
10410
+ return;
10411
+ span.prefixRecorded = true;
10412
+ this.rememberSyncInterval(span, endedAt);
10413
+ }
10414
+ rememberSyncInterval(span, endedAt) {
10415
+ const request = span.request;
10416
+ this.recent.push({ kind: request.metadata.kind, ...request.metadata.task === undefined ? {} : { task: request.metadata.task }, phase: span.phase, startedAt: span.startedAt, endedAt, async: false, privacyCurrent: request.logPrivacy });
10417
+ while (this.recent.length > this.maxRecentSpans) {
10418
+ this.recent.shift();
10419
+ this.droppedRecent++;
10420
+ }
10421
+ }
10422
+ finishRequest(id, request, outcome) {
10423
+ if (!this.requests.delete(id))
10424
+ return;
10425
+ request.settled = true;
10426
+ for (const [spanId, span] of this.spans)
10427
+ if (span.request === request)
10428
+ this.spans.delete(spanId);
10429
+ this.completedRequests++;
10430
+ const durationMs = this.roundMs(this.readNow() - request.startedAt);
10431
+ if (durationMs < CONTROL_WRITE_SLOW_MS)
10432
+ return;
10433
+ this.slowRequests++;
10434
+ const phases = request.phaseOrder.map((phase) => ({ phase, durationMs: this.roundMs(request.phases.get(phase) ?? 0) }));
10435
+ this.emit("slow", {
10436
+ type: "control_write_slow",
10437
+ kind: request.metadata.kind,
10438
+ ...request.metadata.task === undefined ? {} : { task: request.metadata.task },
10439
+ durationMs,
10440
+ outcome,
10441
+ phases: phases.slice(0, MAX_LOOP_SPANS),
10442
+ ...phases.length > MAX_LOOP_SPANS ? { truncated: true } : {}
10443
+ }, request.logPrivacy);
10444
+ }
10445
+ expireRequests(now) {
10446
+ for (const [id, request] of this.requests) {
10447
+ if (now - request.startedAt < CONTROL_WRITE_REQUEST_MAX_AGE_MS)
10448
+ continue;
10449
+ request.settled = true;
10450
+ request.privacySuppressed = true;
10451
+ this.requests.delete(id);
10452
+ this.expiredRequests++;
10453
+ }
10454
+ for (const [id, span] of this.spans)
10455
+ if (span.request.settled)
10456
+ this.spans.delete(id);
10457
+ }
10458
+ emit(key, value, privacyCurrent) {
10459
+ if (this.stopped || !this.privateNow(privacyCurrent))
10460
+ return;
10461
+ const now = this.readNow(), last = this.lastEmit.get(key);
10462
+ if (last !== undefined && now - last < this.rateLimitMs) {
10463
+ this.droppedLogs++;
10464
+ return;
10465
+ }
10466
+ while (this.emitted.length && now - this.emitted[0] >= 60000)
10467
+ this.emitted.shift();
10468
+ if (this.emitted.length >= MAX_EMITS_PER_MINUTE) {
10469
+ this.droppedLogs++;
10470
+ return;
10471
+ }
10472
+ let line;
10473
+ try {
10474
+ line = JSON.stringify(value);
10475
+ } catch {
10476
+ this.droppedLogs++;
10477
+ return;
10478
+ }
10479
+ if (Buffer.byteLength(line) > CONTROL_WRITE_MAX_LINE_BYTES) {
10480
+ const minimal = value.type === "control_write_loop_delay" ? { type: "control_write_loop_delay", delayMs: typeof value.delayMs === "number" ? value.delayMs : 0, attribution: "unknown", spans: [], truncated: true } : { type: "control_write_slow", kind: value.kind, ...value.task === undefined ? {} : { task: value.task }, durationMs: value.durationMs, outcome: value.outcome, phases: [], truncated: true };
10481
+ line = JSON.stringify(minimal);
10482
+ if (Buffer.byteLength(line) > CONTROL_WRITE_MAX_LINE_BYTES) {
10483
+ this.droppedLogs++;
10484
+ return;
10485
+ }
10486
+ }
10487
+ this.lastEmit.set(key, now);
10488
+ this.emitted.push(now);
10489
+ try {
10490
+ this.options.emit(line);
10491
+ } catch {
10492
+ this.droppedLogs++;
10493
+ }
10494
+ }
10495
+ privateNow(guard) {
10496
+ try {
10497
+ return guard() === true;
10498
+ } catch {
10499
+ return false;
10500
+ }
10501
+ }
10502
+ safeMetadata(metadata) {
10503
+ if (!isObject(metadata))
10504
+ return;
10505
+ try {
10506
+ const kind = ownValue(metadata, "kind"), task = ownValue(metadata, "task");
10507
+ if (typeof kind !== "string" || !CONTROL_WRITE_KINDS.includes(kind) || task !== undefined && !validTaskId(task))
10508
+ return;
10509
+ return { kind, ...task === undefined ? {} : { task } };
10510
+ } catch {
10511
+ return;
10512
+ }
10513
+ }
10514
+ readNow() {
10515
+ let value;
10516
+ try {
10517
+ value = this.nowFn();
10518
+ } catch {
10519
+ this.clockFailures++;
10520
+ return this.lastNow;
10521
+ }
10522
+ if (!Number.isFinite(value) || value < this.lastNow) {
10523
+ this.clockFailures++;
10524
+ return this.lastNow;
10525
+ }
10526
+ this.lastNow = value;
10527
+ return value;
10528
+ }
10529
+ roundMs(value) {
10530
+ const bounded = Number.isFinite(value) ? Math.min(CONTROL_WRITE_MAX_DURATION_MS, Math.max(0, value)) : CONTROL_WRITE_MAX_DURATION_MS;
10531
+ return Math.round(bounded * 10) / 10;
10532
+ }
10533
+ }
10534
+ var CONTROL_WRITE_KINDS, CONTROL_WRITE_PHASES, CONTROL_WRITE_SLOW_MS = 1000, CONTROL_WRITE_LOOP_DELAY_MS = 250, CONTROL_WRITE_SAMPLE_MS = 100, CONTROL_WRITE_MAX_LINE_BYTES = 2048, CONTROL_WRITE_MAX_DURATION_MS = 86400000, CONTROL_WRITE_REQUEST_MAX_AGE_MS, MAX_ACTIVE_REQUESTS = 128, MAX_ACTIVE_SPANS = 256, MAX_RECENT_SPANS = 512, MAX_LOOP_SPANS = 8, MIN_SYNC_PREFIX_MS = 10, MAX_EMITS_PER_MINUTE = 60, RATE_LIMIT_MS = 1000, currentTrace, OPERATIONS;
10535
+ var init_control_write_trace = __esm(() => {
10536
+ CONTROL_WRITE_KINDS = [
10537
+ "hub_task_propose",
10538
+ "hub_review",
10539
+ "hub_task_accept",
10540
+ "hub_task_done",
10541
+ "hub_task_decline",
10542
+ "hub_task_cancel",
10543
+ "hub_task_assign",
10544
+ "task_assign",
10545
+ "task_assign_checked",
10546
+ "task_unassign",
10547
+ "hub_delivery_done",
10548
+ "background"
10549
+ ];
10550
+ CONTROL_WRITE_PHASES = [
10551
+ "client-send",
10552
+ "client-wait",
10553
+ "daemon-dispatch",
10554
+ "daemon-reply",
10555
+ "board-write",
10556
+ "queue-hook",
10557
+ "status-hook",
10558
+ "memory",
10559
+ "external",
10560
+ "snapshot"
10561
+ ];
10562
+ CONTROL_WRITE_REQUEST_MAX_AGE_MS = 5 * 60000;
10563
+ currentTrace = new AsyncLocalStorage;
10564
+ OPERATIONS = {
10565
+ hub_task_propose: "hub_task_propose",
10566
+ hub_review: "hub_review",
10567
+ hub_task_accept: "hub_task_accept",
10568
+ hub_task_done: "hub_task_done",
10569
+ hub_task_decline: "hub_task_decline",
10570
+ hub_task_cancel: "hub_task_cancel",
10571
+ hub_task_assign: "hub_task_assign",
10572
+ task_assign: "task_assign",
10573
+ task_assign_checked: "task_assign_checked",
10574
+ task_unassign: "task_unassign"
10575
+ };
10576
+ });
10577
+
9961
10578
  // src/hub/review-findings.ts
9962
10579
  var MAX_BYTES2;
9963
10580
  var init_review_findings = __esm(() => {
@@ -10003,6 +10620,7 @@ var SDLC_STAGES, PLAYBOOK_STAGES, BASES, STAGE_GATES, GATES;
10003
10620
  var init_sdlc = __esm(() => {
10004
10621
  init_test_integrity();
10005
10622
  init_plan_conformance();
10623
+ init_control_write_trace();
10006
10624
  init_review_findings();
10007
10625
  init_sdlc_metrics();
10008
10626
  init_eval_gate();
@@ -11188,10 +11806,10 @@ var init_shard_bridge = __esm(() => {
11188
11806
  });
11189
11807
 
11190
11808
  // node_modules/@staix/agent-mem/src/memory/cli-attribution.ts
11191
- import { AsyncLocalStorage } from "async_hooks";
11809
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
11192
11810
  var scope;
11193
11811
  var init_cli_attribution = __esm(() => {
11194
- scope = new AsyncLocalStorage;
11812
+ scope = new AsyncLocalStorage2;
11195
11813
  });
11196
11814
 
11197
11815
  // node_modules/@staix/agent-mem/src/memory/private-spans.ts
@@ -11789,7 +12407,7 @@ var init_hook_contract = __esm(() => {
11789
12407
  });
11790
12408
 
11791
12409
  // node_modules/@staix/agent-mem/src/hook-spool.ts
11792
- var RECORD_BYTES, MAX_BYTES6, DRAIN_BYTES, CAPACITY_BYTES, RETIRE_AFTER_MS;
12410
+ var RECORD_BYTES, MAX_BYTES6, HOOK_REPLAY_MAX_BYTES, CAPACITY_BYTES, RETIRE_AFTER_MS;
11793
12411
  var init_hook_spool = __esm(() => {
11794
12412
  init_formats();
11795
12413
  init_capture_input();
@@ -11804,7 +12422,7 @@ var init_hook_spool = __esm(() => {
11804
12422
  init_hook_contract();
11805
12423
  RECORD_BYTES = HOOK_INPUT_BYTES + 8192;
11806
12424
  MAX_BYTES6 = 2 * 1024 * 1024;
11807
- DRAIN_BYTES = 256 * 1024;
12425
+ HOOK_REPLAY_MAX_BYTES = 256 * 1024;
11808
12426
  CAPACITY_BYTES = 256 * 1024;
11809
12427
  RETIRE_AFTER_MS = 7 * 24 * 3600000;
11810
12428
  });
@@ -11851,6 +12469,7 @@ var init_hook_jcode = __esm(() => {
11851
12469
  });
11852
12470
 
11853
12471
  // node_modules/@staix/agent-mem/src/hooks.ts
12472
+ var hookStartedAt;
11854
12473
  var init_hooks = __esm(() => {
11855
12474
  init_hook_project();
11856
12475
  init_hook_native();
@@ -11866,6 +12485,7 @@ var init_hooks = __esm(() => {
11866
12485
  init_config();
11867
12486
  init_hook_contract();
11868
12487
  init_hook_contract();
12488
+ hookStartedAt = performance.now();
11869
12489
  });
11870
12490
 
11871
12491
  // node_modules/@staix/agent-mem/src/ingest.ts
@@ -12013,7 +12633,7 @@ var package_default;
12013
12633
  var init_package = __esm(() => {
12014
12634
  package_default = {
12015
12635
  name: "@staix/agent-hub",
12016
- version: "0.28.0",
12636
+ version: "0.30.0",
12017
12637
  description: "Native multi-agent hub: Claude Code, Codex, Kimi Code, Pi and local inference as peers in one project",
12018
12638
  license: "MIT",
12019
12639
  type: "module",
@@ -12030,7 +12650,7 @@ var init_package = __esm(() => {
12030
12650
  bun: ">=1.4.2"
12031
12651
  },
12032
12652
  dependencies: {
12033
- "@staix/agent-mem": "0.19.0"
12653
+ "@staix/agent-mem": "0.20.1"
12034
12654
  },
12035
12655
  devDependencies: {
12036
12656
  "@modelcontextprotocol/sdk": "^1.27.1",
@@ -12100,6 +12720,7 @@ var init_board = __esm(() => {
12100
12720
  init_task_code_symbols();
12101
12721
  init_plan_conformance();
12102
12722
  init_db_schema();
12723
+ init_control_write_trace();
12103
12724
  init_planning();
12104
12725
  init_envelope();
12105
12726
  init_review_findings();
@@ -12256,7 +12877,7 @@ function profileTaskTools(profile = "dev", stages, criteria = false) {
12256
12877
  if (customProfile && tool.name === "hub_task_propose")
12257
12878
  properties.class = { type: "string", enum: [...new Set([...Object.keys(data.classes), ...PROFILE_CLASS_NAMES.filter((v) => projectProfile("dev").classes[v])])] };
12258
12879
  if (customProfile && properties.plan && tool.name !== "hub_task_accept" && tool.name !== "hub_experiment_record")
12259
- 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" ? "citation ids, DOI or source URL ids" : "short claim ids to verify")])) } };
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")])) } };
12260
12881
  if (customProfile && tool.name === "hub_task_done" && data.name !== "dev") {
12261
12882
  const summaryProp = properties.summary;
12262
12883
  properties.summary = {
@@ -12266,7 +12887,7 @@ function profileTaskTools(profile = "dev", stages, criteria = false) {
12266
12887
  }
12267
12888
  if (customProfile && tool.name === "hub_backlog_split") {
12268
12889
  const parts = properties.parts;
12269
- 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" ? "citation ids, DOI or source URL ids" : "short claim ids to verify")])) } };
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")])) } };
12270
12891
  properties.parts = { ...parts, items: { ...parts.items, properties: { ...parts.items.properties, plan: selectedPlan } } };
12271
12892
  }
12272
12893
  if (criteria && properties.plan && tool.name !== "hub_experiment_record") {
@@ -12294,7 +12915,7 @@ var IMPLEMENTER_HUB_TOOL_NAMES, IMPLEMENTER_PERMISSION_TOOL, ACTIONABLE_MESSAGE_
12294
12915
  name,
12295
12916
  description,
12296
12917
  inputSchema: { type: "object", properties: name === "hub_review" || name === "hub_task_propose" ? { ...properties, request_id: toolWriteRequestId } : properties, required, additionalProperties: false }
12297
- }), 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;
12298
12919
  var init_hub_tools = __esm(() => {
12299
12920
  init_test_integrity();
12300
12921
  init_sdlc();
@@ -12357,7 +12978,7 @@ var init_hub_tools = __esm(() => {
12357
12978
  },
12358
12979
  additionalProperties: false
12359
12980
  };
12360
- acceptPlan = { ...plan, properties: { ...plan.properties, sections: list("document heading anchors, such as report.md#methods"), sources: list("citation ids, DOI or source URL ids"), claims: list("short claim ids to verify") } };
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") } };
12361
12982
  STUDY_TOOLS = new Set(["hub_study_draft", "hub_study_revise", "hub_study_show", "hub_experiment_show", "hub_experiment_record", "hub_study_handoff"]);
12362
12983
  HANDOFF_TOOLS = new Set(["hub_task_handoff", "hub_task_handoffs"]);
12363
12984
  BOUNDED_TASK_TOOLS = new Set(["hub_task_spec", "hub_check_start", "hub_check_result", "hub_check_cancel"]);
@@ -12392,7 +13013,7 @@ var init_hub_tools = __esm(() => {
12392
13013
  tool2("hub_task_accept", "Take a task that was assigned to you, with your plan for it. Set refs.repo for work in another repository; acceptance may correct only that repository metadata.", { id: id2, plan: acceptPlan, refs: { ...refs, properties: { repo: refs.properties.repo } } }, ["id"]),
12393
13014
  tool2("hub_task_cancel", "Cancel proposed/backlog work you proposed as its configured planner, including work an owner accepted and then declined while it has no execution, branch, commit or PR evidence; work with any such evidence is person-only. Keeps history and does not stop a native turn or delete files.", { id: id2, reason: str2, superseded_by: str2, revision: { type: "integer", minimum: 0 } }, ["id", "reason"]),
12394
13015
  tool2("hub_task_decline", "Pass on a task assigned to you; the hub offers it to the next peer. A plain decline is a lasting refusal: you are never offered this task again. Pass capacity: true when you only have no room right now: you are skipped for this move only, and a later reroute or planner pull may offer it to you again.", { id: id2, reason: str2, capacity: { type: "boolean" } }, ["id"]),
12395
- tool2("hub_task_done", "Mark your task finished. It goes to its reviewer with your summary and refs; no duplicate hub_send chat is needed. If a covering spec is unchanged, update it or provide spec_ack with a public reason for each unchanged spec. When the project configures a check for its class, the hub runs it first and the result comes as a task message: a failed check keeps the task with you.", { id: id2, summary: { type: "string", description: "what changed, why, and the check you ran with its result" }, refs, deliverables: deliverablesSchema, spec_ack: specAck }, ["id", "summary"]),
13016
+ tool2("hub_task_done", "Mark your task finished. It goes to its reviewer with your summary and refs; no duplicate hub_send chat is needed. When re-filing work after review, set refs.commit to the new full SHA; summary text does not change the review head. If a covering spec is unchanged, update it or provide spec_ack with a public reason for each unchanged spec. When the project configures a check for its class, the hub runs it first and the result comes as a task message: a failed check keeps the task with you.", { id: id2, summary: { type: "string", description: "what changed, why, and the check you ran with its result" }, refs, deliverables: deliverablesSchema, spec_ack: specAck }, ["id", "summary"]),
12396
13017
  tool2("hub_sdlc_record", "Record artifact ids or claimed local checks on your task. This grants no authority or external-check evidence. Only its owner/reviewer or the person may record; only its reviewer/person may name a reviewed head or phase checklist. Phase evidence refs are content hashes, not external verification.", { id: id2, receipt: { type: "object", properties: { deliverables: deliverablesSchema, stage: { type: "string", enum: ["spec", "plan", "implement", "review", "verify", "merge", "release"] }, repo: str2, issue: id2, pr: id2, localHead: str2, localResult: { type: "string", enum: ["passed", "failed", "unknown"] }, reviewedHead: str2, phase: { type: "object", properties: { id: str2, head: str2, tasks: { type: "array", maxItems: 64, items: { type: "object", properties: { id: id2, after: { type: "array", maxItems: 16, items: id2 } }, required: ["id", "after"], additionalProperties: false } }, criteria: { type: "array", maxItems: 32, items: { type: "object", properties: { id: str2, required: { type: "boolean" }, result: { type: "string", enum: ["verified", "failed", "unverified"] }, refs: { type: "array", maxItems: 8, items: str2 } }, required: ["id", "required", "result", "refs"], additionalProperties: false } } }, required: ["id", "tasks", "criteria"], additionalProperties: false } }, additionalProperties: false } }, ["id", "receipt"]),
12397
13018
  tool2("hub_task_list", "The task board, newest updated first, 50 compact rows (id, state, owner, reviewer, title) per call. The reply is {tasks, total, next?}: pass next as cursor for the following page. Use ids for full rows of specific tasks, or detail: true for full rows of a page. PII tasks show as [pii].", { state: { type: "string", enum: ["proposed", "in_progress", "in_review", "approved", "changes_requested", "cancelled"] }, ready: { type: "boolean", description: "only proposed tasks with nothing left to wait for" }, limit: { type: "integer", minimum: 1, maximum: 200, description: "rows per page, default 50" }, cursor: { type: "string", description: "the next value from the previous reply, unchanged" }, ids: { type: "array", minItems: 1, maxItems: 200, items: { type: "integer", minimum: 1 }, description: "full rows for exactly these task numbers; no paging" }, detail: { type: "boolean", description: "full rows instead of compact rows" } }),
12398
13019
  tool2("hub_specs", "Read the project's design-spec checks or find specs covering repository-relative paths. Read-only; at most 20 result items are returned. Does not reveal private task data.", { action: { type: "string", enum: ["check", "covers"] }, paths: { type: "array", maxItems: 32, items: { type: "string", minLength: 1, maxLength: 2000 } } }, ["action"]),
@@ -12426,10 +13047,11 @@ var init_hub_tools = __esm(() => {
12426
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"]),
12427
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 }),
12428
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 }),
12429
- 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"])
12430
13051
  ];
12431
13052
  TASK_TOOLS.push(...PLANNER_REVIEW_TOOLS);
12432
- TASK_TOOL_NAMES = new Set(TASK_TOOLS.map((t) => t.name));
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]);
12433
13055
  CONDUCTOR_TOOLS = [
12434
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.", {}),
12435
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"]),
@@ -12540,6 +13162,32 @@ var init_local_worker = __esm(() => {
12540
13162
  deliveryOnly = true;
12541
13163
  hubOwned = true;
12542
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
+ }
12543
13191
  sessionId = `agent-hub-local-${randomUUID7()}`;
12544
13192
  routes;
12545
13193
  warnedLegacyRoutes = new Set;
@@ -12570,7 +13218,7 @@ var init_local_worker = __esm(() => {
12570
13218
  throw new Error("route belongs to an ended turn");
12571
13219
  if (signal.aborted)
12572
13220
  throw new Error("turn cancelled before model request");
12573
- 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) : []];
12574
13222
  const result = await this.opts.omni.chat({ model, messages, ...tools ? { tools } : {}, ...judge ? { max_tokens: maxTokens ?? 2048 } : {} }, { signal, ...pii ? { onCampusOnly: true } : {} });
12575
13223
  this.recordUsage(result, model, task);
12576
13224
  if (generation !== this.turn)
@@ -12623,7 +13271,8 @@ var init_local_worker = __esm(() => {
12623
13271
  this.setState("busy");
12624
13272
  if (deliveryId)
12625
13273
  this.delivery({ id: deliveryId, state: "accepted" });
12626
- 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
+ `) }];
12627
13276
  const progress = { sideEffects: 0, last: "" };
12628
13277
  const requestedPolicy = this.opts.turnPolicy?.(envs);
12629
13278
  const policy = envs.some((env) => env.private) ? { ...requestedPolicy, pii: true } : requestedPolicy;
@@ -12760,7 +13409,7 @@ var init_local_worker = __esm(() => {
12760
13409
  this.abort?.abort();
12761
13410
  throw new ApprovalWaitStop(this.approvalStopReason);
12762
13411
  }
12763
- if (!remoteTurn)
13412
+ if (!remoteTurn && name !== "hub_set_title")
12764
13413
  this.observeTool(call, output, policy, `${this.sessionId}.${turn}.${step}`);
12765
13414
  if (name !== "hub_send" && name !== "doc_read" && name !== "web_fetch" && !TASK_TOOL_NAMES.has(name) && !CONDUCTOR_TOOL_NAMES.has(name))
12766
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)) });
@@ -12895,7 +13544,7 @@ var init_local_worker = __esm(() => {
12895
13544
  }
12896
13545
  const fixedModel = policy?.fixedModel ?? this.opts.fixedModel;
12897
13546
  const task = policy?.task === undefined ? undefined : Number(policy.task);
12898
- 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) : []];
12899
13548
  const signal = this.abort.signal;
12900
13549
  const messages = [{ role: "system", content: system(this.opts.cwd, this.opts.preamble) }, ...this.history, ...turnMsgs];
12901
13550
  const routed = await this.hubCall(route, messages, policy, signal);
@@ -15422,6 +16071,8 @@ var init_builtin_metadata = __esm(() => {
15422
16071
  projection: { roleRank: 4, startRank: 0, helpRank: 6, plannerEligible: false },
15423
16072
  api: 1,
15424
16073
  kind: "pi",
16074
+ title: "hub",
16075
+ titlePersistence: "native",
15425
16076
  enabledSwitchStartOnly: true,
15426
16077
  displayName: "Pi",
15427
16078
  family: "pi",
@@ -15446,6 +16097,7 @@ var init_builtin_metadata = __esm(() => {
15446
16097
  projection: { roleRank: 3, helpRank: 7, plannerEligible: false },
15447
16098
  api: 1,
15448
16099
  kind: "local",
16100
+ title: "hub",
15449
16101
  displayName: "Local worker",
15450
16102
  family: "local",
15451
16103
  transport: "hub-native",
@@ -16053,6 +16705,8 @@ function assertNativeCapabilityScope(definition) {
16053
16705
  return;
16054
16706
  const declared = (block, key) => block !== undefined && (key in block);
16055
16707
  const restricted = [
16708
+ ["title", definition.title !== undefined],
16709
+ ["titlePersistence", definition.titlePersistence !== undefined],
16056
16710
  ["start.acpCommand", declared(definition.start, "acpCommand")],
16057
16711
  ["attach.admission", declared(definition.attach, "admission")],
16058
16712
  ["budget.resetSoftTokensOnResume", declared(definition.budget, "resetSoftTokensOnResume")],
@@ -23337,6 +23991,12 @@ function negotiateProtocol(local, remote) {
23337
23991
  }
23338
23992
 
23339
23993
  // src/hub/control-client.ts
23994
+ init_control_write_trace();
23995
+ var clientWriteTelemetry = new ControlWriteTelemetry({ emit: (line) => {
23996
+ if (process.env.AHUB_CONTROL_WRITE_TIMING === "1")
23997
+ console.error(`[agent-hub] ${line}`);
23998
+ } });
23999
+ var publicClientTiming = () => true;
23340
24000
  function stateDirFor(cwd) {
23341
24001
  return projectContext(cwd).stateDir;
23342
24002
  }
@@ -23502,6 +24162,14 @@ class ControlClient {
23502
24162
  return catalog;
23503
24163
  }
23504
24164
  request(msg, timeoutMs = 30000, signal) {
24165
+ if (process.env.AHUB_CONTROL_WRITE_TIMING !== "1")
24166
+ return this.requestBody(msg, timeoutMs, signal);
24167
+ const metadata = controlWriteMetadata(msg);
24168
+ if (!metadata)
24169
+ return this.requestBody(msg, timeoutMs, signal);
24170
+ return clientWriteTelemetry.run({ kind: metadata.kind }, publicClientTiming, () => controlWriteSpan("client-wait", () => this.requestBody(msg, timeoutMs, signal)));
24171
+ }
24172
+ requestBody(msg, timeoutMs, signal) {
23505
24173
  if (this.ws.readyState !== WebSocket.OPEN || this.closing && msg.t !== "request_cancel")
23506
24174
  return Promise.resolve({ ok: false, error: "hub connection is not open" });
23507
24175
  const invocation = msg.t === "task" && msg.op === "hub_invoke";
@@ -23561,7 +24229,7 @@ class ControlClient {
23561
24229
  }
23562
24230
  timer = timeoutMs ? setTimeout(() => abandon(`no answer from the hub within ${Math.round(timeoutMs / 1000)} s`), timeoutMs) : undefined;
23563
24231
  try {
23564
- this.ws.send(JSON.stringify({ ...wire, rid }));
24232
+ controlWriteSpan("client-send", () => this.ws.send(JSON.stringify({ ...wire, rid })));
23565
24233
  } catch {
23566
24234
  abandon("hub request could not be sent");
23567
24235
  }
@@ -23619,6 +24287,8 @@ init_plan_conformance();
23619
24287
  // src/hub/sdlc-release.ts
23620
24288
  init_child_process();
23621
24289
  init_git();
24290
+ import { fileURLToPath } from "url";
24291
+ var CHECKER_PATH = fileURLToPath(new URL("../../scripts/check-docs.mjs", import.meta.url));
23622
24292
 
23623
24293
  // src/hub/tasks.ts
23624
24294
  init_spec_read();
@@ -24235,7 +24905,7 @@ function slugify(input) {
24235
24905
  return input.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
24236
24906
  }
24237
24907
  var captureStackTrace = "captureStackTrace" in Error ? Error.captureStackTrace : (..._args) => {};
24238
- function isObject(data) {
24908
+ function isObject2(data) {
24239
24909
  return typeof data === "object" && data !== null && !Array.isArray(data);
24240
24910
  }
24241
24911
  var allowsEval = /* @__PURE__ */ cached(() => {
@@ -24254,7 +24924,7 @@ var allowsEval = /* @__PURE__ */ cached(() => {
24254
24924
  }
24255
24925
  });
24256
24926
  function isPlainObject(o) {
24257
- if (isObject(o) === false)
24927
+ if (isObject2(o) === false)
24258
24928
  return false;
24259
24929
  const ctor = o.constructor;
24260
24930
  if (ctor === undefined)
@@ -24262,7 +24932,7 @@ function isPlainObject(o) {
24262
24932
  if (typeof ctor !== "function")
24263
24933
  return true;
24264
24934
  const prot = ctor.prototype;
24265
- if (isObject(prot) === false)
24935
+ if (isObject2(prot) === false)
24266
24936
  return false;
24267
24937
  if (Object.prototype.hasOwnProperty.call(prot, "isPrototypeOf") === false) {
24268
24938
  return false;
@@ -26203,7 +26873,7 @@ var $ZodObject = /* @__PURE__ */ $constructor("$ZodObject", (inst, def) => {
26203
26873
  }
26204
26874
  return propValues;
26205
26875
  });
26206
- const isObject2 = isObject;
26876
+ const isObject = isObject2;
26207
26877
  const catchall = def.catchall;
26208
26878
  let value;
26209
26879
  const memo = globalConfig.memoizer;
@@ -26211,7 +26881,7 @@ var $ZodObject = /* @__PURE__ */ $constructor("$ZodObject", (inst, def) => {
26211
26881
  inst._zod.parse = (payload, ctx) => {
26212
26882
  value ?? (value = _normalized.value);
26213
26883
  const input = payload.value;
26214
- if (!isObject2(input)) {
26884
+ if (!isObject(input)) {
26215
26885
  payload.issues.push({
26216
26886
  expected: "object",
26217
26887
  code: "invalid_type",
@@ -26347,7 +27017,7 @@ var $ZodObjectJIT = /* @__PURE__ */ $constructor("$ZodObjectJIT", (inst, def) =>
26347
27017
  return doc.compile();
26348
27018
  };
26349
27019
  let fastpass;
26350
- const isObject2 = isObject;
27020
+ const isObject = isObject2;
26351
27021
  const jit = !globalConfig.jitless;
26352
27022
  const allowsEval2 = allowsEval;
26353
27023
  const fastEnabled = jit && allowsEval2.value;
@@ -26356,7 +27026,7 @@ var $ZodObjectJIT = /* @__PURE__ */ $constructor("$ZodObjectJIT", (inst, def) =>
26356
27026
  inst._zod.parse = (payload, ctx) => {
26357
27027
  value ?? (value = _normalized.value);
26358
27028
  const input = payload.value;
26359
- if (!isObject2(input)) {
27029
+ if (!isObject(input)) {
26360
27030
  payload.issues.push({
26361
27031
  expected: "object",
26362
27032
  code: "invalid_type",
@@ -26494,7 +27164,7 @@ var $ZodDiscriminatedUnion = /* @__PURE__ */ $constructor("$ZodDiscriminatedUnio
26494
27164
  const disc = cached(() => discriminatorMap(def));
26495
27165
  inst._zod.parse = (payload, ctx) => {
26496
27166
  const input = payload.value;
26497
- if (!isObject(input)) {
27167
+ if (!isObject2(input)) {
26498
27168
  payload.issues.push({
26499
27169
  code: "invalid_type",
26500
27170
  expected: "object",