create-byan-agent 2.9.4 → 2.9.6

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 (98) hide show
  1. package/install/bin/byan-cleanup.js +156 -0
  2. package/install/bin/byan-kanban.js +159 -0
  3. package/install/bin/byan-ledger.js +45 -0
  4. package/install/bin/create-byan-agent-v2.js +15 -1
  5. package/install/lib/cleanup/detector.js +154 -0
  6. package/install/lib/cleanup/executor.js +72 -0
  7. package/install/lib/staging-consent.js +149 -0
  8. package/install/lib/subagent-generator.js +208 -0
  9. package/install/lib/token-ledger.js +131 -0
  10. package/install/templates/.claude/agents/bmad-bmad-master.md +14 -0
  11. package/install/templates/.claude/agents/bmad-bmb-agent-builder.md +14 -0
  12. package/install/templates/.claude/agents/bmad-bmb-module-builder.md +14 -0
  13. package/install/templates/.claude/agents/bmad-bmb-workflow-builder.md +14 -0
  14. package/install/templates/.claude/agents/bmad-bmm-analyst.md +14 -0
  15. package/install/templates/.claude/agents/bmad-bmm-architect.md +14 -0
  16. package/install/templates/.claude/agents/bmad-bmm-dev.md +14 -0
  17. package/install/templates/.claude/agents/bmad-bmm-pm.md +14 -0
  18. package/install/templates/.claude/agents/bmad-bmm-quick-flow-solo-dev.md +14 -0
  19. package/install/templates/.claude/agents/bmad-bmm-quinn.md +14 -0
  20. package/install/templates/.claude/agents/bmad-bmm-sm.md +14 -0
  21. package/install/templates/.claude/agents/bmad-bmm-tech-writer.md +14 -0
  22. package/install/templates/.claude/agents/bmad-bmm-ux-designer.md +14 -0
  23. package/install/templates/.claude/agents/bmad-byan-v2.md +14 -0
  24. package/install/templates/.claude/agents/bmad-byan.md +152 -0
  25. package/install/templates/.claude/agents/bmad-carmack.md +14 -0
  26. package/install/templates/.claude/agents/bmad-cis-brainstorming-coach.md +14 -0
  27. package/install/templates/.claude/agents/bmad-cis-creative-problem-solver.md +14 -0
  28. package/install/templates/.claude/agents/bmad-cis-design-thinking-coach.md +14 -0
  29. package/install/templates/.claude/agents/bmad-cis-innovation-strategist.md +14 -0
  30. package/install/templates/.claude/agents/bmad-cis-presentation-master.md +14 -0
  31. package/install/templates/.claude/agents/bmad-cis-storyteller.md +14 -0
  32. package/install/templates/.claude/agents/bmad-claude.md +26 -0
  33. package/install/templates/.claude/agents/bmad-codex.md +26 -0
  34. package/install/templates/.claude/agents/bmad-compliance.md +68 -0
  35. package/install/templates/.claude/agents/bmad-drawio.md +25 -0
  36. package/install/templates/.claude/agents/bmad-expert-merise-agile.md +54 -0
  37. package/install/templates/.claude/agents/bmad-fact-checker.md +14 -0
  38. package/install/templates/.claude/agents/bmad-forgeron.md +14 -0
  39. package/install/templates/.claude/agents/bmad-hermes.md +59 -0
  40. package/install/templates/.claude/agents/bmad-marc.md +25 -0
  41. package/install/templates/.claude/agents/bmad-patnote.md +26 -0
  42. package/install/templates/.claude/agents/bmad-rachid.md +25 -0
  43. package/install/templates/.claude/agents/bmad-tao.md +14 -0
  44. package/install/templates/.claude/agents/bmad-tea-tea.md +14 -0
  45. package/install/templates/.claude/agents/bmad-yanstaller.md +47 -0
  46. package/install/templates/.claude/hooks/fact-check-absolutes.js +185 -0
  47. package/install/templates/.claude/hooks/fd-phase-guard.js +87 -0
  48. package/install/templates/.claude/hooks/fd-response-check.js +92 -0
  49. package/install/templates/.claude/hooks/lib/failure-detector.js +14 -0
  50. package/install/templates/.claude/hooks/pre-compact-save.js +148 -0
  51. package/install/templates/.claude/hooks/stage-to-byan.js +119 -0
  52. package/install/templates/.claude/hooks/tool-failure-guard.js +6 -0
  53. package/install/templates/.claude/hooks/tool-transparency.js +4 -0
  54. package/install/templates/.claude/settings.json +27 -0
  55. package/install/templates/.claude/skills/byan-byan/SKILL.md +115 -163
  56. package/install/templates/.claude/skills/byan-orchestrate/SKILL.md +100 -0
  57. package/install/templates/.githooks/pre-commit +75 -0
  58. package/install/templates/.github/extensions/byan-staging/extension.mjs +169 -0
  59. package/install/templates/.github/extensions/byan-staging/package.json +8 -0
  60. package/install/templates/_byan/mcp/byan-mcp-server/lib/copilot.js +148 -0
  61. package/install/templates/_byan/mcp/byan-mcp-server/lib/fd-state.js +163 -0
  62. package/install/templates/_byan/mcp/byan-mcp-server/lib/kanban.js +226 -0
  63. package/install/templates/_byan/mcp/byan-mcp-server/lib/peer-review.js +187 -0
  64. package/install/templates/_byan/mcp/byan-mcp-server/server.js +463 -0
  65. package/install/templates/detector.js +154 -0
  66. package/package.json +6 -7
  67. package/src/loadbalancer/capability-matrix.js +157 -0
  68. package/src/loadbalancer/config.js +141 -0
  69. package/src/loadbalancer/graceful-degradation.js +212 -0
  70. package/src/loadbalancer/health-probe.js +151 -0
  71. package/src/loadbalancer/hooks/claude-hooks.js +53 -0
  72. package/src/loadbalancer/hooks/copilot-hooks.js +74 -0
  73. package/src/loadbalancer/index.js +81 -0
  74. package/src/loadbalancer/loadbalancer.default.yaml +65 -0
  75. package/src/loadbalancer/loadbalancer.js +324 -0
  76. package/src/loadbalancer/mcp-server.js +304 -0
  77. package/src/loadbalancer/metrics.js +146 -0
  78. package/src/loadbalancer/native/claude-integration.js +64 -0
  79. package/src/loadbalancer/native/copilot-integration.js +59 -0
  80. package/src/loadbalancer/pressure-score.js +102 -0
  81. package/src/loadbalancer/providers/base-provider.js +80 -0
  82. package/src/loadbalancer/providers/byan-api-provider.js +132 -0
  83. package/src/loadbalancer/providers/claude-provider.js +113 -0
  84. package/src/loadbalancer/providers/copilot-provider.js +104 -0
  85. package/src/loadbalancer/rate-limit-tracker.js +216 -0
  86. package/src/loadbalancer/session-bridge.js +179 -0
  87. package/src/loadbalancer/state/db.js +211 -0
  88. package/src/loadbalancer/state/migrations/001-initial.sql +50 -0
  89. package/src/loadbalancer/tools/index.js +123 -0
  90. package/src/loadbalancer/velocity-estimator.js +147 -0
  91. package/src/staging/staging.js +394 -0
  92. package/update-byan-agent/bin/update-byan-agent.js +27 -2
  93. package/API-BYAN-V2.md +0 -741
  94. package/BMAD-QUICK-REFERENCE.md +0 -370
  95. package/CHANGELOG-v2.1.0.md +0 -371
  96. package/MIGRATION-v2.0-to-v2.1.md +0 -430
  97. package/README-BYAN-V2.md +0 -446
  98. package/TEST-GUIDE-v2.3.2.md +0 -161
