kushi-agents 3.4.2 → 3.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/.github/copilot-instructions.kushi.md +38 -0
  2. package/README.md +33 -0
  3. package/bin/cli.mjs +2 -0
  4. package/package.json +17 -4
  5. package/plugin/agents/kushi.agent.md +155 -147
  6. package/plugin/instructions/ado-bootstrap-discovery.instructions.md +111 -0
  7. package/plugin/instructions/ado-engagement-tree.instructions.md +73 -0
  8. package/plugin/instructions/answer-from-evidence.instructions.md +1 -1
  9. package/plugin/instructions/auth-and-retry.instructions.md +51 -16
  10. package/plugin/instructions/azure-auth-patterns.instructions.md +13 -6
  11. package/plugin/instructions/bootstrap-status-format.instructions.md +113 -0
  12. package/plugin/instructions/capture-learnings.instructions.md +95 -0
  13. package/plugin/instructions/cleanup-on-resolution.instructions.md +69 -0
  14. package/plugin/instructions/crm-bootstrap-discovery.instructions.md +79 -0
  15. package/plugin/instructions/crm-internal-vs-confirmed.instructions.md +79 -0
  16. package/plugin/instructions/evidence-confidence-ladder.instructions.md +66 -0
  17. package/plugin/instructions/evidence-layout-canonical.instructions.md +115 -0
  18. package/plugin/instructions/evidence-thoroughness.instructions.md +82 -12
  19. package/plugin/instructions/full-view-gate.instructions.md +91 -0
  20. package/plugin/instructions/m365-id-registry.instructions.md +134 -0
  21. package/plugin/instructions/meetings-verbatim-required.instructions.md +176 -0
  22. package/plugin/instructions/run-reports.instructions.md +129 -0
  23. package/plugin/instructions/scope-boundaries.instructions.md +218 -0
  24. package/plugin/instructions/snapshot-vs-stream.instructions.md +2 -0
  25. package/plugin/instructions/update-ledger.instructions.md +132 -0
  26. package/plugin/instructions/verbatim-by-default.instructions.md +73 -0
  27. package/plugin/instructions/workiq-first.instructions.md +15 -31
  28. package/plugin/instructions/workiq-only.instructions.md +193 -0
  29. package/plugin/learnings/README.md +50 -0
  30. package/plugin/learnings/ado.md +45 -0
  31. package/plugin/learnings/crm.md +96 -0
  32. package/plugin/learnings/cross-cutting.md +36 -0
  33. package/plugin/learnings/email.md +33 -0
  34. package/plugin/learnings/meetings.md +30 -0
  35. package/plugin/learnings/misc.md +46 -0
  36. package/plugin/learnings/onenote.md +215 -0
  37. package/plugin/learnings/sharepoint.md +5 -0
  38. package/plugin/learnings/teams.md +5 -0
  39. package/plugin/plugin.json +22 -2
  40. package/plugin/prompts/apply-ado.prompt.md +14 -0
  41. package/plugin/prompts/propose-ado.prompt.md +12 -0
  42. package/plugin/reference-packs/fde/crm-field-manifest.md +165 -0
  43. package/plugin/skills/apply-ado-update/SKILL.md +125 -0
  44. package/plugin/skills/ask-project/SKILL.md +2 -0
  45. package/plugin/skills/bootstrap-project/SKILL.md +81 -3
  46. package/plugin/skills/propose-ado-update/SKILL.md +108 -0
  47. package/plugin/skills/pull-ado/SKILL.md +173 -23
  48. package/plugin/skills/pull-crm/SKILL.md +168 -15
  49. package/plugin/skills/pull-email/SKILL.md +139 -22
  50. package/plugin/skills/pull-meetings/SKILL.md +109 -25
  51. package/plugin/skills/pull-misc/README.md +84 -0
  52. package/plugin/skills/pull-misc/SKILL.md +257 -0
  53. package/plugin/skills/pull-misc/runner.mjs +280 -0
  54. package/plugin/skills/pull-onenote/README.md +90 -0
  55. package/plugin/skills/pull-onenote/SKILL.md +400 -51
  56. package/plugin/skills/pull-onenote/runner.mjs +356 -0
  57. package/plugin/skills/pull-onenote/scripts/recapture-section-url.mjs +295 -0
  58. package/plugin/skills/pull-onenote/write-snapshot.mjs +271 -0
  59. package/plugin/skills/pull-sharepoint/SKILL.md +44 -12
  60. package/plugin/skills/pull-teams/SKILL.md +40 -11
  61. package/plugin/skills/refresh-project/SKILL.md +33 -2
  62. package/plugin/skills/self-check/run.ps1 +186 -4
  63. package/plugin/templates/ado-update/discussion-comment.template.md +26 -0
  64. package/plugin/templates/ado-update/integrations-ado-writes.example.yml +49 -0
  65. package/plugin/templates/ado-update/proposed.template.md +78 -0
  66. package/plugin/templates/init/external-links.template.txt +30 -0
  67. package/plugin/templates/init/project-integrations.template.yml +57 -2
  68. package/plugin/templates/snapshot/meeting-verbatim.template.md +110 -0
  69. package/plugin/templates/snapshot/meetings-series-index.template.md +3 -1
  70. package/plugin/templates/snapshot/onenote-page.template.md +92 -23
  71. package/plugin/templates/weekly/meetings-stream.template.md +11 -6
  72. package/src/copilot-instructions.mjs +80 -0
  73. package/src/main.mjs +18 -1
