fraim-framework 2.0.165 → 2.0.167

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 (55) hide show
  1. package/dist/src/ai-hub/catalog.js +20 -27
  2. package/dist/src/ai-hub/server.js +418 -2
  3. package/dist/src/ai-hub/word-sideload.js +95 -0
  4. package/dist/src/cli/commands/org.js +40 -0
  5. package/dist/src/cli/commands/test-mcp.js +171 -0
  6. package/dist/src/cli/fraim.js +2 -0
  7. package/dist/src/cli/setup/first-run.js +242 -0
  8. package/dist/src/cli/utils/org-publish.js +98 -0
  9. package/dist/src/config/ai-manager-hiring.js +121 -0
  10. package/dist/src/config/compat.js +16 -0
  11. package/dist/src/config/feature-flags.js +25 -0
  12. package/dist/src/config/persona-capability-bundles.js +273 -0
  13. package/dist/src/config/persona-hiring.js +270 -0
  14. package/dist/src/config/portfolio-slug-overrides.js +17 -0
  15. package/dist/src/config/pricing.js +37 -0
  16. package/dist/src/config/stripe.js +43 -0
  17. package/dist/src/core/config-loader.js +9 -5
  18. package/dist/src/core/config-writer.js +75 -0
  19. package/dist/src/core/fraim-config-schema.generated.js +0 -21
  20. package/dist/src/core/utils/job-aliases.js +47 -0
  21. package/dist/src/core/utils/local-registry-resolver.js +8 -1
  22. package/dist/src/core/utils/workflow-parser.js +174 -0
  23. package/index.js +1 -1
  24. package/package.json +5 -1
  25. package/public/ai-hub/index.html +81 -0
  26. package/public/ai-hub/powerpoint-taskpane/index.html +236 -236
  27. package/public/ai-hub/powerpoint-taskpane/manifest.xml +29 -29
  28. package/public/ai-hub/review.css +13 -0
  29. package/public/ai-hub/script.js +414 -4
  30. package/public/ai-hub/styles.css +56 -0
  31. package/public/first-run/styles.css +73 -73
  32. package/public/portfolio/ashley.html +523 -0
  33. package/public/portfolio/auditya.html +83 -0
  34. package/public/portfolio/banke.html +83 -0
  35. package/public/portfolio/beza.html +659 -0
  36. package/public/portfolio/careena.html +632 -0
  37. package/public/portfolio/casey.html +568 -0
  38. package/public/portfolio/celia.html +490 -0
  39. package/public/portfolio/deidre.html +642 -0
  40. package/public/portfolio/gautam.html +597 -0
  41. package/public/portfolio/hari.html +469 -0
  42. package/public/portfolio/huxley.html +1354 -0
  43. package/public/portfolio/index.html +741 -0
  44. package/public/portfolio/maestro.html +518 -0
  45. package/public/portfolio/mandy.html +590 -0
  46. package/public/portfolio/mona.html +597 -0
  47. package/public/portfolio/pam.html +887 -0
  48. package/public/portfolio/procella.html +107 -0
  49. package/public/portfolio/qasm.html +569 -0
  50. package/public/portfolio/ricardo.html +489 -0
  51. package/public/portfolio/sade.html +560 -0
  52. package/public/portfolio/sam.html +654 -0
  53. package/public/portfolio/sechar.html +580 -0
  54. package/public/portfolio/sreya.html +599 -0
  55. package/public/portfolio/swen.html +601 -0
