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.
Files changed (172) hide show
  1. package/CHANGELOG.md +230 -0
  2. package/README.md +9 -12
  3. package/install/bin/create-byan-agent-v2.js +29 -169
  4. package/install/lib/agent-generator.js +5 -5
  5. package/install/lib/byan-web-integration.js +1 -1
  6. package/install/lib/claude-native-setup.js +1 -1
  7. package/install/lib/phase2-chat.js +3 -10
  8. package/install/lib/platforms/claude-code.js +2 -2
  9. package/install/lib/platforms/index.js +0 -2
  10. package/install/lib/project-agents-generator.js +3 -3
  11. package/install/lib/staging-consent.js +3 -3
  12. package/install/lib/subagent-generator.js +3 -3
  13. package/install/lib/yanstaller/agent-launcher.js +1 -27
  14. package/install/lib/yanstaller/detector.js +4 -4
  15. package/install/lib/yanstaller/installer.js +0 -2
  16. package/install/lib/yanstaller/interviewer.js +1 -1
  17. package/install/lib/yanstaller/platform-selector.js +1 -13
  18. package/install/package.json +1 -1
  19. package/install/src/byan-v2/context/session-state.js +2 -2
  20. package/install/src/byan-v2/index.js +2 -6
  21. package/install/src/byan-v2/orchestrator/generation-state.js +4 -4
  22. package/install/src/webui/api.js +0 -2
  23. package/install/src/webui/chat/bridge.js +1 -13
  24. package/install/src/webui/chat/cli-detector.js +0 -23
  25. package/install/src/webui/public/app.js +1 -3
  26. package/install/src/webui/public/chat.html +0 -2
  27. package/install/src/webui/public/chat.js +0 -1
  28. package/install/src/webui/public/index.html +2 -2
  29. package/install/templates/.claude/CLAUDE.md +13 -2
  30. package/install/templates/.claude/agents/bmad-byan.md +1 -1
  31. package/install/templates/.claude/hooks/autobench-stop-guard.js +286 -0
  32. package/install/templates/.claude/hooks/drain-advisory.js +85 -0
  33. package/install/templates/.claude/hooks/fact-check-absolutes.js +1 -61
  34. package/install/templates/.claude/hooks/fact-check-claims.js +69 -0
  35. package/install/templates/.claude/hooks/fd-response-check.js +37 -46
  36. package/install/templates/.claude/hooks/inject-soul.js +64 -25
  37. package/install/templates/.claude/hooks/leantime-fd-sync.js +216 -0
  38. package/install/templates/.claude/hooks/lib/autobench-config.json +81 -0
  39. package/install/templates/.claude/hooks/lib/autobench-fc-enrich.js +251 -0
  40. package/install/templates/.claude/hooks/lib/autobench-ledger-report.js +253 -0
  41. package/install/templates/.claude/hooks/lib/autobench-runtime.js +199 -0
  42. package/install/templates/.claude/hooks/lib/fact-check-core.js +69 -0
  43. package/install/templates/.claude/hooks/lib/failure-detector.js +18 -4
  44. package/install/templates/.claude/hooks/lib/transcript-read.js +137 -0
  45. package/install/templates/.claude/hooks/soul-memory-check.js +49 -25
  46. package/install/templates/.claude/hooks/soul-memory-triggers.js +27 -8
  47. package/install/templates/.claude/hooks/stage-to-byan.js +25 -7
  48. package/install/templates/.claude/hooks/strict-stop-guard.js +4 -16
  49. package/install/templates/.claude/rules/benchmark.md +251 -0
  50. package/install/templates/.claude/rules/byan-agents.md +0 -1
  51. package/install/templates/.claude/rules/byan-api.md +64 -0
  52. package/install/templates/.claude/rules/fact-check.md +1 -1
  53. package/install/templates/.claude/rules/strict-mode.md +10 -9
  54. package/install/templates/.claude/settings.json +16 -0
  55. package/install/templates/.claude/skills/byan-benchmark/SKILL.md +159 -0
  56. package/install/templates/.claude/skills/byan-byan/SKILL.md +73 -12
  57. package/install/templates/.claude/skills/byan-fact-check/SKILL.md +1 -1
  58. package/install/templates/.claude/skills/byan-hermes-dispatch/SKILL.md +5 -6
  59. package/install/templates/.claude/skills/byan-insight/SKILL.md +56 -0
  60. package/install/templates/.claude/skills/byan-orchestrate/SKILL.md +11 -3
  61. package/install/templates/.claude/skills/byan-strict/SKILL.md +4 -1
  62. package/install/templates/.claude/workflows/INDEX.md +2 -1
  63. package/install/templates/.claude/workflows/byan-benchmark.js +328 -0
  64. package/install/templates/.claude/workflows/check-implementation-readiness.js +1 -1
  65. package/install/templates/_byan/_config/agent-manifest.csv +1 -1
  66. package/install/templates/_byan/_config/autobench.yaml +510 -0
  67. package/install/templates/_byan/_config/strict-mode.yaml +9 -3
  68. package/install/templates/_byan/_config/workflow-manifest.csv +1 -0
  69. package/install/templates/_byan/agent/byan/byan.md +1 -3
  70. package/install/templates/_byan/agent/byan-flat/byan.md +1 -3
  71. package/install/templates/_byan/agent/byan-test/byan-test.md +2 -2
  72. package/install/templates/_byan/agent/byan-test-flat/byan-test.md +2 -2
  73. package/install/templates/_byan/agent/byan.optimized/byan.optimized.md +2 -2
  74. package/install/templates/_byan/agent/byan.optimized-v2/byan.optimized-v2.md +2 -2
  75. package/install/templates/_byan/agent/claude/claude.md +0 -2
  76. package/install/templates/_byan/agent/codex/codex.md +0 -2
  77. package/install/templates/_byan/agent/rachid/rachid.md +2 -10
  78. package/install/templates/_byan/agent/rachid-flat/rachid.md +2 -11
  79. package/install/templates/_byan/agent/turbo-whisper/turbo-whisper.md +2 -5
  80. package/install/templates/_byan/agent/turbo-whisper-integration/turbo-whisper-integration.md +5 -13
  81. package/install/templates/_byan/agent/yanstaller/yanstaller.md +2 -24
  82. package/install/templates/_byan/config.yaml +0 -1
  83. package/install/templates/_byan/core/activation/soul-activation.md +3 -3
  84. package/install/templates/_byan/mcp/byan-mcp-server/bin/byan-insight-digest.js +31 -0
  85. package/install/templates/_byan/mcp/byan-mcp-server/bin/byan-sync-rules.js +20 -4
  86. package/install/templates/_byan/mcp/byan-mcp-server/lib/advisory-autofeed.js +96 -0
  87. package/install/templates/_byan/mcp/byan-mcp-server/lib/index-generator.js +1 -1
  88. package/install/templates/_byan/mcp/byan-mcp-server/lib/insight-harvest.js +220 -0
  89. package/install/templates/_byan/mcp/byan-mcp-server/lib/kanban.js +6 -3
  90. package/install/templates/_byan/mcp/byan-mcp-server/lib/leantime-fd-core.js +205 -0
  91. package/install/templates/_byan/mcp/byan-mcp-server/lib/leantime-sync.js +415 -0
  92. package/install/templates/_byan/mcp/byan-mcp-server/lib/outcome-buffer.js +64 -0
  93. package/install/templates/_byan/mcp/byan-mcp-server/lib/precommit-gate.js +1 -1
  94. package/install/templates/_byan/mcp/byan-mcp-server/lib/strict-activation.js +1 -1
  95. package/install/templates/_byan/mcp/byan-mcp-server/lib/strict-mode.js +8 -0
  96. package/install/templates/_byan/mcp/byan-mcp-server/lib/sync-rules.js +172 -23
  97. package/install/templates/_byan/mcp/byan-mcp-server/lib/workflows-generator.js +1 -0
  98. package/install/templates/_byan/mcp/byan-mcp-server/server.js +262 -81
  99. package/install/templates/_byan/worker/launchers/README.md +4 -24
  100. package/install/templates/_byan/worker/workers.md +8 -9
  101. package/install/templates/_byan/workflow/simple/bmb/byan-benchmark/workflow.md +86 -0
  102. package/install/templates/_byan/workflow/simple/byan/feature-workflow.md +2 -2
  103. package/install/templates/docs/leantime-integration.md +160 -0
  104. package/package.json +3 -7
  105. package/src/byan-v2/context/session-state.js +2 -2
  106. package/src/byan-v2/generation/mantra-validator.js +3 -3
  107. package/src/byan-v2/index.js +1 -5
  108. package/src/byan-v2/integration/voice-integration.js +1 -1
  109. package/src/byan-v2/orchestrator/generation-state.js +4 -4
  110. package/src/loadbalancer/loadbalancer.js +1 -1
  111. package/src/staging/staging.js +20 -6
  112. package/install/bin/build-copilot-stubs.js +0 -138
  113. package/install/lib/platforms/copilot-cli.js +0 -123
  114. package/install/lib/platforms/vscode.js +0 -51
  115. package/install/src/byan-v2/context/copilot-context.js +0 -79
  116. package/install/src/webui/chat/copilot-adapter.js +0 -68
  117. package/install/templates/.claude/agents/bmad-marc.md +0 -25
  118. package/install/templates/.claude/skills/byan-marc/SKILL.md +0 -20
  119. package/install/templates/.github/agents/bmad-agent-bmad-master.md +0 -16
  120. package/install/templates/.github/agents/bmad-agent-bmb-agent-builder.md +0 -16
  121. package/install/templates/.github/agents/bmad-agent-bmb-module-builder.md +0 -16
  122. package/install/templates/.github/agents/bmad-agent-bmb-workflow-builder.md +0 -16
  123. package/install/templates/.github/agents/bmad-agent-bmm-analyst.md +0 -16
  124. package/install/templates/.github/agents/bmad-agent-bmm-architect.md +0 -16
  125. package/install/templates/.github/agents/bmad-agent-bmm-dev.md +0 -16
  126. package/install/templates/.github/agents/bmad-agent-bmm-pm.md +0 -16
  127. package/install/templates/.github/agents/bmad-agent-bmm-quick-flow-solo-dev.md +0 -16
  128. package/install/templates/.github/agents/bmad-agent-bmm-quinn.md +0 -16
  129. package/install/templates/.github/agents/bmad-agent-bmm-sm.md +0 -16
  130. package/install/templates/.github/agents/bmad-agent-bmm-tech-writer.md +0 -16
  131. package/install/templates/.github/agents/bmad-agent-bmm-ux-designer.md +0 -16
  132. package/install/templates/.github/agents/bmad-agent-byan-test.md +0 -33
  133. package/install/templates/.github/agents/bmad-agent-byan-v2.md +0 -44
  134. package/install/templates/.github/agents/bmad-agent-byan.md +0 -1062
  135. package/install/templates/.github/agents/bmad-agent-carmack.md +0 -14
  136. package/install/templates/.github/agents/bmad-agent-cis-brainstorming-coach.md +0 -16
  137. package/install/templates/.github/agents/bmad-agent-cis-creative-problem-solver.md +0 -16
  138. package/install/templates/.github/agents/bmad-agent-cis-design-thinking-coach.md +0 -16
  139. package/install/templates/.github/agents/bmad-agent-cis-innovation-strategist.md +0 -16
  140. package/install/templates/.github/agents/bmad-agent-cis-presentation-master.md +0 -16
  141. package/install/templates/.github/agents/bmad-agent-cis-storyteller.md +0 -16
  142. package/install/templates/.github/agents/bmad-agent-claude.md +0 -49
  143. package/install/templates/.github/agents/bmad-agent-codex.md +0 -49
  144. package/install/templates/.github/agents/bmad-agent-drawio.md +0 -45
  145. package/install/templates/.github/agents/bmad-agent-fact-checker.md +0 -16
  146. package/install/templates/.github/agents/bmad-agent-forgeron.md +0 -15
  147. package/install/templates/.github/agents/bmad-agent-jimmy.md +0 -15
  148. package/install/templates/.github/agents/bmad-agent-marc.md +0 -49
  149. package/install/templates/.github/agents/bmad-agent-mike.md +0 -15
  150. package/install/templates/.github/agents/bmad-agent-patnote.md +0 -49
  151. package/install/templates/.github/agents/bmad-agent-rachid.md +0 -48
  152. package/install/templates/.github/agents/bmad-agent-skeptic.md +0 -16
  153. package/install/templates/.github/agents/bmad-agent-tao.md +0 -14
  154. package/install/templates/.github/agents/bmad-agent-tea-tea.md +0 -16
  155. package/install/templates/.github/agents/bmad-agent-test-dynamic.md +0 -22
  156. package/install/templates/.github/agents/bmad-agent-yanstaller-interview.md +0 -50
  157. package/install/templates/.github/agents/bmad-agent-yanstaller-phase2.md +0 -189
  158. package/install/templates/.github/agents/bmad-agent-yanstaller.md +0 -350
  159. package/install/templates/.github/agents/expert-merise-agile.md +0 -178
  160. package/install/templates/.github/agents/franck.md +0 -379
  161. package/install/templates/.github/agents/hermes.md +0 -575
  162. package/install/templates/.github/extensions/byan-staging/extension.mjs +0 -169
  163. package/install/templates/.github/extensions/byan-staging/package.json +0 -8
  164. package/install/templates/_byan/agent/marc/marc-soul.md +0 -47
  165. package/install/templates/_byan/agent/marc/marc-tao.md +0 -77
  166. package/install/templates/_byan/agent/marc/marc.md +0 -324
  167. package/install/templates/_byan/agent/marc-flat/marc.md +0 -387
  168. package/install/templates/_byan/mcp/byan-mcp-server/lib/copilot.js +0 -148
  169. package/install/templates/_byan/worker/launchers/launch-yanstaller-copilot.md +0 -173
  170. package/install/templates/workers/cost-optimizer.js +0 -169
  171. package/src/byan-v2/context/copilot-context.js +0 -79
  172. 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 file content (Write/Edit
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 file content itself contains the literal phrase "internal error"
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 only trust is_error flag.
34
- if (ECHO_TOOLS.has(toolName)) {
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 _byan/soul-memory.md looking for "last-revision: YYYY-MM-DD"
7
- * (common soul-memory protocol). If missing or stale, emits a reminder
8
- * as additionalContext. Never blocks, always exits 0.
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
- const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
17
- // Gen3 _byan/agent/byan/soul-memory.md first, Gen2 _byan/soul-memory.md fallback.
18
- const memoryGen3 = path.join(projectDir, '_byan', 'agent', 'byan', 'soul-memory.md');
19
- const memoryPath = fs.existsSync(memoryGen3) ? memoryGen3 : path.join(projectDir, '_byan', 'soul-memory.md');
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
- const m = content.match(/last[-_ ]revision\s*[:=]\s*(\d{4}-\d{2}-\d{2})/i);
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
- let additionalContext = '';
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
- try {
35
- if (fs.existsSync(memoryPath)) {
36
- const content = fs.readFileSync(memoryPath, 'utf8');
37
- const last = findLastRevision(content);
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
- if (last == null) {
41
- additionalContext = `BYAN soul-memory reminder: no last-revision marker found in _byan/soul-memory.md. Consider running the soul-revision workflow early this session.`;
42
- } else if (age != null && age > STALE_DAYS) {
43
- additionalContext = `BYAN soul-memory reminder: last revision was ${last} (${age} days ago, threshold ${STALE_DAYS}). Per soul-activation protocol, offer to run _byan/workflows/byan/soul-revision.md after greeting. User can postpone with "pas maintenant" (+7 days).`;
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
- process.stdout.write(JSON.stringify({ systemMessage: additionalContext }));
52
- } else {
53
- process.stdout.write('{}');
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. Max one nudge per session is enforced via a
8
- * file marker under _byan/_memory/.
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
- const memoireDir = path.join(projectDir, '_byan', 'memoire');
18
- const memoryDir = fs.existsSync(memoireDir) ? memoireDir : path.join(projectDir, '_byan', '_memory');
19
- const markerPath = path.join(memoryDir, '.soul-memory-nudge-sent');
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
- (async () => {
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 = `BYAN soul-memory trigger detected (${hit.category}): "${hit.pattern}". Per soul-memory protocol, consider offering the user a mid-session introspection entry. Maximum 2 entries per session, always validated by user.`;
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 };