@@ -0,0 +1,280 @@
1
+ #!/usr/bin/env node
2
+ // pull-misc runner — read external-links.txt, classify by type, route to runner branches.
3
+ // Per pull-misc SKILL.md v0.1.0 (kushi v3.9.0).
4
+ //
5
+ // Usage:
6
+ // node runner.mjs --project HCA --links-file "<path-to-external-links.txt>"
7
+ // node runner.mjs --project HCA --links-file "..." --headless
8
+ // node runner.mjs --project HCA --links-file "..." --types "loop,web" # only fetch some
9
+ // node runner.mjs --project HCA --links-file "..." --titles "ABN Workshop,..." # only specific titles
10
+ //
11
+ // Output is a single JSON object on stdout containing:
12
+ // { project, linksFile, links: [<perLinkRecord>...], runStatus, counts }
13
+
14
+ import fs from 'node:fs';
15
+ import fsp from 'node:fs/promises';
16
+ import path from 'node:path';
17
+ import os from 'node:os';
18
+ import process from 'node:process';
19
+
20
+ // ----- arg parse -----
21
+ const args = Object.fromEntries(
22
+ process.argv.slice(2).reduce((acc, cur, idx, arr) => {
23
+ if (cur.startsWith('--')) {
24
+ const key = cur.replace(/^--/, '');
25
+ const next = arr[idx + 1];
26
+ acc.push([key, next && !next.startsWith('--') ? next : true]);
27
+ }
28
+ return acc;
29
+ }, [])
30
+ );
31
+
32
+ const PROJECT = args.project || null;
33
+ const LINKS_FILE = args['links-file'] || null;
34
+ const ENGAGEMENT_ROOT = args['engagement-root'] || null;
35
+ const HEADLESS = !!args.headless;
36
+ const TYPES_FILTER = args.types ? String(args.types).split(',').map(s => s.trim().toLowerCase()) : null;
37
+ const TITLES_FILTER = args.titles ? String(args.titles).split(',').map(s => s.trim()) : null;
38
+ const TIMEOUT_MS = parseInt(args.timeout || '45000', 10);
39
+ const PROFILE_DIR = path.join(os.homedir(), '.copilot', 'playwright-profile', 'onenote');
40
+
41
+ if (!PROJECT || !LINKS_FILE) {
42
+ console.error('Usage: --project <name> --links-file <path> [--headless] [--types t1,t2] [--titles "t1,t2"]');
43
+ process.exit(2);
44
+ }
45
+
46
+ // ----- constants -----
47
+ const DELEGATED_TYPES = new Set(['onenote', 'sharepoint', 'ado']);
48
+ const BROWSER_TYPES = new Set(['loop']);
49
+ const PLACEHOLDER_RE = /<\s*(PASTE_[A-Z_]*_URL|TODO[^>]*)\s*>/i;
50
+
51
+ // ----- parse external-links.txt -----
52
+ function parseLinksFile(p) {
53
+ const txt = fs.readFileSync(p, 'utf-8');
54
+ const out = [];
55
+ for (const raw of txt.split(/\r?\n/)) {
56
+ const line = raw.trim();
57
+ if (!line || line.startsWith('#')) continue;
58
+ const parts = line.split('|').map(s => s.trim());
59
+ if (parts.length < 4) continue; // malformed
60
+ const [type, owner, title, url, ...noteParts] = parts;
61
+ out.push({
62
+ type: type.toLowerCase(),
63
+ owner,
64
+ title,
65
+ url,
66
+ notes: noteParts.join('|') || ''
67
+ });
68
+ }
69
+ return out;
70
+ }
71
+
72
+ function safeName(s) {
73
+ return s.replace(/[\\/:*?"<>|]/g, '-').replace(/\s+/g, '-').slice(0, 120);
74
+ }
75
+
76
+ function classify(link) {
77
+ if (PLACEHOLDER_RE.test(link.url)) return { route: 'placeholder' };
78
+ if (DELEGATED_TYPES.has(link.type)) return { route: 'delegated', delegated_to: `pull-${link.type}` };
79
+ if (BROWSER_TYPES.has(link.type)) return { route: 'browser' };
80
+ if (link.type === 'file') return { route: 'file' };
81
+ return { route: 'http' };
82
+ }
83
+
84
+ // ----- HTTP fetch branch -----
85
+ async function fetchHttp(url) {
86
+ try {
87
+ const res = await fetch(url, {
88
+ redirect: 'follow',
89
+ headers: { 'User-Agent': 'kushi-pull-misc/0.1', 'Accept': '*/*' },
90
+ signal: AbortSignal.timeout(TIMEOUT_MS)
91
+ });
92
+ const ct = res.headers.get('content-type') || '';
93
+ const etag = res.headers.get('etag') || null;
94
+ const lm = res.headers.get('last-modified') || null;
95
+
96
+ if (res.status === 401 || res.status === 403) {
97
+ return { last_status: 'auth-required', http_status: res.status, content_type: ct };
98
+ }
99
+ if (!res.ok) {
100
+ return { last_status: 'fetch-failed', http_status: res.status, content_type: ct };
101
+ }
102
+
103
+ if (ct.startsWith('application/pdf')) {
104
+ const buf = Buffer.from(await res.arrayBuffer());
105
+ // Defer text extract to a separate step (pdfjs not bundled to keep dep weight low).
106
+ return {
107
+ last_status: 'captured',
108
+ http_status: res.status,
109
+ content_type: ct,
110
+ etag, last_modified_http: lm,
111
+ body: `[binary PDF — ${buf.length} bytes; text extraction not yet implemented in v0.1]`,
112
+ char_count: 0
113
+ };
114
+ }
115
+
116
+ const body = await res.text();
117
+
118
+ // Heuristic: HTML → readability extract; plain → keep raw.
119
+ let extracted = body;
120
+ if (ct.includes('html')) {
121
+ try {
122
+ const { JSDOM } = await import('jsdom');
123
+ const { Readability } = await import('@mozilla/readability');
124
+ const dom = new JSDOM(body, { url });
125
+ const reader = new Readability(dom.window.document);
126
+ const article = reader.parse();
127
+ if (article && article.textContent && article.textContent.length > 100) {
128
+ extracted = `# ${article.title || ''}\n\n${article.textContent.trim()}`;
129
+ }
130
+ } catch (e) {
131
+ // jsdom or readability not available — fall through to raw
132
+ extracted = `[readability unavailable: ${e.message}]\n\n${body.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim()}`;
133
+ }
134
+ }
135
+
136
+ return {
137
+ last_status: 'captured',
138
+ http_status: res.status,
139
+ content_type: ct,
140
+ etag, last_modified_http: lm,
141
+ body: extracted,
142
+ char_count: extracted.length
143
+ };
144
+ } catch (e) {
145
+ return { last_status: 'fetch-failed', error: String(e.message || e) };
146
+ }
147
+ }
148
+
149
+ // ----- File read branch -----
150
+ async function readFileBranch(link) {
151
+ const root = ENGAGEMENT_ROOT || process.cwd();
152
+ const candidate1 = path.resolve(root, link.url);
153
+ const candidate2 = path.resolve(root, PROJECT, link.url);
154
+ const tryPath = fs.existsSync(candidate1) ? candidate1 : (fs.existsSync(candidate2) ? candidate2 : null);
155
+ if (!tryPath) return { last_status: 'fetch-failed', error: `not found at ${candidate1} or ${candidate2}` };
156
+ try {
157
+ const buf = await fsp.readFile(tryPath);
158
+ if (buf.includes(0)) return { last_status: 'skipped-binary', char_count: buf.length };
159
+ const txt = buf.toString('utf-8');
160
+ return { last_status: 'captured', body: txt, char_count: txt.length, resolved_path: tryPath };
161
+ } catch (e) {
162
+ return { last_status: 'fetch-failed', error: String(e.message || e) };
163
+ }
164
+ }
165
+
166
+ // ----- Browser (Loop) branch -----
167
+ async function fetchBrowser(link, ctx) {
168
+ const page = await ctx.newPage();
169
+ try {
170
+ const resp = await page.goto(link.url, { waitUntil: 'domcontentloaded', timeout: TIMEOUT_MS });
171
+ if (/login\.microsoftonline\.com|login\.live\.com/.test(page.url())) {
172
+ return { last_status: 'auth-required', captured_via: 'browser' };
173
+ }
174
+ // Wait for Loop content. Loop uses Fluid; the main content settles in a few seconds.
175
+ await page.waitForTimeout(4000);
176
+ // Try Loop-specific selectors first, then fallback to body
177
+ const body = await page.evaluate(() => {
178
+ const sels = [
179
+ '[data-loop-canvas]',
180
+ '.fui-FluentProvider',
181
+ '[role="main"]',
182
+ 'body'
183
+ ];
184
+ for (const s of sels) {
185
+ const el = document.querySelector(s);
186
+ if (el && el.innerText && el.innerText.length > 100) return el.innerText;
187
+ }
188
+ return document.body ? document.body.innerText : '';
189
+ });
190
+ if (!body || body.length < 50) {
191
+ return { last_status: 'fetch-failed', captured_via: 'browser', error: 'empty or near-empty body', char_count: body?.length || 0 };
192
+ }
193
+ return { last_status: 'captured', captured_via: 'browser', body, char_count: body.length };
194
+ } catch (e) {
195
+ return { last_status: 'fetch-failed', captured_via: 'browser', error: String(e.message || e) };
196
+ } finally {
197
+ await page.close().catch(() => {});
198
+ }
199
+ }
200
+
201
+ // ----- main -----
202
+ (async () => {
203
+ const links = parseLinksFile(LINKS_FILE);
204
+ const filtered = links.filter(l => {
205
+ if (TYPES_FILTER && !TYPES_FILTER.includes(l.type)) return false;
206
+ if (TITLES_FILTER && !TITLES_FILTER.some(t => l.title.includes(t))) return false;
207
+ return true;
208
+ });
209
+
210
+ const needsBrowser = filtered.some(l => classify(l).route === 'browser');
211
+ let ctx = null;
212
+ if (needsBrowser) {
213
+ if (!fs.existsSync(PROFILE_DIR)) {
214
+ console.error('[pull-misc] Playwright profile missing — loop links will be marked auth-required.');
215
+ } else {
216
+ const { chromium } = await import('playwright');
217
+ ctx = await chromium.launchPersistentContext(PROFILE_DIR, { headless: HEADLESS, viewport: { width: 1400, height: 900 } });
218
+ }
219
+ }
220
+
221
+ const results = [];
222
+ const now = new Date().toISOString();
223
+
224
+ for (const link of filtered) {
225
+ const cls = classify(link);
226
+ let rec = {
227
+ type: link.type,
228
+ owner: link.owner,
229
+ title: link.title,
230
+ url: link.url,
231
+ notes: link.notes,
232
+ attempts: 1,
233
+ last_attempt_at: now,
234
+ snapshot_path: `Evidence/<alias>/misc/snapshot/${safeName(link.type)}__${safeName(link.title)}.md`
235
+ };
236
+
237
+ if (cls.route === 'placeholder') {
238
+ Object.assign(rec, { last_status: 'placeholder', captured_via: null });
239
+ } else if (cls.route === 'delegated') {
240
+ Object.assign(rec, { last_status: 'delegated', captured_via: 'delegated', delegated_to: cls.delegated_to });
241
+ } else if (cls.route === 'http') {
242
+ const r = await fetchHttp(link.url);
243
+ Object.assign(rec, r, { captured_via: 'http' });
244
+ if (r.last_status === 'captured') rec.captured_at = now;
245
+ } else if (cls.route === 'file') {
246
+ const r = await readFileBranch(link);
247
+ Object.assign(rec, r, { captured_via: 'file' });
248
+ if (r.last_status === 'captured') rec.captured_at = now;
249
+ } else if (cls.route === 'browser') {
250
+ if (!ctx) {
251
+ Object.assign(rec, { last_status: 'auth-required', captured_via: 'browser', error: 'no playwright profile' });
252
+ } else {
253
+ const r = await fetchBrowser(link, ctx);
254
+ Object.assign(rec, r);
255
+ if (r.last_status === 'captured') rec.captured_at = now;
256
+ }
257
+ }
258
+ results.push(rec);
259
+ }
260
+
261
+ if (ctx) await ctx.close();
262
+
263
+ const counts = {
264
+ total: results.length,
265
+ captured: results.filter(r => r.last_status === 'captured').length,
266
+ placeholder: results.filter(r => r.last_status === 'placeholder').length,
267
+ delegated: results.filter(r => r.last_status === 'delegated').length,
268
+ auth_required: results.filter(r => r.last_status === 'auth-required').length,
269
+ fetch_failed: results.filter(r => r.last_status === 'fetch-failed').length,
270
+ skipped_binary: results.filter(r => r.last_status === 'skipped-binary').length
271
+ };
272
+ const runStatus = counts.auth_required > 0 ? 'auth-required'
273
+ : counts.fetch_failed > 0 ? 'partial'
274
+ : 'ok';
275
+
276
+ console.log(JSON.stringify({ project: PROJECT, linksFile: LINKS_FILE, runStatus, counts, links: results }, null, 2));
277
+ })().catch(e => {
278
+ console.error('[pull-misc] runner crashed:', e);
279
+ process.exit(1);
280
+ });
@@ -0,0 +1,90 @@
1
+ # pull-onenote — runner README
2
+
3
+ ## What this is
4
+
5
+ A Playwright-driven browser scraper for OneNote-for-Web that returns verbatim page bodies for an entire section. Used by the `pull-onenote` skill (v2.6.0+) as the PRIMARY capture path.
6
+
7
+ ## One-time setup
8
+
9
+ ```pwsh
10
+ # 1. Install Node + Playwright if you don't have them
11
+ npm install -g playwright
12
+ npx playwright install chromium
13
+
14
+ # 2. Seed the persistent browser profile (interactive — one-time)
15
+ node plugin/skills/pull-onenote/runner.mjs --bootstrap
16
+ ```
17
+
18
+ When the bootstrap command opens a Chromium window:
19
+ - Sign in with your work account at `onenote.cloud.microsoft`.
20
+ - Check **Keep me signed in**.
21
+ - Wait for the OneNote home page to load.
22
+ - Close the window.
23
+
24
+ The profile is persisted at `~/.copilot/playwright-profile/onenote/`. Subsequent runs reuse it.
25
+
26
+ ## Per-section runs
27
+
28
+ ```pwsh
29
+ node plugin/skills/pull-onenote/runner.mjs `
30
+ --project "HCA" `
31
+ --section-url "https://microsoft-my.sharepoint-df.com/personal/<upn>/_layouts/15/Doc.aspx?sourcedoc=%7B<notebookSourceDoc>%7D&action=edit&wd=target%28<sectionName>.one%7C<sectionFileId>%2F%29" `
32
+ --headless
33
+ ```
34
+
35
+ The runner emits a single JSON object on stdout with this shape:
36
+
37
+ ```json
38
+ {
39
+ "project": "HCA",
40
+ "sectionUrl": "...",
41
+ "runStatus": "ok | auth-required | partial",
42
+ "exitedEarly": false,
43
+ "authRequiredCount": 0,
44
+ "pages": [
45
+ {
46
+ "title": "4/3 - HCA with Jay and Martin",
47
+ "pos": 12, "total": 16,
48
+ "webPageId": "78aac9b5-0629-4daa-ada3-f2436cb2381c",
49
+ "last_status": "captured | short-suspect | auth-required | browser-error",
50
+ "captured_via": "browser",
51
+ "body": "...verbatim text...",
52
+ "bodyLen": 6904,
53
+ "capturedAt": "2026-05-14T03:55:00Z"
54
+ }
55
+ ]
56
+ }
57
+ ```
58
+
59
+ The skill driver merges this into `m365-mutable.json#knownSections.<projectKey>.one_pages[]` and writes the snapshot pages.
60
+
61
+ ## Filtering
62
+
63
+ For retry-only runs, pass `--titles` with a comma-separated list:
64
+
65
+ ```pwsh
66
+ node runner.mjs --project HCA --section-url "..." --titles "4/3 - HCA with Jay and Martin,5/7" --headless
67
+ ```
68
+
69
+ ## Scheduled / unattended runs
70
+
71
+ For Monday-9am-style schedules, always pass `--headless`. The runner detects redirects to `login.microsoftonline.com` and exits early with `runStatus: "auth-required"` so the driver can mark all queued pages as `auth-required` and surface in the run report.
72
+
73
+ There is currently no way to satisfy a Conditional Access / MFA challenge unattended. When that happens (rough estimate: every 1–4 weeks under a Microsoft tenant CA policy), do one interactive `--bootstrap` to refresh the profile and the next scheduled run resumes silently.
74
+
75
+ ## Constructing the section URL
76
+
77
+ Once a project's `knownSections` entry is populated in `m365-mutable.json`, the URL is:
78
+
79
+ ```
80
+ <one_notebookSpoBaseUrl>/_layouts/15/Doc.aspx?sourcedoc=%7B<one_notebookSourceDoc>%7D&action=edit&wd=target(<one_sectionName>%7C<one_sectionFileId>%2F)
81
+ ```
82
+
83
+ URL-encode the section name; double-encode the pipe (`%7C`) and trailing slash (`%2F`).
84
+
85
+ ## Troubleshooting
86
+
87
+ - **Picker shown every run.** First-run only, and only if multiple work accounts are listed. After picking once it sticks.
88
+ - **"OneNote canvas frame did not attach."** Almost always auth — check the headless output's `runStatus`.
89
+ - **Bodies look short.** The `last_status: short-suspect` flag is for this. Verify in OneNote desktop; if genuinely sparse, accept; otherwise re-run.
90
+ - **Pages enumerate fine but no `webPageId` returned.** The `wd=target(...)` URL fragment shape may have changed. Update `parseWebPageIdFromUrl` regex in `runner.mjs`.