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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +920 -2
  3. package/cordis.patch.example.yml +48 -0
  4. package/cordis.patch.yml +13 -0
  5. package/dist/attestation.d.ts +25 -0
  6. package/dist/attestation.js +44 -0
  7. package/dist/containment.d.ts +33 -0
  8. package/dist/containment.js +63 -0
  9. package/dist/context-quality.d.ts +79 -0
  10. package/dist/context-quality.js +181 -0
  11. package/dist/context.d.ts +39 -0
  12. package/dist/context.js +136 -0
  13. package/dist/contracts.d.ts +174 -0
  14. package/dist/contracts.js +415 -0
  15. package/dist/declaration-egress-hook.d.ts +45 -0
  16. package/dist/declaration-egress-hook.js +125 -0
  17. package/dist/declaration-egress.d.ts +37 -0
  18. package/dist/declaration-egress.js +169 -0
  19. package/dist/delegation.d.ts +339 -0
  20. package/dist/delegation.js +933 -0
  21. package/dist/emission.d.ts +70 -0
  22. package/dist/emission.js +310 -0
  23. package/dist/guard.d.ts +162 -0
  24. package/dist/guard.js +873 -0
  25. package/dist/host-events.d.ts +15 -0
  26. package/dist/host-events.js +27 -0
  27. package/dist/index.d.ts +560 -0
  28. package/dist/index.js +1398 -0
  29. package/dist/local-classifier.d.ts +52 -0
  30. package/dist/local-classifier.js +159 -0
  31. package/dist/logging.d.ts +21 -0
  32. package/dist/logging.js +90 -0
  33. package/dist/paths.d.ts +10 -0
  34. package/dist/paths.js +83 -0
  35. package/dist/profiles.d.ts +35 -0
  36. package/dist/profiles.js +95 -0
  37. package/dist/retry-context.d.ts +52 -0
  38. package/dist/retry-context.js +97 -0
  39. package/dist/roles.d.ts +129 -0
  40. package/dist/roles.js +363 -0
  41. package/dist/routing.d.ts +35 -0
  42. package/dist/routing.js +88 -0
  43. package/dist/savings-tracker.d.ts +128 -0
  44. package/dist/savings-tracker.js +340 -0
  45. package/dist/session-events.d.ts +17 -0
  46. package/dist/session-events.js +31 -0
  47. package/dist/verification.d.ts +104 -0
  48. package/dist/verification.js +560 -0
  49. package/package.json +66 -4
