skills/connect-draft/SKILL.md
End-to-end Microsoft Connect (half-yearly performance review) drafting + inject into the Connect tool. Trigger when user mentions Connect, connect draft, 绩效盘点, Microsoft performance review, "write my connect", "draft my connect", "帮我写 connect", or asks to populate v2.msconnect.microsoft.com. Gathers evidence (ADO work items + PRs, SharePoint-authored docs via workiq, historical Connects via Playwright for style), builds a local sign-off flow chart for the user to review, then injects HTML-formatted content (with hyperlinks, nested lists, underlines) directly into the Roosterjs rich-text editor fields via simulated paste events. Asks the user for period dates, repos, SharePoint URLs, and historical Connect IDs at runtime because these vary per person.
npx skillsauth add liulixiang1988/agent-skills connect-draftInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Assemble a Connect (Microsoft performance review) from hard evidence, let the user sign off locally, then inject into the Connect tool — HTML formatting intact.
v2.msconnect.microsoft.com URL or mentions Connect periodmcp__playwright__* — browser_navigate, browser_evaluate, browser_snapshot, browser_take_screenshot, browser_closemcp__workiq__ask_work_iq — SharePoint doc author + content + URL lookupmcp__ado__* — wit_my_work_items, wit_get_work_items_batch_by_ids, search_workitem, repo_search_commits, repo_list_pull_requests_by_repo_or_project, repo_get_repo_by_name_or_idIf any are missing, tell the user and ask them to /mcp to reconnect.
These vary per person — always ask, never assume.
1. Connect period: start date → end date (e.g., "Oct 18, 2025 → Apr 23, 2026")
2. ADO organization (e.g., "o365exchange.visualstudio.com"), project (e.g., "O365 Core")
3. ADO user email (for wit_my_work_items filter)
4. Relevant repos to search for the user's commits/PRs
(e.g., CARESPlat, TenantSearch, Mimir — ask, because repos differ per team)
5. SharePoint doc inventory:
- Either a list of doc titles the user suspects they authored
- Or a SharePoint folder URL for workiq to sweep
6. ⭐ Historical Connect URLs (1-3) for style + voice reference — MANDATORY
Format: v2.msconnect.microsoft.com/historyrag?connectid=<GUID>&pernr=<ID>
If the user can't share URLs, fall back to auto-discovery (see Stage 1d).
7. New Connect URL (the draft page to populate)
Usually just: https://v2.msconnect.microsoft.com/
8. Language preference for the draft (default: match the user's prior Connects;
fall back to English if no priors available)
Don't block on all 8. Ask the first 5 upfront; ask #6-#8 just before the phases that need them. Never skip #6 — voice matching is the primary lever for making the output feel like the user's own writing, not a generic template.
Spawn these in parallel where possible (they don't touch each other):
wit_my_work_items(project, type=myactivity, includeCompleted=true, top=200)
wit_my_work_items(project, type=assignedtome, includeCompleted=true, top=200)
wit_get_work_items_batch_by_ids(ids, fields=[Title, WorkItemType, State, IterationPath, AreaPath, Tags, CreatedDate, ChangedDate, AssignedTo, ClosedDate, Parent])
Filter: keep where ChangedDate ∈ [start, end]. Theme/group items by parent feature or area-path. Save to .claude/connect-draft/ado-inventory.md.
For each repo:
repo_get_repo_by_name_or_id(project, repoName) // sanity check repo exists
repo_search_commits(project, repo, author=<first-name-fragment>, fromDate, toDate, top=50)
If email filter returns empty, use author: <first name fragment> — ADO ADO email + display name matching is quirky. Save PR IDs for hyperlinks.
For each suspect doc title (from user's list):
ask_work_iq("Find the SharePoint document titled '<title>'. Return author, sharepoint URL, 3-sentence summary, any concrete numbers/metrics. If authored by someone other than <user email>, just return author.")
Batch related titles in one question. Save as .claude/connect-draft/cares-docs-authored.md (or similar per-topic).
This is the load-bearing step for voice matching. Skipping it produces a generic template that sounds nothing like the user.
If user provided URLs:
browser_navigate(connectUrl)
browser_evaluate(() => ({
title: document.querySelector('h1').textContent,
text: document.querySelector('main').innerText
}))
If user didn't provide URLs — auto-discover from the Connect home page:
browser_navigate('https://v2.msconnect.microsoft.com/')
browser_evaluate(() => {
// Look for a "History" / "Past connects" section with links to past Connects
const anchors = Array.from(document.querySelectorAll('a[href*="historyrag"], a[href*="connectid="]'));
return anchors.map(a => ({ href: a.href, text: a.textContent.trim() }));
})
Pick the 2-3 most recent past Connects from the list. Confirm with the user before reading ("I found your last 3 Connects — use these for style reference?"). Only proceed without confirmation if the user explicitly said "just use my prior ones".
Persist each Connect's extracted text to .claude/connect-draft/connect-<n>-<period>.md. Extract these specifically:
[Project Name] tags? **Project: X** headers? Bare project names? Nested <ul>? Match it.Save a .claude/connect-draft/voice-profile.md summarizing the extracted style patterns — this becomes the style guide for Stage 2.
Navigate hrweb/sitepages/connects.aspx, hrweb/SitePages/perfphilosophy.aspx, hrweb/SitePages/Performance-and-Development-Evolution-Frequently-Asked-Questions.aspx. For the FAQ page, click-expand all aria-expanded="false" before extracting. Save to .claude/connect-draft/hr-philosophy.md.
Key takeaways to record:
Write Claude Wiki/Connect/FY<year>-<month>-WorkFlowChart.md with:
Write Claude Wiki/Connect/FY<year>-<month>-Connect-Body-HTML.md with paste-ready HTML per field:
Q1 · What results did you deliver, and how did you do it?
Q2 · Reflect on recent setbacks
Q3 · Goal #1/#2/#3 descriptions (+titles)
Q4 · How will your actions and behaviors help you reach your goals?
Structure to use (nested, matches user preference):
<p>Opening sentence w/ three arcs.</p>
<p><strong>What is delivered?</strong><br><em>[prompt reminder]</em></p>
<ul>
<li><strong>Project A</strong>
<ul>
<li><strong>Feature 1:</strong> impact sentence with hyperlinks...</li>
<li><strong>Feature 2:</strong> ...</li>
</ul>
</li>
<li><strong>Project B</strong>...</li>
</ul>
<p><strong>How are they delivered?</strong>...</p>
Hyperlink every referenced artifact:
https://<org>/<project>/_git/<repo>/pullrequest/<id>https://<org>/<project>/_git/<repo>After writing the draft, stop and ask the user:
Iterate until user says "go inject". Don't inject unilaterally.
browser_navigate(newConnectUrl)
// Check editors exist + capture baseline
browser_evaluate(() => {
const editors = Array.from(document.querySelectorAll('[contenteditable="true"][role="textbox"]'));
return editors.map(e => ({id: e.id, aria: e.getAttribute('aria-label'), html: e.innerHTML, text: e.innerText}));
})
Save baseline to .claude/connect-draft/connect-backup-before-inject.json. User can revert by hand from this backup.
async function injectViaPaste(editor, html) {
editor.focus();
const sel = window.getSelection();
const range = document.createRange();
range.selectNodeContents(editor);
sel.removeAllRanges();
sel.addRange(range);
const dt = new DataTransfer();
dt.setData('text/html', html);
dt.setData('text/plain', editor.textContent);
editor.dispatchEvent(new ClipboardEvent('paste', {
bubbles: true, cancelable: true, clipboardData: dt
}));
await new Promise(r => setTimeout(r, 250));
}
This is the only way to preserve formatting — raw innerHTML doesn't update Roosterjs's internal model, and DOM text copy loses formatting.
Locate by aria-label substring:
"What results did you deliver" → Q1 editor"Reflect on recent setbacks" → Q2 editor"Description for goal number" → Goal description (per-goal)"How will your actions and behaviors" → Q4 editorfunction setInputValue(input, value) {
const setter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype, 'value'
).set;
setter.call(input, value);
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
}
Required because React tracks value via its own descriptor; plain input.value = x won't trigger state update.
Find via: document.querySelectorAll('input[aria-label*="Title for goal" i]').
const btn = Array.from(document.querySelectorAll('button'))
.find(b => (b.getAttribute('aria-label') || '').toLowerCase() === 'add a new goal');
btn.click();
await new Promise(r => setTimeout(r, 800));
// Re-query title inputs + desc editors; the new one is at index [2]
Roosterjs strips <u> inside <a> and inline text-decoration: underline during paste sanitization. Work around by DOM-wrapping after paste:
for (const editor of editors) {
for (const a of editor.querySelectorAll('a')) {
if (a.querySelector('u')) continue;
const u = document.createElement('u');
while (a.firstChild) u.appendChild(a.firstChild);
a.appendChild(u);
a.style.textDecoration = 'underline';
}
editor.dispatchEvent(new Event('input', { bubbles: true }));
}
This persists: Roosterjs serializes the current DOM when the user clicks Save as draft.
The page shows counter elements like 5054/6000. Grab them:
Array.from(document.querySelectorAll('*'))
.filter(el => /^\d{2,5}\/\d{3,5}$/.test((el.textContent || '').trim()) && !el.children.length)
.map(el => el.textContent.trim())
Hard limits to respect:
If over, tell the user and offer to trim (HOW section compresses well — collapse nested bullets into sentences).
browser_take_screenshot({ fullPage: true, filename: 'connect-after-inject.png' })
Scroll tip: Q1/Q2/Q4 editor bodies are internally scrollable (.pi-editor-textarea has overflow: auto). To see all content in screenshot, scroll the editor itself via editor.scrollTop = 0 — page-level window.scrollTo won't affect inside.
Never click Save as draft automatically. Report completion with:
[PLACEHOLDER] / <TBD> markers by location (user uses Ctrl+F to find)[Project] bracket tags for each bullet, or nested <ul> per top-level project[PLACEHOLDER] for any partner-team / reviewer / cross-team person names<u> post-paste to force underline (Roosterjs strips it from paste HTML)/mcp reconnect.author: "kefan"). Email-suffix variants like [email protected] also appear — sweep both.null for files it can't index (different team site, email attachments, .vsdx Visio). Don't assume missing = not-user-authored; tell user and let them confirm manually..aspx pages use canvas rendering for Word/PPT viewers. DOM innerText returns 0 chars. Use workiq for doc content; use Playwright only for static HTML pages.[aria-expanded="false"] before extracting text.<h1> date label — old ones have "Three Circles of Impact" framing; new ones have 4 questions directly. Match the format of the NEW Connect being populated.User: "帮我写下一个 connect"
Claude:
I'll gather evidence in parallel, build a local flow chart for your review, then inject into the Connect tool once you sign off. A few inputs I need up front because they vary per person:
- What's the Connect reflection period? (start → end date)
- Your ADO email for the
assignedTofilter- Which repos should I search for your PRs? (e.g., names you've pushed to in that window)
- SharePoint doc titles you think you've authored in that window (or a folder to sweep)
- 1-3 historical Connect URLs (
v2.msconnect.microsoft.com/historyrag?connectid=...) for style referenceAfter you answer, I'll kick off ADO + workiq + Playwright research in parallel, then come back with a flow chart to review.
.claude/connect-draft/ado-inventory.md — work items inventory.claude/connect-draft/commits-<repo>.md — PR/commit inventory per repo.claude/connect-draft/docs-authored.md — SharePoint-authored docs.claude/connect-draft/connect-1|2|3-<period>.md — historical Connect extracts.claude/connect-draft/hr-philosophy.md — HR framework notes.claude/connect-draft/connect-backup-before-inject.json — pre-injection baseline (for revert)Claude Wiki/Connect/FY<Y>-<M>-WorkFlowChart.md — Obsidian sign-off flow chartClaude Wiki/Connect/FY<Y>-<M>-Connect-Body-HTML.md — HTML draft (for audit + backup)development
Register and verify a sandbox-hosted HTTP service through Lumina App Proxy (LuminaProxyAPI). Use when the user wants to spin up a tiny FastAPI server inside a Lumina sandbox, expose it under {appId}.{BaseDomain}, hand the resulting URL to teammates, or compare access_scope=owner vs access_scope=all behavior of the AppProxyAuthHandler. Pairs with lumina-eps-token to acquire the bearer token and create the sandbox first.
tools
Acquire and validate Lumina EPS/LuminaServiceAPI bearer tokens using the CopilotLumina eps_client.py and get-lumina-token.ts helpers. Use when the user asks about Lumina token acquisition, EPS client authentication, testing luminaserviceapi hosts, running eps_client.py, validating v1/v3 EPS routes, resolving bundled helper script paths across .agents/.claude/.copilot installs, or fixing local Bun/Python/uv environment issues for these flows.
testing
Pulls 3-year financial statements (income, balance sheet, cash flow) for a single stock and produces a deep Chinese-language report, OR compares multiple peers head-to-head. Use whenever the user asks to 分析/解读/看一下 a company's 财报/财务/营收/利润/资产负债/现金流, asks for 三年/近三年/最近几年 financials, or wants to compare multiple stocks 对比/PK/比较 on financial metrics. Triggers on phrases like "帮我看下 X 的财报"、"分析 X 这三年的财务"、"X 和 Y 哪个更好"、"对比 A B C 三家". Covers US/HK/KR/A-share stocks via stockanalysis.com.
tools
Record work log / save work summary / add TODO items. TRIGGER when: user says '记录工作', '保存工作', 'record work', 'save work', 'log work', '工作记录', '写工作日志', '保存工作记录', '记录一下', or similar phrases about saving/recording what was done in the current session. Also trigger when user mentions work log, work record, 工作日志, or wants to summarize completed work for future performance review. Also trigger when user says '加一个todo', 'add a todo', '添加todo', '加个待办', '记录todo', or similar phrases about adding a TODO/待办 item to the work log.