create-byan-agent 2.23.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 +230 -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 +2 -6
- 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/drain-advisory.js +85 -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/failure-detector.js +18 -4
- 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 +16 -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-insight/SKILL.md +56 -0
- 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/.claude/workflows/check-implementation-readiness.js +1 -1
- 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/core/activation/soul-activation.md +3 -3
- package/install/templates/_byan/mcp/byan-mcp-server/bin/byan-insight-digest.js +31 -0
- 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 +96 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/index-generator.js +1 -1
- package/install/templates/_byan/mcp/byan-mcp-server/lib/insight-harvest.js +220 -0
- 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/outcome-buffer.js +64 -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 +262 -81
- package/install/templates/_byan/worker/launchers/README.md +4 -24
- package/install/templates/_byan/worker/workers.md +8 -9
- package/install/templates/_byan/workflow/simple/bmb/byan-benchmark/workflow.md +86 -0
- package/install/templates/_byan/workflow/simple/byan/feature-workflow.md +2 -2
- 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/loadbalancer/loadbalancer.js +1 -1
- 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
- package/src/core/dispatcher/execution-router.js +0 -66
|
@@ -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 };
|
|
@@ -10,14 +10,27 @@ const ERROR_PATTERNS = [
|
|
|
10
10
|
/tool_use_error/i,
|
|
11
11
|
];
|
|
12
12
|
|
|
13
|
-
// Tools whose response echoes user-authored or
|
|
13
|
+
// Tools whose response echoes user-authored or stored content (Write/Edit
|
|
14
14
|
// return file paths + content fragments, Read echoes file content
|
|
15
15
|
// verbatim). Pattern match on their response fires false positives when
|
|
16
|
-
// the
|
|
16
|
+
// the content itself contains the literal phrase "internal error"
|
|
17
17
|
// (e.g. a doc about errors, a test fixture, a hook that detects errors).
|
|
18
18
|
// For these, only trust the explicit is_error flag.
|
|
19
19
|
const ECHO_TOOLS = new Set(['Write', 'Edit', 'NotebookEdit', 'Read']);
|
|
20
20
|
|
|
21
|
+
// Two more tool classes echo DATA (not a stderr stream), so content-pattern
|
|
22
|
+
// matching on their response is noise. A genuine failure of either sets is_error
|
|
23
|
+
// (checked before this guard), so we lose no real-failure detection:
|
|
24
|
+
// - MCP tools (mcp__server__tool): byan_fd_* echoes the FD state (which can
|
|
25
|
+
// hold user-authored raw_ideas / notes containing the literal phrase),
|
|
26
|
+
// byan_*_status echoes ledger content, etc.
|
|
27
|
+
// - Bash: its response is command stdout - diagnostics, log greps, test output
|
|
28
|
+
// that legitimately surface error-words. A real Bash failure exits non-zero,
|
|
29
|
+
// which the harness marks as is_error.
|
|
30
|
+
function isEchoHeavy(toolName) {
|
|
31
|
+
return ECHO_TOOLS.has(toolName) || toolName === 'Bash' || toolName.startsWith('mcp__');
|
|
32
|
+
}
|
|
33
|
+
|
|
21
34
|
function detectFailure(payload) {
|
|
22
35
|
if (!payload || typeof payload !== 'object') return null;
|
|
23
36
|
|
|
@@ -30,8 +43,9 @@ function detectFailure(payload) {
|
|
|
30
43
|
}
|
|
31
44
|
}
|
|
32
45
|
|
|
33
|
-
// Do not pattern-match on echo-heavy tools
|
|
34
|
-
|
|
46
|
+
// Do not pattern-match on echo-heavy tools (file-echo + MCP data) — only
|
|
47
|
+
// trust the is_error flag, checked above.
|
|
48
|
+
if (isEchoHeavy(toolName)) {
|
|
35
49
|
return null;
|
|
36
50
|
}
|
|
37
51
|
|
|
@@ -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
|
+
};
|
|
@@ -3,23 +3,30 @@
|
|
|
3
3
|
* SessionStart hook — checks soul-memory last-revision and reminds the
|
|
4
4
|
* agent when > 14 days since last introspection.
|
|
5
5
|
*
|
|
6
|
-
* Parses
|
|
7
|
-
* (
|
|
8
|
-
*
|
|
6
|
+
* Parses the soul-memory file looking for a "last-revision: YYYY-MM-DD"
|
|
7
|
+
* marker (tolerant of markdown bold, e.g. "**last-revision:** 2026-02-21").
|
|
8
|
+
* If missing or stale, emits a reminder as additionalContext so the agent
|
|
9
|
+
* (not just the user) sees it. Never blocks, always exits 0.
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
const fs = require('fs');
|
|
12
13
|
const path = require('path');
|
|
13
14
|
|
|
14
15
|
const STALE_DAYS = 14;
|
|
16
|
+
const SOUL_REVISION_WORKFLOW = '_byan/workflow/simple/byan/soul-revision.md';
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
// Gen3 _byan/agent/byan/soul-memory.md first, Gen2 _byan/soul-memory.md fallback.
|
|
18
|
-
const
|
|
19
|
-
|
|
18
|
+
function memoryPathFor(projectDir) {
|
|
19
|
+
// Gen3 _byan/agent/byan/soul-memory.md first, Gen2 _byan/soul-memory.md fallback.
|
|
20
|
+
const gen3 = path.join(projectDir, '_byan', 'agent', 'byan', 'soul-memory.md');
|
|
21
|
+
return fs.existsSync(gen3) ? gen3 : path.join(projectDir, '_byan', 'soul-memory.md');
|
|
22
|
+
}
|
|
20
23
|
|
|
21
24
|
function findLastRevision(content) {
|
|
22
|
-
|
|
25
|
+
// [^\d]{0,8} tolerates markdown punctuation between the label and the date
|
|
26
|
+
// (e.g. "**last-revision:** 2026-02-21" or "last_revision = 2026-02-21")
|
|
27
|
+
// while forbidding any digit in the gap, so a stray earlier year cannot be
|
|
28
|
+
// captured instead of the real date.
|
|
29
|
+
const m = (content || '').match(/last[-_ ]revision[^\d]{0,8}(\d{4}-\d{2}-\d{2})/i);
|
|
23
30
|
return m ? m[1] : null;
|
|
24
31
|
}
|
|
25
32
|
|
|
@@ -29,26 +36,43 @@ function daysSince(dateStr, now = new Date()) {
|
|
|
29
36
|
return Math.floor((now - then) / (1000 * 60 * 60 * 24));
|
|
30
37
|
}
|
|
31
38
|
|
|
32
|
-
|
|
39
|
+
// Pure reminder builder — returns the additionalContext string (or '').
|
|
40
|
+
function buildReminder(content, memoryRel, now = new Date()) {
|
|
41
|
+
const last = findLastRevision(content);
|
|
42
|
+
const age = last ? daysSince(last, now) : null;
|
|
43
|
+
|
|
44
|
+
if (last == null) {
|
|
45
|
+
return `BYAN soul-memory reminder: no last-revision marker found in ${memoryRel}. Consider running the soul-revision workflow (${SOUL_REVISION_WORKFLOW}) early this session.`;
|
|
46
|
+
}
|
|
47
|
+
if (age != null && age > STALE_DAYS) {
|
|
48
|
+
return `BYAN soul-memory reminder: last revision of ${memoryRel} was ${last} (${age} days ago, threshold ${STALE_DAYS}). Per soul-activation protocol, offer to run ${SOUL_REVISION_WORKFLOW} after greeting. User can postpone with "pas maintenant" (+7 days).`;
|
|
49
|
+
}
|
|
50
|
+
return '';
|
|
51
|
+
}
|
|
33
52
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const age = last ? daysSince(last) : null;
|
|
53
|
+
if (require.main === module) {
|
|
54
|
+
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
55
|
+
const memoryPath = memoryPathFor(projectDir);
|
|
56
|
+
let additionalContext = '';
|
|
39
57
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
additionalContext =
|
|
58
|
+
try {
|
|
59
|
+
if (fs.existsSync(memoryPath)) {
|
|
60
|
+
const content = fs.readFileSync(memoryPath, 'utf8');
|
|
61
|
+
additionalContext = buildReminder(content, path.relative(projectDir, memoryPath));
|
|
44
62
|
}
|
|
63
|
+
} catch {
|
|
64
|
+
// never block
|
|
45
65
|
}
|
|
46
|
-
} catch {
|
|
47
|
-
// never block
|
|
48
|
-
}
|
|
49
66
|
|
|
50
|
-
if (additionalContext) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
67
|
+
if (additionalContext) {
|
|
68
|
+
process.stdout.write(
|
|
69
|
+
JSON.stringify({
|
|
70
|
+
hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext },
|
|
71
|
+
})
|
|
72
|
+
);
|
|
73
|
+
} else {
|
|
74
|
+
process.stdout.write('{}');
|
|
75
|
+
}
|
|
54
76
|
}
|
|
77
|
+
|
|
78
|
+
module.exports = { findLastRevision, daysSince, buildReminder, memoryPathFor, SOUL_REVISION_WORKFLOW };
|
|
@@ -4,19 +4,30 @@
|
|
|
4
4
|
* signals in the user message and suggests a mid-session soul-memory entry.
|
|
5
5
|
*
|
|
6
6
|
* Non-blocking: never rejects the prompt. Emits a short nudge when a
|
|
7
|
-
* trigger keyword matches.
|
|
8
|
-
*
|
|
7
|
+
* trigger keyword matches. One nudge per session is enforced via a file
|
|
8
|
+
* marker; the marker is reset at SessionStart by inject-soul.js so the
|
|
9
|
+
* one-shot is per-session, not per-lifetime.
|
|
10
|
+
*
|
|
11
|
+
* The nudge names the byan_soul_memory_append MCP tool explicitly so the
|
|
12
|
+
* reflect -> append loop actually closes (the agent knows HOW to persist
|
|
13
|
+
* the entry, after the user validates it).
|
|
9
14
|
*/
|
|
10
15
|
|
|
11
16
|
const fs = require('fs');
|
|
12
17
|
const path = require('path');
|
|
13
18
|
|
|
14
19
|
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
20
|
+
|
|
15
21
|
// State marker lives under the memory dir: Gen3 _byan/memoire/ first, Gen2
|
|
16
|
-
// _byan/_memory/ fallback (whichever dir exists; default Gen2).
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
// _byan/_memory/ fallback (whichever dir exists; default Gen2). MUST resolve
|
|
23
|
+
// identically to inject-soul.js nudgeMarkerPath (it resets this marker at
|
|
24
|
+
// SessionStart) — the soul-hooks parity test pins that invariant.
|
|
25
|
+
function markerPathFor(dir) {
|
|
26
|
+
const memoireDir = path.join(dir, '_byan', 'memoire');
|
|
27
|
+
const memoryDir = fs.existsSync(memoireDir) ? memoireDir : path.join(dir, '_byan', '_memory');
|
|
28
|
+
return path.join(memoryDir, '.soul-memory-nudge-sent');
|
|
29
|
+
}
|
|
30
|
+
const markerPath = markerPathFor(projectDir);
|
|
20
31
|
|
|
21
32
|
const TRIGGERS = {
|
|
22
33
|
resonance: ['resonne', 'ca me parle', 'exactement', 'c\'est ca', 'that resonates'],
|
|
@@ -44,7 +55,13 @@ function findTrigger(text) {
|
|
|
44
55
|
return null;
|
|
45
56
|
}
|
|
46
57
|
|
|
47
|
-
|
|
58
|
+
// Build the nudge text. It names the byan_soul_memory_append MCP tool so the
|
|
59
|
+
// reflect -> append loop closes instead of dead-ending at "consider offering".
|
|
60
|
+
function buildNudge(hit) {
|
|
61
|
+
return `BYAN soul-memory trigger detected (${hit.category}): "${hit.pattern}". Per soul-memory protocol, offer the user a mid-session introspection entry; if they validate it, persist it by calling the byan_soul_memory_append MCP tool (entry = the insight, category = ${hit.category}). One nudge per session, always validated by the user first.`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (require.main === module) (async () => {
|
|
48
65
|
let additionalContext = '';
|
|
49
66
|
|
|
50
67
|
try {
|
|
@@ -60,7 +77,7 @@ function findTrigger(text) {
|
|
|
60
77
|
if (!fs.existsSync(markerPath)) {
|
|
61
78
|
const hit = findTrigger(prompt);
|
|
62
79
|
if (hit) {
|
|
63
|
-
additionalContext =
|
|
80
|
+
additionalContext = buildNudge(hit);
|
|
64
81
|
try {
|
|
65
82
|
fs.mkdirSync(path.dirname(markerPath), { recursive: true });
|
|
66
83
|
fs.writeFileSync(markerPath, new Date().toISOString());
|
|
@@ -82,3 +99,5 @@ function findTrigger(text) {
|
|
|
82
99
|
})
|
|
83
100
|
);
|
|
84
101
|
})();
|
|
102
|
+
|
|
103
|
+
module.exports = { findTrigger, buildNudge, markerPathFor, TRIGGERS };
|