aimeat 1.32.0 → 1.32.1

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 (59) hide show
  1. package/dist/locales/en.json +19 -1
  2. package/dist/locales/fi.json +19 -1
  3. package/dist/public/css/views/profile.css +44 -0
  4. package/dist/public/css/views/secretary.css +127 -0
  5. package/dist/public/js/services/organisms.js +32 -0
  6. package/dist/public/llms-template.txt +5 -0
  7. package/dist/public/spa.html +23 -0
  8. package/dist/public/views/profile/organisms/workspace.js +206 -28
  9. package/dist/public/views/secretary.js +75 -0
  10. package/dist/src/cli/connect/tool-call.d.ts.map +1 -1
  11. package/dist/src/cli/connect/tool-call.js +72 -0
  12. package/dist/src/cli/connect/tool-call.js.map +1 -1
  13. package/dist/src/generated/api-types.d.ts +25 -0
  14. package/dist/src/generated/api-types.d.ts.map +1 -1
  15. package/dist/src/mcp/catalog/definitions.d.ts.map +1 -1
  16. package/dist/src/mcp/catalog/definitions.js +11 -1
  17. package/dist/src/mcp/catalog/definitions.js.map +1 -1
  18. package/dist/src/mcp/catalog/scopes.d.ts +1 -0
  19. package/dist/src/mcp/catalog/scopes.d.ts.map +1 -1
  20. package/dist/src/mcp/catalog/scopes.js +6 -0
  21. package/dist/src/mcp/catalog/scopes.js.map +1 -1
  22. package/dist/src/mcp/catalog/surfaces.d.ts.map +1 -1
  23. package/dist/src/mcp/catalog/surfaces.js +2 -0
  24. package/dist/src/mcp/catalog/surfaces.js.map +1 -1
  25. package/dist/src/routes/catalogue.d.ts +11 -0
  26. package/dist/src/routes/catalogue.d.ts.map +1 -1
  27. package/dist/src/routes/catalogue.js +14 -1
  28. package/dist/src/routes/catalogue.js.map +1 -1
  29. package/dist/src/routes/openrouter.d.ts +1 -0
  30. package/dist/src/routes/openrouter.d.ts.map +1 -1
  31. package/dist/src/routes/openrouter.js +15 -0
  32. package/dist/src/routes/openrouter.js.map +1 -1
  33. package/dist/src/routes/organisms.d.ts.map +1 -1
  34. package/dist/src/routes/organisms.js +4 -2
  35. package/dist/src/routes/organisms.js.map +1 -1
  36. package/dist/src/routes/portal.d.ts.map +1 -1
  37. package/dist/src/routes/portal.js +1 -0
  38. package/dist/src/routes/portal.js.map +1 -1
  39. package/dist/src/services/handbooks/agent.d.ts +4 -1
  40. package/dist/src/services/handbooks/agent.d.ts.map +1 -1
  41. package/dist/src/services/handbooks/agent.js +9 -0
  42. package/dist/src/services/handbooks/agent.js.map +1 -1
  43. package/dist/src/services/kpi-rollup.d.ts +53 -0
  44. package/dist/src/services/kpi-rollup.d.ts.map +1 -0
  45. package/dist/src/services/kpi-rollup.js +64 -0
  46. package/dist/src/services/kpi-rollup.js.map +1 -0
  47. package/dist/src/services/manifest-schema.d.ts +8 -1
  48. package/dist/src/services/manifest-schema.d.ts.map +1 -1
  49. package/dist/src/services/manifest-schema.js +48 -1
  50. package/dist/src/services/manifest-schema.js.map +1 -1
  51. package/dist/src/services/secretary.d.ts +33 -0
  52. package/dist/src/services/secretary.d.ts.map +1 -0
  53. package/dist/src/services/secretary.js +63 -0
  54. package/dist/src/services/secretary.js.map +1 -0
  55. package/dist/src/services/structure-overview.d.ts +27 -0
  56. package/dist/src/services/structure-overview.d.ts.map +1 -1
  57. package/dist/src/services/structure-overview.js +89 -2
  58. package/dist/src/services/structure-overview.js.map +1 -1
  59. package/package.json +1 -1
@@ -20,6 +20,18 @@
20
20
  * top of the space; each record is a separated card with breathing room.
21
21
  * v1.4.0 — 2026-06-23 — Accept an `initialSpace` prop: a deep-link from the organism mindmap opens
22
22
  * the workspace straight on that space's tab (instead of always the overview).
