create-byan-agent 2.16.1 → 2.17.1
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 +78 -0
- package/README.md +24 -0
- package/install/lib/claude-native-setup.js +37 -0
- package/install/package.json +1 -1
- package/install/packages/platform-config/lib/validate.js +0 -14
- package/install/src/webui/api.js +6 -0
- package/install/src/webui/server.js +8 -1
- package/install/templates/.claude/CLAUDE.md +18 -0
- package/install/templates/.claude/hooks/lib/strict-config.json +46 -0
- package/install/templates/.claude/hooks/lib/strict-runtime.js +82 -0
- package/install/templates/.claude/hooks/strict-context-inject.js +86 -0
- package/install/templates/.claude/hooks/strict-scope-guard.js +101 -0
- package/install/templates/.claude/hooks/strict-stop-guard.js +100 -0
- package/install/templates/.claude/rules/strict-mode.md +166 -0
- package/install/templates/.claude/settings.json +12 -0
- package/install/templates/.claude/skills/byan-strict/SKILL.md +54 -0
- package/install/templates/.githooks/pre-commit +15 -0
- package/install/templates/_byan/_config/strict-mode.yaml +258 -0
- package/install/templates/_byan/mcp/byan-mcp-server/bin/byan-sync-rules.js +24 -0
- package/install/templates/_byan/mcp/byan-mcp-server/bin/strict-precommit-gate.js +21 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/fd-state.js +2 -1
- package/install/templates/_byan/mcp/byan-mcp-server/lib/precommit-gate.js +120 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/strict-activation.js +76 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/strict-mode.js +391 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/strict-sync.js +140 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/sync-rules.js +261 -0
- package/install/templates/_byan/mcp/byan-mcp-server/server.js +207 -1
- package/package.json +6 -2
- package/src/byan-v2/data/strict-mantras.json +188 -0
- 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.
|
|
3
|
+
"version": "2.17.1",
|
|
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",
|