@mutmutco/kilo-plugin 3.79.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/agent/reviewer.md +108 -0
- package/package.json +23 -0
- package/scripts/command-ladder-core.mjs +334 -0
- package/scripts/command-ladder-gate.mjs +126 -0
- package/scripts/deny-gate-crash.mjs +179 -0
- package/scripts/edit-tool-paths.mjs +113 -0
- package/scripts/env-write-lint.mjs +137 -0
- package/scripts/hook-io.mjs +17 -0
- package/scripts/hook-policy.mjs +73 -0
- package/scripts/hook-run.mjs +170 -0
- package/scripts/hook-trace.mjs +108 -0
- package/scripts/pretooluse-shell-gates.mjs +420 -0
- package/scripts/secret-echo-lint.mjs +170 -0
- package/scripts/secret-redact.mjs +537 -0
- package/scripts/throttle-core.mjs +324 -0
- package/scripts/validate-hook.mjs +156 -0
- package/scripts/vault-edit-gate.mjs +94 -0
- package/server.mjs +237 -0
- package/skills/bootstrap/SKILL.md +493 -0
- package/skills/bootstrap/seeds/Dockerfile.template +30 -0
- package/skills/bootstrap/seeds/README.template.md +36 -0
- package/skills/bootstrap/seeds/architecture.template.md +34 -0
- package/skills/bootstrap/seeds/decisions-readme.template.md +46 -0
- package/skills/bootstrap/seeds/docker-compose.template.yml +26 -0
- package/skills/bootstrap/seeds/gate.template.yml +90 -0
- package/skills/bootstrap/seeds/google-login.template.md +33 -0
- package/skills/bootstrap/seeds/manifest.json +26 -0
- package/skills/bootstrap/seeds/mmi-product-required-checks.template.json +23 -0
- package/skills/browser-automation/SKILL.md +93 -0
- package/skills/doctor/SKILL.md +76 -0
- package/skills/epic/SKILL.md +87 -0
- package/skills/hotfix/SKILL.md +113 -0
- package/skills/mmi/SKILL.md +400 -0
- package/skills/onboard/SKILL.md +70 -0
- package/skills/rcand/SKILL.md +194 -0
- package/skills/release/SKILL.md +546 -0
- package/skills/resume/SKILL.md +68 -0
- package/skills/secrets/SKILL.md +157 -0
- package/skills/stage/SKILL.md +151 -0
- package/skills/worktree/SKILL.md +86 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Shared hook process core. Every active host calls this runner with only its surface token and a
|
|
3
|
+
// policy gate id. Host-specific event/tool matching stays in the adapter; policy and failure behavior
|
|
4
|
+
// stay here.
|
|
5
|
+
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
8
|
+
import { dirname, join, resolve } from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { hookGate, hookSurface } from './hook-policy.mjs';
|
|
11
|
+
|
|
12
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
|
|
14
|
+
export function parseHookArgv(argv) {
|
|
15
|
+
const parsed = { surface: '', gate: '' };
|
|
16
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
17
|
+
if (argv[index] === '--surface') parsed.surface = argv[++index] ?? '';
|
|
18
|
+
else if (argv[index] === '--gate') parsed.gate = argv[++index] ?? '';
|
|
19
|
+
}
|
|
20
|
+
return parsed;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function pluginRoot(surfaceToken, env = process.env, here = HERE) {
|
|
24
|
+
const surface = hookSurface(surfaceToken);
|
|
25
|
+
for (const name of surface.rootEnv) {
|
|
26
|
+
const candidate = env[name];
|
|
27
|
+
if (candidate && existsSync(join(candidate, 'scripts', 'hook-policy.mjs'))) return resolve(candidate);
|
|
28
|
+
}
|
|
29
|
+
return resolve(here, '..');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function payloadMeta(input) {
|
|
33
|
+
try {
|
|
34
|
+
const payload = JSON.parse(Buffer.from(input).toString('utf8'));
|
|
35
|
+
return {
|
|
36
|
+
sessionId: typeof payload?.session_id === 'string' ? payload.session_id : '',
|
|
37
|
+
cwd: typeof payload?.cwd === 'string' ? payload.cwd : '',
|
|
38
|
+
};
|
|
39
|
+
} catch {
|
|
40
|
+
return { sessionId: '', cwd: '' };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizeInput(surface, gate, input) {
|
|
45
|
+
if (surface !== 'cursor') return input;
|
|
46
|
+
try {
|
|
47
|
+
const payload = JSON.parse(Buffer.from(input).toString('utf8'));
|
|
48
|
+
if (!payload || typeof payload !== 'object') return input;
|
|
49
|
+
if (payload.tool_name === 'Shell') {
|
|
50
|
+
payload.tool_name = process.platform === 'win32' ? 'PowerShell' : 'Bash';
|
|
51
|
+
}
|
|
52
|
+
if (!payload.session_id && typeof payload.conversation_id === 'string') {
|
|
53
|
+
payload.session_id = payload.conversation_id;
|
|
54
|
+
}
|
|
55
|
+
if (gate === 'secret-output' && payload.tool_response == null && payload.tool_output != null) {
|
|
56
|
+
payload.tool_response = payload.tool_output;
|
|
57
|
+
}
|
|
58
|
+
return Buffer.from(JSON.stringify(payload));
|
|
59
|
+
} catch {
|
|
60
|
+
return input;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function adaptOutput(surface, stdout) {
|
|
65
|
+
if (surface !== 'cursor' || !stdout.trim()) return stdout;
|
|
66
|
+
try {
|
|
67
|
+
const payload = JSON.parse(stdout);
|
|
68
|
+
const decision = payload?.hookSpecificOutput;
|
|
69
|
+
if (decision?.permissionDecision !== 'deny') return '';
|
|
70
|
+
const reason = decision.permissionDecisionReason || 'MMI policy denied this tool call.';
|
|
71
|
+
return `${JSON.stringify({ permission: 'deny', user_message: reason, agent_message: reason })}\n`;
|
|
72
|
+
} catch {
|
|
73
|
+
return stdout;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function childEnv(surface, input, env) {
|
|
78
|
+
const meta = payloadMeta(input);
|
|
79
|
+
return {
|
|
80
|
+
...env,
|
|
81
|
+
MMI_HOOK_SURFACE: surface,
|
|
82
|
+
...(meta.sessionId && !env.MMI_GATE_SESSION_ID ? { MMI_GATE_SESSION_ID: meta.sessionId } : {}),
|
|
83
|
+
...(meta.cwd && !env.MMI_HOOK_ACTIVITY_CWD ? { MMI_HOOK_ACTIVITY_CWD: meta.cwd } : {}),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function inlineDeny(surface, gate, root) {
|
|
88
|
+
return `${JSON.stringify({
|
|
89
|
+
hookSpecificOutput: {
|
|
90
|
+
hookEventName: 'PreToolUse',
|
|
91
|
+
permissionDecision: 'deny',
|
|
92
|
+
permissionDecisionReason: `MMI ${gate} gate could not run on ${surface} and its fail-closed fallback is missing from the plugin install at ${root}. Reinstall the MMI plugin, or set MMI_GATES_FAIL_OPEN=1 to proceed unguarded.`,
|
|
93
|
+
},
|
|
94
|
+
})}\n`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function invoke(target, args, input, env, timeoutMs) {
|
|
98
|
+
const result = spawnSync(process.execPath, [target, ...args], {
|
|
99
|
+
input,
|
|
100
|
+
encoding: 'utf8',
|
|
101
|
+
env,
|
|
102
|
+
timeout: timeoutMs,
|
|
103
|
+
windowsHide: true,
|
|
104
|
+
});
|
|
105
|
+
return {
|
|
106
|
+
status: result.status ?? 1,
|
|
107
|
+
stdout: typeof result.stdout === 'string' ? result.stdout : '',
|
|
108
|
+
stderr: typeof result.stderr === 'string' ? result.stderr : '',
|
|
109
|
+
error: result.error,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function runPolicyGate({ surface, gate: gateId, input = Buffer.alloc(0), env = process.env, here = HERE }) {
|
|
114
|
+
const gate = hookGate(gateId);
|
|
115
|
+
hookSurface(surface);
|
|
116
|
+
const root = pluginRoot(surface, env, here);
|
|
117
|
+
const target = join(root, 'scripts', gate.script);
|
|
118
|
+
const normalizedInput = normalizeInput(surface, gateId, input);
|
|
119
|
+
const effectiveEnv = childEnv(surface, normalizedInput, env);
|
|
120
|
+
let result;
|
|
121
|
+
|
|
122
|
+
if (!existsSync(target)) {
|
|
123
|
+
result = { status: 1, stdout: '', stderr: `[mmi-${surface}-hook] ${gate.script} not found under ${root}\n` };
|
|
124
|
+
} else {
|
|
125
|
+
result = invoke(target, [], normalizedInput, effectiveEnv, gate.timeoutMs);
|
|
126
|
+
if (result.status === 0 || result.status === 2) {
|
|
127
|
+
return { ...result, stdout: adaptOutput(surface, result.stdout) };
|
|
128
|
+
}
|
|
129
|
+
result.stderr += `[mmi-${surface}-hook] ${gate.script} exited ${result.status}\n`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (gate.failure === 'open') return { ...result, status: 0 };
|
|
133
|
+
|
|
134
|
+
const crashGate = join(root, 'scripts', 'deny-gate-crash.mjs');
|
|
135
|
+
if (!existsSync(crashGate)) {
|
|
136
|
+
return { status: 0, stdout: inlineDeny(surface, gate.fallbackGate, root), stderr: result.stderr };
|
|
137
|
+
}
|
|
138
|
+
const fallback = invoke(crashGate, [gate.fallbackGate], normalizedInput, effectiveEnv, gate.timeoutMs);
|
|
139
|
+
return {
|
|
140
|
+
...fallback,
|
|
141
|
+
stdout: adaptOutput(surface, fallback.stdout),
|
|
142
|
+
stderr: `${result.stderr}${fallback.stderr}`,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function readStdin() {
|
|
147
|
+
try {
|
|
148
|
+
return readFileSync(0);
|
|
149
|
+
} catch {
|
|
150
|
+
return Buffer.alloc(0);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function main(argv = process.argv.slice(2)) {
|
|
155
|
+
const { surface, gate } = parseHookArgv(argv);
|
|
156
|
+
let result;
|
|
157
|
+
try {
|
|
158
|
+
result = runPolicyGate({ surface, gate, input: readStdin() });
|
|
159
|
+
} catch (error) {
|
|
160
|
+
process.stderr.write(`[mmi-hook] ${error.message}\n`);
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
164
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
165
|
+
process.exit(result.status);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Codex may import this file from a replacement cache while process.argv[1] still names the deleted
|
|
169
|
+
// cache path captured when the session loaded. Match the stable relative suffix, not URL identity.
|
|
170
|
+
if (process.argv[1] && process.argv[1].replace(/\\/g, '/').endsWith('scripts/hook-run.mjs')) main();
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Uniform greppable hook-activity log (#2599). Every hook invocation appends ONE line so a
|
|
2
|
+
// user/agent can see "what just ran" (ran / healed X / denied Y / failed Z) and a recurring silent
|
|
3
|
+
// failure is grep-discoverable. This generalizes the per-hook trace pattern that lived only in
|
|
4
|
+
// shell-redirect-lint.mjs into one shared mechanism every hook reuses (epic #2589 transparency lane).
|
|
5
|
+
//
|
|
6
|
+
// Format: one JSONL line per invocation with stable keys —
|
|
7
|
+
// { "ts","surface","event","script","outcome","action", ... }
|
|
8
|
+
// outcome vocabulary: "ran" (pass-through) | "deny" (blocked) | "heal" (transformed output) |
|
|
9
|
+
// "observe" (would-block, observe mode) | "bypass" (escape hatch used) | "failed" (hook errored).
|
|
10
|
+
//
|
|
11
|
+
// Path: repo-local gitignored state (.git/mmi-runtime/hooks/activity.jsonl — never tracked); falls
|
|
12
|
+
// back to a per-cwd tmpdir path outside a repo so the trace still works anywhere. Fail-soft always:
|
|
13
|
+
// the trace must never crash a turn or block a tool call.
|
|
14
|
+
import { createHash } from 'node:crypto';
|
|
15
|
+
import { execFileSync } from 'node:child_process';
|
|
16
|
+
import { appendFileSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
17
|
+
import { dirname, join, resolve } from 'node:path';
|
|
18
|
+
import { tmpdir } from 'node:os';
|
|
19
|
+
|
|
20
|
+
const DEFAULT_SURFACE = 'claude';
|
|
21
|
+
|
|
22
|
+
// #3642: four hooks append on every tool call and nothing ever pruned, so the trace reached 17 MB on
|
|
23
|
+
// the Hub checkout — in `.git/`, so invisible to `git status` and to every worktree/scratch sweep.
|
|
24
|
+
// Trim on append: past MAX_BYTES, keep the last KEEP_BYTES. KEEP sits above the 512 KB tail the
|
|
25
|
+
// doctor's redactor-liveness probe reads (cli/src/hook-activity.ts), so trimming never shortens the
|
|
26
|
+
// window a reader sees. MAX is high enough that the read-rewrite happens once every few weeks.
|
|
27
|
+
// The CLI mirror in cli/src/hook-activity.ts deliberately has no copy of this: with the SessionStart
|
|
28
|
+
// hook retired (#3630) it writes at most a row a session, and the bound is a property of the FILE —
|
|
29
|
+
// whoever appends next enforces it, and that is effectively always this writer.
|
|
30
|
+
const MAX_BYTES = 4 * 1024 * 1024;
|
|
31
|
+
const KEEP_BYTES = 1024 * 1024;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Drop the oldest rows once the log passes MAX_BYTES, keeping the last KEEP_BYTES from the first
|
|
35
|
+
* line boundary (a byte-slice would leave a torn row at the head). Fail-soft: a trim that cannot
|
|
36
|
+
* run leaves the log as it is rather than losing the append behind it.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} path
|
|
39
|
+
* @returns {void}
|
|
40
|
+
*/
|
|
41
|
+
function trimIfOversized(path) {
|
|
42
|
+
try {
|
|
43
|
+
if (statSync(path).size <= MAX_BYTES) return;
|
|
44
|
+
const tail = readFileSync(path, 'utf8').slice(-KEEP_BYTES);
|
|
45
|
+
const firstBreak = tail.indexOf('\n');
|
|
46
|
+
writeFileSync(path, firstBreak === -1 ? '' : tail.slice(firstBreak + 1), 'utf8');
|
|
47
|
+
} catch {
|
|
48
|
+
/* no log yet, or an unreadable one — the append below is still worth attempting */
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function fallbackPath(cwd) {
|
|
53
|
+
const hash = createHash('sha256').update(resolve(cwd)).digest('hex').slice(0, 16);
|
|
54
|
+
return join(tmpdir(), 'mmi-cli', hash, 'hooks', 'activity.jsonl');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Resolve the hook-activity log path for a working directory.
|
|
59
|
+
* Prefers the repo-local gitignored state dir (`.git/mmi-runtime/hooks/activity.jsonl` via
|
|
60
|
+
* `git rev-parse --git-path`); falls back to a per-cwd tmpdir path so the trace still works
|
|
61
|
+
* outside a repo. Env overrides: `MMI_HOOK_ACTIVITY_LOG` (full path, wins outright) and
|
|
62
|
+
* `MMI_HOOK_ACTIVITY_CWD` (resolve against this directory instead of the hook process's own cwd —
|
|
63
|
+
* the Kimi launcher stamps it from the hook payload because Kimi runs plugin hooks with cwd = the
|
|
64
|
+
* plugin root, which is not a repo).
|
|
65
|
+
*
|
|
66
|
+
* @param {string} [cwd]
|
|
67
|
+
* @param {Record<string,string|undefined>} [env]
|
|
68
|
+
* @returns {string}
|
|
69
|
+
*/
|
|
70
|
+
export function activityLogPath(cwd = process.cwd(), env = process.env) {
|
|
71
|
+
if (env.MMI_HOOK_ACTIVITY_LOG) return env.MMI_HOOK_ACTIVITY_LOG;
|
|
72
|
+
const effectiveCwd = env.MMI_HOOK_ACTIVITY_CWD || cwd;
|
|
73
|
+
try {
|
|
74
|
+
return execFileSync(
|
|
75
|
+
'git',
|
|
76
|
+
['-C', effectiveCwd, 'rev-parse', '--path-format=absolute', '--git-path', 'mmi-runtime/hooks/activity.jsonl'],
|
|
77
|
+
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 3000, windowsHide: true },
|
|
78
|
+
).trim();
|
|
79
|
+
} catch {
|
|
80
|
+
return fallbackPath(effectiveCwd);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Append one hook-activity line. Fail-soft: any IO error is swallowed (the trace must never crash a
|
|
86
|
+
* turn). `ts` is stamped and `surface` defaulted when absent.
|
|
87
|
+
*
|
|
88
|
+
* @param {{ surface?: string, event: string, script: string, outcome: string, action?: string, [k: string]: unknown }} entry
|
|
89
|
+
* @param {{ cwd?: string, env?: Record<string,string|undefined> }} [opts]
|
|
90
|
+
* @returns {void}
|
|
91
|
+
*/
|
|
92
|
+
export function appendHookActivity(entry, opts = {}) {
|
|
93
|
+
try {
|
|
94
|
+
const path = activityLogPath(opts.cwd, opts.env);
|
|
95
|
+
const line = {
|
|
96
|
+
ts: new Date().toISOString(),
|
|
97
|
+
// #3563: the same scripts now run under Codex too, and a Codex deny logged as `surface: "claude"`
|
|
98
|
+
// sends anyone reading the trace to the wrong manifest. The Codex launcher sets MMI_HOOK_SURFACE.
|
|
99
|
+
surface: (opts.env ?? process.env).MMI_HOOK_SURFACE || DEFAULT_SURFACE,
|
|
100
|
+
...entry,
|
|
101
|
+
};
|
|
102
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
103
|
+
trimIfOversized(path);
|
|
104
|
+
appendFileSync(path, `${JSON.stringify(line)}\n`, 'utf8');
|
|
105
|
+
} catch {
|
|
106
|
+
/* fail-soft — never crash a turn for the trace */
|
|
107
|
+
}
|
|
108
|
+
}
|