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.
- 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
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,84 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [2.17.0] - 2026-05-27
|
|
11
|
+
|
|
12
|
+
### Added - BYAN Strict Mode shipped to npm + byan_web persistence
|
|
13
|
+
|
|
14
|
+
Anti-downgrade enforcement now packaged for `npx create-byan-agent` and backed by the byan_web API.
|
|
15
|
+
|
|
16
|
+
#### Strict Mode distribution
|
|
17
|
+
|
|
18
|
+
- Mirrored the full strict feature into `install/templates/` so a fresh install ships it: MCP tools (`byan_strict_*`), Claude Code hooks (Stop / PreToolUse / UserPromptSubmit), the `byan-strict` skill, `strict-mode.yaml`, and the generated runtime config.
|
|
19
|
+
- `settings.json` template now registers the three strict hooks.
|
|
20
|
+
- Installer wires the cross-platform pre-commit gate: copies `.githooks/` and sets `core.hooksPath` when the target is a git repo (`claude-native-setup.js`).
|
|
21
|
+
|
|
22
|
+
#### Server-side persistence (API authority)
|
|
23
|
+
|
|
24
|
+
- byan_web migration `033-strict-sessions.sql` + `routes/strict-sessions.js` (POST lock/upsert, PATCH verify/complete/abort, GET list + by id), scoped to the API key user with optional project attachment.
|
|
25
|
+
- New `lib/strict-sync.js` isolates network I/O: each local mutation pushes best-effort to the API; `byan_strict_status` and the pre-commit gate consult the API first and fall back to the local mirror when it is unreachable.
|
|
26
|
+
- `.mcp.json` carries `BYAN_API_TOKEN` via env (no secret committed).
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## [2.16.2] - 2026-05-02
|
|
31
|
+
|
|
32
|
+
### Added - Electron desktop app v1.0 (Linux + Windows)
|
|
33
|
+
|
|
34
|
+
Commits ea7abf3 + e905bee. App lives in `app/` as a standalone package (not a workspace of the root); builds and publishes independently of `create-byan-agent`.
|
|
35
|
+
|
|
36
|
+
#### Core shell and security (F1, F2, F12)
|
|
37
|
+
|
|
38
|
+
- **F1 app shell** — Electron main process in TypeScript (`app/main/`), compiled to `dist/main/`. Window lifecycle, splash, tray (Linux/Win only).
|
|
39
|
+
- **F2 IPC contract** — Preload bridge via `contextBridge` (`app/preload/`). All renderer-to-Node calls go through typed IPC channels; `nodeIntegration: false`, `contextIsolation: true`, `sandbox: true`.
|
|
40
|
+
- **F12 strict CSP** — Content Security Policy header injected by main process; default-src self, no inline scripts, no eval.
|
|
41
|
+
|
|
42
|
+
#### Local server lifecycle and renderer (F3, F13)
|
|
43
|
+
|
|
44
|
+
- **F3 lifecycle** — Main process spawns and supervises the existing `install/src/webui/server.js` local server on a free port; emits `server-ready` IPC event to renderer.
|
|
45
|
+
- **F13 dev hot reload** — `npm run dev` runs renderer (Vite dev server, port 5173), main watcher (`tsc -w`), and preload watcher concurrently via `concurrently`; `BYAN_DEV=1` env flag switches main to load the Vite URL instead of `dist/renderer/index.html`.
|
|
46
|
+
|
|
47
|
+
#### Authentication (F5)
|
|
48
|
+
|
|
49
|
+
- **F5 hybrid login** — Three login modes selectable at runtime: cloud (`byan.acadenice.fr`), local (auto-detected server), custom URL. Mode persisted in app config; switchable from the native menu.
|
|
50
|
+
|
|
51
|
+
#### Onboarding (F4)
|
|
52
|
+
|
|
53
|
+
- **F4 5-step onboarding** — Welcome -> Platform detection -> Config preview -> Apply -> Done. Covers Linux and Windows; macOS branch present but gated (F21 deferred). Onboarding state persisted via Electron store; skipped on subsequent launches.
|
|
54
|
+
|
|
55
|
+
#### Secure storage (F6)
|
|
56
|
+
|
|
57
|
+
- **F6 keytar** — API tokens stored via `keytar` (libsecret on Linux, Credential Manager on Windows). Token stored in the OS keychain, not in plaintext config files; IPC `get-token` / `set-token` channels exposed through preload only.
|
|
58
|
+
|
|
59
|
+
#### Native integration (F19)
|
|
60
|
+
|
|
61
|
+
- **F19 native menu** — Application menu built with `Menu.buildFromTemplate`; entries: File (quit), Edit (cut/copy/paste/select-all), View (reload, devtools in dev mode), Help (about). Consistent on Linux and Windows.
|
|
62
|
+
|
|
63
|
+
#### Cross-platform build (F10)
|
|
64
|
+
|
|
65
|
+
- **F10 build** — `npm run build` compiles main + preload (TypeScript) and bundles renderer (Vite). `npm run build:linux` produces AppImage + deb via electron-builder. `npm run build:win` produces NSIS installer via cross-compilation (Wine on Linux CI or native Windows runner).
|
|
66
|
+
|
|
67
|
+
#### CI matrix (F11, P1)
|
|
68
|
+
|
|
69
|
+
- **F11 + P1 GitHub Actions matrix** — Workflow `.github/workflows/electron-ci.yml` runs on `ubuntu-latest` (Linux build + unit tests) and `windows-latest` (Windows build + unit tests) in parallel. Draft GitHub Release created automatically when a `v*` tag is pushed; AppImage, deb, and NSIS installer attached as artifacts.
|
|
70
|
+
|
|
71
|
+
#### Test suite (F18)
|
|
72
|
+
|
|
73
|
+
- **F18 E2E Playwright** — Playwright suite in `app/__tests__/` using `playwright-electron`; covers: app launch, onboarding flow, login modal, token store round-trip, native menu visibility. `npm run test:e2e` runs the full suite headlessly.
|
|
74
|
+
|
|
75
|
+
### Changed
|
|
76
|
+
|
|
77
|
+
- `install/src/webui/server.js` and `install/src/webui/api.js` — minor edits to support port-injection from the Electron main process (F3: server accepts `BYAN_PORT` env var, binds to `127.0.0.1` only, emits a ready signal to stdout that main process parses).
|
|
78
|
+
|
|
79
|
+
### Notes
|
|
80
|
+
|
|
81
|
+
- **F21 macOS** — deferred; code branch exists, not tested, no CI runner. Target: v1.1.
|
|
82
|
+
- **F9 auto-update** — electron-updater integration deferred to P2 (v1.1). Update check menu item is present but inert.
|
|
83
|
+
- **F14 MCP control panel** — deferred to P2 (v1.1).
|
|
84
|
+
- **First CI run** — push tag `v0.1.0-rc` to trigger the first draft release and validate artifact upload end-to-end before promoting to `v1.0.0`.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
10
88
|
## [2.9.10] - 2026-04-21
|
|
11
89
|
|
|
12
90
|
### Fixed - MCP auth scheme wrong for byan_web API keys
|
package/README.md
CHANGED
|
@@ -428,6 +428,29 @@ Domaines stricts : `security` / `performance` / `compliance` → LEVEL-2 minimum
|
|
|
428
428
|
|
|
429
429
|
---
|
|
430
430
|
|
|
431
|
+
## BYAN Strict Mode — Anti-Downgrade
|
|
432
|
+
|
|
433
|
+
Mode d'enforcement qui empêche l'agent de livrer moins que demandé (un MVP au
|
|
434
|
+
lieu de l'app prod, un stub au lieu de la feature, un template baclé). Actif sur
|
|
435
|
+
les **3 plateformes** : Claude Code, Codex, GitHub Copilot.
|
|
436
|
+
|
|
437
|
+
```
|
|
438
|
+
1. Lock du scope byan_strict_lock_scope (scope verbatim + critères testables)
|
|
439
|
+
2. Build complet pas de MVP, pas de stub, tout gap est signalé
|
|
440
|
+
3. Self-verify >= 3x byan_strict_self_verify (relit la demande initiale)
|
|
441
|
+
4. Complete byan_strict_complete (jeton d'audit)
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
Le commit est **bloqué** par un filet pre-commit tant que la session strict
|
|
445
|
+
engagée n'est pas complétée correctement — y compris pour Codex et Copilot qui
|
|
446
|
+
n'ont pas de hook in-session. Source de vérité unique :
|
|
447
|
+
`_byan/_config/strict-mode.yaml`, régénérée via `byan-sync-rules`.
|
|
448
|
+
|
|
449
|
+
Activation : `byan_fd_start strict:true`, skill `byan-strict`, ou auto-détection
|
|
450
|
+
sur mots-clés (`prod`, `client`, `livrable`, `contrat`, `release`...).
|
|
451
|
+
|
|
452
|
+
---
|
|
453
|
+
|
|
431
454
|
## Workflows Principaux
|
|
432
455
|
|
|
433
456
|
| Workflow | Description | Agent principal |
|
|
@@ -443,6 +466,7 @@ Domaines stricts : `security` / `performance` / `compliance` → LEVEL-2 minimum
|
|
|
443
466
|
| `testarch-atdd` | Générer des tests ATDD avant implémentation | tea |
|
|
444
467
|
| `fact-check` | Analyser une assertion ou un document | fact-checker |
|
|
445
468
|
| `elo-workflow` | Consulter et gérer le score de confiance ELO | byan |
|
|
469
|
+
| `byan-sync-rules` | Régénérer les artefacts du mode strict (3 plateformes) | byan |
|
|
446
470
|
|
|
447
471
|
---
|
|
448
472
|
|
|
@@ -131,6 +131,35 @@ async function installMcpDependencies(mcpServerPath) {
|
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
async function copyGitHooks(projectRoot) {
|
|
135
|
+
// The BYAN Strict Mode pre-commit gate is the cross-platform final net
|
|
136
|
+
// (Codex/Copilot have no in-session hook). Install it whenever the project
|
|
137
|
+
// is a git repo: copy .githooks/ and point core.hooksPath at it.
|
|
138
|
+
const src = path.join(TEMPLATE_ROOT, '.githooks');
|
|
139
|
+
const dst = path.join(projectRoot, '.githooks');
|
|
140
|
+
if (!(await fs.pathExists(src))) return { copied: false, reason: 'no_template' };
|
|
141
|
+
await fs.copy(src, dst, { overwrite: true });
|
|
142
|
+
|
|
143
|
+
const preCommit = path.join(dst, 'pre-commit');
|
|
144
|
+
if (await fs.pathExists(preCommit)) {
|
|
145
|
+
try { await fs.chmod(preCommit, 0o755); } catch { /* non-fatal */ }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Only wire core.hooksPath when this is actually a git repo.
|
|
149
|
+
if (!(await fs.pathExists(path.join(projectRoot, '.git')))) {
|
|
150
|
+
return { copied: true, hooksPath: false, reason: 'not_a_git_repo' };
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
execSync('git config core.hooksPath .githooks', {
|
|
154
|
+
cwd: projectRoot,
|
|
155
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
156
|
+
});
|
|
157
|
+
return { copied: true, hooksPath: true };
|
|
158
|
+
} catch (err) {
|
|
159
|
+
return { copied: true, hooksPath: false, error: err.message || String(err) };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
134
163
|
async function setupClaudeNative(projectRoot, options = {}) {
|
|
135
164
|
const log = options.quiet ? () => {} : (...a) => console.log(...a);
|
|
136
165
|
const results = {};
|
|
@@ -158,6 +187,13 @@ async function setupClaudeNative(projectRoot, options = {}) {
|
|
|
158
187
|
results.mcpConfig = await generateMcpConfig(projectRoot, options);
|
|
159
188
|
log(chalk.green(` ✓ .mcp.json generated (absolute path)`));
|
|
160
189
|
|
|
190
|
+
results.gitHooks = await copyGitHooks(projectRoot);
|
|
191
|
+
if (results.gitHooks.copied && results.gitHooks.hooksPath) {
|
|
192
|
+
log(chalk.green(` ✓ Strict pre-commit gate wired (.githooks + core.hooksPath)`));
|
|
193
|
+
} else if (results.gitHooks.copied) {
|
|
194
|
+
log(chalk.yellow(` ⚠ .githooks copied but not wired (${results.gitHooks.reason || 'no git repo'}); run: git config core.hooksPath .githooks`));
|
|
195
|
+
}
|
|
196
|
+
|
|
161
197
|
if (results.mcp.copied && options.installDeps !== false) {
|
|
162
198
|
results.mcpDeps = await installMcpDependencies(results.mcp.path);
|
|
163
199
|
if (results.mcpDeps.installed) {
|
|
@@ -187,6 +223,7 @@ module.exports = {
|
|
|
187
223
|
copyClaudeSkills,
|
|
188
224
|
copyClaudeSettings,
|
|
189
225
|
copyMcpServer,
|
|
226
|
+
copyGitHooks,
|
|
190
227
|
makeNodeModulesFilter,
|
|
191
228
|
generateMcpConfig,
|
|
192
229
|
installMcpDependencies,
|
package/install/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-byan-agent",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.17.0",
|
|
4
4
|
"description": "BYAN v2.2.2 - Intelligent AI agent installer with multi-platform native support (GitHub Copilot CLI, Claude Code, Codex/OpenCode)",
|
|
5
5
|
"bin": {
|
|
6
6
|
"create-byan-agent": "bin/create-byan-agent-v2.js"
|
|
@@ -24,20 +24,6 @@ async function validateByanWebReachability({ apiUrl, token, timeoutMs = 5000 })
|
|
|
24
24
|
const latencyMs = Date.now() - t0;
|
|
25
25
|
clearTimeout(timer);
|
|
26
26
|
|
|
27
|
-
const ct = (res.headers && typeof res.headers.get === 'function'
|
|
28
|
-
? res.headers.get('content-type')
|
|
29
|
-
: '') || '';
|
|
30
|
-
const lowerCt = ct.toLowerCase();
|
|
31
|
-
|
|
32
|
-
if (lowerCt.includes('text/html')) {
|
|
33
|
-
return {
|
|
34
|
-
reachable: false,
|
|
35
|
-
status: res.status,
|
|
36
|
-
latencyMs,
|
|
37
|
-
error: 'Response is HTML — BYAN_API_URL likely points at the WebUI (behind SSO) instead of the API backend. Try byan-api.<domain> without /api suffix.',
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
27
|
if (res.status >= 200 && res.status < 400) {
|
|
42
28
|
return { reachable: true, status: res.status, latencyMs };
|
|
43
29
|
}
|
package/install/src/webui/api.js
CHANGED
|
@@ -84,6 +84,12 @@ function detectPlatforms(projectRoot) {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
const routes = {
|
|
87
|
+
// Lightweight health-check — used by Electron LocalServer health-ping every 5s.
|
|
88
|
+
'GET health': async (req, res) => {
|
|
89
|
+
res.writeHead(200);
|
|
90
|
+
res.end(JSON.stringify({ ok: true }));
|
|
91
|
+
},
|
|
92
|
+
|
|
87
93
|
'GET status': async (req, res, server) => {
|
|
88
94
|
const projectRoot = server.projectRoot;
|
|
89
95
|
const version = readPackageVersion();
|
|
@@ -51,8 +51,15 @@ class ByanWebUI {
|
|
|
51
51
|
|
|
52
52
|
return new Promise((resolve) => {
|
|
53
53
|
this.server.listen(this.port, () => {
|
|
54
|
-
const
|
|
54
|
+
const addr = this.server.address();
|
|
55
|
+
const assignedPort = (addr && typeof addr === 'object') ? addr.port : this.port;
|
|
56
|
+
const url = `http://localhost:${assignedPort}`;
|
|
55
57
|
console.log(`BYAN WebUI running at ${url}`);
|
|
58
|
+
// Notify parent Electron process (F3 LocalServer) of the assigned port.
|
|
59
|
+
// process.send exists only when forked via child_process.fork().
|
|
60
|
+
if (typeof process.send === 'function') {
|
|
61
|
+
process.send({ type: 'ready', port: assignedPort });
|
|
62
|
+
}
|
|
56
63
|
this.openBrowser(url);
|
|
57
64
|
resolve(this);
|
|
58
65
|
});
|
|
@@ -44,6 +44,7 @@ Voir @.claude/rules/hermes-dispatcher.md pour les commandes Hermes.
|
|
|
44
44
|
- Methodologie: voir @.claude/rules/merise-agile.md
|
|
45
45
|
- Systeme de confiance epistemique: voir @.claude/rules/elo-trust.md
|
|
46
46
|
- Protocol fact-check scientifique: voir @.claude/rules/fact-check.md
|
|
47
|
+
- Mode strict anti-downgrade: voir @.claude/rules/strict-mode.md
|
|
47
48
|
- Systeme API byan_web: voir @.claude/rules/byan-api.md
|
|
48
49
|
|
|
49
50
|
## API byan_web
|
|
@@ -73,3 +74,20 @@ Domaines stricts : security/performance/compliance → LEVEL-2 minimum sinon BLO
|
|
|
73
74
|
|
|
74
75
|
Agent dédié: `@fact-checker` — analyse assertions, audits de documents, chaines de raisonnement.
|
|
75
76
|
Dans BYAN: tapez `[FC]` pour le sous-menu fact-check.
|
|
77
|
+
|
|
78
|
+
## BYAN Strict Mode
|
|
79
|
+
|
|
80
|
+
Mode d'enforcement anti-downgrade : empeche l'agent de livrer moins que demande
|
|
81
|
+
(MVP au lieu de prod, stub au lieu de feature, template bacle). Fonctionne sur
|
|
82
|
+
les 3 plateformes (Claude Code, Codex, Copilot).
|
|
83
|
+
|
|
84
|
+
Protocole : lock du scope -> build complet -> self-verify >= 3 passes -> complete
|
|
85
|
+
(jeton d'audit). Le commit est bloque tant que la verification n'est pas acquise.
|
|
86
|
+
|
|
87
|
+
- Source de verite : `_byan/_config/strict-mode.yaml` (regenerer via `byan-sync-rules`)
|
|
88
|
+
- Outils MCP : `byan_strict_lock_scope`, `byan_strict_self_verify`, `byan_strict_complete`, `byan_strict_status`, `byan_strict_abort`, `byan_strict_suggest`
|
|
89
|
+
- Activation : `byan_fd_start strict:true`, skill `byan-strict`, ou mots-cles (prod, client, livrable...)
|
|
90
|
+
- Filet final : `.githooks/pre-commit` bloque le commit si une session strict est engagee mais non completee
|
|
91
|
+
- Persistance : sessions poussees vers l'API byan_web (autorite ; local = miroir/fallback offline)
|
|
92
|
+
|
|
93
|
+
Detail complet : voir @.claude/rules/strict-mode.md
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_generated_by": "byan-sync-rules",
|
|
3
|
+
"version": 1,
|
|
4
|
+
"min_passes": 3,
|
|
5
|
+
"last_verdict_must_be": "ok",
|
|
6
|
+
"min_score": 95,
|
|
7
|
+
"auto_keywords": [
|
|
8
|
+
"prod",
|
|
9
|
+
"production",
|
|
10
|
+
"client",
|
|
11
|
+
"contrat",
|
|
12
|
+
"template officiel",
|
|
13
|
+
"livrable",
|
|
14
|
+
"deliverable",
|
|
15
|
+
"mise en production",
|
|
16
|
+
"release"
|
|
17
|
+
],
|
|
18
|
+
"completion_claim_markers": [
|
|
19
|
+
"done",
|
|
20
|
+
"finished",
|
|
21
|
+
"complete",
|
|
22
|
+
"delivered",
|
|
23
|
+
"ready",
|
|
24
|
+
"shipped",
|
|
25
|
+
"terminé",
|
|
26
|
+
"fini",
|
|
27
|
+
"livré",
|
|
28
|
+
"prêt",
|
|
29
|
+
"c'est bon",
|
|
30
|
+
"voilà"
|
|
31
|
+
],
|
|
32
|
+
"scope_guard": {
|
|
33
|
+
"enforce_paths": true,
|
|
34
|
+
"exempt_globs": [
|
|
35
|
+
".byan-strict/",
|
|
36
|
+
"_byan-output/",
|
|
37
|
+
".git/"
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
"freshness_window_seconds": 600,
|
|
41
|
+
"banners": {
|
|
42
|
+
"context": "[STRICT MODE ACTIVE]\nYou are under BYAN Strict Mode. Before building:\n 1. Lock the scope (byan_strict_lock_scope) with testable acceptance criteria.\nWhile building:\n 2. Do not downgrade the scope. Surface any gap, do not cut silently.\nBefore delivering:\n 3. Run >= 3 self-verify passes (byan_strict_self_verify), re-reading the\n original request each time. The last pass must report verdict \"ok\".\n 4. Call byan_strict_complete to earn the audit token.\nHard claims (security/performance/compliance) require LEVEL-1 sourcing (95%).\nA commit without a fresh, matching audit token is blocked by the pre-commit gate.\n",
|
|
43
|
+
"stop_block": "Strict mode: the turn cannot end. The locked scope has not passed three self-verify passes with a final \"ok\" verdict. Run byan_strict_self_verify until the scope is satisfied, then byan_strict_complete.",
|
|
44
|
+
"scope_deny": "Strict mode: this write targets a path outside the locked scope. Either it belongs to the scope (re-lock with the corrected paths) or it does not (do not write it)."
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Shared runtime helpers for the BYAN Strict Mode hooks.
|
|
2
|
+
//
|
|
3
|
+
// Reads two files :
|
|
4
|
+
// - .claude/hooks/lib/strict-config.json : generated from strict-mode.yaml
|
|
5
|
+
// by byan-sync-rules (static config : thresholds, keywords, banners).
|
|
6
|
+
// - .byan-strict/state.json : the live session state written by the
|
|
7
|
+
// byan_strict_* MCP tools (lib/strict-mode.js).
|
|
8
|
+
//
|
|
9
|
+
// Hooks only READ here. The authoritative writes live in the MCP tools.
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
function projectRoot() {
|
|
15
|
+
return process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function readJson(filePath) {
|
|
19
|
+
try {
|
|
20
|
+
if (!fs.existsSync(filePath)) return null;
|
|
21
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function loadConfig() {
|
|
28
|
+
const p = path.join(projectRoot(), '.claude', 'hooks', 'lib', 'strict-config.json');
|
|
29
|
+
return readJson(p);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function loadState() {
|
|
33
|
+
const p = path.join(projectRoot(), '.byan-strict', 'state.json');
|
|
34
|
+
return readJson(p);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// A strict session is "engaged" when it is active, has a locked scope, and
|
|
38
|
+
// has not been completed yet. This is the window where enforcement applies.
|
|
39
|
+
function isEngaged(state) {
|
|
40
|
+
return Boolean(state && state.active && state.scope_lock && !state.completed);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function passCount(state) {
|
|
44
|
+
return state && Array.isArray(state.self_verify_passes)
|
|
45
|
+
? state.self_verify_passes.length
|
|
46
|
+
: 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function lastVerdict(state) {
|
|
50
|
+
const passes = state && state.self_verify_passes;
|
|
51
|
+
if (!Array.isArray(passes) || passes.length === 0) return null;
|
|
52
|
+
return passes[passes.length - 1].verdict;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function readStdin() {
|
|
56
|
+
return new Promise((resolve) => {
|
|
57
|
+
if (process.stdin.isTTY) return resolve('');
|
|
58
|
+
let data = '';
|
|
59
|
+
process.stdin.on('data', (c) => (data += c));
|
|
60
|
+
process.stdin.on('end', () => resolve(data));
|
|
61
|
+
process.stdin.on('error', () => resolve(data));
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function parseJson(raw) {
|
|
66
|
+
try {
|
|
67
|
+
return raw ? JSON.parse(raw) : {};
|
|
68
|
+
} catch {
|
|
69
|
+
return {};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = {
|
|
74
|
+
projectRoot,
|
|
75
|
+
loadConfig,
|
|
76
|
+
loadState,
|
|
77
|
+
isEngaged,
|
|
78
|
+
passCount,
|
|
79
|
+
lastVerdict,
|
|
80
|
+
readStdin,
|
|
81
|
+
parseJson,
|
|
82
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* UserPromptSubmit hook — BYAN Strict Mode context injector.
|
|
4
|
+
*
|
|
5
|
+
* Two behaviors :
|
|
6
|
+
* - When a strict session is engaged, inject the strict banner plus a live
|
|
7
|
+
* status line (passes done / required, locked scope hash) so the agent
|
|
8
|
+
* stays anchored to the contract on every turn.
|
|
9
|
+
* - When no session is engaged but the user's prompt contains an activation
|
|
10
|
+
* keyword (prod, production, client, contrat, ...), inject a suggestion to
|
|
11
|
+
* lock strict mode before building. It suggests, it does not auto-lock.
|
|
12
|
+
*
|
|
13
|
+
* Emits empty context on any error.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const {
|
|
17
|
+
loadConfig,
|
|
18
|
+
loadState,
|
|
19
|
+
isEngaged,
|
|
20
|
+
passCount,
|
|
21
|
+
lastVerdict,
|
|
22
|
+
readStdin,
|
|
23
|
+
parseJson,
|
|
24
|
+
} = require('./lib/strict-runtime');
|
|
25
|
+
|
|
26
|
+
function findKeyword(prompt, keywords) {
|
|
27
|
+
if (!prompt || !Array.isArray(keywords)) return null;
|
|
28
|
+
const lower = prompt.toLowerCase();
|
|
29
|
+
for (const k of keywords) {
|
|
30
|
+
const kw = String(k).toLowerCase();
|
|
31
|
+
if (/^[a-z]+$/.test(kw)) {
|
|
32
|
+
if (new RegExp(`\\b${kw}\\b`).test(lower)) return k;
|
|
33
|
+
} else if (lower.includes(kw)) {
|
|
34
|
+
return k;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Pure : returns the additionalContext string (possibly empty).
|
|
41
|
+
function buildContext({ state, config, prompt }) {
|
|
42
|
+
if (isEngaged(state)) {
|
|
43
|
+
const minPasses = (config && config.min_passes) || 3;
|
|
44
|
+
const banner = (config && config.banners && config.banners.context) || '[STRICT MODE ACTIVE]';
|
|
45
|
+
const done = passCount(state);
|
|
46
|
+
const hash = state.scope_lock ? state.scope_lock.scope_hash : 'unknown';
|
|
47
|
+
return (
|
|
48
|
+
`${banner}\n` +
|
|
49
|
+
`Locked scope: ${hash} | self-verify ${done}/${minPasses} | last verdict ${lastVerdict(state) || 'none'}.\n` +
|
|
50
|
+
`Stay inside the locked scope. Do not declare done before byan_strict_complete returns an audit token.`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const keyword = findKeyword(prompt, config && config.auto_keywords);
|
|
55
|
+
if (keyword) {
|
|
56
|
+
return (
|
|
57
|
+
`[STRICT MODE SUGGESTED]\n` +
|
|
58
|
+
`The request mentions "${keyword}", which signals a production-grade deliverable. ` +
|
|
59
|
+
`Before building, consider locking strict mode with byan_strict_lock_scope ` +
|
|
60
|
+
`(verbatim scope + testable acceptance criteria). Strict mode enforces ` +
|
|
61
|
+
`>= ${(config && config.min_passes) || 3} self-verify passes and a 95% confidence floor on hard claims. ` +
|
|
62
|
+
`Confirm with the user, then lock.`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return '';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (require.main === module) {
|
|
70
|
+
(async () => {
|
|
71
|
+
const state = loadState();
|
|
72
|
+
const config = loadConfig();
|
|
73
|
+
const payload = parseJson(await readStdin());
|
|
74
|
+
const prompt = payload.prompt || payload.user_prompt || payload.userPrompt || '';
|
|
75
|
+
|
|
76
|
+
const additionalContext = buildContext({ state, config, prompt });
|
|
77
|
+
process.stdout.write(
|
|
78
|
+
JSON.stringify({
|
|
79
|
+
hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext },
|
|
80
|
+
})
|
|
81
|
+
);
|
|
82
|
+
process.exit(0);
|
|
83
|
+
})();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = { buildContext, findKeyword };
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* PreToolUse hook — BYAN Strict Mode scope guard.
|
|
4
|
+
*
|
|
5
|
+
* When a strict session is engaged and the locked scope declares allowed
|
|
6
|
+
* paths, deny Write/Edit calls that target a file outside those paths. This
|
|
7
|
+
* keeps the agent inside the contract it locked : it cannot silently spread
|
|
8
|
+
* changes across the repo under the cover of the locked task.
|
|
9
|
+
*
|
|
10
|
+
* Exempt paths (the strict bookkeeping, build output, git) are always
|
|
11
|
+
* allowed. If enforce_paths is off or no allowed paths were declared, every
|
|
12
|
+
* write is allowed.
|
|
13
|
+
*
|
|
14
|
+
* Non-blocking on parse error.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const { loadConfig, loadState, isEngaged, projectRoot, readStdin, parseJson } =
|
|
19
|
+
require('./lib/strict-runtime');
|
|
20
|
+
|
|
21
|
+
function toRelative(filePath, root) {
|
|
22
|
+
if (!filePath) return '';
|
|
23
|
+
const abs = path.isAbsolute(filePath) ? filePath : path.join(root, filePath);
|
|
24
|
+
const rel = path.relative(root, abs);
|
|
25
|
+
return rel.split(path.sep).join('/');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function matchesPrefix(rel, prefix) {
|
|
29
|
+
const p = String(prefix).replace(/\/+$/, '');
|
|
30
|
+
return rel === p || rel.startsWith(p + '/');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Pure decision : returns { deny, reason }.
|
|
34
|
+
function decideScope({ state, config, toolName, filePath }) {
|
|
35
|
+
if (!['Write', 'Edit'].includes(toolName)) return { deny: false };
|
|
36
|
+
if (!isEngaged(state)) return { deny: false };
|
|
37
|
+
|
|
38
|
+
const guard = (config && config.scope_guard) || {};
|
|
39
|
+
if (!guard.enforce_paths) return { deny: false };
|
|
40
|
+
|
|
41
|
+
const allowed = (state.scope_lock && state.scope_lock.allowed_paths) || [];
|
|
42
|
+
if (!Array.isArray(allowed) || allowed.length === 0) return { deny: false };
|
|
43
|
+
|
|
44
|
+
const root = projectRoot();
|
|
45
|
+
const rel = toRelative(filePath, root);
|
|
46
|
+
if (!rel) return { deny: false };
|
|
47
|
+
|
|
48
|
+
const exempt = guard.exempt_globs || [];
|
|
49
|
+
if (exempt.some((g) => matchesPrefix(rel, g))) return { deny: false };
|
|
50
|
+
|
|
51
|
+
if (allowed.some((a) => matchesPrefix(rel, a))) return { deny: false };
|
|
52
|
+
|
|
53
|
+
const base =
|
|
54
|
+
(config && config.banners && config.banners.scope_deny) ||
|
|
55
|
+
'Strict mode: this write targets a path outside the locked scope.';
|
|
56
|
+
const reason =
|
|
57
|
+
`${base}\n` +
|
|
58
|
+
`Target: ${rel}\n` +
|
|
59
|
+
`Locked paths: ${allowed.join(', ')}\n` +
|
|
60
|
+
`Either this file belongs to the scope (re-lock with byan_strict_lock_scope ` +
|
|
61
|
+
`including the corrected paths) or it does not (do not write it).`;
|
|
62
|
+
|
|
63
|
+
return { deny: true, reason };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function allow() {
|
|
67
|
+
return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'allow' } };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (require.main === module) {
|
|
71
|
+
(async () => {
|
|
72
|
+
const state = loadState();
|
|
73
|
+
if (!isEngaged(state)) {
|
|
74
|
+
process.stdout.write(JSON.stringify(allow()));
|
|
75
|
+
process.exit(0);
|
|
76
|
+
}
|
|
77
|
+
const config = loadConfig();
|
|
78
|
+
const payload = parseJson(await readStdin());
|
|
79
|
+
const toolName = payload.tool_name || payload.toolName || '';
|
|
80
|
+
const input = payload.tool_input || payload.toolInput || {};
|
|
81
|
+
const filePath = input.file_path || '';
|
|
82
|
+
|
|
83
|
+
const decision = decideScope({ state, config, toolName, filePath });
|
|
84
|
+
if (!decision.deny) {
|
|
85
|
+
process.stdout.write(JSON.stringify(allow()));
|
|
86
|
+
process.exit(0);
|
|
87
|
+
}
|
|
88
|
+
process.stdout.write(
|
|
89
|
+
JSON.stringify({
|
|
90
|
+
hookSpecificOutput: {
|
|
91
|
+
hookEventName: 'PreToolUse',
|
|
92
|
+
permissionDecision: 'deny',
|
|
93
|
+
permissionDecisionReason: decision.reason,
|
|
94
|
+
},
|
|
95
|
+
})
|
|
96
|
+
);
|
|
97
|
+
process.exit(0);
|
|
98
|
+
})();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = { decideScope, toRelative, matchesPrefix };
|