@ryuenn3123/agentic-senior-core 5.8.26 → 6.0.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.
@@ -0,0 +1 @@
1
+ Run the duplicate code audit from skills/asc-dedup/SKILL.md. Scan the specified scope (directory, package, or full repo) for near-duplicate code blocks using jscpd token-level clone detection. Report clusters ranked by duplicated lines, and suggest consolidation only for patterns appearing 3+ times (Rule of Three).
@@ -0,0 +1,2 @@
1
+ description = "On-demand duplicate code audit using token-level clone detection"
2
+ prompt = "Load and follow the asc-dedup skill. Scan the current project (or specified scope) for near-duplicate code blocks using jscpd. Report clusters ranked by duplicated lines. Suggest consolidation only for patterns appearing 3+ times per the Rule of Three."
@@ -0,0 +1,21 @@
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.
4
+
5
+ const SOURCE_EXTENSIONS = new Set([
6
+ 'js', 'ts', 'mjs', 'cjs', 'jsx', 'tsx',
7
+ 'py', 'rb', 'go', 'rs', 'java', 'kt', 'swift', 'cs',
8
+ ]);
9
+
10
+ const LOC_DELTA_THRESHOLD = 30;
11
+ const NEW_FILE_LINE_THRESHOLD = 50;
12
+ const SESSION_DRIFT_THRESHOLD = 4;
13
+ const LADDER_PULSE_INTERVAL = 3;
14
+
15
+ module.exports = {
16
+ SOURCE_EXTENSIONS,
17
+ LOC_DELTA_THRESHOLD,
18
+ NEW_FILE_LINE_THRESHOLD,
19
+ SESSION_DRIFT_THRESHOLD,
20
+ LADDER_PULSE_INTERVAL,
21
+ };
@@ -0,0 +1,283 @@
1
+ #!/usr/bin/env node
2
+ // Agentic Senior Core — PostToolUse duplicate-code detection hook
3
+ // Fires after Edit/Write on qualifying source files. Runs a scoped jscpd scan
4
+ // to detect near-duplicate code blocks. Advisory by default; configurable to block.
5
+ // Supports Claude Code, Codex CLI, GitHub Copilot CLI, and Antigravity IDE.
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const os = require('os');
10
+ const { execSync } = require('child_process');
11
+
12
+ const {
13
+ SOURCE_EXTENSIONS,
14
+ LOC_DELTA_THRESHOLD,
15
+ NEW_FILE_LINE_THRESHOLD,
16
+ } = require('./constants.cjs');
17
+
18
+ const JSCPD_TIMEOUT_MS = 10000;
19
+
20
+ // Recognized source root directories — scan scope walks up to the first match
21
+ const SOURCE_ROOTS = ['src', 'lib', 'app'];
22
+
23
+ let inputBuffer = '';
24
+ process.stdin.setEncoding('utf8');
25
+ process.stdin.on('data', chunk => {
26
+ inputBuffer += chunk;
27
+ try {
28
+ const data = JSON.parse(inputBuffer);
29
+
30
+ let toolName = '';
31
+ let toolInput = {};
32
+ let isAntigravity = false;
33
+
34
+ if (data.toolCall) {
35
+ // Antigravity PreToolUse shape (has toolCall directly)
36
+ isAntigravity = true;
37
+ toolName = data.toolCall.name;
38
+ toolInput = data.toolCall.args || {};
39
+ } else if (data.stepIdx !== undefined && data.transcriptPath) {
40
+ // Antigravity PostToolUse shape — no toolCall in payload,
41
+ // read the tool call details from the transcript at stepIdx
42
+ isAntigravity = true;
43
+ var extracted = extractToolCallFromTranscript(data.transcriptPath, data.stepIdx);
44
+ if (!extracted) { process.exit(0); return; }
45
+ toolName = extracted.toolName;
46
+ toolInput = extracted.toolInput;
47
+ } else {
48
+ // Claude Code / Codex / Copilot path
49
+ toolName = data.tool_name || data.toolName || '';
50
+ toolInput = data.tool_input || data.toolInput || {};
51
+ }
52
+
53
+ const filePath = toolInput.file_path || toolInput.TargetFile || toolInput.path || toolInput.target_file || '';
54
+ if (!filePath) { process.exit(0); return; }
55
+
56
+ const ext = path.extname(filePath).slice(1);
57
+ if (!SOURCE_EXTENSIONS.has(ext)) { process.exit(0); return; }
58
+
59
+ if (!isQualifyingEdit(toolName, toolInput)) { process.exit(0); return; }
60
+
61
+ const config = loadDedupConfig();
62
+ const scanDir = resolveScanDir(filePath, config);
63
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'asc-dedup-'));
64
+
65
+ const ignoreFlags = (config.ignoreDirs || []).map(function (d) { return '--ignore "' + d + '"'; }).join(' ');
66
+ const minTokens = config.minTokens || 30;
67
+ const scanCmd = ' "' + scanDir + '" --min-tokens ' + minTokens
68
+ + ' --reporters json --silent --output "' + tmpDir + '" ' + ignoreFlags;
69
+
70
+ const report = runJscpdScan(scanCmd, tmpDir);
71
+ if (!report) { cleanup(tmpDir); process.exit(0); return; }
72
+
73
+ const finding = checkForDuplicates(report, filePath);
74
+ if (!finding) { cleanup(tmpDir); process.exit(0); return; }
75
+
76
+ const nudge = '[ASC Dedup] ' + path.basename(filePath) + ' looks similar to '
77
+ + finding.matchedFile + ' (' + finding.percent + '% overlap, '
78
+ + finding.lines + ' lines). Ladder step 2: does the codebase already have this? '
79
+ + 'Reuse it, or confirm this is intentional.';
80
+
81
+ if (config.mode === 'block') {
82
+ emitDeny(nudge, isAntigravity);
83
+ } else {
84
+ emitAdvisory(nudge, isAntigravity);
85
+ }
86
+
87
+ cleanup(tmpDir);
88
+ process.exit(0);
89
+ } catch (e) {
90
+ // incomplete json, wait for more chunks
91
+ }
92
+ });
93
+
94
+ // Extract tool call details from the Antigravity transcript at a given step index.
95
+ // PostToolUse on Antigravity only provides stepIdx — the actual tool call args
96
+ // must be recovered from the transcript log. Same approach as post-edit-enforce.js.
97
+ function extractToolCallFromTranscript(transcriptPath, stepIdx) {
98
+ try {
99
+ if (!fs.existsSync(transcriptPath)) return null;
100
+ var lines = fs.readFileSync(transcriptPath, 'utf8').split('\n').filter(Boolean);
101
+
102
+ // Find the step with matching step_index that contains tool_calls
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
+ };
126
+ }
127
+ }
128
+ }
129
+ return null;
130
+ } catch (_) {
131
+ return null;
132
+ }
133
+ }
134
+
135
+ function isQualifyingEdit(toolName, toolInput) {
136
+ var isWrite = ['Write', 'write_to_file', 'write_file'].indexOf(toolName) !== -1;
137
+ var isEdit = ['Edit', 'replace_file_content', 'multi_replace_file_content'].indexOf(toolName) !== -1;
138
+
139
+ if (isWrite) {
140
+ var content = toolInput.content || toolInput.CodeContent || '';
141
+ return content.split('\n').length > NEW_FILE_LINE_THRESHOLD;
142
+ }
143
+ if (isEdit) {
144
+ var newStr = toolInput.new_string || toolInput.ReplacementContent || toolInput.content || '';
145
+ var oldStr = toolInput.old_string || toolInput.TargetContent || '';
146
+ var delta = newStr.split('\n').length - oldStr.split('\n').length;
147
+ return delta > LOC_DELTA_THRESHOLD;
148
+ }
149
+ return false;
150
+ }
151
+
152
+ function resolveScanDir(filePath, config) {
153
+ var cwd = process.cwd();
154
+
155
+ // User override takes priority
156
+ if (config.scanRoot) {
157
+ var override = path.resolve(cwd, config.scanRoot);
158
+ if (fs.existsSync(override)) return override;
159
+ }
160
+
161
+ // Walk up from the file's directory to find a recognized source root
162
+ var dir = path.dirname(path.resolve(filePath));
163
+ while (dir.length >= cwd.length) {
164
+ var basename = path.basename(dir);
165
+ if (SOURCE_ROOTS.indexOf(basename) !== -1) return dir;
166
+ var parent = path.dirname(dir);
167
+ if (parent === dir) break;
168
+ dir = parent;
169
+ }
170
+
171
+ // Fallback: project root
172
+ return cwd;
173
+ }
174
+
175
+ function loadDedupConfig() {
176
+ var candidates = [
177
+ path.join(process.cwd(), '.asc', 'dedup-config.json'),
178
+ path.join(process.cwd(), '.agents', 'dedup-config.json'),
179
+ ];
180
+ for (var i = 0; i < candidates.length; i++) {
181
+ try {
182
+ if (fs.existsSync(candidates[i])) {
183
+ return JSON.parse(fs.readFileSync(candidates[i], 'utf8'));
184
+ }
185
+ } catch (_) {}
186
+ }
187
+ return { mode: 'advisory', minTokens: 30, ignoreDirs: ['tests', 'migrations', 'generated', 'node_modules'] };
188
+ }
189
+
190
+ // minimal: attempt-then-fallback — try scan directly, fall back on failure.
191
+ // No separate --version probes. Upgrade if jscpd provides a stable JS API.
192
+ function runJscpdScan(scanCmd, tmpDir) {
193
+ var binaries = ['bunx jscpd', 'npx jscpd@5', 'npx jscpd'];
194
+ for (var i = 0; i < binaries.length; i++) {
195
+ try {
196
+ execSync(binaries[i] + scanCmd, {
197
+ timeout: JSCPD_TIMEOUT_MS,
198
+ stdio: 'pipe',
199
+ cwd: process.cwd(),
200
+ });
201
+ return loadReport(tmpDir);
202
+ } catch (_) {
203
+ // Try next binary
204
+ }
205
+ }
206
+ return null;
207
+ }
208
+
209
+ function loadReport(tmpDir) {
210
+ var reportPath = path.join(tmpDir, 'jscpd-report.json');
211
+ if (!fs.existsSync(reportPath)) return null;
212
+ try {
213
+ return JSON.parse(fs.readFileSync(reportPath, 'utf8'));
214
+ } catch (_) {
215
+ return null;
216
+ }
217
+ }
218
+
219
+ function checkForDuplicates(report, filePath) {
220
+ var duplicates = report.duplicates || [];
221
+ if (duplicates.length === 0) return null;
222
+
223
+ var normalizedTarget = path.resolve(filePath).replace(/\\/g, '/').toLowerCase();
224
+
225
+ for (var i = 0; i < duplicates.length; i++) {
226
+ var dup = duplicates[i];
227
+ var firstName = path.resolve(dup.firstFile.name).replace(/\\/g, '/').toLowerCase();
228
+ var secondName = path.resolve(dup.secondFile.name).replace(/\\/g, '/').toLowerCase();
229
+
230
+ if (firstName === normalizedTarget || secondName === normalizedTarget) {
231
+ var matchedFile = firstName === normalizedTarget
232
+ ? path.basename(dup.secondFile.name)
233
+ : path.basename(dup.firstFile.name);
234
+ var lines = dup.lines || 0;
235
+ // jscpd v5 reports fragments; estimate overlap percentage from line count
236
+ var totalLines = (report.statistics && report.statistics.total && report.statistics.total.lines) || 1;
237
+ var percent = Math.round((lines / totalLines) * 100);
238
+ return { matchedFile: matchedFile, lines: lines, percent: percent };
239
+ }
240
+ }
241
+ return null;
242
+ }
243
+
244
+ function emitAdvisory(nudge, isAntigravity) {
245
+ if (isAntigravity) {
246
+ process.stdout.write(JSON.stringify({
247
+ injectSteps: [{ ephemeralMessage: nudge }],
248
+ }) + '\n');
249
+ } else {
250
+ var isCopilot = Boolean(process.env.COPILOT_PLUGIN_DATA);
251
+ if (isCopilot) {
252
+ process.stdout.write(JSON.stringify({ additionalContext: nudge }) + '\n');
253
+ } else {
254
+ process.stdout.write(JSON.stringify({
255
+ hookSpecificOutput: {
256
+ hookEventName: 'PostToolUse',
257
+ additionalContext: nudge,
258
+ },
259
+ }) + '\n');
260
+ }
261
+ }
262
+ }
263
+
264
+ function emitDeny(reason, isAntigravity) {
265
+ if (isAntigravity) {
266
+ process.stdout.write(JSON.stringify({ decision: 'deny', reason: reason }) + '\n');
267
+ } else {
268
+ process.stdout.write(JSON.stringify({
269
+ allow_tool: false,
270
+ deny_reason: reason,
271
+ hookSpecificOutput: {
272
+ hookEventName: 'PostToolUse',
273
+ permissionDecision: 'deny',
274
+ permissionDecisionReason: reason,
275
+ },
276
+ }) + '\n');
277
+ }
278
+ process.exit(2);
279
+ }
280
+
281
+ function cleanup(tmpDir) {
282
+ try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) {}
283
+ }
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ // Agentic Senior Core — PreInvocation ladder persistence hook
3
+ // Counters context rot by re-injecting a short decision-ladder reminder
4
+ // periodically, late in context, rather than relying only on rules/ content
5
+ // that sits at the start of the conversation.
6
+ // Antigravity-only: PreInvocation fires before every model call with
7
+ // invocationNum in the input payload. For other hosts, the equivalent
8
+ // pulse is integrated into post-edit-enforce.js via the sourceEditCount counter.
9
+
10
+ const { LADDER_PULSE_INTERVAL } = require('./constants.cjs');
11
+
12
+ const LADDER_REMINDER = '[ASC] Ladder check: (1) needed? (2) exists already \u2014 reuse? '
13
+ + '(3) stdlib/native? (4) existing dep? (5) one function? Then minimal code.';
14
+
15
+ let inputBuffer = '';
16
+ process.stdin.setEncoding('utf8');
17
+ process.stdin.on('data', chunk => {
18
+ inputBuffer += chunk;
19
+ try {
20
+ const payload = JSON.parse(inputBuffer);
21
+
22
+ let shouldInject = false;
23
+ const invNum = payload.invocationNum || 0;
24
+ if (invNum > 0 && invNum % LADDER_PULSE_INTERVAL === 0) {
25
+ shouldInject = true;
26
+ }
27
+
28
+ if (shouldInject) {
29
+ process.stdout.write(JSON.stringify({
30
+ injectSteps: [{
31
+ ephemeralMessage: `**ASC LADDER PULSE**: You have completed several steps. Remember to review the 1-6 decision ladder. Document deferred debt if you take shortcuts.`
32
+ }]
33
+ }) + '\n');
34
+ } else {
35
+ process.stdout.write('{}\n');
36
+ }
37
+ process.exit(0);
38
+ } catch (e) {
39
+ // wait for more chunks
40
+ }
41
+ });
@@ -0,0 +1,26 @@
1
+ {
2
+ "description": "Regex patterns for recurring security anti-patterns",
3
+ "patterns": [
4
+ {
5
+ "id": "insecure-redirect",
6
+ "regex": "location\\.href\\s*=\\s*(?!['\"`])([^;\\n]+)",
7
+ "message": "Unvalidated redirect target assigned to location.href. Ensure the variable is sanitized or use a safe routing method."
8
+ },
9
+ {
10
+ "id": "timing-unsafe-compare",
11
+ "regex": "(password|secret|token|key)\\s*(===|!==|==|!=)",
12
+ "message": "Non-timing-safe string comparison on a secret variable. Use crypto.timingSafeEqual instead."
13
+ },
14
+ {
15
+ "id": "user-input-http",
16
+ "regex": "(axios|fetch|got|superagent)\\s*\\(\\s*.*?(req\\.(query|body|params)|process\\.env)",
17
+ "message": "Potentially unsafe input passed directly into an HTTP client. Validate and sanitize URL parameters first."
18
+ }
19
+ ],
20
+ "fileSpecific": {
21
+ "Dockerfile": {
22
+ "require": "^(?=.*\\nUSER\\s).*$",
23
+ "message": "Missing USER instruction in Dockerfile. The container will run as root by default."
24
+ }
25
+ }
26
+ }
@@ -29,22 +29,29 @@ try {
29
29
  }
