@staix/agent-hub 0.17.0 → 0.18.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 (68) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +2 -2
  3. package/docs/agent-notes/README.md +5 -0
  4. package/docs/agent-notes/adapters.md +2 -0
  5. package/docs/agent-notes/benchmarks.md +1 -0
  6. package/docs/agent-notes/daemon.md +1 -0
  7. package/docs/agent-notes/local-worker.md +1 -0
  8. package/docs/agent-notes/tasks.md +7 -1
  9. package/docs/agent-notes/tests.md +1 -0
  10. package/docs/dogfood.md +15 -1
  11. package/docs/memory.md +1 -1
  12. package/docs/operations.md +105 -16
  13. package/docs/profiles.md +36 -0
  14. package/docs/quickstart.md +9 -3
  15. package/docs/sdlc.md +29 -3
  16. package/docs/security.md +26 -4
  17. package/docs/specs/2026-09-19-agent-hub-design.md +16 -6
  18. package/docs/verification/2026-10-25-0.18.0.md +76 -0
  19. package/docs/verification/2026-10-25-codex-review-live-verdict.md +35 -0
  20. package/docs/verification/2026-10-25-remote-docker-transport.md +46 -0
  21. package/docs/verification/profiles/YYYY-MM-DD-general.md +46 -0
  22. package/docs/verification/profiles/YYYY-MM-DD-research.md +61 -0
  23. package/docs/verification/profiles/YYYY-MM-DD-writing.md +46 -0
  24. package/docs/verified.json +24 -18
  25. package/package.json +2 -2
  26. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  27. package/plugins/agent-hub/server.js +94 -33
  28. package/src/adapters/definitions/builtin-metadata.ts +1 -1
  29. package/src/adapters/local-worker.ts +5 -3
  30. package/src/cli/console-state.ts +9 -2
  31. package/src/cli/help.ts +29 -28
  32. package/src/cli/identity.ts +87 -6
  33. package/src/cli/main.ts +190 -69
  34. package/src/cli/output.ts +7 -2
  35. package/src/cli/sdlc-output.ts +242 -0
  36. package/src/cli/task-output.ts +212 -0
  37. package/src/cli/upgrade-output.ts +57 -0
  38. package/src/hub/board.ts +17 -0
  39. package/src/hub/check-runner.ts +252 -22
  40. package/src/hub/conductor.ts +5 -0
  41. package/src/hub/config-apply.ts +16 -7
  42. package/src/hub/config-edit-host.ts +1 -1
  43. package/src/hub/config-tier.ts +1 -1
  44. package/src/hub/daemon.ts +353 -72
  45. package/src/hub/dogfood-profile.ts +248 -0
  46. package/src/hub/dogfood-read.ts +77 -14
  47. package/src/hub/dogfood.ts +5 -1
  48. package/src/hub/events.ts +1 -1
  49. package/src/hub/host-executable.ts +2 -1
  50. package/src/hub/hub-tools.ts +2 -2
  51. package/src/hub/operational-authority.ts +60 -0
  52. package/src/hub/operational-view.ts +141 -0
  53. package/src/hub/orphan-process.ts +69 -0
  54. package/src/hub/permission-mode.ts +1 -1
  55. package/src/hub/read-only-shell-proof.ts +80 -0
  56. package/src/hub/read-only-shell.ts +162 -0
  57. package/src/hub/reviewer-pool-config.ts +3 -2
  58. package/src/hub/reviewer-pool.ts +3 -0
  59. package/src/hub/routing.ts +23 -6
  60. package/src/hub/sdlc-merge.ts +299 -0
  61. package/src/hub/sdlc-mirror.ts +129 -0
  62. package/src/hub/sdlc.ts +58 -12
  63. package/src/hub/settings.ts +11 -5
  64. package/src/hub/snapshots.ts +60 -0
  65. package/src/hub/task-repository.ts +31 -5
  66. package/src/hub/tasks.ts +241 -16
  67. package/src/local/tools.ts +17 -4
  68. package/src/ui/index.html +2 -1
