create-byan-agent 2.9.4 → 2.9.6
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/install/bin/byan-cleanup.js +156 -0
- package/install/bin/byan-kanban.js +159 -0
- package/install/bin/byan-ledger.js +45 -0
- package/install/bin/create-byan-agent-v2.js +15 -1
- package/install/lib/cleanup/detector.js +154 -0
- package/install/lib/cleanup/executor.js +72 -0
- package/install/lib/staging-consent.js +149 -0
- package/install/lib/subagent-generator.js +208 -0
- package/install/lib/token-ledger.js +131 -0
- package/install/templates/.claude/agents/bmad-bmad-master.md +14 -0
- package/install/templates/.claude/agents/bmad-bmb-agent-builder.md +14 -0
- package/install/templates/.claude/agents/bmad-bmb-module-builder.md +14 -0
- package/install/templates/.claude/agents/bmad-bmb-workflow-builder.md +14 -0
- package/install/templates/.claude/agents/bmad-bmm-analyst.md +14 -0
- package/install/templates/.claude/agents/bmad-bmm-architect.md +14 -0
- package/install/templates/.claude/agents/bmad-bmm-dev.md +14 -0
- package/install/templates/.claude/agents/bmad-bmm-pm.md +14 -0
- package/install/templates/.claude/agents/bmad-bmm-quick-flow-solo-dev.md +14 -0
- package/install/templates/.claude/agents/bmad-bmm-quinn.md +14 -0
- package/install/templates/.claude/agents/bmad-bmm-sm.md +14 -0
- package/install/templates/.claude/agents/bmad-bmm-tech-writer.md +14 -0
- package/install/templates/.claude/agents/bmad-bmm-ux-designer.md +14 -0
- package/install/templates/.claude/agents/bmad-byan-v2.md +14 -0
- package/install/templates/.claude/agents/bmad-byan.md +152 -0
- package/install/templates/.claude/agents/bmad-carmack.md +14 -0
- package/install/templates/.claude/agents/bmad-cis-brainstorming-coach.md +14 -0
- package/install/templates/.claude/agents/bmad-cis-creative-problem-solver.md +14 -0
- package/install/templates/.claude/agents/bmad-cis-design-thinking-coach.md +14 -0
- package/install/templates/.claude/agents/bmad-cis-innovation-strategist.md +14 -0
- package/install/templates/.claude/agents/bmad-cis-presentation-master.md +14 -0
- package/install/templates/.claude/agents/bmad-cis-storyteller.md +14 -0
- package/install/templates/.claude/agents/bmad-claude.md +26 -0
- package/install/templates/.claude/agents/bmad-codex.md +26 -0
- package/install/templates/.claude/agents/bmad-compliance.md +68 -0
- package/install/templates/.claude/agents/bmad-drawio.md +25 -0
- package/install/templates/.claude/agents/bmad-expert-merise-agile.md +54 -0
- package/install/templates/.claude/agents/bmad-fact-checker.md +14 -0
- package/install/templates/.claude/agents/bmad-forgeron.md +14 -0
- package/install/templates/.claude/agents/bmad-hermes.md +59 -0
- package/install/templates/.claude/agents/bmad-marc.md +25 -0
- package/install/templates/.claude/agents/bmad-patnote.md +26 -0
- package/install/templates/.claude/agents/bmad-rachid.md +25 -0
- package/install/templates/.claude/agents/bmad-tao.md +14 -0
- package/install/templates/.claude/agents/bmad-tea-tea.md +14 -0
- package/install/templates/.claude/agents/bmad-yanstaller.md +47 -0
- package/install/templates/.claude/hooks/fact-check-absolutes.js +185 -0
- package/install/templates/.claude/hooks/fd-phase-guard.js +87 -0
- package/install/templates/.claude/hooks/fd-response-check.js +92 -0
- package/install/templates/.claude/hooks/lib/failure-detector.js +14 -0
- package/install/templates/.claude/hooks/pre-compact-save.js +148 -0
- package/install/templates/.claude/hooks/stage-to-byan.js +119 -0
- package/install/templates/.claude/hooks/tool-failure-guard.js +6 -0
- package/install/templates/.claude/hooks/tool-transparency.js +4 -0
- package/install/templates/.claude/settings.json +27 -0
- package/install/templates/.claude/skills/byan-byan/SKILL.md +115 -163
- package/install/templates/.claude/skills/byan-orchestrate/SKILL.md +100 -0
- package/install/templates/.githooks/pre-commit +75 -0
- package/install/templates/.github/extensions/byan-staging/extension.mjs +169 -0
- package/install/templates/.github/extensions/byan-staging/package.json +8 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/copilot.js +148 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/fd-state.js +163 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/kanban.js +226 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/peer-review.js +187 -0
- package/install/templates/_byan/mcp/byan-mcp-server/server.js +463 -0
- package/install/templates/detector.js +154 -0
- package/package.json +6 -7
- package/src/loadbalancer/capability-matrix.js +157 -0
- package/src/loadbalancer/config.js +141 -0
- package/src/loadbalancer/graceful-degradation.js +212 -0
- package/src/loadbalancer/health-probe.js +151 -0
- package/src/loadbalancer/hooks/claude-hooks.js +53 -0
- package/src/loadbalancer/hooks/copilot-hooks.js +74 -0
- package/src/loadbalancer/index.js +81 -0
- package/src/loadbalancer/loadbalancer.default.yaml +65 -0
- package/src/loadbalancer/loadbalancer.js +324 -0
- package/src/loadbalancer/mcp-server.js +304 -0
- package/src/loadbalancer/metrics.js +146 -0
- package/src/loadbalancer/native/claude-integration.js +64 -0
- package/src/loadbalancer/native/copilot-integration.js +59 -0
- package/src/loadbalancer/pressure-score.js +102 -0
- package/src/loadbalancer/providers/base-provider.js +80 -0
- package/src/loadbalancer/providers/byan-api-provider.js +132 -0
- package/src/loadbalancer/providers/claude-provider.js +113 -0
- package/src/loadbalancer/providers/copilot-provider.js +104 -0
- package/src/loadbalancer/rate-limit-tracker.js +216 -0
- package/src/loadbalancer/session-bridge.js +179 -0
- package/src/loadbalancer/state/db.js +211 -0
- package/src/loadbalancer/state/migrations/001-initial.sql +50 -0
- package/src/loadbalancer/tools/index.js +123 -0
- package/src/loadbalancer/velocity-estimator.js +147 -0
- package/src/staging/staging.js +394 -0
- package/update-byan-agent/bin/update-byan-agent.js +27 -2
- package/API-BYAN-V2.md +0 -741
- package/BMAD-QUICK-REFERENCE.md +0 -370
- package/CHANGELOG-v2.1.0.md +0 -371
- package/MIGRATION-v2.0-to-v2.1.md +0 -430
- package/README-BYAN-V2.md +0 -446
- package/TEST-GUIDE-v2.3.2.md +0 -161
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# BYAN pre-commit hook — enforce mantras score >= 80% on staged agent
|
|
3
|
+
# and skill files. Blocks the commit if any file drops below the
|
|
4
|
+
# threshold so the user can't accidentally push non-compliant artefacts.
|
|
5
|
+
#
|
|
6
|
+
# Install :
|
|
7
|
+
# git config core.hooksPath .githooks
|
|
8
|
+
#
|
|
9
|
+
# Bypass (emergency only) :
|
|
10
|
+
# git commit --no-verify
|
|
11
|
+
#
|
|
12
|
+
# Scope : only files matching
|
|
13
|
+
# _byan/bmb/agents/*.md, _byan/agents/*.md,
|
|
14
|
+
# .github/agents/*.md, .claude/skills/*/SKILL.md,
|
|
15
|
+
# .claude/agents/*.md
|
|
16
|
+
# are validated. Non-agent files are ignored.
|
|
17
|
+
|
|
18
|
+
set -euo pipefail
|
|
19
|
+
|
|
20
|
+
THRESHOLD=80
|
|
21
|
+
VALIDATOR="src/byan-v2/generation/mantra-validator.js"
|
|
22
|
+
|
|
23
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
24
|
+
echo "[byan pre-commit] node not found, skipping mantra check"
|
|
25
|
+
exit 0
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
if [ ! -f "$VALIDATOR" ]; then
|
|
29
|
+
exit 0
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
staged=$(git diff --cached --name-only --diff-filter=ACM | grep -E '^(_byan/bmb/agents/.*\.md|_byan/agents/.*\.md|\.github/agents/.*\.md|\.claude/skills/.*SKILL\.md|\.claude/agents/.*\.md)$' || true)
|
|
33
|
+
|
|
34
|
+
if [ -z "$staged" ]; then
|
|
35
|
+
exit 0
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
failed=0
|
|
39
|
+
while IFS= read -r file; do
|
|
40
|
+
[ -z "$file" ] && continue
|
|
41
|
+
[ ! -f "$file" ] && continue
|
|
42
|
+
|
|
43
|
+
score=$(node -e "
|
|
44
|
+
const V = require('./$VALIDATOR');
|
|
45
|
+
const fs = require('fs');
|
|
46
|
+
try {
|
|
47
|
+
const content = fs.readFileSync('$file', 'utf8');
|
|
48
|
+
const v = new V();
|
|
49
|
+
const res = v.validate(content);
|
|
50
|
+
const pct = Math.round((res.compliant.length / res.totalMantras) * 100);
|
|
51
|
+
process.stdout.write(String(pct));
|
|
52
|
+
} catch (e) {
|
|
53
|
+
process.stderr.write(e.message);
|
|
54
|
+
process.stdout.write('0');
|
|
55
|
+
}
|
|
56
|
+
" 2>/dev/null || echo "0")
|
|
57
|
+
|
|
58
|
+
if [ -z "$score" ] || [ "$score" = "0" ]; then
|
|
59
|
+
continue
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
if [ "$score" -lt "$THRESHOLD" ]; then
|
|
63
|
+
echo "[byan pre-commit] FAIL $file : mantra score $score% < $THRESHOLD%"
|
|
64
|
+
failed=1
|
|
65
|
+
fi
|
|
66
|
+
done <<< "$staged"
|
|
67
|
+
|
|
68
|
+
if [ "$failed" -eq 1 ]; then
|
|
69
|
+
echo ""
|
|
70
|
+
echo "Commit blocked by BYAN mantra pre-commit hook."
|
|
71
|
+
echo "Fix the flagged files above, or bypass with 'git commit --no-verify' (emergency only)."
|
|
72
|
+
exit 1
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
exit 0
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copilot CLI extension — BYAN staging (SM1c).
|
|
3
|
+
*
|
|
4
|
+
* Attaches to the current Copilot session via joinSession() and
|
|
5
|
+
* triggers the BYAN staging pipeline at the end of each assistant
|
|
6
|
+
* turn. Delegates to src/staging/staging.js so Claude Code and Copilot
|
|
7
|
+
* CLI share the exact same extract / filter / dedup / queue / flush
|
|
8
|
+
* logic.
|
|
9
|
+
*
|
|
10
|
+
* How it's discovered : Copilot CLI scans .github/extensions/
|
|
11
|
+
* (project) and the user's copilot config for subdirectories
|
|
12
|
+
* containing extension.mjs. This file is auto-launched as a child
|
|
13
|
+
* process, gets @github/copilot-sdk on its module path, and calls
|
|
14
|
+
* joinSession with the hook registration below.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { joinSession } from '@github/copilot-sdk/extension';
|
|
18
|
+
import { createRequire } from 'node:module';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
|
|
22
|
+
const require = createRequire(import.meta.url);
|
|
23
|
+
|
|
24
|
+
const EXTENSION_DIR = path.dirname(new URL(import.meta.url).pathname);
|
|
25
|
+
const PROJECT_ROOT =
|
|
26
|
+
process.env.BYAN_PROJECT_ROOT ||
|
|
27
|
+
process.env.CLAUDE_PROJECT_DIR ||
|
|
28
|
+
findProjectRoot(EXTENSION_DIR);
|
|
29
|
+
|
|
30
|
+
function findProjectRoot(startDir) {
|
|
31
|
+
// Walk up until we find a package.json or .git — else cwd
|
|
32
|
+
let dir = startDir;
|
|
33
|
+
for (let i = 0; i < 6; i++) {
|
|
34
|
+
if (
|
|
35
|
+
fs.existsSync(path.join(dir, 'package.json')) ||
|
|
36
|
+
fs.existsSync(path.join(dir, '.git'))
|
|
37
|
+
) {
|
|
38
|
+
return dir;
|
|
39
|
+
}
|
|
40
|
+
const parent = path.dirname(dir);
|
|
41
|
+
if (parent === dir) break;
|
|
42
|
+
dir = parent;
|
|
43
|
+
}
|
|
44
|
+
return process.cwd();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function loadStaging() {
|
|
48
|
+
try {
|
|
49
|
+
return require(path.join(PROJECT_ROOT, 'src', 'staging', 'staging.js'));
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function readSettingsEnv() {
|
|
56
|
+
const p = path.join(PROJECT_ROOT, '.claude', 'settings.local.json');
|
|
57
|
+
if (!fs.existsSync(p)) return {};
|
|
58
|
+
try {
|
|
59
|
+
const j = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
60
|
+
return j.env || {};
|
|
61
|
+
} catch {
|
|
62
|
+
return {};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function readMemorySyncConfig() {
|
|
67
|
+
const paths = [
|
|
68
|
+
path.join(PROJECT_ROOT, 'loadbalancer.yaml'),
|
|
69
|
+
path.join(PROJECT_ROOT, '_byan', 'config.yaml'),
|
|
70
|
+
];
|
|
71
|
+
for (const p of paths) {
|
|
72
|
+
if (!fs.existsSync(p)) continue;
|
|
73
|
+
try {
|
|
74
|
+
const yaml = require('js-yaml');
|
|
75
|
+
const doc = yaml.load(fs.readFileSync(p, 'utf8'));
|
|
76
|
+
if (doc && doc.memory_sync) return doc.memory_sync;
|
|
77
|
+
} catch {
|
|
78
|
+
// fall through
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function buildConfig() {
|
|
85
|
+
const envFile = readSettingsEnv();
|
|
86
|
+
return {
|
|
87
|
+
byan_api_url: envFile.BYAN_API_URL || process.env.BYAN_API_URL || null,
|
|
88
|
+
byan_api_token: envFile.BYAN_API_TOKEN || process.env.BYAN_API_TOKEN || null,
|
|
89
|
+
memory_sync: readMemorySyncConfig() || {},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Per-turn buffer of recent messages/tool calls keyed by sessionId.
|
|
94
|
+
const turnBuffer = new Map();
|
|
95
|
+
|
|
96
|
+
function bufferFor(sessionId) {
|
|
97
|
+
const key = sessionId || 'default';
|
|
98
|
+
if (!turnBuffer.has(key)) {
|
|
99
|
+
turnBuffer.set(key, { userMessages: [], assistantMessages: [], toolCalls: [] });
|
|
100
|
+
}
|
|
101
|
+
return turnBuffer.get(key);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function clearBuffer(sessionId) {
|
|
105
|
+
turnBuffer.delete(sessionId || 'default');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const session = await joinSession({
|
|
109
|
+
hooks: {
|
|
110
|
+
onSessionStart: async () => {
|
|
111
|
+
await session.log('BYAN staging extension loaded', { ephemeral: true });
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
onUserPromptSubmitted: async (input) => {
|
|
115
|
+
const buf = bufferFor(input.sessionId);
|
|
116
|
+
buf.userMessages.push({ role: 'user', content: input.prompt });
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
onPreToolUse: async (input) => {
|
|
120
|
+
const buf = bufferFor(input.sessionId);
|
|
121
|
+
buf.toolCalls.push({
|
|
122
|
+
name: input.toolName,
|
|
123
|
+
input: input.toolArgs || {},
|
|
124
|
+
});
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
onPostToolUse: async (input) => {
|
|
128
|
+
const buf = bufferFor(input.sessionId);
|
|
129
|
+
// If the last tool call has a text result, treat it as assistant output
|
|
130
|
+
if (input.toolResult && typeof input.toolResult.content === 'string') {
|
|
131
|
+
buf.assistantMessages.push({
|
|
132
|
+
role: 'assistant',
|
|
133
|
+
content: input.toolResult.content,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
onSessionEnd: async (input) => {
|
|
139
|
+
const staging = loadStaging();
|
|
140
|
+
if (!staging) {
|
|
141
|
+
clearBuffer(input?.sessionId);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const buf = bufferFor(input?.sessionId);
|
|
146
|
+
const turn = {
|
|
147
|
+
sessionId: input?.sessionId || null,
|
|
148
|
+
messages: [...buf.userMessages, ...buf.assistantMessages],
|
|
149
|
+
toolCalls: buf.toolCalls,
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
await staging.processTurn({
|
|
154
|
+
turn,
|
|
155
|
+
cliSource: 'copilot-cli',
|
|
156
|
+
config: buildConfig(),
|
|
157
|
+
projectRoot: PROJECT_ROOT,
|
|
158
|
+
flushNow: true,
|
|
159
|
+
});
|
|
160
|
+
} catch {
|
|
161
|
+
// never block session end
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
clearBuffer(input?.sessionId);
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
tools: [],
|
|
169
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@byan/copilot-staging-extension",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Copilot CLI extension that stages conversation knowledge to byan_web /api/memory via the shared BYAN staging core.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "extension.mjs",
|
|
7
|
+
"private": true
|
|
8
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
|
|
5
|
+
const COPILOT_ROOT = process.env.BYAN_COPILOT_ROOT || path.join(os.homedir(), '.copilot', 'session-state');
|
|
6
|
+
|
|
7
|
+
function readJsonl(filePath, limit) {
|
|
8
|
+
if (!fs.existsSync(filePath)) return [];
|
|
9
|
+
const lines = fs.readFileSync(filePath, 'utf8').split('\n').filter(Boolean);
|
|
10
|
+
const out = [];
|
|
11
|
+
for (const line of lines) {
|
|
12
|
+
try {
|
|
13
|
+
out.push(JSON.parse(line));
|
|
14
|
+
} catch {
|
|
15
|
+
// skip malformed
|
|
16
|
+
}
|
|
17
|
+
if (typeof limit === 'number' && out.length >= limit) break;
|
|
18
|
+
}
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function summarizeSession(sessionId) {
|
|
23
|
+
const eventsPath = path.join(COPILOT_ROOT, sessionId, 'events.jsonl');
|
|
24
|
+
if (!fs.existsSync(eventsPath)) return null;
|
|
25
|
+
|
|
26
|
+
const events = readJsonl(eventsPath);
|
|
27
|
+
if (events.length === 0) return null;
|
|
28
|
+
|
|
29
|
+
const start = events.find((e) => e.type === 'session.start');
|
|
30
|
+
const shutdown = events.find((e) => e.type === 'session.shutdown');
|
|
31
|
+
const agent = events.find((e) => e.type === 'subagent.selected');
|
|
32
|
+
|
|
33
|
+
const counts = {};
|
|
34
|
+
for (const e of events) {
|
|
35
|
+
counts[e.type] = (counts[e.type] || 0) + 1;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const userMessages = events.filter((e) => e.type === 'user.message');
|
|
39
|
+
const assistantMessages = events.filter((e) => e.type === 'assistant.message');
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
sessionId,
|
|
43
|
+
startTime: start?.data?.startTime || null,
|
|
44
|
+
endTime: shutdown?.timestamp || null,
|
|
45
|
+
cwd: start?.data?.context?.cwd || null,
|
|
46
|
+
branch: start?.data?.context?.branch || null,
|
|
47
|
+
agent: agent?.data?.agentName || null,
|
|
48
|
+
event_count: events.length,
|
|
49
|
+
user_messages: userMessages.length,
|
|
50
|
+
assistant_messages: assistantMessages.length,
|
|
51
|
+
tool_calls: counts['tool.execution_start'] || 0,
|
|
52
|
+
event_type_counts: counts,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function listSessions({ limit = 20, sinceIso = null, cwdFilter = null } = {}) {
|
|
57
|
+
if (!fs.existsSync(COPILOT_ROOT)) return { root: COPILOT_ROOT, sessions: [], total: 0, exists: false };
|
|
58
|
+
|
|
59
|
+
const dirs = fs
|
|
60
|
+
.readdirSync(COPILOT_ROOT, { withFileTypes: true })
|
|
61
|
+
.filter((d) => d.isDirectory())
|
|
62
|
+
.map((d) => d.name);
|
|
63
|
+
|
|
64
|
+
const summaries = [];
|
|
65
|
+
for (const id of dirs) {
|
|
66
|
+
const s = summarizeSession(id);
|
|
67
|
+
if (!s) continue;
|
|
68
|
+
if (sinceIso && s.startTime && Date.parse(s.startTime) < Date.parse(sinceIso)) continue;
|
|
69
|
+
if (cwdFilter && s.cwd && !s.cwd.includes(cwdFilter)) continue;
|
|
70
|
+
summaries.push(s);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
summaries.sort((a, b) => {
|
|
74
|
+
const at = Date.parse(a.startTime || 0);
|
|
75
|
+
const bt = Date.parse(b.startTime || 0);
|
|
76
|
+
return bt - at;
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
root: COPILOT_ROOT,
|
|
81
|
+
total: summaries.length,
|
|
82
|
+
exists: true,
|
|
83
|
+
sessions: summaries.slice(0, limit),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function readSessionEvents({ sessionId, types = null, limit = 200 } = {}) {
|
|
88
|
+
if (!sessionId || typeof sessionId !== 'string') {
|
|
89
|
+
throw new Error('sessionId is required');
|
|
90
|
+
}
|
|
91
|
+
const eventsPath = path.join(COPILOT_ROOT, sessionId, 'events.jsonl');
|
|
92
|
+
if (!fs.existsSync(eventsPath)) {
|
|
93
|
+
throw new Error(`events.jsonl not found for session ${sessionId}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const allEvents = readJsonl(eventsPath);
|
|
97
|
+
const filtered = Array.isArray(types) && types.length > 0
|
|
98
|
+
? allEvents.filter((e) => types.includes(e.type))
|
|
99
|
+
: allEvents;
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
sessionId,
|
|
103
|
+
total: allEvents.length,
|
|
104
|
+
returned: Math.min(filtered.length, limit),
|
|
105
|
+
filtered_by_type: Array.isArray(types) ? types : null,
|
|
106
|
+
events: filtered.slice(0, limit),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function searchSessions({ query, types = ['user.message', 'assistant.message'], limit = 50 } = {}) {
|
|
111
|
+
if (!query || typeof query !== 'string') {
|
|
112
|
+
throw new Error('query is required');
|
|
113
|
+
}
|
|
114
|
+
if (!fs.existsSync(COPILOT_ROOT)) return { matches: [], total: 0 };
|
|
115
|
+
|
|
116
|
+
const q = query.toLowerCase();
|
|
117
|
+
const dirs = fs
|
|
118
|
+
.readdirSync(COPILOT_ROOT, { withFileTypes: true })
|
|
119
|
+
.filter((d) => d.isDirectory())
|
|
120
|
+
.map((d) => d.name);
|
|
121
|
+
|
|
122
|
+
const matches = [];
|
|
123
|
+
for (const sessionId of dirs) {
|
|
124
|
+
const eventsPath = path.join(COPILOT_ROOT, sessionId, 'events.jsonl');
|
|
125
|
+
if (!fs.existsSync(eventsPath)) continue;
|
|
126
|
+
const events = readJsonl(eventsPath);
|
|
127
|
+
for (const e of events) {
|
|
128
|
+
if (!types.includes(e.type)) continue;
|
|
129
|
+
const text = typeof e.data?.content === 'string'
|
|
130
|
+
? e.data.content
|
|
131
|
+
: typeof e.data?.text === 'string'
|
|
132
|
+
? e.data.text
|
|
133
|
+
: JSON.stringify(e.data || {});
|
|
134
|
+
if (text.toLowerCase().includes(q)) {
|
|
135
|
+
matches.push({
|
|
136
|
+
sessionId,
|
|
137
|
+
timestamp: e.timestamp,
|
|
138
|
+
type: e.type,
|
|
139
|
+
excerpt: text.slice(0, 300),
|
|
140
|
+
});
|
|
141
|
+
if (matches.length >= limit) break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (matches.length >= limit) break;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return { query, total: matches.length, matches };
|
|
148
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const PHASES = ['BRAINSTORM', 'PRUNE', 'DISPATCH', 'BUILD', 'VALIDATE', 'COMPLETED', 'ABORTED'];
|
|
5
|
+
|
|
6
|
+
function resolveRoot(projectRoot) {
|
|
7
|
+
return projectRoot || process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function statePath(projectRoot) {
|
|
11
|
+
return path.join(resolveRoot(projectRoot), '_byan-output', 'fd-state.json');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function ensureDir(filePath) {
|
|
15
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function readState(projectRoot) {
|
|
19
|
+
const p = statePath(projectRoot);
|
|
20
|
+
if (!fs.existsSync(p)) return null;
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function writeState(state, projectRoot) {
|
|
29
|
+
const p = statePath(projectRoot);
|
|
30
|
+
ensureDir(p);
|
|
31
|
+
fs.writeFileSync(p, JSON.stringify(state, null, 2));
|
|
32
|
+
return p;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function slugify(s) {
|
|
36
|
+
return String(s || 'feature')
|
|
37
|
+
.toLowerCase()
|
|
38
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
39
|
+
.replace(/(^-|-$)/g, '')
|
|
40
|
+
.slice(0, 40);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function stampId(now = new Date(), slug) {
|
|
44
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
45
|
+
const s =
|
|
46
|
+
now.getFullYear().toString() +
|
|
47
|
+
pad(now.getMonth() + 1) +
|
|
48
|
+
pad(now.getDate()) +
|
|
49
|
+
'-' +
|
|
50
|
+
pad(now.getHours()) +
|
|
51
|
+
pad(now.getMinutes()) +
|
|
52
|
+
pad(now.getSeconds());
|
|
53
|
+
return `${s}-${slugify(slug)}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function start({ featureName, projectRoot, now = new Date(), force = false } = {}) {
|
|
57
|
+
const existing = readState(projectRoot);
|
|
58
|
+
if (existing && !['COMPLETED', 'ABORTED'].includes(existing.phase) && !force) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`FD already in progress (phase ${existing.phase}, fd_id ${existing.fd_id}). Abort or complete it first, or pass force=true.`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const state = {
|
|
65
|
+
fd_id: stampId(now, featureName),
|
|
66
|
+
feature_name: featureName || 'unnamed',
|
|
67
|
+
phase: 'BRAINSTORM',
|
|
68
|
+
started_at: now.toISOString(),
|
|
69
|
+
updated_at: now.toISOString(),
|
|
70
|
+
phase_history: [{ phase: 'BRAINSTORM', entered_at: now.toISOString() }],
|
|
71
|
+
raw_ideas: [],
|
|
72
|
+
backlog: [],
|
|
73
|
+
dispatch_table: [],
|
|
74
|
+
commits: [],
|
|
75
|
+
notes: [],
|
|
76
|
+
};
|
|
77
|
+
writeState(state, projectRoot);
|
|
78
|
+
return state;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function status({ projectRoot } = {}) {
|
|
82
|
+
const state = readState(projectRoot);
|
|
83
|
+
if (!state) {
|
|
84
|
+
return { active: false, phase: null, fd_id: null };
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
active: !['COMPLETED', 'ABORTED'].includes(state.phase),
|
|
88
|
+
...state,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const BRAINSTORM_MIN_IDEAS = 10;
|
|
93
|
+
|
|
94
|
+
export function advance({ to, note, projectRoot, now = new Date(), force = false } = {}) {
|
|
95
|
+
if (!PHASES.includes(to)) {
|
|
96
|
+
throw new Error(`Invalid target phase ${to}. Must be one of ${PHASES.join(', ')}`);
|
|
97
|
+
}
|
|
98
|
+
const state = readState(projectRoot);
|
|
99
|
+
if (!state) throw new Error('No active FD session. Call start() first.');
|
|
100
|
+
if (['COMPLETED', 'ABORTED'].includes(state.phase)) {
|
|
101
|
+
throw new Error(`Current FD session is ${state.phase} and cannot advance.`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const order = PHASES.indexOf(state.phase);
|
|
105
|
+
const target = PHASES.indexOf(to);
|
|
106
|
+
if (target < order && !['ABORTED', 'COMPLETED'].includes(to)) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`Cannot move backwards from ${state.phase} to ${to}. Use abort() or fix the workflow.`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// BRAINSTORM exit gate : need >= BRAINSTORM_MIN_IDEAS raw ideas
|
|
113
|
+
if (
|
|
114
|
+
state.phase === 'BRAINSTORM' &&
|
|
115
|
+
to !== 'BRAINSTORM' &&
|
|
116
|
+
!['ABORTED'].includes(to) &&
|
|
117
|
+
!force
|
|
118
|
+
) {
|
|
119
|
+
const n = Array.isArray(state.raw_ideas) ? state.raw_ideas.length : 0;
|
|
120
|
+
if (n < BRAINSTORM_MIN_IDEAS) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`BRAINSTORM requires at least ${BRAINSTORM_MIN_IDEAS} raw ideas before advancing (currently ${n}). Add more via update({ patch: { raw_ideas: [...] } }), or pass force=true to skip.`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
state.phase = to;
|
|
128
|
+
state.updated_at = now.toISOString();
|
|
129
|
+
state.phase_history.push({ phase: to, entered_at: now.toISOString(), note: note || null });
|
|
130
|
+
|
|
131
|
+
writeState(state, projectRoot);
|
|
132
|
+
return state;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function update({ patch = {}, projectRoot, now = new Date() } = {}) {
|
|
136
|
+
const state = readState(projectRoot);
|
|
137
|
+
if (!state) throw new Error('No active FD session.');
|
|
138
|
+
|
|
139
|
+
const allowed = ['raw_ideas', 'backlog', 'dispatch_table', 'commits', 'notes', 'feature_name'];
|
|
140
|
+
for (const key of Object.keys(patch)) {
|
|
141
|
+
if (!allowed.includes(key)) {
|
|
142
|
+
throw new Error(`Field "${key}" is not patchable. Allowed: ${allowed.join(', ')}`);
|
|
143
|
+
}
|
|
144
|
+
state[key] = patch[key];
|
|
145
|
+
}
|
|
146
|
+
state.updated_at = now.toISOString();
|
|
147
|
+
|
|
148
|
+
writeState(state, projectRoot);
|
|
149
|
+
return state;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function abort({ reason, projectRoot, now = new Date() } = {}) {
|
|
153
|
+
const state = readState(projectRoot);
|
|
154
|
+
if (!state) throw new Error('No FD session to abort.');
|
|
155
|
+
state.phase = 'ABORTED';
|
|
156
|
+
state.updated_at = now.toISOString();
|
|
157
|
+
state.phase_history.push({ phase: 'ABORTED', entered_at: now.toISOString(), note: reason || null });
|
|
158
|
+
writeState(state, projectRoot);
|
|
159
|
+
return state;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export const ALL_PHASES = PHASES;
|
|
163
|
+
export const BRAINSTORM_MIN = BRAINSTORM_MIN_IDEAS;
|