@event4u/agent-config 9.30.0 → 9.32.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +60 -292
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +32 -1
- package/dist/agent-src/rules/delegation-policy.md +11 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/scripts/work_engine/delivery_state.ts +6 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +332 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/refine.ts +106 -1
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/test.ts +50 -15
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +49 -15
- package/dist/agent-src/templates/scripts/work_engine/state.ts +74 -0
- package/dist/agent-src/templates/scripts/work_engine/state_io.ts +6 -0
- package/dist/cli-delegate/chunk-2WELHO3P.js +582 -0
- package/dist/cli-delegate/{chunk-IYTGIG2Y.js → chunk-542DZR4P.js} +118 -148
- package/dist/cli-delegate/{chunk-66OVWGB5.js → chunk-AG47LAGQ.js} +1 -1
- package/dist/cli-delegate/{chunk-MP3JT3IU.js → chunk-DS6BLDMG.js} +1 -1
- package/dist/cli-delegate/{chunk-O3CO5BFB.js → chunk-EJITJBJX.js} +12 -1
- package/dist/cli-delegate/{chunk-EQY4FLJB.js → chunk-ITIQSGOQ.js} +3 -3
- package/dist/cli-delegate/{chunk-LCFJIQ4J.js → chunk-JEJABGKB.js} +110 -6
- package/dist/cli-delegate/chunk-PZWRPRWP.js +38 -0
- package/dist/cli-delegate/{chunk-BTJNRUX3.js → chunk-QEEB4LHO.js} +1 -1
- package/dist/cli-delegate/{chunk-DDUNNFYX.js → chunk-UVKNIJH2.js} +2 -2
- package/dist/cli-delegate/cmd_analyze_session.js +1 -1
- package/dist/cli-delegate/cmd_brand_status.js +1 -1
- package/dist/cli-delegate/cmd_conformance.js +154 -41
- package/dist/cli-delegate/cmd_doctor.js +4 -3
- package/dist/cli-delegate/cmd_explain.js +2 -2
- package/dist/cli-delegate/cmd_export.js +4 -4
- package/dist/cli-delegate/cmd_handoff.js +168 -59
- package/dist/cli-delegate/cmd_mcp_available.js +1 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_packs_active.js +2 -2
- package/dist/cli-delegate/cmd_prune.js +4 -4
- package/dist/cli-delegate/cmd_route_audit.js +2 -2
- package/dist/cli-delegate/cmd_route_explain.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +19 -4
- package/dist/cli-delegate/cmd_settings_get.js +2 -55
- package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +4 -4
- package/dist/cli-delegate/cmd_uninstall.js +4 -4
- package/dist/cli-delegate/cmd_update.js +1 -1
- package/dist/cli-delegate/cmd_upgrade.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +4 -4
- package/dist/cli-delegate/cmd_versions.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +3 -3
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +1411 -810
- package/dist/install/install.mjs +121 -16
- package/dist/mcp/registry-manifest.json +1 -1
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +365 -143
- package/dist/scripts/_lib/agent_settings.js +111 -2
- package/dist/scripts/_lib/agent_settings.js.map +1 -1
- package/dist/server/schemas/settings.js +2 -2
- package/dist/server/schemas/settings.js.map +1 -1
- package/package.json +1 -1
- package/src/agent-src/templates/scripts/work_engine/delivery_state.ts +6 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +332 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/refine.ts +106 -1
- package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +50 -15
- package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +49 -15
- package/src/agent-src/templates/scripts/work_engine/state.ts +74 -0
- package/src/agent-src/templates/scripts/work_engine/state_io.ts +6 -0
- package/src/config/pack-size-budget.json +3 -2
- package/src/scripts/_cli/cmd_session_recycle.ts +25 -0
- package/src/scripts/_cli/explain_last/inputs.ts +13 -0
- package/src/scripts/_cli/handoff_generate.ts +36 -1
- package/src/scripts/_cli/handoff_sessions.ts +140 -9
- package/src/scripts/_lib/agent_settings.ts +119 -2
- package/src/scripts/_lib/carrier_divergence.ts +120 -2
- package/src/scripts/_lib/envelope_grounding.ts +206 -0
- package/src/scripts/_lib/host_capability.ts +79 -0
- package/src/scripts/_lib/session_eol.ts +85 -0
- package/src/scripts/_lib/subagent_capsule.ts +256 -1
- package/src/scripts/check_secret_leak.test.ts +43 -0
- package/src/scripts/check_secret_leak.ts +12 -0
- package/src/scripts/handoff_context_hook.ts +66 -18
- package/src/scripts/hooks/session_eol_hook.ts +12 -12
- package/src/scripts/hooks/turn_end_gate_hook.ts +296 -88
- package/src/scripts/language_mirror_hook.ts +69 -16
- package/src/scripts/measure_turn_end_gate.ts +129 -51
- package/src/scripts/report_carrier_divergence.ts +143 -30
- package/src/scripts/report_conformance_funnel.ts +18 -4
- package/src/scripts/routing_doctor.ts +25 -2
- package/src/scripts/schemas/dod.schema.json +45 -0
- package/src/server/schemas/settings.ts +2 -2
- package/dist/cli-delegate/chunk-RVGBH4XK.js +0 -142
|
@@ -72,7 +72,7 @@ import fs from "node:fs";
|
|
|
72
72
|
import path from "node:path";
|
|
73
73
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
74
74
|
|
|
75
|
-
import { atomic_write_json } from "./hooks/state_io.js";
|
|
75
|
+
import { atomic_write_json, atomic_write_text, is_replay_mode } from "./hooks/state_io.js";
|
|
76
76
|
import { readHookStdin } from "./hooks/hook_stdin.js";
|
|
77
77
|
import { humanAuthoredLead, isSyntheticPrompt } from "./_lib/prompt_shape.js";
|
|
78
78
|
|
|
@@ -92,7 +92,21 @@ export const STATE_FILE = path.join("agents", "state", "language-mirror.json");
|
|
|
92
92
|
* re-emit that follows it (round-5 § 6.1). Its existence — not a counter — is
|
|
93
93
|
* what bounds the re-injection to ONE per compaction event.
|
|
94
94
|
*/
|
|
95
|
-
export const
|
|
95
|
+
export const PIN_LOST_DIR = path.join("agents", "state", "language-mirror.pin-lost");
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Per-SESSION marker path. R2 finding 10: a single shared marker made the
|
|
99
|
+
* mechanism cross-talk between concurrent sessions — the normal shape for this
|
|
100
|
+
* repo's worktree workflow. Session B's next `post_tool_use` consumed the marker
|
|
101
|
+
* session A's compaction had set, so A never got the one re-emit that is the
|
|
102
|
+
* whole point of the mechanism, and B got a pin notice derived from whichever
|
|
103
|
+
* session last wrote the shared state. An empty id degrades to a literal
|
|
104
|
+
* bucket rather than colliding silently.
|
|
105
|
+
*/
|
|
106
|
+
export function pinLostMarker(session_id: string): string {
|
|
107
|
+
const safe = (session_id || "unknown-session").replace(/[^A-Za-z0-9._-]/g, "_");
|
|
108
|
+
return path.join(PIN_LOST_DIR, `${safe}.marker`);
|
|
109
|
+
}
|
|
96
110
|
|
|
97
111
|
type JsonValue = string | number | boolean | null | JsonValue[] | { [k: string]: JsonValue };
|
|
98
112
|
type JsonObject = { [k: string]: JsonValue };
|
|
@@ -387,29 +401,68 @@ function _extractPrompt(payload: JsonObject): string {
|
|
|
387
401
|
return "";
|
|
388
402
|
}
|
|
389
403
|
|
|
390
|
-
/**
|
|
391
|
-
|
|
404
|
+
/** The envelope's session id, or a literal bucket when the host sends none. */
|
|
405
|
+
function _sessionId(envelope: JsonObject): string {
|
|
406
|
+
const v = envelope["session_id"];
|
|
407
|
+
return typeof v === "string" && v.trim() !== "" ? v : "unknown-session";
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Record that compaction is about to drop the context copy of the pin.
|
|
412
|
+
*
|
|
413
|
+
* R2 finding 14: this used to write with a raw `mkdirSync` + `writeFileSync`,
|
|
414
|
+
* bypassing both conventions the surrounding code follows — the module's own
|
|
415
|
+
* atomic writer, and the `is_replay_mode()` guard that keeps a replayed event
|
|
416
|
+
* from mutating live state. A replayed compaction wrote the marker for real.
|
|
417
|
+
*/
|
|
418
|
+
/**
|
|
419
|
+
* The pre-R2 layout wrote a single FILE at the path that is now a DIRECTORY.
|
|
420
|
+
* A consumer upgrading with a leftover legacy marker — compaction happened, no
|
|
421
|
+
* `post_tool_use` followed — hits `EEXIST` on the mkdir and `ENOTDIR` on the
|
|
422
|
+
* write (reproduced against Node), which the catch below swallows as
|
|
423
|
+
* "observability only". The failure would then be silent AND permanent for that
|
|
424
|
+
* root: `existsSync` always false, so the mechanism is dead with no diagnostic.
|
|
425
|
+
* One unlink at the moment of collision, and only when the path is a file.
|
|
426
|
+
*/
|
|
427
|
+
function _migrateLegacyMarker(consumer_root: string): void {
|
|
428
|
+
const legacy = path.join(consumer_root, PIN_LOST_DIR);
|
|
429
|
+
try {
|
|
430
|
+
if (fs.statSync(legacy).isFile()) {
|
|
431
|
+
fs.rmSync(legacy, { force: true });
|
|
432
|
+
}
|
|
433
|
+
} catch {
|
|
434
|
+
// Absent, already a directory, or unreadable — nothing to migrate.
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export function _setPinLost(consumer_root: string, session_id: string): void {
|
|
439
|
+
if (is_replay_mode()) return;
|
|
440
|
+
_migrateLegacyMarker(consumer_root);
|
|
392
441
|
try {
|
|
393
|
-
|
|
394
|
-
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
395
|
-
fs.writeFileSync(target, new Date().toISOString());
|
|
442
|
+
atomic_write_text(path.join(consumer_root, pinLostMarker(session_id)), new Date().toISOString());
|
|
396
443
|
} catch {
|
|
397
444
|
// Observability only — a failed marker write costs one un-restored pin,
|
|
398
445
|
// never a broken turn.
|
|
399
446
|
}
|
|
400
447
|
}
|
|
401
448
|
|
|
402
|
-
export function _pinLost(consumer_root: string): boolean {
|
|
449
|
+
export function _pinLost(consumer_root: string, session_id: string): boolean {
|
|
403
450
|
try {
|
|
404
|
-
return fs.existsSync(path.join(consumer_root,
|
|
451
|
+
return fs.existsSync(path.join(consumer_root, pinLostMarker(session_id)));
|
|
405
452
|
} catch {
|
|
406
453
|
return false;
|
|
407
454
|
}
|
|
408
455
|
}
|
|
409
456
|
|
|
410
|
-
function _clearPinLost(consumer_root: string): void {
|
|
457
|
+
function _clearPinLost(consumer_root: string, session_id: string): void {
|
|
458
|
+
// R2 round 2, finding 8: the replay guard covered only the WRITE. A replayed
|
|
459
|
+
// `post_tool_use` therefore deleted a live marker it was forbidden to create,
|
|
460
|
+
// and the real session lost the single re-emit this whole mechanism exists to
|
|
461
|
+
// deliver — replay destroying live state while blocked from writing it, which
|
|
462
|
+
// is worse than either direction alone.
|
|
463
|
+
if (is_replay_mode()) return;
|
|
411
464
|
try {
|
|
412
|
-
fs.rmSync(path.join(consumer_root,
|
|
465
|
+
fs.rmSync(path.join(consumer_root, pinLostMarker(session_id)), { force: true });
|
|
413
466
|
} catch {
|
|
414
467
|
// If it cannot be cleared the next tool call re-emits once more. One
|
|
415
468
|
// duplicate line is the failure mode; a loop is not, because the marker
|
|
@@ -422,8 +475,8 @@ function _clearPinLost(consumer_root: string): void {
|
|
|
422
475
|
* and only when a pin actually exists — an absent or undetermined pin is not
|
|
423
476
|
* an obligation, and inventing one here would be worse than the gap.
|
|
424
477
|
*/
|
|
425
|
-
export function _reEmitAfterCompaction(consumer_root: string): number {
|
|
426
|
-
if (!_pinLost(consumer_root)) {
|
|
478
|
+
export function _reEmitAfterCompaction(consumer_root: string, session_id: string): number {
|
|
479
|
+
if (!_pinLost(consumer_root, session_id)) {
|
|
427
480
|
return EXIT_ALLOW;
|
|
428
481
|
}
|
|
429
482
|
const previous = _loadState(path.join(consumer_root, STATE_FILE));
|
|
@@ -431,7 +484,7 @@ export function _reEmitAfterCompaction(consumer_root: string): number {
|
|
|
431
484
|
const source: PinSource = previous.source === "system-locale" ? "system-locale" : "prompt";
|
|
432
485
|
// Clear FIRST: if the write below throws, the marker must not survive to
|
|
433
486
|
// re-fire on every subsequent tool call — that is the shape § 6.2 refuses.
|
|
434
|
-
_clearPinLost(consumer_root);
|
|
487
|
+
_clearPinLost(consumer_root, session_id);
|
|
435
488
|
if (language !== "de" && language !== "en") {
|
|
436
489
|
return EXIT_ALLOW;
|
|
437
490
|
}
|
|
@@ -479,11 +532,11 @@ export function run(
|
|
|
479
532
|
// and this marker is the entire difference between the two.
|
|
480
533
|
const event = typeof envelope["event"] === "string" ? envelope["event"] : "";
|
|
481
534
|
if (event === "pre_compact") {
|
|
482
|
-
_setPinLost(options.consumer_root);
|
|
535
|
+
_setPinLost(options.consumer_root, _sessionId(envelope));
|
|
483
536
|
return EXIT_ALLOW;
|
|
484
537
|
}
|
|
485
538
|
if (event === "post_tool_use") {
|
|
486
|
-
return _reEmitAfterCompaction(options.consumer_root);
|
|
539
|
+
return _reEmitAfterCompaction(options.consumer_root, _sessionId(envelope));
|
|
487
540
|
}
|
|
488
541
|
|
|
489
542
|
const prompt = _extractPrompt(payload);
|
|
@@ -11,37 +11,61 @@
|
|
|
11
11
|
*
|
|
12
12
|
* What it measures, and what it deliberately does NOT:
|
|
13
13
|
*
|
|
14
|
-
* · FIRE RATE of each detector over
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
14
|
+
* · FIRE RATE of each detector PER TURN, over the last assistant text of the
|
|
15
|
+
* turn — the population the shipped gate inspects. R2 finding 1: the first
|
|
16
|
+
* version scored every assistant ENTRY and published the result as a
|
|
17
|
+
* per-turn rate, which inflated the population ~8.6x and scored a
|
|
18
|
+
* closing-paragraph detector over mid-turn prose.
|
|
19
|
+
* · The independent scanner's own language total, printed for CONTEXT only.
|
|
20
|
+
* It is not an agreement figure and no overlap is claimed from it: the two
|
|
21
|
+
* instruments read different spans (its first prose line; this one strips
|
|
22
|
+
* code/quotes/paths from the reply and then scores lead-first via the same
|
|
23
|
+
* classifier), and this script does no per-turn matching between them.
|
|
24
|
+
* R2 finding 7 removed the per-session min/max arithmetic that used to
|
|
25
|
+
* report disjoint findings of equal count as full agreement.
|
|
20
26
|
* · It does NOT compute recall against the 20 promissory occurrences the
|
|
21
27
|
* round-5 audit reported. Those came from subagent transcript READING;
|
|
22
28
|
* `conformance_scan.ts` states in its own header that promissory
|
|
23
29
|
* closings are "deliberately NOT scanned", so there is no machine-
|
|
24
30
|
* readable ground truth to score against. Claiming a recall figure here
|
|
25
31
|
* would invent one.
|
|
32
|
+
* · KNOWN RESIDUAL, R2 finding 8, not fixed here: this script re-derives the
|
|
33
|
+
* language pin with `classify` per user entry, while the gate reads the pin
|
|
34
|
+
* `language_mirror_hook` persisted — which also applies a system-locale
|
|
35
|
+
* fallback and a keep-previous rule. Where those diverge, a turn can move
|
|
36
|
+
* between "pinned de", "pinned en" and "no obligation", so detector B's
|
|
37
|
+
* rate here is close to but not identical with the gate's. Reaching parity
|
|
38
|
+
* means reusing `nextState`, which is a larger change than a measurement
|
|
39
|
+
* correction and would itself need re-measuring.
|
|
26
40
|
*
|
|
27
41
|
* Usage:
|
|
28
42
|
* ./scripts-run src/scripts/measure_turn_end_gate --store <dir> [--limit N]
|
|
29
43
|
*/
|
|
30
44
|
import * as fs from 'node:fs';
|
|
31
45
|
import * as path from 'node:path';
|
|
46
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
32
47
|
|
|
33
|
-
import { classify } from './language_mirror_hook.js';
|
|
34
|
-
import { assistantText,
|
|
48
|
+
import { classify, isSyntheticPrompt } from './language_mirror_hook.js';
|
|
49
|
+
import { assistantText, scanSession, userText } from './conformance_scan.js';
|
|
35
50
|
import { detectLanguage, detectPromissory } from './hooks/turn_end_gate_hook.js';
|
|
36
51
|
|
|
37
52
|
interface Counts {
|
|
38
|
-
|
|
53
|
+
/**
|
|
54
|
+
* TURNS, not entries. R2 finding 1: the first version incremented a counter
|
|
55
|
+
* named `assistant_turns` on every assistant entry and ran both detectors on
|
|
56
|
+
* each one, while the gate only ever evaluates the LAST assistant text of a
|
|
57
|
+
* turn. A turn with k prose entries contributed k samples to the instrument
|
|
58
|
+
* and 1 to the gate, so the published "18.0% of turns" was a per-ENTRY rate
|
|
59
|
+
* wearing a per-turn label — and detector A, a closing-paragraph detector by
|
|
60
|
+
* construction, was being scored over mid-turn prose.
|
|
61
|
+
*/
|
|
62
|
+
turns: number;
|
|
63
|
+
/** Assistant entries seen — kept only to show the ratio the old bug hid. */
|
|
64
|
+
assistant_entries: number;
|
|
39
65
|
promissory_fires: number;
|
|
40
66
|
language_fires: number;
|
|
41
|
-
/**
|
|
42
|
-
|
|
43
|
-
/** Detector B fired where the scanner saw nothing — candidate false positives. */
|
|
44
|
-
language_only: number;
|
|
67
|
+
/** The independent scanner's own per-session language total, for context. */
|
|
68
|
+
scanner_language: number;
|
|
45
69
|
sessions: number;
|
|
46
70
|
}
|
|
47
71
|
|
|
@@ -68,11 +92,11 @@ function sessionFiles(store: string, limit: number): string[] {
|
|
|
68
92
|
|
|
69
93
|
export function measure(store: string, limit: number): Counts {
|
|
70
94
|
const c: Counts = {
|
|
71
|
-
|
|
95
|
+
turns: 0,
|
|
96
|
+
assistant_entries: 0,
|
|
72
97
|
promissory_fires: 0,
|
|
73
98
|
language_fires: 0,
|
|
74
|
-
|
|
75
|
-
language_only: 0,
|
|
99
|
+
scanner_language: 0,
|
|
76
100
|
sessions: 0,
|
|
77
101
|
};
|
|
78
102
|
|
|
@@ -86,20 +110,36 @@ export function measure(store: string, limit: number): Counts {
|
|
|
86
110
|
c.sessions += 1;
|
|
87
111
|
const sessionId = path.basename(file).slice(0, 8);
|
|
88
112
|
|
|
89
|
-
// The scanner's
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
|
|
113
|
+
// The independent scanner's own language total for this session,
|
|
114
|
+
// reported side by side and NOT as agreement — see the header for why
|
|
115
|
+
// no overlap is derivable from two totals (R2 finding 7).
|
|
116
|
+
//
|
|
117
|
+
// A throw is surfaced, not swallowed to zero: swallowing silently
|
|
118
|
+
// reclassified a whole session's fires as scanner-disagreements.
|
|
93
119
|
try {
|
|
94
|
-
|
|
120
|
+
c.scanner_language += scanSession(sessionId, lines).violations.filter(
|
|
95
121
|
(v) => v.check === 'language-pin',
|
|
96
122
|
).length;
|
|
97
|
-
} catch {
|
|
98
|
-
|
|
123
|
+
} catch (err) {
|
|
124
|
+
process.stderr.write(
|
|
125
|
+
`measure-turn-end-gate: scanSession failed for ${sessionId} — excluded from the scanner total: ${String(err)}\n`,
|
|
126
|
+
);
|
|
99
127
|
}
|
|
100
128
|
|
|
101
129
|
let pinned: 'de' | 'en' | 'und' = 'und';
|
|
102
|
-
|
|
130
|
+
// The LAST assistant text since the last genuine user prompt. This is
|
|
131
|
+
// the population the gate inspects; anything else is a different
|
|
132
|
+
// measurement wearing the same label (R2 finding 1).
|
|
133
|
+
let pendingReply: string | null = null;
|
|
134
|
+
let pendingPin: 'de' | 'en' | 'und' = 'und';
|
|
135
|
+
|
|
136
|
+
const scoreTurn = (): void => {
|
|
137
|
+
if (pendingReply === null) return;
|
|
138
|
+
c.turns += 1;
|
|
139
|
+
if (detectPromissory(pendingReply) !== null) c.promissory_fires += 1;
|
|
140
|
+
if (detectLanguage(pendingReply, pendingPin) !== null) c.language_fires += 1;
|
|
141
|
+
pendingReply = null;
|
|
142
|
+
};
|
|
103
143
|
|
|
104
144
|
for (const raw of lines) {
|
|
105
145
|
let entry: Record<string, unknown>;
|
|
@@ -111,10 +151,14 @@ export function measure(store: string, limit: number): Counts {
|
|
|
111
151
|
|
|
112
152
|
const u = userText(entry);
|
|
113
153
|
if (u !== null) {
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
|
|
154
|
+
// `isSyntheticPrompt`, the SAME filter the gate uses for the
|
|
155
|
+
// ordinal — not `isInjectedBody`. R2 round 2, finding 10: the
|
|
156
|
+
// two filters classify differently, and any entry they disagree
|
|
157
|
+
// about shifts this instrument's turn boundary away from the
|
|
158
|
+
// gate's own. Population parity is the entire point of this
|
|
159
|
+
// correction, so the filter has to be the same one.
|
|
160
|
+
if (!isSyntheticPrompt(u)) {
|
|
161
|
+
scoreTurn(); // the previous turn ends here
|
|
118
162
|
const verdict = classify(u);
|
|
119
163
|
if (verdict.language !== 'und') pinned = verdict.language;
|
|
120
164
|
}
|
|
@@ -123,39 +167,38 @@ export function measure(store: string, limit: number): Counts {
|
|
|
123
167
|
|
|
124
168
|
const a = assistantText(entry);
|
|
125
169
|
if (a === null || a.trim() === '') continue;
|
|
126
|
-
c.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
c.language_fires += 1;
|
|
131
|
-
myLanguage += 1;
|
|
132
|
-
}
|
|
170
|
+
c.assistant_entries += 1;
|
|
171
|
+
// Overwrite: only the last one in the turn is the gate's input.
|
|
172
|
+
pendingReply = a;
|
|
173
|
+
pendingPin = pinned;
|
|
133
174
|
}
|
|
134
|
-
|
|
135
|
-
const agree = Math.min(myLanguage, scannerLanguage);
|
|
136
|
-
c.language_agree += agree;
|
|
137
|
-
c.language_only += Math.max(0, myLanguage - scannerLanguage);
|
|
175
|
+
scoreTurn(); // the transcript's final turn
|
|
138
176
|
}
|
|
139
177
|
|
|
140
178
|
return c;
|
|
141
179
|
}
|
|
142
180
|
|
|
143
181
|
export function render(c: Counts): string {
|
|
144
|
-
const pct = (n: number): string =>
|
|
145
|
-
c.assistant_turns === 0 ? '0.0' : ((100 * n) / c.assistant_turns).toFixed(1);
|
|
182
|
+
const pct = (n: number): string => (c.turns === 0 ? '0.0' : ((100 * n) / c.turns).toFixed(1));
|
|
146
183
|
return [
|
|
147
|
-
`measure-turn-end-gate · ${c.sessions} sessions · ${c.
|
|
184
|
+
`measure-turn-end-gate · ${c.sessions} sessions · ${c.turns} turns · ${c.assistant_entries} assistant entries`,
|
|
148
185
|
'',
|
|
149
186
|
` detector A (promissory) fires on ${c.promissory_fires} turns (${pct(c.promissory_fires)}%)`,
|
|
150
187
|
` detector B (language) fires on ${c.language_fires} turns (${pct(c.language_fires)}%)`,
|
|
151
|
-
` of which the independent scanner also flagged: ${c.language_agree}`,
|
|
152
|
-
` fired where the scanner saw nothing: ${c.language_only}`,
|
|
153
188
|
'',
|
|
154
|
-
'
|
|
155
|
-
' the
|
|
156
|
-
'
|
|
157
|
-
'
|
|
158
|
-
'
|
|
189
|
+
' Rates are per TURN, over the last assistant text of each turn — the same',
|
|
190
|
+
' population the shipped gate inspects. The entry count is printed beside',
|
|
191
|
+
' the turn count because the first version of this script scored every',
|
|
192
|
+
' assistant ENTRY and labelled the result per-turn; the gap between the two',
|
|
193
|
+
' numbers is the size of that error.',
|
|
194
|
+
'',
|
|
195
|
+
` For context only, the independent scanner's own language total: ${c.scanner_language}`,
|
|
196
|
+
' NOT an agreement figure. The two instruments read different spans (its',
|
|
197
|
+
' first prose line vs. this stripped, lead-first-scored reply) and this',
|
|
198
|
+
' script does no per-turn matching between them, so no overlap can be claimed from these',
|
|
199
|
+
' two totals — the previous version claimed one from per-session min/max',
|
|
200
|
+
' arithmetic, which reports disjoint findings of equal count as full',
|
|
201
|
+
' agreement.',
|
|
159
202
|
'',
|
|
160
203
|
' No recall figure for detector A: promissory closings are not machine-',
|
|
161
204
|
' scanned anywhere in this tree, so there is no ground truth to score.',
|
|
@@ -170,7 +213,18 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
170
213
|
store = argv[i + 1]!;
|
|
171
214
|
i += 1;
|
|
172
215
|
} else if (argv[i] === '--limit' && argv[i + 1] !== undefined) {
|
|
173
|
-
|
|
216
|
+
// R2 finding 15: an unvalidated parseInt made `--limit abc` yield
|
|
217
|
+
// NaN, `slice(0, NaN)` return nothing, and the script exit 2 with
|
|
218
|
+
// "no .jsonl sessions under <store>" — a real input error reported
|
|
219
|
+
// as a missing corpus.
|
|
220
|
+
const parsed = Number.parseInt(argv[i + 1]!, 10);
|
|
221
|
+
if (!Number.isFinite(parsed) || parsed < 1) {
|
|
222
|
+
process.stderr.write(
|
|
223
|
+
`measure-turn-end-gate: --limit must be a positive integer, got ${JSON.stringify(argv[i + 1])}\n`,
|
|
224
|
+
);
|
|
225
|
+
return 2;
|
|
226
|
+
}
|
|
227
|
+
limit = parsed;
|
|
174
228
|
i += 1;
|
|
175
229
|
}
|
|
176
230
|
}
|
|
@@ -189,6 +243,30 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
189
243
|
return 0;
|
|
190
244
|
}
|
|
191
245
|
|
|
192
|
-
|
|
246
|
+
// Bundle-safety, same guard as the sibling hook. R2 finding 15: a substring
|
|
247
|
+
// test on argv[1] auto-runs the module when esbuild inlines it into a bundle,
|
|
248
|
+
// where every module shares the bundle's `import.meta.url`.
|
|
249
|
+
declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
|
|
250
|
+
function _isCliEntry(): boolean {
|
|
251
|
+
if (typeof __AGENT_CONFIG_BUNDLE__ !== 'undefined' && __AGENT_CONFIG_BUNDLE__) {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
if (process.argv[1] === undefined) {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
258
|
+
if (import.meta.url === argvUrl) {
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
try {
|
|
262
|
+
const here = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
263
|
+
const argv = fs.realpathSync(path.resolve(process.argv[1]));
|
|
264
|
+
return here === argv;
|
|
265
|
+
} catch {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (_isCliEntry()) {
|
|
193
271
|
process.exit(main());
|
|
194
272
|
}
|