30
30
  } catch (_) {}
31
31
 
32
- const SOURCE_EXTENSIONS = new Set([
33
- 'js', 'ts', 'mjs', 'cjs', 'jsx', 'tsx',
34
- 'py', 'rb', 'go', 'rs', 'java', 'kt', 'swift', 'cs',
35
- ]);
32
+ let SECURITY_PATTERNS = { patterns: [], fileSpecific: {} };
33
+ try {
34
+ const secPath = path.join(__dirname, 'lib', 'known-security-patterns.json');
35
+ if (fs.existsSync(secPath)) {
36
+ SECURITY_PATTERNS = JSON.parse(fs.readFileSync(secPath, 'utf8'));
37
+ }
38
+ } catch (_) {}
36
39
 
37
- const LOC_DELTA_THRESHOLD = 30;
38
- const NEW_FILE_LINE_THRESHOLD = 50;
39
- const SESSION_DRIFT_THRESHOLD = 4;
40
+ const {
41
+ SOURCE_EXTENSIONS,
42
+ LOC_DELTA_THRESHOLD,
43
+ NEW_FILE_LINE_THRESHOLD,
44
+ SESSION_DRIFT_THRESHOLD,
45
+ LADDER_PULSE_INTERVAL,
46
+ } = require('./constants.cjs');
40
47
 
