@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.
- package/CHANGELOG.md +17 -0
- package/README.md +16 -6
- package/docs/agent-notes/README.md +4 -0
- package/docs/agent-notes/adapters.md +17 -3
- package/docs/agent-notes/budget.md +4 -4
- package/docs/agent-notes/bus.md +1 -0
- package/docs/agent-notes/daemon.md +14 -9
- package/docs/agent-notes/tasks.md +9 -4
- package/docs/agent-notes/tests.md +3 -3
- package/docs/dogfood.md +9 -4
- package/docs/events.md +21 -7
- package/docs/operations.md +90 -18
- package/docs/profiles.md +1 -0
- package/docs/quickstart.md +9 -3
- package/docs/sdlc.md +17 -0
- package/docs/security.md +37 -10
- package/docs/specs/2026-09-19-agent-hub-design.md +74 -10
- package/docs/verification/2026-10-23-0.15.0.md +80 -0
- package/docs/verification/2026-10-23-daemon-responsiveness.md +44 -0
- package/docs/verification/2026-10-23-headless-claude-permissions.md +47 -0
- package/docs/verification/2026-10-23-terminal-claude-execution-root.md +41 -0
- package/docs/verified.json +29 -17
- package/package.json +1 -1
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +992 -312
- package/src/adapters/acp.ts +43 -7
- package/src/adapters/claude-channel.ts +26 -4
- package/src/adapters/claude-implementer.ts +243 -0
- package/src/adapters/claude-print-child.ts +90 -0
- package/src/adapters/claude-task-sessions.ts +80 -0
- package/src/adapters/compat.ts +4 -0
- package/src/adapters/conformance/enrollment.ts +2 -1
- package/src/adapters/conformance/fakes/acp-server.ts +3 -1
- package/src/adapters/definitions/builtin-metadata.ts +14 -2
- package/src/adapters/definitions/native-channel-state.ts +1 -1
- package/src/adapters/definitions/native-cli-entry-claude.ts +13 -8
- package/src/adapters/definitions/native-cli-entry-types.ts +2 -0
- package/src/adapters/definitions/native-factories.ts +8 -0
- package/src/adapters/kimi-wire-usage.ts +303 -0
- package/src/adapters/registry-types.ts +7 -3
- package/src/adapters/registry.ts +14 -2
- package/src/adapters/reviewer-print.ts +30 -53
- package/src/cli/console-config.ts +123 -0
- package/src/cli/console-state.ts +28 -1
- package/src/cli/console.ts +25 -1
- package/src/cli/facts-hook.ts +9 -5
- package/src/cli/help.ts +4 -3
- package/src/cli/launch.ts +10 -4
- package/src/cli/main.ts +47 -9
- package/src/cli/output.ts +28 -0
- package/src/cli/reset.ts +1 -1
- package/src/cli/settings-edit.ts +89 -0
- package/src/cli/terminal-recovery.ts +1 -6
- package/src/hub/action-enforce.ts +82 -0
- package/src/hub/action-policy.ts +22 -10
- package/src/hub/board.ts +26 -2
- package/src/hub/budget.ts +5 -0
- package/src/hub/bus.ts +5 -5
- package/src/hub/claude-launch-proof.ts +33 -0
- package/src/hub/config-apply.ts +5 -0
- package/src/hub/config-edit-host.ts +173 -0
- package/src/hub/config-edit.ts +282 -0
- package/src/hub/config-tier.ts +89 -0
- package/src/hub/control-client.ts +24 -3
- package/src/hub/daemon.ts +368 -59
- package/src/hub/deliverable-files.ts +131 -0
- package/src/hub/deliverables.ts +125 -0
- package/src/hub/delivery-journal.ts +18 -2
- package/src/hub/events.ts +3 -1
- package/src/hub/hub-tools.ts +19 -3
- package/src/hub/instances.ts +7 -4
- package/src/hub/kimi-usage.ts +144 -0
- package/src/hub/manager.ts +1 -1
- package/src/hub/peers.ts +7 -0
- package/src/hub/permission-payload.ts +15 -0
- package/src/hub/routing.ts +118 -3
- package/src/hub/settings.ts +1 -1
- package/src/hub/tasks.ts +202 -15
- package/src/hub/token-window.ts +33 -0
- package/src/hub/usage-rollup.ts +16 -6
- package/src/hub/workflow.ts +16 -0
- 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();
|