@ronaldjdevfs/forge 1.2.0 → 1.3.0-beta

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.
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync } from "fs";
4
+ import { join } from "path";
5
+ import { buildGraph } from "./graph.mjs";
6
+ import { buildContext } from "./context.mjs";
7
+ import { detectFeaturesOnSrc, allChecks } from "./detect.mjs";
8
+ import { evaluateRules } from "./registry/rules.mjs";
9
+ import {
10
+ CYAN, GREEN, RED, YELLOW, BOLD, RESET, DIM,
11
+ } from "./formatter.mjs";
12
+ import { writeAuditLog } from "./forgeSentinel-lib.mjs";
13
+
14
+ const ROOT = process.cwd();
15
+
16
+ const ALLOWED_EXTS = [".ts", ".mjs", ".js", ".tsx", ".jsx"];
17
+
18
+ /**
19
+ * Parse Cursor preToolUse event from stdin.
20
+ * Cursor sends: { toolName, args: { filePath, content } }
21
+ */
22
+ function parseCursorEvent(stdin) {
23
+ if (!stdin) return null;
24
+ try {
25
+ const event = JSON.parse(stdin);
26
+
27
+ // Cursor preToolUse format
28
+ if (event.toolName && event.args?.filePath) {
29
+ return { filePath: event.args.filePath, content: event.args.content || "" };
30
+ }
31
+
32
+ // Alternative: direct filePath + content
33
+ if (event.filePath && event.content !== undefined) {
34
+ return { filePath: event.filePath, content: event.content };
35
+ }
36
+
37
+ // Array of proposed edits
38
+ if (Array.isArray(event.edits)) {
39
+ return event.edits.map(e => ({
40
+ filePath: e.filePath || e.path || "",
41
+ content: e.content || e.newContent || "",
42
+ })).filter(e => e.filePath)[0] || null;
43
+ }
44
+
45
+ return null;
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ function isSourceFile(filePath) {
52
+ return filePath.startsWith("src/") && ALLOWED_EXTS.some(ext => filePath.endsWith(ext));
53
+ }
54
+
55
+ function hasCriticalViolations(result) {
56
+ return result.violations.some(v => v.severity === "CRITICAL" || v.severity === "ERROR");
57
+ }
58
+
59
+ /**
60
+ * Quick check against proposed content.
61
+ * Returns violations that would be introduced by the proposed content.
62
+ */
63
+ async function checkProposedFile(filePath, content) {
64
+ if (!isSourceFile(filePath)) {
65
+ return { violations: [], total: 0, hasCritical: false, hasErrors: false };
66
+ }
67
+
68
+ const ctx = await buildContext();
69
+ const features = detectFeaturesOnSrc();
70
+ const graph = buildGraph(ROOT);
71
+ const results = allChecks(features, graph, ctx);
72
+
73
+ const violations = [];
74
+ for (const [, cat] of Object.entries(results)) {
75
+ for (const check of cat.checks) {
76
+ if (check.pass) continue;
77
+
78
+ const touchesFile = (check.detail && check.detail.includes(filePath)) ||
79
+ (check.label && check.label.includes(filePath));
80
+
81
+ if (!touchesFile) continue;
82
+
83
+ violations.push(check);
84
+ }
85
+ }
86
+
87
+ const graphViolations = evaluateRules(graph, ctx).filter(v => {
88
+ return (v.file && v.file.includes(filePath)) ||
89
+ (v.from && v.from.includes(filePath)) ||
90
+ (v.to && v.to.includes(filePath));
91
+ });
92
+
93
+ const allViolations = [...violations, ...graphViolations];
94
+ const hasCritical = allViolations.some(v => v.severity === "CRITICAL");
95
+ const hasErrors = allViolations.some(v => v.severity === "ERROR");
96
+
97
+ return { violations: allViolations, total: allViolations.length, hasCritical, hasErrors };
98
+ }
99
+
100
+ function buildDenyPayload(result) {
101
+ const criticalCount = result.violations.filter(v => v.severity === "CRITICAL").length;
102
+ const errorCount = result.violations.filter(v => v.severity === "ERROR").length;
103
+
104
+ return {
105
+ deny: true,
106
+ severity: criticalCount > 0 ? "CRITICAL" : "ERROR",
107
+ message: `[forgeSmith] ⚠ ${result.total} violación(es) arquitectónica(s) (${criticalCount} CRITICAL, ${errorCount} ERROR). Corregí antes de escribir.`,
108
+ violations: result.violations.slice(0, 10).map(v => ({
109
+ severity: v.severity,
110
+ label: v.label,
111
+ rule: v.rule,
112
+ fix: v.fix,
113
+ })),
114
+ };
115
+ }
116
+
117
+ function buildAllowPayload() {
118
+ return { deny: false };
119
+ }
120
+
121
+ async function main() {
122
+ const chunks = [];
123
+ if (!process.stdin.isTTY) {
124
+ for await (const chunk of process.stdin) chunks.push(chunk);
125
+ }
126
+ const stdin = Buffer.concat(chunks).toString("utf-8");
127
+
128
+ if (!stdin) {
129
+ process.stdout.write(JSON.stringify(buildAllowPayload()));
130
+ process.exit(0);
131
+ }
132
+
133
+ const edit = parseCursorEvent(stdin);
134
+ if (!edit || !edit.filePath) {
135
+ process.stdout.write(JSON.stringify(buildAllowPayload()));
136
+ process.exit(0);
137
+ }
138
+
139
+ const result = await checkProposedFile(edit.filePath, edit.content || "");
140
+
141
+ writeAuditLog(process.env, {
142
+ ts: new Date().toISOString(),
143
+ hook: "forgeSmith",
144
+ file: edit.filePath,
145
+ total: result.total,
146
+ hasCritical: result.hasCritical,
147
+ hasErrors: result.hasErrors,
148
+ }, ROOT);
149
+
150
+ if (result.total > 0 && hasCriticalViolations(result)) {
151
+ process.stdout.write(JSON.stringify(buildDenyPayload(result)));
152
+ } else {
153
+ process.stdout.write(JSON.stringify(buildAllowPayload()));
154
+ }
155
+
156
+ process.exit(0);
157
+ }
158
+
159
+ if (process.argv[1]?.endsWith("forgeSmith.mjs")) {
160
+ main().catch(() => {
161
+ process.stdout.write(JSON.stringify({ deny: false }));
162
+ process.exit(0);
163
+ });
164
+ }
@@ -18,9 +18,16 @@ import { fileURLToPath } from 'node:url';
18
18
 
19
19
  const __dirname = fileURLToPath(new URL('.', import.meta.url));
20
20
 
21
- const HARNESS_DIRS = ['.claude', '.cursor', '.gemini', '.codex', '.agents', '.opencode', '.kiro'];
22
-
23
- const VALID_COMMANDS = ['forge', 'cast', 'inspect', 'quench', 'temper', 'chain', 'graph', 'relocate', 'reforge', 'smelt', 'inscribe'];
21
+ const HARNESS_DIRS = [
22
+ '.claude', '.cursor', '.gemini', '.codex', '.agents',
23
+ '.opencode', '.kiro', '.rovodev', '.trae', '.trae-cn', '.pi',
24
+ ];
25
+
26
+ const VALID_COMMANDS = [
27
+ 'forge', 'cast', 'inspect', 'quench', 'temper', 'chain', 'graph',
28
+ 'relocate', 'reforge', 'smelt', 'inscribe', 'armorer',
29
+ 'assay', 'nail', 'unnail', 'hook', 'api', 'rollback', 'state',
30
+ ];
24
31
 
25
32
  const PIN_MARKER = '<!-- forge-pinned-skill -->';
26
33
 
@@ -1,224 +1,30 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * posttool.mjs — PostToolUse hook para Forge.
4
+ * posttool.mjs — DEPRECATED: Backward-compat wrapper.
5
5
  *
6
- * Se ejecuta después de que el agente escribe código. Analiza los archivos
7
- * modificados y reporta violaciones arquitectónicas.
6
+ * Use forgeSentinel.mjs en su lugar.
7
+ * node forgeSentinel.mjs [options]
8
8
  *
9
- * Uso (CLI):
10
- * node posttool.mjs → analiza archivos changed en git
11
- * node posttool.mjs <file1> [file2...] → analiza archivos específicos
12
- * node posttool.mjs --diff → solo archivos changed vs default branch
13
- * node posttool.mjs --json → salida JSON
14
- * node posttool.mjs --strict → exit code 1 si hay CRITICAL/ERROR
15
- * node posttool.mjs --reminder → solo recordatorio (no bloquea)
16
- *
17
- * Uso (API):
18
- * import { postToolCheck } from "./posttool.mjs";
19
- * const result = await postToolCheck([...files]);
20
- *
21
- * Integración en SKILL.md (Fase 7 del pipeline):
22
- * node .opencode/skills/forge/scripts/posttool.mjs --reminder
9
+ * Este wrapper delega en forgeSentinel.mjs para mantener compatibilidad
10
+ * con scripts y referencias existentes.
23
11
  */
24
12
 
25
- import { readFileSync, existsSync, statSync, readdirSync } from "fs";
26
- import { join, relative, resolve, extname } from "path";
27
- import { execFileSync } from "child_process";
28
- import { buildGraph } from "./graph.mjs";
29
- import { buildContext } from "./context.mjs";
30
- import { detectFeaturesOnSrc, allChecks, loadAllInlineIgnores, isIgnored } from "./detect.mjs";
31
- import { evaluateRules } from "./registry/rules.mjs";
32
- import {
33
- CYAN, GREEN, RED, YELLOW, BOLD, RESET, DIM, GRAY,
34
- formatViolation, formatCheck, formatJson,
35
- } from "./formatter.mjs";
36
-
37
- const ROOT = process.cwd();
38
-
39
- function getChangedFiles() {
40
- let files = [];
41
- try {
42
- const defaultBranch = execFileSync("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] })
43
- .trim().replace("refs/remotes/origin/", "");
44
- const raw = execFileSync("git", ["diff", "--name-only", "--diff-filter=ACMR", `${defaultBranch}...HEAD`], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
45
- files = raw.trim().split("\n").filter(Boolean);
46
- } catch {}
47
-
48
- if (files.length === 0) {
49
- try {
50
- const raw = execFileSync("git", ["status", "--porcelain"], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
51
- files = raw.split("\n").filter(Boolean).map(l => l.slice(3));
52
- } catch {}
53
- }
54
-
55
- return files.filter(f => f.startsWith("src/"));
56
- }
57
-
58
- function readFile(path) {
59
- try { return readFileSync(path, "utf-8"); } catch { return null; }
60
- }
61
-
62
- /**
63
- * Check modified files for violations.
64
- * Returns { violations, graphViolations, total, hasCritical, hasErrors, summary }
65
- */
66
- export async function postToolCheck(files, opts = {}) {
67
- const { strict = false, reminder = false } = opts;
68
-
69
- if (!files || files.length === 0) {
70
- files = getChangedFiles();
71
- }
72
-
73
- // Only check source files
74
- files = files.filter(f =>
75
- f.endsWith(".ts") || f.endsWith(".mjs") || f.endsWith(".js") || f.endsWith(".tsx")
76
- );
77
-
78
- if (files.length === 0) {
79
- return { violations: [], graphViolations: [], total: 0, hasCritical: false, hasErrors: false, filesChecked: 0, summary: "Sin archivos fuente modificados" };
80
- }
81
-
82
- // Build context and run full audit
83
- const ctx = await buildContext();
84
- const features = detectFeaturesOnSrc();
85
- const graph = buildGraph(ROOT);
86
- const results = allChecks(features, graph, ctx);
87
-
88
- // Load inline ignores
89
- const allIgnores = loadAllInlineIgnores(join(ROOT, "src"));
90
-
91
- // Collect violations that touch the modified files
92
- const violations = [];
93
- for (const [, cat] of Object.entries(results)) {
94
- for (const check of cat.checks) {
95
- if (check.pass) continue;
96
-
97
- // Only include violations touching modified files
98
- const touchesFile = files.some(f =>
99
- (check.detail && check.detail.includes(f)) ||
100
- (check.label && check.label.includes(f))
101
- );
102
-
103
- if (!touchesFile && !reminder) continue;
104
-
105
- // Check inline ignores
106
- if (isIgnored(check, allIgnores)) continue;
107
-
108
- violations.push(check);
109
- }
110
- }
111
-
112
- // Also evaluate graph rules for changed files
113
- const graphViolations = evaluateRules(graph, ctx).filter(v => {
114
- const touchesFile = files.some(f =>
115
- (v.file && v.file.includes(f)) ||
116
- (v.from && v.from.includes(f)) ||
117
- (v.to && v.to.includes(f))
118
- );
119
- return touchesFile || reminder;
120
- });
121
-
122
- const allViolations = [...violations, ...graphViolations];
123
- const hasCritical = allViolations.some(v => v.severity === "CRITICAL");
124
- const hasErrors = allViolations.some(v => v.severity === "ERROR");
125
-
126
- return {
127
- violations: allViolations,
128
- total: allViolations.length,
129
- hasCritical,
130
- hasErrors,
131
- filesChecked: files.length,
132
- summary: hasCritical
133
- ? `⚠ ${allViolations.length} violación(es) — ${allViolations.filter(v => v.severity === "CRITICAL").length} CRITICAL, ${allViolations.filter(v => v.severity === "ERROR").length} ERROR`
134
- : hasErrors
135
- ? `⚠ ${allViolations.length} violación(es) — ${allViolations.filter(v => v.severity === "ERROR").length} ERROR`
136
- : `${allViolations.length} violación(es) menores (WARNING/INFO)`,
137
- };
138
- }
139
-
140
- function printResult(result, opts = {}) {
141
- const { reminder = false } = opts;
142
-
143
- console.log(`\n${BOLD}${CYAN}═══ Forge PostTool Hook ═══${RESET}`);
144
-
145
- if (result.total === 0) {
146
- console.log(` ${GREEN}✔${RESET} Sin violaciones arquitectónicas en archivos modificados`);
147
- console.log(` ${DIM}Archivos revisados: ${result.filesChecked}${RESET}\n`);
148
- return;
149
- }
150
-
151
- console.log(` ${DIM}Archivos revisados: ${result.filesChecked}${RESET}`);
152
- console.log(` ${DIM}Violaciones: ${result.total}${RESET}\n`);
13
+ import { join, dirname } from "path";
14
+ import { fileURLToPath } from "url";
153
15
 
154
- if (reminder) {
155
- console.log(` ${YELLOW}Recordatorio:${RESET} Los archivos modificados tienen ${result.total} violación(es) arquitectónica(s).`);
156
- console.log(` Ejecuta ${CYAN}forge quench${RESET} para ver el detalle completo.\n`);
16
+ const __dirname = dirname(fileURLToPath(import.meta.url));
157
17
 
158
- if (result.hasCritical) {
159
- const criticalCount = result.violations.filter(v => v.severity === "CRITICAL").length;
160
- console.log(` ${RED}⚠ ${criticalCount} violación(es) CRITICAL detectadas${RESET}`);
161
- }
162
- if (result.hasErrors) {
163
- const errorCount = result.violations.filter(v => v.severity === "ERROR").length;
164
- console.log(` ${RED}⚠ ${errorCount} violación(es) ERROR detectadas${RESET}`);
165
- }
166
-
167
- // Print first 5 violations as summary
168
- const toShow = result.violations.slice(0, 5);
169
- for (const v of toShow) {
170
- console.log(formatCheck(v));
171
- }
172
- if (result.violations.length > 5) {
173
- console.log(` ${DIM}... y ${result.violations.length - 5} más${RESET}`);
174
- }
175
- console.log();
176
- return;
177
- }
18
+ // Re-export for backward compat
19
+ export { runSentinelCheck as postToolCheck } from "./forgeSentinel-lib.mjs";
178
20
 
179
- // Full output
180
- for (const v of result.violations) {
181
- console.log(formatCheck(v));
182
- }
183
-
184
- console.log(`\n ${BOLD}Resumen:${RESET} ${result.summary}`);
185
- if (result.hasCritical) {
186
- console.log(` ${RED}⚠ Se requiere acción correctiva antes de continuar${RESET}`);
187
- }
188
- console.log();
189
- }
190
-
191
- async function main() {
21
+ if (process.argv[1]?.endsWith("posttool.mjs")) {
22
+ const forgeSentinel = join(__dirname, "forgeSentinel.mjs");
192
23
  const args = process.argv.slice(2);
193
- const isJson = args.includes("--json");
194
- const strict = args.includes("--strict");
195
- const reminder = args.includes("--reminder");
196
- const isDiff = args.includes("--diff");
197
-
198
- let files;
199
- if (isDiff) {
200
- files = getChangedFiles();
201
- } else {
202
- // Files passed as arguments
203
- files = args.filter(a => !a.startsWith("--"));
204
- if (files.length === 0) {
205
- files = getChangedFiles();
206
- }
207
- }
208
-
209
- const result = await postToolCheck(files, { strict, reminder });
210
-
211
- if (isJson) {
212
- console.log(formatJson(result));
213
- } else {
214
- printResult(result, { reminder });
215
- }
216
-
217
- if (strict && (result.hasCritical || result.hasErrors)) {
218
- process.exit(1);
24
+ const { execFileSync } = await import("child_process");
25
+ try {
26
+ execFileSync(process.execPath, [forgeSentinel, ...args], { stdio: "inherit" });
27
+ } catch {
28
+ process.exit(0);
219
29
  }
220
30
  }
221
-
222
- if (process.argv[1]?.endsWith("posttool.mjs")) {
223
- main().catch(console.error);
224
- }