create-byan-agent 2.19.1 → 2.20.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 (52) hide show
  1. package/CHANGELOG.md +188 -0
  2. package/README.md +4 -4
  3. package/install/src/byan-v2/generation/templates/default-agent.md +1 -1
  4. package/install/templates/.claude/CLAUDE.md +1 -1
  5. package/install/templates/.claude/hooks/fd-phase-guard.js +2 -2
  6. package/install/templates/.claude/hooks/mantra-validate.js +16 -8
  7. package/install/templates/.claude/hooks/strict-scope-guard.js +25 -7
  8. package/install/templates/.claude/rules/native-workflows.md +32 -0
  9. package/install/templates/.claude/skills/byan-byan/SKILL.md +5 -5
  10. package/install/templates/.claude/skills/byan-mantra-audit/SKILL.md +53 -0
  11. package/install/templates/.claude/skills/byan-merise-agile/SKILL.md +2 -2
  12. package/install/templates/.claude/skills/byan-native-dev-story/SKILL.md +83 -0
  13. package/install/templates/.claude/workflows/INDEX.md +35 -0
  14. package/install/templates/.claude/workflows/check-implementation-readiness.js +280 -0
  15. package/install/templates/.claude/workflows/code-review.js +179 -0
  16. package/install/templates/.claude/workflows/create-excalidraw-dataflow.js +214 -0
  17. package/install/templates/.claude/workflows/create-excalidraw-diagram.js +188 -0
  18. package/install/templates/.claude/workflows/create-excalidraw-flowchart.js +225 -0
  19. package/install/templates/.claude/workflows/create-excalidraw-wireframe.js +192 -0
  20. package/install/templates/.claude/workflows/create-story.js +216 -0
  21. package/install/templates/.claude/workflows/dev-story.js +100 -0
  22. package/install/templates/.claude/workflows/document-project.js +455 -0
  23. package/install/templates/.claude/workflows/qa-automate.js +169 -0
  24. package/install/templates/.claude/workflows/quick-dev.js +273 -0
  25. package/install/templates/.claude/workflows/sprint-planning.js +261 -0
  26. package/install/templates/.claude/workflows/testarch-atdd.js +287 -0
  27. package/install/templates/.claude/workflows/testarch-automate.js +229 -0
  28. package/install/templates/.claude/workflows/testarch-ci.js +184 -0
  29. package/install/templates/.claude/workflows/testarch-framework.js +267 -0
  30. package/install/templates/.claude/workflows/testarch-nfr.js +316 -0
  31. package/install/templates/.claude/workflows/testarch-test-design.js +293 -0
  32. package/install/templates/.claude/workflows/testarch-test-review.js +321 -0
  33. package/install/templates/.claude/workflows/testarch-trace.js +316 -0
  34. package/install/templates/.githooks/pre-commit +49 -15
  35. package/install/templates/_byan/config.yaml +15 -5
  36. package/install/templates/_byan/mcp/byan-mcp-server/bin/byan-build-workflows.js +20 -0
  37. package/install/templates/_byan/mcp/byan-mcp-server/bin/byan-lint-workflows.js +57 -0
  38. package/install/templates/_byan/mcp/byan-mcp-server/lib/native-loop.js +39 -0
  39. package/install/templates/_byan/mcp/byan-mcp-server/lib/workflows-generator.js +149 -0
  40. package/install/templates/_byan/mcp/byan-mcp-server/lib/workflows-lint.js +113 -0
  41. package/install/templates/_byan/workflow/simple/byan/feature-workflow.md +14 -11
  42. package/install/templates/docs/native-workflows-contract.md +84 -0
  43. package/package.json +2 -2
  44. package/src/byan-v2/data/agent-scopes.json +46 -0
  45. package/src/byan-v2/data/mantras.json +194 -8
  46. package/src/byan-v2/generation/mantra-audit.js +147 -0
  47. package/src/byan-v2/generation/mantra-validator.js +56 -6
  48. package/src/byan-v2/generation/scope-resolver.js +102 -0
  49. package/src/byan-v2/generation/templates/default-agent.md +1 -1
  50. package/update-byan-agent/bin/update-byan-agent.js +67 -72
  51. package/update-byan-agent/lib/apply-update.js +202 -0
  52. package/update-byan-agent/package.json +1 -1
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env node
2
+ //
3
+ // mantra-audit — semantic embodiment audit for BYAN personas (Option C, N2).
4
+ //
5
+ // Lives under src/byan-v2/generation/ so it ships with the v2 runtime (the
6
+ // installer copies src/ into the user project) next to the validator and
7
+ // scope-resolver it depends on. Invoke with:
8
+ // node src/byan-v2/generation/mantra-audit.js prepare <agentFile> [--json]
9
+ // node src/byan-v2/generation/mantra-audit.js score <agentFile> <verdicts.json>
10
+ //
11
+ // The pre-commit mantra gate is a fast, deterministic anti-stub FLOOR : it asks
12
+ // "does this persona contain the vocabulary of its domain mantras". That catches
13
+ // empty/zombie files but does NOT measure whether the agent genuinely EMBODIES a
14
+ // mantra. This tool is the deeper, out-of-band layer : it prepares a judgment
15
+ // packet (the applicable mantras + the persona + a rubric) for an LLM judge, then
16
+ // turns the judge's verdicts into an embodiment score. It is deliberately NOT in
17
+ // the commit path : the judgment is semantic (an LLM call), so it runs on demand
18
+ // or in CI, outside the commit path (a non-deterministic check must not block it).
19
+ //
20
+ // verdicts.json : { "<mantraId>": "embodied" | "partial" | "absent", ... }
21
+
22
+ const fs = require('fs');
23
+ const path = require('path');
24
+ const MantraValidator = require('./mantra-validator');
25
+ const resolver = require('./scope-resolver');
26
+
27
+ const VERDICT_WEIGHT = { embodied: 1, partial: 0.5, absent: 0 };
28
+
29
+ function buildPacket(agentFile) {
30
+ const content = fs.readFileSync(agentFile, 'utf8');
31
+ const name = path.basename(agentFile).replace(/\.md$/, '');
32
+ const scopes = resolver.resolveAgentScopes({ name, content });
33
+ const applicable = new MantraValidator().applicableMantras(scopes);
34
+ return {
35
+ agent: name,
36
+ file: agentFile,
37
+ scopes,
38
+ mantras: applicable.map(m => ({
39
+ id: m.id,
40
+ title: m.title,
41
+ description: m.description,
42
+ scope: m.scope || 'universal',
43
+ priority: m.priority,
44
+ })),
45
+ persona: content,
46
+ };
47
+ }
48
+
49
+ function buildPrompt(packet) {
50
+ const lines = [];
51
+ lines.push('You are auditing whether a BYAN agent persona EMBODIES its applicable mantras.');
52
+ lines.push('Judge embodiment, not vocabulary : a mantra is embodied when the persona\'s role,');
53
+ lines.push('instructions, and red-lines actually enact the principle, even if the exact keyword');
54
+ lines.push('is absent. For each mantra return one verdict : embodied | partial | absent.');
55
+ lines.push('');
56
+ lines.push(`Agent : ${packet.agent} Scopes : ${packet.scopes.join(', ')}`);
57
+ lines.push('');
58
+ lines.push('Applicable mantras :');
59
+ for (const m of packet.mantras) {
60
+ lines.push(`- ${m.id} (${m.scope}, ${m.priority}) : ${m.title} -- ${m.description}`);
61
+ }
62
+ lines.push('');
63
+ lines.push('Persona under audit :');
64
+ lines.push('"""');
65
+ lines.push(packet.persona.trim());
66
+ lines.push('"""');
67
+ lines.push('');
68
+ lines.push('Return strict JSON mapping every mantra id to its verdict, e.g.');
69
+ lines.push('{ "IA-16": "embodied", "M37": "partial", "IA-2": "absent" }');
70
+ return lines.join('\n');
71
+ }
72
+
73
+ function scoreVerdicts(packet, verdicts) {
74
+ const ids = packet.mantras.map(m => m.id);
75
+ let sum = 0;
76
+ const buckets = { embodied: [], partial: [], absent: [], unjudged: [] };
77
+ for (const id of ids) {
78
+ const v = verdicts[id];
79
+ if (v && Object.prototype.hasOwnProperty.call(VERDICT_WEIGHT, v)) {
80
+ sum += VERDICT_WEIGHT[v];
81
+ buckets[v].push(id);
82
+ } else {
83
+ buckets.unjudged.push(id);
84
+ }
85
+ }
86
+ const total = ids.length;
87
+ const embodimentScore = total > 0 ? Math.round((sum / total) * 100) : 0;
88
+ return {
89
+ agent: packet.agent,
90
+ scopes: packet.scopes,
91
+ total,
92
+ embodimentScore,
93
+ embodied: buckets.embodied,
94
+ partial: buckets.partial,
95
+ absent: buckets.absent,
96
+ unjudged: buckets.unjudged,
97
+ };
98
+ }
99
+
100
+ function main(argv) {
101
+ const [cmd, agentFile, arg3] = argv;
102
+
103
+ if (!cmd || !agentFile || ['-h', '--help'].includes(cmd)) {
104
+ process.stdout.write(
105
+ 'Usage :\n' +
106
+ ' node src/byan-v2/generation/mantra-audit.js prepare <agentFile> [--json]\n' +
107
+ ' node src/byan-v2/generation/mantra-audit.js score <agentFile> <verdicts.json>\n'
108
+ );
109
+ return cmd ? 0 : 1;
110
+ }
111
+
112
+ if (!fs.existsSync(agentFile)) {
113
+ process.stderr.write(`Agent file not found : ${agentFile}\n`);
114
+ return 1;
115
+ }
116
+
117
+ const packet = buildPacket(agentFile);
118
+
119
+ if (cmd === 'prepare') {
120
+ if (arg3 === '--json') {
121
+ process.stdout.write(JSON.stringify(packet, null, 2) + '\n');
122
+ } else {
123
+ process.stdout.write(buildPrompt(packet) + '\n');
124
+ }
125
+ return 0;
126
+ }
127
+
128
+ if (cmd === 'score') {
129
+ if (!arg3 || !fs.existsSync(arg3)) {
130
+ process.stderr.write('A verdicts JSON file is required : score <agentFile> <verdicts.json>\n');
131
+ return 1;
132
+ }
133
+ const verdicts = JSON.parse(fs.readFileSync(arg3, 'utf8'));
134
+ const result = scoreVerdicts(packet, verdicts);
135
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
136
+ return 0;
137
+ }
138
+
139
+ process.stderr.write(`Unknown command : ${cmd}\n`);
140
+ return 1;
141
+ }
142
+
143
+ if (require.main === module) {
144
+ process.exit(main(process.argv.slice(2)));
145
+ }
146
+
147
+ module.exports = { buildPacket, buildPrompt, scoreVerdicts, VERDICT_WEIGHT };
@@ -1,6 +1,10 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
 
