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
@@ -1,14 +1,23 @@
1
1
  ---
2
2
  name: "pull-crm"
3
- version: "2.0.0"
4
- description: "Pull CRM (Dataverse) evidence (snapshot: engagement record fields; stream: notes/activities/status changes). Conditional az login per az-auth-conditional rule."
3
+ version: "2.3.1"
4
+ description: "Pull CRM (Dataverse) evidence (snapshot: engagement record VERBATIM long-text fields + ALL annotations with formatted values; stream: notes/activities/status changes). Per-record explicit $select + formatted-value annotations. FDE entity sets use crm-field-manifest.md. v2.3.0: exhaustive 4-step resolution sequence (title → account[s] → wide-text → recent-slice → ask). Anti-patterns codified: no statecode filter, new_companyname is NOT a field, never disable from one shallow probe."
5
5
  ---
6
6
 
7
7
  # Skill: pull-crm
8
8
 
9
+
10
+ > **v3.7.6 contracts** — This skill operates under four HARD-rule doctrines:
11
+ > - `verbatim-by-default.instructions.md` — full bodies/notetext/fields by default; no preview-grade pulls accepted.
12
+ > - `capture-learnings.instructions.md` — every fix/discovery is logged to `plugin/learnings/<source>.md` immediately.
13
+ > - `cleanup-on-resolution.instructions.md` — when a value resolves, all stale `no-match` / `not yet` notes referencing the prior unresolved state must be rewritten in the same turn.
14
+ > - `run-reports.instructions.md` — every refresh writes a per-user report under `Evidence/<alias>/refresh-reports/YYYY-MM-DD-HHMM_refresh.md`.
15
+
16
+ > **Canonical evidence layout** (HARD, kushi v3.12.1+): all artifacts produced by this skill MUST be written under `<project>/Evidence/<alias>/<source>/{snapshot,stream,...}/` — sibling folders under `<project>/` (e.g. `<project>/<source>-context/`, `<project>/<source>/`, `<project>/_Weekly Summaries/`) are FORBIDDEN. See `evidence-layout-canonical.instructions.md`.
17
+
9
18
  Pulls **crm** evidence in two shapes per `snapshot-vs-stream.instructions.md`:
10
19
 
