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.
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
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,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-byan-agent",
3
- "version": "2.16.1",
3
+ "version": "2.17.1",
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
  }
@@ -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 url = `http://localhost:${this.port}`;
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 };