@@ -0,0 +1,61 @@
1
+ # Research profile weekly verification receipt
2
+
3
+ Template only. No pilot result or verdict has been recorded.
4
+
5
+ - Week ending: YYYY-MM-DD
6
+ - Profile id: research
7
+ - Window bounds: unknown
8
+
9
+ ## Recorded profile JSON
10
+
11
+ Run ahub report --dogfood --since 7d --json once at the review boundary. Replace the null below with only the emitted profile subsection. Preserve numbers and null values. Include no project/person name, task title, path, raw log or model text; use anonymous identifiers where necessary.
12
+
13
+ null
14
+
15
+ ## Research measurements
16
+
17
+ Copy only emitted counts and anonymous task/claim slots. These placeholders remain unknown until recorded evidence supports a value.
18
+
19
+ - Current-plan declared sources and claims per task: unknown
20
+ - Claims reaching a person verdict: unknown
21
+ - Claim slots with model label, person verdict and agreement: unknown
22
+ - Agreement counts by supported/unsupported/unclear label: unknown
23
+ - Recorded task-check outcomes by class alias: unknown
24
+ - Proven checks.fact_check selection and pass/fail by alias: unknown
25
+ - Missing check keys per run: unknown
26
+ - Section-overlap notices and confirmed section edits: unknown
27
+
28
+ Task-check history alone does not prove checks.fact_check ran. A model label or task approval alone does not prove a person verdict or agreement. Preserve every emitted null.
29
+
30
+ ## Verification verdicts
31
+
32
+ Choose exactly one listed verdict for each claim after inspecting recorded evidence. Unknown data alone cannot establish support. These placeholders are not verdicts.
33
+
34
+ - writing-live-example
35
+ - Verdict: [supported | partly supported | not supported]
36
+ - Evidence task/event/receipt ids: []
37
+ - Remaining unknowns: unknown
38
+ - general-live-example
39
+ - Verdict: [supported | partly supported | not supported]
40
+ - Evidence task/event/receipt ids: []
41
+ - Remaining unknowns: unknown
42
+ - research-live-example
43
+ - Verdict: [supported | partly supported | not supported]
44
+ - Evidence task/event/receipt ids: []
45
+ - Remaining unknowns: unknown
46
+ - native-model-ingestion
47
+ - Verdict: [supported | partly supported | not supported]
48
+ - Evidence task/event/receipt ids: []
49
+ - Remaining unknowns: unknown
50
+ - context-retention
51
+ - Verdict: [supported | partly supported | not supported]
52
+ - Evidence task/event/receipt ids: []
53
+ - Remaining unknowns: unknown
54
+ - native-account-research
55
+ - Verdict: [supported | partly supported | not supported]
56
+ - Evidence task/event/receipt ids: []
57
+ - Remaining unknowns: unknown
58
+ - research-measurement
59
+ - Verdict: [supported | partly supported | not supported]
60
+ - Evidence task/event/receipt ids: []
61
+ - Remaining unknowns: unknown
@@ -0,0 +1,46 @@
1
+ # Writing profile weekly verification receipt
2
+
3
+ Template only. No pilot result or verdict has been recorded.
4
+
5
+ - Week ending: YYYY-MM-DD
6
+ - Profile id: writing
7
+ - Window bounds: unknown
8
+
9
+ ## Recorded profile JSON
10
+
11
+ Run ahub report --dogfood --since 7d --json once at the review boundary. Replace the null below with only the emitted profile subsection. Preserve numbers and null values. Include no project/person name, task title, path, raw log or model text; use anonymous identifiers where necessary.
12
+
13
+ null
14
+
15
+ ## Verification verdicts
16
+
17
+ Choose exactly one listed verdict for each claim after inspecting recorded evidence. Unknown data alone cannot establish support. These placeholders are not verdicts.
18
+
19
+ - writing-live-example
20
+ - Verdict: [supported | partly supported | not supported]
21
+ - Evidence task/event/receipt ids: []
22
+ - Remaining unknowns: unknown
23
+ - general-live-example
24
+ - Verdict: [supported | partly supported | not supported]
25
+ - Evidence task/event/receipt ids: []
26
+ - Remaining unknowns: unknown
27
+ - research-live-example
28
+ - Verdict: [supported | partly supported | not supported]
29
+ - Evidence task/event/receipt ids: []
30
+ - Remaining unknowns: unknown
31
+ - native-model-ingestion
32
+ - Verdict: [supported | partly supported | not supported]
33
+ - Evidence task/event/receipt ids: []
34
+ - Remaining unknowns: unknown
35
+ - context-retention
36
+ - Verdict: [supported | partly supported | not supported]
37
+ - Evidence task/event/receipt ids: []
38
+ - Remaining unknowns: unknown
39
+ - native-account-research
40
+ - Verdict: [supported | partly supported | not supported]
41
+ - Evidence task/event/receipt ids: []
42
+ - Remaining unknowns: unknown
43
+ - research-measurement
44
+ - Verdict: [supported | partly supported | not supported]
45
+ - Evidence task/event/receipt ids: []
46
+ - Remaining unknowns: unknown
@@ -2,7 +2,7 @@
2
2
  "schemaVersion": 1,
