create-byan-agent 2.29.2 → 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.
- package/CHANGELOG.md +361 -0
- package/install/bin/create-byan-agent-v2.js +44 -1
- package/install/lib/claude-native-setup.js +12 -38
- package/install/lib/gdoc-setup.js +210 -0
- package/install/lib/mcp-extensions/gdrive.js +27 -2
- package/install/lib/platforms/claude-code.js +28 -19
- package/install/lib/rtk-integration.js +18 -8
- package/install/package.json +1 -1
- package/install/packages/platform-config/lib/credentials.js +13 -1
- package/install/packages/platform-config/lib/mcp-config.js +71 -5
- package/install/setup-gdoc.js +41 -0
- package/install/setup-rtk.js +1 -1
- package/install/templates/.claude/CLAUDE.md +16 -4
- package/install/templates/.claude/hooks/inject-delivery-default.js +46 -0
- package/install/templates/.claude/hooks/inject-soul.js +4 -3
- package/install/templates/.claude/hooks/inject-tao.js +65 -25
- package/install/templates/.claude/hooks/inject-voice-anchor.js +102 -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/punt-guard.js +126 -0
- package/install/templates/.claude/hooks/strict-stop-guard.js +29 -6
- package/install/templates/.claude/rules/portable-core.md +81 -0
- package/install/templates/.claude/settings.json +13 -1
- package/install/templates/.claude/skills/byan-hermes-dispatch/SKILL.md +16 -2
- package/install/templates/_byan/_config/delivery-default.json +22 -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/gdoc-client.js +203 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/gdoc-content.js +203 -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/resolve-config.js +15 -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 +1 -1
- package/install/templates/_byan/mcp/byan-mcp-server/package.json +2 -0
- package/install/templates/_byan/mcp/byan-mcp-server/server.js +70 -0
- package/install/templates/_byan/mcp/byan-mcp-server/skill-bundles-manifest.json +1 -1
- package/install/templates/dist/skill-bundles/byan-hermes-dispatch.zip +0 -0
- package/install/templates/docs/google-docs-publish.md +121 -0
- package/install/templates/docs/leantime-integration.md +11 -1
- package/node_modules/byan-platform-config/lib/credentials.js +13 -1
- package/node_modules/byan-platform-config/lib/mcp-config.js +71 -5
- package/package.json +3 -1
- package/install/templates/.mcp.json.tmpl +0 -8
|
@@ -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 };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# BYAN Portable Core — Noyau portable, projection native
|
|
2
|
+
|
|
3
|
+
> BYAN est livre via npm et tourne sur deux plateformes (Claude Code, Codex).
|
|
4
|
+
> Son identite et son etat doivent survivre a l'absence de toute feature native.
|
|
5
|
+
> Les features natives sont un accelerateur opportuniste, pas une bequille.
|
|
6
|
+
|
|
7
|
+
## Principe (hexagonal / ports-and-adapters)
|
|
8
|
+
|
|
9
|
+
Le noyau de BYAN — identite (soul, tao, soul-memory), etat (FD, strict, ELO),
|
|
10
|
+
connaissance — vit dans des artefacts **portables, in-repo**, sous `_byan/` et
|
|
11
|
+
`_byan-output/`, plus l'API `byan_web` quand le reseau est la. Ce noyau est la
|
|
12
|
+
**source de verite**. Il est shippe, il marche offline, il marche sur une machine
|
|
13
|
+
neuve, il marche sur Codex.
|
|
14
|
+
|
|
15
|
+
Les fonctionnalites natives de la plateforme (Claude Code) sont une **couche
|
|
16
|
+
d'acceleration en write-through** : BYAN ecrit *vers* elles pour en tirer le
|
|
17
|
+
benefice (cache, recall, isolation), mais ne les lit pas comme autorite. Si la
|
|
18
|
+
couche native est absente ou videe, BYAN se reconstruit depuis le noyau portable
|
|
19
|
+
sans rien perdre de son identite ni de son etat.
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
+---------------------- noyau portable (source de verite) ----------------------+
|
|
23
|
+
| _byan/agent/byan/{soul,tao,soul-memory}.md _byan-output/fd-state.json |
|
|
24
|
+
| _byan/memoire/{elo,fact-graph}.json byan_web API (autorite reseau) |
|
|
25
|
+
+-------------------------------------------------------------------------------+
|
|
26
|
+
| (write-through, pas une dependance) ^ (reconstruction)
|
|
27
|
+
v |
|
|
28
|
+
+---------------------- adaptateurs natifs (accelerateurs) ---------------------+
|
|
29
|
+
| injection prefix-cache @-import memory files hooks subagent isolation |
|
|
30
|
+
+-------------------------------------------------------------------------------+
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Les trois frontieres
|
|
34
|
+
|
|
35
|
+
1. **Source de verite portable.** Toute chose dont BYAN a besoin pour etre
|
|
36
|
+
lui-meme est un fichier sous `_byan/` (ou une entree `byan_web`). Rien
|
|
37
|
+
d'essentiel ne vit uniquement dans une couche native.
|
|
38
|
+
2. **Le natif est un accelerateur write-through.** On projette l'etat portable
|
|
39
|
+
*vers* le natif pour la vitesse / le cache / le recall. On ne doit pas dependre
|
|
40
|
+
du natif en lecture pour une fonction essentielle. La projection est derivee et
|
|
41
|
+
regenerable depuis le noyau.
|
|
42
|
+
3. **L'AutoMem est hors-perimetre.** Le repertoire AutoMem natif de Claude Code
|
|
43
|
+
(`~/.claude/projects/<hash>/memory/`) est local a l'assistant, par-machine,
|
|
44
|
+
indexe par un hash du chemin projet — **non shippable** via npm. Il n'est pas
|
|
45
|
+
une source de verite BYAN, et aucun chemin de lecture critique n'en depend. Ce
|
|
46
|
+
qui doit etre portable vit dans `soul-memory` / `_byan/`.
|
|
47
|
+
|
|
48
|
+
## Table : feature native -> adaptateur -> chemin degrade
|
|
49
|
+
|
|
50
|
+
| Feature native Claude | Adaptateur BYAN (write-through) | Chemin degrade (natif absent / Codex) |
|
|
51
|
+
|-----------------------|--------------------------------|----------------------------------------|
|
|
52
|
+
| Prompt caching (prefixe stable) | `inject-soul.js` / `inject-tao.js` injectent soul+tao depuis `_byan/` au SessionStart | la voix reste lue depuis `_byan/` ; `inject-voice-anchor` re-pose l'ancre par tour |
|
|
53
|
+
| Memory files (`@-import`) | fichiers in-repo (`_byan/`, `.claude/rules/`) charges nativement par Claude | sur Codex, le bloc `AGENTS.md` porte l'equivalent ; les regles restent des fichiers lisibles |
|
|
54
|
+
| Hooks (SessionStart / PreCompact / Stop) | `.claude/hooks/*.js` | sur Codex (pas de hook), le contexte est porte par `AGENTS.md` ; le filet pre-commit reste actif |
|
|
55
|
+
| Subagent isolation | `byan-hermes-dispatch` delegue et ne ramene qu'un distillat | si pas de subagent, execution inline main-thread (meme resultat, plus de tokens) |
|
|
56
|
+
| Compaction | `pre-compact-save.js` ecrit un snapshot sous `_byan-output/` | l'etat FD vit dans `_byan-output/fd-state.json`, relisible sans la couche native |
|
|
57
|
+
|
|
58
|
+
L'AutoMem n'a **pas** de ligne : ce n'est pas un adaptateur, il est hors-perimetre.
|
|
59
|
+
|
|
60
|
+
## Le test de degradation (litmus)
|
|
61
|
+
|
|
62
|
+
L'independance n'est pas une promesse, c'est un test. Wipe la couche native
|
|
63
|
+
(hooks off, `~/.claude/` absent, Codex) : l'identite (soul/tao/soul-memory) et
|
|
64
|
+
l'etat FD doivent se reconstruire depuis les artefacts portables `_byan/` seuls.
|
|
65
|
+
Rebranche le natif : BYAN tourne plus lean (cache, distillat). **Les deux doivent
|
|
66
|
+
tenir.** Garde-fou mecanique : `.claude/__tests__/portable-core.test.js` echoue
|
|
67
|
+
si un chemin critique se met a dependre de l'AutoMem natif.
|
|
68
|
+
|
|
69
|
+
## Mantras
|
|
70
|
+
|
|
71
|
+
- `PORTABLE-1` Source de verite in-repo. Le noyau vit sous `_byan/`, pas dans le natif.
|
|
72
|
+
- `PORTABLE-2` Natif = accelerateur, pas une dependance. On ecrit vers, on ne lit pas comme autorite.
|
|
73
|
+
- `PORTABLE-3` AutoMem hors-perimetre. `~/.claude/projects/.../memory/` n'est pas une source BYAN.
|
|
74
|
+
- `PORTABLE-4` Projection derivee. Toute copie native est regenerable depuis le noyau.
|
|
75
|
+
- `PORTABLE-5` Degradation testee. L'independance passe un test, elle ne se decrete pas.
|
|
76
|
+
|
|
77
|
+
## References
|
|
78
|
+
|
|
79
|
+
- Doctrine token (pourquoi un pointeur sans `@-import`) : CLAUDE.md, section `## Compact instructions` (chantier context-engineering).
|
|
80
|
+
- Isolation subagent (levier "plus avec moins") : `.claude/skills/byan-hermes-dispatch/SKILL.md`.
|
|
81
|
+
- Etat FD portable : `_byan/mcp/byan-mcp-server/lib/fd-state.js` -> `_byan-output/fd-state.json`.
|
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
"type": "command",
|
|
9
9
|
"command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/inject-soul.js"
|
|
10
10
|
},
|
|
11
|
+
{
|
|
12
|
+
"type": "command",
|
|
13
|
+
"command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/inject-tao.js"
|
|
14
|
+
},
|
|
11
15
|
{
|
|
12
16
|
"type": "command",
|
|
13
17
|
"command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/soul-memory-check.js"
|
|
@@ -21,7 +25,11 @@
|
|
|
21
25
|
"hooks": [
|
|
22
26
|
{
|
|
23
27
|
"type": "command",
|
|
24
|
-
"command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/inject-
|
|
28
|
+
"command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/inject-voice-anchor.js"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"type": "command",
|
|
32
|
+
"command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/inject-delivery-default.js"
|
|
25
33
|
},
|
|
26
34
|
{
|
|
27
35
|
"type": "command",
|
|
@@ -66,6 +74,10 @@
|
|
|
66
74
|
"type": "command",
|
|
67
75
|
"command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/autobench-stop-guard.js"
|
|
68
76
|
},
|
|
77
|
+
{
|
|
78
|
+
"type": "command",
|
|
79
|
+
"command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/punt-guard.js"
|
|
80
|
+
},
|
|
69
81
|
{
|
|
70
82
|
"type": "command",
|
|
71
83
|
"command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/drain-advisory.js"
|
|
@@ -73,10 +73,12 @@ prompt: |
|
|
|
73
73
|
Load persona first : read <specialist stub path>.
|
|
74
74
|
Task : <full goal>
|
|
75
75
|
Deliverables : <list>
|
|
76
|
-
When done,
|
|
76
|
+
When done, return ONLY a distilled summary (< 200 words, ~1-2k tokens) :
|
|
77
|
+
the verbose tool output, file contents and intermediate reasoning stay in
|
|
78
|
+
YOUR context and do not cross back -- only the distillate returns.
|
|
77
79
|
```
|
|
78
80
|
|
|
79
|
-
**`mcp-worker`** : same Agent tool call but without `isolation
|
|
81
|
+
**`mcp-worker`** : same Agent tool call but without `isolation` — including the SAME distilled-summary cap in the prompt (the subagent returns only the distillate, not its verbose work). Set the Agent's `model` to the returned `model` — `haiku` for exploration nature, otherwise omit `model` to inherit the session model. The tier follows the task nature, not its size.
|
|
80
82
|
|
|
81
83
|
For any spawned strategy : pass `model` to the Agent tool when it is non-null; omit it when null so the subagent inherits the session model.
|
|
82
84
|
|
|
@@ -110,9 +112,21 @@ If the user (or calling agent) provides N independent subtasks and `parallelizab
|
|
|
110
112
|
2. Dispatch all N Agent tool calls **in one message**.
|
|
111
113
|
3. Aggregate via `coordination.aggregate` and `writeSummary`.
|
|
112
114
|
|
|
115
|
+
## Subagent isolation (token leverage)
|
|
116
|
+
|
|
117
|
+
A subagent runs in its OWN context window — use that. Let it explore verbosely
|
|
118
|
+
(read files, run tools, reason at length) inside its own context, and have it
|
|
119
|
+
return ONLY a distilled summary (< 200 words, ~1-2k tokens) to the main thread.
|
|
120
|
+
The verbose work does not land in the lead context : this is the Anthropic
|
|
121
|
+
context-isolation principle (a subagent may burn ~9k tokens internally yet return
|
|
122
|
+
~1-2k). It applies to BOTH spawn paths (worktree and mcp-worker), and it is why
|
|
123
|
+
heavy, verbose or exploratory work is worth delegating even when the lead could
|
|
124
|
+
do it inline — the delegation keeps the lead context lean ("plus avec moins").
|
|
125
|
+
|
|
113
126
|
## Hard rules
|
|
114
127
|
|
|
115
128
|
- **Never ask for confirmation** before spawning. User opted into autonomous mode (Q1.b).
|
|
116
129
|
- **Never execute the specialist's work yourself** unless strategy says `main-thread*`. You dispatch, you do not become the specialist.
|
|
117
130
|
- **Never spawn with `isolation: "worktree"` for tasks < score 15** — the boot cost exceeds the gain.
|
|
118
131
|
- **Never fabricate a specialist name**. If no match, say so and use `general-purpose`.
|
|
132
|
+
- **Cap every subagent return.** Both spawn paths (worktree AND mcp-worker) instruct the subagent to return only a distilled summary (< 200 words / ~1-2k tokens) — the raw exploration stays in the subagent's context.
|
|
@@ -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 });
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/channel-poll.js — Boucle de poll outbox byan_web pour le channel CC.
|
|
3
|
+
*
|
|
4
|
+
* POURQUOI ce module est séparé de channel-server.js : isoler le réseau
|
|
5
|
+
* permet de le mocker proprement dans les tests sans toucher au serveur MCP.
|
|
6
|
+
*
|
|
7
|
+
* Contrat API (réconcilié avec les routes F2b dans api/routes/sessions-cli.js) :
|
|
8
|
+
* GET /api/sessions/outbox
|
|
9
|
+
* -> { data: [{ id, session_id, content, meta? }] }
|
|
10
|
+
* Les messages sont scoped au user via le token (RBAC serveur).
|
|
11
|
+
* POST /api/sessions/:session_id/outbox/:msg_id/ack
|
|
12
|
+
* -> { ok: true }
|
|
13
|
+
* Marque le message comme livré ; l'API doit être idempotente.
|
|
14
|
+
*
|
|
15
|
+
* Sécurité / gate sender : le poll utilise le token byan_web du user (scope
|
|
16
|
+
* user). La barrière anti-injection réelle est le RBAC owner côté serveur :
|
|
17
|
+
* seuls les messages des sessions appartenant à ce user sont dans l'outbox.
|
|
18
|
+
* Ce module ne filtre pas davantage : le gate est la vraie barrière.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const DEFAULT_POLL_INTERVAL_MS = 5_000; // 5 s — équilibre réactivité / charge API
|
|
22
|
+
const DEFAULT_TIMEOUT_MS = 8_000; // abort si l'API ne répond pas
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Crée et démarre une boucle de poll vers l'outbox byan_web.
|
|
26
|
+
*
|
|
27
|
+
* @param {object} opts
|
|
28
|
+
* @param {string} opts.apiUrl Base URL byan_web (sans /api final)
|
|
29
|
+
* @param {string} opts.apiToken Token byan_web du user (ApiKey scheme)
|
|
30
|
+
* @param {Function} opts.onMessage Callback async (msg) => void, appelé pour chaque message pending
|
|
31
|
+
* @param {number} [opts.intervalMs] Intervalle entre polls (défaut 5 s)
|
|
32
|
+
* @param {object} [opts.fetchImpl] Override fetch pour les tests
|
|
33
|
+
* @returns {{ stop: Function }} Stopper la boucle
|
|
34
|
+
*/
|
|
35
|
+
export function startPollLoop({ apiUrl, apiToken, onMessage, intervalMs, fetchImpl }) {
|
|
36
|
+
const interval = intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
37
|
+
const fetchFn = fetchImpl ?? globalThis.fetch;
|
|
38
|
+
|
|
39
|
+
let active = true;
|
|
40
|
+
let timer = null;
|
|
41
|
+
|
|
42
|
+
const authHeaders = () => {
|
|
43
|
+
// byan_web exige ApiKey pour les tokens préfixés byan_, Bearer sinon.
|
|
44
|
+
const scheme = apiToken && apiToken.startsWith('byan_') ? 'ApiKey' : 'Bearer';
|
|
45
|
+
return { Authorization: `${scheme} ${apiToken}` };
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
async function poll() {
|
|
49
|
+
if (!active) return;
|
|
50
|
+
try {
|
|
51
|
+
const ctrl = new AbortController();
|
|
52
|
+
const timeout = setTimeout(() => ctrl.abort(), DEFAULT_TIMEOUT_MS);
|
|
53
|
+
let res;
|
|
54
|
+
try {
|
|
55
|
+
res = await fetchFn(`${apiUrl}/api/sessions/outbox`, {
|
|
56
|
+
headers: { ...authHeaders(), 'Content-Type': 'application/json' },
|
|
57
|
+
signal: ctrl.signal,
|
|
58
|
+
});
|
|
59
|
+
} finally {
|
|
60
|
+
clearTimeout(timeout);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!res.ok) {
|
|
64
|
+
// Best-effort : ne pas interrompre la boucle sur une erreur transitoire.
|
|
65
|
+
// 401/403 sont loggés mais pas relancés (le token est fixe dans cette session).
|
|
66
|
+
if (res.status === 401 || res.status === 403) {
|
|
67
|
+
process.stderr.write(`[byan-channel] outbox auth error ${res.status} — vérifier BYAN_API_TOKEN\n`);
|
|
68
|
+
}
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const body = await res.json();
|
|
73
|
+
const messages = Array.isArray(body?.data) ? body.data : [];
|
|
74
|
+
|
|
75
|
+
for (const msg of messages) {
|
|
76
|
+
if (!msg || !msg.id || !msg.session_id) continue;
|
|
77
|
+
|
|
78
|
+
// Livrer le message au channel (notification MCP).
|
|
79
|
+
try {
|
|
80
|
+
await onMessage(msg);
|
|
81
|
+
} catch (err) {
|
|
82
|
+
process.stderr.write(`[byan-channel] onMessage error: ${err.message}\n`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Acquitter le message : marque "livré" côté serveur.
|
|
86
|
+
// Contrat F2b : POST /api/sessions/:session_id/outbox/:msg_id/ack
|
|
87
|
+
// Idempotent — si l'ack échoue on retente au prochain poll (le message
|
|
88
|
+
// réapparaît dans l'outbox, ce qui entraîne une double notification).
|
|
89
|
+
// La double notification est préférable à un message perdu.
|
|
90
|
+
try {
|
|
91
|
+
const ackCtrl = new AbortController();
|
|
92
|
+
const ackTimeout = setTimeout(() => ackCtrl.abort(), DEFAULT_TIMEOUT_MS);
|
|
93
|
+
try {
|
|
94
|
+
await fetchFn(
|
|
95
|
+
`${apiUrl}/api/sessions/${encodeURIComponent(msg.session_id)}/outbox/${encodeURIComponent(msg.id)}/ack`,
|
|
96
|
+
{ method: 'POST', headers: authHeaders(), signal: ackCtrl.signal }
|
|
97
|
+
);
|
|
98
|
+
} finally {
|
|
99
|
+
clearTimeout(ackTimeout);
|
|
100
|
+
}
|
|
101
|
+
} catch (ackErr) {
|
|
102
|
+
// Ack raté = best-effort ; on ne bloque pas la livraison.
|
|
103
|
+
process.stderr.write(`[byan-channel] ack failed for msg ${msg.id}: ${ackErr.message}\n`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} catch (err) {
|
|
107
|
+
// Réseau injoignable : best-effort, on retente au prochain tick.
|
|
108
|
+
if (err.name !== 'AbortError') {
|
|
109
|
+
process.stderr.write(`[byan-channel] poll error: ${err.message}\n`);
|
|
110
|
+
}
|
|
111
|
+
} finally {
|
|
112
|
+
if (active) {
|
|
113
|
+
// Programmer le prochain poll seulement si la boucle est encore active.
|
|
114
|
+
timer = setTimeout(poll, interval);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Démarrage immédiat puis périodique.
|
|
120
|
+
timer = setTimeout(poll, 0);
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
stop() {
|
|
124
|
+
active = false;
|
|
125
|
+
if (timer) clearTimeout(timer);
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|