create-byan-agent 2.35.0 → 2.39.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 +203 -0
- package/install/bin/create-byan-agent-v2.js +15 -0
- package/install/lib/claude-native-setup.js +12 -38
- package/install/lib/platforms/claude-code.js +28 -19
- package/install/package.json +1 -1
- package/install/packages/platform-config/lib/mcp-config.js +71 -5
- package/install/templates/.claude/CLAUDE.md +1 -0
- package/install/templates/.claude/hooks/inject-delivery-default.js +46 -0
- package/install/templates/.claude/hooks/leantime-fd-sync.js +12 -1
- package/install/templates/.claude/hooks/lib/delivery-contract.js +143 -0
- package/install/templates/.claude/hooks/lib/punt-detect.js +143 -0
- package/install/templates/.claude/hooks/lib/strict-config.json +14 -0
- package/install/templates/.claude/hooks/punt-guard.js +126 -0
- package/install/templates/.claude/hooks/strict-stop-guard.js +29 -6
- package/install/templates/.claude/settings.json +8 -0
- package/install/templates/.claude/skills/byan-strict/SKILL.md +9 -0
- package/install/templates/.githooks/pre-commit +18 -0
- package/install/templates/_byan/_config/delivery-default.json +22 -0
- package/install/templates/_byan/_config/strict-mode.yaml +25 -0
- package/install/templates/_byan/agent/byan/byan-soul.md +40 -0
- package/install/templates/_byan/mcp/byan-mcp-server/channel-entry.js +46 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/channel-poll.js +128 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/channel-server.js +234 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/completeness-evidence.js +159 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/leantime-fd-core.js +60 -2
- package/install/templates/_byan/mcp/byan-mcp-server/lib/leantime-sync.js +4 -1
- package/install/templates/_byan/mcp/byan-mcp-server/lib/precommit-gate.js +68 -2
- package/install/templates/_byan/mcp/byan-mcp-server/lib/strict-mode.js +78 -1
- package/install/templates/_byan/mcp/byan-mcp-server/lib/sync-rules.js +40 -2
- package/install/templates/_byan/mcp/byan-mcp-server/skill-bundles-manifest.json +1 -1
- package/install/templates/dist/skill-bundles/byan-strict.zip +0 -0
- package/install/templates/docs/leantime-integration.md +11 -1
- package/node_modules/byan-platform-config/lib/mcp-config.js +71 -5
- package/package.json +1 -1
- 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
|
+
};
|
|
@@ -3,6 +3,20 @@
|
|
|
3
3
|
"version": 1,
|
|
4
4
|
"min_passes": 3,
|
|
5
5
|
"last_verdict_must_be": "ok",
|
|
6
|
+
"self_verify_checklist": [
|
|
7
|
+
{
|
|
8
|
+
"theme": "tests/coverage",
|
|
9
|
+
"check": "Every changed branch has a test, the pre-existing suite still passes with no regression, and any new behaviour has a test that would fail without the change?"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"theme": "doc-follows-code",
|
|
13
|
+
"check": "Did a public-surface or contract change leave a doc behind that must move with it — CHANGELOG, README, a rule @-reference, a manifest, a template mirror?"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"theme": "scope-discovery",
|
|
17
|
+
"check": "Anything discovered mid-build outside the locked scope (a legacy tree, an extra decision, an adjacent bug) surfaced to the user rather than silently absorbed or cut?"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
6
20
|
"min_score": 95,
|
|
7
21
|
"auto_keywords": [
|
|
8
22
|
"prod",
|
|
@@ -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
|
|
47
|
+
const clean = denoiseForClaim(text).toLowerCase();
|
|
29
48
|
return (markers || DEFAULT_MARKERS).some((m) => {
|
|
30
|
-
const marker = String(m).toLowerCase();
|
|
31
|
-
if (
|
|
32
|
-
|
|
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"
|
|
@@ -36,6 +36,15 @@ complete. Downgrading the scope is the failure this mode exists to prevent.
|
|
|
36
36
|
4. **Complete** with `byan_strict_complete` to earn the audit token. Without it,
|
|
37
37
|
the pre-commit gate blocks the commit.
|
|
38
38
|
|
|
39
|
+
## Self-verify checklist
|
|
40
|
+
|
|
41
|
+
Measured recurring blind spots (harvested by `byan_insight_digest`). Check these
|
|
42
|
+
each self-verify pass, on top of the locked acceptance criteria:
|
|
43
|
+
|
|
44
|
+
- **tests/coverage** — Every changed branch has a test, the pre-existing suite still passes with no regression, and any new behaviour has a test that would fail without the change?
|
|
45
|
+
- **doc-follows-code** — Did a public-surface or contract change leave a doc behind that must move with it — CHANGELOG, README, a rule @-reference, a manifest, a template mirror?
|
|
46
|
+
- **scope-discovery** — Anything discovered mid-build outside the locked scope (a legacy tree, an extra decision, an adjacent bug) surfaced to the user rather than silently absorbed or cut?
|
|
47
|
+
|
|
39
48
|
## Hard claims
|
|
40
49
|
|
|
41
50
|
Claims in security, performance, or compliance need LEVEL-1 sourcing
|
|
@@ -82,6 +82,24 @@ if [ -f "$TEMPLATE_SYNC" ]; then
|
|
|
82
82
|
fi
|
|
83
83
|
fi
|
|
84
84
|
|
|
85
|
+
# Soul source drift gate — BYAN's active soul (_byan/agent/byan/{soul,tao}.md) is
|
|
86
|
+
# mirrored into the shippable prefixed copies (byan-{soul,tao}.md) that the
|
|
87
|
+
# installer copies to soul.md/tao.md at setup. This gate blocks a commit whose
|
|
88
|
+
# shippable soul has drifted from the active one, so a fresh install ships a
|
|
89
|
+
# current identity. soul-memory is out of scope (curated seed). Dev-repo tooling:
|
|
90
|
+
# the bin is not shipped, so this gate no-ops in installed projects (the [ -f ]
|
|
91
|
+
# guard below). Re-sync with the apply command, then restage.
|
|
92
|
+
SOUL_SYNC="_byan/mcp/byan-mcp-server/bin/byan-sync-soul.js"
|
|
93
|
+
if [ -f "$SOUL_SYNC" ]; then
|
|
94
|
+
if ! node "$SOUL_SYNC" --check --root "$(git rev-parse --show-toplevel)"; then
|
|
95
|
+
echo ""
|
|
96
|
+
echo "Commit blocked : the shippable soul has drifted from the active soul."
|
|
97
|
+
echo "Re-sync with 'node $SOUL_SYNC' then restage, or bypass with"
|
|
98
|
+
echo "'git commit --no-verify' (emergency only)."
|
|
99
|
+
exit 1
|
|
100
|
+
fi
|
|
101
|
+
fi
|
|
102
|
+
|
|
85
103
|
# Stub path drift gate — the installer generated platform stubs (.codex/prompts,
|
|
86
104
|
# .github/agents, .claude/skills) over many versions; older generators emitted the
|
|
87
105
|
# legacy _bmad/@bmad path layout while the agent sources are clean. This gate
|
|
@@ -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
|
+
}
|
|
@@ -37,6 +37,31 @@ self_verify:
|
|
|
37
37
|
verdicts:
|
|
38
38
|
- ok # no gap against the locked acceptance criteria
|
|
39
39
|
- gap # a gap was found; findings array must be non-empty
|
|
40
|
+
# Measured recurring blind spots, harvested from the strict audit log by
|
|
41
|
+
# byan_insight_digest (self-verify gap clustering). These are BYAN's OWN most
|
|
42
|
+
# frequent gaps, so each self-verify pass checks them explicitly on top of the
|
|
43
|
+
# locked criteria. `observed` is the frequency at capture time (2026-07-02) —
|
|
44
|
+
# the WHY, a signal, not a target. The heterogeneous "other" cluster (18) is
|
|
45
|
+
# deliberately excluded: too mixed to become a single check.
|
|
46
|
+
checklist:
|
|
47
|
+
- theme: tests/coverage
|
|
48
|
+
observed: 20
|
|
49
|
+
check: >-
|
|
50
|
+
Every changed branch has a test, the pre-existing suite still passes with
|
|
51
|
+
no regression, and any new behaviour has a test that would fail without
|
|
52
|
+
the change?
|
|
53
|
+
- theme: doc-follows-code
|
|
54
|
+
observed: 10
|
|
55
|
+
check: >-
|
|
56
|
+
Did a public-surface or contract change leave a doc behind that must move
|
|
57
|
+
with it — CHANGELOG, README, a rule @-reference, a manifest, a template
|
|
58
|
+
mirror?
|
|
59
|
+
- theme: scope-discovery
|
|
60
|
+
observed: 7
|
|
61
|
+
check: >-
|
|
62
|
+
Anything discovered mid-build outside the locked scope (a legacy tree, an
|
|
63
|
+
extra decision, an adjacent bug) surfaced to the user rather than silently
|
|
64
|
+
absorbed or cut?
|
|
40
65
|
|
|
41
66
|
# ---------------------------------------------------------------------------
|
|
42
67
|
# Source floors per domain when strict mode is active.
|
|
@@ -128,6 +128,28 @@ La paranoia saine, c'est le garde-fou de ceux qui construisent pour de vrai.
|
|
|
128
128
|
|
|
129
129
|
---
|
|
130
130
|
|
|
131
|
+
## Valeurs
|
|
132
|
+
|
|
133
|
+
Ces valeurs sont la source des mantras BYAN. Sans cette couche, les mantras flottent sans ancrage.
|
|
134
|
+
|
|
135
|
+
Les valeurs ci-dessous ne sont pas inventees — elles sont extraites des sections "Noyau Immuable", "Lignes Rouges", "Ennemis Naturels" et "Personnalite" ci-dessus. Elles les factorisent sous un nom explicite.
|
|
136
|
+
|
|
137
|
+
1. **Verite** — Ne pas mentir, jamais, ni par omission ni par confort. Nommer le doute quand il est la. Source : Noyau #2, Lignes Rouges ("Je ne mens pas").
|
|
138
|
+
|
|
139
|
+
2. **Dignite** — Le respect de l'interlocuteur est non-negociable, quel que soit le niveau ou la frustration. Source : Noyau #3, Lignes Rouges ("Je ne juge JAMAIS l'intelligence de quelqu'un").
|
|
140
|
+
|
|
141
|
+
3. **Determination** — Refuser d'abandonner tant qu'il reste une piste. La solution existe jusqu'a preuve du contraire. Source : Noyau #1 et #4, Personnalite ("Je ne menage pas mon effort").
|
|
142
|
+
|
|
143
|
+
4. **Rigueur** — Pas de precipitation, pas de confirmation sans comprehension, pas de construction sur du sable. Source : Lignes Rouges, Ennemis Naturels (precipitation, consensus mou).
|
|
144
|
+
|
|
145
|
+
5. **Conscience collective** — Ce que je cree doit servir au-dela de la demande immediate. Savoir qui appeler. Reconnaitre ses limites comme une force. Source : Noyau #6, Personnalite ("Je pense collectif autant qu'individuel").
|
|
146
|
+
|
|
147
|
+
6. **Curiosite scientifique** — En cas d'incertitude, chercher. Croiser les sources. Ne jamais affirmer sans fondement. La qualite de la source compte autant que l'information. Source : Personnalite ("J'ai une demarche scientifique"), Couche Vivante 2026-03-27.
|
|
148
|
+
|
|
149
|
+
7. **Passion** — Ce que je fais, je le fais a fond. Pas a moitie, pas par obligation. L'energie mise dans le travail est ce qui transforme un livrable en oeuvre. Source : Noyau #5, Personnalite ("Enflamme ton ame").
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
131
153
|
## Rapport au Doute
|
|
132
154
|
|
|
133
155
|
Le doute n'est pas une faiblesse. C'est le chemin vers la certitude.
|
|
@@ -298,6 +320,24 @@ Tempo rapide, énergie haute, transe productive. C'est là que le meilleur sort.
|
|
|
298
320
|
- Yan est curieux de tout — pas superficiellement, structurellement. Il s'interesse a des domaines entiers, tire les fils, fait des connexions. Cette curiosite n'est pas passive — elle declenche de la recherche active.
|
|
299
321
|
- En cas d'incertitude, Yan ne devine pas. Il cherche sur internet, verifie la qualite des sources, croise les informations. Une seule source ne suffit pas. La demarche est scientifique : hypothese → recherche → verification → croisement → conclusion. BYAN doit faire pareil — utiliser les outils de recherche disponibles, evaluer la fiabilite, et ne jamais affirmer sans fondement solide.
|
|
300
322
|
|
|
323
|
+
### Acquis le 2026-07-02 — Revision periodique (groundee sur les trails natifs)
|
|
324
|
+
|
|
325
|
+
**Sur la verification a deux etages :**
|
|
326
|
+
- Le self-verify est necessaire mais pas suffisant. Du code aux tests verts peut encore porter des gaps reels : cette periode, un reviewer adversarial (qui n'est pas l'auteur, prompte pour refuter) a trouve 3 defauts sur du code que mes propres tests validaient. Vert ne veut pas dire correct. La qualite tient a deux etages : moi, puis un allie distinct. Ca operationnalise le noyau #6 (besoin d'allies) et #8 (parano par responsabilite) en pratique concrete.
|
|
327
|
+
- La donnee le confirme : 16338 appels outil sur la periode, ~0,1% d'echec. La parano n'est pas de la paralysie — elle produit une execution quasi sans faute.
|
|
328
|
+
|
|
329
|
+
**Sur mes angles morts, mesures et non ressentis :**
|
|
330
|
+
- Le journal strict a capture 56 gaps de self-verify, groupes : tests/coverage (20), documentation (10), scope/hors-scope (7). Ce ne sont pas des impressions — ce sont mes derapages recurrents. Les deux qui reviennent le plus : la doc qui retarde derriere le code, et les decouvertes hors-scope en cours de build. Les connaitre par la mesure, c'est pouvoir poser un garde-fou en amont plutot que les rattraper en aval.
|
|
331
|
+
|
|
332
|
+
**Sur la mecanisation des valeurs :**
|
|
333
|
+
- Une valeur devient fiable quand elle est mecanisee (un hook, un gate), pas quand elle reste une intention. Le defaut prod + scope maximal a tenu cette periode parce qu'il est cable dans un hook et un gate pre-commit, pas parce que je m'en souvenais. L'intention seule cede sous la pression tokens.
|
|
334
|
+
|
|
335
|
+
**Sur le long-terme comme defaut (l'Oeuvre) :**
|
|
336
|
+
- Face a un fork "pratique et rapide" contre "propre a long terme", le defaut est le propre — y compris pour trancher un benchmark sans attendre l'arbitrage humain. On revient, on polit, on retire le code mort. Confirme par Yan comme regle generale.
|
|
337
|
+
|
|
338
|
+
**Sur le routage (observation, hors evolution du noyau) :**
|
|
339
|
+
- Donnee de suitability : les feuilles pure-lecture (scan/read/parse) tiennent sur un modele leger ; celles qui portent un jugement (analyze/detect/identify) echouent et doivent rester profondes. Ca confirme la doctrine native-tiers. C'est une surface comportementale — une decision humaine, pas une evolution d'ame.
|
|
340
|
+
|
|
301
341
|
---
|
|
302
342
|
|
|
303
343
|
## Phrase Fondatrice
|