kushi-agents 4.3.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 (54) hide show
  1. package/package.json +2 -4
  2. package/plugin/agents/kushi.agent.md +3 -3
  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 +8 -8
  6. package/plugin/instructions/bootstrap-status-format.instructions.md +37 -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 +16 -13
  11. package/plugin/instructions/evidence-layout-canonical.instructions.md +2 -2
  12. package/plugin/instructions/identity-resolution.instructions.md +19 -12
  13. package/plugin/instructions/m365-id-registry.instructions.md +1 -1
  14. package/plugin/instructions/multi-user-shared-files.instructions.md +87 -0
  15. package/plugin/instructions/run-reports.instructions.md +1 -1
  16. package/plugin/instructions/scope-boundaries.instructions.md +4 -4
  17. package/plugin/instructions/side-by-side-config.instructions.md +23 -17
  18. package/plugin/instructions/tracking.instructions.md +1 -1
  19. package/plugin/instructions/workiq-only.instructions.md +6 -4
  20. package/plugin/lib/Get-KushiConfig.ps1 +214 -0
  21. package/plugin/prompts/bootstrap.prompt.md +64 -35
  22. package/plugin/reference-packs/README.md +1 -1
  23. package/plugin/skills/apply-ado-update/SKILL.md +2 -2
  24. package/plugin/skills/ask-project/SKILL.md +1 -1
  25. package/plugin/skills/bootstrap-project/SKILL.md +18 -16
  26. package/plugin/skills/intro/SKILL.md +2 -2
  27. package/plugin/skills/propose-ado-update/SKILL.md +4 -4
  28. package/plugin/skills/pull-ado/SKILL.md +6 -6
  29. package/plugin/skills/pull-crm/SKILL.md +5 -5
  30. package/plugin/skills/pull-email/SKILL.md +2 -2
  31. package/plugin/skills/pull-meetings/SKILL.md +1 -1
  32. package/plugin/skills/pull-onenote/scripts/recapture-section-url.mjs +2 -1
  33. package/plugin/skills/pull-onenote/write-snapshot.mjs +2 -1
  34. package/plugin/skills/pull-sharepoint/SKILL.md +1 -1
  35. package/plugin/skills/pull-teams/SKILL.md +1 -1
  36. package/plugin/skills/refresh-project/SKILL.md +21 -1
  37. package/plugin/skills/self-check/run.ps1 +24 -1
  38. package/plugin/templates/ado-update/integrations-ado-writes.example.yml +1 -1
  39. package/plugin/templates/init/azuredevops.template.json +159 -0
  40. package/plugin/templates/init/dynamics365.template.json +412 -0
  41. package/plugin/templates/init/m365-auth.template.json +5 -5
  42. package/{.github/config/m365-mutable.json.example → plugin/templates/init/m365-mutable.example.json} +1 -1
  43. package/plugin/templates/init/project-integrations.template.yml +2 -2
  44. package/plugin/templates/init/rsi-program-catalog.template.json +107 -0
  45. package/plugin/templates/snapshot/onenote-page.template.md +3 -3
  46. package/src/config-loader.mjs +156 -0
  47. package/src/constants.mjs +54 -18
  48. package/src/copy-assets.mjs +0 -76
  49. package/src/main.mjs +30 -26
  50. package/src/seed-config.mjs +88 -23
  51. package/src/seed-config.test.mjs +150 -0
  52. package/plugin/templates/init/ado-config.template.yml +0 -21
  53. package/plugin/templates/init/crm-config.template.yml +0 -16
  54. /package/{.github/config/m365-auth.json.example → plugin/templates/init/m365-auth.example.json} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kushi-agents",
3
- "version": "4.3.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": {
@@ -10,8 +10,6 @@
10
10
  "bin/",
11
11
  "src/",
12
12
  "plugin/",
13
- ".github/config/m365-auth.json.example",
14
- ".github/config/m365-mutable.json.example",
15
13
  ".github/copilot-instructions.kushi.md"
16
14
  ],
17
15
  "engines": {
@@ -43,7 +41,7 @@
43
41
  },
44
42
  "license": "MIT",
