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.
- package/CHANGELOG.md +188 -0
- package/README.md +4 -4
- package/install/src/byan-v2/generation/templates/default-agent.md +1 -1
- package/install/templates/.claude/CLAUDE.md +1 -1
- package/install/templates/.claude/hooks/fd-phase-guard.js +2 -2
- package/install/templates/.claude/hooks/mantra-validate.js +16 -8
- package/install/templates/.claude/hooks/strict-scope-guard.js +25 -7
- package/install/templates/.claude/rules/native-workflows.md +32 -0
- package/install/templates/.claude/skills/byan-byan/SKILL.md +5 -5
- package/install/templates/.claude/skills/byan-mantra-audit/SKILL.md +53 -0
- package/install/templates/.claude/skills/byan-merise-agile/SKILL.md +2 -2
- package/install/templates/.claude/skills/byan-native-dev-story/SKILL.md +83 -0
- package/install/templates/.claude/workflows/INDEX.md +35 -0
- package/install/templates/.claude/workflows/check-implementation-readiness.js +280 -0
- package/install/templates/.claude/workflows/code-review.js +179 -0
- package/install/templates/.claude/workflows/create-excalidraw-dataflow.js +214 -0
- package/install/templates/.claude/workflows/create-excalidraw-diagram.js +188 -0
- package/install/templates/.claude/workflows/create-excalidraw-flowchart.js +225 -0
- package/install/templates/.claude/workflows/create-excalidraw-wireframe.js +192 -0
- package/install/templates/.claude/workflows/create-story.js +216 -0
- package/install/templates/.claude/workflows/dev-story.js +100 -0
- package/install/templates/.claude/workflows/document-project.js +455 -0
- package/install/templates/.claude/workflows/qa-automate.js +169 -0
- package/install/templates/.claude/workflows/quick-dev.js +273 -0
- package/install/templates/.claude/workflows/sprint-planning.js +261 -0
- package/install/templates/.claude/workflows/testarch-atdd.js +287 -0
- package/install/templates/.claude/workflows/testarch-automate.js +229 -0
- package/install/templates/.claude/workflows/testarch-ci.js +184 -0
- package/install/templates/.claude/workflows/testarch-framework.js +267 -0
- package/install/templates/.claude/workflows/testarch-nfr.js +316 -0
- package/install/templates/.claude/workflows/testarch-test-design.js +293 -0
- package/install/templates/.claude/workflows/testarch-test-review.js +321 -0
- package/install/templates/.claude/workflows/testarch-trace.js +316 -0
- package/install/templates/.githooks/pre-commit +49 -15
- package/install/templates/_byan/config.yaml +15 -5
- package/install/templates/_byan/mcp/byan-mcp-server/bin/byan-build-workflows.js +20 -0
- package/install/templates/_byan/mcp/byan-mcp-server/bin/byan-lint-workflows.js +57 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/native-loop.js +39 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/workflows-generator.js +149 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/workflows-lint.js +113 -0
- package/install/templates/_byan/workflow/simple/byan/feature-workflow.md +14 -11
- package/install/templates/docs/native-workflows-contract.md +84 -0
- package/package.json +2 -2
- package/src/byan-v2/data/agent-scopes.json +46 -0
- package/src/byan-v2/data/mantras.json +194 -8
- package/src/byan-v2/generation/mantra-audit.js +147 -0
- package/src/byan-v2/generation/mantra-validator.js +56 -6
- package/src/byan-v2/generation/scope-resolver.js +102 -0
- package/src/byan-v2/generation/templates/default-agent.md +1 -1
- package/update-byan-agent/bin/update-byan-agent.js +67 -72
- package/update-byan-agent/lib/apply-update.js +202 -0
- 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
|
-
|
|
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:
|
|
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
|
|
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 =
|
|
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 >=
|
|
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 >=
|
|
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 +
|
|
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(
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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:
|
|
134
|
-
|
|
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
|
-
|
|
137
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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(`
|
|
195
|
+
console.log(chalk.green(` FS migration applied (backup: ${r.backup})`));
|
|
201
196
|
}
|
|
202
197
|
}
|
|
203
198
|
} catch (e) {
|
|
204
|
-
console.warn(chalk.yellow(`
|
|
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
|
|
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(`
|
|
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(`
|
|
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) {
|