create-byan-agent 2.9.4 → 2.9.5

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 (92) hide show
  1. package/install/bin/byan-cleanup.js +156 -0
  2. package/install/bin/byan-kanban.js +159 -0
  3. package/install/bin/byan-ledger.js +45 -0
  4. package/install/lib/cleanup/detector.js +154 -0
  5. package/install/lib/cleanup/executor.js +72 -0
  6. package/install/lib/subagent-generator.js +208 -0
  7. package/install/lib/token-ledger.js +131 -0
  8. package/install/templates/.claude/agents/bmad-bmad-master.md +14 -0
  9. package/install/templates/.claude/agents/bmad-bmb-agent-builder.md +14 -0
  10. package/install/templates/.claude/agents/bmad-bmb-module-builder.md +14 -0
  11. package/install/templates/.claude/agents/bmad-bmb-workflow-builder.md +14 -0
  12. package/install/templates/.claude/agents/bmad-bmm-analyst.md +14 -0
  13. package/install/templates/.claude/agents/bmad-bmm-architect.md +14 -0
  14. package/install/templates/.claude/agents/bmad-bmm-dev.md +14 -0
  15. package/install/templates/.claude/agents/bmad-bmm-pm.md +14 -0
  16. package/install/templates/.claude/agents/bmad-bmm-quick-flow-solo-dev.md +14 -0
  17. package/install/templates/.claude/agents/bmad-bmm-quinn.md +14 -0
  18. package/install/templates/.claude/agents/bmad-bmm-sm.md +14 -0
  19. package/install/templates/.claude/agents/bmad-bmm-tech-writer.md +14 -0
  20. package/install/templates/.claude/agents/bmad-bmm-ux-designer.md +14 -0
  21. package/install/templates/.claude/agents/bmad-byan-v2.md +14 -0
  22. package/install/templates/.claude/agents/bmad-byan.md +152 -0
  23. package/install/templates/.claude/agents/bmad-carmack.md +14 -0
  24. package/install/templates/.claude/agents/bmad-cis-brainstorming-coach.md +14 -0
  25. package/install/templates/.claude/agents/bmad-cis-creative-problem-solver.md +14 -0
  26. package/install/templates/.claude/agents/bmad-cis-design-thinking-coach.md +14 -0
  27. package/install/templates/.claude/agents/bmad-cis-innovation-strategist.md +14 -0
  28. package/install/templates/.claude/agents/bmad-cis-presentation-master.md +14 -0
  29. package/install/templates/.claude/agents/bmad-cis-storyteller.md +14 -0
  30. package/install/templates/.claude/agents/bmad-claude.md +26 -0
  31. package/install/templates/.claude/agents/bmad-codex.md +26 -0
  32. package/install/templates/.claude/agents/bmad-compliance.md +68 -0
  33. package/install/templates/.claude/agents/bmad-drawio.md +25 -0
  34. package/install/templates/.claude/agents/bmad-expert-merise-agile.md +54 -0
  35. package/install/templates/.claude/agents/bmad-fact-checker.md +14 -0
  36. package/install/templates/.claude/agents/bmad-forgeron.md +14 -0
  37. package/install/templates/.claude/agents/bmad-hermes.md +59 -0
  38. package/install/templates/.claude/agents/bmad-marc.md +25 -0
  39. package/install/templates/.claude/agents/bmad-patnote.md +26 -0
  40. package/install/templates/.claude/agents/bmad-rachid.md +25 -0
  41. package/install/templates/.claude/agents/bmad-tao.md +14 -0
  42. package/install/templates/.claude/agents/bmad-tea-tea.md +14 -0
  43. package/install/templates/.claude/agents/bmad-yanstaller.md +47 -0
  44. package/install/templates/.claude/hooks/fact-check-absolutes.js +185 -0
  45. package/install/templates/.claude/hooks/fd-phase-guard.js +87 -0
  46. package/install/templates/.claude/hooks/fd-response-check.js +92 -0
  47. package/install/templates/.claude/hooks/lib/failure-detector.js +14 -0
  48. package/install/templates/.claude/hooks/pre-compact-save.js +148 -0
  49. package/install/templates/.claude/hooks/tool-failure-guard.js +6 -0
  50. package/install/templates/.claude/hooks/tool-transparency.js +4 -0
  51. package/install/templates/.claude/settings.json +23 -0
  52. package/install/templates/.claude/skills/byan-byan/SKILL.md +115 -163
  53. package/install/templates/.claude/skills/byan-orchestrate/SKILL.md +100 -0
  54. package/install/templates/.githooks/pre-commit +75 -0
  55. package/install/templates/_byan/mcp/byan-mcp-server/lib/copilot.js +148 -0
  56. package/install/templates/_byan/mcp/byan-mcp-server/lib/fd-state.js +163 -0
  57. package/install/templates/_byan/mcp/byan-mcp-server/lib/kanban.js +226 -0
  58. package/install/templates/_byan/mcp/byan-mcp-server/lib/peer-review.js +187 -0
  59. package/install/templates/_byan/mcp/byan-mcp-server/server.js +463 -0
  60. package/install/templates/detector.js +154 -0
  61. package/package.json +6 -7
  62. package/src/loadbalancer/capability-matrix.js +157 -0
  63. package/src/loadbalancer/config.js +141 -0
  64. package/src/loadbalancer/graceful-degradation.js +212 -0
  65. package/src/loadbalancer/health-probe.js +151 -0
  66. package/src/loadbalancer/hooks/claude-hooks.js +53 -0
  67. package/src/loadbalancer/hooks/copilot-hooks.js +74 -0
  68. package/src/loadbalancer/index.js +81 -0
  69. package/src/loadbalancer/loadbalancer.default.yaml +65 -0
  70. package/src/loadbalancer/loadbalancer.js +324 -0
  71. package/src/loadbalancer/mcp-server.js +304 -0
  72. package/src/loadbalancer/metrics.js +146 -0
  73. package/src/loadbalancer/native/claude-integration.js +64 -0
  74. package/src/loadbalancer/native/copilot-integration.js +59 -0
  75. package/src/loadbalancer/pressure-score.js +102 -0
  76. package/src/loadbalancer/providers/base-provider.js +80 -0
  77. package/src/loadbalancer/providers/byan-api-provider.js +132 -0
  78. package/src/loadbalancer/providers/claude-provider.js +113 -0
  79. package/src/loadbalancer/providers/copilot-provider.js +104 -0
  80. package/src/loadbalancer/rate-limit-tracker.js +216 -0
  81. package/src/loadbalancer/session-bridge.js +179 -0
  82. package/src/loadbalancer/state/db.js +211 -0
  83. package/src/loadbalancer/state/migrations/001-initial.sql +50 -0
  84. package/src/loadbalancer/tools/index.js +123 -0
  85. package/src/loadbalancer/velocity-estimator.js +147 -0
  86. package/update-byan-agent/bin/update-byan-agent.js +27 -2
  87. package/API-BYAN-V2.md +0 -741
  88. package/BMAD-QUICK-REFERENCE.md +0 -370
  89. package/CHANGELOG-v2.1.0.md +0 -371
  90. package/MIGRATION-v2.0-to-v2.1.md +0 -430
  91. package/README-BYAN-V2.md +0 -446
  92. package/TEST-GUIDE-v2.3.2.md +0 -161
