@nexrall/code-core 1.3.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Cross-File Breakage Guard — deterministic dangling-reference detection.
3
+ *
4
+ * THE PROBLEM (a top real-world failure of every coding agent):
5
+ * The agent edits file A — renames or deletes an exported function/class/
6
+ * const — and forgets that files B, C, D still import or call the old name.
7
+ * The edit to A succeeds, the agent moves on (often declaring success), and
8
+ * the project no longer compiles. Because the tool call itself succeeded and
9
+ * the model doesn't re-check the whole repo, this breakage is invisible until
10
+ * a build/test runs — which the agent may never do. Frontier agents mitigate
11
+ * this only by (sometimes) running a build afterwards; there is no structural
12
+ * detector wired into the edit itself.
13
+ *
14
+ * THE APPROACH:
15
+ * 1. Diff the edit payload (old → new) to find EXPORTED top-level symbols that
16
+ * were REMOVED or RENAMED (present in old, absent in new). This is a pure,
17
+ * language-aware-ish regex pass — deterministic, no model output.
18
+ * 2. For each removed/renamed symbol, scan the rest of the workspace (ripgrep,
19
+ * falling back to a bounded fs walk) for surviving references. If found, we
20
+ * surface them as a WARNING appended to the edit's tool result — not a
21
+ * refusal: the agent may legitimately be mid-refactor and about to fix the
22
+ * call-sites next. The point is to make the blast radius VISIBLE at the
23
+ * moment of the edit, so it can't be silently forgotten.
24
+ *
25
+ * Language coverage is JS/TS-first (the primary ecosystem here) plus Python;
26
+ * the symbol extractor degrades gracefully (returns nothing) for others rather
27
+ * than emitting noise.
28
+ */
29
+ export interface RemovedSymbol {
30
+ name: string;
31
+ kind: string;
32
+ }
33
+ /** Extract named exported/public symbols from a file's content. */
34
+ export declare function extractExportedSymbols(path: string, content: string): RemovedSymbol[];
35
+ /**
36
+ * Compare old vs new content of an edited file and return exported/public
37
+ * symbols that DISAPPEARED (removed or renamed). These are the names whose
38
+ * external call-sites may now be dangling.
39
+ */
40
+ export declare function findRemovedExports(path: string, oldContent: string, newContent: string): RemovedSymbol[];
41
+ export declare function isCheckableSymbol(name: string): boolean;
42
+ //# sourceMappingURL=crossFile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crossFile.d.ts","sourceRoot":"","sources":["../../src/agent/crossFile.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AA6BD,mEAAmE;AACnE,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,aAAa,EAAE,CAiCrF;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,aAAa,EAAE,CAKxG;AAKD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAIvD"}
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ /**
3
+ * Cross-File Breakage Guard — deterministic dangling-reference detection.
4
+ *
5
+ * THE PROBLEM (a top real-world failure of every coding agent):
6
+ * The agent edits file A — renames or deletes an exported function/class/
7
+ * const — and forgets that files B, C, D still import or call the old name.
8
+ * The edit to A succeeds, the agent moves on (often declaring success), and
9
+ * the project no longer compiles. Because the tool call itself succeeded and
10
+ * the model doesn't re-check the whole repo, this breakage is invisible until
11
+ * a build/test runs — which the agent may never do. Frontier agents mitigate
12
+ * this only by (sometimes) running a build afterwards; there is no structural
13
+ * detector wired into the edit itself.
14
+ *
15
+ * THE APPROACH:
16
+ * 1. Diff the edit payload (old → new) to find EXPORTED top-level symbols that
17
+ * were REMOVED or RENAMED (present in old, absent in new). This is a pure,
18
+ * language-aware-ish regex pass — deterministic, no model output.
19
+ * 2. For each removed/renamed symbol, scan the rest of the workspace (ripgrep,
20
+ * falling back to a bounded fs walk) for surviving references. If found, we
21
+ * surface them as a WARNING appended to the edit's tool result — not a
22
+ * refusal: the agent may legitimately be mid-refactor and about to fix the
23
+ * call-sites next. The point is to make the blast radius VISIBLE at the
24
+ * moment of the edit, so it can't be silently forgotten.
25
+ *
26
+ * Language coverage is JS/TS-first (the primary ecosystem here) plus Python;
27
+ * the symbol extractor degrades gracefully (returns nothing) for others rather
28
+ * than emitting noise.
29
+ */
30
+ Object.defineProperty(exports, "__esModule", { value: true });
31
+ exports.extractExportedSymbols = extractExportedSymbols;
32
+ exports.findRemovedExports = findRemovedExports;
33
+ exports.isCheckableSymbol = isCheckableSymbol;
34
+ // ── Exported-symbol extraction ────────────────────────────────────────────────
35
+ // Matches the common export forms. We only care about NAMED, top-level exports
36
+ // because those are what other files import by name.
37
+ const JS_EXPORT_RES = [
38
+ { kind: 'function', re: /\bexport\s+(?:async\s+)?function\s*\*?\s+([A-Za-z_$][\w$]*)/g },
39
+ { kind: 'class', re: /\bexport\s+(?:abstract\s+)?class\s+([A-Za-z_$][\w$]*)/g },
40
+ { kind: 'const', re: /\bexport\s+(?:const|let|var)\s+([A-Za-z_$][\w$]*)/g },
41
+ { kind: 'interface', re: /\bexport\s+interface\s+([A-Za-z_$][\w$]*)/g },
42
+ { kind: 'type', re: /\bexport\s+type\s+([A-Za-z_$][\w$]*)/g },
43
+ { kind: 'enum', re: /\bexport\s+(?:const\s+)?enum\s+([A-Za-z_$][\w$]*)/g },
44
+ ];
45
+ // export { a, b as c } — the EXPORTED (outward) names are `a` and `c`.
46
+ const JS_EXPORT_LIST_RE = /\bexport\s*\{([^}]*)\}/g;
47
+ // Python: a module-level `def name` / `class Name` is the public surface.
48
+ const PY_DEF_RES = [
49
+ { kind: 'def', re: /^def\s+([A-Za-z_][\w]*)/gm },
50
+ { kind: 'class', re: /^class\s+([A-Za-z_][\w]*)/gm },
51
+ ];
52
+ function isJsLike(path) {
53
+ return /\.(?:m?[jt]sx?|cjs|cts|mts)$/.test(path);
54
+ }
55
+ function isPy(path) {
56
+ return /\.py$/.test(path);
57
+ }
58
+ /** Extract named exported/public symbols from a file's content. */
59
+ function extractExportedSymbols(path, content) {
60
+ const out = new Map(); // name → kind (first wins)
61
+ const add = (name, kind) => {
62
+ if (name && !out.has(name))
63
+ out.set(name, kind);
64
+ };
65
+ if (isJsLike(path)) {
66
+ for (const { kind, re } of JS_EXPORT_RES) {
67
+ re.lastIndex = 0;
68
+ let m;
69
+ while ((m = re.exec(content)))
70
+ add(m[1], kind);
71
+ }
72
+ JS_EXPORT_LIST_RE.lastIndex = 0;
73
+ let lm;
74
+ while ((lm = JS_EXPORT_LIST_RE.exec(content))) {
75
+ for (const part of lm[1].split(',')) {
76
+ const seg = part.trim();
77
+ if (!seg || seg === 'default')
78
+ continue;
79
+ // `orig as exported` → exported name is what matters; plain `name`.
80
+ const asMatch = /(?:\bas\s+)([A-Za-z_$][\w$]*)\s*$/.exec(seg);
81
+ const name = asMatch ? asMatch[1] : seg.replace(/\s.*$/, '');
82
+ if (name && name !== 'default')
83
+ add(name, 'export');
84
+ }
85
+ }
86
+ }
87
+ else if (isPy(path)) {
88
+ for (const { kind, re } of PY_DEF_RES) {
89
+ re.lastIndex = 0;
90
+ let m;
91
+ while ((m = re.exec(content)))
92
+ add(m[1], kind);
93
+ }
94
+ }
95
+ return [...out.entries()].map(([name, kind]) => ({ name, kind }));
96
+ }
97
+ /**
98
+ * Compare old vs new content of an edited file and return exported/public
99
+ * symbols that DISAPPEARED (removed or renamed). These are the names whose
100
+ * external call-sites may now be dangling.
101
+ */
102
+ function findRemovedExports(path, oldContent, newContent) {
103
+ const before = extractExportedSymbols(path, oldContent);
104
+ if (before.length === 0)
105
+ return [];
106
+ const afterNames = new Set(extractExportedSymbols(path, newContent).map((s) => s.name));
107
+ return before.filter((s) => !afterNames.has(s.name));
108
+ }
109
+ // Identifier-ish names worth checking. Skip very short / trivial names that would
110
+ // produce noisy matches (e.g. `x`, `id`, `on`), and common words that collide.
111
+ const NOISY_NAMES = new Set(['default', 'index', 'main', 'test', 'get', 'set', 'run', 'app', 'data', 'item', 'value']);
112
+ function isCheckableSymbol(name) {
113
+ if (name.length < 4)
114
+ return false;
115
+ if (NOISY_NAMES.has(name.toLowerCase()))
116
+ return false;
117
+ return /^[A-Za-z_$][\w$]*$/.test(name);
118
+ }
119
+ //# sourceMappingURL=crossFile.js.map
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Edit-Completeness Guard — deterministic detection of silent code destruction.
3
+ *
4
+ * THE PROBLEM (a failure mode every coding agent still exhibits):
5
+ * When an agent rewrites a whole file with `write_file`, it often gets "lazy"
6
+ * and replaces large chunks of real code with an elision placeholder:
7
+ * // ... rest of the code stays the same
8
+ * # ... existing imports unchanged
9
+ * /* ... keep the previous implementation ... *\/
10
+ * The written file looks plausible and syntactically fine, the agent reports
11
+ * "done" — but the elided code is GONE. This is unrecoverable data loss inside
12
+ * the user's working tree. Editors mitigate it by preferring diff-style edits,
13
+ * but a full-file `write_file` remains a live footgun on every agent.
14
+ *
15
+ * A second, related bug: writing a file that still contains unresolved merge
16
+ * conflict markers (<<<<<<<, =======, >>>>>>>) — always a mistake.
17
+ *
18
+ * THE APPROACH (deterministic, content-only — cannot be hallucinated away):
19
+ * Scan the NEW content for elision placeholders (a comment lead + an explicit
20
+ * "rest/remaining/unchanged/existing/keep/omitted…" keyword). A bare `// ...`
21
+ * or a `...args` spread never matches — the keyword requirement keeps false
22
+ * positives near zero. When such a placeholder is written OVER an existing
23
+ * file, it is almost certainly destroying elided code, so we REFUSE the write
24
+ * and tell the agent to either send the full content or use edit_file for a
25
+ * targeted change. Merge-conflict markers are refused for any write.
26
+ *
27
+ * This is a REFUSAL (not a nudge) because the loss is silent and irreversible
28
+ * — exactly the class where failing closed is correct. New-file creation is
29
+ * exempt from the elision rule (a brand-new file cannot lose existing code),
30
+ * but merge markers are refused even there.
31
+ */
32
+ export interface CompletenessProblem {
33
+ kind: 'elision-placeholder' | 'merge-conflict-marker';
34
+ reason: string;
35
+ /** The offending line (trimmed, capped) for the error message. */
36
+ sample: string;
37
+ }
38
+ /**
39
+ * Inspect NEW file content (optionally against whether it overwrites an existing
40
+ * file). Returns the FIRST problem found, or null when the write looks complete.
41
+ *
42
+ * @param newContent the content about to be written
43
+ * @param overwriting true when an existing file is being replaced (enables the
44
+ * elision-placeholder rule; a new file can't lose code)
45
+ */
46
+ export declare function checkEditCompleteness(newContent: string, overwriting: boolean): CompletenessProblem | null;
47
+ //# sourceMappingURL=editCompleteness.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"editCompleteness.d.ts","sourceRoot":"","sources":["../../src/agent/editCompleteness.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,qBAAqB,GAAG,uBAAuB,CAAC;IACtD,MAAM,EAAE,MAAM,CAAC;IACf,kEAAkE;IAClE,MAAM,EAAE,MAAM,CAAC;CAChB;AAiCD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,OAAO,GAAG,mBAAmB,GAAG,IAAI,CAoC1G"}
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ /**
3
+ * Edit-Completeness Guard — deterministic detection of silent code destruction.
4
+ *
5
+ * THE PROBLEM (a failure mode every coding agent still exhibits):
6
+ * When an agent rewrites a whole file with `write_file`, it often gets "lazy"
7
+ * and replaces large chunks of real code with an elision placeholder:
8
+ * // ... rest of the code stays the same
9
+ * # ... existing imports unchanged
10
+ * /* ... keep the previous implementation ... *\/
11
+ * The written file looks plausible and syntactically fine, the agent reports
12
+ * "done" — but the elided code is GONE. This is unrecoverable data loss inside
13
+ * the user's working tree. Editors mitigate it by preferring diff-style edits,
14
+ * but a full-file `write_file` remains a live footgun on every agent.
15
+ *
16
+ * A second, related bug: writing a file that still contains unresolved merge
17
+ * conflict markers (<<<<<<<, =======, >>>>>>>) — always a mistake.
18
+ *
19
+ * THE APPROACH (deterministic, content-only — cannot be hallucinated away):
20
+ * Scan the NEW content for elision placeholders (a comment lead + an explicit
21
+ * "rest/remaining/unchanged/existing/keep/omitted…" keyword). A bare `// ...`
22
+ * or a `...args` spread never matches — the keyword requirement keeps false
23
+ * positives near zero. When such a placeholder is written OVER an existing
24
+ * file, it is almost certainly destroying elided code, so we REFUSE the write
25
+ * and tell the agent to either send the full content or use edit_file for a
26
+ * targeted change. Merge-conflict markers are refused for any write.
27
+ *
28
+ * This is a REFUSAL (not a nudge) because the loss is silent and irreversible
29
+ * — exactly the class where failing closed is correct. New-file creation is
30
+ * exempt from the elision rule (a brand-new file cannot lose existing code),
31
+ * but merge markers are refused even there.
32
+ */
33
+ Object.defineProperty(exports, "__esModule", { value: true });
34
+ exports.checkEditCompleteness = checkEditCompleteness;
35
+ // Comment leads across languages: // # /* * <!-- -- ; % (best-effort).
36
+ // The placeholder must combine a comment lead, an ellipsis OR an elision verb,
37
+ // and an explicit "the-rest-is-unchanged" keyword. Requiring the keyword is what
38
+ // prevents matching legitimate `// ...` separators, `...spread`, Python `...`,
39
+ // docstrings, or ranges.
40
+ const ELISION_KEYWORD = '(?:rest|remaining|remainder|unchanged|existing|previous|prior|same\\s+as|as\\s+before|keep\\s+(?:the|existing|current)|leave\\s+(?:the|existing)|other\\s+(?:methods|functions|imports|code)|the\\s+(?:same|rest)|omitted|snip(?:ped)?|truncat(?:ed|ion)|no\\s+changes?|implementation\\s+(?:stays|remains|unchanged)|code\\s+(?:here|continues|stays|remains|unchanged)|continues?\\s+(?:as|below|above)|goes?\\s+here)';
41
+ // A comment line that contains an ellipsis/elipsis-ish token AND the keyword.
42
+ // Two shapes:
43
+ // 1. comment + "..." + keyword → "// ... rest of the code"
44
+ // 2. comment + keyword + "..." → "# existing imports ..."
45
+ // 3. comment + keyword (no dots) → "// rest of the file unchanged"
46
+ const COMMENT_LEAD = '(?:\\/\\/+|#+|\\/\\*+|\\*|<!--|--|;+|%+)';
47
+ const ELISION_RES = [
48
+ new RegExp(`^\\s*${COMMENT_LEAD}\\s*(?:\\.\\.\\.|…)\\s*[\\w\\s'"()-]*?\\b${ELISION_KEYWORD}\\b`, 'i'),
49
+ new RegExp(`^\\s*${COMMENT_LEAD}\\s*[\\w\\s'"()-]*?\\b${ELISION_KEYWORD}\\b[\\w\\s'"()-]*?(?:\\.\\.\\.|…)`, 'i'),
50
+ new RegExp(`^\\s*${COMMENT_LEAD}\\s*(?:\\.\\.\\.|…)?\\s*(?:the\\s+)?${ELISION_KEYWORD}\\s+(?:of\\s+)?(?:the\\s+)?(?:code|file|function|method|imports?|class|logic|implementation|content)\\b`, 'i'),
51
+ // A short comment line that ENDS in a strong "left-out" keyword:
52
+ // "// other methods unchanged" · "# rest omitted" · "// snipped"
53
+ new RegExp(`^\\s*${COMMENT_LEAD}\\s*[\\w\\s'"()-]{0,60}?\\b(?:unchanged|omitted|snip(?:ped)?|truncated|elided|not\\s+shown)\\s*(?:\\*\\/|-->|)?\\s*$`, 'i'),
54
+ ];
55
+ // Merge-conflict markers at line start (7 identical chars is the git default).
56
+ // `<<<<<<<` and `>>>>>>>` are effectively never legitimate content. A bare line
57
+ // of `=======` IS common (markdown heading underline, ASCII dividers), so it is
58
+ // only treated as a conflict marker when a `<<<<<<<`/`>>>>>>>` also appears
59
+ // (i.e. a genuine conflict hunk), handled in checkEditCompleteness.
60
+ const CONFLICT_START_RE = /^(?:<{7}|>{7})(?:\s|$)/;
61
+ const CONFLICT_MID_RE = /^={7}(?:\s|$)/;
62
+ /**
63
+ * Inspect NEW file content (optionally against whether it overwrites an existing
64
+ * file). Returns the FIRST problem found, or null when the write looks complete.
65
+ *
66
+ * @param newContent the content about to be written
67
+ * @param overwriting true when an existing file is being replaced (enables the
68
+ * elision-placeholder rule; a new file can't lose code)
69
+ */
70
+ function checkEditCompleteness(newContent, overwriting) {
71
+ if (typeof newContent !== 'string' || newContent.length === 0)
72
+ return null;
73
+ const lines = newContent.split('\n');
74
+ // Merge-conflict markers — refuse for any write (new or overwrite). A `<<<<<<<`
75
+ // or `>>>>>>>` line alone is conclusive; a `=======` line is only a marker when
76
+ // it co-occurs with a start/end marker (avoids flagging markdown/ASCII rules).
77
+ let sawStartOrEnd = null;
78
+ let sawMid = null;
79
+ for (const line of lines) {
80
+ if (CONFLICT_START_RE.test(line)) {
81
+ sawStartOrEnd = line.trim();
82
+ break;
83
+ }
84
+ if (CONFLICT_MID_RE.test(line) && sawMid === null)
85
+ sawMid = line.trim();
86
+ }
87
+ if (sawStartOrEnd || (sawMid && lines.some((l) => CONFLICT_START_RE.test(l)))) {
88
+ return {
89
+ kind: 'merge-conflict-marker',
90
+ reason: 'content still contains an unresolved merge-conflict marker',
91
+ sample: (sawStartOrEnd ?? sawMid ?? '').slice(0, 120),
92
+ };
93
+ }
94
+ if (!overwriting)
95
+ return null; // new file cannot elide pre-existing code
96
+ for (const line of lines) {
97
+ for (const re of ELISION_RES) {
98
+ if (re.test(line)) {
99
+ return {
100
+ kind: 'elision-placeholder',
101
+ reason: 'content contains an elision placeholder that would silently delete the omitted code',
102
+ sample: line.trim().slice(0, 120),
103
+ };
104
+ }
105
+ }
106
+ }
107
+ return null;
108
+ }
109
+ //# sourceMappingURL=editCompleteness.js.map
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Flaky-Test Detection — deterministic non-determinism guard.
3
+ *
4
+ * THE PROBLEM (a subtle failure every coding agent shares):
5
+ * A test suite that passes and fails intermittently — timing races, ordering
6
+ * dependence, RNG without a seed, wall-clock assumptions, network — poisons the
7
+ * agent's single most-trusted signal. Two bad outcomes follow:
8
+ * 1. The agent "fixes" a flaky failure by editing unrelated code, chasing a
9
+ * ghost, or worse, by weakening the test (reward hacking).
10
+ * 2. The agent simply RE-RUNS the suite until it happens to go green and then
11
+ * declares success — a green run of a flaky test proves nothing. Frontier
12
+ * agents have no structural way to notice they are doing this.
13
+ *
14
+ * THE APPROACH (deterministic — derived from the run history, not model output):
15
+ * We already record every verification run (command + pass/fail) in the
16
+ * progress ledger. Tag each run with a monotonically-increasing "mutation
17
+ * epoch" that increments whenever a source file is written. Then: if the SAME
18
+ * normalized command produced BOTH a pass and a fail while the epoch never
19
+ * changed between them (i.e. NO edit happened in between), the outcome flipped
20
+ * with identical inputs → the test is flaky (or environment-dependent). That is
21
+ * a mathematical fact about the observed runs, not a guess.
22
+ *
23
+ * On detection we surface a one-shot warning telling the agent to stop trusting
24
+ * a green run, investigate the source of non-determinism, and NOT paper over it
25
+ * by re-running or by editing the test to pass. Never blocks.
26
+ */
27
+ export interface VerificationRun {
28
+ /** Normalized command (already sliced/trimmed by the ledger). */
29
+ cmd: string;
30
+ ok: boolean;
31
+ /** Mutation epoch at the time this run executed (see ledger). */
32
+ epoch: number;
33
+ }
34
+ export interface FlakyFinding {
35
+ cmd: string;
36
+ /** How many passes and fails were observed at the SAME epoch. */
37
+ passes: number;
38
+ fails: number;
39
+ }
40
+ export declare function normalizeVerifyCmd(cmd: string): string;
41
+ /**
42
+ * Scan verification history for commands that produced BOTH pass and fail at the
43
+ * SAME mutation epoch (no edit between the differing outcomes). Returns one
44
+ * finding per flaky command.
45
+ */
46
+ export declare function detectFlaky(runs: VerificationRun[]): FlakyFinding[];
47
+ //# sourceMappingURL=flaky.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flaky.d.ts","sourceRoot":"","sources":["../../src/agent/flaky.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,MAAM,WAAW,eAAe;IAC9B,iEAAiE;IACjE,GAAG,EAAE,MAAM,CAAC;IACZ,EAAE,EAAE,OAAO,CAAC;IACZ,iEAAiE;IACjE,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,iEAAiE;IACjE,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf;AAKD,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAOtD;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,eAAe,EAAE,GAAG,YAAY,EAAE,CAqBnE"}
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ /**
3
+ * Flaky-Test Detection — deterministic non-determinism guard.
4
+ *
5
+ * THE PROBLEM (a subtle failure every coding agent shares):
6
+ * A test suite that passes and fails intermittently — timing races, ordering
7
+ * dependence, RNG without a seed, wall-clock assumptions, network — poisons the
8
+ * agent's single most-trusted signal. Two bad outcomes follow:
9
+ * 1. The agent "fixes" a flaky failure by editing unrelated code, chasing a
10
+ * ghost, or worse, by weakening the test (reward hacking).
11
+ * 2. The agent simply RE-RUNS the suite until it happens to go green and then
12
+ * declares success — a green run of a flaky test proves nothing. Frontier
13
+ * agents have no structural way to notice they are doing this.
14
+ *
15
+ * THE APPROACH (deterministic — derived from the run history, not model output):
16
+ * We already record every verification run (command + pass/fail) in the
17
+ * progress ledger. Tag each run with a monotonically-increasing "mutation
18
+ * epoch" that increments whenever a source file is written. Then: if the SAME
19
+ * normalized command produced BOTH a pass and a fail while the epoch never
20
+ * changed between them (i.e. NO edit happened in between), the outcome flipped
21
+ * with identical inputs → the test is flaky (or environment-dependent). That is
22
+ * a mathematical fact about the observed runs, not a guess.
23
+ *
24
+ * On detection we surface a one-shot warning telling the agent to stop trusting
25
+ * a green run, investigate the source of non-determinism, and NOT paper over it
26
+ * by re-running or by editing the test to pass. Never blocks.
27
+ */
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.normalizeVerifyCmd = normalizeVerifyCmd;
30
+ exports.detectFlaky = detectFlaky;
31
+ // Normalize a command so trivially-different invocations of the same suite group
32
+ // together: collapse whitespace, drop a leading `cd … &&`, strip common
33
+ // non-semantic flags that don't change WHICH tests run.
34
+ function normalizeVerifyCmd(cmd) {
35
+ let c = cmd.trim().replace(/\s+/g, ' ');
36
+ c = c.replace(/^cd\s+[^&]+&&\s*/i, '');
37
+ // Drop reporter/verbosity/color flags that don't change the test SELECTION.
38
+ c = c.replace(/\s--(?:reporter|reporters|verbose|silent|color|no-color|colors|coverage)(?:=\S+)?/gi, '');
39
+ c = c.replace(/\s-v\b/g, '');
40
+ return c.trim();
41
+ }
42
+ /**
43
+ * Scan verification history for commands that produced BOTH pass and fail at the
44
+ * SAME mutation epoch (no edit between the differing outcomes). Returns one
45
+ * finding per flaky command.
46
+ */
47
+ function detectFlaky(runs) {
48
+ // Group by (normalized cmd, epoch) → tally pass/fail.
49
+ const groups = new Map();
50
+ for (const r of runs) {
51
+ const norm = normalizeVerifyCmd(r.cmd);
52
+ const key = `${r.epoch}\u0000${norm}`;
53
+ let g = groups.get(key);
54
+ if (!g) {
55
+ g = { cmd: norm, passes: 0, fails: 0 };
56
+ groups.set(key, g);
57
+ }
58
+ if (r.ok)
59
+ g.passes += 1;
60
+ else
61
+ g.fails += 1;
62
+ }
63
+ // A group with BOTH a pass and a fail (same epoch, same command) is flaky.
64
+ // Aggregate across epochs so we report each command once.
65
+ const byCmd = new Map();
66
+ for (const g of groups.values()) {
67
+ if (g.passes > 0 && g.fails > 0) {
68
+ const prev = byCmd.get(g.cmd);
69
+ if (prev) {
70
+ prev.passes += g.passes;
71
+ prev.fails += g.fails;
72
+ }
73
+ else
74
+ byCmd.set(g.cmd, { cmd: g.cmd, passes: g.passes, fails: g.fails });
75
+ }
76
+ }
77
+ return [...byCmd.values()];
78
+ }
79
+ //# sourceMappingURL=flaky.js.map
@@ -1,5 +1,11 @@
1
1
  import type { Message, AgentLoopOptions } from '../types';