@@ -0,0 +1,156 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * BYAN cleanup CLI.
4
+ *
5
+ * Scans the current project for stale Claude Code skills and stale
6
+ * root-level doc files, then archives them (safe default) or hard
7
+ * deletes (with --hard).
8
+ *
9
+ * Flags :
10
+ * --dry-run list candidates, do nothing
11
+ * --yes skip confirmation (batch mode)
12
+ * --hard delete instead of archive
13
+ * --skills-only | --docs-only restrict scope
14
+ * --root <dir> override project root (default cwd)
15
+ */
16
+
17
+ const path = require('path');
18
+ const { findStaleSkills, findStaleDocs } = require('../lib/cleanup/detector');
19
+ const { archive, deleteHard } = require('../lib/cleanup/executor');
20
+
21
+ function parseArgs(argv) {
22
+ const out = {
23
+ dryRun: false,
24
+ yes: false,
25
+ hard: false,
26
+ skillsOnly: false,
27
+ docsOnly: false,
28
+ root: process.cwd(),
29
+ };
30
+ for (let i = 2; i < argv.length; i++) {
31
+ const a = argv[i];
32
+ if (a === '--dry-run') out.dryRun = true;
33
+ else if (a === '--yes' || a === '-y') out.yes = true;
34
+ else if (a === '--hard') out.hard = true;
35
+ else if (a === '--skills-only') out.skillsOnly = true;
36
+ else if (a === '--docs-only') out.docsOnly = true;
37
+ else if (a === '--root') out.root = argv[++i];
38
+ else if (a === '-h' || a === '--help') out.help = true;
39
+ }
40
+ return out;
41
+ }
42
+
43
+ function usage() {
44
+ console.log(
45
+ [
46
+ 'byan-cleanup — scan a BYAN project for stale skills + docs',
47
+ '',
48
+ 'Usage:',
49
+ ' byan-cleanup [--dry-run] [--yes] [--hard] [--skills-only|--docs-only] [--root <dir>]',
50
+ '',
51
+ 'Flags:',
52
+ ' --dry-run list candidates, do nothing',
53
+ ' --yes skip confirmation (batch mode)',
54
+ ' --hard delete instead of archive (default: archive)',
55
+ ' --skills-only scan only .claude/skills/',
56
+ ' --docs-only scan only root *.md / *.txt',
57
+ ' --root <dir> project root (default: cwd)',
58
+ '',
59
+ 'Default behavior: archive to _byan-output/archive/<timestamp>/',
60
+ ].join('\n')
61
+ );
62
+ }
63
+
64
+ function fmtSize(n) {
65
+ if (n < 1024) return `${n}B`;
66
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)}KB`;
67
+ return `${(n / 1024 / 1024).toFixed(1)}MB`;
68
+ }
69
+
70
+ function printGroup(label, items) {
71
+ if (items.length === 0) {
72
+ console.log(`\n ${label}: none`);
73
+ return;
74
+ }
75
+ console.log(`\n ${label} (${items.length}):`);
76
+ for (const it of items) {
77
+ console.log(` - ${it.name} ${fmtSize(it.size || 0)}`);
78
+ for (const r of it.reasons) console.log(` · ${r}`);
79
+ }
80
+ }
81
+
82
+ async function confirm(prompt) {
83
+ if (process.stdin.isTTY) {
84
+ const readline = require('readline');
85
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
86
+ return new Promise((resolve) => {
87
+ rl.question(prompt, (ans) => {
88
+ rl.close();
89
+ resolve(/^y(es)?$/i.test((ans || '').trim()));
90
+ });
91
+ });
92
+ }
93
+ // non-TTY : refuse action unless --yes
94
+ return false;
95
+ }
96
+
97
+ async function main() {
98
+ const args = parseArgs(process.argv);
99
+ if (args.help) {
100
+ usage();
101
+ process.exit(0);
102
+ }
103
+
104
+ const root = path.resolve(args.root);
105
+ const skillsDir = path.join(root, '.claude', 'skills');
106
+
107
+ const skills = args.docsOnly ? [] : findStaleSkills(skillsDir);
108
+ const docs = args.skillsOnly ? [] : findStaleDocs(root);
109
+
110
+ console.log(`BYAN cleanup scan — root: ${root}`);
111
+ printGroup('Stale skills', skills);
112
+ printGroup('Stale docs', docs);
113
+ const total = skills.length + docs.length;
114
+ console.log(`\n Total candidates: ${total}`);
115
+
116
+ if (args.dryRun) {
117
+ console.log('\n--dry-run: no action taken.');
118
+ process.exit(0);
119
+ }
120
+
121
+ if (total === 0) {
122
+ console.log('Nothing to clean.');
123
+ process.exit(0);
124
+ }
125
+
126
+ const action = args.hard ? 'HARD DELETE (irreversible)' : 'archive to _byan-output/archive/<timestamp>/';
127
+ console.log(`\nAction: ${action}`);
128
+
129
+ let proceed = args.yes;
130
+ if (!proceed) {
131
+ proceed = await confirm(`Proceed with ${total} item(s) ? (y/N): `);
132
+ }
133
+
134
+ if (!proceed) {
135
+ console.log('Aborted.');
136
+ process.exit(0);
137
+ }
138
+
139
+ const all = [...skills, ...docs];
140
+ if (args.hard) {
141
+ const r = deleteHard(all);
142
+ console.log(`\nDeleted ${r.deleted.length} items.`);
143
+ if (r.errors.length) console.log(`Errors: ${r.errors.length}`);
144
+ for (const e of r.errors) console.log(` ! ${e.path} — ${e.error}`);
145
+ } else {
146
+ const r = archive(all, { archiveRoot: path.join(root, '_byan-output', 'archive') });
147
+ console.log(`\nArchived ${r.moved.length} items to ${r.archiveDir}`);
148
+ if (r.errors.length) console.log(`Errors: ${r.errors.length}`);
149
+ for (const e of r.errors) console.log(` ! ${e.path} — ${e.error}`);
150
+ }
151
+ }
152
+
153
+ main().catch((err) => {
154
+ console.error('byan-cleanup failed:', err.message);
155
+ process.exit(1);
156
+ });
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * BYAN kanban CLI — view a party-mode-session board and stand-up feed.
4
+ *
5
+ * Usage :
6
+ * byan-kanban <session-id> # show board + recent stand-ups
7
+ * byan-kanban <session-id> --json
8
+ * byan-kanban <session-id> --standups-only
9
+ * byan-kanban list # list known sessions with boards
10
+ * byan-kanban <session-id> --root <dir>
11
+ */
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+
16
+ const COLORS = { todo: 90, doing: 33, blocked: 31, review: 36, done: 32, dim: 2, reset: 0 };
17
+ const FG = (n, s) => `\x1b[${n}m${s}\x1b[0m`;
18
+
19
+ function parseArgs(argv) {
20
+ const out = { json: false, standupsOnly: false, root: process.cwd(), sessionId: null, list: false };
21
+ for (let i = 2; i < argv.length; i++) {
22
+ const a = argv[i];
23
+ if (a === '--json') out.json = true;
24
+ else if (a === '--standups-only') out.standupsOnly = true;
25
+ else if (a === '--root') out.root = argv[++i];
26
+ else if (a === 'list') out.list = true;
27
+ else if (!out.sessionId) out.sessionId = a;
28
+ }
29
+ return out;
30
+ }
31
+
32
+ function sessionsRoot(root) {
33
+ return path.join(root, '_byan-output', 'party-mode-sessions');
34
+ }
35
+
36
+ function listSessions(root) {
37
+ const dir = sessionsRoot(root);
38
+ if (!fs.existsSync(dir)) return [];
39
+ return fs
40
+ .readdirSync(dir, { withFileTypes: true })
41
+ .filter((e) => e.isDirectory())
42
+ .map((e) => {
43
+ const kanban = path.join(dir, e.name, 'kanban.json');
44
+ const standup = path.join(dir, e.name, 'standup.jsonl');
45
+ return {
46
+ session_id: e.name,
47
+ has_kanban: fs.existsSync(kanban),
48
+ has_standup: fs.existsSync(standup),
49
+ };
50
+ })
51
+ .sort((a, b) => (a.session_id < b.session_id ? 1 : -1));
52
+ }
53
+
54
+ function renderBoard(board) {
55
+ if (!board) return '(no kanban for this session)';
56
+ const cols = board.columns || ['todo', 'doing', 'blocked', 'review', 'done'];
57
+ const rows = cols.map((c) => {
58
+ const cards = Object.values(board.cards || {}).filter((card) => card.column === c);
59
+ return { col: c, cards };
60
+ });
61
+
62
+ const lines = [];
63
+ lines.push(FG(COLORS.dim, `session ${board.session_id} — updated ${board.updated_at}`));
64
+ lines.push('');
65
+ for (const r of rows) {
66
+ const color = COLORS[r.col] || 0;
67
+ lines.push(FG(color, `[${r.col.toUpperCase()}] (${r.cards.length})`));
68
+ if (r.cards.length === 0) {
69
+ lines.push(FG(COLORS.dim, ' (empty)'));
70
+ } else {
71
+ for (const c of r.cards) {
72
+ const assignee = c.assignee ? ` @${c.assignee}` : '';
73
+ const blocker = c.column === 'blocked' && c.blocker_reason ? ` — blocked: ${c.blocker_reason}` : '';
74
+ lines.push(` - [${c.priority}] ${c.id} ${c.title}${assignee}${blocker}`);
75
+ }
76
+ }
77
+ lines.push('');
78
+ }
79
+ return lines.join('\n');
80
+ }
81
+
82
+ function renderStandups(entries) {
83
+ if (entries.length === 0) return FG(COLORS.dim, '(no stand-ups yet)');
84
+ return entries
85
+ .map((e) => {
86
+ const blockers = e.blockers && e.blockers.length > 0
87
+ ? FG(COLORS.blocked, ` blockers=${e.blockers.join('|')}`)
88
+ : '';
89
+ return `${e.timestamp} ${e.agent}: ${e.did || '-'}${blockers} → ${e.next || '-'}`;
90
+ })
91
+ .join('\n');
92
+ }
93
+
94
+ function main() {
95
+ const args = parseArgs(process.argv);
96
+ const root = path.resolve(args.root);
97
+
98
+ if (args.list || !args.sessionId) {
99
+ const sessions = listSessions(root);
100
+ if (args.json) {
101
+ process.stdout.write(JSON.stringify(sessions, null, 2) + '\n');
102
+ return;
103
+ }
104
+ if (sessions.length === 0) {
105
+ console.log('(no party-mode sessions found)');
106
+ return;
107
+ }
108
+ console.log('sessions :');
109
+ for (const s of sessions) {
110
+ const flags =
111
+ (s.has_kanban ? 'K' : '-') + (s.has_standup ? 'S' : '-');
112
+ console.log(` [${flags}] ${s.session_id}`);
113
+ }
114
+ console.log('\npass a session id to view its board.');
115
+ return;
116
+ }
117
+
118
+ const sessionDir = path.join(sessionsRoot(root), args.sessionId);
119
+ const kanbanPath = path.join(sessionDir, 'kanban.json');
120
+ const standupPath = path.join(sessionDir, 'standup.jsonl');
121
+
122
+ let board = null;
123
+ if (fs.existsSync(kanbanPath)) {
124
+ try {
125
+ board = JSON.parse(fs.readFileSync(kanbanPath, 'utf8'));
126
+ } catch {
127
+ board = null;
128
+ }
129
+ }
130
+
131
+ let standups = [];
132
+ if (fs.existsSync(standupPath)) {
133
+ standups = fs
134
+ .readFileSync(standupPath, 'utf8')
135
+ .split('\n')
136
+ .filter(Boolean)
137
+ .map((l) => {
138
+ try {
139
+ return JSON.parse(l);
140
+ } catch {
141
+ return null;
142
+ }
143
+ })
144
+ .filter(Boolean);
145
+ }
146
+
147
+ if (args.json) {
148
+ process.stdout.write(JSON.stringify({ board, standups }, null, 2) + '\n');
149
+ return;
150
+ }
151
+
152
+ if (!args.standupsOnly) {
153
+ console.log(renderBoard(board));
154
+ }
155
+ console.log('stand-ups :');
156
+ console.log(renderStandups(standups.slice(-10)));
157
+ }
158
+
159
+ main();
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * BYAN token ledger CLI.
4
+ *
5
+ * Usage :
6
+ * byan-ledger # full session summary
7
+ * byan-ledger --json # machine-readable
8
+ * byan-ledger --since 2026-04-19T15:00:00Z
9
+ * byan-ledger --root <dir> # override project root
10
+ */
11
+
12
+ const path = require('path');
13
+ const { readLog, summarize, renderReport, defaultLogPath } = require('../lib/token-ledger');
14
+
15
+ function parseArgs(argv) {
16
+ const out = { json: false, since: null, root: process.cwd() };
17
+ for (let i = 2; i < argv.length; i++) {
18
+ const a = argv[i];
19
+ if (a === '--json') out.json = true;
20
+ else if (a === '--since') out.since = argv[++i];
21
+ else if (a === '--root') out.root = argv[++i];
22
+ else if (a === '-h' || a === '--help') out.help = true;
23
+ }
24
+ return out;
25
+ }
26
+
27
+ function main() {
28
+ const args = parseArgs(process.argv);
29
+ if (args.help) {
30
+ console.log('byan-ledger [--json] [--since <iso>] [--root <dir>]');
31
+ process.exit(0);
32
+ }
33
+
34
+ const logPath = defaultLogPath(path.resolve(args.root));
35
+ const entries = readLog(logPath);
36
+ const stats = summarize(entries, { since: args.since });
37
+
38
+ if (args.json) {
39
+ process.stdout.write(JSON.stringify(stats, null, 2) + '\n');
40
+ } else {
41
+ process.stdout.write(renderReport(stats) + '\n');
42
+ }
43
+ }
44
+
45
+ main();
@@ -16,6 +16,7 @@ const { generateProjectAgentsDoc } = require('../lib/project-agents-generator');
16
16
  const { launchPhase2Chat, generateDefaultConfig } = require('../lib/phase2-chat');
17
17
  const { setupByanWebIntegration } = require('../lib/byan-web-integration');
18
18
  const { setupClaudeNative } = require('../lib/claude-native-setup');
19
+ const { setupStagingConsent } = require('../lib/staging-consent');
19
20
 
20
21
  const BYAN_VERSION = require('../package.json').version;
21
22
 
@@ -1355,11 +1356,24 @@ async function install() {
1355
1356
  if (needsClaude || needsCopilot) {
1356
1357
  console.log();
1357
1358
  console.log(chalk.cyan('byan_web integration (optional — service payant)'));
1359
+ let byanWebResult = { configured: false };
1358
1360
  try {
1359
- await setupByanWebIntegration(projectRoot);
1361
+ byanWebResult = await setupByanWebIntegration(projectRoot);
1360
1362
  } catch (error) {
1361
1363
  console.log(chalk.yellow(` ⚠ byan_web setup skipped: ${error.message}`));
1362
1364
  }
1365
+
1366
+ if (byanWebResult && byanWebResult.configured) {
1367
+ console.log();
1368
+ console.log(chalk.cyan('byan_web memory-sync opt-in (consent)'));
1369
+ try {
1370
+ await setupStagingConsent(projectRoot, {
1371
+ byanWebConfigured: true,
1372
+ });
1373
+ } catch (error) {
1374
+ console.log(chalk.yellow(` ⚠ memory-sync setup skipped: ${error.message}`));
1375
+ }
1376
+ }
1363
1377
  }
1364
1378
 
1365
1379
  // Step 8: Create config.yaml
@@ -0,0 +1,154 @@
1
+ /**
2
+ * BYAN cleanup detector.
3
+ *
4
+ * Two scans :
5
+ * - findStaleSkills(skillsDir) : Claude Code skills under .claude/skills/
6
+ * that look like auto-generated stubs with no real BYAN value (test
7
+ * agents, self-referential, platform skills, or tiny SKILL.md files).
8
+ * - findStaleDocs(projectRoot) : root-level .md files that look like
9
+ * historical artifacts (old version docs, sprint reports, test plans,
10
+ * interview summaries, announcement guides).
11
+ *
12
+ * All detection is pattern-based with a hard allowlist ; no file is
13
+ * flagged without a named reason.
14
+ */
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+
19
+ const SKILL_ALLOWLIST = new Set([
20
+ 'byan-fact-check',
21
+ 'byan-elo-trust',
22
+ 'byan-merise-agile',
23
+ 'byan-hermes-dispatch',
24
+ 'byan-forge',
25
+ 'byan-byan', // BYAN itself as a skill — core, never flag
26
+ ]);
27
+
28
+ const SKILL_PATTERNS = [
29
+ { re: /(^|-)test(ing|-dynamic)?$/i, reason: 'test/placeholder agent (not a real BYAN capability)' },
30
+ { re: /^byan-byan-(v2|test)$/i, reason: 'versioned or test stub of BYAN itself' },
31
+ { re: /^byan-(claude|codex)$/i, reason: 'platform name (not an agent role)' },
32
+ { re: /^byan-skeptic$/i, reason: 'experimental agent not wired in menus' },
33
+ ];
34
+
35
+ const DOC_ALLOWLIST = new Set([
36
+ 'README.md',
37
+ 'LICENSE',
38
+ 'LICENSE.md',
39
+ 'CHANGELOG.md',
40
+ 'CONTRIBUTING.md',
41
+ 'CODE_OF_CONDUCT.md',
42
+ 'CLAUDE.md',
43
+ 'GUIDE-UTILISATION.md',
44
+ 'NOTES.md',
45
+ ]);
46
+
47
+ const DOC_PATTERNS = [
48
+ { re: /^SPRINT\d+-.*\.md$/i, reason: 'sprint report' },
49
+ { re: /^BYAN-V\d+(\.\d+)*-.*\.md$/i, reason: 'old version snapshot doc' },
50
+ { re: /^CHANGELOG-v\d+(\.\d+)*\.md$/i, reason: 'versioned changelog (current is CHANGELOG.md)' },
51
+ { re: /^MIGRATION-.*\.md$/i, reason: 'migration guide for an older version' },
52
+ { re: /^README-BYAN-V\d+(\.\d+)*\.md$/i, reason: 'versioned README (current is README.md)' },
53
+ { re: /^API-BYAN-V\d+(\.\d+)*\.md$/i, reason: 'versioned API doc' },
54
+ { re: /^TEST-GUIDE-.*\.md$/i, reason: 'historical test guide' },
55
+ { re: /-MANUAL-TEST-PLAN\.md$/i, reason: 'manual test plan' },
56
+ { re: /-STATUS-REPORT\.md$/i, reason: 'status report' },
57
+ { re: /-COMPLETE\.md$/i, reason: 'completion report' },
58
+ { re: /-SUMMARY(\.txt|\.md)$/i, reason: 'summary artifact' },
59
+ { re: /-DELIVERY-SUMMARY\.md$/i, reason: 'delivery summary' },
60
+ { re: /^interview-summary-.*\.md$/i, reason: 'interview summary' },
61
+ { re: /^fd-.*\.md$/i, reason: 'forge-persona session artifact' },
62
+ { re: /^AGENT-LAUNCHER-DOC.*\.md$/i, reason: 'agent launcher legacy doc' },
63
+ { re: /^ANNOUNCEMENT-GUIDE-.*\.md$/i, reason: 'announcement guide for older version' },
64
+ { re: /^SESSION-RESUME-.*\.md$/i, reason: 'session resume artifact' },
65
+ { re: /^BYAN-V2-.*\.md$/i, reason: 'v2-specific doc (keep only if still referenced)' },
66
+ { re: /^BMAD-QUICK-REFERENCE.*\.md$/i, reason: 'legacy quick reference' },
67
+ { re: /^100-PERCENT-.*\.md$/i, reason: 'milestone completion report' },
68
+ ];
69
+
70
+ function readFrontmatter(filePath) {
71
+ try {
72
+ const raw = fs.readFileSync(filePath, 'utf8');
73
+ const m = raw.match(/^---\s*\n([\s\S]*?)\n---/);
74
+ if (!m) return {};
75
+ const out = {};
76
+ for (const line of m[1].split('\n')) {
77
+ const kv = line.match(/^(\w[\w-]*)\s*:\s*(.*)$/);
78
+ if (kv) out[kv[1]] = kv[2].replace(/^["']|["']$/g, '').trim();
79
+ }
80
+ return out;
81
+ } catch {
82
+ return {};
83
+ }
84
+ }
85
+
86
+ function findStaleSkills(skillsDir) {
87
+ if (!fs.existsSync(skillsDir)) return [];
88
+ const found = [];
89
+ const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
90
+ for (const e of entries) {
91
+ if (!e.isDirectory()) continue;
92
+ if (SKILL_ALLOWLIST.has(e.name)) continue;
93
+
94
+ const skillFile = path.join(skillsDir, e.name, 'SKILL.md');
95
+ if (!fs.existsSync(skillFile)) continue;
96
+
97
+ const reasons = [];
98
+
99
+ for (const { re, reason } of SKILL_PATTERNS) {
100
+ if (re.test(e.name)) reasons.push(reason);
101
+ }
102
+
103
+ const size = fs.statSync(skillFile).size;
104
+ if (size < 200) reasons.push(`tiny SKILL.md (${size} bytes — likely placeholder stub)`);
105
+
106
+ const fm = readFrontmatter(skillFile);
107
+ if (!fm.description || fm.description.length < 40) {
108
+ reasons.push('missing or too-short description in frontmatter');
109
+ }
110
+
111
+ if (reasons.length > 0) {
112
+ found.push({
113
+ path: path.join(skillsDir, e.name),
114
+ name: e.name,
115
+ reasons,
116
+ size,
117
+ });
118
+ }
119
+ }
120
+ return found;
121
+ }
122
+
123
+ function findStaleDocs(projectRoot) {
124
+ if (!fs.existsSync(projectRoot)) return [];
125
+ const found = [];
126
+ const entries = fs.readdirSync(projectRoot, { withFileTypes: true });
127
+ for (const e of entries) {
128
+ if (!e.isFile()) continue;
129
+ if (!/\.(md|txt)$/i.test(e.name)) continue;
130
+ if (DOC_ALLOWLIST.has(e.name)) continue;
131
+
132
+ const reasons = [];
133
+ for (const { re, reason } of DOC_PATTERNS) {
134
+ if (re.test(e.name)) reasons.push(reason);
135
+ }
136
+
137
+ if (reasons.length > 0) {
138
+ const p = path.join(projectRoot, e.name);
139
+ const size = fs.statSync(p).size;
140
+ found.push({ path: p, name: e.name, reasons, size });
141
+ }
142
+ }
143
+ return found;
144
+ }
145
+
146
+ module.exports = {
147
+ SKILL_ALLOWLIST,
148
+ SKILL_PATTERNS,
149
+ DOC_ALLOWLIST,
150
+ DOC_PATTERNS,
151
+ findStaleSkills,
152
+ findStaleDocs,
153
+ readFrontmatter,
154
+ };
@@ -0,0 +1,72 @@
1
+ /**
2
+ * BYAN cleanup executor.
3
+ *
4
+ * Two removal modes :
5
+ * - archive(items, archiveDir) : move each item into a timestamped
6
+ * subdir of archiveDir (reversible).
7
+ * - deleteHard(items) : rm -rf each item (irreversible).
8
+ *
9
+ * Both return per-item results so the caller can print a clean summary.
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ function timestampDir() {
16
+ const d = new Date();
17
+ const pad = (n) => String(n).padStart(2, '0');
18
+ return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
19
+ }
20
+
21
+ function archive(items, { archiveRoot, now = new Date() } = {}) {
22
+ if (!Array.isArray(items) || items.length === 0) return { moved: [], errors: [] };
23
+
24
+ const stamp =
25
+ now instanceof Date
26
+ ? (() => {
27
+ const pad = (n) => String(n).padStart(2, '0');
28
+ return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
29
+ })()
30
+ : String(now);
31
+
32
+ const baseRoot = archiveRoot || path.join('_byan-output', 'archive');
33
+ const dest = path.join(baseRoot, stamp);
34
+ fs.mkdirSync(dest, { recursive: true });
35
+
36
+ const moved = [];
37
+ const errors = [];
38
+ for (const item of items) {
39
+ const src = item.path || item;
40
+ const name = path.basename(src);
41
+ const target = path.join(dest, name);
42
+ try {
43
+ fs.renameSync(src, target);
44
+ moved.push({ from: src, to: target });
45
+ } catch (err) {
46
+ errors.push({ path: src, error: err.message });
47
+ }
48
+ }
49
+ return { moved, errors, archiveDir: dest };
50
+ }
51
+
52
+ function deleteHard(items) {
53
+ if (!Array.isArray(items) || items.length === 0) return { deleted: [], errors: [] };
54
+ const deleted = [];
55
+ const errors = [];
56
+ for (const item of items) {
57
+ const src = item.path || item;
58
+ try {
59
+ fs.rmSync(src, { recursive: true, force: true });
60
+ deleted.push(src);
61
+ } catch (err) {
62
+ errors.push({ path: src, error: err.message });
63
+ }
64
+ }
65
+ return { deleted, errors };
66
+ }
67
+
68
+ module.exports = {
69
+ archive,
70
+ deleteHard,
71
+ timestampDir,
72
+ };