@ryuenn3123/agentic-senior-core 6.3.0 → 6.4.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.
@@ -1,6 +1,5 @@
1
- // Agentic Senior Core — shared constants for hook modules
2
- // Single source of truth for thresholds and extension sets used by
3
- // post-edit-enforce.js and dedup-gate.js.
1
+ const fs = require('fs');
2
+ const path = require('path');
4
3
 
5
4
  const SOURCE_EXTENSIONS = new Set([
6
5
  'js', 'ts', 'mjs', 'cjs', 'jsx', 'tsx',
@@ -12,10 +11,39 @@ const NEW_FILE_LINE_THRESHOLD = 50;
12
11
  const SESSION_DRIFT_THRESHOLD = 4;
13
12
  const LADDER_PULSE_INTERVAL = 3;
14
13
 
14
+ function loadDedupConfig(cwd = process.cwd()) {
15
+ const candidates = [
16
+ path.join(cwd, '.asc', 'dedup-config.json'),
17
+ path.join(cwd, '.agents', 'dedup-config.json'),
18
+ ];
19
+ for (let i = 0; i < candidates.length; i++) {
20
+ try {
21
+ if (fs.existsSync(candidates[i])) {
22
+ return JSON.parse(fs.readFileSync(candidates[i], 'utf8'));
23
+ }
24
+ } catch (_) {}
25
+ }
26
+ return {};
27
+ }
28
+
29
+ function getThresholds(cwd = process.cwd()) {
30
+ const config = loadDedupConfig(cwd);
31
+ return {
32
+ NEW_FILE_LINE_THRESHOLD: typeof config.NEW_FILE_LINE_THRESHOLD === 'number'
33
+ ? config.NEW_FILE_LINE_THRESHOLD
34
+ : NEW_FILE_LINE_THRESHOLD,
35
+ LOC_DELTA_THRESHOLD: typeof config.LOC_DELTA_THRESHOLD === 'number'
36
+ ? config.LOC_DELTA_THRESHOLD
37
+ : LOC_DELTA_THRESHOLD,
38
+ };
39
+ }
40
+
15
41
  module.exports = {
16
42
  SOURCE_EXTENSIONS,
17
43
  LOC_DELTA_THRESHOLD,
18
44
  NEW_FILE_LINE_THRESHOLD,
19
45
  SESSION_DRIFT_THRESHOLD,
20
46
  LADDER_PULSE_INTERVAL,
47
+ getThresholds,
48
+ loadDedupConfig,
21
49
  };
@@ -56,9 +56,9 @@ process.stdin.on('data', chunk => {
56
56
  const ext = path.extname(filePath).slice(1);
57
57
  if (!SOURCE_EXTENSIONS.has(ext)) { process.exit(0); return; }
58
58
 
59
- if (!isQualifyingEdit(toolName, toolInput)) { process.exit(0); return; }
60
-
61
59
  const config = loadDedupConfig();
60
+ if (!isQualifyingEdit(toolName, toolInput, config)) { process.exit(0); return; }
61
+
62
62
  const scanDir = resolveScanDir(filePath, config);
63
63
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'asc-dedup-'));
64
64
 
@@ -101,50 +101,67 @@ function extractToolCallFromTranscript(transcriptPath, stepIdx) {
101
101
 
102
102
  // Find the step with matching step_index that contains tool_calls
103
103
  for (var i = lines.length - 1; i >= 0; i--) {
104
- var step = JSON.parse(lines[i]);
105
- if (step.step_index !== stepIdx || !step.tool_calls) continue;
106
-
107
- for (var j = 0; j < step.tool_calls.length; j++) {
108
- var tc = step.tool_calls[j];
109
- if (tc.name === 'replace_file_content' || tc.name === 'multi_replace_file_content') {
110
- return {
111
- toolName: 'Edit',
112
- toolInput: {
113
- file_path: tc.args.TargetFile || '',
114
- new_string: tc.args.ReplacementContent || '',
115
- old_string: tc.args.TargetContent || '',
116
- },
117
- };
118
- } else if (tc.name === 'write_to_file') {
119
- return {
120
- toolName: 'Write',
121
- toolInput: {
122
- file_path: tc.args.TargetFile || '',
123
- content: tc.args.CodeContent || '',
124
- },
125
- };
104
+ try {
105
+ var step = JSON.parse(lines[i]);
106
+ if (step.step_index !== stepIdx || !step.tool_calls) continue;
107
+
108
+ for (var j = 0; j < step.tool_calls.length; j++) {
109
+ var tc = step.tool_calls[j];
110
+ if (tc.name === 'replace_file_content' || tc.name === 'multi_replace_file_content') {
111
+ return {
112
+ toolName: 'Edit',
113
+ toolInput: {
114
+ file_path: tc.args.TargetFile || '',
115
+ new_string: tc.args.ReplacementContent || '',
116
+ old_string: tc.args.TargetContent || '',
117
+ },
118
+ };
119
+ } else if (tc.name === 'write_to_file') {
120
+ return {
121
+ toolName: 'Write',
122
+ toolInput: {
123
+ file_path: tc.args.TargetFile || '',
124
+ content: tc.args.CodeContent || '',
125
+ },
126
+ };
127
+ }
128
+ }
129
+ } catch (lineErr) {
130
+ if (process.env.ASC_DEBUG) {
131
+ console.error('[ASC Debug] Transcript line parse failed line ' + i + ':', lineErr.message);
126
132
  }
133
+ // Skip malformed line and keep scanning
127
134
  }
128
135
  }
129
136
  return null;
130
- } catch (_) {
137
+ } catch (err) {
138
+ if (process.env.ASC_DEBUG) {
139
+ console.error('[ASC Debug] extractToolCallFromTranscript failed:', err.message);
140
+ }
131
141
  return null;
132
142
  }
133
143
  }
134
144
 
135
- function isQualifyingEdit(toolName, toolInput) {
145
+ function isQualifyingEdit(toolName, toolInput, config) {
136
146
  var isWrite = ['Write', 'write_to_file', 'write_file'].indexOf(toolName) !== -1;
137
147
  var isEdit = ['Edit', 'replace_file_content', 'multi_replace_file_content'].indexOf(toolName) !== -1;
138
148
 
149
+ var newFileThreshold = (config && typeof config.NEW_FILE_LINE_THRESHOLD === 'number')
150
+ ? config.NEW_FILE_LINE_THRESHOLD
151
+ : NEW_FILE_LINE_THRESHOLD;
152
+ var locDeltaThreshold = (config && typeof config.LOC_DELTA_THRESHOLD === 'number')
153
+ ? config.LOC_DELTA_THRESHOLD
154
+ : LOC_DELTA_THRESHOLD;
155
+
139
156
  if (isWrite) {
140
157
  var content = toolInput.content || toolInput.CodeContent || '';
141
- return content.split('\n').length > NEW_FILE_LINE_THRESHOLD;
158
+ return content.split('\n').length > newFileThreshold;
142
159
  }
143
160
  if (isEdit) {
144
161
  var newStr = toolInput.new_string || toolInput.ReplacementContent || toolInput.content || '';
145
162
  var oldStr = toolInput.old_string || toolInput.TargetContent || '';
146
163
  var delta = newStr.split('\n').length - oldStr.split('\n').length;
147
- return delta > LOC_DELTA_THRESHOLD;
164
+ return delta > locDeltaThreshold;
148
165
  }
149
166
  return false;
150
167
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-senior-core",
3
- "version": "6.3.0",
3
+ "version": "6.4.0",
4
4
  "description": "Universal AI coding rules. Because your AI writes code like it gets paid by the line.",
5
5
  "contextFileName": "rules/agentic-senior-core.md",
6
6
  "rules": [
@@ -38,6 +38,7 @@ When you pick the minimal option at step 5 or 6, and it isn't obviously trivial:
38
38
  - Delete code that carries no behavior, safety, or test value.
39
39
  - When brevity and readability conflict, readability wins.
40
40
  - Prefer named functions over closures/inline lambdas once logic exceeds a trivial expression.
41
+ - Arrow function shorthand (no braces, implicit return) must not return a void-typed expression — e.g. `onClick={() => setCount(count + 1)}` or `arr.forEach(item => sideEffect(item))`. This trips `@typescript-eslint/no-confusing-void-expression` under strict TS lint configs. Not JSX-specific — applies to any callback assignment in `.js`/`.ts`/`.jsx`/`.tsx` where the shorthand body calls a void-returning function. Use braces instead: `onClick={() => { setCount(count + 1); }}`.
41
42
 
42
43
  ## Architecture
43
44
 
@@ -31,7 +31,8 @@ Adapter hosts (one file per project): Cursor, Devin Desktop, Cline, GitHub Copil
31
31
  asc status # Show detected hosts
32
32
  asc adapter --all # Generate all adapters
33
33
  asc adapter --cursor # Generate for specific host
34
- asc uninstall # Remove all ASC adapter files
34
+ asc install-git-hook # Install native Git pre-commit hook (recommended for all hosts)
35
+ asc uninstall # Remove all ASC adapter files and git hooks
35
36
  asc uninstall --dry-run # Preview what would be removed
36
37
  ```
37
38
 
@@ -41,3 +42,4 @@ asc uninstall --dry-run # Preview what would be removed
41
42
  - Cursor uses `.mdc` format with `alwaysApply: true` frontmatter.
42
43
  - Windsurf is now Devin Desktop. Use `--devin` for the preferred path, `--windsurf` for legacy.
43
44
  - Zed also reads `AGENTS.md` natively, so the adapter is optional.
45
+ - **Git Pre-Commit Hook (`asc install-git-hook`)**: Host plugin runtimes vary — adapter hosts and certain chat surfaces (e.g., Antigravity IDE / Antigravity 2.0 chat interface) do not run agent lifecycle hooks. Installing the native Git pre-commit hook ensures 100% deterministic duplicate code blocking and ESLint auto-fixing directly via Git on all hosts.
@@ -38,6 +38,7 @@ When you pick the minimal option at step 5 or 6, and it isn't obviously trivial:
38
38
  - Delete code that carries no behavior, safety, or test value.
39
39
  - When brevity and readability conflict, readability wins.
40
40
  - Prefer named functions over closures/inline lambdas once logic exceeds a trivial expression.
41
+ - Arrow function shorthand (no braces, implicit return) must not return a void-typed expression — e.g. `onClick={() => setCount(count + 1)}` or `arr.forEach(item => sideEffect(item))`. This trips `@typescript-eslint/no-confusing-void-expression` under strict TS lint configs. Not JSX-specific — applies to any callback assignment in `.js`/`.ts`/`.jsx`/`.tsx` where the shorthand body calls a void-returning function. Use braces instead: `onClick={() => { setCount(count + 1); }}`.
41
42
 
42
43
  ## Architecture
43
44
 
package/AGENTS.md CHANGED
@@ -33,6 +33,7 @@ When you pick the minimal option at step 5 or 6, and it isn't obviously trivial:
33
33
  - Delete code that carries no behavior, safety, or test value.
34
34
  - When brevity and readability conflict, readability wins.
35
35
  - Prefer named functions over closures/inline lambdas once logic exceeds a trivial expression.
36
+ - Arrow function shorthand (no braces, implicit return) must not return a void-typed expression — e.g. `onClick={() => setCount(count + 1)}` or `arr.forEach(item => sideEffect(item))`. This trips `@typescript-eslint/no-confusing-void-expression` under strict TS lint configs. Not JSX-specific — applies to any callback assignment in `.js`/`.ts`/`.jsx`/`.tsx` where the shorthand body calls a void-returning function. Use braces instead: `onClick={() => { setCount(count + 1); }}`.
36
37
 
37
38
  ## Architecture
38
39
 
@@ -31,14 +31,15 @@ function printUsage() {
31
31
  console.log('Adapter install (one file per project):');
32
32
  console.log(' asc adapter --cursor --devin --cline --copilot --kiro --continue --zed --aider --kilocode --roo --openhands --windsurf --all\n');
33
33
  console.log('Commands:');
34
- console.log(' adapter Generate instruction-tier adapter files');
35
- console.log(' global Install rules to user-level (global) locations');
36
- console.log(' uninstall Remove ASC adapter files from this project');
37
- console.log(' clean Remove v4 per-project artifacts');
38
- console.log(' status Show detected IDEs and install hints');
39
- console.log(' mcp Start MCP stdio server');
40
- console.log(' --version Show version');
41
- console.log(' --help Show this help');
34
+ console.log(' adapter Generate instruction-tier adapter files');
35
+ console.log(' global Install rules to user-level (global) locations');
36
+ console.log(' install-git-hook Install Git pre-commit hook for duplicate code enforcement');
37
+ console.log(' uninstall Remove ASC adapter files and git hooks from this project');
38
+ console.log(' clean Remove v4 per-project artifacts');
39
+ console.log(' status Show detected IDEs and install hints');
40
+ console.log(' mcp Start MCP stdio server');
41
+ console.log(' --version Show version');
42
+ console.log(' --help Show this help');
42
43
  }
43
44
 
44
45
  async function main() {
@@ -55,6 +56,12 @@ async function main() {
55
56
  return;
56
57
  }
57
58
 
59
+ if (commandArgument === 'install-git-hook' || commandArgument === 'git-hook') {
60
+ const { runGitHookCommand } = await import('../lib/cli/commands/git-hook.mjs');
61
+ await runGitHookCommand(commandArguments);
62
+ return;
63
+ }
64
+
58
65
  if (commandArgument === 'adapter') {
59
66
  const { runAdapterCommand } = await import('../lib/cli/commands/adapter.mjs');
60
67
  await runAdapterCommand(commandArguments);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-senior-core",
3
- "version": "6.3.0",
3
+ "version": "6.4.0",
4
4
  "description": "Universal AI coding rules. Write code like a staff engineer.",
5
5
  "author": "fatidaprilian",
6
6
  "license": "MIT",
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
+ import { installGitPreCommitHook } from './git-hook-generator.mjs';
4
5
 
5
6
  const currentFilePath = fileURLToPath(import.meta.url);
6
7
  const currentDirectoryPath = path.dirname(currentFilePath);
@@ -157,5 +158,12 @@ export async function runAdapterCommand(commandArguments) {
157
158
  if (success) successCount++;
158
159
  }
159
160
 
160
- console.log(`\nGenerated ${successCount}/${requestedAdapters.length} adapter file(s).`);
161
+ // Automatically install Git pre-commit hook for non-hook host backstop
162
+ const hookResult = installGitPreCommitHook({ cwd: targetDirectory });
163
+ if (hookResult.installed) {
164
+ const relPath = path.relative(targetDirectory, hookResult.hookPath) || hookResult.hookPath;
165
+ console.log(` Git Pre-Commit Hook: ${relPath} ... OK`);
166
+ }
167
+
168
+ console.log(`\nGenerated ${successCount}/${requestedAdapters.length} adapter file(s) and configured Git pre-commit hook.`);
161
169
  }
@@ -0,0 +1,273 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { compileAndSaveValidator } from '../../core/rule-compiler.mjs';
4
+
5
+ export const ASC_HOOK_HEADER = '# Agentic Senior Core Git Pre-Commit Hook';
6
+ export const ASC_HOOK_RUNNER_REL_PATH = path.join('.asc', 'hooks', 'pre-commit-runner.cjs').replace(/\\/g, '/');
7
+
8
+ export function generatePreCommitRunnerScript() {
9
+ return `#!/usr/bin/env node
10
+ // # Agentic Senior Core -- Git Pre-Commit Dedup & Quality Gate
11
+ // Auto-generated by Agentic Senior Core (ASC). DO NOT EDIT DIRECTLY.
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const os = require('os');
16
+ const { execSync } = require('child_process');
17
+
18
+ const SOURCE_EXTENSIONS = new Set([
19
+ 'js', 'ts', 'mjs', 'cjs', 'jsx', 'tsx',
20
+ 'py', 'rb', 'go', 'rs', 'java', 'kt', 'swift', 'cs',
21
+ ]);
22
+
23
+ const JSCPD_TIMEOUT_MS = 10000;
24
+
25
+ function getStagedFiles(cwd) {
26
+ try {
27
+ const output = execSync('git diff --cached --name-only --diff-filter=ACM', { encoding: 'utf8', cwd });
28
+ return output.split('\\n').map(f => f.trim()).filter(Boolean);
29
+ } catch (err) {
30
+ return [];
31
+ }
32
+ }
33
+
34
+ function loadDedupConfig(cwd) {
35
+ const candidates = [
36
+ path.join(cwd, '.asc', 'dedup-config.json'),
37
+ path.join(cwd, '.agents', 'dedup-config.json'),
38
+ ];
39
+ for (let i = 0; i < candidates.length; i++) {
40
+ try {
41
+ if (fs.existsSync(candidates[i])) {
42
+ return JSON.parse(fs.readFileSync(candidates[i], 'utf8'));
43
+ }
44
+ } catch (_) {}
45
+ }
46
+ return { minTokens: 30, ignoreDirs: ['tests', 'migrations', 'generated', 'node_modules'] };
47
+ }
48
+
49
+ function runEslintAutoFix(cwd, stagedFiles) {
50
+ const hasTsConfig = fs.existsSync(path.join(cwd, 'tsconfig.json'));
51
+ if (!hasTsConfig) return;
52
+
53
+ const eslintConfigNames = [
54
+ 'eslint.config.js', 'eslint.config.mjs', 'eslint.config.cjs', 'eslint.config.ts',
55
+ '.eslintrc', '.eslintrc.js', '.eslintrc.cjs', '.eslintrc.json', '.eslintrc.yaml', '.eslintrc.yml'
56
+ ];
57
+ const hasEslintConfig = eslintConfigNames.some(name => fs.existsSync(path.join(cwd, name)));
58
+ if (!hasEslintConfig) return;
59
+
60
+ const tsJsExts = new Set(['js', 'ts', 'jsx', 'tsx', 'mjs', 'cjs']);
61
+ const stagedTsJsFiles = stagedFiles.filter(file => {
62
+ const ext = path.extname(file).slice(1).toLowerCase();
63
+ return tsJsExts.has(ext);
64
+ });
65
+
66
+ if (stagedTsJsFiles.length === 0) return;
67
+
68
+ try {
69
+ const fileList = stagedTsJsFiles.map(f => \`"\${f}"\`).join(' ');
70
+ execSync(\`npx eslint --fix \${fileList}\`, { stdio: 'ignore', cwd });
71
+ execSync(\`git add \${fileList}\`, { stdio: 'ignore', cwd });
72
+ } catch (_) {
73
+ // Silently ignore ESLint auto-fix errors (never block commit on lint failure alone)
74
+ }
75
+ }
76
+
77
+ function runJscpdScan(scanCmd, cwd, tmpDir) {
78
+ const binaries = ['bunx jscpd', 'npx jscpd@5', 'npx jscpd'];
79
+ for (let i = 0; i < binaries.length; i++) {
80
+ try {
81
+ execSync(binaries[i] + scanCmd, {
82
+ timeout: JSCPD_TIMEOUT_MS,
83
+ stdio: 'pipe',
84
+ cwd,
85
+ });
86
+ return loadReport(tmpDir);
87
+ } catch (_) {
88
+ // Try next binary
89
+ }
90
+ }
91
+ return null;
92
+ }
93
+
94
+ function loadReport(tmpDir) {
95
+ const reportPath = path.join(tmpDir, 'jscpd-report.json');
96
+ if (!fs.existsSync(reportPath)) return null;
97
+ try {
98
+ return JSON.parse(fs.readFileSync(reportPath, 'utf8'));
99
+ } catch (_) {
100
+ return null;
101
+ }
102
+ }
103
+
104
+ function checkForDuplicates(report, stagedSourceFiles, cwd) {
105
+ const duplicates = report.duplicates || [];
106
+ if (duplicates.length === 0) return null;
107
+
108
+ const normalizedStagedMap = new Map();
109
+ for (const f of stagedSourceFiles) {
110
+ const norm = path.resolve(cwd, f).replace(/\\\\/g, '/').toLowerCase();
111
+ normalizedStagedMap.set(norm, f);
112
+ }
113
+
114
+ for (let i = 0; i < duplicates.length; i++) {
115
+ const dup = duplicates[i];
116
+ const firstName = path.resolve(cwd, dup.firstFile.name).replace(/\\\\/g, '/').toLowerCase();
117
+ const secondName = path.resolve(cwd, dup.secondFile.name).replace(/\\\\/g, '/').toLowerCase();
118
+
119
+ const isFirstStaged = normalizedStagedMap.has(firstName);
120
+ const isSecondStaged = normalizedStagedMap.has(secondName);
121
+
122
+ if (isFirstStaged || isSecondStaged) {
123
+ const stagedFile = isFirstStaged ? normalizedStagedMap.get(firstName) : normalizedStagedMap.get(secondName);
124
+ const matchedFile = isFirstStaged ? path.basename(dup.secondFile.name) : path.basename(dup.firstFile.name);
125
+ const lines = dup.lines || 0;
126
+ const totalLines = (report.statistics && report.statistics.total && report.statistics.total.lines) || 1;
127
+ const percent = Math.round((lines / totalLines) * 100);
128
+ return { stagedFile, matchedFile, lines, percent };
129
+ }
130
+ }
131
+ return null;
132
+ }
133
+
134
+ function runPreCommitGate() {
135
+ const cwd = process.cwd();
136
+
137
+ // 0. Run syntactic validator if present
138
+ const validatorPath = path.join(cwd, '.asc', 'hooks', 'pre-commit-validator.cjs');
139
+ if (fs.existsSync(validatorPath)) {
140
+ try {
141
+ execSync(\`node "\${validatorPath}"\`, { stdio: 'inherit', cwd });
142
+ } catch (valErr) {
143
+ process.exit(valErr.status || 1);
144
+ }
145
+ }
146
+
147
+ const stagedFiles = getStagedFiles(cwd);
148
+ if (stagedFiles.length === 0) {
149
+ process.exit(0);
150
+ }
151
+
152
+ // 1. Filter staged files by SOURCE_EXTENSIONS
153
+ const stagedSourceFiles = stagedFiles.filter(f => {
154
+ const ext = path.extname(f).slice(1).toLowerCase();
155
+ return SOURCE_EXTENSIONS.has(ext);
156
+ });
157
+
158
+ if (stagedSourceFiles.length === 0) {
159
+ process.exit(0);
160
+ }
161
+
162
+ // 2. Optional pre-step: ESLint auto-fix
163
+ runEslintAutoFix(cwd, stagedFiles);
164
+
165
+ // Re-fetch staged files after possible eslint auto-fix & git add
166
+ const currentStagedSource = getStagedFiles(cwd).filter(f => {
167
+ const ext = path.extname(f).slice(1).toLowerCase();
168
+ return SOURCE_EXTENSIONS.has(ext);
169
+ });
170
+
171
+ if (currentStagedSource.length === 0) {
172
+ process.exit(0);
173
+ }
174
+
175
+ // 3. Run jscpd dedup scan
176
+ const config = loadDedupConfig(cwd);
177
+ const scanDirs = Array.from(new Set(currentStagedSource.map(f => {
178
+ const resolved = path.resolve(cwd, f);
179
+ return path.dirname(resolved);
180
+ })));
181
+
182
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'asc-git-dedup-'));
183
+ const ignoreFlags = (config.ignoreDirs || ['tests', 'migrations', 'generated', 'node_modules'])
184
+ .map(d => \`--ignore "\${d}"\`).join(' ');
185
+ const minTokens = config.minTokens || 30;
186
+
187
+ let finding = null;
188
+
189
+ for (const scanDir of scanDirs) {
190
+ const scanCmd = \` "\${scanDir}" --min-tokens \${minTokens} --reporters json --silent --output "\${tmpDir}" \${ignoreFlags}\`;
191
+ const report = runJscpdScan(scanCmd, cwd, tmpDir);
192
+ if (report) {
193
+ finding = checkForDuplicates(report, currentStagedSource, cwd);
194
+ if (finding) break;
195
+ }
196
+ }
197
+
198
+ try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) {}
199
+
200
+ if (finding) {
201
+ console.error(\`\\x1b[31m[ASC Dedup]\\x1b[0m \${path.basename(finding.stagedFile)} looks similar to \${finding.matchedFile} (\${finding.percent}% overlap, \${finding.lines} lines)\`);
202
+ console.error(\`\\x1b[33mCommit blocked to prevent duplication. Use 'git commit --no-verify' to bypass if intentional.\\x1b[0m\`);
203
+ process.exit(1);
204
+ }
205
+
206
+ process.exit(0);
207
+ }
208
+
209
+ runPreCommitGate();
210
+ `;
211
+ }
212
+
213
+ /**
214
+ * Installs or updates the Git pre-commit hook in the target repository.
215
+ * @param {Object} options
216
+ * @param {string} [options.cwd] Working directory.
217
+ * @returns {{ installed: boolean, hookPath: string, runnerPath: string, reason?: string }} Installation result.
218
+ */
219
+ export function installGitPreCommitHook({ cwd = process.cwd() } = {}) {
220
+ const gitDir = path.join(cwd, '.git');
221
+ const huskyDir = path.join(cwd, '.husky');
222
+
223
+ if (!fs.existsSync(gitDir) && !fs.existsSync(huskyDir)) {
224
+ return { installed: false, hookPath: '', runnerPath: '', reason: 'Not a git repository' };
225
+ }
226
+
227
+ const ascHooksDir = path.join(cwd, '.asc', 'hooks');
228
+ if (!fs.existsSync(ascHooksDir)) {
229
+ fs.mkdirSync(ascHooksDir, { recursive: true });
230
+ }
231
+
232
+ // Compile syntactic validator script as fallback/compatibility
233
+ try { compileAndSaveValidator({ cwd }); } catch (_) {}
234
+
235
+ const runnerPath = path.join(ascHooksDir, 'pre-commit-runner.cjs');
236
+ const runnerScriptContent = generatePreCommitRunnerScript();
237
+ fs.writeFileSync(runnerPath, runnerScriptContent, { encoding: 'utf8', mode: 0o755 });
238
+
239
+ let hookPath = '';
240
+ const invocationCommand = `node .asc/hooks/pre-commit-runner.cjs`;
241
+
242
+ if (fs.existsSync(huskyDir)) {
243
+ hookPath = path.join(huskyDir, 'pre-commit');
244
+ if (fs.existsSync(hookPath)) {
245
+ const existing = fs.readFileSync(hookPath, 'utf8');
246
+ if (!existing.includes(ASC_HOOK_HEADER) && !existing.includes(ASC_HOOK_RUNNER_REL_PATH)) {
247
+ const updated = existing.trimEnd() + `\n\n${ASC_HOOK_HEADER}\n${invocationCommand}\n`;
248
+ fs.writeFileSync(hookPath, updated, { encoding: 'utf8', mode: 0o755 });
249
+ }
250
+ } else {
251
+ const content = `#!/bin/sh\n${ASC_HOOK_HEADER}\n${invocationCommand}\n`;
252
+ fs.writeFileSync(hookPath, content, { encoding: 'utf8', mode: 0o755 });
253
+ }
254
+ } else {
255
+ const hooksDir = path.join(gitDir, 'hooks');
256
+ if (!fs.existsSync(hooksDir)) {
257
+ fs.mkdirSync(hooksDir, { recursive: true });
258
+ }
259
+ hookPath = path.join(hooksDir, 'pre-commit');
260
+ if (fs.existsSync(hookPath)) {
261
+ const existing = fs.readFileSync(hookPath, 'utf8');
262
+ if (!existing.includes(ASC_HOOK_HEADER) && !existing.includes(ASC_HOOK_RUNNER_REL_PATH)) {
263
+ const updated = existing.trimEnd() + `\n\n${ASC_HOOK_HEADER}\n${invocationCommand}\n`;
264
+ fs.writeFileSync(hookPath, updated, { encoding: 'utf8', mode: 0o755 });
265
+ }
266
+ } else {
267
+ const content = `#!/bin/sh\n${ASC_HOOK_HEADER}\n${invocationCommand}\n`;
268
+ fs.writeFileSync(hookPath, content, { encoding: 'utf8', mode: 0o755 });
269
+ }
270
+ }
271
+
272
+ return { installed: true, hookPath, runnerPath };
273
+ }
@@ -0,0 +1,24 @@
1
+ import path from 'node:path';
2
+ import { installGitPreCommitHook } from './git-hook-generator.mjs';
3
+
4
+ /**
5
+ * Runs the `asc install-git-hook` command to install Git pre-commit hooks.
6
+ * @param {string[]} commandArguments
7
+ */
8
+ export async function runGitHookCommand(commandArguments) {
9
+ const targetDirectory = process.cwd();
10
+ console.log('Agentic Senior Core -- Git Pre-Commit Hook Installer\n');
11
+
12
+ const result = installGitPreCommitHook({ cwd: targetDirectory });
13
+
14
+ if (!result.installed) {
15
+ console.error(`Failed: ${result.reason || 'Could not install pre-commit hook'}`);
16
+ process.exit(1);
17
+ }
18
+
19
+ console.log(` Pre-commit hook: ${result.hookPath} ... OK`);
20
+ if (result.runnerPath) {
21
+ console.log(` Runner script: ${result.runnerPath} ... OK`);
22
+ }
23
+ console.log('\nGit pre-commit hook successfully installed.');
24
+ }
@@ -316,5 +316,27 @@ export async function runGlobalCommand(commandArguments) {
316
316
  if (success) successCount++;
317
317
  }
318
318
 
319
- console.log(`\nInstalled ${successCount}/${requestedTargets.length} global target(s).`);
319
+ // Automatically setup global Git hook runner in ~/.asc/hooks and configure git config --global core.hooksPath
320
+ try {
321
+ const { execSync } = await import('node:child_process');
322
+ const { generatePreCommitRunnerScript } = await import('./git-hook-generator.mjs');
323
+
324
+ const ascGlobalHooksDir = path.join(HOME, '.asc', 'hooks');
325
+ await fs.mkdir(ascGlobalHooksDir, { recursive: true });
326
+
327
+ const globalRunnerPath = path.join(ascGlobalHooksDir, 'pre-commit-runner.cjs');
328
+ const runnerContent = generatePreCommitRunnerScript();
329
+ await fs.writeFile(globalRunnerPath, runnerContent, { encoding: 'utf8', mode: 0o755 });
330
+
331
+ const globalHookPath = path.join(ascGlobalHooksDir, 'pre-commit');
332
+ const hookContent = `#!/bin/sh\n# Agentic Senior Core Global Git Pre-Commit Hook\nnode "${globalRunnerPath.replace(/\\/g, '/')}"\n`;
333
+ await fs.writeFile(globalHookPath, hookContent, { encoding: 'utf8', mode: 0o755 });
334
+
335
+ execSync(`git config --global core.hooksPath "${ascGlobalHooksDir.replace(/\\/g, '/')}"`, { stdio: 'ignore' });
336
+ console.log(` Global Git Pre-Commit Hook: ~/.asc/hooks (via git config --global) ... OK`);
337
+ } catch (err) {
338
+ // Best effort global git hook registration
339
+ }
340
+
341
+ console.log(`\nInstalled ${successCount}/${requestedTargets.length} global target(s) and configured Global Git Hook.`);
320
342
  }
@@ -18,6 +18,12 @@ const ADAPTER_FILES = [
18
18
  { label: 'Roo Code', path: '.roo/rules/agentic-senior-core.md' },
19
19
  { label: 'OpenHands', path: '.openhands/microagents/agentic-senior-core.md' },
20
20
  { label: 'ASC Compiled Validator', path: '.asc/hooks/pre-commit-validator.cjs' },
21
+ { label: 'ASC Git Pre-Commit Runner', path: '.asc/hooks/pre-commit-runner.cjs' },
22
+ ];
23
+
24
+ const GIT_HOOK_PATHS = [
25
+ { label: 'Husky Pre-Commit Hook', path: '.husky/pre-commit' },
26
+ { label: 'Git Pre-Commit Hook', path: '.git/hooks/pre-commit' },
21
27
  ];
22
28
 
23
29
  async function pathExists(filePath) {
@@ -32,7 +38,7 @@ async function pathExists(filePath) {
32
38
  async function isAscFile(filePath) {
33
39
  try {
34
40
  const content = await fs.readFile(filePath, 'utf8');
35
- return content.includes(ASC_SIGNATURE);
41
+ return content.includes(ASC_SIGNATURE) || content.includes('pre-commit-runner.cjs');
36
42
  } catch {
37
43
  return false;
38
44
  }
@@ -69,6 +75,50 @@ export async function runUninstallCommand(commandArguments) {
69
75
  }
70
76
  }
71
77
 
78
+ for (const hookItem of GIT_HOOK_PATHS) {
79
+ const fullPath = path.join(targetDirectory, hookItem.path);
80
+ if (!(await pathExists(fullPath))) continue;
81
+
82
+ try {
83
+ const content = await fs.readFile(fullPath, 'utf8');
84
+ if (!content.includes('Agentic Senior Core') && !content.includes('pre-commit-runner.cjs')) {
85
+ continue;
86
+ }
87
+
88
+ found++;
89
+
90
+ // Check if file contains ONLY ASC lines or if it has extra user commands
91
+ const lines = content.split('\n');
92
+ const nonAscLines = lines.filter(l => {
93
+ const trimmed = l.trim();
94
+ return trimmed && !trimmed.startsWith('#!') && !trimmed.includes('Agentic Senior Core') && !trimmed.includes('pre-commit-runner.cjs') && !trimmed.includes('pre-commit-validator.cjs');
95
+ });
96
+
97
+ if (nonAscLines.length === 0) {
98
+ // Safe to remove completely
99
+ if (dryRun) {
100
+ console.log(` would remove: ${hookItem.path} (${hookItem.label})`);
101
+ } else {
102
+ await fs.rm(fullPath, { force: true });
103
+ console.log(` removed: ${hookItem.path} (${hookItem.label})`);
104
+ removed++;
105
+ }
106
+ } else {
107
+ // File has user modifications / additional commands — preserve user commands, remove ASC block
108
+ if (dryRun) {
109
+ console.log(` would clean ASC block from: ${hookItem.path} (${hookItem.label})`);
110
+ } else {
111
+ const cleanedLines = lines.filter(l => !l.includes('Agentic Senior Core') && !l.includes('pre-commit-runner.cjs') && !l.includes('pre-commit-validator.cjs'));
112
+ await fs.writeFile(fullPath, cleanedLines.join('\n').trimEnd() + '\n', 'utf8');
113
+ console.log(` cleaned ASC lines from: ${hookItem.path} (${hookItem.label})`);
114
+ removed++;
115
+ }
116
+ }
117
+ } catch (err) {
118
+ console.log(` warn: could not process ${hookItem.path}: ${err.message}`);
119
+ }
120
+ }
121
+
72
122
  if (found === 0) {
73
123
  console.log('No ASC adapter files found in this project.');
74
124
  return;
@@ -0,0 +1,178 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+
5
+ export const DEFAULT_RULE_CAP = 25;
6
+
7
+ /**
8
+ * Resolves the global user configuration directory path.
9
+ * @returns {string} Absolute path to global config directory.
10
+ */
11
+ export function getUserConfigDir() {
12
+ return path.join(os.homedir(), '.gemini', 'config');
13
+ }
14
+
15
+ /**
16
+ * Resolves the project-local configuration directory path.
17
+ * @param {string} cwd Working directory of the project.
18
+ * @returns {string} Absolute path to project config directory.
19
+ */
20
+ export function getProjectConfigDir(cwd = process.cwd()) {
21
+ return path.join(cwd, '.agents');
22
+ }
23
+
24
+ /**
25
+ * Resolves the JSON preferences file path for a given scope.
26
+ * @param {Object} options
27
+ * @param {string} [options.cwd] Project working directory.
28
+ * @param {'user'|'project'} options.scope Preference scope.
29
+ * @returns {string} Absolute path to preferences JSON file.
30
+ */
31
+ export function getPreferencesFilePath({ cwd = process.cwd(), scope = 'project' } = {}) {
32
+ const baseDir = scope === 'user' ? getUserConfigDir() : getProjectConfigDir(cwd);
33
+ return path.join(baseDir, 'adaptive_preferences.json');
34
+ }
35
+
36
+ /**
37
+ * Initializes an empty preferences structure.
38
+ * @returns {Object} Fresh preferences data object.
39
+ */
40
+ export function createEmptyPreferences() {
41
+ return {
42
+ version: '1.0.0',
43
+ updatedAt: new Date().toISOString(),
44
+ rules: []
45
+ };
46
+ }
47
+
48
+ /**
49
+ * Loads preference rules from storage for a specific scope.
50
+ * @param {Object} options
51
+ * @param {string} [options.cwd] Project working directory.
52
+ * @param {'user'|'project'} [options.scope] Target scope.
53
+ * @returns {Object} Preferences data object.
54
+ */
55
+ export function loadPreferences({ cwd = process.cwd(), scope = 'project' } = {}) {
56
+ const filePath = getPreferencesFilePath({ cwd, scope });
57
+ if (!fs.existsSync(filePath)) {
58
+ return createEmptyPreferences();
59
+ }
60
+
61
+ try {
62
+ const raw = fs.readFileSync(filePath, 'utf8');
63
+ const parsed = JSON.parse(raw);
64
+ if (!parsed || !Array.isArray(parsed.rules)) {
65
+ return createEmptyPreferences();
66
+ }
67
+ return parsed;
68
+ } catch (err) {
69
+ return createEmptyPreferences();
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Saves preference rules to storage for a specific scope.
75
+ * @param {Object} options
76
+ * @param {string} [options.cwd] Project working directory.
77
+ * @param {'user'|'project'} [options.scope] Target scope.
78
+ * @param {Object} options.preferences Preferences payload to write.
79
+ */
80
+ export function savePreferences({ cwd = process.cwd(), scope = 'project', preferences } = {}) {
81
+ const filePath = getPreferencesFilePath({ cwd, scope });
82
+ const dirPath = path.dirname(filePath);
83
+
84
+ if (!fs.existsSync(dirPath)) {
85
+ fs.mkdirSync(dirPath, { recursive: true });
86
+ }
87
+
88
+ const payload = {
89
+ ...createEmptyPreferences(),
90
+ ...preferences,
91
+ updatedAt: new Date().toISOString()
92
+ };
93
+
94
+ fs.writeFileSync(filePath, JSON.stringify(payload, null, 2), 'utf8');
95
+ return payload;
96
+ }
97
+
98
+ /**
99
+ * Adds or updates a rule within the specified scope, enforcing deduplication and max rule cap.
100
+ * @param {Object} options
101
+ * @param {string} [options.cwd] Project working directory.
102
+ * @param {'user'|'project'} [options.scope] Target scope.
103
+ * @param {Object} options.rule Rule definition object.
104
+ * @param {number} [options.maxCap] Maximum allowed rules per scope.
105
+ * @returns {{ preferences: Object, added: boolean, capExceeded: boolean }} Result summary.
106
+ */
107
+ export function addRule({ cwd = process.cwd(), scope = 'project', rule, maxCap = DEFAULT_RULE_CAP } = {}) {
108
+ if (!rule || !rule.pattern) {
109
+ throw new Error('Rule pattern is required');
110
+ }
111
+
112
+ const prefs = loadPreferences({ cwd, scope });
113
+ const normalizedPattern = String(rule.pattern).trim().toLowerCase();
114
+
115
+ const existingIndex = prefs.rules.findIndex(
116
+ r => String(r.pattern).trim().toLowerCase() === normalizedPattern
117
+ );
118
+
119
+ const formattedRule = {
120
+ id: rule.id || `rule_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`,
121
+ type: rule.type === 'syntactic' ? 'syntactic' : 'taste',
122
+ pattern: String(rule.pattern).trim(),
123
+ reason: String(rule.reason || '').trim(),
124
+ source: rule.source || 'learn',
125
+ createdAt: rule.createdAt || new Date().toISOString()
126
+ };
127
+
128
+ let added = false;
129
+ if (existingIndex >= 0) {
130
+ // Update existing rule
131
+ prefs.rules[existingIndex] = {
132
+ ...prefs.rules[existingIndex],
133
+ ...formattedRule
134
+ };
135
+ } else {
136
+ // Append new rule
137
+ prefs.rules.push(formattedRule);
138
+ added = true;
139
+ }
140
+
141
+ let capExceeded = false;
142
+ if (prefs.rules.length > maxCap) {
143
+ capExceeded = true;
144
+ // Trim oldest rules exceeding cap
145
+ prefs.rules = prefs.rules.slice(prefs.rules.length - maxCap);
146
+ }
147
+
148
+ const saved = savePreferences({ cwd, scope, preferences: prefs });
149
+ return { preferences: saved, added, capExceeded };
150
+ }
151
+
152
+ /**
153
+ * Resolves active combined rules from both user and project scopes.
154
+ * User scope rules come first, project scope rules override or extend.
155
+ * @param {Object} options
156
+ * @param {string} [options.cwd] Project working directory.
157
+ * @returns {{ syntactic: Array<Object>, taste: Array<Object> }} Active rules grouped by track.
158
+ */
159
+ export function resolveActiveRules({ cwd = process.cwd() } = {}) {
160
+ const userPrefs = loadPreferences({ cwd, scope: 'user' });
161
+ const projectPrefs = loadPreferences({ cwd, scope: 'project' });
162
+
163
+ const combinedRules = [...userPrefs.rules, ...projectPrefs.rules];
164
+
165
+ // Deduplicate combined by pattern (project scope overrides user scope if identical)
166
+ const ruleMap = new Map();
167
+ for (const r of combinedRules) {
168
+ const key = String(r.pattern).trim().toLowerCase();
169
+ ruleMap.set(key, r);
170
+ }
171
+
172
+ const allActive = Array.from(ruleMap.values());
173
+
174
+ return {
175
+ syntactic: allActive.filter(r => r.type === 'syntactic'),
176
+ taste: allActive.filter(r => r.type === 'taste')
177
+ };
178
+ }
@@ -0,0 +1,64 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { addRule } from './adaptive-preferences.mjs';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+
9
+ /**
10
+ * Loads the curated catalog of known UI/code slop patterns.
11
+ * @returns {Array<{ id: string, regex: string, message: string }>} Catalog patterns.
12
+ */
13
+ export function getSlopCatalog() {
14
+ const catalogPath = path.resolve(
15
+ __dirname,
16
+ '../../.agents/plugins/agentic-senior-core/hooks/lib/known-ui-slop-patterns.json'
17
+ );
18
+
19
+ if (!fs.existsSync(catalogPath)) {
20
+ return [];
21
+ }
22
+
23
+ try {
24
+ const content = fs.readFileSync(catalogPath, 'utf8');
25
+ const parsed = JSON.parse(content);
26
+ return parsed.patterns || [];
27
+ } catch (err) {
28
+ return [];
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Seeds selected slop pattern IDs into the target preference scope (Khroma Elicitation pattern).
34
+ * @param {Object} options
35
+ * @param {string} [options.cwd] Working directory.
36
+ * @param {'user'|'project'} [options.scope] Target scope.
37
+ * @param {Array<string>} options.selectedIds List of pattern IDs selected by the user.
38
+ * @returns {Array<Object>} List of added rule results.
39
+ */
40
+ export function bootstrapPreferences({ cwd = process.cwd(), scope = 'project', selectedIds = [] } = {}) {
41
+ const catalog = getSlopCatalog();
42
+ const results = [];
43
+
44
+ for (const id of selectedIds) {
45
+ const matched = catalog.find(item => item.id === id);
46
+ if (!matched) continue;
47
+
48
+ const res = addRule({
49
+ cwd,
50
+ scope,
51
+ rule: {
52
+ id: `bootstrap_${matched.id}`,
53
+ type: 'syntactic',
54
+ pattern: matched.regex,
55
+ reason: matched.message,
56
+ source: 'bootstrap'
57
+ }
58
+ });
59
+
60
+ results.push(res);
61
+ }
62
+
63
+ return results;
64
+ }
@@ -0,0 +1,35 @@
1
+ import { execSync } from 'child_process';
2
+
3
+ /**
4
+ * Detects recent git file rollbacks or reverts in the workspace to trigger signal denoising.
5
+ * @param {Object} options
6
+ * @param {string} [options.cwd] Working directory.
7
+ * @returns {{ hasReverts: boolean, revertedFiles: Array<string>, prompt: string }} Denoising status payload.
8
+ */
9
+ export function detectRevertedFiles({ cwd = process.cwd() } = {}) {
10
+ try {
11
+ // Check unstaged or staged diffs showing file deletions/rollbacks
12
+ const statusOutput = execSync('git status --porcelain', { cwd, encoding: 'utf8' });
13
+ const lines = statusOutput.split('\n').map(l => l.trim()).filter(Boolean);
14
+
15
+ // Filter modified or deleted files
16
+ const modifiedOrDeleted = lines
17
+ .filter(line => line.startsWith(' M') || line.startsWith(' D') || line.startsWith('D '))
18
+ .map(line => line.substring(3).trim());
19
+
20
+ if (modifiedOrDeleted.length === 0) {
21
+ return { hasReverts: false, revertedFiles: [], prompt: '' };
22
+ }
23
+
24
+ const firstFile = modifiedOrDeleted[0];
25
+ const prompt = `[ASC Signal Denoising] Revert/edit detected on ${firstFile}. Was this caused by a specific UI/code slop pattern? (Run /asc-learn to log or press Enter to skip)`;
26
+
27
+ return {
28
+ hasReverts: true,
29
+ revertedFiles: modifiedOrDeleted,
30
+ prompt
31
+ };
32
+ } catch (err) {
33
+ return { hasReverts: false, revertedFiles: [], prompt: '' };
34
+ }
35
+ }
@@ -0,0 +1,105 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { resolveActiveRules } from './adaptive-preferences.mjs';
4
+
5
+ /**
6
+ * Compiles active syntactic preference rules into a standalone Node.js validator script string.
7
+ * @param {Object} options
8
+ * @param {string} [options.cwd] Working directory.
9
+ * @returns {string} Executable Node.js script code.
10
+ */
11
+ export function generateValidatorScript({ cwd = process.cwd() } = {}) {
12
+ const { syntactic } = resolveActiveRules({ cwd });
13
+
14
+ const rulesJson = JSON.stringify(syntactic, null, 2);
15
+
16
+ return `#!/usr/bin/env node
17
+ // # Agentic Senior Core -- Compiled Pre-Commit Validator
18
+ // Auto-generated by Agentic Senior Core (ASC) Adaptive Preference Compiler
19
+ // DO NOT EDIT DIRECTLY. Compiled from Track A Syntactic Rules.
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+ const { execSync } = require('child_process');
24
+
25
+ const ACTIVE_SYNTACTIC_RULES = ${rulesJson};
26
+
27
+ function getStagedFiles() {
28
+ try {
29
+ const output = execSync('git diff --cached --name-only --diff-filter=ACM', { encoding: 'utf8' });
30
+ return output.split('\\n').map(f => f.trim()).filter(Boolean);
31
+ } catch (err) {
32
+ return [];
33
+ }
34
+ }
35
+
36
+ function runValidation() {
37
+ if (ACTIVE_SYNTACTIC_RULES.length === 0) {
38
+ process.exit(0);
39
+ }
40
+
41
+ const stagedFiles = getStagedFiles();
42
+ if (stagedFiles.length === 0) {
43
+ process.exit(0);
44
+ }
45
+
46
+ let violationFound = false;
47
+
48
+ for (const filePath of stagedFiles) {
49
+ if (!fs.existsSync(filePath)) continue;
50
+ // Skip binary / non-text files
51
+ const ext = path.extname(filePath).toLowerCase();
52
+ if (['.png', '.jpg', '.jpeg', '.gif', '.ico', '.pdf', '.zip'].includes(ext)) continue;
53
+
54
+ let content;
55
+ try {
56
+ content = fs.readFileSync(filePath, 'utf8');
57
+ } catch (e) {
58
+ continue;
59
+ }
60
+
61
+ for (const rule of ACTIVE_SYNTACTIC_RULES) {
62
+ if (!rule.pattern) continue;
63
+ try {
64
+ const regex = new RegExp(rule.pattern, 'i');
65
+ if (regex.test(content)) {
66
+ console.error(\`\\x1b[31m[ASC Rule Violation]\\x1b[0m File \${filePath} violates active syntactic rule:\`);
67
+ console.error(\` Pattern: \${rule.pattern}\`);
68
+ console.error(\` Reason: \${rule.reason || 'Banned pattern'}\`);
69
+ violationFound = true;
70
+ }
71
+ } catch (err) {
72
+ // Invalid regex ignore
73
+ }
74
+ }
75
+ }
76
+
77
+ if (violationFound) {
78
+ console.error('\\x1b[31m[ASC Gate Abort]\\x1b[0m Commit blocked by ASC compiled syntactic preference check.');
79
+ process.exit(1);
80
+ }
81
+
82
+ process.exit(0);
83
+ }
84
+
85
+ runValidation();
86
+ `;
87
+ }
88
+
89
+ /**
90
+ * Writes the compiled validator script to disk.
91
+ * @param {Object} options
92
+ * @param {string} [options.cwd] Working directory.
93
+ * @returns {string} Absolute path to compiled script.
94
+ */
95
+ export function compileAndSaveValidator({ cwd = process.cwd() } = {}) {
96
+ const ascDir = path.join(cwd, '.asc', 'hooks');
97
+ if (!fs.existsSync(ascDir)) {
98
+ fs.mkdirSync(ascDir, { recursive: true });
99
+ }
100
+
101
+ const scriptContent = generateValidatorScript({ cwd });
102
+ const scriptPath = path.join(ascDir, 'pre-commit-validator.cjs');
103
+ fs.writeFileSync(scriptPath, scriptContent, { encoding: 'utf8', mode: 0o755 });
104
+ return scriptPath;
105
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ryuenn3123/agentic-senior-core",
3
- "version": "6.3.0",
3
+ "version": "6.4.0",
4
4
  "type": "module",
5
5
  "description": "Agentic Senior Core: Universal AI coding rules and workflows. Write code like a staff engineer, not a junior.",
6
6
  "bin": {
@@ -11,13 +11,7 @@
11
11
  "files": [
12
12
  ".asc/",
13
13
  "bin/",
14
- "lib/cli/commands/adapter.mjs",
15
- "lib/cli/commands/global.mjs",
16
- "lib/cli/commands/clean.mjs",
17
- "lib/cli/commands/uninstall.mjs",
18
- "lib/cli/commands/status.mjs",
19
- "lib/cli/commands/mcp.mjs",
20
- "lib/cli/ascx/",
14
+ "lib/",
21
15
  ".agents/",
22
16
  "CONVENTIONS.md",
23
17
  "gemini-extension.json",
package/plugin.yaml CHANGED
@@ -1,5 +1,5 @@
1
1
  name: agentic-senior-core
2
- version: 6.3.0
2
+ version: 6.4.0
3
3
  description: Universal AI coding rules. Write code like a staff engineer.
4
4
  author: fatidaprilian
5
5
  provides_hooks: