create-byan-agent 2.19.1 → 2.20.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 +188 -0
- package/README.md +4 -4
- package/install/src/byan-v2/generation/templates/default-agent.md +1 -1
- package/install/templates/.claude/CLAUDE.md +1 -1
- package/install/templates/.claude/hooks/fd-phase-guard.js +2 -2
- package/install/templates/.claude/hooks/mantra-validate.js +16 -8
- package/install/templates/.claude/hooks/strict-scope-guard.js +25 -7
- package/install/templates/.claude/rules/native-workflows.md +32 -0
- package/install/templates/.claude/skills/byan-byan/SKILL.md +5 -5
- package/install/templates/.claude/skills/byan-mantra-audit/SKILL.md +53 -0
- package/install/templates/.claude/skills/byan-merise-agile/SKILL.md +2 -2
- package/install/templates/.claude/skills/byan-native-dev-story/SKILL.md +83 -0
- package/install/templates/.claude/workflows/INDEX.md +35 -0
- package/install/templates/.claude/workflows/check-implementation-readiness.js +280 -0
- package/install/templates/.claude/workflows/code-review.js +179 -0
- package/install/templates/.claude/workflows/create-excalidraw-dataflow.js +214 -0
- package/install/templates/.claude/workflows/create-excalidraw-diagram.js +188 -0
- package/install/templates/.claude/workflows/create-excalidraw-flowchart.js +225 -0
- package/install/templates/.claude/workflows/create-excalidraw-wireframe.js +192 -0
- package/install/templates/.claude/workflows/create-story.js +216 -0
- package/install/templates/.claude/workflows/dev-story.js +100 -0
- package/install/templates/.claude/workflows/document-project.js +455 -0
- package/install/templates/.claude/workflows/qa-automate.js +169 -0
- package/install/templates/.claude/workflows/quick-dev.js +273 -0
- package/install/templates/.claude/workflows/sprint-planning.js +261 -0
- package/install/templates/.claude/workflows/testarch-atdd.js +287 -0
- package/install/templates/.claude/workflows/testarch-automate.js +229 -0
- package/install/templates/.claude/workflows/testarch-ci.js +184 -0
- package/install/templates/.claude/workflows/testarch-framework.js +267 -0
- package/install/templates/.claude/workflows/testarch-nfr.js +316 -0
- package/install/templates/.claude/workflows/testarch-test-design.js +293 -0
- package/install/templates/.claude/workflows/testarch-test-review.js +321 -0
- package/install/templates/.claude/workflows/testarch-trace.js +316 -0
- package/install/templates/.githooks/pre-commit +49 -15
- package/install/templates/_byan/config.yaml +15 -5
- package/install/templates/_byan/mcp/byan-mcp-server/bin/byan-build-workflows.js +20 -0
- package/install/templates/_byan/mcp/byan-mcp-server/bin/byan-lint-workflows.js +57 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/native-loop.js +39 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/workflows-generator.js +149 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/workflows-lint.js +113 -0
- package/install/templates/_byan/workflow/simple/byan/feature-workflow.md +14 -11
- package/install/templates/docs/native-workflows-contract.md +84 -0
- package/package.json +2 -2
- package/src/byan-v2/data/agent-scopes.json +46 -0
- package/src/byan-v2/data/mantras.json +194 -8
- package/src/byan-v2/generation/mantra-audit.js +147 -0
- package/src/byan-v2/generation/mantra-validator.js +56 -6
- package/src/byan-v2/generation/scope-resolver.js +102 -0
- package/src/byan-v2/generation/templates/default-agent.md +1 -1
- package/update-byan-agent/bin/update-byan-agent.js +67 -72
- package/update-byan-agent/lib/apply-update.js +202 -0
- package/update-byan-agent/package.json +1 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# BYAN pre-commit hook
|
|
3
|
-
#
|
|
4
|
-
#
|
|
2
|
+
# BYAN pre-commit hook. Three gates run in order:
|
|
3
|
+
# 1. Strict Mode gate : block if a strict session is engaged but not completed.
|
|
4
|
+
# 2. Native-workflow lint : block if a .claude/workflows/*.js couples to state.
|
|
5
|
+
# 3. Mantra floor : block if a Gen3 persona source scores below the floor.
|
|
5
6
|
#
|
|
6
7
|
# Install :
|
|
7
8
|
# git config core.hooksPath .githooks
|
|
@@ -9,16 +10,22 @@
|
|
|
9
10
|
# Bypass (emergency only) :
|
|
10
11
|
# git commit --no-verify
|
|
11
12
|
#
|
|
12
|
-
#
|
|
13
|
-
# _byan/
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
13
|
+
# Mantra gate scope : the canonical Gen3 persona SOURCES
|
|
14
|
+
# _byan/agent/<name>/<name>.md
|
|
15
|
+
# are validated with DOMAIN-AWARE scoring (each persona is scored only against
|
|
16
|
+
# the mantras applicable to its declared scope, via scope-resolver). Deployed
|
|
17
|
+
# artifacts (.github/agents, .claude/skills, .claude/agents) are derived loader
|
|
18
|
+
# stubs or non-persona procedures; the non-blocking Stop hook (mantra-validate.js)
|
|
19
|
+
# warns on those. THRESHOLD is an anti-stub FLOOR, not a quality bar: it catches
|
|
20
|
+
# near-empty / zombie persona files. Genuine personas score well above it; deep
|
|
21
|
+
# quality is the job of the semantic embodiment audit (src/byan-v2/generation/mantra-audit.js),
|
|
22
|
+
# which runs out-of-band, not at commit time.
|
|
17
23
|
|
|
18
24
|
set -euo pipefail
|
|
19
25
|
|
|
20
|
-
THRESHOLD=
|
|
26
|
+
THRESHOLD=30
|
|
21
27
|
VALIDATOR="src/byan-v2/generation/mantra-validator.js"
|
|
28
|
+
RESOLVER="src/byan-v2/generation/scope-resolver.js"
|
|
22
29
|
|
|
23
30
|
if ! command -v node >/dev/null 2>&1; then
|
|
24
31
|
echo "[byan pre-commit] node not found, skipping mantra check"
|
|
@@ -40,11 +47,27 @@ if [ -f "$STRICT_GATE" ]; then
|
|
|
40
47
|
fi
|
|
41
48
|
fi
|
|
42
49
|
|
|
50
|
+
# Native workflow lint — a .claude/workflows/*.js script runs outside the
|
|
51
|
+
# conversation turn where the strict/FD hooks fire, so the surviving net is this
|
|
52
|
+
# gate : a native script must not couple directly to BYAN state internals
|
|
53
|
+
# (import/require lib/fd-state.js or the strict-mode lib). State goes through the
|
|
54
|
+
# byan_fd_* / byan_strict_* MCP tools. No-op if the linter is absent.
|
|
55
|
+
WF_LINT="_byan/mcp/byan-mcp-server/bin/byan-lint-workflows.js"
|
|
56
|
+
if [ -f "$WF_LINT" ]; then
|
|
57
|
+
if ! node "$WF_LINT" --root "$(git rev-parse --show-toplevel)"; then
|
|
58
|
+
echo ""
|
|
59
|
+
echo "Commit blocked : native workflow script couples to BYAN state internals."
|
|
60
|
+
echo "Use the byan_fd_* / byan_strict_* MCP tools instead of importing lib/fd-state.js."
|
|
61
|
+
echo "Bypass with 'git commit --no-verify' (emergency only)."
|
|
62
|
+
exit 1
|
|
63
|
+
fi
|
|
64
|
+
fi
|
|
65
|
+
|
|
43
66
|
if [ ! -f "$VALIDATOR" ]; then
|
|
44
67
|
exit 0
|
|
45
68
|
fi
|
|
46
69
|
|
|
47
|
-
staged=$(git diff --cached --name-only --diff-filter=ACM | grep -E '^
|
|
70
|
+
staged=$(git diff --cached --name-only --diff-filter=ACM | grep -E '^_byan/agent/[^/]+/[^/]+\.md$' || true)
|
|
48
71
|
|
|
49
72
|
if [ -z "$staged" ]; then
|
|
50
73
|
exit 0
|
|
@@ -55,13 +78,23 @@ while IFS= read -r file; do
|
|
|
55
78
|
[ -z "$file" ] && continue
|
|
56
79
|
[ ! -f "$file" ] && continue
|
|
57
80
|
|
|
81
|
+
# Skip dev/test/variant artifacts that are not shipped personas.
|
|
82
|
+
case "$file" in
|
|
83
|
+
*test*|*optimized*|*turbo-whisper*) continue ;;
|
|
84
|
+
esac
|
|
85
|
+
|
|
86
|
+
# Domain-aware score : resolve the persona's scope set, then score only the
|
|
87
|
+
# applicable mantras (universal + the persona's domain, behavioral excluded).
|
|
58
88
|
score=$(node -e "
|
|
59
89
|
const V = require('./$VALIDATOR');
|
|
90
|
+
const R = require('./$RESOLVER');
|
|
60
91
|
const fs = require('fs');
|
|
92
|
+
const path = require('path');
|
|
61
93
|
try {
|
|
62
94
|
const content = fs.readFileSync('$file', 'utf8');
|
|
63
|
-
const
|
|
64
|
-
const
|
|
95
|
+
const name = path.basename('$file').replace(/\\.md\$/, '');
|
|
96
|
+
const scopes = R.resolveAgentScopes({ name, content });
|
|
97
|
+
const res = new V().validate(content, { scope: scopes });
|
|
65
98
|
const pct = Math.round((res.compliant.length / res.totalMantras) * 100);
|
|
66
99
|
process.stdout.write(String(pct));
|
|
67
100
|
} catch (e) {
|
|
@@ -75,15 +108,16 @@ while IFS= read -r file; do
|
|
|
75
108
|
fi
|
|
76
109
|
|
|
77
110
|
if [ "$score" -lt "$THRESHOLD" ]; then
|
|
78
|
-
echo "[byan pre-commit] FAIL $file : mantra score $score% < $THRESHOLD%"
|
|
111
|
+
echo "[byan pre-commit] FAIL $file : mantra score $score% < $THRESHOLD% (anti-stub floor)"
|
|
79
112
|
failed=1
|
|
80
113
|
fi
|
|
81
114
|
done <<< "$staged"
|
|
82
115
|
|
|
83
116
|
if [ "$failed" -eq 1 ]; then
|
|
84
117
|
echo ""
|
|
85
|
-
echo "Commit blocked by BYAN mantra pre-commit
|
|
86
|
-
echo "
|
|
118
|
+
echo "Commit blocked by BYAN mantra pre-commit floor."
|
|
119
|
+
echo "The flagged persona scores below the anti-stub floor : flesh it out, or bypass"
|
|
120
|
+
echo "with 'git commit --no-verify' (emergency only)."
|
|
87
121
|
exit 1
|
|
88
122
|
fi
|
|
89
123
|
|
|
@@ -58,15 +58,25 @@ bmad_features:
|
|
|
58
58
|
validate_at_phase_end: true
|
|
59
59
|
auto_summarize: true
|
|
60
60
|
|
|
61
|
-
# MantraValidator: Validate agents against
|
|
61
|
+
# MantraValidator: Validate agents against 71 mantras
|
|
62
62
|
mantras:
|
|
63
63
|
validate: true
|
|
64
|
-
|
|
64
|
+
# Domain-aware anti-stub floor for the pre-commit gate and Stop hook. Each
|
|
65
|
+
# persona is scored only against its applicable mantras (universal + its
|
|
66
|
+
# declared scope) via src/byan-v2/generation/scope-resolver.js. This is a
|
|
67
|
+
# floor that catches empty / zombie personas, not a deep quality bar; deep
|
|
68
|
+
# embodiment is the out-of-band audit (src/byan-v2/generation/mantra-audit.js).
|
|
69
|
+
min_score: 30
|
|
65
70
|
enforce_on_generation: true
|
|
66
71
|
fail_on_low_score: false # If true, fails generation on low score
|
|
67
|
-
categories
|
|
68
|
-
|
|
69
|
-
|
|
72
|
+
# Mantra scopes (revived from the former dead `categories` list). The
|
|
73
|
+
# per-agent scope map lives in src/byan-v2/data/agent-scopes.json.
|
|
74
|
+
scopes:
|
|
75
|
+
- universal
|
|
76
|
+
- sdlc-process
|
|
77
|
+
- sdlc-code
|
|
78
|
+
- sdlc-modeling
|
|
79
|
+
- sdlc-test
|
|
70
80
|
|
|
71
81
|
# VoiceIntegration: Turbo Whisper voice input for hands-free interaction
|
|
72
82
|
voice_integration:
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { buildWorkflowsRegistry } from '../lib/workflows-generator.js';
|
|
3
|
+
|
|
4
|
+
// Regenerate .claude/workflows/INDEX.md from the workflow manifest.
|
|
5
|
+
// Usage: node bin/byan-build-workflows.js [--root <dir>]
|
|
6
|
+
|
|
7
|
+
function parseArgs(argv) {
|
|
8
|
+
const args = {};
|
|
9
|
+
for (let i = 2; i < argv.length; i++) {
|
|
10
|
+
if (argv[i] === '--root') args.projectRoot = argv[++i];
|
|
11
|
+
}
|
|
12
|
+
return args;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const r = buildWorkflowsRegistry(parseArgs(process.argv));
|
|
16
|
+
const c = r.counts;
|
|
17
|
+
process.stdout.write(
|
|
18
|
+
`[byan-build-workflows] ${r.written ? 'wrote' : 'unchanged'} ${r.path} ` +
|
|
19
|
+
`(autonomous ${c.autonomous}, pipeline ${c.pipeline})\n`
|
|
20
|
+
);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { execFileSync } from 'node:child_process';
|
|
5
|
+
import { validateContract } from '../lib/workflows-lint.js';
|
|
6
|
+
|
|
7
|
+
// Validate native workflow scripts under .claude/workflows/ against the full
|
|
8
|
+
// contract (state-coupling + clock/RNG + meta-literal) AND node --check syntax.
|
|
9
|
+
// Exits non-zero on any violation (used by the pre-commit gate).
|
|
10
|
+
// Usage: node bin/byan-lint-workflows.js [--root <dir>]
|
|
11
|
+
|
|
12
|
+
function parseArgs(argv) {
|
|
13
|
+
const args = {};
|
|
14
|
+
for (let i = 2; i < argv.length; i++) {
|
|
15
|
+
if (argv[i] === '--root') args.projectRoot = argv[++i];
|
|
16
|
+
}
|
|
17
|
+
return args;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const args = parseArgs(process.argv);
|
|
21
|
+
const root = args.projectRoot || process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
22
|
+
const dir = path.join(root, '.claude', 'workflows');
|
|
23
|
+
|
|
24
|
+
let files = [];
|
|
25
|
+
try {
|
|
26
|
+
files = fs.readdirSync(dir, { withFileTypes: true })
|
|
27
|
+
.filter((e) => e.isFile() && e.name.endsWith('.js'))
|
|
28
|
+
.map((e) => path.join(dir, e.name));
|
|
29
|
+
} catch (_e) {
|
|
30
|
+
process.stdout.write('[byan-lint-workflows] no .claude/workflows/ directory - nothing to lint\n');
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let failed = 0;
|
|
35
|
+
for (const file of files) {
|
|
36
|
+
const violations = validateContract(fs.readFileSync(file, 'utf8'));
|
|
37
|
+
|
|
38
|
+
// Syntax gate: a native script must parse.
|
|
39
|
+
try {
|
|
40
|
+
execFileSync('node', ['--check', file], { stdio: 'pipe' });
|
|
41
|
+
} catch (e) {
|
|
42
|
+
violations.push({ id: 'node-check', msg: `node --check failed: ${String(e.stderr || e.message).split('\n')[0]}` });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (violations.length) {
|
|
46
|
+
failed += 1;
|
|
47
|
+
for (const v of violations) {
|
|
48
|
+
process.stderr.write(`[byan-lint-workflows] ${file}: ${v.id} - ${v.msg}\n`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (failed === 0) {
|
|
54
|
+
process.stdout.write(`[byan-lint-workflows] OK - ${files.length} native workflows pass the contract (state, clock/RNG, meta, node --check)\n`);
|
|
55
|
+
process.exit(0);
|
|
56
|
+
}
|
|
57
|
+
process.exit(1);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Pure, tested helpers for native-workflow RGR (red-green-refactor) loops.
|
|
2
|
+
//
|
|
3
|
+
// The in-CLI Workflow runtime sandbox forbids import/require/fs INSIDE a
|
|
4
|
+
// .claude/workflows/*.js script, so a native script MUST inline these tiny
|
|
5
|
+
// functions verbatim. This module is the canonical, unit-tested reference; the
|
|
6
|
+
// inlined copies in scripts must mirror it. Keeping the logic here (and tested)
|
|
7
|
+
// is what lets the doc-only "3 consecutive failures -> HALT" rule become a real,
|
|
8
|
+
// verifiable JS counter.
|
|
9
|
+
|
|
10
|
+
export const DEFAULT_MAX_CYCLES = 3;
|
|
11
|
+
|
|
12
|
+
// Decide whether the RGR loop must stop.
|
|
13
|
+
// green=true -> done, not aborted (story task is green)
|
|
14
|
+
// not green, cycles >= cap -> done, aborted (no convergence; hard exit)
|
|
15
|
+
// otherwise -> keep looping
|
|
16
|
+
// Returns { done, abort, reason }.
|
|
17
|
+
export function convergenceGuard({ cycles, green, maxCycles = DEFAULT_MAX_CYCLES }) {
|
|
18
|
+
if (green) return { done: true, abort: false, reason: 'green' };
|
|
19
|
+
if (cycles >= maxCycles) {
|
|
20
|
+
return { done: true, abort: true, reason: `no convergence after ${maxCycles} cycles` };
|
|
21
|
+
}
|
|
22
|
+
return { done: false, abort: false, reason: 'continue' };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Structured verdict a native dev-story run returns to the orchestrating skill
|
|
26
|
+
// for the human gate. Pure constructor — no side effects, no state mutation.
|
|
27
|
+
export function buildVerdict({ storyKey, green, cycles, blocking = [], maxCycles = DEFAULT_MAX_CYCLES }) {
|
|
28
|
+
const aborted = !green && cycles >= maxCycles;
|
|
29
|
+
return {
|
|
30
|
+
workflow: 'dev-story',
|
|
31
|
+
storyKey: storyKey || null,
|
|
32
|
+
status: green ? 'review-ready' : aborted ? 'aborted-no-convergence' : 'in-progress',
|
|
33
|
+
green: Boolean(green),
|
|
34
|
+
cycles,
|
|
35
|
+
maxCycles,
|
|
36
|
+
blocking: Array.isArray(blocking) ? blocking : [],
|
|
37
|
+
needsHumanGate: true,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// BYAN native-workflow registry generator + dual-path resolver.
|
|
2
|
+
//
|
|
3
|
+
// Phase 1 of the native-workflow bridge: BYAN's markdown/YAML workflows are
|
|
4
|
+
// LLM-interpreted and human-gated; Claude Code's in-CLI Workflow tool runs a
|
|
5
|
+
// deterministic JS script with no in-run human gate. Only the workflows whose
|
|
6
|
+
// steps run WITHOUT a per-step human gate (autonomous + deterministic pipeline)
|
|
7
|
+
// can be hosted by the native tool. The gated majority stays markdown by design.
|
|
8
|
+
//
|
|
9
|
+
// This module is the single source of truth for "which workflows are portable"
|
|
10
|
+
// and "where does a workflow physically live" (dual-path: native .js preferred,
|
|
11
|
+
// markdown fallback). It is manifest-driven and deterministic so regeneration is
|
|
12
|
+
// idempotent (writeIfChanged). It mirrors the index-generator pattern and reuses
|
|
13
|
+
// its CSV loader — no duplicate parser.
|
|
14
|
+
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { loadManifests } from './index-generator.js';
|
|
18
|
+
|
|
19
|
+
function resolveRoot(projectRoot) {
|
|
20
|
+
return projectRoot || process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Portable buckets, derived from the read-based classification of the 45-row
|
|
24
|
+
// manifest (see docs/native-workflows-contract.md). Membership is data, not a
|
|
25
|
+
// guess: each name must also exist in the manifest or it is surfaced as drift.
|
|
26
|
+
export const PORTABLE = {
|
|
27
|
+
autonomous: [
|
|
28
|
+
'dev-story',
|
|
29
|
+
'create-story',
|
|
30
|
+
'qa-automate',
|
|
31
|
+
'testarch-atdd',
|
|
32
|
+
'testarch-automate',
|
|
33
|
+
'testarch-ci',
|
|
34
|
+
'testarch-framework',
|
|
35
|
+
'testarch-nfr',
|
|
36
|
+
'testarch-test-design',
|
|
37
|
+
'testarch-test-review',
|
|
38
|
+
'testarch-trace',
|
|
39
|
+
],
|
|
40
|
+
pipeline: [
|
|
41
|
+
'sprint-planning',
|
|
42
|
+
'code-review',
|
|
43
|
+
'document-project',
|
|
44
|
+
'check-implementation-readiness',
|
|
45
|
+
'quick-dev',
|
|
46
|
+
'create-excalidraw-diagram',
|
|
47
|
+
'create-excalidraw-dataflow',
|
|
48
|
+
'create-excalidraw-flowchart',
|
|
49
|
+
'create-excalidraw-wireframe',
|
|
50
|
+
],
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Bucket of a workflow name. 'gated' = stays LLM-interpreted markdown.
|
|
54
|
+
export function classify(name) {
|
|
55
|
+
if (PORTABLE.autonomous.includes(name)) return 'autonomous';
|
|
56
|
+
if (PORTABLE.pipeline.includes(name)) return 'pipeline';
|
|
57
|
+
return 'gated';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function nativeRel(name) {
|
|
61
|
+
return `.claude/workflows/${name}.js`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Dual-path resolver. Prefers the native script .claude/workflows/<name>.js if
|
|
65
|
+
// it exists, else falls back to the markdown workflow path from the manifest.
|
|
66
|
+
// Pure existence check, no side effects. Returns {name, kind, rel, path} or null.
|
|
67
|
+
export function resolveWorkflow(name, { projectRoot, workflows } = {}) {
|
|
68
|
+
const root = resolveRoot(projectRoot);
|
|
69
|
+
const nativeAbs = path.join(root, '.claude', 'workflows', `${name}.js`);
|
|
70
|
+
if (fs.existsSync(nativeAbs)) {
|
|
71
|
+
return { name, kind: 'native', rel: nativeRel(name), path: nativeAbs };
|
|
72
|
+
}
|
|
73
|
+
const rows = workflows || loadManifests(root).workflows;
|
|
74
|
+
const row = rows.find((w) => w.name === name);
|
|
75
|
+
if (row && row.path) {
|
|
76
|
+
return { name, kind: 'markdown', rel: row.path, path: path.join(root, row.path) };
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Render the human/agent-readable registry of portable workflows. Deterministic:
|
|
82
|
+
// sorted, no timestamp. native/markdown status reflects which .js exist on disk.
|
|
83
|
+
export function renderRegistry({ workflows = [], projectRoot } = {}) {
|
|
84
|
+
const root = resolveRoot(projectRoot);
|
|
85
|
+
const byName = new Map(workflows.map((w) => [w.name, w]));
|
|
86
|
+
const lines = [];
|
|
87
|
+
lines.push('# BYAN Native Workflows');
|
|
88
|
+
lines.push('');
|
|
89
|
+
lines.push("> Registre des workflows portables vers l'outil Workflow natif de Claude Code.");
|
|
90
|
+
lines.push('> Genere automatiquement — ne pas editer a la main. Source : `_byan/_config/workflow-manifest.csv`.');
|
|
91
|
+
lines.push('> Regenerer : `node _byan/mcp/byan-mcp-server/bin/byan-build-workflows.js`.');
|
|
92
|
+
lines.push('>');
|
|
93
|
+
lines.push("> Resolution dual-path : le skill prefere `.claude/workflows/<name>.js` s'il existe,");
|
|
94
|
+
lines.push('> sinon il retombe sur le workflow markdown du manifest. Les workflows gated (a gate');
|
|
95
|
+
lines.push('> humain par etape) restent markdown interprete — ils ne sont pas portables.');
|
|
96
|
+
lines.push('');
|
|
97
|
+
|
|
98
|
+
for (const bucket of ['autonomous', 'pipeline']) {
|
|
99
|
+
const names = PORTABLE[bucket].filter((n) => byName.has(n)).slice().sort();
|
|
100
|
+
lines.push(`## ${bucket} (${names.length})`);
|
|
101
|
+
lines.push('');
|
|
102
|
+
for (const name of names) {
|
|
103
|
+
const nativeAbs = path.join(root, '.claude', 'workflows', `${name}.js`);
|
|
104
|
+
const status = fs.existsSync(nativeAbs) ? 'native' : 'markdown';
|
|
105
|
+
const src = byName.get(name).path || '';
|
|
106
|
+
lines.push(`- \`${name}\` — ${status} — source \`${src}\``);
|
|
107
|
+
}
|
|
108
|
+
lines.push('');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Drift guard: a portable name absent from the manifest is a real problem
|
|
112
|
+
// (renamed/removed upstream). Surface it instead of silently dropping it.
|
|
113
|
+
const declared = [...PORTABLE.autonomous, ...PORTABLE.pipeline];
|
|
114
|
+
const missing = declared.filter((n) => !byName.has(n)).slice().sort();
|
|
115
|
+
if (missing.length) {
|
|
116
|
+
lines.push('## drift (noms portables absents du manifest)');
|
|
117
|
+
lines.push('');
|
|
118
|
+
for (const n of missing) lines.push(`- \`${n}\``);
|
|
119
|
+
lines.push('');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return lines.join('\n');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function writeIfChanged(filePath, content) {
|
|
126
|
+
const prev = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : null;
|
|
127
|
+
if (prev === content) return false;
|
|
128
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
129
|
+
fs.writeFileSync(filePath, content);
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Build (or refresh) .claude/workflows/INDEX.md from the manifest. Idempotent.
|
|
134
|
+
export function buildWorkflowsRegistry({ projectRoot } = {}) {
|
|
135
|
+
const root = resolveRoot(projectRoot);
|
|
136
|
+
const { workflows } = loadManifests(root);
|
|
137
|
+
const content = renderRegistry({ workflows, projectRoot: root });
|
|
138
|
+
const indexPath = path.join(root, '.claude', 'workflows', 'INDEX.md');
|
|
139
|
+
const written = writeIfChanged(indexPath, content);
|
|
140
|
+
const inManifest = (n) => workflows.some((w) => w.name === n);
|
|
141
|
+
return {
|
|
142
|
+
written,
|
|
143
|
+
path: indexPath,
|
|
144
|
+
counts: {
|
|
145
|
+
autonomous: PORTABLE.autonomous.filter(inManifest).length,
|
|
146
|
+
pipeline: PORTABLE.pipeline.filter(inManifest).length,
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Linter for native workflow scripts (enforcement-bridge F3).
|
|
2
|
+
//
|
|
3
|
+
// A .claude/workflows/*.js runs OUTSIDE the conversation turn, so BYAN's
|
|
4
|
+
// main-thread hooks (strict-scope-guard, strict-stop-guard, fd-phase-guard) do
|
|
5
|
+
// not fire for it. The structural net that survives is this lint + the
|
|
6
|
+
// pre-commit gate: a native script must NOT couple directly to BYAN state
|
|
7
|
+
// internals. State goes through the byan_fd_* / byan_strict_* MCP tools.
|
|
8
|
+
//
|
|
9
|
+
// Forbidden: importing/requiring lib/fd-state.js (or the strict-mode lib).
|
|
10
|
+
// Comments are stripped before matching so the contract comment in a script
|
|
11
|
+
// that NAMES fd-state.js (to explain the rule) does not self-trip.
|
|
12
|
+
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
|
|
16
|
+
// Strip /* block */ and // line comments. Preserve "://" inside strings (URLs)
|
|
17
|
+
// by only treating // as a comment when not preceded by a colon.
|
|
18
|
+
export function stripComments(src) {
|
|
19
|
+
let s = String(src).replace(/\/\*[\s\S]*?\*\//g, '');
|
|
20
|
+
s = s.replace(/(^|[^:])\/\/[^\n]*/g, '$1');
|
|
21
|
+
return s;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const RULES = [
|
|
25
|
+
{
|
|
26
|
+
id: 'import-fd-state',
|
|
27
|
+
re: /\bimport\b[^\n;]*?from\s*['"][^'"]*fd-state[^'"]*['"]/,
|
|
28
|
+
msg: 'import of fd-state is forbidden; mutate FD state via the byan_fd_* MCP tools',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 'require-fd-state',
|
|
32
|
+
re: /\brequire\s*\(\s*['"][^'"]*fd-state[^'"]*['"]\s*\)/,
|
|
33
|
+
msg: 'require of fd-state is forbidden; mutate FD state via the byan_fd_* MCP tools',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'dynamic-import-fd-state',
|
|
37
|
+
re: /\bimport\s*\(\s*['"][^'"]*fd-state[^'"]*['"]\s*\)/,
|
|
38
|
+
msg: 'dynamic import of fd-state is forbidden; mutate FD state via the byan_fd_* MCP tools',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: 'import-strict-mode-lib',
|
|
42
|
+
re: /\b(?:import\b[^\n;]*?from\s*|require\s*\(\s*|import\s*\(\s*)['"][^'"]*lib\/strict-mode[^'"]*['"]/,
|
|
43
|
+
msg: 'import of the strict-mode lib is forbidden; use the byan_strict_* MCP tools',
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
// Lint one script's source for state coupling. Comment-stripped so a contract
|
|
48
|
+
// comment that NAMES fd-state does not self-trip. Returns [{ id, msg }].
|
|
49
|
+
export function lintSource(src) {
|
|
50
|
+
const code = stripComments(src);
|
|
51
|
+
const out = [];
|
|
52
|
+
for (const rule of RULES) {
|
|
53
|
+
if (rule.re.test(code)) out.push({ id: rule.id, msg: rule.msg });
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Wall-clock / RNG primitives break the Workflow runtime's resume (the launch
|
|
59
|
+
// validator rejects them). It scans the RAW text, so a token in a COMMENT or a
|
|
60
|
+
// string literal breaks invocation just the same - we therefore check the raw
|
|
61
|
+
// source, NOT the comment-stripped one. This is the exact failure that a manual
|
|
62
|
+
// review caught while porting; mechanizing it here keeps it from recurring.
|
|
63
|
+
const CLOCK_RNG_RE = /Date\.now|Math\.random|new Date/;
|
|
64
|
+
|
|
65
|
+
export function clockRngViolations(src) {
|
|
66
|
+
const m = String(src).match(CLOCK_RNG_RE);
|
|
67
|
+
if (!m) return [];
|
|
68
|
+
return [{
|
|
69
|
+
id: 'clock-or-rng',
|
|
70
|
+
msg: `wall-clock/RNG token "${m[0]}" is forbidden anywhere in a native workflow (breaks resume; the launch validator scans raw text - even comments and strings). Pass timestamps/ids via args.`,
|
|
71
|
+
}];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// A native workflow script must start with a pure `export const meta = {` literal
|
|
75
|
+
// (after an optional shebang and blank lines). Otherwise the launch validator
|
|
76
|
+
// rejects it.
|
|
77
|
+
export function metaLiteralViolations(src) {
|
|
78
|
+
const body = String(src).replace(/^/, '');
|
|
79
|
+
const firstReal = body
|
|
80
|
+
.split('\n')
|
|
81
|
+
.map((l) => l.trim())
|
|
82
|
+
.find((l) => l.length > 0 && !l.startsWith('#!'));
|
|
83
|
+
if (firstReal && /^export const meta\s*=\s*\{/.test(firstReal)) return [];
|
|
84
|
+
return [{
|
|
85
|
+
id: 'meta-literal-first',
|
|
86
|
+
msg: 'a native workflow script must begin with `export const meta = {` (pure literal) after an optional shebang',
|
|
87
|
+
}];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Full native-workflow contract: state-coupling (comment-stripped) + clock/RNG
|
|
91
|
+
// (raw) + meta-literal-first. Returns the combined [{ id, msg }] violations.
|
|
92
|
+
export function validateContract(src) {
|
|
93
|
+
return [...lintSource(src), ...clockRngViolations(src), ...metaLiteralViolations(src)];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Lint every *.js in a directory (non-recursive; native workflows are flat)
|
|
97
|
+
// against the FULL contract. Returns [{ file, violations }] for offending files.
|
|
98
|
+
export function lintWorkflowsDir(dir) {
|
|
99
|
+
let entries;
|
|
100
|
+
try {
|
|
101
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
102
|
+
} catch (_e) {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
const results = [];
|
|
106
|
+
for (const e of entries) {
|
|
107
|
+
if (!e.isFile() || !e.name.endsWith('.js')) continue;
|
|
108
|
+
const file = path.join(dir, e.name);
|
|
109
|
+
const violations = validateContract(fs.readFileSync(file, 'utf8'));
|
|
110
|
+
if (violations.length) results.push({ file, violations });
|
|
111
|
+
}
|
|
112
|
+
return results;
|
|
113
|
+
}
|
|
@@ -27,7 +27,7 @@ INIT
|
|
|
27
27
|
→ DISPATCH (Worker: EconomicDispatcher — quelle brique BYAN ?)
|
|
28
28
|
→ BUILD (Agent ou Worker selon complexité)
|
|
29
29
|
→ REVIEW (Agent: Quinn — pre-flight humain vs critères VALIDATE)
|
|
30
|
-
→ VALIDATE (MantraValidator + tests — score ≥
|
|
30
|
+
→ VALIDATE (MantraValidator domain-aware + tests — score ≥ 30 floor anti-stub)
|
|
31
31
|
├─ OK → DOC (Agent: Paige — documenter ce qui a été livré)
|
|
32
32
|
└─ KO → REFACTOR (boucle vers BUILD avec correctifs ciblés)
|
|
33
33
|
→ COMPLETED
|
|
@@ -105,13 +105,16 @@ INIT
|
|
|
105
105
|
**Qui :** Worker — EconomicDispatcher logic
|
|
106
106
|
**Rôle :** Pour chaque feature du backlog, déterminer quelle brique BYAN est impliquée.
|
|
107
107
|
|
|
108
|
-
**Matrice de dispatch
|
|
108
|
+
**Matrice de dispatch** (source de vérité : `byan_dispatch` MCP / `_byan/mcp/byan-mcp-server/lib/dispatch.js`) :
|
|
109
109
|
|
|
110
|
-
| Score complexité |
|
|
111
|
-
|
|
112
|
-
| <
|
|
113
|
-
|
|
|
114
|
-
|
|
|
110
|
+
| Score complexité | Route | Stratégie |
|
|
111
|
+
|------------------|-------|-----------|
|
|
112
|
+
| < 15 | `main-thread` | Inline dans le contexte courant, zéro overhead de délégation |
|
|
113
|
+
| < 40 + parallélisable | `agent-subagent-worktree` | Agent tool Claude Code avec isolation worktree |
|
|
114
|
+
| < 40 séquentiel | `mcp-worker-haiku` | Worker Haiku léger via MCP |
|
|
115
|
+
| ≥ 40 | `main-thread-opus` | Garde en main thread, raisonnement Opus |
|
|
116
|
+
|
|
117
|
+
> Le score (0-100) est estimé depuis la complexité de la tâche (longueur si absent). Appeler `byan_dispatch` pour le calcul — ne pas réinventer les seuils ici.
|
|
115
118
|
|
|
116
119
|
**Questions posées pour chaque feature :**
|
|
117
120
|
1. Un **Agent existant** peut-il gérer ça ? (lister les candidats)
|
|
@@ -136,7 +139,7 @@ INIT
|
|
|
136
139
|
|
|
137
140
|
## Étape 5 : BUILD
|
|
138
141
|
|
|
139
|
-
**Qui :** Agent
|
|
142
|
+
**Qui :** Agent ou worker selon la route `byan_dispatch` (voir la matrice Étape 4)
|
|
140
143
|
**Rôle :** Implémenter la feature — code, agent, workflow, ou context.
|
|
141
144
|
|
|
142
145
|
**Règles BUILD :**
|
|
@@ -165,7 +168,7 @@ INIT
|
|
|
165
168
|
**Protocole :**
|
|
166
169
|
1. Charger les critères VALIDATE attendus :
|
|
167
170
|
- Tests prévus pour cette feature (liste TDD de l'étape BUILD)
|
|
168
|
-
- Score MantraValidator cible (≥
|
|
171
|
+
- Score MantraValidator cible (≥ 30, floor anti-stub domain-aware)
|
|
169
172
|
- Mantras les plus à risque selon le type de changement
|
|
170
173
|
2. Quinn (ou reviewer) inspecte le diff :
|
|
171
174
|
- Lisibilité, nommage, taille des fonctions
|
|
@@ -197,13 +200,13 @@ INIT
|
|
|
197
200
|
|
|
198
201
|
**Protocole :**
|
|
199
202
|
1. Lancer `npm test` — tous les tests doivent passer (zéro régression)
|
|
200
|
-
2. Score MantraValidator ≥
|
|
203
|
+
2. Score MantraValidator domain-aware ≥ 30 (floor anti-stub)
|
|
201
204
|
3. Fact-check final sur tout claim absolu introduit dans la doc
|
|
202
205
|
4. BYAN challenge la feature une dernière fois :
|
|
203
206
|
- "Est-ce que c'est la solution la plus simple ?" (mantra #37)
|
|
204
207
|
- "Quelles sont les conséquences non voulues ?" (mantra #39)
|
|
205
208
|
5. **Décision binaire :**
|
|
206
|
-
- Tests verts ET score ≥
|
|
209
|
+
- Tests verts ET score ≥ 30 (floor anti-stub) ET fact-check OK → `VALIDATE: OK` → étape DOC
|
|
207
210
|
- Sinon → `VALIDATE: KO` → étape REFACTOR
|
|
208
211
|
|
|
209
212
|
**Output :** Verdict `{ status: "OK" | "KO", tests, mantra_score, blocking_issues }`
|