@@ -756,6 +756,14 @@ function renderRail() {
756
756
  titleSpan.textContent = conv.title || '';
757
757
  bodyDiv.appendChild(titleSpan);
758
758
  btn.appendChild(bodyDiv);
759
+ // Issue #566 R7: mark runs of a personalized (taught/customized) job.
760
+ if (isTaughtJob(conv.jobId)) {
761
+ const taught = document.createElement('span');
762
+ taught.className = 'taught-badge';
763
+ taught.textContent = 'Personalized';
764
+ taught.title = 'Personalized for this project (taught or customized in your fraim/personalized-employee layer)';
765
+ btn.appendChild(taught);
766
+ }
759
767
  const dotClass = conversationStateDotClass(conv);
760
768
  const statusDot = document.createElement('span');
761
769
  statusDot.className = 'state-dot conv-state-dot dot-' + dotClass;
@@ -766,6 +774,16 @@ function renderRail() {
766
774
  statusSpan.textContent = conversationStateLabel(conv);
767
775
  statusSpan.classList.add(conversationUiState(conv));
768
776
  btn.appendChild(statusSpan);
777
+ // Issue #578: sourceTrigger chip — show scheduled/webhook badge on automated runs.
778
+ if (conv.sourceTrigger && conv.sourceTrigger !== 'manager') {
779
+ const trigBadge = document.createElement('span');
780
+ trigBadge.className = 'trigger-badge trigger-badge--' + conv.sourceTrigger;
781
+ trigBadge.textContent = conv.sourceTrigger === 'scheduled' ? 'Scheduled' : 'Webhook';
782
+ trigBadge.title = conv.sourceTrigger === 'scheduled'
783
+ ? 'This run was started by the built-in cron scheduler'
784
+ : 'This run was triggered by an inbound webhook';
785
+ btn.appendChild(trigBadge);
786
+ }
769
787
  // Issue #442: A/B badge on rail entry.
770
788
  if (conv.compareMode === 'ab') {
771
789
  const badge = document.createElement('span');
@@ -1420,6 +1438,9 @@ function renderActive() {
1420
1438
  // Issue #512 R7 — review experience: completion card + format artifact strip
1421
1439
  // + unified action bar (review actions sit beside the always-on coaching chips).
1422
1440
  renderReviewExperience(conv);
1441
+ // Issue #566 R4 — after the manager confirms success, offer to teach this as a
1442
+ // job (ad-hoc) or remember the coached steps for the job (structured).
1443
+ renderGrowthOffer(conv);
1423
1444
  syncThreadMessageViewport();
1424
1445
  const m = els['messages'];
1425
1446
  const runningShouldStickToBottom = !!m && conv.status === 'running'
@@ -2833,6 +2854,157 @@ function renderReviewExperience(conv) {
2833
2854
  }
2834
2855
  }
2835
2856
 
2857
+ // ---------------------------------------------------------------------------
2858
+ // Issue #566 — personalize the employee: success-confirmed growth offer.
2859
+ // After the manager confirms a run succeeded (Approve / Mark complete / Good
2860
+ // job), the employee offers to make the work repeatable. Ad-hoc runs with no
2861
+ // matching job → "teach as a job"; structured runs the manager coached →
2862
+ // "remember these steps" (customize). The offer is a thin relay: accepting it
2863
+ // starts the evolve-employee job seeded with the run; that job owns the actual
2864
+ // teaching, dedup, ownership, validation, and security review.
2865
+ // ---------------------------------------------------------------------------
2866
+
2867
+ function jobCatalogEntry(jobId) {
2868
+ const jobs = (state.bootstrap && state.bootstrap.jobs) ? state.bootstrap.jobs : [];
2869
+ return jobs.find((j) => j.id === jobId) || null;
2870
+ }
2871
+
2872
+ // A run's job is personalized when its catalog entry came from the
2873
+ // personalized-employee layer (R7). No author/attribution is tracked.
2874
+ function isTaughtJob(jobId) {
2875
+ const entry = jobCatalogEntry(jobId);
2876
+ return !!(entry && entry.personalized);
2877
+ }
2878
+
2879
+ function growthOfferHost() {
2880
+ const messages = els['messages'];
2881
+ if (!messages) return null;
2882
+ let host = messages.querySelector('#growth-offer');
2883
+ if (!host) {
2884
+ host = document.createElement('div');
2885
+ host.id = 'growth-offer';
2886
+ host.className = 'review-completion';
2887
+ }
2888
+ // Keep it after the latest message / completion card.
2889
+ messages.appendChild(host);
2890
+ return host;
2891
+ }
2892
+
2893
+ function clearGrowthOffer() {
2894
+ const messages = els['messages'];
2895
+ const existing = messages && messages.querySelector('#growth-offer');
2896
+ if (existing) existing.remove();
2897
+ }
2898
+
2899
+ function renderGrowthOffer(conv) {
2900
+ // Gate on the run being successfully done (R4):
2901
+ // - Ad-hoc (watercooler) runs: any *completed* run qualifies. This is how an
2902
+ // existing watercooler conversation already sitting in the rail shows a
2903
+ // teach trigger when the manager opens it (the issue's headline use case) —
2904
+ // ad-hoc runs have no review/approve step, so "done" is the signal.
2905
+ // - Structured runs: require the manager's explicit approval (reviewApproved).
2906
+ // A structured run that merely finished is still awaiting the manager's
2907
+ // review, so we do not offer to customize it until they confirm it was good.
2908
+ // Never offer while a run is still in flight, on the teach run itself, or once
2909
+ // dismissed/acted on for this run.
2910
+ const isAdhoc = !!(conv && conv.jobId === '__freeform__');
2911
+ const succeeded = !!(conv && (conv.reviewApproved || (isAdhoc && conv.status === 'completed')));
2912
+ const isTeachRun = !!(conv && conv.jobId === 'evolve-employee');
2913
+ if (!succeeded || isTeachRun || conv.growthOfferDismissed || conv.growthOfferActed) {
2914
+ clearGrowthOffer();
2915
+ return;
2916
+ }
2917
+ const host = growthOfferHost();
2918
+ if (!host) return;
2919
+ const adhoc = conv.jobId === '__freeform__';
2920
+ host.innerHTML = '';
2921
+
2922
+ const card = document.createElement('div');
2923
+ card.className = 'review-card review-card--offer';
2924
+ card.id = 'growth-offer-card';
2925
+ const heading = document.createElement('div');
2926
+ heading.className = 'rc-heading';
2927
+ heading.textContent = adhoc ? '🎓 Want me to learn this?' : '🎓 Remember these steps?';
2928
+ card.appendChild(heading);
2929
+
2930
+ const rows = adhoc
2931
+ ? [
2932
+ ['What happened', 'You approved an ad-hoc task that has no matching job yet.'],
2933
+ ['Offer', 'I can turn this into a repeatable job so you can ask for it by name next time.'],
2934
+ ]
2935
+ : [
2936
+ ['What happened', `You approved "${conv.jobTitle || conv.title || 'this run'}" and coached it along the way.`],
2937
+ ['Offer', 'I can fold those steps into this job so I do them automatically next time.'],
2938
+ ];
2939
+ for (const [k, v] of rows) {
2940
+ const r = document.createElement('div');
2941
+ r.className = 'rc-row';
2942
+ const ks = document.createElement('span');
2943
+ ks.className = 'rc-k';
2944
+ ks.textContent = k;
2945
+ const vs = document.createElement('span');
2946
+ vs.className = 'rc-v';
2947
+ vs.textContent = v;
2948
+ r.appendChild(ks);
2949
+ r.appendChild(vs);
2950
+ card.appendChild(r);
2951
+ }
2952
+ host.appendChild(card);
2953
+
2954
+ const strip = document.createElement('div');
2955
+ strip.className = 'rc-artifact-strip';
2956
+ const teachBtn = document.createElement('button');
2957
+ teachBtn.type = 'button';
2958
+ teachBtn.className = 'rc-art-btn rc-art-btn--teach';
2959
+ teachBtn.id = 'growth-teach-btn';
2960
+ teachBtn.textContent = adhoc ? '🎓 Teach as a job' : '✨ Remember for this job';
2961
+ teachBtn.addEventListener('click', () => {
2962
+ conv.growthOfferActed = true;
2963
+ upsertConversation(conv);
2964
+ startTeachFlow({ seedConv: conv, mode: adhoc ? 'teach' : 'customize', seedSource: 'run' });
2965
+ });
2966
+ const dismissBtn = document.createElement('button');
2967
+ dismissBtn.type = 'button';
2968
+ dismissBtn.className = 'rc-art-btn ghost';
2969
+ dismissBtn.id = 'growth-dismiss-btn';
2970
+ dismissBtn.textContent = 'Not now';
2971
+ dismissBtn.addEventListener('click', () => {
2972
+ conv.growthOfferDismissed = true;
2973
+ upsertConversation(conv);
2974
+ renderActive();
2975
+ });
2976
+ strip.appendChild(teachBtn);
2977
+ strip.appendChild(dismissBtn);
2978
+ host.appendChild(strip);
2979
+ }
2980
+
2981
+ // Start the evolve-employee job, optionally seeded from a completed run or a
2982
+ // document. This is the single entry point shared by the success-confirmed
2983
+ // offer and the "+ New job" palette teach entries (R1/R4).
2984
+ function startTeachFlow(opts) {
2985
+ const options = opts || {};
2986
+ const employeeId = (state.bootstrap && state.bootstrap.preferences && state.bootstrap.preferences.employeeId) || 'claude';
2987
+ const seedConv = options.seedConv || null;
2988
+ let instructions = '';
2989
+ if (options.seedSource === 'run' && seedConv) {
2990
+ const transcript = (seedConv.messages || [])
2991
+ .map((m) => `${m.role}: ${typeof m.text === 'string' ? m.text : ''}`)
2992
+ .join('\n')
2993
+ .slice(0, 4000);
2994
+ if (options.mode === 'customize') {
2995
+ instructions = `Customize the "${seedConv.jobTitle || seedConv.jobId}" job. I coached this run with steps you should remember next time. Use this run as the seed: pre-fill the intake from the coaching turns and confirm with me before changing anything. Keep the same owning employee and preserve the existing phases.\n\nRun: ${seedConv.title || ''}\n\nTranscript:\n${transcript}`;
2996
+ } else {
2997
+ instructions = `Teach yourself a new repeatable job from this successful run. Use the run as the seed: restate the ask, the steps you performed, and the deliverable; propose a job name; check for an existing job first; and recommend which employee should own it.\n\nRun: ${seedConv.title || ''}\n\nTranscript:\n${transcript}`;
2998
+ }
2999
+ } else if (options.seedSource === 'document') {
3000
+ instructions = 'Teach yourself a new job from an SOP or document. Ask me for the document, then map its sections and steps onto job phases and confirm the mapping with me before drafting. Recommend which employee should own it.';
3001
+ } else {
3002
+ instructions = 'Teach yourself a new job. I will describe the capability I want. Pre-fill the plan, check for an existing job first, and recommend which employee should own it.';
3003
+ }
3004
+ const job = { id: 'evolve-employee', title: options.mode === 'customize' ? 'Improve a job' : 'Teach a new skill' };
3005
+ startRun(job, instructions, employeeId);
3006
+ }
3007
+
2836
3008
  // Comments follow the artifact (R7.7): a PR opens GitHub; local artifacts open
2837
3009
  // from disk; reports view inline. Nothing new is invented - these surface what
2838
3010
  // the read-side already knows and otherwise leave a status hint.
@@ -3188,10 +3360,28 @@ function renderCpRows(searchText) {
3188
3360
  }
3189
3361
  }
3190
3362
 
3191
- // Build flat row list: recent first, then catalog
3363
+ // Issue #566 R4: teach entries start the evolve-employee job. They appear at
3364
+ // the top of the catalog, only when no persona filter is active (teaching is
3365
+ // not persona-scoped), and match a search over their labels.
3366
+ const teachRows = [];
3367
+ if (personaFilter === null) {
3368
+ if (!q || 'teach a new skill'.includes(q)) {
3369
+ teachRows.push({ type: 'teach', teachSeed: 'description', job: { id: 'evolve-employee', title: 'Teach a new skill', intent: 'Teach me a new job in plain language.' } });
3370
+ }
3371
+ if (!q || 'teach from a document'.includes(q) || 'sop'.includes(q)) {
3372
+ teachRows.push({ type: 'teach', teachSeed: 'document', job: { id: 'evolve-employee', title: 'Teach from a document', intent: 'Point me at an SOP or document and I will turn it into a phased job.' } });
3373
+ }
3374
+ }
3375
+
3376
+ // Build flat row list: recent first, then catalog jobs, then teach entries
3377
+ // last. Teach entries go after the catalog jobs so the first runnable job
3378
+ // stays the default keyboard selection (ArrowDown+Enter runs a job, not a
3379
+ // teach flow). The flat order must match the render order below so
3380
+ // click/keyboard indices line up.
3192
3381
  state.cpRows = [
3193
3382
  ...recentRows,
3194
3383
  ...catalogJobs.map((j) => ({ type: 'job', job: j, instructions: '' })),
3384
+ ...teachRows,
3195
3385
  ];
3196
3386
  state.cpHighlightIndex = -1;
3197
3387
 
@@ -3206,13 +3396,17 @@ function renderCpRows(searchText) {
3206
3396
  });
3207
3397
  }
