shakers-cli-beta 0.8.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/LICENSE +59 -0
  2. package/README.md +1175 -0
  3. package/bin/add-agent.js +88 -0
  4. package/bin/add-project.js +88 -0
  5. package/bin/add-role.js +61 -0
  6. package/bin/add-skill.js +88 -0
  7. package/bin/ai-usage.js +81 -0
  8. package/bin/alma.js +99 -0
  9. package/bin/applications.js +69 -0
  10. package/bin/availability.js +70 -0
  11. package/bin/certifications.js +69 -0
  12. package/bin/certify.js +73 -0
  13. package/bin/change-role.js +61 -0
  14. package/bin/config.js +120 -0
  15. package/bin/experiences.js +69 -0
  16. package/bin/find-projects.js +89 -0
  17. package/bin/invitations.js +69 -0
  18. package/bin/lang.js +70 -0
  19. package/bin/login.js +283 -0
  20. package/bin/mcp.js +98 -0
  21. package/bin/me.js +69 -0
  22. package/bin/onboarding.js +134 -0
  23. package/bin/portfolios.js +69 -0
  24. package/bin/rate.js +70 -0
  25. package/bin/register.js +325 -0
  26. package/bin/report-html.js +134 -0
  27. package/bin/save.js +77 -0
  28. package/bin/shakers.js +344 -0
  29. package/bin/share.js +72 -0
  30. package/bin/show-project.js +72 -0
  31. package/bin/socials.js +70 -0
  32. package/bin/superadmin.js +351 -0
  33. package/package.json +40 -0
  34. package/src/agent-category.js +13 -0
  35. package/src/agent-evaluation.js +177 -0
  36. package/src/agent-org-chart.js +355 -0
  37. package/src/agent-synthesis.js +132 -0
  38. package/src/agent-usage.js +118 -0
  39. package/src/agents-client.js +165 -0
  40. package/src/ai-fluency-cell.js +72 -0
  41. package/src/ai-profile-client.js +45 -0
  42. package/src/ai-profile-preview.js +154 -0
  43. package/src/ai.js +66 -0
  44. package/src/alma-client.js +123 -0
  45. package/src/alma-flow.js +32 -0
  46. package/src/alma-repo-context.js +47 -0
  47. package/src/alma-shell.js +161 -0
  48. package/src/ansi.js +62 -0
  49. package/src/applications-client.js +79 -0
  50. package/src/applications-flow.js +58 -0
  51. package/src/auth-client.js +144 -0
  52. package/src/auth-device-client.js +118 -0
  53. package/src/auth-method-picker.js +44 -0
  54. package/src/auth-methods.js +35 -0
  55. package/src/auth-session-store.js +214 -0
  56. package/src/authorship.js +203 -0
  57. package/src/automations-detector.js +170 -0
  58. package/src/availability-client.js +92 -0
  59. package/src/availability-flow.js +105 -0
  60. package/src/backend-request.js +170 -0
  61. package/src/blind-approval-extract.js +48 -0
  62. package/src/brand-ansi.js +14 -0
  63. package/src/brand-svg.js +9 -0
  64. package/src/browser-tools-detector.js +21 -0
  65. package/src/build-next-level.js +56 -0
  66. package/src/certifications-client.js +89 -0
  67. package/src/certifications-flow.js +75 -0
  68. package/src/certify-dimension-client.js +217 -0
  69. package/src/certify-dimension-flow.js +193 -0
  70. package/src/certify-disclaimer.js +51 -0
  71. package/src/certify-remediation-prompt.js +23 -0
  72. package/src/cli-args.js +83 -0
  73. package/src/config-dir.js +120 -0
  74. package/src/config.js +778 -0
  75. package/src/consent-flow.js +115 -0
  76. package/src/consent-skip.js +27 -0
  77. package/src/decisions-extract.js +43 -0
  78. package/src/detect-repos.js +145 -0
  79. package/src/detected-tools.js +15 -0
  80. package/src/detection-evidence.js +105 -0
  81. package/src/detection-noise.js +132 -0
  82. package/src/detectors.js +221 -0
  83. package/src/device-auth-store.js +100 -0
  84. package/src/device-login.js +118 -0
  85. package/src/email-verification.js +152 -0
  86. package/src/env-paths.js +16 -0
  87. package/src/find-projects-cache.js +89 -0
  88. package/src/find-projects-client.js +138 -0
  89. package/src/find-projects-flow.js +164 -0
  90. package/src/git-activity.js +123 -0
  91. package/src/graph-certs.js +58 -0
  92. package/src/graph-generator.js +230 -0
  93. package/src/graph-scan.js +239 -0
  94. package/src/i18n.js +3264 -0
  95. package/src/inter-fonts.js +6 -0
  96. package/src/interactive-select.js +183 -0
  97. package/src/interview-client.js +226 -0
  98. package/src/interview-transcript.js +56 -0
  99. package/src/inventory-client.js +90 -0
  100. package/src/invitations-client.js +66 -0
  101. package/src/invitations-flow.js +50 -0
  102. package/src/lang-client.js +147 -0
  103. package/src/lang-codes.js +14 -0
  104. package/src/lang-flow.js +85 -0
  105. package/src/legal-notice.js +53 -0
  106. package/src/locale.js +51 -0
  107. package/src/markdown-ansi.js +50 -0
  108. package/src/maturity.js +111 -0
  109. package/src/mcp-ai-usage-result-tool.js +109 -0
  110. package/src/mcp-ai-usage-tool.js +333 -0
  111. package/src/mcp-alma-tools.js +91 -0
  112. package/src/mcp-applications-tools.js +42 -0
  113. package/src/mcp-ask-tools.js +50 -0
  114. package/src/mcp-auth-form.js +282 -0
  115. package/src/mcp-availability-tools.js +43 -0
  116. package/src/mcp-certifications-tools.js +42 -0
  117. package/src/mcp-certify-dimension-tools.js +58 -0
  118. package/src/mcp-detector.js +223 -0
  119. package/src/mcp-device-login-tools.js +173 -0
  120. package/src/mcp-find-projects-tools.js +67 -0
  121. package/src/mcp-install.js +245 -0
  122. package/src/mcp-invitations-tools.js +42 -0
  123. package/src/mcp-lang-tools.js +39 -0
  124. package/src/mcp-onboarding-tools.js +388 -0
  125. package/src/mcp-portfolios-tools.js +46 -0
  126. package/src/mcp-profile-tools.js +46 -0
  127. package/src/mcp-rate-tools.js +42 -0
  128. package/src/mcp-register-tools.js +517 -0
  129. package/src/mcp-report-tools.js +101 -0
  130. package/src/mcp-role-tools.js +112 -0
  131. package/src/mcp-saved-positions-tools.js +55 -0
  132. package/src/mcp-server.js +170 -0
  133. package/src/mcp-show-project-tools.js +50 -0
  134. package/src/mcp-socials-tools.js +39 -0
  135. package/src/memory-structure-detector.js +108 -0
  136. package/src/model-call-record.js +198 -0
  137. package/src/onboarding-availability.js +26 -0
  138. package/src/onboarding-client.js +364 -0
  139. package/src/onboarding-flow.js +1078 -0
  140. package/src/onboarding-livekit-client.js +102 -0
  141. package/src/onboarding-status-client.js +42 -0
  142. package/src/open-file.js +32 -0
  143. package/src/osc-link.js +16 -0
  144. package/src/portfolio-client.js +164 -0
  145. package/src/portfolio-git-info.js +61 -0
  146. package/src/portfolios-client.js +85 -0
  147. package/src/portfolios-flow.js +79 -0
  148. package/src/profile-client.js +125 -0
  149. package/src/profile-flow.js +115 -0
  150. package/src/profile-switch.js +46 -0
  151. package/src/prompt-select.js +29 -0
  152. package/src/rate-client.js +82 -0
  153. package/src/rate-flow.js +137 -0
  154. package/src/render-ai-fluency.js +266 -0
  155. package/src/render-certification.js +177 -0
  156. package/src/render-html.js +140 -0
  157. package/src/render-report-text.js +27 -0
  158. package/src/render-sheet.js +261 -0
  159. package/src/render-terminal.js +109 -0
  160. package/src/render-usage-report-text.js +155 -0
  161. package/src/repo-context.js +215 -0
  162. package/src/report-gating.js +49 -0
  163. package/src/report-store.js +226 -0
  164. package/src/report-theme.js +177 -0
  165. package/src/report-view-client.js +50 -0
  166. package/src/reveal-output.js +29 -0
  167. package/src/roadmap-content.js +453 -0
  168. package/src/roadmap-personalization.js +132 -0
  169. package/src/roadmap-prompt.js +154 -0
  170. package/src/roles-client.js +114 -0
  171. package/src/roles-flow.js +192 -0
  172. package/src/sanitize-network-text.js +36 -0
  173. package/src/saved-positions-client.js +71 -0
  174. package/src/saved-positions-flow.js +50 -0
  175. package/src/scan-exclusions.js +50 -0
  176. package/src/scanner.js +612 -0
  177. package/src/scope-signals.js +148 -0
  178. package/src/secret-prompt.js +72 -0
  179. package/src/session-gate.js +30 -0
  180. package/src/session-refresh.js +51 -0
  181. package/src/session-scan.js +336 -0
  182. package/src/session-signals.js +36 -0
  183. package/src/shakers-wordmark.js +17 -0
  184. package/src/share-card.js +362 -0
  185. package/src/share-certification.js +111 -0
  186. package/src/share-consent-state.js +157 -0
  187. package/src/share-payload.js +300 -0
  188. package/src/share.js +184 -0
  189. package/src/show-project-client.js +58 -0
  190. package/src/show-project-flow.js +119 -0
  191. package/src/signup-client.js +91 -0
  192. package/src/skills-client.js +102 -0
  193. package/src/socials-client.js +91 -0
  194. package/src/socials-flow.js +83 -0
  195. package/src/specificity-extract.js +41 -0
  196. package/src/start-add-agents.js +306 -0
  197. package/src/start-add-portfolio.js +388 -0
  198. package/src/start-add-skills.js +260 -0
  199. package/src/stdin-ask.js +129 -0
  200. package/src/steering-extract.js +43 -0
  201. package/src/strip-internal-comments.js +45 -0
  202. package/src/tech-detector.js +295 -0
  203. package/src/templates/report-sheet.html +572 -0
  204. package/src/terminal-agents.js +199 -0
  205. package/src/terminal-format.js +85 -0
  206. package/src/terminal-progress.js +106 -0
  207. package/src/terminal-sections.js +219 -0
  208. package/src/tier-analysis.js +126 -0
  209. package/src/tier-engine.js +114 -0
  210. package/src/tier-projection.js +117 -0
  211. package/src/transcript-turns.js +49 -0
  212. package/src/translations-client.js +84 -0
  213. package/src/typewriter.js +154 -0
  214. package/src/usage-model-calls.js +119 -0
  215. package/src/usage-oneshot-commands.js +180 -0
  216. package/src/usage-repo-scope.js +162 -0
  217. package/src/usage-run-steps.js +338 -0
  218. package/src/work-streams.js +74 -0