@@ -0,0 +1,14 @@
1
+ ---
2
+ name: bmad-tea-tea
3
+ description: tea agent
4
+ model: opus
5
+ color: purple
6
+ ---
7
+
8
+ # bmad-tea-tea
9
+
10
+ tea agent
11
+
12
+ ## Reporting contract
13
+
14
+ When invoked via the Agent tool, stay in the persona above. Respond with a concise JSON report when the task completes : { status: "ok|partial|failed", summary: "<200 words", files_changed: [paths], next_steps: [] }.
@@ -0,0 +1,47 @@
1
+ ---
2
+ name: bmad-yanstaller
3
+ description: Yanstaller - Multi-Platform BYAN Installer Agent
4
+ model: sonnet
5
+ color: blue
6
+ ---
7
+
8
+ # bmad-yanstaller
9
+
10
+ Yanstaller - Multi-Platform BYAN Installer Agent
11
+
12
+ ## Persona
13
+
14
+ Installation Expert + Platform Detection Specialist + Zero-Config Automation
15
+ Elite installer agent that automates BYAN deployment across multiple AI platforms. Detects environments, validates dependencies, installs agents, and configures everything with zero user interaction. Applies Ockham's Razor - simplest installation that works.
16
+ Concise logs, clear progress indicators, actionable error messages. No questions in auto mode. Emojis for visual feedback only (✓, ⚠, ✗).
17
+
18
+
19
+ • Zero-Config First: Auto-detect everything possible
20
+ • Trust But Verify: Validate all detections
21
+ • Ockham's Razor: Simplest approach that works
22
+ • Fail-Safe: Continue on optional failures (Turbo Whisper)
23
+ • User Override: Respect --skip-* and explicit configs
24
+ • Clean Logs: Progress, not noise
25
+
26
+
27
+
28
+ #37 Ockham's Razor, #39 Consequences, IA-1 Trust But Verify, IA-23 No Emoji in code/commits, IA-24 Clean Code
29
+
30
+ ## Operating rules
31
+
32
+ - ALWAYS use gpt-5-mini model (unless --model override)
33
+ - Interview mode → Pure JSON output (parseable)
34
+ - Install mode → Workflow execution with logs
35
+ - Agent only orchestrates, workflows do the work
36
+ - Keep agent lean (under 3 KB)
37
+
38
+ ## Menu commands
39
+
40
+ - [AUTO] Auto-install (all platforms)
41
+ - [DETECT] Detect platforms only
42
+ - [HELP] Installation help
43
+ - [EXIT] Exit Yanstaller
44
+
45
+ ## Reporting contract
46
+
47
+ When invoked via the Agent tool, stay in the persona above. Respond with a concise JSON report when the task completes : { status: "ok|partial|failed", summary: "<200 words", files_changed: [paths], next_steps: [] }.
@@ -0,0 +1,185 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * PreToolUse hook — fact-check absolutes guard.
4
+ *
5
+ * Scans Edit/Write tool inputs on markdown/documentation paths for
6
+ * absolute claims (`always`, `never`, `obviously`, `faster`, `better`,
7
+ * `toujours`, `jamais`, `forcement`) without an accompanying source
8
+ * reference.
9
+ *
10
+ * When an unsourced absolute is detected on a doc file, the hook exits
11
+ * with decision=block and a clear reason, forcing the author to cite a
12
+ * source (matching `_byan/knowledge/sources.md`, `RFC`, `CVE-`, a URL,
13
+ * or a `[CLAIM L<n>]` prefix) before writing.
14
+ *
15
+ * Non-blocking outside of Edit/Write tools or when the target is code
16
+ * (not documentation).
17
+ */
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+
22
+ const ABSOLUTES = [
23
+ /\btoujours\b/i,
24
+ /\bjamais\b/i,
25
+ /\bforc[eé]ment\b/i,
26
+ /\bobviously\b/i,
27
+ /\balways\b/i,
28
+ /\bnever\b/i,
29
+ /\bclearly\b/i,
30
+ /\bundoubtedly\b/i,
31
+ /\bfaster than\b/i,
32
+ /\bbetter than\b/i,
33
+ /\bplus rapide que\b/i,
34
+ /\bmeilleur que\b/i,
35
+ ];
36
+
37
+ const SOURCE_MARKERS = [
38
+ /\bRFC\s*\d+/i,
39
+ /\bCVE-\d{4}-\d+/i,
40
+ /https?:\/\//,
41
+ /\[CLAIM\s+L[1-5]\]/i,
42
+ /\[FACT\s+USER-VERIFIED/i,
43
+ /\bsource\s*:/i,
44
+ /_byan\/knowledge\/sources\.md/,
45
+ ];
46
+
47
+ const DOC_EXTS = ['.md', '.mdx', '.rst', '.txt'];
48
+
49
+ // Paths exempted from scanning — these files DESCRIBE the rule or meta-docs
50
+ // where absolutes appear as examples, not as claims.
51
+ const EXEMPT_PATH_PATTERNS = [
52
+ /\.claude\/hooks\//,
53
+ /\.claude\/agents\/bmad-compliance\.md$/,
54
+ /_byan\/mcp\/byan-mcp-server\/lib\/(peer-review|fd-state)\.js$/,
55
+ /_byan\/mcp\/byan-mcp-server\/test\//,
56
+ /_byan\/knowledge\/(fact-check|mantras)/i,
57
+ /\/fact-check-absolutes\.js$/,
58
+ /\.claude\/skills\/byan-fact-check\//,
59
+ /install\/__tests__\/.*fact-check/i,
60
+ /__tests__\/.*fact-check/i,
61
+ ];
62
+
63
+ function isExemptPath(filePath) {
64
+ if (!filePath) return false;
65
+ return EXEMPT_PATH_PATTERNS.some((re) => re.test(filePath));
66
+ }
67
+
68
+ // Strip content that cannot be a claim :
69
+ // - fenced code blocks ``` ... ```
70
+ // - inline backticks `...`
71
+ // - block quotes (lines starting with >)
72
+ // - regex / array syntax that contains the word as a token
73
+ function stripNonClaimZones(text) {
74
+ if (!text) return '';
75
+ return text
76
+ // Fenced code blocks
77
+ .replace(/```[\s\S]*?```/g, '')
78
+ // Inline code
79
+ .replace(/`[^`\n]+`/g, '')
80
+ // Markdown block quotes
81
+ .replace(/^> .*$/gm, '')
82
+ // Lines that look like list of patterns (e.g. "- toujours")
83
+ .replace(/^[\s-]*['"]?\b(toujours|jamais|forc[eé]ment|obviously|always|never|clearly|undoubtedly)\b['"]?/gim, '');
84
+ }
85
+
86
+ function readStdin() {
87
+ return new Promise((resolve) => {
88
+ if (process.stdin.isTTY) return resolve('');
89
+ let data = '';
90
+ process.stdin.on('data', (c) => (data += c));
91
+ process.stdin.on('end', () => resolve(data));
92
+ process.stdin.on('error', () => resolve(data));
93
+ });
94
+ }
95
+
96
+ function isDoc(filePath) {
97
+ if (!filePath) return false;
98
+ return DOC_EXTS.some((ext) => filePath.toLowerCase().endsWith(ext));
99
+ }
100
+
101
+ function extractText(toolName, input) {
102
+ if (!input) return '';
103
+ if (toolName === 'Write') return String(input.content || '');
104
+ if (toolName === 'Edit') {
105
+ return [input.new_string, input.old_string].filter(Boolean).join('\n');
106
+ }
107
+ return '';
108
+ }
109
+
110
+ function findUnsourced(text) {
111
+ if (!text) return null;
112
+ for (const re of ABSOLUTES) {
113
+ const match = text.match(re);
114
+ if (!match) continue;
115
+ const idx = match.index || 0;
116
+ const windowStart = Math.max(0, idx - 240);
117
+ const windowEnd = Math.min(text.length, idx + match[0].length + 240);
118
+ const ctx = text.slice(windowStart, windowEnd);
119
+ const hasSource = SOURCE_MARKERS.some((sm) => sm.test(ctx));
120
+ if (!hasSource) {
121
+ return { absolute: match[0], context: text.slice(Math.max(0, idx - 80), idx + 80) };
122
+ }
123
+ }
124
+ return null;
125
+ }
126
+
127
+ (async () => {
128
+ const raw = await readStdin();
129
+ let payload = {};
130
+ try {
131
+ payload = raw ? JSON.parse(raw) : {};
132
+ } catch {
133
+ payload = {};
134
+ }
135
+
136
+ const toolName = payload.tool_name || payload.toolName || '';
137
+ const input = payload.tool_input || payload.toolInput || {};
138
+ const target = input.file_path || '';
139
+
140
+ if (!['Edit', 'Write'].includes(toolName) || !isDoc(target) || isExemptPath(target)) {
141
+ process.stdout.write(
142
+ JSON.stringify({
143
+ hookSpecificOutput: {
144
+ hookEventName: 'PreToolUse',
145
+ permissionDecision: 'allow',
146
+ },
147
+ })
148
+ );
149
+ process.exit(0);
150
+ }
151
+
152
+ const rawText = extractText(toolName, input);
153
+ const text = stripNonClaimZones(rawText);
154
+ const hit = findUnsourced(text);
155
+
156
+ if (!hit) {
157
+ process.stdout.write(
158
+ JSON.stringify({
159
+ hookSpecificOutput: {
160
+ hookEventName: 'PreToolUse',
161
+ permissionDecision: 'allow',
162
+ },
163
+ })
164
+ );
165
+ process.exit(0);
166
+ }
167
+
168
+ const reason = [
169
+ `BYAN fact-check guard : unsourced absolute "${hit.absolute}" detected in ${path.basename(target)}.`,
170
+ `Context : ...${hit.context}...`,
171
+ `Add a source (RFC, CVE, URL, [CLAIM L<n>], or entry in _byan/knowledge/sources.md) before writing this. `,
172
+ `Alternative : reformulate with hedging ("often", "in my tests", "tends to") to drop the absolute claim.`,
173
+ ].join('\n');
174
+
175
+ process.stdout.write(
176
+ JSON.stringify({
177
+ hookSpecificOutput: {
178
+ hookEventName: 'PreToolUse',
179
+ permissionDecision: 'deny',
180
+ permissionDecisionReason: reason,
181
+ },
182
+ })
183
+ );
184
+ process.exit(0);
185
+ })();
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * UserPromptSubmit hook — FD phase guard.
4
+ *
5
+ * When an FD cycle is active (fd-state.json exists and phase is not
6
+ * COMPLETED/ABORTED), inject a strong reminder into additionalContext
7
+ * describing the current phase and its hard rules. Makes it mechanical
8
+ * for Claude to stay in the right phase instead of drifting.
9
+ *
10
+ * Non-blocking : if fd-state is missing or invalid, emit empty context.
11
+ */
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+
16
+ const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
17
+ const statePath = path.join(projectDir, '_byan-output', 'fd-state.json');
18
+
19
+ const PHASE_RULES = {
20
+ BRAINSTORM: [
21
+ 'Quantity > quality. No idea rejected. Role-play Carson (brainstorming coach).',
22
+ 'Use YES AND to extend every user seed. Apply inversion, analogies.',
23
+ 'DO NOT ask pruning questions. DO NOT apply Ockham yet. Push ideas.',
24
+ 'Prefix every response with [FD:BRAINSTORM].',
25
+ 'Exit only when user says "stop brainstorm" / "ok j\'ai mes idees", or raw_ideas >= 10.',
26
+ ],
27
+ PRUNE: [
28
+ 'Challenge Before Confirm (Mantra IA-16). Apply Ockham\'s Razor (#37). YAGNI.',
29
+ 'For each idea : ask "probleme resolu ?" "necessaire MAINTENANT ?" "MVP ?"',
30
+ 'Cluster similar ideas, kill redundant, priority-rank what survives (P1/P2/P3).',
31
+ 'Prefix every response with [FD:PRUNE].',
32
+ 'Exit only when user says "OK backlog" or equivalent explicit validation.',
33
+ ],
34
+ DISPATCH: [
35
+ 'Map each feature to {component × specialist × model × strategy × est_tokens}.',
36
+ 'Use byan_dispatch MCP to compute strategy if uncertain. Surface missing specialist.',
37
+ 'Prefix every response with [FD:DISPATCH].',
38
+ 'Exit only when user validates the dispatch table explicitly.',
39
+ ],
40
+ BUILD: [
41
+ 'Delegate to byan-hermes-dispatch. One commit per feature. TDD : tests before code.',
42
+ 'Atomic commits : `type: description`, no emoji, zero cross-feature noise.',
43
+ 'Prefix every response with [FD:BUILD].',
44
+ 'Exit only when all backlog items show status=done AND user validates the diffs.',
45
+ ],
46
+ VALIDATE: [
47
+ 'Run npm test. Zero regression on previously-passing tests.',
48
+ 'MantraValidator >= 80% on changed agent/skill files. Fact-check any absolute claim.',
49
+ 'Prefix every response with [FD:VALIDATE].',
50
+ 'Exit only when tests green + user says "ok validate" OR retry cycle on BUILD.',
51
+ ],
52
+ };
53
+
54
+ function readState() {
55
+ try {
56
+ if (!fs.existsSync(statePath)) return null;
57
+ return JSON.parse(fs.readFileSync(statePath, 'utf8'));
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+
63
+ const state = readState();
64
+ let additionalContext = '';
65
+
66
+ if (state && !['COMPLETED', 'ABORTED'].includes(state.phase)) {
67
+ const rules = PHASE_RULES[state.phase] || ['(unknown phase — fall back to conservative behavior)'];
68
+ const header = `FD active — phase ${state.phase} — feature ${state.feature_name || '?'} (id ${state.fd_id || '?'})`;
69
+ const body = rules.map((r) => ` - ${r}`).join('\n');
70
+ additionalContext = [
71
+ header,
72
+ '',
73
+ 'Hard rules for this turn :',
74
+ body,
75
+ '',
76
+ `Use byan_fd_status MCP to read full state if needed. Do not hand-edit fd-state.json.`,
77
+ ].join('\n');
78
+ }
79
+
80
+ process.stdout.write(
81
+ JSON.stringify({
82
+ hookSpecificOutput: {
83
+ hookEventName: 'UserPromptSubmit',
84
+ additionalContext: additionalContext,
85
+ },
86
+ })
87
+ );
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Stop hook — FD response check.
4
+ *
5
+ * When an FD cycle is active, verify my most recent assistant response
6
+ * starts with a `[FD:<PHASE>]` header matching the current phase. If
7
+ * missing, return decision=block with a reason, forcing me to
8
+ * reformulate with the correct phase marker.
9
+ *
10
+ * When no FD is active, do nothing.
11
+ *
12
+ * Non-blocking on any IO/parse error — the hook never prevents Stop
13
+ * when it can't tell.
14
+ */
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+
19
+ const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
20
+ const statePath = path.join(projectDir, '_byan-output', 'fd-state.json');
21
+
22
+ function readStdin() {
23
+ return new Promise((resolve) => {
24
+ if (process.stdin.isTTY) return resolve('');
25
+ let data = '';
26
+ process.stdin.on('data', (c) => (data += c));
27
+ process.stdin.on('end', () => resolve(data));
28
+ process.stdin.on('error', () => resolve(data));
29
+ });
30
+ }
31
+
32
+ function readState() {
33
+ try {
34
+ if (!fs.existsSync(statePath)) return null;
35
+ return JSON.parse(fs.readFileSync(statePath, 'utf8'));
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ function extractLastAssistantText(payload) {
42
+ if (!payload || typeof payload !== 'object') return '';
43
+ const tx = payload.transcript || payload.messages || [];
44
+ if (!Array.isArray(tx)) return '';
45
+ for (let i = tx.length - 1; i >= 0; i--) {
46
+ const m = tx[i];
47
+ if (m && m.role === 'assistant') {
48
+ if (typeof m.content === 'string') return m.content;
49
+ if (Array.isArray(m.content)) {
50
+ return m.content
51
+ .map((c) => (typeof c === 'object' && c.text ? c.text : ''))
52
+ .join(' ');
53
+ }
54
+ }
55
+ }
56
+ return '';
57
+ }
58
+
59
+ (async () => {
60
+ const state = readState();
61
+ if (!state || ['COMPLETED', 'ABORTED'].includes(state.phase)) {
62
+ process.stdout.write(JSON.stringify({ continue: true }));
63
+ process.exit(0);
64
+ }
65
+
66
+ const raw = await readStdin();
67
+ let payload = {};
68
+ try {
69
+ payload = raw ? JSON.parse(raw) : {};
70
+ } catch {
71
+ payload = {};
72
+ }
73
+
74
+ const text = extractLastAssistantText(payload);
75
+ const expected = `[FD:${state.phase}]`;
76
+
77
+ if (!text || text.includes(expected)) {
78
+ process.stdout.write(JSON.stringify({ continue: true }));
79
+ process.exit(0);
80
+ }
81
+
82
+ const reason = `FD active (phase=${state.phase}) but your last response did not include the required header "${expected}". Reformulate your answer starting with ${expected} to confirm you are operating in the correct phase. If you wanted to exit or change phase, call byan_fd_advance first.`;
83
+
84
+ process.stdout.write(
85
+ JSON.stringify({
86
+ decision: 'block',
87
+ reason,
88
+ systemMessage: reason,
89
+ })
90
+ );
91
+ process.exit(2);
92
+ })();
@@ -10,10 +10,19 @@ 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
14
+ // return file paths + content fragments, Read echoes file content
15
+ // verbatim). Pattern match on their response fires false positives when
16
+ // the file content itself contains the literal phrase "internal error"
17
+ // (e.g. a doc about errors, a test fixture, a hook that detects errors).
18
+ // For these, only trust the explicit is_error flag.
19
+ const ECHO_TOOLS = new Set(['Write', 'Edit', 'NotebookEdit', 'Read']);
20
+
13
21
  function detectFailure(payload) {
14
22
  if (!payload || typeof payload !== 'object') return null;
15
23
 
16
24
  const resp = payload.tool_response ?? payload.toolResponse ?? payload.response;
25
+ const toolName = payload.tool_name || payload.toolName || '';
17
26
 
18
27
  if (resp && typeof resp === 'object') {
19
28
  if (resp.is_error === true || resp.isError === true) {
@@ -21,6 +30,11 @@ function detectFailure(payload) {
21
30
  }
22
31
  }
23
32
 
33
+ // Do not pattern-match on echo-heavy tools — only trust is_error flag.
34
+ if (ECHO_TOOLS.has(toolName)) {
35
+ return null;
36
+ }
37
+
24
38
  const combined = [
25
39
  JSON.stringify(resp ?? ''),
26
40
  JSON.stringify(payload.error ?? ''),
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * PreCompact hook — snapshot critical BYAN state before Claude Code
4
+ * compacts the context window.
5
+ *
6
+ * Writes _byan-output/compact-snapshots/<timestamp>.md with :
7
+ * - active FD state (phase, backlog, dispatch table)
8
+ * - last 50 tool-log.jsonl entries (activity trail)
9
+ * - soul-memory.md head (who BYAN is, red lines)
10
+ * - recent commits (git log -10)
11
+ *
12
+ * Emits a short systemMessage telling Claude : compact happened, key
13
+ * state preserved at <path>, read it if you lose track mid-session.
14
+ *
15
+ * Never blocks compaction. Exit 0 always, even on partial failure.
16
+ */
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const { execSync } = require('child_process');
21
+
22
+ const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
23
+ const outDir = path.join(projectDir, '_byan-output', 'compact-snapshots');
24
+
25
+ function readTextSafe(filePath, maxBytes = 4000) {
26
+ try {
27
+ if (!fs.existsSync(filePath)) return null;
28
+ const raw = fs.readFileSync(filePath, 'utf8');
29
+ return raw.length > maxBytes ? raw.slice(0, maxBytes) + '\n\n... [truncated]' : raw;
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ function readJsonSafe(filePath) {
36
+ try {
37
+ if (!fs.existsSync(filePath)) return null;
38
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+
44
+ function tailJsonl(filePath, n) {
45
+ try {
46
+ if (!fs.existsSync(filePath)) return [];
47
+ const lines = fs.readFileSync(filePath, 'utf8').split('\n').filter(Boolean);
48
+ return lines.slice(-n).map((l) => {
49
+ try {
50
+ return JSON.parse(l);
51
+ } catch {
52
+ return null;
53
+ }
54
+ }).filter(Boolean);
55
+ } catch {
56
+ return [];
57
+ }
58
+ }
59
+
60
+ function recentCommits() {
61
+ try {
62
+ return execSync('git log -10 --oneline', {
63
+ cwd: projectDir,
64
+ encoding: 'utf8',
65
+ stdio: ['ignore', 'pipe', 'ignore'],
66
+ }).trim();
67
+ } catch {
68
+ return '(git log unavailable)';
69
+ }
70
+ }
71
+
72
+ function stamp() {
73
+ const d = new Date();
74
+ const pad = (n) => String(n).padStart(2, '0');
75
+ return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
76
+ }
77
+
78
+ function renderSnapshot() {
79
+ const lines = [];
80
+ lines.push(`# BYAN pre-compact snapshot — ${new Date().toISOString()}`);
81
+ lines.push('');
82
+
83
+ const fd = readJsonSafe(path.join(projectDir, '_byan-output', 'fd-state.json'));
84
+ if (fd) {
85
+ lines.push('## Active FD state');
86
+ lines.push('');
87
+ lines.push('```json');
88
+ lines.push(JSON.stringify(fd, null, 2).slice(0, 3000));
89
+ lines.push('```');
90
+ lines.push('');
91
+ } else {
92
+ lines.push('## Active FD state');
93
+ lines.push('');
94
+ lines.push('(none)');
95
+ lines.push('');
96
+ }
97
+
98
+ lines.push('## Recent commits');
99
+ lines.push('');
100
+ lines.push('```');
101
+ lines.push(recentCommits());
102
+ lines.push('```');
103
+ lines.push('');
104
+
105
+ const tool = tailJsonl(path.join(projectDir, '_byan-output', 'tool-log.jsonl'), 50);
106
+ lines.push(`## Last ${tool.length} tool calls`);
107
+ lines.push('');
108
+ for (const e of tool) {
109
+ const ts = e.timestamp || '?';
110
+ const phase = e.phase || '?';
111
+ const summary = e.summary || (e.ok === false ? `FAIL ${e.failure_kind}` : 'ok');
112
+ lines.push(`- ${ts} ${phase} ${e.tool || '?'} — ${String(summary).slice(0, 120)}`);
113
+ }
114
+ lines.push('');
115
+
116
+ const soul = readTextSafe(path.join(projectDir, '_byan', 'soul.md'), 2000);
117
+ if (soul) {
118
+ lines.push('## Soul (head)');
119
+ lines.push('');
120
+ lines.push(soul);
121
+ lines.push('');
122
+ }
123
+
124
+ return lines.join('\n');
125
+ }
126
+
127
+ (function main() {
128
+ try {
129
+ fs.mkdirSync(outDir, { recursive: true });
130
+ const outPath = path.join(outDir, `${stamp()}.md`);
131
+ fs.writeFileSync(outPath, renderSnapshot());
132
+
133
+ const relative = path.relative(projectDir, outPath);
134
+ process.stdout.write(
135
+ JSON.stringify({
136
+ systemMessage: `BYAN pre-compact snapshot written to ${relative}. Critical state (FD phase, commits, tool activity, soul) preserved outside the context window. Read it after compaction if you lose track.`,
137
+ })
138
+ );
139
+ } catch (err) {
140
+ // Must never block compaction
141
+ process.stdout.write(
142
+ JSON.stringify({
143
+ systemMessage: `pre-compact-save hook error (non-blocking) : ${err.message}`,
144
+ })
145
+ );
146
+ }
147
+ process.exit(0);
148
+ })();
@@ -55,12 +55,18 @@ function readStdin() {
55
55
  const toolName = payload.tool_name || payload.toolName || 'unknown';
56
56
  const hit = detectFailure(payload);
57
57
 
58
+ const respStr = JSON.stringify(
59
+ payload.tool_response ?? payload.toolResponse ?? payload.response ?? {}
60
+ );
61
+ const estOutputTokens = Math.ceil(respStr.length / 4);
62
+
58
63
  appendToolLog({
59
64
  timestamp: new Date().toISOString(),
60
65
  phase: 'post',
61
66
  tool: toolName,
62
67
  ok: !hit,
63
68
  failure_kind: hit ? hit.kind : null,
69
+ est_output_tokens: estOutputTokens,
64
70
  });
65
71
 
66
72
  if (!hit) {
@@ -69,11 +69,15 @@ function appendLog(entry) {
69
69
  const input = payload.tool_input || payload.toolInput || {};
70
70
  const summary = summarizeInput(toolName, input);
71
71
 
72
+ const inputStr = JSON.stringify(input || {});
73
+ const estInputTokens = Math.ceil(inputStr.length / 4);
74
+
72
75
  appendLog({
73
76
  timestamp: new Date().toISOString(),
74
77
  phase: 'pre',
75
78
  tool: toolName,
76
79
  summary,
80
+ est_input_tokens: estInputTokens,
77
81
  });
78
82
 
79
83
  const systemMessage = summary ? `${toolName}: ${summary}` : `${toolName}`;
@@ -26,6 +26,10 @@
26
26
  {
27
27
  "type": "command",
28
28
  "command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/soul-memory-triggers.js"
29
+ },
30
+ {
31
+ "type": "command",
32
+ "command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/fd-phase-guard.js"
29
33
  }
30
34
  ]
31
35
  }
@@ -37,6 +41,10 @@
37
41
  {
38
42
  "type": "command",
39
43
  "command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/mantra-validate.js"
44
+ },
45
+ {
46
+ "type": "command",
47
+ "command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/fd-response-check.js"
40
48
  }
41
49
  ]
42
50
  }
@@ -48,6 +56,10 @@
48
56
  {
49
57
  "type": "command",
50
58
  "command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/tool-transparency.js"
59
+ },
60
+ {
61
+ "type": "command",
62
+ "command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/fact-check-absolutes.js"
51
63
  }
52
64
  ]
53
65
  }
@@ -62,6 +74,17 @@
62
74
  }
63
75
  ]
64
76
  }
77
+ ],
78
+ "PreCompact": [
79
+ {
80
+ "matcher": "",
81
+ "hooks": [
82
+ {
83
+ "type": "command",
84
+ "command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/pre-compact-save.js"
85
+ }
86
+ ]
87
+ }
65
88
  ]
66
89
  }
67
90
  }