kushi-agents 5.2.0 → 5.4.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.
- package/README.md +24 -0
- package/bin/cli.mjs +112 -1
- package/package.json +2 -2
- package/plugin/agents/kushi.agent.md +194 -191
- package/plugin/instructions/global-wiki.instructions.md +79 -0
- package/plugin/instructions/multi-wiki-routing.instructions.md +117 -0
- package/plugin/instructions/release-genealogy.instructions.md +52 -52
- package/plugin/instructions/system-health.instructions.md +51 -0
- package/plugin/skills/ask-project/SKILL.md +14 -0
- package/plugin/skills/doctor/SKILL.md +72 -0
- package/plugin/skills/doctor/doctor.ps1 +260 -0
- package/plugin/skills/doctor/evals/evals.json +28 -0
- package/plugin/skills/global-wiki/.created-by-skill-creator +1 -0
- package/plugin/skills/global-wiki/SKILL.md +87 -0
- package/plugin/skills/global-wiki/evals/evals.json +43 -0
- package/plugin/skills/promote/.created-by-skill-creator +1 -0
- package/plugin/skills/promote/SKILL.md +125 -0
- package/plugin/skills/promote/evals/evals.json +35 -0
- package/plugin/skills/self-check/SKILL.md +5 -1
- package/plugin/skills/self-check/run.ps1 +178 -14
- package/plugin/skills/teach/SKILL.md +2 -0
- package/plugin/skills/teach/evals/evals.json +22 -0
- package/src/cli-no-args.test.mjs +30 -0
- package/src/doctor.test.mjs +93 -0
- package/src/global-wiki-cli.mjs +158 -0
- package/src/global-wiki.mjs +503 -0
- package/src/global-wiki.test.mjs +135 -0
- package/src/promote.test.mjs +161 -0
- package/src/setup-wizard.mjs +133 -0
- package/src/setup-wizard.test.mjs +74 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// kushi v5.4.0 — doctor end-to-end tests.
|
|
2
|
+
// Runs doctor.ps1 against the live repo (this code IS the repo). Verifies:
|
|
3
|
+
// (1) ends with exit 0 on a clean repo
|
|
4
|
+
// (2) --json mode emits valid JSON with sections + summary
|
|
5
|
+
// (3) JSON shape carries section names, statuses, and fixes
|
|
6
|
+
// (4) doctor SKILL.md + doctrine + evals.json all ship
|
|
7
|
+
|
|
8
|
+
import test from 'node:test';
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import os from 'node:os';
|
|
13
|
+
import { spawnSync } from 'node:child_process';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
|
|
16
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
17
|
+
const doctorPs1 = path.join(repoRoot, 'plugin', 'skills', 'doctor', 'doctor.ps1');
|
|
18
|
+
const TESTTMP = path.join(repoRoot, '.testtmp');
|
|
19
|
+
if (!fs.existsSync(TESTTMP)) fs.mkdirSync(TESTTMP, { recursive: true });
|
|
20
|
+
|
|
21
|
+
function runDoctor(extraArgs = []) {
|
|
22
|
+
const args = ['-NoProfile', '-File', doctorPs1, '-Repo', repoRoot, ...extraArgs];
|
|
23
|
+
const r = spawnSync('pwsh', args, { encoding: 'utf-8', timeout: 180_000 });
|
|
24
|
+
return { stdout: r.stdout || '', stderr: r.stderr || '', status: r.status ?? 1 };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
test('doctor: SKILL.md + doctor.ps1 + evals.json all ship', () => {
|
|
28
|
+
assert.ok(fs.existsSync(doctorPs1), 'doctor.ps1 exists');
|
|
29
|
+
assert.ok(fs.existsSync(path.join(repoRoot, 'plugin', 'skills', 'doctor', 'SKILL.md')), 'SKILL.md exists');
|
|
30
|
+
assert.ok(fs.existsSync(path.join(repoRoot, 'plugin', 'skills', 'doctor', 'evals', 'evals.json')), 'evals.json exists');
|
|
31
|
+
assert.ok(fs.existsSync(path.join(repoRoot, 'plugin', 'instructions', 'system-health.instructions.md')), 'doctrine exists');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('doctor: runs end-to-end against the repo and exits 0', () => {
|
|
35
|
+
const r = runDoctor();
|
|
36
|
+
// Doctor may exit 0 (green) or 1 (red) depending on environment; we only
|
|
37
|
+
// require that it actually executed every section banner.
|
|
38
|
+
assert.ok(/1\. Environment/i.test(r.stdout), `section 1 banner present\n${r.stdout}\n${r.stderr}`);
|
|
39
|
+
assert.ok(/2\. self-check/i.test(r.stdout), 'section 2 banner present');
|
|
40
|
+
assert.ok(/3\. canary evals/i.test(r.stdout), 'section 3 banner present');
|
|
41
|
+
assert.ok(/4\. skill-checker/i.test(r.stdout), 'section 4 banner present');
|
|
42
|
+
assert.ok(/5\. live-install drift/i.test(r.stdout), 'section 5 banner present');
|
|
43
|
+
assert.ok(/6\. global wiki shape/i.test(r.stdout), 'section 6 banner present');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('doctor: --json mode produces valid JSON with sections + summary', () => {
|
|
47
|
+
const r = runDoctor(['-Json']);
|
|
48
|
+
// Find the JSON payload (PowerShell may prepend write-host banners if not suppressed,
|
|
49
|
+
// but -Json mode in our script avoids them — locate the first { and parse from there).
|
|
50
|
+
const start = r.stdout.indexOf('{');
|
|
51
|
+
assert.ok(start >= 0, `JSON payload found in stdout:\n${r.stdout}\n${r.stderr}`);
|
|
52
|
+
const payload = r.stdout.slice(start);
|
|
53
|
+
let parsed;
|
|
54
|
+
assert.doesNotThrow(() => { parsed = JSON.parse(payload); }, 'json parses');
|
|
55
|
+
assert.ok(Array.isArray(parsed.sections), 'sections is an array');
|
|
56
|
+
assert.ok(parsed.summary, 'summary present');
|
|
57
|
+
assert.equal(typeof parsed.summary.green, 'number', 'green count is number');
|
|
58
|
+
assert.equal(typeof parsed.summary.yellow, 'number', 'yellow count is number');
|
|
59
|
+
assert.equal(typeof parsed.summary.red, 'number', 'red count is number');
|
|
60
|
+
// Each section must have name + status.
|
|
61
|
+
for (const s of parsed.sections) {
|
|
62
|
+
assert.ok(typeof s.name === 'string' && s.name.length > 0, 'section has name');
|
|
63
|
+
assert.ok(['green','yellow','red'].includes(s.status), `valid status: ${s.status}`);
|
|
64
|
+
}
|
|
65
|
+
// All six core probes must appear.
|
|
66
|
+
const names = parsed.sections.map((s) => s.name);
|
|
67
|
+
for (const expected of ['environment','self-check','canary-evals','skill-checker','live-install','global-wiki']) {
|
|
68
|
+
assert.ok(names.includes(expected), `section "${expected}" present (got: ${names.join(', ')})`);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('doctor: exit code is non-zero when a red section is present (synthetic)', () => {
|
|
73
|
+
// We synthesize a "red" condition by pointing doctor at a non-existent repo
|
|
74
|
+
// — self-check/run.ps1 won't be found, which produces a red section.
|
|
75
|
+
const fakeRepo = path.join(TESTTMP, `doctor-fake-${Date.now()}`);
|
|
76
|
+
fs.mkdirSync(fakeRepo, { recursive: true });
|
|
77
|
+
try {
|
|
78
|
+
// Minimal package.json so doctor can read a version.
|
|
79
|
+
fs.writeFileSync(path.join(fakeRepo, 'package.json'), JSON.stringify({ name: 'fake', version: '0.0.1' }));
|
|
80
|
+
const args = ['-NoProfile', '-File', doctorPs1, '-Repo', fakeRepo, '-Json'];
|
|
81
|
+
const r = spawnSync('pwsh', args, { encoding: 'utf-8', timeout: 60_000 });
|
|
82
|
+
// Find the JSON payload.
|
|
83
|
+
const start = (r.stdout || '').indexOf('{');
|
|
84
|
+
if (start >= 0) {
|
|
85
|
+
const parsed = JSON.parse(r.stdout.slice(start));
|
|
86
|
+
// We expect at least one red section (self-check + skill-checker both missing).
|
|
87
|
+
assert.ok(parsed.summary.red >= 1, `expected >=1 red section, got: ${JSON.stringify(parsed.summary)}`);
|
|
88
|
+
}
|
|
89
|
+
assert.notEqual(r.status, 0, 'doctor must exit non-zero on red');
|
|
90
|
+
} finally {
|
|
91
|
+
fs.rmSync(fakeRepo, { recursive: true, force: true });
|
|
92
|
+
}
|
|
93
|
+
});
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// kushi v5.3.0 — CLI dispatch helpers for global wiki + promote.
|
|
2
|
+
// These wrap the pure functions in global-wiki.mjs with console I/O,
|
|
3
|
+
// project-root resolution, and exit-code handling.
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { globalInit, globalStatus, globalAsk, globalLint, promote, resolveGlobalRoot } from './global-wiki.mjs';
|
|
8
|
+
|
|
9
|
+
export async function runGlobalInit() {
|
|
10
|
+
const result = globalInit();
|
|
11
|
+
console.log('');
|
|
12
|
+
console.log(` Global wiki root: ${result.root}`);
|
|
13
|
+
console.log(` State dir : ${result.state}`);
|
|
14
|
+
if (result.created.length) {
|
|
15
|
+
console.log(` Created (${result.created.length}): ${result.created.join(', ')}`);
|
|
16
|
+
}
|
|
17
|
+
if (result.skipped.length) {
|
|
18
|
+
console.log(` Skipped (${result.skipped.length} already present)`);
|
|
19
|
+
}
|
|
20
|
+
console.log('');
|
|
21
|
+
console.log(" Next: 'kushi global status' or 'kushi promote <project> <page>'");
|
|
22
|
+
console.log('');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function runGlobalStatus() {
|
|
26
|
+
const status = globalStatus();
|
|
27
|
+
console.log('');
|
|
28
|
+
console.log(` Global wiki root: ${status.root}`);
|
|
29
|
+
if (!status.initialized) {
|
|
30
|
+
console.log(' Status: not initialized. Run `kushi global init`.');
|
|
31
|
+
console.log('');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
console.log(` Pages : ${status.counts.pages}`);
|
|
35
|
+
console.log(` Answers : ${status.counts.answers}`);
|
|
36
|
+
console.log(` Reports : ${status.counts.reports}`);
|
|
37
|
+
console.log(` Open review : ${status.counts.review_items}`);
|
|
38
|
+
console.log(` Newest mtime: ${status.newest_iso || '(none)'}`);
|
|
39
|
+
console.log('');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function runGlobalAsk(question) {
|
|
43
|
+
if (!question || !question.trim()) {
|
|
44
|
+
console.error('\n Usage: kushi global ask <question>\n');
|
|
45
|
+
process.exitCode = 1;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const result = globalAsk({ question });
|
|
49
|
+
console.log('');
|
|
50
|
+
console.log(` Question: ${question}`);
|
|
51
|
+
console.log(` Result : ${result.message}`);
|
|
52
|
+
if (result.hits.length) {
|
|
53
|
+
console.log('');
|
|
54
|
+
console.log(' Hits:');
|
|
55
|
+
for (const h of result.hits) {
|
|
56
|
+
console.log(` ${h.provenance} ${h.name} (score=${h.score})`);
|
|
57
|
+
console.log(` file: ${h.file}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
console.log('');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function runGlobalLint() {
|
|
64
|
+
const result = globalLint();
|
|
65
|
+
console.log('');
|
|
66
|
+
console.log(` Global wiki lint @ ${resolveGlobalRoot()}`);
|
|
67
|
+
if (!result.initialized) {
|
|
68
|
+
console.log(' Status: not initialized. Run `kushi global init`.');
|
|
69
|
+
console.log('');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (result.findings.length === 0) {
|
|
73
|
+
console.log(' ✅ No findings.');
|
|
74
|
+
console.log('');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
console.log(` ⚠️ ${result.findings.length} finding(s):`);
|
|
78
|
+
for (const f of result.findings) {
|
|
79
|
+
console.log(` [${f.class}] ${f.file}:${f.line} — ${f.snippet}`);
|
|
80
|
+
console.log(` fix: ${f.fix}`);
|
|
81
|
+
}
|
|
82
|
+
console.log('');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function runPromote(project, pagePath, { force = false } = {}) {
|
|
86
|
+
const projectRoot = resolveProjectRoot(project);
|
|
87
|
+
if (!projectRoot) {
|
|
88
|
+
console.error(`\n Could not resolve project '${project}' relative to ${process.cwd()}.\n`);
|
|
89
|
+
process.exitCode = 1;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const fullSource = resolveSourcePage(projectRoot, pagePath);
|
|
93
|
+
if (!fullSource) {
|
|
94
|
+
console.error(`\n Could not find page '${pagePath}' under ${projectRoot}.\n`);
|
|
95
|
+
process.exitCode = 1;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const result = promote({
|
|
100
|
+
project,
|
|
101
|
+
sourcePath: fullSource,
|
|
102
|
+
projectRoot,
|
|
103
|
+
force,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (result.refused) {
|
|
107
|
+
console.error('');
|
|
108
|
+
console.error(` ❌ promote refused — ${result.detections.length} identifier hit(s):`);
|
|
109
|
+
for (const d of result.detections) {
|
|
110
|
+
console.error(` [${d.kind}] "${d.pattern}" × ${d.count} (lines: ${d.line_numbers.join(', ')})`);
|
|
111
|
+
}
|
|
112
|
+
console.error('');
|
|
113
|
+
console.error(' Review the hits, then re-run with --force to redact and write.');
|
|
114
|
+
console.error('');
|
|
115
|
+
process.exitCode = 2;
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
console.log('');
|
|
120
|
+
console.log(' ✅ Promoted:');
|
|
121
|
+
console.log(` source : ${result.source}`);
|
|
122
|
+
console.log(` target : ${result.target}`);
|
|
123
|
+
console.log(` slug : ${result.slug}`);
|
|
124
|
+
console.log(` redacts: ${result.redactions.length} (${result.redactions.join(', ') || 'none'})`);
|
|
125
|
+
console.log(` at : ${result.promoted_at}`);
|
|
126
|
+
console.log('');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function resolveProjectRoot(project) {
|
|
130
|
+
const cwd = process.cwd();
|
|
131
|
+
const candidates = [
|
|
132
|
+
path.resolve(cwd, project),
|
|
133
|
+
path.resolve(cwd, '..', project),
|
|
134
|
+
];
|
|
135
|
+
for (const c of candidates) {
|
|
136
|
+
if (fs.existsSync(c)) return c;
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function resolveSourcePage(projectRoot, pagePath) {
|
|
142
|
+
// Allow either an absolute-from-project path or a State-relative path.
|
|
143
|
+
const direct = path.isAbsolute(pagePath) ? pagePath : path.join(projectRoot, pagePath);
|
|
144
|
+
if (fs.existsSync(direct)) return direct;
|
|
145
|
+
|
|
146
|
+
// Try to locate under Evidence/*/State/.
|
|
147
|
+
const evidence = path.join(projectRoot, 'Evidence');
|
|
148
|
+
if (fs.existsSync(evidence)) {
|
|
149
|
+
for (const alias of fs.readdirSync(evidence)) {
|
|
150
|
+
const cand = path.join(evidence, alias, 'State', pagePath);
|
|
151
|
+
if (fs.existsSync(cand)) return cand;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// Plain <project>/State/.
|
|
155
|
+
const plain = path.join(projectRoot, 'State', pagePath);
|
|
156
|
+
if (fs.existsSync(plain)) return plain;
|
|
157
|
+
return null;
|
|
158
|
+
}
|