@luanpdd/kit-mcp 1.34.0 → 1.36.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 +1 -1
- package/bin/cli.js +2 -2
- package/bin/mcp.js +6 -6
- package/bin/ui.js +74 -74
- package/gates/ai-prompt-stability.md +120 -120
- package/gates/budget-description.md +68 -68
- package/gates/confidence.md +29 -29
- package/gates/dependency-check.md +33 -33
- package/gates/dept-cycle-prevention.md +179 -179
- package/gates/golden-signals-coverage.md +133 -133
- package/gates/legacy-refactor-safety.md +178 -178
- package/gates/multi-tenant-rls-coverage.md +102 -102
- package/gates/no-personal-uuid.md +72 -72
- package/gates/obs-agents-mcp-supabase.md +86 -86
- package/gates/obs-skills-frontmatter.md +76 -76
- package/gates/observability-coverage.md +151 -151
- package/gates/omm-no-regression.md +83 -83
- package/gates/postmortem-template-required.md +127 -127
- package/gates/prr-checklist-coverage.md +128 -128
- package/gates/regression.md +32 -32
- package/gates/release-pipeline-policy.md +132 -132
- package/gates/secrets-scan.md +33 -33
- package/gates/service-role-not-in-user-facing.md +113 -113
- package/gates/skill-must-include.md +71 -71
- package/gates/sync-idempotent.md +62 -62
- package/gates/verify-phase-goal.md +34 -34
- package/kit/agents/designer-ui.md +216 -216
- package/kit/agents/workflow-generator.md +537 -0
- package/kit/commands/adicionar-backlog.md +1 -1
- package/kit/commands/adicionar-fase.md +1 -1
- package/kit/commands/adicionar-tarefa.md +1 -1
- package/kit/commands/auditar-observabilidade.md +103 -103
- package/kit/commands/auditar-toil.md +129 -129
- package/kit/commands/caracterizar-prompt.md +195 -195
- package/kit/commands/criar-workflow.md +158 -0
- package/kit/commands/definir-perfil.md +1 -1
- package/kit/commands/definir-slo.md +108 -108
- package/kit/commands/fio.md +1 -1
- package/kit/commands/golden-signals.md +142 -142
- package/kit/commands/instrumentar-fase.md +200 -200
- package/kit/commands/investigar-producao.md +162 -162
- package/kit/commands/observabilidade.md +118 -118
- package/kit/commands/postmortem.md +179 -179
- package/kit/commands/prr.md +205 -205
- package/kit/commands/publicar-rapido.md +207 -207
- package/kit/commands/risk-budget.md +220 -220
- package/kit/commands/sre.md +230 -230
- package/kit/file-manifest.json +5 -2
- package/kit/framework/references/output-style.md +22 -22
- package/kit/hooks/post-apply-migration.js +199 -199
- package/kit/hooks/sidecar-tool-publisher.js +210 -210
- package/kit/skills/_shared-dados-distribuidos/glossary.md +224 -224
- package/kit/skills/_shared-legacy/glossary.md +389 -389
- package/kit/skills/_shared-multi-tenant/glossary.md +186 -186
- package/kit/skills/_shared-observability/glossary.md +396 -396
- package/kit/skills/_shared-sre/glossary.md +712 -712
- package/kit/skills/_shared-supabase/glossary.md +234 -234
- package/kit/skills/blameless-postmortems/SKILL.md +340 -340
- package/kit/skills/burn-rate-alerting/SKILL.md +258 -258
- package/kit/skills/cascading-failures/SKILL.md +311 -311
- package/kit/skills/core-analysis-loop/SKILL.md +352 -352
- package/kit/skills/distributed-tracing/SKILL.md +362 -362
- package/kit/skills/dynamic-workflow-authoring/SKILL.md +327 -0
- package/kit/skills/eliminating-toil/SKILL.md +243 -243
- package/kit/skills/event-based-slos/SKILL.md +296 -296
- package/kit/skills/four-golden-signals/SKILL.md +314 -314
- package/kit/skills/hermetic-builds/SKILL.md +323 -323
- package/kit/skills/legacy-monster-methods/SKILL.md +444 -444
- package/kit/skills/llm-as-dependency/SKILL.md +436 -436
- package/kit/skills/load-shedding-graceful-degradation/SKILL.md +396 -396
- package/kit/skills/observability-driven-development/SKILL.md +315 -315
- package/kit/skills/observability-maturity-model/SKILL.md +222 -222
- package/kit/skills/opentelemetry-standard/SKILL.md +351 -351
- package/kit/skills/production-readiness-review/SKILL.md +305 -305
- package/kit/skills/release-engineering/SKILL.md +367 -367
- package/kit/skills/retry-strategies/SKILL.md +372 -372
- package/kit/skills/sre-risk-management/SKILL.md +221 -221
- package/kit/skills/structured-events/SKILL.md +265 -265
- package/kit/skills/supabase-cron-queues/SKILL.md +275 -275
- package/kit/skills/supabase-database-functions/SKILL.md +332 -332
- package/kit/skills/supabase-declarative-schema/SKILL.md +183 -183
- package/kit/skills/supabase-pgvector-rag/SKILL.md +253 -253
- package/kit/skills/supabase-postgres-style/SKILL.md +138 -138
- package/kit/skills/supabase-storage/SKILL.md +234 -234
- package/kit/skills/telemetry-pipelines/SKILL.md +259 -259
- package/kit/skills/telemetry-sampling/SKILL.md +256 -256
- package/kit/skills/ui-anti-padroes-ia/SKILL.md +261 -261
- package/kit/skills/ui-contexto-produto/SKILL.md +248 -248
- package/kit/skills/ui-cor-estrategia/SKILL.md +213 -213
- package/kit/skills/ui-critica-auditoria/SKILL.md +260 -260
- package/kit/skills/ui-motion-funcional/SKILL.md +264 -264
- package/kit/skills/ui-ritmo-espacial/SKILL.md +259 -259
- package/kit/skills/ui-tipografia/SKILL.md +211 -211
- package/package.json +1 -1
- package/src/cli/index.js +1114 -1114
- package/src/cli/render.js +194 -194
- package/src/cli/upgrade-check.js +135 -135
- package/src/core/error-redaction.js +76 -76
- package/src/core/failures.js +153 -153
- package/src/core/gate-runner.js +205 -205
- package/src/core/gates.js +82 -82
- package/src/core/logger.js +170 -170
- package/src/core/manifest-verify.js +174 -174
- package/src/core/metrics.js +268 -268
- package/src/core/notify.js +60 -60
- package/src/core/path-safety.js +141 -141
- package/src/core/replays.js +120 -120
- package/src/core/ui.js +185 -185
- package/src/mcp-server/install.js +149 -149
- package/src/mcp-server/roots.js +124 -124
- package/src/ui/auto-spawn.js +113 -113
- package/src/ui/browser.js +78 -78
- package/src/ui/client.js +130 -130
- package/src/ui/events.js +65 -65
- package/src/ui/lockfile.js +191 -191
- package/src/ui/port.js +67 -67
- package/src/ui/server.js +547 -547
- package/src/ui/wrapper.js +129 -129
package/src/core/gate-runner.js
CHANGED
|
@@ -1,205 +1,205 @@
|
|
|
1
|
-
// Gate runner — execute a gate with explicit user confirmation.
|
|
2
|
-
//
|
|
3
|
-
// Two modes:
|
|
4
|
-
// - shell: gate body has a ## Check section with one or more fenced code
|
|
5
|
-
// blocks → present what will run, ask y/N, execute via bash
|
|
6
|
-
// - manual: gate body has no executable check → present the body, ask the
|
|
7
|
-
// user to pick passed | warn | block
|
|
8
|
-
//
|
|
9
|
-
// Returns a structured verdict: { id, verdict, blocking, exitCode?, stdout?, stderr? }
|
|
10
|
-
//
|
|
11
|
-
// Safety:
|
|
12
|
-
// - Never runs without confirmation in interactive mode
|
|
13
|
-
// - In non-interactive (--yes), runs only the extracted ## Check shell blocks
|
|
14
|
-
// - Always logs the exact command and the cwd before executing
|
|
15
|
-
// - Captures stdout/stderr for the orchestrator to decide what to do next
|
|
16
|
-
|
|
17
|
-
import path from 'node:path';
|
|
18
|
-
import fs from 'node:fs/promises';
|
|
19
|
-
import { spawn } from 'node:child_process';
|
|
20
|
-
import os from 'node:os';
|
|
21
|
-
import { createInterface } from 'node:readline/promises';
|
|
22
|
-
import { stdin as input, stdout as output, stderr } from 'node:process';
|
|
23
|
-
import { getGate } from './gates.js';
|
|
24
|
-
|
|
25
|
-
export async function runGate(id, opts = {}) {
|
|
26
|
-
const projectRoot = path.resolve(opts.projectRoot ?? process.cwd());
|
|
27
|
-
const yes = !!opts.yes;
|
|
28
|
-
const onLog = opts.onLog ?? ((s) => stderr.write(s + '\n'));
|
|
29
|
-
const interactive = opts.interactive !== false && !yes;
|
|
30
|
-
|
|
31
|
-
const gate = await getGate(id, opts.gatesRoot);
|
|
32
|
-
const parsed = parseGateBody(gate.content);
|
|
33
|
-
|
|
34
|
-
onLog('');
|
|
35
|
-
onLog(`Gate: ${gate.id} [stage=${gate.stage}, blocking=${gate.blocking}]`);
|
|
36
|
-
if (gate.description) onLog(`Description: ${gate.description}`);
|
|
37
|
-
onLog('');
|
|
38
|
-
|
|
39
|
-
if (parsed.shellBlocks.length > 0) {
|
|
40
|
-
return runShellGate(gate, parsed, { projectRoot, yes, interactive, onLog });
|
|
41
|
-
}
|
|
42
|
-
return runManualGate(gate, parsed, { projectRoot, interactive, onLog });
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// --- shell-mode gates ---
|
|
46
|
-
|
|
47
|
-
async function runShellGate(gate, parsed, { projectRoot, yes, interactive, onLog }) {
|
|
48
|
-
const script = parsed.shellBlocks.join('\n\n');
|
|
49
|
-
|
|
50
|
-
onLog(`Will execute (cwd=${projectRoot}):`);
|
|
51
|
-
onLog('─────');
|
|
52
|
-
onLog(script);
|
|
53
|
-
onLog('─────');
|
|
54
|
-
|
|
55
|
-
let proceed = yes;
|
|
56
|
-
if (interactive && !yes) {
|
|
57
|
-
proceed = await ask('execute? [y/N] ');
|
|
58
|
-
}
|
|
59
|
-
if (!proceed) {
|
|
60
|
-
return { id: gate.id, verdict: 'skipped', blocking: gate.blocking, reason: 'user declined or non-interactive without --yes' };
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const { exitCode, stdout, stderr: errOut } = await execScript(script, projectRoot);
|
|
64
|
-
const verdict = mapVerdict(exitCode, gate);
|
|
65
|
-
onLog(`exit=${exitCode} → verdict=${verdict}`);
|
|
66
|
-
|
|
67
|
-
return {
|
|
68
|
-
id: gate.id,
|
|
69
|
-
verdict,
|
|
70
|
-
blocking: gate.blocking,
|
|
71
|
-
exitCode,
|
|
72
|
-
stdout: trim(stdout),
|
|
73
|
-
stderr: trim(errOut),
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// --- manual-mode gates ---
|
|
78
|
-
|
|
79
|
-
async function runManualGate(gate, parsed, { projectRoot, interactive, onLog }) {
|
|
80
|
-
onLog('This gate has no executable check. Body:');
|
|
81
|
-
onLog('─────');
|
|
82
|
-
onLog(parsed.body.trim());
|
|
83
|
-
onLog('─────');
|
|
84
|
-
|
|
85
|
-
if (!interactive) {
|
|
86
|
-
return { id: gate.id, verdict: 'manual', blocking: gate.blocking, reason: 'manual gate; no auto-decision in non-interactive mode' };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const choice = await askChoice('verdict? [p]assed / [w]arn / [b]lock / [s]kip: ', {
|
|
90
|
-
p: 'passed', w: 'warn', b: 'block', s: 'skipped',
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
return { id: gate.id, verdict: choice, blocking: gate.blocking };
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// --- parsing ---
|
|
97
|
-
|
|
98
|
-
function parseGateBody(content) {
|
|
99
|
-
const body = content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, '');
|
|
100
|
-
const checkSection = extractSection(body, 'Check');
|
|
101
|
-
const shellBlocks = extractCodeBlocks(checkSection || '');
|
|
102
|
-
return { body, checkSection, shellBlocks };
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function extractSection(body, heading) {
|
|
106
|
-
// Line-by-line: find `## Heading`, capture everything until the next `## ` or EOF.
|
|
107
|
-
// Plain regex with `\Z` doesn't exist in JS, and `(?=^##|$)` is awkward — easier this way.
|
|
108
|
-
const lines = body.split(/\r?\n/);
|
|
109
|
-
const startRe = new RegExp(`^##\\s+${heading}\\s*$`, 'i');
|
|
110
|
-
let start = -1, end = lines.length;
|
|
111
|
-
for (let i = 0; i < lines.length; i++) {
|
|
112
|
-
if (startRe.test(lines[i])) { start = i + 1; break; }
|
|
113
|
-
}
|
|
114
|
-
if (start === -1) return null;
|
|
115
|
-
for (let i = start; i < lines.length; i++) {
|
|
116
|
-
if (/^##\s+/.test(lines[i])) { end = i; break; }
|
|
117
|
-
}
|
|
118
|
-
return lines.slice(start, end).join('\n').trim();
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function extractCodeBlocks(text) {
|
|
122
|
-
const out = [];
|
|
123
|
-
const re = /```(?:bash|sh|shell)?\s*\n([\s\S]*?)```/g;
|
|
124
|
-
let m;
|
|
125
|
-
while ((m = re.exec(text)) !== null) {
|
|
126
|
-
const code = m[1].trim();
|
|
127
|
-
if (code) out.push(code);
|
|
128
|
-
}
|
|
129
|
-
return out;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// --- exec ---
|
|
133
|
-
|
|
134
|
-
async function execScript(script, cwd) {
|
|
135
|
-
// SEC-14-04: use mkdtemp for crypto-safe random directory naming, write the
|
|
136
|
-
// script INSIDE it, then cleanup recursive. Predictable timestamp+rand-suffix
|
|
137
|
-
// filenames are unsafe in multi-user /tmp — attacker can pre-create a symlink
|
|
138
|
-
// at the predicted path before fs.writeFile, and `spawn(bash, [tmp])` would
|
|
139
|
-
// execute the symlink target. mkdtemp uses the OS-level mkdtemp(3) syscall
|
|
140
|
-
// (POSIX) / equivalent (Windows) which atomically creates a directory with
|
|
141
|
-
// a random suffix and returns the actual path. The new dir gets 0700 from
|
|
142
|
-
// process umask on POSIX (umask 022 → 0700; default Node runtime). Even if
|
|
143
|
-
// umask is permissive, the script file inside is written with mode 0o700.
|
|
144
|
-
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'kit-gate-'));
|
|
145
|
-
const tmp = path.join(dir, 'gate.sh');
|
|
146
|
-
await fs.writeFile(tmp, script, { encoding: 'utf8', mode: 0o700 });
|
|
147
|
-
try {
|
|
148
|
-
const child = spawn('bash', [tmp], { cwd, env: process.env });
|
|
149
|
-
const stdout = [], stderrOut = [];
|
|
150
|
-
child.stdout.on('data', (b) => stdout.push(b));
|
|
151
|
-
child.stderr.on('data', (b) => stderrOut.push(b));
|
|
152
|
-
const exitCode = await new Promise((resolve, reject) => {
|
|
153
|
-
child.on('error', (e) => reject(new Error(`failed to spawn bash: ${e.message}. Install Git Bash or WSL on Windows.`)));
|
|
154
|
-
child.on('close', resolve);
|
|
155
|
-
});
|
|
156
|
-
return {
|
|
157
|
-
exitCode: exitCode ?? -1,
|
|
158
|
-
stdout: Buffer.concat(stdout).toString('utf8'),
|
|
159
|
-
stderr: Buffer.concat(stderrOut).toString('utf8'),
|
|
160
|
-
};
|
|
161
|
-
} finally {
|
|
162
|
-
// Recursive cleanup — even if spawn errored above, the dir gets removed.
|
|
163
|
-
// force:true swallows ENOENT (e.g. if script self-deleted). recursive:true
|
|
164
|
-
// walks the dir; even if the gate body wrote temp files inside cwd, cwd is
|
|
165
|
-
// separate from `dir` so we won't blast user files.
|
|
166
|
-
await fs.rm(dir, { recursive: true, force: true }).catch(() => {});
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// --- verdict mapping ---
|
|
171
|
-
|
|
172
|
-
function mapVerdict(exitCode, gate) {
|
|
173
|
-
if (exitCode === 0) return 'passed';
|
|
174
|
-
return gate.blocking ? 'block' : 'warn';
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// --- prompts ---
|
|
178
|
-
|
|
179
|
-
async function ask(question) {
|
|
180
|
-
const rl = createInterface({ input, output });
|
|
181
|
-
try {
|
|
182
|
-
const a = (await rl.question(question)).trim().toLowerCase();
|
|
183
|
-
return a === 'y' || a === 'yes';
|
|
184
|
-
} finally {
|
|
185
|
-
rl.close();
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
async function askChoice(question, mapping) {
|
|
190
|
-
const rl = createInterface({ input, output });
|
|
191
|
-
try {
|
|
192
|
-
while (true) {
|
|
193
|
-
const a = (await rl.question(question)).trim().toLowerCase();
|
|
194
|
-
if (mapping[a]) return mapping[a];
|
|
195
|
-
output.write(`unknown choice "${a}". try one of: ${Object.keys(mapping).join(', ')}\n`);
|
|
196
|
-
}
|
|
197
|
-
} finally {
|
|
198
|
-
rl.close();
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function trim(s) {
|
|
203
|
-
if (!s) return s;
|
|
204
|
-
return s.length > 4000 ? s.slice(0, 4000) + `\n…(truncated, ${s.length} bytes total)` : s;
|
|
205
|
-
}
|
|
1
|
+
// Gate runner — execute a gate with explicit user confirmation.
|
|
2
|
+
//
|
|
3
|
+
// Two modes:
|
|
4
|
+
// - shell: gate body has a ## Check section with one or more fenced code
|
|
5
|
+
// blocks → present what will run, ask y/N, execute via bash
|
|
6
|
+
// - manual: gate body has no executable check → present the body, ask the
|
|
7
|
+
// user to pick passed | warn | block
|
|
8
|
+
//
|
|
9
|
+
// Returns a structured verdict: { id, verdict, blocking, exitCode?, stdout?, stderr? }
|
|
10
|
+
//
|
|
11
|
+
// Safety:
|
|
12
|
+
// - Never runs without confirmation in interactive mode
|
|
13
|
+
// - In non-interactive (--yes), runs only the extracted ## Check shell blocks
|
|
14
|
+
// - Always logs the exact command and the cwd before executing
|
|
15
|
+
// - Captures stdout/stderr for the orchestrator to decide what to do next
|
|
16
|
+
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import fs from 'node:fs/promises';
|
|
19
|
+
import { spawn } from 'node:child_process';
|
|
20
|
+
import os from 'node:os';
|
|
21
|
+
import { createInterface } from 'node:readline/promises';
|
|
22
|
+
import { stdin as input, stdout as output, stderr } from 'node:process';
|
|
23
|
+
import { getGate } from './gates.js';
|
|
24
|
+
|
|
25
|
+
export async function runGate(id, opts = {}) {
|
|
26
|
+
const projectRoot = path.resolve(opts.projectRoot ?? process.cwd());
|
|
27
|
+
const yes = !!opts.yes;
|
|
28
|
+
const onLog = opts.onLog ?? ((s) => stderr.write(s + '\n'));
|
|
29
|
+
const interactive = opts.interactive !== false && !yes;
|
|
30
|
+
|
|
31
|
+
const gate = await getGate(id, opts.gatesRoot);
|
|
32
|
+
const parsed = parseGateBody(gate.content);
|
|
33
|
+
|
|
34
|
+
onLog('');
|
|
35
|
+
onLog(`Gate: ${gate.id} [stage=${gate.stage}, blocking=${gate.blocking}]`);
|
|
36
|
+
if (gate.description) onLog(`Description: ${gate.description}`);
|
|
37
|
+
onLog('');
|
|
38
|
+
|
|
39
|
+
if (parsed.shellBlocks.length > 0) {
|
|
40
|
+
return runShellGate(gate, parsed, { projectRoot, yes, interactive, onLog });
|
|
41
|
+
}
|
|
42
|
+
return runManualGate(gate, parsed, { projectRoot, interactive, onLog });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// --- shell-mode gates ---
|
|
46
|
+
|
|
47
|
+
async function runShellGate(gate, parsed, { projectRoot, yes, interactive, onLog }) {
|
|
48
|
+
const script = parsed.shellBlocks.join('\n\n');
|
|
49
|
+
|
|
50
|
+
onLog(`Will execute (cwd=${projectRoot}):`);
|
|
51
|
+
onLog('─────');
|
|
52
|
+
onLog(script);
|
|
53
|
+
onLog('─────');
|
|
54
|
+
|
|
55
|
+
let proceed = yes;
|
|
56
|
+
if (interactive && !yes) {
|
|
57
|
+
proceed = await ask('execute? [y/N] ');
|
|
58
|
+
}
|
|
59
|
+
if (!proceed) {
|
|
60
|
+
return { id: gate.id, verdict: 'skipped', blocking: gate.blocking, reason: 'user declined or non-interactive without --yes' };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const { exitCode, stdout, stderr: errOut } = await execScript(script, projectRoot);
|
|
64
|
+
const verdict = mapVerdict(exitCode, gate);
|
|
65
|
+
onLog(`exit=${exitCode} → verdict=${verdict}`);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
id: gate.id,
|
|
69
|
+
verdict,
|
|
70
|
+
blocking: gate.blocking,
|
|
71
|
+
exitCode,
|
|
72
|
+
stdout: trim(stdout),
|
|
73
|
+
stderr: trim(errOut),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// --- manual-mode gates ---
|
|
78
|
+
|
|
79
|
+
async function runManualGate(gate, parsed, { projectRoot, interactive, onLog }) {
|
|
80
|
+
onLog('This gate has no executable check. Body:');
|
|
81
|
+
onLog('─────');
|
|
82
|
+
onLog(parsed.body.trim());
|
|
83
|
+
onLog('─────');
|
|
84
|
+
|
|
85
|
+
if (!interactive) {
|
|
86
|
+
return { id: gate.id, verdict: 'manual', blocking: gate.blocking, reason: 'manual gate; no auto-decision in non-interactive mode' };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const choice = await askChoice('verdict? [p]assed / [w]arn / [b]lock / [s]kip: ', {
|
|
90
|
+
p: 'passed', w: 'warn', b: 'block', s: 'skipped',
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return { id: gate.id, verdict: choice, blocking: gate.blocking };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// --- parsing ---
|
|
97
|
+
|
|
98
|
+
function parseGateBody(content) {
|
|
99
|
+
const body = content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, '');
|
|
100
|
+
const checkSection = extractSection(body, 'Check');
|
|
101
|
+
const shellBlocks = extractCodeBlocks(checkSection || '');
|
|
102
|
+
return { body, checkSection, shellBlocks };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function extractSection(body, heading) {
|
|
106
|
+
// Line-by-line: find `## Heading`, capture everything until the next `## ` or EOF.
|
|
107
|
+
// Plain regex with `\Z` doesn't exist in JS, and `(?=^##|$)` is awkward — easier this way.
|
|
108
|
+
const lines = body.split(/\r?\n/);
|
|
109
|
+
const startRe = new RegExp(`^##\\s+${heading}\\s*$`, 'i');
|
|
110
|
+
let start = -1, end = lines.length;
|
|
111
|
+
for (let i = 0; i < lines.length; i++) {
|
|
112
|
+
if (startRe.test(lines[i])) { start = i + 1; break; }
|
|
113
|
+
}
|
|
114
|
+
if (start === -1) return null;
|
|
115
|
+
for (let i = start; i < lines.length; i++) {
|
|
116
|
+
if (/^##\s+/.test(lines[i])) { end = i; break; }
|
|
117
|
+
}
|
|
118
|
+
return lines.slice(start, end).join('\n').trim();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function extractCodeBlocks(text) {
|
|
122
|
+
const out = [];
|
|
123
|
+
const re = /```(?:bash|sh|shell)?\s*\n([\s\S]*?)```/g;
|
|
124
|
+
let m;
|
|
125
|
+
while ((m = re.exec(text)) !== null) {
|
|
126
|
+
const code = m[1].trim();
|
|
127
|
+
if (code) out.push(code);
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// --- exec ---
|
|
133
|
+
|
|
134
|
+
async function execScript(script, cwd) {
|
|
135
|
+
// SEC-14-04: use mkdtemp for crypto-safe random directory naming, write the
|
|
136
|
+
// script INSIDE it, then cleanup recursive. Predictable timestamp+rand-suffix
|
|
137
|
+
// filenames are unsafe in multi-user /tmp — attacker can pre-create a symlink
|
|
138
|
+
// at the predicted path before fs.writeFile, and `spawn(bash, [tmp])` would
|
|
139
|
+
// execute the symlink target. mkdtemp uses the OS-level mkdtemp(3) syscall
|
|
140
|
+
// (POSIX) / equivalent (Windows) which atomically creates a directory with
|
|
141
|
+
// a random suffix and returns the actual path. The new dir gets 0700 from
|
|
142
|
+
// process umask on POSIX (umask 022 → 0700; default Node runtime). Even if
|
|
143
|
+
// umask is permissive, the script file inside is written with mode 0o700.
|
|
144
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'kit-gate-'));
|
|
145
|
+
const tmp = path.join(dir, 'gate.sh');
|
|
146
|
+
await fs.writeFile(tmp, script, { encoding: 'utf8', mode: 0o700 });
|
|
147
|
+
try {
|
|
148
|
+
const child = spawn('bash', [tmp], { cwd, env: process.env });
|
|
149
|
+
const stdout = [], stderrOut = [];
|
|
150
|
+
child.stdout.on('data', (b) => stdout.push(b));
|
|
151
|
+
child.stderr.on('data', (b) => stderrOut.push(b));
|
|
152
|
+
const exitCode = await new Promise((resolve, reject) => {
|
|
153
|
+
child.on('error', (e) => reject(new Error(`failed to spawn bash: ${e.message}. Install Git Bash or WSL on Windows.`)));
|
|
154
|
+
child.on('close', resolve);
|
|
155
|
+
});
|
|
156
|
+
return {
|
|
157
|
+
exitCode: exitCode ?? -1,
|
|
158
|
+
stdout: Buffer.concat(stdout).toString('utf8'),
|
|
159
|
+
stderr: Buffer.concat(stderrOut).toString('utf8'),
|
|
160
|
+
};
|
|
161
|
+
} finally {
|
|
162
|
+
// Recursive cleanup — even if spawn errored above, the dir gets removed.
|
|
163
|
+
// force:true swallows ENOENT (e.g. if script self-deleted). recursive:true
|
|
164
|
+
// walks the dir; even if the gate body wrote temp files inside cwd, cwd is
|
|
165
|
+
// separate from `dir` so we won't blast user files.
|
|
166
|
+
await fs.rm(dir, { recursive: true, force: true }).catch(() => {});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// --- verdict mapping ---
|
|
171
|
+
|
|
172
|
+
function mapVerdict(exitCode, gate) {
|
|
173
|
+
if (exitCode === 0) return 'passed';
|
|
174
|
+
return gate.blocking ? 'block' : 'warn';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// --- prompts ---
|
|
178
|
+
|
|
179
|
+
async function ask(question) {
|
|
180
|
+
const rl = createInterface({ input, output });
|
|
181
|
+
try {
|
|
182
|
+
const a = (await rl.question(question)).trim().toLowerCase();
|
|
183
|
+
return a === 'y' || a === 'yes';
|
|
184
|
+
} finally {
|
|
185
|
+
rl.close();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function askChoice(question, mapping) {
|
|
190
|
+
const rl = createInterface({ input, output });
|
|
191
|
+
try {
|
|
192
|
+
while (true) {
|
|
193
|
+
const a = (await rl.question(question)).trim().toLowerCase();
|
|
194
|
+
if (mapping[a]) return mapping[a];
|
|
195
|
+
output.write(`unknown choice "${a}". try one of: ${Object.keys(mapping).join(', ')}\n`);
|
|
196
|
+
}
|
|
197
|
+
} finally {
|
|
198
|
+
rl.close();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function trim(s) {
|
|
203
|
+
if (!s) return s;
|
|
204
|
+
return s.length > 4000 ? s.slice(0, 4000) + `\n…(truncated, ${s.length} bytes total)` : s;
|
|
205
|
+
}
|
package/src/core/gates.js
CHANGED
|
@@ -1,82 +1,82 @@
|
|
|
1
|
-
// Gates extracted from inline workflow steps into reusable, named, file-backed checks.
|
|
2
|
-
//
|
|
3
|
-
// A gate is a markdown file under `gates/` with frontmatter:
|
|
4
|
-
//
|
|
5
|
-
// ---
|
|
6
|
-
// id: regression
|
|
7
|
-
// stage: pre-verify # pre-plan | pre-execute | pre-verify | post-verify | any
|
|
8
|
-
// blocking: true # true → must pass to advance; false → warn only
|
|
9
|
-
// ---
|
|
10
|
-
// <inline shell or natural-language check description>
|
|
11
|
-
//
|
|
12
|
-
// Gates are consumed by orchestrator workflows OR by `/saude` to spot-check.
|
|
13
|
-
// Running a gate here returns a structured verdict; actually executing the
|
|
14
|
-
// shell side is delegated to the orchestrator (we don't want to shell-out
|
|
15
|
-
// from the MCP server without confirmation).
|
|
16
|
-
|
|
17
|
-
import path from 'node:path';
|
|
18
|
-
import fs from 'node:fs/promises';
|
|
19
|
-
import { fileURLToPath } from 'node:url';
|
|
20
|
-
|
|
21
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
22
|
-
const __dirname = path.dirname(__filename);
|
|
23
|
-
export const DEFAULT_GATES_ROOT = path.resolve(__dirname, '../../gates');
|
|
24
|
-
|
|
25
|
-
// P2: TTL cache for listGates (mirrors PERF-01 in kit.js). Gates change rarely;
|
|
26
|
-
// inside a single Claude Code session we may call listGates → getGate → gatesForStage
|
|
27
|
-
// in sequence — without cache, that's 3 full directory walks of the gates dir.
|
|
28
|
-
const GATES_CACHE_TTL_MS = 30_000;
|
|
29
|
-
const gatesCache = new Map(); // gatesRoot -> { value, ts }
|
|
30
|
-
|
|
31
|
-
export function clearGatesCache() { gatesCache.clear(); }
|
|
32
|
-
|
|
33
|
-
export async function listGates(gatesRoot = DEFAULT_GATES_ROOT) {
|
|
34
|
-
const cached = gatesCache.get(gatesRoot);
|
|
35
|
-
if (cached && Date.now() - cached.ts < GATES_CACHE_TTL_MS) {
|
|
36
|
-
return cached.value;
|
|
37
|
-
}
|
|
38
|
-
let entries;
|
|
39
|
-
try { entries = await fs.readdir(gatesRoot, { withFileTypes: true }); }
|
|
40
|
-
catch { return []; }
|
|
41
|
-
const out = [];
|
|
42
|
-
for (const e of entries) {
|
|
43
|
-
if (!e.isFile() || !e.name.endsWith('.md')) continue;
|
|
44
|
-
const abs = path.join(gatesRoot, e.name);
|
|
45
|
-
const raw = await fs.readFile(abs, 'utf8');
|
|
46
|
-
const meta = parseFrontmatter(raw);
|
|
47
|
-
out.push({
|
|
48
|
-
id: meta.id ?? e.name.replace(/\.md$/, ''),
|
|
49
|
-
stage: meta.stage ?? 'any',
|
|
50
|
-
blocking: meta.blocking !== false && meta.blocking !== 'false',
|
|
51
|
-
description: meta.description ?? '',
|
|
52
|
-
absPath: abs,
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
const value = out.sort((a, b) => a.id.localeCompare(b.id));
|
|
56
|
-
gatesCache.set(gatesRoot, { value, ts: Date.now() });
|
|
57
|
-
return value;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export async function getGate(id, gatesRoot = DEFAULT_GATES_ROOT) {
|
|
61
|
-
const all = await listGates(gatesRoot);
|
|
62
|
-
const g = all.find(x => x.id === id);
|
|
63
|
-
if (!g) throw new Error(`Unknown gate: ${id}. Available: ${all.map(x => x.id).join(', ')}`);
|
|
64
|
-
const raw = await fs.readFile(g.absPath, 'utf8');
|
|
65
|
-
return { ...g, content: raw };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export async function gatesForStage(stage, gatesRoot = DEFAULT_GATES_ROOT) {
|
|
69
|
-
const all = await listGates(gatesRoot);
|
|
70
|
-
return all.filter(g => g.stage === stage || g.stage === 'any');
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function parseFrontmatter(raw) {
|
|
74
|
-
const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
75
|
-
if (!m) return {};
|
|
76
|
-
const out = {};
|
|
77
|
-
for (const line of m[1].split(/\r?\n/)) {
|
|
78
|
-
const mm = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
|
|
79
|
-
if (mm) out[mm[1]] = mm[2].trim();
|
|
80
|
-
}
|
|
81
|
-
return out;
|
|
82
|
-
}
|
|
1
|
+
// Gates extracted from inline workflow steps into reusable, named, file-backed checks.
|
|
2
|
+
//
|
|
3
|
+
// A gate is a markdown file under `gates/` with frontmatter:
|
|
4
|
+
//
|
|
5
|
+
// ---
|
|
6
|
+
// id: regression
|
|
7
|
+
// stage: pre-verify # pre-plan | pre-execute | pre-verify | post-verify | any
|
|
8
|
+
// blocking: true # true → must pass to advance; false → warn only
|
|
9
|
+
// ---
|
|
10
|
+
// <inline shell or natural-language check description>
|
|
11
|
+
//
|
|
12
|
+
// Gates are consumed by orchestrator workflows OR by `/saude` to spot-check.
|
|
13
|
+
// Running a gate here returns a structured verdict; actually executing the
|
|
14
|
+
// shell side is delegated to the orchestrator (we don't want to shell-out
|
|
15
|
+
// from the MCP server without confirmation).
|
|
16
|
+
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import fs from 'node:fs/promises';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
|
|
21
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
22
|
+
const __dirname = path.dirname(__filename);
|
|
23
|
+
export const DEFAULT_GATES_ROOT = path.resolve(__dirname, '../../gates');
|
|
24
|
+
|
|
25
|
+
// P2: TTL cache for listGates (mirrors PERF-01 in kit.js). Gates change rarely;
|
|
26
|
+
// inside a single Claude Code session we may call listGates → getGate → gatesForStage
|
|
27
|
+
// in sequence — without cache, that's 3 full directory walks of the gates dir.
|
|
28
|
+
const GATES_CACHE_TTL_MS = 30_000;
|
|
29
|
+
const gatesCache = new Map(); // gatesRoot -> { value, ts }
|
|
30
|
+
|
|
31
|
+
export function clearGatesCache() { gatesCache.clear(); }
|
|
32
|
+
|
|
33
|
+
export async function listGates(gatesRoot = DEFAULT_GATES_ROOT) {
|
|
34
|
+
const cached = gatesCache.get(gatesRoot);
|
|
35
|
+
if (cached && Date.now() - cached.ts < GATES_CACHE_TTL_MS) {
|
|
36
|
+
return cached.value;
|
|
37
|
+
}
|
|
38
|
+
let entries;
|
|
39
|
+
try { entries = await fs.readdir(gatesRoot, { withFileTypes: true }); }
|
|
40
|
+
catch { return []; }
|
|
41
|
+
const out = [];
|
|
42
|
+
for (const e of entries) {
|
|
43
|
+
if (!e.isFile() || !e.name.endsWith('.md')) continue;
|
|
44
|
+
const abs = path.join(gatesRoot, e.name);
|
|
45
|
+
const raw = await fs.readFile(abs, 'utf8');
|
|
46
|
+
const meta = parseFrontmatter(raw);
|
|
47
|
+
out.push({
|
|
48
|
+
id: meta.id ?? e.name.replace(/\.md$/, ''),
|
|
49
|
+
stage: meta.stage ?? 'any',
|
|
50
|
+
blocking: meta.blocking !== false && meta.blocking !== 'false',
|
|
51
|
+
description: meta.description ?? '',
|
|
52
|
+
absPath: abs,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
const value = out.sort((a, b) => a.id.localeCompare(b.id));
|
|
56
|
+
gatesCache.set(gatesRoot, { value, ts: Date.now() });
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function getGate(id, gatesRoot = DEFAULT_GATES_ROOT) {
|
|
61
|
+
const all = await listGates(gatesRoot);
|
|
62
|
+
const g = all.find(x => x.id === id);
|
|
63
|
+
if (!g) throw new Error(`Unknown gate: ${id}. Available: ${all.map(x => x.id).join(', ')}`);
|
|
64
|
+
const raw = await fs.readFile(g.absPath, 'utf8');
|
|
65
|
+
return { ...g, content: raw };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function gatesForStage(stage, gatesRoot = DEFAULT_GATES_ROOT) {
|
|
69
|
+
const all = await listGates(gatesRoot);
|
|
70
|
+
return all.filter(g => g.stage === stage || g.stage === 'any');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function parseFrontmatter(raw) {
|
|
74
|
+
const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
75
|
+
if (!m) return {};
|
|
76
|
+
const out = {};
|
|
77
|
+
for (const line of m[1].split(/\r?\n/)) {
|
|
78
|
+
const mm = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
|
|
79
|
+
if (mm) out[mm[1]] = mm[2].trim();
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|