@staix/agent-hub 0.14.0 → 0.15.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 (82) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +16 -6
  3. package/docs/agent-notes/README.md +4 -0
  4. package/docs/agent-notes/adapters.md +17 -3
  5. package/docs/agent-notes/budget.md +4 -4
  6. package/docs/agent-notes/bus.md +1 -0
  7. package/docs/agent-notes/daemon.md +14 -9
  8. package/docs/agent-notes/tasks.md +9 -4
  9. package/docs/agent-notes/tests.md +3 -3
  10. package/docs/dogfood.md +9 -4
  11. package/docs/events.md +21 -7
  12. package/docs/operations.md +90 -18
  13. package/docs/profiles.md +1 -0
  14. package/docs/quickstart.md +9 -3
  15. package/docs/sdlc.md +17 -0
  16. package/docs/security.md +37 -10
  17. package/docs/specs/2026-09-19-agent-hub-design.md +74 -10
  18. package/docs/verification/2026-10-23-0.15.0.md +80 -0
  19. package/docs/verification/2026-10-23-daemon-responsiveness.md +44 -0
  20. package/docs/verification/2026-10-23-headless-claude-permissions.md +47 -0
  21. package/docs/verification/2026-10-23-terminal-claude-execution-root.md +41 -0
  22. package/docs/verified.json +29 -17
  23. package/package.json +1 -1
  24. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  25. package/plugins/agent-hub/server.js +992 -312
  26. package/src/adapters/acp.ts +43 -7
  27. package/src/adapters/claude-channel.ts +26 -4
  28. package/src/adapters/claude-implementer.ts +243 -0
  29. package/src/adapters/claude-print-child.ts +90 -0
  30. package/src/adapters/claude-task-sessions.ts +80 -0
  31. package/src/adapters/compat.ts +4 -0
  32. package/src/adapters/conformance/enrollment.ts +2 -1
  33. package/src/adapters/conformance/fakes/acp-server.ts +3 -1
  34. package/src/adapters/definitions/builtin-metadata.ts +14 -2
  35. package/src/adapters/definitions/native-channel-state.ts +1 -1
  36. package/src/adapters/definitions/native-cli-entry-claude.ts +13 -8
  37. package/src/adapters/definitions/native-cli-entry-types.ts +2 -0
  38. package/src/adapters/definitions/native-factories.ts +8 -0
  39. package/src/adapters/kimi-wire-usage.ts +303 -0
  40. package/src/adapters/registry-types.ts +7 -3
  41. package/src/adapters/registry.ts +14 -2
  42. package/src/adapters/reviewer-print.ts +30 -53
  43. package/src/cli/console-config.ts +123 -0
  44. package/src/cli/console-state.ts +28 -1
  45. package/src/cli/console.ts +25 -1
  46. package/src/cli/facts-hook.ts +9 -5
  47. package/src/cli/help.ts +4 -3
  48. package/src/cli/launch.ts +10 -4
  49. package/src/cli/main.ts +47 -9
  50. package/src/cli/output.ts +28 -0
  51. package/src/cli/reset.ts +1 -1
  52. package/src/cli/settings-edit.ts +89 -0
  53. package/src/cli/terminal-recovery.ts +1 -6
  54. package/src/hub/action-enforce.ts +82 -0
  55. package/src/hub/action-policy.ts +22 -10
  56. package/src/hub/board.ts +26 -2
  57. package/src/hub/budget.ts +5 -0
  58. package/src/hub/bus.ts +5 -5
  59. package/src/hub/claude-launch-proof.ts +33 -0
  60. package/src/hub/config-apply.ts +5 -0
  61. package/src/hub/config-edit-host.ts +173 -0
  62. package/src/hub/config-edit.ts +282 -0
  63. package/src/hub/config-tier.ts +89 -0
  64. package/src/hub/control-client.ts +24 -3
  65. package/src/hub/daemon.ts +368 -59
  66. package/src/hub/deliverable-files.ts +131 -0
  67. package/src/hub/deliverables.ts +125 -0
  68. package/src/hub/delivery-journal.ts +18 -2
  69. package/src/hub/events.ts +3 -1
  70. package/src/hub/hub-tools.ts +19 -3
  71. package/src/hub/instances.ts +7 -4
  72. package/src/hub/kimi-usage.ts +144 -0
  73. package/src/hub/manager.ts +1 -1
  74. package/src/hub/peers.ts +7 -0
  75. package/src/hub/permission-payload.ts +15 -0
  76. package/src/hub/routing.ts +118 -3
  77. package/src/hub/settings.ts +1 -1
  78. package/src/hub/tasks.ts +202 -15
  79. package/src/hub/token-window.ts +33 -0
  80. package/src/hub/usage-rollup.ts +16 -6
  81. package/src/hub/workflow.ts +16 -0
  82. package/src/ui/index.html +115 -1
