@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.
Files changed (118) hide show
  1. package/README.md +1 -1
  2. package/bin/cli.js +2 -2
  3. package/bin/mcp.js +6 -6
  4. package/bin/ui.js +74 -74
  5. package/gates/ai-prompt-stability.md +120 -120
  6. package/gates/budget-description.md +68 -68
  7. package/gates/confidence.md +29 -29
  8. package/gates/dependency-check.md +33 -33
  9. package/gates/dept-cycle-prevention.md +179 -179
  10. package/gates/golden-signals-coverage.md +133 -133
  11. package/gates/legacy-refactor-safety.md +178 -178
  12. package/gates/multi-tenant-rls-coverage.md +102 -102
  13. package/gates/no-personal-uuid.md +72 -72
  14. package/gates/obs-agents-mcp-supabase.md +86 -86
  15. package/gates/obs-skills-frontmatter.md +76 -76
  16. package/gates/observability-coverage.md +151 -151
  17. package/gates/omm-no-regression.md +83 -83
  18. package/gates/postmortem-template-required.md +127 -127
  19. package/gates/prr-checklist-coverage.md +128 -128
  20. package/gates/regression.md +32 -32
  21. package/gates/release-pipeline-policy.md +132 -132
  22. package/gates/secrets-scan.md +33 -33
  23. package/gates/service-role-not-in-user-facing.md +113 -113
  24. package/gates/skill-must-include.md +71 -71
  25. package/gates/sync-idempotent.md +62 -62
  26. package/gates/verify-phase-goal.md +34 -34
  27. package/kit/agents/designer-ui.md +216 -216
  28. package/kit/agents/workflow-generator.md +537 -0
  29. package/kit/commands/adicionar-backlog.md +1 -1
  30. package/kit/commands/adicionar-fase.md +1 -1
  31. package/kit/commands/adicionar-tarefa.md +1 -1
  32. package/kit/commands/auditar-observabilidade.md +103 -103
  33. package/kit/commands/auditar-toil.md +129 -129
  34. package/kit/commands/caracterizar-prompt.md +195 -195
  35. package/kit/commands/criar-workflow.md +158 -0
  36. package/kit/commands/definir-perfil.md +1 -1
  37. package/kit/commands/definir-slo.md +108 -108
  38. package/kit/commands/fio.md +1 -1
  39. package/kit/commands/golden-signals.md +142 -142
  40. package/kit/commands/instrumentar-fase.md +200 -200
  41. package/kit/commands/investigar-producao.md +162 -162
  42. package/kit/commands/observabilidade.md +118 -118
  43. package/kit/commands/postmortem.md +179 -179
  44. package/kit/commands/prr.md +205 -205
  45. package/kit/commands/publicar-rapido.md +207 -207
  46. package/kit/commands/risk-budget.md +220 -220
  47. package/kit/commands/sre.md +230 -230
  48. package/kit/file-manifest.json +5 -2
  49. package/kit/framework/references/output-style.md +22 -22
  50. package/kit/hooks/post-apply-migration.js +199 -199
  51. package/kit/hooks/sidecar-tool-publisher.js +210 -210
  52. package/kit/skills/_shared-dados-distribuidos/glossary.md +224 -224
  53. package/kit/skills/_shared-legacy/glossary.md +389 -389
  54. package/kit/skills/_shared-multi-tenant/glossary.md +186 -186
  55. package/kit/skills/_shared-observability/glossary.md +396 -396
  56. package/kit/skills/_shared-sre/glossary.md +712 -712
  57. package/kit/skills/_shared-supabase/glossary.md +234 -234
  58. package/kit/skills/blameless-postmortems/SKILL.md +340 -340
  59. package/kit/skills/burn-rate-alerting/SKILL.md +258 -258
  60. package/kit/skills/cascading-failures/SKILL.md +311 -311
  61. package/kit/skills/core-analysis-loop/SKILL.md +352 -352
  62. package/kit/skills/distributed-tracing/SKILL.md +362 -362
  63. package/kit/skills/dynamic-workflow-authoring/SKILL.md +327 -0
  64. package/kit/skills/eliminating-toil/SKILL.md +243 -243
  65. package/kit/skills/event-based-slos/SKILL.md +296 -296
  66. package/kit/skills/four-golden-signals/SKILL.md +314 -314
  67. package/kit/skills/hermetic-builds/SKILL.md +323 -323
  68. package/kit/skills/legacy-monster-methods/SKILL.md +444 -444
  69. package/kit/skills/llm-as-dependency/SKILL.md +436 -436
  70. package/kit/skills/load-shedding-graceful-degradation/SKILL.md +396 -396
  71. package/kit/skills/observability-driven-development/SKILL.md +315 -315
  72. package/kit/skills/observability-maturity-model/SKILL.md +222 -222
  73. package/kit/skills/opentelemetry-standard/SKILL.md +351 -351
  74. package/kit/skills/production-readiness-review/SKILL.md +305 -305
  75. package/kit/skills/release-engineering/SKILL.md +367 -367
  76. package/kit/skills/retry-strategies/SKILL.md +372 -372
  77. package/kit/skills/sre-risk-management/SKILL.md +221 -221
  78. package/kit/skills/structured-events/SKILL.md +265 -265
  79. package/kit/skills/supabase-cron-queues/SKILL.md +275 -275
  80. package/kit/skills/supabase-database-functions/SKILL.md +332 -332
  81. package/kit/skills/supabase-declarative-schema/SKILL.md +183 -183
  82. package/kit/skills/supabase-pgvector-rag/SKILL.md +253 -253
  83. package/kit/skills/supabase-postgres-style/SKILL.md +138 -138
  84. package/kit/skills/supabase-storage/SKILL.md +234 -234
  85. package/kit/skills/telemetry-pipelines/SKILL.md +259 -259
  86. package/kit/skills/telemetry-sampling/SKILL.md +256 -256
  87. package/kit/skills/ui-anti-padroes-ia/SKILL.md +261 -261
  88. package/kit/skills/ui-contexto-produto/SKILL.md +248 -248
  89. package/kit/skills/ui-cor-estrategia/SKILL.md +213 -213
  90. package/kit/skills/ui-critica-auditoria/SKILL.md +260 -260
  91. package/kit/skills/ui-motion-funcional/SKILL.md +264 -264
  92. package/kit/skills/ui-ritmo-espacial/SKILL.md +259 -259
  93. package/kit/skills/ui-tipografia/SKILL.md +211 -211
  94. package/package.json +1 -1
  95. package/src/cli/index.js +1114 -1114
  96. package/src/cli/render.js +194 -194
  97. package/src/cli/upgrade-check.js +135 -135
  98. package/src/core/error-redaction.js +76 -76
  99. package/src/core/failures.js +153 -153
  100. package/src/core/gate-runner.js +205 -205
  101. package/src/core/gates.js +82 -82
  102. package/src/core/logger.js +170 -170
  103. package/src/core/manifest-verify.js +174 -174
  104. package/src/core/metrics.js +268 -268
  105. package/src/core/notify.js +60 -60
  106. package/src/core/path-safety.js +141 -141
  107. package/src/core/replays.js +120 -120
  108. package/src/core/ui.js +185 -185
  109. package/src/mcp-server/install.js +149 -149
  110. package/src/mcp-server/roots.js +124 -124
  111. package/src/ui/auto-spawn.js +113 -113
  112. package/src/ui/browser.js +78 -78
  113. package/src/ui/client.js +130 -130
  114. package/src/ui/events.js +65 -65
  115. package/src/ui/lockfile.js +191 -191
  116. package/src/ui/port.js +67 -67
  117. package/src/ui/server.js +547 -547
  118. package/src/ui/wrapper.js +129 -129
@@ -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
+ }