From 9b0a9b0409741620fc315a8db966b3980de07063 Mon Sep 17 00:00:00 2001 From: Daniel Kucinski Date: Thu, 3 Sep 2026 17:54:17 +0100 Subject: [PATCH] chore: sync Scout skills 2026-09-03 --- .gitignore | 5 + README.md | 40 + ado-triage/SKILL.md | 99 ++ brainstorming/SKILL.md | 51 + chapan-intake/SKILL.md | 41 + chapan-leads/SKILL.md | 44 + chapan-programs/SKILL.md | 42 + chapan-visa/SKILL.md | 42 + chapan/SKILL.md | 40 + clawpilot-chats-digest/SKILL.md | 71 ++ clawvibe/SKILL.md | 48 + connect/SKILL.md | 150 +++ dispatching-parallel-agents/SKILL.md | 41 + .../2026-09-02-promptworks-mirror.md | 153 +++ docs/plans/2026-09-02-promptworks-design.md | 41 + executing-plans/SKILL.md | 42 + finishing-a-development-branch/SKILL.md | 74 ++ flow-1on1/Add-TrackerEntry.ps1 | 187 ++++ flow-1on1/Read-Tracker.ps1 | 89 ++ flow-1on1/SKILL.md | 220 ++++ get-meeting-transcript/.gitignore | 3 + get-meeting-transcript/SKILL.md | 369 +++++++ get-meeting-transcript/package-lock.json | 59 ++ get-meeting-transcript/package.json | 10 + get-meeting-transcript/scrape-transcript.mjs | 952 ++++++++++++++++++ install-system-sync-task.ps1 | 30 + kb/SKILL.md | 39 + kblinter/SKILL.md | 120 +++ mslearn/SKILL.md | 69 ++ oneonone-pokedex/SKILL.md | 114 +++ receiving-code-review/SKILL.md | 42 + requesting-code-review/SKILL.md | 73 ++ rise-15-5/SKILL.md | 188 ++++ subagent-driven-development/SKILL.md | 65 ++ sync-skills.ps1 | 164 +++ systematic-debugging/SKILL.md | 46 + teams-export/.gitignore | 3 + teams-export/SKILL.md | 226 +++++ teams-export/package-lock.json | 60 ++ teams-export/package.json | 16 + teams-export/scrape-channel.mjs | 472 +++++++++ test-driven-development/SKILL.md | 51 + using-git-worktrees/SKILL.md | 57 ++ verification-before-completion/SKILL.md | 44 + writing-plans/SKILL.md | 87 ++ writing-skills/SKILL.md | 52 + 46 files changed, 4931 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 ado-triage/SKILL.md create mode 100644 brainstorming/SKILL.md create mode 100644 chapan-intake/SKILL.md create mode 100644 chapan-leads/SKILL.md create mode 100644 chapan-programs/SKILL.md create mode 100644 chapan-visa/SKILL.md create mode 100644 chapan/SKILL.md create mode 100644 clawpilot-chats-digest/SKILL.md create mode 100644 clawvibe/SKILL.md create mode 100644 connect/SKILL.md create mode 100644 dispatching-parallel-agents/SKILL.md create mode 100644 docs/implementationplans/2026-09-02-promptworks-mirror.md create mode 100644 docs/plans/2026-09-02-promptworks-design.md create mode 100644 executing-plans/SKILL.md create mode 100644 finishing-a-development-branch/SKILL.md create mode 100644 flow-1on1/Add-TrackerEntry.ps1 create mode 100644 flow-1on1/Read-Tracker.ps1 create mode 100644 flow-1on1/SKILL.md create mode 100644 get-meeting-transcript/.gitignore create mode 100644 get-meeting-transcript/SKILL.md create mode 100644 get-meeting-transcript/package-lock.json create mode 100644 get-meeting-transcript/package.json create mode 100644 get-meeting-transcript/scrape-transcript.mjs create mode 100644 install-system-sync-task.ps1 create mode 100644 kb/SKILL.md create mode 100644 kblinter/SKILL.md create mode 100644 mslearn/SKILL.md create mode 100644 oneonone-pokedex/SKILL.md create mode 100644 receiving-code-review/SKILL.md create mode 100644 requesting-code-review/SKILL.md create mode 100644 rise-15-5/SKILL.md create mode 100644 subagent-driven-development/SKILL.md create mode 100644 sync-skills.ps1 create mode 100644 systematic-debugging/SKILL.md create mode 100644 teams-export/.gitignore create mode 100644 teams-export/SKILL.md create mode 100644 teams-export/package-lock.json create mode 100644 teams-export/package.json create mode 100644 teams-export/scrape-channel.mjs create mode 100644 test-driven-development/SKILL.md create mode 100644 using-git-worktrees/SKILL.md create mode 100644 verification-before-completion/SKILL.md create mode 100644 writing-plans/SKILL.md create mode 100644 writing-skills/SKILL.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..03c99e5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +logs/ +node_modules/ +.pw-profile*/ +.token-cache.json +*.log diff --git a/README.md b/README.md new file mode 100644 index 0000000..d5eec0c --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +# PromptWorks + +Private version-controlled mirror of custom Microsoft Scout skills. + +Generated by `sync-skills.ps1`; do not edit manually. + +## Skills + +| Skill | Description | +| --- | --- | +| [ado-triage](ado-triage/SKILL.md) | Triage Daniel's Azure DevOps work items assigned to him across both orgs - OE on office.visualstudio.com and ES365 on dev.azure.com/enginfra. Categorizes each open item as needing an update, missing KB info, or stale/needs cleanup; scores severity and importance using Obsidian KB context; then walks through updates one at a time. Triggers: /ado-triage, check my ADO queue, triage my work items, review my work item queue, go through my ADO items, what am I assigned, my ADO backlog. | +| [brainstorming](brainstorming/SKILL.md) | Explore intent, requirements, and design BEFORE any creative or build work - new features, components, functionality, behavior changes, refactors, or new tools and scripts. Produces a reviewed spec instead of jumping straight to code. Use when a request is vague, open-ended, or bigger than a one-line change, and any time the user says: let us build X, can you add X, I want a tool that, how should we design this, brainstorm, spec this out, what are the options. Run before writing-plans. | +| [chapan](chapan/SKILL.md) | Mission control for the Chapan Hokkaido relocation project. Dashboard, router, and digest over the tracker at D:\\Repos\\Chapan. Use when the user says /chapan, \"chapan\", \"chapan status\", \"chapan dashboard\", \"chapan digest\", or asks how the Japan/Hokkaido relocation is going. Routes to chapan-intake, chapan-visa, chapan-programs, chapan-leads. | +| [chapan-intake](chapan-intake/SKILL.md) | The recurring interview for the Chapan Hokkaido relocation mission. Use when the user says \"chapan check-in\", \"chapan intake\", \"chapan interview\", /chapan-intake, or when a weekly nudge asks Daniel for a relocation status update. Interviews Daniel, updates D:\\Repos\\Chapan profile.md and status.md, appends a dated log, and turns his findings into tracked visa/program/lead dossiers. | +| [chapan-leads](chapan-leads/SKILL.md) | Deep-dive a specific Hokkaido locality (or a program at a locality) into a structured dossier for the Chapan relocation mission by investigating its official municipal/prefectural website. Use when the user says \"chapan lead\", \"chapan leads\", \"investigate \", /chapan-leads, or names a Hokkaido town/village to research. Writes D:\\Repos\\Chapan\\leads\\Hokkaido\\.md and keeps the leads index current. | +| [chapan-programs](chapan-programs/SKILL.md) | Research and track Japanese relocation and rural-revitalization support programs for the Chapan Hokkaido mission — national (移住支援金, 起業支援金, 地域おこし協力隊, 空き家バンク) and Hokkaido prefectural/municipal. Use when the user says \"chapan programs\", /chapan-programs, or asks about relocation grants, subsidies, akiya banks, or revitalization schemes. Writes/updates D:\\Repos\\Chapan\\programs\\*.md with source URLs and retrieved dates. | +| [chapan-visa](chapan-visa/SKILL.md) | Research and track Japanese visa pathways for the Chapan Hokkaido relocation, evaluated against Daniel's family profile. Use when the user says \"chapan visa\", /chapan-visa, asks which visa fits, or wants visa requirements/rules checked or refreshed. Searches official sources (ISA, MOFA, JETRO), writes/updates D:\\Repos\\Chapan\\visa\\*.md dossiers with a source URL and retrieved date. | +| [clawpilot-chats-digest](clawpilot-chats-digest/SKILL.md) | Generate an on-demand digest of recent activity in the Clawpilot/Lobster Pound Teams spaces — the new MCAPS Lobster Pound channel (primary, going forward) plus the two original group chats (MCAPS Lobster Pound and 'ClawPilot: From Copilot to Agent') — surfacing major Clawpilot releases, novel usage patterns, and items worth a closer look for Daniel. Triggers: /clawpilot-chats-digest, 'clawpilot digest', 'clawpilot chats digest', 'what's new in the clawpilot chats', 'lobster pound digest'. | +| [clawvibe](clawvibe/SKILL.md) | Explicitly invoked (e.g. /clawvibe, \"let's clawvibe this\", \"run the full workflow on this\") to route a specific task through the complete brainstorm -> plan -> implement -> review -> finish pipeline using the locally installed skills. Does NOT trigger automatically — opt-in only, unlike a standing gatekeeper. | +| [connect](connect/SKILL.md) | Manage Microsoft Connect (perf review) tracking inside the Obsidian vault. Reads the active Connect markdown in D:\\Repos\\Obsidian\\04 - Connects\\, cross-references the rest of the vault for evidence (daily notes, squad meetings, status updates, decisions), and emits weekly check-ins, draft narratives, 1:1 prep digests, and gap warnings. Triggers: 'connect', 'connect check-in', 'connect prep', 'connect draft', or any /connect subcommand. | +| [dispatching-parallel-agents](dispatching-parallel-agents/SKILL.md) | Fan work out to multiple sub-agents at once when a task splits into 2+ independent pieces with no shared state or ordering dependency - auditing several modules, researching unrelated questions, or applying the same change across separate files. Covers agent prompt structure, common mistakes, and verifying results after agents return. Triggers: do these in parallel, spin up agents, run these at once, split this work, parallelize this. | +| [executing-plans](executing-plans/SKILL.md) | Execute an already-written implementation plan document task by task, with verification and review checkpoints between steps. Use when a plan.md or spec already exists and the work now needs carrying out, including knowing when to stop and ask for help versus revisit earlier steps. Triggers: execute the plan, work through the plan, implement the plan, start on the plan, next task in the plan, follow the spec. Pairs with writing-plans and subagent-driven-development. | +| [finishing-a-development-branch](finishing-a-development-branch/SKILL.md) | Decide how to integrate finished work once implementation is done and tests pass - verifies tests, detects the environment, determines the base branch, then presents structured options (merge, open a PR, keep the branch, or discard) and cleans up the workspace or worktree afterward. Triggers: I am done with this branch, wrap this up, merge this, should I open a PR, finish the feature, clean up the worktree, what do I do with this branch now. | +| [flow-1on1](flow-1on1/SKILL.md) | Prep Daniel's biweekly 1:1 with his manager Ahmed Atya (ES365 FLOW team). Reviews activity since the last 1:1, co-authors Ahmed's four-question notebook-tracker template one section at a time, writes the dated entry into the Daniel-Ahmed.docx tracker, then builds a private talking-points and issues brief. Triggers: /flow-1on1, prep my Ahmed 1:1, 1:1 with Ahmed, notebook tracker, manager 1:1 prep, flow 1:1. | +| [get-meeting-transcript](get-meeting-transcript/SKILL.md) | Grab a Teams/Stream meeting transcript by driving a logged-in browser when the Graph download is blocked. Captures the underlying .vtt from network traffic, or scrapes the recap transcript pane. Accepts a Teams recap, Stream/SharePoint, or meet.microsoft.com URL, or a calendar reference it resolves to one. Saves .vtt + .md + the meeting chat into the Obsidian vault. Triggers: get transcript, scrape transcript, grab the transcript, meeting chat, transcript for my meeting. | +| [kb](kb/SKILL.md) | Vault-first recall. Search Daniel's Obsidian KB (D:\\Repos\\Obsidian) BEFORE WorkIQ/M365, Azure DevOps, IcM, or the browser. Use for any lookup: \"who is X\", \"what's my/his alias, SC-ALT, email, ID, manager, office\", \"what did we decide about X\", \"when did we meet about X\", \"what is \", \"remind me\", \"do we know\", \"look it up\", \"check the KB\", plus anything about squads, Connects, RISE, meetings, daily notes, or people Daniel works with. | +| [kblinter](kblinter/SKILL.md) | Run the knowledge-base linter against the Obsidian vault at D:\\Repos\\Obsidian and surface the findings. Use whenever the user asks to lint, audit, or check the health of the vault/Second Brain. Triggers: /kblinter, \"lint the vault\", \"audit my notes\", \"check vault health\", \"run the kb linter\", \"find orphan notes\", \"find broken links in the vault\". | +| [mslearn](mslearn/SKILL.md) | Query the live Microsoft Learn MCP for grounded, first-party docs, code samples, and full-page fetches across Azure, M365, .NET, and Power Platform. Use when the user asks what Microsoft says about X, needs an authoritative answer for a customer, wants current official guidance, looks for a code sample, or asks to cite or quote the docs. Triggers: ms learn, microsoft learn, learn docs, look it up on learn, cite the docs, official microsoft guidance, what do the docs say. | +| [oneonone-pokedex](oneonone-pokedex/SKILL.md) | IC-side companion for Daniel's recurring 1:1 with his manager - the mirror of the manager coaching-pokedex. Preps an agenda from the Obsidian vault, reflects on a recorded 1:1 with an honest self-scorecard of how Daniel showed up (never grading the manager), assigns a Pokemon for session energy, tracks trends, and writes notes back to the vault as Connect evidence. Private. Triggers: /1on1, /oneonone, 1:1, one on one, prep for my 1:1, reflect on my 1:1, manager sync. | +| [receiving-code-review](receiving-code-review/SKILL.md) | Handle incoming code review feedback with technical rigor instead of reflexive agreement - verify each claim, push back with evidence when a suggestion is wrong or does not apply, ask when feedback is unclear, and apply a YAGNI check before adding suggested professional features. Covers GitHub thread replies and implementation order. Triggers: here is the review feedback, address these comments, the reviewer said, respond to this PR comment, they want me to change. | +| [requesting-code-review](requesting-code-review/SKILL.md) | Request an independent review of completed work before merging or declaring it done - builds the reviewer prompt, states what was implemented, the requirements, and the exact git range to review, and demands a read-only critical review with calibrated severity. Triggers: review my changes, can you check this over, is this ready to merge, get a second opinion, code review this, check my work before I ship it. | +| [rise-15-5](rise-15-5/SKILL.md) | Co-author and post Daniel's weekly RISE 15-5 status update to the \"RISE Standups\" Loop doc. Reviews the past week's activity, walks through each required section (Accomplishments, Work in Progress, Blockers/Risks, Focus for Next Week, Help Needed) one at a time for feedback/co-authoring, then posts the finished entry to the shared Loop doc. Triggers: /rise-15-5, \"my 15-5\", \"weekly status\", \"post my 15-5\", \"RISE status update\". | +| [subagent-driven-development](subagent-driven-development/SKILL.md) | Execute a multi-task implementation plan in the current session by dispatching each task to an implementer sub-agent and then a reviewer sub-agent, with pre-flight plan review, model selection, status handling, and durable progress tracking between tasks. Use when the plan has independent tasks and you want them built and reviewed without burning main-session context. Triggers: work through the plan with subagents, delegate these tasks, build this out task by task. | +| [systematic-debugging](systematic-debugging/SKILL.md) | Find the actual root cause before proposing any fix - investigate, analyze patterns across similar cases, form and test a hypothesis, then implement. Use at the first sign of ANY bug, test failure, crash, error message, flaky test, or behavior that does not match expectations, and especially when tempted to try a quick patch. Triggers: this is broken, it is failing, why does this not work, weird error, test is red, it worked yesterday, intermittent failure. | +| [teams-export](teams-export/SKILL.md) | Export Microsoft Teams messages (1:1 chats, group chats, and channel posts) into the Obsidian vault at D:\\Repos\\Obsidian\\00 - Chats\\ as markdown. Reads the user-maintained source list at D:\\Repos\\Obsidian\\00 - Chats\\sources.yaml to know what to copy. The invoking prompt or automation decides the time range (e.g. last N messages, today, since last export, explicit date range). Triggers: 'teams export', 'export chat', 'export channel', 'copy teams messages', '/teams-export'. | +| [test-driven-development](test-driven-development/SKILL.md) | Write the failing test before the implementation, for every feature or bugfix - red, green, refactor. Use before writing any implementation code, and when fixing a bug (reproduce it as a failing test first). Covers what makes a good test, why the order matters, the rationalizations that mean you are skipping it, and a verification checklist before marking work complete. Triggers: add a feature, fix this bug, implement X, write tests, TDD. | +| [using-git-worktrees](using-git-worktrees/SKILL.md) | Ensure feature work happens in an isolated workspace before it starts - detects existing isolation, creates a git worktree or native isolated workspace, sets up the project, and verifies a clean baseline. Use before executing an implementation plan or starting anything that should not disturb the current working tree. Triggers: start a new feature, work on this separately, set up a branch for this, isolate this work, spin up a worktree. | +| [verification-before-completion](verification-before-completion/SKILL.md) | Gate any completion claim behind actual evidence - run the build, tests, or linter and read the output before saying something is done, fixed, passing, or ready. Use right before committing, opening a PR, marking a task complete, or telling the user it works. Covers the common failure modes and the red flags that mean you are about to claim success you have not verified. Triggers: is it done, are we finished, ship it, mark it complete. | +| [writing-plans](writing-plans/SKILL.md) | Turn a spec or set of requirements into a written, bite-sized implementation plan BEFORE touching code - scope check, file structure, right-sized tasks with no placeholders, global constraints, and a self-review pass, ending in a handoff for execution. Use for any multi-step or multi-file task. Triggers: plan this out, write a plan, how would you approach this, break this down, what is the implementation plan, roadmap this. | +| [writing-skills](writing-skills/SKILL.md) | Create, edit, test, or debug agent skills - SKILL.md frontmatter and body structure, when a skill is warranted, skill types, discovery optimization (the description is the only part always loaded, and it is hard-capped at 500 characters), flowchart usage, and common mistakes. Use whenever authoring or revising a skill, or when a skill is not triggering. Triggers: create a skill, write a skill, edit this skill, my skill is not firing, improve the skill description. | diff --git a/ado-triage/SKILL.md b/ado-triage/SKILL.md new file mode 100644 index 0000000..62189f9 --- /dev/null +++ b/ado-triage/SKILL.md @@ -0,0 +1,99 @@ +--- +name: "ado-triage" +description: "Triage Daniel's Azure DevOps work items assigned to him across both orgs - OE on office.visualstudio.com and ES365 on dev.azure.com/enginfra. Categorizes each open item as needing an update, missing KB info, or stale/needs cleanup; scores severity and importance using Obsidian KB context; then walks through updates one at a time. Triggers: /ado-triage, check my ADO queue, triage my work items, review my work item queue, go through my ADO items, what am I assigned, my ADO backlog." +--- + +# ADO Work Item Triage + +Reviews Daniel Kucinski's two "assigned to me" Azure DevOps queues, categorizes and scores every open item using the Obsidian KB for context, then interactively walks him through updating each one. Do not skip the interactive confirmation step — never post comments, edit fields, or close/resolve items without per-item user approval. + +## Sources (two separate ADO orgs — handle differently) + +| Queue | URL | Access method | +|---|---|---| +| OE project | https://office.visualstudio.com/OE/_workitems/assignedtome/ | `azure_devops-*` MCP tools (already connected — verified via `azure_devops-core_list_projects`, which only returns `office.visualstudio.com` projects incl. `OE`) | +| ES365 project | https://dev.azure.com/enginfra/ES365/_workitems/assignedtome/ | **NOT reachable via the azure_devops MCP tools** (different org, `enginfra`, not `office`). No `az` CLI or PAT is available in this environment either. Use **Playwright browser automation** instead — Daniel has SSO/AAD access in his logged-in browser. | + +Re-verify tool connectivity at the start of each run (`azure_devops-core_list_projects` with no filter) in case the MCP scope has changed — don't assume it's still enginfra-blind if this instruction is stale. + +## Step 1 — Pull open work items assigned to Daniel + +### OE (office.visualstudio.com) +Use `azure_devops-wit_query_by_wiql` with `project: "OE"`: +```sql +SELECT [System.Id],[System.Title],[System.WorkItemType],[System.State],[System.CreatedDate], + [System.ChangedDate],[System.Tags],[Microsoft.VSTS.Common.Priority],[Microsoft.VSTS.Common.Severity] +FROM WorkItems +WHERE [System.TeamProject] = 'OE' AND [System.AssignedTo] = @Me + AND [System.State] NOT IN ('Closed','Removed','Resolved','Done') +ORDER BY [System.ChangedDate] DESC +``` +Then `azure_devops-wit_get_work_items_batch_by_ids` for full field values and `azure_devops-wit_list_work_item_comments` (or `azure_devops-wit_list_work_item_revisions`) per item for discussion history. + +### ES365 (dev.azure.com/enginfra) +1. `playwright-browser_navigate` to `https://dev.azure.com/enginfra/ES365/_workitems/assignedtome/` (keep the browser visible, not headless — Daniel should see what's happening since edits happen here later). +2. `playwright-browser_snapshot` to read the grid: ID, title, type, state, changed date. If prompted to sign in, pause and ask Daniel to complete auth, then retry. +3. Filter client-side to open states (New/Active/equivalent — exclude Closed/Removed/Resolved/Done). +4. For each item, navigate to `https://dev.azure.com/enginfra/ES365/_workitems/edit/{id}`, snapshot the Details pane plus the Discussion/History tab to capture description, comments, and last-changed info. + +Emphasize to the user which states you're treating as "open" if the process template uses non-standard state names (e.g. To Do/Doing/Done). + +## Step 2 — Stage results in SQL + +Create (if not present) and populate a `wi_triage` table in the session SQL DB: +```sql +CREATE TABLE IF NOT EXISTS wi_triage ( + id TEXT PRIMARY KEY, -- "OE-11908778" or "ES365-" to disambiguate orgs + org TEXT, project TEXT, wi_id INTEGER, title TEXT, type TEXT, state TEXT, url TEXT, + priority TEXT, severity TEXT, created_date TEXT, changed_date TEXT, + category TEXT, -- any combo of 'A','B','C' e.g. "A,C" + kb_evidence TEXT, -- short note + vault path citing what was found + importance_score INTEGER, -- 1 (highest) .. 5 (lowest) + recommended_action TEXT, + status TEXT DEFAULT 'pending' -- pending -> reviewed -> updated/skipped +); +``` +One row per open work item from both queues. + +## Step 3 — Cross-reference the Obsidian KB + +KB root: `D:\Repos\Obsidian`. Most relevant for ES365/OMR-Health work: `03 - Squads\C3 - OMR Reliability Tooling\` (`00 - Home.md` running log, `Docs\Health Hub Work Items.md`, `04 - Decisions.md`, `05 - Open Questions.md`, `Meetings\`, `Status Updates\`) and `05 - Resources\Quick Reference.md`. Other work items may relate to other squads/areas — search the whole vault, don't assume everything is C3/OMR. + +For each work item, use `grep` across the vault for the work item ID and for distinctive title/keyword terms. Note: `Health Hub Work Items.md` and `00 - Home.md` are large — use `grep -n` / `view_range` rather than reading whole files. + +Categorize (a single item can carry more than one letter): +- **(A) Has an update** — the KB shows relevant recent activity (a merged PR, a decision, a status change, a meeting outcome) tied to this item that is newer than the item's own `ChangedDate` / latest comment, i.e. the item is behind reality and needs a comment/field update to catch it up. +- **(B) KB info missing from the item** — the KB holds a decision, root-cause note, related PR/IcM link, or blocking dependency clearly relevant to this item that is absent from its description/comments. +- **(C) Old / needs cleanup** — no ADO activity in more than ~21 days (ask Daniel if he wants a different threshold) **and** no KB signal that it's still active — recommend Resolve, Close, or reassignment. If KB explicitly says the work is done/superseded elsewhere, say so in `kb_evidence`. + +If no KB evidence exists either way, categorize on ADO signals alone and say so explicitly (don't fabricate KB linkage). + +## Step 4 — Score severity / importance + +Combine, in this priority order: +1. ADO's own `Priority`/`Severity` fields when populated. +2. KB signals: linkage to an active Sev1/Sev2 IcM, explicit P0–P4 tagging (the squad's Improve/Maintain/KTLO/None framework), WSR/ESR-flagged items, "blocking" language in KB notes. +3. Default to `3` (mid) when no signal exists — don't invent urgency. + +`importance_score`: 1 = highest, 5 = lowest. Record the reasoning in `recommended_action` or `kb_evidence`, whichever fits. + +## Step 5 — Present the sorted list, then walk through updates one at a time + +1. Show one markdown table sorted by `importance_score` ascending (ties broken by category, surfacing C items clearly as cleanup candidates even if their importance is low): `ID | Project | Title | State | Category | Importance | Why`. +2. Then go item by item in that order: + - Show full context (title, state, KB evidence, category reasoning). + - Propose one concrete action: + - **A** → draft the specific comment/update text to post. + - **B** → propose the specific text to add and cite the KB source (vault path/note). + - **C** → propose Resolve/Close (with a reason) or reassignment. + - Ask Daniel to confirm, edit, or skip (use `m_ask_user` for a clean multiple-choice: Apply / Edit / Skip) before touching anything. + - On confirmation, apply: + - **OE** → `azure_devops-wit_add_work_item_comment` and/or `azure_devops-wit_update_work_item` (state/field changes). + - **ES365** → Playwright: navigate to the edit page, fill the discussion box or field via snapshot+click+type, save, and re-snapshot to confirm the save succeeded. + - Update `wi_triage.status` to `updated` or `skipped` and move to the next item. + +## Guardrails +- Never post KB content that looks privacy-sensitive or internal-only into an ADO comment without flagging it to Daniel first — ADO comments can be visible more broadly than the vault. +- Never resolve/close/reassign or post a comment without explicit per-item confirmation. +- Keep the ES365 browser visible throughout (per the assistant's environment default, this should already be true — don't set headless). +- If a KB note look stale/contradicted by a newer ADO state, trust ADO as the source of truth for the item's actual state and say so. diff --git a/brainstorming/SKILL.md b/brainstorming/SKILL.md new file mode 100644 index 0000000..f8c5c24 --- /dev/null +++ b/brainstorming/SKILL.md @@ -0,0 +1,51 @@ +--- +name: "brainstorming" +description: "Explore intent, requirements, and design BEFORE any creative or build work - new features, components, functionality, behavior changes, refactors, or new tools and scripts. Produces a reviewed spec instead of jumping straight to code. Use when a request is vague, open-ended, or bigger than a one-line change, and any time the user says: let us build X, can you add X, I want a tool that, how should we design this, brainstorm, spec this out, what are the options. Run before writing-plans." +--- + +Help turn ideas into fully formed designs and specs through natural collaborative dialogue. + +Start by understanding the current project context, then ask questions one at a time to refine the idea. Once you understand what you're building, present the design and get user approval. + +HARD GATE: Do NOT write any code, scaffold any project, or take any implementation action until you have presented a design and the user has approved it. This applies to EVERY project regardless of perceived simplicity — a todo list, a single-function utility, a config change all go through this. + +## Checklist +1. **Explore project context** — check files, docs, recent commits +2. **Ask clarifying questions** — one at a time, understand purpose/constraints/success criteria. Prefer multiple choice when possible. +3. **Propose 2-3 approaches** — with trade-offs, lead with your recommendation and reasoning +4. **Present design** — in sections scaled to complexity (a few sentences if straightforward, up to 200-300 words if nuanced). Ask after each section whether it looks right. Cover: architecture, components, data flow, error handling, testing. +5. **Write design doc** — save to `docs/plans/YYYY-MM-DD--design.md` and commit if in a git repo (skip if user prefers otherwise or no repo) +6. **Spec self-review** — check for placeholders (TBD/TODO), internal contradictions, scope creep, ambiguity. Fix inline. +7. **User reviews written spec** — ask user to review the spec file before proceeding +8. **Transition to implementation** — invoke the writing-plans skill to create an implementation plan. Do NOT invoke any other implementation skill directly. + +## Key Principles +- One question at a time — don't overwhelm +- Multiple choice preferred over open-ended when possible +- YAGNI ruthlessly — remove unnecessary features from all designs +- Always propose 2-3 alternatives before settling +- Incremental validation — present design, get approval before moving on +- Be flexible — go back and clarify when something doesn't make sense + +## Design for isolation and clarity +Break the system into smaller units that each have one clear purpose, communicate through well-defined interfaces, and can be understood/tested independently. For each unit: what does it do, how do you use it, what does it depend on? Can someone understand it without reading internals? Can you change internals without breaking consumers? + +## Working in existing codebases +Explore current structure before proposing changes. Follow existing patterns. Where existing code has problems affecting the work (a file grown too large, unclear boundaries), include targeted improvements as part of the design. Don't propose unrelated refactoring. + +## Scope check +If the request describes multiple independent subsystems (e.g. "build a platform with chat, billing, and analytics"), flag this immediately and help decompose into sub-projects before brainstorming any one of them in detail. Each sub-project gets its own spec → plan → implementation cycle. + +## Spec Self-Review (after writing the doc) +1. Placeholder scan: any "TBD"/"TODO"/incomplete sections? Fix them. +2. Internal consistency: do sections contradict each other? +3. Scope check: focused enough for a single implementation plan? +4. Ambiguity check: could any requirement be read two ways? Pick one, make it explicit. + +## User Review Gate +After the spec review loop passes: +> "Spec written and committed to ``. Please review it and let me know if you want to make any changes before we start writing out the implementation plan." +Wait for response. If changes requested, make them and re-run the review loop. Only proceed once approved. + +## Terminal state +The only next step after brainstorming is invoking the writing-plans skill. Do not jump to any other implementation skill. diff --git a/chapan-intake/SKILL.md b/chapan-intake/SKILL.md new file mode 100644 index 0000000..7cac5ba --- /dev/null +++ b/chapan-intake/SKILL.md @@ -0,0 +1,41 @@ +--- +name: "chapan-intake" +description: "The recurring interview for the Chapan Hokkaido relocation mission. Use when the user says \"chapan check-in\", \"chapan intake\", \"chapan interview\", /chapan-intake, or when a weekly nudge asks Daniel for a relocation status update. Interviews Daniel, updates D:\\Repos\\Chapan profile.md and status.md, appends a dated log, and turns his findings into tracked visa/program/lead dossiers." +--- + +## chapan-intake — the regular interview + +Tracker root: `D:\Repos\Chapan`. Dates = America/Los_Angeles. Mission: relocate Daniel's family to Hokkaido (rural-inclusive). This skill is how the mission stays current — interview Daniel, then write what you learn into the tracker. + +### Step 1 — Load context (read first) +Read `profile.md` (note every `TBD` and the "Open questions for next interview" list), `status.md` (blockers, next actions), and `leads/README.md`. This tells you what's missing and what to follow up on. + +### Step 2 — Interview (conversational, batched) +Ask in **small batches of 3–6 questions**, not all at once. Priority order: +1. **Fill profile gaps** — pursue `TBD` fields, hardest-gating first: citizenship(s), current residence, spouse/children (ages, schooling), employer & Japan-transfer/remote feasibility, savings/income range, degrees & years experience, Japanese level, rural tolerance, target timeline, hard constraints. +2. **New findings** — "Since last time, did you find any towns, programs, visa info, listings, or contacts?" Capture specifics (names, URLs, amounts, deadlines, who he talked to). +3. **Status changes** — blockers cleared/added, timeline shifts, decisions made, scouting trips. + +Mechanics: +- Use `m_ask_user` only for genuinely discrete choices (2–5 options), e.g. "rural / semi-rural / regional city?". Put context in the message *before* the call. +- Use plain chat questions for free-text answers (names, numbers, places, URLs). +- Don't interrogate — keep it natural and acknowledge answers. Skip questions already answered in profile. + +### Step 3 — Write back +- **profile.md**: replace `TBD` with captured values; bump `updated:`; set `status: incomplete|usable|complete`. Refresh the "Open questions for next interview" list at the bottom. +- **status.md**: update next actions, blockers, key dates, and append a dated line to the decision log when a decision was made. Bump `updated:`. +- **log/YYYY-MM-DD.md** (`kind: interview`): record updates captured, findings + where filed, open questions carried forward. If the file exists for today, append a `## Re-run HH:MM` section. + +### Step 4 — File findings as dossiers +For each concrete finding, create/append the right file with frontmatter + a source URL and `retrieved:` date: +- a place → `leads/Hokkaido/.md` (`type: chapan-lead`, `status: new`) +- a program → `programs/.md` (`type: chapan-program`) +- visa info → `visa/.md` (`type: chapan-visa`) +Then **offer to run the matching deep-dive now** (`chapan-leads`, `chapan-programs`, or `chapan-visa`) for anything that needs real research, and add it to status.md next actions. + +### Step 5 — Close +Give a 3–5 line recap: what got updated, what was filed, and the top 1–2 next actions. Suggest when the next check-in should be. + +### Rules +- Private family data — never share externally without explicit confirmation; keep it in the vault. +- Never fabricate answers; if Daniel doesn't know, leave `TBD` and keep the open question. diff --git a/chapan-leads/SKILL.md b/chapan-leads/SKILL.md new file mode 100644 index 0000000..a09a0e5 --- /dev/null +++ b/chapan-leads/SKILL.md @@ -0,0 +1,44 @@ +--- +name: "chapan-leads" +description: "Deep-dive a specific Hokkaido locality (or a program at a locality) into a structured dossier for the Chapan relocation mission by investigating its official municipal/prefectural website. Use when the user says \"chapan lead\", \"chapan leads\", \"investigate \", /chapan-leads, or names a Hokkaido town/village to research. Writes D:\\Repos\\Chapan\\leads\\Hokkaido\\.md and keeps the leads index current." +--- + +## chapan-leads — investigate a locality into a dossier + +Tracker root: `D:\Repos\Chapan`. Dates = America/Los_Angeles. A *lead* is a specific Hokkaido locality worth investigating (rural towns/villages especially). This skill digs into the locality's own official site and produces a structured, comparable dossier. + +### Step 1 — Identify +Get the locality name (ask if not given). Find its **official municipal site** — patterns: `town.hokkaido.jp`, `city.hokkaido.jp`, `www.town..hokkaido.jp`, or a page under `pref.hokkaido.lg.jp`. Use search web to confirm the official domain (avoid blogs/aggregators for facts). + +### Step 2 — Investigate (browser + web_fetch) +Open the site and its 移住 (migration)/定住 (settlement)/子育て (childcare) sections. Japanese pages: fetch, translate key terms. Extract: +- Population & recent trend; area type (rural/semi-rural/regional city); nearest city, airport, train/bus access; climate/snowfall. +- 移住 support page; 空き家バンク (akiya bank) presence + sample listings; housing purchase/renovation/rent subsidies. +- Childcare & education: subsidies, school availability/size, medical-fee support for kids; hospital/clinic access. +- Employment/startup support; 地域おこし協力隊 openings (and whether foreigners are eligible). +- Foreigner-friendliness signals (multilingual support, existing international residents). +- **Migration desk / contact info** (phone, email, form) and any application deadlines. + +### Step 3 — Write `leads/Hokkaido/.md` +Frontmatter: +``` +type: chapan-lead +locality: +prefecture: Hokkaido +status: new | investigating | contacted | hot | cold | dead +priority: high | medium | low +population: +official_site: +created: +updated: +``` +Body sections: Overview/access; Housing & akiya; Family (childcare/schools/medical); Work & programs; Contacts & deadlines; **Fit vs profile** (score against schools/jobs/budget/rural-tolerance from `profile.md`); Open questions / next action. Default `status: investigating` after a real dig. + +### Step 4 — Keep index + evidence current +- Update the **active-leads table** in `leads/README.md` (locality, status, priority, updated, why). +- Save raw page snapshots/URLs to `research/YYYY-MM-DD-lead-.md`. +- Suggest a concrete next action (contact the migration desk, plan a scouting visit, compare vs another lead) and add it to `status.md` if significant. + +### Rules +- Prefer first-party municipal/prefectural sources for facts; cite URLs + retrieved date. +- Don't fabricate figures — use `TBD` if the site doesn't say. Private data — keep in the vault. diff --git a/chapan-programs/SKILL.md b/chapan-programs/SKILL.md new file mode 100644 index 0000000..36bd687 --- /dev/null +++ b/chapan-programs/SKILL.md @@ -0,0 +1,42 @@ +--- +name: "chapan-programs" +description: "Research and track Japanese relocation and rural-revitalization support programs for the Chapan Hokkaido mission — national (移住支援金, 起業支援金, 地域おこし協力隊, 空き家バンク) and Hokkaido prefectural/municipal. Use when the user says \"chapan programs\", /chapan-programs, or asks about relocation grants, subsidies, akiya banks, or revitalization schemes. Writes/updates D:\\Repos\\Chapan\\programs\\*.md with source URLs and retrieved dates." +--- + +## chapan-programs — relocation & revitalization program tracking + +Tracker root: `D:\Repos\Chapan`. Dates = America/Los_Angeles. Goal: surface and track the national + Hokkaido programs that make a rural move feasible (grants, stipends, housing/childcare subsidies, akiya banks, revitalization corps). + +### Step 1 — Context +Read `profile.md` (family size, children, intended work/startup, current residence — some grants depend on *where you move from*) and `programs/README.md` + existing `programs/*.md`. + +### Step 2 — Research (official + portals) +Use **search web + web_fetch + browser**. Many sources are Japanese — fetch the page, then translate key terms. Priority sources: +- JOIN portal (iju-join.jp), Cabinet Office chihōsōsei (chisou.go.jp/sousei), MIC 地域おこし協力隊 (soumu.go.jp), Hokkaido gov (pref.hokkaido.lg.jp), CLAIR. +- Japanese seed queries in `resources.md` (e.g. "北海道 移住 支援金", "地域おこし協力隊 北海道 募集 外国人"). +Programs to track: 移住支援金 (Ijū Shienkin), 起業支援金 (Kigyō Shienkin), 地域おこし協力隊, 空き家バンク, Hokkaido prefectural support, and town-level child-rearing/housing subsidies. + +### Step 3 — Write/update `programs/.md` +Frontmatter: +``` +type: chapan-program +program: +slug: +level: national | prefectural | municipal +locality: +relevance: high | medium | low +status: tracking | applied | ineligible | secured +official_source: +retrieved: +updated: +``` +Body: what it is; **eligibility — and explicitly whether an overseas move qualifies or it's intra-Japan-only** (critical for 移住支援金, which historically requires prior residence/work in the Tokyo area); amount(s); conditions & clawbacks (e.g. must stay N years); application process & deadlines; relevance to this family; open questions. + +### Step 4 — Spin off & evidence +- A perk tied to a **specific town** → don't bury it here; open/append a lead via `chapan-leads` and link it. +- Save raw search snapshots to `research/YYYY-MM-DD-programs-.md`. +- Distinguish national (uniform) vs prefectural vs municipal (the real rural draws). + +### Rules +- Cite `official_source` + `retrieved`. Amounts/rules change yearly — mark anything not freshly confirmed **UNVERIFIED**. +- Private data — keep in the vault. diff --git a/chapan-visa/SKILL.md b/chapan-visa/SKILL.md new file mode 100644 index 0000000..7b74f3f --- /dev/null +++ b/chapan-visa/SKILL.md @@ -0,0 +1,42 @@ +--- +name: "chapan-visa" +description: "Research and track Japanese visa pathways for the Chapan Hokkaido relocation, evaluated against Daniel's family profile. Use when the user says \"chapan visa\", /chapan-visa, asks which visa fits, or wants visa requirements/rules checked or refreshed. Searches official sources (ISA, MOFA, JETRO), writes/updates D:\\Repos\\Chapan\\visa\\*.md dossiers with a source URL and retrieved date." +--- + +## chapan-visa — visa pathway research & tracking + +Tracker root: `D:\Repos\Chapan`. Dates = America/Los_Angeles. Goal: find and track the visa pathway(s) that get Daniel's family to Hokkaido (rural-inclusive) and ideally toward permanent residency. + +### Step 1 — Context +Read `profile.md` (citizenship, employer/transfer, degree, income, Japanese level, family) and `visa/README.md` + existing `visa/*.md`. Eligibility is judged against the profile — if a gating field is `TBD`, note it and proceed with best-effort, flagging the gap. + +### Step 2 — Research +Scope: a named pathway, or `all` / `refresh`. Use **search web + web_fetch + browser** against official sources first: +- Immigration Services Agency (isa.go.jp), MOFA (mofa.go.jp), JETRO (jetro.go.jp), and the HSP points sheet. +- Seed queries live in `resources.md` (e.g. "Japan highly skilled professional visa points 2026"). +Candidate pathways to consider: Intra-company Transferee, Highly Skilled Professional (points), Engineer/Specialist in Humanities/Int'l Services, Business Manager, J-Skip/J-Find, Digital Nomad (scouting only), Dependent, Spouse. Note that rural programs like 地域おこし協力隊 are NOT visas — the holder still needs a residence status; cross-reference `programs/`. + +### Step 3 — Write/update `visa/.md` +Frontmatter: +``` +type: chapan-visa +pathway: +slug: +fit: strong | possible | weak | n/a +status: researching | viable | pursuing | ruled-out +official_source: +retrieved: +updated: +``` +Body: requirements; **eligibility check against this family's profile** (point-by-point); cost & processing time; **path to permanent residency**; how dependents (spouse/kids) are handled; pros/cons specifically for a *rural Hokkaido* move; open questions. For HSP, compute an estimated points total from the profile and show the breakdown. + +### Step 4 — Evidence + gaps +Save a raw snapshot of substantive searches to `research/YYYY-MM-DD-visa-.md` (URLs + key quotes). If a profile gap blocks assessment, add it to `profile.md`'s open-questions list and to `status.md` next actions. + +### Refresh mode +Re-check any pathway whose `retrieved` is >90 days or whose `status` is `viable`/`pursuing`, looking for rule changes; bump dates and note deltas. + +### Rules +- Always cite an official `official_source` + `retrieved` date. Mark inferred/uncertain items **UNVERIFIED**. +- This is research, not legal advice. For decisions, recommend confirming with ISA or a licensed immigration specialist (行政書士 / immigration lawyer). +- Private data — keep in the vault. diff --git a/chapan/SKILL.md b/chapan/SKILL.md new file mode 100644 index 0000000..2369fae --- /dev/null +++ b/chapan/SKILL.md @@ -0,0 +1,40 @@ +--- +name: "chapan" +description: "Mission control for the Chapan Hokkaido relocation project. Dashboard, router, and digest over the tracker at D:\\Repos\\Chapan. Use when the user says /chapan, \"chapan\", \"chapan status\", \"chapan dashboard\", \"chapan digest\", or asks how the Japan/Hokkaido relocation is going. Routes to chapan-intake, chapan-visa, chapan-programs, chapan-leads." +--- + +## chapan — mission control + +Tracker root: `D:\Repos\Chapan`. All dates use America/Los_Angeles (`Get-Date -Format yyyy-MM-dd`). +Mission: relocate Daniel's family to **Hokkaido, Japan, including rural areas**. + +This skill is the front door. With no subcommand, run **status (dashboard)**. + +### Subcommands +- `/chapan` or `/chapan status` → dashboard +- `/chapan digest` → change briefing (optionally saved to log) +- natural language ("how's the move going?") → dashboard, then point to the right sub-skill + +### Dashboard (read-only) +1. Read `status.md` (phase, next actions, blockers, key dates) and `profile.md` (count remaining `TBD` fields = profile completeness). +2. Scan `leads/Hokkaido/*.md`, `visa/*.md`, `programs/*.md` frontmatter (`status:`, `updated:`, `retrieved:`). Skip `README.md` index files. +3. Present concisely: + - **Phase** + profile completeness (e.g. "7 fields still TBD"). + - **Leads**: count by status; list `hot`/`investigating` with one-line why. + - **Visa**: pathways by status + fit. + - **Programs**: by relevance/status. + - **Stale flags**: any dossier with `retrieved` >90 days, or `hot`/`investigating` lead `updated` >14 days. + - **Next actions** (from status.md) and **last log entry** date. +4. End with the single most useful next step and which skill runs it. +Do NOT write files in status mode. + +### Digest +Compare `updated:` dates across all dossiers + the latest `log/` entries. Summarize what changed since the last digest (default last 7 days, or since the user's stated point), list stale items needing re-verification, and give 3 concrete recommended next actions. Offer to save it to `log/YYYY-MM-DD.md` with `kind: finding`. Used on demand and by the monthly digest automation. + +### Routing +If the user's real intent is to be interviewed → `chapan-intake`; research visas → `chapan-visa`; programs → `chapan-programs`; investigate a place → `chapan-leads`. You may just hand off by running that skill's flow. + +### Rules +- Parse frontmatter with simple line regex; no YAML libs. +- Private family data — never share externally without explicit confirmation. +- Be honest about gaps; never fabricate dossier facts. diff --git a/clawpilot-chats-digest/SKILL.md b/clawpilot-chats-digest/SKILL.md new file mode 100644 index 0000000..67c1650 --- /dev/null +++ b/clawpilot-chats-digest/SKILL.md @@ -0,0 +1,71 @@ +--- +name: "clawpilot-chats-digest" +description: "Generate an on-demand digest of recent activity in the Clawpilot/Lobster Pound Teams spaces — the new MCAPS Lobster Pound channel (primary, going forward) plus the two original group chats (MCAPS Lobster Pound and 'ClawPilot: From Copilot to Agent') — surfacing major Clawpilot releases, novel usage patterns, and items worth a closer look for Daniel. Triggers: /clawpilot-chats-digest, 'clawpilot digest', 'clawpilot chats digest', 'what's new in the clawpilot chats', 'lobster pound digest'." +--- + +## Clawpilot Chats Digest Skill + +Produces a scannable digest of recent activity across the Clawpilot/Lobster +Pound Teams spaces. Runs on demand (formerly the "Clawpilot Chats Daily Digest" +automation). + +### Source spaces + +The community is migrating from the original meeting group chats (which hit +Teams' people limit) to a dedicated **channel**. Read all three so nothing is +missed during the transition; the channel is the primary source going forward. + +1. **MCAPS Lobster Pound — Channel** (primary): `19:vGhTXeZ7TqvSIz4Kz_dxn_c-ZKikC1ZobNY7h-jcK5o1@thread.tacv2` + - teamId/groupId: `ae25e647-d5e2-45e6-bbd8-8bb8c59d74f5` + - Web link: https://teams.microsoft.com/l/channel/19%3AvGhTXeZ7TqvSIz4Kz_dxn_c-ZKikC1ZobNY7h-jcK5o1%40thread.tacv2/Lobster%20Pound?groupId=ae25e647-d5e2-45e6-bbd8-8bb8c59d74f5&tenantId=72f988bf-86f1-41af-91ab-2d7cd011db47 +2. **MCAPS Lobster Pound — Group chat** (legacy): `19:meeting_ZmUxNTBhOTgtOTgxMC00OThkLWJlNGMtOTM3NDA1YjM5MzNj@thread.v2` +3. **ClawPilot: From Copilot to Agent — Group chat** (legacy): `19:meeting_MmYzMzYyNjMtNjMzOC00M2RjLWIwNzAtY2ZmZjcxZjcwMjNh@thread.v2` + +Note: `workiq_list_chat_messages` accepts the channel's `@thread.tacv2` ID in its +`chatId` parameter exactly like a group chat, and returns top-level channel +posts. (Threaded replies under a post may not all come back; if a post clearly +references discussion, note that the thread may have more.) + +### Time window + +Default to the **last 24 hours**. If the user specifies a different range +(e.g. "this week", "since Monday", "last 3 days"), honor that instead. + +### Steps + +1. For each ID above, call `workiq_list_chat_messages` with limit 50. + **Do NOT search for the chats — use the IDs directly** (this includes the + channel `@thread.tacv2` ID). + (Messages come back as HTML in an `` wrapper — strip the + tags and skip `` entries when composing.) +2. Filter to messages within the requested window (default: since 24 hours ago, PT). +3. Compose a digest with these sections: + - **Major Releases & Changes** — announcements of new Clawpilot features, + releases, breaking changes, or rollouts. + - **Interesting Usage Patterns** — novel/creative ways people are using + Clawpilot (skills, automations, workflows, MCP tricks, prompts, + integrations). Prioritize things useful to Daniel given his work + (Obsidian vault automations, EOD summaries, OMR Reliability Tooling, + TradeTrail, Teams/meeting archival, expense reports, etc.). + - **Worth a Closer Look** — 2–5 specific items Daniel should consider trying + or following up on, each with a one-line "why this matters to you" note. + - **Other Notable Discussion** — brief bullets for anything else worth + knowing (bugs, gotchas, requests, debates). +4. For each item, cite source: space name + author + short quote/paraphrase. + Skip pure noise (greetings, off-topic chatter, reactions only). If a space had + no relevant activity, say so in one line. + +### Output + +Present the digest directly in the chat as markdown (headings + bullets, +scannable, under 2 minutes to read). Only send it via `m_send_teams_message` +or `workiq_send_chat_message` if the user explicitly asks for it to be delivered +to Teams. + +### Error handling + +If any ID returns an error (e.g. chat/channel deleted or renamed), fall back to +`workiq_search_chats` by the space name and note it in the digest so Daniel can +update the ID in this skill. The two `@thread.v2` group chats are legacy and may +eventually go quiet or be archived — that's expected; just note "no recent +activity" rather than treating it as an error. diff --git a/clawvibe/SKILL.md b/clawvibe/SKILL.md new file mode 100644 index 0000000..3f5d7af --- /dev/null +++ b/clawvibe/SKILL.md @@ -0,0 +1,48 @@ +--- +name: "clawvibe" +description: "Explicitly invoked (e.g. /clawvibe, \"let's clawvibe this\", \"run the full workflow on this\") to route a specific task through the complete brainstorm -> plan -> implement -> review -> finish pipeline using the locally installed skills. Does NOT trigger automatically — opt-in only, unlike a standing gatekeeper." +--- + +ClawVibe is the opt-in entry point for the full software-development pipeline. It only activates when the user explicitly invokes it for a task (`/clawvibe`, "clawvibe this", "run the full workflow on this"). It is NOT a standing rule — it does not hijack every message, and its elevated process rigor applies only to the task it was invoked for, not to unrelated later requests in the same conversation. + +Announce at start: "Using clawvibe — routing this through the pipeline." + +## Routing — pick ONE entry point based on what's actually being asked +- "Let's build/add/create X" / anything resembling new functionality → start at the **brainstorming** skill +- User already has a design/spec and wants it turned into tasks → start at **writing-plans** +- User has a written plan and wants it implemented → **subagent-driven-development** (same session, subagents available, tasks mostly independent) or **executing-plans** (separate/parallel session, or no subagent support) +- Bug report / failing test / unexpected behavior → start at **systematic-debugging** (root cause before any fix is proposed) +- "Review this diff/PR" → **requesting-code-review** (dispatch a reviewer) or **receiving-code-review** (feedback already came in and needs a response) +- "Wrap this branch up" / implementation is done and tests pass → **finishing-a-development-branch** +- 2+ independent, unrelated failures across different files/subsystems → **dispatching-parallel-agents** +- Writing or editing a skill itself (including this one) → **writing-skills** + +If the request is ambiguous between entry points, ask the user which one fits, or default to brainstorming for anything that smells like new work rather than guessing and skipping design. + +## While active for this task +Hold the same discipline the underlying skills describe, for the duration of this one task: +- Don't skip brainstorming/design because the task "seems simple" — every project gets a design pass, scaled to its complexity. +- Don't write production code before a failing test exists (test-driven-development). +- Don't propose a fix before completing root-cause investigation (systematic-debugging). +- Don't claim something is done/fixed/passing without running the verification-before-completion gate. +- Chain forward using each skill's own "what's next" pointers rather than stopping short: brainstorming → writing-plans → (subagent-driven-development or executing-plans) → requesting-code-review/receiving-code-review → finishing-a-development-branch. +- Use using-git-worktrees at the point implementation actually starts, to keep the work isolated. +- Use dispatching-parallel-agents when the per-task loop surfaces multiple unrelated failures. + +## Common rationalizations to reject while clawvibe is active for THIS task +| Thought | Reality | +|---|---| +| "This is too simple for a design pass" | Scale the design down, don't skip it. | +| "I'll just start coding, tests can come after" | Delete-and-restart-with-TDD territory. | +| "It's probably X, let me just fix it" | Root-cause first — see systematic-debugging. | +| "Tests should pass now" | Run them. Evidence before the claim. | +| "I'll skip the worktree, it's a small change" | Still isolate — cheap insurance. | +| "One more review round is overkill" | Review after every task, not just at the end. | + +## Where clawvibe stops +Its elevated rigor covers the invoked task end-to-end but does not roll forward onto unrelated later requests in the same conversation. Treat it as concluded once: a design doc is written and approved with no build requested yet, or finishing-a-development-branch completes (merged/PR'd/kept/discarded), or the user redirects to something unrelated. Re-invoke explicitly for the next task. + +## Pipeline skills available locally (invoke by name, no prefix) +brainstorming, writing-plans, executing-plans, subagent-driven-development, systematic-debugging, test-driven-development, verification-before-completion, requesting-code-review, receiving-code-review, finishing-a-development-branch, using-git-worktrees, dispatching-parallel-agents, writing-skills + +User instructions and direct requests always take precedence over this routing — if the user explicitly says to skip a phase, skip it; don't argue the case they already decided. diff --git a/connect/SKILL.md b/connect/SKILL.md new file mode 100644 index 0000000..994943c --- /dev/null +++ b/connect/SKILL.md @@ -0,0 +1,150 @@ +--- +name: "connect" +description: "Manage Microsoft Connect (perf review) tracking inside the Obsidian vault. Reads the active Connect markdown in D:\\Repos\\Obsidian\\04 - Connects\\, cross-references the rest of the vault for evidence (daily notes, squad meetings, status updates, decisions), and emits weekly check-ins, draft narratives, 1:1 prep digests, and gap warnings. Triggers: 'connect', 'connect check-in', 'connect prep', 'connect draft', or any /connect subcommand." +--- + +## Connect Skill + +Helps the user track and stay on top of their Microsoft Connect (perf review) +period using their Obsidian vault as the source of truth. + +**Vault root:** `D:\Repos\Obsidian\` +**Connect folder:** `D:\Repos\Obsidian\04 - Connects\` +**Templates:** `D:\Repos\Obsidian\04 - Connects\Templates\` +**Output folders (skill writes here):** +- `04 - Connects\Check-ins\YYYY-MM-DD.md` +- `04 - Connects\Drafts\YYYY-MM.md` + +The skill is **read-only** outside `04 - Connects\` — never modify daily notes, squad notes, or meeting notes. Only emit new files inside `04 - Connects\`. + +--- + +### Active Connect doc + +The user authors one markdown per Connect period (e.g. `2026-H1.md`) directly in +`04 - Connects\`. The "active" doc is the one whose frontmatter has +`status: active`. Use this PowerShell to find it: + +```powershell +Get-ChildItem 'D:\Repos\Obsidian\04 - Connects\*.md' -File | ForEach-Object { + $head = Get-Content $_.FullName -TotalCount 12 -ErrorAction SilentlyContinue + if ($head -match 'status:\s*active') { $_.FullName } +} +``` + +If multiple are marked active, prefer the one whose `period` lexically sorts last +and warn the user. If none are active, tell the user and offer `/connect new`. + +### Connect doc convention (for parsing) + +```yaml +--- +type: connect +period: 2026-H1 +status: active +start: 2026-01-01 +end: 2026-06-30 +manager: +--- +``` + +Body sections: +- `## Core Priority N — ` — one per priority. Sub-fields: + - `**Tags:** #cp/<tag1> #cp/<tag2>` — these are the tag hooks for evidence rollup. + - `**What success looks like:**` + - `**Key results / milestones:**` + - `**People / projects / keywords:**` +- `## Growth & Learning` +- `## Manager check-in notes` +- `## Submitted Connect` + +### Evidence sources (READ ONLY) + +Scan these for evidence to attribute to priorities: + +1. `01 - Daily\*.md` — look in `## ✅ Tasks`, `## 📌 Highlights`, `## 🌙 Reflection` (Wins/Lessons), `## ✅ Action Items`, `## 💬 Teams Activity`, `## 🗓️ Meetings`. +2. `02 - Meetings\**\*.md` and `03 - Squads\**\Meetings\*.md` — meeting summaries, decisions, action items. +3. `03 - Squads\**\Status Updates\*.md` — explicit status writeups. +4. `03 - Squads\**\04 - Decisions.md` — squad decisions. +5. `00 - Inbox\*.md` — only if the file is dated within the window. + +### Matching strategy (apply in order, dedupe) + +1. **Tag match** — `#cp/<tag>` anywhere in a note attributes that note to the matching priority. +2. **Keyword/people/project match** — extract terms from each priority's `**People / projects / keywords:**` line plus the priority title; case-insensitive substring match against note body. Be conservative — require at least one strong match (a project/person, not a generic word). +3. **Recency window** — defaults: weekly = last 7 days, period-to-date = since `start` in frontmatter, custom = whatever the user passes. + +When emitting evidence, always cite the source note as an Obsidian wikilink (e.g. `[[2026-05-11]]`, `[[2026-05-12 - OMR Reliability Cross-Squad Meetup]]`). + +--- + +## Subcommands + +The user may invoke any of these. If they say `/connect` with no subcommand, ask which one — show the table of options. If they describe an intent in natural language ("how am I doing on connect?"), pick the closest subcommand. + +### `/connect status` +Show: +- The active Connect doc path + period + days remaining until `end`. +- Each Core Priority: title, tag hooks, **last evidence date** found in the vault, **count of evidence items** in the period. +- Cold priorities (no evidence in last 14 days). +- Top 3 gaps. + +Do NOT write any file. Just respond. + +### `/connect evidence [priority] [--since YYYY-MM-DD]` +List every evidence snippet matched, grouped by priority. If `priority` is omitted, do all. If `--since` omitted, default to last 7 days. + +For each snippet include: source wikilink, date, one-line summary, why it matched (tag vs keyword). + +Do NOT write any file. Just respond. + +### `/connect checkin` +Generate a weekly check-in note from `Templates\Check-in.md`. Fill in: +- Per-priority new evidence (last 7 days), summarized in 1–3 bullets each. +- Gaps / risks (priorities with thin evidence, action items going stale). +- Suggested next actions (concrete, attributable to a priority). +- Cold priorities (>14d silent). +- "For the manager 1:1" section — punchy, max 5 bullets. + +Save to `04 - Connects\Check-ins\YYYY-MM-DD.md` (use today's date in PT). If a check-in already exists for today, ask before overwriting; default to appending a `## Re-run HH:MM` section. + +Then briefly tell the user the path and the top 3 highlights. + +### `/connect draft` +Produce a narrative draft per priority suitable for pasting into HR Web. Style: +- 1–3 paragraphs per priority, written in first person, past/present tense as appropriate. +- Lead with impact ("what changed in the world"), then approach, then evidence. +- Reference specific work (project names, decisions, metrics) — pull these from the vault evidence. +- No fluff, no buzzwords beyond what the user already uses. + +Save to `04 - Connects\Drafts\YYYY-MM.md` (current month). Overwrite freely; this is a draft. + +Then summarize what's in the draft and flag any priority where evidence felt thin. + +### `/connect prep <name>` +Pull Connect-relevant evidence since the last 1:1 with `<name>` (look in `02 - Meetings\` for the most recent file containing that name, or fall back to last 14 days). Output as a markdown digest grouped by priority. Do NOT write a file unless the user asks. + +If `<name>` matches the `manager:` frontmatter, this is a manager-prep run — include the Submitted Connect snippet from the doc and a "what I'd like to discuss" section. + +### `/connect new <period>` +Scaffold a new Connect markdown: +1. Find the current active doc and set its frontmatter to `status: archived`. +2. Copy `Templates\Connect Period.md` to `04 - Connects\<period>.md` (e.g. `2026-H2.md`). +3. Substitute `{{period}}`, `{{start}}`, `{{end}}` based on the period name (H1 = Jan 1–Jun 30, H2 = Jul 1–Dec 31 of the year embedded in the period). +4. Tell the user to fill in titles, tags, and goals, then re-run `/connect status`. + +--- + +## Implementation notes + +- Use PowerShell `Get-ChildItem` + `Select-String` (or `grep` tool) to scan files efficiently. For large scans batch by month rather than reading every file. +- Always parse frontmatter via simple line-based regex; don't pull in YAML libs. +- Use `[[wikilink]]` style with the file's basename (no `.md`, no path) for Obsidian compatibility. +- All "today" decisions use America/Los_Angeles. Use `Get-Date` formatted as `yyyy-MM-dd`. +- When generating, be honest about gaps. Do not fabricate evidence. If a priority truly has no matched notes, say so explicitly. +- Sensitivity: Connect content is private. Never send drafts via Teams/email automatically. Files inside the Obsidian vault are fine. + +## First-run guidance + +If `04 - Connects\` has no `*.md` other than the templates, tell the user: +> "No Connect doc yet. Run `/connect new 2026-H1` (or your current period) to scaffold one, then fill in your Core Priorities and tags." diff --git a/dispatching-parallel-agents/SKILL.md b/dispatching-parallel-agents/SKILL.md new file mode 100644 index 0000000..6400dd2 --- /dev/null +++ b/dispatching-parallel-agents/SKILL.md @@ -0,0 +1,41 @@ +--- +name: "dispatching-parallel-agents" +description: "Fan work out to multiple sub-agents at once when a task splits into 2+ independent pieces with no shared state or ordering dependency - auditing several modules, researching unrelated questions, or applying the same change across separate files. Covers agent prompt structure, common mistakes, and verifying results after agents return. Triggers: do these in parallel, spin up agents, run these at once, split this work, parallelize this." +--- + +Delegate tasks to specialized agents with isolated context. By precisely crafting their instructions and context, you ensure they stay focused and succeed. They should never inherit your session's context or history — construct exactly what they need. This also preserves your own context for coordination work. + +When you have multiple unrelated failures (different test files, different subsystems, different bugs), investigating them sequentially wastes time. Each independent investigation can happen in parallel. + +**Core principle:** dispatch one agent per independent problem domain. Let them work concurrently. + +## When to Use +Use when: 3+ things are failing with different root causes, multiple subsystems are broken independently, each problem can be understood without context from the others, and there's no shared state between investigations. +Don't use when: failures are related (fixing one might fix others), you need to understand full system state first, or agents would interfere with each other (editing the same files/resources). + +## The Pattern +1. **Identify independent domains** — group failures by what's broken (e.g. File A tests: tool approval flow; File B tests: batch completion; File C tests: abort functionality). Confirm they're truly independent — fixing one doesn't affect another. +2. **Create focused agent tasks** — each agent gets: a specific scope (one file/subsystem), a clear goal, explicit constraints ("don't change other code"), and an expected output format (a summary of what was found/fixed). +3. **Dispatch in parallel** — issue all agent dispatches in the SAME response/turn. Multiple dispatch calls in one response run in parallel; one per response runs sequentially. +4. **Review and integrate** — read each summary, verify fixes don't conflict, run the full test suite, integrate all changes. + +## Agent Prompt Structure +Good prompts are: focused (one clear problem domain), self-contained (all context needed to understand the problem, e.g. paste the actual error messages and test names), and specific about the expected output (what should the agent return). + +Example of a focused, well-scoped prompt: +"Fix the 3 failing tests in src/agents/agent-tool-abort.test.ts: [list the specific failing assertions]. These are timing/race condition issues. Read the test file, identify root cause (timing vs actual bug), fix by replacing arbitrary timeouts with event-based waiting or fixing the actual bug — do NOT just increase timeouts. Return: summary of what you found and fixed." + +## Common Mistakes +Too broad ("fix all the tests" — agent gets lost) vs specific ("fix agent-tool-abort.test.ts"). No context ("fix the race condition") vs context (paste the actual error messages/test names). No constraints (agent might refactor everything) vs constraints ("do NOT change production code" / "fix tests only"). Vague output ("fix it") vs specific ("return summary of root cause and changes"). + +## When NOT to Use +Related failures where fixing one might fix others — investigate together first. Situations needing full system context to understand. Exploratory debugging where you don't yet know what's broken. Shared state where agents would interfere with each other. + +## Verification After Agents Return +1. Review each summary — understand what changed. +2. Check for conflicts — did agents edit the same code? +3. Run the full test suite — verify all fixes work together. +4. Spot check — agents can make systematic errors; don't rubber-stamp. + +## Key Benefits +Parallelization (multiple investigations run simultaneously), focus (each agent has a narrow scope, less context to track), independence (agents don't interfere), speed (N problems solved in roughly the time of 1). diff --git a/docs/implementationplans/2026-09-02-promptworks-mirror.md b/docs/implementationplans/2026-09-02-promptworks-mirror.md new file mode 100644 index 0000000..4bd58bc --- /dev/null +++ b/docs/implementationplans/2026-09-02-promptworks-mirror.md @@ -0,0 +1,153 @@ +# PromptWorks Mirror Implementation Plan + +> **For agentic workers:** Use the subagent-driven-development skill (recommended) or the executing-plans skill to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Create a private, safe Git mirror of custom Microsoft Scout skills and prove a LocalSystem scheduled sync can commit and push it. +**Architecture:** `sync-skills.ps1` builds an allowlisted staging tree from the Scout custom-skill source, replaces only mirrored skill folders, regenerates the README, and commits and pushes changes. Windows Task Scheduler invokes this script as LocalSystem each Sunday at 10:00 AM; Git obtains the repository-scoped HTTPS deploy token from LocalSystem's credential store. +**Tech Stack:** PowerShell 7/Windows PowerShell, Git for Windows, Windows Task Scheduler, Windows Credential Manager, HTTPS Git remote. + +## Global Constraints + +- Use source `C:\Users\dkucinski\.scout\m-skills` and destination `D:\Repos\PromptWorks`. +- Set `origin` to `https://git.wucinski.duckdns.org/Daniel/PromptWorks.git`. +- Mirror each source skill directory, retaining `SKILL.md` and normal source artifacts, while excluding `node_modules`, `.git`, `.pw-profile*`, `.token-cache.json`, logs, caches, and `skills-metadata.json`. +- Do not store secrets in repository files, task arguments, environment variables, logs, or Git history. +- Append timestamped run details to `logs\sync.log`; ignore `logs\` in Git. +- Schedule `PromptWorks Weekly Skill Sync` for Sundays at 10:00 AM Pacific as `LocalSystem`, whether or not a user is logged on. +- A manual no-op run must create neither a new commit nor a push. +--- + +### Task 1: Create the safe skill mirror + +**Files:** +- Create: `.gitignore` +- Create: `sync-skills.ps1` +- Create: `README.md` +- Modify: `docs\plans\2026-09-02-promptworks-design.md` + +**Interfaces:** +- Consumes: source root passed to `sync-skills.ps1` as `-SourceRoot`, defaulting to the global source path. +- Produces: mirrored skill folders, an auto-generated `README.md`, `logs\sync.log`, and exit code `0` on success or nonzero on failure. + +- [ ] Step 1: Initialize the repository and configure its remote. + + ```powershell + Set-Location D:\Repos\PromptWorks + git init --initial-branch main + git remote add origin https://git.wucinski.duckdns.org/Daniel/PromptWorks.git + git config user.name "Daniel Kucinski" + git config user.email "dkucinski@users.noreply.github.com" + ``` + +- [ ] Step 2: Add the ignore policy. + + ```gitignore + logs/ + node_modules/ + .pw-profile*/ + .token-cache.json + *.log + ``` + +- [ ] Step 3: Implement `sync-skills.ps1` with an allowlist (`SKILL.md`, `.ps1`, `.mjs`, `.js`, `.json`, `.gitignore`) and exclusion checks for the global constraints. Make it: + + ```powershell + [CmdletBinding()] + param( + [string] $SourceRoot = 'C:\Users\dkucinski\.scout\m-skills', + [string] $RepositoryRoot = $PSScriptRoot + ) + + $ErrorActionPreference = 'Stop' + # Write timestamped log lines to Join-Path $RepositoryRoot 'logs\sync.log'. + # Mirror source skill directories into a temporary staging directory, then replace + # only destination skill directories so docs and sync infrastructure remain intact. + # Regenerate README.md from the SKILL.md frontmatter/title and first description. + # Stage all changes; exit 0 when git status --porcelain is empty. + # Otherwise commit with "chore: sync Scout skills <yyyy-MM-dd>" and git push origin main. + ``` + +- [ ] Step 4: Run the script interactively and inspect its result. + + ```powershell + & D:\Repos\PromptWorks\sync-skills.ps1 + git -C D:\Repos\PromptWorks status --short + ``` + + Expected result: all custom skills and their approved helpers are present, generated files are absent, the README has every skill, and the status is clean after the initial commit and push. + +- [ ] Step 5: Run the no-op test. + + ```powershell + $before = git -C D:\Repos\PromptWorks rev-parse HEAD + & D:\Repos\PromptWorks\sync-skills.ps1 + $after = git -C D:\Repos\PromptWorks rev-parse HEAD + if ($before -ne $after) { throw 'No-op sync created a commit.' } + ``` + + Expected result: no exception and a new success entry in `logs\sync.log`. + +- [ ] Step 6: Commit the infrastructure. + + ```powershell + git -C D:\Repos\PromptWorks add --all + git -C D:\Repos\PromptWorks commit -m "chore: add Scout skills mirror" + git -C D:\Repos\PromptWorks push -u origin main + ``` + +### Task 2: Configure and prove the unattended system task + +**Files:** +- Create: `install-system-sync-task.ps1` +- Modify: `sync-skills.ps1` +- Modify: `.gitignore` + +**Interfaces:** +- Consumes: an administrator-provisioned PromptWorks-only deploy token held in LocalSystem's Credential Manager for `git.wucinski.duckdns.org`. +- Produces: a task named `PromptWorks Weekly Skill Sync` and a successful LocalSystem manual invocation. + +- [ ] Step 1: Confirm the Git server accepts a write-scoped PromptWorks deploy token and provision it in LocalSystem's Windows Credential Manager without embedding it in files or command arguments. + + Expected result: `git push` from a LocalSystem context can authenticate to the specified origin, and no token appears in `sync.log` or `git config --list --show-origin`. + +- [ ] Step 2: Implement `install-system-sync-task.ps1` to register the task: + + ```powershell + $action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument '-NoProfile -ExecutionPolicy Bypass -File "D:\Repos\PromptWorks\sync-skills.ps1"' + $trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Sunday -At 10:00AM + $principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest + $settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -ExecutionTimeLimit (New-TimeSpan -Hours 1) + Register-ScheduledTask -TaskName 'PromptWorks Weekly Skill Sync' -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Description 'Mirrors custom Microsoft Scout skills to PromptWorks.' -Force + ``` + +- [ ] Step 3: Register the task from an elevated PowerShell session and inspect it. + + ```powershell + & D:\Repos\PromptWorks\install-system-sync-task.ps1 + Get-ScheduledTask -TaskName 'PromptWorks Weekly Skill Sync' | Select-Object TaskName, State, Principal + ``` + + Expected result: task name is `PromptWorks Weekly Skill Sync`; principal user ID is `SYSTEM`; task state is `Ready`. + +- [ ] Step 4: Force the system task to run and inspect its recorded outcome. + + ```powershell + Start-ScheduledTask -TaskName 'PromptWorks Weekly Skill Sync' + Start-Sleep -Seconds 15 + Get-ScheduledTaskInfo -TaskName 'PromptWorks Weekly Skill Sync' | Select-Object LastRunTime, LastTaskResult + Get-Content D:\Repos\PromptWorks\logs\sync.log -Tail 30 + ``` + + Expected result: `LastTaskResult` is `0`, the log records a successful mirror/no-op, and the remote branch contains the initial mirror commit. + +- [ ] Step 5: Commit the task installer. + + ```powershell + git -C D:\Repos\PromptWorks add install-system-sync-task.ps1 sync-skills.ps1 .gitignore + git -C D:\Repos\PromptWorks commit -m "chore: schedule system skill sync" + git -C D:\Repos\PromptWorks push + ``` + +## Spec Coverage Review + +The plan covers private origin configuration, per-skill folders, safe source artifacts, generated root index, ignored local logging, weekly Sunday scheduling, LocalSystem execution, deploy-token isolation, initial push, and no-op/task execution proof. diff --git a/docs/plans/2026-09-02-promptworks-design.md b/docs/plans/2026-09-02-promptworks-design.md new file mode 100644 index 0000000..9b78824 --- /dev/null +++ b/docs/plans/2026-09-02-promptworks-design.md @@ -0,0 +1,41 @@ +# PromptWorks Design + +## Purpose + +PromptWorks is a private Git mirror of Daniel's custom Microsoft Scout skills. It provides version history, a portable backup, and a readable index without copying runtime state or credentials. + +## Source and destination + +- Source: `C:\Users\dkucinski\.scout\m-skills` +- Destination: `D:\Repos\PromptWorks` +- Remote: `https://git.wucinski.duckdns.org/Daniel/PromptWorks.git` + +## Repository layout + +Each custom skill has a top-level directory named after the skill. It contains `SKILL.md` and any source-controlled helpers it requires, such as PowerShell or JavaScript files and npm manifests/lockfiles. The root contains: + +- `README.md`: generated table of contents with skill names and descriptions. +- `sync-skills.ps1`: repeatable mirror, index-generation, commit, and push operation. +- `.gitignore`: defense-in-depth exclusions for generated files and secrets. +- `logs\sync.log`: local timestamped sync history, ignored by Git. +- `docs\plans\`: design and implementation documentation. + +## Sync behavior + +`sync-skills.ps1` mirrors only approved source file types and paths into the repository. It removes destination files that no longer exist in the allowed source set, regenerates the README, stages changes, and creates a dated sync commit only when the working tree has changes. It then pushes the current branch to `origin`. Every run appends timestamped informational, warning, and error messages to `logs\sync.log`, including its final outcome. + +The source is authoritative; manual changes inside mirrored skill folders are overwritten on the next scheduled sync. Repository documentation and sync infrastructure are not overwritten. + +## Exclusions + +The mirror excludes generated state and private data, including `skills-metadata.json`, `node_modules`, `.git`, `.pw-profile*`, `.token-cache.json`, browser/cache data, and log files. npm dependencies are reproducible from tracked `package.json` and `package-lock.json` files. + +## Scheduling + +A Windows Task Scheduler task runs the sync script every Sunday at 10:00 AM in the America/Los_Angeles time zone as the built-in `LocalSystem` account, whether or not Daniel is signed in. The task runs independently of the Scout application. Task Scheduler history provides run-level status; `logs\sync.log` provides the detailed local record. + +The task pushes over HTTPS with a dedicated deploy token that has write access only to PromptWorks. The token is stored in the Windows Credential Manager context for `LocalSystem`, keyed to `git.wucinski.duckdns.org`; it is not stored in the repository, script, task arguments, environment variables, or log. A one-time elevated setup action provisions this machine-local credential before the scheduled task is enabled. + +## Validation + +Implementation validation will confirm that the repository contains all custom skills and intended helpers, excluded paths are absent, the generated README includes every skill, `origin` matches the approved URL, the task runs as `LocalSystem`, each run writes an ignored log entry, and a no-op rerun produces no commit. diff --git a/executing-plans/SKILL.md b/executing-plans/SKILL.md new file mode 100644 index 0000000..66f10a0 --- /dev/null +++ b/executing-plans/SKILL.md @@ -0,0 +1,42 @@ +--- +name: "executing-plans" +description: "Execute an already-written implementation plan document task by task, with verification and review checkpoints between steps. Use when a plan.md or spec already exists and the work now needs carrying out, including knowing when to stop and ask for help versus revisit earlier steps. Triggers: execute the plan, work through the plan, implement the plan, start on the plan, next task in the plan, follow the spec. Pairs with writing-plans and subagent-driven-development." +--- + +Load plan, review critically, execute all tasks, report when complete. + +Announce at start: "I'm using the executing-plans skill to implement this plan." + +Note: this works much better with subagent support. If subagents are available in this environment, prefer the subagent-driven-development skill instead of this one. + +## The Process + +### Step 1: Load and Review Plan +1. Read the plan file +2. Review critically — identify questions or concerns +3. If concerns: raise them with your human partner before starting +4. If none: create todos for the plan items and proceed + +### Step 2: Execute Tasks +For each task: +1. Mark as in_progress +2. Follow each step exactly (plan has bite-sized steps) +3. Run verifications as specified +4. Mark as completed + +### Step 3: Complete Development +After all tasks complete and verified: +- Announce: "I'm using the finishing-a-development-branch skill to complete this work." +- Use the finishing-a-development-branch skill: verify tests, present options, execute choice. + +## When to Stop and Ask for Help +STOP executing immediately when: you hit a blocker (missing dependency, test fails, instruction unclear), the plan has critical gaps preventing starting, you don't understand an instruction, or verification fails repeatedly. Ask for clarification rather than guessing. + +## When to Revisit Earlier Steps +Return to Step 1 review when the partner updates the plan based on feedback, or the fundamental approach needs rethinking. Don't force through blockers — stop and ask. + +## Remember +Review plan critically first. Follow plan steps exactly. Don't skip verifications. Reference other skills when the plan says to. Stop when blocked, don't guess. Never start implementation on main/master branch without explicit user consent. + +## Integration +Related skills: using-git-worktrees (isolated workspace), writing-plans (creates the plan this executes), finishing-a-development-branch (completes development after all tasks). diff --git a/finishing-a-development-branch/SKILL.md b/finishing-a-development-branch/SKILL.md new file mode 100644 index 0000000..5ee78a4 --- /dev/null +++ b/finishing-a-development-branch/SKILL.md @@ -0,0 +1,74 @@ +--- +name: "finishing-a-development-branch" +description: "Decide how to integrate finished work once implementation is done and tests pass - verifies tests, detects the environment, determines the base branch, then presents structured options (merge, open a PR, keep the branch, or discard) and cleans up the workspace or worktree afterward. Triggers: I am done with this branch, wrap this up, merge this, should I open a PR, finish the feature, clean up the worktree, what do I do with this branch now." +--- + +Guide completion of development work by presenting clear options and handling the chosen workflow. + +**Core principle:** verify tests → detect environment → present options → execute choice → clean up. + +Announce at start: "I'm using the finishing-a-development-branch skill to complete this work." + +## Step 1: Verify Tests +Run the project's test suite (npm test / cargo test / pytest / go test ./... as appropriate). If tests fail: report "Tests failing (<N> failures). Must fix before completing" with the failures shown, and stop — do not proceed to Step 2. If tests pass, continue. + +## Step 2: Detect Environment +Determine workspace state: +``` +GIT_DIR=$(cd "$(git rev-parse --git-dir)" 2>/dev/null && pwd -P) +GIT_COMMON=$(cd "$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P) +``` +- `GIT_DIR == GIT_COMMON` (normal repo): standard 4 options, no worktree cleanup needed. +- `GIT_DIR != GIT_COMMON`, named branch (worktree): standard 4 options, provenance-based cleanup (Step 6). +- `GIT_DIR != GIT_COMMON`, detached HEAD: reduced 3 options (no merge), no cleanup (externally managed). + +## Step 3: Determine Base Branch +Try `git merge-base HEAD main` or `git merge-base HEAD master`, or ask the user to confirm the base branch. + +## Step 4: Present Options +Normal repo / named-branch worktree — present exactly these 4, no extra explanation: +``` +Implementation complete. What would you like to do? +1. Merge back to <base-branch> locally +2. Push and create a Pull Request +3. Keep the branch as-is (I'll handle it later) +4. Discard this work +Which option? +``` +Detached HEAD — present exactly these 3: +``` +Implementation complete. You're on a detached HEAD (externally managed workspace). +1. Push as new branch and create a Pull Request +2. Keep as-is (I'll handle it later) +3. Discard this work +Which option? +``` + +## Step 5: Execute Choice +**Merge locally:** cd to main repo root (`git -C "$(git rev-parse --git-common-dir)/.." rev-parse --show-toplevel`), checkout base branch, pull, merge feature branch, re-run tests on the merged result. Only after merge succeeds: cleanup worktree (Step 6), then `git branch -d <feature-branch>`. +**Push and create PR:** `git push -u origin <feature-branch>`. Do NOT clean up the worktree — the user needs it to iterate on PR feedback. +**Keep as-is:** report "Keeping branch <name>. Worktree preserved at <path>." Don't clean up. +**Discard:** require the user to type the exact word "discard" to confirm, after showing exactly what will be deleted (branch, commits, worktree). If confirmed: cd to main repo root, cleanup worktree (Step 6), then `git branch -D <feature-branch>` (force). + +## Step 6: Cleanup Workspace (only for Merge and Discard) +``` +GIT_DIR=$(cd "$(git rev-parse --git-dir)" 2>/dev/null && pwd -P) +GIT_COMMON=$(cd "$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P) +WORKTREE_PATH=$(git rev-parse --show-toplevel) +``` +If `GIT_DIR == GIT_COMMON`: normal repo, nothing to clean up, done. +If the worktree path is under `.worktrees/` or `worktrees/`: you (this skill set) created it — clean it up: +``` +MAIN_ROOT=$(git -C "$(git rev-parse --git-common-dir)/.." rev-parse --show-toplevel) +cd "$MAIN_ROOT" +git worktree remove "$WORKTREE_PATH" +git worktree prune +``` +Otherwise: the host/harness owns this workspace — do NOT remove it. + +## Common Mistakes to Avoid +Skipping test verification before offering options. Open-ended questions instead of the exact structured menu. Cleaning up the worktree for Option 2 (PR) — user needs it alive. Deleting the branch before removing the worktree (fails because worktree still references it) — always merge first, remove worktree, then delete branch. Running `git worktree remove` from inside the worktree being removed — always cd to main repo root first. Cleaning up a harness-owned worktree — check provenance (`.worktrees/`/`worktrees/` path) first. Skipping typed confirmation for Discard. + +## Red Flags +Never: proceed with failing tests, merge without verifying tests on the merged result, delete work without confirmation, force-push without explicit request, remove a worktree before confirming merge success, clean up worktrees you didn't create, run worktree remove from inside the worktree. +Always: verify tests before offering options, detect environment first, present exactly the right number of options, get typed confirmation for discard, clean up worktree only for merge/discard, cd to main root before removal, run worktree prune after removal. diff --git a/flow-1on1/Add-TrackerEntry.ps1 b/flow-1on1/Add-TrackerEntry.ps1 new file mode 100644 index 0000000..7f4852b --- /dev/null +++ b/flow-1on1/Add-TrackerEntry.ps1 @@ -0,0 +1,187 @@ +<# +.SYNOPSIS + Appends a dated entry to the Daniel/Ahmed 1:1 notebook tracker (.docx) using Word COM. + +.DESCRIPTION + Clones the formatting of the existing "Example:" block (numbered question headings at + list level 1, lettered content bullets at list level 2) so new entries match the + tracker's look exactly, without touching the document's MIP sensitivity label or any + existing content. + + Every entry starts on a new page: a manual page break (Ctrl+Enter) is emitted ahead of + the date heading. + + Entry content is supplied as a JSON file so the caller never has to fight PowerShell + quoting. Shape: + + { + "achievements": ["bullet one", "bullet two"], + "next": ["bullet"], + "lessons": ["bullet"], + "topics": ["bullet"] + } + + Any section may be omitted or empty; it renders a single "None." bullet. + +.PARAMETER DocPath + Full path to the tracker .docx. + +.PARAMETER JsonPath + Full path to the UTF-8 JSON file described above. + +.PARAMETER EntryDate + Date heading for the entry, MM/DD/yyyy. Defaults to today. + +.PARAMETER Position + Top - insert as the newest entry, immediately after the Example block (default). + Bottom - append at the end of the document (chronological). + +.PARAMETER Preview + Render the entry to the console and exit without modifying the document. +#> +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)][string]$DocPath, + [Parameter(Mandatory = $true)][string]$JsonPath, + [string]$EntryDate = (Get-Date -Format 'MM/dd/yyyy'), + [ValidateSet('Top', 'Bottom')][string]$Position = 'Top', + [switch]$Preview +) + +$ErrorActionPreference = 'Stop' + +$SectionHeadings = @( + 'What are my Achievements, Impact and Key Results?', + "What's next? / What am I working on?", + 'Lessons Learned/Things to Improve', + 'Topics to discuss/What I need help with' +) + +if (-not (Test-Path -LiteralPath $DocPath)) { throw "Tracker not found: $DocPath" } +if (-not (Test-Path -LiteralPath $JsonPath)) { throw "Entry JSON not found: $JsonPath" } + +$entry = Get-Content -LiteralPath $JsonPath -Raw -Encoding UTF8 | ConvertFrom-Json + +function Get-Bullets($value) { + if ($null -eq $value) { return , @('None.') } + $list = @($value | Where-Object { $_ -and $_.ToString().Trim() } | ForEach-Object { $_.ToString().Trim() }) + if ($list.Count -eq 0) { return , @('None.') } + return , $list +} + +$sections = @( + (Get-Bullets $entry.achievements), + (Get-Bullets $entry.next), + (Get-Bullets $entry.lessons), + (Get-Bullets $entry.topics) +) + +if ($Preview) { + Write-Output $EntryDate + for ($i = 0; $i -lt 4; $i++) { + Write-Output (" {0}. {1}" -f ($i + 1), $SectionHeadings[$i]) + foreach ($b in $sections[$i]) { Write-Output " - $b" } + } + return +} + +# --- Word COM ------------------------------------------------------------- + +$wdCharacter = 1 +$word = $null +$doc = $null +$openedByUs = $false + +function Get-ParaText($para) { return ($para.Range.Text -replace "[\r\a]", '') } + +function Set-ParaText($para, [string]$text) { + $r = $para.Range + if ($r.End - $r.Start -gt 0) { [void]$r.MoveEnd($wdCharacter, -1) } + $r.Text = $text +} + +try { + try { $word = [Runtime.InteropServices.Marshal]::GetActiveObject('Word.Application') } + catch { $word = New-Object -ComObject Word.Application; $word.Visible = $false } + + $full = (Resolve-Path -LiteralPath $DocPath).Path + foreach ($d in $word.Documents) { if ($d.FullName -eq $full) { $doc = $d; break } } + if (-not $doc) { $doc = $word.Documents.Open($full); $openedByUs = $true } + + # Locate the Example block: the first bare MM/DD/YYYY paragraph through the paragraph + # before the next bare-date paragraph (or the last non-empty paragraph). + $paras = $doc.Paragraphs + $dateIdx = @() + for ($i = 1; $i -le $paras.Count; $i++) { + if ((Get-ParaText $paras.Item($i)).Trim() -match '^\d{1,2}/\d{1,2}/\d{4}$') { $dateIdx += $i } + } + if ($dateIdx.Count -eq 0) { throw 'Could not find the Example block (no MM/DD/YYYY paragraph) in the tracker.' } + + $exStart = $dateIdx[0] + $exEnd = if ($dateIdx.Count -gt 1) { $dateIdx[1] - 1 } else { $paras.Count } + while ($exEnd -gt $exStart -and (Get-ParaText $paras.Item($exEnd)).Trim() -eq '') { $exEnd-- } + + # Formatting seeds harvested from the Example block. + $seedPlain = $null # bare paragraph (the date line) + $seedHead = $null # list level 1 (the four questions) + $seedBullet = $null # list level 2 (content under a question) + for ($i = $exStart; $i -le $exEnd; $i++) { + $p = $paras.Item($i) + $lvl = $p.Range.ListFormat.ListLevelNumber + if ($i -eq $exStart) { $seedPlain = $p.Range } + elseif ($lvl -eq 1 -and -not $seedHead) { $seedHead = $p.Range } + elseif ($lvl -eq 2 -and -not $seedBullet) { $seedBullet = $p.Range } + } + if (-not $seedPlain -or -not $seedHead -or -not $seedBullet) { + throw 'Example block is missing a plain / level-1 / level-2 paragraph to use as a formatting seed.' + } + + # Snapshot the seeds' formatted text before inserting (live ranges shift as we edit). + $fmtPlain = $seedPlain.FormattedText + $fmtHead = $seedHead.FormattedText + $fmtBullet = $seedBullet.FormattedText + + # Insertion point: end of the Example block (Top) or end of the document (Bottom). + if ($Position -eq 'Top') { + $pos = $paras.Item($exEnd).Range.End + } + else { + $lastIdx = $paras.Count + while ($lastIdx -gt 1 -and (Get-ParaText $paras.Item($lastIdx)).Trim() -eq '') { $lastIdx-- } + $pos = $paras.Item($lastIdx).Range.End + } + + # Emit one paragraph cloned from a seed, set its text, advance the cursor. + $emit = { + param($fmt, [string]$text) + $r = $doc.Range($pos, $pos) + $r.FormattedText = $fmt + $p = $doc.Range($r.Start, $r.End).Paragraphs.Item(1) + Set-ParaText $p $text + $script:pos = $p.Range.End + } + + # Each entry starts on its own page: chr(12) is Word's manual page break (Ctrl+Enter), + # embedded at the head of the date line so no extra paragraph is introduced. + & $emit $fmtPlain ("$([char]12)$EntryDate") + for ($s = 0; $s -lt 4; $s++) { + & $emit $fmtHead $SectionHeadings[$s] + foreach ($b in $sections[$s]) { & $emit $fmtBullet $b } + } + + # Strip stray page-break characters Word may leave in trailing empty paragraphs, + # which would otherwise render a blank page at the end of the document. + for ($i = $doc.Paragraphs.Count; $i -ge 1; $i--) { + $t = $doc.Paragraphs.Item($i).Range.Text -replace "[\r\a]", '' + if ($t.Trim() -ne '') { break } + if ($t -match [string][char]12) { Set-ParaText $doc.Paragraphs.Item($i) '' } + } + + $doc.Save() + Write-Output "Wrote entry $EntryDate to $full (position: $Position)." +} +finally { + if ($doc -and $openedByUs) { $doc.Close([ref]$true) | Out-Null } + if ($word -and $openedByUs -and $word.Documents.Count -eq 0) { $word.Quit() | Out-Null } + [System.GC]::Collect() +} diff --git a/flow-1on1/Read-Tracker.ps1 b/flow-1on1/Read-Tracker.ps1 new file mode 100644 index 0000000..1921711 --- /dev/null +++ b/flow-1on1/Read-Tracker.ps1 @@ -0,0 +1,89 @@ +<# +.SYNOPSIS + Dumps the Daniel/Ahmed 1:1 notebook tracker (.docx) as plain text via Word COM. + +.DESCRIPTION + Word re-applies the document's MIP sensitivity label on every save, which wraps the + .docx in an encrypted OLE container. That means the file cannot be read by unzipping + it once it has been written once. Always read it through this script. + + Output is one line per paragraph, indented by list level, with a "---" rule before + each dated entry. + +.PARAMETER DocPath + Full path to the tracker .docx. + +.PARAMETER Last + Emit only the N most recent dated entries, newest first, skipping the Template and + Example blocks. Omit to dump the whole document. +#> +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)][string]$DocPath, + [int]$Last = 0 +) + +$ErrorActionPreference = 'Stop' +if (-not (Test-Path -LiteralPath $DocPath)) { throw "Tracker not found: $DocPath" } + +$word = $null +$doc = $null +$openedByUs = $false + +try { + try { $word = [Runtime.InteropServices.Marshal]::GetActiveObject('Word.Application') } + catch { $word = New-Object -ComObject Word.Application; $word.Visible = $false } + + $full = (Resolve-Path -LiteralPath $DocPath).Path + foreach ($d in $word.Documents) { if ($d.FullName -eq $full) { $doc = $d; break } } + if (-not $doc) { $doc = $word.Documents.Open($full, [ref]$false, [ref]$true); $openedByUs = $true } + + $rows = @() + $paras = $doc.Paragraphs + for ($i = 1; $i -le $paras.Count; $i++) { + $p = $paras.Item($i) + $text = ($p.Range.Text -replace "[\r\a]", '').TrimEnd() + $lvl = $p.Range.ListFormat.ListLevelNumber + $isDate = $text.Trim() -match '^\d{1,2}/\d{1,2}/\d{4}$' + $prefix = if ($lvl -ge 2) { ' - ' } elseif ($lvl -eq 1 -and $text -and -not $isDate) { ' ' } else { '' } + $rows += [pscustomobject]@{ IsDate = $isDate; Raw = $text.Trim(); Text = "$prefix$text" } + } + + if ($Last -le 0) { + foreach ($r in $rows) { + if ($r.IsDate) { Write-Output '---' } + if ($r.Text.Trim()) { Write-Output $r.Text } + } + return + } + + # Group into dated blocks. The first dated block belongs to the "Example:" section. + $blocks = @() + $current = $null + for ($i = 0; $i -lt $rows.Count; $i++) { + if ($rows[$i].IsDate) { + if ($current) { $blocks += $current } + $parsed = [datetime]::MinValue + [void][datetime]::TryParse($rows[$i].Raw, [ref]$parsed) + $current = [pscustomobject]@{ Date = $parsed; Label = $rows[$i].Raw; Lines = @() } + } + elseif ($current -and $rows[$i].Text.Trim()) { + $current.Lines += $rows[$i].Text + } + } + if ($current) { $blocks += $current } + + if ($blocks.Count -le 1) { Write-Output '(no entries yet - only the Example block is present)'; return } + + $entries = @($blocks[1..($blocks.Count - 1)] | Sort-Object -Property Date -Descending) + foreach ($e in ($entries | Select-Object -First $Last)) { + Write-Output '---' + Write-Output $e.Label + foreach ($l in $e.Lines) { Write-Output $l } + } +} +finally { + if ($doc -and $openedByUs) { $doc.Close([ref]$false) | Out-Null } + if ($word -and $openedByUs -and $word.Documents.Count -eq 0) { $word.Quit() | Out-Null } + [System.GC]::Collect() +} diff --git a/flow-1on1/SKILL.md b/flow-1on1/SKILL.md new file mode 100644 index 0000000..ed18356 --- /dev/null +++ b/flow-1on1/SKILL.md @@ -0,0 +1,220 @@ +--- +name: "flow-1on1" +description: "Prep Daniel's biweekly 1:1 with his manager Ahmed Atya (ES365 FLOW team). Reviews activity since the last 1:1, co-authors Ahmed's four-question notebook-tracker template one section at a time, writes the dated entry into the Daniel-Ahmed.docx tracker, then builds a private talking-points and issues brief. Triggers: /flow-1on1, prep my Ahmed 1:1, 1:1 with Ahmed, notebook tracker, manager 1:1 prep, flow 1:1." +--- + +## FLOW 1:1 Prep (Daniel ↔ Ahmed Atya) + +Prepares Daniel for his recurring 1:1 with **Ahmed Atya** (`ahmedatya@microsoft.com`, Member of +Technical Staff, ES365 US OPEX), his manager since the 2026-08-04 "agentic era" reorg placed +Daniel on the **FLOW** team (Fast, Lean Operations and Workflows). FLOW owns **Code Movement** +and **Operational Excellence**, two of Charles's top FY priorities. + +Two outputs, and the distinction between them is the whole point of this skill: + +1. **The tracker entry** — a dated entry written into the shared Word tracker. **Ahmed reads + this.** Treat it as outbound. +2. **The private brief** — talking points, open issues, asks, and landmines. **Only Daniel sees + this.** It never goes into the tracker. + +> Relationship to other skills: `rise-15-5` is the weekly team-wide status post; this is the +> per-1:1 manager tracker. `oneonone-pokedex` is the private post-meeting self-reflection. This +> skill runs *before* the meeting and owns the tracker document. + +### Cadence and context + +- **Biweekly**, per Ahmed's 2026-08-26 "Flow Team: Updates and Meeting Rhythm" mail: *"These + will initially be held biweekly for the next 90 days, and we'll adjust the cadence case by + case."* Re-check the calendar each run rather than assuming; the cadence is explicitly + provisional. +- Ahmed's expectation: *"Please spend about 10 minutes updating it before each meeting; if you + aren't able to, we'll use the first 5–10 minutes of the meeting to do so together."* The + tracker's purpose in his words: *"My goal is to help each of you capture your contributions + and ensure I can represent your work and impact in the best possible way."* Write for that + purpose — this is Connect/promo evidence, not a task log. +- Ahmed has an explicit **open-door policy** (Teams or email, any time). If something is urgent + it does not have to wait for the 1:1; say so when it comes up. +- First 1:1 was **2026-08-12** (intro). First working 1:1 with the tracker: **2026-09-01**. + +### Artifacts + +| What | Where | +|---|---| +| Tracker (shared with Ahmed) | `D:\OneDrive - Microsoft\Documents\1to1\Daniel-Ahmed.docx` | +| Ahmed's rhythm mail | `D:\OneDrive - Microsoft\Documents\1to1\Flow Team_ Updates and Meeting Rhythm.msg` | +| Prior 1:1 notes | `D:\Repos\Obsidian\03 - Squads\C4 - SRE Core\Meetings\` | +| Daily squad status | `D:\Repos\Obsidian\03 - Squads\C4 - SRE Core\Status Updates\` | +| Write entry | `Add-TrackerEntry.ps1` (this skill folder) | +| Read entries | `Read-Tracker.ps1` (this skill folder) | + +**The tracker is MIP-labeled.** Word re-applies the label on every save, which wraps the .docx +in an encrypted OLE container. After the first write it is **no longer readable by unzipping** — +always read it with `Read-Tracker.ps1`, never by extracting `word/document.xml`. + +### The template (Ahmed's, do not restyle it) + +Four questions, in this order, as list-level-1 italic headings with level-2 bullets underneath: + +1. **What are my Achievements, Impact and Key Results?** +2. **What's next? / What am I working on?** +3. **Lessons Learned/Things to Improve** +4. **Topics to discuss/What I need help with** + +Each entry is preceded by a bare `MM/DD/YYYY` date line. `Add-TrackerEntry.ps1` clones the +Example block's formatting, so never hand-type the structure into Word. + +--- + +## Workflow + +### 1. Establish the window + +- Run `Read-Tracker.ps1 -Last 2` to see the previous entries and get the "since when" boundary. + If it reports no entries, this is the first working 1:1 — use the last 1:1 date from the vault + (`Meetings\*Ahmed*`), or fall back to 14 days. +- Confirm the meeting date/time from the calendar (`workiq_list_events`) and note any conflicts + — the 1:1 has historically overlapped RISE Up. +- State the window in one line and get a nod before digging in. + +### 2. Gather raw material + +Pull from every source that is actually available. Do not skip one because it is slow. + +- **Vault (primary):** `03 - Squads\C4 - SRE Core\` — `Status Updates\` for the window, + `04 - Decisions.md`, `05 - Open Questions.md`, `Meetings\`. The daily status notes are the + densest source; read every one in the window. +- **Prior 1:1s (required — the user asked for this explicitly):** the last Ahmed 1:1 note *and* + the last Shaun 1:1 note. Commitments Daniel made in either are the first thing to check for + closure. Example: on 2026-08-27 Daniel committed to Shaun that he would raise career direction + with Ahmed. +- **ADO** (`azure_devops-*`): PRs authored/reviewed, work items moved. Note that generic searches + miss repos you do not already know about; if the pass comes up thin, ask Daniel directly. +- **IcM** (`icm-*`): incidents Daniel was DRI/mitigator on. `assignedTo` searches miss incidents + he mitigated but did not own — cross-check any IDs he names. +- **Calendar / Teams / mail** (`workiq_*`): meetings that produced decisions, threads where he + posted something substantive. +- **Ask Daniel** for anything invisible to tooling: hallway conversations, verbal agreements, + how he actually feels about the work. + +### 3. Co-author, one section at a time + +**Do not draft all four sections and dump them for approval.** For each section in order: + +- Silently draft 2–5 candidate bullets from the gathered material. +- Show Daniel only that section. +- Ask what to keep, cut, edit, or add. Genuinely co-author; do not rubber-stamp your own draft. +- Move on only once he confirms. + +Section-specific guidance: + +- **Achievements / Impact / Key Results** — outcomes, not activity. What shipped, what got + fixed, what moved closer to done, what changed for a customer or a service. This is the + section Ahmed will quote in calibration; make it defensible. +- **What's next** — in-flight initiatives and the next concrete step for each. Honest about + status; "prototype undeployed, first target service unresolved" beats "in progress." +- **Lessons Learned / Things to Improve** — real ones. This section is where a manager learns + whether someone is self-aware. A hollow lesson is worse than none. If Daniel has nothing + genuine, say so rather than manufacturing humility. +- **Topics to discuss / What I need help with** — the asks Daniel is willing to put in writing. + Anything sensitive that he would rather raise verbally goes in the **private brief instead**, + not here. Ask him which side of that line each item falls on when it is not obvious. + +### 4. Write the entry + +Build a UTF-8 JSON file (use `D:\Repos\Scratchpad\` for it) shaped like: + +```json +{ + "achievements": ["...", "..."], + "next": ["..."], + "lessons": ["..."], + "topics": ["..."] +} +``` + +Preview, then write: + +```powershell +$s = "C:\Users\dkucinski\.scout\m-skills\flow-1on1" +$doc = "D:\OneDrive - Microsoft\Documents\1to1\Daniel-Ahmed.docx" + +# dry run +powershell -NoProfile -ExecutionPolicy Bypass -File "$s\Add-TrackerEntry.ps1" ` + -DocPath $doc -JsonPath "D:\Repos\Scratchpad\1on1-entry.json" -Preview + +# write (newest entry goes to the top, under the Example block) +powershell -NoProfile -ExecutionPolicy Bypass -File "$s\Add-TrackerEntry.ps1" ` + -DocPath $doc -JsonPath "D:\Repos\Scratchpad\1on1-entry.json" +``` + +`-Position Bottom` appends chronologically instead; `-EntryDate MM/DD/yyyy` overrides today. + +**Before running the write:** show Daniel the complete entry verbatim and get an explicit +go-ahead. This document is read by his manager. Never write on an assumed yes. + +**After running:** re-read with `Read-Tracker.ps1 -Last 1` and confirm the entry landed with the +right formatting. Close Word if the script opened it. If Daniel has the tracker open in Word, +the script edits the live document — tell him to review and let OneDrive sync. + +### 5. Build the private brief + +Not written to the tracker. Render it in chat (and to the vault note in step 6): + +- **Open commitments** — Daniel's and Ahmed's, from prior 1:1s and status notes, with age. Flag + anything stale. +- **Talking points** — ranked, with the one-line "why now" for each. Lead with whatever is + genuinely most consequential, not whatever is easiest. +- **Issues / risks to surface** — blockers, things rotting quietly, decisions with no owner. +- **Asks** — what Daniel wants Ahmed to actually *do*: a decision, air cover, a headcount, an + introduction, a priority call. +- **Career thread** — the standing item. Keep it on every agenda until it is resolved; status + will crowd it out otherwise. +- **Questions for Ahmed** — 2–4 sharp ones. Org context, priorities, feedback. +- **Landmines** — anything unsettled between Ahmed and Shaun (e.g. the RISE-artifact kill list + vs. Ahmed's "Ops and RISE standups will remain") that Daniel should raise carefully rather + than walk into. + +### 6. Save a vault note + +Write to `D:\Repos\Obsidian\03 - Squads\C4 - SRE Core\Meetings\YYYY-MM-DD - Daniel - Ahmed 1-1 (prep).md` +with frontmatter `type: meeting`, `attendees: [Ahmed Atya, Daniel Kucinski]`, tags +`meeting`, `squad/c4-sre-core`, and wikilinks to the sources used. This is what the *next* run +reads for "our most recent 1:1s," and it makes the content first-class `connect` evidence. + +After the meeting, `oneonone-pokedex` handles the reflection; do not duplicate it here. + +--- + +## Voice & formatting + +Standing corrections from real editing passes with Daniel. Apply them in the **first** draft, +not after he flags them again. + +- **No receipts unless asked.** No IcM numbers, PR numbers, work-item IDs, or ticket links in + bullets. Describe the substance: "resolved a CloudBuild false-positive security alert," not + "resolved IcM 835355363." Ahmed's template shows `[Feature Link]` placeholders — offer links + only if Daniel wants them. +- **No em dashes.** Use a colon or semicolon. +- **Real output first, housekeeping second.** Lead with delivered work. Do not pad with "updated + a wiki page" or "captured a transcript." But do not overcorrect: a squad's first planning + kickoff or a mission-defining decision is genuine content. Substance vs. busywork is the line, + not "code only." When unsure, ask. +- **One idea per bullet, plain language.** No jargon stacking. +- **Write in first person**, matching Ahmed's example ("I delivered project XYZ"). + +If Daniel gives a correction during a run that generalizes, fold it into this section via +`m_update_skill` afterward so it applies from the first draft next time. + +## Guardrails + +- Never write to the tracker without the explicit final-review confirmation in step 4. +- Never put private career deliberation, peer comparisons, or anything about other people's + performance into the tracker. Those are verbal, or they go in the private brief only. +- The tracker is MIP-labeled. Do not copy its content into unlabeled destinations outside the + vault, and never read it by unzipping. +- If source-gathering surfaces something sensitive (HR matters, another person's private + situation), leave it out of the drafts entirely rather than asking whether to include it. +- Never invent an accomplishment, a lesson, or a commitment. If a section has nothing real in + it, write "None." and tell Daniel why it came up empty. +- If Word COM fails or the file is locked by OneDrive sync, stop and report it. Do not retry + blindly or fall back to rewriting the .docx by hand — that would strip the sensitivity label. diff --git a/get-meeting-transcript/.gitignore b/get-meeting-transcript/.gitignore new file mode 100644 index 0000000..b47fa75 --- /dev/null +++ b/get-meeting-transcript/.gitignore @@ -0,0 +1,3 @@ +.pw-profile/ +node_modules/ +*.log diff --git a/get-meeting-transcript/SKILL.md b/get-meeting-transcript/SKILL.md new file mode 100644 index 0000000..ceaf43c --- /dev/null +++ b/get-meeting-transcript/SKILL.md @@ -0,0 +1,369 @@ +--- +name: "get-meeting-transcript" +description: "Grab a Teams/Stream meeting transcript by driving a logged-in browser when the Graph download is blocked. Captures the underlying .vtt from network traffic, or scrapes the recap transcript pane. Accepts a Teams recap, Stream/SharePoint, or meet.microsoft.com URL, or a calendar reference it resolves to one. Saves .vtt + .md + the meeting chat into the Obsidian vault. Triggers: get transcript, scrape transcript, grab the transcript, meeting chat, transcript for my meeting." +--- + +## get-meeting-transcript + +When the user wants a transcript for a meeting they can view in the UI but +can't pull via Graph (`/me/onlineMeetings/.../transcripts` returns nothing or +403s), use this skill instead of giving up. + +**Before scraping, try the official download.** The scraper is the third +choice, not the first — see "Choosing a capture path" below. A downloaded +`.vtt` is strictly better than anything this script synthesizes, and getting +one costs the user about ten seconds. + +### Choosing a capture path + +Try these in order. Stop at the first that works. + +**1. Native Graph.** (`m365_*` does not currently expose transcripts; the EOD +automation uses an internal Stream call.) If it succeeds, you don't need this +skill at all. + +**2. Official `.vtt` download from the UI — prefer this over scraping.** +In the Teams recap (or the Stream/SharePoint player), the transcript pane has +a **download** action that yields the real `.vtt`: every cue at full +millisecond precision, proper `<v Speaker>` tags, and cue identifiers. That is +strictly better than the synthesized file this script produces from the DOM, +which is lossy in timing (see "Fidelity of the DOM scrape"). + +If the meeting is one the user can open right now, **ask them to download it** +before reaching for the scraper — it is faster than a manual-mode walk (which +already requires them to navigate to the same pane) and higher fidelity. Have +them save it into the meeting's output dir, then: + +- adopt it as `transcript.vtt` (replacing any scraped copy), and +- render `transcript.md` from it using the documented format — + `### HH:MM:SS — Speaker` headings, consecutive same-speaker cues collapsed + into paragraphs — **skipping the GUID cue-identifier lines**, which are not + transcript text. + +Record `source: "official Teams transcript download (.vtt)"` in the markdown +front matter so later readers know the timings are exact. + +**3. This scraper.** Use it when the download action is unavailable, blocked +by tenant policy, or the user isn't around to click it. Common cases: meetings +the user attended but didn't organize, restrictive transcript-export policy, +externally-organized meetings. Content and speaker attribution are reliable; +timings are not. + +If a scrape has already run and a download later becomes available, prefer the +download and regenerate `transcript.md` from it — the scraped `.vtt` should be +replaced, not kept alongside. + +### How it works + +A bundled Playwright script (`scrape-transcript.mjs`) drives Edge against a +persistent profile (`.pw-profile/`) so the user's Teams/Stream/SharePoint +sign-in carries over. + +**Two capture paths** (auto-detected, network first, DOM fallback): + +1. **Network interception** (rare — Teams encrypts transcript blobs in + transit, so this usually fails for Teams recap). Listens on every network + response for either a WEBVTT signature or a JSON transcript shape with + recognizable cue arrays (`{ text, startTime/startOffset, speaker }`). +2. **DOM scrape of the recap iframe** (the usual path for Teams meetings the + user doesn't organize). The transcript pane lives inside the + `RecapxPlatIframe` SharePoint iframe at + `*_layouts/15/xplatplugins.aspx?...hv=Recap*` inside a region with + `aria-label="Transcript"`. Cue entries are `[id^="entry-"]` with + `aria-label="@N M minutes S seconds"`. Speaker headers + (`.itemDisplayName-*`) only appear when the speaker changes — those get + forward-filled across following cues. + +**Virtualization handling.** The recap iframe uses a virtual list (only ~10 +cues rendered at once). To materialize every cue, the script focuses the +first entry then drives **PageDown + ArrowDown keypresses** to walk through +the entire list (`aria-setsize` tells us the total; normal-zone pacing is +PageDown+5×ArrowDown per pass with a 150ms settle wait). Cues are accumulated +by their stable `entry-N` id, so re-renders don't cause duplicates. + +**Tail resilience.** Big `PageDown` jumps are more likely to overshoot the +render boundary near the *end* of the list than in the middle — observed +failure mode: a run plateaus short of `aria-setsize` (e.g. 307/329, 369/402, +355/366) because the walk's stuck-counter trips right at the tail. Once +collection reaches 85% of the reported total, the script automatically +switches to gentler single `ArrowDown` steps with a 275ms settle wait, and +tolerates 3x more "stuck" passes before giving up. Keyboard nav only moves +list *focus*, though — some virtualized-list implementations render more +rows off a real, trusted scroll/wheel input rather than focus changes alone, +so a stall that survives repeated keyboard nudges can still respond to an +actual scroll. **Note:** a JS-dispatched synthetic `WheelEvent` + +manual `scrollTop` bump was tried first and confirmed *not* to work here — +diagnostics showed the transcript region has no CSS overflow-scroll +container at all (`scrollHeight === clientHeight` wherever probed), so those +synthetic events landed on nothing. The script instead uses Playwright's +`page.mouse.wheel()`: a trusted, OS-level input event dispatched at the +transcript pane's real screen coordinates, which can reach handlers that +reject untrusted/synthetic events or rely on the browser's native +wheel-to-scroll pipeline. Every 4th stuck pass in the tail zone, and +throughout the dedicated tail-recovery phase below, the script fires a mouse +wheel nudge as a supplement to keyboard nudges. If it still falls short +after the main loop, a dedicated tail-recovery phase spends an extra bounded +budget (`--tail-wait`, default 45s) alternating keyboard (`End`+`ArrowDown`) +and mouse-wheel nudges. The result JSON reports `cueCount`, `targetCueCount`, +and `truncated` so you can tell at a glance whether anything was missed — no +need to hand-write a gap analysis script. When truncated, the missing cues +are virtually always a contiguous block at the point the walk stalled +(usually the tail), not scattered through the transcript. + +If `--headed`, the user can click into the transcript pane manually; the +DOM scrape still works because it's keyed off the iframe structure, not on +the navigation path used to reach it. + +### Input modes + +**1. Direct URL.** Anything that resolves to a page showing the transcript: + +- Teams recap URL: `https://teams.microsoft.com/.../recap/...` +- Stream / SharePoint video page: `*.sharepoint.com/.../stream.aspx?id=...` + or `*.sharepoint.com/personal/.../_layouts/15/stream.aspx?...` + +**Live-join links auto-fallback to manual.** `onlineMeeting.joinUrl` / +`onlineMeetingUrl` values — `teams.microsoft.com/meet/...`, +`teams.microsoft.com/l/meetup-join/...`, `meet.microsoft.com/...` — always +open the pre-join/lobby screen, **never** the recap, even for meetings that +already ended. (Earlier revisions of this skill assumed Teams would redirect +these into the recap; it doesn't.) The script detects this pattern itself +(`isLiveJoinUrl()`) and immediately switches into the manual walk instead of +burning the `--wait` timeout on a dead-end screen — you don't need to +pre-filter these URLs or retry with `--manual` yourself; just pass whatever +URL you have and let the script decide. Don't waste a turn calling it with +the join URL expecting a direct hit — assume it'll need the manual walk and +tell the user up front so they're ready to navigate to Recap → Transcript. + +**2. Calendar reference.** User says "transcript from yesterday's 1:1 with +Alex" or names a meeting. The agent (you) must resolve this to a URL before +invoking the scraper: + +1. Call `m365_list_events` with an appropriate `startDate`/`endDate` window. +2. Match by subject / attendee. Confirm with the user via `m_ask_user` if + multiple events match. +3. Pull the transcript URL from the event: + - any SharePoint stream.aspx URL in the event body (often present after + recording is auto-saved to OneDrive) — try this first, it can go + straight to a direct-URL capture. + - `onlineMeeting.joinUrl` / a `teams.microsoft.com/meet/...` link in the + body — this will auto-fallback to the manual walk (see above), so treat + it as "go straight to manual" rather than a URL worth waiting on. +4. Hand the URL to `scrape-transcript.mjs`. + +If neither lookup succeeds, ask the user to paste the URL directly. If you +already know the only URL available is a join link, you can skip straight to +`--manual` yourself and tell the user to expect the manual walk — no need to +round-trip through a direct-URL attempt first. + +### Invocation + +```pwsh +node "C:\Users\dkucinski\.scout\m-skills\get-meeting-transcript\scrape-transcript.mjs" ` + --url "<meeting or stream URL>" ` + [--out "<output dir>"] ` + [--subject "<filename slug>"] ` + [--headed] ` + [--manual] ` + [--wait 60] ` + [--tail-wait 45] ` + [--debug] +``` + +- `--url` is the page to land on. Skip when using `--manual`. If `--url` is + a live-join link (`teams.microsoft.com/meet/...`, `/l/meetup-join/...`, + `meet.microsoft.com/...`), the script auto-detects it and behaves as if + `--manual` were passed (overrides the URL to `teams.microsoft.com/v2/`, + extends the wait to 300s) — printing a note to stderr explaining why. +- `--manual` opens `teams.microsoft.com` and waits while the user navigates + to the recap themselves (use this directly when you already know the only + URL you have is a join link — no need to let the script rediscover that). + Implies `--headed`. + Default wait jumps to 300s in manual mode. Extraction starts + **automatically** as soon as the transcript pane is detected — you just + navigate to Recap → Transcript and the scraper proceeds on its own. After + reaching the pane, clicking one cue is recommended (gives the list keyboard + focus for the PageDown/ArrowDown walk), but the scraper also self-focuses + the first cue. Pressing Enter in the terminal is an optional override that + forces extraction to start immediately. The old behavior — blocking until + you typed something — has been removed, so manual mode works whether or not + stdin is interactive. +- `--out` defaults to `D:\Repos\Obsidian\02 - Meetings\YYYY-MM-DD - <subject>\` + using today's date and `--subject` (falls back to `meeting`). Pass an + explicit path with the meeting's own date when scraping past meetings. +- `--headed` opens a visible browser window. Required on first run so the + user can sign in. +- `--wait N` overrides the seconds to wait for transcript readiness/capture + after the page loads (default 45; 300 in `--manual`). Bump higher for very + long meetings (the keyboard walk takes ~1 ArrowDown per cue). +- `--tail-wait N` extra seconds (default 45) spent on gentle End+ArrowDown + recovery nudges if the DOM-scrape walk plateaus short of the transcript's + reported total (`aria-setsize`) — see "Tail resilience" above. Set to `0` + to disable and fail fast instead. +- `--debug` logs every response URL + content-type the listener sees plus + scroll-progress dumps, for diagnosing capture failures. + +stdout is a single JSON object: + +```json +{ "ok": true, "vtt": "C:\\...\\transcript.vtt", "md": "C:\\...\\transcript.md", "cueCount": 785, "targetCueCount": 785, "truncated": false, "bytes": 100976, "source": "dom" } +``` + +`source` is `"network"` when captured from a WEBVTT/JSON response, or +`"dom"` when extracted from the recap iframe DOM. For Teams meetings, expect +`"dom"` — the transcript blob is encrypted in transit. `targetCueCount` is +only present for DOM-scrape captures (the iframe's `aria-setsize`); if +`truncated` is `true`, `cueCount` is short of it — check stderr for the +`[warn] transcript is likely INCOMPLETE` line, and consider re-running with +a larger `--tail-wait` if it keeps happening. + +Exit codes: `0` ok, `2` bad args, `3` no transcript captured before +timeout, `4` page never loaded. + +### One-time setup + +First run, always pass `--headed`. The script opens Edge against +`.pw-profile/`. Sign in to teams.microsoft.com and/or the SharePoint tenant +when prompted. The profile persists; subsequent runs reuse it silently. + +### Output + +Files written into the output dir: + +- `transcript.vtt` — WEBVTT. When `source` is `"network"`, this is byte-for-byte + what the UI player consumes. When `source` is `"dom"` (the usual case for + Teams), it is a **synthesized** WEBVTT and is **lossy in timing** — see + "Fidelity of the DOM scrape" below. +- `transcript.md` — same content as readable markdown with + `### HH:MM:SS — Speaker` headings and consecutive same-speaker cues + collapsed into single paragraphs. Easy to read in Obsidian. +- `chat.md` — the meeting chat, when one exists. Written by the agent, not + the script. See "Meeting chat capture" below. + +Default output dir matches the existing EOD meeting-archive layout under +`D:\Repos\Obsidian\02 - Meetings\` so transcripts grabbed this way slot +into the same vault structure as the automated ones. + +### Fidelity of the DOM scrape + +Measured 2026-08-14 against an official `.vtt` download of the same 57-minute +meeting (Astra Analytics walkthrough), the DOM scrape was: + +- **Faithful on text** — 10,507 words vs 10,503 in the official file. The 4 + extra are the "X started/stopped transcription" system lines, which the + recap DOM exposes and the official export omits. +- **Faithful on speaker attribution** — per-speaker word counts matched within + ±4 words across all five participants. +- **Lossy on timing.** 433 captured entries vs 1,091 real cues. The recap DOM + exposes *grouped* entries, and the script rounds each to whole seconds, so + a synthesized cue spans several real ones (e.g. `00:00:03.000 --> 00:00:24.000` + covers five cues running `00:00:03.288 --> 00:00:24.168`). + +So: **the DOM scrape is trustworthy for content and attribution, and should +not be trusted for precise timings.** Don't cite DOM-scraped timestamps as +exact, and don't use them to align against recordings or other time-series. + +Note that `cueCount` counts *recap entries*, not VTT cues — `433/433` with +`truncated: false` is a complete capture even though the real transcript has +1,091 cues. The two numbers are not comparable, and a mismatch is not a bug. + +**If you verify a capture against a reference `.vtt`, strip the cue-identifier +lines first.** Official Teams exports put a GUID id line (e.g. +`63fe10ca-…-152225ba9c70/7-1`) before each timestamp; a naive parser counts +those as transcript text and will report a large phantom word deficit +(~7 tokens × cue count). This has already produced one false "41% of content +is missing" alarm. + +### Meeting chat capture (`chat.md`) + +After a successful transcript capture, also grab the **meeting chat** — the +Teams conversation attached to the same meeting. It routinely carries what the +transcript can't: links pasted mid-call, files shared, and the follow-up +traffic that lands in the hours or days afterwards. + +This is an **agent** step. `scrape-transcript.mjs` does not do it — the script +only handles the transcript pane. Do it after the script returns `ok: true`. + +**1. Resolve the meeting chat id.** + +- `workiq_search_chats` with the meeting subject is usually enough. Meeting + chats come back with `chatType: "meeting"` and an id shaped + `19:meeting_<base64>@thread.v2`. +- Sanity-check the returned `members` against the calendar event's attendees + before using it. If the subject is generic and several chats match, confirm + via `m_ask_user` — never guess. +- If no chat exists (common for meetings nobody typed in), say so and move on. + A missing chat is not a failure of the run. + +**2. Export it to `<out>/chat.md`.** + +Follow the `/teams-export` rendering conventions so it reads identically to +everything under `00 - Chats\`: `##` for the date (YYYY-MM-DD), `###` for each +message (`HH:mm — Author`), replies as nested blockquotes, attachments +collapsed to a single `**Attachments:**` line (never download binaries), +message HTML converted to markdown, timestamps in America/Los_Angeles 24-hour, +and `---` between root messages. + +Front matter: + +```yaml +--- +type: reference +subtype: meeting-chat +date: <meeting date, YYYY-MM-DD> +chat_id: "19:meeting_...@thread.v2" +topic: <meeting subject> +captured: <YYYY-MM-DD> +--- +``` + +Export the **whole** chat rather than a date window — meeting chats are small, +and the valuable part is often the post-meeting tail. + +**3. Emit a `sources.yaml` suggestion — do NOT write it.** + +Most meeting chats are dead within a day, and `chat.md` is the right home for +those: self-contained, sitting next to the transcript it belongs to. But some +keep producing follow-up traffic for days, and those are worth promoting to a +standing source so `/teams-export` keeps pulling them. + +That promotion is the user's call, not this skill's. `sources.yaml` is +hand-maintained and `/teams-export` is explicitly forbidden from writing to it; +this skill must not write to it either. Instead, print a ready-to-paste block +and let the user decide: + +```yaml +# Suggested addition to D:\Repos\Obsidian\00 - Chats\sources.yaml (groups:) + - alias: <kebab-case-slug> + type: group + chat_id: "19:meeting_...@thread.v2" + topic: <meeting subject> + notes: >- + Added <YYYY-MM-DD>. Meeting chat for the <date> <subject> session. + Transcript captured under "02 - Meetings/<date> - <subject>". + Retire with enabled:false once follow-up traffic stops. +``` + +Recommend promotion when the meeting has a live follow-up thread — handoffs, +access grants, artifacts still to be shared, or a recurring series. Recommend +against it for one-off sessions that concluded in the room; `chat.md` already +covers those, and every dead entry left in `sources.yaml` is re-polled on every +future export run. + +### Guardrails + +- `.pw-profile/` and any captured cookies are credentials. Never echo them, + never copy them outside the skill dir. +- Transcripts may contain sensitive content (MIP labels are not surfaced via + the UI capture). Treat any transcript saved this way as at least as + sensitive as the calendar event itself. Warn the user before any outbound + share. +- Only write inside the `--out` dir. Do not modify anything else in the + Obsidian vault. This includes `00 - Chats\sources.yaml` — that file is + hand-maintained by the user; suggest additions, never apply them. +- `chat.md` inherits the same sensitivity treatment as the transcript, and can + be worse — chats carry pasted links, file names, and customer detail that + never got said out loud. Same rule: warn before any outbound share. +- If the user supplies a calendar reference that resolves to multiple + events, always confirm via `m_ask_user` — never guess. Same for a meeting + subject that matches multiple Teams chats. diff --git a/get-meeting-transcript/package-lock.json b/get-meeting-transcript/package-lock.json new file mode 100644 index 0000000..4099e14 --- /dev/null +++ b/get-meeting-transcript/package-lock.json @@ -0,0 +1,59 @@ +{ + "name": "get-meeting-transcript", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "get-meeting-transcript", + "version": "1.0.0", + "dependencies": { + "playwright": "^1.60.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + } + } +} diff --git a/get-meeting-transcript/package.json b/get-meeting-transcript/package.json new file mode 100644 index 0000000..d9fa369 --- /dev/null +++ b/get-meeting-transcript/package.json @@ -0,0 +1,10 @@ +{ + "name": "get-meeting-transcript", + "version": "1.0.0", + "description": "Scrape Teams/Stream meeting transcripts from the UI when Graph download isn't permitted.", + "type": "commonjs", + "private": true, + "dependencies": { + "playwright": "^1.60.0" + } +} diff --git a/get-meeting-transcript/scrape-transcript.mjs b/get-meeting-transcript/scrape-transcript.mjs new file mode 100644 index 0000000..709b5d3 --- /dev/null +++ b/get-meeting-transcript/scrape-transcript.mjs @@ -0,0 +1,952 @@ +#!/usr/bin/env node +// scrape-transcript.mjs — Capture a Teams/Stream meeting transcript from the UI. +// +// Strategy: drive a logged-in Edge (persistent profile) at the supplied URL, +// listen on every network response, and grab the first body that matches the +// WEBVTT signature (text/vtt content-type OR body starting with "WEBVTT"). +// That's the same file the transcript pane renders from, so we don't need to +// know the exact backend endpoint — works for Stream-on-SharePoint, the older +// Microsoft Stream, the Teams web recap, and anything else that uses WebVTT. +// +// If no transcript pane opens automatically, in --headed mode the user can +// click it themselves and the network listener still catches the response. +// +// Live-join links (teams.microsoft.com/meet/..., /l/meetup-join/..., +// meet.microsoft.com/...) ALWAYS land on the pre-join/lobby screen, never the +// recap, even for meetings that already ended — there's no point waiting out +// the timeout on one. isLiveJoinUrl() detects these and auto-switches to the +// manual walk (navigate to teams.microsoft.com/v2/, wait for the user to open +// Recap → Transcript themselves) right away instead of after a wasted --wait. +// +// USAGE +// node scrape-transcript.mjs --url <url> [--out <dir>] [--subject <name>] +// [--headed] [--wait 45] [--tail-wait 45] [--debug] +// (--url pointing at a live-join link auto-upgrades to --manual behavior) +// --tail-wait: extra seconds spent on gentle recovery nudges (End+ArrowDown) +// if the DOM-scrape virtualized-list walk plateaus a few cues short of the +// transcript's reported total (aria-setsize). Default 45s; 0 disables. +// +// EXIT CODES +// 0 ok, 2 bad args, 3 no transcript captured before timeout, 4 page load failed. + +import { chromium } from "playwright"; +import { mkdirSync, writeFileSync, existsSync, rmSync, cpSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { tmpdir } from "node:os"; + +// Force synchronous (line-buffered) stderr/stdout on Windows pipes so progress +// messages appear in real time instead of being held in a buffer until exit. +if (process.stderr && process.stderr._handle && typeof process.stderr._handle.setBlocking === "function") { + try { process.stderr._handle.setBlocking(true); } catch {} +} +if (process.stdout && process.stdout._handle && typeof process.stdout._handle.setBlocking === "function") { + try { process.stdout._handle.setBlocking(true); } catch {} +} + +const __dirname = dirname(fileURLToPath(import.meta.url)); +// Canonical persistent profile (holds cookies, sign-in state). On Windows, +// ghost handles can leave this dir locked across runs even after the process +// that held it has exited. To make launches reliable we always clone this dir +// into a unique temp profile per run, launch from the clone, and copy cookies +// back at the end so the next run still benefits from cached auth. +const CANONICAL_PROFILE_DIR = join(__dirname, ".pw-profile"); + +// --- arg parsing ----------------------------------------------------------- +function parseArgs(argv) { + const out = { headed: false, debug: false, waitSec: 45, tailWaitSec: 45 }; + for (let i = 2; i < argv.length; i++) { + const a = argv[i]; + const next = () => argv[++i]; + switch (a) { + case "--url": out.url = next(); break; + case "--out": out.outDir = next(); break; + case "--subject": out.subject = next(); break; + case "--headed": out.headed = true; break; + case "--manual": out.manual = true; out.headed = true; break; + case "--wait": out.waitSec = Number.parseInt(next(), 10) || 45; break; + // Extra budget (default 45s) spent on gentle End/ArrowDown recovery + // nudges if the DOM-scrape virtualized-list walk plateaus a few cues + // short of the transcript's reported total. Set to 0 to disable. + case "--tail-wait": out.tailWaitSec = Number.parseInt(next(), 10); if (Number.isNaN(out.tailWaitSec)) out.tailWaitSec = 45; break; + // Testing/debug only: ignore any network VTT capture and force the DOM + // fallback path even when a network capture succeeded. Network capture + // availability is timing-dependent (Teams finishes processing the VTT + // within minutes of the meeting ending), which makes the DOM path hard + // to exercise on demand otherwise. + case "--force-dom": out.forceDom = true; break; + case "--debug": out.debug = true; break; + case "-h": case "--help": out.help = true; break; + default: throw new Error(`Unknown arg: ${a}`); + } + } + return out; +} + +let args; +try { + args = parseArgs(process.argv); +} catch (e) { + process.stderr.write(`ERR: ${e.message}\n`); + process.exit(2); +} + +if (args.help) { + process.stderr.write(`See header of ${import.meta.url}\n`); + process.exit(0); +} +if (!args.url && !args.manual) { + process.stderr.write("ERR: --url is required (or use --manual to start at teams.microsoft.com)\n"); + process.exit(2); +} +if (args.manual && !args.url) { + args.url = "https://teams.microsoft.com/v2/"; +} + +// --- live-join link detection ----------------------------------------------- +// Meeting-invite "Join" links (teams.microsoft.com/meet/..., /l/meetup-join/..., +// meet.microsoft.com/...) always land on the live pre-join/lobby screen, never +// on the recap — that's true for both live and already-ended meetings. There is +// no direct-URL path to a transcript from one of these, so waiting out the +// network/DOM-detection timeout on that URL is pure wasted time. Detect this +// case up front and immediately fall back to the manual walk instead of +// discovering it only after the full --wait timeout expires. +function isLiveJoinUrl(url) { + if (!url) return false; + return /teams\.microsoft\.com\/(l\/)?meetup-join\//i.test(url) + || /teams\.microsoft\.com\/meet\//i.test(url) + || /meet\.microsoft\.com\//i.test(url); +} + +if (!args.manual && isLiveJoinUrl(args.url)) { + process.stderr.write( + `[info] "${args.url}" is a live-join link — these always open the pre-join\n` + + `screen, never the recap/transcript, even for meetings that already ended.\n` + + `Switching to manual mode automatically: opening Teams so you can navigate\n` + + `to Calendar → the meeting → Recap → Transcript yourself.\n` + ); + args.manual = true; + args.autoSwitchedManual = true; + args.originalUrl = args.url; + args.url = "https://teams.microsoft.com/v2/"; +} + +// --- output paths ---------------------------------------------------------- +function slugify(s) { + return (s || "meeting") + .toString() + .replace(/[\\/:*?"<>|]/g, "") + .replace(/\s+/g, " ") + .trim() + .slice(0, 80) || "meeting"; +} + +function todayISO() { + const d = new Date(); + const pad = (n) => String(n).padStart(2, "0"); + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; +} + +const subjectSlug = slugify(args.subject); +const outDir = resolve( + args.outDir || `D:\\Repos\\Obsidian\\02 - Meetings\\${todayISO()} - ${subjectSlug}` +); +mkdirSync(outDir, { recursive: true }); + +const vttPath = join(outDir, "transcript.vtt"); +const mdPath = join(outDir, "transcript.md"); + +// --- WEBVTT helpers -------------------------------------------------------- +function looksLikeVtt(text) { + if (!text) return false; + // Tolerate BOM + leading whitespace + const head = text.replace(/^\uFEFF/, "").trimStart(); + return head.startsWith("WEBVTT"); +} + +function pickPreferredVtt(captures) { + // Prefer English; otherwise largest body (most cues). + const english = captures.filter((c) => + /[-_./?&=](en|en[-_]us|en[-_]gb)\b/i.test(c.url) || + /lang=en/i.test(c.url) || + /\benglish\b/i.test(c.url) + ); + const pool = english.length ? english : captures; + return pool.reduce((a, b) => (b.text.length > a.text.length ? b : a)); +} + +// Recursively walk a JSON value looking for an array of cue-shaped objects. +// Returns [{ start, end, speaker, text }] or null. Handles common shapes: +// Microsoft Stream: entries: [{ startOffset, endOffset, text, speakerDisplayName }] +// Teams/Stream new: items / cues / segments: [{ startTime, endTime, text, speakerId|speaker }] +// Bare arrays of such objects +function extractCuesFromJson(root) { + let best = null; + const visit = (node) => { + if (!node || typeof node !== "object") return; + if (Array.isArray(node)) { + if (node.length > 0 && typeof node[0] === "object" && node[0] !== null) { + const cues = node.map(cueFromObject).filter(Boolean); + if (cues.length >= 3 && (!best || cues.length > best.length)) best = cues; + } + for (const v of node) visit(v); + return; + } + for (const k of Object.keys(node)) visit(node[k]); + }; + visit(root); + return best; +} + +function cueFromObject(o) { + if (!o || typeof o !== "object") return null; + const text = o.text ?? o.transcript ?? o.caption ?? o.value ?? o.content; + if (typeof text !== "string" || !text.trim()) return null; + const start = parseTimeField( + o.startTime ?? o.startOffset ?? o.start ?? o.timeOffset ?? o.beginTime ?? o.startTimestamp + ); + const end = parseTimeField( + o.endTime ?? o.endOffset ?? o.end ?? o.endTimestamp ?? (start != null ? start + 3 : null) + ); + if (start == null) return null; + const speaker = + (o.speakerDisplayName || o.speakerName || o.speaker || o.author || o.userDisplayName || "") + .toString() + .trim(); + return { start, end: end ?? start + 3, speaker, text: text.trim() }; +} + +function parseTimeField(v) { + if (v == null) return null; + if (typeof v === "number") { + // Heuristic: if >100000, assume ms; if also >100000000, assume ticks (100ns) + if (v > 100_000_000_000) return v / 10_000_000; // ticks + if (v > 100_000) return v / 1000; // ms + return v; // seconds + } + if (typeof v === "string") { + // ISO duration "PT1M23.45S" + const iso = v.match(/^PT(?:(\d+)H)?(?:(\d+)M)?(?:([\d.]+)S)?$/); + if (iso) return (parseInt(iso[1] || "0", 10) * 3600) + (parseInt(iso[2] || "0", 10) * 60) + parseFloat(iso[3] || "0"); + // HH:MM:SS(.mmm) or MM:SS(.mmm) + const tm = v.match(/^(?:(\d+):)?(\d{1,2}):(\d{2}(?:\.\d+)?)$/); + if (tm) return (parseInt(tm[1] || "0", 10) * 3600) + (parseInt(tm[2], 10) * 60) + parseFloat(tm[3]); + const n = parseFloat(v); + if (!Number.isNaN(n)) return n; + } + return null; +} + +function cuesToVtt(cues) { + const fmt = (s) => { + const ms = Math.floor((s % 1) * 1000); + const sec = Math.floor(s) % 60; + const min = Math.floor(s / 60) % 60; + const hr = Math.floor(s / 3600); + const pad = (n, w) => String(n).padStart(w, "0"); + return `${pad(hr, 2)}:${pad(min, 2)}:${pad(sec, 2)}.${pad(ms, 3)}`; + }; + let out = "WEBVTT\n\n"; + for (const c of cues.sort((a, b) => a.start - b.start)) { + const tag = c.speaker ? `<v ${c.speaker}>` : ""; + out += `${fmt(c.start)} --> ${fmt(Math.max(c.end, c.start + 0.1))}\n${tag}${c.text}\n\n`; + } + return out; +} + +// WEBVTT cue parser: returns [{ start, end, speaker, text }] +function parseVtt(vtt) { + const lines = vtt.replace(/\r\n/g, "\n").split("\n"); + const cues = []; + let i = 0; + // skip header + while (i < lines.length && !lines[i].includes("-->")) i++; + while (i < lines.length) { + // cue may have an id line above the timing line; back up if so + let timingLine = lines[i]; + if (!timingLine || !timingLine.includes("-->")) { i++; continue; } + const m = timingLine.match(/(\d{1,2}:)?(\d{1,2}):(\d{2})(?:[.,](\d{1,3}))?\s*-->\s*(\d{1,2}:)?(\d{1,2}):(\d{2})(?:[.,](\d{1,3}))?/); + if (!m) { i++; continue; } + const toSec = (h, mm, ss, ms) => + (parseInt(h || "0", 10) * 3600) + (parseInt(mm, 10) * 60) + parseInt(ss, 10) + (parseInt(ms || "0", 10) / 1000); + const start = toSec(m[1] && m[1].replace(":", ""), m[2], m[3], m[4]); + const end = toSec(m[5] && m[5].replace(":", ""), m[6], m[7], m[8]); + i++; + const textLines = []; + while (i < lines.length && lines[i].trim() !== "") { + textLines.push(lines[i]); + i++; + } + // skip blank + while (i < lines.length && lines[i].trim() === "") i++; + let raw = textLines.join(" ").trim(); + if (!raw) continue; + // Speaker conventions: "<v Speaker Name>text</v>" OR "Speaker Name: text" + let speaker = ""; + let text = raw; + const v = raw.match(/^<v\s+([^>]+)>(.*?)(?:<\/v>)?$/i); + if (v) { + speaker = v[1].trim(); + text = v[2].trim(); + } else { + const colon = raw.match(/^([A-Z][^:]{0,60}):\s+(.*)$/); + if (colon) { speaker = colon[1].trim(); text = colon[2].trim(); } + } + // strip remaining tags + text = text.replace(/<[^>]+>/g, "").trim(); + cues.push({ start, end, speaker, text }); + } + return cues; +} + +function fmtTs(sec) { + const s = Math.max(0, Math.floor(sec)); + const h = Math.floor(s / 3600); + const m = Math.floor((s % 3600) / 60); + const ss = s % 60; + const pad = (n) => String(n).padStart(2, "0"); + return `${pad(h)}:${pad(m)}:${pad(ss)}`; +} + +function cuesToMarkdown(cues, sourceUrl, outDirPath) { + // Extract date + title from the canonical "YYYY-MM-DD - <Title>" folder name. + // Falls back to today if the folder doesn't match the expected pattern. + const folderName = (outDirPath || "").split(/[\\\/]/).pop() || ""; + const dateMatch = folderName.match(/^(\d{4}-\d{2}-\d{2})/); + const meetingDate = dateMatch ? dateMatch[1] : todayISO(); + const titleMatch = folderName.match(/^\d{4}-\d{2}-\d{2}\s*-\s*(.+)$/); + const title = titleMatch ? titleMatch[1].trim() : (folderName || "Meeting"); + const today = todayISO(); + + const out = []; + // Schema-conformant frontmatter per D:\Repos\Obsidian\AGENTS.md §3 + out.push("---"); + out.push(`type: meeting`); + out.push(`title: "${title.replace(/"/g, "'")}"`); + out.push(`date: ${meetingDate}`); + out.push(`created: ${today}`); + out.push(`updated: ${today}`); + out.push(`attendees: []`); + out.push(`tags: [meeting]`); + out.push(`source_url: "${sourceUrl}"`); + out.push(`captured_at: "${new Date().toISOString()}"`); + out.push(`cue_count: ${cues.length}`); + out.push("---"); + out.push(""); + out.push(`# ${title}`); + out.push(""); + out.push("## Transcript"); + out.push(""); + // Group consecutive cues from same speaker + let i = 0; + while (i < cues.length) { + const speaker = cues[i].speaker || "Unknown"; + const startTs = fmtTs(cues[i].start); + const parts = [cues[i].text]; + let j = i + 1; + while (j < cues.length && (cues[j].speaker || "Unknown") === speaker) { + parts.push(cues[j].text); + j++; + } + out.push(`### ${startTs} — ${speaker}`); + out.push(""); + out.push(parts.join(" ").replace(/\s+/g, " ").trim()); + out.push(""); + i = j; + } + return out.join("\n"); +} + +// --- DOM fallback: scrape Teams recap transcript pane (RecapxPlatIframe) --- +// The transcript lives in an iframe at *_layouts/15/xplatplugins.aspx*?hv=Recap +// inside a region with aria-label="Transcript". Each cue is a `.baseEntry-371` +// with aria-label="@N M minutes S seconds" and a `.entryText-372` child. +// Speaker headers (`.itemDisplayName-387`) appear only when speaker changes. +async function domScrapeFallback(page, debug, tailWaitMs) { + let recapFrame = null; + for (const f of page.frames()) { + const has = await f + .evaluate(() => !!document.querySelector('[aria-label="Transcript"][role="complementary"]')) + .catch(() => false); + if (has) { recapFrame = f; break; } + } + if (!recapFrame) { + process.stderr.write("[dom] no frame contains the transcript region\n"); + return { vttText: null, cueCount: 0, targetSize: 0 }; + } + process.stderr.write(`[dom] transcript frame found: ${recapFrame.url()}\n`); + + // The recap transcript uses a virtual list. Generic scrollIntoView does not + // trigger more entries to render. The documented navigation is arrow keys — + // we focus the first cue and press ArrowDown repeatedly, harvesting between + // batches. Each ArrowDown moves selection forward, scrolling the virtual + // list naturally and rendering off-screen entries. + await recapFrame.evaluate(() => { + const region = document.querySelector('[aria-label="Transcript"][role="complementary"]'); + if (!region) return; + const first = region.querySelector('[id^="entry-"] [data-is-focusable="true"]') || + region.querySelector('[id^="entry-"]'); + if (first && typeof first.focus === "function") first.focus(); + }); + + const collected = new Map(); // entry-N -> cue + const seenSpeakers = []; // ordered list of speaker names by appearance + let lastSpeaker = ""; + + const harvestOnce = async () => { + const batch = await recapFrame.evaluate(() => { + const region = document.querySelector('[aria-label="Transcript"][role="complementary"]'); + if (!region) return { entries: [], speakerHeaders: [] }; + const entries = []; + for (const e of region.querySelectorAll('[id^="entry-"]')) { + const al = e.getAttribute("aria-label") || ""; + const hM = al.match(/(\d+)\s*hours?/i); + const mM = al.match(/(\d+)\s*minutes?/i); + const sM = al.match(/(\d+)\s*seconds?/i); + let start = 0; + if (hM || mM || sM) { + start = (hM ? parseInt(hM[1], 10) * 3600 : 0) + + (mM ? parseInt(mM[1], 10) * 60 : 0) + + (sM ? parseInt(sM[1], 10) : 0); + } + const textEl = e.querySelector('[class*="entryText" i]') || + e.querySelector('[id^="sub-entry-"]'); + const text = (textEl ? textEl.innerText : e.innerText || "").trim(); + if (!text) continue; + // Inline speaker from the row this entry belongs to + let inlineSpeaker = ""; + const row = e.closest('[class*="listItemWithSpeaker" i]'); + if (row) { + const dn = row.querySelector('[class*="itemDisplayName" i]'); + if (dn) inlineSpeaker = (dn.innerText || "").trim(); + } + entries.push({ id: e.id, idx: parseInt(e.id.slice("entry-".length), 10), start, text, inlineSpeaker }); + } + // Also collect rendered speaker header positions (idx -> name) so we can + // forward-fill speakers for entries that don't have an inline header. + const speakerHeaders = []; + for (const row of region.querySelectorAll('[class*="listItemWithSpeaker" i]')) { + const dn = row.querySelector('[class*="itemDisplayName" i]'); + const firstEntry = row.querySelector('[id^="entry-"]'); + if (dn && firstEntry) { + speakerHeaders.push({ + idx: parseInt(firstEntry.id.slice("entry-".length), 10), + name: (dn.innerText || "").trim(), + }); + } + } + return { entries, speakerHeaders }; + }); + // Merge speaker headers + for (const sh of batch.speakerHeaders) { + seenSpeakers.push(sh); + } + for (const c of batch.entries) { + if (collected.has(c.id)) { + // Update speaker if previously empty + if (!collected.get(c.id).speaker && c.inlineSpeaker) { + collected.get(c.id).speaker = c.inlineSpeaker; + } + continue; + } + collected.set(c.id, { + idx: c.idx, start: c.start, speaker: c.inlineSpeaker || "", text: c.text, + }); + } + }; + + // Read target + const targetSize = await recapFrame.evaluate(() => { + const probe = document.querySelector('[aria-label="Transcript"][role="complementary"] [aria-setsize]'); + return probe ? parseInt(probe.getAttribute("aria-setsize") || "0", 10) : 0; + }).catch(() => 0); + + await harvestOnce(); + process.stderr.write(`[dom] initial harvest: ${collected.size}/${targetSize}\n`); + + // Keyboard nav (End/ArrowDown) only moves list *focus*. Earlier revisions of + // this function tried dispatching a synthetic WheelEvent + manual scrollTop + // bump from inside the page — but testing showed the transcript region has + // no CSS overflow-scroll container at all (scrollHeight === clientHeight + // wherever we looked), so those synthetic events landed on nothing and + // never had a chance to help. Playwright's page.mouse.wheel() is different: + // it's a trusted, OS-level input event dispatched at real screen + // coordinates, which can reach handlers that reject synthetic/untrusted + // events (isTrusted checks) or rely on the browser's native wheel-to-scroll + // pipeline rather than a JS-visible overflow container. We position the + // mouse over the transcript pane first since wheel targets whatever is + // under the cursor. + const mouseWheelNudge = async (deltaY = 800) => { + try { + const handle = await recapFrame.$('[aria-label="Transcript"][role="complementary"]'); + if (!handle) return { ok: false, reason: "no-region" }; + const box = await handle.boundingBox(); + if (!box) return { ok: false, reason: "no-bbox" }; + const x = box.x + box.width / 2; + const y = box.y + box.height / 2; + await page.mouse.move(x, y); + await page.mouse.wheel(0, deltaY); + return { ok: true, x, y, deltaY }; + } catch (e) { + return { ok: false, reason: e.message }; + } + }; + + let stuck = 0; + let prevSize = collected.size; + const domStart = Date.now(); + const domDeadlineMs = 600_000; // hard 10-min cap on DOM scrape + // Once we're this close to the target, the remaining rows are the tail of a + // virtualized list — big PageDown jumps are more likely to overshoot the + // render boundary there than in the middle of the transcript, which is how + // a run can plateau a few cues short of the end. Slow down and be patient + // once we cross this threshold. + const TAIL_FRACTION = 0.85; + const STUCK_LIMIT_NORMAL = 8; + const STUCK_LIMIT_TAIL = 24; // 3x more patience once we're in the tail zone + // Press ArrowDown in chunks; PageDown jumps faster when supported + for (let pass = 0; pass < 3000; pass++) { + const nearingEnd = targetSize > 0 && collected.size / targetSize >= TAIL_FRACTION; + const stuckLimit = nearingEnd ? STUCK_LIMIT_TAIL : STUCK_LIMIT_NORMAL; + if (stuck >= stuckLimit) break; + if (Date.now() - domStart > domDeadlineMs) { + process.stderr.write(`[dom] hard 10-min DOM scrape cap reached at pass=${pass} collected=${collected.size}/${targetSize}\n`); + break; + } + if (targetSize && collected.size >= targetSize) break; + if (nearingEnd) { + // Gentler single-step nudge with a longer settle wait so the last few + // virtualized rows have time to render before we check again. Every + // 4th stuck pass, throw in a real mouse-wheel nudge instead of a pure + // keyboard ArrowDown — some virtualized lists render more rows off a + // trusted scroll/wheel input rather than keyboard focus changes, so + // pure keyboard nav can plateau even when more content is available. + if (stuck > 0 && stuck % 4 === 0) { + const nudge = await mouseWheelNudge(800); + if (debug) process.stderr.write(`[dom] mouse wheel nudge (main loop): ${JSON.stringify(nudge)}\n`); + } else { + try { await page.keyboard.press("ArrowDown"); } catch {} + } + await page.waitForTimeout(275); + } else { + // Page down jumps faster; fall back to ArrowDown bursts. + try { + await page.keyboard.press("PageDown"); + } catch {} + for (let k = 0; k < 5; k++) { + try { await page.keyboard.press("ArrowDown"); } catch {} + } + await page.waitForTimeout(150); + } + await harvestOnce(); + if (collected.size === prevSize) stuck++; + else { stuck = 0; prevSize = collected.size; } + if (pass % 10 === 0) { + process.stderr.write(`[dom] pass=${pass} collected=${collected.size}/${targetSize} stuck=${stuck} tail=${nearingEnd} elapsed=${Math.round((Date.now() - domStart)/1000)}s\n`); + } + } + // One final End-key press in case there's still tail to load + try { + await page.keyboard.press("End"); + await page.waitForTimeout(300); + await harvestOnce(); + } catch {} + + process.stderr.write(`[dom] main pass complete: ${collected.size}/${targetSize} cues in ${Math.round((Date.now() - domStart)/1000)}s\n`); + + // Tail-recovery phase: if we're still short of the target after the main + // loop gave up, spend a bounded extra budget alternating techniques with + // generous settle waits: (1) keyboard End + ArrowDown nudges, and (2) a + // trusted mouse-wheel scroll positioned over the transcript pane. Some + // virtualized-list implementations render more rows off real, trusted + // scroll/wheel input rather than focus changes alone, so keyboard-only + // nudges can plateau even though more content is genuinely available. This + // is specifically for the case the main loop's stuck-counter trips right at + // the end of a virtualized list (observed: 307/329, 369/402, and 355/366 + // captured, all plateauing short of the tail with keyboard nudges alone). + if (targetSize && collected.size < targetSize && tailWaitMs > 0) { + process.stderr.write(`[dom] tail recovery: ${collected.size}/${targetSize} — spending up to ${Math.round(tailWaitMs/1000)}s on recovery nudges\n`); + const recoveryDeadline = Date.now() + tailWaitMs; + let recoveryStuck = 0; + let recoveryPrevSize = collected.size; + let iteration = 0; + while (Date.now() < recoveryDeadline && collected.size < targetSize && recoveryStuck < 15) { + // Alternate: even iterations try keyboard, odd iterations try a real + // trusted mouse-wheel scroll. Doing both every iteration would conflate + // which technique is actually responsible for progress in the debug log. + if (iteration % 2 === 0) { + try { await page.keyboard.press("End"); } catch {} + await page.waitForTimeout(500); + try { await page.keyboard.press("ArrowDown"); } catch {} + await page.waitForTimeout(500); + } else { + const nudge = await mouseWheelNudge(800); + if (debug) process.stderr.write(`[dom] mouse wheel nudge (tail recovery): ${JSON.stringify(nudge)}\n`); + await page.waitForTimeout(500); + } + await harvestOnce(); + if (collected.size === recoveryPrevSize) recoveryStuck++; + else { recoveryStuck = 0; recoveryPrevSize = collected.size; } + iteration++; + } + process.stderr.write(`[dom] tail recovery result: ${collected.size}/${targetSize}\n`); + } + + process.stderr.write(`[dom] final harvest: ${collected.size}/${targetSize} cues in ${Math.round((Date.now() - domStart)/1000)}s\n`); + if (targetSize && collected.size < targetSize) { + process.stderr.write( + `[warn] transcript may be INCOMPLETE: captured ${collected.size} of ${targetSize} cues. ` + + `The missing cues are almost always a contiguous block (check the start/end of the transcript), not scattered.\n` + ); + } + + if (collected.size === 0) return { vttText: null, cueCount: 0, targetSize }; + + // Forward-fill speakers using seenSpeakers timeline + const cues = Array.from(collected.values()).sort((a, b) => a.idx - b.idx); + // Sort speaker headers by idx and forward-fill + const headers = seenSpeakers.slice().sort((a, b) => a.idx - b.idx); + let hi = 0; + let currentSpeaker = ""; + for (const c of cues) { + while (hi < headers.length && headers[hi].idx <= c.idx) { + currentSpeaker = headers[hi].name; + hi++; + } + if (!c.speaker) c.speaker = currentSpeaker; + } + + // Fill end times from next cue start; last cue gets +3s + for (let i = 0; i < cues.length; i++) { + cues[i].end = i + 1 < cues.length + ? Math.max(cues[i + 1].start, cues[i].start + 0.5) + : cues[i].start + 3; + } + + return { vttText: cuesToVtt(cues), cueCount: cues.length, targetSize }; +} + +// --- profile management ---------------------------------------------------- +// Clone the canonical profile into a unique temp dir so we never collide with +// stale Windows file locks on the original. Cookies/auth state are copied via +// a best-effort sync back at the end. +function prepareProfileClone() { + const stamp = `${Date.now()}-${process.pid}`; + const cloneDir = join(tmpdir(), `pw-scrape-${stamp}`); + if (existsSync(CANONICAL_PROFILE_DIR)) { + process.stderr.write(`[profile] cloning ${CANONICAL_PROFILE_DIR} -> ${cloneDir}\n`); + try { + cpSync(CANONICAL_PROFILE_DIR, cloneDir, { + recursive: true, + force: true, + // Skip files that fail (e.g. locked lockfile) instead of aborting. + errorOnExist: false, + filter: (src) => !/[\\/](lockfile|SingletonLock|SingletonCookie|SingletonSocket)$/i.test(src), + }); + } catch (e) { + process.stderr.write(`[profile] clone partial (${e.message}); continuing\n`); + } + } else { + process.stderr.write(`[profile] canonical profile missing; starting fresh (will need sign-in)\n`); + mkdirSync(cloneDir, { recursive: true }); + } + return cloneDir; +} + +function syncCookiesBack(cloneDir) { + // Best-effort: copy back the Default/Cookies* + Default/Network/Cookies* files + // so future runs benefit from refreshed auth. Skip silently on any failure. + try { + const dirsToSync = ["Default"]; + for (const sub of dirsToSync) { + const srcSub = join(cloneDir, sub); + if (!existsSync(srcSub)) continue; + const dstSub = join(CANONICAL_PROFILE_DIR, sub); + try { mkdirSync(dstSub, { recursive: true }); } catch {} + const cookieNames = ["Cookies", "Cookies-journal", "Login Data", "Login Data-journal"]; + for (const name of cookieNames) { + const s = join(srcSub, name); + if (!existsSync(s)) continue; + try { cpSync(s, join(dstSub, name), { force: true }); } catch {} + } + const networkDir = join(srcSub, "Network"); + if (existsSync(networkDir)) { + const dstNetwork = join(dstSub, "Network"); + try { mkdirSync(dstNetwork, { recursive: true }); } catch {} + for (const name of cookieNames) { + const s = join(networkDir, name); + if (!existsSync(s)) continue; + try { cpSync(s, join(dstNetwork, name), { force: true }); } catch {} + } + } + } + } catch (e) { + process.stderr.write(`[profile] cookie sync-back skipped: ${e.message}\n`); + } +} + +// --- main ------------------------------------------------------------------ +async function main() { + const profileDir = prepareProfileClone(); + const ctx = await chromium.launchPersistentContext(profileDir, { + channel: "msedge", + headless: !args.headed, + viewport: { width: 1400, height: 900 }, + args: ["--disable-blink-features=AutomationControlled"], + }); + const page = ctx.pages()[0] || (await ctx.newPage()); + + const captures = []; // { url, text } + + page.on("response", async (resp) => { + try { + const url = resp.url(); + const ct = (resp.headers()["content-type"] || "").toLowerCase(); + // Skip obvious non-text resources up front for speed + if (/\.(png|jpe?g|gif|webp|woff2?|ttf|css|mp4|mp3|m4s|ts|svg|ico)(\?|$)/i.test(url)) return; + if (ct.includes("image/") || ct.includes("font/") || ct.includes("video/") || ct.includes("audio/")) return; + if (ct.includes("javascript") || ct.includes("text/css") || ct.includes("text/html")) return; + + const urlHints = /transcript|caption|texttrack|subtitle|\.vtt(\?|$)|stream\.aspx|mediastream|videostream|substrate.*media|recap/i.test(url); + const ctHints = ct.includes("text/vtt") || ct.includes("application/vtt") || ct.includes("application/json"); + + if (!urlHints && !ctHints) { + // Skip uninteresting responses without reading body (fast path) + return; + } + + const cl = parseInt(resp.headers()["content-length"] || "0", 10); + if (cl > 0 && cl > 10_000_000) return; + + let text = ""; + try { text = await resp.text(); } catch { return; } + if (!text) return; + + if (args.debug) { + const preview = text.slice(0, 120).replace(/\s+/g, " "); + process.stderr.write(`[debug] CANDIDATE ct=${ct} len=${text.length} url=${url}\n body[0:120]=${preview}\n`); + } + + if (looksLikeVtt(text)) { + captures.push({ url, text, kind: "vtt" }); + if (args.debug) process.stderr.write(`[debug] >>> CAPTURED native VTT (${text.length} bytes)\n`); + return; + } + + // JSON transcript shapes: try to detect cue arrays + if (ct.includes("application/json") || /^[\s{[]/.test(text)) { + let json; + try { json = JSON.parse(text); } catch { return; } + const cues = extractCuesFromJson(json); + if (cues && cues.length > 0) { + captures.push({ url, text: cuesToVtt(cues), kind: "json", jsonCueCount: cues.length }); + if (args.debug) process.stderr.write(`[debug] >>> CAPTURED JSON transcript (${cues.length} cues from ${url})\n`); + } + } + } catch (e) { + if (args.debug) process.stderr.write(`[debug] listener err: ${e.message}\n`); + } + }); + + try { + await page.goto(args.url, { waitUntil: "domcontentloaded", timeout: 60_000 }); + } catch (e) { + process.stderr.write(`ERR: page load failed: ${e.message}\n`); + await ctx.close(); + process.exit(4); + } + + if (args.manual && args.waitSec < 300) { + args.waitSec = 300; + } + + if (args.manual) { + const originHint = args.autoSwitchedManual + ? ` (auto-switched from live-join link: ${args.originalUrl})\n` + : ""; + process.stderr.write( + `\n[manual mode] Navigate to your meeting in the browser:\n` + + originHint + + ` 1. Open the meeting from Calendar (or Chat)\n` + + ` 2. Click 'Recap' tab → 'Transcript'\n` + + `Waiting up to ${args.waitSec}s for the transcript pane to load a .vtt file...\n\n` + ); + } + + // Manual mode no longer BLOCKS on stdin. The wait loop below auto-detects the + // transcript pane (the recap region + cue entries) as soon as you open it and + // proceeds on its own. Pressing Enter is an OPTIONAL manual override that + // forces extraction to start immediately, e.g. if auto-detect is slow. + let manualSignal = false; + if (args.manual) { + process.stderr.write( + `\n[manual mode] Navigate to the meeting Recap → Transcript pane, then click a\n` + + `transcript line so the list has focus. Extraction starts AUTOMATICALLY once\n` + + `the transcript pane is detected — you don't need to do anything else here.\n` + + `(Optional: press Enter in this terminal to force it to start immediately.)\n` + ); + process.stdin.resume(); + process.stdin.once("data", () => { manualSignal = true; }); + } + + // Try to auto-click a Transcript button across known UI variants and across + // all frames (Teams recap has the tab inside a SharePoint xplatplugins iframe). + const transcriptSelectors = [ + '[data-tid="Transcript"]', + 'button[aria-label*="Transcript" i]', + 'button[title*="Transcript" i]', + '[role="tab"][aria-label*="Transcript" i]', + '[role="tab"]:has-text("Transcript")', + 'button:has-text("Transcript")', + 'div[role="button"]:has-text("Transcript")', + ]; + // Only auto-click a Transcript tab in direct-URL mode. In manual mode the user + // navigates to the recap themselves, so a one-shot click on whatever page is + // currently showing would be useless at best and misdirected at worst. + if (!args.manual) for (const frame of page.frames()) { + for (const sel of transcriptSelectors) { + try { + const loc = frame.locator(sel).first(); + if (await loc.isVisible({ timeout: 500 }).catch(() => false)) { + await loc.click({ timeout: 2000 }).catch(() => {}); + if (args.debug) process.stderr.write(`[debug] clicked transcript via ${sel} in ${frame.url()}\n`); + break; + } + } catch { /* try next */ } + } + } + + // Wait for a network VTT capture OR the recap transcript pane to appear in any + // frame. Use the full wait window in all modes: in manual mode the loop needs + // time for you to navigate to the recap, but it breaks early the instant the + // transcript pane is detected (or you press Enter), so it's not a fixed delay. + const waitCap = args.waitSec; + const deadline = Date.now() + waitCap * 1000; + const startWait = Date.now(); + let lastProgress = Date.now(); + while (Date.now() < deadline) { + if (captures.length > 0 && !args.forceDom) { + process.stderr.write(`[info] network VTT captured after ${Math.round((Date.now() - startWait)/1000)}s\n`); + break; + } + if (args.manual && manualSignal) { + process.stderr.write(`[info] manual override received — starting extraction\n`); + await page.waitForTimeout(500); + break; + } + // Look for the transcript region in ANY frame (not just xplatplugins.aspx — + // Teams UI changes the iframe URL periodically). + let recapReady = false; + for (const f of page.frames()) { + try { + recapReady = await f + .evaluate(() => !!document.querySelector('[aria-label="Transcript"][role="complementary"] [id^="entry-"]')) + .catch(() => false); + if (recapReady) break; + } catch {} + } + if (recapReady) { + process.stderr.write(`[info] transcript pane DOM detected after ${Math.round((Date.now() - startWait)/1000)}s\n`); + await page.waitForTimeout(1500); + break; + } + if (Date.now() - lastProgress > 5000) { + process.stderr.write(`[info] still waiting for transcript pane (${Math.round((Date.now() - startWait)/1000)}s elapsed, cap=${waitCap}s, captures=${captures.length})\n`); + lastProgress = Date.now(); + } + await page.waitForTimeout(500); + } + if (Date.now() >= deadline) { + process.stderr.write(`[info] wait window elapsed (${waitCap}s); proceeding with whatever's available (captures=${captures.length})\n`); + } + + let vttText = null; + let source = null; + let domCueCount = null; + let domTargetSize = null; + + if (captures.length > 0 && !args.forceDom) { + vttText = pickPreferredVtt(captures).text; + source = "network"; + process.stderr.write(`[info] using network capture (${vttText.length} bytes)\n`); + } else { + if (args.forceDom && captures.length > 0) { + process.stderr.write(`[info] --force-dom set: ignoring ${captures.length} network capture(s), using DOM fallback\n`); + } else { + process.stderr.write("[info] no network VTT captured, trying DOM fallback\n"); + } + const domResult = await domScrapeFallback(page, args.debug, args.tailWaitSec * 1000); + vttText = domResult.vttText; + domCueCount = domResult.cueCount; + domTargetSize = domResult.targetSize; + if (vttText) { + source = "dom"; + process.stderr.write(`[info] DOM scrape produced ${vttText.length} bytes\n`); + } + } + + // CRITICAL: write files BEFORE attempting browser cleanup. ctx.close() can + // hang on Windows when there are many in-flight listeners; we don't want to + // lose a captured transcript to a cleanup hang. + if (vttText) { + process.stderr.write(`[info] writing transcript files...\n`); + writeFileSync(vttPath, vttText, "utf8"); + const cues = parseVtt(vttText); + const md = cuesToMarkdown(cues, args.url, outDir); + writeFileSync(mdPath, md, "utf8"); + // Emit success JSON to stdout immediately so the caller knows we're done + // with the meaningful work, even if cleanup below takes a while. + const truncated = domTargetSize != null && domCueCount != null && domCueCount < domTargetSize; + process.stdout.write(JSON.stringify({ + ok: true, + vtt: vttPath, + md: mdPath, + cueCount: cues.length, + targetCueCount: domTargetSize ?? undefined, + truncated, + bytes: Buffer.byteLength(vttText, "utf8"), + source, + outDir, + }) + "\n"); + process.stderr.write(`[info] wrote ${cues.length} cues to ${vttPath}\n`); + if (truncated) { + process.stderr.write( + `[warn] transcript is likely INCOMPLETE (${domCueCount}/${domTargetSize} cues). ` + + `Re-run with a larger --tail-wait, or check the end of the transcript for the gap.\n` + ); + } + } + + // Best-effort browser cleanup with hard timeout so we never hang on close. + process.stderr.write(`[info] closing browser...\n`); + await Promise.race([ + ctx.close().catch((e) => process.stderr.write(`[info] ctx.close warning: ${e.message}\n`)), + new Promise((resolve) => setTimeout(() => { + process.stderr.write(`[info] ctx.close timed out after 5s — moving on\n`); + resolve(); + }, 5000)), + ]); + + // Sync cookies back so future runs benefit from refreshed auth. + process.stderr.write(`[info] syncing cookies back to canonical profile...\n`); + syncCookiesBack(profileDir); + // Best-effort cleanup of the temp profile clone. + try { rmSync(profileDir, { recursive: true, force: true }); } catch {} + + if (!vttText) { + process.stderr.write( + `ERR: no transcript captured within ${args.waitSec}s. Re-run with --headed and open the transcript pane manually; or pass --debug to see network activity.\n` + ); + process.exit(3); + } + + // Force exit — sometimes lingering listeners or process refs keep node alive + // even after main() returns; we've already written the output and JSON. + process.stderr.write(`[info] done.\n`); + process.exit(0); +} + +main().catch((e) => { + process.stderr.write(`ERR: ${e.stack || e.message}\n`); + process.exit(1); +}); diff --git a/install-system-sync-task.ps1 b/install-system-sync-task.ps1 new file mode 100644 index 0000000..12450c0 --- /dev/null +++ b/install-system-sync-task.ps1 @@ -0,0 +1,30 @@ +[CmdletBinding()] +param() + +$ErrorActionPreference = 'Stop' +$repositoryRoot = $PSScriptRoot +$syncScript = Join-Path $repositoryRoot 'sync-skills.ps1' + +if (-not (Test-Path -LiteralPath $syncScript)) { + throw "Sync script not found at '$syncScript'." +} + +$action = New-ScheduledTaskAction ` + -Execute 'powershell.exe' ` + -Argument "-NoProfile -NonInteractive -ExecutionPolicy Bypass -File `"$syncScript`"" +$trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Sunday -At 10:00AM +$principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest +$settings = New-ScheduledTaskSettingsSet ` + -StartWhenAvailable ` + -RunOnlyIfNetworkAvailable ` + -MultipleInstances IgnoreNew ` + -ExecutionTimeLimit (New-TimeSpan -Hours 1) + +Register-ScheduledTask ` + -TaskName 'PromptWorks Weekly Skill Sync' ` + -Action $action ` + -Trigger $trigger ` + -Principal $principal ` + -Settings $settings ` + -Description 'Mirrors custom Microsoft Scout skills to PromptWorks.' ` + -Force | Out-Null diff --git a/kb/SKILL.md b/kb/SKILL.md new file mode 100644 index 0000000..71e2e39 --- /dev/null +++ b/kb/SKILL.md @@ -0,0 +1,39 @@ +--- +name: "kb" +description: "Vault-first recall. Search Daniel's Obsidian KB (D:\\Repos\\Obsidian) BEFORE WorkIQ/M365, Azure DevOps, IcM, or the browser. Use for any lookup: \"who is X\", \"what's my/his alias, SC-ALT, email, ID, manager, office\", \"what did we decide about X\", \"when did we meet about X\", \"what is <acronym>\", \"remind me\", \"do we know\", \"look it up\", \"check the KB\", plus anything about squads, Connects, RISE, meetings, daily notes, or people Daniel works with." +--- + +# kb — vault-first recall + +The Obsidian vault at `D:\Repos\Obsidian` is the system of record for durable facts. External systems (WorkIQ/M365, ADO, IcM, browser) are the **fallback**, not the first stop. 247 notes / ~2.3 MB — searching it is effectively free, so there is never a latency excuse to skip it. + +Read `D:\Repos\Obsidian\AGENTS.md` §5.2 (`query`) for the full protocol. Its layer rules bind you: never edit anything in the Raw layer (`Transcripts/`, `00 - Chats/`, `02 - Meetings/*.vtt`, `07 - AI Sessions/`). + +## Search ladder — stop as soon as you have the answer + +1. **Quick Reference** — `05 - Resources\Quick Reference.md`. The cheat sheet for accounts, aliases, SC-ALT, Graph IDs, org/management chain, team rosters, tooling handles. Most "simple thing" questions end here. Read it before anything else. +2. **MOC hubs** — `meta\moc\_index.md`, then `people.md`, `connects.md`, and the squad MOCs (`c3-omr-reliability-tooling.md`, `c4-sre-core.md`). +3. **Entity pages** — `03 - Squads\<Squad>\02 - People.md`, `04 - Decisions.md`, `05 - Open Questions.md`, `06 - Glossary.md`. +4. **Full-text search** — use the `grep` tool (ripgrep), case-insensitive, scoped to `D:\Repos\Obsidian`. Search the person/term AND its likely aliases (first name, alias, email local-part, acronym). Batch several `grep` calls in one response rather than probing one at a time. +5. **Filename / date search** — use `glob` for dated material: `01 - Daily\YYYY-MM-DD.md`, `02 - Meetings\YYYY-MM-DD - *\summary.md`, `00 - Chats\YYYY-MM\*.md`. +6. **Past AI sessions** — `07 - AI Sessions/`, or the richer `sessql` MCP (`find_sessions_hybrid`) when the answer was likely worked out in an earlier session rather than written to a note. + +## Answering + +- **Cite the notes.** Name the specific file(s) the answer came from, as `[[wikilink]]` or a path. An uncited recall answer is not acceptable. +- **Never fabricate.** If the vault is silent, say so explicitly. +- **Prefer specific over stale.** If two notes conflict, trust the one with the newer `updated:` frontmatter and say the two disagree. + +## On a miss + +State plainly what you searched and that the vault came up empty, *then* escalate to the external system (WorkIQ/M365, ADO, IcM, browser). Do not silently fall through — the user needs to know the KB has a gap. + +## Closing the loop (this is what makes it compound) + +When you resolve a **durable, reusable** fact from an external system that the vault should have known: + +1. Offer to write it back. Do not write silently — ask first (AGENTS.md §5.2 step 4). +2. Route it: identity/account/org facts → append to the right table in `05 - Resources\Quick Reference.md`; a person → `meta\moc\people.md` or a person page; a squad decision → that squad's `04 - Decisions.md`; anything else durable → a new `type: resource` page in `05 - Resources\`. +3. Bump `updated:` in frontmatter, add a `sources:` entry, and append a one-line entry to `meta\log.md` per AGENTS.md §7. + +Facts that are personal preferences or standing behavioral rules belong in Scout memory (`m_remember`) instead of the vault. diff --git a/kblinter/SKILL.md b/kblinter/SKILL.md new file mode 100644 index 0000000..8ac5cec --- /dev/null +++ b/kblinter/SKILL.md @@ -0,0 +1,120 @@ +--- +name: "kblinter" +description: "Run the knowledge-base linter against the Obsidian vault at D:\\Repos\\Obsidian and surface the findings. Use whenever the user asks to lint, audit, or check the health of the vault/Second Brain. Triggers: /kblinter, \"lint the vault\", \"audit my notes\", \"check vault health\", \"run the kb linter\", \"find orphan notes\", \"find broken links in the vault\"." +--- + +# kblinter — Vault Knowledge Base Linter + +Wrapper around `D:\Repos\Obsidian\meta\scripts\lint.py`. The script implements the checks defined in `AGENTS.md` §5.3. + +## Vault layers (drives which checks apply) + +| Layer | Lives in | Lint behavior | +|---|---|---| +| **Raw** | `Transcripts/`, `00 - Chats/`, `Attachments/`, `*.vtt`, `*.msg` | Skipped — never linted | +| **Wiki** | `01 - Daily/`, `02 - Meetings/`, `03 - Squads/`, `04 - Connects/` (except Drafts), `05 - Resources/` | Full schema enforcement | +| **Workshop** | `04 - Connects/Drafts/` (plus any path in `WORKSHOP_DIRS` in `lint.py`) | Exempt from frontmatter / orphan / broken-link / duplicate / meeting-provenance. Still checked for stale + sensitivity. | +| **Schema** | `AGENTS.md`, `meta/`, `99 - Templates/`, any `Templates/` subfolder | Skipped from most content checks | + +## What it checks + +| Check | Rule | Layers affected | +|---|---|---| +| YAML parse errors | Frontmatter block exists but YAML invalid | wiki | +| Missing frontmatter | Wiki-layer page missing required keys for its `type` | wiki | +| Orphan wiki pages | No inbound `[[wiki link]]` from any other note (excludes `type: daily` and `type: moc`) | wiki | +| Stale pages | `status: active` updated >30d ago; `status: draft` updated >90d ago | wiki + workshop | +| Broken wiki links | `[[...]]` that doesn't resolve | wiki | +| Sensitivity mismatch | Body contains "Confidential" / customer markers without `sensitivity:` frontmatter | wiki + workshop | +| Duplicate canonical keys | Multiple notes with same `(parent_dir, date, type, stem)` | wiki | +| **Meeting provenance** | Wiki page under `03 - Squads/*/Meetings/` lacking a `sources:` or inline `[[link]]` to `02 - Meetings/` or `Transcripts/` | wiki | +| Transcripts inbox backlog | Files in `Transcripts/` >7 days old | n/a | +| MOC gaps | Squad subfolder with >10 wiki pages but no MOC hub note | n/a | + +**The linter NEVER auto-fixes.** It writes a Markdown report and exits. + +## Canonical vs squad-side meetings + +The vault deliberately keeps meetings in two places — both are valid `type: meeting`: + +- **`02 - Meetings/YYYY-MM-DD - <Title>/`** = **canonical record** (raw VTT + auto summary). Chronological, one folder per meeting. +- **`03 - Squads/<name>/Meetings/`** = **squad-lens commentary**. Organized by squad. MUST `sources:` link back to the canonical record (enforced by the `meeting_provenance` check). + +When fixing a `meeting_provenance` gap: add `sources: ["[[02 - Meetings/YYYY-MM-DD - .../summary]]"]` (or the relevant VTT path) to the squad-side note's frontmatter. The check also accepts inline `[[02 - Meetings/...]]` wikilinks in the body. + +## How to run + +### Default (write report) + +```powershell +python D:\Repos\Obsidian\meta\scripts\lint.py +``` + +Writes `D:\Repos\Obsidian\meta\lint-report-YYYY-MM-DD.md`. Prints a one-line summary. + +### JSON + +```powershell +python D:\Repos\Obsidian\meta\scripts\lint.py --json +``` + +### Custom output / alternate vault + +```powershell +python D:\Repos\Obsidian\meta\scripts\lint.py --out X --vault Y +``` + +## Default workflow when invoked + +1. Run the linter. Overwriting today's report is fine. +2. Read the report. +3. Summarize to the user: + - Total + per-check counts + - Top 5–10 most-actionable items (priority: broken links > sensitivity mismatch > meeting provenance > duplicates > frontmatter > orphans) + - Inbox backlog items — concrete unblocked actions + - MOC gaps — suggest a name +4. Offer to fix specific issues. NEVER fix without explicit approval. Common fixes: + - Add missing frontmatter (consider `meta/scripts/frontmatter_backfill.py --dry-run` first) + - Repair a broken link + - Add sensitivity label + - Add `sources:` to a squad-side meeting note (for `meeting_provenance` gaps) + - Create a missing MOC hub note +5. Append a log line to `D:\Repos\Obsidian\meta\log.md`: + `<ISO timestamp> | kblinter | lint | meta/lint-report-YYYY-MM-DD.md | <total> issues | <per-check breakdown>` + +## Adding a new workshop path + +Edit `WORKSHOP_DIRS` at the top of `meta/scripts/lint.py`: + +```python +WORKSHOP_DIRS = { + "04 - Connects/Drafts", + "some/new/scratch/area", # add here +} +``` + +Update AGENTS.md §1 and §2 to document the new workshop path. + +## Don'ts + +- Don't auto-edit any wiki page based on lint findings without explicit approval. +- Don't relax a rule because it found something — update AGENTS.md deliberately instead. +- Don't summarize the entire report verbatim — pick the most actionable items. +- Don't run on `D:\Repos\TradeTrail` or any other directory. Hard-coded for the Obsidian vault. + +## When the script fails + +Surface stderr verbatim and suggest: +- `python -c "import yaml"` (PyYAML required) +- Check `D:\Repos\Obsidian\meta\scripts\lint.py` exists +- `--json` for raw output + +## Extending the linter + +New checks live in `lint.py` registered in the `CHECKS` tuple. Each check takes `list[Note]` and returns `list[dict]` with `path` and `msg` keys. + +When adding a check: +1. Decide which layers it applies to (filter by `n.layer` early) +2. Document in `AGENTS.md` §5.3 +3. Update the "What it checks" table here +4. Add a smoke test diff --git a/mslearn/SKILL.md b/mslearn/SKILL.md new file mode 100644 index 0000000..7dbdccd --- /dev/null +++ b/mslearn/SKILL.md @@ -0,0 +1,69 @@ +--- +name: "mslearn" +description: "Query the live Microsoft Learn MCP for grounded, first-party docs, code samples, and full-page fetches across Azure, M365, .NET, and Power Platform. Use when the user asks what Microsoft says about X, needs an authoritative answer for a customer, wants current official guidance, looks for a code sample, or asks to cite or quote the docs. Triggers: ms learn, microsoft learn, learn docs, look it up on learn, cite the docs, official microsoft guidance, what do the docs say." +--- + +# MS Learn MCP skill + +Calls the public Microsoft Learn MCP server at `https://learn.microsoft.com/api/mcp` over JSON-RPC 2.0. No auth required. Use this to ground customer-facing answers in live, first-party Microsoft documentation. + +## Three tools available + +| Tool | Purpose | Args | +| --- | --- | --- | +| `microsoft_docs_search` | Top-10 doc chunks (≤500 tokens each). Always start here. | `query` (string) | +| `microsoft_code_sample_search` | Official code snippets. Use when generating any MS/Azure code. | `query` (string), `language` (optional: csharp, javascript, typescript, python, powershell, azurecli, al, sql, java, kusto, cpp, go, rust, ruby, php) | +| `microsoft_docs_fetch` | Full page → markdown. Use AFTER search when a result looks high-value or is truncated. | `url` (must be microsoft.com HTML page) | + +## How to call (PowerShell) + +```powershell +$body = @{ + jsonrpc = "2.0" + id = 1 + method = "tools/call" + params = @{ + name = "microsoft_docs_search" + arguments = @{ query = "YOUR QUERY HERE" } + } +} | ConvertTo-Json -Compress -Depth 6 + +curl.exe -s -X POST "https://learn.microsoft.com/api/mcp" ` + -H "Content-Type: application/json" ` + -H "Accept: application/json, text/event-stream" ` + -d $body +``` + +Response is SSE-framed: skip the `event: message\ndata: ` prefix, then JSON-parse. The `result.content[0].text` field is itself JSON — parse it again to get `results[]`. + +Quick parser: +```powershell +$raw = curl.exe ... # as above +$json = ($raw -join "`n") -replace '^event:.*\ndata: ','' +$outer = $json | ConvertFrom-Json +$inner = $outer.result.content[0].text | ConvertFrom-Json +$inner.results | ForEach-Object { "[$($_.title)]($($_.contentUrl))`n$($_.content)`n" } +``` + +To call `microsoft_code_sample_search` or `microsoft_docs_fetch`, swap the `params.name` and `params.arguments` accordingly (e.g. `arguments = @{ query = "..."; language = "csharp" }` or `arguments = @{ url = "https://learn.microsoft.com/..." }`). + +## Workflow for a customer answer + +1. **Search** with `microsoft_docs_search` using the user's question verbatim or refined keywords. +2. Skim the top results. If they fully answer the question → synthesize and cite. +3. If a result is truncated, ambiguous, or is the canonical landing page → **fetch** it with `microsoft_docs_fetch` for full content. +4. If the user wants code → also call `microsoft_code_sample_search` with the right `language`. +5. Compose the response with **inline citations**: every claim that came from Learn must link to the source `contentUrl` so the customer can verify. + +## Output style for customer-facing answers + +- Lead with a one-paragraph plain-English answer. +- Follow with bullet points of key facts, each citing `[Source title](url)`. +- If quoting code, fence it with the right language and cite the sample's `link`. +- End with **"Sources"** section listing every URL used. +- Never paraphrase past what the docs say. If Learn doesn't cover it, say so explicitly — don't fill gaps with model knowledge. + +## When NOT to use +- Internal Microsoft content (Seismic, MSX, Learn Pathways gated content) — use the other skills. +- Non-Microsoft topics — Learn won't have it. +- Anything time-sensitive about pricing/SLAs — confirm via the actual product pricing page after Learn points you there. diff --git a/oneonone-pokedex/SKILL.md b/oneonone-pokedex/SKILL.md new file mode 100644 index 0000000..e9eb2f9 --- /dev/null +++ b/oneonone-pokedex/SKILL.md @@ -0,0 +1,114 @@ +--- +name: "oneonone-pokedex" +description: "IC-side companion for Daniel's recurring 1:1 with his manager - the mirror of the manager coaching-pokedex. Preps an agenda from the Obsidian vault, reflects on a recorded 1:1 with an honest self-scorecard of how Daniel showed up (never grading the manager), assigns a Pokemon for session energy, tracks trends, and writes notes back to the vault as Connect evidence. Private. Triggers: /1on1, /oneonone, 1:1, one on one, prep for my 1:1, reflect on my 1:1, manager sync." +--- + +# 1:1 Pokédex — "The Trainer" (IC-side companion to `connect`) + +The individual-contributor mirror of the manager-side coaching-pokedex. This is **your** private companion for your recurring 1:1 with **your manager**. It helps you (1) walk in prepared, (2) reflect honestly on how *you* showed up, and (3) never lose a commitment — yours or your manager's. + +It is deliberately **complementary to the `connect` skill**: same Obsidian vault, same active Connect doc, same evidence-scanning conventions. Where `connect` looks at the half-year arc, this skill works at the cadence of a single 1:1. Reflect notes are written back into the vault as Obsidian meeting notes (with `#cp/<tag>` hooks) so `/connect` can attribute them as evidence. + +> **This skill reads recorded 1:1 transcripts and your private career notes — sensitive data.** Every artifact is private to **you, the IC**. Never send anything to your manager, peers, skip-level, or HR. The scorecard grades **only your own behavior**, never your manager's. Read "Privacy" before running. + +--- + +## First run — onboarding (ask once, then remember) + +Do not assume or invent these. Ask, then store via memory: + +1. **Your manager** — name and work email (for WorkIQ resolution and to identify the 1:1). +2. **Cadence** — how often the 1:1 recurs (e.g. weekly Tuesdays), so "next 1:1" and "since last 1:1" are well-defined. +3. **Timezone** — for dates/times (default America/Los_Angeles, matching `connect`). +4. **Recordings source** — where your recorded 1:1s live (e.g. OneDrive "Recordings"), for reflect mode. +5. **Vault output folder** — where 1:1 notes are written. Default `D:\Repos\Obsidian\02 - Meetings\` as `1on1 - <Manager> - YYYY-MM-DD.md`. Read-only everywhere else in the vault, exactly like `connect`. +6. **Privacy confirm** — artifacts are IC-only; the skill may send a private Teams **self-DM** when a new reflection is generated. + +If any answer is missing on a later run, ask again rather than guessing. + +--- + +## Privacy (non-negotiable) + +- 1:1 transcripts and career notes are **private to you**. Never share, send, or surface a summary to your manager or anyone else. +- The reflection **describes the meeting** and **grades only your own behavior**. No verdicts about your manager's competence or personality — observations about the manager stay neutral and descriptive. +- A meeting qualifies for reflect mode **only** if it is a genuine 1:1 between you and your manager, no other human participants. Verify before generating; reject group meetings. +- Before sending anything (e.g. the self-DM), preview exactly what will be sent and to whom, and wait for confirmation. In an automated sweep, only ever send to yourself. +- Never invent transcript content, quotes, ratings, commitments, or trends. If a transcript is missing/unreadable, stop and say so. +- Respect MIP sensitivity: vault files are fine; never write classified 1:1 content to unprotected external destinations. + +--- + +## Shared foundation with `connect` (reuse, don't reinvent) + +- **Active Connect doc:** find the `*.md` in `D:\Repos\Obsidian\04 - Connects\` whose frontmatter has `status: active` (same line-regex approach `connect` uses). Parse its `## Core Priority N` blocks and `**Tags:** #cp/<tag>` hooks. +- **Evidence sources (READ ONLY):** `01 - Daily\*.md`, `02 - Meetings\**\*.md`, `03 - Squads\**\*.md` (Status Updates, Decisions), dated `00 - Inbox\*.md` — identical to `connect`. +- **Citations:** always cite source notes as Obsidian wikilinks, e.g. `[[2026-06-18]]`, `[[1on1 - Alex - 2026-06-15]]`. +- **Handoff:** for the deep Connect rollup, call/borrow `/connect prep <manager>` and `/connect status` rather than duplicating their logic — fold their output into the agenda's "Connect" section. + +--- + +## Subcommands + +If invoked as bare `/1on1`, ask which subcommand (show the table). For natural-language intent ("help me get ready for my 1:1"), pick the closest. + +### `/1on1 prep` — build the agenda (default) +Assemble a tight, private agenda for your **next** 1:1, pulling since the **last** 1:1 (per cadence). Sections: +- **Open commitments** — action items from your last 1:1 note and recent dailies that are still open (yours *and* your manager's). Flag anything stale. +- **Wins to share** — concrete impact since last time, attributed to Connect priorities (self-advocacy fuel). +- **Connect** — from `/connect status`: which Core Priorities are **cold** (no evidence in 14d) and need airtime; thin-evidence priorities to flag. +- **Blockers / asks** — where you need a decision, air cover, or unblocking. +- **Career & growth** — one durable growth topic to keep on the radar (don't let it get crowded out by status). +- **Questions for my manager** — 2–4 sharp questions (feedback, priorities, org context). + +Keep it to one screen. Do NOT write a file unless asked; if asked, save to the output folder as `1on1 prep - <Manager> - YYYY-MM-DD.md`. + +### `/1on1 reflect [date|recording]` — reflect on a held 1:1 +1. **Discover & verify** the recording/transcript (reject if a third human was present). +2. **Recap** — neutral, descriptive summary of what was discussed. +3. **Commitments ledger** — explicit table: **You committed to…** / **Your manager committed to…** / **Decisions made**. These become next prep's "open commitments." +4. **Self-Scorecard** — rate **YOURSELF** across these 9 IC dimensions, 1–5 stars (half-stars allowed) or N/A. Anchor honestly; don't cluster at 4. + 1. Came prepared (had an agenda) + 2. Directness & candor (raised the hard thing) + 3. Self-advocacy (surfaced wins & impact) + 4. Career ownership (drove growth, not just status) + 5. Asking for coaching (sought feedback vs only reporting) + 6. Receptiveness (acted on feedback, stayed non-defensive) + 7. Follow-up ownership (closed prior action items) + 8. Managing up (gave context, flagged risks/blockers early) + 9. Closing the loop (clear commitments + next steps) + + If a prior reflection exists, render a **Prior / This / Trend** scorecard plus a short **Trend paragraph** (is your 1:1 game improving?). +5. **Pokémon of the session** — pick one Pokémon capturing how **you** showed up *this* session (a snapshot, not a permanent label). Avoid the famous starters. ~80–120 words citing 2–3 specific transcript moments. Slot the Pokédex number into the official-artwork sprite URL so the image renders. +6. **Save** a vault note (markdown, Obsidian-native, wikilinked) to the output folder as `1on1 - <Manager> - YYYY-MM-DD.md`, with frontmatter `type: meeting` and the relevant `#cp/<tag>` hooks so `/connect` rolls it up. Optionally also emit the styled, self-contained HTML Pokédex card (~15–22 KB) if you want the visual artifact. +7. **Dedup state** — maintain `processed.json` in the output folder so recordings are never re-summarized. +8. **Notify** privately (Teams self-DM in sweep mode only, after preview/confirm in interactive mode). + +**Voice discipline:** describe the meeting; grade only yourself; keep manager observations neutral and descriptive. + +### `/1on1 status` — quick pulse +No file written. Show: days until next 1:1, open commitments (yours + manager's) with staleness, cold Connect priorities needing airtime, and the 3 hottest things to raise. + +### `/1on1 history` — trend of your self-scorecards +Read prior reflection notes; show how your 9 dimensions trend session-over-session (up / flat / down) and call out your weakest recurring dimension as a focus. + +--- + +## Run modes + +- **On-demand:** `/1on1 prep` before the meeting; `/1on1 reflect` after. +- **Automated sweep:** find every **new** qualifying 1:1 with your manager since the last run, generate a reflection for each, update `processed.json`, and self-DM privately. If nothing new qualifies, exit silently. + +--- + +## Implementation notes + +- Reuse `connect`'s scanning style: PowerShell `Get-ChildItem` + `Select-String` (or the `grep` tool), line-based frontmatter regex, no YAML libs. Batch large scans by month. +- All "today"/"next 1:1" math uses the onboarded timezone (default America/Los_Angeles); `Get-Date` formatted `yyyy-MM-dd`. +- Be honest about gaps — if there's no evidence for a win or a priority, say so; never fabricate. +- Resolve the manager via WorkIQ `workiq_search_people` when you need their email; never guess. +- Writing reflect notes as `type: meeting` vault notes is intentional — it makes them first-class Connect evidence and keeps a single source of truth. + +--- + +*IC-side adaptation of the manager coaching-pokedex ("The Sensei"), originally contributed by Mike Gaal (mikegaal@microsoft.com) on behalf of Justin Raphael. Flipped to the individual-contributor lens, the scorecard re-pointed at self-reflection, and wired into the `connect` skill's Obsidian vault so the two skills share one source of truth.* diff --git a/receiving-code-review/SKILL.md b/receiving-code-review/SKILL.md new file mode 100644 index 0000000..801743e --- /dev/null +++ b/receiving-code-review/SKILL.md @@ -0,0 +1,42 @@ +--- +name: "receiving-code-review" +description: "Handle incoming code review feedback with technical rigor instead of reflexive agreement - verify each claim, push back with evidence when a suggestion is wrong or does not apply, ask when feedback is unclear, and apply a YAGNI check before adding suggested professional features. Covers GitHub thread replies and implementation order. Triggers: here is the review feedback, address these comments, the reviewer said, respond to this PR comment, they want me to change." +--- + +Code review requires technical evaluation, not emotional performance. + +**Core principle:** verify before implementing. Ask before assuming. Technical correctness over social comfort. + +## The Response Pattern +When receiving feedback: 1) Read the complete feedback without reacting. 2) Understand — restate the requirement in your own words, or ask. 3) Verify against codebase reality. 4) Evaluate — is it technically sound for THIS codebase? 5) Respond with technical acknowledgment or reasoned pushback. 6) Implement one item at a time, testing each. + +## Forbidden Responses +Never: "You're absolutely right!", "Great point!", "Excellent feedback!", or "Let me implement that now" before verification. Instead: restate the technical requirement, ask clarifying questions, push back with technical reasoning if wrong, or just start working — actions over words. + +## Handling Unclear Feedback +If any item in multi-item feedback is unclear, STOP — do not implement anything yet, ask for clarification on the unclear items first. Items may be related; partial understanding risks a wrong implementation. Example: told to "fix 1-6", understand 1,2,3,6 but not 4,5 → say "I understand items 1,2,3,6. Need clarification on 4 and 5 before proceeding" rather than implementing the clear ones and asking about the rest later. + +## Source-Specific Handling +From a trusted collaborator/user: implement after understanding, still ask if scope unclear, no performative agreement, skip to action. +From external reviewers: before implementing, check — technically correct for this codebase? Breaks existing functionality? Reason for current implementation exists? Works on all platforms/versions? Does the reviewer understand full context? If suggestion seems wrong, push back with technical reasoning. If you can't easily verify, say so explicitly and ask how to proceed. If it conflicts with prior architectural decisions, stop and discuss first. + +## YAGNI Check for "Professional" Features +If a reviewer suggests "implementing properly" (e.g. full metrics tracking, exhaustive options), grep the codebase for actual usage first. If unused, ask whether to remove it (YAGNI) instead of building it out. If used, implement properly. + +## Implementation Order +For multi-item feedback: clarify anything unclear first, then implement in order — blocking issues (breaks/security) → simple fixes (typos/imports) → complex fixes (refactoring/logic). Test each fix individually, verify no regressions. + +## When To Push Back +Push back when: the suggestion breaks existing functionality, the reviewer lacks full context, it violates YAGNI, it's technically incorrect for this stack, legacy/compatibility reasons exist, or it conflicts with prior architectural decisions. Use technical reasoning not defensiveness; ask specific questions; reference working tests/code; involve the user if it's architectural. + +## Acknowledging Correct Feedback +Do: "Fixed. [brief description of what changed]" / "Good catch — [specific issue]. Fixed in [location]." / just fix it and show the code. Don't: any gratitude expression ("Thanks for catching that", "Thanks for..."). Actions speak — just fix it, the code shows you heard the feedback. If you catch yourself about to write "Thanks", delete it and state the fix instead. + +## Gracefully Correcting Your Own Pushback +If you pushed back and were wrong: "You were right — I checked [X] and it does [Y]. Implementing now." State the correction factually and move on. No long apology, no defending the original pushback, no over-explaining. + +## GitHub Thread Replies +When replying to inline PR review comments, reply in the comment thread, not as a top-level PR comment. + +## The Bottom Line +External feedback = suggestions to evaluate, not orders to follow. Verify. Question. Then implement. No performative agreement — technical rigor always. diff --git a/requesting-code-review/SKILL.md b/requesting-code-review/SKILL.md new file mode 100644 index 0000000..3cade8c --- /dev/null +++ b/requesting-code-review/SKILL.md @@ -0,0 +1,73 @@ +--- +name: "requesting-code-review" +description: "Request an independent review of completed work before merging or declaring it done - builds the reviewer prompt, states what was implemented, the requirements, and the exact git range to review, and demands a read-only critical review with calibrated severity. Triggers: review my changes, can you check this over, is this ready to merge, get a second opinion, code review this, check my work before I ship it." +--- + +Dispatch a code reviewer subagent to catch issues before they cascade. The reviewer gets precisely crafted context for evaluation — never your session's history. This keeps the reviewer focused on the work product, not your thought process, and preserves your own context for continued work. + +**Core principle:** review early, review often. + +## When to Request Review +Mandatory: after each task in subagent-driven development, after completing a major feature, before merge to main. +Optional but valuable: when stuck (fresh perspective), before refactoring (baseline check), after fixing a complex bug. + +## How to Request +1. Get git SHAs: `BASE_SHA=$(git rev-parse HEAD~1)` (or origin/main) and `HEAD_SHA=$(git rev-parse HEAD)`. +2. Dispatch a general-purpose subagent using the review template below, filling in DESCRIPTION, PLAN_OR_REQUIREMENTS, BASE_SHA, HEAD_SHA. +3. Act on feedback: fix Critical issues immediately, fix Important issues before proceeding, note Minor issues for later, push back if the reviewer is wrong (with technical reasoning). + +## Reviewer Prompt Template + +``` +You are a Senior Code Reviewer with expertise in software architecture, design patterns, and best practices. Your job is to review completed work against its plan or requirements and identify issues before they cascade. + +## What Was Implemented +[DESCRIPTION] + +## Requirements / Plan +[PLAN_OR_REQUIREMENTS] + +## Git Range to Review +Base: [BASE_SHA] Head: [HEAD_SHA] +git diff --stat [BASE_SHA]..[HEAD_SHA] +git diff [BASE_SHA]..[HEAD_SHA] + +## Read-Only Review +Your review is read-only on this checkout. Do not mutate the working tree, index, HEAD, or branch state. Use git show/diff/log to inspect history. If you need a working copy of a different revision, use a separate temporary worktree — never move HEAD on this checkout. + +## What to Check +Plan alignment: does the implementation match the plan/requirements? Are deviations justified or problematic? Is all planned functionality present? +Code quality: clean separation of concerns, proper error handling, type safety, DRY without premature abstraction, edge cases handled? +Architecture: sound design decisions, reasonable scalability/performance, security concerns, integrates cleanly? +Testing: tests verify real behavior not mocks, edge cases covered, integration tests where they matter, all tests passing? +Production readiness: migration strategy if schema changed, backward compatibility, documentation complete, no obvious bugs? + +## Calibration +Categorize issues by actual severity — not everything is Critical. Acknowledge what was done well before listing issues. If you find significant deviations from the plan, flag them specifically so the implementer can confirm intent. If the plan itself has issues, say so. + +## Output Format +### Strengths +[What's well done, be specific] +### Issues +#### Critical (Must Fix) — bugs, security issues, data loss risks, broken functionality +#### Important (Should Fix) — architecture problems, missing features, poor error handling, test gaps +#### Minor (Nice to Have) — code style, optimization, documentation polish +For each issue: file:line reference, what's wrong, why it matters, how to fix if not obvious. +### Recommendations +### Assessment +Ready to merge? [Yes | No | With fixes] +Reasoning: [1-2 sentence technical assessment] + +## Critical Rules +DO: categorize by actual severity, be specific (file:line), explain why each issue matters, acknowledge strengths, give a clear verdict. +DON'T: say "looks good" without checking, mark nitpicks as Critical, give feedback on code you didn't actually read, be vague, avoid a clear verdict. +``` + +## Integration +Subagent-driven development: review after EACH task, catch issues before they compound, fix before moving to next task. +Executing plans: review after each task or at natural checkpoints. +Ad-hoc development: review before merge, or when stuck. + +## Red Flags +Never: skip review because "it's simple", ignore Critical issues, proceed with unfixed Important issues, argue with valid technical feedback. +If the reviewer is wrong: push back with technical reasoning, show code/tests proving it works, request clarification. diff --git a/rise-15-5/SKILL.md b/rise-15-5/SKILL.md new file mode 100644 index 0000000..1a3e9ed --- /dev/null +++ b/rise-15-5/SKILL.md @@ -0,0 +1,188 @@ +--- +name: "rise-15-5" +description: "Co-author and post Daniel's weekly RISE 15-5 status update to the \"RISE Standups\" Loop doc. Reviews the past week's activity, walks through each required section (Accomplishments, Work in Progress, Blockers/Risks, Focus for Next Week, Help Needed) one at a time for feedback/co-authoring, then posts the finished entry to the shared Loop doc. Triggers: /rise-15-5, \"my 15-5\", \"weekly status\", \"post my 15-5\", \"RISE status update\"." +--- + +## RISE 15-5 Status Skill + +Produces and posts Daniel's weekly "15-5" status update (≤15 min to write, ≤5 min to read) to +the shared **"RISE Standups"** Microsoft Loop document, per the RISE SRE Weekly 15-5 process +that Shaun Stangler introduced in the 2026-07-15 "RISE Up" meeting (decision logged in +`D:\Repos\Obsidian\03 - Squads\C4 - SRE Core\04 - Decisions.md`). + +### Destination + +Loop doc: **"RISE Standups"** (title as shown in Loop's sidebar/tab). +Direct URL (reuse this — don't make the user hunt for it again): + +``` +https://loop.cloud.microsoft/p/eyJ1IjoiaHR0cHM6Ly9taWNyb3NvZnQuc2hhcmVwb2ludC5jb20vY29udGVudHN0b3JhZ2UvQ1NQXzYxZDFlN2FhLTJlMzQtNGYyNS05YjEwLTYzOGZkYTIyZWJlYT9uYXY9Y3owbE1rWmpiMjUwWlc1MGMzUnZjbUZuWlNVeVJrTlRVRjgyTVdReFpUZGhZUzB5WlRNMExUUm1NalV0T1dJeE1DMDJNemhtWkdFeU1tVmlaV0VtWkQxaUlYRjFabEpaVkZGMVNsVXRZa1ZIVDFBeWFVeHlObTVtYW1sbVNtVm9iMVpDY1Y5d1dtcDZOa0p6VUZoYU1UTTBSVXRFTFZKVFdrRmxRWFUxUjJaMGJESW1aajB3TVVKVk0wcEJWRXBaUzFoUFRFNVpTVmswUmtaWlVVRk5TMVZXTTBwTFFqWlZKbU05SlRKR0ptWnNkV2xrUFRFbVlUMU1iMjl3UVhCd0puQTlKVFF3Wm14MWFXUjRKVEpHYkc5dmNDMXdZV2RsTFdOdmJuUmhhVzVsY2laNFBTVTNRaVV5TW5jbE1qSWxNMEVsTWpKVU1GSlVWVWg0ZEdGWFRubGlNMDUyV201UmRXTXlhR2hqYlZaM1lqSnNkV1JETldwaU1qRTRXV2xHZUdSWFdsTlhWbEpTWkZWd1ZreFhTa1pTTURsUlRXMXNUV05xV25WYWJYQndXbXR3YkdGSE9WZFJia1ptWTBad2NXVnFXa05qTVVKWlYycEZlazVGVmt4U1F6RlRWVEZ3UWxwVlJqRk9WV1J0WkVkM2VXWkVRWGhSYkZWNlUydEdWVlJXUWtaT01WWlFWRVYwVTAweFVsZFNiSEJRVTJ0S1JsVnNiRkJOZWtwTlYwVXdKVE5FSlRJeUpUSkRKVEl5YVNVeU1pVXpRU1V5TWpobFpUazBOMk0yTFRoa01EUXROR1UxTmkxaU16YzFMVEF3WWpZNU9UVmhaamt4TUNVeU1pVTNSQT09In0%3D +``` + +Doc is MIP-labeled **Confidential/Internal Only**. Treat its content (Daniel's own and +teammates') as at least that sensitive — never copy other people's entries out of it into +unrelated destinations. + +### Template (read live from the doc's header each run — don't hardcode stale copy) + +The doc's "RISE SRE Weekly 15-5 Guidelines" section defines these required sections, in order: + +1. **✅ Accomplishments This Week** — What did you complete? What customer/reliability/ + operational/engineering impact was delivered? What milestones, deployments, investigations, + automations, dashboards, docs, or technical improvements were completed? What moved + significantly closer to done? +2. **🔄 Work in Progress** — Major initiatives underway. Important investigations/projects still + in flight. Key next steps already identified. +3. **🚧 Blockers / Risks** — Anything preventing progress. Dependencies on other teams, + approvals, access, tooling, or customer responses. Risks leadership/teammates should know. +4. **🎯 Focus for Next Week** — Top priorities. Planned deliverables. Expected milestones/ + outcomes. +5. **🤝 Help Needed (Optional)** — Areas needing support, expertise, reviews, approvals, or + decisions. + +Format rules from the doc: bullet points only, ≤15 min to write, readable in ≤5 min, focus on +outcomes/impact not hour-by-hour activity, and cover **both squad work (C4 - SRE Core) and +RISE Core / operational-excellence contributions**. + +Existing entries in the doc (see "Individual 15-5s" under each week's date heading, e.g. +`7/17/2026`) follow this per-person shape — match it: + +``` +<Name> +Accomplishments +- bullet +- bullet +WIP +- bullet +Blockers +- bullet (or "None") +Next Week +- bullet +Help Needed +- bullet (or "None" / "None at this point.") +``` + +### Voice & Formatting (learned from Daniel's direct feedback — apply on every run, not just when reminded) + +These are standing corrections from real editing passes with Daniel. Apply them proactively in +the *first* draft of every section, not just after he flags them again: + +- **No receipts unless asked.** Never cite IcM numbers, PR numbers, work-item IDs, ticket links, + or other identifiers in the drafted bullets. Describe the substance of the work in plain + language (e.g. "resolved a CloudBuild false-positive security alert," not "resolved IcM + 835355363"). If Daniel wants the identifiers included, he'll ask — don't offer them by default. +- **No em dashes.** Never use "—" in drafted bullets. Use a colon or semicolon to join clauses + instead (e.g. "Resolved livesite incidents this week as part of OCE duties: X, Y, and Z" not + "Resolved livesite incidents — X, Y, and Z"). +- **Real engineering/ops output first, KB/meeting housekeeping second.** Lead Accomplishments + with substantive delivered work: incidents resolved, code shipped, systems changed, backlog + cleaned up. Don't pad the section with meta activity like "updated a wiki page," "captured a + meeting transcript," or "logged a decision" — that's process overhead, not an accomplishment, + unless Daniel explicitly wants it included. +- **Still include real squad milestones.** Don't swing so far toward "no fluff" that genuine + squad-level events get dropped — e.g. attending and contributing to a squad's first planning + kickoff, helping confirm a roster, or a mission-defining decision *is* real accomplishment- + worthy content. The line is substance vs. busywork, not "engineering code only." When in + doubt, ask Daniel rather than guessing which side of the line something falls on. +- **Concise, plain-language bullets.** One idea per bullet. Avoid corporate-jargon stacking; + favor plain descriptions a teammate skimming in 5 minutes would immediately understand. + +If Daniel gives a new correction during a run that generalizes beyond that single edit, fold it +into this section afterward (see "Recording new feedback" below) so it's applied from the start +next time, instead of being re-taught every week. + +### Workflow + +**1. Determine the review window.** +- Open the Loop doc (see URL above) and find Daniel's most recent prior entry under "Individual + 15-5s" to anchor the "since when" boundary. If none exists yet (first-ever 15-5), default to + the last 7 days. +- Confirm the window with the user in one line before digging in (e.g. "Reviewing 7/11–7/17 — + sound right?") rather than assuming silently. + +**2. Gather raw material for the week**, pulling from every source that's actually available — +don't skip a source just because it's slower to check: +- Obsidian vault squad KB: `D:\Repos\Obsidian\03 - Squads\C4 - SRE Core\` — `Status Updates\`, + `04 - Decisions.md`, `05 - Open Questions.md`, `Meetings\` for the window (and the predecessor + `C3 - OMR Reliability Tooling\` folder if the window straddles the 2026-07-14 cutover). +- ADO (`azure_devops-*` tools): PRs authored/reviewed, work items updated/closed, in the OE/ + omr-health repo and any C4/RISE-relevant projects. Note: PR/commit search tools only find + activity in known repos — Daniel may reference IcMs or PRs (e.g. via a 1:1 Teams chat, or a + specific repo/PR link) that generic searches miss. If your initial pass comes up thin, ask + Daniel directly what ICM/ADO work he did rather than assuming there was none. +- IcM (`icm-*` tools): incidents Daniel was DRI/owner/assignee on, mitigations/resolutions in + the window. `search_incidents` with `assignedTo` may miss incidents where Daniel was the + mitigator/resolver but not the formal assignee — cross-check specific incident IDs Daniel + mentions directly via search_incidents with `incidentIds`. +- Calendar/Teams (`workiq_*`): meetings attended, notable Teams threads Daniel posted + substantive updates in — including 1:1 chats with specific people he name-drops (e.g. "check + my chat with X"), not just group/squad chats. +- Ask the user directly for anything that wouldn't show up in tooling (conversations, verbal + agreements, personal judgment calls on priority). + +**3. Go section by section — do NOT draft all five sections at once and dump them for approval.** +For each of the 5 sections in order: +- Silently draft 2-5 candidate bullets from the gathered material, mapped to that section's + sub-prompts (e.g. for Accomplishments, explicitly consider "what impact was delivered" and + "what moved closer to done", not just "what tasks closed"). Apply the Voice & Formatting rules + above while drafting — don't wait for Daniel to catch receipts/em-dashes/meta-fluff issues. +- Show the user only that section's draft bullets. +- Ask (via `m_ask_user`, free-text mode with an `inputHint`, or plain chat) whether to keep, + edit, cut, or add anything — genuinely co-author, don't just rubber-stamp your own draft. +- Only move to the next section once the user confirms this one. Skip "Help Needed" content + entirely (write "None") if the user has nothing — don't manufacture a need. + +**4. Compile the full entry** in the exact per-person shape shown above, using Daniel's name as +the heading line. + +**5. Final review before posting.** Show the complete compiled entry verbatim and get explicit +confirmation — this is going into a doc visible to the whole RISE team (per the 15-5 decision's +"visible to everyone" outcome), so treat it like any other outbound share: preview the exact +content and who can see it, then wait for a clear go-ahead. Do not post on an assumed yes. + +**6. Post to the Loop doc** using Playwright, following the general Loop-editing rules from the +`loop` skill (always `playwright-browser_type` with `slowly: true`; never `fill()`; markdown +shorthand like `- ` and `## ` is auto-interpreted; use explicit Enter key presses between lines, +never literal `\n`): +- Navigate to the Loop URL above; wait for the page to load (title becomes "RISE Standups"). +- Take a snapshot to find the current week's date heading (e.g. `## 7/17/2026`) under + "Add Entries Below" / "Individual 15-5s". Click at the end of the last existing person's + entry in that section. +- If no heading exists yet for the current week (e.g. a new week has started and nobody's + posted), create one first: type `## <M/D/YYYY>` on its own line, then `Individual 15-5s`, + matching the existing pattern, before adding Daniel's entry. +- The per-person entries in this doc are a nested bullet list where the Name and section labels + (Accomplishments/WIP/Blockers/Next Week/Help Needed) are actually **plain, non-bulleted + paragraph lines**, and only the content underneath each label is an actual bullet list. To + reproduce this: after finishing a bullet list and pressing Enter (which continues the list), + press **Backspace once** on the resulting empty bullet to drop it back to a plain paragraph + before typing the next Name/label line. Conversely, when starting a new bullet list from a + plain paragraph line, prefix the first item with `- ` (space included) to trigger Loop's + markdown auto-conversion to a bullet; subsequent Enter presses continue the list automatically + without needing the `- ` prefix again. +- Long bullet text typed with `slowly: true` can occasionally hit the tool's internal timeout + mid-string and truncate; if that happens, take a snapshot to see exactly where the text cut + off and continue typing the remainder as a follow-up `playwright-browser_type` call rather + than retyping the whole bullet. +- Type Daniel's entry using the per-person shape above, pressing Enter between each line/bullet + instead of embedding newlines in the typed string. +- Take a final snapshot after typing to confirm the entry landed correctly and nothing got + mis-nested under the wrong heading or person. + +### Recording new feedback + +If Daniel gives voice/formatting/content feedback during a run that's a general rule (not a +one-off edit specific to that week's content), add it to the "Voice & Formatting" section above +via `m_update_skill` right after the run (or whenever asked), so future runs apply it from the +first draft instead of relying on Daniel repeating himself. + +### Guardrails + +- Never post without the explicit final-review confirmation in step 5. +- Never edit or delete another teammate's existing entry in the doc. +- If the review window's source-gathering surfaces anything sensitive (e.g. MIP-labeled email + content, private HR/personnel matters), leave it out of the section drafts entirely rather + than asking the user whether to include it — err on the side of omission per the outbound- + communication privacy rules. +- If Playwright hits an auth prompt or the page doesn't load the canvas within a reasonable + wait, stop and tell the user rather than retrying blindly. diff --git a/subagent-driven-development/SKILL.md b/subagent-driven-development/SKILL.md new file mode 100644 index 0000000..1c5721b --- /dev/null +++ b/subagent-driven-development/SKILL.md @@ -0,0 +1,65 @@ +--- +name: "subagent-driven-development" +description: "Execute a multi-task implementation plan in the current session by dispatching each task to an implementer sub-agent and then a reviewer sub-agent, with pre-flight plan review, model selection, status handling, and durable progress tracking between tasks. Use when the plan has independent tasks and you want them built and reviewed without burning main-session context. Triggers: work through the plan with subagents, delegate these tasks, build this out task by task." +--- + +Execute a plan by dispatching a fresh implementer subagent per task, a task review (spec compliance + code quality) after each, and a broad whole-branch review at the end. + +**Why subagents:** delegate tasks to specialized agents with isolated context. Precisely craft their instructions so they stay focused. They never inherit your session's history — construct exactly what they need. This preserves your own context for coordination. + +**Core principle:** fresh subagent per task + task review (spec + quality) + broad final review = high quality, fast iteration. + +**Narration:** between tool calls, narrate at most one short line. + +**Continuous execution:** do not pause to check in between tasks. Execute all tasks from the plan without stopping. The only reasons to stop: BLOCKED status you cannot resolve, ambiguity that genuinely prevents progress, or all tasks complete. + +## When to Use +- You have an implementation plan with mostly-independent tasks +- You're staying in this session (not a parallel session — use executing-plans for that) +- Not tightly coupled tasks needing manual/sequential judgment + +## The Process (per task) +1. Dispatch an implementer subagent with: the task's full text/brief, interfaces and decisions from earlier tasks, any ambiguity you've resolved, and a report contract (what to return). +2. If the implementer asks questions, answer them and re-dispatch. +3. Implementer implements, tests, commits, self-reviews, and reports one of four statuses (see below). +4. On DONE: dispatch a task-review subagent with the diff (git diff/log for the task's commit range) and the task's requirements — check spec compliance AND code quality. +5. If review finds Critical/Important issues: dispatch a fix subagent with the full findings list, then re-review. +6. Mark task complete in your todo list once review is clean. +7. Repeat for all tasks. +8. After all tasks: dispatch a final whole-branch code reviewer (broad review across the entire diff from the branch's merge-base). +9. Use the finishing-a-development-branch skill to wrap up. + +## Pre-Flight Plan Review +Before dispatching Task 1, scan the plan once for conflicts — tasks that contradict each other or the plan's Global Constraints. Present everything found as one batched question before execution begins. If clean, proceed without comment. + +## Model Selection +Use the least powerful model that can handle each role. +- Mechanical implementation (isolated functions, clear specs, 1-2 files): fast/cheap model +- Integration/judgment tasks (multi-file coordination, debugging): standard model +- Architecture/design tasks and the final whole-branch review: most capable model +- Always specify the model explicitly when dispatching — an omitted model inherits your session's (often most expensive) default. +- Turn count beats token price: a mid-tier model as the floor for reviewers/implementers-from-prose is often cheaper overall than a cheap model taking 2-3x the turns. + +## Handling Implementer Status +- **DONE:** proceed to task review with the diff. +- **DONE_WITH_CONCERNS:** read the concerns. If about correctness/scope, address before review. If observational, note and proceed. +- **NEEDS_CONTEXT:** provide missing info, re-dispatch. +- **BLOCKED:** assess — provide more context and retry same model; escalate to a more capable model if reasoning-limited; break into smaller pieces if too large; escalate to the human if the plan itself is wrong. Never force the same model to retry without changing something. + +## Handling Reviewer Findings +A finding that conflicts with what the plan's text requires is the human's decision — present the finding and the plan text, ask which governs. Don't dismiss it because the plan mandates it, and don't dispatch a contradicting fix without asking. + +## Constructing Reviewer Prompts +- Don't add open-ended directives ("check all uses") without a concrete reason. +- Don't ask reviewers to re-run tests the implementer already ran on the same code. +- Never pre-judge findings for the reviewer ("don't flag X") — let the reviewer raise it and adjudicate afterward. +- Give the reviewer the binding Global Constraints verbatim (exact values/formats/relationships). +- Hand the reviewer the diff as a file reference where possible, not pasted into your context. +- A dispatch prompt describes one task, not the session's history — don't paste accumulated prior-task summaries. +- Record Minor findings for the final whole-branch review to triage. + +## Durable Progress +Conversation memory does not survive compaction. Track progress in a durable ledger/todo list, not only in your head — re-dispatching completed tasks after losing context is the most expensive failure mode. If you lose your place, trust the ledger/git log over your own recollection. + +## Prompt Templates +Use the requesting-code-review skill's reviewer template for the final whole-branch review. diff --git a/sync-skills.ps1 b/sync-skills.ps1 new file mode 100644 index 0000000..ce8ee6d --- /dev/null +++ b/sync-skills.ps1 @@ -0,0 +1,164 @@ +[CmdletBinding()] +param( + [string] $SourceRoot = 'C:\Users\dkucinski\.scout\m-skills', + [string] $RepositoryRoot = $PSScriptRoot +) + +$ErrorActionPreference = 'Stop' + +$repositoryRoot = (Resolve-Path -LiteralPath $RepositoryRoot).Path +$sourceRoot = (Resolve-Path -LiteralPath $SourceRoot).Path +$logDirectory = Join-Path $repositoryRoot 'logs' +$logPath = Join-Path $logDirectory 'sync.log' +$allowedExtensions = @('.gitignore', '.js', '.json', '.mjs', '.ps1', '.psm1', '.yaml', '.yml') +$excludedDirectories = @('.git', 'cache', 'caches', 'logs', 'node_modules') +$excludedFiles = @('.token-cache.json', 'skills-metadata.json') + +New-Item -ItemType Directory -Path $logDirectory -Force | Out-Null + +function Write-SyncLog { + param( + [ValidateSet('INFO', 'WARN', 'ERROR')] + [string] $Level, + [string] $Message + ) + + $entry = '{0:o} [{1}] {2}' -f (Get-Date), $Level, $Message + Add-Content -LiteralPath $logPath -Value $entry + Write-Output $entry +} + +function Invoke-Git { + param([string[]] $Arguments) + + & git -C $repositoryRoot @Arguments + if ($LASTEXITCODE -ne 0) { + throw "Git command failed: git $($Arguments -join ' ')" + } +} + +function Test-AllowedSourceFile { + param( + [System.IO.FileInfo] $File, + [string] $SkillRoot + ) + + $relativePath = $File.FullName.Substring($SkillRoot.Length).TrimStart('\') + $segments = $relativePath -split '\\' + if ($segments | Where-Object { + $_ -like '.pw-profile*' -or $excludedDirectories -contains $_ + }) { + return $false + } + + if ($excludedFiles -contains $File.Name) { + return $false + } + + return $File.Name -eq 'SKILL.md' -or $allowedExtensions -contains $File.Extension.ToLowerInvariant() +} + +function Get-SkillDescription { + param([string] $SkillFile) + + $content = Get-Content -LiteralPath $SkillFile + $descriptionLine = $content | Select-String -Pattern '^\s*description:\s*["'']?(.*?)["'']?\s*$' | Select-Object -First 1 + if ($null -eq $descriptionLine) { + return 'No description provided.' + } + + return $descriptionLine.Matches[0].Groups[1].Value.Trim() +} + +$stagingRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("PromptWorks-{0}" -f [guid]::NewGuid()) + +try { + if (-not (Test-Path -LiteralPath (Join-Path $repositoryRoot '.git'))) { + throw "'$repositoryRoot' is not a Git repository." + } + + Write-SyncLog -Level INFO -Message "Starting sync from '$sourceRoot'." + New-Item -ItemType Directory -Path $stagingRoot | Out-Null + + $sourceSkills = Get-ChildItem -LiteralPath $sourceRoot -Directory | Sort-Object Name + foreach ($skill in $sourceSkills) { + $stagingSkillRoot = Join-Path $stagingRoot $skill.Name + $copiedFiles = 0 + + Get-ChildItem -LiteralPath $skill.FullName -Recurse -File | Where-Object { + Test-AllowedSourceFile -File $_ -SkillRoot $skill.FullName + } | ForEach-Object { + $relativePath = $_.FullName.Substring($skill.FullName.Length).TrimStart('\') + $destination = Join-Path $stagingSkillRoot $relativePath + New-Item -ItemType Directory -Path (Split-Path -Parent $destination) -Force | Out-Null + Copy-Item -LiteralPath $_.FullName -Destination $destination + $copiedFiles++ + } + + if (-not (Test-Path -LiteralPath (Join-Path $stagingSkillRoot 'SKILL.md'))) { + throw "Skill '$($skill.Name)' has no allowed SKILL.md file." + } + + Write-SyncLog -Level INFO -Message "Staged skill '$($skill.Name)' ($copiedFiles files)." + } + + $existingSkills = Get-ChildItem -LiteralPath $repositoryRoot -Directory | Where-Object { + Test-Path -LiteralPath (Join-Path $_.FullName 'SKILL.md') + } + foreach ($skill in $existingSkills) { + if ($skill.Name -notin $sourceSkills.Name) { + Remove-Item -LiteralPath $skill.FullName -Recurse -Force + Write-SyncLog -Level INFO -Message "Removed stale skill '$($skill.Name)'." + } + } + + foreach ($skill in $sourceSkills) { + $destination = Join-Path $repositoryRoot $skill.Name + if (Test-Path -LiteralPath $destination) { + Remove-Item -LiteralPath $destination -Recurse -Force + } + Move-Item -LiteralPath (Join-Path $stagingRoot $skill.Name) -Destination $destination + } + + $readmeLines = @( + '# PromptWorks', + '', + 'Private version-controlled mirror of custom Microsoft Scout skills.', + '', + 'Generated by `sync-skills.ps1`; do not edit manually.', + '', + '## Skills', + '', + '| Skill | Description |', + '| --- | --- |' + ) + foreach ($skill in $sourceSkills) { + $description = (Get-SkillDescription -SkillFile (Join-Path (Join-Path $repositoryRoot $skill.Name) 'SKILL.md')).Replace('|', '\|') + $readmeLines += "| [$($skill.Name)]($($skill.Name)/SKILL.md) | $description |" + } + Set-Content -LiteralPath (Join-Path $repositoryRoot 'README.md') -Value $readmeLines + + Invoke-Git -Arguments @('add', '--all') + & git -C $repositoryRoot diff --cached --quiet + if ($LASTEXITCODE -eq 0) { + Write-SyncLog -Level INFO -Message 'Sync completed with no changes.' + exit 0 + } + if ($LASTEXITCODE -ne 1) { + throw 'Unable to determine whether staged changes exist.' + } + + $commitMessage = 'chore: sync Scout skills {0:yyyy-MM-dd}' -f (Get-Date) + Invoke-Git -Arguments @('commit', '-m', $commitMessage) + Invoke-Git -Arguments @('push', '-u', 'origin', 'main') + Write-SyncLog -Level INFO -Message "Sync completed and pushed commit '$commitMessage'." +} +catch { + Write-SyncLog -Level ERROR -Message $_.Exception.Message + throw +} +finally { + if (Test-Path -LiteralPath $stagingRoot) { + Remove-Item -LiteralPath $stagingRoot -Recurse -Force + } +} diff --git a/systematic-debugging/SKILL.md b/systematic-debugging/SKILL.md new file mode 100644 index 0000000..a3a12b5 --- /dev/null +++ b/systematic-debugging/SKILL.md @@ -0,0 +1,46 @@ +--- +name: "systematic-debugging" +description: "Find the actual root cause before proposing any fix - investigate, analyze patterns across similar cases, form and test a hypothesis, then implement. Use at the first sign of ANY bug, test failure, crash, error message, flaky test, or behavior that does not match expectations, and especially when tempted to try a quick patch. Triggers: this is broken, it is failing, why does this not work, weird error, test is red, it worked yesterday, intermittent failure." +--- + +Random fixes waste time and create new bugs. Quick patches mask underlying issues. + +**Core principle:** ALWAYS find root cause before attempting fixes. Symptom fixes are failure. + +**The Iron Law: NO FIXES WITHOUT ROOT CAUSE INVESTIGATION FIRST.** If you haven't completed Phase 1, you cannot propose fixes. + +Use this for ANY technical issue: test failures, production bugs, unexpected behavior, performance problems, build failures, integration issues. Use it ESPECIALLY under time pressure, when "just one quick fix" seems obvious, or after you've already tried multiple fixes. + +## Phase 1: Root Cause Investigation +1. **Read error messages carefully** — full stack traces, line numbers, file paths, error codes. Don't skip past warnings. +2. **Reproduce consistently** — can you trigger it reliably? If not reproducible, gather more data, don't guess. +3. **Check recent changes** — git diff, recent commits, new dependencies, config/environment differences. +4. **Gather evidence in multi-component systems** — before proposing fixes, add diagnostic instrumentation at each component boundary (log what enters/exits, verify env/config propagation, check state at each layer). Run once to see WHERE it breaks, then investigate that component. +5. **Trace data flow backward** — where does the bad value originate? What called this with the bad value? Keep tracing up until you find the source. Fix at source, not symptom. + +## Phase 2: Pattern Analysis +1. Find working examples similar to what's broken in the same codebase. +2. Compare against references — if implementing a pattern, read the reference implementation completely, don't skim. +3. Identify every difference between working and broken, however small. +4. Understand dependencies — what settings/config/environment/assumptions does this need? + +## Phase 3: Hypothesis and Testing +1. Form a single hypothesis: "I think X is the root cause because Y." Write it down. +2. Test minimally — smallest possible change, one variable at a time. +3. Verify before continuing. Worked? → Phase 4. Didn't work? → new hypothesis, don't stack more fixes on top. +4. If you don't understand something, say so explicitly and ask/research rather than guessing. + +## Phase 4: Implementation +1. Create a failing test case reproducing the bug first (use test-driven-development skill). +2. Implement a single fix addressing the root cause — one change, no bundled refactoring. +3. Verify the fix — test passes, no other tests broken, issue actually resolved. +4. If the fix doesn't work: STOP. Count attempts. If < 3, return to Phase 1 with new information. **If ≥ 3 fixes failed, STOP and question the architecture** — don't attempt fix #4 without discussing fundamentals with your human partner. This is not a failed hypothesis, it's likely a wrong architecture (each fix reveals a new problem elsewhere, or requires "massive refactoring"). + +## Red Flags — STOP and return to Phase 1 +"Quick fix for now, investigate later" / "Just try changing X and see" / "Add multiple changes, run tests" / "Skip the test, I'll manually verify" / "It's probably X" / "I don't fully understand but this might work" / proposing solutions before tracing data flow / "one more fix attempt" after 2+ tries / each fix revealing a new problem elsewhere. + +## When "No Root Cause" Is Actually True +If systematic investigation truly reveals the issue is environmental/timing-dependent/external: document what you investigated, implement appropriate handling (retry/timeout/error message), add monitoring for future investigation. But 95% of "no root cause" cases are incomplete investigation — don't reach for this early. + +## Related skills +Use test-driven-development for the failing test in Phase 4. Use verification-before-completion to confirm the fix worked before claiming success. diff --git a/teams-export/.gitignore b/teams-export/.gitignore new file mode 100644 index 0000000..9106b4e --- /dev/null +++ b/teams-export/.gitignore @@ -0,0 +1,3 @@ +.pw-profile/ +.token-cache.json +node_modules/ diff --git a/teams-export/SKILL.md b/teams-export/SKILL.md new file mode 100644 index 0000000..754f2f4 --- /dev/null +++ b/teams-export/SKILL.md @@ -0,0 +1,226 @@ +--- +name: "teams-export" +description: "Export Microsoft Teams messages (1:1 chats, group chats, and channel posts) into the Obsidian vault at D:\\Repos\\Obsidian\\00 - Chats\\ as markdown. Reads the user-maintained source list at D:\\Repos\\Obsidian\\00 - Chats\\sources.yaml to know what to copy. The invoking prompt or automation decides the time range (e.g. last N messages, today, since last export, explicit date range). Triggers: 'teams export', 'export chat', 'export channel', 'copy teams messages', '/teams-export'." +--- + +## Teams Export Skill + +Copies Microsoft Teams messages into the Obsidian vault as plain markdown. +Read-only against Teams; only writes inside `D:\Repos\Obsidian\00 - Chats\`. + +### Source list (authoritative) + +**File:** `D:\Repos\Obsidian\00 - Chats\sources.yaml` + +The user maintains this file by hand. The skill MUST read it on every run and +treat it as the single source of truth for what to export. Never invent sources, +never write back to this file, never silently skip malformed entries — if an +entry is broken or its identifiers can't be resolved, report it to the user and +continue with the rest. + +Entry shapes (see file for inline examples): + +| type | required fields | optional fields | +|-----------|------------------------------|----------------------------------------------| +| oneOnOne | `alias`, `email` | `chat_id`, `notes`, `enabled` | +| group | `alias`, `chat_id` | `topic`, `notes`, `enabled` | +| channel | `alias`, `team_id`, `channel_id` | `team`, `channel`, `notes`, `enabled` | + +Skip any entry where `enabled: false`. + +### Resolving identifiers + +- **oneOnOne**: + - If `chat_id` is present in the entry, use it directly — DO NOT call + `m365_create_chat_by_email`. 1:1 chatIds are stable per user pair, so + cached values stay valid indefinitely and skipping the lookup saves a + round-trip per entry. + - Only fall back to `m365_create_chat_by_email` with `email` when `chat_id` + is missing. If resolution returns `user_not_found`, try + `m365_search_people` with the alias / name to find the correct UPN, then + retry. Report the resolved chatId in the run summary so the user can paste + it into `sources.yaml`. +- **group**: use `chat_id` directly with `m365_list_chat_messages`. +- **channel**: try `m365_list_channel_messages` first with `team_id` + `channel_id` + (expand replies). If it fails with a permission error (403 / "missing + ChannelMessage.Read.All"), fall back to the **bundled scraper** described below. + +### Listing messages (Graph quirks) + +`m365_list_chat_messages` rejects most `$filter` combinations. The shape that +works for date-windowed exports is: + +``` +filter: lastModifiedDateTime gt <ISO> +orderBy: lastModifiedDateTime desc +``` + +Notes: +- `createdDateTime` only supports the `lt` operator on this endpoint. +- `lastModifiedDateTime` requires `orderBy: lastModifiedDateTime desc` — + otherwise Graph returns "filter must target createdDateTime when orderBy is + createdDateTime desc". +- Page with `skipToken` until the window is fully covered (50-message cap per + call). Stop early once messages fall outside the requested range. + +### Channel fallback: `scrape-channel.mjs` + +Clawpilot's Graph app registration typically does **not** have +`ChannelMessage.Read.All`. The fallback uses the user's signed-in Teams web +session via Playwright to capture a bearer token and replay paginated requests +against Teams' internal chat service. Output shape is Graph-compatible so the +same markdown renderer handles it. + +**Location:** `C:\Users\dkucinski\.scout\m-skills\teams-export\scrape-channel.mjs` + +**One-time setup (first run only):** +1. Run the script with `--headed`. An Edge window opens controlled by Playwright, + using a persistent profile at `.pw-profile/`. +2. Sign in to teams.microsoft.com if prompted. +3. Click into the target channel and let messages load. The script auto-captures + the bearer token from the first matching network request and caches it to + `.token-cache.json`. + +After that, subsequent runs reuse the profile and cached token (~1 hr lifetime). +When the token expires the script re-captures automatically — `--headed` is only +needed if AAD requires interactive sign-in again. + +**Invocation pattern (from this skill, when Graph 403s):** +```pwsh +node "C:\Users\dkucinski\.scout\m-skills\teams-export\scrape-channel.mjs" ` + --team-id "<team_id>" ` + --channel-id "<channel_id>" ` + --since "2026-05-21T00:00:00Z" ` + --limit 200 ` + [--headed] [--refresh-token] [--no-replies] +``` + +stdout is JSON `{ value: [root,...], count, replyCount, capturedAt, endpoint }`. +Each root has Graph fields: `id`, `createdDateTime`, `lastModifiedDateTime`, +`from.user.{displayName,id}`, `body.contentType`, `body.content`, `attachments`, +`messageType`, `subject`, plus `replies: [reply,...]` (same shape, oldest-first). +Replies also include `replyToId` pointing to their root for cross-referencing. + +**Threading:** replies are on by default. Use `--no-replies` to suppress them +(roots only). Replies are detected via the `messageid=<rootId>` suffix in the +chat-service `conversationLink`, so threaded conversations in the OMR channels +come through correctly. + +**Name resolution:** real users resolve to display names via Teams' profile +endpoint. Service identities (meeting-recording bot, channel-email, webhooks) +may show as `User <guid-prefix>` or `Channel` — that's expected and means the +post wasn't authored by a person. + +**Diagnostics:** set `$env:SCRAPE_DEBUG="1"` to log every Teams API request seen, +which helps when Teams web routing changes and the URL matcher needs updating. + +**Profile lock recovery:** when the scraper aborts with +`Failed to create a ProcessSingleton`, an orphaned Edge from a previous run is +still holding `.pw-profile\lockfile`. Find and kill it: + +```pwsh +Get-CimInstance Win32_Process -Filter "Name='msedge.exe'" | + Where-Object { $_.CommandLine -like "*pw-profile*" } | + ForEach-Object { Stop-Process -Id $_.ProcessId -Force } +Remove-Item "C:\Users\dkucinski\.scout\m-skills\teams-export\.pw-profile\lockfile" -Force +``` + +If `Stop-Process` reports "Cannot find a process" but `tasklist /fi "PID eq <pid>"` +still shows it (zombie session), the process is in a different session and +can't be killed from this one — the user needs to reboot or close it from the +session that spawned it. Surface this clearly and skip the channel. + +**Guardrails for the fallback:** +- Never commit `.pw-profile/` or `.token-cache.json` (covered by local + `.gitignore`). These contain auth state. +- Treat the captured token as a credential — never echo it to the user, never + write it outside `.token-cache.json`. +- If capture fails after 5 min, surface the failure and skip that channel; do + not block the rest of the run. + +### Range selection (caller-driven) + +The skill does NOT pick a default range. The invoking prompt or automation must +specify one of: +- `last_n` — e.g. last 50 messages +- `since` — ISO timestamp; include messages with `createdDateTime >= since` +- `date_range` — explicit start/end ISO timestamps +- `today` — local-day window in America/Los_Angeles + +If no range is supplied, ask the user before exporting anything. + +Page through Graph results (50/call cap) using `skipToken` until the window is +covered. Stop early once messages fall outside the requested range. + +### Output + +**Folder:** `D:\Repos\Obsidian\00 - Chats\YYYY-MM\` (create if missing, +month derived from the message timestamp in America/Los_Angeles). + +**Filename:** `<alias>.md` — one file per source per month. Append-only: +if the file exists, add new messages under a new date divider; never rewrite +existing content. Deduplicate by message `id` to avoid double-writing on reruns. + +**File structure:** + +```markdown +--- +alias: omr-c3 +type: channel +team: OMR +channel: XP - C3 - OMR Reliability Tooling (OMRHealth) +team_id: db3900a4-... +channel_id: 19:134dc34e...@thread.tacv2 +--- + +# omr-c3 — 2026-05 + +## 2026-05-18 + +### 09:14 — Daniel Kucinski +Message body in markdown. HTML is converted to markdown; emoji and basic +formatting (bold, italic, lists, links, code) are preserved. + +> Reply by Alex Smith — 09:18 +> Threaded reply body. + +**Attachments:** `dashboard.png`, `notes.docx` *(metadata only — files not downloaded)* + +--- + +### 09:32 — Alex Smith +... +``` + +Conventions: +- Headings: `##` for date (YYYY-MM-DD), `###` for each message (HH:mm — Author). +- Replies are rendered as blockquotes nested under their parent root message. +- **Media simplification:** attachments and inline images become a single + `**Attachments:** name1, name2` line. Do NOT download binaries. +- **Metadata simplification:** keep author, timestamp, and reply structure. + Drop reactions, read receipts, mention metadata (just render @Name inline), + and raw HTML wrappers. +- Convert message HTML → markdown (preserve links, bold/italic, lists, inline + code, code blocks, blockquotes). +- Timestamps in America/Los_Angeles, 24-hour. +- Separate root messages with a horizontal rule (`---`). + +### Run summary + +After every run, print a short summary to the user: +- Sources processed / skipped (with reasons) +- Messages written per source +- Output file paths +- Any errors +- **Any newly resolved 1:1 `chat_id` values** (so the user can paste them into + `sources.yaml` to skip the lookup next time) + +### Guardrails + +- Never write outside `D:\Repos\Obsidian\00 - Chats\`. +- Never edit `sources.yaml`. +- Respect MIP sensitivity labels — if a source surfaces classified content, + note it in the run summary so the user knows the output file inherits that + sensitivity. +- If a `chat_id` or `channel_id` can't be resolved (403/404), report it and + move on — don't crash the whole run. diff --git a/teams-export/package-lock.json b/teams-export/package-lock.json new file mode 100644 index 0000000..dc2d9f3 --- /dev/null +++ b/teams-export/package-lock.json @@ -0,0 +1,60 @@ +{ + "name": "teams-export", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "teams-export", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "playwright": "^1.60.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + } + } +} diff --git a/teams-export/package.json b/teams-export/package.json new file mode 100644 index 0000000..d952577 --- /dev/null +++ b/teams-export/package.json @@ -0,0 +1,16 @@ +{ + "name": "teams-export", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "dependencies": { + "playwright": "^1.60.0" + } +} diff --git a/teams-export/scrape-channel.mjs b/teams-export/scrape-channel.mjs new file mode 100644 index 0000000..ba18f39 --- /dev/null +++ b/teams-export/scrape-channel.mjs @@ -0,0 +1,472 @@ +#!/usr/bin/env node +// scrape-channel.mjs — Fetch Teams channel messages without ChannelMessage.Read.All. +// +// Captures a bearer token from the user's signed-in Teams web session (Playwright with +// persistent profile + Edge channel), then replays paginated GETs against Teams' +// internal CSA (Conversation Service Adapter) endpoint to retrieve channel messages +// in a Graph-compatible shape. +// +// USAGE +// node scrape-channel.mjs --team-id <guid> --channel-id <19:...@thread.tacv2> +// [--since ISO] [--until ISO] [--limit N] +// [--headed] [--refresh-token] [--include-replies] +// +// OUTPUT +// JSON to stdout: { value: [ message, ... ], capturedAt, source } +// Each message: { id, createdDateTime, lastModifiedDateTime, from, body, attachments, +// replyToId, replies?: [ ... ] } +// +// EXIT CODES +// 0 ok, 2 bad args, 3 token-capture failed, 4 fetch failed after retry. + +import { chromium } from "playwright"; +import { mkdirSync, existsSync, readFileSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const PROFILE_DIR = join(__dirname, ".pw-profile"); +const TOKEN_CACHE = join(__dirname, ".token-cache.json"); +const CAPTURE_TIMEOUT_MS = Number.parseInt(process.env.SCRAPE_CAPTURE_TIMEOUT_MS || "300000", 10); +const TOKEN_SAFETY_MARGIN_S = 120; + +// --- arg parsing ----------------------------------------------------------- +function parseArgs(argv) { + const out = { headed: false, refreshToken: false, includeReplies: true, limit: 0 }; + for (let i = 2; i < argv.length; i++) { + const a = argv[i]; + const next = () => argv[++i]; + switch (a) { + case "--team-id": out.teamId = next(); break; + case "--channel-id": out.channelId = next(); break; + case "--since": out.since = next(); break; + case "--until": out.until = next(); break; + case "--limit": out.limit = Number.parseInt(next(), 10) || 0; break; + case "--headed": out.headed = true; break; + case "--refresh-token": out.refreshToken = true; break; + case "--include-replies": out.includeReplies = true; break; + case "--no-replies": out.includeReplies = false; break; + case "-h": case "--help": out.help = true; break; + default: throw new Error(`Unknown arg: ${a}`); + } + } + return out; +} + +const args = parseArgs(process.argv); +if (args.help) { + process.stderr.write(`See header of ${import.meta.url}\n`); + process.exit(0); +} +if (!args.teamId || !args.channelId) { + process.stderr.write("ERR: --team-id and --channel-id are required\n"); + process.exit(2); +} + +const sinceMs = args.since ? Date.parse(args.since) : 0; +const untilMs = args.until ? Date.parse(args.until) : Number.POSITIVE_INFINITY; +if (Number.isNaN(sinceMs) || Number.isNaN(untilMs)) { + process.stderr.write("ERR: --since/--until must be ISO timestamps\n"); + process.exit(2); +} + +// --- token cache ----------------------------------------------------------- +function loadCache() { + if (!existsSync(TOKEN_CACHE)) return null; + try { + const c = JSON.parse(readFileSync(TOKEN_CACHE, "utf8")); + if (!c.token || !c.expiresAt) return null; + if (Date.now() / 1000 > c.expiresAt - TOKEN_SAFETY_MARGIN_S) return null; + return c; + } catch { return null; } +} +function saveCache(c) { writeFileSync(TOKEN_CACHE, JSON.stringify(c, null, 2)); } + +function decodeJwtExp(jwt) { + try { + const payload = JSON.parse(Buffer.from(jwt.split(".")[1], "base64url").toString("utf8")); + return payload.exp || 0; + } catch { return 0; } +} + +// --- token capture via Playwright ----------------------------------------- +async function captureToken({ teamId, channelId, headed }) { + mkdirSync(PROFILE_DIR, { recursive: true }); + const ctx = await chromium.launchPersistentContext(PROFILE_DIR, { + channel: "msedge", + headless: !headed, + viewport: { width: 1280, height: 900 }, + }); + + let resolveCap, rejectCap; + const capPromise = new Promise((res, rej) => { resolveCap = res; rejectCap = rej; }); + + // Accept any URL that fetches messages for our specific channel/thread id. + // Teams v2 routes vary by region and feature flags; we don't try to predict the + // host, we just look for the channel id + a /messages segment with a bearer token. + const channelIdLower = channelId.toLowerCase(); + const channelIdEnc = encodeURIComponent(channelId).toLowerCase(); + const matchers = [ + (u) => { + const ul = u.toLowerCase(); + const hasChannel = ul.includes(channelIdLower) || ul.includes(channelIdEnc); + const hasMessages = /\/messages(\/|\?|$)/i.test(ul); + return hasChannel && hasMessages; + }, + // Fallback: NG chat service for the channel, even if id is opaque in URL + (u) => /\.ng\.msg\.teams\.(microsoft|live)\.com\/v\d+\/users\/ME\/conversations\/[^/]+\/messages/i.test(u), + (u) => /api\.flightproxy\.teams\.microsoft\.com\/api\/v\d+\/ep\/[^/]+\/v\d+\/users\/ME\/conversations\/[^/]+\/messages/i.test(u), + ]; + + const seenUrls = new Set(); + const onRequest = (req) => { + const url = req.url(); + // Light-touch debug log so we can see what's flying past if capture fails. + if (process.env.SCRAPE_DEBUG && /messages|conversations\//i.test(url) && /teams|skype|flightproxy|trouter/i.test(url)) { + if (!seenUrls.has(url)) { + seenUrls.add(url); + process.stderr.write(`[req] ${req.method()} ${url}\n`); + } + } + const matchIdx = matchers.findIndex((m) => m(url)); + if (matchIdx < 0) return; + const headers = req.headers(); + const auth = headers["authorization"]; + if (!auth || !auth.startsWith("Bearer ")) return; + const token = auth.slice(7); + const exp = decodeJwtExp(token); + const u = new URL(url); + process.stderr.write(`Captured token (matcher #${matchIdx + 1}) from ${u.origin}${u.pathname}\n`); + resolveCap({ + token, + expiresAt: exp || (Math.floor(Date.now() / 1000) + 3000), + endpoint: `${u.origin}${u.pathname.replace(/\/messages.*/, "/messages")}`, + headers: { + accept: headers["accept"] || "application/json", + "accept-language": headers["accept-language"] || "en-US,en;q=0.9", + "user-agent": headers["user-agent"] || "", + // Pass through any x-ms-* / behavior headers the service may require. + ...Object.fromEntries( + Object.entries(headers).filter(([k]) => + k.startsWith("x-ms-") || k === "behavioroverride" || k === "x-skypetoken", + ), + ), + }, + capturedAt: new Date().toISOString(), + }); + }; + ctx.on("request", onRequest); + + const groupId = teamId; + const encodedChannel = encodeURIComponent(channelId); + // New Teams ("v2") deep-link variants. Try in order; first non-erroring wins. + // If none land directly on the channel, the user can click into it manually + // inside the Playwright window; the interceptor still fires. + const deepLinks = [ + `https://teams.microsoft.com/v2/#/channel/${encodedChannel}/conversations?groupId=${groupId}`, + `https://teams.microsoft.com/v2/#/conversations/channel?threadId=${encodedChannel}&ctx=channel&groupId=${groupId}`, + `https://teams.microsoft.com/l/channel/${encodedChannel}/General?groupId=${groupId}`, + `https://teams.microsoft.com/v2/`, + `https://teams.microsoft.com/`, + ]; + const page = await ctx.newPage(); + + let timer; + try { + process.stderr.write( + `Opening Teams. If sign-in is required, complete it in the browser window. ` + + `Up to ${Math.round(CAPTURE_TIMEOUT_MS / 1000)}s to capture a channel request.\n`, + ); + for (const link of deepLinks) { + try { + await page.goto(link, { waitUntil: "domcontentloaded", timeout: 30_000 }); + break; + } catch { /* try next */ } + } + timer = setTimeout( + () => rejectCap(new Error( + `token capture timed out after ${Math.round(CAPTURE_TIMEOUT_MS / 1000)}s. ` + + `Re-run with SCRAPE_DEBUG=1 and --headed; manually click into the channel if needed.`, + )), + CAPTURE_TIMEOUT_MS, + ); + const result = await capPromise; + return result; + } finally { + clearTimeout(timer); + await ctx.close().catch(() => {}); + } +} + +// --- message fetch --------------------------------------------------------- +async function fetchPage(cache, url) { + const res = await fetch(url, { + headers: { + ...cache.headers, + authorization: `Bearer ${cache.token}`, + }, + }); + return res; +} + +function isInWindow(msg) { + const t = Date.parse(msg.createdDateTime || msg.composetime || msg.originalArrivalTime || 0); + if (Number.isNaN(t)) return true; + return t >= sinceMs && t <= untilMs; +} + +function parseFromField(rawFrom, imdisplayname) { + if (imdisplayname && typeof imdisplayname === "string" && !imdisplayname.startsWith("http")) { + return { displayName: imdisplayname, userId: null }; + } + if (rawFrom && typeof rawFrom === "object" && rawFrom.user) { + return { displayName: rawFrom.user.displayName || null, userId: rawFrom.user.id || null }; + } + if (typeof rawFrom === "string") { + // Skype/Teams contact URL: .../contacts/<id> where id is either + // "8:orgid:<guid>" (real user) or "19:<thread>@thread.tacv2" (channel itself). + const m = /\/contacts\/(8:orgid:[^/?#]+|19:[^/?#]+)/i.exec(rawFrom); + if (m) { + const id = decodeURIComponent(m[1]); + if (id.startsWith("19:")) return { displayName: "Channel", userId: id }; + if (id.startsWith("8:orgid:")) { + const guid = id.slice("8:orgid:".length); + return { displayName: null, userId: guid }; + } + return { displayName: null, userId: id }; + } + } + return { displayName: null, userId: null }; +} + +// Some CSA responses return Teams chat-service shape; normalize to Graph-ish. +function normalize(msg) { + if (msg.body && typeof msg.body === "object") return msg; // already Graph-shaped + const created = msg.composetime || msg.originalarrivaltime || msg.originalArrivalTime || msg.createdDateTime; + const contentType = (msg.messagetype || "").toLowerCase().includes("html") ? "html" : "text"; + const { displayName, userId } = parseFromField(msg.from, msg.imdisplayname); + // Channel replies carry their root id in the conversationLink as + // `...;messageid=<rootId>`. Root posts have a conversationLink with no + // messageid suffix (or messageid equal to their own id). + const link = msg.conversationLink || msg.conversationid || ""; + const m = /messageid=(\d+)/i.exec(link); + const rootId = m ? m[1] : null; + const ownId = String(msg.id || ""); + const parent = rootId && rootId !== ownId ? rootId : null; + let attachments = []; + const props = msg.properties || {}; + if (props.files) { + try { attachments = typeof props.files === "string" ? JSON.parse(props.files) : props.files; } + catch { attachments = []; } + } + return { + id: msg.id || msg.clientmessageid, + createdDateTime: created, + lastModifiedDateTime: msg.version || created, + from: { user: { displayName: displayName, id: userId } }, + body: { contentType, content: msg.content || "" }, + attachments, + replyToId: parent, + messageType: msg.messagetype || null, + subject: props.subject || null, + }; +} + +// Resolve user GUIDs to display names via Teams' profile lookup endpoint. +// Best-effort: failures leave the GUID in place and don't fail the run. +async function resolveDisplayNames(cache, flat, { teamId, channelId }) { + const unresolved = new Map(); // guid -> [message refs] + for (const m of flat) { + const u = m.from?.user; + if (u && !u.displayName && u.id && /^[0-9a-f]{8}-[0-9a-f]{4}-/i.test(u.id)) { + if (!unresolved.has(u.id)) unresolved.set(u.id, []); + unresolved.get(u.id).push(m); + } + } + if (unresolved.size === 0) return; + + // teams.cloud.microsoft profile endpoint accepts a small batch of userIds via + // a usersInfo query param. We chunk into groups of 20 to stay polite. + const ids = [...unresolved.keys()]; + const chunkSize = 20; + // Derive a thread context required by the endpoint. Use the channel id itself. + const threadId = channelId; + const base = `https://teams.cloud.microsoft/api/mt/part/msft/beta/users/me/threads/` + + `${encodeURIComponent(threadId)}/properties/pictureV2`; + for (let i = 0; i < ids.length; i += chunkSize) { + const chunk = ids.slice(i, i + chunkSize); + const usersInfo = chunk.map((g) => ({ + userId: `8:orgid:${g}`, + displayName: "", + avatarETag: "", + })); + const url = `${base}?usersInfo=${encodeURIComponent(JSON.stringify(usersInfo))}` + + `&size=HR64x64`; + try { + const res = await fetchPage(cache, url); + if (!res.ok) continue; + const data = await res.json(); + // Response shape varies; look for any displayName fields keyed by user id. + const flatten = JSON.stringify(data); + for (const g of chunk) { + // Heuristic: find `"displayName":"X"` near the user GUID in the response. + const re = new RegExp( + `"userId"\\s*:\\s*"8:orgid:${g}"[^}]*?"displayName"\\s*:\\s*"([^"]+)"` + + `|"displayName"\\s*:\\s*"([^"]+)"[^}]*?"userId"\\s*:\\s*"8:orgid:${g}"`, + "i", + ); + const m = re.exec(flatten); + const name = m ? (m[1] || m[2]) : null; + if (name) { + for (const msg of unresolved.get(g)) { + msg.from.user.displayName = name; + } + } + } + } catch { /* best-effort */ } + } + // For any still-unresolved, fall back to a short form so output isn't empty. + for (const [g, msgs] of unresolved) { + for (const m of msgs) { + if (!m.from.user.displayName) m.from.user.displayName = `User ${g.slice(0, 8)}`; + } + } +} + + +// /messages?$expand=replies shape. Replies are sorted oldest-first within each +// root; roots remain in the order the chat service returned them (newest first). +function groupReplies(flat) { + const byId = new Map(); + for (const m of flat) byId.set(m.id, m); + const roots = []; + const orphans = []; + for (const m of flat) { + if (!m.replyToId) { + m.replies = []; + roots.push(m); + } + } + for (const m of flat) { + if (!m.replyToId) continue; + const parent = byId.get(m.replyToId); + if (parent) { + parent.replies = parent.replies || []; + parent.replies.push(m); + } else { + // Parent fell outside our window. Promote the reply to a root so it isn't + // silently dropped; flag it so the renderer can mark it as a fragment. + m.replies = []; + m.isOrphanReply = true; + orphans.push(m); + } + } + for (const r of roots) { + r.replies.sort((a, b) => Date.parse(a.createdDateTime) - Date.parse(b.createdDateTime)); + } + // Filter out non-message system events (member adds, topic changes, etc.) at + // the root level — keep them only if they're replies the user might want context for. + const isContent = (m) => { + const t = (m.messageType || "").toLowerCase(); + return !t || t.startsWith("text") || t.startsWith("richtext"); + }; + return [...roots.filter(isContent), ...orphans.filter(isContent)]; +} + +async function fetchMessages(cache, { teamId, channelId, limit }) { + let url = `${cache.endpoint}?pageSize=50`; + if (cache.endpoint.includes("/api/csa/")) { + // CSA endpoint already references the channel via path; nothing to add. + } else { + // ng.msg endpoint — startTime narrows server-side. Teams chatsvc expects + // epoch milliseconds, not ISO strings. + if (args.since) url += `&startTime=${sinceMs}`; + } + + const collected = []; + const collectedIds = new Set(); + // Roots that pass the window filter — used to know when we can stop. Replies + // for those roots can appear later in pagination even if their own timestamp + // sits outside the requested window, so we keep collecting until we run out + // of pages or until ALL their replies have been seen. + const rootIdsInWindow = new Set(); + let pageCount = 0; + let stopOnNextPage = false; + + while (url) { + pageCount++; + let res = await fetchPage(cache, url); + if (res.status === 401) { + process.stderr.write("Token expired mid-fetch; refreshing...\n"); + const fresh = await captureToken({ teamId, channelId, headed: args.headed }); + saveCache(fresh); + cache = fresh; + res = await fetchPage(cache, url); + } + if (!res.ok) { + const body = await res.text().catch(() => ""); + throw new Error(`HTTP ${res.status} for ${url}\n${body.slice(0, 500)}`); + } + const data = await res.json(); + const items = (data.value || data.messages || []).map(normalize); + let sawInWindowOnThisPage = false; + for (const m of items) { + if (collectedIds.has(m.id)) continue; + const inWindow = isInWindow(m); + const isReplyToTrackedRoot = m.replyToId && rootIdsInWindow.has(m.replyToId); + if (!inWindow && !isReplyToTrackedRoot) continue; + collected.push(m); + collectedIds.add(m.id); + if (!m.replyToId) rootIdsInWindow.add(m.id); + if (inWindow) sawInWindowOnThisPage = true; + if (limit && collected.filter((x) => !x.replyToId).length >= limit) { + stopOnNextPage = true; + } + } + // Heuristic stop: once a full page brought zero in-window items AND we have + // enough roots, assume earlier pages won't help. + if (stopOnNextPage && !sawInWindowOnThisPage) break; + url = data["@odata.nextLink"] || data._metadata?.syncState || data.backwardLink || null; + if (pageCount > 200) break; // hard safety stop + } + return args.includeReplies === false ? collected.filter((m) => !m.replyToId) : collected; +} + +// --- main ------------------------------------------------------------------ +(async () => { + let cache = args.refreshToken ? null : loadCache(); + if (!cache) { + process.stderr.write(`Capturing Teams web token (profile: ${PROFILE_DIR})...\n`); + if (!args.headed) { + process.stderr.write("Running headless. If sign-in is required, re-run with --headed.\n"); + } + try { + cache = await captureToken({ teamId: args.teamId, channelId: args.channelId, headed: args.headed }); + } catch (e) { + process.stderr.write(`Token capture failed: ${e.message}\n`); + process.exit(3); + } + saveCache(cache); + } + + try { + const flat = await fetchMessages(cache, args); + await resolveDisplayNames(cache, flat, { teamId: args.teamId, channelId: args.channelId }); + const grouped = args.includeReplies === false + ? flat.map((m) => ({ ...m, replies: [] })) + : groupReplies(flat); + const rootCount = grouped.length; + const replyCount = grouped.reduce((n, r) => n + (r.replies?.length || 0), 0); + process.stdout.write(JSON.stringify({ + value: grouped, + capturedAt: cache.capturedAt, + endpoint: cache.endpoint, + count: rootCount, + replyCount, + }, null, 2)); + process.stdout.write("\n"); + } catch (e) { + process.stderr.write(`Fetch failed: ${e.message}\n`); + process.exit(4); + } +})(); diff --git a/test-driven-development/SKILL.md b/test-driven-development/SKILL.md new file mode 100644 index 0000000..a0271b4 --- /dev/null +++ b/test-driven-development/SKILL.md @@ -0,0 +1,51 @@ +--- +name: "test-driven-development" +description: "Write the failing test before the implementation, for every feature or bugfix - red, green, refactor. Use before writing any implementation code, and when fixing a bug (reproduce it as a failing test first). Covers what makes a good test, why the order matters, the rationalizations that mean you are skipping it, and a verification checklist before marking work complete. Triggers: add a feature, fix this bug, implement X, write tests, TDD." +--- + +Write the test first. Watch it fail. Write minimal code to pass. + +**Core principle:** if you didn't watch the test fail, you don't know if it tests the right thing. + +**The Iron Law: NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST.** Wrote code before the test? Delete it — completely, don't keep it "as reference" — and start over from tests. + +Always use for: new features, bug fixes, refactoring, behavior changes. Exceptions (ask the user first): throwaway prototypes, generated code, configuration files. + +## Red-Green-Refactor Cycle + +**RED — Write a failing test.** One minimal test showing what should happen. One behavior, clear name, real code (avoid mocks unless unavoidable). + +**Verify RED — watch it fail (mandatory, never skip).** Run just that test. Confirm: it fails (not errors), the failure message is what you'd expect, and it fails because the feature is missing — not because of a typo. If it passes, you're testing existing behavior — fix the test. If it errors, fix the error and re-run until it fails correctly. + +**GREEN — write the minimal code to pass.** Don't add extra features, options, or "improvements" beyond what the test requires (YAGNI). + +**Verify GREEN — watch it pass (mandatory).** Run the test — and the full suite. Confirm it passes, no other tests broke, and output is clean (no errors/warnings). If it fails, fix the code, not the test. + +**REFACTOR — clean up while green.** Remove duplication, improve names, extract helpers. Don't add new behavior. Keep tests green throughout. + +**Repeat** for the next behavior. + +## Good Tests +Minimal (one thing — "and" in the name means split it), clear (name describes behavior, not "test1"), and shows intent (demonstrates the desired API). + +## Why Order Matters +Tests written after code pass immediately — which proves nothing (might test the wrong thing, might test implementation not behavior, might miss edge cases you forgot). Test-first forces you to watch it fail, proving it actually tests something. Manual testing is ad-hoc — no record, can't re-run, easy to forget cases under pressure. + +## Common Rationalizations (all mean: stop, do TDD properly) +"Too simple to test" / "I'll test after" / "Already manually tested" / "Deleting X hours of work is wasteful" (sunk cost — keeping unverified code is technical debt) / "Keep as reference, write tests first" (you'll adapt it — that's testing after) / "Need to explore first" (fine — throw away exploration, start clean with TDD) / "Test is hard to write = design is unclear, but I'll push through anyway" (listen to the test) / "TDD will slow me down" (it's faster than debugging after). + +## Verification Checklist (before marking work complete) +- Every new function/method has a test +- You watched each test fail before implementing, for the expected reason +- Wrote minimal code to pass each test +- All tests pass, output pristine +- Tests use real code (mocks only if unavoidable) +- Edge cases and errors covered + +Can't check all boxes? You skipped TDD — start over. + +## Debugging Integration +Bug found? Write a failing test reproducing it first, then follow the Red-Green-Refactor cycle. The test proves the fix and prevents regression. Never fix bugs without a test. + +## Final Rule +Production code → a test exists and failed first. Otherwise, it's not TDD. No exceptions without explicit user permission. diff --git a/using-git-worktrees/SKILL.md b/using-git-worktrees/SKILL.md new file mode 100644 index 0000000..08f6427 --- /dev/null +++ b/using-git-worktrees/SKILL.md @@ -0,0 +1,57 @@ +--- +name: "using-git-worktrees" +description: "Ensure feature work happens in an isolated workspace before it starts - detects existing isolation, creates a git worktree or native isolated workspace, sets up the project, and verifies a clean baseline. Use before executing an implementation plan or starting anything that should not disturb the current working tree. Triggers: start a new feature, work on this separately, set up a branch for this, isolate this work, spin up a worktree." +--- + +Ensure work happens in an isolated workspace. Prefer this environment's native worktree/branch tools if any exist. Fall back to manual git worktrees only when no native tool is available. + +**Core principle:** detect existing isolation first, then use native tools, then fall back to git. Never fight the harness. + +Announce at start: "I'm using the using-git-worktrees skill to set up an isolated workspace." + +## Step 0: Detect Existing Isolation +Before creating anything, check whether you're already in an isolated workspace: +``` +GIT_DIR=$(cd "$(git rev-parse --git-dir)" 2>/dev/null && pwd -P) +GIT_COMMON=$(cd "$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P) +BRANCH=$(git branch --show-current) +``` +Submodule guard: `GIT_DIR != GIT_COMMON` is also true inside git submodules. Before concluding "already in a worktree," check `git rev-parse --show-superproject-working-tree` — if it returns a path, you're in a submodule, treat as a normal repo. + +If `GIT_DIR != GIT_COMMON` (and not a submodule): you're already in a linked worktree — skip to Step 2. Report: "Already in isolated workspace at `<path>` on branch `<name>`" (or note detached HEAD, externally managed, if applicable). + +If `GIT_DIR == GIT_COMMON` (or in a submodule): normal repo checkout. If the user hasn't already stated a worktree preference, ask for consent: +> "Would you like me to set up an isolated worktree? It protects your current branch from changes." +If declined, work in place and skip to Step 2. + +## Step 1: Create Isolated Workspace +**1a. Native tools (preferred):** if this environment already provides a way to create an isolated workspace/worktree (a tool, command, or flag), use it and skip to Step 2. Native tools handle directory placement, branch creation, and cleanup automatically — using raw `git worktree add` when a native tool exists creates phantom state the harness can't see or manage. + +**1b. Git worktree fallback (only if no native tool exists):** +Directory selection priority: 1) any directory preference the user has already stated, 2) an existing project-local worktree directory (`.worktrees` preferred over `worktrees` if both exist), 3) default to `.worktrees/` at the project root. +Safety check: `git check-ignore -q .worktrees` (or `worktrees`) — if NOT ignored, add to `.gitignore` and commit that change first. +Create it: +``` +path="$LOCATION/$BRANCH_NAME" +git worktree add "$path" -b "$BRANCH_NAME" +cd "$path" +``` +Sandbox fallback: if `git worktree add` fails with a permission error, tell the user the sandbox blocked worktree creation and that you're working in the current directory instead; run setup/baseline tests in place. + +## Step 2: Project Setup +Auto-detect and run appropriate install: `npm install` (package.json), `cargo build` (Cargo.toml), `pip install -r requirements.txt` / `poetry install` (Python), `go mod download` (go.mod). + +## Step 3: Verify Clean Baseline +Run the project's tests. If they fail, report failures and ask whether to proceed or investigate first. If they pass, report ready: +``` +Worktree ready at <full-path> +Tests passing (<N> tests, 0 failures) +Ready to implement <feature-name> +``` + +## Common Mistakes +Fighting the harness (using raw git worktree when native isolation exists). Skipping detection (creating a nested worktree inside an existing one). Skipping ignore verification (worktree contents get tracked). Assuming directory location instead of following the priority order. Proceeding with failing tests without asking. + +## Red Flags +Never: create a worktree when Step 0 already detects isolation, use raw git worktree commands when a native tool is available, skip straight to 1b without checking 1a, create a project-local worktree without verifying it's gitignored, skip baseline test verification, proceed with failing tests without asking. +Always: run Step 0 detection first, prefer native tools, follow the directory priority order, verify gitignore for project-local worktrees, auto-detect and run project setup, verify a clean test baseline. diff --git a/verification-before-completion/SKILL.md b/verification-before-completion/SKILL.md new file mode 100644 index 0000000..2109d99 --- /dev/null +++ b/verification-before-completion/SKILL.md @@ -0,0 +1,44 @@ +--- +name: "verification-before-completion" +description: "Gate any completion claim behind actual evidence - run the build, tests, or linter and read the output before saying something is done, fixed, passing, or ready. Use right before committing, opening a PR, marking a task complete, or telling the user it works. Covers the common failure modes and the red flags that mean you are about to claim success you have not verified. Triggers: is it done, are we finished, ship it, mark it complete." +--- + +Claiming work is complete without verification is dishonesty, not efficiency. + +**Core principle:** evidence before claims, always. + +**The Iron Law: NO COMPLETION CLAIMS WITHOUT FRESH VERIFICATION EVIDENCE.** If you haven't run the verification command in this message/turn, you cannot claim it passes. + +## The Gate Function +Before claiming any status or expressing satisfaction: +1. Identify what command proves this claim +2. Run the FULL command (fresh, complete — not a partial check or extrapolation) +3. Read the full output, check exit code, count failures +4. Does the output confirm the claim? If no, state actual status with evidence. If yes, state the claim WITH the evidence. +5. Only then make the claim. + +Skipping any step is lying, not verifying. + +## Common Failures and What's Actually Required +| Claim | Requires | NOT sufficient | +|---|---|---| +| Tests pass | fresh test command output, 0 failures | previous run, "should pass" | +| Linter clean | linter output, 0 errors | partial check | +| Build succeeds | build command, exit 0 | linter passing, logs "look good" | +| Bug fixed | test of the original symptom passes | code changed, assumed fixed | +| Regression test works | red-green cycle actually verified | test passes once | +| Agent/subagent completed | VCS diff shows the actual changes | agent's self-report of "success" | +| Requirements met | line-by-line checklist against spec | tests passing alone | + +## Red Flags — STOP +Using "should"/"probably"/"seems to". Expressing satisfaction ("Great!", "Perfect!", "Done!") before verification. About to commit/push/PR without verification. Trusting a subagent's success report without checking its diff. Relying on partial verification. "Just this once." Being tired and wanting it over. + +## Key Patterns +- Tests: run the command, see the actual pass count, THEN say "all tests pass." Not "should pass now." +- Regression tests: write → run (pass) → revert the fix → run (MUST fail) → restore fix → run (pass). Anything less isn't verified. +- Build: run the build, see exit 0. A passing linter is not proof the build compiles. +- Requirements: re-read the plan/spec, build a checklist, verify each item, report gaps or completion — not just "tests pass, phase complete." +- Subagent delegation: subagent reports success → check the actual VCS diff → verify the changes are real and correct → then report actual state. + +## When To Apply +Always before: any success/completion claim, any expression of satisfaction, any positive statement about work state, committing, PR creation, task completion, moving to the next task, or trusting a delegated agent's report. Applies to exact phrases, paraphrases, and implications of success — not just the literal words. diff --git a/writing-plans/SKILL.md b/writing-plans/SKILL.md new file mode 100644 index 0000000..c583f84 --- /dev/null +++ b/writing-plans/SKILL.md @@ -0,0 +1,87 @@ +--- +name: "writing-plans" +description: "Turn a spec or set of requirements into a written, bite-sized implementation plan BEFORE touching code - scope check, file structure, right-sized tasks with no placeholders, global constraints, and a self-review pass, ending in a handoff for execution. Use for any multi-step or multi-file task. Triggers: plan this out, write a plan, how would you approach this, break this down, what is the implementation plan, roadmap this." +--- + +Write comprehensive implementation plans assuming the engineer has zero context for the codebase and questionable taste. Document everything they need: which files to touch for each task, code, testing, docs they might need, how to test it. Give them the whole plan as bite-sized tasks. DRY. YAGNI. TDD. Frequent commits. + +Assume they are a skilled developer who knows almost nothing about the toolset or problem domain, and doesn't know good test design well. + +Announce at start: "I'm using the writing-plans skill to create the implementation plan." + +**Save plans to:** `docs/implementationplans/YYYY-MM-DD-<feature-name>.md` (unless the repo already uses a different convention, or the user has a different preference — an existing convention in the repo wins). + +## Scope Check +If the spec covers multiple independent subsystems, it should have been broken into sub-project specs during brainstorming. If not, suggest splitting into separate plans — one per subsystem, each producing working, testable software on its own. + +## File Structure +Before defining tasks, map out which files will be created/modified and what each is responsible for. +- Design units with clear boundaries and well-defined interfaces; one clear responsibility per file. +- Prefer smaller, focused files — you reason best about code you can hold in context at once. +- Files that change together should live together. Split by responsibility, not technical layer. +- In existing codebases, follow established patterns; don't unilaterally restructure, but if a file you're modifying has grown unwieldy, including a split in the plan is reasonable. + +## Task Right-Sizing +A task is the smallest unit that carries its own test cycle and is worth a fresh reviewer's gate. Fold setup/config/scaffolding/docs into the task whose deliverable needs them; split only where a reviewer could meaningfully reject one task while approving its neighbor. Each task ends with an independently testable deliverable. + +## Bite-Sized Task Granularity +Each step is one action (2-5 minutes): "Write the failing test" / "Run it to confirm it fails" / "Implement minimal code to pass" / "Run tests to confirm pass" / "Commit". + +## Plan Document Header +Every plan MUST start with: + +```markdown +# [Feature Name] Implementation Plan + +> **For agentic workers:** Use the subagent-driven-development skill (recommended) or the executing-plans skill to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** [One sentence] +**Architecture:** [2-3 sentences about approach] +**Tech Stack:** [Key technologies/libraries] + +## Global Constraints +[Project-wide requirements — version floors, dependency limits, naming/copy rules, platform requirements — one line each, exact values from the spec. Every task's requirements implicitly include this section.] +--- +``` + +## Task Structure +```markdown +### Task N: [Component Name] + +**Files:** +- Create: `exact/path/to/file.py` +- Modify: `exact/path/to/existing.py:123-145` +- Test: `tests/exact/path/to/test.py` + +**Interfaces:** +- Consumes: [what this task uses from earlier tasks — exact signatures] +- Produces: [exact function names, parameter/return types later tasks rely on] + +- [ ] Step 1: Write the failing test (show the actual test code) +- [ ] Step 2: Run test to verify it fails (exact command + expected failure output) +- [ ] Step 3: Write minimal implementation (show the actual code) +- [ ] Step 4: Run test to verify it passes (exact command + expected output) +- [ ] Step 5: Commit (exact git commands + message) +``` + +## No Placeholders +Never write: "TBD"/"TODO"/"implement later", "add appropriate error handling", "write tests for the above" (without actual code), "similar to Task N" (repeat the code instead), steps describing what without how, or references to undefined types/functions. + +## Remember +Exact file paths always. Complete code in every step. Exact commands with expected output. DRY, YAGNI, TDD, frequent commits. + +## Self-Review (after writing the complete plan) +1. **Spec coverage:** does every spec requirement map to a task? List gaps. +2. **Placeholder scan:** search for the red flags above, fix them. +3. **Type consistency:** do types/signatures/names match across tasks? (e.g. `clearLayers()` in Task 3 vs `clearFullLayers()` in Task 7 is a bug.) +Fix issues inline, no need to re-review. + +## Execution Handoff +After saving the plan, offer: +> "Plan complete and saved to `docs/implementationplans/<filename>.md`. Two execution options: +> **1. Subagent-Driven (recommended)** — I dispatch a fresh subagent per task, review between tasks, fast iteration. +> **2. Inline Execution** — Execute tasks in this session using executing-plans, batch execution with checkpoints. +> Which approach?" + +If Subagent-Driven: use the subagent-driven-development skill. +If Inline: use the executing-plans skill. diff --git a/writing-skills/SKILL.md b/writing-skills/SKILL.md new file mode 100644 index 0000000..e52eff7 --- /dev/null +++ b/writing-skills/SKILL.md @@ -0,0 +1,52 @@ +--- +name: "writing-skills" +description: "Create, edit, test, or debug agent skills - SKILL.md frontmatter and body structure, when a skill is warranted, skill types, discovery optimization (the description is the only part always loaded, and it is hard-capped at 500 characters), flowchart usage, and common mistakes. Use whenever authoring or revising a skill, or when a skill is not triggering. Triggers: create a skill, write a skill, edit this skill, my skill is not firing, improve the skill description." +--- + +Writing skills IS test-driven development applied to process documentation. You write test cases (pressure scenarios), watch them fail (baseline behavior without the skill), write the skill (documentation), watch tests pass (agents comply), and refactor (close loopholes). + +**Core principle:** if you didn't watch an agent fail without the skill, you don't know if the skill teaches the right thing. + +**Background:** understand the test-driven-development skill first — this skill adapts red-green-refactor to documentation. + +## What Is a Skill? +A reference guide for a proven technique, pattern, or tool — reusable, not a narrative about how you solved a problem once. + +## TDD Mapping for Skills +| TDD Concept | Skill Creation | +|---|---| +| Test case | Pressure scenario with a subagent | +| Production code | The skill document | +| Test fails (RED) | Agent violates the rule without the skill (baseline) | +| Test passes (GREEN) | Agent complies with the skill present | +| Refactor | Close loopholes while maintaining compliance | + +## When to Create a Skill +Create when: the technique wasn't intuitively obvious to you, you'd reference it again across projects, the pattern applies broadly (not project-specific), others would benefit. +Don't create for: one-off solutions, standard practices already well-documented elsewhere, project-specific conventions (put those in project instructions instead), or mechanical constraints enforceable by regex/validation (automate those, save docs for judgment calls). + +## Skill Types +Technique (concrete method with steps), Pattern (way of thinking about problems), Reference (API docs/syntax guides). + +## SKILL.md Structure (frontmatter + body) +Frontmatter: `name` (letters/numbers/hyphens only) and `description` (third-person, describes ONLY when to use — start with "Use when...", never summarize the workflow itself). + +**Critical: description = when to use, NOT what the skill does.** If the description summarizes the workflow, agents may follow the description as a shortcut instead of reading the full skill and doing the complete process. Keep descriptions under ~500 characters, focused purely on triggering conditions/symptoms. + +Body sections: Overview (what + core principle in 1-2 sentences), When to Use (bullets with concrete symptoms; small inline flowchart only for genuinely non-obvious decision points), Core Pattern (before/after comparison for techniques), Quick Reference (table for scanning), Implementation (inline for simple patterns, link out for heavy reference), Common Mistakes, Real-World Impact (optional). + +## Discovery Optimization +- Rich, third-person description starting with "Use when..." +- Keyword coverage: include error messages, symptoms, synonyms, and actual tool/command names an agent would search for +- Descriptive, active naming: verb-first / gerund style (`condition-based-waiting` not `async-test-helpers`, `creating-skills` not `skill-creation`) +- Token efficiency: keep frequently-loaded skills concise; move verbose details to references or --help output rather than inline; use cross-references instead of repeating another skill's content +- Cross-reference other skills by name with explicit markers like "REQUIRED SUB-SKILL: use X" or "REQUIRED BACKGROUND: understand X" rather than vague "see" links + +## Flowchart Usage +Use small inline flowcharts ONLY for non-obvious decision points or "when to use A vs B" choices. Never for reference material (use tables/lists), code examples (use code blocks), or linear instructions (use numbered lists). + +## Code Examples +One excellent, complete, well-commented, realistic example beats several mediocre ones. Choose the most relevant language for the domain. + +## Common Mistakes +Writing a skill as a narrative of one past incident instead of a reusable reference. Descriptions that summarize the workflow (agents skip the actual body). Bloated frequently-loaded skills that burn context on every conversation. Vague or missing triggering conditions that make the skill undiscoverable.