kushi-agents 4.4.0 → 4.4.1

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 (37) hide show
  1. package/package.json +1 -1
  2. package/plugin/agents/kushi.agent.md +1 -1
  3. package/plugin/instructions/ado-engagement-tree.instructions.md +1 -1
  4. package/plugin/instructions/az-auth-conditional.instructions.md +2 -2
  5. package/plugin/instructions/azure-auth-patterns.instructions.md +6 -6
  6. package/plugin/instructions/bootstrap-status-format.instructions.md +13 -2
  7. package/plugin/instructions/cleanup-on-resolution.instructions.md +1 -1
  8. package/plugin/instructions/crm-bootstrap-discovery.instructions.md +1 -1
  9. package/plugin/instructions/deferred-retry-on-workiq-fail.instructions.md +155 -0
  10. package/plugin/instructions/engagement-root-resolution.instructions.md +13 -10
  11. package/plugin/instructions/evidence-layout-canonical.instructions.md +2 -2
  12. package/plugin/instructions/identity-resolution.instructions.md +15 -8
  13. package/plugin/instructions/m365-id-registry.instructions.md +1 -1
  14. package/plugin/instructions/scope-boundaries.instructions.md +4 -4
  15. package/plugin/instructions/workiq-only.instructions.md +4 -2
  16. package/plugin/lib/Get-KushiConfig.ps1 +112 -7
  17. package/plugin/prompts/bootstrap.prompt.md +64 -49
  18. package/plugin/skills/apply-ado-update/SKILL.md +2 -2
  19. package/plugin/skills/bootstrap-project/SKILL.md +10 -10
  20. package/plugin/skills/propose-ado-update/SKILL.md +3 -3
  21. package/plugin/skills/pull-ado/SKILL.md +6 -6
  22. package/plugin/skills/pull-crm/SKILL.md +5 -5
  23. package/plugin/skills/pull-email/SKILL.md +2 -2
  24. package/plugin/skills/pull-meetings/SKILL.md +1 -1
  25. package/plugin/skills/pull-onenote/scripts/recapture-section-url.mjs +2 -1
  26. package/plugin/skills/pull-onenote/write-snapshot.mjs +2 -1
  27. package/plugin/skills/pull-sharepoint/SKILL.md +1 -1
  28. package/plugin/skills/pull-teams/SKILL.md +1 -1
  29. package/plugin/skills/refresh-project/SKILL.md +21 -1
  30. package/plugin/skills/self-check/run.ps1 +24 -1
  31. package/plugin/templates/ado-update/integrations-ado-writes.example.yml +1 -1
  32. package/plugin/templates/init/m365-auth.template.json +5 -5
  33. package/plugin/templates/init/project-integrations.template.yml +2 -2
  34. package/plugin/templates/snapshot/onenote-page.template.md +3 -3
  35. package/src/config-loader.mjs +92 -5
  36. package/plugin/templates/init/ado-config.template.yml +0 -21
  37. package/plugin/templates/init/crm-config.template.yml +0 -16
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kushi-agents",
3
- "version": "4.4.0",
3
+ "version": "4.4.1",
4
4
  "description": "Install Kushi — multi-source project evidence agent with snapshot+stream capture across Email, Teams, OneNote, SharePoint, Meetings, CRM, ADO. WorkIQ-only for M365 sources (Graph / m365_* FORBIDDEN as fallbacks; user-paste is first-class). Host-agnostic.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -100,7 +100,7 @@ When a user message arrives:
100
100
  ```
101
101
  <workspace>/.kushi/config/user/project-evidence.yml ← personal: alias, engagement_root, active_projects (seeded by installer; never overwritten)
102
102
  <workspace>/.kushi/config/shared/integrations.yml ← optional global CRM/ADO defaults (seeded by installer; never overwritten)
103
- <engagement-root>/.project-evidence/ ← per-machine, per-user M365 + CRM + ADO config (OneDrive-synced)
103
+ <workspace>/.kushi/config/ ← per-machine, per-user M365 config + per-project shared ADO/CRM connection (v4.4.0+, replaces legacy `<engagement-root>/.project-evidence/`)
104
104
  m365/m365-auth.json
105
105
  m365/m365-mutable.json
106
106
  crm/config.yml
@@ -49,7 +49,7 @@ After the Engagement WI ID is resolved:
49
49
 
50
50
  ## Source files
51
51
 
52
- - `<engagement-root>/.project-evidence/ado/config.yml` — connection: `tenantId`, `organization`, `defaultProject`, `apiVersion`, `azDevOpsResourceId`.
52
+ - `<workspace>/.kushi/config/shared/integrations.yml` — connection: `tenantId`, `organization`, `defaultProject`, `apiVersion`, `azDevOpsResourceId`.
53
53
  - `<engagement-root>/<project>/integrations.yml#ado` — per-project: `engagement_id`, `queryId`, `area_paths`, `iteration_paths`.
54
54
  - `<engagement-root>/<project>/integrations.yml#boundaries.ado` — required scope (`area_paths` + optional `work_item_ids`).
55
55
 
@@ -10,8 +10,8 @@ The skill calls `az account get-access-token` only when CRM (Dataverse) or ADO a
10
10
  ## Decision logic
11
11
 
12
12
  ```
13
- crm_enabled = Test-Path <engagement-root>/.project-evidence/crm/config.yml
14
- ado_enabled = Test-Path <engagement-root>/.project-evidence/ado/config.yml
13
+ crm_enabled = Test-Path <workspace>/.kushi/config/shared/integrations.yml
14
+ ado_enabled = Test-Path <workspace>/.kushi/config/shared/integrations.yml
15
15
 
16
16
  if (NOT crm_enabled AND NOT ado_enabled):
17
17
  Skip az check entirely. Display "az sign-in skipped (no CRM/ADO configured)".
@@ -39,7 +39,7 @@ Acquire each token **once per run** (per `auth-and-retry.instructions.md §2`).
39
39
  ### 2.1 CRM / Dataverse
40
40
 
41
41
  ```powershell
