kushi-agents 3.4.2 → 3.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/.github/copilot-instructions.kushi.md +38 -0
  2. package/README.md +33 -0
  3. package/bin/cli.mjs +2 -0
  4. package/package.json +17 -4
  5. package/plugin/agents/kushi.agent.md +155 -147
  6. package/plugin/instructions/ado-bootstrap-discovery.instructions.md +111 -0
  7. package/plugin/instructions/ado-engagement-tree.instructions.md +73 -0
  8. package/plugin/instructions/answer-from-evidence.instructions.md +1 -1
  9. package/plugin/instructions/auth-and-retry.instructions.md +51 -16
  10. package/plugin/instructions/azure-auth-patterns.instructions.md +13 -6
  11. package/plugin/instructions/bootstrap-status-format.instructions.md +113 -0
  12. package/plugin/instructions/capture-learnings.instructions.md +95 -0
  13. package/plugin/instructions/cleanup-on-resolution.instructions.md +69 -0
  14. package/plugin/instructions/crm-bootstrap-discovery.instructions.md +79 -0
  15. package/plugin/instructions/crm-internal-vs-confirmed.instructions.md +79 -0
  16. package/plugin/instructions/evidence-confidence-ladder.instructions.md +66 -0
  17. package/plugin/instructions/evidence-layout-canonical.instructions.md +115 -0
  18. package/plugin/instructions/evidence-thoroughness.instructions.md +82 -12
  19. package/plugin/instructions/full-view-gate.instructions.md +91 -0
  20. package/plugin/instructions/m365-id-registry.instructions.md +134 -0
  21. package/plugin/instructions/meetings-verbatim-required.instructions.md +176 -0
  22. package/plugin/instructions/run-reports.instructions.md +129 -0
  23. package/plugin/instructions/scope-boundaries.instructions.md +218 -0
  24. package/plugin/instructions/snapshot-vs-stream.instructions.md +2 -0
  25. package/plugin/instructions/update-ledger.instructions.md +132 -0
  26. package/plugin/instructions/verbatim-by-default.instructions.md +73 -0
  27. package/plugin/instructions/workiq-first.instructions.md +15 -31
  28. package/plugin/instructions/workiq-only.instructions.md +193 -0
  29. package/plugin/learnings/README.md +50 -0
  30. package/plugin/learnings/ado.md +45 -0
  31. package/plugin/learnings/crm.md +96 -0
  32. package/plugin/learnings/cross-cutting.md +36 -0
  33. package/plugin/learnings/email.md +33 -0
  34. package/plugin/learnings/meetings.md +30 -0
  35. package/plugin/learnings/misc.md +46 -0
  36. package/plugin/learnings/onenote.md +215 -0
  37. package/plugin/learnings/sharepoint.md +5 -0
  38. package/plugin/learnings/teams.md +5 -0
  39. package/plugin/plugin.json +22 -2
  40. package/plugin/prompts/apply-ado.prompt.md +14 -0
  41. package/plugin/prompts/propose-ado.prompt.md +12 -0
  42. package/plugin/reference-packs/fde/crm-field-manifest.md +165 -0
  43. package/plugin/skills/apply-ado-update/SKILL.md +125 -0
  44. package/plugin/skills/ask-project/SKILL.md +2 -0
  45. package/plugin/skills/bootstrap-project/SKILL.md +81 -3
  46. package/plugin/skills/propose-ado-update/SKILL.md +108 -0
  47. package/plugin/skills/pull-ado/SKILL.md +173 -23
  48. package/plugin/skills/pull-crm/SKILL.md +168 -15
  49. package/plugin/skills/pull-email/SKILL.md +139 -22
  50. package/plugin/skills/pull-meetings/SKILL.md +109 -25
  51. package/plugin/skills/pull-misc/README.md +84 -0
  52. package/plugin/skills/pull-misc/SKILL.md +257 -0
  53. package/plugin/skills/pull-misc/runner.mjs +280 -0
  54. package/plugin/skills/pull-onenote/README.md +90 -0
  55. package/plugin/skills/pull-onenote/SKILL.md +400 -51
  56. package/plugin/skills/pull-onenote/runner.mjs +356 -0
  57. package/plugin/skills/pull-onenote/scripts/recapture-section-url.mjs +295 -0
  58. package/plugin/skills/pull-onenote/write-snapshot.mjs +271 -0
  59. package/plugin/skills/pull-sharepoint/SKILL.md +44 -12
  60. package/plugin/skills/pull-teams/SKILL.md +40 -11
  61. package/plugin/skills/refresh-project/SKILL.md +33 -2
  62. package/plugin/skills/self-check/run.ps1 +186 -4
  63. package/plugin/templates/ado-update/discussion-comment.template.md +26 -0
  64. package/plugin/templates/ado-update/integrations-ado-writes.example.yml +49 -0
  65. package/plugin/templates/ado-update/proposed.template.md +78 -0
  66. package/plugin/templates/init/external-links.template.txt +30 -0
  67. package/plugin/templates/init/project-integrations.template.yml +57 -2
  68. package/plugin/templates/snapshot/meeting-verbatim.template.md +110 -0
  69. package/plugin/templates/snapshot/meetings-series-index.template.md +3 -1
  70. package/plugin/templates/snapshot/onenote-page.template.md +92 -23
  71. package/plugin/templates/weekly/meetings-stream.template.md +11 -6
  72. package/src/copilot-instructions.mjs +80 -0
  73. package/src/main.mjs +18 -1
