kushi-agents 5.0.0 → 5.0.2

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 (54) hide show
  1. package/README.md +30 -7
  2. package/bin/cli.mjs +73 -45
  3. package/package.json +51 -51
  4. package/plugin/instructions/agentskills-compliance.instructions.md +144 -0
  5. package/plugin/instructions/multi-host-install.instructions.md +125 -0
  6. package/plugin/instructions/plan-validate-execute.instructions.md +75 -0
  7. package/plugin/instructions/release-genealogy.instructions.md +52 -0
  8. package/plugin/skills/aggregate-project/SKILL.md +11 -2
  9. package/plugin/skills/apply-ado-update/SKILL.md +11 -2
  10. package/plugin/skills/ask-project/SKILL.md +1 -1
  11. package/plugin/skills/bootstrap-project/SKILL.md +39 -127
  12. package/plugin/skills/bootstrap-project/references/discovery-sweep.md +40 -0
  13. package/plugin/skills/bootstrap-project/references/pull-dispatch.md +50 -0
  14. package/plugin/skills/bootstrap-project/references/registry-persistence.md +55 -0
  15. package/plugin/skills/build-state/SKILL.md +50 -2
  16. package/plugin/skills/consolidate-evidence/SKILL.md +11 -2
  17. package/plugin/skills/dashboard/SKILL.md +20 -1
  18. package/plugin/skills/emit-vertex/SKILL.md +10 -1
  19. package/plugin/skills/fde-intake/SKILL.md +10 -1
  20. package/plugin/skills/fde-report/SKILL.md +10 -1
  21. package/plugin/skills/fde-triage/SKILL.md +10 -1
  22. package/plugin/skills/intro/SKILL.md +1 -1
  23. package/plugin/skills/link-entities/SKILL.md +43 -1
  24. package/plugin/skills/project-status/SKILL.md +1 -1
  25. package/plugin/skills/propose-ado-update/SKILL.md +11 -2
  26. package/plugin/skills/pull-ado/SKILL.md +26 -9
  27. package/plugin/skills/pull-crm/SKILL.md +39 -125
  28. package/plugin/skills/pull-crm/references/dataverse-doctrine.md +108 -0
  29. package/plugin/skills/pull-crm/references/legacy-shape.md +28 -0
  30. package/plugin/skills/pull-email/SKILL.md +33 -79
  31. package/plugin/skills/pull-email/references/retrieval-order.md +43 -0
  32. package/plugin/skills/pull-email/references/two-pass-pull.md +41 -0
  33. package/plugin/skills/pull-loop/SKILL.md +194 -177
  34. package/plugin/skills/pull-meetings/SKILL.md +35 -72
  35. package/plugin/skills/pull-meetings/references/legacy-stream.md +15 -0
  36. package/plugin/skills/pull-meetings/references/verbatim-capture.md +61 -0
  37. package/plugin/skills/pull-misc/SKILL.md +24 -7
  38. package/plugin/skills/pull-onenote/SKILL.md +207 -555
  39. package/plugin/skills/pull-onenote/references/playwright-fallback.md +111 -0
  40. package/plugin/skills/pull-onenote/references/preflight.md +85 -0
  41. package/plugin/skills/pull-onenote/references/runtime-contract.md +118 -0
  42. package/plugin/skills/pull-sharepoint/SKILL.md +26 -9
  43. package/plugin/skills/pull-teams/SKILL.md +26 -9
  44. package/plugin/skills/refresh-project/SKILL.md +24 -2
  45. package/plugin/skills/self-check/SKILL.md +9 -1
  46. package/plugin/skills/self-check/run.ps1 +216 -4
  47. package/plugin/skills/setup/SKILL.md +14 -120
  48. package/plugin/skills/setup/references/onedrive-pin-sync.md +60 -0
  49. package/plugin/skills/setup/references/recovery-prompts.md +81 -0
  50. package/plugin/skills/tour/SKILL.md +18 -1
  51. package/plugin/skills/vertex-link/SKILL.md +1 -1
  52. package/src/constants.mjs +39 -1
  53. package/src/multi-host-install.test.mjs +170 -0
  54. package/src/multi-host.mjs +277 -0
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: "pull-crm"
3
- version: "3.0.0"
4
- description: "v3.0.0 (kushi v4.9.0): Pull CRM (Dataverse) evidence as Comprehensive Structured Capture (CSC) blocks written to weekly/YYYY-MM-DD_crm-csc.md. One block per record modified that week, upserted in _index/entities.yml. Dataverse REST retrieval doctrine (Rules 1-6) UNCHANGED — only output shape changes. Record fields → CSC sections (owner→Participants, long-text→Topics, statecode→Decisions, etc.). No snapshot/+stream/ split. Per-contributor _index/. See weekly-csc + comprehensive-structured-capture doctrines."
3
+ version: "3.0.1"
4
+ description: "USE WHEN refresh-project / bootstrap-project dispatches CRM source OR the user says \"pull CRM for <X>\" AND the project has integrations.yml#boundaries.crm.entity_uri or hint set. DO NOT USE for non-kushi Dataverse queries. Capability: pulls CRM (Dataverse) evidence as CSC blocks written to weekly/YYYY-MM-DD_crm-csc.md, one block per record/contact touched, upserted in _index/entities.yml. WorkIQ-only; CRM-internal-vs-confirmed rule applies."
5
5
  ---
6
6
 
7
7
  # Skill: pull-crm
@@ -21,6 +21,14 @@ description: "v3.0.0 (kushi v4.9.0): Pull CRM (Dataverse) evidence as Comprehens
21
21
  > - ~~`verbatim-by-default.instructions.md`~~ (LEGACY — superseded by CSC).
22
22
  > - ~~`snapshot-vs-stream.instructions.md`~~ (LEGACY — superseded by weekly-csc).
23
23
 
24
+ ## Gotchas
25
+
26
+ - **CRM internal-vs-confirmed rule** — never assert a field as "confirmed by the customer" unless it appears in a customer-authored note / email / meeting transcript. Internal-only fields render as `(internal Dataverse note)`. See `crm-internal-vs-confirmed.instructions.md`.
27
+ - **Shallow probe can falsely declare `crm.disabled: true`** — empty Dataverse probe responses are NOT proof the source is disabled. Re-test via WorkIQ with a known account name before flagging disabled.
28
+ - **Record URLs include environment ID** — `crm://entity=account&id=<guid>&env=<env-id>`. Different environments mean different entities; never collapse cross-env records into one anchor.
29
+ - **Opportunity stage changes are stream events, not snapshot** — each stage transition writes a CSC bullet under "Dates & Numbers" with the timestamp; the snapshot stage is the latest.
30
+ - **Bulk-enumerate phrasings punt to admin Graph** — `"list all accounts in Dataverse"` returns nothing useful. Always scope to a hint (account name, opportunity number) per `crm-bootstrap-discovery.instructions.md`.
31
+
24
32
  ## v4.9.0 — Comprehensive Structured Capture (CSC) + weekly/ layout (HARD RULE; supersedes all snapshot/+stream/ guidance below)
