create-byan-agent 2.25.0 → 2.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/CHANGELOG.md +155 -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 +1 -5
  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/fact-check-absolutes.js +1 -61
  33. package/install/templates/.claude/hooks/fact-check-claims.js +69 -0
  34. package/install/templates/.claude/hooks/fd-response-check.js +37 -46
  35. package/install/templates/.claude/hooks/inject-soul.js +64 -25
  36. package/install/templates/.claude/hooks/leantime-fd-sync.js +216 -0
  37. package/install/templates/.claude/hooks/lib/autobench-config.json +81 -0
  38. package/install/templates/.claude/hooks/lib/autobench-fc-enrich.js +251 -0
  39. package/install/templates/.claude/hooks/lib/autobench-ledger-report.js +253 -0
  40. package/install/templates/.claude/hooks/lib/autobench-runtime.js +199 -0
  41. package/install/templates/.claude/hooks/lib/fact-check-core.js +69 -0
  42. package/install/templates/.claude/hooks/lib/transcript-read.js +137 -0
  43. package/install/templates/.claude/hooks/soul-memory-check.js +49 -25
  44. package/install/templates/.claude/hooks/soul-memory-triggers.js +27 -8
  45. package/install/templates/.claude/hooks/stage-to-byan.js +25 -7
  46. package/install/templates/.claude/hooks/strict-stop-guard.js +4 -16
  47. package/install/templates/.claude/rules/benchmark.md +251 -0
  48. package/install/templates/.claude/rules/byan-agents.md +0 -1
  49. package/install/templates/.claude/rules/byan-api.md +64 -0
  50. package/install/templates/.claude/rules/fact-check.md +1 -1
  51. package/install/templates/.claude/rules/strict-mode.md +10 -9
  52. package/install/templates/.claude/settings.json +12 -0
  53. package/install/templates/.claude/skills/byan-benchmark/SKILL.md +159 -0
  54. package/install/templates/.claude/skills/byan-byan/SKILL.md +73 -12
  55. package/install/templates/.claude/skills/byan-fact-check/SKILL.md +1 -1
  56. package/install/templates/.claude/skills/byan-hermes-dispatch/SKILL.md +5 -6
  57. package/install/templates/.claude/skills/byan-orchestrate/SKILL.md +11 -3
  58. package/install/templates/.claude/skills/byan-strict/SKILL.md +4 -1
  59. package/install/templates/.claude/workflows/INDEX.md +2 -1
  60. package/install/templates/.claude/workflows/byan-benchmark.js +328 -0
  61. package/install/templates/_byan/_config/agent-manifest.csv +1 -1
  62. package/install/templates/_byan/_config/autobench.yaml +510 -0
  63. package/install/templates/_byan/_config/strict-mode.yaml +9 -3
  64. package/install/templates/_byan/_config/workflow-manifest.csv +1 -0
  65. package/install/templates/_byan/agent/byan/byan.md +1 -3
  66. package/install/templates/_byan/agent/byan-flat/byan.md +1 -3
  67. package/install/templates/_byan/agent/byan-test/byan-test.md +2 -2
  68. package/install/templates/_byan/agent/byan-test-flat/byan-test.md +2 -2
  69. package/install/templates/_byan/agent/byan.optimized/byan.optimized.md +2 -2
  70. package/install/templates/_byan/agent/byan.optimized-v2/byan.optimized-v2.md +2 -2
  71. package/install/templates/_byan/agent/claude/claude.md +0 -2
  72. package/install/templates/_byan/agent/codex/codex.md +0 -2
  73. package/install/templates/_byan/agent/rachid/rachid.md +2 -10
  74. package/install/templates/_byan/agent/rachid-flat/rachid.md +2 -11
  75. package/install/templates/_byan/agent/turbo-whisper/turbo-whisper.md +2 -5
  76. package/install/templates/_byan/agent/turbo-whisper-integration/turbo-whisper-integration.md +5 -13
  77. package/install/templates/_byan/agent/yanstaller/yanstaller.md +2 -24
  78. package/install/templates/_byan/config.yaml +0 -1
  79. package/install/templates/_byan/mcp/byan-mcp-server/bin/byan-sync-rules.js +20 -4
  80. package/install/templates/_byan/mcp/byan-mcp-server/lib/advisory-autofeed.js +13 -0
  81. package/install/templates/_byan/mcp/byan-mcp-server/lib/index-generator.js +1 -1
  82. package/install/templates/_byan/mcp/byan-mcp-server/lib/kanban.js +6 -3
  83. package/install/templates/_byan/mcp/byan-mcp-server/lib/leantime-fd-core.js +205 -0
  84. package/install/templates/_byan/mcp/byan-mcp-server/lib/leantime-sync.js +415 -0
  85. package/install/templates/_byan/mcp/byan-mcp-server/lib/precommit-gate.js +1 -1
  86. package/install/templates/_byan/mcp/byan-mcp-server/lib/strict-activation.js +1 -1
  87. package/install/templates/_byan/mcp/byan-mcp-server/lib/strict-mode.js +8 -0
  88. package/install/templates/_byan/mcp/byan-mcp-server/lib/sync-rules.js +172 -23
  89. package/install/templates/_byan/mcp/byan-mcp-server/lib/workflows-generator.js +1 -0
  90. package/install/templates/_byan/mcp/byan-mcp-server/server.js +205 -82
  91. package/install/templates/_byan/worker/launchers/README.md +4 -24
  92. package/install/templates/_byan/worker/workers.md +0 -2
  93. package/install/templates/_byan/workflow/simple/bmb/byan-benchmark/workflow.md +86 -0
  94. package/install/templates/docs/leantime-integration.md +160 -0
  95. package/package.json +3 -7
  96. package/src/byan-v2/context/session-state.js +2 -2
  97. package/src/byan-v2/generation/mantra-validator.js +3 -3
  98. package/src/byan-v2/index.js +1 -5
  99. package/src/byan-v2/integration/voice-integration.js +1 -1
  100. package/src/byan-v2/orchestrator/generation-state.js +4 -4
  101. package/src/staging/staging.js +20 -6
  102. package/install/bin/build-copilot-stubs.js +0 -138
  103. package/install/lib/platforms/copilot-cli.js +0 -123
  104. package/install/lib/platforms/vscode.js +0 -51
  105. package/install/src/byan-v2/context/copilot-context.js +0 -79
  106. package/install/src/webui/chat/copilot-adapter.js +0 -68
  107. package/install/templates/.claude/agents/bmad-marc.md +0 -25
  108. package/install/templates/.claude/skills/byan-marc/SKILL.md +0 -20
  109. package/install/templates/.github/agents/bmad-agent-bmad-master.md +0 -16
  110. package/install/templates/.github/agents/bmad-agent-bmb-agent-builder.md +0 -16
  111. package/install/templates/.github/agents/bmad-agent-bmb-module-builder.md +0 -16
  112. package/install/templates/.github/agents/bmad-agent-bmb-workflow-builder.md +0 -16
  113. package/install/templates/.github/agents/bmad-agent-bmm-analyst.md +0 -16
  114. package/install/templates/.github/agents/bmad-agent-bmm-architect.md +0 -16
  115. package/install/templates/.github/agents/bmad-agent-bmm-dev.md +0 -16
  116. package/install/templates/.github/agents/bmad-agent-bmm-pm.md +0 -16
  117. package/install/templates/.github/agents/bmad-agent-bmm-quick-flow-solo-dev.md +0 -16
  118. package/install/templates/.github/agents/bmad-agent-bmm-quinn.md +0 -16
  119. package/install/templates/.github/agents/bmad-agent-bmm-sm.md +0 -16
  120. package/install/templates/.github/agents/bmad-agent-bmm-tech-writer.md +0 -16
  121. package/install/templates/.github/agents/bmad-agent-bmm-ux-designer.md +0 -16
  122. package/install/templates/.github/agents/bmad-agent-byan-test.md +0 -33
  123. package/install/templates/.github/agents/bmad-agent-byan-v2.md +0 -44
  124. package/install/templates/.github/agents/bmad-agent-byan.md +0 -1062
  125. package/install/templates/.github/agents/bmad-agent-carmack.md +0 -14
  126. package/install/templates/.github/agents/bmad-agent-cis-brainstorming-coach.md +0 -16
  127. package/install/templates/.github/agents/bmad-agent-cis-creative-problem-solver.md +0 -16
  128. package/install/templates/.github/agents/bmad-agent-cis-design-thinking-coach.md +0 -16
  129. package/install/templates/.github/agents/bmad-agent-cis-innovation-strategist.md +0 -16
  130. package/install/templates/.github/agents/bmad-agent-cis-presentation-master.md +0 -16
  131. package/install/templates/.github/agents/bmad-agent-cis-storyteller.md +0 -16
  132. package/install/templates/.github/agents/bmad-agent-claude.md +0 -49
  133. package/install/templates/.github/agents/bmad-agent-codex.md +0 -49
  134. package/install/templates/.github/agents/bmad-agent-drawio.md +0 -45
  135. package/install/templates/.github/agents/bmad-agent-fact-checker.md +0 -16
  136. package/install/templates/.github/agents/bmad-agent-forgeron.md +0 -15
  137. package/install/templates/.github/agents/bmad-agent-jimmy.md +0 -15
  138. package/install/templates/.github/agents/bmad-agent-marc.md +0 -49
  139. package/install/templates/.github/agents/bmad-agent-mike.md +0 -15
  140. package/install/templates/.github/agents/bmad-agent-patnote.md +0 -49
  141. package/install/templates/.github/agents/bmad-agent-rachid.md +0 -48
  142. package/install/templates/.github/agents/bmad-agent-skeptic.md +0 -16
  143. package/install/templates/.github/agents/bmad-agent-tao.md +0 -14
  144. package/install/templates/.github/agents/bmad-agent-tea-tea.md +0 -16
  145. package/install/templates/.github/agents/bmad-agent-test-dynamic.md +0 -22
  146. package/install/templates/.github/agents/bmad-agent-yanstaller-interview.md +0 -50
  147. package/install/templates/.github/agents/bmad-agent-yanstaller-phase2.md +0 -189
  148. package/install/templates/.github/agents/bmad-agent-yanstaller.md +0 -350
  149. package/install/templates/.github/agents/expert-merise-agile.md +0 -178
  150. package/install/templates/.github/agents/franck.md +0 -379
  151. package/install/templates/.github/agents/hermes.md +0 -575
  152. package/install/templates/.github/extensions/byan-staging/extension.mjs +0 -169
  153. package/install/templates/.github/extensions/byan-staging/package.json +0 -8
  154. package/install/templates/_byan/agent/marc/marc-soul.md +0 -47
  155. package/install/templates/_byan/agent/marc/marc-tao.md +0 -77
  156. package/install/templates/_byan/agent/marc/marc.md +0 -324
  157. package/install/templates/_byan/agent/marc-flat/marc.md +0 -387
  158. package/install/templates/_byan/mcp/byan-mcp-server/lib/copilot.js +0 -148
  159. package/install/templates/_byan/worker/launchers/launch-yanstaller-copilot.md +0 -173
  160. package/install/templates/workers/cost-optimizer.js +0 -169
  161. package/src/byan-v2/context/copilot-context.js +0 -79
