create-byan-agent 2.16.1 → 2.17.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 (30) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +24 -0
  3. package/install/lib/claude-native-setup.js +37 -0
  4. package/install/package.json +1 -1
  5. package/install/packages/platform-config/lib/validate.js +0 -14
  6. package/install/src/webui/api.js +6 -0
  7. package/install/src/webui/server.js +8 -1
  8. package/install/templates/.claude/CLAUDE.md +18 -0
  9. package/install/templates/.claude/hooks/lib/strict-config.json +46 -0
  10. package/install/templates/.claude/hooks/lib/strict-runtime.js +82 -0
  11. package/install/templates/.claude/hooks/strict-context-inject.js +86 -0
  12. package/install/templates/.claude/hooks/strict-scope-guard.js +101 -0
  13. package/install/templates/.claude/hooks/strict-stop-guard.js +100 -0
  14. package/install/templates/.claude/rules/strict-mode.md +166 -0
  15. package/install/templates/.claude/settings.json +12 -0
  16. package/install/templates/.claude/skills/byan-strict/SKILL.md +54 -0
  17. package/install/templates/.githooks/pre-commit +15 -0
  18. package/install/templates/_byan/_config/strict-mode.yaml +258 -0
  19. package/install/templates/_byan/mcp/byan-mcp-server/bin/byan-sync-rules.js +24 -0
  20. package/install/templates/_byan/mcp/byan-mcp-server/bin/strict-precommit-gate.js +21 -0
  21. package/install/templates/_byan/mcp/byan-mcp-server/lib/fd-state.js +2 -1
  22. package/install/templates/_byan/mcp/byan-mcp-server/lib/precommit-gate.js +120 -0
  23. package/install/templates/_byan/mcp/byan-mcp-server/lib/strict-activation.js +76 -0
  24. package/install/templates/_byan/mcp/byan-mcp-server/lib/strict-mode.js +391 -0
  25. package/install/templates/_byan/mcp/byan-mcp-server/lib/strict-sync.js +140 -0
  26. package/install/templates/_byan/mcp/byan-mcp-server/lib/sync-rules.js +261 -0
  27. package/install/templates/_byan/mcp/byan-mcp-server/server.js +207 -1
  28. package/package.json +6 -2
  29. package/src/byan-v2/data/strict-mantras.json +188 -0
  30. package/src/byan-v2/generation/mantra-validator.js +39 -4