package/src/ui/index.html CHANGED
@@ -201,6 +201,15 @@ details.settings-group{border-bottom:1px solid var(--line)}
201
201
  details.settings-group>summary{background:var(--panel-soft)}
202
202
  details.settings-group .row{border-bottom:1px dashed var(--line)}
203
203
  details.settings-group .row:last-child{border-bottom:0}
204
+ /* Structure (#689): instances, lifecycle and the executable keys. Tier 3 is text only: no control, a command to run in the console. */
205
+ .structure-group .row{padding:var(--space-3) var(--space-4)}
206
+ .structure-instance,.structure-sdlc,.structure-add,.structure-backups{border-bottom:1px dashed var(--line)}
207
+ .structure-tier3{background:var(--panel-soft);color:var(--ink);border:1px solid var(--line);border-radius:var(--radius-surface);padding:var(--space-3) var(--space-4);margin:var(--space-3) var(--space-4)}
208
+ .structure-tier3 code{display:block;overflow-wrap:anywhere;color:var(--muted)}
209
+ .structure-group button,.structure-group input,.structure-group select{min-height:44px}
210
+ .structure-field{display:grid;grid-template-columns:minmax(0,12rem) minmax(0,1fr);gap:var(--space-2);align-items:center;margin:var(--space-2) 0}
211
+ .structure-diff{white-space:pre-wrap;overflow-wrap:anywhere;background:var(--panel-soft);color:var(--ink);border:1px solid var(--line);border-radius:var(--radius-control);padding:var(--space-2) var(--space-3);margin:var(--space-2) 0}
212
+ .structure-error{color:var(--red);background:var(--red-soft);border:1px solid var(--red);border-radius:var(--radius-control);padding:var(--space-2) var(--space-3);margin:var(--space-2) 0}
204
213
  /* Peer colours: the registry's hue, one variant per theme, generated by peer-palette.ts */
205
214
  /* PEER_PALETTE_CSS */
206
215
 
@@ -233,7 +242,7 @@ details.settings-group .row:last-child{border-bottom:0}
233
242
  .board-unknown{padding:var(--space-3) var(--space-4);color:var(--amber)}
234
243
  @media(max-width:1100px){.board-columns{grid-template-columns:repeat(2,minmax(0,1fr))}}
235
244
  @media(max-width:900px){.stack{gap:var(--space-4)}.stream{height:300px}footer{padding:var(--space-4)}}
236
- @media(max-width:600px){.split{grid-template-columns:minmax(0,1fr)}.board-columns{grid-template-columns:1fr;padding:var(--space-3)}.board-toolbar{align-items:stretch}.board-toolbar select{max-width:none;width:100%}.board-toolbar label{width:100%}}
245
+ @media(max-width:600px){.structure-field{grid-template-columns:minmax(0,1fr)}.structure-tier3{margin:var(--space-3)}.split{grid-template-columns:minmax(0,1fr)}.board-columns{grid-template-columns:1fr;padding:var(--space-3)}.board-toolbar{align-items:stretch}.board-toolbar select{max-width:none;width:100%}.board-toolbar label{width:100%}}
237
246
  /* Reduced motion (#469 R6): one rule for the whole page. Every transition and animation is off, whatever rule declares it. */