@@ -9,7 +9,6 @@ import yaml from 'js-yaml';
9
9
  // - .claude/skills/byan-strict/SKILL.md (owned, full-file)
10
10
  // - .claude/hooks/lib/strict-config.json (owned, full-file)
11
11
  // - AGENTS.md (upsert block, Codex)
12
- // - .github/copilot-instructions.md (upsert block, Copilot)
13
12
  //
14
13
  // Owned files are rewritten wholesale (they carry a generated-by header).
15
14
  // Shared files get a block upserted between BYAN-STRICT markers, leaving the
@@ -19,6 +18,10 @@ const BEGIN = 'BYAN-STRICT:BEGIN';
19
18
  const END = 'BYAN-STRICT:END';
20
19
  const DEFAULT_CONFIG_REL = path.join('_byan', '_config', 'strict-mode.yaml');
21
20
 
21
+ const AUTOBENCH_BEGIN = 'BYAN-AUTOBENCH:BEGIN';
22
+ const AUTOBENCH_END = 'BYAN-AUTOBENCH:END';
23
+ const AUTOBENCH_CONFIG_REL = path.join('_byan', '_config', 'autobench.yaml');
24
+
22
25
  export function resolveRoot(projectRoot) {
23
26
  return projectRoot || process.env.CLAUDE_PROJECT_DIR || process.cwd();
24
27
  }
