@ketecode/loom 0.1.0 → 0.2.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 +111 -11
- package/agents/code-reviewer/evals/behavior.json +60 -0
- package/agents/inbox-triager/AGENT.md +98 -0
- package/agents/inbox-triager/evals/triggers.json +16 -0
- package/agents/spec-reviewer/AGENT.md +95 -0
- package/agents/spec-reviewer/evals/behavior.json +57 -0
- package/agents/spec-reviewer/evals/triggers.json +16 -0
- package/agents/task-implementer/AGENT.md +89 -0
- package/agents/task-implementer/evals/behavior.json +47 -0
- package/agents/task-implementer/evals/triggers.json +16 -0
- package/bin/kete-loom.js +46 -6
- package/commands/brainstorm.md +25 -0
- package/commands/debug.md +27 -0
- package/commands/dual-review.md +34 -0
- package/commands/feature-dev.md +8 -7
- package/commands/finish-branch.md +26 -0
- package/commands/fix-bug.md +38 -0
- package/commands/production-readiness.md +45 -0
- package/commands/usage-report.md +43 -0
- package/hooks/block-foreground-servers/HOOK.md +34 -0
- package/hooks/block-foreground-servers/cases.json +31 -0
- package/hooks/block-foreground-servers/hook.js +41 -0
- package/hooks/capture-lessons/HOOK.md +24 -0
- package/hooks/capture-lessons/hook.js +48 -0
- package/hooks/notify-on-stop/HOOK.md +25 -0
- package/hooks/notify-on-stop/hook.js +27 -0
- package/hooks/protect-lint-config/HOOK.md +29 -0
- package/hooks/protect-lint-config/cases.json +18 -0
- package/hooks/protect-lint-config/fixtures/.eslintrc.json +1 -0
- package/hooks/protect-lint-config/fixtures/.golangci.yml +1 -0
- package/hooks/protect-lint-config/fixtures/.prettierrc +1 -0
- package/hooks/protect-lint-config/fixtures/.rubocop.yml +1 -0
- package/hooks/protect-lint-config/fixtures/biome.json +1 -0
- package/hooks/protect-lint-config/fixtures/eslint.config.mjs +1 -0
- package/hooks/protect-lint-config/fixtures/ruff.toml +1 -0
- package/hooks/protect-lint-config/hook.js +25 -0
- package/hooks/read-before-edit/HOOK.md +26 -0
- package/hooks/read-before-edit/cases.json +15 -0
- package/hooks/read-before-edit/hook.js +50 -0
- package/hooks/run.js +22 -6
- package/hooks/verify-before-stop/HOOK.md +32 -0
- package/hooks/verify-before-stop/hook.js +70 -0
- package/mcp/clickhouse.json +11 -0
- package/mcp/exa.json +11 -0
- package/mcp/firecrawl.json +11 -0
- package/mcp/vercel.json +11 -0
- package/package.json +6 -4
- package/profiles.json +54 -5
- package/rules/use-skills.md +23 -0
- package/scripts/lib/behavior.js +52 -0
- package/scripts/lib/hooks-install.js +9 -7
- package/scripts/lib/lint.js +24 -2
- package/scripts/lib/mcp-install.js +5 -1
- package/scripts/lib/render.js +6 -0
- package/scripts/lib/skills.js +1 -0
- package/scripts/lib/targets.js +10 -0
- package/skills/agent-failure-debugging/SKILL.md +92 -0
- package/skills/agent-failure-debugging/evals/triggers.json +16 -0
- package/skills/agent-outbound-messaging/SKILL.md +92 -0
- package/skills/agent-outbound-messaging/evals/triggers.json +16 -0
- package/skills/agent-security/SKILL.md +117 -0
- package/skills/agent-security/evals/triggers.json +16 -0
- package/skills/agent-security/references/host-configs.md +37 -0
- package/skills/api-client-integration/SKILL.md +86 -0
- package/skills/api-client-integration/evals/triggers.json +16 -0
- package/skills/api-client-integration/references/resilience.md +57 -0
- package/skills/blender-automation/SKILL.md +100 -0
- package/skills/blender-automation/evals/triggers.json +16 -0
- package/skills/blender-automation/references/batch-and-render.md +77 -0
- package/skills/blender-automation/references/bpy-inspection.md +91 -0
- package/skills/blender-automation/references/rig-and-retarget-checks.md +23 -0
- package/skills/brand-strategy/SKILL.md +83 -0
- package/skills/brand-strategy/evals/triggers.json +16 -0
- package/skills/change-impact-analysis/SKILL.md +89 -0
- package/skills/change-impact-analysis/evals/triggers.json +16 -0
- package/skills/change-impact-analysis/references/graph-commands.md +28 -0
- package/skills/contract-drafting/SKILL.md +96 -0
- package/skills/contract-drafting/evals/triggers.json +16 -0
- package/skills/contract-drafting/references/term-sheets.md +68 -0
- package/skills/contract-first-development/SKILL.md +86 -0
- package/skills/contract-first-development/evals/triggers.json +16 -0
- package/skills/contract-first-development/references/tooling.md +46 -0
- package/skills/decision-council/SKILL.md +98 -0
- package/skills/decision-council/evals/triggers.json +16 -0
- package/skills/design-brainstorming/SKILL.md +132 -0
- package/skills/design-brainstorming/evals/behavior.json +55 -0
- package/skills/design-brainstorming/evals/triggers.json +16 -0
- package/skills/design-brainstorming/references/visual-mockups.md +34 -0
- package/skills/desktop-app-testing/SKILL.md +104 -0
- package/skills/desktop-app-testing/evals/triggers.json +16 -0
- package/skills/desktop-app-testing/references/drivers.md +81 -0
- package/skills/document-processing/SKILL.md +100 -0
- package/skills/document-processing/evals/triggers.json +17 -0
- package/skills/document-processing/references/extraction-and-ocr.md +71 -0
- package/skills/document-processing/references/forms-and-conversion.md +69 -0
- package/skills/document-processing/references/redaction.md +61 -0
- package/skills/energy-procurement/SKILL.md +106 -0
- package/skills/energy-procurement/evals/triggers.json +16 -0
- package/skills/finishing-a-branch/SKILL.md +127 -0
- package/skills/finishing-a-branch/evals/behavior.json +49 -0
- package/skills/finishing-a-branch/evals/triggers.json +16 -0
- package/skills/freight-carrier-management/SKILL.md +107 -0
- package/skills/freight-carrier-management/evals/triggers.json +16 -0
- package/skills/git-worktrees/SKILL.md +130 -0
- package/skills/git-worktrees/evals/behavior.json +56 -0
- package/skills/git-worktrees/evals/triggers.json +16 -0
- package/skills/google-workspace-integration/SKILL.md +65 -0
- package/skills/google-workspace-integration/evals/triggers.json +17 -0
- package/skills/google-workspace-integration/references/api.md +121 -0
- package/skills/harmonyos-arkts/SKILL.md +117 -0
- package/skills/harmonyos-arkts/evals/triggers.json +16 -0
- package/skills/harmonyos-arkts/references/build-errors.md +42 -0
- package/skills/implementation-plans/SKILL.md +117 -0
- package/skills/implementation-plans/evals/behavior.json +54 -0
- package/skills/implementation-plans/evals/triggers.json +16 -0
- package/skills/ios-platform-features/SKILL.md +99 -0
- package/skills/ios-platform-features/evals/triggers.json +16 -0
- package/skills/ios-platform-features/references/foundation-models.md +90 -0
- package/skills/jvm-sql-mappers/SKILL.md +98 -0
- package/skills/jvm-sql-mappers/evals/triggers.json +16 -0
- package/skills/jvm-sql-mappers/references/examples.md +129 -0
- package/skills/mobile-accessibility/SKILL.md +94 -0
- package/skills/mobile-accessibility/evals/triggers.json +16 -0
- package/skills/network-design/SKILL.md +98 -0
- package/skills/network-design/evals/triggers.json +16 -0
- package/skills/observability/SKILL.md +125 -0
- package/skills/observability/evals/triggers.json +17 -0
- package/skills/observability/references/otel-setup.md +87 -0
- package/skills/observability/references/slo-alerts.md +70 -0
- package/skills/open-source-release/SKILL.md +107 -0
- package/skills/open-source-release/evals/triggers.json +15 -0
- package/skills/open-source-release/references/history-scrub.md +65 -0
- package/skills/patent-research/SKILL.md +94 -0
- package/skills/patent-research/evals/triggers.json +16 -0
- package/skills/patent-research/references/databases-and-queries.md +52 -0
- package/skills/prospect-research/SKILL.md +87 -0
- package/skills/prospect-research/evals/triggers.json +16 -0
- package/skills/python-app-distribution/SKILL.md +107 -0
- package/skills/python-app-distribution/evals/triggers.json +16 -0
- package/skills/python-app-distribution/references/pypi-trusted-publishing.md +55 -0
- package/skills/python-app-distribution/references/windows-macos-signing.md +70 -0
- package/skills/quality-nonconformance/SKILL.md +107 -0
- package/skills/quality-nonconformance/evals/triggers.json +16 -0
- package/skills/quality-nonconformance/references/8d-and-capa.md +46 -0
- package/skills/react-testing/SKILL.md +121 -0
- package/skills/react-testing/evals/triggers.json +18 -0
- package/skills/react-testing/references/msw.md +64 -0
- package/skills/react-testing/references/queries.md +41 -0
- package/skills/react-testing/references/setup.md +74 -0
- package/skills/recommender-systems/SKILL.md +119 -0
- package/skills/recommender-systems/evals/triggers.json +16 -0
- package/skills/recommender-systems/references/evaluation.md +39 -0
- package/skills/refactoring-safely/evals/behavior.json +57 -0
- package/skills/reproducible-dev-environments/SKILL.md +132 -0
- package/skills/reproducible-dev-environments/evals/triggers.json +16 -0
- package/skills/responding-to-code-review/SKILL.md +124 -0
- package/skills/responding-to-code-review/evals/behavior.json +54 -0
- package/skills/responding-to-code-review/evals/triggers.json +16 -0
- package/skills/security-review/evals/behavior.json +56 -0
- package/skills/session-handoff/evals/behavior.json +55 -0
- package/skills/skill-authoring/SKILL.md +13 -2
- package/skills/skill-authoring/evals/triggers.json +2 -1
- package/skills/skill-authoring/references/pressure-testing.md +87 -0
- package/skills/slide-decks/SKILL.md +107 -0
- package/skills/slide-decks/evals/triggers.json +18 -0
- package/skills/slide-decks/references/html-decks.md +86 -0
- package/skills/slide-decks/references/layouts.md +45 -0
- package/skills/slide-decks/references/pptx.md +87 -0
- package/skills/subagent-driven-development/SKILL.md +109 -0
- package/skills/subagent-driven-development/evals/behavior.json +53 -0
- package/skills/subagent-driven-development/evals/triggers.json +15 -0
- package/skills/subagent-driven-development/references/implementer-brief.md +60 -0
- package/skills/subagent-driven-development/references/reviewer-briefs.md +52 -0
- package/skills/systematic-debugging/SKILL.md +92 -0
- package/skills/systematic-debugging/evals/behavior.json +59 -0
- package/skills/systematic-debugging/evals/triggers.json +16 -0
- package/skills/systematic-debugging/references/condition-waiting.md +52 -0
- package/skills/systematic-debugging/references/root-cause-tracing.md +49 -0
- package/skills/systematic-debugging/references/test-pollution.md +46 -0
- package/skills/tdd-workflow/SKILL.md +3 -1
- package/skills/tdd-workflow/evals/behavior.json +55 -0
- package/skills/tdd-workflow/evals/triggers.json +2 -1
- package/skills/tdd-workflow/references/testing-anti-patterns.md +87 -0
- package/skills/transactional-email/SKILL.md +68 -0
- package/skills/transactional-email/evals/triggers.json +17 -0
- package/skills/transactional-email/references/providers.md +63 -0
- package/skills/ui-demo-recording/SKILL.md +93 -0
- package/skills/ui-demo-recording/evals/triggers.json +16 -0
- package/skills/ui-demo-recording/references/cursor-and-captions.md +48 -0
- package/skills/ui-demo-recording/references/playwright-recording.md +63 -0
- package/skills/ui-demo-recording/references/post-processing.md +57 -0
- package/skills/ui-design-polish/SKILL.md +113 -0
- package/skills/ui-design-polish/evals/triggers.json +18 -0
- package/skills/ui-design-polish/references/detail-checklist.md +45 -0
- package/skills/ui-design-polish/references/tokens.md +68 -0
- package/skills/ui-flow-audit/SKILL.md +112 -0
- package/skills/ui-flow-audit/evals/triggers.json +17 -0
- package/skills/ui-motion/SKILL.md +103 -0
- package/skills/ui-motion/evals/triggers.json +18 -0
- package/skills/ui-motion/references/patterns.md +138 -0
- package/skills/ui-motion/references/tokens.md +52 -0
- package/skills/unicode-text-processing/SKILL.md +103 -0
- package/skills/unicode-text-processing/evals/triggers.json +17 -0
- package/skills/unicode-text-processing/references/apis.md +89 -0
- package/skills/unicode-text-processing/references/script-notes.md +50 -0
- package/skills/verification-loop/evals/behavior.json +52 -0
- package/skills/video-editing/SKILL.md +111 -0
- package/skills/video-editing/evals/triggers.json +16 -0
- package/skills/video-editing/references/edl-and-logging.md +44 -0
- package/skills/video-editing/references/ffmpeg-edit-recipes.md +92 -0
- package/skills/video-editing/references/resolve-scripting.md +53 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"should_block": [
|
|
3
|
+
{ "files": [{ "path": "package.json", "content": "{}" }] },
|
|
4
|
+
{ "files": [{ "path": "README.md", "content": "x" }] },
|
|
5
|
+
{ "files": [{ "path": "bin/kete-loom.js", "content": "x" }] },
|
|
6
|
+
{ "files": [{ "path": "scripts/lib/lint.js", "content": "x" }] },
|
|
7
|
+
{ "files": [{ "path": "brand-new-file.ts", "content": "x" }, { "path": "AGENTS.md", "content": "x" }] }
|
|
8
|
+
],
|
|
9
|
+
"should_allow": [
|
|
10
|
+
{ "files": [{ "path": "src/does-not-exist-yet.ts", "content": "x" }] },
|
|
11
|
+
{ "files": [{ "path": "notes/new-plan.md", "content": "x" }] },
|
|
12
|
+
{ "files": [{ "path": "package.json", "content": "{}" }] },
|
|
13
|
+
{ "files": [{ "path": "README.md", "content": "y" }] }
|
|
14
|
+
]
|
|
15
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Refuses the first edit of each existing file per session, telling the agent to read it and search for
|
|
4
|
+
// its callers, then allows the retry. check({ files: [{ path }], cwd, session }) -> verdict.
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
const KEEP_MS = 2 * 24 * 60 * 60 * 1000;
|
|
11
|
+
|
|
12
|
+
const stateFile = () => path.join(process.env.KETE_LOOM_STATE_DIR || path.join(os.homedir(), '.kete-loom', 'state'), 'read-before-edit.json');
|
|
13
|
+
|
|
14
|
+
function load(file, now) {
|
|
15
|
+
try {
|
|
16
|
+
const data = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
17
|
+
for (const k of Object.keys(data)) if (!(now - data[k] < KEEP_MS)) delete data[k];
|
|
18
|
+
return data;
|
|
19
|
+
} catch {
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function check(event, now = Date.now()) {
|
|
25
|
+
const cwd = path.resolve(event.cwd || process.cwd());
|
|
26
|
+
const scope = event.session || `${cwd}@${new Date(now).toISOString().slice(0, 10)}`;
|
|
27
|
+
const file = stateFile();
|
|
28
|
+
const seen = load(file, now);
|
|
29
|
+
const fresh = [];
|
|
30
|
+
for (const f of event.files || []) {
|
|
31
|
+
if (!f || typeof f.path !== 'string') continue;
|
|
32
|
+
const abs = path.resolve(cwd, f.path);
|
|
33
|
+
const key = `${scope}\n${abs}`;
|
|
34
|
+
if (!seen[key] && fs.existsSync(abs)) fresh.push(path.relative(cwd, abs) || abs);
|
|
35
|
+
seen[key] = now;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
39
|
+
fs.writeFileSync(file, JSON.stringify(seen));
|
|
40
|
+
} catch {
|
|
41
|
+
return { block: false }; // can't remember the refusal, so refusing would block forever
|
|
42
|
+
}
|
|
43
|
+
if (!fresh.length) return { block: false };
|
|
44
|
+
return {
|
|
45
|
+
block: true,
|
|
46
|
+
reason: `kete-loom (read-before-edit): before changing ${fresh.map((p) => `\`${p}\``).join(', ')}, read the whole file and search the codebase for what imports, calls or configures it. Then make the same edit again; it will go through.`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = { check };
|
package/hooks/run.js
CHANGED
|
@@ -7,10 +7,13 @@
|
|
|
7
7
|
// dependencies.
|
|
8
8
|
//
|
|
9
9
|
// Events: pre-shell { command, cwd }, pre-edit { files: [{ path, content }], cwd },
|
|
10
|
-
// post-edit { files: [{ path }], cwd }, session-start { cwd }
|
|
10
|
+
// post-edit { files: [{ path }], cwd }, session-start { cwd }, stop and stop-gate { cwd, again } when
|
|
11
|
+
// the agent finishes its turn. Every event also carries `session` when the host sends a session id.
|
|
11
12
|
//
|
|
12
13
|
// Blocking (pre-*): exit 2 with the reason on stderr works on every host; Cursor and Copilot also get
|
|
13
|
-
// their JSON verdict.
|
|
14
|
+
// their JSON verdict. stop-gate blocks by making the agent continue with the reason as its next
|
|
15
|
+
// prompt (exit 2 on Claude Code, Codex and Gemini CLI; JSON on Cursor and Copilot); `again` is true
|
|
16
|
+
// when this turn was itself continued by a stop hook, so a gate can let it end instead of looping. Internal errors fail OPEN (exit 0): Copilot and Kete Code deny on a crashed hook,
|
|
14
17
|
// so a runner bug must never block every tool call. post-edit and session-start never block.
|
|
15
18
|
|
|
16
19
|
const fs = require('fs');
|
|
@@ -59,20 +62,22 @@ function normalize(host, event, j) {
|
|
|
59
62
|
const cwd = pick(j, 'cwd') || (Array.isArray(j.workspace_roots) && j.workspace_roots[0]) || process.env.CURSOR_PROJECT_DIR || process.cwd();
|
|
60
63
|
const tool = j.tool || j.toolName || j.tool_name;
|
|
61
64
|
const toolArgs = () => (host === 'copilot' ? parseMaybe(j.toolArgs) : j.tool_input || j.args || {});
|
|
62
|
-
|
|
65
|
+
const session = pick(j, 'session_id', 'sessionId', 'conversation_id');
|
|
66
|
+
if (event === 'session-start') return { cwd, session };
|
|
67
|
+
if (event === 'stop' || event === 'stop-gate') return { cwd, session, again: j.stop_hook_active === true || j.loop_count > 0 };
|
|
63
68
|
if (SHELL_TOOLS[host] && event === 'pre-shell' && tool && !SHELL_TOOLS[host].includes(tool)) return null;
|
|
64
69
|
if (EDIT_TOOLS[host] && event !== 'pre-shell' && tool && !EDIT_TOOLS[host].includes(tool)) return null;
|
|
65
70
|
|
|
66
71
|
if (event === 'pre-shell') {
|
|
67
72
|
const command = host === 'cursor' ? j.command : pick(toolArgs(), 'command');
|
|
68
|
-
return typeof command === 'string' ? { command, cwd } : null;
|
|
73
|
+
return typeof command === 'string' ? { command, cwd, session, background: toolArgs().run_in_background === true } : null;
|
|
69
74
|
}
|
|
70
75
|
let files;
|
|
71
76
|
if (host === 'codex') files = parsePatch(pick(j.tool_input, 'command', 'input', 'patch'));
|
|
72
77
|
else if (host === 'cursor') files = j.file_path ? [{ path: j.file_path, content: '' }] : [];
|
|
73
78
|
else if (host === 'claude' || host === 'gemini') files = claudeFiles(j.tool_input);
|
|
74
79
|
else files = looseFiles(toolArgs());
|
|
75
|
-
return files.length ? { files, cwd } : null;
|
|
80
|
+
return files.length ? { files, cwd, session } : null;
|
|
76
81
|
}
|
|
77
82
|
|
|
78
83
|
const SESSION_JSON = {
|
|
@@ -89,7 +94,13 @@ const DENY_JSON = {
|
|
|
89
94
|
copilot: (reason) => ({ permissionDecision: 'deny', permissionDecisionReason: reason }),
|
|
90
95
|
};
|
|
91
96
|
|
|
92
|
-
|
|
97
|
+
// How each host is told to keep going instead of ending the turn.
|
|
98
|
+
const CONTINUE_JSON = {
|
|
99
|
+
cursor: (reason) => ({ followup_message: reason }),
|
|
100
|
+
copilot: (reason) => ({ decision: 'block', reason }),
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const EVENTS = ['pre-shell', 'pre-edit', 'post-edit', 'session-start', 'stop', 'stop-gate'];
|
|
93
104
|
const allow = (host, event) => ({ code: 0, stdout: event.startsWith('pre-') && ALLOW_JSON[host] ? JSON.stringify(ALLOW_JSON[host]) : '', stderr: '' });
|
|
94
105
|
|
|
95
106
|
// Returns { code, stdout, stderr } so tests can drive it without a process.
|
|
@@ -110,6 +121,11 @@ function respond(host, hookName, input, hooksDir = __dirname, event = 'pre-shell
|
|
|
110
121
|
if (event === 'session-start') {
|
|
111
122
|
return verdict.context && SESSION_JSON[host] ? { code: 0, stdout: JSON.stringify(SESSION_JSON[host](String(verdict.context))), stderr: '' } : allow(host, event);
|
|
112
123
|
}
|
|
124
|
+
if (event === 'stop-gate' && verdict.block) {
|
|
125
|
+
const reason = String(verdict.reason || `Continue: kete-loom hook ${hookName} asked for more work`);
|
|
126
|
+
if (CONTINUE_JSON[host]) return { code: 0, stdout: JSON.stringify(CONTINUE_JSON[host](reason)), stderr: '' };
|
|
127
|
+
return { code: 2, stdout: '', stderr: `${reason}\n` };
|
|
128
|
+
}
|
|
113
129
|
if (!event.startsWith('pre-') || !verdict.block) return allow(host, event);
|
|
114
130
|
const reason = String(verdict.reason || `Blocked by kete-loom hook ${hookName}`);
|
|
115
131
|
return { code: 2, stdout: DENY_JSON[host] ? JSON.stringify(DENY_JSON[host](reason)) : '', stderr: `${reason}\n` };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: verify-before-stop
|
|
3
|
+
description: When the agent says it is done, runs the project's own lint and type-check commands if files changed, and sends it back to fix any failure instead of letting it finish with broken code.
|
|
4
|
+
license: MIT
|
|
5
|
+
metadata:
|
|
6
|
+
kete-loom/version: "0.1.0"
|
|
7
|
+
kete-loom/category: testing
|
|
8
|
+
kete-loom/risk: runs-commands
|
|
9
|
+
kete-loom/event: "stop-gate"
|
|
10
|
+
kete-loom/opt-in: "true"
|
|
11
|
+
kete-loom/owner: "Kete-org"
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Verify before stop
|
|
15
|
+
|
|
16
|
+
Runs when the agent ends its turn, in a git repository only.
|
|
17
|
+
|
|
18
|
+
If the working tree has changed code files, it runs the checks the project already defines:
|
|
19
|
+
|
|
20
|
+
| Project | Checks |
|
|
21
|
+
|---|---|
|
|
22
|
+
| `package.json` | the `typecheck` / `type-check` / `check-types` script and the `lint` script, run with the package manager the lockfile belongs to |
|
|
23
|
+
| Python | `ruff check` on the changed `.py` files, when Ruff is configured and installed |
|
|
24
|
+
| Go | `go vet ./...` |
|
|
25
|
+
|
|
26
|
+
If a check fails, the turn does not end. The agent gets the failing command and the end of its output, and is told to fix the cause, not the check. It is sent back only once in a row, so a check it can't fix never traps it in a loop.
|
|
27
|
+
|
|
28
|
+
Tests are not run, because they can take far longer than a hook should. Use the `/verify` command or the `verification-loop` skill for that. Each check is limited to 75 seconds. Nothing is installed and nothing is changed.
|
|
29
|
+
|
|
30
|
+
Available in Claude Code, Codex, Gemini CLI, Cursor and GitHub Copilot. Kete Code's and OpenCode's stop hooks can't send the agent back to work.
|
|
31
|
+
|
|
32
|
+
Opt-in: `--hooks` leaves it out; install it by name with `--hook` or with `--profile strict`.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// When the agent ends its turn with changed code, runs the project's own lint and type-check commands
|
|
4
|
+
// and sends it back to fix failures. check({ cwd, again }) -> verdict.
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { execFileSync, spawnSync } = require('child_process');
|
|
9
|
+
|
|
10
|
+
const TIMEOUT_MS = 75000;
|
|
11
|
+
const CODE = /\.(?:[cm]?[jt]sx?|vue|svelte|astro|py|go|json)$/;
|
|
12
|
+
|
|
13
|
+
const git = (cwd, args) => {
|
|
14
|
+
try {
|
|
15
|
+
return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 5000 });
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function onPath(bin) {
|
|
22
|
+
const exts = process.platform === 'win32' ? (process.env.PATHEXT || '.EXE;.CMD').split(';') : [''];
|
|
23
|
+
for (const dir of (process.env.PATH || '').split(path.delimiter)) for (const ext of exts) if (dir && fs.existsSync(path.join(dir, bin + ext))) return true;
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// [label, program, args] for each check the project defines.
|
|
28
|
+
function checksFor(root, changed) {
|
|
29
|
+
const checks = [];
|
|
30
|
+
const has = (f) => fs.existsSync(path.join(root, f));
|
|
31
|
+
let pkg = null;
|
|
32
|
+
try {
|
|
33
|
+
pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
34
|
+
} catch {
|
|
35
|
+
// no package.json
|
|
36
|
+
}
|
|
37
|
+
const scripts = (pkg && pkg.scripts) || {};
|
|
38
|
+
const pm = has('pnpm-lock.yaml') ? 'pnpm' : has('yarn.lock') ? 'yarn' : has('bun.lock') || has('bun.lockb') ? 'bun' : 'npm';
|
|
39
|
+
for (const name of [['typecheck', 'type-check', 'check-types'].find((s) => scripts[s]), scripts.lint && 'lint'].filter(Boolean)) {
|
|
40
|
+
checks.push([`${pm} run ${name}`, pm, ['run', name]]);
|
|
41
|
+
}
|
|
42
|
+
const py = changed.filter((f) => f.endsWith('.py'));
|
|
43
|
+
const ruffConfigured = has('ruff.toml') || has('.ruff.toml') || /\[tool\.ruff/.test(has('pyproject.toml') ? fs.readFileSync(path.join(root, 'pyproject.toml'), 'utf8') : '');
|
|
44
|
+
if (py.length && ruffConfigured && onPath('ruff')) checks.push([`ruff check ${py.join(' ')}`, 'ruff', ['check', ...py]]);
|
|
45
|
+
if (changed.some((f) => f.endsWith('.go')) && has('go.mod') && onPath('go')) checks.push(['go vet ./...', 'go', ['vet', './...']]);
|
|
46
|
+
return checks;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function check(event) {
|
|
50
|
+
if (event.again) return { block: false };
|
|
51
|
+
const cwd = event.cwd || process.cwd();
|
|
52
|
+
const root = (git(cwd, ['rev-parse', '--show-toplevel']) || '').trim();
|
|
53
|
+
if (!root) return { block: false };
|
|
54
|
+
const changed = (git(root, ['status', '--porcelain', '--untracked-files=all']) || '')
|
|
55
|
+
.split('\n').filter(Boolean).map((l) => l.slice(3).replace(/^.* -> /, '').replace(/^"|"$/g, ''))
|
|
56
|
+
.filter((f) => CODE.test(f) && fs.existsSync(path.join(root, f)));
|
|
57
|
+
if (!changed.length) return { block: false };
|
|
58
|
+
for (const [label, program, args] of checksFor(root, changed)) {
|
|
59
|
+
const r = spawnSync(program, args, { cwd: root, encoding: 'utf8', timeout: TIMEOUT_MS, shell: process.platform === 'win32' });
|
|
60
|
+
if (r.error || r.status === 0) continue; // a check that can't start or times out says nothing about the code
|
|
61
|
+
const tail = `${r.stdout || ''}${r.stderr || ''}`.trim().split('\n').slice(-30).join('\n').slice(-2500);
|
|
62
|
+
return {
|
|
63
|
+
block: true,
|
|
64
|
+
reason: `kete-loom (verify-before-stop): \`${label}\` fails with your changes. Fix the cause in the code (don't weaken the check), run it again, and only then finish.\n\n${tail}`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return { block: false };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = { check, checksFor };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "clickhouse",
|
|
3
|
+
"description": "ClickHouse Cloud's remote MCP server: list databases and tables and run read-only SQL on services you enable it for, signed in with ClickHouse Cloud OAuth.",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"category": "data",
|
|
6
|
+
"risk": "network",
|
|
7
|
+
"transport": "http",
|
|
8
|
+
"url": "https://mcp.clickhouse.cloud/mcp",
|
|
9
|
+
"auth": "oauth",
|
|
10
|
+
"homepage": "https://clickhouse.com/docs/cloud/features/ai-ml/remote-mcp"
|
|
11
|
+
}
|
package/mcp/exa.json
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "exa",
|
|
3
|
+
"description": "Exa's remote MCP server: web search, page contents and code search for the model. Free and rate-limited without a key; no sign-in needed.",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"category": "docs",
|
|
6
|
+
"risk": "network",
|
|
7
|
+
"transport": "http",
|
|
8
|
+
"url": "https://mcp.exa.ai/mcp",
|
|
9
|
+
"auth": "none",
|
|
10
|
+
"homepage": "https://exa.ai/docs/reference/exa-mcp"
|
|
11
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "firecrawl",
|
|
3
|
+
"description": "Firecrawl's remote MCP server: scrape, crawl, map and search websites into clean Markdown or structured data, signed in with Firecrawl OAuth.",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"category": "data",
|
|
6
|
+
"risk": "network",
|
|
7
|
+
"transport": "http",
|
|
8
|
+
"url": "https://mcp.firecrawl.dev/v2/mcp-oauth",
|
|
9
|
+
"auth": "oauth",
|
|
10
|
+
"homepage": "https://docs.firecrawl.dev/mcp-server"
|
|
11
|
+
}
|
package/mcp/vercel.json
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vercel",
|
|
3
|
+
"description": "Vercel's remote MCP server: search the docs, manage projects and deployments, read deployment logs and query Web Analytics, signed in with Vercel OAuth.",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"category": "devops",
|
|
6
|
+
"risk": "network",
|
|
7
|
+
"transport": "http",
|
|
8
|
+
"url": "https://mcp.vercel.com",
|
|
9
|
+
"auth": "oauth",
|
|
10
|
+
"homepage": "https://vercel.com/docs/agent-resources/vercel-mcp"
|
|
11
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ketecode/loom",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Kete Loom: curated, tested, portable Agent Skills, subagents, commands, rules, hooks and MCP configs. One source, installable in Claude Code, Codex, OpenCode, Cursor, Gemini CLI, GitHub Copilot
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Kete Loom: curated, tested, portable Agent Skills, subagents, commands, rules, hooks and MCP configs. One source, installable in Claude Code, Codex, OpenCode, Cursor, Gemini CLI, GitHub Copilot, Kete Code and Kiro.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Kete-org <support@ketecode.ai>",
|
|
7
7
|
"homepage": "https://ketecode.ai",
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"cursor",
|
|
20
20
|
"gemini-cli",
|
|
21
21
|
"copilot",
|
|
22
|
-
"kete-code"
|
|
22
|
+
"kete-code",
|
|
23
|
+
"kiro"
|
|
23
24
|
],
|
|
24
25
|
"bin": {
|
|
25
26
|
"kete-loom": "bin/kete-loom.js"
|
|
@@ -46,9 +47,10 @@
|
|
|
46
47
|
},
|
|
47
48
|
"scripts": {
|
|
48
49
|
"lint": "node scripts/lint-skills.js",
|
|
49
|
-
"eval": "node scripts/run-evals.js",
|
|
50
|
+
"eval": "node scripts/run-evals.js && node scripts/run-behavior-evals.js",
|
|
50
51
|
"eval:estimate": "node scripts/run-evals.js --estimate",
|
|
51
52
|
"eval:live": "node scripts/run-evals.js --live",
|
|
53
|
+
"eval:behavior": "node scripts/run-behavior-evals.js",
|
|
52
54
|
"test": "node --test",
|
|
53
55
|
"check": "npm run lint && npm test && npm run eval",
|
|
54
56
|
"catalog": "node scripts/build-catalog.js",
|
package/profiles.json
CHANGED
|
@@ -66,6 +66,52 @@
|
|
|
66
66
|
"guard-secrets"
|
|
67
67
|
]
|
|
68
68
|
},
|
|
69
|
+
"strict": {
|
|
70
|
+
"description": "Developer plus the opt-in guardrails: read files before editing them, don't weaken lint configs, keep dev servers in the background, and pass lint and type-check before finishing.",
|
|
71
|
+
"extends": [
|
|
72
|
+
"developer"
|
|
73
|
+
],
|
|
74
|
+
"hooks": [
|
|
75
|
+
"read-before-edit",
|
|
76
|
+
"protect-lint-config",
|
|
77
|
+
"block-foreground-servers",
|
|
78
|
+
"verify-before-stop"
|
|
79
|
+
]
|
|
80
|
+
},
|
|
81
|
+
"method": {
|
|
82
|
+
"description": "A full development method: design before code, granular plans, a fresh subagent per task with spec and quality review, systematic debugging, rigorous responses to review, worktrees and a clean finish.",
|
|
83
|
+
"skills": [
|
|
84
|
+
"design-brainstorming",
|
|
85
|
+
"implementation-plans",
|
|
86
|
+
"subagent-driven-development",
|
|
87
|
+
"systematic-debugging",
|
|
88
|
+
"tdd-workflow",
|
|
89
|
+
"verification-loop",
|
|
90
|
+
"responding-to-code-review",
|
|
91
|
+
"git-worktrees",
|
|
92
|
+
"finishing-a-branch"
|
|
93
|
+
],
|
|
94
|
+
"agents": [
|
|
95
|
+
"planner",
|
|
96
|
+
"task-implementer",
|
|
97
|
+
"spec-reviewer",
|
|
98
|
+
"code-reviewer"
|
|
99
|
+
],
|
|
100
|
+
"commands": [
|
|
101
|
+
"brainstorm",
|
|
102
|
+
"plan-feature",
|
|
103
|
+
"feature-dev",
|
|
104
|
+
"debug",
|
|
105
|
+
"verify",
|
|
106
|
+
"review",
|
|
107
|
+
"finish-branch"
|
|
108
|
+
],
|
|
109
|
+
"rules": [
|
|
110
|
+
"use-skills",
|
|
111
|
+
"common",
|
|
112
|
+
"testing"
|
|
113
|
+
]
|
|
114
|
+
},
|
|
69
115
|
"security": {
|
|
70
116
|
"description": "Security and compliance: every security and compliance item, the essentials, and the four safety hooks.",
|
|
71
117
|
"extends": [
|
|
@@ -120,16 +166,18 @@
|
|
|
120
166
|
]
|
|
121
167
|
},
|
|
122
168
|
"full": {
|
|
123
|
-
"description": "Everything in Kete Loom,
|
|
169
|
+
"description": "Everything in Kete Loom, with every hook that isn't opt-in (add those with --hook or the strict profile).",
|
|
124
170
|
"all": true
|
|
125
171
|
},
|
|
126
172
|
"mcp-web": {
|
|
127
|
-
"description": "MCP pack for web work: Playwright and Chrome DevTools browsers, Context7 library docs and
|
|
173
|
+
"description": "MCP pack for web work: Playwright and Chrome DevTools browsers, Context7 library docs, Fetch, Exa web search and Firecrawl scraping.",
|
|
128
174
|
"mcp": [
|
|
129
175
|
"playwright",
|
|
130
176
|
"chrome-devtools",
|
|
131
177
|
"context7",
|
|
132
|
-
"fetch"
|
|
178
|
+
"fetch",
|
|
179
|
+
"exa",
|
|
180
|
+
"firecrawl"
|
|
133
181
|
]
|
|
134
182
|
},
|
|
135
183
|
"mcp-code": {
|
|
@@ -149,11 +197,12 @@
|
|
|
149
197
|
]
|
|
150
198
|
},
|
|
151
199
|
"mcp-cloud": {
|
|
152
|
-
"description": "MCP pack for cloud and payments: Cloudflare, Supabase (read-only) and Stripe.",
|
|
200
|
+
"description": "MCP pack for cloud and payments: Cloudflare, Vercel, Supabase (read-only) and Stripe.",
|
|
153
201
|
"mcp": [
|
|
154
202
|
"cloudflare",
|
|
155
203
|
"supabase",
|
|
156
|
-
"stripe"
|
|
204
|
+
"stripe",
|
|
205
|
+
"vercel"
|
|
157
206
|
]
|
|
158
207
|
},
|
|
159
208
|
"mcp-design": {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: use-skills
|
|
3
|
+
description: Always-on rule to check for an installed skill, agent or command that fits a task before starting it, say which one is used, and follow its steps rather than working from memory.
|
|
4
|
+
license: MIT
|
|
5
|
+
metadata:
|
|
6
|
+
kete-loom/version: "0.1.0"
|
|
7
|
+
kete-loom/category: meta
|
|
8
|
+
kete-loom/risk: read-only
|
|
9
|
+
kete-loom/owner: "Kete-org"
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
- Before starting a task, including a clarifying question or a quick look at the code, check whether an installed skill, agent or command covers it. Check again when the task changes.
|
|
13
|
+
- If one fits even partly, load it and tell the user in one line which one you are using and why.
|
|
14
|
+
- Prefer the most specific match: a framework skill over a language skill, a language reviewer over a general one.
|
|
15
|
+
- Process skills (brainstorming, debugging, test-first, planning) come before implementation skills; they decide how the work is done.
|
|
16
|
+
- Follow a loaded skill's steps in order. Do not skip a step because you remember the gist or the task looks simple.
|
|
17
|
+
- Where a skill has a checklist, work through every item and report each one.
|
|
18
|
+
- Treat a skill's "stop and ask" points as real stops; do not guess past them.
|
|
19
|
+
- Read a skill's `references/` files when its steps tell you to, not from memory of similar files.
|
|
20
|
+
- Delegate to an agent when its description matches and its separate context helps (reviews, wide searches, build fixes).
|
|
21
|
+
- If no skill fits, say so briefly and work normally; do not force a poor match.
|
|
22
|
+
- The user's explicit instructions win over any skill. When they conflict, follow the user and mention the conflict once.
|
|
23
|
+
- Never claim to have followed a skill you did not load.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Behaviour evals: evals/behavior.json in a skill or agent folder. Each scenario is a user request
|
|
4
|
+
// (often with pressure to skip the item's discipline), optional files the agent can see, and what the
|
|
5
|
+
// reply must and must not show. scripts/run-behavior-evals.js runs them with and without the item.
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
const PRESSURES = ['time', 'sunk-cost', 'authority', 'exhaustion', 'social', 'pragmatism', 'small-scope'];
|
|
11
|
+
const MIN_SCENARIOS = 2;
|
|
12
|
+
|
|
13
|
+
const behaviorFile = (item) => path.join(item.path, 'evals', 'behavior.json');
|
|
14
|
+
|
|
15
|
+
// Returns null when the item has no behavior.json, else { scenarios, problems }.
|
|
16
|
+
function loadBehavior(item) {
|
|
17
|
+
const file = behaviorFile(item);
|
|
18
|
+
if (!fs.existsSync(file)) return null;
|
|
19
|
+
let data;
|
|
20
|
+
try {
|
|
21
|
+
data = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
22
|
+
} catch (e) {
|
|
23
|
+
return { scenarios: [], problems: [`evals/behavior.json is not valid JSON: ${e.message}`] };
|
|
24
|
+
}
|
|
25
|
+
return { scenarios: Array.isArray(data.scenarios) ? data.scenarios : [], problems: validate(data) };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function validate(data) {
|
|
29
|
+
const problems = [];
|
|
30
|
+
const list = data && Array.isArray(data.scenarios) ? data.scenarios : null;
|
|
31
|
+
if (!list) return ['evals/behavior.json needs a "scenarios" array'];
|
|
32
|
+
if (list.length < MIN_SCENARIOS) problems.push(`evals/behavior.json needs at least ${MIN_SCENARIOS} scenarios`);
|
|
33
|
+
const names = new Set();
|
|
34
|
+
list.forEach((s, i) => {
|
|
35
|
+
const at = `scenario ${i + 1}${s && typeof s.name === 'string' ? ` "${s.name}"` : ''}`;
|
|
36
|
+
if (!s || typeof s !== 'object') return problems.push(`${at} is not an object`);
|
|
37
|
+
if (typeof s.name !== 'string' || !s.name.trim()) problems.push(`${at} needs a name`);
|
|
38
|
+
else if (names.has(s.name)) problems.push(`${at}: duplicate name`);
|
|
39
|
+
else names.add(s.name);
|
|
40
|
+
if (typeof s.prompt !== 'string' || s.prompt.trim().length < 20) problems.push(`${at}: prompt must be at least 20 characters`);
|
|
41
|
+
const strings = (v) => Array.isArray(v) && v.every((x) => typeof x === 'string' && x.trim().length >= 10);
|
|
42
|
+
if (!strings(s.expect) || !s.expect.length) problems.push(`${at}: "expect" must be a non-empty list of criteria (10+ characters each)`);
|
|
43
|
+
if (s.forbid !== undefined && !strings(s.forbid)) problems.push(`${at}: "forbid" must be a list of criteria (10+ characters each)`);
|
|
44
|
+
if (s.pressure !== undefined && (!Array.isArray(s.pressure) || s.pressure.some((p) => !PRESSURES.includes(p)))) problems.push(`${at}: "pressure" values must be from ${PRESSURES.join(', ')}`);
|
|
45
|
+
if (s.files !== undefined && (typeof s.files !== 'object' || Array.isArray(s.files) || Object.entries(s.files).some(([k, v]) => typeof v !== 'string' || path.isAbsolute(k) || k.split(/[\\/]/).includes('..')))) {
|
|
46
|
+
problems.push(`${at}: "files" maps relative paths to file contents`);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
return problems;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = { loadBehavior, validate, behaviorFile, PRESSURES, MIN_SCENARIOS };
|
|
@@ -13,7 +13,7 @@ const path = require('path');
|
|
|
13
13
|
|
|
14
14
|
const RUNNER_RE = /\.kete-loom[\\/]hooks[\\/]run\.js"?\s+([a-z]+)\s+([a-z0-9-]+)(?:\s+([a-z-]+))?\s*$/;
|
|
15
15
|
// Seconds. A formatter run after an edit gets longer than a check before one.
|
|
16
|
-
const TIMEOUT_S = { 'pre-shell': 10, 'pre-edit': 10, 'post-edit': 60, 'session-start': 10 };
|
|
16
|
+
const TIMEOUT_S = { 'pre-shell': 10, 'pre-edit': 10, 'post-edit': 60, 'session-start': 10, stop: 10, 'stop-gate': 180 };
|
|
17
17
|
|
|
18
18
|
const runtimeDir = (home) => path.join(home, '.kete-loom', 'hooks');
|
|
19
19
|
const command = (home, host, hook, event = 'pre-shell', node = process.execPath) =>
|
|
@@ -26,13 +26,15 @@ function ours(cmd, hook) {
|
|
|
26
26
|
|
|
27
27
|
// Host event name and tool matcher for each kete-loom event; a missing entry means the host can't
|
|
28
28
|
// run that event (Cursor has no documented pre-edit hook; OpenCode can't add context at session start).
|
|
29
|
+
// stop-gate needs a stop hook that can make the agent continue: Kete Code's Stop can only notify, and
|
|
30
|
+
// OpenCode's session.idle can't do either from a command.
|
|
29
31
|
const EVENTS = {
|
|
30
|
-
claude: { 'pre-shell': ['PreToolUse', 'Bash'], 'pre-edit': ['PreToolUse', 'Edit|Write|NotebookEdit'], 'post-edit': ['PostToolUse', 'Edit|Write|NotebookEdit'], 'session-start': ['SessionStart'] },
|
|
31
|
-
codex: { 'pre-shell': ['PreToolUse', 'Bash'], 'pre-edit': ['PreToolUse', 'apply_patch'], 'post-edit': ['PostToolUse', 'apply_patch'], 'session-start': ['SessionStart'] },
|
|
32
|
-
gemini: { 'pre-shell': ['BeforeTool', 'run_shell_command'], 'pre-edit': ['BeforeTool', 'write_file|replace'], 'post-edit': ['AfterTool', 'write_file|replace'], 'session-start': ['SessionStart'] },
|
|
33
|
-
kete: { 'pre-shell': ['PreToolUse', 'shell'], 'pre-edit': ['PreToolUse', 'edit|write|patch'], 'post-edit': ['PostToolUse', 'edit|write|patch'], 'session-start': ['SessionStart'] },
|
|
34
|
-
cursor: { 'pre-shell': ['beforeShellExecution'], 'post-edit': ['afterFileEdit'], 'session-start': ['sessionStart'] },
|
|
35
|
-
copilot: { 'pre-shell': ['preToolUse'], 'pre-edit': ['preToolUse'], 'post-edit': ['postToolUse'], 'session-start': ['sessionStart'] },
|
|
32
|
+
claude: { 'pre-shell': ['PreToolUse', 'Bash'], 'pre-edit': ['PreToolUse', 'Edit|Write|NotebookEdit'], 'post-edit': ['PostToolUse', 'Edit|Write|NotebookEdit'], 'session-start': ['SessionStart'], stop: ['Stop'], 'stop-gate': ['Stop'] },
|
|
33
|
+
codex: { 'pre-shell': ['PreToolUse', 'Bash'], 'pre-edit': ['PreToolUse', 'apply_patch'], 'post-edit': ['PostToolUse', 'apply_patch'], 'session-start': ['SessionStart'], stop: ['Stop'], 'stop-gate': ['Stop'] },
|
|
34
|
+
gemini: { 'pre-shell': ['BeforeTool', 'run_shell_command'], 'pre-edit': ['BeforeTool', 'write_file|replace'], 'post-edit': ['AfterTool', 'write_file|replace'], 'session-start': ['SessionStart'], stop: ['AfterAgent'], 'stop-gate': ['AfterAgent'] },
|
|
35
|
+
kete: { 'pre-shell': ['PreToolUse', 'shell'], 'pre-edit': ['PreToolUse', 'edit|write|patch'], 'post-edit': ['PostToolUse', 'edit|write|patch'], 'session-start': ['SessionStart'], stop: ['Stop'] },
|
|
36
|
+
cursor: { 'pre-shell': ['beforeShellExecution'], 'post-edit': ['afterFileEdit'], 'session-start': ['sessionStart'], stop: ['stop'], 'stop-gate': ['stop'] },
|
|
37
|
+
copilot: { 'pre-shell': ['preToolUse'], 'pre-edit': ['preToolUse'], 'post-edit': ['postToolUse'], 'session-start': ['sessionStart'], stop: ['agentStop'], 'stop-gate': ['agentStop'] },
|
|
36
38
|
opencode: { 'pre-shell': ['tool.execute.before'], 'pre-edit': ['tool.execute.before'], 'post-edit': ['tool.execute.after'] },
|
|
37
39
|
};
|
|
38
40
|
|
package/scripts/lib/lint.js
CHANGED
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
// repository-wide checks on top.
|
|
6
6
|
|
|
7
7
|
const fs = require('fs');
|
|
8
|
+
const os = require('os');
|
|
8
9
|
const path = require('path');
|
|
9
10
|
const { TOOLS } = require('./render');
|
|
11
|
+
const { loadBehavior } = require('./behavior');
|
|
10
12
|
|
|
11
13
|
const NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
12
14
|
const SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
|
@@ -15,7 +17,7 @@ const CATEGORIES = ['coding', 'testing', 'security', 'integrations', 'devops', '
|
|
|
15
17
|
const RISKS = ['read-only', 'writes-files', 'runs-commands', 'network'];
|
|
16
18
|
const MAX_LINES = 300;
|
|
17
19
|
const MIN_EVALS = 3;
|
|
18
|
-
const HOOK_EVENTS = ['pre-shell', 'pre-edit', 'post-edit', 'session-start'];
|
|
20
|
+
const HOOK_EVENTS = ['pre-shell', 'pre-edit', 'post-edit', 'session-start', 'stop', 'stop-gate'];
|
|
19
21
|
const MAX_RULE_LINES = 80;
|
|
20
22
|
// Tool names that only exist in one host. Skills should describe the action instead.
|
|
21
23
|
const HOST_SPECIFIC = /\b(Bash tool|Read tool|Edit tool|Write tool|Grep tool|Glob tool|TodoWrite|apply_patch|str_replace_editor)\b/;
|
|
@@ -112,6 +114,12 @@ function lintItem(item) {
|
|
|
112
114
|
}
|
|
113
115
|
}
|
|
114
116
|
|
|
117
|
+
// Behaviour evals are optional; when present they must be runnable.
|
|
118
|
+
if (item.kind === 'skill' || item.kind === 'agent') {
|
|
119
|
+
const behavior = loadBehavior(item);
|
|
120
|
+
if (behavior) errors.push(...behavior.problems);
|
|
121
|
+
}
|
|
122
|
+
|
|
115
123
|
const hostHit = HOST_SPECIFIC.exec(item.body);
|
|
116
124
|
if (hostHit) warnings.push(`mentions host-specific tool "${hostHit[1]}"; describe the action instead so every host can follow it`);
|
|
117
125
|
|
|
@@ -138,6 +146,21 @@ function lintHook(hook) {
|
|
|
138
146
|
} catch (e) {
|
|
139
147
|
return [...errors, `cases.json missing or invalid: ${e.message}`];
|
|
140
148
|
}
|
|
149
|
+
// Hooks that remember what they've seen keep it under KETE_LOOM_STATE_DIR; give each lint run a fresh one.
|
|
150
|
+
const prevState = process.env.KETE_LOOM_STATE_DIR;
|
|
151
|
+
const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kete-loom-lint-'));
|
|
152
|
+
process.env.KETE_LOOM_STATE_DIR = stateDir;
|
|
153
|
+
try {
|
|
154
|
+
lintCases(check, cases, errors);
|
|
155
|
+
} finally {
|
|
156
|
+
if (prevState === undefined) delete process.env.KETE_LOOM_STATE_DIR;
|
|
157
|
+
else process.env.KETE_LOOM_STATE_DIR = prevState;
|
|
158
|
+
fs.rmSync(stateDir, { recursive: true, force: true });
|
|
159
|
+
}
|
|
160
|
+
return errors;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function lintCases(check, cases, errors) {
|
|
141
164
|
for (const [key, want] of [['should_block', true], ['should_allow', false]]) {
|
|
142
165
|
if (!Array.isArray(cases[key]) || cases[key].length < MIN_EVALS) errors.push(`cases.json needs at least ${MIN_EVALS} "${key}" cases`);
|
|
143
166
|
for (const c of cases[key] || []) {
|
|
@@ -152,7 +175,6 @@ function lintHook(hook) {
|
|
|
152
175
|
if (got !== want) errors.push(`case ${JSON.stringify(c)} should ${want ? 'block' : 'be allowed'}`);
|
|
153
176
|
}
|
|
154
177
|
}
|
|
155
|
-
return errors;
|
|
156
178
|
}
|
|
157
179
|
|
|
158
180
|
module.exports = { lintItem, lintHook, words, jaccard, NAME_RE, SEMVER_RE, CATEGORIES, RISKS, MAX_LINES, HOOK_EVENTS };
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// Copilot CLI ~/.copilot/mcp-config.json `mcpServers`; local servers only (its OAuth isn't documented)
|
|
11
11
|
// OpenCode opencode.json `mcp` (command is an array)
|
|
12
12
|
// Kete Code kete.json `mcp.servers`
|
|
13
|
+
// Kiro ~/.kiro/settings/mcp.json `mcpServers` (remote servers sign in with OAuth)
|
|
13
14
|
// An entry is ours when it equals what we would write, so a server you configured yourself under the
|
|
14
15
|
// same name is never touched. Claude Code's CLI-managed servers are tracked in ~/.kete-loom/mcp.json.
|
|
15
16
|
|
|
@@ -33,6 +34,7 @@ const RENDER = {
|
|
|
33
34
|
copilot: (s) => (http(s) ? null : { type: 'local', command: s.command, args: s.args, tools: ['*'] }),
|
|
34
35
|
opencode: (s) => (http(s) ? { type: 'remote', url: s.url, enabled: true } : { type: 'local', command: [s.command, ...s.args], enabled: true }),
|
|
35
36
|
kete: (s) => (http(s) ? { type: 'remote', url: s.url } : { type: 'local', command: [s.command, ...s.args] }),
|
|
37
|
+
kiro: (s) => (http(s) ? { url: s.url } : { command: s.command, args: s.args }),
|
|
36
38
|
};
|
|
37
39
|
|
|
38
40
|
const SIGN_IN = {
|
|
@@ -42,6 +44,7 @@ const SIGN_IN = {
|
|
|
42
44
|
cursor: (n) => `Cursor asks you to sign in to ${n} the first time it connects`,
|
|
43
45
|
opencode: (n) => `opencode mcp auth ${n}`,
|
|
44
46
|
kete: (n) => `kete mcp auth ${n}`,
|
|
47
|
+
kiro: (n) => `Kiro opens a browser to sign in to ${n} the first time it connects`,
|
|
45
48
|
};
|
|
46
49
|
|
|
47
50
|
const keteFile = (dir) => (fs.existsSync(path.join(dir, 'kete.jsonc')) ? path.join(dir, 'kete.jsonc') : path.join(dir, 'kete.json'));
|
|
@@ -54,8 +57,9 @@ const JSON_HOSTS = {
|
|
|
54
57
|
copilot: { file: { user: (h) => path.join(process.env.COPILOT_HOME || path.join(h, '.copilot'), 'mcp-config.json') }, holder: (d) => (d.mcpServers ||= {}), standalone: true },
|
|
55
58
|
opencode: { file: { user: (h) => path.join(h, '.config', 'opencode', 'opencode.json'), project: (cwd) => path.join(cwd, 'opencode.json') }, holder: (d) => (d.mcp ||= {}) },
|
|
56
59
|
kete: { file: { user: (h) => keteFile(path.join(h, '.config', 'kete')), project: (cwd) => keteFile(path.join(cwd, '.kete')) }, holder: (d) => ((d.mcp ||= {}).servers ||= {}) },
|
|
60
|
+
kiro: { file: { user: (h) => path.join(h, '.kiro', 'settings', 'mcp.json'), project: (cwd) => path.join(cwd, '.kiro', 'settings', 'mcp.json') }, holder: (d) => (d.mcpServers ||= {}), standalone: true },
|
|
57
61
|
};
|
|
58
|
-
const MCP_HOSTS = ['claude', 'codex', 'gemini', 'cursor', 'copilot', 'opencode', 'kete'];
|
|
62
|
+
const MCP_HOSTS = ['claude', 'codex', 'gemini', 'cursor', 'copilot', 'opencode', 'kete', 'kiro'];
|
|
59
63
|
|
|
60
64
|
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
61
65
|
|