25
33
 
26
34
  Per `comprehensive-structured-capture.instructions.md` and `weekly-csc.instructions.md`:
@@ -70,8 +78,8 @@ Per `comprehensive-structured-capture.instructions.md` and `weekly-csc.instructi
70
78
  - Open questions in notes → **Open Questions**
71
79
  - Customer-stated requirements → **Customer Asks**
72
80
  - Related records → **Artifacts/Links**
73
- - Entity id: `crm://entity=<logicalName>/id=<guid>`.
74
-
81
+ - Entity id: `crm://entity=<logicalName>/id=<guid>`.
82
+
75
83
  Pulls **crm** evidence in two shapes per `snapshot-vs-stream.instructions.md`:
76
84
 
77
85
  - **snapshot/** — engagement record with EVERY field the API returns (owner, status, account, dates, custom fields), all long-text fields VERBATIM, plus every related annotation (note) verbatim
@@ -133,137 +141,29 @@ Before issuing the snapshot fetch, inspect `crm.entitySetName`:
133
141
 
134
142
  ## Dataverse retrieval doctrine (REQUIRED — applies to every REST call below)
135
143
 
136
- Borrowed from production CRM-sync hardening. Every Dataverse REST call in this skill MUST follow these rules, or option-set / lookup fields will render as raw GUIDs and integer codes instead of human labels:
137
-
138
- ### Rule 1 — Always request formatted values
139
-
140
- ```powershell
141
- $headers = @{
142
- Authorization = "Bearer $token"
143
- Accept = 'application/json'
144
- 'OData-Version' = '4.0'
145
- 'OData-MaxVersion' = '4.0'
146
- Prefer = 'odata.include-annotations="OData.Community.Display.V1.FormattedValue"'
147
- }
148
- ```
149
-
150
- This makes Dataverse return paired properties like `statuscode@OData.Community.Display.V1.FormattedValue` and `_ownerid_value@OData.Community.Display.V1.FormattedValue` alongside the raw codes/GUIDs.
151
-
152
- ### Rule 2 — Use `PSObject.Properties[...]` accessor for `@`-named annotation properties
153
-
154
- Dotted access fails for property names containing `@`. Always use the indexer:
155
-
156
- ```powershell
157
- $ownerDisplay = $row.PSObject.Properties['_ownerid_value@OData.Community.Display.V1.FormattedValue'].Value
158
- $status = $row.PSObject.Properties['statuscode@OData.Community.Display.V1.FormattedValue'].Value
159
- ```
160
-
161
- ### Rule 3 — Inspect one live row before widening
162
-
163
- If field names are uncertain (new entity, schema drift, 400 error), fetch a single row first and dump property names. Do NOT keep iterating filters blindly:
164
-
165
- ```powershell
166
- $url = "$baseUrl/api/data/$api/${entitySet}?`$orderby=modifiedon desc&`$top=1"
167
- $row = (Invoke-RestMethod -Uri $url -Headers $headers -Method Get).value | Select-Object -First 1
168
- $row.PSObject.Properties.Name | Sort-Object
169
- ```
170
-
171
- Cache the discovered shape to `crm.fieldMap`.
172
-
173
- ### Rule 4 — Add one field/filter at a time
174
-
175
- If a query returns `400 Could not find a property named ...`, REVERT to the last known-good query shape, then add ONE field/filter and rerun. Do not widen scope past the failure.
176
-
177
- ### Rule 5 — Always include `statecode` and `statuscode` in candidate searches
178
-
179
- Otherwise inactive/closed records become invisible during ranking.
180
-
181
- ### Rule 6 — Prefer formatted values in rendered output
182
-
183
- Snapshot files render the formatted display value in the main body. Keep raw GUID / integer code only in `## All other fields` for diagnostic value.
144
+ > **Load `references/dataverse-doctrine.md`** for the 6 retrieval rules (formatted-value headers, PSObject indexer, one-row inspect, one-field-at-a-time, statecode inclusion, formatted-value rendering), the 4-step resolution sequence when `crm.recordId` is unset, anti-patterns, and the per-record Step A/B/D fetch procedure. Load when issuing any Dataverse REST call or resolving an unknown record ID.
184
145
 
185
146
  ## Resolution order (when `crm.recordId` is unset)
186
147
 
187
- If `boundaries.crm.record_ids` is empty AND a project token is provided, resolve in this order. Persist whichever step resolves to `m365Mutable.knownSections.<project>.crm.recordId` immediately.
188
-
189
- **CRITICAL anti-patterns — read before writing any filter:**
190
-
191
- - **NEVER add a `statecode` filter** to any candidate-resolution query. The default CRM list view may only show active records, hiding inactive / withdrawn / closed / deferred records. Always query all states and report `statecode`+`statuscode` formatted values per Rule 5 + Rule 6.
192
- - **`new_companyname` is NOT a valid attribute on `new_frontierengineeringtriage`.** The customer is a navigation lookup (`_new_customer_value`). Filtering on `new_companyname` will always return 0 and the 400 will look like an auth/scope failure. Always resolve customer via the `accounts` entity then `_new_customer_value eq <accountid>`.
193
- - **Never give up after one shallow probe.** This 4-step sequence is exhaustive — only step 4's user-ask is a stop. If WorkIQ is the only path tried, that is a defect; the Dataverse REST sequence below MUST be attempted before declaring no-match. See `plugin/instructions/crm-bootstrap-discovery.instructions.md`.
194
-
195
- 1. **Title-first**: filter the entity by its title-equivalent field (FDE: `new_title` or `msfre_name`) `contains '<token>'`, ordered by `modifiedon desc`, top 20.
196
- 2. **Account fallback**: if step 1 returns 0, resolve customer in `accounts` by `name contains '<token>'`. For EACH matching account, query the engagement entity by `_<customerLookup>_value eq <accountId>` (not just the first match — token like "Deere" can resolve to multiple accounts e.g. `JOHN DEERE`, `DEERE COMPANY`, `JOHN DEERE COASTRUCTION`).
197
- 3. **Wide text fallback**: if both above fail, retry contains on the long-text fields most likely to mention the customer by name: `new_businessscenariotechnicalblocker`, `new_engagedwith`, `new_engagementobjectives` (FDE entity). One field per query; combine results.
198
- 4. **Recent-slice client-rank**: if all above fail, pull the most recent 200 records (`$select` minimal: id, title, requestId, customer-formatted, statecode-formatted, statuscode-formatted, modifiedon) and rank client-side by needle match against title / request id / customer display.
199
- 5. **Ask user**: present top 5 candidates (id, title, customer display, status, modifiedon). Never auto-pick on multi-match.
200
-
201
- After steps 1–4 all return 0, write the candidate-search trail (queries attempted + result counts) to `Evidence/<alias>/refresh-reports/<ts>_refresh.md#crm-resolution-attempts` so the next refresh has audit. Do NOT silently set `boundaries.crm.disabled: true` — only step 5 (user declines or confirms none) writes `disabled` with `reason: 'no-match-after-full-4-step-resolution-<date>'`.
202
-
148
+ > **Load `references/dataverse-doctrine.md`** § "Resolution order" for the full 5-step title-first account wide-text recent-slice ask-user resolution sequence, anti-patterns (no statecode filter, no `new_companyname`), and audit trail rules.
203
149
 
204
150
 
205
151
  For EACH record id in `boundaries.crm.record_ids`:
206
152
 
207
153
  ### Step A — Resolve field map (once per project, cached)
208
154
 
209
- If `m365Mutable.knownSections.<project>.crm.fieldMap` is empty, probe:
210
-
211
- ```http
212
- GET /api/data/v9.2/EntityDefinitions(LogicalName='<entity-logical-name>')/Attributes?$select=LogicalName,DisplayName,AttributeType
213
- ```
214
-
215
- Persist the resolved attribute list to `crm.fieldMap`. For FDE entities, cross-check against `reference-packs/fde/crm-field-manifest.md`.
155
+ If `m365Mutable.knownSections.<project>.crm.fieldMap` is empty, probe EntityDefinitions. Persist to `crm.fieldMap`. For FDE entities, cross-check against `reference-packs/fde/crm-field-manifest.md`.
216
156
 
217
157
  ### Step B — Fetch the record with explicit $select + annotation $expand
218
158
 
219
- Headers per **Dataverse retrieval doctrine Rule 1** (formatted values).
220
-
221
- ```http
222
- GET /api/data/v9.2/<entitySet>(<recordId>)?$select=<every-attribute-from-fieldMap>&$expand=Annotations($select=annotationid,subject,notetext,createdon,_createdby_value,filename,mimetype)
223
- Prefer: odata.include-annotations="OData.Community.Display.V1.FormattedValue"
224
- ```
225
-
226
- WorkIQ-first equivalent (when host fallback to REST isn't available):
227
-
228
- ```
229
- workiq ask -q "Fetch the FULL Dataverse record from entity set <entitySet> with id <recordId>. Return EVERY field VERBATIM (do not summarize, do not truncate any long-text/Memo field). Then fetch ALL Annotations (notes) related to this record (objectid eq <recordId>). For each annotation return: subject, full notetext verbatim, createdon, createdby name. Output as JSON or markdown table — but do not paraphrase any long-text content."
230
- ```
231
-
232
- If the response is summarized (heuristics: any long-text field appears trimmed, an annotation count exceeds the rendered count, phrases like `"and N more notes"` appear): **retry once** with the wording `Return raw field values verbatim, every annotation in full. Do not summarize, do not paginate, do not omit.`
233
-
234
- ### Step C — Write snapshot file (one per record)
235
-
236
- > **LEGACY (pre-v4.9.0).** Superseded by v4.9.0 weekly/ + CSC writer above. The Dataverse REST fetch (Step A + Step B) still runs; only the file write changes. Kept for historical reference; not executed.
237
-
238
- Write `snapshot/<entitySet>/<record-id>.md` with:
239
-
240
- - File header (record id, MSX opp, status, owner, last-fetched timestamp).
241
- - `## Source Basis` — tool used, boundary applied, fieldMap source, annotation count.
242
- - `## AI Narrative Summary` (3+ paragraphs — REQUIRED FIRST per `evidence-thoroughness.instructions.md`).
243
- - `## Engagement context` — every long-text field as a verbatim blockquote (see crm-field-manifest.md for the FDE shape).
244
- - `## All other fields` — 2-column table; empty fields shown as `_(empty)_` so absence is visible.
245
- - `## Notes (verbatim)` — one `### Note — <subject> (<createdon> by <createdby>)` block per annotation, full `notetext` as a verbatim blockquote, oldest first. NO truncation regardless of length.
159
+ Headers per Dataverse retrieval doctrine Rule 1 (formatted values). Use `$expand=Annotations`. WorkIQ fallback available when REST host is unavailable.
246
160
 
247
- Write to: `<engagement-root>/<project>/Evidence/<alias>/crm/snapshot/<entitySet>/<record-id>.md`
248
-
249
- Use template: `templates/snapshot/crm-<kind>.template.md` (FDE entities: shape comes from `reference-packs/fde/crm-field-manifest.md`).
161
+ > **Load `references/dataverse-doctrine.md`** § "Per-record fetch" for the full REST URL templates, WorkIQ-equivalent prompt, summarization-detection heuristics, and retry wording.
250
162
 
251
163
  ### Step D — Failure handling
252
164
 
253
- - Per-record fetch failed → write the snapshot file with a `❌ record-fetch-failed` marker + `next_step: ask user to paste record fields` and continue to next record.
254
- - Annotation fetch failed but record fetched → write the record fields, mark `## Notes (verbatim)` with `❌ annotation-fetch-failed — N annotations expected` and continue.
255
-
256
- ## Stream pass
257
-
258
- > **LEGACY (pre-v4.9.0).** Superseded by v4.9.0 weekly/ + CSC writer above. Kept for historical reference; not executed.
259
-
260
- Per `evidence-thoroughness.instructions.md`: stream files (per-week) start with an **AI Narrative Summary** covering what changed and what it means, then every annotation verbatim with author + timestamp + every field-level change with old → new + actor + timestamp. Bucketed by ISO week into `stream/<record-id>/<YYYY-MM-DD>_crm-stream.md`.
261
-
262
- Write to: `<engagement-root>/<project>/Evidence/<alias>/crm/stream/<YYYY-MM-DD>_crm-stream.md` (date = Monday of the ISO week the events fall in).
263
-
264
- Use template: `templates/weekly/crm-summary.template.md`
265
-
266
- If a week file already exists, MERGE (dedupe by event ID, append new events, keep existing).
165
+ - Per-record fetch failed → write evidence file with `❌ record-fetch-failed` marker + `next_step: ask user to paste record fields`, continue.
166
+ - Annotation fetch failed but record fetched → write record fields, mark `❌ annotation-fetch-failed`, continue.
267
167
 
268
168
  ## Tools (in order)
269
169
 
@@ -298,11 +198,11 @@ After successful pass:
298
198
  - Hint missing AND fuzzy resolution returns 0 candidates → ask user once, persist answer to mutable, continue.
299
199
  - Multiple plausible candidates → ask user to pick, persist answer.
300
200
  - All paths failed for a given record → write evidence file with `❌ all paths failed` marker, log to run-log errors, continue with rest of run.
301
-
302
- ## References (v4.4.7)
303
-
304
- - Name → ID resolution follows ..\..\instructions\fuzzy-disambiguation.instructions.md (universal fuzzy contract).
305
- - After this pull completes, the per-source verification gate runs: ..\..\instructions\per-source-verification-gate.instructions.md (retry once, then write FOLLOW-UPS.md).
201
+
202
+ ## References (v4.4.7)
203
+
204
+ - Name → ID resolution follows ..\..\instructions\fuzzy-disambiguation.instructions.md (universal fuzzy contract).
205
+ - After this pull completes, the per-source verification gate runs: ..\..\instructions\per-source-verification-gate.instructions.md (retry once, then write FOLLOW-UPS.md).
306
206
 
307
207
 
308
208
  ## Issue Recovery
@@ -317,10 +217,24 @@ An entity that cannot meet the threshold is flagged `low_signal: true` in `_inde
317
217
 
318
218
  ## Changelog
319
219
 
220
+ - **v3.0.1 (kushi v5.0.1, 2026-05-26)**: agentskills.io spec-compliance pass. Extracted Dataverse
221
+ retrieval doctrine (Rules 1–6, 4-step resolution, Step A/B/D procedure) →
222
+ `references/dataverse-doctrine.md`; legacy snapshot/stream file format →
223
+ `references/legacy-shape.md`. SKILL.md trimmed from 343 to ~210 lines. Behaviour unchanged;
224
+ load-on-trigger pointers added.
320
225
  - **v3.0.0 (kushi v4.9.0, 2026-05-26)**: BREAKING. Output is now a single weekly CSC file per ISO week
321
226
  (`weekly/<YYYY-MM-DD>_crm-csc.md`) + per-contributor `_index/entities.yml`. snapshot/`<entitySet>`/
322
227
  + stream/ writes removed. Dataverse REST retrieval doctrine (Rules 1–6) and 4-step resolution
323
228
  UNCHANGED — only output shape changes. Record fields mapped to CSC sections
324
229
  (Owner→Participants, long-text→Topics, statecode→Decisions, etc.). AI Narrative Summary requirement
325
230
  removed. Legacy snapshot/+stream/ folders left readable; no migration.
326
- - **v2.x.x**: prior snapshot/+stream/ + verbatim-by-default shape. See git history.
231
+ - **v2.x.x**: prior snapshot/+stream/ + verbatim-by-default shape. See git history.
232
+
233
+ ## Validation loop
234
+
235
+ After writing outputs:
236
+
237
+ 1. Run self-check targeted at this skill: `pwsh plugin/skills/self-check/run.ps1 -Targeted pull-crm`
238
+ 2. If failures: fix and re-run the affected step (not the whole skill).
239
+ 3. Repeat until self-check exits 0.
240
+ 4. Only then update `run-log.yml` with success status.
@@ -0,0 +1,108 @@
1
+ # references/dataverse-doctrine.md (pull-crm)
2
+
3
+ > **Load this file when** issuing Dataverse REST calls — i.e., when you need the 6 retrieval rules (formatted values, PSObject indexer, one-row inspect, one-field-at-a-time, statecode inclusion, formatted-value preference) and the 4-step resolution sequence for unknown record IDs.
4
+
5
+ ## Dataverse retrieval doctrine (REQUIRED — applies to every REST call)
6
+
7
+ Borrowed from production CRM-sync hardening. Every Dataverse REST call in this skill MUST follow these rules, or option-set / lookup fields will render as raw GUIDs and integer codes instead of human labels:
8
+
9
+ ### Rule 1 — Always request formatted values
10
+
11
+ ```powershell
12
+ $headers = @{
13
+ Authorization = "Bearer $token"
14
+ Accept = 'application/json'
15
+ 'OData-Version' = '4.0'
16
+ 'OData-MaxVersion' = '4.0'
17
+ Prefer = 'odata.include-annotations="OData.Community.Display.V1.FormattedValue"'
18
+ }
19
+ ```
20
+
21
+ This makes Dataverse return paired properties like `statuscode@OData.Community.Display.V1.FormattedValue` and `_ownerid_value@OData.Community.Display.V1.FormattedValue` alongside the raw codes/GUIDs.
22
+
23
+ ### Rule 2 — Use `PSObject.Properties[...]` accessor for `@`-named annotation properties
24
+
25
+ Dotted access fails for property names containing `@`. Always use the indexer:
26
+
27
+ ```powershell
28
+ $ownerDisplay = $row.PSObject.Properties['_ownerid_value@OData.Community.Display.V1.FormattedValue'].Value
29
+ $status = $row.PSObject.Properties['statuscode@OData.Community.Display.V1.FormattedValue'].Value
30
+ ```
31
+
32
+ ### Rule 3 — Inspect one live row before widening
33
+
34
+ If field names are uncertain (new entity, schema drift, 400 error), fetch a single row first and dump property names. Do NOT keep iterating filters blindly:
35
+
36
+ ```powershell
37
+ $url = "$baseUrl/api/data/$api/${entitySet}?`$orderby=modifiedon desc&`$top=1"
38
+ $row = (Invoke-RestMethod -Uri $url -Headers $headers -Method Get).value | Select-Object -First 1
39
+ $row.PSObject.Properties.Name | Sort-Object
40
+ ```
41
+
42
+ Cache the discovered shape to `crm.fieldMap`.
43
+
44
+ ### Rule 4 — Add one field/filter at a time
45
+
46
+ If a query returns `400 Could not find a property named ...`, REVERT to the last known-good query shape, then add ONE field/filter and rerun. Do not widen scope past the failure.
47
+
48
+ ### Rule 5 — Always include `statecode` and `statuscode` in candidate searches
49
+
50
+ Otherwise inactive/closed records become invisible during ranking.
51
+
52
+ ### Rule 6 — Prefer formatted values in rendered output
53
+
54
+ Snapshot files render the formatted display value in the main body. Keep raw GUID / integer code only in `## All other fields` for diagnostic value.
55
+
56
+ ## Resolution order (when `crm.recordId` is unset)
57
+
58
+ If `boundaries.crm.record_ids` is empty AND a project token is provided, resolve in this order. Persist whichever step resolves to `m365Mutable.knownSections.<project>.crm.recordId` immediately.
59
+
60
+ **CRITICAL anti-patterns — read before writing any filter:**
61
+
62
+ - **NEVER add a `statecode` filter** to any candidate-resolution query. The default CRM list view may only show active records, hiding inactive / withdrawn / closed / deferred records. Always query all states and report `statecode`+`statuscode` formatted values per Rule 5 + Rule 6.
63
+ - **`new_companyname` is NOT a valid attribute on `new_frontierengineeringtriage`.** The customer is a navigation lookup (`_new_customer_value`). Filtering on `new_companyname` will always return 0 and the 400 will look like an auth/scope failure. Always resolve customer via the `accounts` entity then `_new_customer_value eq <accountid>`.
64
+ - **Never give up after one shallow probe.** This 4-step sequence is exhaustive — only step 4's user-ask is a stop. If WorkIQ is the only path tried, that is a defect; the Dataverse REST sequence below MUST be attempted before declaring no-match. See `plugin/instructions/crm-bootstrap-discovery.instructions.md`.
65
+
66
+ 1. **Title-first**: filter the entity by its title-equivalent field (FDE: `new_title` or `msfre_name`) `contains '<token>'`, ordered by `modifiedon desc`, top 20.
67
+ 2. **Account fallback**: if step 1 returns 0, resolve customer in `accounts` by `name contains '<token>'`. For EACH matching account, query the engagement entity by `_<customerLookup>_value eq <accountId>` (not just the first match — token like "Deere" can resolve to multiple accounts e.g. `JOHN DEERE`, `DEERE COMPANY`, `JOHN DEERE COASTRUCTION`).
68
+ 3. **Wide text fallback**: if both above fail, retry contains on the long-text fields most likely to mention the customer by name: `new_businessscenariotechnicalblocker`, `new_engagedwith`, `new_engagementobjectives` (FDE entity). One field per query; combine results.
69
+ 4. **Recent-slice client-rank**: if all above fail, pull the most recent 200 records (`$select` minimal: id, title, requestId, customer-formatted, statecode-formatted, statuscode-formatted, modifiedon) and rank client-side by needle match against title / request id / customer display.
70
+ 5. **Ask user**: present top 5 candidates (id, title, customer display, status, modifiedon). Never auto-pick on multi-match.
71
+
72
+ After steps 1–4 all return 0, write the candidate-search trail (queries attempted + result counts) to `Evidence/<alias>/refresh-reports/<ts>_refresh.md#crm-resolution-attempts` so the next refresh has audit. Do NOT silently set `boundaries.crm.disabled: true` — only step 5 (user declines or confirms none) writes `disabled` with `reason: 'no-match-after-full-4-step-resolution-<date>'`.
73
+
74
+ ## Per-record fetch (Step A–B)
75
+
76
+ For EACH record id in `boundaries.crm.record_ids`:
77
+
78
+ ### Step A — Resolve field map (once per project, cached)
79
+
80
+ If `m365Mutable.knownSections.<project>.crm.fieldMap` is empty, probe:
81
+
82
+ ```http
83
+ GET /api/data/v9.2/EntityDefinitions(LogicalName='<entity-logical-name>')/Attributes?$select=LogicalName,DisplayName,AttributeType
84
+ ```
85
+
86
+ Persist the resolved attribute list to `crm.fieldMap`. For FDE entities, cross-check against `reference-packs/fde/crm-field-manifest.md`.
87
+
88
+ ### Step B — Fetch the record with explicit $select + annotation $expand
89
+
90
+ Headers per **Dataverse retrieval doctrine Rule 1** (formatted values).
91
+
92
+ ```http
93
+ GET /api/data/v9.2/<entitySet>(<recordId>)?$select=<every-attribute-from-fieldMap>&$expand=Annotations($select=annotationid,subject,notetext,createdon,_createdby_value,filename,mimetype)
94
+ Prefer: odata.include-annotations="OData.Community.Display.V1.FormattedValue"
95
+ ```
96
+
97
+ WorkIQ-first equivalent (when host fallback to REST isn't available):
98
+
99
+ ```
100
+ workiq ask -q "Fetch the FULL Dataverse record from entity set <entitySet> with id <recordId>. Return EVERY field VERBATIM (do not summarize, do not truncate any long-text/Memo field). Then fetch ALL Annotations (notes) related to this record (objectid eq <recordId>). For each annotation return: subject, full notetext verbatim, createdon, createdby name. Output as JSON or markdown table — but do not paraphrase any long-text content."
101
+ ```
102
+
103
+ If the response is summarized (heuristics: any long-text field appears trimmed, an annotation count exceeds the rendered count, phrases like `"and N more notes"` appear): **retry once** with the wording `Return raw field values verbatim, every annotation in full. Do not summarize, do not paginate, do not omit.`
104
+
105
+ ### Step D — Failure handling
106
+
107
+ - Per-record fetch failed → write the snapshot file with a `❌ record-fetch-failed` marker + `next_step: ask user to paste record fields` and continue to next record.
108
+ - Annotation fetch failed but record fetched → write the record fields, mark `## Notes (verbatim)` with `❌ annotation-fetch-failed — N annotations expected` and continue.
@@ -0,0 +1,28 @@
1
+ # references/legacy-shape.md (pull-crm)
2
+
3
+ > **Load this file when** maintaining or reading pre-v4.9.0 `snapshot/` or `stream/` CRM evidence files — i.e., when legacy evidence exists on disk and you need to understand the prior snapshot format (AI Narrative Summary, per-record file layout, annotation rendering) or stream format.
4
+
5
+ ## Step C — Write snapshot file (LEGACY, pre-v4.9.0)
6
+
7
+ Write `snapshot/<entitySet>/<record-id>.md` with:
8
+
9
+ - File header (record id, MSX opp, status, owner, last-fetched timestamp).
10
+ - `## Source Basis` — tool used, boundary applied, fieldMap source, annotation count.
11
+ - `## AI Narrative Summary` (3+ paragraphs — REQUIRED FIRST per `evidence-thoroughness.instructions.md`).
12
+ - `## Engagement context` — every long-text field as a verbatim blockquote (see crm-field-manifest.md for the FDE shape).
13
+ - `## All other fields` — 2-column table; empty fields shown as `_(empty)_` so absence is visible.
14
+ - `## Notes (verbatim)` — one `### Note — <subject> (<createdon> by <createdby>)` block per annotation, full `notetext` as a verbatim blockquote, oldest first. NO truncation regardless of length.
15
+
16
+ Write to: `<engagement-root>/<project>/Evidence/<alias>/crm/snapshot/<entitySet>/<record-id>.md`
17
+
18
+ Use template: `templates/snapshot/crm-<kind>.template.md` (FDE entities: shape comes from `reference-packs/fde/crm-field-manifest.md`).
19
+
20
+ ## Stream pass (LEGACY, pre-v4.9.0)
21
+
22
+ Per `evidence-thoroughness.instructions.md`: stream files (per-week) start with an **AI Narrative Summary** covering what changed and what it means, then every annotation verbatim with author + timestamp + every field-level change with old → new + actor + timestamp. Bucketed by ISO week into `stream/<record-id>/<YYYY-MM-DD>_crm-stream.md`.
23
+
24
+ Write to: `<engagement-root>/<project>/Evidence/<alias>/crm/stream/<YYYY-MM-DD>_crm-stream.md` (date = Monday of the ISO week the events fall in).
25
+
26
+ Use template: `templates/weekly/crm-summary.template.md`
27
+
28
+ If a week file already exists, MERGE (dedupe by event ID, append new events, keep existing).
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: "pull-email"
3
- version: "3.0.0"
4
- description: "v3.0.0 (kushi v4.9.0): Pull Email evidence as Comprehensive Structured Capture (CSC) blocks written to weekly/YYYY-MM-DD_email-csc.md. One block per email thread touched that week, upserted in _index/entities.yml. WorkIQ-ONLY via CSC canonical prompts. No snapshot/+stream/ split. Per-contributor _index/. See weekly-csc + comprehensive-structured-capture doctrines."
3
+ version: "3.0.1"
4
+ description: "USE WHEN refresh-project / bootstrap-project dispatches Email source OR the user says \"pull email for <X>\" AND the project has integrations.yml#boundaries.email folder/keyword set. DO NOT USE for general inbox triage. Capability: pulls Email evidence as CSC blocks written to weekly/YYYY-MM-DD_email-csc.md, one block per conversation touched, upserted in _index/entities.yml. WorkIQ-only Graph m365_* is FORBIDDEN as fallback."
5
5
  ---
6
6
 
7
7
  # Skill: pull-email
@@ -21,6 +21,14 @@ description: "v3.0.0 (kushi v4.9.0): Pull Email evidence as Comprehensive Struct
21
21
  > - ~~`verbatim-by-default.instructions.md`~~ (LEGACY — superseded by CSC).
22
22
  > - ~~`snapshot-vs-stream.instructions.md`~~ (LEGACY — superseded by weekly-csc).
23
23
 
24
+ ## Gotchas
25
+
26
+ - **WorkIQ-only — `m365_get_email` / `m365_search_emails` / Graph REST are FORBIDDEN as fallbacks.** Per `workiq-only.instructions.md`. If WorkIQ returns `throttled` or `degraded`, log it and retry on the next refresh — never fall back to Graph.
27
+ - **Conversation IDs collapse threads — message IDs do not** — the canonical email entity id is `email://conversation_id=<id>`. One CSC block per conversation per week; individual messages roll up into the block''s "Who Said What" section.
28
+ - **Forbidden bulk-enumerate phrasings** — bulk "list … mail folders", folder-ID lookups, and structured-field mail-folder searches all punt to Graph. Use the customer-hint discovery sweep per `email-bootstrap-discovery.instructions.md` (which holds the canonical forbidden list).
29
+ - **Shared mailboxes need explicit boundary entries** — emails from a shared mailbox the contributor has delegated access to do NOT appear in `me/messages`. Add `boundaries.email.shared_mailboxes: [...]` and dispatch one WorkIQ call per shared mailbox.
30
+ - **HTML body bodies leak tracking pixels into citations** — the CSC writer strips `<img>` tags and inline base64 before persisting; if a weekly file shows `data:image/...` in a citation, the stripper regressed — re-pull that conversation with the current writer.
31
+
24
32
  ## v4.9.0 — Comprehensive Structured Capture (CSC) + weekly/ layout (HARD RULE; supersedes all snapshot/+stream/ guidance below)
25
33
 
26
34
  Per `comprehensive-structured-capture.instructions.md` and `weekly-csc.instructions.md`:
@@ -108,81 +116,13 @@ plugin/instructions/scope-boundaries.instructions.md.
108
116
 
109
117
  ## Retrieval order (deterministic — folder fast path → root fallback)
110
118
 
111
- Borrowed from production email-context hardening. WorkIQ is more reliable when the query is folder-scoped to one known project folder than when it scans the whole mailbox. Order:
112
-
113
- ### Order 1 — Exact project-folder fast path (when hint exists)
114
-
115
- If `m365Mutable.knownSections.<project>.emailContext.folder` (or `.folderId`) is set with `confidence >= medium`:
116
-
117
- ```
118
- workiq ask -q "Search my Outlook mailbox for emails from <window-start> onward in folder(s) '<projectFolder>' (including all subfolders) related to <project 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."
119
- ```
120
-
121
- If this path returns 0 results AND the same query has succeeded recently in this project's run-log, retry the exact-folder query ONCE before escalating. WorkIQ has been observed to return spurious empty hits on first call.
122
-
123
- ### Order 2 — Root-scope fallback
124
-
125
- Only if Order 1 returns insufficient/empty results after the retry, OR the project has no folder hint, OR multiple plausible hints exist:
126
-
127
- ```
128
- workiq ask -q "Search my Outlook mailbox for emails from <window-start> onward in folder(s) <boundaries.email.mailboxes joined> (including all subfolders) related to <project aliases>. Return: sent datetime, subject, sender, recipients, folder path, message link, and a short relevance reason."
129
- ```
130
-
131
- ### Throttle handling (HARD stop in same run)
132
-
133
- If WorkIQ returns `tooManyRequests`, `More than 3 retries performed`, or `We're experiencing high demand`:
134
-
135
- - Mark `sources.email.coverage_state = throttled-tooManyRequests` in run-log.
136
- - Write what enumeration HAS been collected so far to the message-index file with a `⚠️ throttled — partial enumeration` banner.
137
- - Stop email pulls for this run. Do NOT issue broader mailbox queries.
138
- - Continue with non-email sources.
139
-
140
- ### Degraded list-only state
141
-
142
- If Step A enumeration succeeds but Step B body fetch fails repeatedly (host + WorkIQ both returning empty / `body-unavailable` for >50% of messages):
143
-
144
- - Set `sources.email.coverage_state = degraded-list-only`.
145
- - Keep the message index with sent date, subject, sender, recipients, folder path, message link as evidence — these ARE usable signals at list level.
146
- - Mark each affected message with `❌ body-unavailable` in the weekly stream file.
147
- - Do NOT discard list-level evidence. Do NOT loop on broad fallback queries trying to rescue bodies.
119
+ > **Load `references/retrieval-order.md`** for the full folder-scoped fast path WorkIQ prompt, root-scope fallback, throttle-handling rules (HARD stop), and degraded list-only state logic. Load when dispatching the email enumeration query.
148
120
 
149
121
  ## Two-pass pull (REQUIRED — no single-call summarization)
150
122
 
151
- Same anti-summarization pattern as `pull-onenote` v2.2.0. WorkIQ summarizes by default relying on a single "give me all emails this week" call returns a curated subset, not the full set. Doctrine: **enumerate the message list first, then fetch each message body individually.**
152
-
153
- ### Step A — Enumerate the message list
154
-
155
- For each `boundaries.email.mailboxes[i]` × the date window (and optional sender_domains / subject_keywords):
123
+ > **Load `references/two-pass-pull.md`** for the Step A enumeration prompt, Step B per-message body fetch prompt with doubled-strict retry, summarization-detection heuristics, and Step C thread-grouping procedure. Load when executing the pull after retrieval order is determined.
156
124
 
157
- ```
158
- workiq ask -q "List EVERY email in mailbox <mb> received between <start> and <end> [filtered to senders @<domain>] [containing subject keyword <kw>]. Return one row per message: messageId, internetMessageId, conversationId, sentDate, fromAddress, toAddresses, ccAddresses, subject, hasAttachments, sizeKB. Do NOT summarize, do NOT omit, do NOT group by thread yet, do NOT add commentary. Flat table only."
159
- ```
160
-
161
- If the response is summarized (heuristics: `"and N more"`, `"sample"`, row count noticeably less than expected, message bodies included instead of just the index): **retry once** with `Return as a flat table with no commentary, no grouping, no truncation. Message count must equal the actual count in the date window.`
162
-
163
- Persist the enumerated list to `Evidence/<alias>/email/_index/<YYYY-MM-DD>_message-index.md` (date = Monday of the ISO week). This makes the run idempotent + resumable.
164
-
165
- ### Step B — Per-message body fetch (one WorkIQ call per message)
166
-
167
- For each message in the index, ONE WorkIQ call (per `workiq-only.instructions.md`):
168
-
169
- ```
170
- workiq ask -q "Get the FULL body of email with messageId <id> (mailbox <mb>). Return: full text body verbatim (no summarization, no truncation), all headers (from, to, cc, bcc, sentDate, subject, conversationId, internetMessageId, references, in-reply-to), attachment list (filename + size + mimetype, do NOT download binary), and any inline images as alt text or skip with marker."
171
- ```
172
-
173
- **FORBIDDEN** (per workiq-only): `m365_get_email`, `m365_search_emails`, `m365_list_emails`, Graph REST. Do NOT add them as fallbacks. If WorkIQ body fetch fails, use the doubled-strict retry then user-paste — NEVER fall through to a host m365_* call.
174
-
175
- If body comes back < 200 chars or includes phrases like `"unable to retrieve"`, `"body unavailable"`, `"truncated"`: doubled-strict retry once with `Return the raw email body, no summary, no truncation. If you cannot return the full body, say "body-unavailable" exactly and nothing else.` On second failure, record that message as `❌ body-unavailable` in the weekly stream file + continue (or prompt user to paste — first-class evidence path per workiq-only).
176
-
177
- ### Step C — Group by conversationId into threads
178
-
179
- After all bodies fetched, group messages by `conversationId`. For each thread:
180
-
181
- - Sort by `sentDate` ascending.
182
- - Write a `## Thread: <subject>` block in the weekly stream file with:
183
- - **AI Narrative Summary (REQUIRED FIRST, 3+ paragraphs)** — conversation arc, who's pushing what, decisions, asks, tone (per `evidence-thoroughness.instructions.md`).
184
- - One `### Message <N> — <fromAddress> (<sentDate>)` block per message with: full headers (to/cc/subject), full body verbatim as a blockquote, attachment list.
185
- - Append the thread block to the weekly file (`<YYYY-MM-DD>_email-stream.md`, date = Monday of the ISO week).
125
+ Doctrine summary: **enumerate the message list first (Step A), then fetch each message body individually (Step B), then group by conversationId (Step C).** Single-call summarization is a defect.
186
126
 
187
127
  ## Snapshot pass
188
128
 
@@ -249,11 +189,11 @@ An entity that cannot meet the threshold is flagged `low_signal: true` in `_inde
249
189
  - Enumeration step (Step A) failed → write `_index/<date>_message-index.md` with failure marker, log to run-log errors, do NOT proceed to per-message loop.
250
190
  - Per-message fetch failed → record marker, continue.
251
191
  - All paths failed → write evidence file with `❌ all paths failed` marker, log to run-log errors, continue with rest of run.
252
-
253
- ## References (v4.4.7)
254
-
255
- - Name → ID resolution follows ..\..\instructions\fuzzy-disambiguation.instructions.md (universal fuzzy contract).
256
- - After this pull completes, the per-source verification gate runs: ..\..\instructions\per-source-verification-gate.instructions.md (retry once, then write FOLLOW-UPS.md).
192
+
193
+ ## References (v4.4.7)
194
+
195
+ - Name → ID resolution follows ..\..\instructions\fuzzy-disambiguation.instructions.md (universal fuzzy contract).
196
+ - After this pull completes, the per-source verification gate runs: ..\..\instructions\per-source-verification-gate.instructions.md (retry once, then write FOLLOW-UPS.md).
257
197
 
258
198
 
259
199
  ## Issue Recovery
@@ -262,9 +202,23 @@ When this skill exposes a reusable defect (auth pattern, doctrine gap, layout mi
262
202
 
263
203
  ## Changelog
264
204
 
205
+ - **v3.0.1 (kushi v5.0.1, 2026-05-26)**: agentskills.io spec-compliance pass. Extracted retrieval
206
+ order (folder fast path, root fallback, throttle handling, degraded state) →
207
+ `references/retrieval-order.md`; two-pass pull procedure (Steps A/B/C prompts, retry wording,
208
+ grouping) → `references/two-pass-pull.md`. SKILL.md trimmed from 287 to ~200 lines. Behaviour
209
+ unchanged; load-on-trigger pointers added.
265
210
  - **v3.0.0 (kushi v4.9.0, 2026-05-26)**: BREAKING. Output is now a single weekly CSC file per ISO week
266
211
  (`weekly/<YYYY-MM-DD>_email-csc.md`) + per-contributor `_index/entities.yml`. snapshot/ and stream/
267
212
  writes removed. WorkIQ prompts switched to CSC canonical prompts. AI Narrative Summary requirement
268
213
  removed (CSC bulleted sections carry the load). Verbatim body-fetch loop removed.
269
214
  Legacy snapshot/+stream/ folders left readable; no migration.
270
- - **v2.x.x**: prior snapshot/+stream/ + verbatim-by-default shape. See git history.
215
+ - **v2.x.x**: prior snapshot/+stream/ + verbatim-by-default shape. See git history.
216
+
217
+ ## Validation loop
218
+
219
+ After writing outputs:
220
+
221
+ 1. Run self-check targeted at this skill: `pwsh plugin/skills/self-check/run.ps1 -Targeted pull-email`
222
+ 2. If failures: fix and re-run the affected step (not the whole skill).
223
+ 3. Repeat until self-check exits 0.
224
+ 4. Only then update `run-log.yml` with success status.
@@ -0,0 +1,43 @@
1
+ # references/retrieval-order.md (pull-email)
2
+
3
+ > **Load this file when** executing the email retrieval (Order 1 exact-folder fast path vs Order 2 root-scope fallback) — i.e., when you need the exact WorkIQ prompt for folder-scoped vs root-scoped queries, throttle handling rules, and the degraded list-only state logic.
4
+
5
+ ## Retrieval order (deterministic — folder fast path → root fallback)
6
+
7
+ Borrowed from production email-context hardening. WorkIQ is more reliable when the query is folder-scoped to one known project folder than when it scans the whole mailbox.
8
+
9
+ ### Order 1 — Exact project-folder fast path (when hint exists)
10
+
11
+ If `m365Mutable.knownSections.<project>.emailContext.folder` (or `.folderId`) is set with `confidence >= medium`:
12
+
13
+ ```
14
+ workiq ask -q "Search my Outlook mailbox for emails from <window-start> onward in folder(s) '<projectFolder>' (including all subfolders) related to <project 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."
15
+ ```
16
+
17
+ If this path returns 0 results AND the same query has succeeded recently in this project's run-log, retry the exact-folder query ONCE before escalating. WorkIQ has been observed to return spurious empty hits on first call.
18
+
19
+ ### Order 2 — Root-scope fallback
20
+
21
+ Only if Order 1 returns insufficient/empty results after the retry, OR the project has no folder hint, OR multiple plausible hints exist:
22
+
23
+ ```
24
+ workiq ask -q "Search my Outlook mailbox for emails from <window-start> onward in folder(s) <boundaries.email.mailboxes joined> (including all subfolders) related to <project aliases>. Return: sent datetime, subject, sender, recipients, folder path, message link, and a short relevance reason."
25
+ ```
26
+
27
+ ### Throttle handling (HARD stop in same run)
28
+
29
+ If WorkIQ returns `tooManyRequests`, `More than 3 retries performed`, or `We're experiencing high demand`:
30
+
31
+ - Mark `sources.email.coverage_state = throttled-tooManyRequests` in run-log.
32
+ - Write what enumeration HAS been collected so far to the message-index file with a `⚠️ throttled — partial enumeration` banner.
33
+ - Stop email pulls for this run. Do NOT issue broader mailbox queries.
34
+ - Continue with non-email sources.
35
+
36
+ ### Degraded list-only state
37
+
38
+ If Step A enumeration succeeds but Step B body fetch fails repeatedly (host + WorkIQ both returning empty / `body-unavailable` for >50% of messages):
39
+
40
+ - Set `sources.email.coverage_state = degraded-list-only`.
41
+ - Keep the message index with sent date, subject, sender, recipients, folder path, message link as evidence — these ARE usable signals at list level.
42
+ - Mark each affected message with `❌ body-unavailable` in the weekly stream file.
43
+ - Do NOT discard list-level evidence. Do NOT loop on broad fallback queries trying to rescue bodies.
@@ -0,0 +1,41 @@
1
+ # references/two-pass-pull.md (pull-email)
2
+
3
+ > **Load this file when** executing the per-mailbox enumeration (Step A), per-message body fetch (Step B), or thread-grouping (Step C) — i.e., when you need the exact WorkIQ prompt templates, summarization-detection heuristics, doubled-strict retry wording, and the legacy grouping procedure.
4
+
5
+ ## Two-pass pull (REQUIRED — no single-call summarization)
6
+
7
+ Same anti-summarization pattern as `pull-onenote` v2.2.0. WorkIQ summarizes by default — relying on a single "give me all emails this week" call returns a curated subset, not the full set. Doctrine: **enumerate the message list first, then fetch each message body individually.**
8
+
9
+ ### Step A — Enumerate the message list
10
+
11
+ For each `boundaries.email.mailboxes[i]` × the date window (and optional sender_domains / subject_keywords):
12
+
13
+ ```
14
+ workiq ask -q "List EVERY email in mailbox <mb> received between <start> and <end> [filtered to senders @<domain>] [containing subject keyword <kw>]. Return one row per message: messageId, internetMessageId, conversationId, sentDate, fromAddress, toAddresses, ccAddresses, subject, hasAttachments, sizeKB. Do NOT summarize, do NOT omit, do NOT group by thread yet, do NOT add commentary. Flat table only."
15
+ ```
16
+
17
+ If the response is summarized (heuristics: `"and N more"`, `"sample"`, row count noticeably less than expected, message bodies included instead of just the index): **retry once** with `Return as a flat table with no commentary, no grouping, no truncation. Message count must equal the actual count in the date window.`
18
+
19
+ Persist the enumerated list to `Evidence/<alias>/email/_index/<YYYY-MM-DD>_message-index.md` (date = Monday of the ISO week). This makes the run idempotent + resumable.
20
+
21
+ ### Step B — Per-message body fetch (one WorkIQ call per message)
22
+
23
+ For each message in the index, ONE WorkIQ call (per `workiq-only.instructions.md`):
24
+
25
+ ```
26
+ workiq ask -q "Get the FULL body of email with messageId <id> (mailbox <mb>). Return: full text body verbatim (no summarization, no truncation), all headers (from, to, cc, bcc, sentDate, subject, conversationId, internetMessageId, references, in-reply-to), attachment list (filename + size + mimetype, do NOT download binary), and any inline images as alt text or skip with marker."
27
+ ```
28
+
29
+ **FORBIDDEN** (per workiq-only): `m365_get_email`, `m365_search_emails`, `m365_list_emails`, Graph REST. Do NOT add them as fallbacks. If WorkIQ body fetch fails, use the doubled-strict retry then user-paste — NEVER fall through to a host m365_* call.
30
+
31
+ If body comes back < 200 chars or includes phrases like `"unable to retrieve"`, `"body unavailable"`, `"truncated"`: doubled-strict retry once with `Return the raw email body, no summary, no truncation. If you cannot return the full body, say "body-unavailable" exactly and nothing else.` On second failure, record that message as `❌ body-unavailable` in the weekly stream file + continue (or prompt user to paste — first-class evidence path per workiq-only).
32
+
33
+ ### Step C — Group by conversationId into threads
34
+
35
+ After all bodies fetched, group messages by `conversationId`. For each thread:
36
+
37
+ - Sort by `sentDate` ascending.
38
+ - Write a `## Thread: <subject>` block in the weekly stream file with:
39
+ - **AI Narrative Summary (REQUIRED FIRST, 3+ paragraphs)** — conversation arc, who's pushing what, decisions, asks, tone (per `evidence-thoroughness.instructions.md`).
40
+ - One `### Message <N> — <fromAddress> (<sentDate>)` block per message with: full headers (to/cc/subject), full body verbatim as a blockquote, attachment list.
41
+ - Append the thread block to the weekly file (`<YYYY-MM-DD>_email-stream.md`, date = Monday of the ISO week).