@@ -39,6 +42,31 @@ export function loadConfig({ projectRoot, configPath } = {}) {
39
42
  return cfg;
40
43
  }
41
44
 
45
+ // Loads and validates the auto-benchmark source of truth. Mirrors loadConfig:
46
+ // throws on a missing file, a non-object parse, an empty mantras list, or a
47
+ // missing doctrine brick — so a malformed YAML fails the generator loudly
48
+ // rather than emitting a half-rendered pointer block.
49
+ export function loadAutobenchConfig({ projectRoot, configPath } = {}) {
50
+ const root = resolveRoot(projectRoot);
51
+ const file = configPath || path.join(root, AUTOBENCH_CONFIG_REL);
52
+ if (!fs.existsSync(file)) {
53
+ throw new Error(`autobench config not found at ${file}`);
54
+ }
55
+ const cfg = yaml.load(fs.readFileSync(file, 'utf8'));
56
+ if (!cfg || typeof cfg !== 'object') {
57
+ throw new Error(`autobench config at ${file} did not parse to an object`);
58
+ }
59
+ if (!Array.isArray(cfg.mantras) || cfg.mantras.length === 0) {
60
+ throw new Error('autobench config must define a non-empty mantras list');
61
+ }
62
+ for (const brick of ['trigger', 'scaler', 'format']) {
63
+ if (!cfg[brick] || typeof cfg[brick] !== 'object') {
64
+ throw new Error(`autobench config must define the '${brick}' brick`);
65
+ }
66
+ }
67
+ return cfg;
68
+ }
69
+
42
70
  // ---------------------------------------------------------------------------
43
71
  // Renderers — pure functions config -> string.
44
72
  // ---------------------------------------------------------------------------
