@aarushpandey/gitagent 1.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/CONTRIBUTING.md +104 -0
- package/LICENSE +21 -0
- package/README.md +570 -0
- package/TESTING.md +290 -0
- package/action.yml +113 -0
- package/examples/README.md +124 -0
- package/examples/sample-audit-trail-issue-4.md +112 -0
- package/examples/sample-review-tqec-pr894-v1-raw-flawed.md +71 -0
- package/examples/sample-review-tqec-pr894-v2-raw.md +48 -0
- package/examples/sample-review-tqec-pr894-v3-curated.md +118 -0
- package/examples/verify-marker-precedence/README.md +97 -0
- package/examples/verify-marker-precedence/conftest.py +15 -0
- package/examples/verify-marker-precedence/pyproject.toml +8 -0
- package/examples/verify-marker-precedence/test_marker_precedence.py +56 -0
- package/examples/verify-marker-precedence/verify_precedence.py +67 -0
- package/examples/workflows/issue-fix.yml +32 -0
- package/examples/workflows/pr-review.yml +34 -0
- package/package.json +75 -0
- package/scripts/verify.js +478 -0
- package/src/agents/agentLoop.js +176 -0
- package/src/agents/engineeringAgent.js +51 -0
- package/src/agents/reviewCopilot.js +79 -0
- package/src/agents/tools.js +486 -0
- package/src/cli/output.js +137 -0
- package/src/config.js +22 -0
- package/src/mapper/fileRelevance.js +113 -0
- package/src/mapper/repoMap.js +105 -0
- package/src/orchestrator.js +336 -0
- package/src/pipeline.js +985 -0
- package/src/prompts/engineering.js +189 -0
- package/src/prompts/review.js +149 -0
- package/src/utils/cost.js +47 -0
- package/src/utils/diffLines.js +67 -0
- package/src/utils/githubUrl.js +8 -0
- package/src/web/public/index.html +128 -0
- package/src/web/server.js +51 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
const { computeCost } = require('../utils/cost');
|
|
2
|
+
|
|
3
|
+
const c = {
|
|
4
|
+
reset: '\x1b[0m',
|
|
5
|
+
dim: '\x1b[2m',
|
|
6
|
+
bold: '\x1b[1m',
|
|
7
|
+
cyan: '\x1b[36m',
|
|
8
|
+
green: '\x1b[32m',
|
|
9
|
+
yellow: '\x1b[33m',
|
|
10
|
+
red: '\x1b[31m',
|
|
11
|
+
magenta: '\x1b[35m',
|
|
12
|
+
blue: '\x1b[34m'
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function banner() {
|
|
16
|
+
return `${c.cyan}${c.bold}
|
|
17
|
+
╔════════════════════════════════════════════╗
|
|
18
|
+
║ github-agent — autonomous PR engineer ║
|
|
19
|
+
║ engineering → self-review → ship ║
|
|
20
|
+
╚════════════════════════════════════════════╝${c.reset}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function step(label) {
|
|
24
|
+
return `\n${c.bold}${c.blue}▸ ${label}${c.reset}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function info(text) {
|
|
28
|
+
return `${c.dim} ${text}${c.reset}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function ok(text) {
|
|
32
|
+
return `${c.green} ✓ ${text}${c.reset}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function warn(text) {
|
|
36
|
+
return `${c.yellow} ⚠ ${text}${c.reset}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function err(text) {
|
|
40
|
+
return `${c.red} ✗ ${text}${c.reset}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function tool(name, preview) {
|
|
44
|
+
return `${c.dim} 🔧 ${c.reset}${c.magenta}${name}${c.reset}${c.dim}${preview ? '(' + preview + ')' : ''}${c.reset}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function thought(turn, text) {
|
|
48
|
+
const oneLine = text.replace(/\s+/g, ' ').trim();
|
|
49
|
+
const trimmed = oneLine.length > 200 ? oneLine.slice(0, 197) + '...' : oneLine;
|
|
50
|
+
return `${c.dim} 💭 [turn ${turn}] ${trimmed}${c.reset}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function usageSummary(label, usage) {
|
|
54
|
+
const cost = computeCost(usage);
|
|
55
|
+
return `${c.bold}${label}${c.reset}\n` +
|
|
56
|
+
` input: ${usage.input_tokens.toLocaleString()} tok` +
|
|
57
|
+
` · output: ${usage.output_tokens.toLocaleString()} tok` +
|
|
58
|
+
` · cache_read: ${usage.cache_read_input_tokens.toLocaleString()} tok` +
|
|
59
|
+
` · cache_create: ${(usage.cache_creation_input_tokens || 0).toLocaleString()} tok\n` +
|
|
60
|
+
` ${c.green}cost: $${cost.total_usd.toFixed(4)}${c.reset}` +
|
|
61
|
+
` ${c.dim}(in $${cost.input_usd.toFixed(4)} + out $${cost.output_usd.toFixed(4)} + cache_r $${cost.cache_read_usd.toFixed(4)} + cache_c $${cost.cache_creation_usd.toFixed(4)})${c.reset}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function makeAgentEventHandler(log) {
|
|
65
|
+
return (e) => {
|
|
66
|
+
if (e.type === 'turn_start') {
|
|
67
|
+
// Quiet — don't spam. The thoughts/tools will print.
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (e.type === 'thought') {
|
|
71
|
+
log(thought(e.turn, e.text));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (e.type === 'tool_call') {
|
|
75
|
+
log(tool(e.name, e.preview));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (e.type === 'tool_result') {
|
|
79
|
+
if (e.ok) {
|
|
80
|
+
if (e.flaky) log(warn(` → ok (flaky: passed after ${e.attempts} attempts)`));
|
|
81
|
+
else log(`${c.dim} → ok${c.reset}`);
|
|
82
|
+
} else {
|
|
83
|
+
log(err(` → ${e.error}`));
|
|
84
|
+
}
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (e.type === 'finished') {
|
|
88
|
+
log(ok(`Agent finished after ${e.turn} turn(s)`));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (e.type === 'iteration_limit') {
|
|
92
|
+
log(warn(`Agent hit iteration limit (${e.turn})`));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (e.type === 'no_tools') {
|
|
96
|
+
log(warn(`Agent stopped without calling finish (stop_reason=${e.stop_reason})`));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (e.type === 'cost_limit_hit') {
|
|
100
|
+
log(err(`Cost limit hit at turn ${e.turn}: $${e.costUsd.toFixed(4)} > $${e.limit}`));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (e.type === 'api_retry') {
|
|
104
|
+
const reason = (e.error && (e.error.status || e.error.code)) || 'unknown';
|
|
105
|
+
log(warn(`API call failed (${reason}); retrying in ${e.delayMs}ms (attempt ${e.nextAttempt})`));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function makeStageEventHandler(log) {
|
|
112
|
+
return (e) => {
|
|
113
|
+
if (e.stage === 'engineering_start') log(step('Engineering agent — autonomous fix loop'));
|
|
114
|
+
else if (e.stage === 'engineering_aborted') log(err('Engineering agent did not complete'));
|
|
115
|
+
else if (e.stage === 'no_diff') log(warn('Agent finished but produced no diff'));
|
|
116
|
+
else if (e.stage === 'self_review_start') log(step('Self-review — auditing the diff'));
|
|
117
|
+
else if (e.stage === 'self_review_done') log(ok(`Review verdict: ${e.verdict}`));
|
|
118
|
+
else if (e.stage === 'revision_start') log(step('Revision pass — addressing review feedback'));
|
|
119
|
+
else if (e.stage === 'revision_done') log(ok('Revision complete'));
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = {
|
|
124
|
+
banner,
|
|
125
|
+
step,
|
|
126
|
+
info,
|
|
127
|
+
ok,
|
|
128
|
+
warn,
|
|
129
|
+
err,
|
|
130
|
+
tool,
|
|
131
|
+
thought,
|
|
132
|
+
usageSummary,
|
|
133
|
+
computeCost,
|
|
134
|
+
makeAgentEventHandler,
|
|
135
|
+
makeStageEventHandler,
|
|
136
|
+
c
|
|
137
|
+
};
|
package/src/config.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
MODEL: 'claude-sonnet-4-6',
|
|
3
|
+
MAX_REVIEW_FILE_BYTES: 200_000,
|
|
4
|
+
|
|
5
|
+
MAX_AGENT_ITERATIONS: 18,
|
|
6
|
+
MAX_TURN_OUTPUT_TOKENS: 8192,
|
|
7
|
+
MAX_REVISION_PASSES: 1,
|
|
8
|
+
|
|
9
|
+
TEST_COMMAND_TIMEOUT_MS: 5 * 60 * 1000,
|
|
10
|
+
TOOL_OUTPUT_TRUNCATE: 8000,
|
|
11
|
+
|
|
12
|
+
// Cost per 1M tokens (USD) — Claude Sonnet pricing.
|
|
13
|
+
// Update here if Anthropic rates change.
|
|
14
|
+
COST_INPUT_PER_MTOK: 3.0,
|
|
15
|
+
COST_OUTPUT_PER_MTOK: 15.0,
|
|
16
|
+
COST_CACHE_READ_PER_MTOK: 0.30,
|
|
17
|
+
COST_CACHE_CREATION_PER_MTOK: 3.75,
|
|
18
|
+
|
|
19
|
+
// Hard kill switch — abort an agent loop if its cost crosses this many USD.
|
|
20
|
+
// Override per-run with --max-cost=X.XX
|
|
21
|
+
DEFAULT_MAX_USD_PER_RUN: 5.0
|
|
22
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Lightweight keyword-based file relevance scorer.
|
|
2
|
+
//
|
|
3
|
+
// The engineering agent previously had to explore big repos by grep-and-read
|
|
4
|
+
// alone, which wastes context and turns on Qiskit-class trees (thousands of
|
|
5
|
+
// files across multiple sub-packages). This module picks a shortlist of
|
|
6
|
+
// probably-relevant files given the issue text, using a fast local scorer —
|
|
7
|
+
// no embeddings, no extra API call.
|
|
8
|
+
//
|
|
9
|
+
// It is intentionally simple:
|
|
10
|
+
// 1. Tokenize the issue title + body (alphanumeric, lowercased, len >= 3,
|
|
11
|
+
// with a small stopword set).
|
|
12
|
+
// 2. For every repo file, score:
|
|
13
|
+
// path_score — how many issue tokens appear in the file path
|
|
14
|
+
// (basename weighted 3x, dir components 1x)
|
|
15
|
+
// content_score — how many issue tokens appear in the first 2 KB
|
|
16
|
+
// (weight 1 per occurrence, capped per token)
|
|
17
|
+
// 3. Return the top-K files by total score, ties broken by shorter paths
|
|
18
|
+
// (so `src/auth/login.py` beats `tests/integration/auth/login_test.py`).
|
|
19
|
+
//
|
|
20
|
+
// This is a prefilter, not a selector: the agent is still free to explore.
|
|
21
|
+
// We give it a starting hint so it doesn't burn turn 1-5 on directory walks.
|
|
22
|
+
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const path = require('path');
|
|
25
|
+
|
|
26
|
+
const STOPWORDS = new Set([
|
|
27
|
+
'the','a','an','and','or','but','not','to','of','in','on','for','with',
|
|
28
|
+
'is','was','were','be','been','being','are','am','do','does','did',
|
|
29
|
+
'this','that','these','those','it','its','as','if','then','else','when',
|
|
30
|
+
'at','by','from','into','out','up','down','over','under','than','so','too',
|
|
31
|
+
'very','can','cannot','should','would','could','may','might','must','will',
|
|
32
|
+
'shall','have','has','had','i','you','he','she','we','they','my','your',
|
|
33
|
+
'his','her','our','their','me','him','us','them','error','bug','issue',
|
|
34
|
+
'fix','fixes','fixed','broken','breaks','please','need','needs','want',
|
|
35
|
+
'wants','make','add','remove','support','feature','problem'
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
function tokenize(text) {
|
|
39
|
+
if (!text) return new Set();
|
|
40
|
+
const toks = String(text)
|
|
41
|
+
.toLowerCase()
|
|
42
|
+
.split(/[^a-z0-9_]+/)
|
|
43
|
+
.filter(t => t.length >= 3 && !STOPWORDS.has(t));
|
|
44
|
+
return new Set(toks);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function scorePath(filePath, tokens) {
|
|
48
|
+
const lower = filePath.toLowerCase();
|
|
49
|
+
const base = path.basename(lower, path.extname(lower));
|
|
50
|
+
const baseTokens = new Set(base.split(/[^a-z0-9]+/).filter(Boolean));
|
|
51
|
+
const dirTokens = new Set(
|
|
52
|
+
path.dirname(lower).split(/[\\/]+/).flatMap(p => p.split(/[^a-z0-9]+/)).filter(Boolean)
|
|
53
|
+
);
|
|
54
|
+
let score = 0;
|
|
55
|
+
for (const tok of tokens) {
|
|
56
|
+
if (baseTokens.has(tok)) score += 3;
|
|
57
|
+
if (dirTokens.has(tok)) score += 1;
|
|
58
|
+
}
|
|
59
|
+
return score;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function scoreContent(absPath, tokens, budget = 2048) {
|
|
63
|
+
let buf = '';
|
|
64
|
+
try {
|
|
65
|
+
const fd = fs.openSync(absPath, 'r');
|
|
66
|
+
const b = Buffer.alloc(budget);
|
|
67
|
+
const n = fs.readSync(fd, b, 0, budget, 0);
|
|
68
|
+
fs.closeSync(fd);
|
|
69
|
+
buf = b.slice(0, n).toString('utf8').toLowerCase();
|
|
70
|
+
} catch {
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
let score = 0;
|
|
74
|
+
for (const tok of tokens) {
|
|
75
|
+
// A token that appears at all counts once; bursts cap at 3 to avoid
|
|
76
|
+
// letting a doctest with the token 40 times dominate the ranking.
|
|
77
|
+
const occurrences = (buf.match(new RegExp(`\\b${escapeRegex(tok)}\\b`, 'g')) || []).length;
|
|
78
|
+
if (occurrences > 0) score += Math.min(3, occurrences);
|
|
79
|
+
}
|
|
80
|
+
return score;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function escapeRegex(s) {
|
|
84
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function rankFiles({ repoPath, files, issueText, topK = 30 }) {
|
|
88
|
+
const tokens = tokenize(issueText);
|
|
89
|
+
if (tokens.size === 0) {
|
|
90
|
+
// Nothing to score against. Return a stable head slice.
|
|
91
|
+
return files.slice(0, topK).map(f => ({ path: f, score: 0, pathScore: 0, contentScore: 0 }));
|
|
92
|
+
}
|
|
93
|
+
const scored = [];
|
|
94
|
+
for (const rel of files) {
|
|
95
|
+
const abs = path.join(repoPath, rel);
|
|
96
|
+
const pScore = scorePath(rel, tokens);
|
|
97
|
+
// Only pay the file-read cost for files that already look plausible on
|
|
98
|
+
// path alone, or whose cheap path score is non-zero. This keeps the
|
|
99
|
+
// scorer fast on 10k-file repos.
|
|
100
|
+
const cScore = pScore > 0 ? scoreContent(abs, tokens) : 0;
|
|
101
|
+
const total = pScore * 2 + cScore;
|
|
102
|
+
if (total > 0) {
|
|
103
|
+
scored.push({ path: rel, score: total, pathScore: pScore, contentScore: cScore });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
scored.sort((a, b) => {
|
|
107
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
108
|
+
return a.path.length - b.path.length;
|
|
109
|
+
});
|
|
110
|
+
return scored.slice(0, topK);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = { rankFiles, tokenize, scorePath, scoreContent };
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
// Directories to skip when walking. Covers Node, Python, Rust, Go, Java,
|
|
5
|
+
// common CI/IDE artefacts, and doc-build output — so the walker does not
|
|
6
|
+
// explode on large real-world projects (TQEC-scale Python, Kubernetes,
|
|
7
|
+
// Rust monorepos with vendored deps).
|
|
8
|
+
const IGNORE_DIRS = new Set([
|
|
9
|
+
// Node
|
|
10
|
+
'node_modules', 'dist', 'build', '.next', 'coverage', '.turbo',
|
|
11
|
+
// VCS
|
|
12
|
+
'.git', '.hg', '.svn',
|
|
13
|
+
// Python
|
|
14
|
+
'__pycache__', '.venv', 'venv', 'env',
|
|
15
|
+
'.mypy_cache', '.pytest_cache', '.ruff_cache', '.pytype',
|
|
16
|
+
'.tox', '.nox', '.eggs', 'htmlcov',
|
|
17
|
+
// Rust / Go / Java / Gradle
|
|
18
|
+
'target', 'vendor', '.gradle', '.m2',
|
|
19
|
+
// Editor / OS
|
|
20
|
+
'.idea', '.vscode', '.DS_Store',
|
|
21
|
+
// Docs build
|
|
22
|
+
'_build', 'site',
|
|
23
|
+
]);
|
|
24
|
+
// Source files: the agent is expected to edit these.
|
|
25
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
26
|
+
// JS/TS
|
|
27
|
+
'.js', '.ts', '.tsx', '.jsx', '.mjs', '.cjs',
|
|
28
|
+
// Python (incl. Cython/typeshed)
|
|
29
|
+
'.py', '.pyx', '.pxd', '.pyi',
|
|
30
|
+
// Native / scientific Python dependencies
|
|
31
|
+
'.c', '.cpp', '.cc', '.cxx', '.h', '.hpp', '.hh',
|
|
32
|
+
// Other mainstream
|
|
33
|
+
'.rs', '.go', '.java', '.kt', '.rb', '.php', '.swift', '.scala',
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
// Project/config files: the agent can READ these to orient itself (detect
|
|
37
|
+
// test command, read CONTRIBUTING.md, read PR template, understand package
|
|
38
|
+
// layout), but shouldn't treat them as normal source targets.
|
|
39
|
+
const CONFIG_EXTENSIONS = new Set([
|
|
40
|
+
'.toml', '.cfg', '.ini', '.yaml', '.yml', '.json',
|
|
41
|
+
'.md', '.rst', '.txt',
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
const SPECIAL_FILENAMES = new Set([
|
|
45
|
+
'Makefile', 'makefile', 'GNUmakefile',
|
|
46
|
+
'Dockerfile', 'dockerfile',
|
|
47
|
+
'tox.ini', 'noxfile.py', 'pyproject.toml', 'setup.py', 'setup.cfg',
|
|
48
|
+
'package.json', 'Cargo.toml', 'go.mod',
|
|
49
|
+
'CONTRIBUTING.md', 'CONTRIBUTING.rst',
|
|
50
|
+
'CODE_OF_CONDUCT.md',
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
const ALLOWED_EXTENSIONS = new Set([...SOURCE_EXTENSIONS, ...CONFIG_EXTENSIONS]);
|
|
54
|
+
|
|
55
|
+
// Hard cap so a single list_files call can't consume the agent's context.
|
|
56
|
+
const DEFAULT_FILE_CAP = 2000;
|
|
57
|
+
|
|
58
|
+
function buildRepoMap(rootPath, { prefix = '', maxFiles = DEFAULT_FILE_CAP } = {}) {
|
|
59
|
+
const files = [];
|
|
60
|
+
let total = 0;
|
|
61
|
+
let truncated = false;
|
|
62
|
+
|
|
63
|
+
function walk(dir, rel) {
|
|
64
|
+
if (truncated) return;
|
|
65
|
+
let entries;
|
|
66
|
+
try {
|
|
67
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
68
|
+
} catch {
|
|
69
|
+
return; // unreadable dir — skip, don't crash the walk
|
|
70
|
+
}
|
|
71
|
+
for (const entry of entries) {
|
|
72
|
+
if (truncated) return;
|
|
73
|
+
if (IGNORE_DIRS.has(entry.name)) continue;
|
|
74
|
+
if (entry.name.startsWith('.') && IGNORE_DIRS.has(entry.name)) continue;
|
|
75
|
+
const full = path.join(dir, entry.name);
|
|
76
|
+
const next = rel ? `${rel}/${entry.name}` : entry.name;
|
|
77
|
+
if (entry.isDirectory()) {
|
|
78
|
+
walk(full, next);
|
|
79
|
+
} else if (entry.isFile()) {
|
|
80
|
+
const ext = path.extname(entry.name);
|
|
81
|
+
const isRelevant = ALLOWED_EXTENSIONS.has(ext) || SPECIAL_FILENAMES.has(entry.name);
|
|
82
|
+
if (!isRelevant) continue;
|
|
83
|
+
total++;
|
|
84
|
+
if (files.length >= maxFiles) {
|
|
85
|
+
truncated = true;
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
files.push(prefix ? `${prefix}/${next}` : next);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
walk(rootPath, '');
|
|
94
|
+
return { files, total, truncated, cap: maxFiles };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module.exports = {
|
|
98
|
+
buildRepoMap,
|
|
99
|
+
IGNORE_DIRS,
|
|
100
|
+
ALLOWED_EXTENSIONS,
|
|
101
|
+
SOURCE_EXTENSIONS,
|
|
102
|
+
CONFIG_EXTENSIONS,
|
|
103
|
+
SPECIAL_FILENAMES,
|
|
104
|
+
DEFAULT_FILE_CAP
|
|
105
|
+
};
|