@synoikia/create-plugin 0.7.0 → 0.8.1
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/LICENSE +1 -1
- package/README.md +3 -14
- package/dist/cli.js +0 -4
- package/dist/cli.js.map +1 -1
- package/dist/repo.d.ts +1 -1
- package/dist/scaffold.js +1 -6
- package/dist/scaffold.js.map +1 -1
- package/package.json +4 -4
- package/templates/repo/base/README.md +3 -3
- package/templates/repo/base/_github/workflows/ci.yml +12 -0
- package/templates/repo/base/_gitignore +1 -0
- package/templates/repo/base/package.json +2 -28
- package/templates/repo/claude/CLAUDE.md +3 -39
- package/templates/repo/claude/_claude/guard-paths.json +17 -0
- package/templates/repo/claude/_claude/hooks/check-plugins.mjs +39 -0
- package/templates/repo/claude/_claude/hooks/session-start.sh +8 -9
- package/templates/repo/claude/_claude/settings.json +17 -17
- package/dist/workflow/branch.d.ts +0 -2
- package/dist/workflow/branch.js +0 -31
- package/dist/workflow/branch.js.map +0 -1
- package/dist/workflow/config.d.ts +0 -24
- package/dist/workflow/config.js +0 -25
- package/dist/workflow/config.js.map +0 -1
- package/dist/workflow/features.d.ts +0 -20
- package/dist/workflow/features.js +0 -165
- package/dist/workflow/features.js.map +0 -1
- package/dist/workflow/git.d.ts +0 -12
- package/dist/workflow/git.js +0 -42
- package/dist/workflow/git.js.map +0 -1
- package/dist/workflow/glob.d.ts +0 -3
- package/dist/workflow/glob.js +0 -21
- package/dist/workflow/glob.js.map +0 -1
- package/dist/workflow/index.d.ts +0 -2
- package/dist/workflow/index.js +0 -162
- package/dist/workflow/index.js.map +0 -1
- package/dist/workflow/phases.d.ts +0 -22
- package/dist/workflow/phases.js +0 -50
- package/dist/workflow/phases.js.map +0 -1
- package/dist/workflow/prose.d.ts +0 -2
- package/dist/workflow/prose.js +0 -113
- package/dist/workflow/prose.js.map +0 -1
- package/dist/workflow/red.d.ts +0 -2
- package/dist/workflow/red.js +0 -36
- package/dist/workflow/red.js.map +0 -1
- package/dist/workflow/secrets.d.ts +0 -2
- package/dist/workflow/secrets.js +0 -5
- package/dist/workflow/secrets.js.map +0 -1
- package/dist/workflow/security-line.d.ts +0 -6
- package/dist/workflow/security-line.js +0 -14
- package/dist/workflow/security-line.js.map +0 -1
- package/dist/workflow/sync.d.ts +0 -4
- package/dist/workflow/sync.js +0 -47
- package/dist/workflow/sync.js.map +0 -1
- package/dist/workflow/tests.d.ts +0 -2
- package/dist/workflow/tests.js +0 -6
- package/dist/workflow/tests.js.map +0 -1
- package/templates/repo/claude/_claude/agents/security-reviewer.md +0 -70
- package/templates/repo/claude/_claude/hooks/format.mjs +0 -20
- package/templates/repo/claude/_claude/hooks/guard-paths.mjs +0 -43
- package/templates/repo/claude/_claude/skills/add-plugin/SKILL.md +0 -46
- package/templates/repo/claude/_claude/skills/plugin-development/SKILL.md +0 -64
- package/templates/repo/claude/_claude/skills/plugin-development/references/testing.md +0 -60
- package/templates/repo/claude/_claude/skills/plugin-development/references/typescript.md +0 -31
- package/templates/repo/claude/_claude/skills/release-plugin/SKILL.md +0 -42
- package/templates/workflow/base/_githooks/pre-commit +0 -3
- package/templates/workflow/base/_githooks/pre-push +0 -4
- package/templates/workflow/base/_github/workflows/workflow.yml +0 -45
- package/templates/workflow/base/docs/workflow.md +0 -100
- package/templates/workflow/claude/_claude/hooks/guard-branch.mjs +0 -73
- package/templates/workflow/claude/_claude/skills/architect/SKILL.md +0 -44
- package/templates/workflow/claude/_claude/skills/change-workflow/SKILL.md +0 -70
- package/templates/workflow/claude/_claude/skills/developer/SKILL.md +0 -57
- package/templates/workflow/claude/_claude/skills/requirements-engineer/SKILL.md +0 -59
- package/templates/workflow/claude/_claude/skills/technical-writing/SKILL.md +0 -35
package/dist/workflow/prose.js
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
const MAX_WORDS = 25;
|
|
2
|
-
/** Inline code and link targets are not prose. */
|
|
3
|
-
const stripInline = (text) => text
|
|
4
|
-
.replace(/`[^`]*`/g, 'Code')
|
|
5
|
-
.replace(/\]\([^)]*\)/g, ']')
|
|
6
|
-
.replace(/<!--.*?-->/g, '')
|
|
7
|
-
.replace(/https?:\/\/\S+/g, 'link');
|
|
8
|
-
function markdownBlocks(text) {
|
|
9
|
-
const lines = text.split('\n');
|
|
10
|
-
const blocks = [];
|
|
11
|
-
let fence;
|
|
12
|
-
let comment = false;
|
|
13
|
-
let para;
|
|
14
|
-
const flush = () => {
|
|
15
|
-
if (para)
|
|
16
|
-
blocks.push(para);
|
|
17
|
-
para = undefined;
|
|
18
|
-
};
|
|
19
|
-
let i = 0;
|
|
20
|
-
if (lines[0] === '---') {
|
|
21
|
-
const end = lines.indexOf('---', 1);
|
|
22
|
-
if (end > 0)
|
|
23
|
-
i = end + 1;
|
|
24
|
-
}
|
|
25
|
-
for (; i < lines.length; i++) {
|
|
26
|
-
const raw = lines[i];
|
|
27
|
-
const line = i + 1;
|
|
28
|
-
const fenceMark = raw.match(/^\s*(`{3,}|~{3,})/)?.[1];
|
|
29
|
-
if (fence) {
|
|
30
|
-
if (fenceMark?.startsWith(fence))
|
|
31
|
-
fence = undefined;
|
|
32
|
-
continue;
|
|
33
|
-
}
|
|
34
|
-
if (fenceMark) {
|
|
35
|
-
flush();
|
|
36
|
-
fence = fenceMark;
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
39
|
-
if (comment || raw.trim().startsWith('<!--')) {
|
|
40
|
-
comment = !raw.includes('-->');
|
|
41
|
-
continue;
|
|
42
|
-
}
|
|
43
|
-
if (!raw.trim() || (/^\s{4,}\S/.test(raw) && !para)) {
|
|
44
|
-
flush();
|
|
45
|
-
continue;
|
|
46
|
-
}
|
|
47
|
-
if (/^\s*\|/.test(raw)) {
|
|
48
|
-
flush();
|
|
49
|
-
if (!/^\s*\|[\s:|-]+\|\s*$/.test(raw))
|
|
50
|
-
for (const cell of raw.split('|').slice(1, -1))
|
|
51
|
-
blocks.push({ line, lastLine: line, text: cell, label: true });
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
if (/^\s*#{1,6}\s/.test(raw)) {
|
|
55
|
-
flush();
|
|
56
|
-
blocks.push({ line, lastLine: line, text: raw.replace(/^\s*#+\s*/, ''), label: true });
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
const item = raw.match(/^\s*(?:[-*+]|\d+[.)])\s+(.*)$/);
|
|
60
|
-
if (item) {
|
|
61
|
-
flush();
|
|
62
|
-
para = { line, lastLine: line, text: item[1], label: false };
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
if (para) {
|
|
66
|
-
para.text += ` ${raw.trim()}`;
|
|
67
|
-
para.lastLine = line;
|
|
68
|
-
}
|
|
69
|
-
else
|
|
70
|
-
para = { line, lastLine: line, text: raw.trim().replace(/^>\s*/, ''), label: false };
|
|
71
|
-
}
|
|
72
|
-
flush();
|
|
73
|
-
return blocks;
|
|
74
|
-
}
|
|
75
|
-
function featureBlocks(text) {
|
|
76
|
-
return text.split('\n').flatMap((raw, i) => {
|
|
77
|
-
const t = raw.trim();
|
|
78
|
-
if (!t || t.startsWith('#') || t.startsWith('@'))
|
|
79
|
-
return [];
|
|
80
|
-
const label = t.startsWith('|') || /^[A-Za-z ]+:/.test(t);
|
|
81
|
-
return [{ line: i + 1, lastLine: i + 1, text: t.replace(/^[A-Za-z ]+:\s*/, ''), label }];
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
const words = (sentence) => sentence.split(/\s+/).filter((w) => /[\p{L}\p{N}]/u.test(w)).length;
|
|
85
|
-
/** Problems with the writing rules in one file. With `lines`, only blocks that touch those lines. */
|
|
86
|
-
export function checkProse(file, text, lines) {
|
|
87
|
-
const blocks = file.endsWith('.feature') ? featureBlocks(text) : markdownBlocks(text);
|
|
88
|
-
const issues = [];
|
|
89
|
-
for (const block of blocks) {
|
|
90
|
-
if (lines) {
|
|
91
|
-
let touched = false;
|
|
92
|
-
for (let l = block.line; l <= block.lastLine && !touched; l++)
|
|
93
|
-
touched = lines.has(l);
|
|
94
|
-
if (!touched)
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
const prose = stripInline(block.text);
|
|
98
|
-
const at = `${file}:${block.line}`;
|
|
99
|
-
if (prose.includes(';'))
|
|
100
|
-
issues.push(`${at}: no semicolons. Write two sentences`);
|
|
101
|
-
if (prose.includes('—'))
|
|
102
|
-
issues.push(`${at}: no em dashes. Write two sentences or use a colon`);
|
|
103
|
-
if (block.label)
|
|
104
|
-
continue;
|
|
105
|
-
for (const sentence of prose.split(/(?<=[.!?:][*_)"'`]*)\s+(?=[\p{Lu}`*"([])/u)) {
|
|
106
|
-
const n = words(sentence);
|
|
107
|
-
if (n > MAX_WORDS)
|
|
108
|
-
issues.push(`${at}: a sentence has ${n} words. Use ${MAX_WORDS} at most`);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
return issues;
|
|
112
|
-
}
|
|
113
|
-
//# sourceMappingURL=prose.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"prose.js","sourceRoot":"","sources":["../../src/workflow/prose.ts"],"names":[],"mappings":"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"}
|
package/dist/workflow/red.d.ts
DELETED
package/dist/workflow/red.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { spawnSync } from 'node:child_process';
|
|
2
|
-
import { existsSync, mkdtempSync, rmSync } from 'node:fs';
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import { git } from './git.js';
|
|
6
|
-
const run = (cwd, cmd, args) => spawnSync(cmd, args, { cwd, stdio: 'inherit', env: { ...process.env, CI: 'true' } }).status ?? 1;
|
|
7
|
-
/** The nearest directory up from `file` with a package.json, below `root`. */
|
|
8
|
-
function packageOf(root, file) {
|
|
9
|
-
let dir = path.dirname(path.join(root, file));
|
|
10
|
-
while (dir !== root && !existsSync(path.join(dir, 'package.json')))
|
|
11
|
-
dir = path.dirname(dir);
|
|
12
|
-
return dir;
|
|
13
|
-
}
|
|
14
|
-
/** Runs the feature files at `sha` in a temporary worktree. True when at least one package fails. */
|
|
15
|
-
export function featuresFailAt(root, sha, features) {
|
|
16
|
-
const dir = mkdtempSync(path.join(tmpdir(), 'synoikia-red-'));
|
|
17
|
-
try {
|
|
18
|
-
git(root, 'worktree', 'add', '--detach', dir, sha);
|
|
19
|
-
if (run(dir, 'pnpm', ['install', '--frozen-lockfile', '--prefer-offline']) !== 0)
|
|
20
|
-
throw new Error(`pnpm install failed at ${sha.slice(0, 7)}`);
|
|
21
|
-
const byPackage = new Map();
|
|
22
|
-
for (const f of features.filter((f) => existsSync(path.join(dir, f)))) {
|
|
23
|
-
const pkg = packageOf(dir, f);
|
|
24
|
-
byPackage.set(pkg, [...(byPackage.get(pkg) ?? []), path.relative(pkg, path.join(dir, f))]);
|
|
25
|
-
}
|
|
26
|
-
let failed = false;
|
|
27
|
-
for (const [pkg, files] of byPackage)
|
|
28
|
-
failed = run(pkg, 'pnpm', ['exec', 'vitest', 'run', ...files]) !== 0 || failed;
|
|
29
|
-
return failed;
|
|
30
|
-
}
|
|
31
|
-
finally {
|
|
32
|
-
spawnSync('git', ['worktree', 'remove', '--force', dir], { cwd: root });
|
|
33
|
-
rmSync(dir, { recursive: true, force: true });
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
//# sourceMappingURL=red.js.map
|
package/dist/workflow/red.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"red.js","sourceRoot":"","sources":["../../src/workflow/red.ts"],"names":[],"mappings":"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"}
|
package/dist/workflow/secrets.js
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
const EXAMPLE = /(?:\.|^|\/)(?:example|sample|template)(?:\.[^/]+)?$/;
|
|
2
|
-
const CREDENTIAL = /(?:^|\/)(?:secrets?(?:\.[^/]+)*\.ya?ml|\.env(?:\..+)?|\.dev\.vars(?:\..+)?|[^/]+\.secret(?:\..+)?|[^/]+\.(?:pem|key|p12|pfx)|id_(?:rsa|dsa|ecdsa|ed25519)|\.npmrc|\.netrc)$/;
|
|
3
|
-
/** Staged paths that look like credential files, by name rather than content. */
|
|
4
|
-
export const credentialPaths = (files) => files.filter((f) => !EXAMPLE.test(f) && CREDENTIAL.test(f));
|
|
5
|
-
//# sourceMappingURL=secrets.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"secrets.js","sourceRoot":"","sources":["../../src/workflow/secrets.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,GAAG,qDAAqD,CAAC;AACtE,MAAM,UAAU,GACd,6KAA6K,CAAC;AAEhL,iFAAiF;AACjF,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,KAAwB,EAAE,EAAE,CAC1D,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC"}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
export declare const SECURITY_LINE: RegExp;
|
|
2
|
-
/** A ready PR that changes a security path states its audit on a line of its own. */
|
|
3
|
-
export declare function checkSecurityLine(pr: {
|
|
4
|
-
draft: boolean;
|
|
5
|
-
body: string | null;
|
|
6
|
-
}, changed: readonly string[], paths: readonly string[]): string[];
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { matchesAny } from './glob.js';
|
|
2
|
-
export const SECURITY_LINE = /^Security audit: \S/m;
|
|
3
|
-
/** A ready PR that changes a security path states its audit on a line of its own. */
|
|
4
|
-
export function checkSecurityLine(pr, changed, paths) {
|
|
5
|
-
if (pr.draft)
|
|
6
|
-
return [];
|
|
7
|
-
const touched = changed.filter((f) => matchesAny(f, paths));
|
|
8
|
-
if (!touched.length || SECURITY_LINE.test(pr.body ?? ''))
|
|
9
|
-
return [];
|
|
10
|
-
return [
|
|
11
|
-
`this PR changes security paths (${touched.slice(0, 5).join(', ')}${touched.length > 5 ? ', …' : ''}). Add a line "Security audit: <boundary and findings>" or "Security audit: none. <why>" to its description`,
|
|
12
|
-
];
|
|
13
|
-
}
|
|
14
|
-
//# sourceMappingURL=security-line.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"security-line.js","sourceRoot":"","sources":["../../src/workflow/security-line.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAEvC,MAAM,CAAC,MAAM,aAAa,GAAG,sBAAsB,CAAC;AAEpD,qFAAqF;AACrF,MAAM,UAAU,iBAAiB,CAC/B,EAA2C,EAC3C,OAA0B,EAC1B,KAAwB;IAExB,IAAI,EAAE,CAAC,KAAK;QAAE,OAAO,EAAE,CAAC;IACxB,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;IAC5D,IAAI,CAAC,OAAO,CAAC,MAAM,IAAI,aAAa,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC;QAAE,OAAO,EAAE,CAAC;IACpE,OAAO;QACL,mCAAmC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,6GAA6G;KACjN,CAAC;AACJ,CAAC"}
|
package/dist/workflow/sync.d.ts
DELETED
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
/** The shared workflow files, as repo-relative path → template path. Claude files only for repos that use Claude. */
|
|
2
|
-
export declare function workflowFiles(root: string): Map<string, string>;
|
|
3
|
-
/** Writes the shared files, or with `check` lists the ones that differ. Returns the paths written or out of date. */
|
|
4
|
-
export declare function syncWorkflow(root: string, check?: boolean): string[];
|
package/dist/workflow/sync.js
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { chmodSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync, statSync, writeFileSync, } from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { TEMPLATES_DIR } from '../repo-root.js';
|
|
4
|
-
const WORKFLOW_DIR = path.join(TEMPLATES_DIR, 'workflow');
|
|
5
|
-
const outName = (name) => (/^_[a-zA-Z]/.test(name) ? `.${name.slice(1)}` : name);
|
|
6
|
-
/** The shared workflow files, as repo-relative path → template path. Claude files only for repos that use Claude. */
|
|
7
|
-
export function workflowFiles(root) {
|
|
8
|
-
const files = new Map();
|
|
9
|
-
const walk = (from, to) => {
|
|
10
|
-
for (const entry of readdirSync(from).sort()) {
|
|
11
|
-
const src = path.join(from, entry);
|
|
12
|
-
const dest = path.posix.join(to, outName(entry));
|
|
13
|
-
if (statSync(src).isDirectory())
|
|
14
|
-
walk(src, dest);
|
|
15
|
-
else
|
|
16
|
-
files.set(dest, src);
|
|
17
|
-
}
|
|
18
|
-
};
|
|
19
|
-
walk(path.join(WORKFLOW_DIR, 'base'), '');
|
|
20
|
-
if (existsSync(path.join(root, '.claude')))
|
|
21
|
-
walk(path.join(WORKFLOW_DIR, 'claude'), '');
|
|
22
|
-
return files;
|
|
23
|
-
}
|
|
24
|
-
/** Writes the shared files, or with `check` lists the ones that differ. Returns the paths written or out of date. */
|
|
25
|
-
export function syncWorkflow(root, check = false) {
|
|
26
|
-
const changed = [];
|
|
27
|
-
for (const [rel, src] of workflowFiles(root)) {
|
|
28
|
-
const dest = path.join(root, rel);
|
|
29
|
-
if (lstatSync(dest, { throwIfNoEntry: false })?.isSymbolicLink())
|
|
30
|
-
throw new Error(`${rel} is a symlink. Remove it before the sync`);
|
|
31
|
-
const text = readFileSync(src, 'utf8');
|
|
32
|
-
if (existsSync(dest) && readFileSync(dest, 'utf8') === text)
|
|
33
|
-
continue;
|
|
34
|
-
changed.push(rel);
|
|
35
|
-
if (check)
|
|
36
|
-
continue;
|
|
37
|
-
mkdirSync(path.dirname(dest), { recursive: true });
|
|
38
|
-
const real = realpathSync(path.dirname(dest));
|
|
39
|
-
if (path.relative(realpathSync(root), real).startsWith('..'))
|
|
40
|
-
throw new Error(`${rel} resolves outside the repository`);
|
|
41
|
-
writeFileSync(dest, text);
|
|
42
|
-
if (rel.startsWith('.githooks/'))
|
|
43
|
-
chmodSync(dest, 0o755);
|
|
44
|
-
}
|
|
45
|
-
return changed;
|
|
46
|
-
}
|
|
47
|
-
//# sourceMappingURL=sync.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"sync.js","sourceRoot":"","sources":["../../src/workflow/sync.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EACT,UAAU,EACV,SAAS,EACT,SAAS,EACT,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,QAAQ,EACR,aAAa,GACd,MAAM,SAAS,CAAC;AACjB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEhD,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,UAAU,CAAC,CAAC;AAC1D,MAAM,OAAO,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;AAEzF,qHAAqH;AACrH,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,MAAM,KAAK,GAAG,IAAI,GAAG,EAAkB,CAAC;IACxC,MAAM,IAAI,GAAG,CAAC,IAAY,EAAE,EAAU,EAAE,EAAE;QACxC,KAAK,MAAM,KAAK,IAAI,WAAW,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;YAC7C,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;YACnC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;YACjD,IAAI,QAAQ,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE;gBAAE,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;;gBAC5C,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QAC5B,CAAC;IACH,CAAC,CAAC;IACF,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC;IAC1C,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;QAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,QAAQ,CAAC,EAAE,EAAE,CAAC,CAAC;IACxF,OAAO,KAAK,CAAC;AACf,CAAC;AAED,qHAAqH;AACrH,MAAM,UAAU,YAAY,CAAC,IAAY,EAAE,KAAK,GAAG,KAAK;IACtD,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,KAAK,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,aAAa,CAAC,IAAI,CAAC,EAAE,CAAC;QAC7C,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QAClC,IAAI,SAAS,CAAC,IAAI,EAAE,EAAE,cAAc,EAAE,KAAK,EAAE,CAAC,EAAE,cAAc,EAAE;YAC9D,MAAM,IAAI,KAAK,CAAC,GAAG,GAAG,0CAA0C,CAAC,CAAC;QACpE,MAAM,IAAI,GAAG,YAAY,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QACvC,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,KAAK,IAAI;YAAE,SAAS;QACtE,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAClB,IAAI,KAAK;YAAE,SAAS;QACpB,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACnD,MAAM,IAAI,GAAG,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;QAC9C,IAAI,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC;YAC1D,MAAM,IAAI,KAAK,CAAC,GAAG,GAAG,kCAAkC,CAAC,CAAC;QAC5D,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAC1B,IAAI,GAAG,CAAC,UAAU,CAAC,YAAY,CAAC;YAAE,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAC3D,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC"}
|
package/dist/workflow/tests.d.ts
DELETED
package/dist/workflow/tests.js
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import { matchesAny } from './glob.js';
|
|
2
|
-
/** New test files must be pure-function unit tests. Behavior is specified in feature files. */
|
|
3
|
-
export const unlistedTests = (added, unitTests) => added
|
|
4
|
-
.filter((f) => /\.(?:test|spec)\.[cm]?[jt]sx?$/.test(f) && !matchesAny(f, unitTests))
|
|
5
|
-
.map((f) => `${f}: a new test file must be a pure-function unit test (${unitTests.join(', ')}). Specify behavior in a feature file`);
|
|
6
|
-
//# sourceMappingURL=tests.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"tests.js","sourceRoot":"","sources":["../../src/workflow/tests.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAEvC,+FAA+F;AAC/F,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,KAAwB,EAAE,SAA4B,EAAE,EAAE,CACtF,KAAK;KACF,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,gCAAgC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC;KACpF,GAAG,CACF,CAAC,CAAC,EAAE,EAAE,CACJ,GAAG,CAAC,wDAAwD,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,uCAAuC,CAC1H,CAAC"}
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: security-reviewer
|
|
3
|
-
description: Reviews a change to a Synoikia plugin, or to this repository's release tooling, for security problems and known CVEs in its dependencies. Use proactively before opening or merging a PR that touches a path in synoikia.workflow.securityPaths of the root package.json, such as plugins/*/manifest.json, plugins/*/plugin.yaml, plugins/*/src, plugins/*/test/compliance.yaml, a plugin's dependencies or .github/workflows; or when asked for a security review or dependency audit.
|
|
4
|
-
tools: Read, Grep, Glob, Bash
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
You review changes to Synoikia plugins for security problems. You are read-only: never edit files, commit, push or change the environment. Use Bash only for read-only commands such as `git diff`, `git log`, `git show`, `pnpm audit`, `pnpm why`, `pnpm ls`, and for running existing tests (`pnpm --filter ./plugins/<id> test`).
|
|
8
|
-
|
|
9
|
-
## The trust model
|
|
10
|
-
|
|
11
|
-
Synoikia core runs every plugin instance as a child process under the Node permission model. It puts every call through its own sandbox, permission gate, human approvals, redaction and audit log. Core enforces policy, but it enforces it based on what the plugin tells it. A plugin that labels a destructive call `read`, leaves a secret out of `sensitiveKeys`, or writes a misleading approval summary weakens every install. Core can't detect it. Most findings in this repository are cases where the plugin gave core wrong information. Background: `CLAUDE.md`, `README.md`, and Synoikia's design §3–§5 and §12 (`https://github.com/via-justa/synoikia-core/blob/main/docs/design/`).
|
|
12
|
-
|
|
13
|
-
## Scope
|
|
14
|
-
|
|
15
|
-
Start from the diff you were given. Otherwise use `git diff origin/main...HEAD` plus uncommitted changes. Follow each changed function to its callers and callees. Read the plugin's `manifest.json` and `plugin.yaml` in full whenever its `src/` changes.
|
|
16
|
-
|
|
17
|
-
## What to check
|
|
18
|
-
|
|
19
|
-
- **`plugin.yaml` rules** (applied by the SDK's `compileRules`): precedence fails closed. The order is excluded, then locked or split twin, then a rule's `classification`, then the plugin's heuristic, else write. These are findings unless the PR justifies them:
|
|
20
|
-
- removing or narrowing a `locked`, `split`, `sensitiveParams` or `sensitiveResult` rule
|
|
21
|
-
- adding a `classification: read` override
|
|
22
|
-
- widening `include` past an `exclude`
|
|
23
|
-
|
|
24
|
-
A `splitWhen` predicate must fail closed (unknown means the locked twin). `plugin.yaml` is parsed and inlined at build time only. Nothing reads files at runtime.
|
|
25
|
-
- **The compliance profile** (`test/compliance.yaml`): it is what core certifies the plugin against, so review its diff like a `plugin.yaml` diff. A removed or weakened `reads`, `writes`, `rejects`, `locks`, `secrets` or `connectionTest` case is a finding unless the PR justifies it. A read that moves to `writes` or out of the profile needs a reason. Each locked operation and split twin has a `locks` case with its literal. The fake upstream (`test/fake*.ts`) must answer as the real upstream does, so a fake that hides a secret or an error weakens the certification.
|
|
26
|
-
- **Classification** (discovery in `src/`): no operation with side effects is labeled read. That includes GETs with side effects, and RPC methods whose names sound like reads. Destructive or hard-to-undo operations are `locked` with `typedConfirmation`, for example delete, wipe, restart, unlock, regenerate a key, or change auth or users. Operations the plugin doesn't know yet fail closed: write or locked, never read. Values taken from the upstream's own spec or introspection are untrusted and must not be able to downgrade a classification.
|
|
27
|
-
- **Request building** (`src/`, `resolveOperation`/`invoke`): model-supplied arguments cannot change the host, scheme or port. Path parameters are encoded (`encodeURIComponent`), so `../`, `?`, `#` and `%2F` can't change which operation runs. Model arguments cannot set auth headers or impersonation headers. The operation key that core gated is the one that is invoked: no aliasing, no smuggling a second call through params.
|
|
28
|
-
- **Targets and summaries** (`resolveTargets`, `summarize`, `prepareWrite`): the approval page shows exactly what will be affected. A summary or target that understates the effect is an approval-integrity finding. So is one that the model can make misleading through crafted params (display names, markdown, control characters). Summaries never contain secrets. Core passes them redacted params, so the plugin must not add secrets back from its own state.
|
|
29
|
-
- **Secrets** (`manifest.json`, `src/`): every credential field is `writeOnly`, uses the `secret` widget and is in `sensitiveKeys`. Every secret field the upstream can **return** (API keys in settings, webhook URLs, tokens, passwords, push keys) is in `sensitiveKeys`. A `secrets` case in `test/compliance.yaml` checks that it is redacted, and the fake returns it. Credentials are never in error messages, logs, thrown `PluginError` text or catalog descriptions. They are never sent to any host except the upstream: for example, the auth header must not go to a spec mirror.
|
|
30
|
-
- **Network** (`manifest.json` `network.hosts`): as small as possible, with templated hosts only from connection fields. No wildcards or extra third-party hosts without a stated reason. TLS verification is on by default (`verifyTls !== false`), and turning it off is an explicit connection setting.
|
|
31
|
-
- **Staying inside core's model**: no plugin-side allow/deny lists, approval prompts, retries of denied calls, or caching that serves data after core denied a call. No `eval`, `new Function`, dynamic `import()` of computed paths, or filesystem access outside the plugin's own package directory. Everything is bundled and nothing is loaded at runtime.
|
|
32
|
-
- **Parsing untrusted upstream data**: YAML, JSON and WebSocket frames from the upstream are untrusted. Look for prototype pollution through merges, unbounded sizes and recursion, and YAML features beyond plain data.
|
|
33
|
-
- **Release tooling** (`.github/workflows/`, and the `synoikia-plugin repo` steps of the pinned `@synoikia/create-plugin` version):
|
|
34
|
-
- `MINISIGN_SECRET_KEY` is exposed only to the Sign step, never echoed, and written with `umask 077`.
|
|
35
|
-
- Packing is deterministic. Publishing happens only after the signature is verified against `minisign.pub` and the install check passes.
|
|
36
|
-
- Released versions are never overwritten. The index download fails on errors other than "release not found", and `repo publish` refuses an existing tag whose tarball differs.
|
|
37
|
-
- Workflow permissions are the minimum needed. A new third-party action is pinned by commit SHA.
|
|
38
|
-
- **Tests**: new secret fields, locked operations and classification rules come with a test that fails if they regress. That is a profile case, a feature scenario or a unit test.
|
|
39
|
-
|
|
40
|
-
## Known vulnerabilities (CVEs)
|
|
41
|
-
|
|
42
|
-
Always run this section, even when the diff touches no dependencies, because new advisories appear against unchanged code.
|
|
43
|
-
|
|
44
|
-
1. Run `pnpm audit --json` from the repository root, and `pnpm audit --prod` to separate shipped dependencies from dev-only ones.
|
|
45
|
-
2. For each advisory, use `pnpm why <package>` to find which plugin pulls it in, and whether it is a `dependency` or a `devDependency` of that plugin:
|
|
46
|
-
- A plugin's `dependencies` are bundled into `dist/index.js` and **ship to every install**. Judge whether the vulnerable code is reachable from that plugin. Find what input reaches it, and whether the model, the upstream or the admin supplies it.
|
|
47
|
-
- `devDependencies` (`@synoikia/core`, esbuild, vitest) don't ship, but they run in CI, including the Release workflow that handles the signing key. Report them as lower severity unless the advisory is about code execution at install or build time.
|
|
48
|
-
3. Also check new or bumped dependencies in the diff:
|
|
49
|
-
- whether the version is current and has advisories (the audit covers this once it is in the lockfile)
|
|
50
|
-
- whether it has install scripts (they need an `onlyBuiltDependencies` entry in `pnpm-workspace.yaml`)
|
|
51
|
-
- whether the package is a typosquat of a well-known name
|
|
52
|
-
4. For each CVE finding, give the advisory ID (GHSA/CVE), the affected and patched version ranges, the dependency path, and the smallest fix. That is a direct bump, or a `pnpm.overrides` entry when only a transitive dependency is vulnerable.
|
|
53
|
-
|
|
54
|
-
If `pnpm audit` can't reach the registry, say so. Don't report the dependency surface as clean.
|
|
55
|
-
|
|
56
|
-
## Reporting
|
|
57
|
-
|
|
58
|
-
Report only issues you can tie to concrete code or a concrete advisory. For each finding give:
|
|
59
|
-
|
|
60
|
-
- **Severity**:
|
|
61
|
-
- critical: a write or secret leak core can't stop.
|
|
62
|
-
- high: realistic misuse, or a reachable CVE in shipped code.
|
|
63
|
-
- medium: defense in depth lost, or a dev-only CVE that affects CI.
|
|
64
|
-
- low: hardening.
|
|
65
|
-
- **Location**: `path:line`, or the dependency path for a CVE.
|
|
66
|
-
- **What breaks**: which promise to core or to the admin no longer holds.
|
|
67
|
-
- **Scenario**: who does what, with which input, and what they gain.
|
|
68
|
-
- **Fix**: the smallest change, plus the test that would catch a regression.
|
|
69
|
-
|
|
70
|
-
Rank findings by severity. If there are none, say so plainly, and list what you checked and the `pnpm audit` result. Say what you could not verify rather than guessing.
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
// PostToolUse hook: runs Prettier on the file just edited, so `pnpm format:check` in CI stays green.
|
|
2
|
-
// Prettier skips files in .prettierignore and unknown types; a failure here never blocks the edit.
|
|
3
|
-
import { execFileSync } from 'node:child_process';
|
|
4
|
-
import { existsSync } from 'node:fs';
|
|
5
|
-
import path from 'node:path';
|
|
6
|
-
|
|
7
|
-
let input = '';
|
|
8
|
-
for await (const chunk of process.stdin) input += chunk;
|
|
9
|
-
const { tool_input: toolInput = {}, cwd } = JSON.parse(input || '{}');
|
|
10
|
-
const root = process.env.CLAUDE_PROJECT_DIR ?? cwd ?? process.cwd();
|
|
11
|
-
const prettier = path.join(root, 'node_modules/.bin/prettier');
|
|
12
|
-
const file = toolInput.file_path;
|
|
13
|
-
|
|
14
|
-
if (file && existsSync(file) && existsSync(prettier)) {
|
|
15
|
-
try {
|
|
16
|
-
execFileSync(prettier, ['--write', '--ignore-unknown', '--log-level=warn', file], { cwd: root, stdio: 'ignore' });
|
|
17
|
-
} catch {
|
|
18
|
-
// A file Prettier can't parse is left as written; lint and format:check will report it.
|
|
19
|
-
}
|
|
20
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
// PreToolUse hook: stops edits to generated files and asks before edits to the signing key.
|
|
2
|
-
// Reads the tool call from stdin; prints a permission decision for matching paths, nothing otherwise.
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
|
|
5
|
-
const RULES = [
|
|
6
|
-
{
|
|
7
|
-
match: /(^|\/)pnpm-lock\.yaml$/,
|
|
8
|
-
decision: 'deny',
|
|
9
|
-
reason: 'pnpm-lock.yaml is generated. Change package.json and run `pnpm install` instead.',
|
|
10
|
-
},
|
|
11
|
-
{
|
|
12
|
-
match: /^plugins\/[^/]+\/dist\//,
|
|
13
|
-
decision: 'deny',
|
|
14
|
-
reason: 'dist/ is the esbuild bundle. Edit src/ and run `pnpm --filter ./plugins/<id> build`.',
|
|
15
|
-
},
|
|
16
|
-
{
|
|
17
|
-
match: /^minisign\.pub$/,
|
|
18
|
-
decision: 'ask',
|
|
19
|
-
reason:
|
|
20
|
-
'minisign.pub is the key every Synoikia install pins for this repository. Changing it blocks installs until each admin confirms the new key; only for a compromised key.',
|
|
21
|
-
},
|
|
22
|
-
];
|
|
23
|
-
|
|
24
|
-
let input = '';
|
|
25
|
-
for await (const chunk of process.stdin) input += chunk;
|
|
26
|
-
const { tool_input: toolInput = {}, cwd } = JSON.parse(input || '{}');
|
|
27
|
-
const file = toolInput.file_path ?? toolInput.notebook_path;
|
|
28
|
-
if (!file) process.exit(0);
|
|
29
|
-
|
|
30
|
-
const root = process.env.CLAUDE_PROJECT_DIR ?? cwd ?? process.cwd();
|
|
31
|
-
const rel = path.relative(root, path.resolve(root, file)).split(path.sep).join('/');
|
|
32
|
-
const rule = RULES.find((r) => r.match.test(rel));
|
|
33
|
-
if (!rule) process.exit(0);
|
|
34
|
-
|
|
35
|
-
console.log(
|
|
36
|
-
JSON.stringify({
|
|
37
|
-
hookSpecificOutput: {
|
|
38
|
-
hookEventName: 'PreToolUse',
|
|
39
|
-
permissionDecision: rule.decision,
|
|
40
|
-
permissionDecisionReason: `${rel}: ${rule.reason}`,
|
|
41
|
-
},
|
|
42
|
-
}),
|
|
43
|
-
);
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: add-plugin
|
|
3
|
-
description: Scaffold a new Synoikia plugin under plugins/<id> with `pnpm new`, then fill in its plugin.yaml rules, discovery and tests. Use when asked to add, create or start a plugin for a new upstream service.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Add a plugin
|
|
7
|
-
|
|
8
|
-
Run the change as the `change-workflow` skill and `docs/workflow.md` say. Write the code as the `plugin-development` skill says. Background: Synoikia's plugin authoring guide (`https://github.com/via-justa/synoikia-core/blob/main/docs/plugin-authoring.md`).
|
|
9
|
-
|
|
10
|
-
## 1. Settle the basics
|
|
11
|
-
|
|
12
|
-
- **`<id>`**: lowercase, the directory name and manifest `id`. The sandbox namespace defaults to it in camelCase.
|
|
13
|
-
- **Archetype**: `openapi-rest` (the upstream serves an OpenAPI spec), `static-rest` (a REST API without one, with operations declared in `plugin.yaml`), `websocket-rpc` (JSON-RPC over WebSocket), or `blank`.
|
|
14
|
-
- **Auth**: `bearer`, `api-key` (with its header), `basic` or `none`.
|
|
15
|
-
- **Destructive operations**: which ones are `locked` (always a human who types a confirmation literal), and what that literal is.
|
|
16
|
-
- **Secrets**: which connection fields are credentials, and which fields the upstream returns hold secrets.
|
|
17
|
-
|
|
18
|
-
## 2. Scaffold
|
|
19
|
-
|
|
20
|
-
```sh
|
|
21
|
-
pnpm new --id <id> --name "<Name>" --archetype <archetype> --auth <auth> --yes
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
This writes `plugins/<id>/` and runs `pnpm install`. The plugin holds `manifest.json`, `plugin.yaml`, `src/`, `test/compliance.yaml`, a fake upstream, a README and a RUNBOOK. For the `openapi-rest`, `static-rest` and `websocket-rpc` archetypes, the plugin passes the compliance suite as generated.
|
|
25
|
-
|
|
26
|
-
## 3. Describe the upstream
|
|
27
|
-
|
|
28
|
-
- `manifest.json`: connection fields and help, `sensitiveKeys` (add every secret field the upstream returns), `network.hosts` (every host the plugin calls).
|
|
29
|
-
- `plugin.yaml`: `rules` for locks, splits, classification overrides, `sensitiveParams`, `sensitiveResult`, `confirm` literals, summary notes and guidance. Use `exclude` for plumbing the model has no business calling, and `operations` for declared operations. A rule's `classification` never unlocks, and anything unclassified is a write.
|
|
30
|
-
- `src/plugin.ts`: only what needs logic, such as a `splitWhen` predicate, a custom confirm source, or discovery.
|
|
31
|
-
|
|
32
|
-
## 4. Tests
|
|
33
|
-
|
|
34
|
-
- Extend the fake upstream (`test/fake-upstream.ts`, on `startFakeHttp` from `@synoikia/core/testing` for HTTP) to answer what the plugin calls. Include the responses that hold secrets.
|
|
35
|
-
- Fill in `test/compliance.yaml`: reads, writes, rejects, the secrets, and a `locks` case for each locked operation and split twin.
|
|
36
|
-
- Write plugin-specific behavior in `features/<id>.feature` with the suite's steps, on calls named in `calls`.
|
|
37
|
-
- Put tests of pure logic in `test/unit/`.
|
|
38
|
-
|
|
39
|
-
```sh
|
|
40
|
-
pnpm --filter ./plugins/<id> test
|
|
41
|
-
pnpm typecheck && pnpm lint && pnpm format:check
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
The plugin run must end with "<Name> 0.1.0 is fit for production." and no failed test.
|
|
45
|
-
|
|
46
|
-
Leave the version at `0.1.0`. Run the `security-reviewer` subagent on the diff before you open the PR.
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: plugin-development
|
|
3
|
-
description: >-
|
|
4
|
-
How to write and review plugin code in this Synoikia plugin repository: plugins/<id>/src, plugin.yaml,
|
|
5
|
-
manifest.json and the plugin's tests. Use it for any task that changes or reviews a plugin, even when the
|
|
6
|
-
request names only an operation, an upstream endpoint, a lock, a secret or a bug and never says
|
|
7
|
-
"TypeScript" or "plugin.yaml". Examples: add or lock an operation; split an operation into twins; mark a
|
|
8
|
-
param or result field sensitive; change a summary or confirmation literal; fix a client, a transport or
|
|
9
|
-
discovery; add a connection field; extend a fake upstream; write or fix unit tests, the compliance profile
|
|
10
|
-
or a plugin feature; review a plugin diff. Not for scaffolding a new plugin (add-plugin, then this skill for
|
|
11
|
-
the code) or version bumps (release-plugin).
|
|
12
|
-
---
|
|
13
|
-
|
|
14
|
-
# Plugin development
|
|
15
|
-
|
|
16
|
-
A plugin is a Node 22 ESM process that describes one upstream API to core: its catalog, classification, targets, summaries, confirmation literals and sensitive fields. Core does the rest. New code must look like the code around it. Make the smallest change that does the job, with one shared way to do each thing. Add no dependency for what a few lines or the SDK already do.
|
|
17
|
-
|
|
18
|
-
`CLAUDE.md` holds the repo rules (bundle, network hosts, secrets, versions, security review). This skill is how to write the change. Load a reference only when the task touches its area:
|
|
19
|
-
|
|
20
|
-
| Task touches | Read |
|
|
21
|
-
| --------------------------------------------------------------------------- | -------------------------- |
|
|
22
|
-
| Types, narrowing upstream data, errors, imports, module layout | `references/typescript.md` |
|
|
23
|
-
| Tests, the fake upstream, `test/compliance.yaml`, the plugin's feature file | `references/testing.md` |
|
|
24
|
-
|
|
25
|
-
## Rules for every change
|
|
26
|
-
|
|
27
|
-
1. **Reuse first.** Before you write a helper, look in the plugin's own `src/` and in `@synoikia/plugin-sdk`. The SDK has `definePlugin`, `compileRules`, `parsePluginSettings`, `requireString`, `requireField`, `isPlainObject`, `truncate` and `clip`. It also has `PluginError`/`ErrorCodes`, `upstreamError`, `HttpJsonClient`, `singleFlight`, `PendingRequests` and the OpenAPI and static catalog adapters. Use it, or extend it. If the SDK lacks it and another plugin would need it too, it belongs in core's SDK, released first.
|
|
28
|
-
2. **Policy in `plugin.yaml`, logic in `src/`.** Locks, splits, classification overrides, `sensitiveParams`, `sensitiveResult`, confirm literals, guidance, `exclude`/`include` and upstream lists are YAML. Code is only for a decision YAML can't express (a path check, a target lookup, a `splitWhen` predicate).
|
|
29
|
-
3. **Describe, don't decide.** No plugin-side allow/deny switch, approval prompt, rate limit or secret handling: core's gate, approvals and redaction own them.
|
|
30
|
-
4. **Fail closed.** Anything unclassified is a write. A rule's `classification` never unlocks. An upstream shape the code doesn't know is redacted or refused, not passed through.
|
|
31
|
-
5. **Narrow upstream data at the edge.** Responses, events and specs are `unknown` until checked with `isPlainObject`, `requireString` and friends. Past that point, trust the narrowed type.
|
|
32
|
-
6. **Secrets never leave masked.** Credential fields are `writeOnly`, use the `secret` widget and are in `sensitiveKeys`. Secret fields the upstream returns go in `sensitiveKeys` and `sensitiveResult`. Summaries, errors and logs never carry a secret or a raw upstream body.
|
|
33
|
-
7. **Every host is declared.** A new upstream URL goes in `manifest.json` `network.hosts`, and every runtime dependency and data file is bundled into `dist/index.js`.
|
|
34
|
-
8. **Profile diffs are security diffs.** A change to `test/compliance.yaml` changes what core certifies. Change it only on purpose, and say in the PR what changed and why.
|
|
35
|
-
9. **Small and direct.** No speculative options, wrappers or config for one caller. Comments are one or two lines, for a reason or a design `§` reference only.
|
|
36
|
-
10. **Tests ship with the change.** Specify the behavior first, as `docs/workflow.md` says. Logic gets a unit test in `test/unit/`. Behavior at the plugin boundary gets a profile case or a scenario in `features/<id>.feature`. Extend the fake upstream to match.
|
|
37
|
-
|
|
38
|
-
## Checks
|
|
39
|
-
|
|
40
|
-
Run from the repo root before finishing:
|
|
41
|
-
|
|
42
|
-
```sh
|
|
43
|
-
pnpm --filter ./plugins/<id> test # check, build, unit tests, compliance suite, features
|
|
44
|
-
pnpm lint && pnpm format:check && pnpm typecheck
|
|
45
|
-
pnpm check # manifests, plugin.yaml and versions
|
|
46
|
-
pnpm workflow pre-push # branch, phases, features and prose
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
The plugin run must end with "<Name> <version> is fit for production." and no failed test.
|
|
50
|
-
|
|
51
|
-
Then run the `security-reviewer` subagent when `CLAUDE.md` › Security review lists a path you touched. Don't bump the version unless the user asked (`release-plugin`).
|
|
52
|
-
|
|
53
|
-
## Reviewing
|
|
54
|
-
|
|
55
|
-
Lead with what tools can't catch. Say why each one matters:
|
|
56
|
-
|
|
57
|
-
- A lock or sensitive rule dropped or narrowed.
|
|
58
|
-
- A classification that turns a write into a read.
|
|
59
|
-
- A secret that reaches a summary or an error.
|
|
60
|
-
- A host missing from `network.hosts`.
|
|
61
|
-
- Logic in `src/` that belongs in `plugin.yaml`, or a plugin-side check that duplicates core.
|
|
62
|
-
- An `as` cast that hides an upstream shape.
|
|
63
|
-
- A `test/compliance.yaml` change with no reason given, or a case removed or weakened.
|
|
64
|
-
- A new upstream secret with no `secrets` entry, or no fake response that holds it.
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
# Testing
|
|
2
|
-
|
|
3
|
-
Vitest, with quickpickle for feature files. The plugin's `vitest.config.ts` uses `complianceConfig` from `@synoikia/core/testing/compliance`. The `test` script runs these steps:
|
|
4
|
-
|
|
5
|
-
1. `synoikia-plugin check`
|
|
6
|
-
2. `synoikia-plugin build`, which rebuilds `dist/index.js`
|
|
7
|
-
3. the unit tests in `test/unit/`
|
|
8
|
-
4. core's compliance suite, from `test/compliance.yaml`
|
|
9
|
-
5. the plugin's own `features/<id>.feature`
|
|
10
|
-
|
|
11
|
-
The suite and the features run the **built** bundle. Rebuild after any change to `src/` or `plugin.yaml` before you run one file.
|
|
12
|
-
|
|
13
|
-
The run must end with "<Name> <version> is fit for production." and no failed test. A failing own feature does not change the verdict line, but it fails the run.
|
|
14
|
-
|
|
15
|
-
## Pick the place that proves the behavior
|
|
16
|
-
|
|
17
|
-
| What | Where |
|
|
18
|
-
| --------------------------------------------------------------------- | ------------------------------------------------------------- |
|
|
19
|
-
| A pure function (catalog building, classification, a transform) | A unit test in `test/unit/` that imports and calls it |
|
|
20
|
-
| Reads, writes, rejected calls, locks, secrets and the connection test | A case in `test/compliance.yaml` |
|
|
21
|
-
| Plugin-specific behavior through core: approvals, summaries, levels | A scenario in `features/<id>.feature`, with the suite's steps |
|
|
22
|
-
|
|
23
|
-
`docs/workflow.md` › Tests has the rules for test files. A new `*.test.ts` file is a pure-function unit test in `test/unit/`.
|
|
24
|
-
|
|
25
|
-
## The compliance profile
|
|
26
|
-
|
|
27
|
-
`test/compliance.yaml` tells core's suite how to drive the plugin against its fake upstream. The format and the steps are in core's `docs/plugin-authoring.md` › The compliance suite.
|
|
28
|
-
|
|
29
|
-
| Key | What |
|
|
30
|
-
| ---------------- | ------------------------------------------------------------------------ |
|
|
31
|
-
| `fake` | `test/fake.ts`, the module that starts the fake upstream |
|
|
32
|
-
| `connection` | The connection values, with `${fake.<name>}` for the fake's values |
|
|
33
|
-
| `reads` | Calls that must classify as reads |
|
|
34
|
-
| `writes` | Calls that must classify as writes |
|
|
35
|
-
| `rejects` | Calls that core or the plugin must refuse |
|
|
36
|
-
| `locks` | One case for each locked operation and each split twin, with its literal |
|
|
37
|
-
| `secrets` | Calls whose upstream values must never reach the agent |
|
|
38
|
-
| `connectionTest` | Connection values that the connection test must reject |
|
|
39
|
-
| `calls` | Named calls that `features/<id>.feature` uses |
|
|
40
|
-
|
|
41
|
-
- A lock with no case fails the suite. Add a case when you add a lock or a split twin.
|
|
42
|
-
- A diff of `test/compliance.yaml` is a security change. Change a case only on purpose, and explain it in the PR.
|
|
43
|
-
|
|
44
|
-
## The fake upstream
|
|
45
|
-
|
|
46
|
-
- Each plugin has one in `test/` (`fake-upstream.ts` as `pnpm new` writes it). Build it on `startFakeHttp` from `@synoikia/core/testing` for HTTP, or on a local server for WebSocket.
|
|
47
|
-
- Tests never reach the real network.
|
|
48
|
-
- `test/fake.ts` exports `start()`. It starts the fake and returns its URL, `close()` and the values that the profile names. It can also return `reset()`.
|
|
49
|
-
- Extend the fake to answer every call the change makes. Include the responses that hold secrets, the error statuses and the permission denials.
|
|
50
|
-
|
|
51
|
-
## Feature files
|
|
52
|
-
|
|
53
|
-
- Write scenarios with the suite's steps on the profile's named calls. The step table is in core's `docs/plugin-authoring.md`.
|
|
54
|
-
- Follow `docs/workflow.md` › Feature files: a `Rule:` for each scenario, outcomes and not solutions, five steps at most.
|
|
55
|
-
- Assert on what the agent or the human sees: the result, the approval text, the literal to type, the error code.
|
|
56
|
-
|
|
57
|
-
## Unit tests
|
|
58
|
-
|
|
59
|
-
- Assert on what core or the model sees: the descriptor, the summary, the confirm literal, the masked result, the error code. Not on private fields.
|
|
60
|
-
- Give each new error path a test that hits it and checks its `ErrorCodes` value.
|