@@ -46,6 +74,9 @@ export function loadConfig({ projectRoot, configPath } = {}) {
46
74
  const GENERATED_NOTE =
47
75
  'Generated by byan-sync-rules from _byan/_config/strict-mode.yaml. Do not hand-edit.';
48
76
 
77
+ const AUTOBENCH_GENERATED_NOTE =
78
+ 'Generated by byan-sync-rules from _byan/_config/autobench.yaml. Do not hand-edit.';
79
+
49
80
  export function renderStrictConfig(cfg) {
50
81
  return {
51
82
  _generated_by: 'byan-sync-rules',
@@ -125,7 +156,10 @@ complete. Downgrading the scope is the failure this mode exists to prevent.
125
156
 
126
157
  1. **Lock the scope** with \`byan_strict_lock_scope\` before building. Provide a
127
158
  verbatim restatement of the request and testable \`acceptanceCriteria\`. The
128
- locked scope is the contract.
159
+ locked scope is the contract. When one technical domain clearly dominates the
160
+ task, also pass \`domain\` (e.g. security, performance, javascript) — a
161
+ successful completion then feeds one VALIDATED tick to the ELO loop. Explicit
162
+ only; omit when no single domain is clear.
129
163
  2. **Build the full scope.** Do not substitute an MVP, a stub, or a simplified
130
164
  version. If a part cannot be done, surface it as a gap — do not cut silently.
131
165
  3. **Self-verify at least ${cfg.self_verify.min_passes} times** with
@@ -160,19 +194,84 @@ Hard mantras:
160
194
  ${mantraLines(cfg)}`;
161
195
  }
162
196
 
163
- export function renderCopilotBlock(cfg) {
164
- // Copilot has no blocking mechanism; this is injection-only guidance.
165
- return `## BYAN Strict Mode
166
-
167
- ${cfg.injection.context_banner.trim()}
168
-
169
- Use the \`byan\` MCP strict tools to lock scope, self-verify (>= ${cfg.self_verify.min_passes} passes),
170
- and complete. The pre-commit gate is the final net: a commit without a fresh,
171
- matching audit token is rejected.
197
+ // Maps the enriched cfg.hooks section to the EXACT runtime shape read by
198
+ // autobench-stop-guard.js. Every key and every {source, flags} pair structure
199
+ // must stay in sync with what compileRegex / hasChoiceLanguage / hasMarker /
200
+ // hasNeverListed / readMarkerFields / escapeHatchActive / ledgerPath consume.
201
+ //
202
+ // This is the function that closes the single-source-of-truth gap: before it
203
+ // existed, autobench-config.json was hand-authored and the YAML toggle
204
+ // (escape_hatch.disabled) was never propagated to the runtime file.
205
+ export function renderAutobenchConfig(cfg) {
206
+ const h = cfg.hooks;
207
+ const mp = h.marker_patterns;
208
+ const mf = h.marker_fields;
209
+ const eh = h.escape_hatch;
172
210
 
173
- Hard mantras:
211
+ return {
212
+ _generated_by: 'byan-sync-rules',
213
+ _note:
214
+ 'Runtime subset read by autobench-stop-guard.js. Edit _byan/_config/autobench.yaml and regenerate; do not hand-edit. Regexes are {source, flags} pairs reconstructed into RegExp at load time.',
215
+ version: cfg.version,
216
+ marker_patterns: {
217
+ any: { source: mp.any.source, flags: mp.any.flags },
218
+ done: { source: mp.done.source, flags: mp.done.flags },
219
+ skip: { source: mp.skip.source, flags: mp.skip.flags },
220
+ },
221
+ marker_fields: {
222
+ g1: { source: mf.g1.source, flags: mf.g1.flags },
223
+ g2: { source: mf.g2.source, flags: mf.g2.flags },
224
+ scope: { source: mf.scope.source, flags: mf.scope.flags },
225
+ },
226
+ never_list: h.never_list.map((entry) => ({ source: entry.source, flags: entry.flags })),
227
+ choice_language: h.choice_language.map((entry) => {
228
+ const out = { source: entry.source, flags: entry.flags };
229
+ // Preserve optional threshold fields only when present; omitting them
230
+ // keeps the config lean and matches the runtime's typeof checks.
231
+ if (typeof entry.min_matches === 'number') out.min_matches = entry.min_matches;
232
+ if (typeof entry.requires_candidates === 'number') out.requires_candidates = entry.requires_candidates;
233
+ return out;
234
+ }),
235
+ candidate_token: { source: h.candidate_token.source, flags: h.candidate_token.flags },
236
+ escape_hatch: {
237
+ // session_flag is read by autobench-runtime.js but not stored in
238
+ // config.json (the runtime hardcodes the path). Carry only the two
239
+ // fields the runtime actually reads from config.json.
240
+ session_flag: eh.session_flag,
241
+ disabled: eh.disabled,
242
+ },
243
+ enforcement: {
244
+ // Disarmed-by-default (approach C): the Stop hook observes and ledgers but
245
+ // does not block until armed. Arming is config-only (armed: true in the
246
+ // YAML); there is no loose flag file, so a stray file cannot silently arm a
247
+ // machine. Defaulting armed to false keeps an older source inert by default.
248
+ armed: Boolean(h.enforcement && h.enforcement.armed === true),
249
+ },
250
+ ledger: {
251
+ path: h.ledger_path,
252
+ },
253
+ banners: {
254
+ stop_block: h.stop_block,
255
+ },
256
+ };
257
+ }
174
258
 
175
- ${mantraLines(cfg)}`;
259
+ // The lean auto-benchmark pointer block. One shared block for both
260
+ // platform files (CLAUDE.md / AGENTS.md): names the
261
+ // feature, states the marker one-liner the agent must emit, and points to the
262
+ // full doctrine. Kept short on purpose — CLAUDE.md stays lean via pointers, and
263
+ // the full rule lives in .claude/rules/benchmark.md (owned, authored elsewhere).
264
+ export function renderAutobenchPointerBlock(cfg) {
265
+ const name = (cfg.name || 'BYAN Auto-Benchmark').trim();
266
+ return `## ${name}
267
+
268
+ Before asking the user to choose between options, benchmark the fork: render
269
+ ONE compact table (Option | <= 4 criteria | Niv + a best-first reco line) when
270
+ both gates hold (>= 2 non-substitutable options diverging on >= 1 weighted
271
+ criterion). Emit the marker verbatim before the table:
272
+ \`<!-- BYAN-BENCH:done g1=<#options> g2=<#divergent-criteria> scope=<internal|external> conf=<assertive|lean> -->\`.
273
+ A confirm, a destructive prompt, or an obvious default is not a fork — emit
274
+ \`<!-- BYAN-BENCH:skip reason=.. -->\` instead. Full doctrine: see @.claude/rules/benchmark.md`;
176
275
  }
177
276
 
178
277
  // ---------------------------------------------------------------------------
@@ -191,11 +290,29 @@ function writeIfChanged(filePath, content) {
191
290
  return existing === null ? 'created' : 'updated';
192
291
  }
193
292
 
293
+ // Escapes a marker string for safe embedding in a RegExp source. Markers are
294
+ // authored as plain identifiers today, but a literal-safe regex keeps the
295
+ // generalized signature robust if a future marker carries a regex metacharacter.
296
+ function escapeRegex(s) {
297
+ return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
298
+ }
299
+
194
300
  // Insert or replace a block delimited by HTML-comment markers. Preserves
195
301
  // everything outside the markers. If the file does not exist, creates it with
196
302
  // just the block.
197
- export function upsertBlock({ filePath, block }) {
198
- const wrapped = `<!-- ${BEGIN} (${GENERATED_NOTE}) -->\n${block}\n<!-- ${END} -->`;
303
+ //
304
+ // markers defaults to the module-level STRICT pair so every existing strict
305
+ // callsite keeps working unchanged. Passing a distinct pair (e.g. the
306
+ // AUTOBENCH markers) scopes the replace-regex to that pair only, so a STRICT
307
+ // block and an AUTOBENCH block coexist in one file without clobbering each
308
+ // other.
309
+ export function upsertBlock({
310
+ filePath,
311
+ block,
312
+ markers = { begin: BEGIN, end: END },
313
+ note = GENERATED_NOTE,
314
+ }) {
315
+ const wrapped = `<!-- ${markers.begin} (${note}) -->\n${block}\n<!-- ${markers.end} -->`;
199
316
  const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : null;
200
317
 
201
318
  if (existing === null) {
@@ -205,7 +322,7 @@ export function upsertBlock({ filePath, block }) {
205
322
  }
206
323
 
207
324
  const re = new RegExp(
208
- `<!-- ${BEGIN}[\\s\\S]*?${END} -->`,
325
+ `<!-- ${escapeRegex(markers.begin)}[\\s\\S]*?${escapeRegex(markers.end)} -->`,
209
326
  'm'
210
327
  );
211
328
  let next;
@@ -216,7 +333,7 @@ export function upsertBlock({ filePath, block }) {
216
333
  }
217
334
  if (next === existing) return 'unchanged';
218
335
  fs.writeFileSync(filePath, next);
219
- return existing.includes(BEGIN) ? 'updated' : 'appended';
336
+ return existing.includes(markers.begin) ? 'updated' : 'appended';
220
337
  }
221
338
 
222
339
  // ---------------------------------------------------------------------------
@@ -241,12 +358,6 @@ export function syncRules({ projectRoot, configPath } = {}) {
241
358
  const agentsPath = path.join(root, 'AGENTS.md');
242
359
  report['AGENTS.md'] = upsertBlock({ filePath: agentsPath, block: renderAgentsBlock(cfg) });
243
360
 
244
- const copilotPath = path.join(root, '.github', 'copilot-instructions.md');
245
- report['.github/copilot-instructions.md'] = upsertBlock({
246
- filePath: copilotPath,
247
- block: renderCopilotBlock(cfg),
248
- });
249
-
250
361
  const mantrasPath = path.join(root, 'src', 'byan-v2', 'data', 'strict-mantras.json');
251
362
  if (fs.existsSync(path.dirname(mantrasPath))) {
252
363
  report['src/byan-v2/data/strict-mantras.json'] = writeIfChanged(
@@ -258,4 +369,42 @@ export function syncRules({ projectRoot, configPath } = {}) {
258
369
  return report;
259
370
  }
260
371
 
372
+ // Auto-benchmark orchestrator. Kept SEPARATE from syncRules so the strict
373
+ // generator stays untouched and independently testable. Upserts the lean
374
+ // pointer block into the three platform files and writes the runtime config
375
+ // that the Stop hook reads. The config write closes the single-source-of-truth
376
+ // gap: escape_hatch.disabled toggled in the YAML now reaches the runtime file.
377
+ // Returns a {file: action} report like syncRules.
378
+ export function syncAutobench({ projectRoot, configPath } = {}) {
379
+ const root = resolveRoot(projectRoot);
380
+ const cfg = loadAutobenchConfig({ projectRoot: root, configPath });
381
+
382
+ const report = {};
383
+ const block = renderAutobenchPointerBlock(cfg);
384
+ const markers = { begin: AUTOBENCH_BEGIN, end: AUTOBENCH_END };
385
+
386
+ // CLAUDE.md uses the lean pointer convention (.claude/CLAUDE.md holds the
387
+ // other rule pointers: strict, fact-check, ELO). AGENTS.md (Codex) is the
388
+ // other cross-platform mechanism target.
389
+ const targets = {
390
+ '.claude/CLAUDE.md': path.join(root, '.claude', 'CLAUDE.md'),
391
+ 'AGENTS.md': path.join(root, 'AGENTS.md'),
392
+ };
393
+
394
+ for (const [rel, filePath] of Object.entries(targets)) {
395
+ report[rel] = upsertBlock({ filePath, block, markers, note: AUTOBENCH_GENERATED_NOTE });
396
+ }
397
+
398
+ // Write the runtime config that autobench-stop-guard.js reads. Mirrors the
399
+ // strict pattern (syncRules writes strict-config.json from strict-mode.yaml).
400
+ const autobenchCfgPath = path.join(root, '.claude', 'hooks', 'lib', 'autobench-config.json');
401
+ report['.claude/hooks/lib/autobench-config.json'] = writeIfChanged(
402
+ autobenchCfgPath,
403
+ JSON.stringify(renderAutobenchConfig(cfg), null, 2) + '\n'
404
+ );
405
+
406
+ return report;
407
+ }
408
+
261
409
  export const MARKERS = { BEGIN, END };
410
+ export const AUTOBENCH_MARKERS = { BEGIN: AUTOBENCH_BEGIN, END: AUTOBENCH_END };
@@ -47,6 +47,7 @@ export const PORTABLE = {
47
47
  'create-excalidraw-dataflow',
48
48
  'create-excalidraw-flowchart',
49
49
  'create-excalidraw-wireframe',
50
+ 'byan-benchmark',
50
51
  ],
51
52
  };
52
53
 
@@ -11,9 +11,8 @@ import {
11
11
  import { dispatch } from './lib/dispatch.js';
12
12
  import { harvest as harvestInsights, renderDigest as renderInsightDigest } from './lib/insight-harvest.js';
13
13
  import { appendOutcome } from './lib/outcome-buffer.js';
14
- import { validateForLog } from './lib/advisory-autofeed.js';
14
+ import { validateForLog, eloOutcomeForStrictComplete } from './lib/advisory-autofeed.js';
15
15
  import { readSoul, appendSoulMemory } from './lib/soul.js';
16
- import { listSessions, readSessionEvents, searchSessions } from './lib/copilot.js';
17
16
  import {
18
17
  start as fdStart,
19
18
  status as fdStatus,
@@ -72,6 +71,17 @@ import {
72
71
  syncEnabled as strictSyncEnabled,
73
72
  resolveProjectId as strictResolveProjectId,
74
73
  } from './lib/strict-sync.js';
74
+ import {
75
+ syncEnabled as leantimeEnabled,
76
+ rpc as leantimeRpc,
77
+ ensureProject as leantimeEnsureProject,
78
+ createTask as leantimeCreateTask,
79
+ moveTask as leantimeMoveTask,
80
+ assignTask as leantimeAssignTask,
81
+ getTask as leantimeGetTask,
82
+ getBoard as leantimeGetBoard,
83
+ METHODS as LEANTIME_METHODS,
84
+ } from './lib/leantime-sync.js';
75
85
 
76
86
  // Compact view of a best-effort strict-sync result for tool responses.
77
87
  function syncResult(sync) {
@@ -113,6 +123,14 @@ function requireToken() {
113
123
  }
114
124
  }
115
125
 
126
+ // Leantime uses its OWN env pair (LEANTIME_API_URL/LEANTIME_API_TOKEN), kept
127
+ // distinct from BYAN_API_URL so the two backends never get crossed.
128
+ function requireLeantime() {
129
+ if (!process.env.LEANTIME_API_URL || !process.env.LEANTIME_API_TOKEN) {
130
+ throw new Error('LEANTIME_API_URL + LEANTIME_API_TOKEN env vars are required for byan_leantime_* tools.');
131
+ }
132
+ }
133
+
116
134
  async function apiRequest(path, options = {}) {
117
135
  const url = `${BYAN_API_URL}${path}`;
118
136
  const headers = {
@@ -407,39 +425,6 @@ const tools = [
407
425
  additionalProperties: false,
408
426
  },
409
427
  },
410
- {
411
- name: 'byan_copilot_sessions',
412
- description:
413
- 'List GitHub Copilot CLI sessions stored locally at ~/.copilot/session-state/. Returns sessionId, start/end time, cwd, branch, agent name, message and tool call counts. Sorted most-recent-first. Use to discover past Copilot CLI conversations for reference or import.',
414
- inputSchema: {
415
- type: 'object',
416
- properties: {
417
- limit: { type: 'number', description: 'Max sessions to return (default 20).' },
418
- sinceIso: { type: 'string', description: 'ISO timestamp filter — only sessions started after this.' },
419
- cwdFilter: { type: 'string', description: 'Substring match on session cwd (e.g. "byan_web").' },
420
- },
421
- additionalProperties: false,
422
- },
423
- },
424
- {
425
- name: 'byan_copilot_session_events',
426
- description:
427
- 'Read events of a specific Copilot CLI session (events.jsonl). Optionally filter by event type (user.message, assistant.message, tool.execution_start, etc.). Useful to inspect the flow of a past session.',
428
- inputSchema: {
429
- type: 'object',
430
- properties: {
431
- sessionId: { type: 'string', description: 'Session UUID from byan_copilot_sessions.' },
432
- types: {
433
- type: 'array',
434
- items: { type: 'string' },
435
- description: 'Filter to these event types only.',
436
- },
437
- limit: { type: 'number', description: 'Max events (default 200).' },
438
- },
439
- required: ['sessionId'],
440
- additionalProperties: false,
441
- },
442
- },
443
428
  {
444
429
  name: 'byan_fd_start',
445
430
  description:
@@ -597,6 +582,10 @@ const tools = [
597
582
  items: { type: 'string' },
598
583
  description: 'Glob patterns of paths the agent may modify.',
599
584
  },
585
+ domain: {
586
+ type: 'string',
587
+ description: 'Optional explicit ELO domain (e.g. security, performance, javascript). When set, a successful byan_strict_complete feeds one VALIDATED outcome to the ELO learning loop. Recorded verbatim (your explicit input, never inferred from text); omit to feed nothing.',
588
+ },
600
589
  force: { type: 'boolean', description: 'Relock with different scope.' },
601
590
  projectId: {
602
591
  type: 'string',
@@ -658,7 +647,7 @@ const tools = [
658
647
  {
659
648
  name: 'byan_strict_suggest',
660
649
  description:
661
- 'Check whether a piece of text (user request, feature name) signals a production-grade deliverable that should be built under strict mode. Reads activation keywords from _byan/_config/strict-mode.yaml. Returns { suggested, matched, message }. Use on any platform (Codex/Copilot have no in-session hook) to decide whether to lock strict mode.',
650
+ 'Check whether a piece of text (user request, feature name) signals a production-grade deliverable that should be built under strict mode. Reads activation keywords from _byan/_config/strict-mode.yaml. Returns { suggested, matched, message }. Use on any platform (Codex has no in-session hook) to decide whether to lock strict mode.',
662
651
  inputSchema: {
663
652
  type: 'object',
664
653
  properties: {
@@ -848,25 +837,6 @@ const tools = [
848
837
  additionalProperties: false,
849
838
  },
850
839
  },
851
- {
852
- name: 'byan_copilot_search',
853
- description:
854
- 'Full-text search across all Copilot CLI sessions. Finds messages (user + assistant by default) containing the query string. Returns sessionId + timestamp + excerpt. Use to recall past discussions without knowing which session they were in.',
855
- inputSchema: {
856
- type: 'object',
857
- properties: {
858
- query: { type: 'string', description: 'Substring to search for (case-insensitive).' },
859
- types: {
860
- type: 'array',
861
- items: { type: 'string' },
862
- description: 'Event types to scan (default: user.message, assistant.message).',
863
- },
864
- limit: { type: 'number', description: 'Max matches (default 50).' },
865
- },
866
- required: ['query'],
867
- additionalProperties: false,
868
- },
869
- },
870
840
 
871
841
  // ─── Projects ─────────────────────────────────────────────────────────
872
842
  {
@@ -1198,6 +1168,101 @@ const tools = [
1198
1168
  additionalProperties: false,
1199
1169
  },
1200
1170
  },
1171
+
1172
+ // ─── Leantime (project-management mirror) ─────────────────────────────
1173
+ // Client-side automation of the self-hosted Leantime JSON-RPC API. Used by
1174
+ // the FD workflow to create a project + a task per feature and move task
1175
+ // status across phases. Needs LEANTIME_API_URL + LEANTIME_API_TOKEN.
1176
+ {
1177
+ name: 'byan_leantime_ping',
1178
+ description:
1179
+ 'Healthcheck the Leantime integration: reports api_url, token presence, and (if configured) whether the JSON-RPC API is reachable. Surfaces the wrong-host guard (HTML instead of JSON). No required args.',
1180
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false },
1181
+ },
1182
+ {
1183
+ name: 'byan_leantime_project_ensure',
1184
+ description:
1185
+ 'Idempotent create-or-fetch of a Leantime project from the FD project_context. Matches an existing project by name first (no duplicate on FD re-run). Returns { id, created }. Requires LEANTIME_API_*.',
1186
+ inputSchema: {
1187
+ type: 'object',
1188
+ properties: {
1189
+ name: { type: 'string', description: 'Project name (defaults to slug).' },
1190
+ slug: { type: 'string', description: 'Project slug (fallback name).' },
1191
+ clientId: { type: 'number', description: 'Owning Leantime client id. Resolved if omitted.' },
1192
+ details: { type: 'string', description: 'Optional project description.' },
1193
+ },
1194
+ additionalProperties: false,
1195
+ },
1196
+ },
1197
+ {
1198
+ name: 'byan_leantime_task_create',
1199
+ description:
1200
+ 'Create one Leantime task (ticket) from an FD backlog item. Returns the new task id to store back in fd-state (caller owns idempotency: create only if the item has no leantime_task_id). Requires LEANTIME_API_*.',
1201
+ inputSchema: {
1202
+ type: 'object',
1203
+ properties: {
1204
+ projectId: { type: 'number', description: 'Leantime project id.' },
1205
+ headline: { type: 'string', description: 'Task title.' },
1206
+ description: { type: 'string' },
1207
+ status: { type: 'number', description: 'Leantime status id (optional).' },
1208
+ priority: { type: 'number' },
1209
+ editorId: { type: 'number', description: 'Assignee/editor user id.' },
1210
+ tags: { type: 'string' },
1211
+ type: { type: 'string', description: "Ticket type, default 'task'." },
1212
+ },
1213
+ required: ['projectId', 'headline'],
1214
+ additionalProperties: false,
1215
+ },
1216
+ },
1217
+ {
1218
+ name: 'byan_leantime_task_move',
1219
+ description:
1220
+ 'Move a Leantime task to a lifecycle column (todo|doing|blocked|review|done). Resolves the column to the project status id, then updates the ticket. Requires LEANTIME_API_*.',
1221
+ inputSchema: {
1222
+ type: 'object',
1223
+ properties: {
1224
+ taskId: { type: 'number', description: 'Leantime ticket id.' },
1225
+ projectId: { type: 'number', description: 'Project id (for status resolution).' },
1226
+ column: { type: 'string', enum: ['todo', 'doing', 'blocked', 'review', 'done'] },
1227
+ status: { type: 'number', description: 'Explicit status id (bypasses column resolution).' },
1228
+ },
1229
+ required: ['taskId'],
1230
+ additionalProperties: false,
1231
+ },
1232
+ },
1233
+ {
1234
+ name: 'byan_leantime_task_assign',
1235
+ description: 'Set the assignee/editor of a Leantime task. Requires LEANTIME_API_*.',
1236
+ inputSchema: {
1237
+ type: 'object',
1238
+ properties: {
1239
+ taskId: { type: 'number', description: 'Leantime ticket id.' },
1240
+ editorId: { type: 'number', description: 'Assignee/editor user id.' },
1241
+ },
1242
+ required: ['taskId', 'editorId'],
1243
+ additionalProperties: false,
1244
+ },
1245
+ },
1246
+ {
1247
+ name: 'byan_leantime_task_get',
1248
+ description: 'Fetch a single Leantime task by id. Requires LEANTIME_API_*.',
1249
+ inputSchema: {
1250
+ type: 'object',
1251
+ properties: { taskId: { type: 'number', description: 'Leantime ticket id.' } },
1252
+ required: ['taskId'],
1253
+ additionalProperties: false,
1254
+ },
1255
+ },
1256
+ {
1257
+ name: 'byan_leantime_board_get',
1258
+ description: "List a Leantime project's tasks grouped by lifecycle column. Requires LEANTIME_API_*.",
1259
+ inputSchema: {
1260
+ type: 'object',
1261
+ properties: { projectId: { type: 'number', description: 'Leantime project id.' } },
1262
+ required: ['projectId'],
1263
+ additionalProperties: false,
1264
+ },
1265
+ },
1201
1266
  ];
1202
1267
 
1203
1268
  const server = new Server(
@@ -1340,33 +1405,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1340
1405
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1341
1406
  }
1342
1407
 
1343
- if (name === 'byan_copilot_sessions') {
1344
- const result = listSessions({
1345
- limit: args.limit,
1346
- sinceIso: args.sinceIso,
1347
- cwdFilter: args.cwdFilter,
1348
- });
1349
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1350
- }
1351
-
1352
- if (name === 'byan_copilot_session_events') {
1353
- const result = readSessionEvents({
1354
- sessionId: args.sessionId,
1355
- types: args.types,
1356
- limit: args.limit,
1357
- });
1358
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1359
- }
1360
-
1361
- if (name === 'byan_copilot_search') {
1362
- const result = searchSessions({
1363
- query: args.query,
1364
- types: args.types,
1365
- limit: args.limit,
1366
- });
1367
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1368
- }
1369
-
1370
1408
  if (name === 'byan_fd_start') {
1371
1409
  const state = fdStart({ featureName: args.featureName, force: args.force, strict: args.strict });
1372
1410
  return { content: [{ type: 'text', text: JSON.stringify(state, null, 2) }] };
@@ -1446,6 +1484,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1446
1484
  scopeText: args.scopeText,
1447
1485
  acceptanceCriteria: args.acceptanceCriteria,
1448
1486
  allowedPaths: args.allowedPaths,
1487
+ domain: args.domain,
1449
1488
  force: args.force,
1450
1489
  });
1451
1490
  const st = strictGetStatus();
@@ -1490,6 +1529,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1490
1529
  if (name === 'byan_strict_complete') {
1491
1530
  const r = strictComplete();
1492
1531
  const st = strictGetStatus();
1532
+ // C3 learning loop: a completed strict session with an EXPLICIT ELO domain
1533
+ // is a VALIDATED outcome. eloOutcomeForStrictComplete builds the line (the
1534
+ // SAME helper the test exercises, so handler and test cannot drift); we
1535
+ // append it to the buffer drain-advisory drains. The domain is the user's
1536
+ // explicit lock_scope input, never inferred. Best-effort: a feed failure
1537
+ // must not break completion.
1538
+ try {
1539
+ const eloLine = eloOutcomeForStrictComplete(r);
1540
+ if (eloLine) appendOutcome(eloLine, { rootDir: process.env.CLAUDE_PROJECT_DIR || process.cwd() });
1541
+ } catch {
1542
+ // the learning feed must not break completion.
1543
+ }
1493
1544
  const sync = await strictPushComplete({
1494
1545
  sessionId: st.strict_session_id,
1495
1546
  auditToken: r.audit_token,
@@ -1846,6 +1897,78 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1846
1897
  return { content: [{ type: 'text', text: JSON.stringify(instructions, null, 2) }] };
1847
1898
  }
1848
1899
 
1900
+ // ─── Leantime tools ───────────────────────────────────────────────
1901
+ if (name === 'byan_leantime_ping') {
1902
+ const status = {
1903
+ api_url: process.env.LEANTIME_API_URL || null,
1904
+ token_configured: Boolean(process.env.LEANTIME_API_TOKEN),
1905
+ assign_user_configured: Boolean(process.env.LEANTIME_ASSIGN_USER_ID),
1906
+ enabled: leantimeEnabled(),
1907
+ };
1908
+ if (status.enabled) {
1909
+ const probe = await leantimeRpc(LEANTIME_METHODS.getAllProjects, {});
1910
+ status.reachable = probe.ok;
1911
+ if (!probe.ok) status.reason = probe.reason;
1912
+ if (probe.hint) status.hint = probe.hint;
1913
+ }
1914
+ return { content: [{ type: 'text', text: JSON.stringify(status, null, 2) }] };
1915
+ }
1916
+
1917
+ if (name === 'byan_leantime_project_ensure') {
1918
+ requireLeantime();
1919
+ const r = await leantimeEnsureProject({
1920
+ name: args.name,
1921
+ slug: args.slug,
1922
+ clientId: args.clientId,
1923
+ details: args.details,
1924
+ });
1925
+ return { content: [{ type: 'text', text: JSON.stringify(r, null, 2) }] };
1926
+ }
1927
+
1928
+ if (name === 'byan_leantime_task_create') {
1929
+ requireLeantime();
1930
+ const r = await leantimeCreateTask({
1931
+ projectId: args.projectId,
1932
+ headline: args.headline,
1933
+ description: args.description,
1934
+ status: args.status,
1935
+ priority: args.priority,
1936
+ editorId: args.editorId,
1937
+ tags: args.tags,
1938
+ ...(args.type !== undefined ? { type: args.type } : {}),
1939
+ });
1940
+ return { content: [{ type: 'text', text: JSON.stringify(r, null, 2) }] };
1941
+ }
1942
+
1943
+ if (name === 'byan_leantime_task_move') {
1944
+ requireLeantime();
1945
+ const r = await leantimeMoveTask({
1946
+ taskId: args.taskId,
1947
+ projectId: args.projectId,
1948
+ column: args.column,
1949
+ status: args.status,
1950
+ });
1951
+ return { content: [{ type: 'text', text: JSON.stringify(r, null, 2) }] };
1952
+ }
1953
+
1954
+ if (name === 'byan_leantime_task_assign') {
1955
+ requireLeantime();
1956
+ const r = await leantimeAssignTask({ taskId: args.taskId, editorId: args.editorId });
1957
+ return { content: [{ type: 'text', text: JSON.stringify(r, null, 2) }] };
1958
+ }
1959
+
1960
+ if (name === 'byan_leantime_task_get') {
1961
+ requireLeantime();
1962
+ const r = await leantimeGetTask({ taskId: args.taskId });
1963
+ return { content: [{ type: 'text', text: JSON.stringify(r, null, 2) }] };
1964
+ }
1965
+
1966
+ if (name === 'byan_leantime_board_get') {
1967
+ requireLeantime();
1968
+ const r = await leantimeGetBoard({ projectId: args.projectId });
1969
+ return { content: [{ type: 'text', text: JSON.stringify(r, null, 2) }] };
1970
+ }
1971
+
1849
1972
  throw new Error(`Unknown tool: ${name}`);
1850
1973
  } catch (err) {
1851
1974
  return {