kushi-agents 4.4.0 → 4.4.3

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 (45) hide show
  1. package/bin/cli.mjs +2 -4
  2. package/package.json +1 -1
  3. package/plugin/agents/kushi.agent.md +5 -1
  4. package/plugin/instructions/ado-engagement-tree.instructions.md +1 -1
  5. package/plugin/instructions/az-auth-conditional.instructions.md +2 -2
  6. package/plugin/instructions/azure-auth-patterns.instructions.md +6 -6
  7. package/plugin/instructions/bootstrap-status-format.instructions.md +15 -2
  8. package/plugin/instructions/cleanup-on-resolution.instructions.md +1 -1
  9. package/plugin/instructions/communication-guidelines.instructions.md +79 -0
  10. package/plugin/instructions/crm-bootstrap-discovery.instructions.md +1 -1
  11. package/plugin/instructions/deferred-retry-on-workiq-fail.instructions.md +155 -0
  12. package/plugin/instructions/engagement-root-resolution.instructions.md +13 -10
  13. package/plugin/instructions/evidence-layout-canonical.instructions.md +2 -2
  14. package/plugin/instructions/fallback-status-reporting.instructions.md +104 -0
  15. package/plugin/instructions/fde-grounding.instructions.md +146 -0
  16. package/plugin/instructions/identity-resolution.instructions.md +15 -8
  17. package/plugin/instructions/m365-id-registry.instructions.md +1 -1
  18. package/plugin/instructions/scope-boundaries.instructions.md +4 -4
  19. package/plugin/instructions/status-taxonomy.instructions.md +118 -0
  20. package/plugin/instructions/workiq-only.instructions.md +4 -2
  21. package/plugin/lib/Get-KushiConfig.ps1 +112 -7
  22. package/plugin/prompts/bootstrap.prompt.md +64 -49
  23. package/plugin/skills/apply-ado-update/SKILL.md +2 -2
  24. package/plugin/skills/bootstrap-project/SKILL.md +10 -10
  25. package/plugin/skills/propose-ado-update/SKILL.md +3 -3
  26. package/plugin/skills/pull-ado/SKILL.md +27 -13
  27. package/plugin/skills/pull-crm/SKILL.md +20 -9
  28. package/plugin/skills/pull-email/SKILL.md +2 -2
  29. package/plugin/skills/pull-meetings/SKILL.md +1 -1
  30. package/plugin/skills/pull-onenote/scripts/recapture-section-url.mjs +2 -1
  31. package/plugin/skills/pull-onenote/write-snapshot.mjs +2 -1
  32. package/plugin/skills/pull-sharepoint/SKILL.md +1 -1
  33. package/plugin/skills/pull-teams/SKILL.md +1 -1
  34. package/plugin/skills/refresh-project/SKILL.md +21 -1
  35. package/plugin/skills/self-check/run.ps1 +24 -1
  36. package/plugin/templates/ado-update/integrations-ado-writes.example.yml +1 -1
  37. package/plugin/templates/init/integrations.template.yml +32 -19
  38. package/plugin/templates/init/m365-auth.template.json +5 -5
  39. package/plugin/templates/init/project-integrations.template.yml +2 -2
  40. package/plugin/templates/snapshot/onenote-page.template.md +3 -3
  41. package/src/config-loader.mjs +92 -5
  42. package/src/main.mjs +2 -30
  43. package/plugin/templates/init/ado-config.template.yml +0 -21
  44. package/plugin/templates/init/crm-config.template.yml +0 -16
  45. package/src/prompt.mjs +0 -42
@@ -15,16 +15,16 @@
15
15
  "dataverse": "https://iscrm.crm.dynamics.com"
16
16
  },