@@ -0,0 +1,215 @@
1
+ # Learnings — OneNote (`pull-onenote`)
2
+
3
+ Newest on top. Format defined in [`README.md`](./README.md).
4
+
5
+ _(no entries yet — append the moment a fix lands during a `pull-onenote` run)_
6
+
7
+
8
+ ## 2026-05-14 — Pre-flight gate: distinguish notebook-unavailable from auth-required
9
+
10
+ **Trigger:** AGCO refresh, immediately after v3.10.2 (URL synthesis killed). Even with a freshly user-pasted `one_sectionWebUrl` and a valid Edge profile, the runner returned `auth-required`. Manual sanity-check: the user opened `https://onenote.cloud.microsoft/` directly in their browser and got the **"Sorry, we ran into a problem"** dialog at the notebook-list level — before they could even click into the AGCO section. So the runner couldn't possibly succeed; the failure was OneNote-for-Web side, not auth.
11
+
12
+ **Root cause classification was overloaded.** The runner only knew two end-states: "canvas frame attached → success" and "didn't attach → auth-required". But OneNote-for-Web has at least three failure surfaces:
13
+ 1. **Login redirect** (`login.microsoftonline.com`) — genuine auth-required.
14
+ 2. **Service/notebook error dialog** ("Sorry, we ran into a problem", "We couldn't open", "This notebook can't be opened", "There was a problem") — service- or notebook-side. Auth is fine.
15
+ 3. **Silent timeout** (no chrome, no error, no redirect) — usually network or extreme service degradation.
16
+
17
+ Conflating #2 with #1 sends the operator down the wrong recovery path: re-bootstrap auth when the real fix is to recover the notebook (open in OneNote desktop, force sync, wait, or re-capture the section URL).
18
+
19
+ **Fix (v3.11.0):**
20
+
21
+ - New `preflightOneNoteWeb()` in `runner.mjs` runs BEFORE navigating to any section URL. Probes `https://onenote.cloud.microsoft/` and classifies the end-state into `ok` / `auth-required` / `onenote-web-unavailable`. The pre-flight is also exposed as a standalone CLI mode (`--preflight`) for gate drivers.
22
+ - The canvas-attach wait loop now ALSO scans every frame's body for the same error-dialog patterns each tick, so a section-load error (e.g. moved section, sync drift) is caught ~500ms after the dialog appears, not after `TIMEOUT_MS`.
23
+ - New `runStatus: "notebook-unavailable"` is emitted for #2 — distinct from `auth-required` — and the runner's error message includes a verbatim recovery checklist (hard-refresh; open in OneNote desktop; wait 10–15min; re-capture URL).
24
+
25
+ **Doctrinal lesson codified into `pull-onenote/SKILL.md` Pre-flight A.4:** the three-way classification IS the contract; the runner MUST distinguish all three end-states; auto-retry is allowed for `auth-required` (next refresh) but FORBIDDEN for `notebook-unavailable` — that one needs human/notebook-side recovery first.
26
+
27
+ **Validation marker:** when AGCO's notebook becomes available again in OneNote-for-Web, re-run kushi pull and confirm the new runStatus values round-trip through to refresh-reports. (Pending — notebook still showing the dialog as of write time.)
28
+
29
+
30
+ ## 2026-05-14 — Conditional Access requires Edge; cookie domains don't transfer; URL must be canonical
31
+
32
+ **Trigger:** Pulling AGCO OneNote evidence for the first time in this session. Bootstrap appeared to succeed (no errors), but every subsequent headless run returned `auth-required`. Re-bootstrapping multiple times did not help.
33
+
34
+ **Three independent root causes had stacked, each masking the next:**
35
+
36
+ 1. **Wrong URL formula (fixed in v3.10.0).** AGCO's pre-doctrine registry entry had a synthesized URL using `wd=target(/<name>/)` (no fileId, no pipe). The canonical formula is `wd=target(<sectionName>|<sectionFileId>/)`. Wrong formula → OneNote-for-Web silently shows "Sorry, we ran into a problem" — indistinguishable from auth failure to a headless runner. Fixed by `recapture-section-url.mjs` auto-heal from sibling registry entries (HCA shares the same notebook, so its `notebookSourceDoc` + `spoBaseUrl` were inherited).
37
+
38
+ 2. **Cookie-domain isolation (fixed in v3.10.1).** Bootstrap was navigating only to `https://onenote.cloud.microsoft/`. But the canonical Doc.aspx URLs live on `https://microsoft-my.sharepoint-df.com/personal/<upn>/...`. These are SEPARATE cookie domains — signing into one does NOT authenticate the other. After `--bootstrap` succeeded, headless runs against the SPO URL still hit `login.microsoftonline.com` because SPO had no cookies. Fixed by making `--bootstrap` walk: `onenote.cloud.microsoft` → `microsoft-my.sharepoint.com` → `microsoft-my.sharepoint-df.com` in the same session, so cookies plant on all three.
39
+
40
+ 3. **Conditional Access blocks vanilla Chromium (fixed in v3.10.1).** Even after fixing the cookie-domain issue, the SPO sign-in surface returned a hard block: **"You can't get there from here — this application contains sensitive information and can only be accessed from devices or client applications that meet Microsoft management compliance policy. Since you're using Chrome... Alternatively, you can use Microsoft Edge or Internet Explorer."** Playwright's bundled Chromium is not Intune-managed. Fixed by switching `runner.mjs` to `chromium.launchPersistentContext(profile, { channel: 'msedge', ... })` — Playwright drives the user's installed Edge, which IS Intune-trusted.
41
+
42
+ **Doctrinal lessons codified into `pull-onenote/SKILL.md` Pre-flight A.1/A.2/A.3:**
43
+
44
+ - **A.1** — `channel: 'msedge'` is HARD-required. Vanilla Chromium WILL be CA-blocked in Microsoft tenants. Do not fall back to Chromium.
45
+ - **A.2** — Bootstrap MUST visit both `onenote.cloud.microsoft` AND both `sharepoint.com` + `sharepoint-df.com` to plant cookies in all required domains. Single-host bootstrap is broken by design.
46
+ - **A.3** — When switching browser channel (Chromium ↔ Edge), DELETE the existing profile first; cookie/cache formats are not compatible.
47
+
48
+ **The "auth-required" signal is overloaded.** It can mean: (a) genuine cookie expiry, (b) wrong URL formula returning OneNote's "Sorry" dialog, (c) cookies present for one domain but not the navigation target's domain, (d) Conditional Access blocking the browser channel. The runner cannot distinguish these — bootstrap-then-retest is the only protocol. v3.10.1 closes (b)/(c)/(d) as systemic failure modes; only (a) should remain after this release.
49
+
50
+ **Validation:** AGCO bootstrap completed cleanly with Edge + dual-surface walk on 2026-05-14. Profile size shrank from 27MB (Chromium) to ~8MB (Edge, just cookies after fresh start). Sign-in walked through MFA on `onenote.cloud.microsoft` and silently SSO'd on the SPO hosts.
51
+
52
+
53
+ ## 2026-05-13 — WorkIQ has TWO answer modes; index field names are the contract
54
+
55
+ **Trigger:** After shipping v3.7.7 (no-fabrication rule), user disputed the claim that OneNote bodies cannot be retrieved and pasted a historical `m365-mutable.json` shape that used `one_sectionFileId`, `one_sectionPath`, `one_sectionOneNoteGuid`, citing a prior project-evidence flow that successfully pulled OneNote pages.
56
+
57
+ **Root cause:** WorkIQ has two answer paths:
58
+ 1. **Search-index extractor** — triggers when the query body contains the literal M365 search index field names (`wdsectionfileid`, `wdsectiongroupid`, `wdpartid`, `wdsectiononenoteguid`). Returns the indexed body fragment verbatim.
59
+ 2. **LLM-summary path** — triggers when the query uses prose like "the page titled X" or "page id Y" or "the OneNote section called Z". Synthesizes a summary from titles + adjacent evidence. This is what v2.2.0 and v2.3.0 hit.
60
+
61
+ The first eight `pull-onenote` versions used prose phrasing (`"sectionFileId <id>"`, `"pageId <pageId>"`) — this is mechanically different from `wdsectionfileid = <id>` / `wdpartid = <id>`. The previous project-evidence flow used the index field names verbatim, which is why it worked.
62
+
63
+ **Fix shipped (v2.4.0 of pull-onenote):**
64
+ 1. New "Canonical lookup keys" subsection in Step A enumerates the index field name → m365-mutable.json key mapping (`wdsectionfileid` ↔ `one_sectionFileId`, etc.) with explicit "DO NOT GUESS, DO NOT PARAPHRASE" directive.
65
+ 2. Step A.1 (by `wdsectionfileid`) and A.2 (by `wdsectiongroupid`) WorkIQ queries rewritten to use the index field names verbatim with `=` syntax.
66
+ 3. Step B verbatim probe rewritten: `wdpartid = <wdpartid> AND wdsectionfileid = <sectionFileId>` lookup, asks for the **indexed page body content** (not "the body of the page").
67
+ 4. Step B explanatory paragraph added: "WorkIQ has two answer modes... the field names are the contract."
68
+ 5. Step B retry phrasing now explicitly tells WorkIQ "Your previous response was generated by the LLM-summary path, not the search-index extractor. Re-run the query against the search index using wdpartid = <wdpartid> AND wdsectionfileid = <sectionFileId>."
69
+ 6. Step C stream pass also rewritten to use `wdsectionfileid` / `wdsectiongroupid` / `lastModifiedDateTime` field names.
70
+ 7. Failure-handling note for Graph `/me/onenote/*` updated with empirical 401 evidence (az CLI app id `04b07795-…` lacks Notes.Read; tenant denies admin consent).
71
+
72
+ **Doctrinal lesson:** when an enterprise tool exposes index-aware query semantics, **document the literal field names in the skill** instead of paraphrasing. Paraphrasing routes to the wrong answer path. This applies to all pull-* skills, not just OneNote — verify the same pattern for SharePoint (`SiteId`, `WebId`, `ListItemId`, `Path`), Teams (`channelIdentity`, `chatId`, `messageId`), Email (`internetMessageId`, `conversationId`).
73
+
74
+
75
+ ## 2026-05-13 — WorkIQ summarization masquerades as captured bodies; never fabricate narrative
76
+
77
+ **Trigger:** User said "onenote is still sparse, did you rerun or tighten" after the HCA refresh shipped 5 OneNote page snapshots that were each ~1.5K bytes. Inspection showed every page file had: header + `❌ Partial via WorkIQ — body not extractable` marker + a 3-paragraph **AI Narrative Summary inferred from adjacent emails and chat traffic** ("plausibly the engagement-level rollup page", "if Usha's backfill includes verbatim chat-summary…"). The narrative was speculation, not capture.
78
+
79
+ **Root cause (skill-level):**
80
+ - `pull-onenote/SKILL.md` v2.2.0 allowed snapshot files with a `page-body-unavailable` marker AND an AI Narrative Summary in the same file. The depth-bar said "AI Narrative Summary REQUIRED FIRST" without an exception for the unavailable case, so the producer satisfied the contract by inferring narrative from adjacent evidence.
81
+ - Graph `/me/onenote/*` is not a viable fallback in this tenant — Notes.Read scope requires admin consent that is denied. WorkIQ is the only path, and WorkIQ summarizes by default.
82
+
83
+ **Fix shipped (v2.3.0 of pull-onenote, plus verbatim-by-default v3.7.6.1):**
84
+ 1. `pull-onenote` Step B now uses a strict verbatim-or-marker probe (the WorkIQ prompt forces one of two outcomes only — verbatim body OR the literal `page-body-unavailable: <reason>` marker, no third option).
85
+ 2. Verbatim acceptance check (HARD): rejects responses containing `"plausibly"`, `"likely"`, `"appears to"`, `"inferred from"`, `"based on adjacent evidence"`, `"this page is about"`, `"key topics include"`.
86
+ 3. No-fabrication rule (HARD): if the body is unavailable, the snapshot file MUST contain ONLY the header + the marker + a `next_step` asking the user to paste. AI Narrative Summary is forbidden in this case. Empty is correct.
87
+ 4. `items_verbatim` added to run-log alongside `items_pulled` and `items_enumerated`. Run is classified `partial-bodies` when the verbatim ratio is < 0.5 — stops "5 page files written" from masquerading as "5 pages captured" in the per-user refresh report.
88
+ 5. `verbatim-by-default.instructions.md` adds anti-pattern #8: "Inferred narrative as substitute for body".
89
+
90
+ **Recovery action for HCA (this turn):** the 5 existing HCA OneNote page files at `HCA\Evidence\ushak\onenote\snapshot\pages\` will be rewritten to the v2.3.0 shape (header + unavailable marker + paste-ask, no inferred narrative) and the user will be asked once at the end of the next refresh to paste the page bodies.
91
+
92
+
93
+ ## 2026-05-14 — v3.7.8 retraction + v3.7.9 corrected doctrine
94
+
95
+ **What v3.7.8 claimed (WRONG):** WorkIQ has a "search-index extractor" mode triggered by literal field names (`wdsectionfileid`, `wdpartid`) in the query body. Using these field names would return verbatim indexed bodies; using natural language would route to summary mode.
96
+
97
+ **What was empirically proven against HCA on 2026-05-13/14:**
98
+
99
+ 1. WorkIQ does NOT honor `wdsectionfileid = <id>` as filter syntax — it routes to summary mode AND returns "OneNote internal properties not exposed as searchable fields" refusal text.
100
+ 2. The wdpartid GUIDs we observed in earlier runs were **URL fragments inside SharePoint Doc.aspx hyperlinks** that WorkIQ rendered as response footnotes — not search-index extractor outputs.
101
+ 3. The Nova-pattern (natural-language query naming the section + notebook by display name and the page by quoted title) is the actual working pattern. It returned a real verbatim body for the HCA `4/3 - HCA with Jay and Martin` page.
102
+ 4. **Body retrieval is non-deterministic** — the same 4/3 page returned a verbatim body at 19:42 PDT and `BODY-NOT-EXPOSED` at 19:48 PDT, same query, no edits. The M365 search index's exposure of OneNote bodies oscillates over time.
103
+ 5. **The blocker for months was the WorkIQ EULA.** Without `workiq accept-eula`, every OneNote query silently returns nothing useful. This is a one-time setup step, not a per-call gate.
104
+
105
+ **v3.7.9 corrected doctrine (now in pull-onenote SKILL.md v2.5.0):**
106
+
107
+ - Pre-flight: probe WorkIQ; if EULA prompt returned, run `workiq accept-eula` and retry.
108
+ - Step A enumerate: natural-language query naming section + notebook by display name (NOT field-name filter syntax). Returns a markdown table with one row per page; wdpartid GUIDs extracted from response URL fragments per row.
109
+ - Step B per-page: natural-language query naming page title + section + notebook by display name. Asks for verbatim body or the literal string `BODY-NOT-EXPOSED`.
110
+ - Per-page retry registry: every page lives in `m365-mutable.json#knownSections.<projectKey>.one_pages` with `last_status` and `attempts`. Pages stuck at `BODY-NOT-EXPOSED` are retried on every refresh until they succeed or the user pastes.
111
+ - Snapshot files carry yaml front-matter with the same fields, so refresh runs can read state from disk if the registry is unavailable.
112
+
113
+ **HCA result (2026-05-14):** 18 pages enumerated. 1 captured verbatim (4/3). 15 pending retry (BODY-NOT-EXPOSED). 2 enumeration-only (will be probed in Step B on next refresh).
114
+
115
+ **Key lesson:** when a doctrine is grounded in pattern-matching against tool responses (e.g. "field names route to extractor"), validate it empirically against the live tool BEFORE shipping. The v3.7.8 doctrine was internally consistent and self-citing but never actually tested end-to-end — the 4/3 success that motivated v3.7.9 happened only after honestly retracting v3.7.8 and replicating the Nova workflow step-by-step.
116
+
117
+
118
+ ## 2026-05-14 — v3.7.9 retraction + v3.8.0 architectural pivot
119
+
120
+ **What v3.7.9 (yesterday) shipped:** WorkIQ natural-language by display name + per-page retry registry. Validated 1-page capture and codified as primary path.
121
+
122
+ **What we proved against HCA on 2026-05-14:** the v3.7.9 capture-rate is structurally too low for a Mon-9am scheduled refresh. WorkIQ body retrieval is non-deterministic (same page flips exposed/not exposed across queries minutes apart), and across 18 enumerated HCA pages WorkIQ returned exactly 1 verbatim body. A Monday-9am run that captures 1 page out of 16-18 is not a refresh, it's a coincidence.
123
+
124
+ **The pivot — browser-scrape via OneNote-for-Web with persisted Playwright profile:**
125
+
126
+ - Constructed the OneNote-for-Web deep-link URL using values already in `m365-mutable.json`: `<spoBaseUrl>/_layouts/15/Doc.aspx?sourcedoc={<notebookSourceDoc>}&action=edit&wd=target(<sectionName>|<sectionFileId>/)`.
127
+ - Picked the Microsoft work account at the consent prompt (one-time).
128
+ - Found the OneNote canvas frame at `ffc-onenote.officeapps.live.com/onenoteframe.aspx` (nested 2 frames deep).
129
+ - Enumerated pages from the accessibility tree: every page has `aria-label="<title>, page X of N, Page. Select to open page contents."` — gives ordered, complete, authoritative page list.
130
+ - Clicked each page in the rail, waited 2.5s for canvas to settle, read `document.querySelector('#PageContentWrapper').innerText` — got full verbatim body.
131
+
132
+ **Result:** 16/16 HCA pages captured (~120KB total) in ~50 seconds. Compare to WorkIQ's 1/18 in 30+ minutes of probing. Includes pages WorkIQ flagged BODY-NOT-EXPOSED on previous attempts, proving the bodies were always retrievable — WorkIQ just couldn't reach them.
133
+
134
+ **Architectural decision:**
135
+
136
+ - Browser-scrape is the PRIMARY path in pull-onenote v2.6.0.
137
+ - WorkIQ is the FALLBACK (when Playwright profile auth-expires) AND the source of stream events (page-edit signals via search index — those ARE deterministic).
138
+ - Per-page registry stores BOTH `webPageId` (browser navigation GUID) AND `wdpartid` (WorkIQ correlation GUID). New `last_status` value `auth-required` for unattended-MFA-blocked runs.
139
+ - New runner: `plugin/skills/pull-onenote/runner.mjs` (Playwright + `launchPersistentContext` for unattended refreshes).
140
+
141
+ **Known gap (documented, accepted):** Conditional Access / MFA challenges cannot be satisfied unattended. Roughly every 1-4 weeks the runner will hit a sign-in redirect, exit with `runStatus: "auth-required"`, mark all queued pages `auth-required`, and surface in the run report. The user does ONE interactive `node runner.mjs --bootstrap` and the next scheduled run resumes silently.
142
+
143
+ **Lessons compounded across v3.7.x → v3.8.0:**
144
+
145
+ 1. When a doctrine is grounded in pattern-matching against tool responses, validate it empirically end-to-end BEFORE shipping. v3.7.8 and v3.7.9 both shipped with one-page-of-evidence and were retracted within 24 hours.
146
+ 2. When a primary path's capture-rate drops below ~80%, treat it as architecturally inadequate, not "needs more retries". v3.7.9's retry registry was the correct durability layer for a HIGH-capture-rate path; on a 5%-capture-rate path it just made the bad output more visible.
147
+ 3. Browser automation is acceptable infrastructure for evidence pulls when the tool surface is deterministic and the auth model can be persisted across runs. The Loop and expense-report skills already proved this; pull-onenote v2.6.0 follows their lead.
148
+ 4. Always store identifiers in EVERY form a tool surface uses. `wdpartid` (WorkIQ) and `webPageId` (browser) are both small strings; storing both costs nothing and keeps both retrieval paths viable.
149
+
150
+
151
+ ## 2026-05-18 — Bare runner JSON + hand-rolled file writes = layout violation + UTF-8 corruption (v3.11.5)
152
+
153
+ **Trigger (John Deere):** After fixing the bootstrap sign-in and single-page regex bugs, the JD scrape succeeded — but the agent wrote a single `section.md` at `Evidence/ushak/onenote/snapshot/section.md` instead of the doctrine-mandated `snapshot/pages/<safe-title>.md`. The PowerShell-piped UTF-8 also corrupted every NBSP to `┬á` (1825 occurrences in a 13 KB body). Neither defect was caught at the runner level — both were the driver's responsibility, and the driver was the agent improvising.
154
+
155
+ **Root cause: the runner JSON contract has no automated writer.** `runner.mjs` is silent about the canonical snapshot layout from `snapshot-vs-stream.instructions.md` (`snapshot/pages/<safe-title>.md`, one file per page, with the full front-matter schema). Any driver — PowerShell, Clawpilot, future automation — has to re-derive: which directory, what filename, what front-matter keys, what registry shape, what run-report format. That's where things go wrong, every time.
156
+
157
+ Additionally, PowerShell's default `Out-File -Encoding utf8` writes UTF-8-BOM and re-encodes non-ASCII via the system code page — NBSP (U+00A0) becomes `┬á` (the CP1252 bytes for the UTF-8 encoding of NBSP). Even `Tee-Object` does this. The only safe way to round-trip OneNote body bytes through PowerShell is `[IO.File]::WriteAllText($path, $text, [Text.UTF8Encoding]::new($false))` — or to avoid PowerShell entirely.
158
+
159
+ **Fix (v3.11.5):**
160
+
161
+ - New `plugin/skills/pull-onenote/write-snapshot.mjs` is the single supported driver. It:
162
+ - invokes `runner.mjs` via `child_process.spawnSync` (no shell, UTF-8 preserved end-to-end);
163
+ - writes one `snapshot/pages/<safe-title>.md` per captured page, with the full front-matter schema mandated by `pull-onenote/SKILL.md` §"Snapshot file shape";
164
+ - upserts `m365Mutable.knownSections.<project>.one_pages[]` with attempts counter, dual-ID schema, snapshot_path, captured_at;
165
+ - emits a per-run report at `Evidence/<alias>/onenote/refresh-reports/<YYYYMMDD-HHMM>-onenote.md` per `run-reports.instructions.md`.
166
+ - `pull-onenote/SKILL.md` "Canonical CLI invocations" section now mandates `write-snapshot.mjs` as the production path; the bare `runner.mjs` is documented only as a diagnostic tool.
167
+
168
+ **Doctrinal lessons:**
169
+
170
+ 1. Any tool whose output requires structured file-layout work MUST ship a writer of its own, not delegate to a shell script. Hand-rolled wiring is where doctrine gets lost.
171
+ 2. PowerShell `Out-File`, `Set-Content`, `Tee-Object`, and stdout `>` redirection ALL re-encode non-ASCII when capturing piped data. If a tool's output contains non-ASCII (and OneNote bodies always do — NBSP, smart quotes, em-dashes, bullet chars), the driver MUST use `child_process` or write through Node, not PowerShell pipes.
172
+
173
+ **Validation:** JD `3/13 - account team` re-pulled via `write-snapshot.mjs` on 2026-05-18 — clean layout (`Evidence/ushak/onenote/snapshot/pages/3-13-account-team.md`), clean UTF-8 (`-match '┬á'` returned False), registry upserted with attempts=1, run report written.
174
+
175
+
176
+ ## 2026-05-18 — Bootstrap sign-in skipped + single-page section regex (v3.11.2 / v3.11.3)
177
+
178
+ **Triggers (John Deere refresh):**
179
+
180
+ 1. Re-bootstrap appeared to succeed but every subsequent scrape returned `auth-required`.
181
+ 2. After fixing #1, scrape preflight passed and the canvas frame attached, but page enumeration timed out — even with a 3-minute window — for a section with only one page.
182
+
183
+ **Root causes:**
184
+
185
+ 1. `--bootstrap` used `page.waitForURL(/onenote\.cloud\.microsoft|m365\.cloud\.microsoft/)` which **matches instantly** because we just navigated to `https://onenote.cloud.microsoft/`. The 5-min wait collapsed to ~0 and we seeded SharePoint cookies before the user could even type their email. Logs showed `Step 1/2: Sign in…` immediately followed by `Step 2/2: Seeding SharePoint cookies` with no real wait between them. Subsequent preflight failed because OneNote-for-Web cookies (different from the SPO cookies) were never minted.
186
+ 2. The page-rail enumerator regex only accepted multi-page aria labels (`, page N of M, Page.`). **Single-page sections** (e.g. John Deere's section with just `3/13 - account team`) render as `<title>, Page. Selected.` — the regex never matched and `waitForFunction` hung until timeout, producing `pages: []` with error `frame.waitForFunction: Timeout`.
187
+
188
+ **Fixes:**
189
+
190
+ - **v3.11.2** — `--bootstrap` now waits for a real OneNote post-auth UI indicator (`[aria-label*="Account manager" i], [data-automationid="NotebookList"], button[aria-label*="notebook" i], iframe[src*="onenoteframe.aspx"]`) — same selectors `preflightOneNoteWeb` uses. Logs `Sign-in detected (OneNote chrome rendered).` when satisfied, or a warning if 5 min elapses without sign-in.
191
+ - **v3.11.3** — page-rail `waitForFunction` and the `pages` enumerator both accept either format:
192
+ - multi-page: `<title>, page N of M, Page.`
193
+ - single-page: `<title>, Page. Selected.` → emitted as `{ pos: 1, total: 1 }`
194
+ - dedup via `seen` set so a page matched by both rules isn't double-counted.
195
+
196
+ **Validation:** JD `John Deere.one` captured end-to-end on 2026-05-18 — 1/1 pages, 13 KB verbatim body for `3/13 - account team` (Black Box → Dual Write FDE intake meeting). Snapshot at `John Deere/Evidence/ushak/onenote/snapshot/section.md`.
197
+
198
+ **Doctrinal lesson:** any `waitForURL` against a URL we just navigated to is a no-op — must wait for a **post-auth UI signal** (chrome that only renders after token exchange). Same anti-pattern would apply to any future bootstrap (SharePoint, Loop, M365 admin, etc.). And: any aria-label-driven enumerator must handle the **N=1 special-case format** for that UI surface, not assume the multi-element format applies universally.
199
+
200
+
201
+ ## 2026-05-18 — WorkIQ OneNote three-tier output codified (kushi v3.11.1)
202
+
203
+ **What happened.** User pointed out that `hca-snip.txt` and `hca-onenote-raw.txt` (Scratchpad, 2026-05-13) exist as WorkIQ-derived OneNote evidence, contradicting the agent's earlier claim that "OneNote was unavailable" during JD bootstrap. Root cause: I had asked WorkIQ for "all 21 pages' full verbatim bodies in one call" — WorkIQ silently degraded to tier A (page enumeration) and tier B (search snippets) and returned an explicit note `combined content size … exceeds the maximum response payload that Copilot can safely render in-chat`. I read that as "OneNote unavailable" instead of "Tier C must be per-page."
204
+
205
+ **Empirical contract (now codified in `workiq-only.instructions.md`):**
206
+
207
+ WorkIQ returns OneNote in THREE tiers:
208
+
209
+ - **Tier A — Enumeration**: titles + wdpartid + wdsectionfileid + last-modified + author + deep-link. Bulk OK. (HCA evidence: request-ids `bc780473-66d7-4809-89dc-e910e4b8ced8`, `fdbf3290-649d-4066-84ea-ceab678697ed`.)
210
+ - **Tier B — Search snippets**: ~500-char Graph search snippet per page, verbatim from search index. Bulk OK. (HCA evidence: request-ids `f3744946-5b22-4c09-96ac-714c8508d5f6`, `d93deb47-147b-4d19-8cd7-c0a0e2f76f30`.)
211
+ - **Tier C — Full verbatim body**: ONE page per call. Bulk refused.
212
+
213
+ **Defect signature:** "give me full bodies of all pages in section X" prompt → response includes the phrase `combined content size … exceeds the maximum response payload` plus a tier-A inventory table. Misreading that as "OneNote unavailable" is the defect. Correct response: re-issue the per-page tier-C prompt (one wdpartid at a time).
214
+
215
+ **Result.** `workiq-only.instructions.md` OneNote section now lists all four prompts (Tier A discovery, Tier A page index, Tier B snippets, Tier C single-page body) with the exact WorkIQ wording. `pull-onenote` v2.9.0 Playwright-primary doctrine is unchanged — Playwright is not a Graph call, and it remains the verbatim-bulk path. WorkIQ tier C is the fallback when Playwright auth expires (do per-page tier-C calls in that mode, not bulk).
@@ -0,0 +1,5 @@
1
+ # Learnings — SharePoint / OneDrive (`pull-sharepoint`)
2
+
3
+ Newest on top. Format defined in [`README.md`](./README.md).
4
+
5
+ _(no entries yet — append the moment a fix lands during a `pull-sharepoint` run)_
@@ -0,0 +1,5 @@
1
+ # Learnings — Teams (`pull-teams`)
2
+
3
+ Newest on top. Format defined in [`README.md`](./README.md).
4
+
5
+ _(no entries yet — append the moment a fix lands during a `pull-teams` run)_
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kushi",
3
- "description": "Multi-source project evidence + Q&A agent. Snapshot + stream capture across Email, Teams, OneNote, SharePoint, Meetings, CRM, ADO; plus read-only natural-language Q&A over the captured evidence. WorkIQ-first. Host-agnostic. Three install profiles: core (aggregator only), standard (default — adds bootstrap/refresh + FDE authoring), full (adds State/ rollup).",
4
- "version": "3.3.0",
3
+ "description": "Multi-source project evidence + Q&A agent. Snapshot + stream capture across Email, Teams, OneNote, SharePoint, Meetings, CRM, ADO; plus read-only natural-language Q&A over the captured evidence. WorkIQ-only for M365 sources (Graph / m365_* FORBIDDEN as fallbacks; user-paste is first-class). Host-agnostic. Three install profiles: core (aggregator only), standard (default — adds bootstrap/refresh + FDE authoring), full (adds State/ rollup).",
4
+ "version": "3.12.1",
5
5
  "author": "ushakrishnan",
6
6
  "repository": "https://github.com/gim-home/kushi",
7
7
  "default_profile": "standard",
@@ -91,6 +91,26 @@
91
91
  "verbs": [
92
92
  "state"
93
93
  ]
94
+ },
95
+ "preview": {
96
+ "extends": "standard",
97
+ "description": "PREVIEW: opt-in two-way sync skills. Adds propose-ado-update (read-only proposal generator) and apply-ado-update (gated; v0.1.0-preview is dry-mode only — produces planned.jsonl, no real ADO writes). Governed by update-ledger.instructions.md. See docs/concepts/roadmap.md and docs/how-to/two-way-ado-update.md.",
98
+ "skills": [
99
+ "propose-ado-update",
100
+ "apply-ado-update"
101
+ ],
102
+ "prompts": [
103
+ "propose-ado",
104
+ "apply-ado"
105
+ ],
106
+ "templates": [
107
+ "ado-update"
108
+ ],
109
+ "reference_packs": [],
110
+ "verbs": [
111
+ "propose-ado",
112
+ "apply-ado"
113
+ ]
94
114
  }