42
- $crmConfig = Get-Content "$engagementRoot\.project-evidence\crm\config.yml" -Raw | ConvertFrom-Yaml # or JSON variant
42
+ $crmConfig = (Get-Content "$workspace\.kushi\config\shared\integrations.yml" -Raw | ConvertFrom-Yaml).crm # v4.4.0+ was <engagement-root>\.project-evidence\crm\config.yml
43
43
  $tenant = $crmConfig.tenantId
44
44
  $crmBaseUrl = $crmConfig.baseUrl
45
45
  $crmToken = (az account get-access-token --tenant $tenant --resource $crmBaseUrl --query accessToken -o tsv --only-show-errors 2>&1).Trim()
@@ -60,7 +60,7 @@ $crmHeaders = @{
60
60
  ADO's resource is the constant `499b84ac-1321-427f-aa17-267ca6975798` (Visual Studio Team Services). Read it from config — never hardcode:
61
61
 
62
62
  ```powershell
63
- $adoConfig = Get-Content "$engagementRoot\.project-evidence\ado\config.yml" -Raw | ConvertFrom-Yaml
63
+ $adoConfig = (Get-Content "$workspace\.kushi\config\shared\integrations.yml" -Raw | ConvertFrom-Yaml).ado # v4.4.0+ — was <engagement-root>\.project-evidence\ado\config.yml
64
64
  $tenant = $adoConfig.tenantId
65
65
  $adoRes = $adoConfig.resource # 499b84ac-1321-427f-aa17-267ca6975798
66
66
  $adoToken = (az account get-access-token --tenant $tenant --resource $adoRes --query accessToken -o tsv --only-show-errors 2>&1).Trim()
@@ -127,7 +127,7 @@ Allowed tenants are config-driven, not hardcoded. Validate before the first ADO
127
127
 
128
128
  ```powershell
129
129
  # $account from Section 1
130
- $adoConfig = Get-Content "$engagementRoot\.project-evidence\ado\config.yml" -Raw | ConvertFrom-Yaml
130
+ $adoConfig = (Get-Content "$workspace\.kushi\config\shared\integrations.yml" -Raw | ConvertFrom-Yaml).ado # v4.4.0+
131
131
  $allowedTenants = @($adoConfig.allowedTenantIds)
132
132
  $currentTenant = $account.tenantId
133
133
  if ($allowedTenants.Count -gt 0 -and $allowedTenants -notcontains $currentTenant) {
@@ -224,9 +224,9 @@ Maintained alongside `auth-and-retry §3` (canonical) — quick lookup:
224
224
 
225
225
  | Service | Config file (engagement-scoped) |
226
226
  |---------|---------------------------------|
227
- | Dynamics 365 / CRM | `<engagement-root>/.project-evidence/crm/config.yml` |
228
- | Azure DevOps | `<engagement-root>/.project-evidence/ado/config.yml` |
229
- | Microsoft Graph / M365 / OneNote | `<engagement-root>/.project-evidence/m365/m365-mutable.json` + `<engagement-root>/.project-evidence/m365/m365-auth.json` |
227
+ | Dynamics 365 / CRM | `<workspace>/.kushi/config/shared/integrations.yml` |
228
+ | Azure DevOps | `<workspace>/.kushi/config/shared/integrations.yml` |
229
+ | Microsoft Graph / M365 / OneNote | `<workspace>/.kushi/config/user/m365-mutable.json` + `<workspace>/.kushi/config/user/m365-auth.json` |
230
230
  | WorkIQ CLI path | `<workspace>/.kushi/config/user/project-evidence.yml` (workspace-scoped) |
231
231
  | Global integrations (optional) | `<workspace>/.kushi/config/shared/integrations.yml` |
232
232
 
@@ -22,8 +22,8 @@ When `bootstrap-project` (or any full refresh / retry workflow) finishes a proje
22
22
 
23
23
  | Check | Status | Notes |
24
24
  |---|---|---|
25
- | `.project-evidence/ado/config.yml` filled | resolved \| blocked-auth \| missing | ... |
26
- | `.project-evidence/crm/config.yml` filled | ... | ... |
25
+ | `.kushi/config/shared/integrations.yml` filled | resolved \| blocked-auth \| missing | ... |
26
+ | `.kushi/config/shared/integrations.yml` filled | ... | ... |
27
27
  | `<project>/integrations.yml` boundaries present | ... | ... |
28
28
  | `az` CLI tenant matches | cli-available \| blocked-auth | ... |
29
29
 
@@ -54,6 +54,17 @@ When `bootstrap-project` (or any full refresh / retry workflow) finishes a proje
54
54
  | CRM | blocked-auth | tenant mismatch | `az login --tenant <id>` |
55
55
  | Email | throttled-tooManyRequests | WorkIQ rate-limited | retry next refresh |
56
56
 
57
+ ## Deferred Retries (kushi v4.4.1+, per `deferred-retry-on-workiq-fail.instructions.md`)
58
+
59
+ Omit this section entirely if `<project>/Evidence/<alias>/_deferred-retries/` is empty across all contributors.
60
+
61
+ | Source | Target | Attempts | Marker | First seen | Discovered by |
62
+ |---|---|---|---|---|---|
63
+ | meetings | "JD FDE Intake" 2026-05-13 | 1 | `Evidence/ushak/_deferred-retries/2026-05-20-1230_meetings_empty.yml` | 2026-05-20 | ushak |
64
+ | onenote | "Architecture overview" | 2 | `Evidence/ushak/_deferred-retries/2026-05-19-0810_onenote_throttled.yml` | 2026-05-19 | ushak |
65
+
66
+ Markers older than 5 attempts auto-promote to `OPEN-QUESTIONS-DRAFT.md`. The next `refresh` drains the queue (Step 2a in `refresh-project/SKILL.md`) — do NOT manually retry by calling `m365_get_*` / Graph; those are forbidden per `workiq-only.instructions.md`.
67
+
57
68
  ## Bootstrap Status
58
69
 
59
70
  ONE final line, normalized:
@@ -28,7 +28,7 @@ When a resolution succeeds, in the same turn, prune:
28
28
  - Remove inline comments like `# TODO: find this`, `# could not resolve as of YYYY-MM-DD`.
29
29
  - Keep `pinned_on` + `pinned_by` (those are durable provenance).
30
30
 
31
- ### `<engagement-root>/.project-evidence/m365/m365-mutable.json`
31
+ ### `<workspace>/.kushi/config/user/m365-mutable.json`
32
32
  - Remove entries under `m365Mutable.unresolved.<project>.<source>` if the same key was just resolved into `m365Mutable.knownSections.<project>.<source>`.
33
33
  - Replace any `confidence: 'low'` entry with the new `confidence: 'high'` entry on resolution; do not keep both.
34
34
 
@@ -21,7 +21,7 @@ Any other path that writes `disabled: true` is a defect. The correct fallback fo
21
21
 
22
22
  ```powershell
23
23
  # 1. Acquire Dataverse token (per pull-crm Step Auth)
24
- $config = Get-Content "<engagement-root>/.project-evidence/crm/config.yml" | ConvertFrom-Yaml
24
+ $config = Get-Content "<workspace>/.kushi/config/shared/integrations.yml" | ConvertFrom-Yaml
25
25
  $token = (az account get-access-token --tenant $config.tenant_id --resource $config.base_url --query accessToken -o tsv --only-show-errors).Trim()
26
26
  $headers = @{
27
27
  Authorization = "Bearer $token"
@@ -0,0 +1,155 @@
1
+ ---
2
+ applyTo: "**"
3
+ description: "When WorkIQ fails (and the doubled-strict retry also fails), Kushi MUST NOT fall back to Graph / m365_get_* / Dataverse REST as a workaround. Instead, write a deferred-retry marker, surface it in the run report, and continue. The next refresh drains the queue. This is the v4.4.1 hard rule that closes the last Graph-fallback escape hatch."
4
+ ---
5
+
6
+ # Deferred-Retry on WorkIQ Failure (HARD RULE, kushi v4.4.1+)
7
+
8
+ ## Why this exists
9
+
10
+ `workiq-only.instructions.md` (v3.11.0+) forbade Graph / `m365_get_*` as a **first-class fallback**. But the doctrine's step 3 still said *"ask the user to paste"* — which in practice agents interpreted as:
11
+
12
+ 1. *"WorkIQ failed → block the entire run and wait for the human."* (bootstrap stops mid-flight, evidence for OTHER sources is lost)
13
+ 2. *"WorkIQ failed → silently try `m365_get_*` since it's right there in the tool list."* (the very anti-pattern workiq-only was meant to kill)
14
+ 3. *"WorkIQ failed → skip the source, no record."* (next refresh never retries; the gap is permanent)
15
+
16
+ All three are defects. This rule replaces them with a deterministic, auditable, **non-blocking** flow.
17
+
18
+ ## The rule (HARD)
19
+
20
+ When a WorkIQ call fails (auth error, empty after doubled-strict retry, throttled, CLI error, or any non-success), Kushi MUST:
21
+
22
+ 1. **NOT call Graph, `m365_get_*`, or any other M365 host tool as a fallback.** No exceptions. Even if the tool is right there and looks like it would work. The doctrine in `workiq-only.instructions.md` is absolute.
23
+ 2. **Write a deferred-retry marker** (see schema below).
24
+ 3. **Surface a one-liner** in coverage.md, the per-user run report, and the project's `bootstrap-status.md` `## Deferred Retries` section.
25
+ 4. **Continue the run** for all other sources. Never block the orchestrator on a single source's failure.
26
+ 5. **Inform the user** at end-of-run with the count of deferred items and the path to the queue directory.
27
+
28
+ The next `refresh-project` run drains the queue first (Step 2a). On success, the marker is deleted. On repeated failure, the marker's `attempts` counter increments and the user is re-informed.
29
+
30
+ ## Marker file location
31
+
32
+ ```
33
+ <engagement-root>/<project>/Evidence/<alias>/_deferred-retries/<YYYY-MM-DD-HHmm>_<source>_<short-reason>.yml
34
+ ```
35
+
36
+ - Per-contributor (`<alias>`) — never shared, so multi-user safety is moot.
37
+ - Sortable by filename (chronological).
38
+ - One marker per failed call. Do NOT batch — granular markers make retry deterministic.
39
+
40
+ ## Marker schema
41
+
42
+ ```yaml
43
+ # Deferred-retry marker — auto-generated. Do NOT hand-edit.
44
+ # Drained by refresh-project Step 2a. Deleted on retry success.
45
+ schema_version: 1
46
+ source: meetings # one of: email, teams, meetings, onenote, sharepoint, crm, ado, misc, identity
47
+ project: HCA
48
+ alias: ushak
49
+ created_at: 2026-05-20T12:30:00Z
50
+ attempts: 1
51
+ last_attempt_at: 2026-05-20T12:30:00Z
52
+ window: { from: '2026-04-20', to: '2026-05-20' }
53
+ target:
54
+ # source-specific identifying fields — exactly what the retry needs to re-issue the call
55
+ subject: "JD FDE Intake"
56
+ date: "2026-05-13"
57
+ workiq:
58
+ command: 'workiq ask -q "Find the Teams meeting titled \"JD FDE Intake\" that occurred on 2026-05-13..."'
59
+ request_id: '54d9c6bc-6e56-43b6-9eb7-ac23e86e2cc0' # if WorkIQ returned one before failing
60
+ error_class: 'empty-after-doubled-strict' # auth-error | empty | empty-after-doubled-strict | throttled | cli-error | timeout
61
+ error_message: 'WorkIQ returned a summary only after both prompts.'
62
+ user_message: |
63
+ Could not retrieve the full verbatim transcript of "JD FDE Intake" (2026-05-13) via WorkIQ.
64
+ The next refresh will retry automatically. To populate now, paste the transcript into
65
+ Evidence/ushak/meetings/snapshot/2026-05-13_jd-fde-intake_transcript.md and mark this marker
66
+ resolved by deleting the file.
67
+ ```
68
+
69
+ ## Producer contract (every pull-* skill + identity-resolution)
70
+
71
+ Before considering a source "failed and skipped", the skill MUST:
72
+
73
+ 1. Call WorkIQ once with the canonical prompt from `workiq-only.instructions.md`.
74
+ 2. On weak result, call the doubled-strict retry prompt from the same table.
75
+ 3. If both fail:
76
+ - Write the marker per the schema above.
77
+ - Add a row to coverage.md: `Source: WorkIQ → DEFERRED (marker: <relative-path>)`.
78
+ - Continue the run. Do NOT throw. Do NOT call any other M365 tool.
79
+
80
+ Pseudocode (PowerShell shape):
81
+
82
+ ```powershell
83
+ $result = Invoke-WorkIQ -Query $canonical
84
+ if (-not (Test-Sufficient $result)) {
85
+ $result = Invoke-WorkIQ -Query $doubledStrict
86
+ }
87
+ if (-not (Test-Sufficient $result)) {
88
+ Write-DeferredRetryMarker -Source $src -Target $target -Window $win -Workiq $workiqMeta
89
+ Add-CoverageRow -Source 'WorkIQ' -Status 'DEFERRED' -MarkerPath $markerPath
90
+ return # CONTINUE the orchestrator with the next source. NEVER call m365_get_*.
91
+ }
92
+ ```
93
+
94
+ ## Consumer contract (refresh-project)
95
+
96
+ `refresh-project` SKILL Step 2a (NEW, REQUIRED) runs **before** the per-source dispatch loop:
97
+
98
+ ```
99
+ For each <alias>/_deferred-retries/*.yml in chronological order:
100
+ - Load marker
101
+ - Re-issue the canonical WorkIQ query with original window + target
102
+ - If success → write artifact to its canonical Evidence/ path, delete marker, log "drained" in refresh report
103
+ - If failure → increment attempts, update last_attempt_at, leave marker in place, log "still-deferred"
104
+ After drain → run normal Step 2 per-source dispatch
105
+ After Step 2 → report drain results in the run report's `## Deferred-retry drain` section
106
+ ```
107
+
108
+ If `attempts` reaches 5, do NOT keep silently retrying. Promote to a project-level Open Question:
109
+
110
+ ```
111
+ - [ ] DEFERRED-RETRY (5 attempts): <source> for <target>. See <marker-path>. Manual intervention required (paste verbatim, or remove the marker if no longer needed).
112
+ ```
113
+
114
+ ## bootstrap-status.md integration
115
+
116
+ `bootstrap-status-format.instructions.md` mandates a `## Deferred Retries` section. Format:
117
+
118
+ ```
119
+ ## Deferred Retries
120
+
121
+ | Source | Target | Attempts | Marker | First seen |
122
+ |---|---|---|---|---|
123
+ | meetings | JD FDE Intake 2026-05-13 | 1 | Evidence/ushak/_deferred-retries/2026-05-20-1230_meetings_empty.yml | 2026-05-20 |
124
+ ```
125
+
126
+ Section is omitted entirely when no markers exist.
127
+
128
+ ## What "informing the user" looks like
129
+
130
+ At end-of-run summary (every `pull-*`, `bootstrap-project`, `refresh-project`):
131
+
132
+ ```
133
+ ⚠ Deferred retries this run: 2 (will retry on next `refresh <project>`)
134
+ - meetings/JD FDE Intake 2026-05-13 → Evidence/ushak/_deferred-retries/2026-05-20-1230_meetings_empty.yml
135
+ - onenote/Architecture overview → Evidence/ushak/_deferred-retries/2026-05-20-1231_onenote_throttled.yml
136
+ ```
137
+
138
+ Never silent. Never collapsed. The user sees exactly which calls deferred and where to find the markers.
139
+
140
+ ## Anti-patterns (defects)
141
+
142
+ 1. **Calling `m365_get_*` / Graph REST after a WorkIQ failure.** FORBIDDEN. Even "just to check if it works." Even "as a last resort." Write the marker; move on.
143
+ 2. **Throwing / blocking the orchestrator on a single source's WorkIQ failure.** FORBIDDEN. Bootstrap and refresh dispatch every enabled source; a single failure must not stop the others.
144
+ 3. **Silently skipping a source with no marker.** FORBIDDEN. If WorkIQ failed and you did NOT write a marker, the user has no signal and `refresh` will never retry. Always mark.
145
+ 4. **Asking the user to paste mid-run as the primary recovery.** Pasting is a manual override the user MAY do later (by populating the artifact directly and deleting the marker). It is NOT the agent's recovery flow. The agent's recovery flow is: mark + continue + inform.
146
+ 5. **Promoting markers to OPEN-QUESTIONS-DRAFT.md before 5 attempts.** Too noisy. Let refresh drain naturally.
147
+ 6. **Storing markers outside `Evidence/<alias>/_deferred-retries/`.** Wrong location = the drainer can't find them. Wrong scope = breaks multi-user (markers are per-contributor by design).
148
+
149
+ ## Cross-references
150
+
151
+ - `workiq-only.instructions.md` — WorkIQ is the only path. This file defines what happens when that path fails.
152
+ - `identity-resolution.instructions.md` — "Failure modes" table now cites this rule.
153
+ - `bootstrap-status-format.instructions.md` — `## Deferred Retries` section contract.
154
+ - `multi-user-shared-files.instructions.md` — `_deferred-retries/` is per-contributor (under `Evidence/<alias>/`) so multi-user collision doctrine does not apply.
155
+ - `verbatim-by-default.instructions.md` — deferring is NOT a silent skip. A marker IS the audit trail.
@@ -15,20 +15,23 @@ Resolve `<engagement-root>` in this order — first match wins:
15
15
  2. **`customer_workspace/FDEDocs/`** — if the user's workspace has this symlink, follow it. Common in FDE-style installs.
16
16
  3. **Ask the user** once and persist the answer to `<workspace>/.kushi/config/user/project-evidence.yml engagement_root`.
17
17
 
18
- ## Live config location
18
+ ## Live config location (v4.4.0+)
19
19
 
20
- Once `<engagement-root>` is known, live filled configs live at:
20
+ Live filled configs live under the workspace (where `.kushi/` is), NOT under the engagement root:
21
21
 
22
22
  ```
23
- <engagement-root>/.project-evidence/
24
- m365/m365-auth.json
25
- m365/m365-mutable.json
26
- crm/config.yml
27
- ado/config.yml
28
- project-evidence.yml optional per-engagement override
23
+ <workspace>/.kushi/config/
24
+ user/ ← per-contributor (gitignored)
25
+ project-evidence.yml identity, engagement_root, workiq cli path
26
+ m365-auth.json tenant + default notebook + mailbox folders + SP root
27
+ m365-mutable.json discovered IDs (knownSections.<projectKey>)
28
+ shared/ team-owned (safe to commit)
29
+ integrations.yml ADO + CRM connection blocks (per project)
29
30
  ```
30
31
 
31
- This folder is OneDrive-synced (typically) and follows the user across machines.
32
+ The legacy location `<engagement-root>/.project-evidence/` is no longer used as of kushi v4.4.0. Existing installs are migrated on first run of v4.4.0+. The new layout is host-agnostic: workspace is always where `.kushi/` lives (vscode target: project root; clawpilot target: any `cwd` where the user invokes Kushi).
33
+
34
+ `<engagement-root>` is still where per-project Evidence/, State/, and `<project>/integrations.yml` (per-project boundaries) live.
32
35
 
33
36
  ## Project resolution
34
37
 
@@ -42,7 +45,7 @@ Once `<engagement-root>` is known, individual projects are subfolders:
42
45
 
43
46
  Project name resolution (always fuzzy):
44
47
 
45
- 1. Match against keys in `<engagement-root>/.project-evidence/m365/m365-mutable.json m365Mutable.knownSections`.
48
+ 1. Match against keys in `<workspace>/.kushi/config/user/m365-mutable.json m365Mutable.knownSections`.
46
49
  2. Match against `active_projects:` in `<workspace>/.kushi/config/user/project-evidence.yml`.
47
50
  3. Match against actual subfolder names under `<engagement-root>`.
48
51
 
@@ -85,7 +85,7 @@ Anything outside these paths is invisible to them — by design. The path IS the
85
85
 
86
86
  `plugin/skills/self-check/run.ps1` -Deep MUST detect any `<project>/<sibling>/` folder under engagement roots where:
87
87
 
88
- - `<sibling>` is not in the allow-list `@('Evidence','State','Reports','.kushi','.kushi-reference','.project-evidence','.vscode')`, AND
88
+ - `<sibling>` is not in the allow-list `@('Evidence','State','Reports','.kushi','.kushi-reference','.vscode')`, AND
89
89
  - `<sibling>` contains markdown files matching the per-source patterns (`*-summary.md`, `*-stream.md`, `*-context*`, `current-state.md`, `index.md`, etc.).
90
90
 
91
91
  When detected → emit a D14 finding with the canonical replacement path from Rule 2.
@@ -105,7 +105,7 @@ Together they guarantee that two contributors running the same verb on the same
105
105
 
106
106
  - `snapshot-vs-stream.instructions.md` — the two shapes inside each `<source>/` folder.
107
107
  - `scope-boundaries.instructions.md` — what each source is allowed to query (orthogonal: scope vs path).
108
- - `side-by-side-config.instructions.md` — config files (mutable hints, integrations.yml) also outside `<project>/`, but under `<engagement-root>/.project-evidence/`.
108
+ - `side-by-side-config.instructions.md` — config files (mutable hints, integrations.yml) live under `<workspace>/.kushi/config/` (v4.4.0+, was `<engagement-root>/.project-evidence/`).
109
109
  - `run-reports.instructions.md` — every layout-migration MUST appear in the refresh report.
110
110
  - `cleanup-on-resolution.instructions.md` — once a legacy folder is migrated, all stale references in older summaries/notes get rewritten in the same turn.
111
111
  - `bootstrap-project/SKILL.md` — creates the canonical tree on first run.
@@ -37,30 +37,37 @@ Map the response:
37
37
  > ✓ Identity: `Alex Smith <alex@microsoft.com>` (alias=`alex`). Edit `.kushi/config/user/project-evidence.yml` to override.
38
38
  3. **Continue** the prompt.
39
39
 
40
- ## Failure modes
40
+ ## Failure modes (kushi v4.4.1+: never block, never fallback to Graph)
41
+
42
+ Per `deferred-retry-on-workiq-fail.instructions.md`, identity resolution NEVER blocks the orchestrator and NEVER calls `m365_get_*` / Graph as a fallback. On WorkIQ failure, the agent writes a deferred-retry marker and continues with a derived alias so evidence still lands somewhere it can later be re-keyed.
41
43
 
42
44
  | Scenario | Behavior |
43
45
  |-----------------------------------------------|------------------------------------------------------------------------------------------------------------------------------|
44
- | WorkIQ returns auth error | Block: "Sign in to WorkIQ first: `workiq accept-eula && workiq ask -q ping`". Do not proceed. |
45
- | WorkIQ returns empty / NO_RESULTS | Block: "WorkIQ could not resolve your identity. Try `workiq ask -q 'who am I'` and retry." |
46
- | WorkIQ binary missing | This should already have been caught by the installer's pre-flight. If reached, block with the same install hint. |
47
- | User has explicit non-placeholder values | Skip resolution entirely. Never overwrite user-set values. |
46
+ | WorkIQ returns auth error | Write deferred-retry marker (`source: identity`, `error_class: auth-error`). Echo: *" Identity unresolved (WorkIQ auth). Using alias=`<from-config-or-env>`; will retry on next refresh. Sign in with `workiq accept-eula && workiq ask -q ping` and re-run."* Continue. **Do NOT call `m365_get_my_profile` or Graph `/me`.** |
47
+ | WorkIQ returns empty / NO_RESULTS | Write marker (`error_class: empty`). Echo: *" Identity unresolved (WorkIQ empty). Using alias=`<env-user>`; will retry on next refresh."* Continue. |
48
+ | WorkIQ binary missing | Installer pre-flight should catch this. If reached at skill-time, write marker (`error_class: cli-missing`), echo the install hint, continue with alias=`<env-user>`. |
49
+ | User has explicit non-placeholder values | Skip resolution entirely. Never overwrite user-set values. No marker needed. |
48
50
  | Alias collision with another contributor | Bootstrap detects existing `Evidence/<alias>/` whose `contributors.yml` records a different email → ask the user to disambiguate (suggest `<alias>-<tenant-prefix>` e.g. `alex-ms`). |
49
51
 
52
+ **Derived alias fallback (when WorkIQ unavailable):** use `$env:USERNAME` (Windows) or `$env:USER` (POSIX) lowercased. Persist provisionally with `alias_resolved_from: env-fallback-pending-workiq`. The next refresh's deferred-retry drain will re-issue the WorkIQ probe and rename `Evidence/<env-alias>/` → `Evidence/<workiq-alias>/` if they differ.
53
+
50
54
  ## What NOT to do
51
55
 
52
56
  * Do NOT ask the user `What alias should Kushi use?`. The legacy onboarding prompt is gone.
53
- * Do NOT call `m365_*` / Graph as a fallback. WorkIQ is the single source of identity truth (`workiq-first.instructions.md`).
57
+ * Do NOT call `m365_get_my_profile`, `m365_*`, or Graph `/me` as a fallback. WorkIQ is the single source of identity truth. On failure, write a marker and use the env-fallback. See `workiq-only.instructions.md` and `deferred-retry-on-workiq-fail.instructions.md`.
58
+ * Do NOT block the bootstrap orchestrator on identity failure. Continue with the env-fallback alias; the marker drives the eventual reconciliation.
54
59
  * Do NOT resolve on every run. Once persisted, the config values are authoritative.
55
60
 
56
61
  ## Integration with other doctrine
57
62
 
58
- * `workiq-first.instructions.md` — identity resolution is the canonical example of WorkIQ-first. Add to the inventory.
63
+ * `workiq-only.instructions.md` — identity resolution is the canonical example of WorkIQ-only. M365 fallbacks are forbidden.
64
+ * `deferred-retry-on-workiq-fail.instructions.md` — on WorkIQ failure, mark + continue + inform; never block.
59
65
  * `bootstrap-project` SKILL — Step 0 is identity resolution. Step 1 is project context.
60
66
  * `tracking.instructions.md` — the resolved identity goes into the tracking artifact's frontmatter under `actor:`.
61
67
 
62
68
  ## References
63
69
 
64
- * `workiq-first.instructions.md` — the parent doctrine.
70
+ * `workiq-only.instructions.md` — the parent doctrine (supersedes legacy `workiq-first`).
71
+ * `deferred-retry-on-workiq-fail.instructions.md` — failure-mode contract.
65
72
  * `engagement-root-resolution.instructions.md` — `projects_root` resolution (separate from identity).
66
73
  * `templates/init/project-evidence.template.yml` — defaults to `<auto>` for these three fields.
@@ -12,7 +12,7 @@ priority: HARD
12
12
 
13
13
  ## The registry
14
14
 
15
- `<engagement-root>/.project-evidence/m365/m365-mutable.json#knownSections.<projectKey>` is the **single source of truth** for canonical M365 identifiers per project.
15
+ `<workspace>/.kushi/config/user/m365-mutable.json#knownSections.<projectKey>` is the **single source of truth** for canonical M365 identifiers per project.
16
16
 
17
17
  Schema (populate every key the source supports):
18
18
 
@@ -21,7 +21,7 @@ host-fallback Graph call, and every CRM/ADO probe MUST honor.
21
21
  Before issuing **any** WorkIQ query, `m365_*` host call, Graph REST call, or CRM/ADO
22
22
  REST call, the skill MUST resolve the boundary it is operating inside from
23
23
  `<engagement-root>/<project>/integrations.yml boundaries:` (per-source) or the
24
- global `<engagement-root>/.project-evidence/{crm,ado}/config.yml` (auth +
24
+ global `<workspace>/.kushi/config/shared/integrations.yml` (auth +
25
25
  tenant + org). The resolved boundary MUST be recorded in the evidence file's
26
26
  `## Source Basis` block, e.g.:
27
27
 
@@ -57,8 +57,8 @@ returns evidence over the same surface — no run-to-run drift.
57
57
 
58
58
  | Skill | Hard prerequisite | Failure message (verbatim) |
59
59
  |---|---|---|
60
- | `pull-crm` | `<engagement-root>/.project-evidence/crm/config.yml` exists with non-placeholder `environmentUrl` | `crm-config-missing — drop a filled config.yml at <engagement-root>/.project-evidence/crm/config.yml. See plugin/templates/init/crm-config.template.yml.` |
61
- | `pull-ado` | `<engagement-root>/.project-evidence/ado/config.yml` exists with non-placeholder `organization` AND `defaultProject` | `ado-config-missing — drop a filled config.yml at <engagement-root>/.project-evidence/ado/config.yml. See plugin/templates/init/ado-config.template.yml.` |
60
+ | `pull-crm` | `<workspace>/.kushi/config/shared/integrations.yml` exists with non-placeholder `crm.environmentUrl` | `crm-config-missing — fill the crm: block in <workspace>/.kushi/config/shared/integrations.yml. See plugin/templates/init/integrations.template.yml.` |
61
+ | `pull-ado` | `<workspace>/.kushi/config/shared/integrations.yml` exists with non-placeholder `ado.organization` AND `ado.defaultProject` | `ado-config-missing — fill the ado: block in <workspace>/.kushi/config/shared/integrations.yml. See plugin/templates/init/integrations.template.yml.` |
62
62
  | `propose-ado-update` | both above + `integrations.yml ado.engagement_id > 0` | per `propose-ado-update/SKILL.md` Prerequisites table |
63
63
 
64
64
  Pre-v3.7.0 behavior allowed `pull-crm` to "narrate from email" when the live
@@ -172,7 +172,7 @@ On first run for a new project, bootstrap MUST:
172
172
  3. For sources where bootstrap cannot auto-populate, write the source as
173
173
  **disabled** (`enabled: false`) with a one-liner in `OPEN-QUESTIONS-DRAFT.md`
174
174
  asking the user to fill the boundary and re-enable.
175
- 4. For CRM and ADO, ALSO check that the global `<engagement-root>/.project-evidence/{crm,ado}/config.yml`
175
+ 4. For CRM and ADO, ALSO check that the global `<workspace>/.kushi/config/shared/integrations.yml`
176
176
  exists and has non-placeholder values; if not, scaffold from
177
177
  `plugin/templates/init/{crm,ado}-config.template.yml` and park in
178
178
  `OPEN-QUESTIONS-DRAFT.md` with the path the user needs to fill.
@@ -42,8 +42,8 @@ For every M365 source in scope:
42
42
 
43
43
  1. **WorkIQ FIRST and ONLY.** Issue the canonical query from the table below.
44
44
  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.
45
- 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.
46
- 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.
45
+ 3. **If WorkIQ still fails or is unavailable**: follow `deferred-retry-on-workiq-fail.instructions.md` — write a deferred-retry marker under `<project>/Evidence/<alias>/_deferred-retries/`, surface a one-liner in coverage.md and the run report, and **continue the run** for the remaining sources. The next `refresh-project` drains the queue. User-paste remains available as a manual recovery the user MAY do later by populating the artifact directly and deleting the marker — it is NOT the agent's recovery flow.
46
+ 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 — **including as a "last-resort" fallback after WorkIQ fails** (kushi v4.4.1+). Calling them is a defect; coverage.md must NOT show them in the attempt trail. The deferred-retry marker IS the audit trail for WorkIQ failures.
47
47
  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.
48
48
 
49
49
  ## Canonical WorkIQ commands (CODIFIED — do not re-discover)
@@ -174,6 +174,7 @@ If `Result: SUMMARY-ONLY` after doubled-strict retry: the artifact is acceptable
174
174
 
175
175
  ## Anti-patterns (defects)
176
176
 
177
+ 0. **Falling back to `m365_get_*` / Graph after a WorkIQ failure (kushi v4.4.1+).** FORBIDDEN. Even framed as "a last-resort partial," "just to try," or "we already have a WorkIQ marker so this is harmless." The doctrine is: WorkIQ fail → deferred-retry marker → continue → next refresh retries. Never a Graph escape hatch. See `deferred-retry-on-workiq-fail.instructions.md`.
177
178
  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.
178
179
  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.
179
180
  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.
@@ -192,3 +193,4 @@ If `Result: SUMMARY-ONLY` after doubled-strict retry: the artifact is acceptable
192
193
  - `plugin/instructions/ado-bootstrap-discovery.instructions.md` — ADO is OUT of WorkIQ scope; it uses ADO REST.
193
194
  - `plugin/instructions/scope-boundaries.instructions.md` — every pull-* still respects integrations.yml#boundaries.
194
195
  - `plugin/instructions/evidence-thoroughness.instructions.md` — verbatim is the bar; this rule operationalizes how to hit it.
196
+ - `plugin/instructions/deferred-retry-on-workiq-fail.instructions.md` (kushi v4.4.1+) — defines exactly what happens when WorkIQ fails. NO Graph fallback. Marker + continue + inform.
@@ -53,6 +53,92 @@ param(
53
53
 
54
54
  $ErrorActionPreference = 'Stop'
55
55
 
56
+ # Schema-aware required-field map (kushi v4.4.1+).
57
+ # For each logical config name, list the dot-paths that MUST be populated
58
+ # (non-empty, non-sentinel) before the config is considered usable. The check
59
+ # runs IN ADDITION to the substring sentinel check.
60
+ $script:RequiredFields = @{
61
+ 'm365-auth' = @(
62
+ 'm365Auth.defaultTenantId',
63
+ 'm365Auth.oneNote.defaultNotebookName',
64
+ 'm365Auth.oneNote.defaultNotebookId',
65
+ 'm365Auth.oneNote.defaultLinkOwner',
66
+ 'm365Auth.emailContext.folders',
67
+ 'm365Auth.sharePointContext.localProjectsRoot'
68
+ )
69
+ 'project-evidence' = @(
70
+ 'identity.alias',
71
+ 'identity.email',
72
+ 'engagement_root'
73
+ )
74
+ # integrations.yml is shared and per-project — required-field check is
75
+ # the consumer skill's job (it knows which sources are enabled).
76
+ 'integrations' = @()
77
+ }
78
+
79
+ # Sentinel substrings that indicate "still a placeholder."
80
+ $script:Sentinels = @(
81
+ '__FILL_ME_IN__',
82
+ '<auto>',
83
+ '<TENANT_ID>',
84
+ '<NOTEBOOK_ID>',
85
+ '<NOTEBOOK_NAME>',
86
+ '<LINK_OWNER>',
87
+ '<SP_LOCAL_ROOT>',
88
+ '<your-alias>',
89
+ '<Your Full Name>',
90
+ 'your.email@example.com'
91
+ )
92
+
93
+ function Test-Sentinel {
94
+ param([string] $Value)
95
+ if ([string]::IsNullOrWhiteSpace($Value)) { return $true }
96
+ foreach ($s in $script:Sentinels) {
97
+ if ($Value -like "*$s*") { return $true }
98
+ }
99
+ return $false
100
+ }
101
+
102
+ function Get-DotPath {
103
+ param([object] $Obj, [string] $DotPath)
104
+ $cur = $Obj
105
+ foreach ($seg in $DotPath -split '\.') {
106
+ if ($null -eq $cur) { return $null }
107
+ if ($cur -is [System.Collections.IDictionary]) {
108
+ $cur = $cur[$seg]
109
+ } else {
110
+ $cur = $cur.$seg
111
+ }
112
+ }
113
+ return $cur
114
+ }
115
+
116
+ function Test-RequiredFields {
117
+ param([string] $Name, [object] $Parsed)
118
+ $required = $script:RequiredFields[$Name]
119
+ if (-not $required) { return @() }
120
+ $missing = @()
121
+ foreach ($field in $required) {
122
+ $val = Get-DotPath -Obj $Parsed -DotPath $field
123
+ if ($null -eq $val) { $missing += $field; continue }
124
+ # Arrays: must have at least one non-sentinel element.
125
+ if ($val -is [System.Collections.IEnumerable] -and -not ($val -is [string])) {
126
+ $arr = @($val)
127
+ if ($arr.Count -eq 0) { $missing += $field; continue }
128
+ $allBlank = $true
129
+ foreach ($item in $arr) {
130
+ if ($item -is [string]) {
131
+ if (-not (Test-Sentinel $item)) { $allBlank = $false; break }
132
+ } else { $allBlank = $false; break }
133
+ }
134
+ if ($allBlank) { $missing += $field }
135
+ continue
136
+ }
137
+ if ($val -is [string] -and (Test-Sentinel $val)) { $missing += $field }
138
+ }
139
+ return ,$missing
140
+ }
141
+
56
142
  function Get-DefaultExtension([string] $name) {
57
143
  if ($name -match '^(project-evidence|integrations)$') { return 'yml' }
58
144
  return 'json'
@@ -84,9 +170,13 @@ if ($Path) { return $resolved }
84
170
  $raw = Get-Content -LiteralPath $resolved -Raw
85
171
 
86
172
  if (-not $AllowPlaceholders) {
87
- if ($raw -match '__FILL_ME_IN__' -or $raw -match '<auto>') {
173
+ $sentinelHit = $false
174
+ foreach ($s in $script:Sentinels) {
175
+ if ($raw -like "*$s*") { $sentinelHit = $true; break }
176
+ }
177
+ if ($sentinelHit) {
88
178
  throw @"
89
- Kushi config '$resolved' still has template placeholders (__FILL_ME_IN__ or <auto>).
179
+ Kushi config '$resolved' still has template placeholders (e.g. __FILL_ME_IN__, <auto>, <TENANT_ID>).
90
180
  Edit the file with your actual values, or pass -AllowPlaceholders to bypass this check.
91
181
  "@
92
182
  }
@@ -94,16 +184,31 @@ Edit the file with your actual values, or pass -AllowPlaceholders to bypass this
94
184
 
95
185
  if ($Raw) { return $raw }
96
186
 
97
- switch ($ext) {
187
+ $parsed = switch ($ext) {
98
188
  'json' {
99
- return ($raw | ConvertFrom-Json -Depth 100)
189
+ $raw | ConvertFrom-Json -Depth 100
100
190
  }
101
191
  { $_ -in 'yml','yaml' } {
102
192
  if (Get-Module -ListAvailable -Name 'powershell-yaml') {
103
193
  Import-Module powershell-yaml -ErrorAction Stop
104
- return ConvertFrom-Yaml -Yaml $raw
194
+ ConvertFrom-Yaml -Yaml $raw
195
+ } else {
196
+ Write-Warning "powershell-yaml not installed; required-field validation skipped. Install with: Install-Module powershell-yaml -Scope CurrentUser"
197
+ $null
105
198
  }
106
- Write-Warning "powershell-yaml not installed; returning raw YAML text. Install with: Install-Module powershell-yaml -Scope CurrentUser"
107
- return $raw
108
199
  }
109
200
  }
201
+
202
+ if (-not $AllowPlaceholders -and $null -ne $parsed) {
203
+ $missing = Test-RequiredFields -Name $Name -Parsed $parsed
204
+ if ($missing.Count -gt 0) {
205
+ throw @"
206
+ Kushi config '$resolved' is missing required fields:
207
+ $(($missing | ForEach-Object { " - $_" }) -join "`n")
208
+ Edit the file with your actual values, or pass -AllowPlaceholders to bypass this check.
209
+ "@
210
+ }
211
+ }
212
+
213
+ if ($null -eq $parsed) { return $raw }
214
+ return $parsed