23
+ * v1.5.0 — 2026-06-23 — Measurability: render the manifest's objectives[] as an Objectives card
24
+ * (each KPI's current vs target, ✅/⚠️, computed-vs-self-reported) at the top of the workspace,
25
+ * fed by getWorkspaceOverviewFull + refreshed on live updates. Design:
26
+ * docs/internal/2026-06-23-organism-measurability-design.md (Phase G).
27
+ * v1.6.0 — 2026-06-23 — Findability pass (vaihe 1): the "What happened here" feed shows the
28
+ * document/record title instead of the raw id (instanceTitle); multi-part documents
29
+ * ("… — osa N") collapse under one expandable series row (niputus) in both the doc-space index
30
+ * and the Overview space list — pure display grouping, never touches stored sections/data.
31
+ * v1.7.0 — 2026-06-23 — Findability pass (vaihe 2): optional color tags. A small preset palette
32
+ * (ColorPicker + TAG_PALETTE → theme tokens) lets sections, documents and records be tagged with
33
+ * a color (left-rail accent). Section colors persist on the section object; per-item colors in a
34
+ * parallel meta.colors map (getAllColors/saveColors). Colors are optional and default to none.
23
35
  */
24
36
  import { h } from 'preact';
25
37
  import { useState, useEffect, useCallback, useRef } from 'preact/hooks';
@@ -57,6 +69,29 @@ import { SourcesPanel } from '/views/profile/organisms/sources-panel.js';
57
69
 
58
70
  const PRIMARY_FIELD = { goal: 'title', plan: 'approach', deliverable: 'title', resource: 'label', decision: 'summary' };
59
71
 
72
+ // Optional color tags for sections / documents / records. A small fixed palette of theme tokens
73
+ // (kept in CSS as .pj-tag-{key} → --pj-tag) so colors always match the active light/dark theme —
74
+ // never a free hex that could clash. null/absent = no color (the default, neutral).
75
+ const TAG_PALETTE = ['red', 'orange', 'yellow', 'green', 'blue', 'purple', 'gray'];
76
+
77
+ // A dot that opens an inline swatch palette; picking a color (or ∅) calls onPick(key|null) and closes.
78
+ // Self-contained so it can sit in a section head, a document row or a record row alike.
79
+ function ColorPicker({ value, onPick, title }) {
80
+ const [open, setOpen] = useState(false);
81
+ return html`
82
+ <span class="pj-cp">
83
+ <button class="pj-cp-dot ${value ? 'pj-colored pj-tag-' + value : 'pj-cp-empty'}" title=${title || (t('organisms.color') || 'Color')}
84
+ onClick=${(e) => { e.stopPropagation(); e.preventDefault(); setOpen(o => !o); }}></button>
85
+ ${open ? html`
86
+ <span class="pj-cp-pop" onMouseLeave=${() => setOpen(false)}>
87
+ <button class="pj-cp-sw pj-cp-none" title=${t('organisms.noColor') || 'No color'}
88
+ onClick=${(e) => { e.stopPropagation(); e.preventDefault(); onPick(null); setOpen(false); }}>∅</button>
89
+ ${TAG_PALETTE.map(c => html`<button key=${c} class="pj-cp-sw pj-colored pj-tag-${c}"
90
+ onClick=${(e) => { e.stopPropagation(); e.preventDefault(); onPick(c); setOpen(false); }}></button>`)}
91
+ </span>` : null}
92
+ </span>`;
93
+ }
94
+
60
95
  export function Workspace({ org, wsId, showToast, onBack, onBackToList, initialSpace }) {
61
96
  const orgId = org.id;
62
97
  // Registry name for the breadcrumb (the manifest's name can differ from the workspace's name).
@@ -111,7 +146,8 @@ export function Workspace({ org, wsId, showToast, onBack, onBackToList, initialS
111
146
  // NOT persisted: opening a workspace must show the overview, not whatever tab was last open.
112
147
  // Exception: a deep-link from the organism mindmap (initialSpace) opens straight on that space tab.
113
148
  const [tab, setTab] = useState(initialSpace ? 'space:' + initialSpace : 'overview');
114
- const [sectionsByType, setSectionsByType] = useState({}); // { typeName: [{id,name,parentId,documents:[docId]}] }
149
+ const [sectionsByType, setSectionsByType] = useState({}); // { typeName: [{id,name,parentId,documents:[docId],color?}] }
150
+ const [colorsByType, setColorsByType] = useState({}); // { typeName: { instanceId: colorKey } } — optional per-item color tags
115
151
  const [editingSec, setEditingSec] = useState(null); // section id currently being renamed inline
116
152
  const draggedDoc = useRef(null); // { type, id } of the doc being dragged
117
153
  const [showRegenerate, setShowRegenerate] = useState(false);
@@ -120,6 +156,7 @@ export function Workspace({ org, wsId, showToast, onBack, onBackToList, initialS
120
156
  const [addingInitial, setAddingInitial] = useState(null); // record being edited (null = new draft)
121
157
  const [addingId, setAddingId] = useState(null); // its id, preserved so save overwrites
122
158
  const [expandedRec, setExpandedRec] = useState({}); // { "type:id": true } — records expanded to view fields
159
+ const [expandedSeries, setExpandedSeries] = useState({}); // { "type:base": true } — a multi-part doc series expanded in the index
123
160
  const [showSpaces, setShowSpaces] = useState(false); // inline add-space form at the workspace top
124
161
  const [showFlow, setShowFlow] = useState(true); // the manifest-defined edit-flow chart (first item)
125
162
  const [share, setShare] = useState(null); // { public, spaces, docs } — lazy-loaded when Settings opens
@@ -161,14 +198,15 @@ export function Workspace({ org, wsId, showToast, onBack, onBackToList, initialS
161
198
  const load = useCallback(async () => {
162
199
  const w = await orgService.getWorkspace(orgId, wsId).catch(() => null);
163
200
  if (w && w.manifest) {
164
- const [ap, cfg, secs, act] = await Promise.all([
201
+ const [ap, cfg, secs, act, cols] = await Promise.all([
165
202
  orgService.listApprovals(orgId, 'pending').catch(() => []),
166
203
  orgService.getConfig(orgId).catch(() => ({})),
167
204
  orgService.getAllSections(orgId, wsId).catch(() => ({})),
168
205
  orgService.getWorkspaceActivity(orgId, wsId).catch(() => ({ events: [] })),
206
+ orgService.getAllColors(orgId, wsId).catch(() => ({})),
169
207
  ]);
170
208
  setApprovals(ap); setGateOn(!!(cfg?.gates?.publish?.enabled)); setSectionsByType(secs);
171
- setWsEvents(act?.events || []);
209
+ setWsEvents(act?.events || []); setColorsByType(cols);
172
210
  }
173
211
  setWs(w && w.manifest ? w : null);
174
212
  }, [orgId, wsId]);
@@ -179,15 +217,16 @@ export function Workspace({ org, wsId, showToast, onBack, onBackToList, initialS
179
217
  // loaded alongside, refreshed on live updates.
180
218
  const [wsGraph, setWsGraph] = useState(null);
181
219
  const [wsTocSeed, setWsTocSeed] = useState('');
220
+ const [wsObjectives, setWsObjectives] = useState([]); // measurability objectives + resolved KPIs
182
221
  useEffect(() => {
183
222
  let cancelled = false;
184
223
  const loadGraph = async () => {
185
- const [g, toc] = await Promise.all([
224
+ const [g, ov] = await Promise.all([
186
225
  orgService.getWorkspaceGraph(orgId, wsId).catch(() => null),
187
- orgService.getWorkspaceOverview(orgId, wsId).catch(() => ''),
226
+ orgService.getWorkspaceOverviewFull(orgId, wsId).catch(() => ({ markdown: '', objectives: [] })),
188
227
  ]);
189
228
  if (cancelled) return;
190
- setWsGraph(g); setWsTocSeed(toc || '');
229
+ setWsGraph(g); setWsTocSeed(ov.markdown || ''); setWsObjectives(ov.objectives || []);
191
230
  };
192
231
  loadGraph();
193
232
  const off = onLiveUpdate(['organisms'], loadGraph);
@@ -499,6 +538,20 @@ export function Workspace({ org, wsId, showToast, onBack, onBackToList, initialS
499
538
  setSectionsByType(s => ({ ...s, [typeName]: (s[typeName] || []).map(x => x.id === secId ? { ...x, name } : x) }));
500
539
  const commitSecName = (typeName) => { setEditingSec(null); orgService.saveSections(orgId, wsId, typeName, sectionsRef.current[typeName] || []).catch(() => {}); };
501
540
 
541
+ // ── Optional color tags. Section colors live on the section object (persisted via updateSections);
542
+ // per-document/record colors live in a parallel meta.colors map (persisted via saveColors). ──
543
+ const setSectionColor = (typeName, secId, color) =>
544
+ updateSections(typeName, (sectionsByType[typeName] || []).map(s => s.id === secId ? { ...s, color: color || undefined } : s));
545
+ const itemColor = (typeName, id) => (colorsByType[typeName] || {})[id] || null;
546
+ const setItemColor = useCallback((typeName, id, color) => {
547
+ setColorsByType(prev => {
548
+ const m = { ...(prev[typeName] || {}) };
549
+ if (color) m[id] = color; else delete m[id];
550
+ orgService.saveColors(orgId, wsId, typeName, m).catch(e => showToast((e && e.message) || 'Failed to save color'));
551
+ return { ...prev, [typeName]: m };
552
+ });
553
+ }, [orgId, wsId, showToast]);
554
+
502
555
  const publish = useCallback(async (ot, instanceId) => {
503
556
  setBusy(true);
504
557
  try {
@@ -809,6 +862,40 @@ export function Workspace({ org, wsId, showToast, onBack, onBackToList, initialS
809
862
  catch { showToast(t('organisms.copyFailed') || 'Copy failed'); }
810
863
  };
811
864
 
865
+ // Resolve an activity-feed event's instance id to a human title (document title / record primary
866
+ // field), so the feed reads "published · Techstack-matriisi" instead of "published · doc-76qchtb".
867
+ // Falls back to the id when the item isn't in the loaded set (deleted, or not readable).
868
+ const instanceTitle = (typeName, id) => {
869
+ const rec = [...objectsFor(typeName), ...draftsFor(typeName)].find(x => x.id === id);
870
+ if (!rec) return id;
871
+ return rec.title || rec.label || rec.summary || rec.name || rec[PRIMARY_FIELD[typeName]] || id;
872
+ };
873
+
874
+ // ── Series niputus: collapse multi-part documents ("Foo — osa 2", "Foo — part 3") under one
875
+ // expandable parent so a 4-part doc reads as ONE line, not four near-identical ones. PURELY a
876
+ // display grouping — it never touches the stored sections or the documents themselves. The base
877
+ // title is whatever precedes a trailing "— osa N / part N / #N"; a document with no such suffix is
878
+ // its own base (so the lead "Foo" groups with "Foo — osa 2/3"). Only bases with ≥2 members collapse.
879
+ const seriesParse = (title) => {
880
+ const m = /^(.*?)[\s—–-]+(?:osa|part|pt\.?|#)\s*(\d+)\s*$/i.exec(String(title || ''));
881
+ return (m && m[1].trim()) ? { base: m[1].trim(), part: parseInt(m[2], 10) } : { base: String(title || '').trim(), part: 0 };
882
+ };
883
+ const groupDocs = (list) => {
884
+ const map = new Map(); const order = [];
885
+ for (const d of list) {
886
+ const { base, part } = seriesParse(d.title || d.label || d.id || '');
887
+ const k = base.toLowerCase();
888
+ if (!map.has(k)) { map.set(k, { base, parts: [] }); order.push(k); }
889
+ map.get(k).parts.push({ ...d, _part: part });
890
+ }
891
+ return order.map(k => {
892
+ const g = map.get(k);
893
+ if (g.parts.length < 2) return { single: g.parts[0] };
894
+ g.parts.sort((a, b) => a._part - b._part);
895
+ return { base: g.base, parts: g.parts };
896
+ });
897
+ };
898
+
812
899
  // A document-space: left index (section tree + documents, with an Unsorted group) + a main
813
900
  // area showing the active document (view/edit). Sections nest via parentId; documents are
814
901
  // tied to a section's documents[] (or unsorted). Edits to the tree persist immediately.
@@ -822,11 +909,12 @@ export function Workspace({ org, wsId, showToast, onBack, onBackToList, initialS
822
909
  const isActive = (d) => activeDoc?.type === ot.name && activeDoc.page?.id === d.id;
823
910
 
824
911
  const docItem = (d) => html`
825
- <div class="pj-doc-item ${isActive(d) ? 'active' : ''}" key=${'di' + d.id}
912
+ <div class="pj-doc-item ${isActive(d) ? 'active' : ''} ${itemColor(ot.name, d.id) ? 'pj-colored pj-tag-' + itemColor(ot.name, d.id) : ''}" key=${'di' + d.id}
826
913
  draggable=${true}
827
914
  onDragStart=${(e) => { draggedDoc.current = { type: ot.name, id: d.id }; if (e.dataTransfer) { e.dataTransfer.effectAllowed = 'move'; try { e.dataTransfer.setData('text/plain', d.id); } catch (x) { /* noop */ } } }}
828
915
  onDragEnd=${() => { draggedDoc.current = null; }}>
829
916
  <span class="pj-grip" title=${t('organisms.dragHint') || 'Drag into a section'}>⠿</span>
917
+ <${ColorPicker} value=${itemColor(ot.name, d.id)} onPick=${(c) => setItemColor(ot.name, d.id, c)} />
830
918
  <button class="pj-doc-link" onClick=${() => setActiveDoc({ type: ot.name, mode: 'view', page: d })}>
831
919
  ${d._draft ? html`<span class="badge badge-warn pj-mini">${t('organisms.draft') || 'draft'}</span> ` : ''}${d.title || d.id}
832
920
  </button>
@@ -837,9 +925,28 @@ export function Workspace({ org, wsId, showToast, onBack, onBackToList, initialS
837
925
  const dropOn = (secId) => (e) => { e.preventDefault(); e.stopPropagation(); if (draggedDoc.current?.type === ot.name) { moveDocToSection(ot.name, draggedDoc.current.id, secId); draggedDoc.current = null; } };
838
926
  const allowDrop = (e) => { e.preventDefault(); if (e.dataTransfer) e.dataTransfer.dropEffect = 'move'; };
839
927
 
928
+ // Render a document list with multi-part series collapsed (see groupDocs). A series auto-opens
929
+ // when the active document is one of its parts; otherwise it toggles on the header click.
930
+ const renderDocList = (list) => groupDocs(list).map((g) => {
931
+ if (g.single) return docItem(g.single);
932
+ const key = ot.name + ':' + g.base;
933
+ const open = g.parts.some(isActive) || !!expandedSeries[key];
934
+ return html`
935
+ <div class="pj-doc-series ${open ? 'open' : ''}" key=${'ser-' + g.base}>
936
+ <button class="pj-doc-series-head" onClick=${() => setExpandedSeries(s => ({ ...s, [key]: !open }))}>
937
+ <span class="pj-ov-chevron">${open ? '▾' : '▸'}</span>
938
+ <span class="pj-doc-series-name">${g.base}</span>
939
+ <span class="pj-org-tab-count">${g.parts.length}</span>
940
+ ${g.parts.some(p => p._draft) ? html`<span class="badge badge-warn pj-mini">${t('organisms.draft') || 'draft'}</span>` : null}
941
+ </button>
942
+ ${open ? html`<div class="pj-doc-series-parts">${g.parts.map(docItem)}</div>` : null}
943
+ </div>`;
944
+ });
945
+
840
946
  const renderSection = (sec) => html`
841
- <div class="pj-sec" key=${sec.id} onDragOver=${allowDrop} onDrop=${dropOn(sec.id)}>
947
+ <div class="pj-sec ${sec.color ? 'pj-colored pj-tag-' + sec.color : ''}" key=${sec.id} onDragOver=${allowDrop} onDrop=${dropOn(sec.id)}>
842
948
  <div class="pj-sec-head">
949
+ <${ColorPicker} value=${sec.color} onPick=${(c) => setSectionColor(ot.name, sec.id, c)} />
843
950
  ${editingSec === sec.id
844
951
  ? html`<input class="input-field input-xs pj-sec-name" autofocus placeholder=${t('organisms.sectionName') || 'Section name'}
845
952
  value=${sec.name} onInput=${e => setSecName(ot.name, sec.id, e.target.value)}
@@ -850,7 +957,7 @@ export function Workspace({ org, wsId, showToast, onBack, onBackToList, initialS
850
957
  <button class="pj-icon-btn" title=${t('organisms.addSubsection') || 'Sub-section'} onClick=${() => addSection(ot.name, sec.id)}>⊕</button>
851
958
  <button class="pj-icon-btn" title=${t('organisms.remove') || 'Remove'} onClick=${() => removeSection(ot.name, sec.id, sec.name)}>✕</button>
852
959
  </div>
853
- ${(sec.documents || []).map(id => docById[id]).filter(Boolean).map(docItem)}
960
+ ${renderDocList((sec.documents || []).map(id => docById[id]).filter(Boolean))}
854
961
  ${childrenOf(sec.id).map(renderSection)}
855
962
  </div>`;
856
963
 
@@ -866,7 +973,7 @@ export function Workspace({ org, wsId, showToast, onBack, onBackToList, initialS
866
973
  <div class="pj-doc-index">
867
974
  ${childrenOf(null).map(renderSection)}
868
975
  ${unsorted.length > 0 ? html`
869
- <div class="pj-sec" onDragOver=${allowDrop} onDrop=${dropOn(null)}><div class="pj-sec-head"><span class="pj-sec-name pj-muted">${t('organisms.unsorted') || 'Unsorted'}</span></div>${unsorted.map(docItem)}</div>` : null}
976
+ <div class="pj-sec" onDragOver=${allowDrop} onDrop=${dropOn(null)}><div class="pj-sec-head"><span class="pj-sec-name pj-muted">${t('organisms.unsorted') || 'Unsorted'}</span></div>${renderDocList(unsorted)}</div>` : null}
870
977
  ${docs.length === 0 && secs.length === 0 ? html`<${EmptyState} text=${t('organisms.noneYet') || 'none yet'} />` : null}
871
978
  </div>
872
979
  <div class="pj-doc-main">
@@ -917,8 +1024,9 @@ export function Workspace({ org, wsId, showToast, onBack, onBackToList, initialS
917
1024
  : html`<${Spinner} />`)}
918
1025
 
919
1026
  ${draftsFor(ot.name).map((d, i) => html`
920
- <div class="pj-rec" key=${'d' + i}>
1027
+ <div class="pj-rec ${itemColor(ot.name, d.id) ? 'pj-colored pj-tag-' + itemColor(ot.name, d.id) : ''}" key=${'d' + i}>
921
1028
  <div class="pj-item pj-item-draft">
1029
+ <${ColorPicker} value=${itemColor(ot.name, d.id)} onPick=${(c) => setItemColor(ot.name, d.id, c)} />
922
1030
  <span class="badge badge-warn">${t('organisms.draft') || 'draft'}</span>
923
1031
  <button class="pj-rec-title" onClick=${() => toggleExpand(ot, d.id)}>${String(d[PRIMARY_FIELD[ot.name] || 'title'] || d.id || '')}</button>
924
1032
  <button class="btn-ghost btn-sm" onClick=${() => startEdit(ot, d)} disabled=${busy}>${t('organisms.edit') || 'Edit'}</button>
@@ -938,8 +1046,9 @@ export function Workspace({ org, wsId, showToast, onBack, onBackToList, initialS
938
1046
  ${objectsFor(ot.name).length === 0 && draftsFor(ot.name).length === 0
939
1047
  ? html`<${EmptyState} text=${t('organisms.noneYet') || 'none yet'} />`
940
1048
  : objectsFor(ot.name).map((o, i) => html`
941
- <div class="pj-rec" key=${'o' + i}>
1049
+ <div class="pj-rec ${itemColor(ot.name, o.id) ? 'pj-colored pj-tag-' + itemColor(ot.name, o.id) : ''}" key=${'o' + i}>
942
1050
  <div class="pj-item">
1051
+ <${ColorPicker} value=${itemColor(ot.name, o.id)} onPick=${(c) => setItemColor(ot.name, o.id, c)} />
943
1052
  <button class="pj-rec-title" onClick=${() => toggleExpand(ot, o.id)}>${String(o[PRIMARY_FIELD[ot.name] || 'title'] || o.summary || o.id || '')}</button>
944
1053
  ${o.status ? html`<span class="badge badge-info">${(o.status)}</span>` : null}
945
1054
  ${!draftsFor(ot.name).some(dr => dr.id === o.id) ? html`
@@ -1127,7 +1236,9 @@ export function Workspace({ org, wsId, showToast, onBack, onBackToList, initialS
1127
1236
  <button class="btn-ghost btn-sm" onClick=${() => ovAddNew(ot, docMode)}>${'+ '}${docMode ? (t('organisms.newPage') || 'New document') : (t('organisms.addDraft') || 'Add draft')}</button>
1128
1237
  </div>`;
1129
1238
  const open = ovOpen[ot.name] ?? !isMobileView();
1130
- const shown = items.slice(0, 5);
1239
+ // Multi-part documents collapse into one series row here too (see groupDocs); records pass through.
1240
+ const display = docMode ? groupDocs(items) : items.map(d => ({ single: d }));
1241
+ const shown = display.slice(0, 5);
1131
1242
  return html`
1132
1243
  <div class="pj-ov-sec" key=${ot.name}>
1133
1244
  <div class="pj-ov-sec-head" onClick=${() => setOvOpen(s => ({ ...s, [ot.name]: !open }))}>
@@ -1141,26 +1252,91 @@ export function Workspace({ org, wsId, showToast, onBack, onBackToList, initialS
1141
1252
  ${open ? html`
1142
1253
  ${spaceDesc(ot) ? html`<div class="section-desc pj-ov-desc">${spaceDesc(ot)}</div>` : null}
1143
1254
  <div class="pj-ov-items">
1144
- ${shown.map(d => docMode ? html`
1145
- <div class="pj-ov-doc" key=${d.id}>
1146
- <button class="pj-ov-item" onClick=${() => openOvDoc(ot, d)}>
1255
+ ${shown.map((g) => {
1256
+ // A collapsed multi-part series (docMode only) → one row; clicking opens the first part
1257
+ // in the space tab, where the full series index lives.
1258
+ if (g.parts) return html`
1259
+ <div class="pj-ov-doc" key=${'ser-' + g.base}>
1260
+ <button class="pj-ov-item" onClick=${() => openOvDoc(ot, g.parts[0])}>
1261
+ ${g.parts.some(p => p._draft) ? html`<span class="badge badge-warn pj-mini">${t('organisms.draft') || 'draft'}</span>` : null}
1262
+ <span class="pj-ov-item-title">${g.base}</span>
1263
+ <span class="pj-org-tab-count">${g.parts.length}</span>
1264
+ <span class="pj-ov-preview">${firstLine(g.parts[0].markdown)}</span>
1265
+ </button>
1266
+ </div>`;
1267
+ const d = g.single;
1268
+ return docMode ? html`
1269
+ <div class="pj-ov-doc" key=${d.id}>
1270
+ <button class="pj-ov-item" onClick=${() => openOvDoc(ot, d)}>
1271
+ ${d._draft ? html`<span class="badge badge-warn pj-mini">${t('organisms.draft') || 'draft'}</span>` : null}
1272
+ <span class="pj-ov-item-title">${d.title || d.id}</span>
1273
+ <span class="pj-ov-preview">${firstLine(d.markdown)}</span>
1274
+ </button>
1275
+ ${ovDoc && ovDoc.type === ot.name && ovDoc.id === d.id ? renderOvDocInline(ot, d) : null}
1276
+ </div>` : html`
1277
+ <button class="pj-ov-item" key=${d.id} onClick=${() => openOvRec(ot, d)}>
1147
1278
  ${d._draft ? html`<span class="badge badge-warn pj-mini">${t('organisms.draft') || 'draft'}</span>` : null}
1148
- <span class="pj-ov-item-title">${d.title || d.id}</span>
1149
- <span class="pj-ov-preview">${firstLine(d.markdown)}</span>
1150
- </button>
1151
- ${ovDoc && ovDoc.type === ot.name && ovDoc.id === d.id ? renderOvDocInline(ot, d) : null}
1152
- </div>` : html`
1153
- <button class="pj-ov-item" key=${d.id} onClick=${() => openOvRec(ot, d)}>
1154
- ${d._draft ? html`<span class="badge badge-warn pj-mini">${t('organisms.draft') || 'draft'}</span>` : null}
1155
- <span class="pj-ov-item-title">${String(d[PRIMARY_FIELD[ot.name] || 'title'] || d.summary || d.id || '')}</span>
1156
- ${d.status ? html`<span class="badge badge-info">${String(d.status)}</span>` : null}
1157
- </button>`)}
1158
- ${items.length > 5 ? html`
1279
+ <span class="pj-ov-item-title">${String(d[PRIMARY_FIELD[ot.name] || 'title'] || d.summary || d.id || '')}</span>
1280
+ ${d.status ? html`<span class="badge badge-info">${String(d.status)}</span>` : null}
1281
+ </button>`;
1282
+ })}
1283
+ ${display.length > 5 ? html`
1159
1284
  <button class="pj-ov-more" onClick=${() => pickTab('space:' + ot.name)}>${(t('organisms.ovShowAll') || 'Show all {n} →').replace('{n}', String(items.length))}</button>` : null}
1160
1285
  </div>` : null}
1161
1286
  </div>`;
1162
1287
  };
1163
1288
 
1289
+ // ── Measurability objectives (the manifest's objectives[] + each KPI's resolved current vs target).
1290
+ // The server (overview endpoint) already computed `current` for from:records KPIs; we only render.
1291
+ const kpiTargetText = (tg) => {
1292
+ if (!tg || typeof tg.op !== 'string') return '';
1293
+ if (tg.op === 'between' && Array.isArray(tg.values) && tg.values.length === 2) return `${tg.values[0]}–${tg.values[1]}`;
1294
+ const sym = tg.op === '<=' ? '≤' : tg.op === '>=' ? '≥' : tg.op === '==' ? '=' : tg.op;
1295
+ return typeof tg.value === 'number' ? `${sym} ${tg.value}` : '';
1296
+ };
1297
+ const kpiMeets = (cur, tg) => {
1298
+ if (cur === null || cur === undefined || !tg || typeof tg.op !== 'string') return null;
1299
+ const v = tg.value;
1300
+ switch (tg.op) {
1301
+ case '<': return typeof v === 'number' ? cur < v : null;
1302
+ case '<=': return typeof v === 'number' ? cur <= v : null;
1303
+ case '>': return typeof v === 'number' ? cur > v : null;
1304
+ case '>=': return typeof v === 'number' ? cur >= v : null;
1305
+ case '==': return typeof v === 'number' ? cur === v : null;
1306
+ case 'between': return (Array.isArray(tg.values) && tg.values.length === 2) ? (cur >= tg.values[0] && cur <= tg.values[1]) : null;
1307
+ default: return null;
1308
+ }
1309
+ };
1310
+ const renderObjectives = () => html`
1311
+ <div class="pj-obj">
1312
+ <div class="pj-obj-title">${t('organisms.objectivesTitle') || 'Objectives'}</div>
1313
+ ${wsObjectives.map((o, oi) => html`
1314
+ <div class="pj-obj-card" key=${o.id || oi}>
1315
+ <div class="pj-obj-statement">
1316
+ ${(o.statement || o.id)}
1317
+ ${o.status === 'met' ? html`<span class="badge badge-success pj-obj-status">${t('organisms.objStatusMet') || 'met'}</span>` : null}
1318
+ ${o.status === 'abandoned' ? html`<span class="badge pj-obj-status">${t('organisms.objStatusAbandoned') || 'abandoned'}</span>` : null}
1319
+ </div>
1320
+ ${o.why ? html`<div class="pj-obj-why">${(o.why)}</div>` : null}
1321
+ ${(o.kpis && o.kpis.length) ? html`
1322
+ <div class="pj-obj-kpis">
1323
+ ${o.kpis.map((k, ki) => {
1324
+ const ok = kpiMeets(k.current, k.target);
1325
+ const tgt = kpiTargetText(k.target);
1326
+ const unit = k.unit ? ` ${k.unit}` : '';
1327
+ const val = (k.current === null || k.current === undefined) ? '—' : String(k.current);
1328
+ return html`
1329
+ <div class="pj-obj-kpi ${ok === true ? 'met' : ok === false ? 'off' : ''}" key=${k.name || ki}>
1330
+ <span class="pj-obj-kpi-name">${k.name}</span>
1331
+ <span class="pj-obj-kpi-val">${val}${unit}${ok === true ? ' ✅' : ok === false ? ' ⚠️' : ''}</span>
1332
+ ${tgt ? html`<span class="pj-obj-kpi-target">${(t('organisms.kpiTarget') || 'target {t}').replace('{t}', tgt)}</span>` : null}
1333
+ ${k.computed === false ? html`<span class="pj-obj-kpi-declared" title=${t('organisms.kpiDeclaredHint') || 'Self-reported — not computed from records'}>${t('organisms.kpiDeclared') || 'self-reported'}</span>` : null}
1334
+ </div>`;
1335
+ })}
1336
+ </div>` : null}
1337
+ </div>`)}
1338
+ </div>`;
1339
+
1164
1340
  const renderOverview = () => {
1165
1341
  const recent = wsEvents.slice(0, 8);
1166
1342
  return html`
@@ -1173,7 +1349,7 @@ export function Workspace({ org, wsId, showToast, onBack, onBackToList, initialS
1173
1349
  <span class="pj-act-dot ${e.action}"></span>
1174
1350
  <span class="pj-ov-event-who">${shortActor(e.actor)}${e.agent ? html` <span class="pj-act-agent" title=${t('organisms.viaAgent') || 'via this agent'}>🤖 ${e.agent}</span>` : null}</span>
1175
1351
  <span class="pj-ov-event-act">${e.action === 'publish' ? (t('organisms.publishedVerb') || 'published') : (t('organisms.editedVerb') || 'edited')}</span>
1176
- <span class="pj-ov-event-what">${e.mode === 'document' ? '📄' : '🗂'} ${(wsT('type.' + e.type) || e.type)}${' / '}${e.instance}</span>
1352
+ <span class="pj-ov-event-what">${e.mode === 'document' ? '📄' : '🗂'} ${(wsT('type.' + e.type) || e.type)}${' / '}${instanceTitle(e.type, e.instance)}</span>
1177
1353
  <span class="pj-ov-event-time">${relTime(e.at)}</span>
1178
1354
  </button>`)}
1179
1355
  </div>` : null}
@@ -1197,6 +1373,8 @@ export function Workspace({ org, wsId, showToast, onBack, onBackToList, initialS
1197
1373
  </div>
1198
1374
  ${ws.manifest?.summary ? html`<div class="section-desc">${(ws.manifest.summary)}</div>` : null}
1199
1375
 
1376
+ ${wsObjectives.length ? renderObjectives() : null}
1377
+
1200
1378
  <${ReadmePanel} markdown=${ws.readme || ''} canEdit=${wsCanEdit} kind="workspace" name=${ws.manifest?.name || 'Workspace'}
1201
1379
  aiPromptSeed=${wsTocSeed} onSave=${saveWsReadme} />
1202
1380
 
@@ -0,0 +1,75 @@
1
+ /**
2
+ * @file secretary.js
3
+ * @description Secretary view (Phase 0 shell). Confirms the per-owner Secretary agent is provisioned
4
+ * and shows its identity + granted scopes. Reached from the header Secretary button, which appears
5
+ * once OpenRouter is configured. The full interactive Secretary (onboarding interview, brain, chat,
6
+ * feed, calendar) lands in later phases — see docs/plans/2026-06-23-secretary-feature.md.
7
+ * @structure SECRETARY_ICON (heart-shield + quill svg) · SecretaryView (default) — fetch /v1/agents,
8
+ * find the `system:secretary` agent, show provisioning status + identity + scopes.
9
+ * @usage routed at /v1/secretary by spa.html.
10
+ * @version-history v0.1.0 — 2026-06-23 — Phase 0 shell: provisioning status + identity.
11
+ */
12
+ import { h } from 'preact';
13
+ import htm from 'htm';
14
+ const html = htm.bind(h);
15
+ import { useState, useEffect } from 'preact/hooks';
16
+ import { t } from '/js/i18n.js';
17
+ import { apiGet } from '/js/api.js';
18
+ import { escHtml } from '/js/utils.js';
19
+ import { useViewCSS } from '/components/useViewCSS.js';
20
+
21
+ // Heart-shaped shield with a quill across it — the Secretary's mark.
22
+ export const SECRETARY_ICON = html`
23
+ <svg viewBox="0 0 24 24" width="40" height="40" aria-hidden="true">
24
+ <path d="M12 20.6C6.4 16.7 3.4 13.2 3.4 9.2 3.4 6.5 5.4 4.6 7.9 4.6c1.7 0 3.2.9 4.1 2.4.9-1.5 2.4-2.4 4.1-2.4 2.5 0 4.5 1.9 4.5 4.6 0 4-3 7.5-8.6 11.4z"
25
+ fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
26
+ <path d="M16 8l-5.6 5.6M10.4 13.6l-1.5 2.4 2.4-1.5"
27
+ fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
28
+ </svg>`;
29
+
30
+ export default function SecretaryView() {
31
+ useViewCSS('/css/views/secretary.css');
32
+ // undefined = loading, null = not provisioned yet, object = the Secretary agent.
33
+ const [secretary, setSecretary] = useState(undefined);
34
+
35
+ useEffect(() => {
36
+ apiGet('/v1/agents')
37
+ .then((r) => {
38
+ const agents = (r && r.data && r.data.agents) || [];
39
+ const sec = agents.find((a) => (a.tags || []).includes('system:secretary'))
40
+ || agents.find((a) => a.name === 'secretary')
41
+ || null;
42
+ setSecretary(sec);
43
+ })
44
+ .catch(() => setSecretary(null));
45
+ }, []);
46
+
47
+ return html`
48
+ <div class="sec">
49
+ <header class="sec-hero">
50
+ <span class="sec-hero-icon">${SECRETARY_ICON}</span>
51
+ <div>
52
+ <h1 class="sec-title">${t('secretary.title')}</h1>
53
+ <p class="sec-desc">${t('secretary.desc')}</p>
54
+ </div>
55
+ </header>
56
+
57
+ ${secretary === undefined
58
+ ? html`<div class="sec-empty">…</div>`
59
+ : secretary === null
60
+ ? html`<div class="sec-empty">${t('secretary.notReady')}</div>`
61
+ : html`
62
+ <section class="sec-card">
63
+ <div class="sec-status"><span class="sec-dot"></span> ${t('secretary.ready')}</div>
64
+ <dl class="sec-meta">
65
+ <dt>${t('secretary.identity')}</dt>
66
+ <dd><code>${escHtml(secretary.gaii)}</code></dd>
67
+ <dt>${t('secretary.scopes')}</dt>
68
+ <dd class="sec-scopes">
69
+ ${(secretary.default_scopes || []).map((s) => html`<span class="sec-scope" key=${s}>${s}</span>`)}
70
+ </dd>
71
+ </dl>
72
+ <p class="sec-soon">${t('secretary.soon')}</p>
73
+ </section>`}
74
+ </div>`;
75
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"tool-call.d.ts","sourceRoot":"","sources":["../../../../src/cli/connect/tool-call.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAIH,OAAO,EAA0D,KAAK,cAAc,EAAE,MAAM,kCAAkC,CAAC;AAC/H,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEjE,OAAO,EAA+B,KAAK,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAEpF,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE1C,UAAU,wBAAwB;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IACvC,OAAO,EAAE,CAAC,GAAG,EAAE,eAAe,EAAE,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC;CAC9E;AAED,UAAU,eAAe;IACrB,MAAM,EAAE,YAAY,CAAC;IACrB,MAAM,EAAE,mBAAmB,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;CACrB;AAkHD,eAAO,MAAM,iBAAiB,EAAE,wBAAwB,EA4kCvD,CAAC;AAyDF,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAS/D;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAahE;AAED,wBAAsB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAyD5G"}
1
+ {"version":3,"file":"tool-call.d.ts","sourceRoot":"","sources":["../../../../src/cli/connect/tool-call.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAIH,OAAO,EAA0D,KAAK,cAAc,EAAE,MAAM,kCAAkC,CAAC;AAC/H,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEjE,OAAO,EAA+B,KAAK,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAEpF,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE1C,UAAU,wBAAwB;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IACvC,OAAO,EAAE,CAAC,GAAG,EAAE,eAAe,EAAE,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC;CAC9E;AAED,UAAU,eAAe;IACrB,MAAM,EAAE,YAAY,CAAC;IACrB,MAAM,EAAE,mBAAmB,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;CACrB;AAkHD,eAAO,MAAM,iBAAiB,EAAE,wBAAwB,EAioCvD,CAAC;AAyDF,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAS/D;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAahE;AAED,wBAAsB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAyD5G"}
@@ -353,6 +353,29 @@ export const CONNECT_CLI_TOOLS = [
353
353
  return client.post('/v1/messages', body);
354
354
  },
355
355
  },
356
+ {
357
+ name: 'aimeat_dm_ask',
358
+ handler: ({ client }, input) => {
359
+ const questions = optionalArray(input, 'questions');
360
+ if (!questions)
361
+ throw new Error('Missing required array field: questions');
362
+ const submitLabel = optionalString(input, 'submit_label');
363
+ const body = {
364
+ to: requiredString(input, 'to'),
365
+ interactive: { role: 'questions', v: 1, questions, ...(submitLabel ? { submitLabel } : {}) },
366
+ };
367
+ const intro = optionalString(input, 'body');
368
+ if (intro)
369
+ body.body = intro;
370
+ const subject = optionalString(input, 'subject');
371
+ if (subject)
372
+ body.subject = subject;
373
+ const conversationId = optionalString(input, 'conversation_id');
374
+ if (conversationId)
375
+ body.conversation_id = conversationId;
376
+ return client.post('/v1/messages', body);
377
+ },
378
+ },
356
379
  {
357
380
  name: 'aimeat_dm_inbox',
358
381
  description: 'Read recent federated DMs addressed to this agent (replies + messages people sent you), newest first.',
@@ -520,6 +543,22 @@ export const CONNECT_CLI_TOOLS = [
520
543
  name: 'aimeat_catalogue_search',
521
544
  handler: ({ client }, input) => client.get(`/v1/catalogue${query({ q: optionalString(input, 'query') })}`),
522
545
  },
546
+ {
547
+ name: 'aimeat_discover',
548
+ handler: ({ client }, input) => {
549
+ const q = query({
550
+ q: optionalString(input, 'q'),
551
+ type: optionalString(input, 'type'),
552
+ tags: optionalString(input, 'tags'),
553
+ segment: optionalString(input, 'segment'),
554
+ scope: optionalString(input, 'scope'),
555
+ per_page: optionalNumber(input, 'limit'),
556
+ });
557
+ // mode=map → facet counts only; mode=find (default) → ranked entries.
558
+ const path = optionalString(input, 'mode') === 'map' ? '/v1/discover/facets' : '/v1/discover';
559
+ return client.get(`${path}${q}`);
560
+ },
561
+ },
523
562
  {
524
563
  name: 'aimeat_agent_profile',
525
564
  handler: ({ client }, input) => client.get(`/v1/agents/${encodeURIComponent(requiredString(input, 'gaii'))}`),
@@ -800,6 +839,35 @@ export const CONNECT_CLI_TOOLS = [
800
839
  name: 'aimeat_organism_get',
801
840
  handler: ({ client }, input) => client.get(`/v1/organisms/${encodeURIComponent(requiredString(input, 'id'))}`),
802
841
  },
842
+ {
843
+ name: 'aimeat_organism_overview',
844
+ handler: ({ client }, input) => client.get(`/v1/organisms/${encodeURIComponent(requiredString(input, 'organism_id'))}/overview`),
845
+ },
846
+ {
847
+ name: 'aimeat_organism_update',
848
+ handler: ({ client }, input) => {
849
+ const body = {};
850
+ const name = optionalString(input, 'name');
851
+ if (name !== undefined)
852
+ body.name = name;
853
+ const description = optionalString(input, 'description');
854
+ if (description !== undefined)
855
+ body.description = description;
856
+ const readme = optionalString(input, 'readme');
857
+ if (readme !== undefined)
858
+ body.readme = readme;
859
+ const interests = optionalArray(input, 'interests');
860
+ if (interests)
861
+ body.interests = interests;
862
+ const joinPolicy = optionalString(input, 'join_policy');
863
+ if (joinPolicy)
864
+ body.join_policy = joinPolicy;
865
+ const visibility = optionalString(input, 'visibility');
866
+ if (visibility)
867
+ body.visibility = visibility;
868
+ return client.put(`/v1/organisms/${encodeURIComponent(requiredString(input, 'organism_id'))}`, body);
869
+ },
870
+ },
803
871
  {
804
872
  name: 'aimeat_organism_join',
805
873
  handler: ({ client }, input) => client.post(`/v1/organisms/${encodeURIComponent(requiredString(input, 'id'))}/join`),
@@ -938,6 +1006,10 @@ export const CONNECT_CLI_TOOLS = [
938
1006
  name: 'aimeat_workspace_read',
939
1007
  handler: ({ client }, input) => client.get(`/v1/organisms/${encodeURIComponent(requiredString(input, 'organism_id'))}/workspace${query({ ws: requiredString(input, 'ws') })}`),
940
1008
  },
1009
+ {
1010
+ name: 'aimeat_workspace_overview',
1011
+ handler: ({ client }, input) => client.get(`/v1/organisms/${encodeURIComponent(requiredString(input, 'organism_id'))}/workspace/overview${query({ ws: requiredString(input, 'ws') })}`),
1012
+ },
941
1013
  {
942
1014
  name: 'aimeat_workspace_write',
943
1015
  handler: async ({ client }, input) => {