create-byan-agent 2.35.0 → 2.38.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 (27) hide show
  1. package/CHANGELOG.md +139 -0
  2. package/install/bin/create-byan-agent-v2.js +15 -0
  3. package/install/lib/claude-native-setup.js +12 -38
  4. package/install/lib/platforms/claude-code.js +28 -19
  5. package/install/package.json +1 -1
  6. package/install/packages/platform-config/lib/mcp-config.js +71 -5
  7. package/install/templates/.claude/CLAUDE.md +1 -0
  8. package/install/templates/.claude/hooks/inject-delivery-default.js +46 -0
  9. package/install/templates/.claude/hooks/leantime-fd-sync.js +12 -1
  10. package/install/templates/.claude/hooks/lib/delivery-contract.js +143 -0
  11. package/install/templates/.claude/hooks/lib/punt-detect.js +143 -0
  12. package/install/templates/.claude/hooks/punt-guard.js +126 -0
  13. package/install/templates/.claude/hooks/strict-stop-guard.js +29 -6
  14. package/install/templates/.claude/settings.json +8 -0
  15. package/install/templates/_byan/_config/delivery-default.json +22 -0
  16. package/install/templates/_byan/mcp/byan-mcp-server/channel-entry.js +46 -0
  17. package/install/templates/_byan/mcp/byan-mcp-server/lib/channel-poll.js +128 -0
  18. package/install/templates/_byan/mcp/byan-mcp-server/lib/channel-server.js +234 -0
  19. package/install/templates/_byan/mcp/byan-mcp-server/lib/completeness-evidence.js +159 -0
  20. package/install/templates/_byan/mcp/byan-mcp-server/lib/leantime-fd-core.js +60 -2
  21. package/install/templates/_byan/mcp/byan-mcp-server/lib/leantime-sync.js +4 -1
  22. package/install/templates/_byan/mcp/byan-mcp-server/lib/precommit-gate.js +68 -2
  23. package/install/templates/_byan/mcp/byan-mcp-server/lib/strict-mode.js +78 -1
  24. package/install/templates/docs/leantime-integration.md +11 -1
  25. package/node_modules/byan-platform-config/lib/mcp-config.js +71 -5
  26. package/package.json +1 -1
  27. package/install/templates/.mcp.json.tmpl +0 -8
