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,220 @@
1
+ // Session insight harvester — read native Claude Code outcome trails and
2
+ // aggregate them into a GATED improvement digest for BYAN.
3
+ //
4
+ // Philosophy (the whole point): OBSERVE and PROPOSE, never silently self-modify.
5
+ // BYAN already has advisory learning surfaces (ELO trust, the suitability
6
+ // ledger) the agent updates by hand; the native hooks already leave outcome
7
+ // trails on disk. This module closes the loop by READING those trails and
8
+ // surfacing a digest with GATED proposals. It writes nothing back to a behavior
9
+ // surface (routing / personas / mantras): applying any change stays a human
10
+ // decision. An agent that rewrote its own routing on a heuristic would be the
11
+ // exact silent-downgrade BYAN exists to prevent.
12
+ //
13
+ // The aggregation is PURE (no I/O) so it is exhaustively unit-testable; the I/O
14
+ // entry takes an injected reader, mirroring template-sync.js / stub-sync.js.
15
+ //
16
+ // Trails consumed (shapes verified against the live repo):
17
+ // _byan-output/tool-log.jsonl post line {phase:'post', tool, ok, est_output_tokens?}
18
+ // .byan-strict/audit.log {event:'self_verify', verdict:'gap', findings:[]}
19
+ // _byan-output/suitability-ledger.json { "model::leaf": {model, leafId, successes, failures} }
20
+ // _byan/memoire/elo-profile.json { domains: { <domain>: {rating, blocked_streak, ...} } }
21
+
22
+ import fs from 'node:fs';
23
+ import path from 'node:path';
24
+
25
+ // Parse a JSONL blob into an array of objects, skipping malformed lines.
26
+ export function parseJsonl(text) {
27
+ if (!text) return [];
28
+ return text
29
+ .split('\n')
30
+ .filter(Boolean)
31
+ .map((l) => {
32
+ try {
33
+ return JSON.parse(l);
34
+ } catch {
35
+ return null;
36
+ }
37
+ })
38
+ .filter(Boolean);
39
+ }
40
+
41
+ // Tool health from tool-log.jsonl post lines: call count, failure rate, the top
42
+ // failing tools, and an output-token cost proxy. est_output_tokens is absent on
43
+ // older lines (added later by the hook), so it defaults to 0.
44
+ export function harvestToolHealth(toolLogEntries) {
45
+ const post = (toolLogEntries || []).filter((e) => e && e.phase === 'post');
46
+ const failures = post.filter((e) => e.ok === false);
47
+ const byTool = {};
48
+ for (const f of failures) byTool[f.tool || 'unknown'] = (byTool[f.tool || 'unknown'] || 0) + 1;
49
+ const topFailing = Object.entries(byTool)
50
+ .sort((a, b) => b[1] - a[1])
51
+ .slice(0, 5)
52
+ .map(([tool, count]) => ({ tool, count }));
53
+ const estOutputTokens = post.reduce((s, e) => s + (e.est_output_tokens || 0), 0);
54
+ return {
55
+ calls: post.length,
56
+ failures: failures.length,
57
+ failureRate: post.length ? +(failures.length / post.length).toFixed(3) : 0,
58
+ topFailing,
59
+ estOutputTokens,
60
+ };
61
+ }
62
+
63
+ // Coarse theme key for a strict gap finding. The categories mirror the recurring
64
+ // gap types BYAN actually hits; anything unmatched is 'other' (never silently
65
+ // dropped — it still counts under 'other').
66
+ function normalizeGap(finding) {
67
+ const s = String(finding).toLowerCase();
68
+ if (/\btest|coverage|spec\b/.test(s)) return 'tests/coverage';
69
+ if (/error|edge|exception|fail|throw/.test(s)) return 'error/edge handling';
70
+ if (/doc|comment|changelog|readme/.test(s)) return 'documentation';
71
+ if (/template|fidelity|sync|twin/.test(s)) return 'template fidelity';
72
+ if (/emoji/.test(s)) return 'emoji';
73
+ if (/scope|downgrade|cut|stub|mvp/.test(s)) return 'scope/downgrade';
74
+ return 'other';
75
+ }
76
+
77
+ // Recurring strict-gap clustering (L3): mine self_verify gap findings from the
78
+ // audit log and group them into themes. A theme is "recurring" at count >= 2.
79
+ export function harvestStrictGaps(auditEntries) {
80
+ const findings = [];
81
+ for (const e of auditEntries || []) {
82
+ if (e && e.event === 'self_verify' && e.verdict === 'gap' && Array.isArray(e.findings)) {
83
+ findings.push(...e.findings);
84
+ }
85
+ }
86
+ const themes = {};
87
+ for (const f of findings) {
88
+ const key = normalizeGap(f);
89
+ if (!themes[key]) themes[key] = { theme: key, count: 0, samples: [] };
90
+ themes[key].count++;
91
+ if (themes[key].samples.length < 2) themes[key].samples.push(String(f).slice(0, 100));
92
+ }
93
+ const recurring = Object.values(themes)
94
+ .filter((t) => t.count >= 2)
95
+ .sort((a, b) => b.count - a.count);
96
+ return { totalGapFindings: findings.length, recurring };
97
+ }
98
+
99
+ // Routing outcomes (L1): surface the suitability ledger as per (cheap-model x
100
+ // leaf) keep-rate rows, busiest first. keepRate = successes / (successes+failures).
101
+ export function harvestRouting(ledger) {
102
+ const rows = [];
103
+ const entries = ledger && typeof ledger === 'object' ? Object.entries(ledger) : [];
104
+ for (const [key, v] of entries) {
105
+ if (!v || typeof v !== 'object') continue;
106
+ const successes = Number(v.successes || 0);
107
+ const failures = Number(v.failures || 0);
108
+ const n = successes + failures;
109
+ if (!n) continue;
110
+ const model = v.model || key.split('::')[0];
111
+ const leaf = v.leafId || key.split('::')[1] || key;
112
+ rows.push({ model, leaf, successes, failures, n, keepRate: +(successes / n).toFixed(2) });
113
+ }
114
+ return rows.sort((a, b) => b.n - a.n);
115
+ }
116
+
117
+ // Domain trust trends from the ELO profile: rating + blocked streak per domain.
118
+ export function harvestEloTrends(eloProfile) {
119
+ const domains = (eloProfile && eloProfile.domains) || {};
120
+ const rows = [];
121
+ for (const [domain, d] of Object.entries(domains)) {
122
+ if (!d || typeof d !== 'object' || typeof d.rating !== 'number') continue;
123
+ rows.push({ domain, rating: d.rating, blockedStreak: d.blocked_streak || 0 });
124
+ }
125
+ return rows.sort((a, b) => b.rating - a.rating);
126
+ }
127
+
128
+ // Assemble the digest and derive GATED proposals. Every proposal is a suggestion
129
+ // for the human to ratify (gated:true) — none is auto-applied. The thresholds
130
+ // are deliberately conservative so noise does not generate proposals.
131
+ export function buildDigest({ toolHealth, gaps, routing, elo } = {}) {
132
+ const proposals = [];
133
+
134
+ if (toolHealth && toolHealth.failureRate > 0.1 && toolHealth.topFailing.length) {
135
+ const t = toolHealth.topFailing[0];
136
+ proposals.push({
137
+ kind: 'tool-reliability',
138
+ gated: true,
139
+ suggestion: `Tool failure rate ${toolHealth.failureRate}; top offender ${t.tool} (${t.count}). Investigate before relying on it.`,
140
+ });
141
+ }
142
+ for (const g of (gaps && gaps.recurring) || []) {
143
+ if (g.count >= 3) {
144
+ proposals.push({
145
+ kind: 'recurring-gap',
146
+ gated: true,
147
+ suggestion: `Recurring self-verify gap "${g.theme}" (${g.count}x). Consider a pre-build checklist item.`,
148
+ });
149
+ }
150
+ }
151
+ for (const r of routing || []) {
152
+ if (r.n >= 5 && r.keepRate < 0.5) {
153
+ proposals.push({
154
+ kind: 'routing',
155
+ gated: true,
156
+ suggestion: `Cheap model ${r.model} underperforms on "${r.leaf}" (keepRate ${r.keepRate}, n=${r.n}). Consider keeping that leaf deep.`,
157
+ });
158
+ }
159
+ }
160
+
161
+ return {
162
+ toolHealth: toolHealth || null,
163
+ recurringGaps: gaps || { totalGapFindings: 0, recurring: [] },
164
+ routingOutcomes: routing || [],
165
+ eloTrends: elo || [],
166
+ proposals,
167
+ };
168
+ }
169
+
170
+ // Human-readable render of a digest (for the CLI and the skill).
171
+ export function renderDigest(d) {
172
+ const lines = ['BYAN session insight digest', ''];
173
+ if (d.toolHealth) {
174
+ lines.push(
175
+ `Tool health: ${d.toolHealth.calls} calls, ${d.toolHealth.failures} failures (rate ${d.toolHealth.failureRate}), ~${d.toolHealth.estOutputTokens} output tokens.`
176
+ );
177
+ if (d.toolHealth.topFailing.length) {
178
+ lines.push(` Top failing: ${d.toolHealth.topFailing.map((t) => `${t.tool}(${t.count})`).join(', ')}`);
179
+ }
180
+ }
181
+ lines.push(`Recurring gaps: ${d.recurringGaps.recurring.map((g) => `${g.theme}(${g.count})`).join(', ') || 'none'}`);
182
+ if (d.routingOutcomes.length) {
183
+ lines.push('Routing outcomes (cheap-model keep-rate):');
184
+ for (const r of d.routingOutcomes.slice(0, 8)) {
185
+ lines.push(` ${r.model}::${r.leaf} -> keep ${r.keepRate} (n=${r.n})`);
186
+ }
187
+ }
188
+ if (d.eloTrends.length) {
189
+ lines.push(`ELO trends: ${d.eloTrends.slice(0, 6).map((e) => `${e.domain}=${e.rating}`).join(', ')}`);
190
+ }
191
+ lines.push('', `Proposals (GATED — human ratifies, nothing auto-applied): ${d.proposals.length}`);
192
+ for (const p of d.proposals) lines.push(` [${p.kind}] ${p.suggestion}`);
193
+ return lines.join('\n');
194
+ }
195
+
196
+ // I/O entry: read the trails under rootDir (missing trail -> empty, so the digest
197
+ // self-disables gracefully on a fresh checkout) and build the digest.
198
+ export function harvest({ rootDir, io = fs } = {}) {
199
+ const readText = (rel) => {
200
+ try {
201
+ return io.readFileSync(path.join(rootDir, rel), 'utf8');
202
+ } catch {
203
+ return '';
204
+ }
205
+ };
206
+ const readJson = (rel) => {
207
+ const t = readText(rel);
208
+ if (!t) return null;
209
+ try {
210
+ return JSON.parse(t);
211
+ } catch {
212
+ return null;
213
+ }
214
+ };
215
+ const toolHealth = harvestToolHealth(parseJsonl(readText('_byan-output/tool-log.jsonl')));
216
+ const gaps = harvestStrictGaps(parseJsonl(readText('.byan-strict/audit.log')));
217
+ const routing = harvestRouting(readJson('_byan-output/suitability-ledger.json'));
218
+ const elo = harvestEloTrends(readJson('_byan/memoire/elo-profile.json'));
219
+ return buildDigest({ toolHealth, gaps, routing, elo });
220
+ }
@@ -9,9 +9,12 @@
9
9
  * Stand-up : _byan-output/party-mode-sessions/<session_id>/standup.jsonl