238
247
  @media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important;scroll-behavior:auto!important}}
239
248
 
@@ -863,6 +872,105 @@ function settingRow(row, raise) {
863
872
  actions.append(save); if (row.type === 'roles') { const inherit = el('button', 'Use profile/shared roles'); inherit.type = 'button'; inherit.dataset.mutation = 'project'; inherit.disabled = managerMode && !snapshotValid; inherit.setAttribute('aria-label', 'Inherit ' + row.label); inherit.addEventListener('click', async () => { out.textContent = 'Restoring inherited role guidance...'; const accepted = await action({action:'setting',key:row.key,value:null}, inherit); out.textContent = accepted ? 'Inherited selection saved. Applies at the next peer start.' : $('notice').textContent; }); actions.append(inherit); } node.append(actions, out);
864
873
  return node;
865
874
  }
875
+ /* Structure (#689): tier 2 edits inside a settings session; tier 3 keys are text only, with the console command that changes them. */
876
+ const STRUCTURE_FIELDS = [['kind', 'text'], ['roles', 'text'], ['start_mode', 'text'], ['review_mode', 'text'], ['worktree', 'text'], ['permission_mode', 'text'], ['readOnly', 'text']];
877
+ const structureState = {revision: {}, lines: [], error: '', field: '', tier: 0, preview: null};
878
+ const structureList = (text) => String(text).split(',').map((item) => item.trim()).filter(Boolean);
879
+ function structureField(form, name, label, placeholder) {
880
+ const row = el('div', undefined, 'structure-field'), input = el('input'), id = 'structure-' + name;
881
+ const title = el('label', label, 'meta'); title.htmlFor = id; input.id = id; input.type = 'text'; input.placeholder = placeholder || ''; input.setAttribute('aria-label', label);
882
+ row.append(title, input); form.append(row); return input;
883
+ }
884
+ function structureResult(box, out) {
885
+ box.replaceChildren();
886
+ if (structureState.error) box.append(el('p', structureState.error + (structureState.field ? ' (' + structureState.field + ')' : ''), 'structure-error'));
887
+ if (structureState.lines.length) box.append(el('pre', structureState.lines.join('\n'), 'structure-diff'));
888
+ if (out) out(box);
889
+ }
890
+ function structureForm(cls, title, build, file, edit, raise) {
891
+ const form = el('div', undefined, cls), head = el('p', title, 'label'), fields = {}, result = el('div');
892
+ form.append(head);
893
+ build(form, fields);
894
+ const preview = el('button', 'Preview change', 'primary'); preview.type = 'button'; preview.dataset.mutation = 'project';
895
+ const reason = structureField(form, 'reason', 'Reason (recorded with the change)', 'why this change');
896
+ const actions = el('div', undefined, 'actions'); actions.append(preview);
897
+ form.append(actions, result);
898
+ const body = () => { const set = fields.set(); return set ? {file, revision: edit.revisions[file], set} : null; };
899
+ preview.addEventListener('click', async () => {
900
+ structureState.error = ''; structureState.field = ''; structureState.lines = [];
901
+ const request = body();
902
+ if (!request) { if (!structureState.error) structureState.error = 'Give a mode or the required checks.'; structureResult(result); return; }
903
+ try {
904
+ const reply = await post('/action', {action:'config_edit_preview', edit: request});
905
+ if (reply.ok === false) { structureState.error = reply.error || 'The hub refused this change.'; structureState.field = reply.field || ''; }
906
+ else { structureState.lines = (reply.lines || []).map(String); structureState.tier = reply.tier || 2; }
907
+ } catch (error) { structureState.error = error.message; }
908
+ structureResult(result, (box) => {
909
+ if (structureState.error || structureState.tier === 3) { if (!structureState.error) box.append(el('p', 'This key runs code or locates secrets: change it in the console, never here.', 'structure-error')); return; }
910
+ const write = el('button', 'Write change', 'primary'); write.type = 'button'; write.dataset.mutation = 'project';
911
+ write.addEventListener('click', async () => {
912
+ if (!reason.value.trim()) { notice('Give a reason before writing.', true); return; }
913
+ const done = await action({action:'config_edit_apply', edit: request, reason: reason.value.trim()}, write);
914
+ if (done) { structureState.lines = []; structureResult(result); }
915
+ });
916
+ box.append(write);
917
+ });
918
+ });
919
+ return form;
920
+ }
921
+ function renderStructure(target, edit, raise) {
922
+ const group = el('div', undefined, 'structure-group'), head = el('div', undefined, 'row-head');
923
+ head.append(el('span', 'Structure', 'label'));
924
+ if (edit.sdlc && edit.sdlc.pending) head.append(badge('restart pending', 'warn'));
925
+ group.append(head);
926
+ if (!raise) group.append(el('p', 'Read only here. Open a settings session with ahub ui --settings to change instances, review, snapshots, approvals and the lifecycle.', 'meta'));
927
+ for (const instance of edit.instances || []) {
928
+ const row = el('div', undefined, 'structure-instance'), line = Object.entries(instance.fields).map(([key, value]) => key + ': ' + (Array.isArray(value) ? value.join(', ') : String(value))).join(' / ');
929
+ row.append(el('p', instance.id, 'label'), el('p', line || 'no tier 2 fields set', 'meta'));
930
+ if (instance.tier3.length) row.append(el('p', instance.tier3.join(', ') + ': set in the console', 'meta'));
931
+ group.append(row);
932
+ }
933
+ for (const block of edit.blocks || []) group.append(el('p', block.path + ': ' + settingText(block.value), 'meta'));
934
+ const lifecycle = el('div', undefined, 'structure-sdlc');
935
+ lifecycle.append(el('p', 'Lifecycle (sdlc.toml): ' + edit.sdlc.mode + '; required checks: ' + (edit.sdlc.requiredChecks.length ? edit.sdlc.requiredChecks.join(', ') : 'none'), 'meta'));
936
+ group.append(lifecycle);
937
+ if (raise) {
938
+ lifecycle.append(structureForm('structure-sdlc-form', 'Change the lifecycle', (form, fields) => {
939
+ const mode = structureField(form, 'sdlc-mode', 'sdlc mode (off, advise, enforce)', edit.sdlc.mode), checks = structureField(form, 'sdlc-checks', 'required checks, comma list', edit.sdlc.requiredChecks.join(', '));
940
+ fields.set = () => { const set = []; if (mode.value.trim()) set.push({path:['mode'], value: mode.value.trim()}); if (checks.value.trim()) set.push({path:['required_checks'], value: structureList(checks.value)}); return set.length ? set : null; };
941
+ }, 'sdlc.toml', edit, raise));
942
+ const add = structureForm('structure-add', 'Add an instance, or change one field of an existing one (type clear to remove a field)', (form, fields) => {
943
+ const id = structureField(form, 'id', 'instance id, <kind>-<name>', 'codex-review');
944
+ const inputs = {}; for (const [name] of STRUCTURE_FIELDS) inputs[name] = structureField(form, name, name + (name === 'roles' ? ' (comma list)' : name === 'readOnly' ? ' (true or false)' : ''), '');
945
+ const convert = (name, text) => name === 'roles' ? structureList(text) : name === 'readOnly' ? text === 'true' : text;
946
+ fields.set = () => {
947
+ const name = id.value.trim(), known = (edit.known || []).includes(name), typed = [];
948
+ for (const [field] of STRUCTURE_FIELDS) { const text = inputs[field].value.trim(); if (text) typed.push([field, text]); }
949
+ if (!name) { structureState.error = 'An instance needs an id and a kind.'; return null; }
950
+ // readOnly is exactly true or false: anything else is the person's typo, never a silent false.
951
+ if (typed.some(([field, text]) => field === 'readOnly' && text !== 'true' && text !== 'false' && text !== 'clear')) { structureState.error = 'readOnly is true or false.'; return null; }
952
+ if (known) {
953
+ // An existing instance changes leaf by leaf: a field left blank keeps its value, and a tier 3 field it holds is never part of the request.
954
+ if (!typed.length) { structureState.error = 'Type the field to change, or clear to remove it.'; return null; }
955
+ return typed.map(([field, text]) => ({path:['instances', name, field], value: text === 'clear' ? null : convert(field, text)}));
956
+ }
957
+ const value = {}; for (const [field, text] of typed) { if (text !== 'clear') value[field] = convert(field, text); }
958
+ if (!value.kind) { structureState.error = 'An instance needs an id and a kind.'; return null; }
959
+ return [{path:['instances', name], value}];
960
+ };
961
+ }, 'config.local.json', edit, raise);
962
+ group.append(add);
963
+ }
964
+ const tier3 = el('div', undefined, 'structure-tier3');
965
+ tier3.append(el('p', 'Executable and secret-locating keys', 'label'), el('p', "Changing one runs code with the daemon's rights or chooses where secrets are read from. It is never done from a browser: use the console command.", 'meta'));
966
+ for (const item of edit.tier3 || []) { tier3.append(el('p', item.key + (item.set ? ' (set)' : ' (not set)'), 'meta'), el('code', item.command)); }
967
+ group.append(tier3);
968
+ const backups = el('div', undefined, 'structure-backups');
969
+ backups.append(el('p', 'Backups (the last ' + (edit.backups || []).length + ')', 'label'));
970
+ for (const item of edit.backups || []) backups.append(el('p', new Date(item.at).toISOString() + ' ' + item.file + ' ' + item.bytes + ' bytes', 'meta'));
971
+ group.append(backups);
972
+ target.append(group);
973
+ }
866
974
  const settingSlots = new Map();