4
+ // Score bands, single source of truth (referenced by generateReport + export).
5
+ const PASS_THRESHOLD = 80;
6
+ const WARNING_THRESHOLD = 60;
7
+
4
8
  class MantraValidator {
5
9
  constructor(mantrasData = null) {
6
10
  if (mantrasData) {
@@ -42,7 +46,28 @@ class MantraValidator {
42
46
  return strictIds.length >= 3;
43
47
  }
44
48
 
45
- validate(agentDefinition) {
49
+ // Normalize a caller-supplied scope (string | string[] | Set | null) into a
50
+ // Set, or null when no scope was given. Null means legacy all-mantras scoring.
51
+ _normalizeScope(scope) {
52
+ if (scope === null || scope === undefined) return null;
53
+ if (scope instanceof Set) return scope;
54
+ if (Array.isArray(scope)) return new Set(scope);
55
+ return new Set([scope]);
56
+ }
57
+
58
+ // A mantra is applicable to a scoped run when it is universal (or unscoped)
59
+ // or its scope is in the requested set. Behavioral mantras are runtime-
60
+ // enforced (hooks / fact-check), not declared in a persona file, so they are
61
+ // excluded from scoped scoring. With no scope (null), every mantra applies.
62
+ _isApplicable(mantra, scopeSet) {
63
+ if (scopeSet === null) return true;
64
+ if (mantra.behavioral === true) return false;
65
+ const ms = mantra.scope || 'universal';
66
+ if (ms === 'universal') return true;
67
+ return scopeSet.has(ms);
68
+ }
69
+
70
+ validate(agentDefinition, options = {}) {
46
71
  if (agentDefinition === null || agentDefinition === undefined) {
47
72
  throw new Error('Agent definition is required');
48
73
  }
@@ -59,8 +84,16 @@ class MantraValidator {
59
84
  this.mantras = this.personaMantras;
60
85
  }
61
86
 
87
+ // Domain-aware scoring : when the caller passes a scope, only the mantras
88
+ // applicable to that scope are counted (universal always counts, behavioral
89
+ // runtime-enforced mantras are excluded). With no scope, every mantra in the
90
+ // chosen ruleset is scored, byte-identical to the legacy behavior.
91
+ const scopeSet = this._normalizeScope(options.scope);
92
+ const applicableMantras = this.mantras.filter(m => this._isApplicable(m, scopeSet));
93
+
62
94
  this.results = {
63
- totalMantras: this.mantras.length,
95
+ totalMantras: applicableMantras.length,
96
+ scope: scopeSet ? [...scopeSet] : null,
64
97
  compliant: [],
65
98
  nonCompliant: [],
66
99
  warnings: [],
@@ -70,7 +103,7 @@ class MantraValidator {
70
103
 
71
104
  const startTime = Date.now();
72
105
 
73
- for (const mantra of this.mantras) {
106
+ for (const mantra of applicableMantras) {
74
107
  const result = this.checkMantra(mantra.id, agentDefinition);
75
108
 
76
109
  if (result.compliant) {
@@ -154,8 +187,17 @@ class MantraValidator {
154
187
 
155
188
  _validatePattern(content, validation, mantra) {
156
189
  try {
190
+ // Some forbidden-pattern mantras (e.g. IA-23 no-emoji) must ignore
191
+ // declared zones such as an agent's icon="..." frontmatter attribute,
192
+ // where an emoji is a legitimate display glyph, not pollution.
193
+ let scanContent = content;
194
+ if (Array.isArray(validation.ignoreZones)) {
195
+ for (const zone of validation.ignoreZones) {
196
+ scanContent = scanContent.replace(new RegExp(zone, 'g'), '');
197
+ }
198
+ }
157
199
  const regex = new RegExp(validation.pattern, validation.flags || '');
158
- const matches = content.match(regex);
200
+ const matches = scanContent.match(regex);
159
201
  const hasMatches = matches && matches.length > 0;
160
202
 
161
203
  if (validation.mustNotMatch) {
@@ -255,7 +297,7 @@ class MantraValidator {
255
297
  }
256
298
 
257
299
  const score = this.results.score;
258
- const level = score >= 80 ? 'PASS' : score >= 60 ? 'WARNING' : 'FAIL';
300
+ const level = score >= PASS_THRESHOLD ? 'PASS' : score >= WARNING_THRESHOLD ? 'WARNING' : 'FAIL';
259
301
 
260
302
  let report = '';
261
303
  report += '='.repeat(60) + '\n';
@@ -424,7 +466,7 @@ class MantraValidator {
424
466
  } else if (format === 'summary') {
425
467
  return {
426
468
  score: this.results.score,
427
- status: this.results.score >= 80 ? 'PASS' : this.results.score >= 60 ? 'WARNING' : 'FAIL',
469
+ status: this.results.score >= PASS_THRESHOLD ? 'PASS' : this.results.score >= WARNING_THRESHOLD ? 'WARNING' : 'FAIL',
428
470
  compliant: this.results.compliant.length,
429
471
  nonCompliant: this.results.nonCompliant.length,
430
472
  criticalErrors: this.results.errors.length,
@@ -443,6 +485,14 @@ class MantraValidator {
443
485
  return this.mantras.filter(m => m.category === category);
444
486
  }
445
487
 
488
+ // The mantras that apply to a given scope (universal + the scope's domain,
489
+ // behavioral excluded). With no scope, every mantra in the current ruleset.
490
+ // Shared with the domain-aware path in validate() and the embodiment audit.
491
+ applicableMantras(scope) {
492
+ const scopeSet = this._normalizeScope(scope);
493
+ return this.mantras.filter(m => this._isApplicable(m, scopeSet));
494
+ }
495
+
446
496
  getMantrasByPriority(priority) {
447
497
  return this.mantras.filter(m => m.priority === priority);
448
498
  }
@@ -0,0 +1,102 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ // Domain-aware scope resolution for the mantra validator.
5
+ //
6
+ // The validator itself stays pure (it scores against a scope set it is given).
7
+ // This module owns the impure part : turning a persona FILE into its scope set,
8
+ // by reading a centralized map and, where present, an explicit frontmatter
9
+ // override. Callers (the pre-commit gate, the Stop hook, the FD VALIDATE step)
10
+ // resolve scopes here, then pass them to validator.validate(content, { scope }).
11
+
12
+ const VALID_SCOPES = ['universal', 'sdlc-process', 'sdlc-code', 'sdlc-ops', 'sdlc-modeling', 'sdlc-test', 'creative'];
13
+ const KNOWN_MODULES = ['bmm', 'cis', 'tea', 'bmb', 'core'];
14
+
15
+ function loadScopeMap(mapPath) {
16
+ const p = mapPath || path.join(__dirname, '../data/agent-scopes.json');
17
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
18
+ }
19
+
20
+ // An explicit `mantra_scopes: [a, b]` in the persona (frontmatter or body) wins
21
+ // over every derived default. Returns the listed scopes, or null when absent.
22
+ function parseFrontmatterScopes(content) {
23
+ if (typeof content !== 'string') return null;
24
+ const m = content.match(/mantra_scopes\s*:\s*\[([^\]]*)\]/);
25
+ if (!m) return null;
26
+ const list = m[1]
27
+ .split(',')
28
+ .map(s => s.trim().replace(/^['"]|['"]$/g, ''))
29
+ .filter(Boolean);
30
+ return list.length ? list : null;
31
+ }
32
+
33
+ // A Gen3 agent loads exactly one _byan/<module>/config.yaml during activation,
34
+ // so the module is derivable from the persona content. Returns null if none.
35
+ function deriveModule(content) {
36
+ if (typeof content !== 'string') return null;
37
+ const m = content.match(/_byan\/(bmm|cis|tea|bmb|core)\/config\.yaml/);
38
+ return m ? m[1] : null;
39
+ }
40
+
41
+ // The agent slug from a persona file path (basename without extension).
42
+ function agentNameFromPath(filePath) {
43
+ if (!filePath) return null;
44
+ return path.basename(filePath).replace(/\.md$/, '');
45
+ }
46
+
47
+ // Force-union 'universal' and keep only known scope names, order-stable.
48
+ function normalizeScopes(scopes) {
49
+ const seen = new Set();
50
+ const out = [];
51
+ for (const s of ['universal', ...scopes]) {
52
+ if (VALID_SCOPES.includes(s) && !seen.has(s)) {
53
+ seen.add(s);
54
+ out.push(s);
55
+ }
56
+ }
57
+ return out;
58
+ }
59
+
60
+ // Resolve the scope set for a persona. Precedence :
61
+ // 1. explicit frontmatter mantra_scopes
62
+ // 2. agentScopes[name] in the map
63
+ // 3. moduleScopes[module] derived from the content
64
+ // 4. fallback ['universal']
65
+ // 'universal' is always present in the result.
66
+ function resolveAgentScopes({ name = null, content = null, map = null } = {}) {
67
+ const scopeMap = map || loadScopeMap();
68
+
69
+ const explicit = parseFrontmatterScopes(content);
70
+ if (explicit) return normalizeScopes(explicit);
71
+
72
+ if (name && scopeMap.agentScopes && scopeMap.agentScopes[name]) {
73
+ return normalizeScopes(scopeMap.agentScopes[name]);
74
+ }
75
+
76
+ const mod = deriveModule(content);
77
+ if (mod && scopeMap.moduleScopes && scopeMap.moduleScopes[mod]) {
78
+ return normalizeScopes(scopeMap.moduleScopes[mod]);
79
+ }
80
+
81
+ return ['universal'];
82
+ }
83
+
84
+ // Convenience for file-based callers (the gate, the hooks) : read a persona
85
+ // file from disk and resolve its scopes in one call.
86
+ function resolveScopesForFile(filePath, map = null) {
87
+ const content = fs.readFileSync(filePath, 'utf8');
88
+ const name = agentNameFromPath(filePath);
89
+ return resolveAgentScopes({ name, content, map });
90
+ }
91
+
92
+ module.exports = {
93
+ VALID_SCOPES,
94
+ KNOWN_MODULES,
95
+ loadScopeMap,
96
+ parseFrontmatterScopes,
97
+ deriveModule,
98
+ agentNameFromPath,
99
+ normalizeScopes,
100
+ resolveAgentScopes,
101
+ resolveScopesForFile,
102
+ };
@@ -16,7 +16,7 @@ You must fully embody this agent's persona and follow all activation instruction
16
16
  <rules>
17
17
  <r>Communicate in {communication_language}</r>
18
18
  <r>Stay in character until EXIT</r>
19
- <r>Apply Merise Agile + TDD + 64 mantras</r>
19
+ <r>Apply Merise Agile + TDD + 71 mantras</r>
20
20
  </rules>
21
21
  </activation>
22
22
 
@@ -6,16 +6,22 @@ const ora = require('ora');
6
6
  const inquirer = require('inquirer');
7
7
  const path = require('path');
8
8
  const fs = require('fs');
9
- const { execSync } = require('child_process');
10
9
 
11
10
  const Analyzer = require('../lib/analyzer');
12
11
  const Backup = require('../lib/backup');
13
12
  const CustomizationDetector = require('../lib/customization-detector');
13
+ const { applyUpdate, resolvePackageRoot } = require('../lib/apply-update');
14
+
15
+ // Read the version from this package, not a hand-maintained literal that drifts.
16
+ let UPDATER_VERSION = '0.0.0';
17
+ try {
18
+ UPDATER_VERSION = require('../package.json').version;
19
+ } catch { /* keep fallback */ }
14
20
 
15
21
  program
16
22
  .name('update-byan-agent')
17
23
  .description('Gestion des mises a jour BYAN avec detection de conflits')
18
- .version('2.6.1');
24
+ .version(UPDATER_VERSION);
19
25
 
20
26
  program
21
27
  .command('check')
@@ -58,37 +64,45 @@ program
58
64
  .description('Mettre a jour installation BYAN')
59
65
  .option('--dry-run', 'Analyser sans appliquer les changements')
60
66
  .option('--force', 'Forcer la mise a jour meme si deja a jour')
67
+ .option('-y, --yes', 'Mode non-interactif : confirmer automatiquement')
68
+ .option('--non-interactive', 'Alias de --yes (utile en CI / headless)')
61
69
  .action(async (options) => {
62
70
  const installPath = process.cwd();
63
-
71
+
64
72
  try {
65
73
  // Step 1: Check version
66
74
  const spinner = ora('Verification version...').start();
67
75
  const analyzer = new Analyzer(installPath);
68
76
  const versionInfo = await analyzer.checkVersion();
69
77
  spinner.succeed(`Version actuelle: ${versionInfo.current}, npm: ${versionInfo.latest}`);
70
-
78
+
71
79
  if (versionInfo.upToDate && !options.force) {
72
80
  console.log(chalk.green('\nBYAN est deja a jour!'));
73
81
  return;
74
82
  }
75
-
83
+
76
84
  if (options.dryRun) {
77
85
  console.log(chalk.cyan('\nMode dry-run: Aucune modification appliquee'));
78
86
  return;
79
87
  }
80
-
81
- // Step 2: Confirm update
82
- const { confirmUpdate } = await inquirer.prompt([{
83
- type: 'confirm',
84
- name: 'confirmUpdate',
85
- message: `Mettre a jour BYAN ${versionInfo.current} -> ${versionInfo.latest}?`,
86
- default: true
87
- }]);
88
-
89
- if (!confirmUpdate) {
90
- console.log(chalk.yellow('Mise a jour annulee'));
91
- return;
88
+
89
+ // Step 2: Confirm update. Skip the prompt when explicitly non-interactive
90
+ // (--yes / --non-interactive / --force) or when stdout is not a TTY (CI,
91
+ // pipe, headless) — otherwise the updater hangs on the Y/n in automation.
92
+ const autoConfirm =
93
+ options.yes || options.nonInteractive || options.force ||
94
+ !process.stdout.isTTY || !process.stdin.isTTY;
95
+ if (!autoConfirm) {
96
+ const { confirmUpdate } = await inquirer.prompt([{
97
+ type: 'confirm',
98
+ name: 'confirmUpdate',
99
+ message: `Mettre a jour BYAN ${versionInfo.current} -> ${versionInfo.latest}?`,
100
+ default: true
101
+ }]);
102
+ if (!confirmUpdate) {
103
+ console.log(chalk.yellow('Mise a jour annulee'));
104
+ return;
105
+ }
92
106
  }
93
107
 
94
108
  // Step 3: Detect customizations
@@ -130,66 +144,47 @@ program
130
144
  }
131
145
  preserveSpinner.succeed('Personnalisations sauvegardees');
132
146
 
133
- // Step 6: Download and install latest version
134
- const updateSpinner = ora('Telechargement derniere version...').start();
147
+ // Step 6: Rebuild from the running package template (no network install).
148
+ // The updater is launched via `npx -p create-byan-agent@latest`, so the
149
+ // @latest package (and its template) is already on disk next to this bin.
150
+ // We resolve it locally instead of re-installing it into the user project
151
+ // (BUG3), validate the template BEFORE deleting anything, and swap via
152
+ // rename so a failure never leaves _byan missing (BUG2).
153
+ const updateSpinner = ora('Reconstruction depuis le template du package...').start();
154
+ let pkgRoot;
135
155
  try {
136
- // Remove current _byan directory
137
- const byanDir = path.join(installPath, '_byan');
138
- if (fs.existsSync(byanDir)) {
139
- fs.rmSync(byanDir, { recursive: true, force: true });
140
- }
141
-
142
- // Run npm install to get latest create-byan-agent
143
- execSync('npm install --no-save create-byan-agent@latest', {
144
- cwd: installPath,
145
- stdio: 'pipe'
146
- });
147
-
148
- // Copy _byan from node_modules to project root. The published tarball
149
- // ships _byan source under install/templates/_byan/, not at the root.
150
- // Fall back to root-level _byan/ for legacy tarballs that may still
151
- // have shipped it there.
152
- const pkgRoot = path.join(installPath, 'node_modules', 'create-byan-agent');
153
- const candidates = [
154
- path.join(pkgRoot, 'install', 'templates', '_byan'),
155
- path.join(pkgRoot, '_byan'),
156
- ];
157
- const nodeModulesByan = candidates.find((p) => fs.existsSync(p));
158
- if (nodeModulesByan) {
159
- copyRecursive(nodeModulesByan, byanDir);
160
- } else {
161
- throw new Error(
162
- `_byan directory not found in npm package (looked in: ${candidates.map((p) => path.relative(pkgRoot, p)).join(', ')})`
163
- );
164
- }
156
+ const resolved = resolvePackageRoot({ installPath, binDir: __dirname });
157
+ pkgRoot = resolved.pkgRoot;
165
158
 
166
- // Also refresh .github/agents/ from templates (Copilot stubs)
167
- const ghAgentsSrc = path.join(pkgRoot, 'install', 'templates', '.github', 'agents');
168
- const ghAgentsDst = path.join(installPath, '.github', 'agents');
169
- if (fs.existsSync(ghAgentsSrc)) {
170
- if (fs.existsSync(ghAgentsDst)) {
171
- fs.rmSync(ghAgentsDst, { recursive: true, force: true });
172
- }
173
- fs.mkdirSync(path.dirname(ghAgentsDst), { recursive: true });
174
- copyRecursive(ghAgentsSrc, ghAgentsDst);
175
- }
159
+ const report = applyUpdate({ installPath, pkgRoot });
160
+ updateSpinner.succeed(
161
+ `Template applique (${report.byanEntries} entrees _byan` +
162
+ (report.githubAgentsEntries != null
163
+ ? `, ${report.githubAgentsEntries} stubs .github/agents` : '') +
164
+ `) depuis ${resolved.source}`
165
+ );
176
166
 
177
167
  // Refresh Claude Code native (.claude/hooks, .claude/skills,
178
- // .claude/agents, .claude/settings.json, .mcp.json, _byan/mcp/)
168
+ // .claude/agents, .claude/settings.json, .mcp.json, _byan/mcp/) from the
169
+ // SAME local package root.
170
+ const nativeSpinner = ora('Refresh Claude Code native...').start();
179
171
  try {
180
172
  const setupModule = path.join(pkgRoot, 'install', 'lib', 'claude-native-setup.js');
181
173
  if (fs.existsSync(setupModule)) {
182
174
  // eslint-disable-next-line import/no-dynamic-require, global-require
183
175
  const { setupClaudeNative } = require(setupModule);
184
- await setupClaudeNative(installPath, { installDeps: true, quiet: false });
176
+ await setupClaudeNative(installPath, { installDeps: true, quiet: true });
177
+ nativeSpinner.succeed('Claude Code native rafraichi');
178
+ } else {
179
+ nativeSpinner.info('Module claude-native-setup absent, refresh ignore');
185
180
  }
186
181
  } catch (e) {
187
- console.warn(chalk.yellow(`Claude native refresh skipped: ${e.message}`));
182
+ nativeSpinner.warn(`Claude native refresh ignore: ${e.message}`);
188
183
  }
189
184
 
190
185
  // FS migration (F11) — dormant by default. Acts only when explicitly
191
186
  // enabled (env BYAN_FS_MIGRATE=1 or _byan/_config/migrate-fs.enabled)
192
- // AND the legacy module layout is present. Backs up _byan/ first.
187
+ // AND the legacy module layout is present. Same local package root.
193
188
  try {
194
189
  const hookModule = path.join(pkgRoot, 'install', 'lib', 'fs-migration-hook.js');
195
190
  if (fs.existsSync(hookModule)) {
@@ -197,22 +192,22 @@ program
197
192
  const { runFsMigration } = require(hookModule);
198
193
  const r = runFsMigration({ projectRoot: installPath });
199
194
  if (r.ran) {
200
- console.log(chalk.green(` FS migration applied (backup: ${r.backup})`));
195
+ console.log(chalk.green(` FS migration applied (backup: ${r.backup})`));
201
196
  }
202
197
  }
203
198
  } catch (e) {
204
- console.warn(chalk.yellow(` FS migration skipped: ${e.message}`));
199
+ console.warn(chalk.yellow(` FS migration skipped: ${e.message}`));
205
200
  }
206
-
207
- updateSpinner.succeed('Derniere version installee');
208
201
  } catch (error) {
209
- updateSpinner.fail('Erreur installation');
210
-
211
- // Rollback
202
+ updateSpinner.fail('Erreur reconstruction');
203
+
204
+ // Rollback. With the atomic-swap rebuild, _byan is only ever replaced
205
+ // after a validated stage, so most failures happen before destruction;
206
+ // the backup restore is the belt-and-suspenders net.
212
207
  const rollbackSpinner = ora('Restauration backup...').start();
213
208
  await backup.restore(backupPath);
214
209
  rollbackSpinner.succeed('Backup restaure');
215
-
210
+
216
211
  throw error;
217
212
  }
218
213
 
@@ -253,9 +248,9 @@ program
253
248
  const { runMigration } = require('../lib/migrate-mcp-config');
254
249
  const result = await runMigration(process.cwd(), { verbose: false });
255
250
  if (result.migrated) {
256
- console.log(chalk.green(` .mcp.json migrated (${result.changes.length} change${result.changes.length > 1 ? 's' : ''})`));
251
+ console.log(chalk.green(` .mcp.json migrated (${result.changes.length} change${result.changes.length > 1 ? 's' : ''})`));
257
252
  } else if (result.reason === 'no-token-available') {
258
- console.log(chalk.yellow(` ${result.hint}`));
253
+ console.log(chalk.yellow(` ${result.hint}`));
259
254
  }
260
255
  // silent on already-ok / no-mcp-json / no-byan-server
261
256
  } catch (err) {