95
115
  }
96
116
  }
@@ -0,0 +1,14 @@
1
+ ---
2
+ name: apply-ado
3
+ description: Apply approved ADO updates from a proposed.md (gated). Currently a preview-stub — writes a planned.jsonl, no real ADO calls yet.
4
+ ---
5
+
6
+ # /apply-ado
7
+
8
+ Route to `@Kushi apply ado <project>`.
9
+
10
+ Delegates to the `apply-ado-update` skill. **Gated** — every write is reviewed (or auto-allowlisted via `<project>/.kushi/ado-update.yml`), every applied write appended to `ledger.jsonl` with a reverse op.
11
+
12
+ In v0.1.0-preview this runs in **dry-mode only** — it produces `planned.jsonl` instead of calling ADO. The real write path lands in a follow-up release once the proposal format has been validated against real projects.
13
+
14
+ Profile: **`preview`** only.
@@ -0,0 +1,12 @@
1
+ ---
2
+ name: propose-ado
3
+ description: Read-only ADO update proposal — generate proposed.md from the latest consolidated evidence. NO writes to ADO.
4
+ ---
5
+
6
+ # /propose-ado
7
+
8
+ Route to `@Kushi propose ado <project>`.
9
+
10
+ Delegates to the `propose-ado-update` skill. Read-only — produces a Markdown preview at `<engagement-root>/ado-updates/<YYYY-MM-DD>/proposed.md`. No ADO writes.
11
+
12
+ Profile: **`preview`** only.
@@ -0,0 +1,165 @@
1
+ # FDE CRM (Dataverse) Field Manifest
2
+
3
+ > Canonical mapping of FDE engagement-submission long-text fields + annotation pull pattern. Consumed by `pull-crm` whenever the configured entity set is an FDE engagement-submission table.
4
+
5
+ Cite as: `[source: reference-packs/fde/crm-field-manifest.md · packaged]`.
6
+
7
+ ---
8
+
9
+ ## Why this manifest exists
10
+
11
+ The FDE engagement-submission record in Dataverse holds the strategic answers an FDE crew needs (business scenario, why current offers don't fit, success criteria, technology workloads, work done so far). These live in **long-text fields** (`Memo` / multi-line text) that must be captured **verbatim** — paraphrasing or summarizing them at pull time strips the context Kushi is supposed to preserve.
12
+
13
+ Pre-v3.7.2, `pull-crm` retrieved a CRM record with whatever fields the API returned by default, which often dropped or truncated the long-text answers. This manifest fixes that by giving `pull-crm` an explicit `$select` list and an explicit annotation fetch loop.
14
+
15
+ ---
16
+
17
+ ## Display-name → logical-name mapping
18
+
19
+ The user-facing labels (left) map to Dataverse logical names (right). Logical names vary per tenant; treat the right column as the **probable** logical name and do the discovery probe described below if a `$select` returns null.
20
+
21
+ | Display label (form question) | Probable logical name | Type |
22
+ |---|---|---|
23
+ | Engaged With | `msfre_engagedwith` | Memo (multi-line) |
24
+ | What is the business scenario or technical blocker that FDE would solve? | `msfre_businessscenario` | Memo |
25
+ | Why are the current offers or solutions not suitable for this customer's needs? | `msfre_currentofferinggap` | Memo |
26
+ | What does success look like for the customer? | `msfre_successcriteria` | Memo |
27
+ | Please list the relevant technology (1st party / 3rd party) workloads being used | `msfre_technologyworkloads` | Memo |
28
+ | Give a description of the work completed so far (if any) | `msfre_workcompletedsofar` | Memo |
29
+ | MSX Opportunity ID | `msfre_msxopportunityid` | Single line |
30
+ | Customer name | `msfre_customername` (or via lookup `_msfre_account_value`) | Lookup |
31
+ | Engagement title | `msfre_engagementtitle` (or `name`) | Single line |
32
+ | FDE crew / division | `msfre_division`, `msfre_crew` | OptionSet / Single line |
33
+ | Status / state | `statuscode`, `statecode` | OptionSet |
34
+ | Created on / by | `createdon`, `_createdby_value` | Datetime / Lookup |
35
+ | Modified on / by | `modifiedon`, `_modifiedby_value` | Datetime / Lookup |
36
+
37
+ ### Discovery probe (run once per project, persist to mutable)
38
+
39
+ If the configured `entitySetName` is `msfre_*` and a `$select` for any of the probable logical names above returns `null`, run a **metadata probe** to confirm the actual logical names:
40
+
41
+ ```http
42
+ GET /api/data/v9.2/EntityDefinitions(LogicalName='msfre_engagementsubmission')/Attributes?$select=LogicalName,DisplayName,AttributeType&$filter=AttributeType eq 'Memo'
43
+ ```
44
+
45
+ Persist the resolved mapping to `m365Mutable.knownSections.<project>.crm.fieldMap` so subsequent pulls skip the probe.
46
+
47
+ ---
48
+
49
+ ## REQUIRED $select for FDE engagement-submission records
50
+
51
+ Every snapshot pull MUST request these fields explicitly (no relying on default projection):
52
+
53
+ ```
54
+ $select=
55
+ msfre_engagementsubmissionid,
56
+ name,
57
+ msfre_engagedwith,
58
+ msfre_businessscenario,
59
+ msfre_currentofferinggap,
60
+ msfre_successcriteria,
61
+ msfre_technologyworkloads,
62
+ msfre_workcompletedsofar,
63
+ msfre_msxopportunityid,
64
+ statuscode,
65
+ statecode,
66
+ createdon,
67
+ createdby,
68
+ modifiedon,
69
+ modifiedby,
70
+ _ownerid_value,
71
+ _msfre_account_value
72
+ ```
73
+
74
+ Then `$expand=Annotations($select=annotationid,subject,notetext,createdon,_createdby_value,filename,mimetype)` to pull every note attached to the record.
75
+
76
+ If the discovery probe returned a different logical name, substitute it before issuing the request.
77
+
78
+ ---
79
+
80
+ ## Annotation (notes) pull pattern
81
+
82
+ CRM Notes are stored as `Annotation` records related to the parent record via `objectid`. They are NOT included by default — they must be expanded.
83
+
84
+ For each annotation returned:
85
+
86
+ 1. Capture **verbatim** the full `notetext` (do not summarize, do not truncate, do not strip newlines).
87
+ 2. Capture metadata: `subject`, `createdon`, `createdby` (resolved name, not GUID), `filename` + `mimetype` if attached.
88
+ 3. Write each annotation as a `### Note — <subject> (<createdon> by <createdby>)` block under a `## Notes (verbatim)` H2 section in the snapshot file.
89
+ 4. If the annotation has an attached file (`filename` non-null), record its filename + mimetype + size; do NOT download the binary unless the user explicitly asks.
90
+
91
+ ---
92
+
93
+ ## Snapshot file shape (FDE CRM record)
94
+
95
+ When the entity is FDE engagement-submission, the snapshot file MUST follow this shape:
96
+
97
+ ```markdown
98
+ # CRM — <customer-name> · <engagement-title>
99
+
100
+ **Record ID:** `<id>` · **MSX Opportunity:** `<msx-id>` · **Last fetched:** `<iso-ts>`
101
+ **Status:** `<statuscode label>` · **Owner:** `<owner display name>`
102
+
103
+ ## Source Basis
104
+
105
+ - Tool: <workiq | dataverse-rest | graph>
106
+ - Boundary: `boundaries.crm.record_ids = [<id>]`
107
+ - Field map: `m365Mutable.knownSections.<project>.crm.fieldMap` (probed <date>) | packaged (default)
108
+ - Annotations fetched: <N>
109
+
110
+ ## AI Narrative Summary
111
+
112
+ (3+ paragraphs — engagement story so far, current stage, what's at stake, latest direction. No paraphrase of the long-text answers below; summarize what they say collectively.)
113
+
114
+ ## Engagement context
115
+
116
+ ### Engaged With
117
+ > (verbatim msfre_engagedwith)
118
+
119
+ ### Business scenario / technical blocker
120
+ > (verbatim msfre_businessscenario)
121
+
122
+ ### Why current offers / solutions not suitable
123
+ > (verbatim msfre_currentofferinggap)
124
+
125
+ ### What success looks like
126
+ > (verbatim msfre_successcriteria)
127
+
128
+ ### Technology workloads (1st / 3rd party)
129
+ > (verbatim msfre_technologyworkloads)
130
+
131
+ ### Work completed so far
132
+ > (verbatim msfre_workcompletedsofar)
133
+
134
+ ## All other fields
135
+
136
+ (Every other field returned by the $select, in a 2-column table: field | value. Empty fields shown as `_(empty)_` so absence is visible.)
137
+
138
+ ## Notes (verbatim)
139
+
140
+ ### Note — <subject> (<createdon> by <createdby>)
141
+ > (verbatim notetext, no truncation)
142
+
143
+ ### Note — <subject> (<createdon> by <createdby>)
144
+ > (verbatim notetext)
145
+
146
+ (... one block per annotation, oldest first ...)
147
+ ```
148
+
149
+ ---
150
+
151
+ ## Anti-patterns (defects)
152
+
153
+ - ❌ Paraphrasing the long-text fields — they MUST be verbatim.
154
+ - ❌ Omitting empty fields — write `_(empty)_` so the reader can see we asked.
155
+ - ❌ Skipping annotations because they look like noise — every annotation goes in.
156
+ - ❌ Truncating long annotations with `...` — record the full text even if it's pages.
157
+ - ❌ Falling back to "narrate from email" when CRM is unreachable — see `pull-crm/SKILL.md` Hard prerequisites; refuse instead.
158
+
159
+ ---
160
+
161
+ ## See also
162
+
163
+ - `intake-questions.md` — the FDE Intake question set this manifest backs.
164
+ - `pull-crm/SKILL.md` — the skill that must apply this manifest.
165
+ - `core-fde-reference.md` — broader FDE operating model.
@@ -0,0 +1,125 @@
1
+ ---
2
+ name: "apply-ado-update"
3
+ version: "0.1.0-preview"
4
+ status: "preview-stub"
5
+ description: "Gated write skill: reads <engagement-root>/<project>/ado-updates/<date>/proposed.md, presents the diff for approval, applies approved items to ADO (PATCH field + POST comment), and appends to the per-project update ledger. v0.1.0-preview is a STUB — runs in dry-mode only and writes a planned-writes log; no real ADO PATCH/POST yet."
6
+ ---
7
+
8
+ # Skill: apply-ado-update
9
+
10
+ Run this when the user says any of: "apply ado update for `<X>`", "@Kushi apply ado `<X>`", "write to ado for `<X>`".
11
+
12
+ This skill is the **only** code path in Kushi authorized to write to ADO. It implements the doctrine in `update-ledger.instructions.md`.
13
+
14
+ ## Status: preview-stub (v0.1.0-preview)
15
+
16
+ In v0.1.0-preview this skill **runs in dry-mode only**. It:
17
+
18
+ - Reads `proposed.md`.
19
+ - Validates the proposal shape and citations.
20
+ - Re-fetches the current ADO Initiative state and re-computes the diff (in case it drifted since `propose-ado-update` ran).
21
+ - Asks the user to approve all / select / none.
22
+ - Writes a **planned-writes log** to `<project>/ado-updates/<YYYY-MM-DD>/planned.jsonl` recording what *would* have been written.
23
+ - Does **not** call any ADO `PATCH` or `POST` endpoint yet.
24
+
25
+ The real write path (PATCH field + POST comment + ledger append) lands in v0.1.x once the proposed.md format, the apply UX, and the ledger schema have been validated against real projects for at least one full week. See `docs/concepts/roadmap.md`.
26
+
27
+ ## Profile
28
+
29
+ Belongs to the **`preview`** profile. Opt in via `npx kushi-agents --clawpilot --profile preview`.
30
+
31
+ ## Deterministic config — do not invent paths
32
+
33
+ Same as `propose-ado-update`. Reads ADO connection from `<engagement-root>/.project-evidence/ado/config.yml`, per-project `engagement_id` and `writes:` block from `<engagement-root>/<project>/integrations.yml ado:`. Never asks for paths the bootstrap layer already resolved.
34
+
35
+ ## Pre-flight (HARD — do not bypass)
36
+
37
+ 1. **Resolve engagement root + project** per `engagement-root-resolution.instructions.md`.
38
+ 2. Confirm `<project>/integrations.yml ado.engagement_id > 0`. If 0 → abort with the same message `propose-ado-update` uses ("ADO Initiative not yet linked").
39
+ 3. Confirm `<project>/ado-updates/<YYYY-MM-DD>/proposed.md` exists. If not → tell user to run `@Kushi propose ado <project>` first. Never fabricate a proposal.
40
+ 4. Per `azure-auth-patterns.instructions.md` — Section 1 (session pre-check) + Section 3 (ADO tenant validation, using `<engagement-root>/.project-evidence/ado/config.yml`) **must** complete green before any further step.
41
+
42
+ ## Steps (current preview-stub behavior)
43
+
44
+ 1. **Load proposed.md** and parse the field-update + comment-update sections (Markdown headings + fenced blocks).
45
+ 2. **Re-fetch current ADO state** (read-only GET) and compare against the `current` value embedded in `proposed.md`. If they differ → warn `proposal-drift`, show the live current value, and require explicit re-approval.
46
+ 3. **Duplicate detection** — list the latest 5 Discussion comments via ADO API; if any contain the Kushi fingerprint line for this same week → mark `duplicate-detected`, skip the comment write, still proceed with field update if not duplicate.
47
+ 4. **Allowlist gate** — every item in `proposed.md` whose `fieldRefName` is NOT in `integrations.yml ado.writes.allowlist.fields` is rejected before approval is even offered. This is the last-line guard against a malformed proposal.
48
+ 5. **Approval gate**:
49
+ - Default: prompt user with the diff and `[a]ll · [s]elect · [n]one`.
50
+ - Auto-apply: if `ado.writes.statusSummary.autoApply: true` AND the field-update item has `confidence: high` AND the new line matches the configured `<MMM YYYY>: ...` pattern → mark approver `auto:append-month-high-conf`. Same for `discussionComment.autoApply` (rare; off by default).
51
+ - All other items → require interactive approval.
52
+ 6. **(Stub)** Instead of calling ADO writes, append to `<project>/ado-updates/<YYYY-MM-DD>/planned.jsonl` one record per approved item:
53
+ ```json
54
+ {
55
+ "timestamp": "2026-05-18T16:30:00Z",
56
+ "engagementId": 12345,
57
+ "kind": "field|comment",
58
+ "fieldRefName": "Custom.FDEStatusSummary",
59
+ "currentValue": "...",
60
+ "proposedValue": "...",
61
+ "approver": "ushak | auto:append-month-high-conf",
62
+ "confidence": "high",
63
+ "evidenceCitations": ["ushak/onenote/snapshot/architecture-decisions.md · 2026-05-12"],
64
+ "fingerprint": "kushi-weekly-2026-05-18-12345-field",
65
+ "executed": false,
66
+ "stubReason": "v0.1.0-preview: write path not yet enabled"
67
+ }
68
+ ```
69
+ 7. **Echo summary** to user: how many items would be written, path to `planned.jsonl`, and the line "No ADO calls were made (preview-stub)."
70
+
71
+ ## Steps (planned for v0.1.x — when write path lands)
72
+
73
+ After step 5 (approval), instead of step 6:
74
+
75
+ 6. **Apply field update** (one PATCH per approved field):
76
+ ```http
77
+ PATCH https://dev.azure.com/{org}/{project}/_apis/wit/workitems/{engagement_id}?api-version=7.1
78
+ Content-Type: application/json-patch+json
79
+
80
+ [
81
+ { "op": "test", "path": "/rev", "value": <currentRev> },
82
+ { "op": "add", "path": "/fields/Custom.FDEStatusSummary", "value": "<new value>" }
83
+ ]
84
+ ```
85
+ The `test` on `/rev` is the optimistic-lock guard. If it fails → ledger entry `error: rev-mismatch`, do NOT retry silently; surface to user.
86
+
87
+ 7. **Apply Discussion comment** (one POST per approved comment):
88
+ ```http
89
+ POST https://dev.azure.com/{org}/{project}/_apis/wit/workItems/{engagement_id}/comments?api-version=7.1-preview.4
90
+ Content-Type: application/json
91
+ { "text": "<rendered HTML>" }
92
+ ```
93
+
94
+ 8. **Append to ledger** at `<project>/ado-updates/<YYYY-MM-DD>/ledger.jsonl` per `update-ledger.instructions.md` §Schema, including the **reverse op** (so a later `revert` skill can roll back).
95
+
96
+ 9. **Optional notify** — if `ado.writes.approvals.notifyOnApply: teams`, post a one-line summary via `m_send_teams_message` to the user (never to others).
97
+
98
+ ## Failure modes (preview-stub)
99
+
100
+ | Symptom | Recovery |
101
+ |---|---|
102
+ | `proposed.md` missing | Stop. Tell user to run `@Kushi propose ado <project>` first. |
103
+ | `integrations.yml` missing or `ado.engagement_id == 0` | Stop with same message `propose-ado-update` uses; do not prompt for an ID. |
104
+ | `ado.writes:` block missing | Stop. Tell user to run `@Kushi propose ado <project>` first (it scaffolds the block). |
105
+ | ADO 401 on the read-only re-fetch in step 2 | Re-acquire token once per `azure-auth-patterns.instructions.md` §4; if still 401, abort `auth-failed`. |
106
+ | Tenant mismatch | Per `azure-auth-patterns.instructions.md` §3 — abort with the exact `az login --tenant <id>` command. |
107
+ | `proposal-drift` (current ADO value differs from `proposed.md` snapshot) | Show diff between live and proposed; require user to re-confirm or re-run `propose-ado-update`. |
108
+ | `duplicate-detected` (comment fingerprint already on the work item) | Skip comment portion; continue with field if not also duplicate. |
109
+ | Allowlist rejection | Refuse the item before approval. Tell user the field reference name is not in `integrations.yml ado.writes.allowlist.fields`. |
110
+
111
+ ## What this skill does NOT do (ever — not even in v0.1.x)
112
+
113
+ - Does NOT auto-apply any item with `confidence < high`.
114
+ - Does NOT auto-apply any item not on the per-project allowlist (`ado.writes.<x>.autoApply: true`).
115
+ - Does NOT write to fields not listed in `ado.writes.allowlist.fields` of `<project>/integrations.yml`.
116
+ - Does NOT post comments other than the single rendered Discussion comment from `proposed.md`.
117
+ - Does NOT bulk-apply across projects in a single call (one project per invocation).
118
+ - Does NOT delete or close work items, change parent links, change area path, or touch any field outside the configured allowlist.
119
+
120
+ ## References
121
+
122
+ - `update-ledger.instructions.md` — ledger schema, reverse-op format, write-path safety doctrine.
123
+ - `azure-auth-patterns.instructions.md` — pre-flight, tenant validation, token reuse, 401 recovery.
124
+ - `propose-ado-update` SKILL.md — produces the `proposed.md` consumed by this skill.
125
+ - `engagement-root-resolution.instructions.md` — resolving `<engagement-root>` and `<project>`.
@@ -20,6 +20,8 @@ The user does NOT need to type `/ask-project` or `@Kushi ask`. If the message:
20
20
  - **Read-only.** No `pull-*`, no Graph writes, no WorkIQ calls during answering. Source pulls are a separate user-initiated step.
21
21
  - **No cross-project bleed.** Answer only from the resolved project's own folders. If the question requires comparing two projects, ask the user to confirm and run `ask-project` per project, then merge in the answer.
22
22
  - **Citation Ledger applies.** Every fact, decision, person, date, $ figure carries inline `[source: <alias>/<folder>/<file> · YYYY-MM-DD]` per `instructions/citation-ledger.instructions.md` and `instructions/answer-from-evidence.instructions.md`. If the corpus does not support an answer, say so explicitly — never invent.
23
+ - **Full-view gate applies.** Walk the standard 7-source set per `instructions/full-view-gate.instructions.md` before answering and render the Source Basis disposition table. Do not call the answer a "full view" if any applicable source is `cached-stale`, `attempted-but-blocked`, or `unknown`.
24
+ - **Confidence ladder applies.** Tag every claim about decisions/status/funding/scope/owner/commitment with `internal-only` / `communicated` / `confirmed` per `instructions/evidence-confidence-ladder.instructions.md`. Never paraphrase a CRM/ADO field flip as a settled fact without a customer-facing confirmation dated AFTER the flip.
23
25
  - **Privacy posture.** Same as Kushi global: no outbound messages, no leaking attendee emails into anything visible to others. Answer goes to chat only.
24
26
  - **Freshness gate, not freshness auto-fix.** If the freshest source relevant to the question is older than `chat.freshness_warn_days` (default 14), warn the user and offer `@Kushi refresh <project>` — but NEVER auto-refresh.
25
27
  - **Reference packs may be consulted for domain doctrine.** When the question maps to a known reference-pack domain (currently only `fde/` — FDE stages, fitness, CRM status meanings, intake gates, risk categories, "MACC", "is this FDE-fit"), ALSO load the matching reference pack as additional grounding using the 3-layer override order (project → user → packaged). Cite reference-pack assertions with `[source: reference-packs/<pack>/<file>.md · <layer>]` where layer is `packaged` / `user-override` / `project-override`. Reference-pack content NEVER overrides project Evidence/ for facts about *this* project; it only provides definitions, gates, and rubrics.