17
17
  "oneNote": {
18
- "defaultNotebookName": "",
19
- "defaultNotebookId": "",
18
+ "defaultNotebookName": "__FILL_ME_IN__",
19
+ "defaultNotebookId": "__FILL_ME_IN__",
20
20
  "defaultSectionResolverUrl": "",
21
21
  "defaultNotebookRootLink": "",
22
- "defaultLinkOwner": ""
22
+ "defaultLinkOwner": "__FILL_ME_IN__"
23
23
  },
24
24
  "emailContext": {
25
25
  "enabled": true,
26
26
  "dateFloor": "",
27
- "folders": [],
27
+ "folders": ["__FILL_ME_IN__"],
28
28
  "includeSubfolders": true,
29
29
  "sourceCoverageLabel": "",
30
30
  "matchingPolicy": {
@@ -56,7 +56,7 @@
56
56
  },
57
57
  "sharePointContext": {
58
58
  "enabled": true,
59
- "localProjectsRoot": "",
59
+ "localProjectsRoot": "__FILL_ME_IN__",
60
60
  "matchingPolicy": {
61
61
  "mode": "fuzzy-folder-name",
62
62
  "rankingOrder": ["exact", "prefix", "contains"],
@@ -3,8 +3,8 @@
3
3
  # AND declares the explicit boundaries inside which Kushi is allowed to query.
4
4
  #
5
5
  # Connection / auth / field mappings are NOT here — those are global at:
6
- # <engagement-root>/.project-evidence/crm/config.yml
7
- # <engagement-root>/.project-evidence/ado/config.yml
6
+ # <workspace>/.kushi/config/shared/integrations.yml
7
+ # <workspace>/.kushi/config/shared/integrations.yml
8
8
  #
9
9
  # Boundaries doctrine: see plugin/instructions/scope-boundaries.instructions.md.
10
10
  # Required boundary keys MUST be filled or the source is disabled (not silently widened).
@@ -20,7 +20,7 @@ captured_at: "<ISO-8601 if captured/user-pasted, else empty>"
20
20
  - **Capture status:** `<last_status>`
21
21
  - **Attempts:** <int>
22
22
  - **Last attempt:** <ts>
23
- - **Source basis:** WorkIQ enumerate + per-page verbatim probe (pull-onenote v2.5.0). Graph `/me/onenote/*` skipped per workspace policy. Per-page retry registry: `.project-evidence/m365/m365-mutable.json#knownSections.<projectKey>.one_pages`.
23
+ - **Source basis:** WorkIQ enumerate + per-page verbatim probe (pull-onenote v2.5.0). Graph `/me/onenote/*` skipped per workspace policy. Per-page retry registry: `.kushi/config/user/m365-mutable.json#knownSections.<projectKey>.one_pages`.
24
24
 
25
25
  <!-- IF last_status == "captured" or "user-pasted" -->
26
26
 
@@ -43,7 +43,7 @@ captured_at: "<ISO-8601 if captured/user-pasted, else empty>"
43
43
 
44
44
  ❌ **page-body-unavailable: WorkIQ returned BODY-NOT-EXPOSED on attempt <N>.**
45
45
 
46
- > Per `pull-onenote` v2.5.0 § "Empirical contract": WorkIQ OneNote body retrieval is non-deterministic — the same page may return a body on a later refresh. This page IS registered for retry in `.project-evidence/m365/m365-mutable.json#knownSections.<projectKey>.one_pages` and will be re-attempted on every refresh until `last_status` becomes `captured` or `user-pasted`. Inferring the page contents from adjacent emails or chat traffic is forbidden.
46
+ > Per `pull-onenote` v2.5.0 § "Empirical contract": WorkIQ OneNote body retrieval is non-deterministic — the same page may return a body on a later refresh. This page IS registered for retry in `.kushi/config/user/m365-mutable.json#knownSections.<projectKey>.one_pages` and will be re-attempted on every refresh until `last_status` becomes `captured` or `user-pasted`. Inferring the page contents from adjacent emails or chat traffic is forbidden.
47
47
 
48
48
  ### next_step
49
49
 
@@ -93,5 +93,5 @@ Per `thoroughness-detector.instructions.md`, the writer must tick all that apply
93
93
  - [ ] If `last_status == captured` or `user-pasted`: AI Narrative Summary (3+ paragraphs) AND verbatim Body section are present and non-empty
94
94
  - [ ] If `last_status == BODY-NOT-EXPOSED`: explicit marker, `next_step` block, and metadata table all present; NO inferred or paraphrased body content
95
95
  - [ ] If `last_status == enumeration-only`: explicit transient marker present; NO body content of any kind
96
- - [ ] Page is registered in `.project-evidence/m365/m365-mutable.json#knownSections.<projectKey>.one_pages` with matching `last_status` and `attempts`
96
+ - [ ] Page is registered in `.kushi/config/user/m365-mutable.json#knownSections.<projectKey>.one_pages` with matching `last_status` and `attempts`
97
97
  - [ ] No fabrication: nothing in this file came from adjacent emails, chats, or other sources to "fill in" what WorkIQ did not return
@@ -1,6 +1,74 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
 
4
+ /**
5
+ * Schema-aware required-field map (kushi v4.4.1+).
6
+ * For each logical config name, list the dot-paths that MUST be populated
7
+ * (non-empty, non-sentinel) before the config is considered usable.
8
+ */
9
+ const REQUIRED_FIELDS = {
10
+ 'm365-auth': [
11
+ 'm365Auth.defaultTenantId',
12
+ 'm365Auth.oneNote.defaultNotebookName',
13
+ 'm365Auth.oneNote.defaultNotebookId',
14
+ 'm365Auth.oneNote.defaultLinkOwner',
15
+ 'm365Auth.emailContext.folders',
16
+ 'm365Auth.sharePointContext.localProjectsRoot',
17
+ ],
18
+ 'project-evidence': [
19
+ 'identity.alias',
20
+ 'identity.email',
21
+ 'engagement_root',
22
+ ],
23
+ 'integrations': [],
24
+ };
25
+
26
+ const SENTINELS = [
27
+ '__FILL_ME_IN__',
28
+ '<auto>',
29
+ '<TENANT_ID>',
30
+ '<NOTEBOOK_ID>',
31
+ '<NOTEBOOK_NAME>',
32
+ '<LINK_OWNER>',
33
+ '<SP_LOCAL_ROOT>',
34
+ '<your-alias>',
35
+ '<Your Full Name>',
36
+ 'your.email@example.com',
37
+ ];
38
+
39
+ function isSentinel(v) {
40
+ if (v === null || v === undefined) return true;
41
+ if (typeof v !== 'string') return false;
42
+ if (v.trim() === '') return true;
43
+ return SENTINELS.some(s => v.includes(s));
44
+ }
45
+
46
+ function getDotPath(obj, dotPath) {
47
+ let cur = obj;
48
+ for (const seg of dotPath.split('.')) {
49
+ if (cur === null || cur === undefined) return undefined;
50
+ cur = cur[seg];
51
+ }
52
+ return cur;
53
+ }
54
+
55
+ function checkRequired(name, parsed) {
56
+ const required = REQUIRED_FIELDS[name] || [];
57
+ const missing = [];
58
+ for (const field of required) {
59
+ const v = getDotPath(parsed, field);
60
+ if (v === null || v === undefined) { missing.push(field); continue; }
61
+ if (Array.isArray(v)) {
62
+ if (v.length === 0 || v.every(x => typeof x === 'string' && isSentinel(x))) {
63
+ missing.push(field);
64
+ }
65
+ continue;
66
+ }
67
+ if (typeof v === 'string' && isSentinel(v)) missing.push(field);
68
+ }
69
+ return missing;
70
+ }
71
+
4
72
  /**
5
73
  * Node-side twin of Get-KushiConfig.ps1.
6
74
  *
@@ -44,18 +112,37 @@ export function loadKushiConfig(name, opts = {}) {
44
112
  const raw = fs.readFileSync(resolved, 'utf8');
45
113
 
46
114
  if (!opts.allowPlaceholders) {
47
- if (raw.includes('__FILL_ME_IN__') || raw.includes('<auto>')) {
115
+ if (SENTINELS.some(s => raw.includes(s))) {
48
116
  throw new Error(
49
- `Kushi config '${resolved}' still has template placeholders. ` +
117
+ `Kushi config '${resolved}' still has template placeholders ` +
118
+ `(e.g. __FILL_ME_IN__, <auto>, <TENANT_ID>). ` +
50
119
  `Edit it with real values, or pass {allowPlaceholders: true}.`
51
120
  );
52
121
  }
53
122
  }
54
123
 
55
124
  if (opts.raw) return raw;
56
- if (ext === 'json') return JSON.parse(raw);
57
- // YAML: return raw — Node has no built-in parser. Caller installs js-yaml if needed.
58
- return raw;
125
+
126
+ let parsed;
127
+ if (ext === 'json') {
128
+ parsed = JSON.parse(raw);
129
+ } else {
130
+ // YAML: return raw — Node has no built-in parser. Caller installs js-yaml if needed.
131
+ return raw;
132
+ }
133
+
134
+ if (!opts.allowPlaceholders) {
135
+ const missing = checkRequired(name, parsed);
136
+ if (missing.length > 0) {
137
+ throw new Error(
138
+ `Kushi config '${resolved}' is missing required fields:\n` +
139
+ missing.map(f => ` - ${f}`).join('\n') + '\n' +
140
+ `Edit it with real values, or pass {allowPlaceholders: true}.`
141
+ );
142
+ }
143
+ }
144
+
145
+ return parsed;
59
146
  }
60
147
 
61
148
  function defaultExt(name) {
package/src/main.mjs CHANGED
@@ -12,7 +12,6 @@ import {
12
12
  PLUGIN_SOURCE_DIR,
13
13
  PROJECT_MARKERS,
14
14
  } from './constants.mjs';
15
- import { promptForDestination } from './prompt.mjs';
16
15
  import { copyAssets } from './copy-assets.mjs';
17
16
  import { mergeSettings } from './settings.mjs';
18
17
  import { mergeCopilotInstructions } from './copilot-instructions.mjs';
@@ -92,32 +91,7 @@ async function installVscode(options, resolved, version) {
92
91
  warnIfNotProjectRoot(projectRoot);
93
92
  }
94
93
 
95
- let dest;
96
- if (options.dest) {
97
- dest = options.dest.replace(/\\/g, '/');
98
- if (path.isAbsolute(dest)) {
99
- console.error('\n Please use a relative path (e.g., .kushi).\n');
100
- process.exit(1);
101
- }
102
- dest = dest.replace(/\/+$/, '');
103
- if (!dest) {
104
- console.error('\n Please provide a non-empty destination path.\n');
105
- process.exit(1);
106
- }
107
- const resolvedPath = path.resolve(projectRoot, dest);
108
- if (
109
- !resolvedPath.startsWith(projectRoot + path.sep) &&
110
- resolvedPath !== projectRoot
111
- ) {
112
- console.error('\n Destination must be within the current project.\n');
113
- process.exit(1);
114
- }
115
- } else if (options.yes) {
116
- dest = DEFAULT_DEST;
117
- } else {
118
- dest = await promptForDestination(DEFAULT_DEST);
119
- }
120
-
94
+ const dest = DEFAULT_DEST;
121
95
  const fullDest = path.resolve(projectRoot, dest);
122
96
  await confirmOverwriteIfExists(fullDest, dest, options.force);
123
97
 
@@ -182,9 +156,7 @@ async function installVscode(options, resolved, version) {
182
156
  */
183
157
  async function installClawpilot(options, resolved, version) {
184
158
  const home = os.homedir();
185
- const fullDest = options.dest
186
- ? path.resolve(options.dest)
187
- : path.join(home, ...CLAWPILOT_DEST_SUBPATH.split('/'));
159
+ const fullDest = path.join(home, ...CLAWPILOT_DEST_SUBPATH.split('/'));
188
160
 
189
161
  const displayDest = fullDest.startsWith(home + path.sep)
190
162
  ? '~' + path.sep + path.relative(home, fullDest)
@@ -1,21 +0,0 @@
1
- # Azure DevOps connection — per-user filled. Lives at:
2
- # <engagement-root>/.project-evidence/ado/config.yml
3
- # NOT in repo. NOT in `.kushi/`.
4
-
5
- tenantId: "72f988bf-86f1-41af-91ab-2d7cd011db47" # Microsoft tenant
6
- organization: "https://dev.azure.com/<your-org>"
7
- defaultProject: "<your-default-ado-project>"
8
- apiVersion: "7.1"
9
-
10
- # ADO scope GUID — fixed across tenants
11
- azDevOpsResourceId: "499b84ac-1321-427f-aa17-267ca6975798"
12
-
13
- # Saved queries / area paths can be overridden per project in
14
- # <engagement-root>/<project>/integrations.yml under `ado:`
15
- defaults:
16
- queryId: ""
17
- areaPath: ""
18
- iterationPath: ""
19
-
20
- auth:
21
- strategy: "az-cli"
@@ -1,16 +0,0 @@
1
- # CRM (Dataverse) connection — per-user filled. Lives at:
2
- # <engagement-root>/.project-evidence/crm/config.yml
3
- # NOT in repo. NOT in `.kushi/`.
4
-
5
- tenantId: "72f988bf-86f1-41af-91ab-2d7cd011db47" # Microsoft tenant
6
- environmentUrl: "https://<your-org>.crm.dynamics.com"
7
- apiVersion: "9.2"
8
-
9
- # Default entity to query. Override per project in
10
- # <engagement-root>/<project>/integrations.yml
11
- defaultEntitySet: "msdyn_engagements"
12
-
13
- # Authentication strategy. Currently only "az-cli" supported.
14
- # kushi will run: az account get-access-token --resource <environmentUrl> --tenant <tenantId>
15
- auth:
16
- strategy: "az-cli"
package/src/prompt.mjs DELETED
@@ -1,42 +0,0 @@
1
- import { createInterface } from 'node:readline/promises';
2
- import path from 'node:path';
3
-
4
- /**
5
- * Prompt the user for a destination directory.
6
- * @param {string} defaultDest
7
- * @returns {Promise<string>}
8
- */
9
- export async function promptForDestination(defaultDest) {
10
- const { stdin: input, stdout: output } = await import('node:process');
11
- const rl = createInterface({ input, output });
12
-
13
- rl.on('close', () => {});
14
-
15
- let dest;
16
- try {
17
- const answer = await rl.question(
18
- ` Where should Kushi assets be installed? (${defaultDest}): `,
19
- );
20
- dest = answer.trim() || defaultDest;
21
- } catch {
22
- process.exit(0);
23
- } finally {
24
- rl.close();
25
- }
26
-
27
- return validatePath(dest);
28
- }
29
-
30
- function validatePath(raw) {
31
- if (path.isAbsolute(raw)) {
32
- console.error('\n Please use a relative path (e.g., .kushi).\n');
33
- process.exit(1);
34
- }
35
- const resolved = path.resolve(process.cwd(), raw);
36
- const root = process.cwd();
37
- if (!resolved.startsWith(root + path.sep) && resolved !== root) {
38
- console.error('\n Destination must be within the current project.\n');
39
- process.exit(1);
40
- }
41
- return raw.replace(/\\/g, '/').replace(/\/+$/, '');
42
- }