2
2
  export declare function resolveMaxIterations(optionValue: number | undefined, settingsRaw: Record<string, unknown>): number;
3
+ /** Approximate serialised request-body size (bytes) for the messages array. */
4
+ export declare function estimateBodyBytes(messages: Message[]): number;
5
+ /** Tools that mutate the filesystem — used by the verification nudge (GAP D). */
6
+ export declare const WRITE_TOOL_NAMES: Set<string>;
7
+ /** Heuristic: does a bash command look like it's running tests/build/lint/typecheck? (GAP D) */
8
+ export declare const VERIFY_CMD_RE: RegExp;
3
9
  /**
4
10
  * Find the latest index ≤ maxIdx where history can be cut safely.
5
11
  *
@@ -14,5 +20,58 @@ export declare function resolveMaxIterations(optionValue: number | undefined, se
14
20
  * exactly when a long task needs it most.
15
21
  */
16
22
  export declare function findSafeCutIndex(messages: Message[], maxIdx: number): number;
23
+ /**
24
+ * Render messages to a plain-text transcript for the summariser (tool noise
25
+ * truncated per-block AND the whole transcript hard-capped). When the transcript
26
+ * would exceed MAX_TRANSCRIPT_CHARS we keep the HEAD (original task + early
27
+ * decisions) and the TAIL (most-recent, highest-signal context) and drop the
28
+ * middle — a middle-out elision that preserves both "what we set out to do" and
29
+ * "where we are now", which is what the continuation summary needs most.
30
+ */
31
+ export declare function transcriptOf(messages: Message[]): string;
32
+ export interface ProgressLedger {
33
+ filesTouched: Map<string, {
34
+ tool: string;
35
+ edits: number;
36
+ }>;
37
+ verifications: Array<{
38
+ cmd: string;
39
+ ok: boolean;
40
+ epoch: number;
41
+ }>;
42
+ /** Test-integrity findings (reward-hacking signals) that must survive compaction. */
43
+ testIntegrity: Array<{
44
+ path: string;
45
+ reason: string;
46
+ }>;
47
+ /**
48
+ * Monotonic mutation epoch: incremented on every successful source write. Two
49
+ * verification runs sharing an epoch had NO edit between them, so a PASS↔FAIL
50
+ * flip across that boundary proves the test is flaky (see detectFlaky).
51
+ */
52
+ epoch: number;
53
+ }
54
+ export declare function createLedger(): ProgressLedger;
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;
57
+ /** Render the ledger as a compact, verbatim block for the compaction preamble. */
58
+ export declare function ledgerSummary(ledger: ProgressLedger): string;
59
+ /**
60
+ * Lossy-but-structure-preserving prune: shrink OLD, large tool_result blocks in
61
+ * place, keeping the last PRUNE_KEEP_RECENT messages untouched. This is tried
62
+ * BEFORE summarisation because it:
63
+ * • keeps every turn and every tool_use/tool_result pair (API stays valid),
64
+ * • never makes an extra model call (summarisation does — cost + latency),
65
+ * • degrades gracefully on repeat (summarise-of-summarise loses the most on
66
+ * long runs; pruning just trims already-consumed output further).
67
+ *
68
+ * IMPORTANT: pruned state is encoded in the content string (PRUNE_MARKER_TAIL
69
+ * suffix), NOT as an extra property on the block — a stray field on a content
70
+ * block is rejected by the Anthropic API as an unknown key (400). This keeps the
71
+ * serialised body schema-clean while remaining idempotent across repeat calls.
72
+ *
73
+ * Returns the number of bytes reclaimed (0 if nothing was prunable).
74
+ */
75
+ export declare function pruneOldToolResults(messages: Message[]): number;
17
76
  export declare function runAgentLoop(initialMessages: Message[], options: AgentLoopOptions): Promise<Message[]>;
18
77
  //# sourceMappingURL=loop.d.ts.map
@@ -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;AAmKlB,wBAAgB,oBAAoB,CAClC,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,MAAM,CAWR;AAgRD;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAK5E;AA6ED,wBAAsB,YAAY,CAChC,eAAe,EAAE,OAAO,EAAE,EAC1B,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,OAAO,EAAE,CAAC,CA+WpB"}
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"}