10
10
  * entries : { agent, timestamp, did, blockers, next }
11
11
  *
12
- * Hermes watches stand-ups : an agent with 2+ consecutive "blocked"
13
- * reports in the stand-up stream is flagged and their card is moved to
14
- * `blocked` column in the kanban.
12
+ * Wiring : the byan-orchestrate skill posts one stand-up per role at the
13
+ * aggregate step (byan_standup_post) and calls byan_standup_blocked to surface
14
+ * stuck roles. In the single-pass aggregate it uses minStreak:1 (one stand-up
15
+ * per role, so a 2-in-a-row streak is unreachable); a non-ok role's card is
16
+ * moved to the `blocked` column at the same step (byan_kanban_move). A
17
+ * longer-lived session that posts repeatedly uses the default minStreak:2.
15
18
  */
16
19
 
17
20
  import fs from 'node:fs';
@@ -0,0 +1,205 @@
1
+ // Pure decision core for the FD -> Leantime auto-sync hook.
2
+ //
3
+ // WHY a separate pure module: a Claude Code hook is an I/O shell (stdin payload,
4
+ // network, disk) that is awkward to unit-test. The risky logic — which Leantime
5
+ // calls a phase transition implies, and the idempotence that stops duplicates —
6
+ // lives here with ZERO I/O, so every transition is testable as a data
7
+ // transform. The shell (.claude/hooks/leantime-fd-sync.js) feeds this the parsed
8
+ // fd-state + the sidecar map and executes the returned intents against
9
+ // lib/leantime-sync.js.
10
+ //
11
+ // State-coupling: this module reads fd-state echoed by the MCP tool; it does not
12
+ // read or write fd-state.json. The Leantime id mapping is the caller's sidecar.
13
+
14
+ // The two FD MCP tools whose result carries the post-transition fd-state.
15
+ export const FD_ADVANCE = 'byan_fd_advance';
16
+ export const FD_UPDATE = 'byan_fd_update';
17
+
18
+ // Recognize the FD tool regardless of the mcp__<server>__ prefix or snake/camel
19
+ // casing; the endsWith fallback keeps it working if the server key is renamed.
20
+ export function fdToolKind(toolName) {
21
+ if (typeof toolName !== 'string') return null;
22
+ if (toolName === FD_ADVANCE || toolName.endsWith(`__${FD_ADVANCE}`) || toolName.endsWith(FD_ADVANCE)) {
23
+ return 'advance';
24
+ }
25
+ if (toolName === FD_UPDATE || toolName.endsWith(`__${FD_UPDATE}`) || toolName.endsWith(FD_UPDATE)) {
26
+ return 'update';
27
+ }
28
+ return null;
29
+ }
30
+
31
+ // Parse the fd-state the MCP tool echoed. The byan_fd_* handlers return
32
+ // JSON.stringify(state); an MCP tool_response wraps it as
33
+ // { content: [{ type:'text', text:'<json>' }] }. Accept that envelope, a raw
34
+ // JSON string, or an already-parsed object. Returns the state object, or null
35
+ // when the shape is unrecognized (the shell then degrades to a file fallback).
36
+ export function parseFdState(toolResponse) {
37
+ if (!toolResponse) return null;
38
+ let candidate = toolResponse;
39
+ if (candidate && typeof candidate === 'object' && Array.isArray(candidate.content)) {
40
+ const textPart = candidate.content.find((p) => p && typeof p.text === 'string');
41
+ if (textPart) candidate = textPart.text;
42
+ }
43
+ if (typeof candidate === 'string') {
44
+ try {
45
+ candidate = JSON.parse(candidate);
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+ if (candidate && typeof candidate === 'object' && typeof candidate.phase === 'string') {
51
+ return candidate;
52
+ }
53
+ return null;
54
+ }
55
+
56
+ // FD phase order. Used to gate task creation to DISPATCH-onward (a feature's
57
+ // task carries its assigned specialist, set at DISPATCH) and to recognize
58
+ // terminal phases.
59
+ const PHASE_RANK = {
60
+ DISCOVERY: 0,
61
+ BRAINSTORM: 1,
62
+ PRUNE: 2,
63
+ DISPATCH: 3,
64
+ BUILD: 4,
65
+ REVIEW: 5,
66
+ VALIDATE: 6,
67
+ REFACTOR: 7,
68
+ DOC: 8,
69
+ COMPLETED: 9,
70
+ ABORTED: 9,
71
+ };
72
+
73
+ function lastReviewStatus(state) {
74
+ const arr = Array.isArray(state.review_findings) ? state.review_findings : [];
75
+ for (let i = arr.length - 1; i >= 0; i -= 1) {
76
+ if (arr[i] && typeof arr[i].status === 'string') return arr[i].status;
77
+ }
78
+ return null;
79
+ }
80
+
81
+ // The board column the mapped tasks should reflect for the current fd-state.
82
+ // Board-wide (not per-feature): a BYAN FD builds its backlog together, and the
83
+ // per-item backlog status is agent-maintained and can lag a phase. Mirrors the
84
+ // SKILL section 2.5 fire points. Returns a column name or null (no move).
85
+ export function columnForState(state) {
86
+ switch (state.phase) {
87
+ case 'ABORTED':
88
+ return null; // leave the board verbatim: the diagnostic of where it died
89
+ case 'COMPLETED':
90
+ return 'done';
91
+ case 'DOC':
92
+ return 'review'; // validated, awaiting the COMPLETED sweep to done
93
+ case 'BUILD':
94
+ return 'doing';
95
+ case 'REFACTOR':
96
+ return 'blocked';
97
+ case 'VALIDATE': {
98
+ const v = state.validate_verdict;
99
+ if (v && v.status === 'KO') return 'blocked';
100
+ if (v && v.status === 'OK') return 'review';
101
+ return 'doing';
102
+ }
103
+ case 'REVIEW': {
104
+ const last = lastReviewStatus(state);
105
+ if (last === 'needs-rework') return 'blocked';
106
+ if (last === 'ready-for-validate') return 'review';
107
+ return 'doing';
108
+ }
109
+ default:
110
+ // DISCOVERY, BRAINSTORM, PRUNE, DISPATCH
111
+ return 'todo';
112
+ }
113
+ }
114
+
115
+ // Decide the ordered Leantime intents for one hook fire. Reconcile-from-state:
116
+ // each fire (re)ensures the project exists, ensures every backlog task exists
117
+ // (DISPATCH onward), and drives all mapped tasks to the column the current
118
+ // fd-state implies. Idempotence is the sidecar's job — project_ensure/task_create
119
+ // are emitted only when the id is absent, so a dropped call last fire is retried
120
+ // and a duplicate is not created.
121
+ //
122
+ // args: { toolName, state, sidecar, assignUserConfigured }
123
+ // sidecar: { projectId?, tasks?: { <backlogId>: taskId } } for THIS fd_id
124
+ // returns: { skip?: reason, intents: [...] }
125
+ //
126
+ // Intent ops (the shell maps each to a leantime-sync call):
127
+ // { op:'project_ensure', name, slug, details }
128
+ // { op:'assign_user' } // only if configured (shell sequences it after project_ensure)
129
+ // { op:'task_create', backlogId, headline }
130
+ // { op:'task_move', backlogId, column }
131
+ export function decideActions({ toolName, state, sidecar = {}, assignUserConfigured = false } = {}) {
132
+ const kind = fdToolKind(toolName);
133
+ if (!kind) return { skip: 'not-fd-tool', intents: [] };
134
+ if (!state || typeof state.phase !== 'string') return { skip: 'no-state', intents: [] };
135
+
136
+ const projectName =
137
+ (state.project_context && state.project_context.name) || state.feature_name || null;
138
+ if (!projectName) return { skip: 'no-project-name', intents: [] };
139
+
140
+ const intents = [];
141
+ const tasks = sidecar.tasks || {};
142
+
143
+ // 1. Ensure the project. Emitted only when the sidecar has no projectId yet.
144
+ if (!sidecar.projectId) {
145
+ intents.push({
146
+ op: 'project_ensure',
147
+ name: projectName,
148
+ slug: (state.project_context && state.project_context.slug) || undefined,
149
+ details: `BYAN FD ${state.fd_id || ''} — auto-synced board.`.trim(),
150
+ });
151
+ if (assignUserConfigured) intents.push({ op: 'assign_user' });
152
+ }
153
+
154
+ const rank = PHASE_RANK[state.phase] ?? 0;
155
+ const backlog = Array.isArray(state.backlog) ? state.backlog : [];
156
+
157
+ // 2. Create a task per backlog item once the FD has reached DISPATCH (the task
158
+ // then carries the dispatched specialist). Skipped items are not created.
159
+ if (rank >= PHASE_RANK.DISPATCH && state.phase !== 'ABORTED') {
160
+ for (const item of backlog) {
161
+ if (!item || !item.id) continue;
162
+ if (item.status === 'skipped') continue;
163
+ if (!tasks[item.id]) {
164
+ intents.push({ op: 'task_create', backlogId: item.id, headline: item.title || item.id });
165
+ }
166
+ }
167
+ }
168
+
169
+ // 3. Move every task (already-mapped + just-created) to the column the current
170
+ // state implies. ABORTED yields no column -> no move. To bound RPC volume
171
+ // (byan_fd_update fires several times per phase), moves are emitted only when
172
+ // the target column changed since the last applied fire (sidecar.lastColumn),
173
+ // OR a task was just created, OR the prior fire left a move unsynced
174
+ // (sidecar.moveFailed) so a dropped move self-heals on the next event.
175
+ const column = columnForState(state);
176
+ const createdThisFire = intents.some((i) => i.op === 'task_create');
177
+ const moveNeeded = column && (column !== sidecar.lastColumn || createdThisFire || sidecar.moveFailed === true);
178
+ if (moveNeeded) {
179
+ const seen = new Set();
180
+ for (const item of backlog) {
181
+ if (!item || !item.id || item.status === 'skipped') continue;
182
+ // a just-created task is moved in the same fire; an already-mapped one is
183
+ // reconciled to the current column (self-heals a dropped earlier move).
184
+ const willExist = tasks[item.id] || intents.some((i) => i.op === 'task_create' && i.backlogId === item.id);
185
+ if (willExist && !seen.has(item.id)) {
186
+ intents.push({ op: 'task_move', backlogId: item.id, column });
187
+ seen.add(item.id);
188
+ }
189
+ }
190
+ // tasks in the sidecar that are no longer in the backlog still get swept to a
191
+ // terminal column on COMPLETED, so the board does not strand a renamed item.
192
+ if (state.phase === 'COMPLETED') {
193
+ for (const backlogId of Object.keys(tasks)) {
194
+ if (!seen.has(backlogId)) {
195
+ intents.push({ op: 'task_move', backlogId, column });
196
+ seen.add(backlogId);
197
+ }
198
+ }
199
+ }
200
+ }
201
+
202
+ // `column` is the current target; the shell persists it as sidecar.lastColumn so
203
+ // the next fire can skip a redundant move sweep.
204
+ return { intents, column };
205
+ }