867
975
  function setText(node, text) { if (node.textContent !== text) node.textContent = text; }
868
976
  function renderSettings(settings, roster = []) {
@@ -876,6 +984,7 @@ function renderSettings(settings, roster = []) {
876
984
  settingSlots.clear();
877
985
  const reload = el('div', undefined, 'settings-reload'); settingSlots.set('$reload', { node: reload, signature: null }); target.append(reload);
878
986
  const undo = el('div'); settingSlots.set('', { node: undo, signature: null }); target.append(undo);
987
+ const structure = el('div'); settingSlots.set('$structure', { node: structure, signature: null }); target.append(structure);
879
988
  for (const group of SETTING_GROUPS) {
880
989
  const rows = settings.rows.filter((row) => row.group === group);
881
990
  if (!rows.length) continue;
@@ -903,6 +1012,11 @@ function renderSettings(settings, roster = []) {
903
1012
  }
904
1013
  }
905
1014
  }
1015
+ const structureSlot = settingSlots.get('$structure'), structureSignature = JSON.stringify([settings.configEdit, raise]);
1016
+ if (structureSlot && structureSlot.signature !== structureSignature) {
1017
+ structureSlot.signature = structureSignature; structureSlot.node.replaceChildren();
1018
+ if (settings.configEdit && Array.isArray(settings.configEdit.instances)) renderStructure(structureSlot.node, settings.configEdit, raise);
1019
+ }
906
1020
  const undoSlot = settingSlots.get(''), undoKey = settings.undo || '';
907
1021
  if (undoSlot && undoSlot.signature !== undoKey) {
908
1022
  undoSlot.signature = undoKey; undoSlot.node.replaceChildren();