3208
3398
 
3209
- // Render catalog section
3399
+ // Render catalog section: catalog jobs first, then teach entries last.
3400
+ // flatIndex continues from recentRows so it matches state.cpRows order.
3210
3401
  const catalogList = document.getElementById('cp-catalog-list');
3211
3402
  if (catalogList) {
3212
3403
  catalogList.innerHTML = '';
3213
3404
  catalogJobs.forEach((job, i) => {
3214
3405
  catalogList.appendChild(buildCpRow({ type: 'job', job, instructions: '' }, recentRows.length + i));
3215
3406
  });
3407
+ teachRows.forEach((row, i) => {
3408
+ catalogList.appendChild(buildCpRow(row, recentRows.length + catalogJobs.length + i));
3409
+ });
3216
3410
  }
3217
3411
 
3218
3412
  // Update employee footer
@@ -3233,7 +3427,7 @@ function buildCpRow(row, flatIndex) {
3233
3427
 
3234
3428
  const icon = document.createElement('span');
3235
3429
  icon.className = 'cp-row-icon';
3236
- icon.textContent = row.type === 'recent' ? '🕐' : '📋';
3430
+ icon.textContent = row.type === 'recent' ? '🕐' : row.type === 'teach' ? '🎓' : '📋';
3237
3431
 
3238
3432
  const body = document.createElement('span');
3239
3433
  body.className = 'cp-row-body';
@@ -3305,6 +3499,12 @@ function selectCpRow(index) {
3305
3499
 
3306
3500
  if (row.type === 'recent' && row.instructions) {
3307
3501
  instr.value = row.instructions;
3502
+ } else if (row.type === 'teach') {
3503
+ // Issue #566: seed the instruction so the manager confirms rather than
3504
+ // re-authoring. The evolve-employee job reads the seed source from this.
3505
+ instr.value = row.teachSeed === 'document'
3506
+ ? 'Teach yourself a new job from this document: '
3507
+ : 'Teach yourself a new job: ';
3308
3508
  } else {
3309
3509
  instr.value = '';
3310
3510
  }
@@ -4055,7 +4255,18 @@ function startPolling() {
4055
4255
  }
4056
4256
  try {
4057
4257
  if (!fraimDone) {
4058
- const run = await requestJson(`/api/ai-hub/runs/${conv.runId}`);
4258
+ const runResp = await fetch(`/api/ai-hub/runs/${conv.runId}`, { headers: { 'x-api-key': 'local-dev' } });
4259
+ if (runResp.status === 404) {
4260
+ // Run no longer in server registry (e.g. after restart). Stop polling.
4261
+ window.clearInterval(state.pollHandle);
4262
+ state.pollHandle = null;
4263
+ conv.status = 'done';
4264
+ upsertConversation(conv);
4265
+ renderRail();
4266
+ renderActive();
4267
+ return;
4268
+ }
4269
+ const run = await runResp.json();
4059
4270
  foldRunIntoConversation(conv, run);
4060
4271
  }
4061
4272
  // Issue #442: also poll the compare run when present and not yet terminal.
@@ -5161,6 +5372,8 @@ function tfSelectProjectView(view, projectId) {
5161
5372
  tfRenderTree();
5162
5373
  tfRenderProjectContextTop();
5163
5374
  tfApplyWorkspaceMode();
5375
+ // Issue #578: reload deployment roster when entering workspace.
5376
+ loadDeployments();
5164
5377
  }
5165
5378
  }
5166
5379
 
@@ -5642,6 +5855,197 @@ function tfShowProjectBrief() { const a = document.getElementById('proj-brief-ac
5642
5855
  function tfShowProjectLearnings() { const a = document.getElementById('proj-learnings-acc'); if (a) a.open = true; }
5643
5856
  function tfHideProjectLearnings() { /* panels removed in #521; brief/learnings are inline top sections */ }
5644
5857
 
5858
+ // ---------------------------------------------------------------------------
5859
+ // Issue #578: Deployment roster (scheduled + webhook triggers)
5860
+ // ---------------------------------------------------------------------------
5861
+
5862
+ async function loadDeployments() {
5863
+ const list = document.getElementById('proj-deployments-list');
5864
+ if (!list) return;
5865
+ try {
5866
+ const [schResp, whResp] = await Promise.all([
5867
+ fetch('/api/ai-hub/schedules'),
5868
+ fetch('/api/ai-hub/webhooks'),
5869
+ ]);
5870
+ const schedules = schResp.ok ? await schResp.json() : [];
5871
+ const webhooks = whResp.ok ? await whResp.json() : [];
5872
+ renderDeploymentList(list, [...schedules, ...webhooks]);
5873
+ } catch {
5874
+ list.textContent = 'Could not load deployments.';
5875
+ }
5876
+ }
5877
+
5878
+ function renderDeploymentList(container, deployments) {
5879
+ container.innerHTML = '';
5880
+ if (!deployments.length) {
5881
+ const empty = document.createElement('p');
5882
+ empty.className = 'dep-empty';
5883
+ empty.textContent = 'No deployments yet. Use + Schedule or + Webhook above to add automated triggers.';
5884
+ container.appendChild(empty);
5885
+ return;
5886
+ }
5887
+ for (const dep of deployments) {
5888
+ const card = document.createElement('div');
5889
+ card.className = 'dep-card';
5890
+ const top = document.createElement('div');
5891
+ top.className = 'dep-card-top';
5892
+ const icon = document.createElement('span');
5893
+ icon.className = 'dep-type-icon';
5894
+ icon.textContent = dep.type === 'scheduled' ? '⏱' : '🔗';
5895
+ const label = document.createElement('span');
5896
+ label.className = 'dep-label';
5897
+ label.textContent = dep.label;
5898
+ const typeBadge = document.createElement('span');
5899
+ typeBadge.className = 'dep-type-badge dep-type-badge--' + dep.type;
5900
+ typeBadge.textContent = dep.type === 'scheduled' ? 'Scheduled' : 'Webhook';
5901
+ top.appendChild(icon);
5902
+ top.appendChild(label);
5903
+ top.appendChild(typeBadge);
5904
+ card.appendChild(top);
5905
+
5906
+ if (dep.cronExpr) {
5907
+ const cron = document.createElement('div');
5908
+ cron.className = 'dep-detail';
5909
+ cron.textContent = dep.cronExpr;
5910
+ card.appendChild(cron);
5911
+ }
5912
+ if (dep.inboundUrl) {
5913
+ const urlRow = document.createElement('div');
5914
+ urlRow.className = 'dep-detail dep-detail--url';
5915
+ const urlCode = document.createElement('code');
5916
+ urlCode.textContent = dep.inboundUrl;
5917
+ urlCode.className = 'dep-inbound-url dep-inbound-url--inline';
5918
+ const copyBtn = document.createElement('button');
5919
+ copyBtn.type = 'button';
5920
+ copyBtn.className = 'hm-copy-btn';
5921
+ copyBtn.textContent = 'Copy';
5922
+ copyBtn.addEventListener('click', () => { navigator.clipboard.writeText(dep.inboundUrl).catch(() => {}); });
5923
+ urlRow.appendChild(urlCode);
5924
+ urlRow.appendChild(copyBtn);
5925
+ card.appendChild(urlRow);
5926
+ }
5927
+
5928
+ const delBtn = document.createElement('button');
5929
+ delBtn.type = 'button';
5930
+ delBtn.className = 'dep-del-btn';
5931
+ delBtn.title = 'Remove this deployment';
5932
+ delBtn.textContent = 'Remove';
5933
+ delBtn.addEventListener('click', async () => {
5934
+ const endpoint = dep.type === 'scheduled' ? 'schedules' : 'webhooks';
5935
+ await fetch('/api/ai-hub/' + endpoint + '/' + dep.id, { method: 'DELETE' });
5936
+ await loadDeployments();
5937
+ });
5938
+ card.appendChild(delBtn);
5939
+ container.appendChild(card);
5940
+ }
5941
+ }
5942
+
5943
+ function initDeploymentButtons() {
5944
+ const addSchBtn = document.getElementById('dep-add-schedule-btn');
5945
+ const addWhBtn = document.getElementById('dep-add-webhook-btn');
5946
+ if (addSchBtn) addSchBtn.addEventListener('click', () => openDeploymentModal('schedule'));
5947
+ if (addWhBtn) addWhBtn.addEventListener('click', () => openDeploymentModal('webhook'));
5948
+
5949
+ // Close buttons
5950
+ const schClose = document.getElementById('dep-schedule-close');
5951
+ const whClose = document.getElementById('dep-webhook-close');
5952
+ if (schClose) schClose.addEventListener('click', () => { document.getElementById('dep-schedule-modal').hidden = true; });
5953
+ if (whClose) whClose.addEventListener('click', () => { document.getElementById('dep-webhook-modal').hidden = true; });
5954
+
5955
+ // Cancel buttons
5956
+ const schCancel = document.getElementById('dep-sch-cancel-btn');
5957
+ const whCancel = document.getElementById('dep-wh-cancel-btn');
5958
+ if (schCancel) schCancel.addEventListener('click', () => { document.getElementById('dep-schedule-modal').hidden = true; });
5959
+ if (whCancel) whCancel.addEventListener('click', () => { document.getElementById('dep-webhook-modal').hidden = true; });
5960
+
5961
+ // Save buttons
5962
+ const schSave = document.getElementById('dep-sch-save-btn');
5963
+ if (schSave) schSave.addEventListener('click', saveScheduleDeployment);
5964
+ const whSave = document.getElementById('dep-wh-save-btn');
5965
+ if (whSave) whSave.addEventListener('click', saveWebhookDeployment);
5966
+
5967
+ // Copy inbound URL button
5968
+ const copyBtn = document.getElementById('dep-wh-copy-btn');
5969
+ if (copyBtn) copyBtn.addEventListener('click', () => {
5970
+ const url = document.getElementById('dep-wh-inbound-url').textContent;
5971
+ navigator.clipboard.writeText(url).catch(() => {});
5972
+ });
5973
+ }
5974
+
5975
+ function openDeploymentModal(type) {
5976
+ const jobs = state.bootstrap?.jobs ?? [];
5977
+ if (type === 'schedule') {
5978
+ const modal = document.getElementById('dep-schedule-modal');
5979
+ const jobSel = document.getElementById('dep-sch-job');
5980
+ jobSel.innerHTML = '';
5981
+ for (const j of jobs) {
5982
+ const opt = document.createElement('option');
5983
+ opt.value = j.id;
5984
+ opt.textContent = j.title;
5985
+ jobSel.appendChild(opt);
5986
+ }
5987
+ document.getElementById('dep-sch-error').hidden = true;
5988
+ document.getElementById('dep-sch-label').value = '';
5989
+ document.getElementById('dep-sch-cron').value = '';
5990
+ document.getElementById('dep-sch-instructions').value = '';
5991
+ modal.hidden = false;
5992
+ } else {
5993
+ const modal = document.getElementById('dep-webhook-modal');
5994
+ const jobSel = document.getElementById('dep-wh-job');
5995
+ jobSel.innerHTML = '';
5996
+ for (const j of jobs) {
5997
+ const opt = document.createElement('option');
5998
+ opt.value = j.id;
5999
+ opt.textContent = j.title;
6000
+ jobSel.appendChild(opt);
6001
+ }
6002
+ document.getElementById('dep-wh-error').hidden = true;
6003
+ document.getElementById('dep-wh-inbound-row').hidden = true;
6004
+ document.getElementById('dep-wh-label').value = '';
6005
+ document.getElementById('dep-wh-instructions').value = '';
6006
+ modal.hidden = false;
6007
+ }
6008
+ }
6009
+
6010
+ async function saveScheduleDeployment() {
6011
+ const label = document.getElementById('dep-sch-label').value.trim();
6012
+ const jobId = document.getElementById('dep-sch-job').value;
6013
+ const cronExpr = document.getElementById('dep-sch-cron').value.trim();
6014
+ const instructions = document.getElementById('dep-sch-instructions').value.trim();
6015
+ const errEl = document.getElementById('dep-sch-error');
6016
+ if (!label || !cronExpr) { errEl.textContent = 'Label and cron expression are required.'; errEl.hidden = false; return; }
6017
+ try {
6018
+ const resp = await fetch('/api/ai-hub/schedules', {
6019
+ method: 'POST',
6020
+ headers: { 'Content-Type': 'application/json' },
6021
+ body: JSON.stringify({ label, jobId, cronExpr, instructions: instructions || undefined }),
6022
+ });
6023
+ if (!resp.ok) { const e = await resp.json().catch(() => ({})); errEl.textContent = e.error || 'Failed to create schedule.'; errEl.hidden = false; return; }
6024
+ document.getElementById('dep-schedule-modal').hidden = true;
6025
+ await loadDeployments();
6026
+ } catch { errEl.textContent = 'Network error — is the Hub running?'; errEl.hidden = false; }
6027
+ }
6028
+
6029
+ async function saveWebhookDeployment() {
6030
+ const label = document.getElementById('dep-wh-label').value.trim();
6031
+ const jobId = document.getElementById('dep-wh-job').value;
6032
+ const instructions = document.getElementById('dep-wh-instructions').value.trim();
6033
+ const errEl = document.getElementById('dep-wh-error');
6034
+ if (!label) { errEl.textContent = 'Label is required.'; errEl.hidden = false; return; }
6035
+ try {
6036
+ const resp = await fetch('/api/ai-hub/webhooks', {
6037
+ method: 'POST',
6038
+ headers: { 'Content-Type': 'application/json' },
6039
+ body: JSON.stringify({ label, jobId, instructions: instructions || undefined }),
6040
+ });
6041
+ if (!resp.ok) { const e = await resp.json().catch(() => ({})); errEl.textContent = e.error || 'Failed to create webhook.'; errEl.hidden = false; return; }
6042
+ const dep = await resp.json();
6043
+ document.getElementById('dep-wh-inbound-url').textContent = dep.inboundUrl;
6044
+ document.getElementById('dep-wh-inbound-row').hidden = false;
6045
+ await loadDeployments();
6046
+ } catch { errEl.textContent = 'Network error — is the Hub running?'; errEl.hidden = false; }
6047
+ }
6048
+
5645
6049
  // ---------------------------------------------------------------------------
5646
6050
  // Company / Manager / Brain content
5647
6051
  // ---------------------------------------------------------------------------
@@ -7615,6 +8019,8 @@ function tfInitShell() {
7615
8019
  tfRenderRail();
7616
8020
  tfRenderProjectTabs();
7617
8021
  tfShowArea('projects');
8022
+ // Issue #578: wire deployment modal buttons.
8023
+ initDeploymentButtons();
7618
8024
  // Open the workspace by default so the conversation surface (#conversation,
7619
8025
  // #new-conv-btn, #empty, the welcome line, the project button) is visible on
7620
8026
  // load — preserving the contract the shipped #339/#347/#429/#442 Hub UI
@@ -7668,4 +8074,8 @@ if (typeof window !== 'undefined') {
7668
8074
  window.persistConversations = persistConversations;
7669
8075
  window.renderRail = renderRail;
7670
8076
  window.renderActive = renderActive;
8077
+ // Issue #566: let the suite open a conversation and read the active one so it
8078
+ // can exercise the success-confirmed growth offer deterministically.
8079
+ window.switchToConversation = switchToConversation;
8080
+ window.activeConversation = activeConversation;
7671
8081
  }
@@ -26,6 +26,10 @@
26
26
  --shadow-lg: 0 0 0 1px rgba(0, 0, 0, 0.10), 0 4px 16px rgba(0, 0, 0, 0.08);
27
27
  --picker-delegation: #5b7eb0;
28
28
  --picker-learning: #8b5fbf;
29
+ /* Issue #566: personalization / "teach" accent (growth offer + taught badge). */
30
+ --teach: #5b3fa8;
31
+ --teach-soft: #f6f2ff;
32
+ --teach-line: #d8cdf2;
29
33
  /* Safe zone for macOS hiddenInset traffic lights (~28px) and
30
34
  Windows titleBarOverlay height (36px). Falls back to 0 in a browser tab. */
31
35
  --titlebar-inset: env(titlebar-area-height, 0px);
@@ -2047,6 +2051,22 @@ button.small { padding: 4px 10px; font-size: 12px; }
2047
2051
  flex-shrink: 0;
2048
2052
  }
2049
2053
 
2054
+ /* Issue #566 R7: "Taught by you" marking on runs of a personalized job.
2055
+ Follows the .ab-badge inline-badge pattern with a distinct purple accent. */
2056
+ .taught-badge {
2057
+ margin-left: auto;
2058
+ padding: 1px 6px;
2059
+ font-size: 9.5px;
2060
+ font-weight: 700;
2061
+ letter-spacing: 0.03em;
2062
+ border-radius: 999px;
2063
+ background: var(--teach-soft);
2064
+ color: var(--teach);
2065
+ border: 1px solid var(--teach-line);
2066
+ white-space: nowrap;
2067
+ flex-shrink: 0;
2068
+ }
2069
+
2050
2070
  /* Full-panel A/B split: #conversation becomes a flex row */
2051
2071
  #conversation.ab-mode {
2052
2072
  display: flex;
@@ -3965,3 +3985,39 @@ body.hub-shell { display: flex; flex-direction: column; height: 100vh; overflow:
3965
3985
  .hs-btn--primary:hover { opacity:.85; }
3966
3986
  .hs-btn--ghost { background:transparent; color:var(--muted); border:1px solid var(--line); }
3967
3987
  .hs-btn--ghost:hover { color:var(--text); border-color:var(--text); }
3988
+
3989
+ /* ── Issue #578: Scheduled + Reactive Employees ── */
3990
+
3991
+ /* sourceTrigger chip on run rail items */
3992
+ .trigger-badge { display:inline-block; padding:1px 6px; border-radius:4px; font-size:10px; font-weight:600; letter-spacing:.02em; flex-shrink:0; }
3993
+ .trigger-badge--scheduled { background:rgba(0,122,255,.12); color:#0071e3; }
3994
+ .trigger-badge--webhook { background:rgba(88,86,214,.12); color:#5856d6; }
3995
+
3996
+ /* Deployment roster accordion */
3997
+ #proj-deployments-acc { margin-top:0; }
3998
+ .dep-actions { display:flex; gap:8px; margin-top:8px; }
3999
+ .dep-add-btn { padding:4px 12px; background:var(--accent-soft,rgba(0,113,227,.08)); color:var(--accent,#0071e3); border:1px solid rgba(0,113,227,.2); border-radius:8px; font-size:12px; font-weight:600; cursor:pointer; }
4000
+ .dep-add-btn:hover { background:var(--accent,#0071e3); color:#fff; }
4001
+
4002
+ /* Deployment cards */
4003
+ .dep-card { display:flex; flex-wrap:wrap; align-items:baseline; gap:6px; padding:8px 10px; background:var(--surface,#fff); border:1px solid var(--line,#e5e5e5); border-radius:10px; margin-bottom:6px; }
4004
+ .dep-card-top { display:flex; align-items:center; gap:6px; flex:1; min-width:0; }
4005
+ .dep-type-icon { font-size:14px; flex-shrink:0; }
4006
+ .dep-label { font-size:13px; font-weight:600; color:var(--text,#1d1d1f); flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
4007
+ .dep-type-badge { display:inline-block; padding:1px 7px; border-radius:4px; font-size:10px; font-weight:600; flex-shrink:0; }
4008
+ .dep-type-badge--scheduled { background:rgba(0,122,255,.12); color:#0071e3; }
4009
+ .dep-type-badge--webhook { background:rgba(88,86,214,.12); color:#5856d6; }
4010
+ .dep-detail { width:100%; font-size:11px; color:var(--muted,#888); margin-left:20px; }
4011
+ .dep-detail--url { display:flex; align-items:center; gap:6px; }
4012
+ .dep-inbound-url { font-size:11px; word-break:break-all; color:var(--muted,#888); flex:1; }
4013
+ .dep-inbound-url--inline { display:inline; font-size:11px; }
4014
+ .dep-del-btn { margin-left:auto; padding:2px 8px; background:transparent; border:1px solid var(--line,#e5e5e5); border-radius:6px; font-size:11px; color:var(--muted,#888); cursor:pointer; flex-shrink:0; }
4015
+ .dep-del-btn:hover { border-color:#ff3b30; color:#ff3b30; }
4016
+ .dep-empty { color:var(--muted,#888); font-size:12px; margin:4px 0 8px; line-height:1.5; }
4017
+
4018
+ /* Deployment modals */
4019
+ .dep-modal-actions { display:flex; gap:8px; margin-top:12px; }
4020
+ .dep-error { color:#ff3b30; font-size:12px; margin-top:4px; }
4021
+ .cancel-button { padding:7px 18px; border-radius:8px; font-size:13px; font-weight:600; cursor:pointer; background:transparent; color:var(--muted,#888); border:1px solid var(--line,#e5e5e5); }
4022
+ .cancel-button:hover { border-color:var(--text,#1d1d1f); color:var(--text,#1d1d1f); }
4023
+ .dep-inbound-url-row { display:flex; align-items:center; gap:8px; padding:6px 10px; background:var(--bg,#f5f5f7); border-radius:8px; margin-top:6px; }