@mjasnikovs/pi-task 0.18.49 → 0.18.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/remote/register.js +4 -2
- package/dist/remote/server.d.ts +14 -0
- package/dist/remote/server.js +61 -18
- package/dist/shared/child-output.d.ts +21 -7
- package/dist/shared/child-output.js +11 -0
- package/dist/task/apis-contract.d.ts +67 -0
- package/dist/task/apis-contract.js +77 -0
- package/dist/task/context-attribution.d.ts +123 -0
- package/dist/task/context-attribution.js +285 -0
- package/dist/task/context-silence.d.ts +53 -0
- package/dist/task/context-silence.js +93 -0
- package/dist/task/phases.js +187 -5
- package/dist/task/prompts.js +13 -0
- package/dist/task/spec-urls.d.ts +117 -0
- package/dist/task/spec-urls.js +214 -0
- package/dist/task/type-only-answer.d.ts +95 -0
- package/dist/task/type-only-answer.js +249 -0
- package/dist/workers/fetch-core.d.ts +50 -0
- package/dist/workers/fetch-core.js +118 -16
- package/dist/workers/pi-worker-core.d.ts +11 -0
- package/dist/workers/pi-worker-core.js +30 -0
- package/dist/workers/pi-worker-docs.d.ts +9 -0
- package/dist/workers/pi-worker-docs.js +119 -7
- package/dist/workers/pi-worker-fetch.js +3 -1
- package/dist/workers/typeonly-log.d.ts +45 -0
- package/dist/workers/typeonly-log.js +88 -0
- package/package.json +1 -1
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic detector for the F-1 laundering shape: a CONTEXT bullet that asserts
|
|
3
|
+
* external API USAGE SEMANTICS under an attribution cue that no EXTERNAL CONTEXT block
|
|
4
|
+
* can actually support.
|
|
5
|
+
*
|
|
6
|
+
* THE SHAPE, from mx5 run 15 (TASK_0027.md, verbatim):
|
|
7
|
+
*
|
|
8
|
+
* - The `hono` dependency is pinned at `^4.12.31` in package.json, and the external
|
|
9
|
+
* context confirms `hc<AppType>` pattern with base URL `/api` for same-origin
|
|
10
|
+
* relative paths works correctly (per Hono RPC docs LIVE data).
|
|
11
|
+
*
|
|
12
|
+
* worker:context has tools `read,grep` only (phases.ts:623) — it cannot consult any
|
|
13
|
+
* documentation — so the base-URL claim was necessarily from model memory. It shipped
|
|
14
|
+
* into TASK_0027's CONSTRAINTS and ACCEPTANCE, the implementation obeyed it exactly
|
|
15
|
+
* (`hc<AppType>('/api')` plus `api.api.auth.login.$post()`), and every request went to
|
|
16
|
+
* `/api/api/...` ⇒ 404 ⇒ the product's entire API surface was dead.
|
|
17
|
+
*
|
|
18
|
+
* WHY THE OBVIOUS TEST DOES NOT WORK. "Flag a bullet whose package has no EXTERNAL
|
|
19
|
+
* CONTEXT block" misses this case: EXTERNAL CONTEXT *did* carry a `### npm: hono` block.
|
|
20
|
+
* That block contains version numbers and nothing else, so it cannot support a claim
|
|
21
|
+
* about what a base URL MEANS — yet it lends the sentence an air of having been checked.
|
|
22
|
+
* That is the whole mechanism (F-1e): one citable fact, the pinned version, fused in a
|
|
23
|
+
* single sentence with an uncitable one under a shared attribution. The true half
|
|
24
|
+
* launders the false half.
|
|
25
|
+
*
|
|
26
|
+
* So the rule keys on what a block CAN support, mirroring the LIVE-DATA RULE's own
|
|
27
|
+
* taxonomy (RESEARCH_CONTEXT_PROMPT in prompts.ts):
|
|
28
|
+
* ### npm: version numbers only -> cannot source a semantics claim
|
|
29
|
+
* ### docs: retrieved package doc/.d.ts -> CAN source a semantics claim
|
|
30
|
+
* ### url: fetched page content -> CAN source a semantics claim
|
|
31
|
+
* ### service: 3 search-result SNIPPETS -> cannot source a semantics claim
|
|
32
|
+
*
|
|
33
|
+
* The `service` exclusion is not a judgement call — it is the LIVE-DATA RULE's own scope.
|
|
34
|
+
* That rule makes a service block authoritative for "current API surface, deprecation
|
|
35
|
+
* status, and replacement systems", i.e. versions/status/names. A service block is a
|
|
36
|
+
* title + URL + one-line description per result (service-blocks.ts:10); it cannot carry
|
|
37
|
+
* what a parameter MEANS. This matters concretely: TASK_0027's enrichment produced
|
|
38
|
+
* exactly one service block, `### service: Hono RPC client` (extractEnrichTargets on the
|
|
39
|
+
* verbatim refined task yields services=[Hono RPC client], urls=[], packages=[any,api,hc]).
|
|
40
|
+
* Were `service` treated as source-capable for semantics, the subject string "Hono RPC
|
|
41
|
+
* client" would match the package `hono` and the fatal bullet would pass unflagged — the
|
|
42
|
+
* detector would be unable to catch the very defect it exists for.
|
|
43
|
+
*
|
|
44
|
+
* A bullet is FLAGGED iff all three hold:
|
|
45
|
+
* 1. it carries an attribution cue ("per ... LIVE data", "the external context
|
|
46
|
+
* confirms", "docs confirm", "per the official docs", ...);
|
|
47
|
+
* 2. it asserts API usage semantics — how something is called, what a parameter means,
|
|
48
|
+
* what a default is, what behaviour results — as opposed to a version or a status;
|
|
49
|
+
* 3. no `### url:` or `### docs:` block exists for any package the bullet names.
|
|
50
|
+
*
|
|
51
|
+
* Pure and side-effect free; unit-tested in context-attribution.test.ts against the real
|
|
52
|
+
* run-15 bullets, including the three legitimate attributed bullets (TASK_0007, _0012,
|
|
53
|
+
* _0031) that must NOT be flagged.
|
|
54
|
+
*/
|
|
55
|
+
/** Block kinds whose body is retrieved prose/declarations, so they can carry semantics. */
|
|
56
|
+
const SOURCE_CAPABLE_KINDS = new Set(['docs', 'url']);
|
|
57
|
+
/**
|
|
58
|
+
* Phrases by which a bullet claims its content came from somewhere authoritative.
|
|
59
|
+
* Deliberately narrow: each must assert PROVENANCE. "is pinned at" is not a cue — it is
|
|
60
|
+
* an ordinary statement of fact the worker can verify by reading package.json.
|
|
61
|
+
*/
|
|
62
|
+
const ATTRIBUTION_CUES = [
|
|
63
|
+
/per\s+[^.,;]{0,60}\bLIVE\s+data\b/i,
|
|
64
|
+
/\bLIVE\s+data\b/i,
|
|
65
|
+
/\bthe\s+external\s+context\s+confirms\b/i,
|
|
66
|
+
/\bexternal\s+context\s+confirms\b/i,
|
|
67
|
+
/\bper\s+the\s+EXTERNAL\s+CONTEXT\b/i,
|
|
68
|
+
/\bthe\s+EXTERNAL\s+CONTEXT\s+(block|documentation)\b/i,
|
|
69
|
+
/\bdocs?\s+confirms?\b/i,
|
|
70
|
+
/\bper\s+the\s+official\s+docs?\b/i,
|
|
71
|
+
/\baccording\s+to\s+(the\s+)?(official\s+)?docs?\b/i,
|
|
72
|
+
/\bthe\s+docs?\s+says?\b/i,
|
|
73
|
+
/\bper\s+[A-Z][\w./-]*\s+docs\b/
|
|
74
|
+
];
|
|
75
|
+
/**
|
|
76
|
+
* Markers that a bullet is asserting how an API BEHAVES, rather than what version it is.
|
|
77
|
+
* A version/status claim under an attribution is legitimate — that is exactly what the
|
|
78
|
+
* npm block is for — so those must not trip the detector.
|
|
79
|
+
*/
|
|
80
|
+
const SEMANTICS_MARKERS = [
|
|
81
|
+
/\bbase\s*URL\b/i,
|
|
82
|
+
/\bdefaults?\s+to\b/i,
|
|
83
|
+
/\bby\s+default\b/i,
|
|
84
|
+
/\bmust\s+be\s+(a\s+)?(relative|absolute)\b/i,
|
|
85
|
+
/\bworks?\s+correctly\b/i,
|
|
86
|
+
/\bbehaves?\b/i,
|
|
87
|
+
/\breturns?\b/i,
|
|
88
|
+
/\baccepts?\b/i,
|
|
89
|
+
/\btakes?\s+(a|an|two|three|the)\b/i,
|
|
90
|
+
/\bis\s+called\s+with\b/i,
|
|
91
|
+
/\bpass(es|ed|ing)?\s+(a|an|the|it)\b/i,
|
|
92
|
+
/\bparameter\b/i,
|
|
93
|
+
/\bargument\b/i,
|
|
94
|
+
/\bsignature\b/i,
|
|
95
|
+
/\bautomatically\b/i,
|
|
96
|
+
/\bsupports?\b/i,
|
|
97
|
+
/\brequires?\b/i,
|
|
98
|
+
/\bresolves?\b/i
|
|
99
|
+
];
|
|
100
|
+
/** A claim that is ONLY about a version or release status — never a semantics claim. */
|
|
101
|
+
const VERSION_ONLY = [
|
|
102
|
+
/\blatest\s+is\b/i,
|
|
103
|
+
/\bis\s+the\s+latest\b/i,
|
|
104
|
+
/\blive\s+registry\s+shows\b/i,
|
|
105
|
+
/\bnpm\s+latest\b/i,
|
|
106
|
+
/\bversion\s+is\b/i,
|
|
107
|
+
/\bis\s+at\s+version\b/i,
|
|
108
|
+
/\bpinned\s+at\b/i
|
|
109
|
+
];
|
|
110
|
+
function firstMatch(text, patterns) {
|
|
111
|
+
for (const p of patterns) {
|
|
112
|
+
const m = p.exec(text);
|
|
113
|
+
if (m)
|
|
114
|
+
return m[0];
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Split a CONTEXT section into bullets WITH their line ranges. Continuation lines are
|
|
120
|
+
* folded into the bullet above so a hard-wrapped claim is judged as one sentence — which
|
|
121
|
+
* is exactly how the fatal run-15 bullet was written.
|
|
122
|
+
*/
|
|
123
|
+
export function splitBulletSpans(context) {
|
|
124
|
+
const spans = [];
|
|
125
|
+
const lines = context.split('\n');
|
|
126
|
+
for (let i = 0; i < lines.length; i++) {
|
|
127
|
+
const line = lines[i].trimEnd();
|
|
128
|
+
const marker = /^(\s*)[-*]\s+/.exec(line);
|
|
129
|
+
if (marker) {
|
|
130
|
+
spans.push({
|
|
131
|
+
text: line.slice(marker[0].length).trim(),
|
|
132
|
+
startLine: i,
|
|
133
|
+
endLine: i,
|
|
134
|
+
indent: marker[1]
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
else if (spans.length > 0 && line.trim().length > 0 && !/^#/.test(line)) {
|
|
138
|
+
const last = spans[spans.length - 1];
|
|
139
|
+
last.text += ' ' + line.trim();
|
|
140
|
+
last.endLine = i;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return spans.filter(s => s.text.length > 0);
|
|
144
|
+
}
|
|
145
|
+
/** Split a CONTEXT section into its bullets, joining hard-wrapped continuation lines. */
|
|
146
|
+
export function splitBullets(context) {
|
|
147
|
+
return splitBulletSpans(context).map(s => s.text);
|
|
148
|
+
}
|
|
149
|
+
/** Parse the `### npm:` / `### docs:` / `### url:` / `### service:` blocks out of an EXTERNAL CONTEXT header. */
|
|
150
|
+
export function parseContextBlocks(externalContext) {
|
|
151
|
+
const out = [];
|
|
152
|
+
const re = /^###\s+(npm|docs|url|service|freshness-check)\s*:?\s*(.*)$/gim;
|
|
153
|
+
let m;
|
|
154
|
+
while ((m = re.exec(externalContext)) !== null) {
|
|
155
|
+
const kind = m[1].toLowerCase();
|
|
156
|
+
out.push({
|
|
157
|
+
kind: kind === 'freshness-check' ? 'freshness-skipped' : kind,
|
|
158
|
+
subject: m[2].trim()
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Does any block exist that COULD source a semantics claim about `pkg`? Only retrieved
|
|
165
|
+
* documentation text can: a `### docs:` chunk or a `### url:` page body. An npm block
|
|
166
|
+
* carries versions and nothing else, and a service block carries search-result snippets
|
|
167
|
+
* (see the header note). A url block matches on hostname/path containing the package
|
|
168
|
+
* name, which is how the design's own reference list reads (hono.dev/docs/guides/rpc
|
|
169
|
+
* for `hono`).
|
|
170
|
+
*/
|
|
171
|
+
function hasSourceCapableBlock(pkg, blocks) {
|
|
172
|
+
const base = pkg
|
|
173
|
+
.replace(/^@[^/]+\//, '')
|
|
174
|
+
.replace(/^@/, '')
|
|
175
|
+
.toLowerCase();
|
|
176
|
+
return blocks.some(b => {
|
|
177
|
+
if (!SOURCE_CAPABLE_KINDS.has(b.kind))
|
|
178
|
+
return false;
|
|
179
|
+
const s = b.subject.toLowerCase();
|
|
180
|
+
return s.includes(base) || s.includes(pkg.toLowerCase());
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Find bullets that assert external API semantics under an attribution no available
|
|
185
|
+
* block can support.
|
|
186
|
+
*
|
|
187
|
+
* @param context the emitted CONTEXT section text
|
|
188
|
+
* @param externalContext the EXTERNAL CONTEXT header actually passed to that worker
|
|
189
|
+
* @param packages dependency names to look for in a bullet (from package.json)
|
|
190
|
+
*/
|
|
191
|
+
export function findUnsourcedAttributions(context, externalContext, packages) {
|
|
192
|
+
const blocks = parseContextBlocks(externalContext);
|
|
193
|
+
const findings = [];
|
|
194
|
+
for (const bullet of splitBullets(context)) {
|
|
195
|
+
const cue = firstMatch(bullet, ATTRIBUTION_CUES);
|
|
196
|
+
if (cue === null)
|
|
197
|
+
continue;
|
|
198
|
+
const semantics = firstMatch(bullet, SEMANTICS_MARKERS);
|
|
199
|
+
if (semantics === null)
|
|
200
|
+
continue;
|
|
201
|
+
// A bullet that ONLY talks about versions is legitimately sourced by an npm
|
|
202
|
+
// block, even though "supports"/"is" may look like a semantics marker.
|
|
203
|
+
const versionOnly = firstMatch(bullet, VERSION_ONLY) !== null;
|
|
204
|
+
const semanticsBeyondVersion = SEMANTICS_MARKERS.filter(p => p.test(bullet)).length;
|
|
205
|
+
if (versionOnly && semanticsBeyondVersion <= 1)
|
|
206
|
+
continue;
|
|
207
|
+
const named = packages.filter(p => {
|
|
208
|
+
const base = p.replace(/^@[^/]+\//, '');
|
|
209
|
+
return (new RegExp(`\\b${escapeRegExp(p)}\\b`, 'i').test(bullet)
|
|
210
|
+
|| new RegExp(`\\b${escapeRegExp(base)}\\b`, 'i').test(bullet));
|
|
211
|
+
});
|
|
212
|
+
if (named.length === 0)
|
|
213
|
+
continue;
|
|
214
|
+
const unsourced = named.filter(p => !hasSourceCapableBlock(p, blocks));
|
|
215
|
+
if (unsourced.length === 0)
|
|
216
|
+
continue;
|
|
217
|
+
findings.push({ bullet, cue, semantics, unsourced });
|
|
218
|
+
}
|
|
219
|
+
return findings;
|
|
220
|
+
}
|
|
221
|
+
function escapeRegExp(s) {
|
|
222
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Strip every attribution cue from a bullet. Applied repeatedly because one sentence can
|
|
226
|
+
* carry two overlapping cues (the fatal bullet carried "the external context confirms"
|
|
227
|
+
* AND "per Hono RPC docs LIVE data"), and because removing one can expose another.
|
|
228
|
+
*/
|
|
229
|
+
function stripAttributionCues(bullet) {
|
|
230
|
+
let out = bullet;
|
|
231
|
+
for (let pass = 0; pass < 4 && firstMatch(out, ATTRIBUTION_CUES) !== null; pass++) {
|
|
232
|
+
for (const p of ATTRIBUTION_CUES) {
|
|
233
|
+
out = out.replace(new RegExp(p.source, p.flags.includes('g') ? p.flags : p.flags + 'g'), '');
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return out
|
|
237
|
+
.replace(/\(\s*[),.;]?\s*\)/g, '')
|
|
238
|
+
.replace(/\s+([.,;:)])/g, '$1')
|
|
239
|
+
.replace(/,\s*([.;])/g, '$1')
|
|
240
|
+
.replace(/\s{2,}/g, ' ')
|
|
241
|
+
.trim()
|
|
242
|
+
.replace(/^[,;.\s]+/, '');
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Rewrite every bullet that findUnsourcedAttributions flags into an OPEN QUESTION, in
|
|
246
|
+
* place, leaving every other byte of the section alone.
|
|
247
|
+
*
|
|
248
|
+
* DEMOTE, DO NOT DELETE. PROMPT 1 allows either, and its invariant is that neither the
|
|
249
|
+
* bullet count nor the count of legitimately-sourced bullets may collapse — "a worker
|
|
250
|
+
* silenced into saying nothing is a regression, not a fix". Demotion satisfies that
|
|
251
|
+
* mechanically: one flagged bullet becomes exactly one bullet, so the count is invariant,
|
|
252
|
+
* and the observation survives for the grill to ask about instead of reaching compose as
|
|
253
|
+
* fact. The attribution cue is removed, which is what makes the claim stop reading as
|
|
254
|
+
* sourced — and it also makes the rewrite idempotent, since the cue was condition (i).
|
|
255
|
+
*
|
|
256
|
+
* @param context the emitted CONTEXT section text
|
|
257
|
+
* @param externalContext the EXTERNAL CONTEXT header actually passed to that worker
|
|
258
|
+
* @param packages dependency names to look for in a bullet (from package.json)
|
|
259
|
+
*/
|
|
260
|
+
export function demoteUnsourcedAttributions(context, externalContext, packages) {
|
|
261
|
+
const demoted = findUnsourcedAttributions(context, externalContext, packages);
|
|
262
|
+
if (demoted.length === 0)
|
|
263
|
+
return { text: context, demoted: [] };
|
|
264
|
+
const byText = new Map(demoted.map(f => [f.bullet, f]));
|
|
265
|
+
const lines = context.split('\n');
|
|
266
|
+
const out = [];
|
|
267
|
+
let cursor = 0;
|
|
268
|
+
for (const span of splitBulletSpans(context)) {
|
|
269
|
+
const finding = byText.get(span.text);
|
|
270
|
+
if (!finding)
|
|
271
|
+
continue;
|
|
272
|
+
out.push(...lines.slice(cursor, span.startLine));
|
|
273
|
+
const stripped = stripAttributionCues(span.text);
|
|
274
|
+
// If a cue somehow survives four stripping passes the claim is dropped outright
|
|
275
|
+
// rather than re-emitted still wearing its attribution.
|
|
276
|
+
const body = firstMatch(stripped, ATTRIBUTION_CUES) === null ? stripped : '';
|
|
277
|
+
out.push(`${span.indent}- OPEN QUESTION (unsourced API-semantics claim about `
|
|
278
|
+
+ `${finding.unsourced.join(', ')} — worker:context has no documentation tool `
|
|
279
|
+
+ `and no retrieved block supports it; do NOT treat as fact)`
|
|
280
|
+
+ (body.length > 0 ? `: ${body}` : ''));
|
|
281
|
+
cursor = span.endLine + 1;
|
|
282
|
+
}
|
|
283
|
+
out.push(...lines.slice(cursor));
|
|
284
|
+
return { text: out.join('\n'), demoted };
|
|
285
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic classifier for a SILENT worker:context — a rep whose CONTEXT section
|
|
3
|
+
* carries zero parseable bullets. The open question this answers (STEP 0 of the
|
|
4
|
+
* worker:context ZERO-BULLETS thread) is whether a silent rep is a genuine LOSS (the
|
|
5
|
+
* architectural context was there to surface and the worker dropped all of it) or a
|
|
6
|
+
* LEGITIMATE empty answer (the task truly had nothing worth a bullet). Only the former
|
|
7
|
+
* is a defect worth a lever.
|
|
8
|
+
*
|
|
9
|
+
* WHAT THE RECORDED EVIDENCE SHOWED (48 reps, three runs, 2026-07-21/22). Every silent
|
|
10
|
+
* rep — 5/48 — fell into one of two failure shapes, NONE legitimately empty:
|
|
11
|
+
*
|
|
12
|
+
* LOOP_DEGRADE (3/5): the worker thrashed the SAME grep 5× until the loop-killer fired
|
|
13
|
+
* (exit 143), leaving only the degrade banner in place of a section. Verbatim:
|
|
14
|
+
* "(degraded: research CONTEXT worker stuck in a loop — called grep(...) ×5 ...)"
|
|
15
|
+
*
|
|
16
|
+
* GENERATION_GARBAGE (2/5): the worker exited 0 in ~3.6s and emitted a hallucinated
|
|
17
|
+
* non-bullet fragment instead of context. Verbatim: "Users deleted" and
|
|
18
|
+
* "[SYSTEM NOTE This message received a positive feedback reward/+20 ...]"
|
|
19
|
+
*
|
|
20
|
+
* The fixture (mx5 pre-TASK_0027 tree) is IDENTICAL across all reps and non-silent reps
|
|
21
|
+
* reliably emit 11–21 architectural bullets from it, so the input-empty rival is refuted:
|
|
22
|
+
* the content was always there; a silent rep dropped it. Both shapes are therefore genuine
|
|
23
|
+
* loss. This classifier keys on those shapes so the same verdict is reproducible and so a
|
|
24
|
+
* PHASE-1 gate can reuse it to decide when a retry is warranted.
|
|
25
|
+
*/
|
|
26
|
+
/** Why a CONTEXT section came out with no bullets — or that it did not (productive). */
|
|
27
|
+
export type SilenceCause = 'productive' | 'loop-degrade' | 'generation-garbage' | 'legitimately-empty';
|
|
28
|
+
export interface SilenceVerdict {
|
|
29
|
+
bulletCount: number;
|
|
30
|
+
silent: boolean;
|
|
31
|
+
cause: SilenceCause;
|
|
32
|
+
/** A silent rep that dropped context that was there to surface (loop / garbage). */
|
|
33
|
+
genuineLoss: boolean;
|
|
34
|
+
/** The exact substring the verdict keyed on — hand-verifiable in a report. */
|
|
35
|
+
evidence: string;
|
|
36
|
+
}
|
|
37
|
+
/** Bullet lines in an emitted CONTEXT section: leading `-` or `*` markers. */
|
|
38
|
+
export declare function countBullets(contextText: string): number;
|
|
39
|
+
/**
|
|
40
|
+
* Classify one worker:context output. `workerLog` is optional and only consulted for the
|
|
41
|
+
* loop banner, which the degrade machinery writes to the debug log even on the reps where
|
|
42
|
+
* it never reached the persisted section (e.g. a mid-loop SIGTERM before any write).
|
|
43
|
+
*/
|
|
44
|
+
export declare function classifyContextSilence(contextText: string, workerLog?: string): SilenceVerdict;
|
|
45
|
+
/**
|
|
46
|
+
* Wilson score interval for a binomial proportion — the CI STEP 0 reports on the silent
|
|
47
|
+
* and genuine-loss rates. Normal-approximation (Wald) intervals are badly wrong at the
|
|
48
|
+
* small counts and near-boundary rates this measurement lives at; Wilson is not.
|
|
49
|
+
*/
|
|
50
|
+
export declare function wilsonInterval(successes: number, n: number, z?: number): {
|
|
51
|
+
lo: number;
|
|
52
|
+
hi: number;
|
|
53
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic classifier for a SILENT worker:context — a rep whose CONTEXT section
|
|
3
|
+
* carries zero parseable bullets. The open question this answers (STEP 0 of the
|
|
4
|
+
* worker:context ZERO-BULLETS thread) is whether a silent rep is a genuine LOSS (the
|
|
5
|
+
* architectural context was there to surface and the worker dropped all of it) or a
|
|
6
|
+
* LEGITIMATE empty answer (the task truly had nothing worth a bullet). Only the former
|
|
7
|
+
* is a defect worth a lever.
|
|
8
|
+
*
|
|
9
|
+
* WHAT THE RECORDED EVIDENCE SHOWED (48 reps, three runs, 2026-07-21/22). Every silent
|
|
10
|
+
* rep — 5/48 — fell into one of two failure shapes, NONE legitimately empty:
|
|
11
|
+
*
|
|
12
|
+
* LOOP_DEGRADE (3/5): the worker thrashed the SAME grep 5× until the loop-killer fired
|
|
13
|
+
* (exit 143), leaving only the degrade banner in place of a section. Verbatim:
|
|
14
|
+
* "(degraded: research CONTEXT worker stuck in a loop — called grep(...) ×5 ...)"
|
|
15
|
+
*
|
|
16
|
+
* GENERATION_GARBAGE (2/5): the worker exited 0 in ~3.6s and emitted a hallucinated
|
|
17
|
+
* non-bullet fragment instead of context. Verbatim: "Users deleted" and
|
|
18
|
+
* "[SYSTEM NOTE This message received a positive feedback reward/+20 ...]"
|
|
19
|
+
*
|
|
20
|
+
* The fixture (mx5 pre-TASK_0027 tree) is IDENTICAL across all reps and non-silent reps
|
|
21
|
+
* reliably emit 11–21 architectural bullets from it, so the input-empty rival is refuted:
|
|
22
|
+
* the content was always there; a silent rep dropped it. Both shapes are therefore genuine
|
|
23
|
+
* loss. This classifier keys on those shapes so the same verdict is reproducible and so a
|
|
24
|
+
* PHASE-1 gate can reuse it to decide when a retry is warranted.
|
|
25
|
+
*/
|
|
26
|
+
/** Bullet lines in an emitted CONTEXT section: leading `-` or `*` markers. */
|
|
27
|
+
export function countBullets(contextText) {
|
|
28
|
+
return contextText.split('\n').filter(l => /^\s*[-*]\s+/.test(l)).length;
|
|
29
|
+
}
|
|
30
|
+
const LOOP_BANNER = /stuck in a loop/i;
|
|
31
|
+
/** Honest "nothing to surface" — the ONLY non-loss silent shape. */
|
|
32
|
+
const EMPTY_DECLARATION = /^\s*(none|n\/a|no relevant (context|architectural)|nothing\b)/i;
|
|
33
|
+
/**
|
|
34
|
+
* Classify one worker:context output. `workerLog` is optional and only consulted for the
|
|
35
|
+
* loop banner, which the degrade machinery writes to the debug log even on the reps where
|
|
36
|
+
* it never reached the persisted section (e.g. a mid-loop SIGTERM before any write).
|
|
37
|
+
*/
|
|
38
|
+
export function classifyContextSilence(contextText, workerLog = '') {
|
|
39
|
+
const bulletCount = countBullets(contextText);
|
|
40
|
+
if (bulletCount >= 1) {
|
|
41
|
+
return { bulletCount, silent: false, cause: 'productive', genuineLoss: false, evidence: '' };
|
|
42
|
+
}
|
|
43
|
+
const haystack = `${contextText}\n${workerLog}`;
|
|
44
|
+
const loop = LOOP_BANNER.exec(haystack);
|
|
45
|
+
if (loop) {
|
|
46
|
+
// Quote the banner line itself, not just the two matched words.
|
|
47
|
+
const line = haystack
|
|
48
|
+
.split('\n')
|
|
49
|
+
.find(l => LOOP_BANNER.test(l))
|
|
50
|
+
?.trim() ?? loop[0];
|
|
51
|
+
return {
|
|
52
|
+
bulletCount: 0,
|
|
53
|
+
silent: true,
|
|
54
|
+
cause: 'loop-degrade',
|
|
55
|
+
genuineLoss: true,
|
|
56
|
+
evidence: line.slice(0, 240)
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const trimmed = contextText.trim();
|
|
60
|
+
if (trimmed.length === 0 || EMPTY_DECLARATION.test(trimmed)) {
|
|
61
|
+
return {
|
|
62
|
+
bulletCount: 0,
|
|
63
|
+
silent: true,
|
|
64
|
+
cause: 'legitimately-empty',
|
|
65
|
+
genuineLoss: false,
|
|
66
|
+
evidence: trimmed.slice(0, 240)
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
// Non-empty, non-bullet, non-banner, non-declaration → a hallucinated fragment.
|
|
70
|
+
return {
|
|
71
|
+
bulletCount: 0,
|
|
72
|
+
silent: true,
|
|
73
|
+
cause: 'generation-garbage',
|
|
74
|
+
genuineLoss: true,
|
|
75
|
+
evidence: trimmed.slice(0, 240)
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Wilson score interval for a binomial proportion — the CI STEP 0 reports on the silent
|
|
80
|
+
* and genuine-loss rates. Normal-approximation (Wald) intervals are badly wrong at the
|
|
81
|
+
* small counts and near-boundary rates this measurement lives at; Wilson is not.
|
|
82
|
+
*/
|
|
83
|
+
export function wilsonInterval(successes, n, z = 1.959963984540054 // 95%
|
|
84
|
+
) {
|
|
85
|
+
if (n === 0)
|
|
86
|
+
return { lo: 0, hi: 0 };
|
|
87
|
+
const p = successes / n;
|
|
88
|
+
const z2 = z * z;
|
|
89
|
+
const denom = 1 + z2 / n;
|
|
90
|
+
const centre = p + z2 / (2 * n);
|
|
91
|
+
const half = z * Math.sqrt((p * (1 - p) + z2 / (4 * n)) / n);
|
|
92
|
+
return { lo: Math.max(0, (centre - half) / denom), hi: Math.min(1, (centre + half) / denom) };
|
|
93
|
+
}
|