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,193 @@
1
+ ---
2
+ applyTo: "**"
3
+ description: "WorkIQ is the canonical path for ALL M365 evidence. Graph REST and m365_get_* host tools are explicitly FORBIDDEN as fallbacks in this workspace because they fail unpredictably (Tool execution failed, 401, 415, throttling). Codifies the exact WorkIQ prompts that are known to work, so skills do not re-discover them every run."
4
+ ---
5
+
6
+ # WorkIQ is the ONLY M365 path (HARD RULE, kushi v3.11.0+)
7
+
8
+ ## Why this supersedes `workiq-first`
9
+
10
+ `workiq-first.instructions.md` (v3.7.0) named WorkIQ as preferred but allowed Graph REST / `m365_get_*` host tools as a "last-resort partial". In practice (observed continuously in this workspace 2026-05-13 through 2026-05-18), **the Graph / m365 host tools fail almost every time** with `Tool execution failed`, `401`, `415`, throttling, or returning empty payloads even when content exists. Meanwhile WorkIQ returns full transcripts, full OneNote page bodies, full chat threads, and full email bodies on the first try.
11
+
12
+ Continuing to attempt Graph / m365_get_* in the cascade:
13
+ - wastes a turn
14
+ - produces a defect-looking trail in coverage.md ("step 1 failed, step 2 failed…")
15
+ - causes the agent to "figure out" the right WorkIQ prompt on every run
16
+ - risks the agent giving up after the Graph failures instead of trying WorkIQ at all
17
+
18
+ This rule replaces the cascade with: **WorkIQ first AND only. If WorkIQ fails, ask the user to paste.** Graph / m365_get_* are **forbidden** from kushi pull-* skills.
19
+
20
+ ## Scope of this rule
21
+
22
+ Applies to ALL evidence retrieval from these M365 sources:
23
+
24
+ - Meeting transcripts
25
+ - Meeting facilitator / Copilot recap notes
26
+ - Teams chats and channel messages
27
+ - OneNote pages (bodies)
28
+ - Email bodies and attachments
29
+ - SharePoint file contents (when text extraction is needed)
30
+ - Calendar events (when not already known by id)
31
+
32
+ **Out of scope** (these are NOT WorkIQ — they remain on their direct paths):
33
+
34
+ - **CRM (Dataverse)**: REST via `az account get-access-token --resource https://iscrm.crm.dynamics.com` per `crm-bootstrap-discovery.instructions.md` and `pull-crm/SKILL.md`. WorkIQ does NOT replace Dataverse REST for CRM because WorkIQ summarizes fields and may miss annotations.
35
+ - **ADO**: REST via `az account get-access-token --resource 499b84ac-1321-427f-aa17-267ca6975798` per `ado-bootstrap-discovery.instructions.md` and `pull-ado/SKILL.md`. WorkIQ does NOT replace ADO REST for ADO because WorkIQ summarizes discussion threads.
36
+ - **Raw chat-message metadata** (when you need the chat-id, member list, or raw JSON for an artifact dump): `m365_list_chat_messages` is **allowed** as a structured-data dump alongside the WorkIQ pull. It is NOT a fallback — it is a separate artifact (`chat-messages.json`) that captures the structured form. WorkIQ captures the human-readable thread.
37
+
38
+ ## The doctrine
39
+
40
+ For every M365 source in scope:
41
+
42
+ 1. **WorkIQ FIRST and ONLY.** Issue the canonical query from the table below.
43
+ 2. **If WorkIQ returns insufficient content** (empty, truncated, or only a summary when the query asked for verbatim): retry ONCE with the doubled-strict prompt from the same row.
44
+ 3. **If WorkIQ still fails or is unavailable**: ask the user to paste the data verbatim. This is a first-class evidence path, not a degradation.
45
+ 4. **DO NOT attempt** `m365_get_transcript`, `m365_get_facilitator_notes`, `m365_list_meetings`, `m365_list_events`, `m365_search_files` (for content), `m365_download_file` (for transcript/notes), or any Graph REST URL for the in-scope sources above. These are FORBIDDEN. Calling them is a defect; coverage.md must NOT show them in the attempt trail.
46
+ 5. **Allowed alongside WorkIQ** (structured-data dumps only): `m365_list_chat_messages` for chat-id'd threads → `chat-messages.json`. These run in parallel with the WorkIQ pull, not as a substitute.
47
+
48
+ ## Canonical WorkIQ commands (CODIFIED — do not re-discover)
49
+
50
+ The CLI is at `C:\Users\ushak\.copilot\bin\workiq.cmd` (resolved from `<USER_HOME>/.copilot/project-evidence.yml workiq.cli_path`; fall back to PATH).
51
+
52
+ Invocation shape:
53
+
54
+ ```powershell
55
+ & "<workiq.cli_path>" ask -q "<prompt>" 2>&1 | Out-File "<temp-dump>" -Encoding utf8
56
+ # Then post-process: strip leading "request-id:" lines, keep transcript body.
57
+ ```
58
+
59
+ ### Meeting transcript (FULL VERBATIM)
60
+
61
+ ```text
62
+ Find the Teams meeting titled "<subject>" that occurred on <YYYY-MM-DD>. Return the full transcript verbatim with speaker labels and timestamps. Do not summarize.
63
+ ```
64
+
65
+ Doubled-strict retry (if first returns a summary):
66
+
67
+ ```text
68
+ Return the COMPLETE, RAW transcript of meeting "<subject>" on <YYYY-MM-DD>. I need every speaker turn in order. Do NOT summarize. Do NOT paraphrase. Do NOT skip any segments. Output as plain text with speaker name colon turn-text per line.
69
+ ```
70
+
71
+ Known-good evidence: JD FDE Intake (2026-05-13) → 8.4KB plain-text transcript with 5+ speakers, first try, request-id `54d9c6bc-6e56-43b6-9eb7-ac23e86e2cc0`, kushi v3.11.0 2026-05-18.
72
+
73
+ ### Meeting Copilot recap / facilitator notes
74
+
75
+ ```text
76
+ Get the Copilot meeting recap (decisions, action items, key points) for the Teams meeting "<subject>" on <YYYY-MM-DD>. Return the FULL recap card verbatim. Do not summarize.
77
+ ```
78
+
79
+ ### Teams chat thread (HUMAN-READABLE)
80
+
81
+ ```text
82
+ Show the full message-by-message reproduction of the Teams chat titled "<topic>" between <YYYY-MM-DD> and <YYYY-MM-DD>. Include sender, ISO timestamp, and full message body for every message. Do not skip, do not summarize.
83
+ ```
84
+
85
+ (Run `m365_list_chat_messages(chatId)` in parallel for the structured JSON dump.)
86
+
87
+ ### OneNote section / page
88
+
89
+ **Three-tier output (CRITICAL — picked the wrong tier = wrong fidelity):**
90
+
91
+ WorkIQ returns OneNote content in three distinct tiers. Each tier is a different prompt. Asking the wrong prompt for the wrong tier wastes the call AND makes the skill think OneNote is broken.
92
+
93
+ | Tier | What WorkIQ returns | Bulk? | Use for |
94
+ |---|---|---|---|
95
+ | **A. Enumeration** | titles + `wdpartid` + `wdsectionfileid` + last-modified + author + deep-link per page | YES — all pages, one call | bootstrap discovery; mutable hints |
96
+ | **B. Search snippet** | ~500-char Graph-search snippet per page (verbatim from search index) | YES — all pages, one call | quick coverage scan; NOT verbatim body substitute |
97
+ | **C. Full verbatim body** | full page text, every paragraph, every table | **NO — ONE page per call**; bulk request is refused with `combined content size … exceeds the maximum response payload that Copilot can safely render in-chat` (HCA evidence 2026-05-13 request-id `fdbf3290-649d-4066-84ea-ceab678697ed`) | snapshot/ verbatim per `pull-onenote` |
98
+
99
+ **DO NOT** ask WorkIQ for "all pages' full bodies in one call." That's the defect signature — it silently degrades to tier A+B and the skill thinks verbatim is unavailable.
100
+
101
+ **Tier A — Enumeration prompt:**
102
+
103
+ ```text
104
+ Search Microsoft 365 OneNote for sections matching the name "<name>". For each match return: section display name, wdsectionfileid, wdsectiongroupid, wdsectiononenoteguid, parentReferenceId (notebook), sourceDoc URL. Flat table, no commentary, no truncation.
105
+ ```
106
+
107
+ **Tier A — Per-section page index prompt (after section is resolved):**
108
+
109
+ ```text
110
+ List EVERY page in OneNote section "<section>.one" (wdsectionfileid <id>). For each page return: title, wdpartid, lastModifiedDateTime, lastModifiedBy, and a deep-link URL. Flat table only, no commentary, no truncation, no summary.
111
+ ```
112
+
113
+ **Tier B — Search snippets prompt:**
114
+
115
+ ```text
116
+ Search OneNote for pages associated with section "<section>.one" (sourceDoc <guid>). For each page return: page title and the exact visible snippet text returned by search (verbatim, not summarized, not rewritten).
117
+ ```
118
+
119
+ **Tier C — Single-page full body prompt (ONE page per call):**
120
+
121
+ ```text
122
+ Get the FULL verbatim body of OneNote page "<title>" (wdpartid <id>) in section "<section>.one". Return every paragraph, every heading, every embedded table, in order, no summarization, no truncation. If you cannot return the full body, say "body-unavailable" exactly and nothing else.
123
+ ```
124
+
125
+ **Per `pull-onenote` v2.9.0+**: Playwright browser-scrape is the PRIMARY path for tier-C bulk capture (16/16 HCA pages tested 2026-05-14) because WorkIQ tier C is one-page-per-call and rate-prone. WorkIQ tier C remains the fallback when the Playwright auth profile expires. Browser-scrape is NOT a Graph call — it's UI automation against OneNote-for-Web — and is therefore compatible with this workiq-only rule.
126
+
127
+ Discovery variant (used at bootstrap to resolve `wdsectionfileid`):
128
+
129
+ ```text
130
+ Search Microsoft 365 OneNote for sections matching the name "<name>". For each match return: section display name, wdsectionfileid, wdsectiongroupid, wdsectiononenoteguid, parentReferenceId (notebook), sourceDoc URL. Flat table, no commentary, no truncation.
131
+ ```
132
+
133
+ ### Email bodies
134
+
135
+ ```text
136
+ Find emails about "<project>" in folder "<folder>" between <YYYY-MM-DD> and <YYYY-MM-DD>. For each email return: sender, recipients, subject, full body verbatim, and reply chain. Do not summarize, do not truncate.
137
+ ```
138
+
139
+ ### SharePoint file content (text-extracted)
140
+
141
+ ```text
142
+ List SharePoint files for "<project>" modified between <YYYY-MM-DD> and <YYYY-MM-DD>. For each file return: file name, URL, author, last-modified, and the full text content (paragraphs and tables) verbatim. Do not summarize.
143
+ ```
144
+
145
+ ### Calendar / online meetings discovery (when joinUrl is unknown)
146
+
147
+ ```text
148
+ List my Teams meetings between <YYYY-MM-DD> and <YYYY-MM-DD> where the subject contains "<token>". Return subject, date, start time, organizer, joinUrl, and Teams chat id.
149
+ ```
150
+
151
+ ## Pre-flight (every M365 pull-* skill)
152
+
153
+ Before the first WorkIQ query in a run:
154
+
155
+ 1. Resolve CLI path: `<USER_HOME>/.copilot/project-evidence.yml workiq.cli_path` OR `Get-Command workiq`. If missing → log `workiq-not-on-path`, write evidence file pointing at install docs, STOP this source.
156
+ 2. Probe with `workiq ask -q "ping"`. If EULA prompt → `workiq accept-eula` once, retry.
157
+ 3. Capture `--version` once into run-log for audit.
158
+
159
+ ## Coverage.md requirements
160
+
161
+ For every WorkIQ-sourced artifact, coverage.md MUST include:
162
+
163
+ ```
164
+ - Source: WorkIQ (workiq.cmd ask)
165
+ - Query: "<exact prompt>"
166
+ - Request-id: <from workiq stderr>
167
+ - Result: SUCCESS | EMPTY | SUMMARY-ONLY (after doubled-strict retry)
168
+ - Artifact: <relative path>, <bytes>
169
+ - Fidelity: full-verbatim | speaker-turns-plain-text | summary-only-warning
170
+ ```
171
+
172
+ If `Result: SUMMARY-ONLY` after doubled-strict retry: the artifact is acceptable but MUST carry the WARNING header per the artifact-class doctrine (e.g. `transcript-source.md` for meetings). It is NOT a substitute for full-verbatim and the source is flagged in run-log.
173
+
174
+ ## Anti-patterns (defects)
175
+
176
+ 1. **Calling `m365_get_transcript`, `m365_get_facilitator_notes`, `m365_list_meetings`, `m365_list_events` from a kushi pull-* skill.** FORBIDDEN. These tools have a near-100% failure rate in this workspace. Use WorkIQ.
177
+ 2. **Using Graph REST URLs directly** (e.g. `https://graph.microsoft.com/v1.0/me/onlineMeetings/...`) from a kushi pull-* skill. FORBIDDEN for the in-scope M365 sources. Use WorkIQ.
178
+ 3. **Re-discovering the right WorkIQ prompt every run.** FORBIDDEN. Use the codified prompts from the table above. If a new source variant is needed, add a row to the table, do not improvise.
179
+ 4. **Stopping after one weak WorkIQ result.** WorkIQ's first response can be a summary; the doubled-strict retry is REQUIRED before falling back to user-paste.
180
+ 5. **Treating user-paste as an error.** It is a first-class evidence path. Coverage.md should label it `Source: User paste on <ISO>` and the artifact stands.
181
+ 6. **Mixing Graph fallbacks with WorkIQ in coverage.md.** If a skill's coverage trail shows `m365_get_transcript: failed` followed by `WorkIQ: success`, the skill is using the old `workiq-first` cascade. Remove the forbidden step.
182
+
183
+ ## Migration note
184
+
185
+ `workiq-first.instructions.md` (v3.7.0) is **deprecated as of kushi v3.11.0** for M365 sources. It remains valid only for the parts that talk about "ask user to paste" as a first-class path. All kushi pull-* skills that pull M365 content (`pull-meetings`, `pull-teams`, `pull-onenote`, `pull-email`, `pull-sharepoint`) must cite `workiq-only.instructions.md` in their front contracts blockquote from v2.x onward. The Graph-fallback paragraph in workiq-first is OVERRIDDEN by this rule.
186
+
187
+ ## Cross-references
188
+
189
+ - `plugin/instructions/meetings-verbatim-required.instructions.md` — transcript artifact classification (transcript.txt is the WorkIQ-derived tier for plain-text speaker turns).
190
+ - `plugin/instructions/crm-bootstrap-discovery.instructions.md` — CRM is OUT of WorkIQ scope; it uses Dataverse REST.
191
+ - `plugin/instructions/ado-bootstrap-discovery.instructions.md` — ADO is OUT of WorkIQ scope; it uses ADO REST.
192
+ - `plugin/instructions/scope-boundaries.instructions.md` — every pull-* still respects integrations.yml#boundaries.
193
+ - `plugin/instructions/evidence-thoroughness.instructions.md` — verbatim is the bar; this rule operationalizes how to hit it.
@@ -0,0 +1,50 @@
1
+ # Learnings register
2
+
3
+ Per-source register of API quirks, fixes, and workarounds discovered during real kushi runs.
4
+
5
+ Governed by [`../instructions/capture-learnings.instructions.md`](../instructions/capture-learnings.instructions.md). Every `pull-*` skill MUST read its register at preflight and append new entries the moment a fix lands mid-run.
6
+
7
+ ## Files
8
+
9
+ | File | Owner skill | Scope |
10
+ |---|---|---|
11
+ | [`ado.md`](./ado.md) | `pull-ado` | Azure DevOps REST + WIQL quirks. |
12
+ | [`crm.md`](./crm.md) | `pull-crm` | Dataverse REST, OData expand quirks, env overrides. |
13
+ | [`email.md`](./email.md) | `pull-email` | Outlook / Graph mail quirks. |
14
+ | [`teams.md`](./teams.md) | `pull-teams` | Teams chat / channel message quirks. |
15
+ | [`meetings.md`](./meetings.md) | `pull-meetings` | Calendar / transcript / online meeting quirks. |
16
+ | [`onenote.md`](./onenote.md) | `pull-onenote` | OneNote page / section quirks. |
17
+ | [`sharepoint.md`](./sharepoint.md) | `pull-sharepoint` | SharePoint / OneDrive file enumeration quirks. |
18
+ | [`cross-cutting.md`](./cross-cutting.md) | (any) | Auth, encoding, PowerShell, host-tool quirks that span sources. |
19
+
20
+ ## How to add an entry
21
+
22
+ 1. The moment a fix or workaround lands during a run, prepend a new entry to the matching file.
23
+ 2. Use the standard format (newest on top):
24
+
25
+ ```markdown
26
+ ### YYYY-MM-DD — short title
27
+
28
+ **Symptom**: ...
29
+
30
+ **Root cause**: ...
31
+
32
+ **Fix / workaround**: ...
33
+
34
+ **Doctrine impact**: ... (or "register-only — TODO promote on next sighting")
35
+
36
+ **Discovered during**: project / skill
37
+ ```
38
+
39
+ 3. If the fix also changes how the skill should always behave, edit the SKILL.md or related instructions file in the **same commit**.
40
+ 4. Never delete entries. To supersede, add a one-liner at the bottom of the old entry: `→ superseded by YYYY-MM-DD entry above`.
41
+
42
+ ## Promotion rules
43
+
44
+ After two sightings of the same learning across projects/runs, **promote** the rule from register to first-class doctrine (SKILL.md or a new `*.instructions.md`). Leave a breadcrumb in the register entry:
45
+
46
+ ```
47
+ → promoted to plugin/instructions/<file>.md on YYYY-MM-DD
48
+ ```
49
+
50
+ Pre-promotion (one sighting) clutters the SKILL.md with one-offs. Wait for the second.
@@ -0,0 +1,45 @@
1
+ # Learnings — Azure DevOps (`pull-ado`)
2
+
3
+ Newest on top. Format defined in [`README.md`](./README.md).
4
+
5
+ ---
6
+
7
+ ### 2026-05-13 — `$top=500` exceeds permissible range on workItems updates endpoint
8
+
9
+ **Symptom**: `GET wit/workItems/{id}/updates?$top=500&api-version=7.1` returns `VS402337: The value provided for the query string parameter '$top' is outside the permissible range.` Result: 0 updates fetched, revision-history section is empty.
10
+
11
+ **Root cause**: ADO REST `$top` max on `/updates` is **200**, not 500 like some sibling endpoints.
12
+
13
+ **Fix / workaround**: Use `$top=200`. If a WI has >200 revisions, paginate with `$skip` or omit `$top` and let the server default apply.
14
+
15
+ **Doctrine impact**: `plugin/skills/pull-ado/SKILL.md` — added explicit `$top=200` to the per-WI pull pseudocode (v3.7.6).
16
+
17
+ **Discovered during**: `HCA / pull-ado` — engagement-96944 + child 96571 both got 0 updates on first pull.
18
+
19
+ ---
20
+
21
+ ### 2026-05-13 — `[Custom.ISCRMRequestId]` field does not exist in IS Engagements WIQL schema
22
+
23
+ **Symptom**: `WIQL` query with `[Custom.ISCRMRequestId] = 'FE-2026-001458'` returns `TF51005: The query references a field that does not exist.`
24
+
25
+ **Root cause**: Earlier doctrine assumed a custom field named `ISCRMRequestId` existed on Engagement WIs to cross-link to CRM. The field was never created in the IS Engagements project schema. The actual link is via title/tags scan.
26
+
27
+ **Fix / workaround**: Drop the `[Custom.ISCRMRequestId]` clause from any WIQL probe. Use the title-substring + Tags scan path instead, then disambiguate by customer name in the title.
28
+
29
+ **Doctrine impact**: `plugin/skills/pull-ado/SKILL.md` — removed the custom-field clause from the resolution-order template (v3.7.6).
30
+
31
+ **Discovered during**: `HCA / pull-ado`.
32
+
33
+ ---
34
+
35
+ ### 2026-05-13 — PowerShell `${id}` interpolation required when next char is `?` or `/`
36
+
37
+ **Symptom**: `Invoke-RestMethod -Uri "$base/workItems/$id?api-version=7.1"` fails because PowerShell parses `$id?` as a single variable and looks for `id?` (which doesn't exist), so the URL becomes malformed.
38
+
39
+ **Root cause**: PowerShell variable name parsing greedy-extends until a non-identifier char. `?` and `/` are valid identifier chars in some contexts.
40
+
41
+ **Fix / workaround**: Always wrap WI-ID interpolation as `${id}` when followed by `?`, `/`, `:`, or `(` — e.g. `"$base/workItems/${id}?api-version=7.1"`.
42
+
43
+ **Doctrine impact**: register-only — TODO promote to a `powershell-quirks.instructions.md` if it bites a second time.
44
+
45
+ **Discovered during**: `HCA / pull-ado` per-WI pull script.
@@ -0,0 +1,96 @@
1
+ # Learnings — CRM / Dataverse (`pull-crm`)
2
+
3
+ Newest on top. Format defined in [`README.md`](./README.md).
4
+
5
+ ---
6
+
7
+ ## 2026-05-18 — Bootstrap declared `crm.disabled: true` from a shallow probe; live REST resolved instantly
8
+
9
+ ### What happened
10
+ John Deere bootstrap (kushi v3.10.x, 2026-05-18 morning) wrote `boundaries.crm.disabled: true, reason: 'no-crm-record-discovered-during-bootstrap-2026-05-18'` to `John Deere/integrations.yml`. As a result, `pull-crm` was never dispatched on bootstrap or on later refreshes, the JD `Evidence/ushak/crm/` folder stayed empty, and the user asked "why is there nothing in CRM?" the same day.
11
+
12
+ A live Dataverse REST probe afternoon of 2026-05-18 — `GET /new_frontierengineeringtriages?$filter=contains(new_title,'Deere')` — returned **1 exact match in <1s**: `FE-2026-001791 — John Deere Dealer Operations (JDDO) Platform Integration Improvement`, status `4 - Technical Assessment`, customer `DEERE COMPANY`. The accounts-fallback (Step 2) also resolved cleanly (3 Deere-named accounts; `DEERE COMPANY` linked to the same FE record).
13
+
14
+ ### Why this was a defect
15
+ The pull-crm SKILL `Resolution order` section already documented the 4-step sequence (title → account → recent-slice → ask) — but bootstrap was not actually executing it. It appears bootstrap relied on a WorkIQ-only / metadata-only probe that didn't reach Dataverse, then silently wrote `disabled: true`. That is the worst possible disposition: it pretends there is no CRM record, hides the failure from future refreshes, and the project loses CRM evidence entirely.
16
+
17
+ Adjacent observation: Nova (sibling tool) does this resolution sequence in its bootstrap and would not have missed FE-2026-001791. The doctrine was present in kushi but the execution path was weak.
18
+
19
+ ### Fix
20
+ - New HARD-rule instruction `plugin/instructions/crm-bootstrap-discovery.instructions.md` — `disabled: true` is ONLY allowed after the FULL 4-step REST sequence returns 0 AND the user is presented with top candidates. Auth/reachability failures must leave the boundary EMPTY (with `reason: 'crm-auth-unavailable-<date>'`) so the next refresh retries — NEVER write `disabled: true`.
21
+ - New HARD-rule instruction `plugin/instructions/crm-internal-vs-confirmed.instructions.md` — once resolved, CRM field values must be tagged `CRM-only` / `Cross-verified` / `Conflicting evidence` and never collapsed into bare assertions in State/, FDE reports, or ask-project answers.
22
+ - `plugin/skills/pull-crm/SKILL.md` v2.2.0 → v2.3.0 — Resolution order hardened with: (a) anti-patterns block at top (no statecode filter, `new_companyname` is NOT a valid attribute, never give up on one probe, iterate ALL matching accounts in Step 2 not just the first), (b) new Step 3 wide-text fallback (`new_businessscenariotechnicalblocker`, `new_engagedwith`, `new_engagementobjectives`), (c) renumbered to 5 steps with explicit user-ask, (d) required attempt-trail logging to refresh-report.
23
+ - `plugin/skills/bootstrap-project/SKILL.md` v2.2.0 → v2.3.0 — Step 4 boundaries gate now references the new instruction and explicitly requires the live 4-step sequence before `disabled: true`.
24
+
25
+ ### Verification
26
+ - JD `integrations.yml` updated: `record_ids: ['FE-2026-001791']`, `request_ids: ['FE-2026-001791']`, `boundaries.crm.disabled` removed, `crm.record_id` / `entity_set` / `customer_account_id` / `resolution_path` populated.
27
+ - Full record + 3 annotations pulled and rendered to `Evidence/ushak/crm/snapshot/new_frontierengineeringtriages/1b529705-2340-f111-88b3-00224803accf.md` (9.5 KB; 58 fields + 3 verbatim notes; long-text `new_businessscenariotechnicalblocker` includes the full JDDO platform pain-points narrative).
28
+
29
+ ### Pattern to remember
30
+ `disabled: true` for CRM is a load-bearing disposition. It MUST be earned by the full resolution sequence, not assumed from absence-of-quick-match. When in doubt, leave the boundary empty so the next refresh retries.
31
+
32
+ ---
33
+
34
+ ### 2026-05-13 — Custom entities don't expose `Annotations` as a navigation property
35
+
36
+ **Symptom**: `GET /api/data/v9.2/new_frontierengineeringtriages(<id>)?$expand=Annotations(...)` returns `0x80060888: Could not find a property named 'Annotations' on type 'Microsoft.Dynamics.CRM.new_frontierengineeringtriage'.`
37
+
38
+ **Root cause**: For OOB entities like `incident`, the `Annotations` navigation collection is generated. For custom entities (prefix `new_`, `crXXX_`, etc.), `$expand=Annotations` is NOT auto-wired even though `annotations` is a polymorphic regarding entity that targets all entities. The expand collection name on the parent entity doesn't exist.
39
+
40
+ **Fix / workaround**: Two requests instead of one expand:
41
+ 1. `GET /api/data/v9.2/<entitySet>(<id>)` — record alone.
42
+ 2. `GET /api/data/v9.2/annotations?$filter=_objectid_value eq <id>&$select=annotationid,subject,notetext,createdon,_createdby_value,filename,mimetype&$orderby=createdon asc` — notes filtered by `_objectid_value`.
43
+
44
+ Both with the formatted-value Prefer header. Merge in code.
45
+
46
+ **Doctrine impact**: `plugin/skills/pull-crm/SKILL.md` — replaced the single `$expand=Annotations` template with the two-request pattern (v3.7.6). Marked the expand pattern OK only for `incident`/`account`/`contact`/`opportunity` and other OOB entities.
47
+
48
+ **Discovered during**: `HCA / pull-crm` — record `e561b31e-...` returned 0 annotations on the expand call; the separate-filter call returned 24.
49
+
50
+ ---
51
+
52
+ ### 2026-05-13 — FDE intake records live in `iscrm.crm.dynamics.com`, not the global default `microsoftit.crm.dynamics.com`
53
+
54
+ **Symptom**: HCA record search via the global `.project-evidence/crm/config.yml` (`environment_url: microsoftit.crm.dynamics.com`, `entity_set: msdyn_engagements`) returned 0 hits. The record exists.
55
+
56
+ **Root cause**: Two distinct CRM environments are in play for IS:
57
+ - `microsoftit.crm.dynamics.com` / `msdyn_engagements` — generic IS engagement entity.
58
+ - `iscrm.crm.dynamics.com` / `new_frontierengineeringtriages` — FDE intake triage entity (custom, `new_` prefix).
59
+
60
+ FDE-intake projects (anything with an FE-YYYY-NNNNNN request id) need the iscrm env override.
61
+
62
+ **Fix / workaround**: Per-project pin in `<project>/integrations.yml`:
63
+ ```yaml
64
+ crm:
65
+ environment_url_override: 'https://iscrm.crm.dynamics.com'
66
+ entity_set_override: 'new_frontierengineeringtriages'
67
+ title_field: 'new_title'
68
+ request_id_field: 'new_requestid'
69
+ customer_lookup: '_new_customer_value'
70
+ ```
71
+ Resolution order: project override > global default. Detect FDE-intake by request id format `^FE-\d{4}-\d{6}$` and prompt to apply the override automatically.
72
+
73
+ **Doctrine impact**:
74
+ - `plugin/skills/pull-crm/SKILL.md` — added FDE-env-override block + auto-detect heuristic (v3.7.6).
75
+ - `plugin/skills/bootstrap-project/SKILL.md` — when seeding `integrations.yml`, ask the user about FDE intake and pre-fill the override block.
76
+ - `plugin/templates/init/integrations.template.yml` — added commented-out override block.
77
+
78
+ **Discovered during**: `HCA / pull-crm`.
79
+
80
+ ---
81
+
82
+ ### 2026-05-13 — Always send the formatted-value Prefer header
83
+
84
+ **Symptom**: Without the header, lookups return only GUIDs (`_new_customer_value: <guid>`) and option-set fields return only numeric codes (`statuscode: 4`). The output is unreadable.
85
+
86
+ **Root cause**: Dataverse OData strips the human-readable formatted values unless the client opts in.
87
+
88
+ **Fix / workaround**: Every Dataverse REST call MUST include:
89
+ ```
90
+ Prefer: odata.include-annotations="OData.Community.Display.V1.FormattedValue"
91
+ ```
92
+ Then read formatted values from `<field>@OData.Community.Display.V1.FormattedValue` properties on the response.
93
+
94
+ **Doctrine impact**: Already in `plugin/skills/pull-crm/SKILL.md` since v3.7.4. Reaffirmed in v3.7.6 with a worked example.
95
+
96
+ **Discovered during**: `HCA / pull-crm` — initial run forgot the header and lookups came back as raw GUIDs.
@@ -0,0 +1,36 @@
1
+ # Learnings — Cross-cutting
2
+
3
+ Newest on top. Format defined in [`README.md`](./README.md). Use this file when a learning spans multiple `pull-*` skills (auth tokens, encoding, PowerShell quirks, host-tool behavior).
4
+
5
+ ---
6
+
7
+ ### 2026-05-13 — Two CRM environments coexist for IS (microsoftit vs iscrm)
8
+
9
+ **Symptom**: Skills assumed a single global CRM env. FDE-intake projects (HCA) live in a different env from generic IS engagements.
10
+
11
+ **Root cause**: Org-level split — see [`crm.md`](./crm.md) for the full entry.
12
+
13
+ **Fix / workaround**: See `crm.md` — per-project override in `integrations.yml`. Cross-cutting note: any future skill that touches Dataverse must read `crm.environment_url_override` and `crm.entity_set_override` from `<project>/integrations.yml` BEFORE falling back to the global config.
14
+
15
+ **Doctrine impact**: tracked in `crm.md` and `pull-crm/SKILL.md`.
16
+
17
+ **Discovered during**: `HCA / pull-crm`.
18
+
19
+ ---
20
+
21
+ ### 2026-05-13 — Mutable hint cache vs project integrations.yml: write to BOTH
22
+
23
+ **Symptom**: Manual HCA refresh pinned resolved IDs only into `<project>/integrations.yml`. The cross-skill mutable cache `<engagement-root>/.project-evidence/m365/m365-mutable.json` was not touched. Next-run fast-path won't fire for sibling skills that read the mutable cache.
24
+
25
+ **Root cause**: `side-by-side-config.instructions.md` says to upsert mutable hints during the run, but the rule wasn't enforced — and skill SKILL.md files mention it inconsistently.
26
+
27
+ **Fix / workaround**: Every `pull-*` skill MUST upsert to BOTH stores in the same turn the ID is resolved:
28
+ 1. `<project>/integrations.yml` under `<source>.<key>` — source of truth, OneDrive-synced.
29
+ 2. `<engagement-root>/.project-evidence/m365/m365-mutable.json` under `m365Mutable.knownSections.<project>.<source>.<key>` — speed cache for cross-skill reuse, with `discoveredOn` + `confidence`.
30
+
31
+ **Doctrine impact**:
32
+ - `plugin/instructions/side-by-side-config.instructions.md` — strengthened "discover → upsert immediately" with explicit dual-store contract (v3.7.6).
33
+ - All `pull-*` SKILL.md files — each pull skill's "Mutable hints to upsert" section now explicitly lists both files (v3.7.6).
34
+ - `plugin/skills/self-check/run.ps1` — new D8 rule: warn if `<project>/integrations.yml` has resolved `ado.engagement_id` or `crm.record_id` but `m365-mutable.json` lacks the matching `knownSections.<project>` entry.
35
+
36
+ **Discovered during**: `HCA / pull-crm + pull-ado` — user asked "did you make sure you updated the config files for all these to be able to do better next time".
@@ -0,0 +1,33 @@
1
+ # Learnings — Email (`pull-email`)
2
+
3
+ Newest on top. Format defined in [`README.md`](./README.md).
4
+
5
+ ## 2026-05-18 — `pull-email` rewritten to WorkIQ-ONLY (kushi v3.11.1); `m365_get_email` / `m365_search_emails` / Graph REST FORBIDDEN
6
+
7
+ **What happened.** During John Deere bootstrap (2026-05-18), email evidence was missing entirely. Root cause: prior pull-email SKILL v2.2.0 listed `m365_get_email` + `m365_search_emails` as the **preferred** body-fetch path with WorkIQ as fallback. In this workspace those host tools have a near-100% failure rate (Tool execution failed / 401 / empty payloads), so pull-email runs gave up before reaching WorkIQ.
8
+
9
+ **Fix.**
10
+ 1. Ran the WorkIQ root-scope keyword query for JD (aliases: John Deere, Deere, JDDO, JDIS, deere.com, johndeere.com; window 2026-04-01 → 2026-05-18). 3 emails returned first try (FDE Intake thread, folder `109. John Deere`), request-ids `941b2dda-3db2-4735-8e4f-1974f6436b3e` and `e26a14ed-e592-4323-8de7-7f197a047318`.
11
+ 2. Wrote durable JD artifacts under `John Deere/email-context/`: `index.md`, `2026-05-18-1100-email-summary.md`, `current-state.md`.
12
+ 3. Upserted `m365Mutable.knownSections."John Deere".emailContext.folder = "109. John Deere"` with `confidence: high` so next run uses the folder-scoped fast path.
13
+ 4. Bumped `pull-email` SKILL v2.2.0 → v2.3.0 with HARD `workiq-only` contract:
14
+ - Front blockquote cites `workiq-only.instructions.md` (v3.11.0).
15
+ - Step B "Per-message body fetch" rewritten — only WorkIQ allowed.
16
+ - Tools section rewritten — `m365_get_email` / `m365_search_emails` / `m365_list_emails` / `m365_list_mail_folders` / `m365_search_mail` / Graph REST URLs explicitly listed as FORBIDDEN.
17
+ - User-paste promoted to first-class fallback (NOT a degradation).
18
+ 5. Same treatment applied to `pull-teams` (v2.0.0 → v2.1.0) and `pull-sharepoint` (v2.0.0 → v2.1.0). `m365_list_chat_messages` retained for `pull-teams` as parallel **structured-data dump** (writes `chat-messages.json`) — explicitly NOT a fallback for the human-readable thread.
19
+
20
+ **Codified canonical prompts** (do not re-derive):
21
+
22
+ - Root-scope discovery (use when no folder hint exists for the project):
23
+ ```
24
+ workiq ask -q "Search my Outlook mailbox for emails from <floor> onward in all folders (including subfolders) related to <aliases>, or from any sender from <sender_domains>. For each email return: sent datetime, subject, sender, recipients, folder path, and a short relevance reason."
25
+ ```
26
+ - Folder-scoped fast path (use when `emailContext.folder` is pinned in mutable with `confidence >= medium`):
27
+ ```
28
+ workiq ask -q "Search my Outlook mailbox for emails from <floor> onward in folder(s) '<folder>' (including all subfolders) related to <aliases>. Return: sent datetime, subject, sender, recipients, folder path, message link, and a short relevance reason. Do not return NO_RESULTS unless the folder truly has no messages in that range."
29
+ ```
30
+ - Per-message body fetch (codified in `workiq-only.instructions.md`).
31
+
32
+ **Defect signature to watch for.** `pull-email` coverage trail showing `m365_get_email: failed` or `m365_search_emails: 0 results` followed by either silence (skill gave up) or a WorkIQ retry. Either pattern is the v2.2.0 cascade leaking through — must be eradicated.
33
+
@@ -0,0 +1,30 @@
1
+ # Learnings — Meetings (`pull-meetings`)
2
+
3
+ Newest on top. Format defined in [`README.md`](./README.md).
4
+
5
+ ## 2026-05-18 — Meetings need a verbatim/ folder because meetings EXPIRE
6
+
7
+ ### What happened
8
+ During John Deere bootstrap on 2026-05-18, `pull-meetings` produced the FDE Intake (2026-05-13) curated 7-section block in `Evidence/ushak/meetings/snapshot/FDE-Intake-John-Deere-2026-05-13.md`. Source was reconstructed from chat (22 messages) because `m365_get_transcript` returned "Tool execution failed" and `m365_get_facilitator_notes` was unavailable. A sibling tool (running independently) successfully pulled a much richer WorkIQ-derived summary AND captured the raw recording URL, AND noted in its output that the recording is at `https://microsoft-my.sharepoint-df.com/.../Recordings/FDE%20Intake%20-%20John%20Deere-20260513_123238-Meeting%20Recording.mp4` — a URL that will be purged when tenant retention kicks in (default ~60 days).
9
+
10
+ ### Why this was a defect
11
+ The curated snapshot, however rich, is a SUMMARY. The 22 raw chat messages, the recording URL, the WorkIQ-returned Copilot summary text, and any shared chat attachments are the **source-of-truth verbatim**. Kushi was not persisting any of them as immutable raw artifacts; the snapshot file is curated text that cannot be re-derived once the recording disappears. Every other evidence class (email, OneNote, SharePoint, CRM, ADO) persists in its source system for years — meetings are the ONE class where the source expires.
12
+
13
+ If the user had asked "what exactly did Sachin say in the chat?" two months from now, the answer would have been "the chat is gone; the recording is gone; the snapshot is what we have." That is unacceptable when the chat thread WAS retrievable at capture time.
14
+
15
+ ### Fix
16
+ - New instruction: `plugin/instructions/meetings-verbatim-required.instructions.md` (HARD rule, v3.10.0, hardened in v3.10.1). Every captured meeting MUST produce `Evidence/<alias>/meetings/verbatim/<YYYY-MM-DD-HHMM>_<slug>/` containing a transcript-class file (`transcript.vtt` preferred, else `transcript.txt`, else `transcript-source.md` with WARNING header). Chat alone is NOT a transcript — the transcript cascade is exhaustive: `m365_get_transcript` → Graph REST `/onlineMeetings/.../transcripts/.../content` → `m365_get_facilitator_notes` → WorkIQ strict full-text pull → recording download.
17
+ - `pull-meetings` bumped 2.1.0 → 2.3.0. Cascade restructured into Half A (verbatim/ capture, transcript-first, REQUIRED first) → Half B (curated stream/ block citing verbatim files).
18
+ - New template: `templates/snapshot/meeting-verbatim.template.md` (Validation enforces transcript-class file presence).
19
+ - `templates/weekly/meetings-stream.template.md` per-meeting block now requires a `Verbatim folder` field + 3 validation checks (folder exists, non-empty, transcript-class present).
20
+ - `templates/snapshot/meetings-series-index.template.md` Notes section documents the verbatim/ sibling.
21
+ - `verbatim-by-default.instructions.md` adds anti-pattern #9.
22
+ - `self-check/run.ps1` adds deep-mode rule D13 (a/b/c): walks stream/*.md, warns if (a) verbatim/<dir> missing, (b) verbatim/<dir> empty, (c) verbatim/<dir> has chat but NO transcript-class file.
23
+
24
+ ### Verification
25
+ After this fix lands, John Deere FDE Intake was backfilled: `Evidence/ushak/meetings/verbatim/2026-05-13-1530_fde-intake-john-deere/` contains chat-messages.json (22 msgs), chat-messages.md (rendered), transcript-source.md (WorkIQ summary), recording-url.txt, coverage.md, captured-at.txt. Recording URL is now captured locally even though the file itself will eventually expire.
26
+
27
+ ### Source
28
+ - User feedback 2026-05-18 ~09:30 EDT: "why is there no meetings transcripts for john deere" followed by paste of sibling tool's rich output and "for meetings alone, create a verbatim folder and put verbatims in it. Since meetings expire, this is required for meetings alone" and "make this true skill and so. not just one off".
29
+
30
+
@@ -0,0 +1,46 @@
1
+ # Misc / external links — learnings
2
+
3
+ ## 2026-05-14 — v3.9.0 introduction
4
+
5
+ **Why pull-misc exists:** The dedicated `pull-*` skills cover the structured M365 surfaces, but projects routinely depend on evidence that lives outside — Loop pages, Learn articles, GitHub repos, PDFs, local files, public web pages. The External Links Context doctrine has tracked these as `<project>/external-links.txt` for a while; v3.9.0 promotes that file from "context for the LLM to read" to "first-class evidence with retry registry + snapshot files."
6
+
7
+ **Design decision: deterministic, not fuzzy.** Considered an auto-discovery approach (crawl SharePoint search / Teams chats / bookmarks for project-name mentions). Rejected for three reasons:
8
+
9
+ 1. SharePoint search-index freshness is non-deterministic — same crawl twice = different results.
10
+ 2. Cross-project bleed (a chat thread mentioning both A and B gets attributed to whichever project the crawl ran for).
11
+ 3. Tenant-wide access spans projects the user isn't consulting on — crawls leak content.
12
+
13
+ The user pasting links into `external-links.txt` is the right boundary: auditable, version-controllable, diffable, deterministic.
14
+
15
+ **Format preserved:** v3.9.0 does NOT change the existing `<type>|<owner>|<title>|<url-or-path>|<notes>` format. Just adds `loop` to the recognized types and builds a runner around it.
16
+
17
+ **Routing model:**
18
+
19
+ - `onenote / sharepoint / ado` → delegated to dedicated skills (recorded as `delegated` in registry, not double-pulled here).
20
+ - `loop` → browser path (Playwright, reuses `~/.copilot/playwright-profile/onenote/` because same M365 cookie scope).
21
+ - `web / learn / docs / github / confluence / pdf / unknown` → HTTP path (fetch + @mozilla/readability for HTML extraction).
22
+ - `file` → local file read.
23
+ - Anything matching `<PASTE_*_URL>` or `<TODO*>` → `placeholder`, surfaced in run report until filled.
24
+
25
+ **Test results 2026-05-14:**
26
+
27
+ - ABN AMRO `external-links.txt` (13 entries): 8 delegated (onenote+sharepoint+ado correctly skipped), 3 placeholders detected, 2 file links failed (paths point at customer_workspace/FDEDocs which doesn't exist on this machine — correct behavior, surfaced as fetch-failed not silently dropped).
28
+ - Synthetic test with `learn.microsoft.com/fabric/` and `httpbin.org/html`: 2/2 captured, Readability extracted clean text (~2KB and ~3KB respectively).
29
+
30
+ **Loop branch is structurally validated but not yet exercised against real Loop URLs** — no project's `external-links.txt` contains a `loop|` entry yet. The branch is a thin variation of pull-onenote v2.6.0 (same Playwright profile, same auth-required detection, body extraction via cascade `[data-loop-canvas]` → `.fui-FluentProvider` → `[role="main"]` → `body`). Will get its first real exercise when a user adds Loop links to a project.
31
+
32
+ **Anti-patterns codified:**
33
+
34
+ 1. ❌ Auto-discovering links by crawling SharePoint or Teams (non-deterministic, boundary-violating).
35
+ 2. ❌ Storing only `url` (not `(type, url)` tuple) as registry key.
36
+ 3. ❌ Silently dropping links when they disappear from `external-links.txt` — mark `removed`, keep snapshot for audit.
37
+ 4. ❌ Pulling delegated types here (creates conflicting evidence with the dedicated pull-* skill).
38
+ 5. ❌ Persisting placeholder URLs as `fetch-failed` — they're `placeholder`, distinct state.
39
+ 6. ❌ Loop link without Playwright profile silently writing empty snapshot — must mark `auth-required`.
40
+
41
+ **Lessons:**
42
+
43
+ 1. The format already existed — promoting it cost less than designing a new one. Always look for existing artifacts before inventing.
44
+ 2. Reusing the OneNote Playwright profile for Loop saved an entire bootstrap step. Same auth scope = same profile.
45
+ 3. Readability is sufficient for ~80% of "give me the body of this web page" cases. Fall back to raw stripped HTML only when it fails.
46
+ 4. Per-link retry registry (same shape as v3.8.0 OneNote `one_pages[]`) is the right durability primitive — survives auth gaps, surfaces placeholders, distinguishes states.