3
3
  "documents": {
4
4
  "README.md": {
5
- "verifiedAgainst": "66db760595ad8bec4d844e4094e8b9db45d38e7a",
5
+ "verifiedAgainst": "af22ee0dd9c6a1f4a5c5fc3f10fbfe4875350c1e",
6
6
  "paths": [
7
7
  "package.json",
8
8
  "src/",
@@ -11,14 +11,14 @@
11
11
  ]
12
12
  },
13
13
  "docs/security.md": {
14
- "verifiedAgainst": "66db760595ad8bec4d844e4094e8b9db45d38e7a",
14
+ "verifiedAgainst": "af22ee0dd9c6a1f4a5c5fc3f10fbfe4875350c1e",
15
15
  "paths": [
16
16
  "src/",
17
17
  "templates/"
18
18
  ]
19
19
  },
20
20
  "docs/operations.md": {
21
- "verifiedAgainst": "66db760595ad8bec4d844e4094e8b9db45d38e7a",
21
+ "verifiedAgainst": "af22ee0dd9c6a1f4a5c5fc3f10fbfe4875350c1e",
22
22
  "paths": [
23
23
  "package.json",
24
24
  "src/",
@@ -35,7 +35,7 @@
35
35
  ]
36
36
  },
37
37
  "docs/quickstart.md": {
38
- "verifiedAgainst": "66db760595ad8bec4d844e4094e8b9db45d38e7a",
38
+ "verifiedAgainst": "af22ee0dd9c6a1f4a5c5fc3f10fbfe4875350c1e",
39
39
  "paths": [
40
40
  "package.json",
41
41
  "src/",
@@ -43,7 +43,7 @@
43
43
  ]
44
44
  },
45
45
  "docs/agent-notes/adapters.md": {
46
- "verifiedAgainst": "66db760595ad8bec4d844e4094e8b9db45d38e7a",
46
+ "verifiedAgainst": "af22ee0dd9c6a1f4a5c5fc3f10fbfe4875350c1e",
47
47
  "paths": [
48
48
  "src/adapters/",
49
49
  "src/pi/",
@@ -71,11 +71,14 @@
71
71
  "src/hub/roster.ts",
72
72
  "src/hub/instances.ts",
73
73
  "src/hub/action-enforce.ts",
74
- "src/hub/permission-payload.ts"
74
+ "src/hub/permission-payload.ts",
75
+ "src/hub/read-only-shell.ts",
76
+ "src/hub/read-only-shell-proof.ts",
77
+ "src/hub/operational-authority.ts"
75
78
  ]
76
79
  },
77
80
  "docs/agent-notes/benchmarks.md": {
78
- "verifiedAgainst": "66db760595ad8bec4d844e4094e8b9db45d38e7a",
81
+ "verifiedAgainst": "af22ee0dd9c6a1f4a5c5fc3f10fbfe4875350c1e",
79
82
  "paths": [
80
83
  "scripts/benchmarks/",
81
84
  "test/benchmarks/",
@@ -87,7 +90,7 @@
87
90
  ]
88
91
  },
89
92
  "docs/agent-notes/budget.md": {
90
- "verifiedAgainst": "66db760595ad8bec4d844e4094e8b9db45d38e7a",
93
+ "verifiedAgainst": "af22ee0dd9c6a1f4a5c5fc3f10fbfe4875350c1e",
91
94
  "paths": [
92
95
  "src/hub/budget.ts",
93
96
  "src/cli/statusline-tee.ts",
@@ -111,7 +114,7 @@
111
114
  ]
112
115
  },
113
116
  "docs/agent-notes/bus.md": {
114
- "verifiedAgainst": "66db760595ad8bec4d844e4094e8b9db45d38e7a",
117
+ "verifiedAgainst": "af22ee0dd9c6a1f4a5c5fc3f10fbfe4875350c1e",
115
118
  "paths": [
116
119
  "src/hub/bus.ts",
117
120
  "src/hub/envelope.ts",
@@ -126,7 +129,7 @@
126
129
  ]
127
130
  },
128
131
  "docs/agent-notes/daemon.md": {
129
- "verifiedAgainst": "66db760595ad8bec4d844e4094e8b9db45d38e7a",
132
+ "verifiedAgainst": "af22ee0dd9c6a1f4a5c5fc3f10fbfe4875350c1e",
130
133
  "paths": [
131
134
  "src/hub/daemon.ts",
132
135
  "src/hub/control-client.ts",
@@ -199,7 +202,7 @@
199
202
  ]
200
203
  },
201
204
  "docs/agent-notes/local-worker.md": {
202
- "verifiedAgainst": "66db760595ad8bec4d844e4094e8b9db45d38e7a",
205
+ "verifiedAgainst": "af22ee0dd9c6a1f4a5c5fc3f10fbfe4875350c1e",
203
206
  "paths": [
204
207
  "src/adapters/local-worker.ts",
205
208
  "src/local/",
@@ -211,7 +214,7 @@
211
214
  ]
212
215
  },
213
216
  "docs/agent-notes/models.md": {
214
- "verifiedAgainst": "66db760595ad8bec4d844e4094e8b9db45d38e7a",
217
+ "verifiedAgainst": "af22ee0dd9c6a1f4a5c5fc3f10fbfe4875350c1e",
215
218
  "paths": [
216
219
  "src/models/",
217
220
  "src/hub/inference.ts",
@@ -219,7 +222,7 @@
219
222
  ]
220
223
  },
221
224
  "docs/agent-notes/tasks.md": {
222
- "verifiedAgainst": "66db760595ad8bec4d844e4094e8b9db45d38e7a",
225
+ "verifiedAgainst": "af22ee0dd9c6a1f4a5c5fc3f10fbfe4875350c1e",
223
226
  "paths": [
224
227
  "src/hub/tasks.ts",
225
228
  "src/hub/board.ts",
@@ -251,11 +254,13 @@
251
254
  "src/hub/spec-read.ts",
252
255
  "src/hub/workflow.ts",
253
256
  "src/hub/deliverables.ts",
254
- "src/hub/deliverable-files.ts"
257
+ "src/hub/deliverable-files.ts",
258
+ "src/hub/sdlc-merge.ts",
259
+ "src/hub/sdlc-mirror.ts"
255
260
  ]
256
261
  },
257
262
  "docs/agent-notes/tests.md": {
258
- "verifiedAgainst": "66db760595ad8bec4d844e4094e8b9db45d38e7a",
263
+ "verifiedAgainst": "af22ee0dd9c6a1f4a5c5fc3f10fbfe4875350c1e",
259
264
  "paths": [
260
265
  "test/",
261
266
  "scripts/check.sh",
@@ -274,11 +279,12 @@
274
279
  "scripts/changed-tests.mjs",
275
280
  "scripts/check-closing-refs.mjs",
276
281
  ".github/workflows/closing-refs.yml",
277
- "scripts/lib/ts-ast.ts"
282
+ "scripts/lib/ts-ast.ts",
283
+ "test/cli-json-guard.test.ts"
278
284
  ]
279
285
  },
280
286
  "docs/agent-notes/README.md": {
281
- "verifiedAgainst": "66db760595ad8bec4d844e4094e8b9db45d38e7a",
287
+ "verifiedAgainst": "af22ee0dd9c6a1f4a5c5fc3f10fbfe4875350c1e",
282
288
  "paths": [
283
289
  "src/cli/console-state.ts",
284
290
  "src/cli/attention.ts",
@@ -287,7 +293,7 @@
287
293
  ]
288
294
  },
289
295
  "src/cli/help.ts": {
290
- "verifiedAgainst": "66db760595ad8bec4d844e4094e8b9db45d38e7a",
296
+ "verifiedAgainst": "af22ee0dd9c6a1f4a5c5fc3f10fbfe4875350c1e",
291
297
  "paths": [
292
298
  "src/cli/main.ts",
293
299
  "src/cli/launch.ts",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@staix/agent-hub",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "Native multi-agent hub: Claude Code, Codex, Kimi Code, Pi and local inference as peers in one project",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -17,7 +17,7 @@
17
17
  "bun": ">=1.4.2"
18
18
  },
19
19
  "dependencies": {
20
- "@staix/agent-mem": "0.7.0"
20
+ "@staix/agent-mem": "0.8.0"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@modelcontextprotocol/sdk": "^1.27.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-hub",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "Channel between Claude Code and the agent-hub daemon: peer messages from Codex, Kimi and the local worker arrive as channel events; hub_send replies.",
5
5
  "author": {
6
6
  "name": "Young Joon Lee",
@@ -82,7 +82,7 @@ function memoryGitEnv(env = process.env, options = {}) {
82
82
  }
83
83
  function memoryGitSync(args, options) {
84
84
  const argv = memoryGitArgv(args);
85
- return childProcess().spawnSync(argv[0], argv.slice(1), { ...options, env: memoryGitEnv(options.env ?? process.env) });
85
+ return childProcess().spawnSync(argv[0], argv.slice(1), { ...ambientGitTimeout !== undefined ? { timeout: ambientGitTimeout, killSignal: "SIGKILL" } : {}, ...options, env: memoryGitEnv(options.env ?? process.env) });
86
86
  }
87
87
  function processSignature(pid) {
88
88
  try {
@@ -118,7 +118,7 @@ var require2, processes, childProcess = () => processes ??= require2("node:child
118
118
  const parent = dirname(absolute);
119
119
  return parent === absolute ? absolute : join(canonical(parent), basename(absolute));
120
120
  }
121
- }, DENY_SEGMENTS, DENY_NAMES, hasSegment = (rel, seg) => `/${rel}/`.includes(`/${seg}/`), SAFE_CONFIG, CREDENTIAL_KEYS, memoryGitArgv = (args, opts = {}) => ["git", "--no-pager", ...SAFE_CONFIG.filter((c) => !opts.credentials || !CREDENTIAL_KEYS.has(c)).flatMap((c) => ["-c", c]), ...args];
121
+ }, DENY_SEGMENTS, DENY_NAMES, hasSegment = (rel, seg) => `/${rel}/`.includes(`/${seg}/`), SAFE_CONFIG, CREDENTIAL_KEYS, memoryGitArgv = (args, opts = {}) => ["git", "--no-pager", ...SAFE_CONFIG.filter((c) => !opts.credentials || !CREDENTIAL_KEYS.has(c)).flatMap((c) => ["-c", c]), ...args], ambientGitTimeout;
122
122
  var init_platform = __esm(() => {
123
123
  require2 = createRequire(import.meta.url);
124
124
  DENY_SEGMENTS = [".maru/secrets", ".agenthub/state", ".agenthub/archive"];
@@ -6154,9 +6154,26 @@ ${preview(content)}`, "write", ctx.signal, file, undefined, String(a.path), a),
6154
6154
  return "error: empty command";
6155
6155
  if (command.length > 4000)
6156
6156
  return "error: command longer than 4000 characters; put it in a script file with write, then run that";
6157
- const refused = approvalError(await ctx.permit(`bash: ${command}`, "bash", ctx.signal, undefined, undefined, undefined, a), "error: the user did not approve this command");
6158
- if (refused)
6159
- return refused;
6157
+ const input = Object.freeze({ ...a, command });
6158
+ for (let attempt = 0;; attempt++) {
6159
+ const refused = approvalError(await ctx.permit(`bash: ${command}`, "bash", ctx.signal, undefined, undefined, undefined, input, attempt ? { forceAsk: true } : undefined), "error: the user did not approve this command");
6160
+ if (refused)
6161
+ return refused;
6162
+ if (ctx.signal?.aborted)
6163
+ return "error: turn cancelled before shell execution";
6164
+ let check = "valid";
6165
+ try {
6166
+ check = ctx.shellExecutionCheck?.(input) ?? "valid";
6167
+ } catch {
6168
+ check = "ask";
6169
+ }
6170
+ if (check === "aborted" || ctx.signal?.aborted)
6171
+ return "error: turn cancelled before shell execution";
6172
+ if (check === "valid")
6173
+ break;
6174
+ if (attempt)
6175
+ return "error: read-only shell proof remained unstable; no command was executed";
6176
+ }
6160
6177
  const res = await sandboxedExec(["/bin/bash", "-c", command], { cwd: ctx.cwd, profile: ctx.sandboxProfile, readOnlyData: !!ctx.readOnly?.length, timeoutMs: (Number(a.timeout_s) || 120) * 1000, ...ctx.sandboxEnv ? { env: ctx.sandboxEnv } : {}, ...ctx.signal ? { signal: ctx.signal } : {} });
6161
6178
  try {
6162
6179
  ctx.onProcessExit?.(res.code);
@@ -6220,7 +6237,7 @@ var init_tools = __esm(() => {
6220
6237
  fn("read", "Read a text file inside the project. Returns numbered lines.", { path: str, offset: { type: "integer", minimum: 1 }, limit: { type: "integer", minimum: 1 } }, ["path"]),
6221
6238
  fn("write", "Create or overwrite a file inside the project. Needs the user's approval.", { path: str, content: str }, ["path", "content"]),
6222
6239
  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"]),
6223
- 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. Needs the user's approval.", { command: str, timeout_s: { type: "integer", minimum: 1, maximum: 600 } }, ["command"]),
6240
+ 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"]),
6224
6241
  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"]),
6225
6242
  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"])
6226
6243
  ];
@@ -10017,7 +10034,7 @@ var init_hub_tools = __esm(() => {
10017
10034
  tool("hub_release_list", "Read public release scope and planned/actual dates for phased planning. Configured planners only.", {}),
10018
10035
  tool("hub_task_propose", "Put a piece of work on the shared task board. The hub assigns an owner by class (routing.toml) unless you name one. Classes: plan, implement, bulk_edit, test, review, summarize, triage. Name the class when you know it; without one the hub tries to pick it. Name yourself as owner to claim work nobody assigned you: it starts in progress. Put the paths the work touches in refs, set refs.repo for work in another repository, and when you claim work, a plan; the hub says when they overlap another open task.", { title: str2, class: { type: "string", enum: ["plan", "implement", "bulk_edit", "test", "review", "summarize", "triage"] }, detail: str2, role: { type: "string", description: "catalog role id for this task class; guidance only, no routing or authority change" }, refs, plan, owner: { type: "string", description: "peer id; yours to claim the work, omit to let the hub route it. With after, the owner is reserved: offered the task first once it is ready" }, after: { type: "array", items: { type: "integer" }, description: "ids of tasks that must be approved first; until then this one waits, offered to nobody, and cannot be claimed" }, backlog: { type: "boolean", description: "stage unoffered work; owner is only a reservation, even when naming yourself" }, urgent: { type: "boolean", description: "hand it over even when its owner is paused for quota only briefly" }, goal: { type: "string", description: "id of this project's long-term task (ahub goal list) the work is part of" } }, ["title"]),
10019
10036
  tool("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, plan: acceptPlan, refs: { ...refs, properties: { repo: refs.properties.repo } } }, ["id"]),
10020
- tool("hub_task_cancel", "Cancel unaccepted proposed/backlog work you proposed as its configured planner; accepted work is person-only. Keeps history and does not stop a native turn or delete files.", { id, reason: str2, superseded_by: str2, revision: { type: "integer", minimum: 0 } }, ["id", "reason"]),
10037
+ tool("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, reason: str2, superseded_by: str2, revision: { type: "integer", minimum: 0 } }, ["id", "reason"]),
10021
10038
  tool("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, reason: str2, capacity: { type: "boolean" } }, ["id"]),
10022
10039
  tool("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 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, summary: { type: "string", description: "what changed, why, and the check you ran with its result" }, refs, deliverables: deliverablesSchema }, ["id", "summary"]),
10023
10040
  tool("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, receipt: { type: "object", properties: { deliverables: deliverablesSchema, stage: { type: "string", enum: ["spec", "plan", "implement", "review", "verify", "merge", "release"] }, repo: str2, issue: id, pr: id, 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, after: { type: "array", maxItems: 16, items: id } }, 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"]),
@@ -10041,7 +10058,7 @@ var init_hub_tools = __esm(() => {
10041
10058
  TASK_TOOL_NAMES = new Set(TASK_TOOLS.map((t) => t.name));
10042
10059
  CONDUCTOR_TOOLS = [
10043
10060
  tool("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.", {}),
10044
- tool("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 and the task's current owner and reviewer may use this tool.", { id }, ["id"]),
10061
+ tool("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 }, ["id"]),
10045
10062
  tool("hub_task_assign", "Name an owner (default role) or reviewer. Only the conductor may name a reviewer through this tool. A proposer may redirect an unaccepted proposed task's owner; a task that still waits reserves that owner. Pull-only peers receive work when named while attached or when they claim it; a peer that cannot take the task is refused with the reason, and the task is unchanged. With an explicit capabilities list, the conductor needs assign capability for either role, the proposer only to give ownership to another peer.", { id, peer: str2, role: { type: "string", enum: ["owner", "reviewer"] } }, ["id", "peer"]),
10046
10063
  tool("hub_task_escalate", "Escalate a task through the normal task flow, as the conductor. Requires assign capability when explicitly listed.", { id }, ["id"]),
10047
10064
  tool("hub_peer_start", "Start local, kimi, qwen, jcode, pi, claude or codex in the start mode a person set for it; you cannot choose the mode. A headless peer starts at once. A TUI peer (claude, and pi and codex by default) opens in a terminal with a fixed command; when the hub cannot open a terminal the call fails with the command for the person to run.", { peer: { type: "string", enum: CONDUCTOR_START_PEERS } }, ["peer"]),
@@ -10367,11 +10384,12 @@ var init_local_worker = __esm(() => {
10367
10384
  return `(stopped after ${maxSteps} steps) ${progress.last}`.trim();
10368
10385
  }
10369
10386
  toolContext(envs, turnSignal, pii, reply) {
10387
+ const executionGeneration = this.turn;
10370
10388
  return {
10371
10389
  cwd: this.opts.cwd,
10372
10390
  deny: this.opts.tools.deny,
10373
10391
  readOnly: this.opts.tools.readOnly,
10374
- permit: async (title, tool, _signal, canonicalTarget, _observe, originalPath, input) => {
10392
+ permit: async (title, tool, _signal, canonicalTarget, _observe, originalPath, input, policy) => {
10375
10393
  if (turnSignal.aborted)
10376
10394
  return "aborted";
10377
10395
  const generation = this.turn;
@@ -10400,7 +10418,7 @@ var init_local_worker = __esm(() => {
10400
10418
  };
10401
10419
  const onAbort = () => finish("aborted");
10402
10420
  turnSignal.addEventListener("abort", onAbort, { once: true });
10403
- this.opts.tools.permit(title, tool, turnSignal, canonicalTarget, observe, originalPath, input).then(finish, (error) => {
10421
+ this.opts.tools.permit(title, tool, turnSignal, canonicalTarget, observe, originalPath, input, policy).then(finish, (error) => {
10404
10422
  turnSignal.removeEventListener("abort", onAbort);
10405
10423
  reject(error);
10406
10424
  });
@@ -10413,6 +10431,7 @@ var init_local_worker = __esm(() => {
10413
10431
  observe({ source: "expired", answerEpoch: this.approvalAnswerEpoch ?? 0, eligibleExpiry: true });
10414
10432
  return picked;
10415
10433
  },
10434
+ shellExecutionCheck: (input) => turnSignal.aborted || executionGeneration !== this.turn ? "aborted" : this.opts.tools.shellExecutionCheck?.(input) ?? "valid",
10416
10435
  sandboxProfile: this.sandboxProfile,
10417
10436
  sandboxEnv: { ...proxyEnv(this.opts.tools.bashNetwork ?? false), AGENTHUB_PEER_ID: this.id },
10418
10437
  signal: turnSignal,
@@ -12704,7 +12723,7 @@ var init_builtin_metadata = __esm(() => {
12704
12723
  headlessForms = Object.freeze(["headless"]);
12705
12724
  hubGrants = {
12706
12725
  ask: "every write, edit and shell command asks",
12707
- "ask-when-needed": "write and edit inside the project run without asking; shell and git writes still ask",
12726
+ "ask-when-needed": "write, edit and proven bounded read-only shell commands inside the project run without asking; other shell commands and git writes still ask",
12708
12727
  "never-ask": "every tool runs without asking, inside the hub sandbox and path guard"
12709
12728
  };
12710
12729
  CLAUDE_REVIEWER_VARIANT = {
@@ -14087,6 +14106,14 @@ var init_work_lists = __esm(() => {
14087
14106
  forbidden = new Set(["__proto__", "constructor", "prototype"]);
14088
14107
  });
14089
14108
 
14109
+ // node_modules/@staix/agent-mem/src/memory/candidate-pending.ts
14110
+ var CANDIDATE_AGE_MS, CANDIDATE_SHOWN_GRACE_MS, CANDIDATE_DUE_MS;
14111
+ var init_candidate_pending = __esm(() => {
14112
+ CANDIDATE_AGE_MS = 30 * 86400000;
14113
+ CANDIDATE_SHOWN_GRACE_MS = 7 * 86400000;
14114
+ CANDIDATE_DUE_MS = 7 * 86400000;
14115
+ });
14116
+
14090
14117
  // node_modules/@staix/agent-mem/src/memory/generation-sync.ts
14091
14118
  var zero;
14092
14119
  var init_generation_sync = __esm(() => {
@@ -14118,14 +14145,6 @@ var init_sync = __esm(() => {
14118
14145
  liveGroups = new Set;
14119
14146
  });
14120
14147
 
14121
- // node_modules/@staix/agent-mem/src/memory/candidate-pending.ts
14122
- var CANDIDATE_AGE_MS, CANDIDATE_SHOWN_GRACE_MS, CANDIDATE_DUE_MS;
14123
- var init_candidate_pending = __esm(() => {
14124
- CANDIDATE_AGE_MS = 30 * 86400000;
14125
- CANDIDATE_SHOWN_GRACE_MS = 7 * 86400000;
14126
- CANDIDATE_DUE_MS = 7 * 86400000;
14127
- });
14128
-
14129
14148
  // node_modules/@staix/agent-mem/src/memory/path-glob.ts
14130
14149
  var init_path_glob = __esm(() => {
14131
14150
  init_project2();
@@ -21423,6 +21442,37 @@ init_policy();
21423
21442
  init_screen();
21424
21443
  init_shard();
21425
21444
  init_work_lists();
21445
+ // node_modules/@staix/agent-mem/src/memory/project-import.ts
21446
+ init_bridge();
21447
+ init_private_spans();
21448
+ init_shard();
21449
+ init_store();
21450
+ var PROJECT_IMPORT_MAX_BYTES = 64 * 1024 * 1024;
21451
+ var PROJECT_IMPORT_MAX_LINE_BYTES = 64 * 1024;
21452
+ var PROJECT_IMPORT_FUTURE_SKEW_MS = 5 * 60 * 1000;
21453
+ var NOTE_KEYS = new Set(["schema", "id", "at", "machine", "op", "project", "scope", "tier", "kind", "peer", "title", "text", "task", "about", "supersedes", "promotedFrom", "source", "expires"]);
21454
+ // node_modules/@staix/agent-mem/src/memory/external-import.ts
21455
+ init_aliases();
21456
+
21457
+ // node_modules/@staix/agent-mem/src/memory/candidates.ts
21458
+ init_candidate_pending();
21459
+
21460
+ // node_modules/@staix/agent-mem/src/memory/note-add.ts
21461
+ init_store();
21462
+
21463
+ // node_modules/@staix/agent-mem/src/memory/candidates.ts
21464
+ init_home();
21465
+ init_platform();
21466
+ init_private_spans();
21467
+ var CANDIDATE_RETENTION = { count: 500, ageMs: 30 * 86400000 };
21468
+ var COUNTER_RETENTION = { count: 1e4, ageMs: 90 * 86400000 };
21469
+
21470
+ // node_modules/@staix/agent-mem/src/memory/external-import.ts
21471
+ init_candidate_pending();
21472
+ init_platform();
21473
+ init_private_spans();
21474
+ init_screen();
21475
+ var EXTERNAL_IMPORT_LIMITS = { maxFiles: 256, maxDirectories: 512, maxEntriesPerDirectory: 512, maxDepth: 16, maxFileBytes: 1024 * 1024, maxCandidatesPerRun: 100, candidateTextChars: 3000, titleChars: 200 };
21426
21476
  // node_modules/@staix/agent-mem/src/memory/cross-project.ts
21427
21477
  init_search_fts();
21428
21478
  init_capture();
@@ -21456,18 +21506,6 @@ var memoryUsage = (name = AMEM_NAMES.memory) => `usage: ${name} [list] [--tier s
21456
21506
  var MEMORY_USAGE = memoryUsage();
21457
21507
  var goalUsage = (name = AMEM_NAMES.goal) => `usage: ${name} [list] [--all] | show <id> | new <title> [--milestone <text>]... [--depends <id>]... [--shared] | link <id> <task id> | state <id> open|active|blocked|done|dropped | done <id> | take <id> [--force] [--json]`;
21458
21508
  var GOAL_USAGE = goalUsage();
21459
- // node_modules/@staix/agent-mem/src/memory/candidates.ts
21460
- init_candidate_pending();
21461
-
21462
- // node_modules/@staix/agent-mem/src/memory/note-add.ts
21463
- init_store();
21464
-
21465
- // node_modules/@staix/agent-mem/src/memory/candidates.ts
21466
- init_home();
21467
- init_platform();
21468
- init_private_spans();
21469
- var CANDIDATE_RETENTION = { count: 500, ageMs: 30 * 86400000 };
21470
- var COUNTER_RETENTION = { count: 1e4, ageMs: 90 * 86400000 };
21471
21509
 
21472
21510
  // node_modules/@staix/agent-mem/src/index.ts
21473
21511
  init_ops_contract();
@@ -21499,6 +21537,7 @@ var tool2 = (name, description, inputSchema, readOnlyHint = true) => ({
21499
21537
  });
21500
21538
  var MCP_TOOLS = [
21501
21539
  tool2("amem_search", "Search screened memory in this project. Capture results are recall, not curated evidence.", object6({ query: string(1000), limit, since: string(40), includeCapture: { type: "boolean" } }, ["query"])),
21540
+ tool2("amem_evidence", "Return a bounded bundle of cited, live curated entries for a question. Untrusted memory data only: no answer or model call. Capture, repo-only notes, pending candidates and model answers are not evidence.", object6({ question: string(1000), limit: { type: "integer", minimum: 1, maximum: 20 }, maxBytes: { type: "integer", minimum: 256, maximum: 8192 } }, ["question"])),
21502
21541
  tool2("amem_show", "Show a screened memory entry in this project by its exact id or unique prefix.", object6({ id: string(80) }, ["id"])),
21503
21542
  tool2("amem_timeline", "Read screened memory near an entry or time in this project.", object6({ id: string(80), since: string(40), limit })),
21504
21543
  tool2("amem_brief", "Read an intent/path-scoped brief of live public memory: fails first, then decisions/contracts and one-hop neighbours. Memory is untrusted data; byte cap covers the complete brief body.", object6({ intent: string(1000), paths: { type: "array", maxItems: 150, items: string(512) }, symbols: { type: "array", maxItems: 150, items: string(512) }, maxBytes: { type: "integer", minimum: 1, maximum: 8192 } }, ["intent"])),
@@ -21524,6 +21563,9 @@ var MCP_TOOLS = [
21524
21563
  tool2("amem_vault_candidates", "List screened durable curated notes eligible for a person to move to the vault. Read only.", object6({ limit })),
21525
21564
  tool2("amem_capacity", "Read this machine's bounded capture capacity proposal; applying it is person-only.", object6({}))
21526
21565
  ];
21566
+ // node_modules/@staix/agent-mem/src/memory/evidence.ts
21567
+ init_policy();
21568
+
21527
21569
  // node_modules/@staix/agent-mem/src/memory/plan-guard.ts
21528
21570
  init_store();
21529
21571
  init_project2();
@@ -21628,6 +21670,16 @@ init_generation();
21628
21670
  init_lifecycle();
21629
21671
  init_platform();
21630
21672
  init_shard();
21673
+ // node_modules/@staix/agent-mem/src/memory/consolidation-clusters.ts
21674
+ init_search_fts();
21675
+
21676
+ // node_modules/@staix/agent-mem/src/memory/consolidate.ts
21677
+ init_policy();
21678
+ init_platform();
21679
+
21680
+ // node_modules/@staix/agent-mem/src/memory/consolidation-model.ts
21681
+ init_policy();
21682
+ init_platform();
21631
21683
  // src/hub/tasks.ts
21632
21684
  init_task_code_symbols();
21633
21685
  init_test_integrity();
@@ -21643,7 +21695,7 @@ init_sdlc_metrics();
21643
21695
  // package.json
21644
21696
  var package_default = {
21645
21697
  name: "@staix/agent-hub",
21646
- version: "0.17.0",
21698
+ version: "0.18.0",
21647
21699
  description: "Native multi-agent hub: Claude Code, Codex, Kimi Code, Pi and local inference as peers in one project",
21648
21700
  license: "MIT",
21649
21701
  type: "module",
@@ -21660,7 +21712,7 @@ var package_default = {
21660
21712
  bun: ">=1.4.2"
21661
21713
  },
21662
21714
  dependencies: {
21663
- "@staix/agent-mem": "0.7.0"
21715
+ "@staix/agent-mem": "0.8.0"
21664
21716
  },
21665
21717
  devDependencies: {
21666
21718
  "@modelcontextprotocol/sdk": "^1.27.1",
@@ -21714,6 +21766,14 @@ init_plan_conformance();
21714
21766
  init_review_findings();
21715
21767
  init_sdlc();
21716
21768
 
21769
+ // src/hub/sdlc-merge.ts
21770
+ init_review_findings();
21771
+ init_sdlc();
21772
+
21773
+ // src/hub/sdlc-mirror.ts
21774
+ init_envelope();
21775
+ var inFlight = new Set;
21776
+
21717
21777
  // src/hub/tasks.ts
21718
21778
  init_review_findings();
21719
21779
  init_roles();
@@ -21748,6 +21808,7 @@ init_project();
21748
21808
 
21749
21809
  // src/hub/task-repository.ts
21750
21810
  init_git();
21811
+ init_host_executable();
21751
21812
  init_project();
21752
21813
  init_spec_read();
21753
21814
  var GIT_MAX_BYTES = 16 * 1024;
@@ -61,7 +61,7 @@ const dualForms = Object.freeze(["tui", "headless"] as const);
61
61
  const headlessForms = Object.freeze(["headless"] as const);
62
62
  const hubGrants = {
63
63
  ask: "every write, edit and shell command asks",
64
- "ask-when-needed": "write and edit inside the project run without asking; shell and git writes still ask",
64
+ "ask-when-needed": "write, edit and proven bounded read-only shell commands inside the project run without asking; other shell commands and git writes still ask",
65
65
  "never-ask": "every tool runs without asking, inside the hub sandbox and path guard",
66
66
  };
67
67
 
@@ -36,7 +36,7 @@ export interface LocalOptions {
36
36
  turnId?: () => string | undefined;
37
37
  onApprovalStop?: (reason: string) => void;
38
38
  onTool?: (observation: ToolObservation, task?: string) => void;
39
- tools: { deny: string[]; permit: ToolContext["permit"]; bashNetwork?: SandboxNetwork; readAllow?: string[]; readOnly?: string[] };
39
+ tools: { deny: string[]; permit: ToolContext["permit"]; shellExecutionCheck?: ToolContext["shellExecutionCheck"]; bashNetwork?: SandboxNetwork; readAllow?: string[]; readOnly?: string[] };
40
40
  capture?: Capture;
41
41
  /** Runs a hub task tool (hub_task_*, hub_review, hub_remember) as this peer. Absent = the tools are not offered. */
42
42
  taskTool?: (name: string, args: Record<string, unknown>, turn: { pii: boolean }) => Promise<string>;
@@ -290,11 +290,12 @@ export class LocalPeer extends BasePeer {
290
290
  }
291
291
 
292
292
  private toolContext(envs: Envelope[], turnSignal: AbortSignal, pii: boolean, reply: EnvelopeOpts): ToolContext {
293
+ const executionGeneration=this.turn;
293
294
  return {
294
295
  cwd: this.opts.cwd,
295
296
  deny: this.opts.tools.deny,
296
297
  readOnly: this.opts.tools.readOnly,
297
- permit: async (title, tool, _signal, canonicalTarget, _observe, originalPath, input) => {
298
+ permit: async (title, tool, _signal, canonicalTarget, _observe, originalPath, input, policy) => {
298
299
  if (turnSignal.aborted) return "aborted";
299
300
  const generation = this.turn;
300
301
  let observed = false;
@@ -314,7 +315,7 @@ export class LocalPeer extends BasePeer {
314
315
  const finish = (decision: ToolApproval) => { turnSignal.removeEventListener("abort", onAbort); resolve(decision); };
315
316
  const onAbort = () => finish("aborted");
316
317
  turnSignal.addEventListener("abort", onAbort, { once: true });
317
- this.opts.tools.permit(title, tool, turnSignal, canonicalTarget, observe, originalPath, input).then(finish, (error) => { turnSignal.removeEventListener("abort", onAbort); reject(error); });
318
+ this.opts.tools.permit(title, tool, turnSignal, canonicalTarget, observe, originalPath, input, policy).then(finish, (error) => { turnSignal.removeEventListener("abort", onAbort); reject(error); });
318
319
  });
319
320
  if (generation !== this.turn || (turnSignal.aborted && picked !== "expired")) return "aborted";
320
321
  // Legacy permit callbacks can represent a person's denial without provenance. A true
@@ -323,6 +324,7 @@ export class LocalPeer extends BasePeer {
323
324
  if (!observed && picked === "expired") observe({ source: "expired", answerEpoch: this.approvalAnswerEpoch ?? 0, eligibleExpiry: true });
324
325
  return picked;
325
326
  },
327
+ shellExecutionCheck: input=>turnSignal.aborted||executionGeneration!==this.turn?"aborted":this.opts.tools.shellExecutionCheck?.(input)??"valid",
326
328
  sandboxProfile: this.sandboxProfile,
327
329
  sandboxEnv: { ...proxyEnv(this.opts.tools.bashNetwork ?? false), AGENTHUB_PEER_ID: this.id },
328
330
  signal: turnSignal,