@@ -0,0 +1,933 @@
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.MIN_SEARCH_CHARS = exports.DEFAULT_LOCAL_ENDPOINT = exports.MAX_VERIFICATION_REPEATS = exports.DEFAULT_VERIFICATION_REPEATS = exports.DELEGATE_WORKER_SCHEMA_COVERS_EVERY_PARAMETER = exports.DELEGATE_WORKER_SCHEMA = exports.DELEGATE_WORKER_OPENAI_SCHEMA = void 0;
37
+ exports.resolveVerificationRepeats = resolveVerificationRepeats;
38
+ exports.resolveChatCompletionsUrl = resolveChatCompletionsUrl;
39
+ exports.parseSearchReplaceBlocks = parseSearchReplaceBlocks;
40
+ exports.applySearchReplaceBlocks = applySearchReplaceBlocks;
41
+ exports.resolveDelegateStatus = resolveDelegateStatus;
42
+ exports.delegateWorker = delegateWorker;
43
+ exports.extractPromptText = extractPromptText;
44
+ exports.estimateTokenCount = estimateTokenCount;
45
+ const fs = __importStar(require("fs"));
46
+ const path = __importStar(require("path"));
47
+ const profiles_1 = require("./profiles");
48
+ const logging_1 = require("./logging");
49
+ const emission_1 = require("./emission");
50
+ const context_1 = require("./context");
51
+ const retry_context_1 = require("./retry-context");
52
+ const verification_1 = require("./verification");
53
+ const contracts_1 = require("./contracts");
54
+ // `scanDLP` deliberately stays in index.ts and is imported from there. Moving it here instead would
55
+ // make verification.ts import from this module while this module imports verification.ts -- a cycle
56
+ // between two leaves. A cycle through the composition root is the shape that already works.
57
+ const index_1 = require("./index");
58
+ const paths_1 = require("./paths");
59
+ /**
60
+ * Locations from the last failed unit, per workspace, waiting to be offered to the next attempt.
61
+ *
62
+ * Process-scoped and consumed once. Keyed by workspace rather than by task name because a retry is
63
+ * usually the same designer asking again in different words, and keying on the words would miss exactly
64
+ * the case this exists for.
65
+ */
66
+ const pendingRetryContext = new Map();
67
+ exports.DELEGATE_WORKER_OPENAI_SCHEMA = {
68
+ type: 'function',
69
+ function: {
70
+ name: 'delegate_worker',
71
+ description: 'Dispatches a discrete implementation, testing, or code-generation task to the configured local execution worker -- any OpenAI-compatible server (LM Studio, Ollama, vLLM, llama.cpp) -- with an isolated context window. The worker has no repository read: declare contextFiles for the code it must see, since it cannot discover anything itself.',
72
+ parameters: {
73
+ type: 'object',
74
+ properties: {
75
+ taskName: {
76
+ type: 'string',
77
+ description: 'A short descriptive identifier for the subtask',
78
+ },
79
+ instruction: {
80
+ type: 'string',
81
+ description: 'The complete technical prompt and specifications for the local worker',
82
+ },
83
+ targetFiles: {
84
+ type: 'array',
85
+ items: { type: 'string' },
86
+ description: 'The files this unit may write. ENFORCED: an emission to any path not listed here is refused and reported, because a unit that writes outside what it declared is how two units come to disagree about the same code. Declare every file the unit creates or changes, including a directory if the unit chooses the filenames within it. Omit the field to leave the unit unrestricted.',
87
+ },
88
+ runVerification: {
89
+ type: 'string',
90
+ description: 'Optional shell command to verify the output. It executes with the authority of the DSH process and requires operator approval unless verificationApproval is set to allow. It runs THREE times by default: three agreeing runs prove the contract stable, and any disagreement reports UNIT_FLAKY rather than a pass, which is what stops a non-deterministic oracle promoting broken code on a lucky run.',
91
+ },
92
+ verificationRepeats: {
93
+ type: 'number',
94
+ description: 'How many times to run runVerification. Defaults to 3 and is capped at 20. Raise it for a rare race condition. Pass 1 ONLY for a suite you have measured as deterministic and whose three-fold cost you are deliberately declining -- a contract that runs once cannot disagree with itself, so a flaky oracle becomes invisible again.',
95
+ },
96
+ attestTargets: {
97
+ type: 'array',
98
+ items: { type: 'string' },
99
+ description: 'Paths to record an OPERATOR_ATTESTED verdict for, IF this unit passes. A separate act from delegation, and refused for a unit that did not pass, because an attestation is a human claim about content and attaching one to a failed or unverified unit would launder a machine outcome into a human statement. Use it to settle a hand-edited file: the registry records the file current SHA-256, the attesting agent and the evidence, with no keys and no signature, so it is tamper-evidence rather than cryptographic non-repudiation.',
100
+ },
101
+ attestEvidence: {
102
+ type: 'string',
103
+ description: 'What was actually checked, recorded verbatim against the attestation. An attestation that does not say what it covers is an anonymous stamp, so this is worth supplying even though the field is optional.',
104
+ },
105
+ attestOperator: {
106
+ type: 'string',
107
+ description: 'The human to name for the attestation, overriding the plugin operatorIdentity. Required in practice: there is NO fallback to the calling agent id, because an autonomous session identifier cannot attest human review, and recording one would produce a LESS attributable audit entry while looking like more. If neither this nor operatorIdentity is set, the attestation is refused and the unit verdict is left intact.',
108
+ },
109
+ contractFiles: {
110
+ type: 'array',
111
+ items: { type: 'string' },
112
+ description: 'Paths to the tests that constitute this unit contract. They are hashed before the worker runs, the worker is forbidden to write them, and they are re-hashed afterwards: any change voids the verdict. The architect owns these files.',
113
+ },
114
+ contextFiles: {
115
+ type: 'array',
116
+ description: 'Existing files the worker needs to see, as { path, startLine?, endLine? }. The plugin reads them into the worker prompt; you receive a record of what was injected and never the contents. Paths outside the workspace are refused, and context carrying a credential is refused rather than transmitted.',
117
+ items: {
118
+ type: 'object',
119
+ properties: {
120
+ path: { type: 'string' },
121
+ startLine: { type: 'number' },
122
+ endLine: { type: 'number' },
123
+ },
124
+ required: ['path'],
125
+ },
126
+ },
127
+ workspaceDir: {
128
+ type: 'string',
129
+ description: 'Absolute path of the directory the worker may write into. Defaults to the session workspace; destinations outside it are refused.',
130
+ },
131
+ },
132
+ required: ['taskName', 'instruction'],
133
+ },
134
+ },
135
+ };
136
+ exports.DELEGATE_WORKER_SCHEMA = exports.DELEGATE_WORKER_OPENAI_SCHEMA;
137
+ exports.DELEGATE_WORKER_SCHEMA_COVERS_EVERY_PARAMETER = true;
138
+ /**
139
+ * How many times a contract should run, from a caller-supplied value that cannot be trusted.
140
+ *
141
+ * **The default is 3, and that is a deliberate break with the previous behaviour.** It used to be 1,
142
+ * chosen to guarantee no caller saw a change. That was backwards for an integrity boundary: it left a
143
+ * non-deterministic oracle unprotected unless the caller happened to KNOW the contract was flaky -- and
144
+ * the caller who does not know is exactly the one whose race condition gets promoted by a lucky pass.
145
+ * A guardrail that has to be opted into is not a guardrail.
146
+ *
147
+ * The cost is real and worth naming: every delegated verification now runs three subprocesses instead of
148
+ * one. A deterministic suite pays that to prove its own stability, which is not wasted -- three agreeing
149
+ * runs are strictly more evidence than one. A caller who has measured their suite and wants the speed
150
+ * back passes `verificationRepeats: 1` explicitly, which is the honest way round: the fast path is the
151
+ * one that requires a decision.
152
+ *
153
+ * The ceiling is not cosmetic. Each repeat is a real subprocess with the host process's authority, so an
154
+ * unbounded value would be a denial-of-service lever through a tool argument.
155
+ */
156
+ exports.DEFAULT_VERIFICATION_REPEATS = 3;
157
+ exports.MAX_VERIFICATION_REPEATS = 20;
158
+ function resolveVerificationRepeats(value) {
159
+ if (value === undefined || value === null)
160
+ return exports.DEFAULT_VERIFICATION_REPEATS;
161
+ if (typeof value !== 'number' || !Number.isInteger(value) || value < 1) {
162
+ return exports.DEFAULT_VERIFICATION_REPEATS;
163
+ }
164
+ return Math.min(value, exports.MAX_VERIFICATION_REPEATS);
165
+ }
166
+ /** Where the local worker is assumed to live when nothing else is configured. */
167
+ exports.DEFAULT_LOCAL_ENDPOINT = 'http://127.0.0.1:1234/v1';
168
+ /**
169
+ * Accept either a base URL or a full chat-completions URL and return the full one, so
170
+ * `localEndpoint: 'http://127.0.0.1:11434/v1'` (Ollama, vLLM, llama.cpp, …) works exactly as
171
+ * written without the operator having to know this plugin appends the path.
172
+ */
173
+ function resolveChatCompletionsUrl(base) {
174
+ const trimmed = String(base || '').trim().replace(/\/+$/, '');
175
+ if (!trimmed)
176
+ return exports.DEFAULT_LOCAL_ENDPOINT + '/chat/completions';
177
+ return /\/chat\/completions$/i.test(trimmed) ? trimmed : trimmed + '/chat/completions';
178
+ }
179
+ /**
180
+ * A one- or two-character search is unique by accident rather than by intent, so it is refused even
181
+ * when the exactly-once rule would allow it. The property that matters is exactness, not cleverness.
182
+ */
183
+ exports.MIN_SEARCH_CHARS = 8;
184
+ const SEARCH_MARKER = '<<<<<<< SEARCH';
185
+ const REPLACE_MARKER = '>>>>>>> REPLACE';
186
+ const DIVIDER_MARKER = '=======';
187
+ /**
188
+ * Recognise a search/replace body. The fenced header is shared with whole-file emission, so the body
189
+ * decides the mode and the worker does not have to know which of the two it is producing.
190
+ *
191
+ * Returns null when there is no delta here, which tells the caller to treat the body as a whole file.
192
+ * A body that *starts* a search/replace block but never finishes it returns an empty list instead --
193
+ * never null -- so a malformed patch cannot fall through and be written over a real file.
194
+ */
195
+ function parseSearchReplaceBlocks(body) {
196
+ if (typeof body !== 'string' || !body.includes(SEARCH_MARKER))
197
+ return null;
198
+ const blocks = [];
199
+ let search = null;
200
+ let replace = null;
201
+ let state = 'idle';
202
+ for (const line of body.split(/\r?\n/)) {
203
+ const trimmed = line.trim();
204
+ if (trimmed === SEARCH_MARKER) {
205
+ search = [];
206
+ replace = [];
207
+ state = 'search';
208
+ continue;
209
+ }
210
+ if (state === 'search' && trimmed === DIVIDER_MARKER) {
211
+ state = 'replace';
212
+ continue;
213
+ }
214
+ if (trimmed === REPLACE_MARKER) {
215
+ if (search && replace)
216
+ blocks.push({ search: search.join('\n'), replace: replace.join('\n') });
217
+ search = null;
218
+ replace = null;
219
+ state = 'idle';
220
+ continue;
221
+ }
222
+ if (state === 'search' && search)
223
+ search.push(line);
224
+ else if (state === 'replace' && replace)
225
+ replace.push(line);
226
+ }
227
+ return blocks;
228
+ }
229
+ /**
230
+ * Apply every block, or none. A partially applied change is worse than no change: it leaves the tree
231
+ * in a state that no contract was written against.
232
+ *
233
+ * Everything is normalised to LF for matching and the file's own ending is restored at the end.
234
+ * Nothing else is normalised -- indentation is bytes -- because a near miss must fail loudly rather
235
+ * than be massaged into a match. Fuzzy patching is not a tuning choice here; it is the mechanism by
236
+ * which a wrong edit lands silently.
237
+ */
238
+ function applySearchReplaceBlocks(content, blocks) {
239
+ if (!Array.isArray(blocks) || blocks.length === 0) {
240
+ return { ok: false, reason: 'the patch contained no complete search/replace block' };
241
+ }
242
+ const usesCrlf = content.includes('\r\n');
243
+ let work = content.split('\r\n').join('\n');
244
+ for (const block of blocks) {
245
+ const search = String(block?.search ?? '').split('\r\n').join('\n');
246
+ const replace = String(block?.replace ?? '').split('\r\n').join('\n');
247
+ if (!search.trim()) {
248
+ return { ok: false, reason: 'a search block was empty' };
249
+ }
250
+ if (search.trim().length < exports.MIN_SEARCH_CHARS) {
251
+ return {
252
+ ok: false,
253
+ reason: 'a search block was too short to be unambiguous (' +
254
+ search.trim().length +
255
+ ' characters, minimum ' +
256
+ exports.MIN_SEARCH_CHARS +
257
+ ')',
258
+ };
259
+ }
260
+ const occurrences = work.split(search).length - 1;
261
+ if (occurrences === 0) {
262
+ return {
263
+ ok: false,
264
+ reason: 'no exact match for a search block (' + search.trim().slice(0, 60) + ')',
265
+ };
266
+ }
267
+ if (occurrences > 1) {
268
+ return {
269
+ ok: false,
270
+ reason: 'a search block matched more than once (' +
271
+ occurrences +
272
+ ' times), so the edit is ambiguous',
273
+ };
274
+ }
275
+ work = work.replace(search, () => replace);
276
+ }
277
+ return { ok: true, content: usesCrlf ? work.split('\n').join('\r\n') : work };
278
+ }
279
+ /**
280
+ * Status precedence, as a pure function so the ordering is testable without a server. Tampering
281
+ * outranks everything: a modified contract voids the run even when verification passed, because
282
+ * what passed was no longer the contract.
283
+ */
284
+ function resolveDelegateStatus(input) {
285
+ // Tampering voids the run whatever else happened: what passed was no longer the contract.
286
+ if (input.contractViolations.length > 0)
287
+ return 'CONTRACT_MODIFIED';
288
+ // Nothing ran, so the outcome is unknown rather than a pass.
289
+ if (input.verificationGate)
290
+ return 'VERIFICATION_NOT_APPROVED';
291
+ // A contract that cannot agree with itself has not established anything about the content, so it
292
+ // outranks both the unit's own result and the project check. This is the clause that stops a
293
+ // non-deterministic oracle from being read as a pass on the run where the defect did not trigger.
294
+ if (input.flaky)
295
+ return 'FLAKY';
296
+ // The project check is evaluated BEFORE `unverified`, and that order is the point. `unverified` is an
297
+ // absence of evidence -- no command was supplied for the unit. A failed project check is positive
298
+ // evidence: something was proven, and what was proven is that the tree is broken. Reporting
299
+ // "unverified" for a unit that demonstrably broke the build would understate what is known.
300
+ if (input.coherenceFailed) {
301
+ return input.isSuccess ? 'INCOHERENT' : 'VERIFICATION_FAILED';
302
+ }
303
+ if (input.unverified)
304
+ return 'UNVERIFIED';
305
+ return input.isSuccess ? 'SUCCESS' : 'VERIFICATION_FAILED';
306
+ }
307
+ async function delegateWorker(params = {}, tracker) {
308
+ const endpoint = resolveChatCompletionsUrl(params.endpoint || profiles_1.PROFILES.WORKER.endpoint || exports.DEFAULT_LOCAL_ENDPOINT);
309
+ const model = params.model || profiles_1.PROFILES.WORKER.model;
310
+ // The fence is built with `\x60` escapes rather than written literally. A literal triple backtick in
311
+ // this file makes it unpatchable by a confined agent, because the emission scanner truncates a fenced
312
+ // body at the first backtick run inside it. The instruction the worker receives is identical.
313
+ const FENCE = '\x60\x60\x60';
314
+ const fileInstruction = 'To change part of an existing file, emit a patch block instead of the whole file:\n' +
315
+ FENCE +
316
+ 'patch file="src/thing.ts"\n<<<<<<< SEARCH\n<the exact existing lines>\n=======\n<the replacement lines>\n>>>>>>> REPLACE\n' +
317
+ FENCE +
318
+ '\n' +
319
+ 'The SEARCH text must match the file exactly and occur exactly once, and there is no fuzzy matching.\n' +
320
+ 'To create a file, or replace one wholesale, wrap it in a code block with the target file path in the header or first line, e.g. ' +
321
+ FENCE +
322
+ 'typescript file="src/math-helper.ts"\n...code...\n' +
323
+ FENCE +
324
+ ' or // FILE: tests/math-helper.test.ts';
325
+ const systemPrompt = params.systemPrompt ||
326
+ 'You are a fast, accurate local coding worker executing a discrete task. ' + fileInstruction;
327
+ const taskText = params.instruction || params.taskPrompt || params.prompt || '';
328
+ // Resolved before the worker runs: the contract hashes have to describe the tree as it was handed
329
+ // over, and context has to be read while the architect is still blind to it.
330
+ const workspaceBase = params.workspaceDir || process.cwd();
331
+ // Recovery from the previous failure in this workspace. Its locations become context for this attempt,
332
+ // once: an old failure must not quietly influence every later unit in the session, so the pending set
333
+ // is consumed here whether or not it turns out to be usable.
334
+ const pendingRetry = params.retryContext === 'off' ? [] : pendingRetryContext.get(workspaceBase) ?? [];
335
+ if (pendingRetry.length > 0)
336
+ pendingRetryContext.delete(workspaceBase);
337
+ const declaredContext = Array.isArray(params.contextFiles) ? params.contextFiles : [];
338
+ let context = (0, context_1.resolveContextFiles)(declaredContext, workspaceBase);
339
+ let retryInjected = [];
340
+ if (pendingRetry.length > 0 && context.errors.length === 0) {
341
+ // Compared as resolved paths, not as the strings the architect typed. A failure reports its location
342
+ // relative to the workspace its command ran in, so a raw string comparison matches only when the
343
+ // architect happened to spell the path exactly as the failing tool did.
344
+ const locate = (p) => (0, paths_1.canonicalisePath)(path.isAbsolute(p) ? p : path.resolve(workspaceBase, p));
345
+ const declaredPaths = new Set(context.injected.map((c) => (0, paths_1.canonicalisePath)(c.path)));
346
+ // A contract file is not context. `contractFiles` exists so the implementer cannot see or edit what
347
+ // judges it -- and a failing contract names itself, so the locations fed back here point straight at
348
+ // it. Resolved independently of `contractPaths` further down because the retry set is decided before
349
+ // that is computed. The integrity check does not cover this case: it protects the contract from being
350
+ // WRITTEN, not from being shown.
351
+ const protectedPaths = new Set((0, contracts_1.resolveContractFiles)(params.contractFiles, workspaceBase).map((p) => (0, paths_1.canonicalisePath)(p)));
352
+ const additions = (0, retry_context_1.retryContextRequests)(pendingRetry).filter((r) => !declaredPaths.has(locate(r.path)) && !protectedPaths.has(locate(r.path)));
353
+ if (additions.length > 0) {
354
+ // Best-effort, and deliberately so. If widening the injection would push it over its byte budget,
355
+ // resolveContextFiles refuses the WHOLE injection and this unit dies for a reason the architect
356
+ // never asked for. So the widened set is used only when it resolves cleanly; otherwise the declared
357
+ // set stands, exactly as it would have without this feature.
358
+ const widened = (0, context_1.resolveContextFiles)([...declaredContext, ...additions], workspaceBase);
359
+ if (widened.errors.length === 0) {
360
+ context = widened;
361
+ retryInjected = additions;
362
+ }
363
+ }
364
+ }
365
+ // Covers the auto-injected set as well as the declared one, so a credential in a file that was pulled
366
+ // in automatically is refused on the same terms as one the architect named.
367
+ if (declaredContext.length > 0 || context.injected.length > 0) {
368
+ if (context.errors.length > 0) {
369
+ return {
370
+ success: false,
371
+ status: 'CONTEXT_REFUSED',
372
+ message: 'Context injection refused:\n' + context.errors.map((e) => ' - ' + e).join('\n'),
373
+ contextErrors: context.errors,
374
+ resolvedWorkspace: workspaceBase,
375
+ filesWritten: [],
376
+ testResults: { passed: 0, failed: 0, output: 'The worker was not called.' },
377
+ tokens: { prompt: 0, completion: 0 },
378
+ };
379
+ }
380
+ // The declared context is about to travel to `endpoint`, which may be a vLLM port on another
381
+ // machine rather than this one. A "local" endpoint that is remote is a cloud, so a credential in
382
+ // the context is refused rather than transmitted: rule 1 does not care which port it is.
383
+ const contextDlp = (0, index_1.scanDLP)(context.text);
384
+ if (contextDlp.hasSensitiveData && contextDlp.highConfidence) {
385
+ const refusal = 'declared context carries a credential (' +
386
+ contextDlp.violations.join(', ') +
387
+ '), so it will not ' +
388
+ 'be sent to the worker endpoint. Narrow the range to exclude it, or remove it from the file.';
389
+ return {
390
+ success: false,
391
+ status: 'CONTEXT_REFUSED',
392
+ message: 'Context injection refused:\n - ' + refusal,
393
+ contextErrors: [refusal],
394
+ resolvedWorkspace: workspaceBase,
395
+ filesWritten: [],
396
+ testResults: { passed: 0, failed: 0, output: 'The worker was not called.' },
397
+ tokens: { prompt: 0, completion: 0 },
398
+ };
399
+ }
400
+ }
401
+ let fileContextText = '';
402
+ if (Array.isArray(params.targetFiles)) {
403
+ fileContextText = 'Target Files:\n' + params.targetFiles.join('\n');
404
+ }
405
+ else if (typeof params.targetFiles === 'string') {
406
+ fileContextText = 'Target Files:\n' + params.targetFiles;
407
+ }
408
+ else if (typeof params.fileContext === 'string') {
409
+ fileContextText = params.fileContext;
410
+ }
411
+ // Appended rather than chosen by an else-if. A unit normally has both targetFiles and context, and
412
+ // the earlier shape meant a declared context was silently dropped whenever targetFiles was present.
413
+ if (context.text) {
414
+ fileContextText += (fileContextText ? '\n\n' : '') + context.text;
415
+ }
416
+ if (params.runVerification) {
417
+ fileContextText += '\nVerification Command:\n' + params.runVerification;
418
+ }
419
+ const combinedPrompt = fileContextText
420
+ ? 'Task: ' + (params.taskName || 'Subtask') + '\n' + taskText + '\n\n' + fileContextText
421
+ : 'Task: ' + (params.taskName || 'Subtask') + '\n' + taskText;
422
+ const turnId = params.turnId ?? Math.floor(Math.random() * 1000000);
423
+ const timeoutMs = params.timeoutMs ?? 300000;
424
+ const contractPaths = (0, contracts_1.resolveContractFiles)(params.contractFiles, workspaceBase);
425
+ const contractBefore = (0, contracts_1.contractFileHashes)(contractPaths);
426
+ const requestStartedAt = Date.now();
427
+ try {
428
+ const controller = new AbortController();
429
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
430
+ const response = await fetch(endpoint, {
431
+ method: 'POST',
432
+ headers: { 'Content-Type': 'application/json' },
433
+ body: JSON.stringify({
434
+ model,
435
+ messages: [
436
+ { role: 'system', content: systemPrompt },
437
+ { role: 'user', content: combinedPrompt },
438
+ ],
439
+ temperature: profiles_1.PROFILES.WORKER.temperature ?? 0.2,
440
+ max_tokens: profiles_1.PROFILES.WORKER.max_tokens ?? 2048,
441
+ stop: profiles_1.PROFILES.WORKER.stop ?? ['<|im_' + 'end|>', '<|endof' + 'text|>'],
442
+ enable_thinking: profiles_1.PROFILES.WORKER.enable_thinking ?? false,
443
+ reasoning_effort: profiles_1.PROFILES.WORKER.reasoning_effort ?? 'none',
444
+ }),
445
+ signal: controller.signal,
446
+ }).finally(() => clearTimeout(timeoutId));
447
+ if (!response.ok) {
448
+ const errText = await response.text();
449
+ return {
450
+ success: false,
451
+ status: 'ERROR',
452
+ message: 'LM Studio returned HTTP ' + response.status + ': ' + errText,
453
+ filesWritten: [],
454
+ testResults: { passed: 0, failed: 0, output: 'Request failed.' },
455
+ tokens: { prompt: 0, completion: 0 },
456
+ };
457
+ }
458
+ const elapsedMs = Date.now() - requestStartedAt;
459
+ const data = await response.json();
460
+ const content = data.choices?.[0]?.message?.content || '';
461
+ const promptTokens = data.usage?.prompt_tokens ?? estimateTokenCount(combinedPrompt);
462
+ const completionTokens = data.usage?.completion_tokens ?? estimateTokenCount(content);
463
+ const totalTokens = data.usage?.total_tokens ?? promptTokens + completionTokens;
464
+ if (!fs.existsSync(workspaceBase)) {
465
+ try {
466
+ fs.mkdirSync(workspaceBase, { recursive: true });
467
+ }
468
+ catch (err) {
469
+ return {
470
+ success: false,
471
+ status: 'ERROR',
472
+ message: "Resolved workspace directory '" +
473
+ workspaceBase +
474
+ "' does not exist and could not be created: " +
475
+ (err?.message || String(err)),
476
+ resolvedWorkspace: workspaceBase,
477
+ filesWritten: [],
478
+ testResults: { passed: 0, failed: 0, output: 'No files written.' },
479
+ tokens: { prompt: promptTokens, completion: completionTokens },
480
+ };
481
+ }
482
+ }
483
+ const emission = (0, emission_1.extractAndEmitFiles)(content, params.targetFiles, workspaceBase, params.emitAllowlist ?? [], contractPaths, { enforceUnitScope: params.unitScope !== 'off' });
484
+ const filesWritten = emission.filesWritten;
485
+ // Remember what we wrote on the architect's behalf, so reading it back can be gated -- distinguishing
486
+ // files the worker created, which the architect never saw, from files it patched, which it did.
487
+ const createdPaths = filesWritten.filter((f) => f.mode !== 'patch').map((f) => f.path);
488
+ const patchedPaths = filesWritten.filter((f) => f.mode === 'patch').map((f) => f.path);
489
+ if (createdPaths.length > 0)
490
+ (0, contracts_1.rememberDelegated)(createdPaths, 'created');
491
+ if (patchedPaths.length > 0)
492
+ (0, contracts_1.rememberDelegated)(patchedPaths, 'patched');
493
+ const policy = params.verificationPolicy ?? verification_1.DEFAULT_VERIFICATION_POLICY;
494
+ let testResults = undefined;
495
+ let verificationGate = undefined;
496
+ // How many times the contract runs, and how many it actually took. The second is reported so a reader
497
+ // can tell a full consensus pass from one that short-circuited, which is the difference between
498
+ // "agreeing three times" and "failing immediately" -- the same verdict, different amounts of evidence.
499
+ const verificationRepeats = resolveVerificationRepeats(params.verificationRepeats);
500
+ let verificationAttempts = 0;
501
+ // Set when repeated runs of the same contract disagreed with each other.
502
+ let flaky = false;
503
+ if (params.runVerification) {
504
+ const decision = (0, verification_1.evaluateVerificationPolicy)(params.runVerification, policy);
505
+ let permitted = decision.kind === 'allow';
506
+ if (decision.kind === 'ask') {
507
+ // No approver means no consent. A missing approval seam must never degrade to a
508
+ // silent yes for a command that runs with the host process's authority.
509
+ permitted = params.verificationApproval
510
+ ? await params.verificationApproval(params.runVerification)
511
+ : false;
512
+ }
513
+ if (permitted) {
514
+ // A non-deterministic oracle -- a property test, a concurrency harness -- can pass a broken
515
+ // implementation on the run where the defect did not trigger, and a pass is what settles a file.
516
+ // Repeating the contract and requiring the runs to AGREE is what turns that from a silent false
517
+ // green into a verdict, without the plugin needing to know which oracles are deterministic.
518
+ //
519
+ // The loop short-circuits ONLY on disagreement, because disagreement is the one outcome that
520
+ // cannot be changed by running more. Consensus cannot be established early: a contract that
521
+ // agreed twice and flipped on the third run is flaky, so both PASS and FAIL have to spend the
522
+ // whole budget to mean what they say.
523
+ //
524
+ // Exiting early on a first-run failure was the first version of this loop, and it was a bug in
525
+ // the direction that matters. A race that happened to lose on runs 1 and 2 would be reported as
526
+ // UNIT_FAILED -- a deterministic regression -- which is exactly the non-determinism this state
527
+ // exists to expose. `FAIL` here means "failed every time we looked", not "failed once".
528
+ //
529
+ // The disagreement check is against the FIRST run rather than the previous one, so PASS-then-FAIL
530
+ // and FAIL-then-PASS both land on FLAKY: two runs, one disagreement, and the same truthful label.
531
+ const repeats = verificationRepeats;
532
+ const first = (0, verification_1.runSandboxVerification)(params.runVerification, workspaceBase, {
533
+ redact: params.redactVerification ?? process.env.DSH_LOCAL_ROUTER_RAW_VERIFICATION !== '1',
534
+ rawLogPath: path.join((0, logging_1.resolveDataDir)(), 'last-verification.log'),
535
+ allowInProcessFallback: policy.allowInProcessFallback,
536
+ timeoutMs: policy.timeoutMs,
537
+ });
538
+ testResults = first;
539
+ const firstPassed = first.failed === 0;
540
+ let attempts = 1;
541
+ for (let attempt = 1; attempt < repeats; attempt++) {
542
+ const next = (0, verification_1.runSandboxVerification)(params.runVerification, workspaceBase, {
543
+ redact: params.redactVerification ?? process.env.DSH_LOCAL_ROUTER_RAW_VERIFICATION !== '1',
544
+ rawLogPath: path.join((0, logging_1.resolveDataDir)(), 'last-verification.log'),
545
+ allowInProcessFallback: policy.allowInProcessFallback,
546
+ timeoutMs: policy.timeoutMs,
547
+ });
548
+ attempts++;
549
+ if ((next.failed === 0) !== firstPassed) {
550
+ flaky = true;
551
+ break;
552
+ }
553
+ }
554
+ verificationAttempts = attempts;
555
+ }
556
+ else {
557
+ verificationGate =
558
+ decision.kind === 'deny'
559
+ ? decision.reason
560
+ : 'approval was not granted (' + decision.reason + ')';
561
+ }
562
+ }
563
+ // A result is a verdict on a contract, and the contract is the verification command. Code
564
+ // produced without one has an unchecked contract: reporting SUCCESS there would be the same
565
+ // false green as counting unrecognised test output as a pass, which this plugin has already
566
+ // been caught doing twice.
567
+ const wroteFiles = filesWritten.length > 0 && emission.errors.length === 0;
568
+ const unverified = !verificationGate && !params.runVerification && wroteFiles;
569
+ // The project's own check, which is the only thing here that spans units.
570
+ //
571
+ // Skipped in two cases, both deliberate. A unit that wrote no files cannot have broken coherence, so
572
+ // a delegation that only answered a question does not pay for a build. And `mode: 'deny'` means no
573
+ // verification command is executed, which this is -- consistent rather than an exception to it.
574
+ let coherenceResults = undefined;
575
+ const coherenceCommand = policy.coherenceVerification;
576
+ if (coherenceCommand && wroteFiles && policy.mode !== 'deny') {
577
+ coherenceResults = (0, verification_1.runSandboxVerification)(coherenceCommand, workspaceBase, {
578
+ redact: params.redactVerification ?? process.env.DSH_LOCAL_ROUTER_RAW_VERIFICATION !== '1',
579
+ // A separate raw log, so a failing project check cannot overwrite the unit's own evidence.
580
+ rawLogPath: path.join((0, logging_1.resolveDataDir)(), 'last-coherence.log'),
581
+ timeoutMs: policy.timeoutMs,
582
+ });
583
+ }
584
+ // A spawn the sandbox refused reports failed > 0, so a check that could not run is a failure here
585
+ // rather than a silent pass -- which is the direction this has to fail in.
586
+ const coherenceFailed = Boolean(coherenceResults && coherenceResults.failed > 0);
587
+ // Re-hash once the worker has finished and verification has run. A violation voids the verdict
588
+ // regardless of what the tests reported, because the tests are no longer the contract.
589
+ const contractAfter = (0, contracts_1.contractFileHashes)(contractPaths);
590
+ const contractViolationsFound = (0, contracts_1.contractViolations)(contractBefore, contractAfter);
591
+ // The unit's own verdict, before the project is considered -- kept separate on purpose.
592
+ // `resolveDelegateStatus` needs this one rather than the combined one, because INCOHERENT means
593
+ // exactly "the unit passed and the project did not", so the status function has to be told which of
594
+ // the two failed. Folding coherence in here first made every broken tree report VERIFICATION_FAILED,
595
+ // which points the architect at the unit when the unit is fine.
596
+ const unitSuccess = contractViolationsFound.length === 0 &&
597
+ !verificationGate &&
598
+ !unverified &&
599
+ // A contract that contradicted itself cannot be a success, whichever way the disagreement fell.
600
+ // `testResults` is the first run, so without this clause a flaky oracle that happened to pass on
601
+ // attempt one would read as a pass -- which is precisely the false green this state exists for.
602
+ !flaky &&
603
+ (!testResults || testResults.failed === 0) &&
604
+ emission.errors.length === 0;
605
+ // What the caller is told: both the unit and the project have to hold.
606
+ const isSuccess = unitSuccess && !coherenceFailed;
607
+ let summaryText = '';
608
+ if (filesWritten.length > 0) {
609
+ summaryText =
610
+ "Task '" +
611
+ (params.taskName || 'Subtask') +
612
+ "' completed. Wrote " +
613
+ filesWritten.length +
614
+ ' file(s):\n' +
615
+ filesWritten
616
+ .map((f) => ' - ' +
617
+ f.path +
618
+ ' (' +
619
+ f.lines +
620
+ ' lines, ' +
621
+ f.bytes +
622
+ ' bytes' +
623
+ (f.mode === 'patch' ? ', patched in place with ' + f.hunks + ' hunk(s)' : '') +
624
+ ')')
625
+ .join('\n');
626
+ }
627
+ else {
628
+ summaryText =
629
+ "Task '" +
630
+ (params.taskName || 'Subtask') +
631
+ "' completed. Worker returned " +
632
+ content.split('\n').length +
633
+ ' line(s) of output.';
634
+ }
635
+ summaryText +=
636
+ '\nWorkspace: ' +
637
+ workspaceBase +
638
+ (params.workspaceSource ? ' (resolved via ' + params.workspaceSource + ')' : '');
639
+ if (params.workspaceSource && /FALLBACK/.test(params.workspaceSource) && filesWritten.length > 0) {
640
+ summaryText +=
641
+ '\nWARNING: the Session workspace could not be resolved, so files were written relative to ' +
642
+ workspaceBase +
643
+ '. Pass absolute paths in targetFiles, or set DSH_WORKSPACE_ROOT, to be certain of the destination.';
644
+ }
645
+ if (emission.errors.length > 0) {
646
+ summaryText +=
647
+ '\nFILE WRITE ERRORS:\n' +
648
+ emission.errors.map((e) => ' - ' + e).join('\n');
649
+ }
650
+ if (verificationGate) {
651
+ summaryText += '\nVerification was NOT run: ' + verificationGate;
652
+ }
653
+ if (unverified) {
654
+ summaryText +=
655
+ '\nUNVERIFIED: no verification command was supplied, so the contract was never checked. ' +
656
+ 'Files were written; nothing was proven.';
657
+ }
658
+ if (testResults) {
659
+ summaryText +=
660
+ '\nVerification Results: Passed ' +
661
+ testResults.passed +
662
+ ', Failed ' +
663
+ testResults.failed +
664
+ '.';
665
+ if (testResults.errorSummary) {
666
+ summaryText += '\nFailures: ' + testResults.errorSummary;
667
+ }
668
+ }
669
+ if (coherenceResults) {
670
+ summaryText +=
671
+ '\nCoherence check: Passed ' +
672
+ coherenceResults.passed +
673
+ ', Failed ' +
674
+ coherenceResults.failed +
675
+ '.';
676
+ if (coherenceFailed) {
677
+ summaryText +=
678
+ '\nINCOHERENT: this unit passed its own contract, but the project check failed. The tree is ' +
679
+ 'broken by this unit even though the tests written for it pass, so the verdict is void.';
680
+ }
681
+ if (coherenceResults.errorSummary) {
682
+ summaryText += '\nCoherence failures: ' + coherenceResults.errorSummary;
683
+ }
684
+ }
685
+ if (contractPaths.length > 0) {
686
+ summaryText +=
687
+ contractViolationsFound.length > 0
688
+ ? '\nCONTRACT MODIFIED, verdict void: ' + contractViolationsFound.join('; ') + '.'
689
+ : '\nContract: ' + contractPaths.length + ' declared file(s), unchanged.';
690
+ }
691
+ if (retryInjected.length > 0) {
692
+ summaryText +=
693
+ '\nContext widened with the previous failure at ' +
694
+ retryInjected.map((c) => c.path).join(', ') +
695
+ '. The worker was shown code the architect did not name.';
696
+ }
697
+ // What the next attempt in this workspace should be shown. Stored only for a failure, and only from
698
+ // locations the failure actually carried: a unit that passed must not seed a retry, and a failure with
699
+ // no usable location clears the set rather than leaving a stale one to resurface later.
700
+ if (unitSuccess) {
701
+ pendingRetryContext.delete(workspaceBase);
702
+ }
703
+ else {
704
+ const failureLocations = (0, retry_context_1.parseFailureLocations)(testResults?.failures);
705
+ if (failureLocations.length > 0)
706
+ pendingRetryContext.set(workspaceBase, failureLocations);
707
+ else
708
+ pendingRetryContext.delete(workspaceBase);
709
+ }
710
+ // Three outcomes, not two. An unverified unit is deliberately not a success in this plugin, but it
711
+ // is not a failure either -- and counting it as one would inflate the very figure this field exists
712
+ // to produce, because an edit delegated without a verification command is the common case.
713
+ //
714
+ // `neverRan` is the second way a contract goes unchecked, and it was missing from this expression
715
+ // long enough to be found by running a real delegation rather than by reading it. The first way is
716
+ // `unverified` -- no command was supplied. The second is `verificationGate` -- a command WAS supplied
717
+ // and the gate stopped it, typically because approval was not granted. Both mean the contract was
718
+ // never checked, which is what `resolveDelegateStatus` has always said at its own guard clause, so
719
+ // this asks the same question it asks rather than inventing a second answer to it.
720
+ //
721
+ // Collapsing the gate case into UNIT_FAILED was not a cosmetic mislabel. `UNIT_FAILED` is what
722
+ // `evaluateSettledFile` renders as "the unit that wrote it failed verification", so a file whose
723
+ // tests passed but which was never allowed to run them was reported to the architect as having
724
+ // failed -- a claim about content that nothing had established either way.
725
+ const neverRan = Boolean(verificationGate) || unverified;
726
+ // Four outcomes, and FLAKY is checked before PASSED for the reason it exists: a contract that
727
+ // disagreed with itself has not established anything about the content, and the run it agreed with
728
+ // itself on is exactly the run that could have passed a broken implementation. Reporting either
729
+ // UNIT_PASSED or UNIT_FAILED here would pick one arbitrary run and present it as the verdict.
730
+ const unitOutcome = flaky
731
+ ? 'UNIT_FLAKY'
732
+ : unitSuccess
733
+ ? 'UNIT_PASSED'
734
+ : neverRan
735
+ ? 'UNIT_UNVERIFIED'
736
+ : 'UNIT_FAILED';
737
+ // Recorded here rather than when the model answered, because the OUTCOME is the point. A unit that
738
+ // failed is a failed attempt, and without that the ledger cannot say how much churn the architect
739
+ // never had to see. `unitSuccess` is the unit's own verdict, matching what `resolveDelegateStatus`
740
+ // is given, so a coherence failure does not read as a failed attempt by this unit.
741
+ if (tracker) {
742
+ tracker.recordUsage({
743
+ turn: turnId,
744
+ route: 'WORKER_LOCAL',
745
+ model,
746
+ reason: 'SUBAGENT_DELEGATION (' + (params.taskName || 'subtask') + ')',
747
+ promptTokens,
748
+ completionTokens,
749
+ totalTokens,
750
+ elapsedMs,
751
+ outcome: unitOutcome,
752
+ succeeded: unitSuccess,
753
+ bytesWritten: filesWritten.reduce((sum, f) => sum + (f.bytes || 0), 0),
754
+ });
755
+ }
756
+ // The registry was written before the verification ran, so the record it holds cannot carry the
757
+ // verdict. This is the second half of that write, and the only thing that joins a per-file sha256 to
758
+ // a per-unit verdict. One expression feeds both records deliberately: the ledger and the registry
759
+ // must not be able to disagree about what happened.
760
+ if (filesWritten.length > 0) {
761
+ (0, contracts_1.recordDelegatedOutcome)(filesWritten.map((f) => f.path), unitOutcome, unitSuccess);
762
+ }
763
+ return {
764
+ success: isSuccess,
765
+ filesWritten: filesWritten.map((f) => f.path),
766
+ filesWrittenRelative: filesWritten.map((f) => f.relativeName || f.path),
767
+ resolvedWorkspace: workspaceBase,
768
+ workspaceSource: params.workspaceSource,
769
+ testResults: testResults ||
770
+ {
771
+ passed: 0,
772
+ failed: 0,
773
+ output: verificationGate
774
+ ? 'Verification not run: ' + verificationGate
775
+ : unverified
776
+ ? 'No verification command was supplied; the contract is unchecked.'
777
+ : 'No verification requested.',
778
+ },
779
+ ...(verificationGate ? { verificationSkipped: verificationGate } : {}),
780
+ // Reported so a consensus pass can be told from a short-circuit: the same verdict, different
781
+ // amounts of evidence behind it.
782
+ ...(verificationAttempts > 0
783
+ ? { verificationRuns: verificationAttempts, verificationRepeats }
784
+ : {}),
785
+ // Metadata only. The architect learns what the worker was shown, never what it says.
786
+ ...(context.injected.length > 0
787
+ ? {
788
+ contextInjected: context.injected.map((c) => ({
789
+ path: c.path,
790
+ relativeName: c.relativeName,
791
+ lineRange: c.lineRange,
792
+ lines: c.lines,
793
+ bytes: c.bytes,
794
+ sha256: c.sha256,
795
+ })),
796
+ }
797
+ : {}),
798
+ // Metadata only. The architect learns what the contract did, never what the code says.
799
+ ...(contractPaths.length > 0
800
+ ? {
801
+ contractFiles: contractPaths.map((p) => ({
802
+ path: p,
803
+ relativeName: path.relative(workspaceBase, p) || p,
804
+ sha256: contractAfter[(0, paths_1.canonicalisePath)(p)] ?? null,
805
+ unchanged: contractBefore[(0, paths_1.canonicalisePath)(p)] === contractAfter[(0, paths_1.canonicalisePath)(p)],
806
+ })),
807
+ contractViolations: contractViolationsFound,
808
+ }
809
+ : {}),
810
+ tokens: {
811
+ prompt: promptTokens,
812
+ completion: completionTokens,
813
+ },
814
+ ...(coherenceResults ? { coherenceResults } : {}),
815
+ summary: summaryText,
816
+ status: resolveDelegateStatus({
817
+ verificationGate,
818
+ unverified,
819
+ contractViolations: contractViolationsFound,
820
+ isSuccess: unitSuccess,
821
+ coherenceFailed,
822
+ flaky,
823
+ }),
824
+ taskName: params.taskName || 'Subtask',
825
+ tokensUsed: totalTokens,
826
+ };
827
+ }
828
+ catch (err) {
829
+ const errMsg = err?.message || String(err);
830
+ return {
831
+ success: false,
832
+ status: 'ERROR',
833
+ message: 'LM Studio at 127.0.0.1:1234 was unreachable or failed: ' + errMsg,
834
+ filesWritten: [],
835
+ testResults: { passed: 0, failed: 0, output: errMsg },
836
+ tokens: { prompt: 0, completion: 0 },
837
+ };
838
+ }
839
+ }
840
+ function extractPromptText(session) {
841
+ if (!session)
842
+ return '';
843
+ const messages = session.messages ||
844
+ session.options?.messages ||
845
+ session.requestOptions?.messages ||
846
+ session.session?.messages;
847
+ if (Array.isArray(messages) && messages.length > 0) {
848
+ for (let i = messages.length - 1; i >= 0; i--) {
849
+ const msg = messages[i];
850
+ if (msg?.role === 'user') {
851
+ if (typeof msg.content === 'string') {
852
+ const text = msg.content.trim();
853
+ if (text.startsWith('[model changed:') || text.startsWith('Current runtime context.')) {
854
+ continue;
855
+ }
856
+ if (text.length > 0)
857
+ return text;
858
+ }
859
+ if (Array.isArray(msg.content)) {
860
+ const textPart = msg.content.find((p) => p.type === 'text');
861
+ if (textPart?.text?.trim()) {
862
+ const text = textPart.text.trim();
863
+ if (!text.startsWith('[model changed:') && !text.startsWith('Current runtime context.')) {
864
+ return text;
865
+ }
866
+ }
867
+ }
868
+ }
869
+ }
870
+ for (let i = messages.length - 1; i >= 0; i--) {
871
+ const content = messages[i]?.content;
872
+ if (typeof content === 'string') {
873
+ const text = content.trim();
874
+ if (text.startsWith('[model changed:') || text.startsWith('Current runtime context.')) {
875
+ continue;
876
+ }
877
+ if (text.length > 0)
878
+ return text;
879
+ }
880
+ }
881
+ }
882
+ try {
883
+ const inbox = session.inbox || session.session?.inbox;
884
+ if (inbox && Array.isArray(inbox['next-turn']) && inbox['next-turn'].length > 0) {
885
+ const item = inbox['next-turn'][inbox['next-turn'].length - 1];
886
+ if (typeof item?.prompt === 'string') {
887
+ const text = item.prompt.trim();
888
+ if (!text.startsWith('[model changed:') && !text.startsWith('Current runtime context.')) {
889
+ if (text.length > 0)
890
+ return text;
891
+ }
892
+ }
893
+ if (typeof item?.content === 'string') {
894
+ const text = item.content.trim();
895
+ if (!text.startsWith('[model changed:') && !text.startsWith('Current runtime context.')) {
896
+ if (text.length > 0)
897
+ return text;
898
+ }
899
+ }
900
+ }
901
+ }
902
+ catch (e) { }
903
+ if (typeof session.input === 'string') {
904
+ const text = session.input.trim();
905
+ if (!text.startsWith('[model changed:') && !text.startsWith('Current runtime context.')) {
906
+ if (text.length > 0)
907
+ return text;
908
+ }
909
+ }
910
+ if (typeof session.prompt === 'string' && session.prompt.trim().length > 0) {
911
+ const clean = session.prompt
912
+ .replace(/^\[model changed:.*?\]\s*/i, '')
913
+ .replace(/^Current runtime context\..*?\n\n/is, '')
914
+ .trim();
915
+ if (clean.length > 0 && !clean.startsWith('[model changed:') && !clean.startsWith('Current runtime context.')) {
916
+ return clean;
917
+ }
918
+ }
919
+ return '';
920
+ }
921
+ function estimateTokenCount(text) {
922
+ if (!text)
923
+ return 0;
924
+ return Math.ceil(text.length / 4);
925
+ }
926
+ // The delta machinery and the module that consumes it now live together, so the two halves are joined
927
+ // here instead of at the composition root. ./emission.ts still cannot import this module back; it is
928
+ // handed its engine instead. Both seams in that module default closed, so a mis-ordered load refuses
929
+ // rather than crashing.
930
+ (0, emission_1.configurePatchEngine)({
931
+ parse: parseSearchReplaceBlocks,
932
+ apply: applySearchReplaceBlocks,
933
+ });