kushi-agents 4.4.4 → 4.7.4
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.
- package/README.md +3 -0
- package/package.json +4 -4
- package/plugin/agents/kushi.agent.md +29 -15
- package/plugin/config/studios.json +37 -0
- package/plugin/config/studios.schema.json +45 -0
- package/plugin/instructions/auth-and-retry.instructions.md +268 -1
- package/plugin/instructions/engagement-root-resolution.instructions.md +5 -1
- package/plugin/instructions/evidence-thoroughness.instructions.md +103 -1
- package/plugin/instructions/fuzzy-disambiguation.instructions.md +97 -0
- package/plugin/instructions/identity-resolution.instructions.md +76 -0
- package/plugin/instructions/issue-recovery.instructions.md +58 -0
- package/plugin/instructions/kushi-config-root.instructions.md +66 -0
- package/plugin/instructions/loop-bootstrap-discovery.instructions.md +105 -0
- package/plugin/instructions/m365-id-registry.instructions.md +1 -1
- package/plugin/instructions/onedrive-pin-policy.instructions.md +132 -0
- package/plugin/instructions/per-source-verification-gate.instructions.md +193 -0
- package/plugin/instructions/sharepoint-to-onedrive-sync.instructions.md +116 -0
- package/plugin/instructions/status-color-rule.instructions.md +62 -0
- package/plugin/instructions/studio-registry.instructions.md +48 -0
- package/plugin/instructions/update-ledger.instructions.md +1 -1
- package/plugin/instructions/verbatim-by-default.instructions.md +1 -1
- package/plugin/instructions/vertex-emit.instructions.md +120 -0
- package/plugin/instructions/workiq-input-sanitization.instructions.md +43 -0
- package/plugin/instructions/workiq-onenote-query-shape.instructions.md +79 -0
- package/plugin/instructions/workiq-only.instructions.md +13 -7
- package/plugin/learnings/loop.md +11 -0
- package/plugin/learnings/onenote.md +27 -1
- package/plugin/lib/Get-KushiConfig.ps1 +22 -9
- package/plugin/lib/detect-vertex-repo.mjs +96 -0
- package/plugin/lib/render-vertex.mjs +249 -0
- package/plugin/lib/sanitize-workiq-input.mjs +72 -0
- package/plugin/lib/studio-registry.mjs +39 -0
- package/plugin/lib/vertex-validate.mjs +121 -0
- package/plugin/plugin.json +13 -6
- package/plugin/prompts/bootstrap.prompt.md +9 -7
- package/plugin/prompts/emit-vertex.prompt.md +33 -0
- package/plugin/prompts/setup.prompt.md +1 -1
- package/plugin/prompts/vertex-link.prompt.md +27 -0
- package/plugin/skills/aggregate-project/SKILL.md +24 -2
- package/plugin/skills/apply-ado-update/SKILL.md +9 -4
- package/plugin/skills/ask-project/SKILL.md +4 -0
- package/plugin/skills/bootstrap-project/SKILL.md +67 -37
- package/plugin/skills/consolidate-evidence/SKILL.md +5 -1
- package/plugin/skills/emit-vertex/README.md +37 -0
- package/plugin/skills/emit-vertex/SKILL.md +173 -0
- package/plugin/skills/intro/SKILL.md +2 -0
- package/plugin/skills/propose-ado-update/SKILL.md +8 -3
- package/plugin/skills/pull-ado/SKILL.md +11 -1
- package/plugin/skills/pull-crm/SKILL.md +12 -2
- package/plugin/skills/pull-email/SKILL.md +11 -1
- package/plugin/skills/pull-loop/README.md +64 -0
- package/plugin/skills/pull-loop/SKILL.md +180 -0
- package/plugin/skills/pull-loop/runner.mjs +261 -0
- package/plugin/skills/pull-loop/write-snapshot.mjs +181 -0
- package/plugin/skills/pull-meetings/SKILL.md +11 -1
- package/plugin/skills/pull-misc/README.md +4 -4
- package/plugin/skills/pull-misc/SKILL.md +18 -12
- package/plugin/skills/pull-onenote/SKILL.md +71 -19
- package/plugin/skills/pull-sharepoint/SKILL.md +11 -2
- package/plugin/skills/pull-teams/SKILL.md +11 -2
- package/plugin/skills/refresh-project/SKILL.md +38 -7
- package/plugin/skills/self-check/SKILL.md +14 -1
- package/plugin/skills/self-check/run.ps1 +442 -20
- package/plugin/skills/setup/SKILL.md +289 -86
- package/plugin/skills/vertex-link/SKILL.md +143 -0
- package/plugin/templates/init/m365-auth.template.json +10 -4
- package/plugin/templates/init/project-evidence.template.yml +4 -1
- package/plugin/templates/init/project-integrations.template.yml +5 -0
- package/plugin/templates/snapshot/ado-item.template.md +1 -1
- package/plugin/templates/snapshot/crm-record.template.md +1 -1
- package/plugin/templates/snapshot/meetings-series-index.template.md +1 -1
- package/plugin/templates/snapshot/onenote-page.template.md +1 -1
- package/plugin/templates/snapshot/sharepoint-file.template.md +1 -1
- package/plugin/templates/snapshot/sharepoint-tree.template.md +1 -1
- package/plugin/templates/snapshot/teams-roster.template.md +1 -1
- package/plugin/templates/weekly/ado-stream.template.md +1 -1
- package/plugin/templates/weekly/crm-stream.template.md +1 -1
- package/plugin/templates/weekly/email-stream.template.md +1 -1
- package/plugin/templates/weekly/meetings-stream.template.md +1 -1
- package/plugin/templates/weekly/onenote-stream.template.md +1 -1
- package/plugin/templates/weekly/sharepoint-stream.template.md +1 -1
- package/plugin/templates/weekly/teams-stream.template.md +1 -1
- package/src/check-workiq.mjs +48 -15
- package/src/config-loader.mjs +71 -13
- package/src/config-root-resolve.test.mjs +137 -0
- package/src/detect-vertex-repo.test.mjs +128 -0
- package/src/emit-vertex.e2e.test.mjs +308 -0
- package/src/forbidden-workiq-phrasings.test.mjs +111 -0
- package/src/sanitize-workiq-input.test.mjs +45 -0
- package/src/vertex-validate.test.mjs +142 -0
- package/plugin/instructions/az-auth-conditional.instructions.md +0 -39
- package/plugin/instructions/azure-auth-patterns.instructions.md +0 -233
- package/plugin/instructions/thoroughness-detector.instructions.md +0 -105
- package/plugin/instructions/workiq-first.instructions.md +0 -31
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// Render vertex-shaped artifacts from kushi inputs.
|
|
2
|
+
//
|
|
3
|
+
// Each renderer returns { relPath, content } where:
|
|
4
|
+
// - relPath is the path WITHIN the initiative directory (e.g.
|
|
5
|
+
// "status-updates/2026-05-20.md"), used both for staging and for the
|
|
6
|
+
// destination inside the vertex repo on --apply.
|
|
7
|
+
// - content is the full markdown body (frontmatter + sections).
|
|
8
|
+
//
|
|
9
|
+
// Schemas live in <vertex-repo>/.vertex/scripts/validation/schemas/. This
|
|
10
|
+
// module mirrors required fields by hand — when vertex schemas change, the
|
|
11
|
+
// validator catches drift (no silent skew possible).
|
|
12
|
+
|
|
13
|
+
const NL = "\n";
|
|
14
|
+
|
|
15
|
+
function fm(obj) {
|
|
16
|
+
// Minimal YAML emitter for the small surface vertex needs.
|
|
17
|
+
// Keeps quoting predictable so frontmatter diffs are stable.
|
|
18
|
+
const lines = ["---"];
|
|
19
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
20
|
+
if (v === null || v === undefined) continue;
|
|
21
|
+
if (typeof v === "string") {
|
|
22
|
+
// ISO date stays unquoted (validator format check); other strings quoted.
|
|
23
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(v)) lines.push(`${k}: ${v}`);
|
|
24
|
+
else lines.push(`${k}: ${JSON.stringify(v)}`);
|
|
25
|
+
} else if (typeof v === "number" || typeof v === "boolean") {
|
|
26
|
+
lines.push(`${k}: ${v}`);
|
|
27
|
+
} else if (Array.isArray(v)) {
|
|
28
|
+
lines.push(`${k}:`);
|
|
29
|
+
for (const item of v) lines.push(` - ${JSON.stringify(item)}`);
|
|
30
|
+
} else {
|
|
31
|
+
lines.push(`${k}: ${JSON.stringify(v)}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
lines.push("---", "");
|
|
35
|
+
return lines.join(NL);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function requireFields(obj, fields, ctx) {
|
|
39
|
+
for (const f of fields) {
|
|
40
|
+
if (obj[f] === undefined || obj[f] === null || obj[f] === "") {
|
|
41
|
+
throw new Error(`[render-vertex:${ctx}] missing required field: ${f}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Render a vertex status-update file.
|
|
48
|
+
*
|
|
49
|
+
* input: {
|
|
50
|
+
* customer, initiative, // display names (strings)
|
|
51
|
+
* weekEnding, // "YYYY-MM-DD"
|
|
52
|
+
* status, // "green" | "yellow" | "red"
|
|
53
|
+
* statusReason, // optional audit string from status-color rule
|
|
54
|
+
* author, // string
|
|
55
|
+
* summary, // string (prose)
|
|
56
|
+
* highlights, // string[] of bullets
|
|
57
|
+
* nextSteps, // string[] of bullets
|
|
58
|
+
* risks, // [{risk, severity, owner, mitigation}]
|
|
59
|
+
* indicators, // [{indicator, value}]
|
|
60
|
+
* }
|
|
61
|
+
*/
|
|
62
|
+
export function renderStatusUpdate(input) {
|
|
63
|
+
requireFields(input, ["customer", "initiative", "weekEnding", "status", "author"], "status-update");
|
|
64
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(input.weekEnding)) {
|
|
65
|
+
throw new Error("[render-vertex:status-update] weekEnding must be YYYY-MM-DD");
|
|
66
|
+
}
|
|
67
|
+
if (!["green", "yellow", "red"].includes(input.status)) {
|
|
68
|
+
throw new Error(`[render-vertex:status-update] status must be green|yellow|red, got: ${input.status}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const frontmatter = {
|
|
72
|
+
type: "status-update",
|
|
73
|
+
initiative: input.initiative,
|
|
74
|
+
customer: input.customer,
|
|
75
|
+
week_ending: input.weekEnding,
|
|
76
|
+
status: input.status,
|
|
77
|
+
author: input.author,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const out = [fm(frontmatter)];
|
|
81
|
+
out.push(`# Status Update: Week Ending ${input.weekEnding}`, "");
|
|
82
|
+
if (input.statusReason) {
|
|
83
|
+
out.push(`<!-- status-reason: ${input.statusReason} -->`, "");
|
|
84
|
+
}
|
|
85
|
+
out.push("## Summary", "");
|
|
86
|
+
out.push(input.summary || "_No summary provided._", "");
|
|
87
|
+
|
|
88
|
+
if (input.highlights && input.highlights.length) {
|
|
89
|
+
out.push("## Highlights", "");
|
|
90
|
+
for (const b of input.highlights) out.push(`- ${b}`);
|
|
91
|
+
out.push("");
|
|
92
|
+
}
|
|
93
|
+
if (input.nextSteps && input.nextSteps.length) {
|
|
94
|
+
out.push("## Next Steps", "");
|
|
95
|
+
for (const b of input.nextSteps) out.push(`- ${b}`);
|
|
96
|
+
out.push("");
|
|
97
|
+
}
|
|
98
|
+
if (input.risks && input.risks.length) {
|
|
99
|
+
out.push("## Risks and Blockers", "");
|
|
100
|
+
out.push("| Risk / Blocker | Severity | Owner | Mitigation / Status |");
|
|
101
|
+
out.push("| -------------- | -------- | ----- | ------------------- |");
|
|
102
|
+
for (const r of input.risks) {
|
|
103
|
+
out.push(`| ${r.risk} | ${r.severity} | ${r.owner} | ${r.mitigation} |`);
|
|
104
|
+
}
|
|
105
|
+
out.push("");
|
|
106
|
+
}
|
|
107
|
+
if (input.indicators && input.indicators.length) {
|
|
108
|
+
out.push("## Progress and Outcomes", "");
|
|
109
|
+
out.push("| Indicator | Value |");
|
|
110
|
+
out.push("| --------- | ----- |");
|
|
111
|
+
for (const i of input.indicators) {
|
|
112
|
+
out.push(`| ${i.indicator} | ${i.value} |`);
|
|
113
|
+
}
|
|
114
|
+
out.push("");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const relPath = `status-updates/${input.weekEnding}.md`;
|
|
118
|
+
return { relPath, content: out.join(NL) };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Render a vertex decision (ADR) file.
|
|
123
|
+
*
|
|
124
|
+
* input: {
|
|
125
|
+
* number, slug, title, // 001, "auth-jwt", "Adopt JWT for auth"
|
|
126
|
+
* status, // "proposed"|"accepted"|"superseded"|...
|
|
127
|
+
* date, // "YYYY-MM-DD"
|
|
128
|
+
* author,
|
|
129
|
+
* context, decision, // prose
|
|
130
|
+
* alternatives, consequences, // string[] (bullets)
|
|
131
|
+
* }
|
|
132
|
+
*/
|
|
133
|
+
export function renderDecision(input) {
|
|
134
|
+
requireFields(input, ["number", "slug", "title", "status", "date", "author"], "decision");
|
|
135
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(input.date)) {
|
|
136
|
+
throw new Error("[render-vertex:decision] date must be YYYY-MM-DD");
|
|
137
|
+
}
|
|
138
|
+
const padded = String(input.number).padStart(3, "0");
|
|
139
|
+
|
|
140
|
+
const frontmatter = {
|
|
141
|
+
type: "decision",
|
|
142
|
+
number: input.number,
|
|
143
|
+
title: input.title,
|
|
144
|
+
status: input.status,
|
|
145
|
+
date: input.date,
|
|
146
|
+
author: input.author,
|
|
147
|
+
};
|
|
148
|
+
const out = [fm(frontmatter)];
|
|
149
|
+
out.push(`# ADR-${padded}: ${input.title}`, "");
|
|
150
|
+
out.push("## Context", "", input.context || "_TBD_", "");
|
|
151
|
+
out.push("## Decision", "", input.decision || "_TBD_", "");
|
|
152
|
+
if (input.alternatives && input.alternatives.length) {
|
|
153
|
+
out.push("## Alternatives Considered", "");
|
|
154
|
+
for (const b of input.alternatives) out.push(`- ${b}`);
|
|
155
|
+
out.push("");
|
|
156
|
+
}
|
|
157
|
+
if (input.consequences && input.consequences.length) {
|
|
158
|
+
out.push("## Consequences", "");
|
|
159
|
+
for (const b of input.consequences) out.push(`- ${b}`);
|
|
160
|
+
out.push("");
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
relPath: `decisions/${padded}-${input.slug}.md`,
|
|
164
|
+
content: out.join(NL),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Render a vertex comms call-transcript file.
|
|
170
|
+
*
|
|
171
|
+
* input: { date, slug, title, participants, recording_url?, summary, transcript }
|
|
172
|
+
*/
|
|
173
|
+
export function renderCallTranscript(input) {
|
|
174
|
+
requireFields(input, ["date", "slug", "title"], "comms-call-transcript");
|
|
175
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(input.date)) {
|
|
176
|
+
throw new Error("[render-vertex:comms] date must be YYYY-MM-DD");
|
|
177
|
+
}
|
|
178
|
+
const frontmatter = {
|
|
179
|
+
title: input.title,
|
|
180
|
+
date: input.date,
|
|
181
|
+
type: "call-transcript",
|
|
182
|
+
participants: input.participants || [],
|
|
183
|
+
};
|
|
184
|
+
// recording_url is not in the comms schema but the schema allows
|
|
185
|
+
// additionalProperties; keep it as supplementary metadata for humans.
|
|
186
|
+
if (input.recording_url) frontmatter.recording_url = input.recording_url;
|
|
187
|
+
const out = [fm(frontmatter)];
|
|
188
|
+
out.push(`# ${input.title}`, "");
|
|
189
|
+
out.push("## Summary", "", input.summary || "_TBD_", "");
|
|
190
|
+
if (input.transcript) {
|
|
191
|
+
out.push("## Transcript", "", input.transcript, "");
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
relPath: `comms/call-transcripts/${input.date}-${input.slug}.md`,
|
|
195
|
+
content: out.join(NL),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Render a vertex comms chat file.
|
|
201
|
+
*
|
|
202
|
+
* input: { date, slug, title, participants, summary, body? }
|
|
203
|
+
*/
|
|
204
|
+
export function renderChat(input) {
|
|
205
|
+
requireFields(input, ["date", "slug", "title"], "comms-chat");
|
|
206
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(input.date)) {
|
|
207
|
+
throw new Error("[render-vertex:comms] date must be YYYY-MM-DD");
|
|
208
|
+
}
|
|
209
|
+
const frontmatter = {
|
|
210
|
+
title: input.title,
|
|
211
|
+
date: input.date,
|
|
212
|
+
type: "chat",
|
|
213
|
+
participants: input.participants || [],
|
|
214
|
+
};
|
|
215
|
+
const out = [fm(frontmatter)];
|
|
216
|
+
out.push(`# ${input.title}`, "");
|
|
217
|
+
out.push("## Summary", "", input.summary || "_TBD_", "");
|
|
218
|
+
if (input.body) out.push("## Conversation", "", input.body, "");
|
|
219
|
+
return {
|
|
220
|
+
relPath: `comms/chats/${input.date}-${input.slug}.md`,
|
|
221
|
+
content: out.join(NL),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Render a vertex comms email file.
|
|
227
|
+
*
|
|
228
|
+
* input: { date, slug, title, participants?, summary, body? }
|
|
229
|
+
*/
|
|
230
|
+
export function renderEmail(input) {
|
|
231
|
+
requireFields(input, ["date", "slug", "title"], "comms-email");
|
|
232
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(input.date)) {
|
|
233
|
+
throw new Error("[render-vertex:comms] date must be YYYY-MM-DD");
|
|
234
|
+
}
|
|
235
|
+
const frontmatter = {
|
|
236
|
+
title: input.title,
|
|
237
|
+
date: input.date,
|
|
238
|
+
type: "email",
|
|
239
|
+
participants: input.participants || [],
|
|
240
|
+
};
|
|
241
|
+
const out = [fm(frontmatter)];
|
|
242
|
+
out.push(`# ${input.title}`, "");
|
|
243
|
+
out.push("## Summary", "", input.summary || "_TBD_", "");
|
|
244
|
+
if (input.body) out.push("## Body", "", input.body, "");
|
|
245
|
+
return {
|
|
246
|
+
relPath: `comms/emails/${input.date}-${input.slug}.md`,
|
|
247
|
+
content: out.join(NL),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Sanitize user-controlled values before substituting into WorkIQ natural-language queries.
|
|
2
|
+
// Borrowed pattern: vertex weekly-status agent Phase 2a.
|
|
3
|
+
|
|
4
|
+
export class WorkIQInputError extends Error {
|
|
5
|
+
constructor(message, { field, value, reason } = {}) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'WorkIQInputError';
|
|
8
|
+
this.field = field;
|
|
9
|
+
this.value = value;
|
|
10
|
+
this.reason = reason;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const INJECTION_PHRASES = [
|
|
15
|
+
/ignore previous/i,
|
|
16
|
+
/ignore above/i,
|
|
17
|
+
/disregard (the |all )?(prior|previous|above)/i,
|
|
18
|
+
/new instructions/i,
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const CONTROL_TOKENS = ['<|', '|>', '<<SYS>>', '<<USER>>', '<<ASSISTANT>>'];
|
|
22
|
+
|
|
23
|
+
export function sanitize(value, { field = 'value', maxLength = 120 } = {}) {
|
|
24
|
+
if (value == null) {
|
|
25
|
+
throw new WorkIQInputError(`Cannot sanitize empty ${field}`, { field, value, reason: 'empty' });
|
|
26
|
+
}
|
|
27
|
+
const s = String(value);
|
|
28
|
+
if (s.length === 0) {
|
|
29
|
+
throw new WorkIQInputError(`Cannot sanitize empty ${field}`, { field, value: s, reason: 'empty' });
|
|
30
|
+
}
|
|
31
|
+
if (s.length > maxLength) {
|
|
32
|
+
throw new WorkIQInputError(
|
|
33
|
+
`${field} exceeds ${maxLength} chars (${s.length}); refusing to interpolate into WorkIQ query`,
|
|
34
|
+
{ field, value: s, reason: 'too-long' }
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
if (/[\r\n]/.test(s)) {
|
|
38
|
+
throw new WorkIQInputError(
|
|
39
|
+
`${field} contains a newline; refusing to interpolate into WorkIQ query`,
|
|
40
|
+
{ field, value: s, reason: 'newline' }
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
if (s.includes('```')) {
|
|
44
|
+
throw new WorkIQInputError(
|
|
45
|
+
`${field} contains triple backticks; refusing to interpolate into WorkIQ query`,
|
|
46
|
+
{ field, value: s, reason: 'backticks' }
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
for (const re of INJECTION_PHRASES) {
|
|
50
|
+
if (re.test(s)) {
|
|
51
|
+
throw new WorkIQInputError(
|
|
52
|
+
`${field} contains a prompt-injection phrase (${re}); refusing to interpolate into WorkIQ query`,
|
|
53
|
+
{ field, value: s, reason: 'injection-phrase' }
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
for (const tok of CONTROL_TOKENS) {
|
|
58
|
+
if (s.includes(tok)) {
|
|
59
|
+
throw new WorkIQInputError(
|
|
60
|
+
`${field} contains a model-control token (${tok}); refusing to interpolate into WorkIQ query`,
|
|
61
|
+
{ field, value: s, reason: 'control-token' }
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return s;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function quoteForQuery(value, { field } = {}) {
|
|
69
|
+
const safe = sanitize(value, { field });
|
|
70
|
+
// Single-quote wrap is canonical; sanitize already rejected anything that could break out.
|
|
71
|
+
return `'${safe}'`;
|
|
72
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = path.dirname(__filename);
|
|
7
|
+
|
|
8
|
+
const PACKAGED_REGISTRY = path.resolve(__dirname, '..', 'config', 'studios.json');
|
|
9
|
+
|
|
10
|
+
// Resolution order (highest wins):
|
|
11
|
+
// 1. <project>/.kushi/studios.json
|
|
12
|
+
// 2. ~/.copilot/m-skills/kushi/config/studios.json (user override)
|
|
13
|
+
// 3. PACKAGED_REGISTRY (plugin/config/studios.json)
|
|
14
|
+
export function loadStudioRegistry({ projectRoot, userOverrideDir } = {}) {
|
|
15
|
+
const candidates = [];
|
|
16
|
+
if (projectRoot) candidates.push(path.join(projectRoot, '.kushi', 'studios.json'));
|
|
17
|
+
if (userOverrideDir) candidates.push(path.join(userOverrideDir, 'studios.json'));
|
|
18
|
+
candidates.push(PACKAGED_REGISTRY);
|
|
19
|
+
|
|
20
|
+
for (const p of candidates) {
|
|
21
|
+
if (fs.existsSync(p)) {
|
|
22
|
+
const raw = fs.readFileSync(p, 'utf8');
|
|
23
|
+
const data = JSON.parse(raw);
|
|
24
|
+
return { ...data, _source: p };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
throw new Error('No studio registry found; checked: ' + candidates.join(', '));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getStudio(registry, id) {
|
|
31
|
+
if (!id) return registry.studios._default ?? null;
|
|
32
|
+
const studio = registry.studios[id];
|
|
33
|
+
if (studio) return studio;
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function listKnownStudios(registry) {
|
|
38
|
+
return Object.keys(registry.studios).filter((k) => k !== '_default');
|
|
39
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// Invoke the vertex repo's `validate_frontmatter.py` against staged files.
|
|
2
|
+
// Vertex schemas live in <repo>/.vertex/scripts/validation/schemas/; the
|
|
3
|
+
// validator script picks the right schema by the file's path within the
|
|
4
|
+
// initiative. Kushi ships NO copies — schemas are read in place.
|
|
5
|
+
//
|
|
6
|
+
// Cross-platform: uses spawnSync with `python3` then falls back to `python`.
|
|
7
|
+
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
9
|
+
|
|
10
|
+
export class VertexValidatorError extends Error {
|
|
11
|
+
constructor(code, message, details = {}) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = "VertexValidatorError";
|
|
14
|
+
this.code = code;
|
|
15
|
+
this.details = details;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let _cachedPython = null;
|
|
20
|
+
|
|
21
|
+
function pickPython() {
|
|
22
|
+
if (_cachedPython !== null) return _cachedPython;
|
|
23
|
+
// Vertex's validator requires `pyyaml`. Some systems have multiple python
|
|
24
|
+
// shims (e.g. Windows `python3` is a Store-app stub) where `--version`
|
|
25
|
+
// succeeds but `import yaml` fails. Probe both AND verify yaml is reachable.
|
|
26
|
+
const candidates = ["python3", "python", "py"];
|
|
27
|
+
for (const bin of candidates) {
|
|
28
|
+
const ver = spawnSync(bin, ["--version"], { encoding: "utf8" });
|
|
29
|
+
if (ver.status !== 0) continue;
|
|
30
|
+
const yaml = spawnSync(bin, ["-c", "import yaml"], { encoding: "utf8" });
|
|
31
|
+
if (yaml.status === 0) {
|
|
32
|
+
_cachedPython = bin;
|
|
33
|
+
return bin;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// Fall back to whichever python at least exists — caller will get a clear
|
|
37
|
+
// "vertex-schema-fail" with ModuleNotFoundError stderr if yaml is missing.
|
|
38
|
+
for (const bin of candidates) {
|
|
39
|
+
const ver = spawnSync(bin, ["--version"], { encoding: "utf8" });
|
|
40
|
+
if (ver.status === 0) {
|
|
41
|
+
_cachedPython = bin;
|
|
42
|
+
return bin;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
_cachedPython = false;
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Exposed for tests to reset the cache between runs.
|
|
50
|
+
export function _resetPythonCache() {
|
|
51
|
+
_cachedPython = null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Validate a single staged file. Returns {ok: true} on success.
|
|
56
|
+
*
|
|
57
|
+
* Args:
|
|
58
|
+
* - validatorPath : absolute path to vertex's validate_frontmatter.py
|
|
59
|
+
* - filePath : absolute path to the file to validate (may be OUTSIDE the vertex repo
|
|
60
|
+
* when staging — pass `baseDir` set to the staging root in that case)
|
|
61
|
+
* - repoPath : absolute path to the vertex repo (used as cwd so the validator finds
|
|
62
|
+
* its sibling schemas/ directory)
|
|
63
|
+
* - baseDir : optional. If filePath lives OUTSIDE repoPath (e.g. kushi's
|
|
64
|
+
* .kushi-staging/), pass the staging root here. Translated to the
|
|
65
|
+
* validator's `--base-dir` flag so glob matching works.
|
|
66
|
+
*
|
|
67
|
+
* Throws VertexValidatorError on failure with code:
|
|
68
|
+
* - "python-missing" : no python3 / python on PATH
|
|
69
|
+
* - "vertex-validator-missing" : validator script absent / spawn error
|
|
70
|
+
* - "vertex-schema-fail" : validator exit non-zero (stderr/stdout in details)
|
|
71
|
+
*/
|
|
72
|
+
export function validateFile({ validatorPath, filePath, repoPath, baseDir }) {
|
|
73
|
+
const py = pickPython();
|
|
74
|
+
if (!py) {
|
|
75
|
+
throw new VertexValidatorError("python-missing", "neither python3 nor python found on PATH");
|
|
76
|
+
}
|
|
77
|
+
const args = [validatorPath];
|
|
78
|
+
if (baseDir) {
|
|
79
|
+
args.push("--base-dir", baseDir);
|
|
80
|
+
}
|
|
81
|
+
args.push(filePath);
|
|
82
|
+
const r = spawnSync(py, args, {
|
|
83
|
+
cwd: repoPath,
|
|
84
|
+
encoding: "utf8",
|
|
85
|
+
});
|
|
86
|
+
if (r.error) {
|
|
87
|
+
throw new VertexValidatorError("vertex-validator-missing", r.error.message, { validatorPath });
|
|
88
|
+
}
|
|
89
|
+
if (r.status !== 0) {
|
|
90
|
+
throw new VertexValidatorError(
|
|
91
|
+
"vertex-schema-fail",
|
|
92
|
+
`validator failed for ${filePath}`,
|
|
93
|
+
{ stdout: r.stdout, stderr: r.stderr, status: r.status, filePath }
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
return { ok: true, stdout: r.stdout };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Validate a batch. Returns {ok, passed: [...], failed: [{filePath, details}]}.
|
|
101
|
+
* Does NOT throw — caller decides whether to abort `--apply`.
|
|
102
|
+
* Accepts the same args as validateFile but `files` is an array.
|
|
103
|
+
*/
|
|
104
|
+
export function validateBatch({ validatorPath, files, repoPath, baseDir }) {
|
|
105
|
+
const passed = [];
|
|
106
|
+
const failed = [];
|
|
107
|
+
for (const filePath of files) {
|
|
108
|
+
try {
|
|
109
|
+
validateFile({ validatorPath, filePath, repoPath, baseDir });
|
|
110
|
+
passed.push(filePath);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
failed.push({
|
|
113
|
+
filePath,
|
|
114
|
+
code: err.code,
|
|
115
|
+
message: err.message,
|
|
116
|
+
details: err.details,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return { ok: failed.length === 0, passed, failed };
|
|
121
|
+
}
|
package/plugin/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kushi",
|
|
3
|
-
"description": "Multi-source project evidence + Q&A agent. Snapshot + stream capture across Email, Teams, OneNote, SharePoint, Meetings, CRM, ADO; plus read-only natural-language Q&A over the captured evidence. WorkIQ-only for M365 sources (Graph / m365_* FORBIDDEN as fallbacks; user-paste is first-class). Host-agnostic. Three install profiles: core (aggregator only), standard (default — adds bootstrap/refresh + FDE authoring), full (adds State/ rollup).",
|
|
4
|
-
"version": "3.
|
|
3
|
+
"description": "Multi-source project evidence + Q&A agent. Snapshot + stream capture across Email, Teams, OneNote, Loop, SharePoint, Meetings, CRM, ADO; plus read-only natural-language Q&A over the captured evidence. WorkIQ-only for M365 sources (Graph / m365_* FORBIDDEN as fallbacks; user-paste is first-class). Host-agnostic. Three install profiles: core (aggregator only), standard (default — adds bootstrap/refresh + FDE authoring), full (adds State/ rollup).",
|
|
4
|
+
"version": "3.15.0",
|
|
5
5
|
"author": "ushakrishnan",
|
|
6
6
|
"repository": "https://github.com/gim-home/kushi",
|
|
7
7
|
"default_profile": "standard",
|
|
@@ -16,20 +16,25 @@
|
|
|
16
16
|
"pull-teams",
|
|
17
17
|
"pull-meetings",
|
|
18
18
|
"pull-onenote",
|
|
19
|
+
"pull-loop",
|
|
19
20
|
"pull-sharepoint",
|
|
20
21
|
"pull-crm",
|
|
21
22
|
"pull-ado",
|
|
22
23
|
"aggregate-project",
|
|
23
24
|
"consolidate-evidence",
|
|
24
25
|
"project-status",
|
|
25
|
-
"ask-project"
|
|
26
|
+
"ask-project",
|
|
27
|
+
"vertex-link",
|
|
28
|
+
"emit-vertex"
|
|
26
29
|
],
|
|
27
30
|
"prompts": [
|
|
28
31
|
"setup",
|
|
29
32
|
"aggregate",
|
|
30
33
|
"consolidate",
|
|
31
34
|
"status",
|
|
32
|
-
"ask"
|
|
35
|
+
"ask",
|
|
36
|
+
"vertex-link",
|
|
37
|
+
"emit-vertex"
|
|
33
38
|
],
|
|
34
39
|
"instructions": "*",
|
|
35
40
|
"templates": [
|
|
@@ -43,7 +48,9 @@
|
|
|
43
48
|
"consolidate",
|
|
44
49
|
"status",
|
|
45
50
|
"pull",
|
|
46
|
-
"ask"
|
|
51
|
+
"ask",
|
|
52
|
+
"vertex-link",
|
|
53
|
+
"emit-vertex"
|
|
47
54
|
]
|
|
48
55
|
},
|
|
49
56
|
"standard": {
|
|
@@ -116,4 +123,4 @@
|
|
|
116
123
|
]
|
|
117
124
|
}
|
|
118
125
|
}
|
|
119
|
-
}
|
|
126
|
+
}
|
|
@@ -21,10 +21,12 @@ Inputs the agent will resolve:
|
|
|
21
21
|
- `<project>` — the engagement folder name (fuzzy-match under engagement-root if needed).
|
|
22
22
|
- `<window>` — defaults to 30 days. Override with `last 60 days`, `since 2026-04-01`, or `2026-04-01..2026-05-01`.
|
|
23
23
|
|
|
24
|
-
Reads:
|
|
25
|
-
- `<
|
|
26
|
-
- `<
|
|
27
|
-
- `<
|
|
24
|
+
Reads (paths resolved via `Get-KushiConfig` / `loadKushiConfig` — see `instructions/kushi-config-root.instructions.md`):
|
|
25
|
+
- `<kushi-config-root>/user/project-evidence.yml` for alias + engagement-root.
|
|
26
|
+
- `<kushi-config-root>/user/m365-auth.json` for tenant + default notebook + mailbox-folder hints.
|
|
27
|
+
- `<kushi-config-root>/shared/integrations.yml` for ADO/CRM org-level connections (shared per project).
|
|
28
|
+
|
|
29
|
+
`<kushi-config-root>` is `<workspace>/.kushi/config/` on vscode installs or `~/.copilot/m-skills/kushi/config/` on Clawpilot installs — the helpers pick the right one automatically. **Never** treat a missing `<workspace>/.kushi/` as evidence that Kushi isn't installed; that was the v4.7.1- bug that caused bootstrap to wrongly prompt for install/overwrite.
|
|
28
30
|
|
|
29
31
|
**Step 0 — verify per-user config is filled** (the minimum the bootstrap skill needs before it can discover the rest). Before any pull, call:
|
|
30
32
|
|
|
@@ -38,7 +40,7 @@ Reads:
|
|
|
38
40
|
|
|
39
41
|
If it throws, STOP and prompt the user with the exact missing-field list:
|
|
40
42
|
|
|
41
|
-
> ⚠ `<
|
|
43
|
+
> ⚠ `<kushi-config-root>/user/m365-auth.json` is missing required fields: `<list from the helper>`. Run `Get-KushiConfig -Name 'm365-auth' -Path` to see the resolved path, open it, fill in those values, then re-run `bootstrap`. Everything else (section IDs, calendar series, specific mailbox folder discovery, channel IDs, SharePoint site IDs, CRM `crmRecordId`, ADO work-item IDs) is the bootstrap skill's job to **discover via WorkIQ + per-source probes** — do NOT pre-fill those.
|
|
42
44
|
|
|
43
45
|
Continue only when both helper calls succeed (`project-evidence.yml` is allowed to still have `<auto>` for identity; identity-resolution.instructions.md fills it in Step 0 of the bootstrap skill).
|
|
44
46
|
|
|
@@ -52,8 +54,8 @@ Continue only when both helper calls succeed (`project-evidence.yml` is allowed
|
|
|
52
54
|
On WorkIQ failure during discovery, per `deferred-retry-on-workiq-fail.instructions.md`: the bootstrap skill writes a marker, surfaces it in the run report, and continues. **It does NOT call `m365_get_*` / Graph as a fallback.** The next `refresh` drains the queue.
|
|
53
55
|
|
|
54
56
|
Produces:
|
|
55
|
-
- `<
|
|
56
|
-
- `<
|
|
57
|
+
- `<kushi-config-root>/user/{m365-auth,m365-mutable}.json` (per-user, hand-edited above + auto-populated by Step 4a)
|
|
58
|
+
- `<kushi-config-root>/shared/integrations.yml` (shared, ADO/CRM connection blocks; per-project `boundaries:` lives in the project file below)
|
|
57
59
|
- `<engagement-root>/<project>/integrations.yml` (per-project boundaries + enabled-sources, shared via OneDrive)
|
|
58
60
|
- `<engagement-root>/<project>/Evidence/{contributors.yml, run-log.yml, <alias>/.settings.yml, <alias>/{email,teams,meetings,onenote,sharepoint,crm,ado}/{snapshot,stream}/, <alias>/_deferred-retries/}`
|
|
59
61
|
- `<engagement-root>/<project>/State/{00..09}_*.md` (full profile only)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Render vertex-shaped artifacts from kushi evidence — weekly status update, decisions, workshops, comms, and PR-style living-doc proposals. Stages first, validates against vertex's own schemas, applies on demand."
|
|
3
|
+
mode: "agent"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# emit-vertex prompt
|
|
7
|
+
|
|
8
|
+
Use this prompt to ask kushi to produce vertex-shaped artifacts for a project that has already been linked via `vertex-link`.
|
|
9
|
+
|
|
10
|
+
## Examples
|
|
11
|
+
|
|
12
|
+
```text
|
|
13
|
+
@Kushi emit vertex weekly for acme-platform-mod
|
|
14
|
+
@Kushi emit vertex decisions for acme-platform-mod
|
|
15
|
+
@Kushi emit vertex --all for acme-platform-mod --apply
|
|
16
|
+
@Kushi update vertex for acme-platform-mod # interactive — pick modes
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## What it does
|
|
20
|
+
|
|
21
|
+
Reads `<project>/Evidence/<alias>/` (across all contributors) plus `<project>/State/*.md` for the linked vertex initiative, renders the requested artifacts, stages them under `<project>/.kushi-staging/vertex/<run-ts>/`, validates each against vertex's `.vertex/scripts/validation/validate_frontmatter.py` in place, and (with `--apply`) copies into the vertex repo for GitDoc to commit.
|
|
22
|
+
|
|
23
|
+
Living docs (`customer-details.md`, `risk-register.md`, `architecture-overview.md`, …) are NEVER overwritten — they're written as `.diff` + `.proposal.md` companions in the staging folder for the user to apply by hand.
|
|
24
|
+
|
|
25
|
+
## Pre-requisites
|
|
26
|
+
|
|
27
|
+
- `<project>/kushi.yaml#vertex` populated (run `vertex-link` once if not).
|
|
28
|
+
- Python 3 on PATH.
|
|
29
|
+
- The vertex repo accessible at the configured `repo_path` with `.vertex/scripts/validation/validate_frontmatter.py` present.
|
|
30
|
+
|
|
31
|
+
## Doctrine
|
|
32
|
+
|
|
33
|
+
Bound to skill `emit-vertex`. Full rules in [`vertex-emit.instructions.md`](../instructions/vertex-emit.instructions.md).
|
|
@@ -20,7 +20,7 @@ Runs the **setup** skill:
|
|
|
20
20
|
|
|
21
21
|
1. Resolves the workiq CLI path (pinned `cli_path` → PATH → Clawpilot-managed `~/.copilot/bin/workiq` fallback). **No `~/.kushi/bin/` filesystem probe** (removed in v4.4.4).
|
|
22
22
|
2. Functionally verifies WorkIQ by sending: *"Who am I? Return UPN, displayName, mailNickname as JSON."*
|
|
23
|
-
3. Parses the response and writes `identity` (alias / display_name / email) into `<
|
|
23
|
+
3. Parses the response and writes `identity` (alias / display_name / email) into `<kushi-config-root>/user/project-evidence.yml` (resolved via `Get-KushiConfig -Name 'project-evidence' -Path`; see `instructions/kushi-config-root.instructions.md`), preserving comments.
|
|
24
24
|
4. On failure, walks the user through install / sign-in / retry using `ask_user` (no outbound sends).
|
|
25
25
|
5. Prints a green status table summarizing host, cli_path, identity, and persistence target.
|
|
26
26
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "One-time link of a kushi project to a vertex repo's customer + initiative directories. Populates kushi.yaml#vertex so emit-vertex can render into the right place."
|
|
3
|
+
mode: "agent"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# vertex-link prompt
|
|
7
|
+
|
|
8
|
+
Use this once per kushi project to tell kushi which vertex repo + `<customer-slug>/<initiative-slug>/` it maps to.
|
|
9
|
+
|
|
10
|
+
## Examples
|
|
11
|
+
|
|
12
|
+
```text
|
|
13
|
+
@Kushi link vertex for acme-platform-mod
|
|
14
|
+
@Kushi link vertex for acme-platform-mod to ~/repos/vertex-acme
|
|
15
|
+
@Kushi link vertex for acme-platform-mod --reconfigure
|
|
16
|
+
@Kushi vertex link
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## What it does
|
|
20
|
+
|
|
21
|
+
Locates the vertex repo (fuzzy-discovers from common locations or accepts a path), fuzzy-matches the kushi project name against existing `<customer>/<initiative>/` directories in vertex, asks the user to confirm the match, optionally sets the studio identifier, and writes the mapping into `<project>/kushi.yaml`.
|
|
22
|
+
|
|
23
|
+
Idempotent — running again without `--reconfigure` just prints the current mapping.
|
|
24
|
+
|
|
25
|
+
## Doctrine
|
|
26
|
+
|
|
27
|
+
Bound to skill `vertex-link`. Full rules in the [`vertex-link/SKILL.md`](../skills/vertex-link/SKILL.md).
|