@@ -0,0 +1,140 @@
1
+ // BYAN Strict Mode — API sync layer.
2
+ //
3
+ // The byan_web API is the authority for strict sessions; the local
4
+ // .byan-strict/ state is a mirror. This module is the only place that talks to
5
+ // the network on behalf of strict mode — strict-mode.js stays pure-local.
6
+ //
7
+ // Every push is best-effort: a missing token, an unreachable API, or a non-2xx
8
+ // response degrades to { synced: false, reason } and never throws. The local
9
+ // protocol must keep working whether or not the API answers. Reads
10
+ // (fetchSession) are how the authority is consulted; callers decide how to
11
+ // reconcile, with the local state as the offline fallback.
12
+
13
+ const DEFAULT_TIMEOUT_MS = 4000;
14
+
15
+ function apiBase() {
16
+ return (process.env.BYAN_API_URL || 'http://localhost:3737').replace(/\/+$/, '');
17
+ }
18
+
19
+ function apiToken() {
20
+ return process.env.BYAN_API_TOKEN || '';
21
+ }
22
+
23
+ function authHeader(token) {
24
+ if (!token) return {};
25
+ const scheme = token.startsWith('byan_') ? 'ApiKey' : 'Bearer';
26
+ return { Authorization: `${scheme} ${token}` };
27
+ }
28
+
29
+ export function syncEnabled({ token = apiToken() } = {}) {
30
+ return Boolean(token);
31
+ }
32
+
33
+ async function request(
34
+ method,
35
+ routePath,
36
+ { body, apiUrl = apiBase(), token = apiToken(), fetchImpl = globalThis.fetch, timeoutMs = DEFAULT_TIMEOUT_MS } = {}
37
+ ) {
38
+ if (!token) return { ok: false, synced: false, reason: 'no_token' };
39
+ if (typeof fetchImpl !== 'function') return { ok: false, synced: false, reason: 'no_fetch' };
40
+
41
+ const controller = new AbortController();
42
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
43
+ try {
44
+ const res = await fetchImpl(`${apiUrl}${routePath}`, {
45
+ method,
46
+ headers: {
47
+ 'Content-Type': 'application/json',
48
+ ...authHeader(token),
49
+ },
50
+ body: body ? JSON.stringify(body) : undefined,
51
+ signal: controller.signal,
52
+ });
53
+ let data = null;
54
+ try {
55
+ data = await res.json();
56
+ } catch {
57
+ data = null;
58
+ }
59
+ if (!res.ok) {
60
+ return { ok: false, synced: false, reason: `http_${res.status}`, status: res.status, data };
61
+ }
62
+ return { ok: true, synced: true, status: res.status, data: data ? data.data : null };
63
+ } catch (err) {
64
+ return { ok: false, synced: false, reason: err && err.name === 'AbortError' ? 'timeout' : 'network_error', error: err ? err.message : String(err) };
65
+ } finally {
66
+ clearTimeout(timer);
67
+ }
68
+ }
69
+
70
+ // POST /api/strict-sessions — create or upsert at scope-lock time.
71
+ export function pushLock(
72
+ { sessionId, scopeLock, projectId = process.env.BYAN_PROJECT_ID || null, featureName = null },
73
+ opts = {}
74
+ ) {
75
+ if (!scopeLock) return Promise.resolve({ ok: false, synced: false, reason: 'no_scope_lock' });
76
+ return request('POST', '/api/strict-sessions', {
77
+ ...opts,
78
+ body: {
79
+ id: sessionId,
80
+ projectId,
81
+ featureName,
82
+ scopeText: scopeLock.scope_text,
83
+ scopeHash: scopeLock.scope_hash,
84
+ acceptanceCriteria: scopeLock.acceptance_criteria,
85
+ allowedPaths: scopeLock.allowed_paths,
86
+ },
87
+ });
88
+ }
89
+
90
+ // PATCH /api/strict-sessions/:id — append one verify pass.
91
+ export function pushVerify({ sessionId, pass }, opts = {}) {
92
+ if (!sessionId || !pass) return Promise.resolve({ ok: false, synced: false, reason: 'missing_args' });
93
+ return request('PATCH', `/api/strict-sessions/${encodeURIComponent(sessionId)}`, {
94
+ ...opts,
95
+ body: {
96
+ verifyPass: {
97
+ pass: pass.pass,
98
+ verdict: pass.verdict,
99
+ findings: pass.findings || [],
100
+ completedAt: pass.completed_at,
101
+ },
102
+ },
103
+ });
104
+ }
105
+
106
+ // PATCH /api/strict-sessions/:id — mark completed, store the audit token.
107
+ export function pushComplete({ sessionId, auditToken, completedAt }, opts = {}) {
108
+ if (!sessionId) return Promise.resolve({ ok: false, synced: false, reason: 'missing_args' });
109
+ return request('PATCH', `/api/strict-sessions/${encodeURIComponent(sessionId)}`, {
110
+ ...opts,
111
+ body: { complete: { auditToken, completedAt } },
112
+ });
113
+ }
114
+
115
+ // PATCH /api/strict-sessions/:id — deliberate abort.
116
+ export function pushAbort({ sessionId, reason }, opts = {}) {
117
+ if (!sessionId) return Promise.resolve({ ok: false, synced: false, reason: 'missing_args' });
118
+ return request('PATCH', `/api/strict-sessions/${encodeURIComponent(sessionId)}`, {
119
+ ...opts,
120
+ body: { abort: { reason: reason || null } },
121
+ });
122
+ }
123
+
124
+ // GET /api/strict-sessions/:id — read the authoritative server record.
125
+ export function fetchSession({ sessionId }, opts = {}) {
126
+ if (!sessionId) return Promise.resolve({ ok: false, synced: false, reason: 'missing_args' });
127
+ return request('GET', `/api/strict-sessions/${encodeURIComponent(sessionId)}`, opts);
128
+ }
129
+
130
+ // Resolve a byan_web project id from the active FD project_context (slug/name).
131
+ // Best-effort: returns the id of the first match, or null when nothing matches.
132
+ export async function resolveProjectId({ slug, name } = {}, opts = {}) {
133
+ const term = name || slug;
134
+ if (!term) return null;
135
+ const res = await request('GET', `/api/projects/search?slug=${encodeURIComponent(term)}`, opts);
136
+ if (res.ok && Array.isArray(res.data) && res.data.length > 0) {
137
+ return res.data[0].id;
138
+ }
139
+ return null;
140
+ }
@@ -0,0 +1,261 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import yaml from 'js-yaml';
4
+
5
+ // byan-sync-rules generator.
6
+ //
7
+ // Reads the single source of truth (_byan/_config/strict-mode.yaml) and emits
8
+ // the per-platform artifacts that enforce BYAN Strict Mode:
9
+ // - .claude/skills/byan-strict/SKILL.md (owned, full-file)
10
+ // - .claude/hooks/lib/strict-config.json (owned, full-file)
11
+ // - AGENTS.md (upsert block, Codex)
12
+ // - .github/copilot-instructions.md (upsert block, Copilot)
13
+ //
14
+ // Owned files are rewritten wholesale (they carry a generated-by header).
15
+ // Shared files get a block upserted between BYAN-STRICT markers, leaving the
16
+ // rest of the file untouched.
17
+
18
+ const BEGIN = 'BYAN-STRICT:BEGIN';
19
+ const END = 'BYAN-STRICT:END';
20
+ const DEFAULT_CONFIG_REL = path.join('_byan', '_config', 'strict-mode.yaml');
21
+
22
+ export function resolveRoot(projectRoot) {
23
+ return projectRoot || process.env.CLAUDE_PROJECT_DIR || process.cwd();
24
+ }
25
+
26
+ export function loadConfig({ projectRoot, configPath } = {}) {
27
+ const root = resolveRoot(projectRoot);
28
+ const file = configPath || path.join(root, DEFAULT_CONFIG_REL);
29
+ if (!fs.existsSync(file)) {
30
+ throw new Error(`strict-mode config not found at ${file}`);
31
+ }
32
+ const cfg = yaml.load(fs.readFileSync(file, 'utf8'));
33
+ if (!cfg || typeof cfg !== 'object') {
34
+ throw new Error(`strict-mode config at ${file} did not parse to an object`);
35
+ }
36
+ if (!Array.isArray(cfg.mantras) || cfg.mantras.length === 0) {
37
+ throw new Error('strict-mode config must define a non-empty mantras list');
38
+ }
39
+ return cfg;
40
+ }
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Renderers — pure functions config -> string.
44
+ // ---------------------------------------------------------------------------
45
+
46
+ const GENERATED_NOTE =
47
+ 'Generated by byan-sync-rules from _byan/_config/strict-mode.yaml. Do not hand-edit.';
48
+
49
+ export function renderStrictConfig(cfg) {
50
+ return {
51
+ _generated_by: 'byan-sync-rules',
52
+ version: cfg.version,
53
+ min_passes: cfg.self_verify.min_passes,
54
+ last_verdict_must_be: cfg.self_verify.last_verdict_must_be,
55
+ min_score: cfg.confidence.min_score,
56
+ auto_keywords: cfg.activation.auto_keywords,
57
+ completion_claim_markers: cfg.hooks.completion_claim_markers,
58
+ scope_guard: cfg.hooks.scope_guard,
59
+ freshness_window_seconds: cfg.hooks.freshness_window_seconds,
60
+ banners: {
61
+ context: cfg.injection.context_banner,
62
+ stop_block: cfg.injection.stop_block_reason,
63
+ scope_deny: cfg.injection.pretooluse_deny_reason,
64
+ },
65
+ };
66
+ }
67
+
68
+ function mantraLines(cfg) {
69
+ return cfg.mantras
70
+ .map((m) => `- **${m.id} ${m.name}** — ${m.rule.trim()}`)
71
+ .join('\n');
72
+ }
73
+
74
+ // Projects the strict mantras into the shape the MantraValidator consumes
75
+ // (mirrors src/byan-v2/data/mantras.json). Lets the pre-commit mantra gate
76
+ // score strict artifacts against the strict ruleset instead of the persona one.
77
+ export function renderMantrasData(cfg) {
78
+ return {
79
+ metadata: {
80
+ source: 'byan-sync-rules',
81
+ generated_from: '_byan/_config/strict-mode.yaml',
82
+ count: cfg.mantras.length,
83
+ },
84
+ mantras: cfg.mantras.map((m) => ({
85
+ id: m.id,
86
+ title: m.name,
87
+ description: m.rule.trim(),
88
+ validation: {
89
+ type: 'keyword',
90
+ keywords: m.validation_keywords || [m.name.toLowerCase()],
91
+ required: true,
92
+ },
93
+ priority: m.priority || 'high',
94
+ })),
95
+ };
96
+ }
97
+
98
+ export function renderSkill(cfg) {
99
+ const triggers = cfg.activation.auto_keywords
100
+ .map((k) => `\`${k}\``)
101
+ .join(', ');
102
+ return `---
103
+ name: byan-strict
104
+ description: >-
105
+ ${cfg.description.trim()} Invoke when the user asks for a production
106
+ deliverable, a complete app, a filled contract from a template, or uses
107
+ any activation keyword (${triggers}). Enforces scope-lock, N>=${cfg.self_verify.min_passes}
108
+ self-verify passes, and a 95% confidence floor on hard claims.
109
+ allowed-tools:
110
+ - mcp__byan__byan_strict_lock_scope
111
+ - mcp__byan__byan_strict_self_verify
112
+ - mcp__byan__byan_strict_complete
113
+ - mcp__byan__byan_strict_status
114
+ - mcp__byan__byan_strict_abort
115
+ ---
116
+
117
+ <!-- ${GENERATED_NOTE} -->
118
+
119
+ # BYAN Strict Mode
120
+
121
+ You are operating under BYAN Strict Mode. The user asked for something
122
+ complete. Downgrading the scope is the failure this mode exists to prevent.
123
+
124
+ ## Protocol
125
+
126
+ 1. **Lock the scope** with \`byan_strict_lock_scope\` before building. Provide a
127
+ verbatim restatement of the request and testable \`acceptanceCriteria\`. The
128
+ locked scope is the contract.
129
+ 2. **Build the full scope.** Do not substitute an MVP, a stub, or a simplified
130
+ version. If a part cannot be done, surface it as a gap — do not cut silently.
131
+ 3. **Self-verify at least ${cfg.self_verify.min_passes} times** with
132
+ \`byan_strict_self_verify\`, re-reading the original request each pass. The
133
+ last pass must report verdict \`ok\`.
134
+ 4. **Complete** with \`byan_strict_complete\` to earn the audit token. Without it,
135
+ the pre-commit gate blocks the commit.
136
+
137
+ ## Hard claims
138
+
139
+ Claims in security, performance, or compliance need LEVEL-1 sourcing
140
+ (${cfg.confidence.min_score}%) or they are BLOCKED.
141
+
142
+ ## Mantras
143
+
144
+ ${mantraLines(cfg)}
145
+ `;
146
+ }
147
+
148
+ export function renderAgentsBlock(cfg) {
149
+ return `## BYAN Strict Mode
150
+
151
+ ${cfg.injection.context_banner.trim()}
152
+
153
+ The strict tools (\`byan_strict_lock_scope\`, \`byan_strict_self_verify\`,
154
+ \`byan_strict_complete\`, \`byan_strict_status\`, \`byan_strict_abort\`) are exposed
155
+ by the \`byan\` MCP server. A commit without a fresh, matching audit token is
156
+ blocked by the pre-commit gate.
157
+
158
+ Hard mantras:
159
+
160
+ ${mantraLines(cfg)}`;
161
+ }
162
+
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.
172
+
173
+ Hard mantras:
174
+
175
+ ${mantraLines(cfg)}`;
176
+ }
177
+
178
+ // ---------------------------------------------------------------------------
179
+ // File operations.
180
+ // ---------------------------------------------------------------------------
181
+
182
+ function ensureDir(filePath) {
183
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
184
+ }
185
+
186
+ function writeIfChanged(filePath, content) {
187
+ const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : null;
188
+ if (existing === content) return 'unchanged';
189
+ ensureDir(filePath);
190
+ fs.writeFileSync(filePath, content);
191
+ return existing === null ? 'created' : 'updated';
192
+ }
193
+
194
+ // Insert or replace a block delimited by HTML-comment markers. Preserves
195
+ // everything outside the markers. If the file does not exist, creates it with
196
+ // just the block.
197
+ export function upsertBlock({ filePath, block }) {
198
+ const wrapped = `<!-- ${BEGIN} (${GENERATED_NOTE}) -->\n${block}\n<!-- ${END} -->`;
199
+ const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : null;
200
+
201
+ if (existing === null) {
202
+ ensureDir(filePath);
203
+ fs.writeFileSync(filePath, wrapped + '\n');
204
+ return 'created';
205
+ }
206
+
207
+ const re = new RegExp(
208
+ `<!-- ${BEGIN}[\\s\\S]*?${END} -->`,
209
+ 'm'
210
+ );
211
+ let next;
212
+ if (re.test(existing)) {
213
+ next = existing.replace(re, wrapped);
214
+ } else {
215
+ next = existing.replace(/\s*$/, '') + '\n\n' + wrapped + '\n';
216
+ }
217
+ if (next === existing) return 'unchanged';
218
+ fs.writeFileSync(filePath, next);
219
+ return existing.includes(BEGIN) ? 'updated' : 'appended';
220
+ }
221
+
222
+ // ---------------------------------------------------------------------------
223
+ // Orchestration.
224
+ // ---------------------------------------------------------------------------
225
+
226
+ export function syncRules({ projectRoot, configPath } = {}) {
227
+ const root = resolveRoot(projectRoot);
228
+ const cfg = loadConfig({ projectRoot: root, configPath });
229
+
230
+ const report = {};
231
+
232
+ const skillPath = path.join(root, '.claude', 'skills', 'byan-strict', 'SKILL.md');
233
+ report['.claude/skills/byan-strict/SKILL.md'] = writeIfChanged(skillPath, renderSkill(cfg));
234
+
235
+ const cfgJsonPath = path.join(root, '.claude', 'hooks', 'lib', 'strict-config.json');
236
+ report['.claude/hooks/lib/strict-config.json'] = writeIfChanged(
237
+ cfgJsonPath,
238
+ JSON.stringify(renderStrictConfig(cfg), null, 2) + '\n'
239
+ );
240
+
241
+ const agentsPath = path.join(root, 'AGENTS.md');
242
+ report['AGENTS.md'] = upsertBlock({ filePath: agentsPath, block: renderAgentsBlock(cfg) });
243
+
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
+ const mantrasPath = path.join(root, 'src', 'byan-v2', 'data', 'strict-mantras.json');
251
+ if (fs.existsSync(path.dirname(mantrasPath))) {
252
+ report['src/byan-v2/data/strict-mantras.json'] = writeIfChanged(
253
+ mantrasPath,
254
+ JSON.stringify(renderMantrasData(cfg), null, 2) + '\n'
255
+ );
256
+ }
257
+
258
+ return report;
259
+ }
260
+
261
+ export const MARKERS = { BEGIN, END };
@@ -46,6 +46,30 @@ import {
46
46
  fcParse,
47
47
  } from './lib/cli.js';
48
48
  import { checkForUpdate, formatApplyInstructions } from './lib/update.js';
49
+ import {
50
+ lockScope as strictLockScope,
51
+ selfVerify as strictSelfVerify,
52
+ complete as strictComplete,
53
+ getStatus as strictGetStatus,
54
+ abort as strictAbort,
55
+ checkAuditTrail as strictCheckAuditTrail,
56
+ } from './lib/strict-mode.js';
57
+ import { detectActivation as strictDetectActivation } from './lib/strict-activation.js';
58
+ import {
59
+ pushLock as strictPushLock,
60
+ pushVerify as strictPushVerify,
61
+ pushComplete as strictPushComplete,
62
+ pushAbort as strictPushAbort,
63
+ fetchSession as strictFetchSession,
64
+ syncEnabled as strictSyncEnabled,
65
+ resolveProjectId as strictResolveProjectId,
66
+ } from './lib/strict-sync.js';
67
+
68
+ // Compact view of a best-effort strict-sync result for tool responses.
69
+ function syncResult(sync) {
70
+ if (!sync) return { synced: false, reason: 'no_result' };
71
+ return sync.synced ? { synced: true } : { synced: false, reason: sync.reason || 'unknown' };
72
+ }
49
73
  import { fileURLToPath } from 'node:url';
50
74
 
51
75
  const __filename = fileURLToPath(import.meta.url);
@@ -412,6 +436,11 @@ const tools = [
412
436
  properties: {
413
437
  featureName: { type: 'string', description: 'Short slug for the feature.' },
414
438
  force: { type: 'boolean', description: 'Overwrite an existing in-progress FD.' },
439
+ strict: {
440
+ type: 'boolean',
441
+ description:
442
+ 'Start the FD under BYAN Strict Mode. Records strict_mode=true and signals that the scope must be locked (byan_strict_lock_scope) before BUILD.',
443
+ },
415
444
  },
416
445
  required: ['featureName'],
417
446
  additionalProperties: false,
@@ -475,6 +504,98 @@ const tools = [
475
504
  additionalProperties: false,
476
505
  },
477
506
  },
507
+ {
508
+ name: 'byan_strict_lock_scope',
509
+ description:
510
+ 'Lock a scope for a BYAN Strict Mode session. Records explicit acceptance criteria and allowed paths. Subsequent work is gated against this scope hash. Pass force=true to relock with a different scope (resets self-verify passes).',
511
+ inputSchema: {
512
+ type: 'object',
513
+ properties: {
514
+ scopeText: {
515
+ type: 'string',
516
+ description: 'Description of the scope (≥ 10 chars). Required.',
517
+ },
518
+ acceptanceCriteria: {
519
+ type: 'array',
520
+ items: { type: 'string' },
521
+ description: 'Non-empty array of explicit deliverable criteria.',
522
+ },
523
+ allowedPaths: {
524
+ type: 'array',
525
+ items: { type: 'string' },
526
+ description: 'Glob patterns of paths the agent may modify.',
527
+ },
528
+ force: { type: 'boolean', description: 'Relock with different scope.' },
529
+ projectId: {
530
+ type: 'string',
531
+ description: 'byan_web project id to attach this session to (authority side). Optional; falls back to BYAN_PROJECT_ID env.',
532
+ },
533
+ featureName: {
534
+ type: 'string',
535
+ description: 'Short feature name for the session (e.g. the FD feature slug). Optional.',
536
+ },
537
+ },
538
+ required: ['scopeText', 'acceptanceCriteria'],
539
+ additionalProperties: false,
540
+ },
541
+ },
542
+ {
543
+ name: 'byan_strict_self_verify',
544
+ description:
545
+ 'Record one self-verify pass against the locked scope. verdict="ok" (zero gaps) or "gap" (findings required). Strict mode requires ≥ 3 passes with the final pass returning "ok" before byan_strict_complete can succeed.',
546
+ inputSchema: {
547
+ type: 'object',
548
+ properties: {
549
+ verdict: {
550
+ type: 'string',
551
+ enum: ['ok', 'gap'],
552
+ description: '"ok" = no gap found ; "gap" = gap found, findings required.',
553
+ },
554
+ findings: {
555
+ type: 'array',
556
+ items: { type: 'string' },
557
+ description: 'Array of gap descriptions. Required when verdict="gap".',
558
+ },
559
+ },
560
+ required: ['verdict'],
561
+ additionalProperties: false,
562
+ },
563
+ },
564
+ {
565
+ name: 'byan_strict_complete',
566
+ description:
567
+ 'Mark the strict session complete. Requires scope locked, ≥ 3 self-verify passes, last pass verdict="ok". Returns audit_token used by the pre-commit hook to authorize the commit.',
568
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false },
569
+ },
570
+ {
571
+ name: 'byan_strict_status',
572
+ description:
573
+ 'Return current strict mode state : scope_locked, scope_hash, acceptance_criteria, pass_count, min_passes, completed, audit_token.',
574
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false },
575
+ },
576
+ {
577
+ name: 'byan_strict_abort',
578
+ description:
579
+ 'Abort the current strict session. Marks inactive in state.json and appends abort entry to audit.log. State preserved for inspection.',
580
+ inputSchema: {
581
+ type: 'object',
582
+ properties: { reason: { type: 'string' } },
583
+ additionalProperties: false,
584
+ },
585
+ },
586
+ {
587
+ name: 'byan_strict_suggest',
588
+ description:
589
+ '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.',
590
+ inputSchema: {
591
+ type: 'object',
592
+ properties: {
593
+ text: { type: 'string', description: 'The request or feature description to scan.' },
594
+ },
595
+ required: ['text'],
596
+ additionalProperties: false,
597
+ },
598
+ },
478
599
  {
479
600
  name: 'byan_review_request',
480
601
  description:
@@ -1175,7 +1296,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1175
1296
  }
1176
1297
 
1177
1298
  if (name === 'byan_fd_start') {
1178
- const state = fdStart({ featureName: args.featureName, force: args.force });
1299
+ const state = fdStart({ featureName: args.featureName, force: args.force, strict: args.strict });
1179
1300
  return { content: [{ type: 'text', text: JSON.stringify(state, null, 2) }] };
1180
1301
  }
1181
1302
 
@@ -1199,6 +1320,91 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1199
1320
  return { content: [{ type: 'text', text: JSON.stringify(state, null, 2) }] };
1200
1321
  }
1201
1322
 
1323
+ if (name === 'byan_strict_lock_scope') {
1324
+ const r = strictLockScope({
1325
+ scopeText: args.scopeText,
1326
+ acceptanceCriteria: args.acceptanceCriteria,
1327
+ allowedPaths: args.allowedPaths,
1328
+ force: args.force,
1329
+ });
1330
+ const st = strictGetStatus();
1331
+ // Attach to a byan_web project: explicit arg, else env, else resolve from
1332
+ // the active FD project_context (best-effort; null degrades to user-scoped).
1333
+ let projectId = args.projectId || process.env.BYAN_PROJECT_ID || null;
1334
+ let featureName = args.featureName || null;
1335
+ if (strictSyncEnabled()) {
1336
+ try {
1337
+ const fd = fdStatus();
1338
+ const pc = fd && fd.project_context;
1339
+ if (pc) {
1340
+ if (!projectId && (pc.slug || pc.name)) {
1341
+ projectId = await strictResolveProjectId({ slug: pc.slug, name: pc.name });
1342
+ }
1343
+ if (!featureName && fd.feature_name) featureName = fd.feature_name;
1344
+ }
1345
+ } catch {
1346
+ // FD context unavailable — stay user-scoped.
1347
+ }
1348
+ }
1349
+ const sync = await strictPushLock({
1350
+ sessionId: st.strict_session_id,
1351
+ scopeLock: r,
1352
+ projectId,
1353
+ featureName,
1354
+ });
1355
+ return { content: [{ type: 'text', text: JSON.stringify({ ...r, project_id: projectId, sync: syncResult(sync) }, null, 2) }] };
1356
+ }
1357
+
1358
+ if (name === 'byan_strict_self_verify') {
1359
+ const r = strictSelfVerify({
1360
+ verdict: args.verdict,
1361
+ findings: args.findings || [],
1362
+ });
1363
+ const st = strictGetStatus();
1364
+ const lastPass = (st.passes || [])[st.passes.length - 1];
1365
+ const sync = await strictPushVerify({ sessionId: st.strict_session_id, pass: lastPass });
1366
+ return { content: [{ type: 'text', text: JSON.stringify({ ...r, sync: syncResult(sync) }, null, 2) }] };
1367
+ }
1368
+
1369
+ if (name === 'byan_strict_complete') {
1370
+ const r = strictComplete();
1371
+ const st = strictGetStatus();
1372
+ const sync = await strictPushComplete({
1373
+ sessionId: st.strict_session_id,
1374
+ auditToken: r.audit_token,
1375
+ completedAt: r.completed_at,
1376
+ });
1377
+ return { content: [{ type: 'text', text: JSON.stringify({ ...r, sync: syncResult(sync) }, null, 2) }] };
1378
+ }
1379
+
1380
+ if (name === 'byan_strict_status') {
1381
+ const local = strictGetStatus();
1382
+ // The API is the authority. When a session exists and the API answers,
1383
+ // surface its record; otherwise fall back to the local mirror (offline).
1384
+ let authority = 'local';
1385
+ let r = local;
1386
+ if (local.strict_session_id && strictSyncEnabled()) {
1387
+ const remote = await strictFetchSession({ sessionId: local.strict_session_id });
1388
+ if (remote.ok && remote.data) {
1389
+ authority = 'api';
1390
+ r = { ...local, api: remote.data };
1391
+ }
1392
+ }
1393
+ return { content: [{ type: 'text', text: JSON.stringify({ ...r, authority }, null, 2) }] };
1394
+ }
1395
+
1396
+ if (name === 'byan_strict_abort') {
1397
+ const st = strictGetStatus();
1398
+ const r = strictAbort({ reason: args.reason });
1399
+ const sync = await strictPushAbort({ sessionId: st.strict_session_id, reason: args.reason });
1400
+ return { content: [{ type: 'text', text: JSON.stringify({ ...r, sync: syncResult(sync) }, null, 2) }] };
1401
+ }
1402
+
1403
+ if (name === 'byan_strict_suggest') {
1404
+ const r = strictDetectActivation({ text: args.text });
1405
+ return { content: [{ type: 'text', text: JSON.stringify(r, null, 2) }] };
1406
+ }
1407
+
1202
1408
  if (name === 'byan_review_request') {
1203
1409
  const r = requestReview({
1204
1410
  task_id: args.task_id,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-byan-agent",
3
- "version": "2.16.1",
3
+ "version": "2.17.0",
4
4
  "description": "BYAN v2.8 - Intelligent AI agent creator with ELO trust system + scientific fact-check + Hermes universal dispatcher + native Claude Code integration (hooks, skills, MCP server). Multi-platform (Copilot CLI, Claude Code, Codex). Merise Agile + TDD + 64 Mantras. ~54% LLM cost savings.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -18,7 +18,11 @@
18
18
  "test:e2e": "jest __tests__/e2e-install-update.test.js",
19
19
  "setup-turbo-whisper": "node install/setup-turbo-whisper.js",
20
20
  "byan": "echo \"BYAN agent installed. Use: copilot and type /agent\"",
21
- "version": "node scripts/sync-install-version.js && git add install/package.json"
21
+ "version": "node scripts/sync-install-version.js && git add install/package.json",
22
+ "app:dev": "npm --prefix app run dev",
23
+ "app:build": "npm --prefix app run build",
24
+ "app:typecheck": "npm --prefix app run typecheck",
25
+ "app:install": "npm --prefix app install"
22
26
  },
23
27
  "keywords": [
24
28
  "byan",