11
- - **snapshot/** — engagement record with every field the API returns (owner, status, account, dates, custom fields)
20
+ - **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
12
21
  - **stream/** — notes added, activities logged, field-level changes (old → new + actor + timestamp)
13
22
 
14
23
  WorkIQ-first per `workiq-first.instructions.md`. Thoroughness per `evidence-thoroughness.instructions.md`; runtime detector + auto-retry + paste-prompt per `thoroughness-detector.instructions.md`. Citations per `citation-ledger.instructions.md`. Side-by-side mutable hints written immediately on discovery per `side-by-side-config.instructions.md`.
@@ -24,19 +33,159 @@ WorkIQ-first per `workiq-first.instructions.md`. Thoroughness per `evidence-thor
24
33
  ## Resolution hints (pinned in mutable)
25
34
 
26
35
  - `crm.recordId` — pinned engagement record GUID (FE-NNNNN-style)
27
- - `crm.entitySetName` — Dataverse entity set name (e.g. `msdyn_engagements`)
36
+ - `crm.entitySetName` — Dataverse entity set name (e.g. `msfre_engagementsubmissions`, `msdyn_engagements`)
37
+ - `crm.fieldMap` — resolved logical-name map per entity (filled by discovery probe; cached)
38
+
39
+ ## Hard prerequisites (REQUIRED — see `scope-boundaries.instructions.md` Rule 3)
40
+
41
+ This skill is **HARD-fail** without both:
42
+
43
+ 1. Global config: `<engagement-root>/.project-evidence/crm/config.yml` exists with non-placeholder `environmentUrl`.
44
+ 2. Per-project boundary: `<engagement-root>/<project>/integrations.yml#boundaries.crm.record_ids` OR `boundaries.crm.request_ids` is non-empty.
45
+
46
+ If either is missing, refuse with the exact message:
47
+
48
+ ```
49
+ crm-config-missing — drop a filled config.yml at
50
+ <engagement-root>/.project-evidence/crm/config.yml. See template at
51
+ plugin/templates/init/crm-config.template.yml. Then add boundaries.crm.record_ids
52
+ (or request_ids) to <project>/integrations.yml.
53
+ ```
54
+
55
+ **No "narrate from email" fallback exists in v3.7.0+.** CRM evidence comes from CRM
56
+ or it is absent — Coverage Notes will say so explicitly. Synthesis across sources
57
+ happens at consolidation, not at pull.
58
+
59
+ ## Field manifest dispatch
60
+
61
+ Before issuing the snapshot fetch, inspect `crm.entitySetName`:
62
+
63
+ - If it starts with `msfre_` (FDE engagement-submission family) → use `reference-packs/fde/crm-field-manifest.md` for the explicit `$select` list, the annotation `$expand`, and the snapshot file shape. The manifest IS the contract.
64
+ - For any other entity → still use the per-record loop below, but discover the column list from EntityDefinitions metadata once (cache to `crm.fieldMap`) and `$select` every column found (no relying on default projection).
65
+
66
+ ## Dataverse retrieval doctrine (REQUIRED — applies to every REST call below)
67
+
68
+ 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:
69
+
70
+ ### Rule 1 — Always request formatted values
71
+
72
+ ```powershell
73
+ $headers = @{
74
+ Authorization = "Bearer $token"
75
+ Accept = 'application/json'
76
+ 'OData-Version' = '4.0'
77
+ 'OData-MaxVersion' = '4.0'
78
+ Prefer = 'odata.include-annotations="OData.Community.Display.V1.FormattedValue"'
79
+ }
80
+ ```
81
+
82
+ 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.
83
+
84
+ ### Rule 2 — Use `PSObject.Properties[...]` accessor for `@`-named annotation properties
85
+
86
+ Dotted access fails for property names containing `@`. Always use the indexer:
87
+
88
+ ```powershell
89
+ $ownerDisplay = $row.PSObject.Properties['_ownerid_value@OData.Community.Display.V1.FormattedValue'].Value
90
+ $status = $row.PSObject.Properties['statuscode@OData.Community.Display.V1.FormattedValue'].Value
91
+ ```
92
+
93
+ ### Rule 3 — Inspect one live row before widening
94
+
95
+ 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:
96
+
97
+ ```powershell
98
+ $url = "$baseUrl/api/data/$api/${entitySet}?`$orderby=modifiedon desc&`$top=1"
99
+ $row = (Invoke-RestMethod -Uri $url -Headers $headers -Method Get).value | Select-Object -First 1
100
+ $row.PSObject.Properties.Name | Sort-Object
101
+ ```
102
+
103
+ Cache the discovered shape to `crm.fieldMap`.
104
+
105
+ ### Rule 4 — Add one field/filter at a time
106
+
107
+ 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.
108
+
109
+ ### Rule 5 — Always include `statecode` and `statuscode` in candidate searches
28
110
 
29
- ## Snapshot pass
111
+ Otherwise inactive/closed records become invisible during ranking.
30
112
 
31
- Write `snapshot/<record-id>.md` per CRM record with EVERY field the API returns (don't editorialize). Last fetched timestamp at top.
113
+ ### Rule 6 Prefer formatted values in rendered output
32
114
 
33
- Write to: `<engagement-root>/<project>/Evidence/<alias>/crm/snapshot/<entity>.md`
115
+ Snapshot files render the formatted display value in the main body. Keep raw GUID / integer code only in `## All other fields` for diagnostic value.
34
116
 
35
- Use template: `templates/snapshot/crm-<kind>.template.md`
117
+ ## Resolution order (when `crm.recordId` is unset)
118
+
119
+ 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.
120
+
121
+ **CRITICAL anti-patterns — read before writing any filter:**
122
+
123
+ - **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.
124
+ - **`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>`.
125
+ - **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`.
126
+
127
+ 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.
128
+ 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`).
129
+ 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.
130
+ 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.
131
+ 5. **Ask user**: present top 5 candidates (id, title, customer display, status, modifiedon). Never auto-pick on multi-match.
132
+
133
+ 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>'`.
134
+
135
+
136
+
137
+ For EACH record id in `boundaries.crm.record_ids`:
138
+
139
+ ### Step A — Resolve field map (once per project, cached)
140
+
141
+ If `m365Mutable.knownSections.<project>.crm.fieldMap` is empty, probe:
142
+
143
+ ```http
144
+ GET /api/data/v9.2/EntityDefinitions(LogicalName='<entity-logical-name>')/Attributes?$select=LogicalName,DisplayName,AttributeType
145
+ ```
146
+
147
+ Persist the resolved attribute list to `crm.fieldMap`. For FDE entities, cross-check against `reference-packs/fde/crm-field-manifest.md`.
148
+
149
+ ### Step B — Fetch the record with explicit $select + annotation $expand
150
+
151
+ Headers per **Dataverse retrieval doctrine Rule 1** (formatted values).
152
+
153
+ ```http
154
+ GET /api/data/v9.2/<entitySet>(<recordId>)?$select=<every-attribute-from-fieldMap>&$expand=Annotations($select=annotationid,subject,notetext,createdon,_createdby_value,filename,mimetype)
155
+ Prefer: odata.include-annotations="OData.Community.Display.V1.FormattedValue"
156
+ ```
157
+
158
+ WorkIQ-first equivalent (when host fallback to REST isn't available):
159
+
160
+ ```
161
+ 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."
162
+ ```
163
+
164
+ 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.`
165
+
166
+ ### Step C — Write snapshot file (one per record)
167
+
168
+ Write `snapshot/<entitySet>/<record-id>.md` with:
169
+
170
+ - File header (record id, MSX opp, status, owner, last-fetched timestamp).
171
+ - `## Source Basis` — tool used, boundary applied, fieldMap source, annotation count.
172
+ - `## AI Narrative Summary` (3+ paragraphs — REQUIRED FIRST per `evidence-thoroughness.instructions.md`).
173
+ - `## Engagement context` — every long-text field as a verbatim blockquote (see crm-field-manifest.md for the FDE shape).
174
+ - `## All other fields` — 2-column table; empty fields shown as `_(empty)_` so absence is visible.
175
+ - `## Notes (verbatim)` — one `### Note — <subject> (<createdon> by <createdby>)` block per annotation, full `notetext` as a verbatim blockquote, oldest first. NO truncation regardless of length.
176
+
177
+ Write to: `<engagement-root>/<project>/Evidence/<alias>/crm/snapshot/<entitySet>/<record-id>.md`
178
+
179
+ Use template: `templates/snapshot/crm-<kind>.template.md` (FDE entities: shape comes from `reference-packs/fde/crm-field-manifest.md`).
180
+
181
+ ### Step D — Failure handling
182
+
183
+ - 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.
184
+ - Annotation fetch failed but record fetched → write the record fields, mark `## Notes (verbatim)` with `❌ annotation-fetch-failed — N annotations expected` and continue.
36
185
 
37
186
  ## Stream pass
38
187
 
39
- Per `evidence-thoroughness.instructions.md`: 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`.
188
+ 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`.
40
189
 
41
190
  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).
42
191
 
@@ -46,8 +195,8 @@ If a week file already exists, MERGE (dedupe by event ID, append new events, kee
46
195
 
47
196
  ## Tools (in order)
48
197
 
49
- 1. **WorkIQ** — ````$WorkIQQuery`````
50
- 2. **Host fallback** — Dataverse REST Web API via `az account get-access-token` against the configured tenant (read connection from `<engagement-root>/.project-evidence/crm/config.yml`)
198
+ 1. **Dataverse REST Web API** (preferred for snapshot gives explicit `$select` + `$expand` control) via `az account get-access-token` against the configured tenant. Read connection from `<engagement-root>/.project-evidence/crm/config.yml`.
199
+ 2. **WorkIQ** — only when REST is unavailable; phrase queries to demand verbatim long-text + every annotation (see Step B template above). WorkIQ summarizes by default, so use this path only as fallback.
51
200
  3. **Graph REST** — last resort, soft-fail per `az-auth-conditional.instructions.md`.
52
201
  4. **Ask user** — paste verbatim source content if all above fail.
53
202
 
@@ -57,19 +206,23 @@ Document which path succeeded under `## Source Basis` in each output file.
57
206
 
58
207
  If discovered, immediately write to `m365Mutable.knownSections.<project>.<source>` with `discoveredOn` + `confidence`:
59
208
 
60
- - `crm.recordId` — pinned engagement record GUID (FE-NNNNN-style)
61
- - `crm.entitySetName` — Dataverse entity set name (e.g. `msdyn_engagements`)
209
+ - `crm.recordId` — pinned engagement record GUID
210
+ - `crm.entitySetName` — Dataverse entity set name
211
+ - `crm.fieldMap` — resolved attribute list (from EntityDefinitions probe)
62
212
 
63
213
  ## Update run-log
64
214
 
65
215
  After successful pass:
66
216
  - `sources.crm.last_pulled = now`
67
217
  - `sources.crm.watermark = now` (stream only; snapshot has no watermark)
68
- - `sources.crm.item_count = <N>`
218
+ - `sources.crm.records_fetched = <N>`
219
+ - `sources.crm.annotations_fetched = <N>` — total notes captured across all records
220
+ - `sources.crm.records_failed = <N>`
69
221
  - For each week touched, add to `weekly_files` index.
70
222
 
71
223
  ## Stop conditions
72
224
 
225
+ - Hard-prereq missing → refuse (see Hard prerequisites above).
73
226
  - Hint missing AND fuzzy resolution returns 0 candidates → ask user once, persist answer to mutable, continue.
74
227
  - Multiple plausible candidates → ask user to pick, persist answer.
75
- - WorkIQ fails AND host fallback fails AND Graph fails → write evidence file with `❌ all paths failed` marker, log to run-log errors, continue with rest of run.
228
+ - 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.
@@ -1,17 +1,25 @@
1
1
  ---
2
2
  name: "pull-email"
3
- version: "2.0.0"
4
- description: "Pull Email evidence (stream only — emails ARE events). WorkIQ-first. Scopes to configured project email folder per m365-mutable hints."
3
+ version: "2.3.1"
4
+ description: "Pull Email evidence (stream only — emails ARE events). WorkIQ-ONLY per workiq-only.instructions.md (m365_get_email / m365_search_emails / Graph REST FORBIDDEN — near-100% failure in this workspace). Folder-scoped fast path → root-scope WorkIQ fallback. Enumerate-then-fetch via two WorkIQ calls per project. Mutable folder-hint upsert during run. Throttle-aware. User-paste is a first-class fallback, NOT Graph."
5
5
  ---
6
6
 
7
7
  # Skill: pull-email
8
8
 
9
+
10
+ > **v3.7.6 + v3.11.0 contracts** — This skill operates under five HARD-rule doctrines:
11
+ > - `verbatim-by-default.instructions.md` — full bodies/notetext/fields by default; no preview-grade pulls accepted.
12
+ > - **`workiq-only.instructions.md` (v3.11.0)** — email list + body fetch go through WorkIQ ONLY. `m365_get_email`, `m365_search_emails`, `m365_list_emails`, and Graph REST URLs are FORBIDDEN as fallbacks (they fail nearly every call in this workspace). The canonical WorkIQ prompts (folder-scoped enumerate, root-scope fallback, per-message body fetch) are codified in that instruction — do not re-discover them.
13
+ > - `capture-learnings.instructions.md` — every fix/discovery is logged to `plugin/learnings/<source>.md` immediately.
14
+ > - `cleanup-on-resolution.instructions.md` — when a value resolves, all stale `no-match` / `not yet` notes referencing the prior unresolved state must be rewritten in the same turn.
15
+ > - `run-reports.instructions.md` — every refresh writes a per-user report under `Evidence/<alias>/refresh-reports/YYYY-MM-DD-HHMM_refresh.md`.
16
+ > - `evidence-layout-canonical.instructions.md` (kushi v3.12.1+) — ALL email artifacts MUST be written under `<project>/Evidence/<alias>/email/{snapshot,stream,_index,_legacy_*}/`. Writing to `<project>/email-context/`, `<project>/email/`, `<project>/_Weekly Summaries/`, or any other sibling path under `<project>/` is a DEFECT.
9
17
  Pulls **email** evidence in two shapes per `snapshot-vs-stream.instructions.md`:
10
18
 
11
19
  - **snapshot/** — (none — emails ARE events, no snapshot)
12
20
  - **stream/** — messages with full body, sender, recipients, attachments, reply chain — grouped by thread
13
21
 
14
- WorkIQ-first per `workiq-first.instructions.md`. Thoroughness per `evidence-thoroughness.instructions.md`; runtime detector + auto-retry + paste-prompt per `thoroughness-detector.instructions.md`. Citations per `citation-ledger.instructions.md`. Side-by-side mutable hints written immediately on discovery per `side-by-side-config.instructions.md`.
22
+ Thoroughness per `evidence-thoroughness.instructions.md`; runtime detector + auto-retry + paste-prompt per `thoroughness-detector.instructions.md`. Citations per `citation-ledger.instructions.md`. Side-by-side mutable hints written immediately on discovery per `side-by-side-config.instructions.md`.
15
23
 
16
24
  ## Inputs
17
25
 
@@ -26,50 +34,159 @@ WorkIQ-first per `workiq-first.instructions.md`. Thoroughness per `evidence-thor
26
34
  - `emailContext.folder` — full path of the project's email folder
27
35
  - `emailContext.folderId` — folder ID for direct lookup
28
36
 
29
- ## Snapshot pass
37
+ ## Boundaries (REQUIRED — see `scope-boundaries.instructions.md`)
30
38
 
31
- _(no snapshot for email emails are inherently event-stream)_
39
+ This skill REFUSES to query unless `<engagement-root>/<project>/integrations.yml#boundaries.email` is satisfied:
40
+
41
+ - `boundaries.email.mailboxes` — REQUIRED, non-empty. Empty = source disabled.
42
+ - `boundaries.email.sender_domains` — optional narrowing.
43
+ - `boundaries.email.subject_keywords` — optional narrowing.
44
+ - `boundaries.date_window_days` — defaults to 30 if absent.
45
+
46
+ Every WorkIQ ask, every `m365_search_emails` / `m365_list_emails` call, every Graph fallback MUST be scoped to those mailboxes + (if set) sender_domains + subject_keywords. Empty hits inside the boundary → write Coverage Notes citing the limiting key; do NOT widen the scope.
47
+
48
+ Refusal message when boundary is missing:
49
+
50
+ ```
51
+ email-boundary-missing — add boundaries.email.mailboxes to
52
+ <engagement-root>/<project>/integrations.yml. See doctrine in
53
+ plugin/instructions/scope-boundaries.instructions.md.
54
+ ```
55
+
56
+ ## Retrieval order (deterministic — folder fast path → root fallback)
57
+
58
+ 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:
59
+
60
+ ### Order 1 — Exact project-folder fast path (when hint exists)
61
+
62
+ If `m365Mutable.knownSections.<project>.emailContext.folder` (or `.folderId`) is set with `confidence >= medium`:
63
+
64
+ ```
65
+ 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."
66
+ ```
67
+
68
+ 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.
69
+
70
+ ### Order 2 — Root-scope fallback
71
+
72
+ Only if Order 1 returns insufficient/empty results after the retry, OR the project has no folder hint, OR multiple plausible hints exist:
73
+
74
+ ```
75
+ 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."
76
+ ```
77
+
78
+ ### Throttle handling (HARD stop in same run)
79
+
80
+ If WorkIQ returns `tooManyRequests`, `More than 3 retries performed`, or `We're experiencing high demand`:
81
+
82
+ - Mark `sources.email.coverage_state = throttled-tooManyRequests` in run-log.
83
+ - Write what enumeration HAS been collected so far to the message-index file with a `⚠️ throttled — partial enumeration` banner.
84
+ - Stop email pulls for this run. Do NOT issue broader mailbox queries.
85
+ - Continue with non-email sources.
86
+
87
+ ### Degraded list-only state
88
+
89
+ If Step A enumeration succeeds but Step B body fetch fails repeatedly (host + WorkIQ both returning empty / `body-unavailable` for >50% of messages):
90
+
91
+ - Set `sources.email.coverage_state = degraded-list-only`.
92
+ - Keep the message index with sent date, subject, sender, recipients, folder path, message link as evidence — these ARE usable signals at list level.
93
+ - Mark each affected message with `❌ body-unavailable` in the weekly stream file.
94
+ - Do NOT discard list-level evidence. Do NOT loop on broad fallback queries trying to rescue bodies.
95
+
96
+ ## Two-pass pull (REQUIRED — no single-call summarization)
32
97
 
33
- Write to: `<engagement-root>/<project>/Evidence/<alias>/email/snapshot/<entity>.md`
98
+ 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.**
34
99
 
35
- Use template: `templates/snapshot/email-<kind>.template.md`
100
+ ### Step A — Enumerate the message list
36
101
 
37
- ## Stream pass
102
+ For each `boundaries.email.mailboxes[i]` × the date window (and optional sender_domains / subject_keywords):
38
103
 
39
- Per `evidence-thoroughness.instructions.md`: every substantive thread gets sender + recipients (to+cc) + subject + **full body or close paraphrase** + attachments + reply chain in order. Group by thread.
104
+ ```
105
+ 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."
106
+ ```
107
+
108
+ 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.`
109
+
110
+ 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.
111
+
112
+ ### Step B — Per-message body fetch (one WorkIQ call per message)
113
+
114
+ For each message in the index, ONE WorkIQ call (per `workiq-only.instructions.md`):
115
+
116
+ ```
117
+ 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."
118
+ ```
119
+
120
+ **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.
121
+
122
+ 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).
123
+
124
+ ### Step C — Group by conversationId into threads
125
+
126
+ After all bodies fetched, group messages by `conversationId`. For each thread:
127
+
128
+ - Sort by `sentDate` ascending.
129
+ - Write a `## Thread: <subject>` block in the weekly stream file with:
130
+ - **AI Narrative Summary (REQUIRED FIRST, 3+ paragraphs)** — conversation arc, who's pushing what, decisions, asks, tone (per `evidence-thoroughness.instructions.md`).
131
+ - One `### Message <N> — <fromAddress> (<sentDate>)` block per message with: full headers (to/cc/subject), full body verbatim as a blockquote, attachment list.
132
+ - Append the thread block to the weekly file (`<YYYY-MM-DD>_email-stream.md`, date = Monday of the ISO week).
133
+
134
+ ## Snapshot pass
135
+
136
+ _(no snapshot for email — emails are inherently event-stream)_
137
+
138
+ ## Stream pass output
40
139
 
41
140
  Write to: `<engagement-root>/<project>/Evidence/<alias>/email/stream/<YYYY-MM-DD>_email-stream.md` (date = Monday of the ISO week the events fall in).
42
141
 
142
+ Write the message index to: `<engagement-root>/<project>/Evidence/<alias>/email/_index/<YYYY-MM-DD>_message-index.md`.
143
+
43
144
  Use template: `templates/weekly/email-summary.template.md`
44
145
 
45
- If a week file already exists, MERGE (dedupe by event ID, append new events, keep existing).
146
+ If a week file already exists, MERGE (dedupe by `internetMessageId`, append new threads, keep existing).
147
+
148
+ ## Tools (WorkIQ ONLY — per `workiq-only.instructions.md`)
46
149
 
47
- ## Tools (in order)
150
+ 1. **WorkIQ (`workiq ask`)** — the ONLY allowed path for both enumeration (Step A) and per-message body fetch (Step B). Always include the boundary keys + the verbatim/no-summary phrasing. Use the canonical prompts codified in `plugin/instructions/workiq-only.instructions.md` (Email Bodies section) — do NOT re-derive them.
151
+ 2. **Ask user (paste verbatim)** — first-class fallback when WorkIQ doubled-strict retry fails or returns `body-unavailable`. NOT a degradation; coverage.md labels it `Source: User paste on <ISO>`.
48
152
 
49
- 1. **WorkIQ** ````$WorkIQQuery`````
50
- 2. **Host fallback** — `m365_search_emails` / `m365_list_emails` scoped to `m365Auth.emailContext.folders`
51
- 3. **Graph REST** — last resort, soft-fail per `az-auth-conditional.instructions.md`.
52
- 4. **Ask user** — paste verbatim source content if all above fail.
153
+ **FORBIDDEN** for this skill (per workiq-only.instructions.md): `m365_get_email`, `m365_search_emails`, `m365_list_emails`, `m365_list_mail_folders`, `m365_search_mail`, Graph REST `https://graph.microsoft.com/v1.0/me/messages*`, Graph REST `https://graph.microsoft.com/v1.0/me/mailFolders*`. These tools have a near-100% failure rate in this workspace. Calling them is a DEFECT; coverage.md must NOT show them in the attempt trail.
53
154
 
54
- Document which path succeeded under `## Source Basis` in each output file.
155
+ Document the WorkIQ request-id + the exact prompt + which boundary was applied under `## Source Basis` in each output file (per workiq-only coverage.md requirements).
55
156
 
56
157
  ## Mutable hints to upsert (during the run, not at the end)
57
158
 
58
- If discovered, immediately write to `m365Mutable.knownSections.<project>.<source>` with `discoveredOn` + `confidence`:
159
+ If discovered, immediately write to `m365Mutable.knownSections.<project>.<source>` with `discoveredOn` + `confidence`. Mandatory in EVERY run mode (bootstrap, refresh, retry):
59
160
 
60
- - `emailContext.folder` — full path of the project's email folder
161
+ - `emailContext.folder` — full path of the project's email folder (upsert only when `confidence >= medium`)
61
162
  - `emailContext.folderId` — folder ID for direct lookup
163
+ - `emailContext.lastFastPathUsed` — boolean; whether Order 1 succeeded this run
164
+
165
+ Upsert only confident folder mappings. Do not write low-confidence guesses — that pollutes the hint cache and degrades future fast-path success.
62
166
 
63
167
  ## Update run-log
64
168
 
65
169
  After successful pass:
66
170
  - `sources.email.last_pulled = now`
67
- - `sources.email.watermark = now` (stream only; snapshot has no watermark)
68
- - `sources.email.item_count = <N>`
171
+ - `sources.email.watermark = now`
172
+ - `sources.email.coverage_state = ok | degraded-list-only | throttled-tooManyRequests | unavailable`
173
+ - `sources.email.retrieval_path = fast-folder | fast-folder-retry | root-fallback`
174
+ - `sources.email.messages_enumerated = <N>` — total messages discovered in scope
175
+ - `sources.email.messages_fetched = <N>` — total messages with full body captured
176
+ - `sources.email.messages_failed = <N>` — `body-unavailable` count
177
+ - `sources.email.threads = <N>` — number of conversations grouped
69
178
  - For each week touched, add to `weekly_files` index.
70
179
 
180
+ ## Depth bar (per `evidence-thoroughness.instructions.md`)
181
+
182
+ The pre-Step-C anti-summary doctrine + the Step-C AI Narrative Summary per thread together produce the depth users expect. Empty thread bodies are a defect — if Step B couldn't fetch a body, the message gets a `❌ body-unavailable` marker, NOT a paraphrase from the index row.
183
+
184
+ **Completeness bar:** `messages_enumerated` should equal `messages_fetched + messages_failed`. Drift surfaces in run-log; consolidate skill renders it as a Coverage Note.
185
+
71
186
  ## Stop conditions
72
187
 
188
+ - Boundary missing → refuse (see Boundaries above).
73
189
  - Hint missing AND fuzzy resolution returns 0 candidates → ask user once, persist answer to mutable, continue.
74
- - Multiple plausible candidatesask user to pick, persist answer.
75
- - WorkIQ fails AND host fallback fails AND Graph fails write evidence file with `❌ all paths failed` marker, log to run-log errors, continue with rest of run.
190
+ - 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.
191
+ - Per-message fetch failedrecord marker, continue.
192
+ - All paths failed → write evidence file with `❌ all paths failed` marker, log to run-log errors, continue with rest of run.