@nexrall/code-core 1.4.0 → 1.4.1

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.
@@ -53,7 +53,7 @@ export interface ProgressLedger {
53
53
  }
54
54
  export declare function createLedger(): ProgressLedger;
55
55
  /** Record one tool call's effect on the ledger (deterministic, no model call). */
56
- export declare function ledgerRecord(ledger: ProgressLedger, toolName: string, input: Record<string, unknown> | undefined, ok: boolean): void;
56
+ export declare function ledgerRecord(ledger: ProgressLedger, toolName: string, input: Record<string, unknown> | undefined, ok: boolean, output?: string): void;
57
57
  /** Render the ledger as a compact, verbatim block for the compaction preamble. */
58
58
  export declare function ledgerSummary(ledger: ProgressLedger): string;
59
59
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,OAAO,EAMP,gBAAgB,EACjB,MAAM,UAAU,CAAC;AAqKlB,wBAAgB,oBAAoB,CAClC,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,MAAM,CAWR;AA8RD,+EAA+E;AAC/E,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAM7D;AAsBD,iFAAiF;AACjF,eAAO,MAAM,gBAAgB,aAA+G,CAAC;AAC7I,gGAAgG;AAChG,eAAO,MAAM,aAAa,QAA2J,CAAC;AAEtL;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAK5E;AAUD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAwBxD;AAoBD,MAAM,WAAW,cAAc;IAC7B,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC3D,aAAa,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAClE,qFAAqF;IACrF,aAAa,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACvD;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,YAAY,IAAI,cAAc,CAE7C;AAED,kFAAkF;AAClF,wBAAgB,YAAY,CAC1B,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAC1C,EAAE,EAAE,OAAO,GACV,IAAI,CAkCN;AAED,kFAAkF;AAClF,wBAAgB,aAAa,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CA6B5D;AAmBD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAmB/D;AA2FD,wBAAsB,YAAY,CAChC,eAAe,EAAE,OAAO,EAAE,EAC1B,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,OAAO,EAAE,CAAC,CA6epB"}
1
+ {"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,OAAO,EAMP,gBAAgB,EACjB,MAAM,UAAU,CAAC;AAqKlB,wBAAgB,oBAAoB,CAClC,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,MAAM,CAWR;AA8RD,+EAA+E;AAC/E,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAM7D;AAsBD,iFAAiF;AACjF,eAAO,MAAM,gBAAgB,aAA+G,CAAC;AAC7I,gGAAgG;AAChG,eAAO,MAAM,aAAa,QAA2J,CAAC;AAEtL;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAK5E;AAUD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAwBxD;AAoBD,MAAM,WAAW,cAAc;IAC7B,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC3D,aAAa,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAClE,qFAAqF;IACrF,aAAa,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACvD;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,YAAY,IAAI,cAAc,CAE7C;AAED,kFAAkF;AAClF,wBAAgB,YAAY,CAC1B,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAC1C,EAAE,EAAE,OAAO,EACX,MAAM,CAAC,EAAE,MAAM,GACd,IAAI,CA4CN;AAED,kFAAkF;AAClF,wBAAgB,aAAa,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CA6B5D;AAmBD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAmB/D;AA2FD,wBAAsB,YAAY,CAChC,eAAe,EAAE,OAAO,EAAE,EAC1B,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,OAAO,EAAE,CAAC,CAufpB"}
@@ -551,7 +551,7 @@ function createLedger() {
551
551
  return { filesTouched: new Map(), verifications: [], testIntegrity: [], epoch: 0 };
552
552
  }
553
553
  /** Record one tool call's effect on the ledger (deterministic, no model call). */