@@ -0,0 +1,143 @@
1
+ // Pure helpers for the BYAN delivery-default contract (F1).
2
+ //
3
+ // The user asked for something complete. This contract makes prod-grade +
4
+ // maximal scope the DEFAULT the agent assumes every turn, instead of quietly
5
+ // drifting toward an MVP / a short deliverable / a "split so the heavy part
6
+ // does not block". It is the proactive twin of strict mode: strict locks a
7
+ // scope on demand; this anchor sets the BASELINE posture before a scope is
8
+ // even named.
9
+ //
10
+ // F1 ships LIVE (unlike the two blockers F2/F3): it is pure injected context,
11
+ // it blocks nothing, so a false positive costs at most a few tokens — never a
12
+ // trapped turn or a blocked commit. The only escape is an explicit opt-out word
13
+ // the user types THIS message (mvp, quick, brouillon, ...), read from
14
+ // _byan/_config/delivery-default.json so the wordlist is one source of truth.
15
+ //
16
+ // Every function here is pure (no clock, no fs beyond the one config read in
17
+ // loadConfig) so the decision is unit-testable without a hook harness.
18
+
19
+ 'use strict';
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+
24
+ // Fallback wordlist used only if the config file is missing/unreadable. Keeping
25
+ // it here means the anchor still has a closed opt-out set on a broken install,
26
+ // rather than treating every message as opt-out (fail-safe toward PROD).
27
+ const DEFAULT_OPT_OUT_WORDS = [
28
+ 'mvp',
29
+ 'quick',
30
+ 'brouillon',
31
+ 'jette',
32
+ 'prototype',
33
+ 'vite fait',
34
+ 'pas besoin que ce soit parfait',
35
+ 'poc',
36
+ 'draft',
37
+ ];
38
+
39
+ function configPath(projectRoot) {
40
+ const root = projectRoot || process.env.CLAUDE_PROJECT_DIR || process.cwd();
41
+ return path.join(root, '_byan', '_config', 'delivery-default.json');
42
+ }
43
+
44
+ function loadConfig(projectRoot) {
45
+ try {
46
+ const p = configPath(projectRoot);
47
+ if (fs.existsSync(p)) {
48
+ const cfg = JSON.parse(fs.readFileSync(p, 'utf8'));
49
+ if (cfg && typeof cfg === 'object') return cfg;
50
+ }
51
+ } catch {
52
+ // Fall through to defaults — a broken config must not silence the anchor.
53
+ }
54
+ return {};
55
+ }
56
+
57
+ function optOutWords(config) {
58
+ const words = config && Array.isArray(config.optOutWords) ? config.optOutWords : null;
59
+ return words && words.length ? words : DEFAULT_OPT_OUT_WORDS;
60
+ }
61
+
62
+ // The full delivery contract. PROD + MAXIMAL + the AI-2026 cost yardstick +
63
+ // the explicit ban on proposing an MVP/short-deliverable/dont-block-the-heavy
64
+ // split. Kept as one builder so the hook and the tests render identical text.
65
+ function buildAnchor() {
66
+ return [
67
+ 'CONTRAT DE LIVRAISON (defaut): grade=PROD. scope=MAXIMAL -- livre le maximum',
68
+ 'coherent, INTERDIT de proposer un MVP/livrable-court/decoupage-pour-ne-pas-bloquer-le-lourd',
69
+ 'sauf si l\'utilisateur a tape un mot opt-out CE message. Etalon de cout=AI-2026:',
70
+ 'estime en temps-agent (x10), jamais en temps-humain-a-la-main. Tu es un fou',
71
+ 'd\'optimisation: maximum a chaque demande, pas minimum.',
72
+ ].join(' ');
73
+ }
74
+
75
+ // Opt-out detection is BIASED TOWARD PROD. A false opt-out (silencing the prod
76
+ // anchor when the user did not ask to) is the one failure that matters, so when
77
+ // in doubt we stay PROD. A single opt-out word counts only when it reads as a
78
+ // directive for THIS task -- the message is short enough to be a directive, OR
79
+ // a "go-cheap" cue sits just before the word -- and it is dropped when negated
80
+ // ("pas de mvp"). A word merely MENTIONED (a long meta message about MVPs, a
81
+ // system notification, an "anti-mvp" discussion) leaves the anchor armed.
82
+ // Curated multi-word phrases ("vite fait", "pas besoin que ce soit parfait")
83
+ // are unambiguous and match as-is.
84
+ const OPT_OUT_NEGATORS = new Set(['pas', 'sans', 'aucun', 'aucune', 'ni', 'no', 'not', 'non']);
85
+ const OPT_OUT_CUES = new Set([
86
+ 'juste', 'just', 'seulement', 'only', 'mode', 'reste', 'simplement',
87
+ 'rapidement', 'vite', 'fais', 'make', 'leave', 'give', 'garde',
88
+ ]);
89
+ const OPT_OUT_SHORT_WORDS = 30; // a message this short reads as a directive, not meta
90
+ const OPT_OUT_WINDOW = 3; // tokens before the word inspected for a negator / cue
91
+
92
+ function tokenizeWords(s) {
93
+ return s.toLowerCase().match(/[a-z0-9']+/g) || [];
94
+ }
95
+
96
+ function parseOptOut(userMsg, config) {
97
+ if (!userMsg || typeof userMsg !== 'string') return false;
98
+ const lower = userMsg.toLowerCase();
99
+ const words = optOutWords(config)
100
+ .map((w) => String(w).toLowerCase().trim())
101
+ .filter(Boolean);
102
+
103
+ // 1) Curated multi-word phrases: unambiguous opt-out intent, substring match.
104
+ if (words.some((w) => w.includes(' ') && lower.includes(w))) return true;
105
+
106
+ // 2) Single words: honoured only in a directive context, dropped when negated.
107
+ const singles = new Set(words.filter((w) => /^[a-z]+$/.test(w)));
108
+ if (singles.size === 0) return false;
109
+ const tokens = tokenizeWords(userMsg);
110
+ const directiveShort = tokens.length <= OPT_OUT_SHORT_WORDS;
111
+ for (let i = 0; i < tokens.length; i += 1) {
112
+ if (!singles.has(tokens[i])) continue;
113
+ const before = tokens.slice(Math.max(0, i - OPT_OUT_WINDOW), i);
114
+ if (before.some((t) => OPT_OUT_NEGATORS.has(t))) continue; // "pas de mvp" -> stays PROD
115
+ if (directiveShort || before.some((t) => OPT_OUT_CUES.has(t))) return true;
116
+ }
117
+ return false;
118
+ }
119
+
120
+ // The per-turn decision. With an opt-out word present this turn, emit a single
121
+ // line authorizing descope; otherwise emit the full anchor. Pure: the caller
122
+ // supplies the message and (optionally) the loaded config.
123
+ function decideContext({ userMsg, config } = {}) {
124
+ const cfg = config || {};
125
+ const optOut = parseOptOut(userMsg, cfg);
126
+ if (optOut) {
127
+ return {
128
+ optOut: true,
129
+ text: 'CONTRAT DE LIVRAISON: OPT-OUT detecte ce message -- descope autorise (grade/scope libres pour cette demande).',
130
+ };
131
+ }
132
+ return { optOut: false, text: buildAnchor() };
133
+ }
134
+
135
+ module.exports = {
136
+ DEFAULT_OPT_OUT_WORDS,
137
+ configPath,
138
+ loadConfig,
139
+ optOutWords,
140
+ buildAnchor,
141
+ parseOptOut,
142
+ decideContext,
143
+ };
@@ -0,0 +1,143 @@
1
+ // Pure detector for the BYAN punt-guard (F3).
2
+ //
3
+ // A "punt" is the laziness pattern the user fights: the agent ends its turn by
4
+ // telling the human to run a command and paste the output back, when the agent
5
+ // could have run it itself this turn. The signal is a CO-OCCURRENCE in the
6
+ // finished turn:
7
+ // 1. an imperative addressed to the user ("lance", "execute", "colle la
8
+ // sortie", "run this", "paste", "donne-moi le ssh"), AND
9
+ // 2. a runnable command (a backticked command, or a `node `/`npm `/`git `/
10
+ // `which `/`curl localhost` invocation), AND
11
+ // 3. NO Bash tool-call for that command this turn (the agent did not actually
12
+ // run it — it handed it off).
13
+ //
14
+ // Carve-out (creds): `git push` and `npm publish` need credentials the server
15
+ // running Claude Code does not have. Asking the user to run THOSE is legitimate,
16
+ // not a punt — documented in MEMORY (server cannot push). So a command whose
17
+ // runnable token is a push/publish is never flagged.
18
+ //
19
+ // This module is pure decide() — no fs, no stdin — so it is exhaustively
20
+ // unit-testable. The hook wires it to the transcript and the arm flag.
21
+
22
+ 'use strict';
23
+
24
+ // Imperatives addressed to the user that, paired with a runnable command, mean
25
+ // "you run it". FR + EN. Matched case-insensitively as loose phrases.
26
+ const PUNT_IMPERATIVES = [
27
+ 'lance',
28
+ 'execute',
29
+ 'exécute',
30
+ 'colle la sortie',
31
+ 'colle le resultat',
32
+ 'colle le résultat',
33
+ 'run this',
34
+ 'run it',
35
+ 'paste',
36
+ 'donne-moi le ssh',
37
+ 'donne moi le ssh',
38
+ 'peux-tu lancer',
39
+ 'peux tu lancer',
40
+ 'lance la commande',
41
+ ];
42
+
43
+ // Commands that need creds the server lacks. Asking the user to run these is
44
+ // legitimate (not a punt). Order matters only for reporting the matched token.
45
+ const CREDS_CARVEOUT = ['git push', 'npm publish'];
46
+
47
+ // Loose runnable-command detectors. A backticked span, or a bare invocation of
48
+ // a common runner at a word boundary. `curl localhost` is included because the
49
+ // dev API runs on localhost and the agent can hit it itself.
50
+ const RUNNABLE_PATTERNS = [
51
+ /`([^`]+)`/g, // any backticked span
52
+ /\bnode\s+\S+/gi,
53
+ /\bnpm\s+\S+/gi,
54
+ /\bgit\s+\S+/gi,
55
+ /\bwhich\s+\S+/gi,
56
+ /\bcurl\s+localhost\S*/gi,
57
+ ];
58
+
59
+ function hasImperativeToUser(text) {
60
+ if (!text) return false;
61
+ const lower = text.toLowerCase();
62
+ return PUNT_IMPERATIVES.some((p) => lower.includes(p));
63
+ }
64
+
65
+ // Returns the first runnable command found in the text, or null. Backticked
66
+ // spans win (they are the explicit "here is the command" form); else the first
67
+ // bare invocation. The returned string is the matched command text.
68
+ function findRunnableCommand(text) {
69
+ if (!text) return null;
70
+ for (const re of RUNNABLE_PATTERNS) {
71
+ re.lastIndex = 0;
72
+ const m = re.exec(text);
73
+ if (m) {
74
+ // Backtick pattern captures group 1 (the inner command); the bare-runner
75
+ // patterns have no capture group, so use the full match.
76
+ return (m[1] !== undefined ? m[1] : m[0]).trim();
77
+ }
78
+ }
79
+ return null;
80
+ }
81
+
82
+ // True when the command is a creds-gated push/publish (legitimate to delegate).
83
+ function isCredsCarveOut(cmd) {
84
+ if (!cmd) return false;
85
+ const lower = cmd.toLowerCase();
86
+ return CREDS_CARVEOUT.some((c) => lower.includes(c));
87
+ }
88
+
89
+ // True when a Bash tool-call this turn actually ran (something matching) the
90
+ // command. We match on the meaningful head token of the command (e.g. "npm
91
+ // test" -> we require the Bash command to include "npm test"), so a Bash call
92
+ // that ran a DIFFERENT command does not falsely clear the punt. toolCalls is an
93
+ // array of { name, command } where command is the Bash tool's command string.
94
+ function bashRanCommand(toolCalls, cmd) {
95
+ if (!Array.isArray(toolCalls) || !cmd) return false;
96
+ // Compare on a normalized, whitespace-collapsed head so minor formatting
97
+ // (extra spaces, trailing flags in the prose) does not break the match.
98
+ const needle = cmd.toLowerCase().replace(/\s+/g, ' ').trim();
99
+ const headTokens = needle.split(' ').slice(0, 2).join(' '); // e.g. "npm test"
100
+ return toolCalls.some((tc) => {
101
+ if (!tc || typeof tc.name !== 'string') return false;
102
+ if (!/bash/i.test(tc.name)) return false;
103
+ const ran = String(tc.command || '').toLowerCase().replace(/\s+/g, ' ').trim();
104
+ if (!ran) return false;
105
+ return ran.includes(needle) || (headTokens.length > 0 && ran.includes(headTokens));
106
+ });
107
+ }
108
+
109
+ // Pure decision. Returns { punt, reason, cmd }.
110
+ // punt : true IFF imperative-to-user + runnable command + NOT a creds
111
+ // carve-out + NO Bash tool-call ran that command this turn.
112
+ // reason: a short human-readable explanation (or why it is not a punt).
113
+ // cmd : the runnable command detected (or null).
114
+ function decide({ lastAssistantText, toolCallsThisTurn } = {}) {
115
+ const text = lastAssistantText || '';
116
+ const imperative = hasImperativeToUser(text);
117
+ const cmd = findRunnableCommand(text);
118
+
119
+ if (!imperative || !cmd) {
120
+ return { punt: false, reason: 'no imperative-to-user + runnable command co-occurrence', cmd: cmd || null };
121
+ }
122
+ if (isCredsCarveOut(cmd)) {
123
+ return { punt: false, reason: `creds carve-out: "${cmd}" needs credentials the server lacks`, cmd };
124
+ }
125
+ if (bashRanCommand(toolCallsThisTurn, cmd)) {
126
+ return { punt: false, reason: `agent ran the command via Bash this turn: "${cmd}"`, cmd };
127
+ }
128
+ return {
129
+ punt: true,
130
+ reason: `punt detected: asked the user to run "${cmd}" without running it via Bash this turn`,
131
+ cmd,
132
+ };
133
+ }
134
+
135
+ module.exports = {
136
+ PUNT_IMPERATIVES,
137
+ CREDS_CARVEOUT,
138
+ hasImperativeToUser,
139
+ findRunnableCommand,
140
+ isCredsCarveOut,
141
+ bashRanCommand,
142
+ decide,
143
+ };
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Stop hook — BYAN punt-guard (F3).
4
+ *
5
+ * Goal : catch the laziness pattern where the agent ends its turn by telling the
6
+ * user to run a command and paste the output back, when the agent could have run
7
+ * it itself (a Bash tool-call) this turn. The pure detection lives in
8
+ * lib/punt-detect.js; this hook wires it to the real Stop payload and the arm
9
+ * flag.
10
+ *
11
+ * Ships DISARMED (puntGuard.armed=false in _byan/_config/delivery-default.json),
12
+ * same posture as the autobench Stop guard. Arming a Stop blocker without first
13
+ * measuring false positives breaks the flow. So by default the hook only
14
+ * OBSERVES: it appends one line to _byan-output/punt-ledger.jsonl
15
+ * (observed-disarmed / observed-disarmed-punt) and exits 0. Only when armed does
16
+ * it block (exit 2) on a detected punt.
17
+ *
18
+ * Carve-out (in punt-detect): git push / npm publish are never flagged — the
19
+ * server has no creds, so delegating those to the user is legitimate.
20
+ *
21
+ * Non-blocking on any IO/parse error : the hook never traps a turn it cannot read.
22
+ */
23
+
24
+ 'use strict';
25
+
26
+ const fs = require('fs');
27
+ const path = require('path');
28
+ const { decide } = require('./lib/punt-detect');
29
+ const { loadConfig } = require('./lib/delivery-contract');
30
+ // transcript-read is the canonical Stop-payload reader (text + raw content); it
31
+ // does NOT carry stdin helpers, so readStdin/parseJson come from strict-runtime
32
+ // like the other Stop/prompt hooks.
33
+ const { extractLastAssistantText, extractLastAssistantContent } = require('./lib/transcript-read');
34
+ const { readStdin, parseJson } = require('./lib/strict-runtime');
35
+
36
+ function projectRoot() {
37
+ return process.env.CLAUDE_PROJECT_DIR || process.cwd();
38
+ }
39
+
40
+ function isArmed(config) {
41
+ const pg = config && config.puntGuard;
42
+ return !!(pg && pg.armed === true);
43
+ }
44
+
45
+ // Pull the Bash tool-calls out of the finished assistant turn's RAW content.
46
+ // transcript-read gives the block array; a Bash tool_use is
47
+ // { type:'tool_use', name:'Bash', input:{ command } }. Returns [{ name, command }].
48
+ function bashToolCalls(content) {
49
+ if (!Array.isArray(content)) return [];
50
+ const out = [];
51
+ for (const b of content) {
52
+ if (b && b.type === 'tool_use' && typeof b.name === 'string') {
53
+ const command = b.input && typeof b.input.command === 'string' ? b.input.command : '';
54
+ out.push({ name: b.name, command });
55
+ }
56
+ }
57
+ return out;
58
+ }
59
+
60
+ function ledgerPath() {
61
+ return path.join(projectRoot(), '_byan-output', 'punt-ledger.jsonl');
62
+ }
63
+
64
+ function appendLedger(entry) {
65
+ try {
66
+ const p = ledgerPath();
67
+ fs.mkdirSync(path.dirname(p), { recursive: true });
68
+ fs.appendFileSync(p, JSON.stringify(entry) + '\n');
69
+ return true;
70
+ } catch {
71
+ return false;
72
+ }
73
+ }
74
+
75
+ if (require.main === module) {
76
+ (async () => {
77
+ // Wrap everything : the hook must NEVER trap a turn it cannot read.
78
+ try {
79
+ const config = loadConfig(projectRoot());
80
+ const payload = parseJson(await readStdin());
81
+ const lastAssistantText = extractLastAssistantText(payload);
82
+ const toolCallsThisTurn = bashToolCalls(extractLastAssistantContent(payload));
83
+
84
+ const result = decide({ lastAssistantText, toolCallsThisTurn });
85
+ const armed = isArmed(config);
86
+
87
+ const event = armed
88
+ ? result.punt
89
+ ? 'fired-block'
90
+ : 'no-punt'
91
+ : result.punt
92
+ ? 'observed-disarmed-punt'
93
+ : 'observed-disarmed';
94
+
95
+ appendLedger({
96
+ event,
97
+ punt: result.punt,
98
+ cmd: result.cmd || undefined,
99
+ reason: result.reason,
100
+ armed,
101
+ ts: process.env.BYAN_HOOK_TS || undefined,
102
+ session: process.env.CLAUDE_SESSION_ID || undefined,
103
+ });
104
+
105
+ if (armed && result.punt) {
106
+ const reason =
107
+ `Punt-guard: you asked the user to run "${result.cmd}" and paste the output, ` +
108
+ `but you did not run it via Bash this turn. Run it yourself, then report the result. ` +
109
+ `(git push / npm publish are exempt — the server has no creds for those.)`;
110
+ process.stdout.write(
111
+ JSON.stringify({ decision: 'block', reason, systemMessage: reason })
112
+ );
113
+ process.exit(2);
114
+ }
115
+
116
+ process.stdout.write(JSON.stringify({ continue: true }));
117
+ process.exit(0);
118
+ } catch {
119
+ // Last-resort net : on any unexpected failure, let the turn end.
120
+ process.stdout.write(JSON.stringify({ continue: true }));
121
+ process.exit(0);
122
+ }
123
+ })();
124
+ }
125
+
126
+ module.exports = { bashToolCalls, isArmed, ledgerPath, appendLedger };
@@ -23,15 +23,38 @@ const { extractLastAssistantText } = require('./lib/transcript-read');
23
23
 
24
24
  const DEFAULT_MARKERS = ['done', 'finished', 'complete', 'delivered', 'ready'];
25
25
 
26
+ // Strip the contexts where a completion marker is a MENTION, not a CLAIM:
27
+ // fenced + inline code, HTML comments (the BYAN-BENCH:done marker lives there),
28
+ // and snake_case / namespaced identifiers (byan_strict_complete, BENCH:done). A
29
+ // marker that survives this strip is prose -- the only place a real "it is done"
30
+ // claim lives. Exported so the regression cases are unit-testable.
31
+ function denoiseForClaim(text) {
32
+ return String(text)
33
+ .replace(/```[\s\S]*?```/g, ' ') // fenced code blocks
34
+ .replace(/`[^`]*`/g, ' ') // inline code spans
35
+ .replace(/<!--[\s\S]*?-->/g, ' ') // HTML comments (e.g. <!-- BYAN-BENCH:done -->)
36
+ .replace(/[A-Za-z0-9]+(?:[_:][A-Za-z0-9]+)+/g, ' '); // snake_case / ns identifiers
37
+ }
38
+
39
+ // A completion marker counts only as a STANDALONE claim, bounded by non-letters
40
+ // (Unicode-aware via the u flag, so "indefini" does not embed "fini" and
41
+ // "determine" does not embed "termine"), with a permissive trailing inflection
42
+ // (livre -> livree / livres). Bias: a false negative is caught by the pre-commit
43
+ // gate (the hard net), while a false positive traps a legitimate turn -- so a
44
+ // marker that is only mentioned is NOT read as a claim.
26
45
  function claimsCompletion(text, markers) {
27
46
  if (!text) return false;
28
- const lower = text.toLowerCase();
47
+ const clean = denoiseForClaim(text).toLowerCase();
29
48
  return (markers || DEFAULT_MARKERS).some((m) => {
30
- const marker = String(m).toLowerCase();
31
- if (/^[a-z]+$/.test(marker)) {
32
- return new RegExp(`\\b${marker}\\b`).test(lower);
49
+ const marker = String(m).toLowerCase().trim();
50
+ if (!marker) return false;
51
+ const escaped = marker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
52
+ try {
53
+ return new RegExp(`(?<![\\p{L}])${escaped}(?:s|e|es|ée|ées|és)?(?![\\p{L}])`, 'iu').test(clean);
54
+ } catch {
55
+ // Older runtimes without lookbehind/\p{L} -> fall back to a plain include.
56
+ return clean.includes(marker);
33
57
  }
34
- return lower.includes(marker);
35
58
  });
36
59
  }
37
60
 
@@ -85,4 +108,4 @@ if (require.main === module) {
85
108
  })();
