dsh-plugin-coding-delegate 0.0.0-stage → 0.2.2
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/LICENSE +21 -0
- package/README.md +920 -2
- package/cordis.patch.example.yml +48 -0
- package/cordis.patch.yml +13 -0
- package/dist/attestation.d.ts +25 -0
- package/dist/attestation.js +44 -0
- package/dist/containment.d.ts +33 -0
- package/dist/containment.js +63 -0
- package/dist/context-quality.d.ts +79 -0
- package/dist/context-quality.js +181 -0
- package/dist/context.d.ts +39 -0
- package/dist/context.js +136 -0
- package/dist/contracts.d.ts +174 -0
- package/dist/contracts.js +415 -0
- package/dist/declaration-egress-hook.d.ts +45 -0
- package/dist/declaration-egress-hook.js +125 -0
- package/dist/declaration-egress.d.ts +37 -0
- package/dist/declaration-egress.js +169 -0
- package/dist/delegation.d.ts +339 -0
- package/dist/delegation.js +933 -0
- package/dist/emission.d.ts +70 -0
- package/dist/emission.js +310 -0
- package/dist/guard.d.ts +162 -0
- package/dist/guard.js +873 -0
- package/dist/host-events.d.ts +15 -0
- package/dist/host-events.js +27 -0
- package/dist/index.d.ts +560 -0
- package/dist/index.js +1398 -0
- package/dist/local-classifier.d.ts +52 -0
- package/dist/local-classifier.js +159 -0
- package/dist/logging.d.ts +21 -0
- package/dist/logging.js +90 -0
- package/dist/paths.d.ts +10 -0
- package/dist/paths.js +83 -0
- package/dist/profiles.d.ts +35 -0
- package/dist/profiles.js +95 -0
- package/dist/retry-context.d.ts +52 -0
- package/dist/retry-context.js +97 -0
- package/dist/roles.d.ts +129 -0
- package/dist/roles.js +363 -0
- package/dist/routing.d.ts +35 -0
- package/dist/routing.js +88 -0
- package/dist/savings-tracker.d.ts +128 -0
- package/dist/savings-tracker.js +340 -0
- package/dist/session-events.d.ts +17 -0
- package/dist/session-events.js +31 -0
- package/dist/verification.d.ts +104 -0
- package/dist/verification.js +560 -0
- package/package.json +66 -4
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export type ClassifierRoute = 'local' | 'cloud';
|
|
2
|
+
export interface ClassifierScores {
|
|
3
|
+
is_private: number;
|
|
4
|
+
complexity: number;
|
|
5
|
+
target: 'LOCAL_5090' | 'CLOUD_DEEPSEEK';
|
|
6
|
+
}
|
|
7
|
+
export interface ClassifierDecision {
|
|
8
|
+
route: ClassifierRoute;
|
|
9
|
+
rationale: string;
|
|
10
|
+
gate: string;
|
|
11
|
+
scores: ClassifierScores;
|
|
12
|
+
latencyMs: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The single source of truth for credential detection, shared by this classifier and
|
|
16
|
+
* the enforced DLP gate in index.ts. One table, so the gate that decides whether a
|
|
17
|
+
* payload may leave the machine can never be weaker than the one that reports on it.
|
|
18
|
+
*
|
|
19
|
+
* Patterns are deliberately NOT global: a `/g` regex carries `lastIndex` between
|
|
20
|
+
* `.test()` calls and silently alternates between matching and not matching.
|
|
21
|
+
*/
|
|
22
|
+
export declare const SECRET_PATTERN_RULES: {
|
|
23
|
+
name: string;
|
|
24
|
+
pattern: RegExp;
|
|
25
|
+
}[];
|
|
26
|
+
/** Convenience view of {@link SECRET_PATTERN_RULES} for callers that want patterns only. */
|
|
27
|
+
export declare const SECRET_PATTERNS: RegExp[];
|
|
28
|
+
/** Shannon entropy in bits per character. */
|
|
29
|
+
export declare function shannonEntropy(value: string): number;
|
|
30
|
+
/** Bits per character above which a token is treated as suspiciously random. */
|
|
31
|
+
export declare const ENTROPY_MIN_BITS_PER_CHAR = 4.5;
|
|
32
|
+
/** Tokens shorter than this are too noisy to score. */
|
|
33
|
+
export declare const ENTROPY_MIN_LENGTH = 20;
|
|
34
|
+
/**
|
|
35
|
+
* The backstop for credentials no keyword or vendor pattern would reveal.
|
|
36
|
+
*
|
|
37
|
+
* Thresholds are measured, not guessed: on a corpus containing sha256 digests, base64
|
|
38
|
+
* payloads, UUIDs, git SHAs, long file paths, prose and minified CSS, 4.5 bits/char at
|
|
39
|
+
* length >= 20 caught every sampled secret with zero false positives. Random keys sit
|
|
40
|
+
* near 5.2 bits/char; the noise sat at 4.2-4.4. Re-tune against a real corpus before
|
|
41
|
+
* trusting the boundary in a new codebase.
|
|
42
|
+
*/
|
|
43
|
+
export declare function findHighEntropyTokens(text: string, options?: {
|
|
44
|
+
minBitsPerChar?: number;
|
|
45
|
+
minLength?: number;
|
|
46
|
+
}): string[];
|
|
47
|
+
export declare function containsSensitiveCredentials(text: string): boolean;
|
|
48
|
+
export declare function evaluateHeuristics(promptSlice: string, secretScanText?: string): ClassifierScores;
|
|
49
|
+
export declare function classifyLocally(promptText: string, options?: {
|
|
50
|
+
windowChars?: number;
|
|
51
|
+
scanFullTextForSecrets?: boolean;
|
|
52
|
+
}): ClassifierDecision;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// The credential scan intentionally covers the WHOLE prompt while the complexity window stays bounded.
|
|
3
|
+
// A credential near the start of a long file must not escape detection, so we scan the full text for secrets.
|
|
4
|
+
// The complexity window remains bounded to keep heuristic scoring fast and predictable.
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ENTROPY_MIN_LENGTH = exports.ENTROPY_MIN_BITS_PER_CHAR = exports.SECRET_PATTERNS = exports.SECRET_PATTERN_RULES = void 0;
|
|
7
|
+
exports.shannonEntropy = shannonEntropy;
|
|
8
|
+
exports.findHighEntropyTokens = findHighEntropyTokens;
|
|
9
|
+
exports.containsSensitiveCredentials = containsSensitiveCredentials;
|
|
10
|
+
exports.evaluateHeuristics = evaluateHeuristics;
|
|
11
|
+
exports.classifyLocally = classifyLocally;
|
|
12
|
+
/**
|
|
13
|
+
* The single source of truth for credential detection, shared by this classifier and
|
|
14
|
+
* the enforced DLP gate in index.ts. One table, so the gate that decides whether a
|
|
15
|
+
* payload may leave the machine can never be weaker than the one that reports on it.
|
|
16
|
+
*
|
|
17
|
+
* Patterns are deliberately NOT global: a `/g` regex carries `lastIndex` between
|
|
18
|
+
* `.test()` calls and silently alternates between matching and not matching.
|
|
19
|
+
*/
|
|
20
|
+
exports.SECRET_PATTERN_RULES = [
|
|
21
|
+
{ name: 'Private Key Block', pattern: /-----BEGIN (?:RSA |EC |OPENSSH |PGP |)PRIVATE KEY-----/i },
|
|
22
|
+
{
|
|
23
|
+
name: 'Generic Secret Keyword',
|
|
24
|
+
pattern: /(?:api_key|apikey|secret_key|private_key|auth_token|access_token|password)\s*[:=]\s*['"]?[A-Za-z0-9_\-\.]{8,}/i,
|
|
25
|
+
},
|
|
26
|
+
{ name: 'GitHub PAT', pattern: /(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{36}/i },
|
|
27
|
+
{ name: 'Fine-grained GitHub PAT', pattern: /github_pat_[A-Za-z0-9_]{20,}/i },
|
|
28
|
+
{ name: 'OpenAI/DeepSeek API Key', pattern: /sk-[a-zA-Z0-9]{32,}/i },
|
|
29
|
+
{ name: 'AWS Access Key', pattern: /AKIA[0-9A-Z]{16}/i },
|
|
30
|
+
{ name: 'Slack Token', pattern: /xox[baprs]-[a-zA-Z0-9]{10,}/i },
|
|
31
|
+
];
|
|
32
|
+
/** Convenience view of {@link SECRET_PATTERN_RULES} for callers that want patterns only. */
|
|
33
|
+
exports.SECRET_PATTERNS = exports.SECRET_PATTERN_RULES.map((rule) => rule.pattern);
|
|
34
|
+
/** Shannon entropy in bits per character. */
|
|
35
|
+
function shannonEntropy(value) {
|
|
36
|
+
if (!value)
|
|
37
|
+
return 0;
|
|
38
|
+
const counts = new Map();
|
|
39
|
+
for (const ch of value)
|
|
40
|
+
counts.set(ch, (counts.get(ch) ?? 0) + 1);
|
|
41
|
+
let entropy = 0;
|
|
42
|
+
for (const count of counts.values()) {
|
|
43
|
+
const p = count / value.length;
|
|
44
|
+
entropy -= p * Math.log2(p);
|
|
45
|
+
}
|
|
46
|
+
return entropy;
|
|
47
|
+
}
|
|
48
|
+
/** Bits per character above which a token is treated as suspiciously random. */
|
|
49
|
+
exports.ENTROPY_MIN_BITS_PER_CHAR = 4.5;
|
|
50
|
+
/** Tokens shorter than this are too noisy to score. */
|
|
51
|
+
exports.ENTROPY_MIN_LENGTH = 20;
|
|
52
|
+
/** Runs worth scoring: base64-ish and hex-ish token shapes. */
|
|
53
|
+
const ENTROPY_TOKEN = /[A-Za-z0-9+/_=-]{20,}/g;
|
|
54
|
+
/**
|
|
55
|
+
* The backstop for credentials no keyword or vendor pattern would reveal.
|
|
56
|
+
*
|
|
57
|
+
* Thresholds are measured, not guessed: on a corpus containing sha256 digests, base64
|
|
58
|
+
* payloads, UUIDs, git SHAs, long file paths, prose and minified CSS, 4.5 bits/char at
|
|
59
|
+
* length >= 20 caught every sampled secret with zero false positives. Random keys sit
|
|
60
|
+
* near 5.2 bits/char; the noise sat at 4.2-4.4. Re-tune against a real corpus before
|
|
61
|
+
* trusting the boundary in a new codebase.
|
|
62
|
+
*/
|
|
63
|
+
function findHighEntropyTokens(text, options = {}) {
|
|
64
|
+
if (!text)
|
|
65
|
+
return [];
|
|
66
|
+
const minBits = options.minBitsPerChar ?? exports.ENTROPY_MIN_BITS_PER_CHAR;
|
|
67
|
+
const minLength = options.minLength ?? exports.ENTROPY_MIN_LENGTH;
|
|
68
|
+
const found = [];
|
|
69
|
+
for (const token of text.match(ENTROPY_TOKEN) ?? []) {
|
|
70
|
+
if (token.length < minLength)
|
|
71
|
+
continue;
|
|
72
|
+
if (shannonEntropy(token) >= minBits)
|
|
73
|
+
found.push(token);
|
|
74
|
+
}
|
|
75
|
+
return found;
|
|
76
|
+
}
|
|
77
|
+
function containsSensitiveCredentials(text) {
|
|
78
|
+
if (!text)
|
|
79
|
+
return false;
|
|
80
|
+
for (const pattern of exports.SECRET_PATTERNS) {
|
|
81
|
+
if (pattern.test(text))
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
const COMPLEX_WORDS = [
|
|
87
|
+
'refactor',
|
|
88
|
+
'architecture',
|
|
89
|
+
'async',
|
|
90
|
+
'thread',
|
|
91
|
+
'concurrency',
|
|
92
|
+
'distributed',
|
|
93
|
+
'kubernetes',
|
|
94
|
+
'algorithm',
|
|
95
|
+
'optimization',
|
|
96
|
+
'recursion',
|
|
97
|
+
'deadlock',
|
|
98
|
+
];
|
|
99
|
+
function evaluateHeuristics(promptSlice, secretScanText) {
|
|
100
|
+
const slice = String(promptSlice ?? '');
|
|
101
|
+
const lines = slice.split('\n').length;
|
|
102
|
+
const length = slice.length;
|
|
103
|
+
const lower = slice.toLowerCase();
|
|
104
|
+
let complexCount = 0;
|
|
105
|
+
for (const word of COMPLEX_WORDS) {
|
|
106
|
+
if (lower.includes(word))
|
|
107
|
+
complexCount++;
|
|
108
|
+
}
|
|
109
|
+
let complexity;
|
|
110
|
+
if (length > 3000 || lines > 100 || complexCount >= 3) {
|
|
111
|
+
complexity = 3;
|
|
112
|
+
}
|
|
113
|
+
else if (length > 1200 || lines > 40 || complexCount >= 1) {
|
|
114
|
+
complexity = 2;
|
|
115
|
+
}
|
|
116
|
+
else if (length > 400) {
|
|
117
|
+
complexity = 1;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
complexity = 0;
|
|
121
|
+
}
|
|
122
|
+
const target = complexity >= 2 ? 'CLOUD_DEEPSEEK' : 'LOCAL_5090';
|
|
123
|
+
const scanText = secretScanText ?? slice;
|
|
124
|
+
const is_private = containsSensitiveCredentials(scanText) ? 0.99 : 0.05;
|
|
125
|
+
return { is_private, complexity, target };
|
|
126
|
+
}
|
|
127
|
+
function classifyLocally(promptText, options) {
|
|
128
|
+
const start = performance.now();
|
|
129
|
+
const windowChars = options?.windowChars ?? 2000;
|
|
130
|
+
const scanFullTextForSecrets = options?.scanFullTextForSecrets ?? true;
|
|
131
|
+
const fullText = String(promptText ?? '');
|
|
132
|
+
const window = fullText.slice(-windowChars);
|
|
133
|
+
const scores = evaluateHeuristics(window, scanFullTextForSecrets ? fullText : window);
|
|
134
|
+
let route;
|
|
135
|
+
let gate;
|
|
136
|
+
let rationale;
|
|
137
|
+
if (scores.is_private > 0.8) {
|
|
138
|
+
route = 'local';
|
|
139
|
+
gate = 'Gate 1 (Local Classifier - Privacy Protection)';
|
|
140
|
+
rationale = `Privacy threshold exceeded (P(private) = ${scores.is_private.toFixed(2)} > 0.80). Routing to the local worker.`;
|
|
141
|
+
}
|
|
142
|
+
else if (scores.complexity >= 2) {
|
|
143
|
+
route = 'cloud';
|
|
144
|
+
gate = 'Gate 1 (Local Classifier - High Complexity)';
|
|
145
|
+
rationale = `High complexity score (complexity = ${scores.complexity} >= 2). Routing to DeepSeek Cloud.`;
|
|
146
|
+
}
|
|
147
|
+
else if (scores.target === 'CLOUD_DEEPSEEK') {
|
|
148
|
+
route = 'cloud';
|
|
149
|
+
gate = 'Gate 1 (Local Classifier - Model Target Choice)';
|
|
150
|
+
rationale = 'Target choice evaluated as CLOUD_DEEPSEEK. Routing to DeepSeek Cloud.';
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
route = 'local';
|
|
154
|
+
gate = 'Gate 1 (Local Classifier - Local Default)';
|
|
155
|
+
rationale = "Request fits within the configured local model's capabilities. Routing to the local worker.";
|
|
156
|
+
}
|
|
157
|
+
const latencyMs = Math.round((performance.now() - start) * 100) / 100;
|
|
158
|
+
return { route, rationale, gate, scores, latencyMs };
|
|
159
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* All plugin state (debug log, savings ledger) lives under one derived directory.
|
|
3
|
+
* It must never be a hard-coded absolute path: the previous build wrote its log
|
|
4
|
+
* into the plugin author's own project directory on every machine, which was
|
|
5
|
+
* correct on exactly one of them.
|
|
6
|
+
* Precedence: explicit env override, then DSH_HOME, then ~/.dsh.
|
|
7
|
+
*/
|
|
8
|
+
export declare function resolveDataDir(): string;
|
|
9
|
+
/**
|
|
10
|
+
* Remove prompt text from a trace payload before it is written to disk.
|
|
11
|
+
*
|
|
12
|
+
* A debug log outlives the session, and call sites pass a prompt slice into it. No prompt content may
|
|
13
|
+
* be persisted, so the value is replaced with its length rather than scanned for secrets: pattern
|
|
14
|
+
* detection has false negatives, and a replacement is a guarantee where a pattern is a probability.
|
|
15
|
+
* Every other field is preserved, because redaction that succeeds by writing nothing removes the
|
|
16
|
+
* diagnostic value the log exists for.
|
|
17
|
+
*
|
|
18
|
+
* Pure and exported so it is testable directly. The caller's object is not mutated.
|
|
19
|
+
*/
|
|
20
|
+
export declare function redactTracePayload(data: any): any;
|
|
21
|
+
export declare function trace(event: string, data: any): void;
|
package/dist/logging.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.resolveDataDir = resolveDataDir;
|
|
37
|
+
exports.redactTracePayload = redactTracePayload;
|
|
38
|
+
exports.trace = trace;
|
|
39
|
+
const fs = __importStar(require("fs"));
|
|
40
|
+
const os = __importStar(require("os"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
/**
|
|
43
|
+
* All plugin state (debug log, savings ledger) lives under one derived directory.
|
|
44
|
+
* It must never be a hard-coded absolute path: the previous build wrote its log
|
|
45
|
+
* into the plugin author's own project directory on every machine, which was
|
|
46
|
+
* correct on exactly one of them.
|
|
47
|
+
* Precedence: explicit env override, then DSH_HOME, then ~/.dsh.
|
|
48
|
+
*/
|
|
49
|
+
function resolveDataDir() {
|
|
50
|
+
const explicit = process.env.DSH_LOCAL_ROUTER_DATA_DIR;
|
|
51
|
+
if (explicit && explicit.trim())
|
|
52
|
+
return explicit.trim();
|
|
53
|
+
const dshHome = process.env.DSH_HOME;
|
|
54
|
+
if (dshHome && dshHome.trim())
|
|
55
|
+
return path.join(dshHome.trim(), 'local-router');
|
|
56
|
+
return path.join(os.homedir(), '.dsh', 'local-router');
|
|
57
|
+
}
|
|
58
|
+
const LOG_FILE = path.join(resolveDataDir(), 'router-debug.log');
|
|
59
|
+
/**
|
|
60
|
+
* Remove prompt text from a trace payload before it is written to disk.
|
|
61
|
+
*
|
|
62
|
+
* A debug log outlives the session, and call sites pass a prompt slice into it. No prompt content may
|
|
63
|
+
* be persisted, so the value is replaced with its length rather than scanned for secrets: pattern
|
|
64
|
+
* detection has false negatives, and a replacement is a guarantee where a pattern is a probability.
|
|
65
|
+
* Every other field is preserved, because redaction that succeeds by writing nothing removes the
|
|
66
|
+
* diagnostic value the log exists for.
|
|
67
|
+
*
|
|
68
|
+
* Pure and exported so it is testable directly. The caller's object is not mutated.
|
|
69
|
+
*/
|
|
70
|
+
function redactTracePayload(data) {
|
|
71
|
+
if (!data || typeof data !== 'object' || Array.isArray(data))
|
|
72
|
+
return data;
|
|
73
|
+
const copy = { ...data };
|
|
74
|
+
if (typeof copy.prompt === 'string') {
|
|
75
|
+
copy.prompt = 'prompt suppressed (' + copy.prompt.length + ' chars)';
|
|
76
|
+
}
|
|
77
|
+
return copy;
|
|
78
|
+
}
|
|
79
|
+
function trace(event, data) {
|
|
80
|
+
const timestamp = new Date().toISOString();
|
|
81
|
+
const payload = redactTracePayload(data);
|
|
82
|
+
const entry = '\n[' + timestamp + '] === ' + event + ' ===\n' +
|
|
83
|
+
(typeof payload === 'string' ? payload : JSON.stringify(payload, null, 2)) +
|
|
84
|
+
'\n';
|
|
85
|
+
try {
|
|
86
|
+
fs.mkdirSync(path.dirname(LOG_FILE), { recursive: true });
|
|
87
|
+
fs.appendFileSync(LOG_FILE, entry, 'utf8');
|
|
88
|
+
}
|
|
89
|
+
catch (err) { }
|
|
90
|
+
}
|
package/dist/paths.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve `p` to a canonical path, following symlinks for the part of it that exists.
|
|
3
|
+
* A destination that does not exist yet has no realpath of its own, so the deepest
|
|
4
|
+
* existing ancestor is resolved and the remaining segments are re-appended.
|
|
5
|
+
*/
|
|
6
|
+
export declare function canonicalisePath(p: string): string;
|
|
7
|
+
/** True when `candidate` is `root` itself or lives beneath it. Case-insensitive on Windows. */
|
|
8
|
+
export declare function isPathWithin(root: string, candidate: string): boolean;
|
|
9
|
+
/** Extensions treated as source code: writes must come from the local worker. */
|
|
10
|
+
export declare const CODE_EXTENSIONS: Set<string>;
|
package/dist/paths.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.CODE_EXTENSIONS = void 0;
|
|
37
|
+
exports.canonicalisePath = canonicalisePath;
|
|
38
|
+
exports.isPathWithin = isPathWithin;
|
|
39
|
+
const fs = __importStar(require("fs"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
/**
|
|
42
|
+
* Resolve `p` to a canonical path, following symlinks for the part of it that exists.
|
|
43
|
+
* A destination that does not exist yet has no realpath of its own, so the deepest
|
|
44
|
+
* existing ancestor is resolved and the remaining segments are re-appended.
|
|
45
|
+
*/
|
|
46
|
+
function canonicalisePath(p) {
|
|
47
|
+
let current = path.resolve(p);
|
|
48
|
+
const tail = [];
|
|
49
|
+
for (;;) {
|
|
50
|
+
if (fs.existsSync(current))
|
|
51
|
+
break;
|
|
52
|
+
const parent = path.dirname(current);
|
|
53
|
+
if (parent === current)
|
|
54
|
+
break;
|
|
55
|
+
tail.unshift(path.basename(current));
|
|
56
|
+
current = parent;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
current = fs.realpathSync(current);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// An unresolvable ancestor is not a reason to trust the path; keep it as written.
|
|
63
|
+
}
|
|
64
|
+
return tail.length > 0 ? path.join(current, ...tail) : current;
|
|
65
|
+
}
|
|
66
|
+
/** True when `candidate` is `root` itself or lives beneath it. Case-insensitive on Windows. */
|
|
67
|
+
function isPathWithin(root, candidate) {
|
|
68
|
+
const flatten = (value) => (process.platform === 'win32' ? value.toLowerCase() : value);
|
|
69
|
+
const from = flatten(path.resolve(root));
|
|
70
|
+
const to = flatten(path.resolve(candidate));
|
|
71
|
+
if (from === to)
|
|
72
|
+
return true;
|
|
73
|
+
const rel = path.relative(from, to);
|
|
74
|
+
return rel !== '' && rel !== '..' && !rel.startsWith('..' + path.sep) && !path.isAbsolute(rel);
|
|
75
|
+
}
|
|
76
|
+
/** Extensions treated as source code: writes must come from the local worker. */
|
|
77
|
+
exports.CODE_EXTENSIONS = new Set([
|
|
78
|
+
'.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs',
|
|
79
|
+
'.py', '.rb', '.go', '.rs', '.java', '.kt', '.kts', '.cs', '.fs', '.vb',
|
|
80
|
+
'.c', '.h', '.cc', '.cpp', '.hpp', '.swift', '.php', '.scala', '.lua', '.dart',
|
|
81
|
+
'.sh', '.bash', '.zsh', '.ps1', '.psm1', '.sql',
|
|
82
|
+
'.html', '.htm', '.css', '.scss', '.sass', '.less', '.vue', '.svelte',
|
|
83
|
+
]);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface ProfileConfig {
|
|
2
|
+
name: string;
|
|
3
|
+
provider: string;
|
|
4
|
+
model: string;
|
|
5
|
+
endpoint?: string;
|
|
6
|
+
contextWindow?: number;
|
|
7
|
+
uncappedContextWindow?: boolean;
|
|
8
|
+
systemInstruction?: string;
|
|
9
|
+
temperature?: number;
|
|
10
|
+
max_tokens?: number;
|
|
11
|
+
stop?: string[];
|
|
12
|
+
enable_thinking?: boolean;
|
|
13
|
+
reasoning_effort?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface Profiles {
|
|
16
|
+
ARCHITECT: ProfileConfig;
|
|
17
|
+
LEAD: ProfileConfig;
|
|
18
|
+
WORKER: ProfileConfig;
|
|
19
|
+
}
|
|
20
|
+
export declare const PROFILES: Profiles;
|
|
21
|
+
/**
|
|
22
|
+
* Measured reference throughput for the local worker models (author's reference rig /
|
|
23
|
+
* LM Studio, temperature 0, single stream, no concurrent load). Printed once at startup
|
|
24
|
+
* for operator reference; live per-call rates ride the LEDGER_AUDIT line instead.
|
|
25
|
+
* These are one machine's numbers: throughput is a property of the hardware and the
|
|
26
|
+
* model, not of the plugin, and a different card will produce different figures.
|
|
27
|
+
*/
|
|
28
|
+
export interface WorkerBenchmark {
|
|
29
|
+
model: string;
|
|
30
|
+
decodeTps: number;
|
|
31
|
+
ttft: string;
|
|
32
|
+
note: string;
|
|
33
|
+
}
|
|
34
|
+
export declare const WORKER_BENCHMARK_SOURCE: string;
|
|
35
|
+
export declare const WORKER_BENCHMARKS: WorkerBenchmark[];
|
package/dist/profiles.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WORKER_BENCHMARKS = exports.WORKER_BENCHMARK_SOURCE = exports.PROFILES = void 0;
|
|
4
|
+
exports.PROFILES = {
|
|
5
|
+
ARCHITECT: {
|
|
6
|
+
name: 'ARCHITECT_CLOUD',
|
|
7
|
+
provider: 'deepseek-official',
|
|
8
|
+
model: 'deepseek-chat',
|
|
9
|
+
uncappedContextWindow: true,
|
|
10
|
+
systemInstruction: 'You are the Lead Architect. You have access to the `delegate_worker` tool. For implementation, ' +
|
|
11
|
+
'component code, file generation, test writing, or heavy algorithmic coding tasks, you MUST call ' +
|
|
12
|
+
'the `delegate_worker` tool to delegate execution to the local worker rather than outputting code ' +
|
|
13
|
+
'in chat markdown. You may read source to engineer a change, but read narrowly and read late: ' +
|
|
14
|
+
'prefer the verdict you already have over opening the file, read only the lines the unit changes, ' +
|
|
15
|
+
'and remember that anything you read is re-sent on every later turn. Do not read back what you ' +
|
|
16
|
+
'delegated. The local worker has no repository read either: it can never find the code it needs, so ' +
|
|
17
|
+
'declare contextFiles for the files it must see, and state the interfaces, types and behaviour it ' +
|
|
18
|
+
'needs. Use contractFiles for the tests that decide the unit. Never assume the worker can ' +
|
|
19
|
+
'discover anything from the codebase, and never assume it can return a large file in one piece.',
|
|
20
|
+
},
|
|
21
|
+
/**
|
|
22
|
+
* The lead tier: a thinking model with repository access that authors each unit's contract. It is
|
|
23
|
+
* deliberately local, because a cloud lead would mean source reaching the cloud and would spend the
|
|
24
|
+
* metered allowance this plugin exists to protect. It is deliberately not given `delegate_worker`:
|
|
25
|
+
* the lead authors contracts, the architect dispatches them.
|
|
26
|
+
*
|
|
27
|
+
* RETIRED, and almost entirely REFERENCE ONLY. The tier was built, measured, and retired; the
|
|
28
|
+
* measurements are in `docs/ROADMAP.md`. What matters for reading this object is that **only
|
|
29
|
+
* `provider` is ever read** — `resolveLeadProviders` derives the `leadTier` allowlist from it, and
|
|
30
|
+
* that is the whole of the plugin's use.
|
|
31
|
+
*
|
|
32
|
+
* Every other field here (`model`, `endpoint`, `contextWindow`, `temperature`, `max_tokens`,
|
|
33
|
+
* `enable_thinking`, `reasoning_effort`, `systemInstruction`) is NEVER APPLIED, and deliberately so:
|
|
34
|
+
* a request the host resolved to a lead provider is left exactly as the host configured it, because
|
|
35
|
+
* the plugin enforces boundaries rather than reinventing a preset it did not write. The agent preset
|
|
36
|
+
* is what actually runs (see `docs/lead-tier.md`).
|
|
37
|
+
*
|
|
38
|
+
* They are kept as the record of what the tier was configured to be, and `lead-tier.test.cjs` asserts
|
|
39
|
+
* that record has not drifted. Read them as documentation, not as configuration: changing one has no
|
|
40
|
+
* effect on any request.
|
|
41
|
+
*/
|
|
42
|
+
LEAD: {
|
|
43
|
+
name: 'LEAD_LOCAL',
|
|
44
|
+
provider: 'lm-studio',
|
|
45
|
+
model: 'qwen/qwen3.8-27b',
|
|
46
|
+
endpoint: 'http://127.0.0.1:1234/v1',
|
|
47
|
+
contextWindow: 32768,
|
|
48
|
+
temperature: 0.2,
|
|
49
|
+
max_tokens: 8192,
|
|
50
|
+
enable_thinking: true,
|
|
51
|
+
reasoning_effort: 'high',
|
|
52
|
+
systemInstruction: 'You are the Lead. You read the repository and author the contract for each unit of work. You do ' +
|
|
53
|
+
'not write implementation code, and you do not dispatch the worker -- the architect does that ' +
|
|
54
|
+
'with your contract. For each unit, state: the files it touches, the exact change expected, the ' +
|
|
55
|
+
'code the worker must see, and the command that decides whether the unit passed. Quote any code ' +
|
|
56
|
+
'you are changing exactly as it appears, because a patch that does not match byte-for-byte is ' +
|
|
57
|
+
'refused rather than approximated.',
|
|
58
|
+
},
|
|
59
|
+
/**
|
|
60
|
+
* The worker. `enable_thinking` and `reasoning_effort` below are ADVISORY ONLY, and are documented
|
|
61
|
+
* as such rather than removed: the plugin does transmit both on every delegation, but LM Studio's
|
|
62
|
+
* per-model setting is authoritative and ignores them — four variants of one request, including
|
|
63
|
+
* `enable_thinking: false` and `reasoning_effort: 'high'`, returned byte-identical output. See
|
|
64
|
+
* `docs/findings.md`. Do not read either as a control that works, and do not expect the worker to be
|
|
65
|
+
* as unthinking as `false` suggests.
|
|
66
|
+
*/
|
|
67
|
+
WORKER: {
|
|
68
|
+
name: 'WORKER_LOCAL',
|
|
69
|
+
provider: 'lm-studio',
|
|
70
|
+
model: 'qwen/qwen3.8-27b',
|
|
71
|
+
endpoint: 'http://127.0.0.1:1234/v1',
|
|
72
|
+
contextWindow: 32768,
|
|
73
|
+
temperature: 0.2,
|
|
74
|
+
max_tokens: 8192,
|
|
75
|
+
stop: ['<|im_end|>', '<|endoftext|>'],
|
|
76
|
+
enable_thinking: false,
|
|
77
|
+
reasoning_effort: 'none',
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
exports.WORKER_BENCHMARK_SOURCE = "measured 2026-10-06 on the author's reference rig / LM Studio, temperature 0, single stream; " +
|
|
81
|
+
'your throughput will differ with your hardware and your model';
|
|
82
|
+
exports.WORKER_BENCHMARKS = [
|
|
83
|
+
{
|
|
84
|
+
model: 'qwen/qwen3.8-27b',
|
|
85
|
+
decodeTps: 130.6,
|
|
86
|
+
ttft: '~240 ms',
|
|
87
|
+
note: 'prefill ~2400 tok/s cold, ~100 ms with a cached prompt',
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
model: 'muse-glimmer-30b',
|
|
91
|
+
decodeTps: 114.4,
|
|
92
|
+
ttft: '~19.7 s',
|
|
93
|
+
note: 'TTFT dominated by JIT model load, not prefill',
|
|
94
|
+
},
|
|
95
|
+
];
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recovery: a failed unit's failure locations become the next attempt's context.
|
|
3
|
+
*
|
|
4
|
+
* The ordinary cause of a unit that "failed for no visible reason" is that the worker was never shown the
|
|
5
|
+
* code it had to change. The architect declares `contextFiles` from its own reading; when that reading is
|
|
6
|
+
* too narrow the worker guesses, and the verdict comes back as a failure that already names the file.
|
|
7
|
+
*
|
|
8
|
+
* That location was being produced and thrown away. `RedactedFailure.location` carries `file:line` or
|
|
9
|
+
* `file:line:col`, `delegateWorker` returns it to the architect, and nothing used it.
|
|
10
|
+
*
|
|
11
|
+
* Recovering it is unusually cheap here, and that is the point: this plugin reads the file and puts it in
|
|
12
|
+
* the WORKER's prompt, while the architect receives metadata only. So the loop can widen the worker's
|
|
13
|
+
* view on a retry without widening the architect's window -- this plugin's own thesis, applied to its own
|
|
14
|
+
* failure path.
|
|
15
|
+
*
|
|
16
|
+
* Two properties the callers must keep, neither of which lives in this file:
|
|
17
|
+
*
|
|
18
|
+
* 1. **Best-effort.** Auto-injected context must never turn a delegation that would have run into a
|
|
19
|
+
* refusal. An automatic convenience that can fail a unit is worse than no convenience, so the caller
|
|
20
|
+
* keeps the declared injection unless the widened one resolves cleanly.
|
|
21
|
+
* 2. **Once.** The caller consumes the locations and clears them, so an old failure cannot silently
|
|
22
|
+
* influence every later unit in the session.
|
|
23
|
+
*/
|
|
24
|
+
/** Where a failure happened, as the redactor reported it. */
|
|
25
|
+
export interface FailureLocation {
|
|
26
|
+
path: string;
|
|
27
|
+
line: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The half-window, in lines, taken either side of a reported failure.
|
|
31
|
+
*
|
|
32
|
+
* A failure is a line, but the cause is usually a few lines up -- an import, a signature, a branch. Ten
|
|
33
|
+
* is enough to cover that and small enough that several files still fit the injection budget.
|
|
34
|
+
*/
|
|
35
|
+
export declare const RETRY_CONTEXT_WINDOW_LINES = 10;
|
|
36
|
+
/**
|
|
37
|
+
* The usable locations in a failure list. A failure without one is skipped rather than guessed at: a
|
|
38
|
+
* wrong path costs a wasted read, and a wrong line costs a misleading window.
|
|
39
|
+
*/
|
|
40
|
+
export declare function parseFailureLocations(failures: any): FailureLocation[];
|
|
41
|
+
/**
|
|
42
|
+
* One window per file, covering every failure reported in it.
|
|
43
|
+
*
|
|
44
|
+
* Per file rather than per failure, because injecting the same file twice would spend the byte budget
|
|
45
|
+
* twice for the same bytes. The window spans the earliest and latest reported lines, so two failures far
|
|
46
|
+
* apart in one file produce one request that covers both.
|
|
47
|
+
*/
|
|
48
|
+
export declare function retryContextRequests(locations: FailureLocation[], windowLines?: number): Array<{
|
|
49
|
+
path: string;
|
|
50
|
+
startLine: number;
|
|
51
|
+
endLine: number;
|
|
52
|
+
}>;
|