kushi-agents 5.2.0 → 5.3.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 +50 -1
- package/package.json +2 -2
- package/plugin/agents/kushi.agent.md +2 -0
- package/plugin/instructions/global-wiki.instructions.md +79 -0
- package/plugin/instructions/multi-wiki-routing.instructions.md +117 -0
- package/plugin/skills/ask-project/SKILL.md +14 -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 +4 -1
- package/plugin/skills/self-check/run.ps1 +63 -0
- package/plugin/skills/teach/SKILL.md +2 -0
- package/plugin/skills/teach/evals/evals.json +22 -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
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// kushi v5.3.0 — promote operation tests.
|
|
2
|
+
// MUST use $env:KUSHI_GLOBAL_ROOT under .testtmp/ — never the real ~/.kushi-global/.
|
|
3
|
+
|
|
4
|
+
import test from 'node:test';
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
|
|
10
|
+
import { detectIdentifiers, promote, globalInit } from './global-wiki.mjs';
|
|
11
|
+
|
|
12
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
13
|
+
const TESTTMP = path.join(repoRoot, '.testtmp');
|
|
14
|
+
|
|
15
|
+
function makeFixtureProject(label, body) {
|
|
16
|
+
const root = path.join(TESTTMP, `promote-${label}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
|
|
17
|
+
const evidence = path.join(root, 'projectroot', 'Evidence', 'acme', 'State', 'answers');
|
|
18
|
+
fs.mkdirSync(evidence, { recursive: true });
|
|
19
|
+
const page = path.join(evidence, '2026-05-27_confidence-ladder.md');
|
|
20
|
+
fs.writeFileSync(page, body);
|
|
21
|
+
return {
|
|
22
|
+
tmpRoot: root,
|
|
23
|
+
projectRoot: path.join(root, 'projectroot'),
|
|
24
|
+
globalRoot: path.join(root, 'global'),
|
|
25
|
+
sourcePath: page,
|
|
26
|
+
relSource: path.relative(path.join(root, 'projectroot'), page),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function rmrf(p) {
|
|
31
|
+
if (fs.existsSync(p)) fs.rmSync(p, { recursive: true, force: true });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
test('promote: detectIdentifiers flags project name, alias, and external email', () => {
|
|
35
|
+
const body = [
|
|
36
|
+
'# notes',
|
|
37
|
+
'Acme is shipping in Q3 with help from lead@acme.com.',
|
|
38
|
+
'acme stakeholder pinged us; also internal kushikd@microsoft.com (ok).',
|
|
39
|
+
'Re-pinged Acme on Friday.',
|
|
40
|
+
].join('\n');
|
|
41
|
+
|
|
42
|
+
const hits = detectIdentifiers(body, { project: 'acme', alias: 'acme' });
|
|
43
|
+
const literal = hits.find((h) => h.kind === 'project-or-alias');
|
|
44
|
+
assert.ok(literal, 'must flag project literal');
|
|
45
|
+
assert.ok(literal.count >= 3, `expected ≥3 acme hits, got ${literal.count}`);
|
|
46
|
+
|
|
47
|
+
const emails = hits.filter((h) => h.kind === 'external-email');
|
|
48
|
+
assert.equal(emails.length, 1, 'must flag exactly the external email');
|
|
49
|
+
assert.equal(emails[0].pattern, 'lead@acme.com');
|
|
50
|
+
|
|
51
|
+
const internalLeak = hits.find((h) => h.pattern && h.pattern.endsWith('@microsoft.com'));
|
|
52
|
+
assert.equal(internalLeak, undefined, '@microsoft.com must NOT be flagged');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('promote: refuses without --force when identifiers are detected', () => {
|
|
56
|
+
const fx = makeFixtureProject(
|
|
57
|
+
'refuse',
|
|
58
|
+
'---\nkushi_state_page: true\n---\n\n# Acme confidence ladder\n\nAcme uses strong/weak/hypothesis tiers. Contact lead@acme.com.\n',
|
|
59
|
+
);
|
|
60
|
+
try {
|
|
61
|
+
globalInit({ root: fx.globalRoot });
|
|
62
|
+
const r = promote({
|
|
63
|
+
project: 'acme',
|
|
64
|
+
sourcePath: fx.sourcePath,
|
|
65
|
+
projectRoot: fx.projectRoot,
|
|
66
|
+
globalRoot: fx.globalRoot,
|
|
67
|
+
});
|
|
68
|
+
assert.equal(r.ok, false, 'must refuse');
|
|
69
|
+
assert.equal(r.refused, true);
|
|
70
|
+
assert.ok(r.detections.length >= 1, 'must surface detections');
|
|
71
|
+
// No target file written under answers/.
|
|
72
|
+
const answers = path.join(fx.globalRoot, 'State', 'answers');
|
|
73
|
+
const files = fs.existsSync(answers) ? fs.readdirSync(answers) : [];
|
|
74
|
+
assert.equal(files.length, 0, 'refusal must leave global answers/ empty');
|
|
75
|
+
} finally {
|
|
76
|
+
rmrf(fx.tmpRoot);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('promote: --force writes redacted target, privacy callout, back-link, and dual log', () => {
|
|
81
|
+
const fx = makeFixtureProject(
|
|
82
|
+
'force',
|
|
83
|
+
'---\nkushi_state_page: true\n---\n\n# Acme confidence ladder\n\nAcme uses strong/weak/hypothesis tiers. Contact lead@acme.com.\n',
|
|
84
|
+
);
|
|
85
|
+
try {
|
|
86
|
+
globalInit({ root: fx.globalRoot });
|
|
87
|
+
const r = promote({
|
|
88
|
+
project: 'acme',
|
|
89
|
+
sourcePath: fx.sourcePath,
|
|
90
|
+
projectRoot: fx.projectRoot,
|
|
91
|
+
globalRoot: fx.globalRoot,
|
|
92
|
+
force: true,
|
|
93
|
+
now: new Date('2026-05-27T12:00:00Z'),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
assert.equal(r.ok, true, 'forced promote must succeed');
|
|
97
|
+
assert.ok(r.target && fs.existsSync(r.target), 'target file must exist');
|
|
98
|
+
assert.ok(r.redactions.length >= 2, `expected ≥2 redactions, got ${r.redactions.length}`);
|
|
99
|
+
|
|
100
|
+
const targetBody = fs.readFileSync(r.target, 'utf8');
|
|
101
|
+
assert.match(targetBody, /^---\n/, 'target must start with frontmatter');
|
|
102
|
+
assert.match(targetBody, /scope: global/, 'target frontmatter must mark scope: global');
|
|
103
|
+
assert.match(targetBody, /promoted_from: "acme\//, 'target must record promoted_from');
|
|
104
|
+
assert.match(targetBody, /promoted_at:/, 'target must record promoted_at');
|
|
105
|
+
assert.match(targetBody, /\[REDACTED\]/, 'target body must contain [REDACTED]');
|
|
106
|
+
// Strip frontmatter AND the privacy callout block (which legitimately echoes the redacted labels) before scanning prose.
|
|
107
|
+
const prose = targetBody
|
|
108
|
+
.replace(/^---\n[\s\S]*?\n---\n/, '')
|
|
109
|
+
.replace(/> \[!warning\] potential-customer-leak[\s\S]*$/m, '');
|
|
110
|
+
assert.doesNotMatch(prose, /lead@acme\.com/, 'external email must be redacted in prose');
|
|
111
|
+
assert.doesNotMatch(prose, /\bAcme\b/i, 'project literal must be redacted in prose');
|
|
112
|
+
assert.match(targetBody, /> \[!warning\] potential-customer-leak/, 'target must carry privacy callout');
|
|
113
|
+
|
|
114
|
+
// Source must gain back-link callout.
|
|
115
|
+
const sourceBody = fs.readFileSync(fx.sourcePath, 'utf8');
|
|
116
|
+
assert.match(sourceBody, /> \[!info\] Promoted to global wiki/, 'source must carry back-link callout');
|
|
117
|
+
assert.match(sourceBody, /answers\/[a-z0-9-]+\.md/, 'back-link must reference the global slug');
|
|
118
|
+
|
|
119
|
+
// Dual log: both project State/log.md and global State/log.md updated.
|
|
120
|
+
// Project log lives at <projectRoot>/Evidence/acme/State/log.md
|
|
121
|
+
const projLog = path.join(fx.projectRoot, 'Evidence', 'acme', 'State', 'log.md');
|
|
122
|
+
assert.ok(fs.existsSync(projLog), 'project log must exist after promote');
|
|
123
|
+
assert.match(fs.readFileSync(projLog, 'utf8'), /promote\b/, 'project log must record promote op');
|
|
124
|
+
|
|
125
|
+
const globalLog = path.join(fx.globalRoot, 'State', 'log.md');
|
|
126
|
+
assert.match(fs.readFileSync(globalLog, 'utf8'), /promote-in/, 'global log must record promote-in op');
|
|
127
|
+
} finally {
|
|
128
|
+
rmrf(fx.tmpRoot);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('promote: clean page (no identifiers) promotes without --force and emits no privacy callout', () => {
|
|
133
|
+
const fx = makeFixtureProject(
|
|
134
|
+
'clean',
|
|
135
|
+
'---\nkushi_state_page: true\n---\n\n# Confidence ladder pattern\n\nUse strong / weak / hypothesis tiers when reporting findings. Contact teammate@microsoft.com.\n',
|
|
136
|
+
);
|
|
137
|
+
try {
|
|
138
|
+
globalInit({ root: fx.globalRoot });
|
|
139
|
+
const r = promote({
|
|
140
|
+
project: 'acme',
|
|
141
|
+
sourcePath: fx.sourcePath,
|
|
142
|
+
projectRoot: fx.projectRoot,
|
|
143
|
+
globalRoot: fx.globalRoot,
|
|
144
|
+
// NOTE: source path lives under Evidence/acme/, so alias guess = 'acme'.
|
|
145
|
+
// Body never mentions 'acme' or 'Acme' literally, and @microsoft.com is allowed.
|
|
146
|
+
});
|
|
147
|
+
assert.equal(r.ok, true, 'clean page must promote without --force');
|
|
148
|
+
assert.equal(r.refused, false);
|
|
149
|
+
assert.equal(r.redactions.length, 0, 'clean page must produce zero redactions');
|
|
150
|
+
|
|
151
|
+
const targetBody = fs.readFileSync(r.target, 'utf8');
|
|
152
|
+
assert.doesNotMatch(
|
|
153
|
+
targetBody,
|
|
154
|
+
/> \[!warning\] potential-customer-leak/,
|
|
155
|
+
'clean promote must NOT emit a privacy callout',
|
|
156
|
+
);
|
|
157
|
+
assert.match(targetBody, /redactions: \[\]/, 'frontmatter must record empty redactions array');
|
|
158
|
+
} finally {
|
|
159
|
+
rmrf(fx.tmpRoot);
|
|
160
|
+
}
|
|
161
|
+
});
|