@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.
- package/.agents/plugins/agentic-senior-core/commands/asc-dedup.md +1 -0
- package/.agents/plugins/agentic-senior-core/commands/asc-dedup.toml +2 -0
- package/.agents/plugins/agentic-senior-core/hooks/constants.cjs +21 -0
- package/.agents/plugins/agentic-senior-core/hooks/dedup-gate.js +283 -0
- package/.agents/plugins/agentic-senior-core/hooks/ladder-pulse.js +41 -0
- package/.agents/plugins/agentic-senior-core/hooks/lib/known-security-patterns.json +26 -0
- package/.agents/plugins/agentic-senior-core/hooks/post-edit-enforce.js +82 -13
- package/.agents/plugins/agentic-senior-core/hooks/pre-tool-dependency-gate.js +5 -5
- package/.agents/plugins/agentic-senior-core/hooks/session-pulse.js +44 -0
- package/.agents/plugins/agentic-senior-core/hooks.json +21 -4
- package/.agents/plugins/agentic-senior-core/plugin.json +2 -2
- package/.agents/plugins/agentic-senior-core/rules/agentic-senior-core.md +4 -0
- package/.agents/plugins/agentic-senior-core/skills/asc-add-feature/SKILL.md +6 -5
- package/.agents/plugins/agentic-senior-core/skills/asc-dedup/SKILL.md +52 -0
- package/.agents/plugins/agentic-senior-core/skills/asc-new-project/SKILL.md +1 -0
- package/.asc/dedup-config.json +6 -0
- package/AGENTS.md +3 -0
- package/README.md +21 -449
- package/gemini-extension.json +1 -1
- package/package.json +2 -1
- package/plugin.yaml +2 -1
|
@@ -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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
38
|
-
|
|
39
|
-
|
|
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',
|
|
47
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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
|
|
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',
|
|
27
|
-
|
|
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
|
-
|
|
121
|
-
|
|
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": "
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"timeout":
|
|
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": "
|
|
4
|
-
"description": "Universal AI coding rules.
|
|
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.
|