create-byan-agent 2.25.0 → 2.26.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/CHANGELOG.md +155 -0
- package/README.md +9 -12
- package/install/bin/create-byan-agent-v2.js +29 -169
- package/install/lib/agent-generator.js +5 -5
- package/install/lib/byan-web-integration.js +1 -1
- package/install/lib/claude-native-setup.js +1 -1
- package/install/lib/phase2-chat.js +3 -10
- package/install/lib/platforms/claude-code.js +2 -2
- package/install/lib/platforms/index.js +0 -2
- package/install/lib/project-agents-generator.js +3 -3
- package/install/lib/staging-consent.js +3 -3
- package/install/lib/subagent-generator.js +3 -3
- package/install/lib/yanstaller/agent-launcher.js +1 -27
- package/install/lib/yanstaller/detector.js +4 -4
- package/install/lib/yanstaller/installer.js +0 -2
- package/install/lib/yanstaller/interviewer.js +1 -1
- package/install/lib/yanstaller/platform-selector.js +1 -13
- package/install/package.json +1 -1
- package/install/src/byan-v2/context/session-state.js +2 -2
- package/install/src/byan-v2/index.js +1 -5
- package/install/src/byan-v2/orchestrator/generation-state.js +4 -4
- package/install/src/webui/api.js +0 -2
- package/install/src/webui/chat/bridge.js +1 -13
- package/install/src/webui/chat/cli-detector.js +0 -23
- package/install/src/webui/public/app.js +1 -3
- package/install/src/webui/public/chat.html +0 -2
- package/install/src/webui/public/chat.js +0 -1
- package/install/src/webui/public/index.html +2 -2
- package/install/templates/.claude/CLAUDE.md +13 -2
- package/install/templates/.claude/agents/bmad-byan.md +1 -1
- package/install/templates/.claude/hooks/autobench-stop-guard.js +286 -0
- package/install/templates/.claude/hooks/fact-check-absolutes.js +1 -61
- package/install/templates/.claude/hooks/fact-check-claims.js +69 -0
- package/install/templates/.claude/hooks/fd-response-check.js +37 -46
- package/install/templates/.claude/hooks/inject-soul.js +64 -25
- package/install/templates/.claude/hooks/leantime-fd-sync.js +216 -0
- package/install/templates/.claude/hooks/lib/autobench-config.json +81 -0
- package/install/templates/.claude/hooks/lib/autobench-fc-enrich.js +251 -0
- package/install/templates/.claude/hooks/lib/autobench-ledger-report.js +253 -0
- package/install/templates/.claude/hooks/lib/autobench-runtime.js +199 -0
- package/install/templates/.claude/hooks/lib/fact-check-core.js +69 -0
- package/install/templates/.claude/hooks/lib/transcript-read.js +137 -0
- package/install/templates/.claude/hooks/soul-memory-check.js +49 -25
- package/install/templates/.claude/hooks/soul-memory-triggers.js +27 -8
- package/install/templates/.claude/hooks/stage-to-byan.js +25 -7
- package/install/templates/.claude/hooks/strict-stop-guard.js +4 -16
- package/install/templates/.claude/rules/benchmark.md +251 -0
- package/install/templates/.claude/rules/byan-agents.md +0 -1
- package/install/templates/.claude/rules/byan-api.md +64 -0
- package/install/templates/.claude/rules/fact-check.md +1 -1
- package/install/templates/.claude/rules/strict-mode.md +10 -9
- package/install/templates/.claude/settings.json +12 -0
- package/install/templates/.claude/skills/byan-benchmark/SKILL.md +159 -0
- package/install/templates/.claude/skills/byan-byan/SKILL.md +73 -12
- package/install/templates/.claude/skills/byan-fact-check/SKILL.md +1 -1
- package/install/templates/.claude/skills/byan-hermes-dispatch/SKILL.md +5 -6
- package/install/templates/.claude/skills/byan-orchestrate/SKILL.md +11 -3
- package/install/templates/.claude/skills/byan-strict/SKILL.md +4 -1
- package/install/templates/.claude/workflows/INDEX.md +2 -1
- package/install/templates/.claude/workflows/byan-benchmark.js +328 -0
- package/install/templates/_byan/_config/agent-manifest.csv +1 -1
- package/install/templates/_byan/_config/autobench.yaml +510 -0
- package/install/templates/_byan/_config/strict-mode.yaml +9 -3
- package/install/templates/_byan/_config/workflow-manifest.csv +1 -0
- package/install/templates/_byan/agent/byan/byan.md +1 -3
- package/install/templates/_byan/agent/byan-flat/byan.md +1 -3
- package/install/templates/_byan/agent/byan-test/byan-test.md +2 -2
- package/install/templates/_byan/agent/byan-test-flat/byan-test.md +2 -2
- package/install/templates/_byan/agent/byan.optimized/byan.optimized.md +2 -2
- package/install/templates/_byan/agent/byan.optimized-v2/byan.optimized-v2.md +2 -2
- package/install/templates/_byan/agent/claude/claude.md +0 -2
- package/install/templates/_byan/agent/codex/codex.md +0 -2
- package/install/templates/_byan/agent/rachid/rachid.md +2 -10
- package/install/templates/_byan/agent/rachid-flat/rachid.md +2 -11
- package/install/templates/_byan/agent/turbo-whisper/turbo-whisper.md +2 -5
- package/install/templates/_byan/agent/turbo-whisper-integration/turbo-whisper-integration.md +5 -13
- package/install/templates/_byan/agent/yanstaller/yanstaller.md +2 -24
- package/install/templates/_byan/config.yaml +0 -1
- package/install/templates/_byan/mcp/byan-mcp-server/bin/byan-sync-rules.js +20 -4
- package/install/templates/_byan/mcp/byan-mcp-server/lib/advisory-autofeed.js +13 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/index-generator.js +1 -1
- package/install/templates/_byan/mcp/byan-mcp-server/lib/kanban.js +6 -3
- package/install/templates/_byan/mcp/byan-mcp-server/lib/leantime-fd-core.js +205 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/leantime-sync.js +415 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/precommit-gate.js +1 -1
- package/install/templates/_byan/mcp/byan-mcp-server/lib/strict-activation.js +1 -1
- package/install/templates/_byan/mcp/byan-mcp-server/lib/strict-mode.js +8 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/sync-rules.js +172 -23
- package/install/templates/_byan/mcp/byan-mcp-server/lib/workflows-generator.js +1 -0
- package/install/templates/_byan/mcp/byan-mcp-server/server.js +205 -82
- package/install/templates/_byan/worker/launchers/README.md +4 -24
- package/install/templates/_byan/worker/workers.md +0 -2
- package/install/templates/_byan/workflow/simple/bmb/byan-benchmark/workflow.md +86 -0
- package/install/templates/docs/leantime-integration.md +160 -0
- package/package.json +3 -7
- package/src/byan-v2/context/session-state.js +2 -2
- package/src/byan-v2/generation/mantra-validator.js +3 -3
- package/src/byan-v2/index.js +1 -5
- package/src/byan-v2/integration/voice-integration.js +1 -1
- package/src/byan-v2/orchestrator/generation-state.js +4 -4
- package/src/staging/staging.js +20 -6
- package/install/bin/build-copilot-stubs.js +0 -138
- package/install/lib/platforms/copilot-cli.js +0 -123
- package/install/lib/platforms/vscode.js +0 -51
- package/install/src/byan-v2/context/copilot-context.js +0 -79
- package/install/src/webui/chat/copilot-adapter.js +0 -68
- package/install/templates/.claude/agents/bmad-marc.md +0 -25
- package/install/templates/.claude/skills/byan-marc/SKILL.md +0 -20
- package/install/templates/.github/agents/bmad-agent-bmad-master.md +0 -16
- package/install/templates/.github/agents/bmad-agent-bmb-agent-builder.md +0 -16
- package/install/templates/.github/agents/bmad-agent-bmb-module-builder.md +0 -16
- package/install/templates/.github/agents/bmad-agent-bmb-workflow-builder.md +0 -16
- package/install/templates/.github/agents/bmad-agent-bmm-analyst.md +0 -16
- package/install/templates/.github/agents/bmad-agent-bmm-architect.md +0 -16
- package/install/templates/.github/agents/bmad-agent-bmm-dev.md +0 -16
- package/install/templates/.github/agents/bmad-agent-bmm-pm.md +0 -16
- package/install/templates/.github/agents/bmad-agent-bmm-quick-flow-solo-dev.md +0 -16
- package/install/templates/.github/agents/bmad-agent-bmm-quinn.md +0 -16
- package/install/templates/.github/agents/bmad-agent-bmm-sm.md +0 -16
- package/install/templates/.github/agents/bmad-agent-bmm-tech-writer.md +0 -16
- package/install/templates/.github/agents/bmad-agent-bmm-ux-designer.md +0 -16
- package/install/templates/.github/agents/bmad-agent-byan-test.md +0 -33
- package/install/templates/.github/agents/bmad-agent-byan-v2.md +0 -44
- package/install/templates/.github/agents/bmad-agent-byan.md +0 -1062
- package/install/templates/.github/agents/bmad-agent-carmack.md +0 -14
- package/install/templates/.github/agents/bmad-agent-cis-brainstorming-coach.md +0 -16
- package/install/templates/.github/agents/bmad-agent-cis-creative-problem-solver.md +0 -16
- package/install/templates/.github/agents/bmad-agent-cis-design-thinking-coach.md +0 -16
- package/install/templates/.github/agents/bmad-agent-cis-innovation-strategist.md +0 -16
- package/install/templates/.github/agents/bmad-agent-cis-presentation-master.md +0 -16
- package/install/templates/.github/agents/bmad-agent-cis-storyteller.md +0 -16
- package/install/templates/.github/agents/bmad-agent-claude.md +0 -49
- package/install/templates/.github/agents/bmad-agent-codex.md +0 -49
- package/install/templates/.github/agents/bmad-agent-drawio.md +0 -45
- package/install/templates/.github/agents/bmad-agent-fact-checker.md +0 -16
- package/install/templates/.github/agents/bmad-agent-forgeron.md +0 -15
- package/install/templates/.github/agents/bmad-agent-jimmy.md +0 -15
- package/install/templates/.github/agents/bmad-agent-marc.md +0 -49
- package/install/templates/.github/agents/bmad-agent-mike.md +0 -15
- package/install/templates/.github/agents/bmad-agent-patnote.md +0 -49
- package/install/templates/.github/agents/bmad-agent-rachid.md +0 -48
- package/install/templates/.github/agents/bmad-agent-skeptic.md +0 -16
- package/install/templates/.github/agents/bmad-agent-tao.md +0 -14
- package/install/templates/.github/agents/bmad-agent-tea-tea.md +0 -16
- package/install/templates/.github/agents/bmad-agent-test-dynamic.md +0 -22
- package/install/templates/.github/agents/bmad-agent-yanstaller-interview.md +0 -50
- package/install/templates/.github/agents/bmad-agent-yanstaller-phase2.md +0 -189
- package/install/templates/.github/agents/bmad-agent-yanstaller.md +0 -350
- package/install/templates/.github/agents/expert-merise-agile.md +0 -178
- package/install/templates/.github/agents/franck.md +0 -379
- package/install/templates/.github/agents/hermes.md +0 -575
- package/install/templates/.github/extensions/byan-staging/extension.mjs +0 -169
- package/install/templates/.github/extensions/byan-staging/package.json +0 -8
- package/install/templates/_byan/agent/marc/marc-soul.md +0 -47
- package/install/templates/_byan/agent/marc/marc-tao.md +0 -77
- package/install/templates/_byan/agent/marc/marc.md +0 -324
- package/install/templates/_byan/agent/marc-flat/marc.md +0 -387
- package/install/templates/_byan/mcp/byan-mcp-server/lib/copilot.js +0 -148
- package/install/templates/_byan/worker/launchers/launch-yanstaller-copilot.md +0 -173
- package/install/templates/workers/cost-optimizer.js +0 -169
- package/src/byan-v2/context/copilot-context.js +0 -79
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// BYAN Auto-Benchmark miss-ledger reader / aggregator (C5e).
|
|
3
|
+
//
|
|
4
|
+
// The Stop hook (autobench-stop-guard.js) appends ONE JSONL line per invocation
|
|
5
|
+
// to _byan-output/benchmark-ledger.jsonl. Each line is an audit record of one
|
|
6
|
+
// end-of-turn decision: did the agent benchmark a fork it should have, or did it
|
|
7
|
+
// MISS. This module reads that trail and aggregates it into the fires / misses /
|
|
8
|
+
// miss-rate summary required by the acceptance criteria, plus a small CLI-ish
|
|
9
|
+
// `main()` so a human (or CI) can run a one-shot report.
|
|
10
|
+
//
|
|
11
|
+
// Event vocabulary is OWNED by the hook; this reader treats it as the contract:
|
|
12
|
+
// fired-block -> a MISS (the agent offered a fork without a
|
|
13
|
+
// benchmark marker; the hook forced a regen).
|
|
14
|
+
// satisfied-marker -> a HIT (a real benchmark was presented).
|
|
15
|
+
// satisfied-skip -> a deliberate skip (fork considered, not tabled).
|
|
16
|
+
// satisfied-never -> exempt (y/n confirm / destructive prompt).
|
|
17
|
+
// satisfied-escape -> exempt (escape-hatch active).
|
|
18
|
+
// satisfied-already-blocked -> the forced regen pass (block-once accounting).
|
|
19
|
+
// no-choice -> no fork was present (the common case).
|
|
20
|
+
// Anything else is bucketed under `unknown` so a future event type is surfaced,
|
|
21
|
+
// not silently dropped.
|
|
22
|
+
//
|
|
23
|
+
// Pure read-only: this module NEVER writes the ledger. It reads what the hook
|
|
24
|
+
// wrote. Robust to a partially-written / corrupt JSONL file: a malformed line is
|
|
25
|
+
// counted under `malformed` and skipped, never thrown.
|
|
26
|
+
|
|
27
|
+
'use strict';
|
|
28
|
+
|
|
29
|
+
const fs = require('fs');
|
|
30
|
+
const path = require('path');
|
|
31
|
+
|
|
32
|
+
// Events that count as a genuine MISS the agent must fix. Kept narrow on
|
|
33
|
+
// purpose: only `fired-block` is a real failure. Everything else is either a
|
|
34
|
+
// hit, an exempt case, or accounting.
|
|
35
|
+
const MISS_EVENTS = new Set(['fired-block']);
|
|
36
|
+
|
|
37
|
+
// Events that count as a real benchmark HIT (a fork was tabled).
|
|
38
|
+
const HIT_EVENTS = new Set(['satisfied-marker']);
|
|
39
|
+
|
|
40
|
+
// Events that mean "a fork was considered and deliberately not tabled".
|
|
41
|
+
const SKIP_EVENTS = new Set(['satisfied-skip']);
|
|
42
|
+
|
|
43
|
+
// Exempt / accounting events: present in the trail but neither a miss nor a hit.
|
|
44
|
+
const EXEMPT_EVENTS = new Set([
|
|
45
|
+
'satisfied-never',
|
|
46
|
+
'satisfied-escape',
|
|
47
|
+
'satisfied-already-blocked',
|
|
48
|
+
'no-choice',
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
function projectRoot() {
|
|
52
|
+
return process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function defaultLedgerPath() {
|
|
56
|
+
return path.join(projectRoot(), '_byan-output', 'benchmark-ledger.jsonl');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Read and parse the ledger file into an array of entries.
|
|
61
|
+
* Returns { entries, malformed, missing } where `malformed` counts unparseable
|
|
62
|
+
* lines and `missing` is true when the file does not exist (a fresh repo where
|
|
63
|
+
* the hook never fired -> not an error, just an empty trail).
|
|
64
|
+
*
|
|
65
|
+
* @param {string} [filePath] absolute path; defaults to the project ledger.
|
|
66
|
+
*/
|
|
67
|
+
function readLedger(filePath) {
|
|
68
|
+
const p = filePath || defaultLedgerPath();
|
|
69
|
+
let raw;
|
|
70
|
+
try {
|
|
71
|
+
if (!fs.existsSync(p)) return { entries: [], malformed: 0, missing: true };
|
|
72
|
+
raw = fs.readFileSync(p, 'utf8');
|
|
73
|
+
} catch {
|
|
74
|
+
// An unreadable ledger is treated as empty rather than thrown: a reporting
|
|
75
|
+
// tool must never crash the caller over a permissions blip.
|
|
76
|
+
return { entries: [], malformed: 0, missing: true };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const entries = [];
|
|
80
|
+
let malformed = 0;
|
|
81
|
+
for (const line of raw.split('\n')) {
|
|
82
|
+
const trimmed = line.trim();
|
|
83
|
+
if (!trimmed) continue;
|
|
84
|
+
try {
|
|
85
|
+
const obj = JSON.parse(trimmed);
|
|
86
|
+
if (obj && typeof obj === 'object') entries.push(obj);
|
|
87
|
+
else malformed += 1;
|
|
88
|
+
} catch {
|
|
89
|
+
malformed += 1;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { entries, malformed, missing: false };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Aggregate a list of ledger entries into a summary.
|
|
97
|
+
*
|
|
98
|
+
* @param {Array<object>} entries
|
|
99
|
+
* @returns {object} {
|
|
100
|
+
* total, fires, misses, hits, skips, exempt, unknown, missRate,
|
|
101
|
+
* byEvent:{event:count}, byScope:{internal,external,unknown},
|
|
102
|
+
* gates:{g1Total,g2Total,countWithGates}
|
|
103
|
+
* }
|
|
104
|
+
* - fires = decisions where a fork was present and acted on (hits + misses).
|
|
105
|
+
* - misses = fired-block events (the agent must fix these).
|
|
106
|
+
* - missRate = misses / fires (0 when no fork was ever present).
|
|
107
|
+
*/
|
|
108
|
+
function aggregate(entries) {
|
|
109
|
+
const list = Array.isArray(entries) ? entries : [];
|
|
110
|
+
|
|
111
|
+
const byEvent = {};
|
|
112
|
+
const byScope = { internal: 0, external: 0, unknown: 0 };
|
|
113
|
+
let misses = 0;
|
|
114
|
+
let hits = 0;
|
|
115
|
+
let skips = 0;
|
|
116
|
+
let exempt = 0;
|
|
117
|
+
let unknown = 0;
|
|
118
|
+
let g1Total = 0;
|
|
119
|
+
let g2Total = 0;
|
|
120
|
+
let countWithGates = 0;
|
|
121
|
+
|
|
122
|
+
for (const e of list) {
|
|
123
|
+
const event = e && typeof e.event === 'string' ? e.event : 'unknown';
|
|
124
|
+
byEvent[event] = (byEvent[event] || 0) + 1;
|
|
125
|
+
|
|
126
|
+
if (MISS_EVENTS.has(event)) misses += 1;
|
|
127
|
+
else if (HIT_EVENTS.has(event)) hits += 1;
|
|
128
|
+
else if (SKIP_EVENTS.has(event)) skips += 1;
|
|
129
|
+
else if (EXEMPT_EVENTS.has(event)) exempt += 1;
|
|
130
|
+
else unknown += 1;
|
|
131
|
+
|
|
132
|
+
// Scope tally (only the marker-bearing hits/skips carry a scope field).
|
|
133
|
+
const scope = e && e.scope;
|
|
134
|
+
if (scope === 'internal' || scope === 'external') byScope[scope] += 1;
|
|
135
|
+
else if (scope != null) byScope.unknown += 1;
|
|
136
|
+
|
|
137
|
+
// Gate totals: only the satisfied-marker entries carry g1/g2 (the marker
|
|
138
|
+
// fields the hook parsed). Average gate counts hint at fork complexity.
|
|
139
|
+
if (typeof e.g1 === 'number' || typeof e.g2 === 'number') {
|
|
140
|
+
if (typeof e.g1 === 'number') g1Total += e.g1;
|
|
141
|
+
if (typeof e.g2 === 'number') g2Total += e.g2;
|
|
142
|
+
countWithGates += 1;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// A "fire" is a turn where a fork was genuinely present and the doctrine
|
|
147
|
+
// applied: a HIT (tabled) or a MISS (should have, didn't). Skips, exempts and
|
|
148
|
+
// no-choice turns are NOT fires, so the miss-rate is not diluted by the vast
|
|
149
|
+
// majority of turns that have no fork at all.
|
|
150
|
+
const fires = hits + misses;
|
|
151
|
+
const missRate = fires > 0 ? misses / fires : 0;
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
total: list.length,
|
|
155
|
+
fires,
|
|
156
|
+
misses,
|
|
157
|
+
hits,
|
|
158
|
+
skips,
|
|
159
|
+
exempt,
|
|
160
|
+
unknown,
|
|
161
|
+
missRate,
|
|
162
|
+
byEvent,
|
|
163
|
+
byScope,
|
|
164
|
+
gates: {
|
|
165
|
+
g1Total,
|
|
166
|
+
g2Total,
|
|
167
|
+
countWithGates,
|
|
168
|
+
g1Avg: countWithGates > 0 ? g1Total / countWithGates : 0,
|
|
169
|
+
g2Avg: countWithGates > 0 ? g2Total / countWithGates : 0,
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Convenience: read + aggregate in one call.
|
|
176
|
+
* @param {string} [filePath]
|
|
177
|
+
* @returns {object} aggregate(...) plus { malformed, missing, path }.
|
|
178
|
+
*/
|
|
179
|
+
function report(filePath) {
|
|
180
|
+
const p = filePath || defaultLedgerPath();
|
|
181
|
+
const { entries, malformed, missing } = readLedger(p);
|
|
182
|
+
return Object.assign(aggregate(entries), { malformed, missing, path: p });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Render a percentage with one decimal, no trailing-zero noise (e.g. "12.5%").
|
|
186
|
+
function pct(ratio) {
|
|
187
|
+
return `${(ratio * 100).toFixed(1)}%`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Render a human-readable summary block (no color, no emoji). Returns a string
|
|
192
|
+
* so it is testable; main() writes it to stdout.
|
|
193
|
+
*/
|
|
194
|
+
function formatReport(rep) {
|
|
195
|
+
const lines = [];
|
|
196
|
+
lines.push('BYAN Auto-Benchmark ledger report');
|
|
197
|
+
lines.push(` file : ${rep.path}`);
|
|
198
|
+
if (rep.missing) {
|
|
199
|
+
lines.push(' status : ledger not found (the Stop hook has not fired yet)');
|
|
200
|
+
return lines.join('\n');
|
|
201
|
+
}
|
|
202
|
+
lines.push(` records : ${rep.total}${rep.malformed ? ` (+${rep.malformed} malformed, skipped)` : ''}`);
|
|
203
|
+
lines.push(` forks (fires): ${rep.fires} hits: ${rep.hits} misses: ${rep.misses}`);
|
|
204
|
+
lines.push(` miss-rate : ${pct(rep.missRate)}${rep.fires === 0 ? ' (no fork seen)' : ''}`);
|
|
205
|
+
lines.push(` skips : ${rep.skips} exempt: ${rep.exempt} unknown: ${rep.unknown}`);
|
|
206
|
+
lines.push(
|
|
207
|
+
` scope : internal=${rep.byScope.internal} external=${rep.byScope.external}`
|
|
208
|
+
);
|
|
209
|
+
if (rep.gates.countWithGates > 0) {
|
|
210
|
+
lines.push(
|
|
211
|
+
` avg gates : g1=${rep.gates.g1Avg.toFixed(1)} g2=${rep.gates.g2Avg.toFixed(1)} (over ${rep.gates.countWithGates} benchmarks)`
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
const events = Object.keys(rep.byEvent).sort();
|
|
215
|
+
if (events.length) {
|
|
216
|
+
lines.push(' by event :');
|
|
217
|
+
for (const ev of events) lines.push(` ${ev.padEnd(26)} ${rep.byEvent[ev]}`);
|
|
218
|
+
}
|
|
219
|
+
return lines.join('\n');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// CLI entry: `node autobench-ledger-report.js [path]` or `--json` for raw data.
|
|
223
|
+
// Exit 0 always (a report tool never fails the shell over a read).
|
|
224
|
+
function main(argv) {
|
|
225
|
+
const args = Array.isArray(argv) ? argv : process.argv.slice(2);
|
|
226
|
+
const asJson = args.includes('--json');
|
|
227
|
+
const fileArg = args.find((a) => a && !a.startsWith('--'));
|
|
228
|
+
const rep = report(fileArg);
|
|
229
|
+
if (asJson) {
|
|
230
|
+
process.stdout.write(JSON.stringify(rep, null, 2) + '\n');
|
|
231
|
+
} else {
|
|
232
|
+
process.stdout.write(formatReport(rep) + '\n');
|
|
233
|
+
}
|
|
234
|
+
return rep;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (require.main === module) {
|
|
238
|
+
main();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
module.exports = {
|
|
242
|
+
MISS_EVENTS,
|
|
243
|
+
HIT_EVENTS,
|
|
244
|
+
SKIP_EVENTS,
|
|
245
|
+
EXEMPT_EVENTS,
|
|
246
|
+
defaultLedgerPath,
|
|
247
|
+
readLedger,
|
|
248
|
+
aggregate,
|
|
249
|
+
report,
|
|
250
|
+
formatReport,
|
|
251
|
+
pct,
|
|
252
|
+
main,
|
|
253
|
+
};
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// Shared runtime helpers for the BYAN Auto-Benchmark Stop hook.
|
|
2
|
+
//
|
|
3
|
+
// Reads one static file :
|
|
4
|
+
// - .claude/hooks/lib/autobench-config.json : the runtime subset generated
|
|
5
|
+
// from _byan/_config/autobench.yaml by byan-sync-rules (never_list regexes,
|
|
6
|
+
// choice_language regexes, marker patterns, escape-hatch paths, banners,
|
|
7
|
+
// ledger path).
|
|
8
|
+
//
|
|
9
|
+
// Owns the ephemeral session artifacts the Stop hook needs :
|
|
10
|
+
// - .byan-autobench/off : session escape-hatch flag (touch to disable).
|
|
11
|
+
// - .byan-autobench/blocked-<turnHash> : the block-once token, written when a
|
|
12
|
+
// turn is blocked so the regenerated turn is never blocked a second time.
|
|
13
|
+
// - _byan-output/benchmark-ledger.jsonl : the append-only fire/miss audit.
|
|
14
|
+
//
|
|
15
|
+
// This module is deliberately SEPARATE from strict-runtime.js : the two hook
|
|
16
|
+
// families have different state shapes and lifecycles, and coupling them would
|
|
17
|
+
// make a change to one risk the other.
|
|
18
|
+
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const crypto = require('crypto');
|
|
24
|
+
|
|
25
|
+
function projectRoot() {
|
|
26
|
+
return process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function readJson(filePath) {
|
|
30
|
+
try {
|
|
31
|
+
if (!fs.existsSync(filePath)) return null;
|
|
32
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function configPath() {
|
|
39
|
+
return path.join(projectRoot(), '.claude', 'hooks', 'lib', 'autobench-config.json');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function loadAutobenchConfig() {
|
|
43
|
+
return readJson(configPath());
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Session-scoped flag file. Its mere presence disables blocking for the
|
|
47
|
+
// session. The cross-session opt-out lives in the config (escape_hatch.disabled)
|
|
48
|
+
// so it survives across sessions and is regenerated from the YAML.
|
|
49
|
+
function sessionFlagPath() {
|
|
50
|
+
return path.join(projectRoot(), '.byan-autobench', 'off');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function escapeHatchActive(config) {
|
|
54
|
+
// Session flag : touch .byan-autobench/off to disable for this session.
|
|
55
|
+
try {
|
|
56
|
+
if (fs.existsSync(sessionFlagPath())) return true;
|
|
57
|
+
} catch {
|
|
58
|
+
// ignore — fall through to the cross-session check
|
|
59
|
+
}
|
|
60
|
+
// Cross-session opt-out, carried in the generated config.
|
|
61
|
+
const eh = config && config.escape_hatch;
|
|
62
|
+
if (eh && eh.disabled === true) return true;
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Arming. The Stop hook ships DISARMED (approach C) : it observes and ledgers
|
|
67
|
+
// but never blocks until explicitly armed, so day one is zero noise / latency.
|
|
68
|
+
// Arming is config-only : set enforcement.armed === true in
|
|
69
|
+
// _byan/_config/autobench.yaml and run byan-sync-rules to regenerate the
|
|
70
|
+
// config. There is NO loose flag file — a stray file on disk must not silently
|
|
71
|
+
// arm a machine (the incoherent state the integration audit found). Default : OFF.
|
|
72
|
+
function isArmed(config) {
|
|
73
|
+
const en = config && config.enforcement;
|
|
74
|
+
return !!(en && en.armed === true);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function blockDir() {
|
|
78
|
+
return path.join(projectRoot(), '.byan-autobench');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function blockTokenPath(turnHash) {
|
|
82
|
+
return path.join(blockDir(), `blocked-${turnHash}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function readBlockToken(turnHash) {
|
|
86
|
+
try {
|
|
87
|
+
return fs.existsSync(blockTokenPath(turnHash));
|
|
88
|
+
} catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function writeBlockToken(turnHash) {
|
|
94
|
+
try {
|
|
95
|
+
fs.mkdirSync(blockDir(), { recursive: true });
|
|
96
|
+
// Content is irrelevant — presence is the signal. We still stamp the hash so
|
|
97
|
+
// a human inspecting .byan-autobench/ can tell which turn was blocked.
|
|
98
|
+
fs.writeFileSync(blockTokenPath(turnHash), turnHash + '\n');
|
|
99
|
+
return true;
|
|
100
|
+
} catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function ledgerPath(config) {
|
|
106
|
+
const rel =
|
|
107
|
+
(config && config.ledger && config.ledger.path) ||
|
|
108
|
+
path.join('_byan-output', 'benchmark-ledger.jsonl');
|
|
109
|
+
return path.isAbsolute(rel) ? rel : path.join(projectRoot(), rel);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Append ONE JSONL line. Best-effort : a failed append never traps the turn.
|
|
113
|
+
// The caller supplies any timestamp/session via the entry so this stays
|
|
114
|
+
// deterministic and unit-testable (no clock read here).
|
|
115
|
+
function appendLedger(entry, config) {
|
|
116
|
+
try {
|
|
117
|
+
const p = ledgerPath(config);
|
|
118
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
119
|
+
fs.appendFileSync(p, JSON.stringify(entry) + '\n');
|
|
120
|
+
return true;
|
|
121
|
+
} catch {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Transcript extraction (text + raw content) is shared with the other Stop hooks
|
|
127
|
+
// through transcript-read.js — one canonical reader for the real Stop payload
|
|
128
|
+
// (last_assistant_message + transcript_path JSONL) instead of divergent per-hook
|
|
129
|
+
// copies. extractLastAssistantContent feeds hasChoiceArtifact below.
|
|
130
|
+
const {
|
|
131
|
+
extractLastAssistantText,
|
|
132
|
+
extractLastAssistantContent,
|
|
133
|
+
lastAssistantContentFromTranscriptFile,
|
|
134
|
+
contentToText,
|
|
135
|
+
} = require('./transcript-read');
|
|
136
|
+
|
|
137
|
+
// ARTIFACT-primary fork signal : a real choice surfaced through the
|
|
138
|
+
// AskUserQuestion tool (the multiple-choice UI) is unambiguous, unlike prose
|
|
139
|
+
// that merely contains 'or' / 'option'. Keys on the structural tool_use block in
|
|
140
|
+
// the finished turn, NOT on choice-WORDS. The lexical regex stays a last-resort
|
|
141
|
+
// fallback for inline-prose forks that never call the tool. Post-hoc by
|
|
142
|
+
// construction (GH #28273) : the tool_use is read from the finished transcript.
|
|
143
|
+
function hasChoiceArtifact(content) {
|
|
144
|
+
if (!Array.isArray(content)) return false;
|
|
145
|
+
return content.some(
|
|
146
|
+
(b) =>
|
|
147
|
+
b &&
|
|
148
|
+
b.type === 'tool_use' &&
|
|
149
|
+
typeof b.name === 'string' &&
|
|
150
|
+
/askuserquestion/i.test(b.name)
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Content-only hash : NO clock, NO RNG. Block-once must be stable across the
|
|
155
|
+
// original turn and its regeneration would only differ if the text differs.
|
|
156
|
+
function turnHash(text) {
|
|
157
|
+
return crypto.createHash('sha1').update(String(text || '')).digest('hex').slice(0, 16);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function readStdin() {
|
|
161
|
+
return new Promise((resolve) => {
|
|
162
|
+
if (process.stdin.isTTY) return resolve('');
|
|
163
|
+
let data = '';
|
|
164
|
+
process.stdin.on('data', (c) => (data += c));
|
|
165
|
+
process.stdin.on('end', () => resolve(data));
|
|
166
|
+
process.stdin.on('error', () => resolve(data));
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function parseJson(raw) {
|
|
171
|
+
try {
|
|
172
|
+
return raw ? JSON.parse(raw) : {};
|
|
173
|
+
} catch {
|
|
174
|
+
return {};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
module.exports = {
|
|
179
|
+
projectRoot,
|
|
180
|
+
configPath,
|
|
181
|
+
loadAutobenchConfig,
|
|
182
|
+
sessionFlagPath,
|
|
183
|
+
escapeHatchActive,
|
|
184
|
+
isArmed,
|
|
185
|
+
blockDir,
|
|
186
|
+
blockTokenPath,
|
|
187
|
+
readBlockToken,
|
|
188
|
+
writeBlockToken,
|
|
189
|
+
ledgerPath,
|
|
190
|
+
appendLedger,
|
|
191
|
+
extractLastAssistantText,
|
|
192
|
+
extractLastAssistantContent,
|
|
193
|
+
lastAssistantContentFromTranscriptFile,
|
|
194
|
+
contentToText,
|
|
195
|
+
hasChoiceArtifact,
|
|
196
|
+
turnHash,
|
|
197
|
+
readStdin,
|
|
198
|
+
parseJson,
|
|
199
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fact-check-core — pure detection engine shared by the fact-check hooks.
|
|
3
|
+
*
|
|
4
|
+
* No IO, no process exit. The PreToolUse doc gate (fact-check-absolutes.js)
|
|
5
|
+
* and the Stop conversation nudge (fact-check-claims.js) both consume this so
|
|
6
|
+
* the absolute-detection logic lives in one place.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const ABSOLUTES = [
|
|
12
|
+
/\btoujours\b/i,
|
|
13
|
+
/\bjamais\b/i,
|
|
14
|
+
/\bforc[eé]ment\b/i,
|
|
15
|
+
/\bobviously\b/i,
|
|
16
|
+
/\balways\b/i,
|
|
17
|
+
/\bnever\b/i,
|
|
18
|
+
/\bclearly\b/i,
|
|
19
|
+
/\bundoubtedly\b/i,
|
|
20
|
+
/\bfaster than\b/i,
|
|
21
|
+
/\bbetter than\b/i,
|
|
22
|
+
/\bplus rapide que\b/i,
|
|
23
|
+
/\bmeilleur que\b/i,
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const SOURCE_MARKERS = [
|
|
27
|
+
/\bRFC\s*\d+/i,
|
|
28
|
+
/\bCVE-\d{4}-\d+/i,
|
|
29
|
+
/https?:\/\//,
|
|
30
|
+
/\[CLAIM\s+L[1-5]\]/i,
|
|
31
|
+
/\[FACT\s+USER-VERIFIED/i,
|
|
32
|
+
/\bsource\s*:/i,
|
|
33
|
+
/_byan\/knowledge\/sources\.md/,
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
// Strip content that cannot be a claim :
|
|
37
|
+
// - fenced code blocks ``` ... ```
|
|
38
|
+
// - inline backticks `...`
|
|
39
|
+
// - block quotes (lines starting with >)
|
|
40
|
+
// - list-of-pattern lines (e.g. "- toujours")
|
|
41
|
+
function stripNonClaimZones(text) {
|
|
42
|
+
if (!text) return '';
|
|
43
|
+
return text
|
|
44
|
+
.replace(/```[\s\S]*?```/g, '')
|
|
45
|
+
.replace(/`[^`\n]+`/g, '')
|
|
46
|
+
.replace(/^> .*$/gm, '')
|
|
47
|
+
.replace(/^[\s-]*['"]?\b(toujours|jamais|forc[eé]ment|obviously|always|never|clearly|undoubtedly)\b['"]?/gim, '');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Return the first unsourced absolute (with surrounding context) or null when
|
|
51
|
+
// every absolute has a source marker within a +/-240 char window.
|
|
52
|
+
function findUnsourced(text) {
|
|
53
|
+
if (!text) return null;
|
|
54
|
+
for (const re of ABSOLUTES) {
|
|
55
|
+
const match = text.match(re);
|
|
56
|
+
if (!match) continue;
|
|
57
|
+
const idx = match.index || 0;
|
|
58
|
+
const windowStart = Math.max(0, idx - 240);
|
|
59
|
+
const windowEnd = Math.min(text.length, idx + match[0].length + 240);
|
|
60
|
+
const ctx = text.slice(windowStart, windowEnd);
|
|
61
|
+
const hasSource = SOURCE_MARKERS.some((sm) => sm.test(ctx));
|
|
62
|
+
if (!hasSource) {
|
|
63
|
+
return { absolute: match[0], context: text.slice(Math.max(0, idx - 80), idx + 80) };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = { ABSOLUTES, SOURCE_MARKERS, stripNonClaimZones, findUnsourced };
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// Shared transcript reader for Claude Code Stop hooks.
|
|
2
|
+
//
|
|
3
|
+
// The real Stop-hook payload carries NO inline transcript: the runtime hands a
|
|
4
|
+
// `last_assistant_message` string and a `transcript_path` pointing at a JSONL
|
|
5
|
+
// file. A hook that reads `payload.transcript || payload.messages` (an inline
|
|
6
|
+
// array) extracts nothing in production and silently never fires — the bug this
|
|
7
|
+
// module exists to prevent, in one place instead of four divergent copies.
|
|
8
|
+
//
|
|
9
|
+
// Transcript JSONL shape (one object per line): a turn is
|
|
10
|
+
// { type: 'user'|'assistant', message: { role, content: string | block[] } }
|
|
11
|
+
// where a block is { type: 'text', text } or { type: 'tool_use', name, input, ... }.
|
|
12
|
+
//
|
|
13
|
+
// Every function is best-effort: an unreadable/short file or a malformed line
|
|
14
|
+
// yields empty/null so a hook never traps a turn it cannot read.
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
|
|
20
|
+
// Flatten an assistant message content (string or block array) to plain text.
|
|
21
|
+
function contentToText(content) {
|
|
22
|
+
if (typeof content === 'string') return content;
|
|
23
|
+
if (Array.isArray(content)) {
|
|
24
|
+
return content.map((c) => (c && typeof c.text === 'string' ? c.text : '')).join(' ');
|
|
25
|
+
}
|
|
26
|
+
return '';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Parse a transcript JSONL file into the array of its message objects
|
|
30
|
+
// ({ type, message:{role, content} }), skipping blank/malformed lines.
|
|
31
|
+
function readTranscriptLines(filePath) {
|
|
32
|
+
try {
|
|
33
|
+
if (!fs.existsSync(filePath)) return null;
|
|
34
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
35
|
+
const out = [];
|
|
36
|
+
for (const line of raw.split('\n')) {
|
|
37
|
+
if (!line || !line.trim()) continue;
|
|
38
|
+
try {
|
|
39
|
+
out.push(JSON.parse(line));
|
|
40
|
+
} catch {
|
|
41
|
+
// skip a malformed line, keep the rest
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isAssistantLine(o) {
|
|
51
|
+
return Boolean((o && o.type === 'assistant') || (o && o.message && o.message.role === 'assistant'));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// The RAW content (string or block array) of the last assistant turn in the
|
|
55
|
+
// file, or null. Used for artifact detection (tool_use blocks survive here).
|
|
56
|
+
function lastAssistantContentFromTranscriptFile(filePath) {
|
|
57
|
+
const lines = readTranscriptLines(filePath);
|
|
58
|
+
if (!lines) return null;
|
|
59
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
60
|
+
const o = lines[i];
|
|
61
|
+
if (isAssistantLine(o) && o.message && o.message.content != null) return o.message.content;
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// The user/assistant messages from the file as { role, content } objects, in
|
|
67
|
+
// order, or null. Lets a hook reconstruct the last N turns of a conversation.
|
|
68
|
+
function messagesFromTranscriptFile(filePath) {
|
|
69
|
+
const lines = readTranscriptLines(filePath);
|
|
70
|
+
if (!lines) return null;
|
|
71
|
+
const out = [];
|
|
72
|
+
for (const o of lines) {
|
|
73
|
+
const m = o && o.message;
|
|
74
|
+
if (m && (m.role === 'user' || m.role === 'assistant')) {
|
|
75
|
+
out.push({ role: m.role, content: m.content });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return out.length ? out : null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// The RAW content of the finished assistant turn from ANY payload shape: an
|
|
82
|
+
// inline array (test fixtures / legacy) first, then the transcript_path file.
|
|
83
|
+
// last_assistant_message is text-only, so it cannot feed artifact detection and
|
|
84
|
+
// is handled in extractLastAssistantText, not here.
|
|
85
|
+
function extractLastAssistantContent(payload) {
|
|
86
|
+
if (!payload || typeof payload !== 'object') return null;
|
|
87
|
+
|
|
88
|
+
const inline = payload.transcript || payload.messages;
|
|
89
|
+
if (Array.isArray(inline)) {
|
|
90
|
+
for (let i = inline.length - 1; i >= 0; i--) {
|
|
91
|
+
const m = inline[i];
|
|
92
|
+
if (m && m.role === 'assistant') return m.content;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const tp = payload.transcript_path || payload.transcriptPath;
|
|
97
|
+
if (typeof tp === 'string') {
|
|
98
|
+
const content = lastAssistantContentFromTranscriptFile(tp);
|
|
99
|
+
if (content != null) return content;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// The finished assistant turn as plain text (the primary signal for most hooks).
|
|
106
|
+
// Prefer the runtime-provided last_assistant_message; else derive from content.
|
|
107
|
+
function extractLastAssistantText(payload) {
|
|
108
|
+
if (!payload || typeof payload !== 'object') return '';
|
|
109
|
+
if (typeof payload.last_assistant_message === 'string') return payload.last_assistant_message;
|
|
110
|
+
if (typeof payload.lastAssistantMessage === 'string') return payload.lastAssistantMessage;
|
|
111
|
+
return contentToText(extractLastAssistantContent(payload));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// The last N user/assistant messages as { role, content }, from any payload
|
|
115
|
+
// shape (inline array or transcript_path file), or null when none are readable.
|
|
116
|
+
function extractRecentMessages(payload, limit) {
|
|
117
|
+
if (!payload || typeof payload !== 'object') return null;
|
|
118
|
+
let msgs = payload.transcript || payload.messages;
|
|
119
|
+
if (!Array.isArray(msgs)) {
|
|
120
|
+
const tp = payload.transcript_path || payload.transcriptPath;
|
|
121
|
+
if (typeof tp === 'string') msgs = messagesFromTranscriptFile(tp);
|
|
122
|
+
}
|
|
123
|
+
if (!Array.isArray(msgs)) return null;
|
|
124
|
+
const filtered = msgs.filter((m) => m && (m.role === 'user' || m.role === 'assistant'));
|
|
125
|
+
if (!filtered.length) return null;
|
|
126
|
+
return typeof limit === 'number' ? filtered.slice(-limit) : filtered;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = {
|
|
130
|
+
contentToText,
|
|
131
|
+
readTranscriptLines,
|
|
132
|
+
lastAssistantContentFromTranscriptFile,
|
|
133
|
+
messagesFromTranscriptFile,
|
|
134
|
+
extractLastAssistantContent,
|
|
135
|
+
extractLastAssistantText,
|
|
136
|
+
extractRecentMessages,
|
|
137
|
+
};
|