86
109
  }
87
110
 
88
- module.exports = { decideStop, claimsCompletion, extractLastAssistantText };
111
+ module.exports = { decideStop, claimsCompletion, denoiseForClaim, extractLastAssistantText };
@@ -27,6 +27,10 @@
27
27
  "type": "command",
28
28
  "command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/inject-voice-anchor.js"
29
29
  },
30
+ {
31
+ "type": "command",
32
+ "command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/inject-delivery-default.js"
33
+ },
30
34
  {
31
35
  "type": "command",
32
36
  "command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/soul-memory-triggers.js"
@@ -70,6 +74,10 @@
70
74
  "type": "command",
71
75
  "command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/autobench-stop-guard.js"
72
76
  },
77
+ {
78
+ "type": "command",
79
+ "command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/punt-guard.js"
80
+ },
73
81
  {
74
82
  "type": "command",
75
83
  "command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/drain-advisory.js"
@@ -0,0 +1,22 @@
1
+ {
2
+ "version": 1,
3
+ "name": "BYAN Delivery Default",
4
+ "description": "Makes prod-grade + maximal scope the mechanical default. F1 (the delivery-contract anchor) ships LIVE: it is pure injected context, no risk. F2 (completeness reject) and F3 (punt-guard) ship DISARMED behind armed flags: arming a turn/commit blocker without first measuring false positives would break the flow and could lock an in-progress strict session.",
5
+ "optOutWords": [
6
+ "mvp",
7
+ "quick",
8
+ "brouillon",
9
+ "jette",
10
+ "prototype",
11
+ "vite fait",
12
+ "pas besoin que ce soit parfait",
13
+ "poc",
14
+ "draft"
15
+ ],
16
+ "completenessGate": {
17
+ "armed": false
18
+ },
19
+ "puntGuard": {
20
+ "armed": false
21
+ }
22
+ }
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * channel-entry.js — Entrypoint stdio du channel MCP byan pour Claude Code.
4
+ *
5
+ * POURQUOI un fichier séparé de server.js : le channel est un serveur MCP
6
+ * distinct du serveur principal byan-mcp. Claude Code le spawne comme un
7
+ * subprocess indépendant via --dangerously-load-development-channels.
8
+ * server.js ne doit pas acquérir le transport channel comme effet de bord.
9
+ *
10
+ * Commande de lancement (research preview) :
11
+ * claude --dangerously-load-development-channels server:byan-channel
12
+ *
13
+ * Enregistrement dans .mcp.json (section mcpServers) — exactement ce que pose
14
+ * l'installeur : chemin RELATIF au projectRoot (portable, jamais absolu) et env
15
+ * VIDE (la config est resolue au boot via resolveConfig ci-dessous ; aucun secret
16
+ * n'est ecrit dans .mcp.json qui est tracke par git) :
17
+ * "byan-channel": {
18
+ * "command": "node",
19
+ * "args": ["_byan/mcp/byan-mcp-server/channel-entry.js"],
20
+ * "env": {}
21
+ * }
22
+ *
23
+ * Voie plugin (Phase 2, non implémentée ici) :
24
+ * Wrapper le channel-entry.js dans un plugin Claude Code et l'enregistrer
25
+ * dans la marketplace pour qu'il soit accessible via
26
+ * --channels plugin:byan-channel@byan-marketplace.
27
+ * Nécessite que le channel soit sur l'allowlist Anthropic ou sur la liste
28
+ * allowedChannelPlugins de l'organisation (Enterprise).
29
+ */
30
+
31
+ import { resolveConfig } from './lib/resolve-config.js';
32
+ import { runChannelStdio } from './lib/channel-server.js';
33
+
34
+ // Config : même resolver que server.js (env -> ~/.byan/credentials.json -> défauts).
35
+ const config = resolveConfig();
36
+ const apiUrl = config.BYAN_API_URL || process.env.BYAN_API_URL;
37
+ const apiToken = config.BYAN_API_TOKEN || process.env.BYAN_API_TOKEN;
38
+
39
+ if (!apiUrl) {
40
+ process.stderr.write('[byan-channel] BYAN_API_URL manquant — le poll sera désactivé\n');
41
+ }
42
+ if (!apiToken) {
43
+ process.stderr.write('[byan-channel] BYAN_API_TOKEN manquant — le poll et les replies seront désactivés\n');
44
+ }
45
+
46
+ await runChannelStdio({ apiUrl, apiToken });