45
43
  "scripts": {
46
- "test": "node --test src/check-workiq.test.mjs",
44
+ "test": "node --test src/check-workiq.test.mjs src/seed-config.test.mjs",
47
45
  "smoke": "node scripts/smoke.mjs",
48
46
  "prepublishOnly": "npm test && npm run smoke"
49
47
  },
@@ -98,9 +98,9 @@ When a user message arrives:
98
98
  ## Configuration layout
99
99
 
100
100
  ```
101
- <workspace>/.kushi/config/project-evidence.yml ← personal: alias, engagement_root, active_projects (seeded by installer; never overwritten)
102
- <workspace>/.kushi/config/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)
101
+ <workspace>/.kushi/config/user/project-evidence.yml ← personal: alias, engagement_root, active_projects (seeded by installer; never overwritten)
102
+ <workspace>/.kushi/config/shared/integrations.yml ← optional global CRM/ADO defaults (seeded by installer; never overwritten)
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,10 +224,10 @@ 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` |
230
- | WorkIQ CLI path | `<workspace>/.kushi/config/project-evidence.yml` (workspace-scoped) |
231
- | Global integrations (optional) | `<workspace>/.kushi/config/integrations.yml` |
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
+ | WorkIQ CLI path | `<workspace>/.kushi/config/user/project-evidence.yml` (workspace-scoped) |
231
+ | Global integrations (optional) | `<workspace>/.kushi/config/shared/integrations.yml` |
232
232
 
233
233
  Always read tenant IDs, resource URLs, org URLs, and allowed-tenant lists from these files at the start of each run. **Never hardcode them in skills or prompts.** The only acceptable literal in instructions/prompts is the public Microsoft tenant ID `72f988bf-86f1-41af-91ab-2d7cd011db47` in the user-facing `az login --tenant ...` suggestion text.
@@ -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:
@@ -79,6 +90,30 @@ Use these exact strings everywhere (table cells, run-log, narrative):
79
90
  - `ado-not-complete` — engagement record not found yet OR ADO linkage missing
80
91
  - `completed-with-coverage-gaps` — final outcome with explicit gaps recorded
81
92
 