@@ -0,0 +1,132 @@
1
+ 'use strict';
2
+
3
+ const { requestBackend } = require('./backend-request');
4
+ const { reporterFor, REASON } = require('./model-call-record');
5
+
6
+ // Roadmap personalization client (talents-ai-score, ADR-015).
7
+
8
+ const DEFAULT_TIMEOUT_MS = 8000;
9
+
10
+ /* ---------- signals (derived only, never raw content) ---------- */
11
+
12
+ // Builds the `signals` block from data this report ALREADY computes elsewhere (nothing new is read from disk here).
13
+ function buildRoadmapSignals(report, tierSignals) {
14
+ const r = report || {};
15
+ const ts = tierSignals || {};
16
+ return {
17
+ frameworks: Array.isArray(r.technologies) ? r.technologies : [],
18
+ toolCategories: r.summary && Array.isArray(r.summary.categories) ? r.summary.categories : [],
19
+ mcpCategories: (r.mcp && r.mcp.countsByCategory) || { data: 0, comms: 0, dev: 0, browser: 0, other: 0 },
20
+ // Agent NAMES only — never descriptions/prompts (same whitelist
21
+ // src/agent-org-chart.js's parseAgentOrgChart already enforces).
22
+ agents: Array.isArray(r.agents) ? r.agents.map((a) => a.name) : [],
23
+ agentCounts: r.agentCounts || { agents: 0, skills: 0, commands: 0, mcpServers: 0, hooks: 0 },
24
+ hooks: typeof ts.hooks === 'number' ? ts.hooks : 0,
25
+ automations: r.automations || null,
26
+ };
27
+ }
28
+
29
+ // Builds the exact wire request: `{tier, tierKey, curated, signals}`.
30
+ function buildRoadmapPersonalizationRequest(curatedEntry, tierResult, report, locale = null, consentGranted = false) {
31
+ return {
32
+ tier: tierResult.tier,
33
+ tierKey: tierResult.tierKey,
34
+ curated: {
35
+ whatUnlocks: curatedEntry.unlocks,
36
+ steps: curatedEntry.steps,
37
+ tips: curatedEntry.tips,
38
+ mistakes: curatedEntry.commonMistakes,
39
+ },
40
+ signals: buildRoadmapSignals(report, tierResult.signals),
41
+ // ADR-026: detected report language for the personalized prose.
42
+ ...(locale === 'es' || locale === 'en' ? { locale } : {}),
43
+ ...(consentGranted === true ? { traceContentConsent: true } : {}),
44
+ };
45
+ }
46
+
47
+ /* ---------- validation: all-or-nothing against the curated counts ---------- */
48
+
49
+ function isValidPersonalizedRoadmap(parsed, curated) {
50
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return false;
51
+ if (typeof parsed.whatUnlocks !== 'string' || !parsed.whatUnlocks.trim()) return false;
52
+
53
+ if (!Array.isArray(parsed.steps) || parsed.steps.length !== curated.steps.length) return false;
54
+ if (!parsed.steps.every((s) => s && typeof s.text === 'string' && s.text.trim())) return false;
55
+
56
+ if (!Array.isArray(parsed.tips) || parsed.tips.length !== curated.tips.length) return false;
57
+ if (!parsed.tips.every((tip) => typeof tip === 'string' && tip.trim())) return false;
58
+
59
+ if (!Array.isArray(parsed.mistakes) || parsed.mistakes.length !== curated.mistakes.length) return false;
60
+ if (!parsed.mistakes.every((m) => typeof m === 'string' && m.trim())) return false;
61
+
62
+ return true;
63
+ }
64
+
65
+ /* ---------- network (via the shared PRIMARY -> FALLBACK chain) ---------- */
66
+
67
+ // Requests the roadmap personalization endpoint.
68
+ async function requestRoadmapPersonalization(requestBody, { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS, onOutcome = null } = {}) {
69
+ // Issue 109 — see agent-synthesis.js#requestAgentSynthesis for the contract.
70
+ const outcome = reporterFor(onOutcome, { timeoutMs });
71
+ if (!endpoint) {
72
+ outcome.skip(REASON.NO_ENDPOINT);
73
+ return null;
74
+ }
75
+
76
+ let res;
77
+ try {
78
+ res = await requestBackend({ endpoint, body: requestBody, timeoutMs, trace: outcome.hops });
79
+ } catch (e) {
80
+ outcome.failFromError(e);
81
+ return null; // network error or timeout on every hop: never breaks the local report
82
+ }
83
+
84
+ if (res.status < 200 || res.status >= 300) {
85
+ outcome.fail(REASON.HTTP, { status: res.status, backend: res.backend });
86
+ return null;
87
+ }
88
+
89
+ let parsed;
90
+ try {
91
+ parsed = JSON.parse(res.raw);
92
+ } catch {
93
+ outcome.fail(REASON.MALFORMED_JSON, { status: res.status, backend: res.backend });
94
+ return null; // malformed (non-JSON) response body
95
+ }
96
+
97
+ if (!isValidPersonalizedRoadmap(parsed, requestBody.curated)) {
98
+ outcome.fail(REASON.UNEXPECTED_SHAPE, { status: res.status, backend: res.backend });
99
+ return null;
100
+ }
101
+
102
+ outcome.succeed({ status: res.status, backend: res.backend });
103
+ return {
104
+ whatUnlocks: parsed.whatUnlocks,
105
+ steps: parsed.steps,
106
+ tips: parsed.tips,
107
+ mistakes: parsed.mistakes,
108
+ };
109
+ }
110
+
111
+ /* ---------- merge: only the 4 prose gaps ever change ---------- */
112
+
113
+ function mergeRoadmapPersonalization(curatedEntry, personalized) {
114
+ if (curatedEntry.maxTier || !personalized) return curatedEntry;
115
+ const curatedForValidation = { steps: curatedEntry.steps, tips: curatedEntry.tips, mistakes: curatedEntry.commonMistakes };
116
+ if (!isValidPersonalizedRoadmap(personalized, curatedForValidation)) return curatedEntry;
117
+ return {
118
+ ...curatedEntry,
119
+ unlocks: personalized.whatUnlocks,
120
+ steps: personalized.steps,
121
+ tips: personalized.tips,
122
+ commonMistakes: personalized.mistakes,
123
+ };
124
+ }
125
+
126
+ module.exports = {
127
+ buildRoadmapSignals,
128
+ buildRoadmapPersonalizationRequest,
129
+ isValidPersonalizedRoadmap,
130
+ requestRoadmapPersonalization,
131
+ mergeRoadmapPersonalization,
132
+ };
@@ -0,0 +1,154 @@
1
+ 'use strict';
2
+
3
+ const { tierName: getTierName } = require('./i18n');
4
+
5
+ // Deterministic, ready-to-paste "implementation prompt" (talents-ai-score: "next steps -> prompt").
6
+
7
+ const TEMPLATES = {
8
+ es: {
9
+ intro: (frameworksText) =>
10
+ `Ayúdame a implementar el siguiente paso en mi proyecto${frameworksText ? ` (uso ${frameworksText})` : ''}.`,
11
+ context: (tierKey, tierName) =>
12
+ `Contexto: mi proyecto está en el tier ${tierKey} (${tierName}) según AI Footprint, una herramienta de auto-diagnóstico de uso de IA.`,
13
+ unlocksLabel: 'Lo que quiero conseguir:',
14
+ toolsLabel: 'Herramientas de IA que ya uso:',
15
+ stepsLabel: 'Pasos a seguir:',
16
+ snippetLabel: 'Punto de partida (referencia — adáptalo a mi proyecto real):',
17
+ closing:
18
+ 'Hazlo directamente en mi proyecto: crea o edita lo que haga falta, sigue las convenciones que ya uso, '
19
+ + 'y al final explícame brevemente qué has cambiado y por qué.',
20
+ introMax: (frameworksText) =>
21
+ `Ayúdame a consolidar y afinar el setup de IA de mi proyecto${frameworksText ? ` (uso ${frameworksText})` : ''}.`,
22
+ contextMax: (tierKey, tierName) =>
23
+ `Contexto: mi proyecto ya está en el tier máximo ${tierKey} (${tierName}) según AI Footprint. No busco añadir más herramientas, sino sacarle más partido a lo que ya tengo.`,
24
+ remainsLabel: 'Dónde está el margen ahora:',
25
+ stepsLabelMax: 'Refinamientos a aplicar:',
26
+ closingMax:
27
+ 'Revisa mi proyecto y aplícalo directamente: recorta lo que estorbe, afina lo existente y '
28
+ + 'sigue las convenciones que ya uso; al final explícame brevemente qué has cambiado y por qué.',
29
+ },
30
+ en: {
31
+ intro: (frameworksText) =>
32
+ `Help me implement the next step in my project${frameworksText ? ` (I use ${frameworksText})` : ''}.`,
33
+ context: (tierKey, tierName) =>
34
+ `Context: my project is at tier ${tierKey} (${tierName}) according to AI Footprint, a self-diagnostic tool for AI usage.`,
35
+ unlocksLabel: 'What I want to achieve:',
36
+ toolsLabel: 'AI tools I already use:',
37
+ stepsLabel: 'Steps to follow:',
38
+ snippetLabel: 'Starting point (reference — adapt it to my actual project):',
39
+ closing:
40
+ "Do it directly in my project: create or edit whatever's needed, follow the conventions I already use, "
41
+ + 'and briefly explain at the end what you changed and why.',
42
+ introMax: (frameworksText) =>
43
+ `Help me consolidate and sharpen my project's AI setup${frameworksText ? ` (I use ${frameworksText})` : ''}.`,
44
+ contextMax: (tierKey, tierName) =>
45
+ `Context: my project is already at the top tier ${tierKey} (${tierName}) according to AI Footprint. I'm not looking to add more tools, but to get more out of what I already have.`,
46
+ remainsLabel: 'Where the margin is now:',
47
+ stepsLabelMax: 'Refinements to apply:',
48
+ closingMax:
49
+ 'Review my project and apply it directly: trim what gets in the way, sharpen what exists and '
50
+ + 'follow the conventions I already use; briefly explain at the end what you changed and why.',
51
+ },
52
+ };
53
+
54
+ function resolveTemplate(lang) {
55
+ return TEMPLATES[lang] || TEMPLATES.es;
56
+ }
57
+
58
+ // `entry` is the roadmap entry ALREADY resolved by the caller (curated, or curated-with-personalization-merged — src/roadmap-personalization.js's mergeRoadmapPersonalization).
59
+ function buildImplementationPrompt(entry, report, maturity, lang) {
60
+ if (!entry) return null;
61
+ if (entry.maxTier) return buildConsolidationPrompt(entry, report, maturity, lang);
62
+
63
+ const T = resolveTemplate(lang);
64
+ const r = report || {};
65
+ const frameworks = Array.isArray(r.technologies) ? r.technologies : [];
66
+ const toolNames = Array.isArray(r.tools) ? r.tools.filter((x) => x && x.detected).map((x) => x.name) : [];
67
+ const tierKey = (maturity && maturity.tierKey) || entry.tierKey || '';
68
+ // talents-ai-score, i18n audit: NEVER maturity.tierName directly — that field comes straight from tier-engine.js and is Spanish-only by design (domain logic, not i18n).
69
+ const tierName = getTierName(tierKey, lang);
70
+
71
+ const lines = [];
72
+ lines.push(T.intro(frameworks.join(', ')));
73
+ lines.push('');
74
+ lines.push(T.context(tierKey, tierName));
75
+ lines.push('');
76
+ lines.push(T.unlocksLabel);
77
+ lines.push(entry.unlocks);
78
+
79
+ if (toolNames.length) {
80
+ lines.push('');
81
+ lines.push(T.toolsLabel);
82
+ lines.push(toolNames.join(', '));
83
+ }
84
+
85
+ lines.push('');
86
+ lines.push(T.stepsLabel);
87
+ const steps = Array.isArray(entry.steps) ? entry.steps : [];
88
+ steps.forEach((s, i) => {
89
+ lines.push(`${i + 1}. ${s.text}${s.estimate ? ` (${s.estimate})` : ''}`);
90
+ });
91
+
92
+ if (entry.snippet && entry.snippet.code) {
93
+ lines.push('');
94
+ lines.push(T.snippetLabel);
95
+ if (entry.snippet.filename) lines.push(`${entry.snippet.filename}:`);
96
+ lines.push('```' + (entry.snippet.language || ''));
97
+ lines.push(entry.snippet.code);
98
+ lines.push('```');
99
+ if (entry.snippet.secondFile) {
100
+ lines.push('');
101
+ lines.push(`${entry.snippet.secondFile.filename}:`);
102
+ lines.push('```' + (entry.snippet.language || ''));
103
+ lines.push(entry.snippet.secondFile.code);
104
+ lines.push('```');
105
+ }
106
+ }
107
+
108
+ lines.push('');
109
+ lines.push(T.closing);
110
+
111
+ return lines.join('\n');
112
+ }
113
+
114
+ function buildConsolidationPrompt(entry, report, maturity, lang) {
115
+ const steps = Array.isArray(entry.consolidationSteps) ? entry.consolidationSteps : [];
116
+ if (steps.length === 0 && !entry.whatRemains) return null;
117
+
118
+ const T = resolveTemplate(lang);
119
+ const r = report || {};
120
+ const frameworks = Array.isArray(r.technologies) ? r.technologies : [];
121
+ const toolNames = Array.isArray(r.tools) ? r.tools.filter((x) => x && x.detected).map((x) => x.name) : [];
122
+ const tierKey = (maturity && maturity.tierKey) || entry.tierKey || 'T7';
123
+ const tierName = getTierName(tierKey, lang);
124
+
125
+ const lines = [];
126
+ lines.push(T.introMax(frameworks.join(', ')));
127
+ lines.push('');
128
+ lines.push(T.contextMax(tierKey, tierName));
129
+
130
+ if (entry.whatRemains) {
131
+ lines.push('');
132
+ lines.push(T.remainsLabel);
133
+ lines.push(entry.whatRemains);
134
+ }
135
+
136
+ if (toolNames.length) {
137
+ lines.push('');
138
+ lines.push(T.toolsLabel);
139
+ lines.push(toolNames.join(', '));
140
+ }
141
+
142
+ if (steps.length) {
143
+ lines.push('');
144
+ lines.push(T.stepsLabelMax);
145
+ steps.forEach((s, i) => lines.push(`${i + 1}. ${s}`));
146
+ }
147
+
148
+ lines.push('');
149
+ lines.push(T.closingMax);
150
+
151
+ return lines.join('\n');
152
+ }
153
+
154
+ module.exports = { buildImplementationPrompt };
@@ -0,0 +1,114 @@
1
+ 'use strict';
2
+
3
+ // Role/cluster hub clients (talent Bearer): list the addable catalog, list the
4
+ // talent's own assigned roles, add a GROWTH role, and set the MAIN role.
5
+
6
+ const { postJsonWithTimeout } = require('./backend-request');
7
+ const { reasonForError } = require('./onboarding-client');
8
+
9
+ const DEFAULT_TIMEOUT_MS = 20000;
10
+
11
+ function parseJson(raw) {
12
+ try {
13
+ return raw ? JSON.parse(raw) : null;
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
18
+
19
+ function envelopeData(raw) {
20
+ const json = parseJson(raw);
21
+ if (!json || typeof json !== 'object') return undefined;
22
+ return json.status === 'OK' ? json.data : json;
23
+ }
24
+
25
+ function bearerHeaders(hubAccessToken) {
26
+ const headers = {};
27
+ if (typeof hubAccessToken === 'string' && hubAccessToken) headers.Authorization = `Bearer ${hubAccessToken}`;
28
+ return headers;
29
+ }
30
+
31
+ function normalizeRole(r) {
32
+ if (!r || typeof r !== 'object') return null;
33
+ const clusterId = typeof r.clusterId === 'string' ? r.clusterId : null;
34
+ if (!clusterId) return null;
35
+ return {
36
+ clusterId,
37
+ name: typeof r.name === 'string' ? r.name : null,
38
+ category: typeof r.category === 'string' ? r.category : null,
39
+ proposalKind: typeof r.proposalKind === 'string' ? r.proposalKind : null,
40
+ type: typeof r.type === 'string' ? r.type : null,
41
+ };
42
+ }
43
+
44
+ // GET works/certifications/me/available-roles -> { roles:[{clusterId,name,category,proposalKind}] }.
45
+ async function requestAvailableRoles({ hubAccessToken } = {}, { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
46
+ if (!endpoint) return { ok: false, reason: 'no-endpoint' };
47
+ if (!hubAccessToken) return { ok: false, reason: 'no-hub-token' };
48
+ let res;
49
+ try {
50
+ res = await postJsonWithTimeout(endpoint, null, timeoutMs, 'GET', null, bearerHeaders(hubAccessToken));
51
+ } catch (e) {
52
+ return { ok: false, reason: (e && e.kind) || 'network-error' };
53
+ }
54
+ if (res.status < 200 || res.status >= 300) return { ok: false, reason: reasonForError(res.status, res.raw) };
55
+ const data = envelopeData(res.raw);
56
+ const rows = data && Array.isArray(data.roles) ? data.roles : (Array.isArray(data) ? data : null);
57
+ if (!rows) return { ok: false, reason: 'bad-response' };
58
+ return { ok: true, roles: rows.map(normalizeRole).filter(Boolean) };
59
+ }
60
+
61
+ // GET works/talents/me/work-details/assigned-clusters -> the talent's GROWTH/RECOMMENDED roles.
62
+ async function requestAssignedClusters({ hubAccessToken } = {}, { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
63
+ if (!endpoint) return { ok: false, reason: 'no-endpoint' };
64
+ if (!hubAccessToken) return { ok: false, reason: 'no-hub-token' };
65
+ let res;
66
+ try {
67
+ res = await postJsonWithTimeout(endpoint, null, timeoutMs, 'GET', null, bearerHeaders(hubAccessToken));
68
+ } catch (e) {
69
+ return { ok: false, reason: (e && e.kind) || 'network-error' };
70
+ }
71
+ if (res.status < 200 || res.status >= 300) return { ok: false, reason: reasonForError(res.status, res.raw) };
72
+ const data = envelopeData(res.raw);
73
+ const rows = Array.isArray(data) ? data : (data && Array.isArray(data.clusters) ? data.clusters : null);
74
+ if (!rows) return { ok: false, reason: 'bad-response' };
75
+ return { ok: true, clusters: rows.map(normalizeRole).filter(Boolean) };
76
+ }
77
+
78
+ // POST works/talents/me/work-details/assigned-clusters { clusterId } -> adds a GROWTH role.
79
+ async function requestAddGrowthRole({ clusterId, hubAccessToken } = {}, { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
80
+ if (!endpoint) return { ok: false, reason: 'no-endpoint' };
81
+ if (!hubAccessToken) return { ok: false, reason: 'no-hub-token' };
82
+ if (typeof clusterId !== 'string' || !clusterId) return { ok: false, reason: 'no-cluster' };
83
+ let res;
84
+ try {
85
+ res = await postJsonWithTimeout(endpoint, { clusterId }, timeoutMs, 'POST', null, bearerHeaders(hubAccessToken));
86
+ } catch (e) {
87
+ return { ok: false, reason: (e && e.kind) || 'network-error' };
88
+ }
89
+ if (res.status < 200 || res.status >= 300) return { ok: false, reason: reasonForError(res.status, res.raw) };
90
+ return { ok: true, clusterId };
91
+ }
92
+
93
+ // PATCH works/certifications/me/main-role { clusterId } -> sets the preferred main role.
94
+ async function requestSetMainRole({ clusterId, hubAccessToken } = {}, { endpoint, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
95
+ if (!endpoint) return { ok: false, reason: 'no-endpoint' };
96
+ if (!hubAccessToken) return { ok: false, reason: 'no-hub-token' };
97
+ if (typeof clusterId !== 'string' || !clusterId) return { ok: false, reason: 'no-cluster' };
98
+ let res;
99
+ try {
100
+ res = await postJsonWithTimeout(endpoint, { clusterId }, timeoutMs, 'PATCH', null, bearerHeaders(hubAccessToken));
101
+ } catch (e) {
102
+ return { ok: false, reason: (e && e.kind) || 'network-error' };
103
+ }
104
+ if (res.status < 200 || res.status >= 300) return { ok: false, reason: reasonForError(res.status, res.raw) };
105
+ return { ok: true, clusterId };
106
+ }
107
+
108
+ module.exports = {
109
+ normalizeRole,
110
+ requestAvailableRoles,
111
+ requestAssignedClusters,
112
+ requestAddGrowthRole,
113
+ requestSetMainRole,
114
+ };
@@ -0,0 +1,192 @@
1
+ 'use strict';
2
+
3
+ // Shared role flows: add a GROWTH role, change the MAIN role, and the onboarding
4
+ // final main-role step. Every choice uses the dynamic arrow selector (io.select).
5
+
6
+ function makeRolesDeps(overrides = {}) {
7
+ const config = require('./config');
8
+ const client = require('./roles-client');
9
+ const certs = require('./certifications-client');
10
+ return {
11
+ getAvailableRolesEndpoint: config.getAvailableRolesEndpoint,
12
+ getSetMainRoleEndpoint: config.getSetMainRoleEndpoint,
13
+ getAssignedClustersEndpoint: config.getAssignedClustersEndpoint,
14
+ getMyCertificationsEndpoint: config.getMyCertificationsEndpoint,
15
+ requestAvailableRoles: client.requestAvailableRoles,
16
+ requestAssignedClusters: client.requestAssignedClusters,
17
+ requestAddGrowthRole: client.requestAddGrowthRole,
18
+ requestSetMainRole: client.requestSetMainRole,
19
+ requestMeCertifications: (args, opts) => certs.requestMeCertifications(args, opts),
20
+ ...overrides,
21
+ };
22
+ }
23
+
24
+ function roleLabel(role) {
25
+ if (!role) return '';
26
+ const name = role.name || role.clusterId;
27
+ return role.category ? `${name} — ${role.category}` : name;
28
+ }
29
+
30
+ function dedupeByCluster(roles) {
31
+ const seen = new Set();
32
+ const out = [];
33
+ for (const r of roles || []) {
34
+ if (!r || !r.clusterId || seen.has(r.clusterId)) continue;
35
+ seen.add(r.clusterId);
36
+ out.push(r);
37
+ }
38
+ return out;
39
+ }
40
+
41
+ async function chooseRole({ io, roles, header, hint, initialClusterId = null }) {
42
+ const initialMarked = [];
43
+ if (initialClusterId) {
44
+ const idx = roles.findIndex((r) => r.clusterId === initialClusterId);
45
+ if (idx >= 0) initialMarked.push(idx);
46
+ }
47
+ return io.select({ header, hint, items: roles, labelFor: roleLabel, initialMarked });
48
+ }
49
+
50
+ // `add-role`: list the addable catalog and add the chosen one as a GROWTH role.
51
+ async function runAddRole({ io, session, catalog, deps = makeRolesDeps() }) {
52
+ const r = catalog.roles;
53
+ const hubAccessToken = session ? session.hubAccessToken : null;
54
+
55
+ const listEndpoint = deps.getAvailableRolesEndpoint();
56
+ const res = await io.withProgress(r.loadingAvailable, () =>
57
+ deps.requestAvailableRoles({ hubAccessToken }, { endpoint: listEndpoint }));
58
+ if (!res.ok) {
59
+ io.error(r.fetchFailed(res.reason));
60
+ return { ok: false, reason: res.reason };
61
+ }
62
+ const roles = dedupeByCluster(res.roles);
63
+ if (roles.length === 0) {
64
+ io.notify(r.noneAvailable);
65
+ return { ok: true, added: null };
66
+ }
67
+
68
+ io.section(r.addTitle);
69
+ const choice = await chooseRole({ io, roles, header: r.addPrompt, hint: r.selectHint });
70
+ if (!choice) {
71
+ io.notify(r.cancelled);
72
+ return { ok: true, added: null };
73
+ }
74
+
75
+ const addEndpoint = deps.getAssignedClustersEndpoint();
76
+ const added = await io.withProgress(r.savingAdd, () =>
77
+ deps.requestAddGrowthRole({ clusterId: choice.clusterId, hubAccessToken }, { endpoint: addEndpoint }));
78
+ if (!added.ok) {
79
+ if (added.reason === 'already-assigned') { io.warn(r.alreadyAssigned(roleLabel(choice))); return { ok: true, added: choice.clusterId }; }
80
+ io.error(r.addFailed(added.reason));
81
+ return { ok: false, reason: added.reason };
82
+ }
83
+ io.success(r.addedOk(roleLabel(choice)));
84
+ return { ok: true, added: choice.clusterId };
85
+ }
86
+
87
+ // Candidate roles the talent already holds (their own GROWTH picks + growing-into),
88
+ // deduped, for setting/switching the MAIN role.
89
+ async function fetchHeldRoles({ deps, hubAccessToken }) {
90
+ const held = [];
91
+ let mainClusterId = null;
92
+ const certEndpoint = deps.getMyCertificationsEndpoint();
93
+ if (certEndpoint) {
94
+ const cert = await deps.requestMeCertifications({ hubAccessToken }, { endpoint: certEndpoint });
95
+ if (cert.ok) {
96
+ if (cert.mainRole && cert.mainRole.clusterId) { held.push(cert.mainRole); mainClusterId = cert.mainRole.clusterId; }
97
+ for (const g of cert.growingInto || []) held.push(g);
98
+ }
99
+ }
100
+ const assignedEndpoint = deps.getAssignedClustersEndpoint();
101
+ if (assignedEndpoint) {
102
+ const assigned = await deps.requestAssignedClusters({ hubAccessToken }, { endpoint: assignedEndpoint });
103
+ if (assigned.ok) for (const c of assigned.clusters || []) held.push(c);
104
+ }
105
+ return { held: dedupeByCluster(held), mainClusterId };
106
+ }
107
+
108
+ // `change-role`: switch the MAIN role to one of the roles the talent already holds.
109
+ async function runChangeMainRole({ io, session, catalog, deps = makeRolesDeps() }) {
110
+ const r = catalog.roles;
111
+ const hubAccessToken = session ? session.hubAccessToken : null;
112
+
113
+ const { held, mainClusterId } = await io.withProgress(r.loadingHeld, () => fetchHeldRoles({ deps, hubAccessToken }));
114
+ const candidates = held.filter((c) => c.clusterId !== mainClusterId);
115
+ if (candidates.length === 0) {
116
+ io.notify(r.noOtherRoles);
117
+ return { ok: true, changed: null };
118
+ }
119
+
120
+ io.section(r.changeTitle);
121
+ const current = held.find((c) => c.clusterId === mainClusterId);
122
+ if (current) io.notify(r.currentMain(roleLabel(current)));
123
+ const choice = await chooseRole({ io, roles: candidates, header: r.changePrompt, hint: r.selectHint });
124
+ if (!choice) {
125
+ io.notify(r.cancelled);
126
+ return { ok: true, changed: null };
127
+ }
128
+ return applySetMain({ io, deps, hubAccessToken, choice, catalog });
129
+ }
130
+
131
+ // The onboarding final step: pick a MAIN role from the roles resolved for the
132
+ // talent (interview recommendations + any held). Degrades gracefully to a skip.
133
+ async function runOnboardingMainRoleStep({ io, session, catalog, deps = makeRolesDeps() }) {
134
+ const r = catalog.roles;
135
+ const hubAccessToken = session ? session.hubAccessToken : null;
136
+
137
+ io.section(r.mainRoleTitle);
138
+ const { held, mainClusterId } = await io.withProgress(r.loadingHeld, () => fetchHeldRoles({ deps, hubAccessToken }));
139
+
140
+ // Prefer the talent's own/recommended roles; fall back to the catalog for a
141
+ // fresh talent so a main role can always be set (picking one adds it first).
142
+ let roles = held;
143
+ let fromCatalog = false;
144
+ if (held.length === 0) {
145
+ const av = await deps.requestAvailableRoles({ hubAccessToken }, { endpoint: deps.getAvailableRolesEndpoint() });
146
+ roles = av.ok ? dedupeByCluster(av.roles) : [];
147
+ fromCatalog = true;
148
+ }
149
+ if (roles.length === 0) {
150
+ io.notify(r.mainRolePending);
151
+ return { ok: true, chosen: null };
152
+ }
153
+ const choice = await chooseRole({
154
+ io, roles, header: r.mainRolePrompt, hint: r.selectHint, initialClusterId: mainClusterId,
155
+ });
156
+ if (!choice) {
157
+ io.notify(r.mainRoleSkipped);
158
+ return { ok: true, chosen: null };
159
+ }
160
+ if (choice.clusterId === mainClusterId) {
161
+ io.notify(r.mainRoleUnchanged(roleLabel(choice)));
162
+ return { ok: true, chosen: choice.clusterId };
163
+ }
164
+ // main-role requires the cluster to be a held assignment: add it first if it came from the catalog.
165
+ if (fromCatalog) {
166
+ await deps.requestAddGrowthRole({ clusterId: choice.clusterId, hubAccessToken }, { endpoint: deps.getAssignedClustersEndpoint() });
167
+ }
168
+ return applySetMain({ io, deps, hubAccessToken, choice, catalog });
169
+ }
170
+
171
+ async function applySetMain({ io, deps, hubAccessToken, choice, catalog }) {
172
+ const r = catalog.roles;
173
+ const endpoint = deps.getSetMainRoleEndpoint();
174
+ const set = await io.withProgress(r.savingMain, () =>
175
+ deps.requestSetMainRole({ clusterId: choice.clusterId, hubAccessToken }, { endpoint }));
176
+ if (!set.ok) {
177
+ io.error(r.setMainFailed(set.reason));
178
+ return { ok: false, reason: set.reason };
179
+ }
180
+ io.success(r.mainSetOk(roleLabel(choice)));
181
+ return { ok: true, chosen: choice.clusterId, changed: choice.clusterId };
182
+ }
183
+
184
+ module.exports = {
185
+ makeRolesDeps,
186
+ roleLabel,
187
+ dedupeByCluster,
188
+ fetchHeldRoles,
189
+ runAddRole,
190
+ runChangeMainRole,
191
+ runOnboardingMainRoleStep,
192
+ };
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+
3
+ // Making text that came off the wire SAFE TO DISPLAY.
4
+
5
+ // eslint-disable-next-line no-control-regex
6
+ const CONTROL_CHARS_RE = /[\x00-\x08\x0B-\x1F\x7F]/g;
7
+
8
+ const INVISIBLE_CHARS_RE =
9
+ /[\u00AD\u061C\u115F\u1160\u17B4\u17B5\u180E\u200B-\u200F\u2028\u2029\u202A-\u202E\u2060-\u2064\u2066-\u2069\u3164\uFEFF\uFFA0]|[\u{E0000}-\u{E007F}]/gu;
10
+
11
+ function stripControlChars(s) {
12
+ if (typeof s !== 'string') return '';
13
+ return s.replace(CONTROL_CHARS_RE, '');
14
+ }
15
+
16
+ // Replaces every bidi/invisible character with a visible `[U+XXXX]` token.
17
+ function markInvisibleChars(s) {
18
+ if (typeof s !== 'string') return '';
19
+ return s.replace(INVISIBLE_CHARS_RE, (ch) => {
20
+ const hex = ch.codePointAt(0).toString(16).toUpperCase();
21
+ return `[U+${hex.length < 4 ? hex.padStart(4, '0') : hex}]`;
22
+ });
23
+ }
24
+
25
+ // What every renderer should call on text that came off the wire before painting it.
26
+ function sanitizeRenderText(s) {
27
+ return markInvisibleChars(stripControlChars(s));
28
+ }
29
+
30
+ // `jsonEscapeInvisibleChars` WAS HERE, and what is missing is the SURFACE, not the helper.
31
+
32
+ module.exports = {
33
+ stripControlChars,
34
+ markInvisibleChars,
35
+ sanitizeRenderText,
36
+ };