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