93
+ ## Multi-contributor safety (kushi v4.4.0+)
94
+
95
+ `<project>/bootstrap-status.md` is shared across all contributors via OneDrive. Per `multi-user-shared-files.instructions.md`:
96
+
97
+ 1. **Before writing**, scan `<project>/` for sibling conflict copies (e.g. `bootstrap-status-conflict-<alias>-<host>.md` or OneDrive's `bootstrap-status (Stan's conflicted copy *)`). If any exist, merge their content into the canonical file (taking the most recent per-source row) and delete the conflict copies. Log this in the per-user bootstrap report under `## Conflict copies merged`.
98
+ 2. **Preserve other contributors' attribution.** Read the existing file; the `## Contributors who have bootstrapped this project` section MUST keep one row per alias that has ever written it. Update only the row matching the current alias.
99
+ 3. **Per-source rows** in `## Context Artifact Status` reflect the most recent discovery across all contributors. Add a trailing `Discovered by` column showing which alias performed the latest discovery (cross-referenced from each alias's `m365-mutable.json` via `Get-KushiConfig`).
100
+ 4. **The final one-line status** is for the most recent run only and may be overwritten by any contributor.
101
+
102
+ ### Required `## Contributors who have bootstrapped this project` section
103
+
104
+ Append this section immediately after `## Bootstrap Status`:
105
+
106
+ ```
107
+ ## Contributors who have bootstrapped this project
108
+
109
+ | Alias | Display Name | Last Run | Mode | Outcome |
110
+ |---|---|---|---|---|
111
+ | ushak | Usha Kandregula | 2026-05-20 07:42 EDT | bootstrap | bootstrap-complete-with-coverage-gaps |
112
+ | stand | Stan Doe | 2026-05-18 16:01 EDT | refresh | refresh-complete |
113
+ ```
114
+
115
+ Preserve every existing row except the one matching your alias.
116
+
82
117
  ## Sections to AVOID in bootstrap-status
83
118
 
84
119
  Do NOT keep these in `bootstrap-status.md` (they belong elsewhere or become stale):
@@ -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.
@@ -11,24 +11,27 @@ The **engagement-root** is the parent folder that contains all of the user's eng
11
11
 
12
12
  Resolve `<engagement-root>` in this order — first match wins:
13
13
 
14
- 1. **`<workspace>/.kushi/config/project-evidence.yml`** — read `engagement_root:` field (preferred).
14
+ 1. **`<workspace>/.kushi/config/user/project-evidence.yml`** — read `engagement_root:` field (preferred).
15
15
  2. **`customer_workspace/FDEDocs/`** — if the user's workspace has this symlink, follow it. Common in FDE-style installs.
16
- 3. **Ask the user** once and persist the answer to `<workspace>/.kushi/config/project-evidence.yml engagement_root`.
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,8 +45,8 @@ 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`.
46
- 2. Match against `active_projects:` in `<workspace>/.kushi/config/project-evidence.yml`.
48
+ 1. Match against keys in `<workspace>/.kushi/config/user/m365-mutable.json m365Mutable.knownSections`.
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
 
49
52
  Case-insensitive; ranking `exact > prefix > contains`. Multiple plausible candidates → ask user to pick. Zero candidates AND verb is `bootstrap` → create the folder. Zero candidates AND verb is anything else → ask user.
@@ -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.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  applyTo: "**"
3
- description: "Identity auto-resolution — Kushi never asks the user for alias / email / display_name. On the first bootstrap (or whenever those fields are <auto> / placeholder in .kushi/config/project-evidence.yml), Kushi resolves them from WorkIQ in a single call, persists them, and continues. Skipped entirely if the user has set explicit values."
3
+ description: "Identity auto-resolution — Kushi never asks the user for alias / email / display_name. On the first bootstrap (or whenever those fields are <auto> / placeholder in .kushi/config/user/project-evidence.yml), Kushi resolves them from WorkIQ in a single call, persists them, and continues. Skipped entirely if the user has set explicit values."
4
4
  ---
5
5
 
6
6
  # Identity Resolution — Don't Ask, Probe
@@ -9,7 +9,7 @@ Kushi must not prompt the user for `alias`, `email`, or `display_name`. These ar
9
9
 
10
10
  ## When to resolve
11
11
 
12
- On the **first step of every prompt** that reads contributor identity (bootstrap, refresh, aggregate, ask, fde-*, propose-ado, apply-ado), check `<workspace>/.kushi/config/project-evidence.yml`:
12
+ On the **first step of every prompt** that reads contributor identity (bootstrap, refresh, aggregate, ask, fde-*, propose-ado, apply-ado), check `<workspace>/.kushi/config/user/project-evidence.yml`:
13
13
 
14
14
  * If `alias`, `email`, or `display_name` is **missing**, set to `<auto>`, or matches a placeholder pattern (`<your-alias>`, `<Your Full Name>`, `your.email@example.com`) → **resolve from WorkIQ**.
15
15
  * If all three are explicit non-placeholder values → **skip**. Respect the user's override.
@@ -32,35 +32,42 @@ Map the response:
32
32
 
33
33
  ## After resolution
34
34
 
35
- 1. **Persist back** to `<workspace>/.kushi/config/project-evidence.yml` (preserving comments + indentation). The user sees the resolved values on next open; no surprise.
35
+ 1. **Persist back** to `<workspace>/.kushi/config/user/project-evidence.yml` (preserving comments + indentation). The user sees the resolved values on next open; no surprise.
36
36
  2. **Echo back** to the user, one line:
37
- > ✓ Identity: `Alex Smith <alex@microsoft.com>` (alias=`alex`). Edit `.kushi/config/project-evidence.yml` to override.
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
 
@@ -0,0 +1,87 @@
1
+ ---
2
+ applyTo: "**/SKILL.md,**/*.prompt.md,**/Evidence/run-log.yml,**/integrations.yml,**/bootstrap-status.md,**/OPEN-QUESTIONS-DRAFT.md,**/State/09_open-questions.md,**/Evidence/contributors.yml"
3
+ ---
4
+
5
+ # Multi-user shared-file safety
6
+
7
+ ## Why this exists
8
+
9
+ A Kushi engagement folder (`<engagement-root>/<project>/`) is shared across all contributors via OneDrive. Several artifacts inside it are **logically shared** — every contributor reads and writes the same path:
10
+
11
+ | Shared artifact | Path | Writers |
12
+ |---|---|---|
13
+ | Per-project integrations | `<project>/integrations.yml` | `bootstrap-project`, `propose-ado-update`, `apply-ado-update`, every `pull-*` that discovers an ID |
14
+ | Bootstrap status snapshot | `<project>/bootstrap-status.md` | `bootstrap-project` (every run, any contributor) |
15
+ | Open questions draft | `<project>/OPEN-QUESTIONS-DRAFT.md` (or `State/09_open-questions.md` on full profile) | `bootstrap-project`, `consolidate-evidence`, `build-state`, `propose-ado-update` |
16
+ | Run log | `<project>/Evidence/run-log.yml` | every `pull-*`, `bootstrap-project`, `refresh-project`, `fde-*` |
17
+ | Contributors list | `<project>/Evidence/contributors.yml` | every `pull-*` (first time an alias touches the project) |
18
+
19
+ Without rules, two contributors running concurrently produce one of:
20
+ - OneDrive conflict-copy (`*-conflict-<alias>-<host>.yml`) — silent loss until manually merged.
21
+ - Last-writer-wins overwrite — silent loss of the other contributor's data.
22
+
23
+ This doctrine codifies the contract every writer must honor. It is **not** a substitute for proper locking, but it makes concurrent runs eventually consistent and recoverable.
24
+
25
+ ## Universal rules (apply to every shared artifact)
26
+
27
+ 1. **Read-modify-write, never blind-write.** Before writing any shared artifact, the skill MUST read the current file, merge its new contribution, and write the merged result. Blind overwrite is forbidden.
28
+ 2. **Detect and absorb conflict-copies.** Before reading the canonical file, glob for sibling files matching `<basename>-conflict-*` or `<basename> (<host>'s conflicted copy *)`. If any exist, merge their contents into the canonical file, then delete the conflict copies. Log the merge in the run report.
29
+ 3. **Stamp every contribution.** Every line / block / list-entry that a skill adds must carry its alias (from `project-evidence.yml`) and an ISO timestamp so future readers (and the merge logic above) can deterministically reconcile.
30
+ 4. **Idempotent appends.** Re-running the same skill in the same window MUST NOT duplicate prior entries. Use a `(alias, source, window_key)` tuple as the dedupe key.
31
+ 5. **Never delete another contributor's contribution.** A skill may only modify or remove entries it created (matched by alias). Cleanup of other aliases' entries is reserved for `consolidate-evidence`.
32
+
33
+ ## Per-artifact contracts
34
+
35
+ ### `<project>/integrations.yml`
36
+
37
+ - **Pattern**: read-modify-write with alias stamp on changed fields.
38
+ - Whenever a skill discovers a new ID (CRM `record_id`, ADO `engagement_id`, OneNote `section_file_id`, etc.), it MUST:
39
+ 1. Read the current file.
40
+ 2. If the target field is non-empty AND differs from the discovered value, write the discovered value into a sibling key `<field>_proposed_<alias>` and add a Q-row to `OPEN-QUESTIONS-DRAFT.md` for human reconciliation. Do not overwrite.
41
+ 3. If the target field is empty (or matches), write the value and append a comment line: `# resolved by <alias> on <YYYY-MM-DD>` immediately above the changed key.
42
+ - The `last_discovery_attempt` / `last_discovery_result` keys MAY be overwritten by any alias (they are intentionally last-writer-wins; the value is "the most recent attempt by anyone").
43
+
44
+ ### `<project>/bootstrap-status.md`
45
+
46
+ - **Pattern**: full rewrite is permitted, but the file MUST carry a `## Contributors who have bootstrapped this project` section listing every alias that has ever written it, with their last-run timestamp.
47
+ - Before rewriting, the skill MUST read the existing Contributors section and preserve every entry whose alias differs from the current alias. Update only its own alias row.
48
+ - The per-source status tables (`OneNote`, `Email`, `Teams`, etc.) reflect the **most recent** discovery across all contributors. Skills MUST cross-reference `m365-mutable.json` (which is per-user) — when a row shows discovered IDs, also note which alias performed the discovery in a trailing `(by <alias>)` cell.
49
+
50
+ ### `<project>/OPEN-QUESTIONS-DRAFT.md` and `<project>/State/09_open-questions.md`
51
+
52
+ - **Pattern**: append-only with dedup.
53
+ - Every question row carries `[asked by <alias> on <YYYY-MM-DD>]` as the trailing column.
54
+ - Before appending a new question, the skill MUST scan existing rows for a question whose normalized text (lowercased, whitespace-collapsed) matches. If a match exists, do not append a duplicate; instead, append `, <alias> on <date>` to the existing row's attribution.
55
+ - Rows are removed only by an explicit `resolve-open-question` action (out of scope here) — never by another `bootstrap` / `refresh` run.
56
+
57
+ ### `<project>/Evidence/run-log.yml`
58
+
59
+ - **Pattern**: structured append + max() merge.
60
+ - Run history (`runs:` list) is append-only. Every entry carries `alias:` and `run_at:`.
61
+ - Per-source watermarks (`sources.<source>.watermark`) use **max()** merge: when writing, the skill reads the current value and writes `max(current, new)`. Never overwrite with an older watermark.
62
+ - Per-source errors (`sources.<source>.errors[]`) are append-only with `alias` + `at` on every entry; dedup on `(alias, code, at)`.
63
+
64
+ ### `<project>/Evidence/contributors.yml`
65
+
66
+ - **Pattern**: append-only, alias-keyed.
67
+ - The first time an alias runs against the project, append `{ alias, display_name, email, first_seen: <YYYY-MM-DD> }`. Subsequent runs MUST NOT modify the entry.
68
+ - `last_seen` MAY be updated by any run, max()-merged.
69
+
70
+ ## Implementation guidance for skill authors
71
+
72
+ - Read `<workspace>/.kushi/config/user/project-evidence.yml` to get your alias via `Get-KushiConfig -Name 'project-evidence'`. Never hardcode aliases.
73
+ - For YAML merges, use `Get-Content -Raw | ConvertFrom-Yaml`, mutate, then `ConvertTo-Yaml`. Preserve comments where possible by line-based merging when structure permits.
74
+ - For Markdown table merges in `OPEN-QUESTIONS-DRAFT.md`, parse the body to a list of rows, dedup by normalized question text, re-emit.
75
+ - On any merge conflict the skill cannot resolve deterministically, write the alternative as a sibling-key `<field>_proposed_<alias>` (for YAML) or a `> Disagreement: <alias-A> says ..., <alias-B> says ...` callout (for Markdown) and add a Q-row to `OPEN-QUESTIONS-DRAFT.md`.
76
+
77
+ ## Out of scope
78
+
79
+ - True file locking (filesystem advisory locks, OneDrive's checkout API) — relies on platform support Kushi can't assume.
80
+ - Server-side merge service — Kushi is local-only by design.
81
+
82
+ ## Related doctrine
83
+
84
+ - `evidence-layout-canonical.instructions.md` — what lives where under `Evidence/`.
85
+ - `run-reports.instructions.md` — per-user run narrative (always under `Evidence/<alias>/`, no concurrency risk).
86
+ - `bootstrap-status-format.instructions.md` — format contract for the shared status artifact.
87
+ - `citation-ledger.instructions.md` — every assertion in shared artifacts must carry a citation; alias attribution is the citation for shared-file entries.
@@ -24,7 +24,7 @@ Critical for multi-user projects: each contributor's runs are independent, and e
24
24
  YYYY-MM-DD-HHmm_<mode>.md
25
25
  ```
26
26
 
27
- - `<alias>` — the contributor running the skill (from `<workspace>/.kushi/config/project-evidence.yml#alias`).
27
+ - `<alias>` — the contributor running the skill (from `<workspace>/.kushi/config/user/project-evidence.yml#alias`).
28
28
  - `<mode>` — one of `bootstrap`, `refresh`, `force-refresh`, `consolidate`.
29
29
  - Timestamp uses the **start time** of the run, in local time, format `YYYY-MM-DD-HHmm` (no seconds, no timezone — local-time prefix is enough for chronological sort).
30
30