41
48
  // Module-level counter for Claude Code path (resets per process spawn)
42
49
  let sourceEditCount = 0;
43
50
 
44
51
  let inputBuffer = '';
45
52
  process.stdin.setEncoding('utf8');
46
- process.stdin.on('data', function (chunk) { inputBuffer += chunk; });
47
- process.stdin.on('end', function () {
53
+ process.stdin.on('data', chunk => {
54
+ inputBuffer += chunk;
48
55
  try {
49
56
  const data = JSON.parse(inputBuffer);
50
57
 
@@ -58,8 +65,9 @@ process.stdin.on('end', function () {
58
65
  processSingleEdit(toolName, toolInput, function(nudge) {
59
66
  emitClaude(nudge);
60
67
  });
61
- } catch (_) {
62
- // Silent fail
68
+ process.exit(0);
69
+ } catch (e) {
70
+ // incomplete json, wait for more chunks
63
71
  }
64
72
  });
65
73
 
@@ -138,6 +146,9 @@ function processSingleEdit(toolName, toolInput, emitFn, skipArray) {
138
146
  findings.push('[ASC Session Drift] ' + SESSION_DRIFT_THRESHOLD + '+ source file edits this session. '
139
147
  + 'Re-read the decision ladder before continuing: (1) Does this need to be built? '
140
148
  + '(2) Does the codebase already have this? (3) Stdlib/native? (4) Existing dependency?');
149
+ } else if (sourceEditCount > 0 && sourceEditCount % LADDER_PULSE_INTERVAL === 0) {
150
+ findings.push('[ASC] Ladder check: (1) needed? (2) exists already \u2014 reuse? '
151
+ + '(3) stdlib/native? (4) existing dep? (5) one function? Then minimal code.');
141
152
  }
142
153
  }
143
154
 
@@ -145,7 +156,6 @@ function processSingleEdit(toolName, toolInput, emitFn, skipArray) {
145
156
  checkDependencyAddition(toolName, toolInput, findings);
146
157
  }
147
158
 
148
- const ext = path.extname(filePath).slice(1);
149
159
  if (SOURCE_EXTENSIONS.has(ext)) {
150
160
  if (toolName === 'Edit') {
151
161
  checkLocDelta(toolInput, filePath, findings);
@@ -154,6 +164,11 @@ function processSingleEdit(toolName, toolInput, emitFn, skipArray) {
154
164
  }
155
165
  }
156
166
 
167
+ checkSecurityPatterns(toolName, toolInput, filePath, findings);
168
+ if (ext === 'js' || ext === 'ts' || ext === 'jsx' || ext === 'tsx' || ext === 'mjs' || ext === 'cjs') {
169
+ checkLinter(filePath, findings);
170
+ }
171
+
157
172
  checkLivingDocNudge(filePath, findings);
158
173
 
159
174
  if (ext !== 'md') {
@@ -231,13 +246,67 @@ function checkLocDelta(toolInput, filePath, findings) {
231
246
  function checkNewFileSize(toolInput, filePath, findings) {
232
247
  var lines = (toolInput.content || '').split('\n').length;
233
248
  if (lines > NEW_FILE_LINE_THRESHOLD) {
249
+ // Step 1-2 coverage ("does this already exist?") moved to dedup-gate.js
250
+ // which provides concrete file-name + overlap-percentage feedback.
234
251
  findings.push(
235
252
  'New file ' + path.basename(filePath) + ' created with ' + lines + ' lines. '
236
- + 'Ladder step 1-2: does this need to be built? Does the codebase already have this?'
253
+ + 'Ladder step 5: can this be one straightforward function?'
237
254
  );
238
255
  }
239
256
  }
240
257
 
258
+ function checkSecurityPatterns(toolName, toolInput, filePath, findings) {
259
+ var target = toolName === 'Edit' ? (toolInput.new_string || '') : (toolInput.content || '');
260
+ if (!target) return;
261
+
262
+ if (SECURITY_PATTERNS.patterns) {
263
+ SECURITY_PATTERNS.patterns.forEach(function (p) {
264
+ try {
265
+ var regex = new RegExp(p.regex, 'ig');
266
+ if (regex.test(target)) {
267
+ findings.push('[ASC Security] ' + p.message);
268
+ }
269
+ } catch (_) {}
270
+ });
271
+ }
272
+
273
+ var basename = path.basename(filePath);
274
+ if (SECURITY_PATTERNS.fileSpecific && SECURITY_PATTERNS.fileSpecific[basename]) {
275
+ var spec = SECURITY_PATTERNS.fileSpecific[basename];
276
+ try {
277
+ var regex = new RegExp(spec.require, 'g');
278
+ if (target.trim().length > 0 && !regex.test(target)) {
279
+ findings.push('[ASC Security] ' + spec.message);
280
+ }
281
+ } catch (_) {}
282
+ }
283
+ }
284
+
285
+ function checkLinter(filePath, findings) {
286
+ try {
287
+ var cwd = process.cwd();
288
+ var hasEslint = fs.existsSync(path.join(cwd, '.eslintrc.json')) ||
289
+ fs.existsSync(path.join(cwd, '.eslintrc.js')) ||
290
+ fs.existsSync(path.join(cwd, 'eslint.config.js')) ||
291
+ (fs.existsSync(path.join(cwd, 'package.json')) && fs.readFileSync(path.join(cwd, 'package.json'), 'utf8').includes('eslintConfig'));
292
+
293
+ if (hasEslint) {
294
+ var execSync = require('child_process').execSync;
295
+ execSync('npx eslint "' + filePath + '" --format json', { cwd: cwd, stdio: 'pipe' });
296
+ }
297
+ } catch (error) {
298
+ if (error.stdout) {
299
+ try {
300
+ var out = JSON.parse(error.stdout.toString());
301
+ if (Array.isArray(out) && out.length > 0 && out[0].messages && out[0].messages.length > 0) {
302
+ var firstErr = out[0].messages[0];
303
+ findings.push('[ASC Linter] ' + firstErr.message + ' at line ' + firstErr.line + '.');
304
+ }
305
+ } catch (_) {}
306
+ }
307
+ }
308
+ }
309
+
241
310
  function checkLivingDocNudge(filePath, findings) {
242
311
  var lower = filePath.toLowerCase();
243
312
  if (lower.includes('schema') || lower.includes('migration') || lower.includes('model') || lower.includes('prisma')) {
@@ -23,8 +23,8 @@ try {
23
23
 
24
24
  let inputBuffer = '';
25
25
  process.stdin.setEncoding('utf8');
26
- process.stdin.on('data', function (chunk) { inputBuffer += chunk; });
27
- process.stdin.on('end', function () {
26
+ process.stdin.on('data', chunk => {
27
+ inputBuffer += chunk;
28
28
  try {
29
29
  const data = JSON.parse(inputBuffer);
30
30
  const isAntigravity = !!data.toolCall;
@@ -117,10 +117,10 @@ process.stdin.on('end', function () {
117
117
  process.exit(2);
118
118
  return;
119
119
  }
120
- } catch (_) {
121
- // Silent fail to ensure session stability
120
+ process.exit(0);
121
+ } catch (e) {
122
+ // incomplete json, wait for more chunks
122
123
  }
123
- process.exit(0);
124
124
  });
125
125
 
126
126
  function isGitCommitOrPush(command) {
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+ // Agentic Senior Core — PreInvocation session pulse hook (Antigravity-specific)
3
+ // Antigravity has no SessionStart event. This fires on the FIRST model call
4
+ // (invocationNum === 0) to inject AGENTS.md into the conversation — the same
5
+ // role session-start.js fills for Claude Code / Codex / Copilot via their
6
+ // real SessionStart hook.
7
+ // Does NOT replace session-start.js — that file still serves other hosts.
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT || path.resolve(__dirname, '..');
13
+
14
+ let inputBuffer = '';
15
+ process.stdin.setEncoding('utf8');
16
+ process.stdin.on('data', chunk => {
17
+ inputBuffer += chunk;
18
+ try {
19
+ const data = JSON.parse(inputBuffer);
20
+
21
+ // Only fire on the very first model call of the session
22
+ if (data.invocationNum !== 0) {
23
+ process.stdout.write(JSON.stringify({}) + '\n');
24
+ process.exit(0);
25
+ return;
26
+ }
27
+
28
+ const agentsPath = path.join(pluginRoot, 'rules', 'agentic-senior-core.md');
29
+ let content;
30
+ try {
31
+ content = fs.readFileSync(agentsPath, 'utf8');
32
+ } catch (_) {
33
+ // Fallback to AGENTS.md at plugin root
34
+ content = fs.readFileSync(path.join(pluginRoot, 'AGENTS.md'), 'utf8');
35
+ }
36
+
37
+ process.stdout.write(JSON.stringify({
38
+ injectSteps: [{ ephemeralMessage: content }],
39
+ }) + '\n');
40
+ process.exit(0);
41
+ } catch (e) {
42
+ // wait for more chunks
43
+ }
44
+ });
@@ -74,14 +74,31 @@
74
74
  "statusMessage": "ASC ladder & spec gate check..."
75
75
  },
76
76
  {
77
- "type": "prompt",
78
- "if": "Edit(**/package.json)",
79
- "prompt": "A new dependency '$ARGUMENTS' was added to package.json. Does it duplicate Node.js standard library or platform features? If yes, return JSON: {\"hookSpecificOutput\": {\"hookEventName\": \"PostToolUse\", \"additionalContext\": \"[ASC Advisory] The newly added dependency appears to duplicate stdlib or platform features. Consider native alternatives.\"}}. If no, return {}.",
80
- "timeout": 30
77
+ "type": "command",
78
+ "command": "node -e \"const p=require('path'),fs=require('fs'),os=require('os');const local=p.join(process.cwd(),'.agents','plugins','agentic-senior-core','hooks','dedup-gate.js');const global=p.join(os.homedir(),'.gemini','config','plugins','agentic-senior-core','hooks','dedup-gate.js');require(fs.existsSync(local)?local:global);\"",
79
+ "commandWindows": "if (Get-Command node -ErrorAction SilentlyContinue) { $root = if ($env:CLAUDE_PLUGIN_ROOT) { $env:CLAUDE_PLUGIN_ROOT } else { \"$env:USERPROFILE\\.gemini\\config\\plugins\\agentic-senior-core\" }; node \"$root\\hooks\\dedup-gate.js\" }",
80
+ "timeout": 15,
81
+ "statusMessage": "ASC duplicate-code scan..."
81
82
  }
82
83
  ]
83
84
  }
84
85
  ],
86
+ "PreInvocation": [
87
+ {
88
+ "type": "command",
89
+ "command": "node -e \"const p=require('path'),fs=require('fs'),os=require('os');const local=p.join(process.cwd(),'.agents','plugins','agentic-senior-core','hooks','session-pulse.js');const global=p.join(os.homedir(),'.gemini','config','plugins','agentic-senior-core','hooks','session-pulse.js');require(fs.existsSync(local)?local:global);\"",
90
+ "commandWindows": "if (Get-Command node -ErrorAction SilentlyContinue) { $root = if ($env:CLAUDE_PLUGIN_ROOT) { $env:CLAUDE_PLUGIN_ROOT } else { \"$env:USERPROFILE\\.gemini\\config\\plugins\\agentic-senior-core\" }; node \"$root\\hooks\\session-pulse.js\" }",
91
+ "timeout": 5,
92
+ "statusMessage": "Loading ASC rules..."
93
+ },
94
+ {
95
+ "type": "command",
96
+ "command": "node -e \"const p=require('path'),fs=require('fs'),os=require('os');const local=p.join(process.cwd(),'.agents','plugins','agentic-senior-core','hooks','ladder-pulse.js');const global=p.join(os.homedir(),'.gemini','config','plugins','agentic-senior-core','hooks','ladder-pulse.js');require(fs.existsSync(local)?local:global);\"",
97
+ "commandWindows": "if (Get-Command node -ErrorAction SilentlyContinue) { $root = if ($env:CLAUDE_PLUGIN_ROOT) { $env:CLAUDE_PLUGIN_ROOT } else { \"$env:USERPROFILE\\.gemini\\config\\plugins\\agentic-senior-core\" }; node \"$root\\hooks\\ladder-pulse.js\" }",
98
+ "timeout": 5,
99
+ "statusMessage": "ASC ladder pulse..."
100
+ }
101
+ ],
85
102
  "PostInvocation": [
86
103
  {
87
104
  "type": "command",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agentic-senior-core",
3
- "version": "5.8.26",
4
- "description": "Universal AI coding rules. Write code like a staff engineer.",
3
+ "version": "6.0.0",
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": [
7
7
  "rules/"
@@ -80,6 +80,10 @@ whether to invoke it. Skip this for trivial edits.
80
80
  - Non-trivial feature in an existing codebase → `/asc-add-feature` (research/plan gate before implementation)
81
81
  - Refactor spanning multiple files or changing architecture → `/asc-refactor` (classifies scope, gates on high-level changes)
82
82
 
83
+ ### Enforcement Fallbacks (For hosts without hook support)
84
+ - **Duplicate-Code Check**: When creating new functions or components, actively check for existing near-duplicates across directories (not just siblings) before implementing. If a similar pattern exists, reuse it. Apply the Rule of Three: consolidate only if a pattern appears 3+ times.
85
+ - **Ladder Persistence**: Before completing a task, explicitly verify you have selected the lowest feasible step on the 1-6 decision ladder. Document deferred technical debt (via `/asc-debt` or inline comment) if a shortcut is taken.
86
+
83
87
  ## Response Style
84
88
 
85
89
  Lead with what the developer needs to act: the command, file path, code change, or decision point. Follow with context only when the action depends on it.