554
- function ledgerRecord(ledger, toolName, input, ok) {
554
+ function ledgerRecord(ledger, toolName, input, ok, output) {
555
555
  if (exports.WRITE_TOOL_NAMES.has(toolName)) {
556
556
  if (!ok)
557
557
  return; // a FAILED write changed nothing — not a durable fact
@@ -565,10 +565,22 @@ function ledgerRecord(ledger, toolName, input, ok) {
565
565
  }
566
566
  // Reward-hacking guard: if this write WEAKENED a test file, record it so the
567
567
  // signal survives compaction and can be surfaced before the agent finishes.
568
- const ti = (0, testIntegrity_1.analyzeWriteToolForTestIntegrity)(toolName, input);
569
- if (ti?.suspicious && p) {
570
- for (const f of ti.findings) {
571
- ledger.testIntegrity.push({ path: p, reason: f.reason });
568
+ const reasons = [];
569
+ // write_file overwrites carry a marker computed by the executor (which had the
570
+ // prior on-disk content) — it detects REMOVED assertions/cases, not just
571
+ // additive skips/tautologies. Prefer it when present.
572
+ const markerReasons = toolName === 'write_file' ? (0, testIntegrity_1.decodeTestIntegrityMarker)(output) : [];
573
+ if (markerReasons.length) {
574
+ reasons.push(...markerReasons);
575
+ }
576
+ else {
577
+ const ti = (0, testIntegrity_1.analyzeWriteToolForTestIntegrity)(toolName, input);
578
+ if (ti?.suspicious)
579
+ reasons.push(...ti.findings.map((f) => f.reason));
580
+ }
581
+ if (reasons.length && p) {
582
+ for (const reason of reasons) {
583
+ ledger.testIntegrity.push({ path: p, reason });
572
584
  }
573
585
  if (ledger.testIntegrity.length > LEDGER_MAX_NOTES * 2) {
574
586
  ledger.testIntegrity.splice(0, ledger.testIntegrity.length - LEDGER_MAX_NOTES);
@@ -1110,16 +1122,25 @@ async function runAgentLoop(initialMessages, options) {
1110
1122
  }
1111
1123
  }
1112
1124
  }
1125
+ // Capture the raw output (may carry the invisible test-integrity
1126
+ // marker from a write_file overwrite) for the ledger, then strip the
1127
+ // marker so neither the UI (onToolResult) nor the model ever see it.
1128
+ let rawOutput;
1129
+ if (name === 'write_file' && result.output) {
1130
+ rawOutput = result.output;
1131
+ result.output = (0, testIntegrity_1.stripTestIntegrityMarker)(result.output);
1132
+ }
1113
1133
  // Notify caller about result
1114
1134
  options.onToolResult(name, result);
1115
- return { block: { ...block, id }, result };
1135
+ return { block: { ...block, id }, result, rawOutput };
1116
1136
  }));
1117
1137
  // Track whether files were mutated / verified this run, for the one-shot
1118
1138
  // end-of-task nudge below (GAP D — see declaration above).
1119
- for (const { block, result } of toolResults) {
1139
+ for (const { block, result, rawOutput } of toolResults) {
1120
1140
  const ok = result.error === undefined;
1121
1141
  // GAP E — feed every successful effect into the deterministic ledger.
1122
- ledgerRecord(ledger, block.name, block.input, ok);
1142
+ // rawOutput carries the (already-stripped-from-view) test-integrity marker.
1143
+ ledgerRecord(ledger, block.name, block.input, ok, rawOutput ?? result.output);
1123
1144
  if (!ok)
1124
1145
  continue; // failed calls don't count either way
1125
1146
  if (exports.WRITE_TOOL_NAMES.has(block.name))
@@ -53,4 +53,10 @@ export declare function analyzeTestEdit(path: string, oldText: string, newText:
53
53
  * - write_file: ('', content) — additive-only detection (no prior content here)
54
54
  */
55
55
  export declare function analyzeWriteToolForTestIntegrity(toolName: string, input: Record<string, unknown> | undefined): TestIntegrityResult | null;
56
+ /** Encode findings as an invisible marker to append to a tool's output string. */
57
+ export declare function encodeTestIntegrityMarker(findings: TestEditFinding[]): string;
58
+ /** Extract weakening reasons from a tool output that may carry the marker. */
59
+ export declare function decodeTestIntegrityMarker(output: string | undefined): string[];
60
+ /** Strip the (invisible) marker from output before it is shown to the model. */
61
+ export declare function stripTestIntegrityMarker(output: string | undefined): string;
56
62
  //# sourceMappingURL=testIntegrity.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"testIntegrity.d.ts","sourceRoot":"","sources":["../../src/agent/testIntegrity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,MAAM,WAAW,eAAe;IAC9B,gEAAgE;IAChE,MAAM,EAAE,MAAM,CAAC;IACf,0CAA0C;IAC1C,IAAI,EAAE,YAAY,GAAG,mBAAmB,GAAG,iBAAiB,GAAG,kBAAkB,GAAG,gBAAgB,CAAC;CACtG;AAED,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,OAAO,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB,QAAQ,EAAE,eAAe,EAAE,CAAC;CAC7B;AAQD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAGhD;AA4ED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,mBAAmB,CAmDnG;AAED;;;;;;;GAOG;AACH,wBAAgB,gCAAgC,CAC9C,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GACzC,mBAAmB,GAAG,IAAI,CA2B5B"}
1
+ {"version":3,"file":"testIntegrity.d.ts","sourceRoot":"","sources":["../../src/agent/testIntegrity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,MAAM,WAAW,eAAe;IAC9B,gEAAgE;IAChE,MAAM,EAAE,MAAM,CAAC;IACf,0CAA0C;IAC1C,IAAI,EAAE,YAAY,GAAG,mBAAmB,GAAG,iBAAiB,GAAG,kBAAkB,GAAG,gBAAgB,CAAC;CACtG;AAED,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,OAAO,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB,QAAQ,EAAE,eAAe,EAAE,CAAC;CAC7B;AAQD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAGhD;AA4ED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,mBAAmB,CAmDnG;AAED;;;;;;;GAOG;AACH,wBAAgB,gCAAgC,CAC9C,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GACzC,mBAAmB,GAAG,IAAI,CA8B5B;AAcD,kFAAkF;AAClF,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,eAAe,EAAE,GAAG,MAAM,CAI7E;AAED,8EAA8E;AAC9E,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,EAAE,CAQ9E;AAED,gFAAgF;AAChF,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAQ3E"}
@@ -32,6 +32,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
32
32
  exports.isTestFile = isTestFile;
33
33
  exports.analyzeTestEdit = analyzeTestEdit;
34
34
  exports.analyzeWriteToolForTestIntegrity = analyzeWriteToolForTestIntegrity;
35
+ exports.encodeTestIntegrityMarker = encodeTestIntegrityMarker;
36
+ exports.decodeTestIntegrityMarker = decodeTestIntegrityMarker;
37
+ exports.stripTestIntegrityMarker = stripTestIntegrityMarker;
35
38
  // ── Is this a test file? ──────────────────────────────────────────────────────
36
39
  // Covers the common conventions across JS/TS, Python, Go, Java/Kotlin, Ruby,
37
40
  // Rust, PHP, C#. Path-based (fast, language-agnostic).
@@ -192,8 +195,55 @@ function analyzeWriteToolForTestIntegrity(toolName, input) {
192
195
  if (toolName === 'write_file') {
193
196
  const content = typeof input.content === 'string' ? input.content : '';
194
197
  // No prior content available at this layer → additive-only ('' → content).
198
+ // The executor closes this gap: when write_file OVERWRITES an existing test
199
+ // file it has the prior on-disk content, runs the full old→new analysis, and
200
+ // embeds a TEST_INTEGRITY_MARKER in its output which ledgerRecord parses.
195
201
  return analyzeTestEdit(path, '', content);
196
202
  }
197
203
  return null;
198
204
  }
205
+ // ── write_file overwrite bridge ────────────────────────────────────────────────
206
+ // A write_file that overwrites an existing test file can silently REMOVE
207
+ // assertions / test cases — invisible to analyzeWriteToolForTestIntegrity above
208
+ // because the prior content isn't available at the loop layer. The executor DOES
209
+ // have the prior content (it reads it to preserve mode / run the cross-file
210
+ // check), so it runs analyzeTestEdit(path, priorContent, newContent) and, when
211
+ // suspicious, appends a machine-readable marker to its tool output. ledgerRecord
212
+ // parses that marker so a write_file overwrite gets the exact same ledger entry
213
+ // and pre-finish nudge as an edit_file weakening would.
214
+ const TEST_INTEGRITY_MARKER_OPEN = '\u0000NEXRALL_TEST_INTEGRITY\u0000';
215
+ const TEST_INTEGRITY_MARKER_CLOSE = '\u0000/NEXRALL_TEST_INTEGRITY\u0000';
216
+ /** Encode findings as an invisible marker to append to a tool's output string. */
217
+ function encodeTestIntegrityMarker(findings) {
218
+ if (!findings.length)
219
+ return '';
220
+ const payload = findings.map((f) => f.reason).join('\u0001');
221
+ return `${TEST_INTEGRITY_MARKER_OPEN}${payload}${TEST_INTEGRITY_MARKER_CLOSE}`;
222
+ }
223
+ /** Extract weakening reasons from a tool output that may carry the marker. */
224
+ function decodeTestIntegrityMarker(output) {
225
+ if (!output)
226
+ return [];
227
+ const start = output.indexOf(TEST_INTEGRITY_MARKER_OPEN);
228
+ if (start === -1)
229
+ return [];
230
+ const from = start + TEST_INTEGRITY_MARKER_OPEN.length;
231
+ const end = output.indexOf(TEST_INTEGRITY_MARKER_CLOSE, from);
232
+ if (end === -1)
233
+ return [];
234
+ return output.slice(from, end).split('\u0001').filter(Boolean);
235
+ }
236
+ /** Strip the (invisible) marker from output before it is shown to the model. */
237
+ function stripTestIntegrityMarker(output) {
238
+ if (!output)
239
+ return output ?? '';
240
+ const start = output.indexOf(TEST_INTEGRITY_MARKER_OPEN);
241
+ if (start === -1)
242
+ return output;
243
+ const end = output.indexOf(TEST_INTEGRITY_MARKER_CLOSE, start);
244
+ if (end === -1)
245
+ return output;
246
+ const cleaned = output.slice(0, start) + output.slice(end + TEST_INTEGRITY_MARKER_CLOSE.length);
247
+ return cleaned.replace(/\n{3,}/g, '\n\n').trimEnd();
248
+ }
199
249
  //# sourceMappingURL=testIntegrity.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/tools/executor.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,EAAyB,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AAy8DtE,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,WAAW,CAAC,EAAE;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,EAClC,OAAO,CAAC,EAAE,aAAa,EACvB,OAAO,CAAC,EAAE,MAAM,EAChB,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,UAAU,CAAC,CAiBrB"}
1
+ {"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/tools/executor.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,EAAyB,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AAk9DtE,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,WAAW,CAAC,EAAE;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,EAClC,OAAO,CAAC,EAAE,aAAa,EACvB,OAAO,CAAC,EAAE,MAAM,EAChB,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,UAAU,CAAC,CAiBrB"}
@@ -44,6 +44,7 @@ const child_process_1 = require("child_process");
44
44
  const sandbox_1 = require("./sandbox");
45
45
  const auth_1 = require("../auth");
46
46
  const editCompleteness_1 = require("../agent/editCompleteness");
47
+ const testIntegrity_1 = require("../agent/testIntegrity");
47
48
  const crossFile_1 = require("../agent/crossFile");
48
49
  const client_1 = require("../api/client");
49
50
  const symbols_1 = require("./symbols");
@@ -370,7 +371,16 @@ async function writeFile(input, workDir) {
370
371
  return { output: `Created ${resolved} (${lines} lines, ${bytes} bytes)` };
371
372
  }
372
373
  const xfile = crossFileBreakageWarning(resolved, normalizeLF(priorContent), normalizeLF(content), workDir ?? process.cwd());
373
- return { output: `Overwrote ${resolved} (${lines} lines, ${bytes} bytes)${xfile}` };
374
+ // Reward-hacking guard: a write_file that OVERWRITES an existing test file can
375
+ // silently drop assertions / test cases. The loop layer can't see the prior
376
+ // content — but we can (we just read it). Run the full old→new analysis and
377
+ // embed an invisible marker that ledgerRecord parses, so this path gets the
378
+ // same ledger entry + pre-finish nudge as an edit_file weakening would.
379
+ let tiMarker = '';
380
+ const ti = (0, testIntegrity_1.analyzeTestEdit)(resolved, normalizeLF(priorContent), normalizeLF(content));
381
+ if (ti.suspicious)
382
+ tiMarker = (0, testIntegrity_1.encodeTestIntegrityMarker)(ti.findings);
383
+ return { output: `Overwrote ${resolved} (${lines} lines, ${bytes} bytes)${xfile}${tiMarker}` };
374
384
  }
375
385
  catch (err) {
376
386
  return { error: err.message };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nexrall/code-core",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "Core agent loop, tools, and extension primitives for Nexrall Code — embed an AI coding agent in any Node.js application.",
5
5
  "license": "MIT",
6
6
  "author": "Nexrall <support@nexrall.com> (https://nexrall.com)",