create-byan-agent 2.23.0 → 2.26.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 +230 -0
- package/README.md +9 -12
- package/install/bin/create-byan-agent-v2.js +29 -169
- package/install/lib/agent-generator.js +5 -5
- package/install/lib/byan-web-integration.js +1 -1
- package/install/lib/claude-native-setup.js +1 -1
- package/install/lib/phase2-chat.js +3 -10
- package/install/lib/platforms/claude-code.js +2 -2
- package/install/lib/platforms/index.js +0 -2
- package/install/lib/project-agents-generator.js +3 -3
- package/install/lib/staging-consent.js +3 -3
- package/install/lib/subagent-generator.js +3 -3
- package/install/lib/yanstaller/agent-launcher.js +1 -27
- package/install/lib/yanstaller/detector.js +4 -4
- package/install/lib/yanstaller/installer.js +0 -2
- package/install/lib/yanstaller/interviewer.js +1 -1
- package/install/lib/yanstaller/platform-selector.js +1 -13
- package/install/package.json +1 -1
- package/install/src/byan-v2/context/session-state.js +2 -2
- package/install/src/byan-v2/index.js +2 -6
- package/install/src/byan-v2/orchestrator/generation-state.js +4 -4
- package/install/src/webui/api.js +0 -2
- package/install/src/webui/chat/bridge.js +1 -13
- package/install/src/webui/chat/cli-detector.js +0 -23
- package/install/src/webui/public/app.js +1 -3
- package/install/src/webui/public/chat.html +0 -2
- package/install/src/webui/public/chat.js +0 -1
- package/install/src/webui/public/index.html +2 -2
- package/install/templates/.claude/CLAUDE.md +13 -2
- package/install/templates/.claude/agents/bmad-byan.md +1 -1
- package/install/templates/.claude/hooks/autobench-stop-guard.js +286 -0
- package/install/templates/.claude/hooks/drain-advisory.js +85 -0
- package/install/templates/.claude/hooks/fact-check-absolutes.js +1 -61
- package/install/templates/.claude/hooks/fact-check-claims.js +69 -0
- package/install/templates/.claude/hooks/fd-response-check.js +37 -46
- package/install/templates/.claude/hooks/inject-soul.js +64 -25
- package/install/templates/.claude/hooks/leantime-fd-sync.js +216 -0
- package/install/templates/.claude/hooks/lib/autobench-config.json +81 -0
- package/install/templates/.claude/hooks/lib/autobench-fc-enrich.js +251 -0
- package/install/templates/.claude/hooks/lib/autobench-ledger-report.js +253 -0
- package/install/templates/.claude/hooks/lib/autobench-runtime.js +199 -0
- package/install/templates/.claude/hooks/lib/fact-check-core.js +69 -0
- package/install/templates/.claude/hooks/lib/failure-detector.js +18 -4
- package/install/templates/.claude/hooks/lib/transcript-read.js +137 -0
- package/install/templates/.claude/hooks/soul-memory-check.js +49 -25
- package/install/templates/.claude/hooks/soul-memory-triggers.js +27 -8
- package/install/templates/.claude/hooks/stage-to-byan.js +25 -7
- package/install/templates/.claude/hooks/strict-stop-guard.js +4 -16
- package/install/templates/.claude/rules/benchmark.md +251 -0
- package/install/templates/.claude/rules/byan-agents.md +0 -1
- package/install/templates/.claude/rules/byan-api.md +64 -0
- package/install/templates/.claude/rules/fact-check.md +1 -1
- package/install/templates/.claude/rules/strict-mode.md +10 -9
- package/install/templates/.claude/settings.json +16 -0
- package/install/templates/.claude/skills/byan-benchmark/SKILL.md +159 -0
- package/install/templates/.claude/skills/byan-byan/SKILL.md +73 -12
- package/install/templates/.claude/skills/byan-fact-check/SKILL.md +1 -1
- package/install/templates/.claude/skills/byan-hermes-dispatch/SKILL.md +5 -6
- package/install/templates/.claude/skills/byan-insight/SKILL.md +56 -0
- package/install/templates/.claude/skills/byan-orchestrate/SKILL.md +11 -3
- package/install/templates/.claude/skills/byan-strict/SKILL.md +4 -1
- package/install/templates/.claude/workflows/INDEX.md +2 -1
- package/install/templates/.claude/workflows/byan-benchmark.js +328 -0
- package/install/templates/.claude/workflows/check-implementation-readiness.js +1 -1
- package/install/templates/_byan/_config/agent-manifest.csv +1 -1
- package/install/templates/_byan/_config/autobench.yaml +510 -0
- package/install/templates/_byan/_config/strict-mode.yaml +9 -3
- package/install/templates/_byan/_config/workflow-manifest.csv +1 -0
- package/install/templates/_byan/agent/byan/byan.md +1 -3
- package/install/templates/_byan/agent/byan-flat/byan.md +1 -3
- package/install/templates/_byan/agent/byan-test/byan-test.md +2 -2
- package/install/templates/_byan/agent/byan-test-flat/byan-test.md +2 -2
- package/install/templates/_byan/agent/byan.optimized/byan.optimized.md +2 -2
- package/install/templates/_byan/agent/byan.optimized-v2/byan.optimized-v2.md +2 -2
- package/install/templates/_byan/agent/claude/claude.md +0 -2
- package/install/templates/_byan/agent/codex/codex.md +0 -2
- package/install/templates/_byan/agent/rachid/rachid.md +2 -10
- package/install/templates/_byan/agent/rachid-flat/rachid.md +2 -11
- package/install/templates/_byan/agent/turbo-whisper/turbo-whisper.md +2 -5
- package/install/templates/_byan/agent/turbo-whisper-integration/turbo-whisper-integration.md +5 -13
- package/install/templates/_byan/agent/yanstaller/yanstaller.md +2 -24
- package/install/templates/_byan/config.yaml +0 -1
- package/install/templates/_byan/core/activation/soul-activation.md +3 -3
- package/install/templates/_byan/mcp/byan-mcp-server/bin/byan-insight-digest.js +31 -0
- package/install/templates/_byan/mcp/byan-mcp-server/bin/byan-sync-rules.js +20 -4
- package/install/templates/_byan/mcp/byan-mcp-server/lib/advisory-autofeed.js +96 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/index-generator.js +1 -1
- package/install/templates/_byan/mcp/byan-mcp-server/lib/insight-harvest.js +220 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/kanban.js +6 -3
- package/install/templates/_byan/mcp/byan-mcp-server/lib/leantime-fd-core.js +205 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/leantime-sync.js +415 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/outcome-buffer.js +64 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/precommit-gate.js +1 -1
- package/install/templates/_byan/mcp/byan-mcp-server/lib/strict-activation.js +1 -1
- package/install/templates/_byan/mcp/byan-mcp-server/lib/strict-mode.js +8 -0
- package/install/templates/_byan/mcp/byan-mcp-server/lib/sync-rules.js +172 -23
- package/install/templates/_byan/mcp/byan-mcp-server/lib/workflows-generator.js +1 -0
- package/install/templates/_byan/mcp/byan-mcp-server/server.js +262 -81
- package/install/templates/_byan/worker/launchers/README.md +4 -24
- package/install/templates/_byan/worker/workers.md +8 -9
- package/install/templates/_byan/workflow/simple/bmb/byan-benchmark/workflow.md +86 -0
- package/install/templates/_byan/workflow/simple/byan/feature-workflow.md +2 -2
- package/install/templates/docs/leantime-integration.md +160 -0
- package/package.json +3 -7
- package/src/byan-v2/context/session-state.js +2 -2
- package/src/byan-v2/generation/mantra-validator.js +3 -3
- package/src/byan-v2/index.js +1 -5
- package/src/byan-v2/integration/voice-integration.js +1 -1
- package/src/byan-v2/orchestrator/generation-state.js +4 -4
- package/src/loadbalancer/loadbalancer.js +1 -1
- package/src/staging/staging.js +20 -6
- package/install/bin/build-copilot-stubs.js +0 -138
- package/install/lib/platforms/copilot-cli.js +0 -123
- package/install/lib/platforms/vscode.js +0 -51
- package/install/src/byan-v2/context/copilot-context.js +0 -79
- package/install/src/webui/chat/copilot-adapter.js +0 -68
- package/install/templates/.claude/agents/bmad-marc.md +0 -25
- package/install/templates/.claude/skills/byan-marc/SKILL.md +0 -20
- package/install/templates/.github/agents/bmad-agent-bmad-master.md +0 -16
- package/install/templates/.github/agents/bmad-agent-bmb-agent-builder.md +0 -16
- package/install/templates/.github/agents/bmad-agent-bmb-module-builder.md +0 -16
- package/install/templates/.github/agents/bmad-agent-bmb-workflow-builder.md +0 -16
- package/install/templates/.github/agents/bmad-agent-bmm-analyst.md +0 -16
- package/install/templates/.github/agents/bmad-agent-bmm-architect.md +0 -16
- package/install/templates/.github/agents/bmad-agent-bmm-dev.md +0 -16
- package/install/templates/.github/agents/bmad-agent-bmm-pm.md +0 -16
- package/install/templates/.github/agents/bmad-agent-bmm-quick-flow-solo-dev.md +0 -16
- package/install/templates/.github/agents/bmad-agent-bmm-quinn.md +0 -16
- package/install/templates/.github/agents/bmad-agent-bmm-sm.md +0 -16
- package/install/templates/.github/agents/bmad-agent-bmm-tech-writer.md +0 -16
- package/install/templates/.github/agents/bmad-agent-bmm-ux-designer.md +0 -16
- package/install/templates/.github/agents/bmad-agent-byan-test.md +0 -33
- package/install/templates/.github/agents/bmad-agent-byan-v2.md +0 -44
- package/install/templates/.github/agents/bmad-agent-byan.md +0 -1062
- package/install/templates/.github/agents/bmad-agent-carmack.md +0 -14
- package/install/templates/.github/agents/bmad-agent-cis-brainstorming-coach.md +0 -16
- package/install/templates/.github/agents/bmad-agent-cis-creative-problem-solver.md +0 -16
- package/install/templates/.github/agents/bmad-agent-cis-design-thinking-coach.md +0 -16
- package/install/templates/.github/agents/bmad-agent-cis-innovation-strategist.md +0 -16
- package/install/templates/.github/agents/bmad-agent-cis-presentation-master.md +0 -16
- package/install/templates/.github/agents/bmad-agent-cis-storyteller.md +0 -16
- package/install/templates/.github/agents/bmad-agent-claude.md +0 -49
- package/install/templates/.github/agents/bmad-agent-codex.md +0 -49
- package/install/templates/.github/agents/bmad-agent-drawio.md +0 -45
- package/install/templates/.github/agents/bmad-agent-fact-checker.md +0 -16
- package/install/templates/.github/agents/bmad-agent-forgeron.md +0 -15
- package/install/templates/.github/agents/bmad-agent-jimmy.md +0 -15
- package/install/templates/.github/agents/bmad-agent-marc.md +0 -49
- package/install/templates/.github/agents/bmad-agent-mike.md +0 -15
- package/install/templates/.github/agents/bmad-agent-patnote.md +0 -49
- package/install/templates/.github/agents/bmad-agent-rachid.md +0 -48
- package/install/templates/.github/agents/bmad-agent-skeptic.md +0 -16
- package/install/templates/.github/agents/bmad-agent-tao.md +0 -14
- package/install/templates/.github/agents/bmad-agent-tea-tea.md +0 -16
- package/install/templates/.github/agents/bmad-agent-test-dynamic.md +0 -22
- package/install/templates/.github/agents/bmad-agent-yanstaller-interview.md +0 -50
- package/install/templates/.github/agents/bmad-agent-yanstaller-phase2.md +0 -189
- package/install/templates/.github/agents/bmad-agent-yanstaller.md +0 -350
- package/install/templates/.github/agents/expert-merise-agile.md +0 -178
- package/install/templates/.github/agents/franck.md +0 -379
- package/install/templates/.github/agents/hermes.md +0 -575
- package/install/templates/.github/extensions/byan-staging/extension.mjs +0 -169
- package/install/templates/.github/extensions/byan-staging/package.json +0 -8
- package/install/templates/_byan/agent/marc/marc-soul.md +0 -47
- package/install/templates/_byan/agent/marc/marc-tao.md +0 -77
- package/install/templates/_byan/agent/marc/marc.md +0 -324
- package/install/templates/_byan/agent/marc-flat/marc.md +0 -387
- package/install/templates/_byan/mcp/byan-mcp-server/lib/copilot.js +0 -148
- package/install/templates/_byan/worker/launchers/launch-yanstaller-copilot.md +0 -173
- package/install/templates/workers/cost-optimizer.js +0 -169
- package/src/byan-v2/context/copilot-context.js +0 -79
- package/src/core/dispatcher/execution-router.js +0 -66
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// drain-advisory.js — Stop hook. At the end of each assistant turn, drain the
|
|
3
|
+
// outcome buffer into BYAN's ADVISORY ledgers (ELO trust, suitability). This is the
|
|
4
|
+
// automatic half of the closed learning loop: outcomes logged during the turn (via
|
|
5
|
+
// byan_outcome_log) are recorded with NO agent action. Behavior surfaces (routing /
|
|
6
|
+
// personas / mantras) are never touched — only advisory data is written.
|
|
7
|
+
//
|
|
8
|
+
// STRICTLY non-blocking. All work is wrapped in try/catch; the hook ALWAYS emits
|
|
9
|
+
// {continue:true} and exits 0, and never throws or exits 2. An advisory feed must
|
|
10
|
+
// never break a turn (the stage-to-byan.js contract: "staging must never break the
|
|
11
|
+
// session"). Idempotent via a line cursor, so a re-fired Stop (stop_hook_active)
|
|
12
|
+
// records nothing new.
|
|
13
|
+
//
|
|
14
|
+
// ESM/CJS: this hook is CommonJS. The ELO engine is CJS (require). The pure libs and
|
|
15
|
+
// the suitability store are ESM under a type:module package, reached via dynamic
|
|
16
|
+
// import() with a file:// URL.
|
|
17
|
+
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const { pathToFileURL } = require('url');
|
|
20
|
+
|
|
21
|
+
function readStdin() {
|
|
22
|
+
return new Promise((resolve) => {
|
|
23
|
+
if (process.stdin.isTTY) return resolve('');
|
|
24
|
+
let data = '';
|
|
25
|
+
process.stdin.on('data', (c) => (data += c));
|
|
26
|
+
process.stdin.on('end', () => resolve(data));
|
|
27
|
+
process.stdin.on('error', () => resolve(data));
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function done() {
|
|
32
|
+
process.stdout.write(JSON.stringify({ continue: true }));
|
|
33
|
+
process.exit(0);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
(async () => {
|
|
37
|
+
try {
|
|
38
|
+
await readStdin(); // the Stop payload is not needed — we drain disk state
|
|
39
|
+
const root = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
40
|
+
const esm = (rel) => import(pathToFileURL(path.join(root, rel)).href);
|
|
41
|
+
|
|
42
|
+
const af = await esm('_byan/mcp/byan-mcp-server/lib/advisory-autofeed.js');
|
|
43
|
+
const buf = await esm('_byan/mcp/byan-mcp-server/lib/outcome-buffer.js');
|
|
44
|
+
|
|
45
|
+
const outcomes = af.parseOutcomes(buf.readBuffer({ rootDir: root }));
|
|
46
|
+
const cursor = buf.readCursor({ rootDir: root });
|
|
47
|
+
const { pending, newCursor } = af.planDrain(outcomes, cursor);
|
|
48
|
+
if (!pending.length) return done();
|
|
49
|
+
|
|
50
|
+
let eloEngine = null;
|
|
51
|
+
let suitability = null;
|
|
52
|
+
for (const o of pending) {
|
|
53
|
+
const rec = af.classifyOutcome(o);
|
|
54
|
+
if (!rec) continue;
|
|
55
|
+
try {
|
|
56
|
+
if (rec.kind === 'elo') {
|
|
57
|
+
if (!eloEngine) {
|
|
58
|
+
const EloEngine = require(path.join(root, 'src', 'byan-v2', 'elo', 'index.js'));
|
|
59
|
+
eloEngine = new EloEngine({
|
|
60
|
+
storagePath: path.join(root, '_byan', 'memoire', 'elo-profile.json'),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
eloEngine.recordResult(rec.domain, rec.result);
|
|
64
|
+
} else if (rec.kind === 'suitability') {
|
|
65
|
+
if (!suitability) {
|
|
66
|
+
suitability = await esm('_byan/mcp/byan-mcp-server/lib/suitability-store.js');
|
|
67
|
+
}
|
|
68
|
+
suitability.record({
|
|
69
|
+
model: rec.model,
|
|
70
|
+
leafId: rec.leafId,
|
|
71
|
+
success: rec.success,
|
|
72
|
+
source: 'autofeed',
|
|
73
|
+
projectRoot: root,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
// one bad record must not abort the drain or block the turn
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
buf.writeCursor(newCursor, { rootDir: root });
|
|
81
|
+
} catch {
|
|
82
|
+
// any failure degrades silently — the feed is housekeeping, never a blocker
|
|
83
|
+
}
|
|
84
|
+
done();
|
|
85
|
+
})();
|
|
@@ -16,33 +16,8 @@
|
|
|
16
16
|
* (not documentation).
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
const fs = require('fs');
|
|
20
19
|
const path = require('path');
|
|
21
|
-
|
|
22
|
-
const ABSOLUTES = [
|
|
23
|
-
/\btoujours\b/i,
|
|
24
|
-
/\bjamais\b/i,
|
|
25
|
-
/\bforc[eé]ment\b/i,
|
|
26
|
-
/\bobviously\b/i,
|
|
27
|
-
/\balways\b/i,
|
|
28
|
-
/\bnever\b/i,
|
|
29
|
-
/\bclearly\b/i,
|
|
30
|
-
/\bundoubtedly\b/i,
|
|
31
|
-
/\bfaster than\b/i,
|
|
32
|
-
/\bbetter than\b/i,
|
|
33
|
-
/\bplus rapide que\b/i,
|
|
34
|
-
/\bmeilleur que\b/i,
|
|
35
|
-
];
|
|
36
|
-
|
|
37
|
-
const SOURCE_MARKERS = [
|
|
38
|
-
/\bRFC\s*\d+/i,
|
|
39
|
-
/\bCVE-\d{4}-\d+/i,
|
|
40
|
-
/https?:\/\//,
|
|
41
|
-
/\[CLAIM\s+L[1-5]\]/i,
|
|
42
|
-
/\[FACT\s+USER-VERIFIED/i,
|
|
43
|
-
/\bsource\s*:/i,
|
|
44
|
-
/_byan\/knowledge\/sources\.md/,
|
|
45
|
-
];
|
|
20
|
+
const { stripNonClaimZones, findUnsourced } = require('./lib/fact-check-core');
|
|
46
21
|
|
|
47
22
|
const DOC_EXTS = ['.md', '.mdx', '.rst', '.txt'];
|
|
48
23
|
|
|
@@ -65,24 +40,6 @@ function isExemptPath(filePath) {
|
|
|
65
40
|
return EXEMPT_PATH_PATTERNS.some((re) => re.test(filePath));
|
|
66
41
|
}
|
|
67
42
|
|
|
68
|
-
// Strip content that cannot be a claim :
|
|
69
|
-
// - fenced code blocks ``` ... ```
|
|
70
|
-
// - inline backticks `...`
|
|
71
|
-
// - block quotes (lines starting with >)
|
|
72
|
-
// - regex / array syntax that contains the word as a token
|
|
73
|
-
function stripNonClaimZones(text) {
|
|
74
|
-
if (!text) return '';
|
|
75
|
-
return text
|
|
76
|
-
// Fenced code blocks
|
|
77
|
-
.replace(/```[\s\S]*?```/g, '')
|
|
78
|
-
// Inline code
|
|
79
|
-
.replace(/`[^`\n]+`/g, '')
|
|
80
|
-
// Markdown block quotes
|
|
81
|
-
.replace(/^> .*$/gm, '')
|
|
82
|
-
// Lines that look like list of patterns (e.g. "- toujours")
|
|
83
|
-
.replace(/^[\s-]*['"]?\b(toujours|jamais|forc[eé]ment|obviously|always|never|clearly|undoubtedly)\b['"]?/gim, '');
|
|
84
|
-
}
|
|
85
|
-
|
|
86
43
|
function readStdin() {
|
|
87
44
|
return new Promise((resolve) => {
|
|
88
45
|
if (process.stdin.isTTY) return resolve('');
|
|
@@ -107,23 +64,6 @@ function extractText(toolName, input) {
|
|
|
107
64
|
return '';
|
|
108
65
|
}
|
|
109
66
|
|
|
110
|
-
function findUnsourced(text) {
|
|
111
|
-
if (!text) return null;
|
|
112
|
-
for (const re of ABSOLUTES) {
|
|
113
|
-
const match = text.match(re);
|
|
114
|
-
if (!match) continue;
|
|
115
|
-
const idx = match.index || 0;
|
|
116
|
-
const windowStart = Math.max(0, idx - 240);
|
|
117
|
-
const windowEnd = Math.min(text.length, idx + match[0].length + 240);
|
|
118
|
-
const ctx = text.slice(windowStart, windowEnd);
|
|
119
|
-
const hasSource = SOURCE_MARKERS.some((sm) => sm.test(ctx));
|
|
120
|
-
if (!hasSource) {
|
|
121
|
-
return { absolute: match[0], context: text.slice(Math.max(0, idx - 80), idx + 80) };
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
return null;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
67
|
(async () => {
|
|
128
68
|
const raw = await readStdin();
|
|
129
69
|
let payload = {};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Stop hook — fact-check conversation claims (non-blocking).
|
|
4
|
+
*
|
|
5
|
+
* The PreToolUse twin (fact-check-absolutes.js) only fires when the agent
|
|
6
|
+
* WRITES an unsourced absolute into a doc file. This hook covers the other,
|
|
7
|
+
* more frequent surface : an unsourced absolute spoken in the assistant's
|
|
8
|
+
* final turn text. It NUDGES (systemMessage, continue) and never blocks —
|
|
9
|
+
* spoken claims are noisier than written docs, so a block would trap
|
|
10
|
+
* legitimate hypotheses/quotes. Same detection engine (fact-check-core).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const { stripNonClaimZones, findUnsourced } = require('./lib/fact-check-core');
|
|
16
|
+
const { extractLastAssistantText } = require('./lib/transcript-read');
|
|
17
|
+
|
|
18
|
+
function readStdin() {
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
if (process.stdin.isTTY) return resolve('');
|
|
21
|
+
let data = '';
|
|
22
|
+
process.stdin.on('data', (c) => (data += c));
|
|
23
|
+
process.stdin.on('end', () => resolve(data));
|
|
24
|
+
process.stdin.on('error', () => resolve(data));
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Pure decision — no IO. Returns { nudge:false } or { nudge:true, absolute, context }.
|
|
29
|
+
function decideClaim({ lastAssistantText }) {
|
|
30
|
+
const text = stripNonClaimZones(lastAssistantText || '');
|
|
31
|
+
const hit = findUnsourced(text);
|
|
32
|
+
if (!hit) return { nudge: false };
|
|
33
|
+
return { nudge: true, absolute: hit.absolute, context: hit.context };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function nudgeMessage(hit) {
|
|
37
|
+
return [
|
|
38
|
+
`BYAN fact-check : unsourced absolute "${hit.absolute}" in this turn.`,
|
|
39
|
+
`Context : ...${hit.context}...`,
|
|
40
|
+
`Consider a source (RFC, CVE, URL, [CLAIM L<n>]) or hedge ("often", "in my tests", "tends to"). Advisory — not blocking.`,
|
|
41
|
+
].join('\n');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (require.main === module) {
|
|
45
|
+
(async () => {
|
|
46
|
+
let payload = {};
|
|
47
|
+
try {
|
|
48
|
+
const raw = await readStdin();
|
|
49
|
+
payload = raw ? JSON.parse(raw) : {};
|
|
50
|
+
} catch {
|
|
51
|
+
payload = {};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let decision = { nudge: false };
|
|
55
|
+
try {
|
|
56
|
+
decision = decideClaim({ lastAssistantText: extractLastAssistantText(payload) });
|
|
57
|
+
} catch {
|
|
58
|
+
decision = { nudge: false };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (decision.nudge) {
|
|
62
|
+
process.stdout.write(JSON.stringify({ systemMessage: nudgeMessage(decision), continue: true }));
|
|
63
|
+
} else {
|
|
64
|
+
process.stdout.write(JSON.stringify({ continue: true }));
|
|
65
|
+
}
|
|
66
|
+
})();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = { decideClaim, nudgeMessage };
|
|
@@ -15,6 +15,10 @@
|
|
|
15
15
|
|
|
16
16
|
const fs = require('fs');
|
|
17
17
|
const path = require('path');
|
|
18
|
+
// Shared transcript reader — the real Stop payload has no inline transcript
|
|
19
|
+
// (last_assistant_message + transcript_path JSONL). Without it this hook read an
|
|
20
|
+
// empty turn and never enforced the [FD:PHASE] header live.
|
|
21
|
+
const { extractLastAssistantText } = require('./lib/transcript-read');
|
|
18
22
|
|
|
19
23
|
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
20
24
|
const statePath = path.join(projectDir, '_byan-output', 'fd-state.json');
|
|
@@ -38,55 +42,42 @@ function readState() {
|
|
|
38
42
|
}
|
|
39
43
|
}
|
|
40
44
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (!Array.isArray(tx)) return '';
|
|
45
|
-
for (let i = tx.length - 1; i >= 0; i--) {
|
|
46
|
-
const m = tx[i];
|
|
47
|
-
if (m && m.role === 'assistant') {
|
|
48
|
-
if (typeof m.content === 'string') return m.content;
|
|
49
|
-
if (Array.isArray(m.content)) {
|
|
50
|
-
return m.content
|
|
51
|
-
.map((c) => (typeof c === 'object' && c.text ? c.text : ''))
|
|
52
|
-
.join(' ');
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return '';
|
|
57
|
-
}
|
|
45
|
+
// Pure decision : returns { block, reason? }. No IO — unit-testable.
|
|
46
|
+
function decideFdResponse({ state, lastAssistantText }) {
|
|
47
|
+
if (!state || ['COMPLETED', 'ABORTED'].includes(state.phase)) return { block: false };
|
|
58
48
|
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
49
|
+
const expected = `[FD:${state.phase}]`;
|
|
50
|
+
const text = lastAssistantText || '';
|
|
51
|
+
// Empty text (cannot read the turn) degrades to allow — never trap a turn we
|
|
52
|
+
// cannot inspect. A present header satisfies.
|
|
53
|
+
if (!text || text.includes(expected)) return { block: false };
|
|
65
54
|
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
payload = raw ? JSON.parse(raw) : {};
|
|
70
|
-
} catch {
|
|
71
|
-
payload = {};
|
|
72
|
-
}
|
|
55
|
+
const reason = `FD active (phase=${state.phase}) but your last response did not include the required header "${expected}". Reformulate your answer starting with ${expected} to confirm you are operating in the correct phase. If you wanted to exit or change phase, call byan_fd_advance first.`;
|
|
56
|
+
return { block: true, reason };
|
|
57
|
+
}
|
|
73
58
|
|
|
74
|
-
|
|
75
|
-
|
|
59
|
+
if (require.main === module) {
|
|
60
|
+
(async () => {
|
|
61
|
+
const state = readState();
|
|
62
|
+
const raw = await readStdin();
|
|
63
|
+
let payload = {};
|
|
64
|
+
try {
|
|
65
|
+
payload = raw ? JSON.parse(raw) : {};
|
|
66
|
+
} catch {
|
|
67
|
+
payload = {};
|
|
68
|
+
}
|
|
76
69
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
70
|
+
const decision = decideFdResponse({ state, lastAssistantText: extractLastAssistantText(payload) });
|
|
71
|
+
if (!decision.block) {
|
|
72
|
+
process.stdout.write(JSON.stringify({ continue: true }));
|
|
73
|
+
process.exit(0);
|
|
74
|
+
}
|
|
81
75
|
|
|
82
|
-
|
|
76
|
+
process.stdout.write(
|
|
77
|
+
JSON.stringify({ decision: 'block', reason: decision.reason, systemMessage: decision.reason })
|
|
78
|
+
);
|
|
79
|
+
process.exit(2);
|
|
80
|
+
})();
|
|
81
|
+
}
|
|
83
82
|
|
|
84
|
-
|
|
85
|
-
JSON.stringify({
|
|
86
|
-
decision: 'block',
|
|
87
|
-
reason,
|
|
88
|
-
systemMessage: reason,
|
|
89
|
-
})
|
|
90
|
-
);
|
|
91
|
-
process.exit(2);
|
|
92
|
-
})();
|
|
83
|
+
module.exports = { decideFdResponse, extractLastAssistantText };
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* SessionStart hook — loads BYAN soul
|
|
4
|
-
*
|
|
3
|
+
* SessionStart hook — loads BYAN soul + soul-memory and injects them into
|
|
4
|
+
* the session's initial context via additionalContext. Tao is intentionally
|
|
5
|
+
* NOT bundled here: inject-tao.js injects the full tao on every
|
|
6
|
+
* UserPromptSubmit (including the first), so duplicating it at SessionStart
|
|
7
|
+
* would double-spend ~15 KB per session for no gain.
|
|
8
|
+
*
|
|
9
|
+
* Also resets the per-session mid-session-nudge one-shot marker so the
|
|
10
|
+
* soul-memory-triggers nudge is per-session (not per-lifetime). Without
|
|
11
|
+
* this reset the one-shot marker, once written, silences the nudge forever.
|
|
5
12
|
*
|
|
6
13
|
* Safe: missing files are skipped silently, script always exits 0.
|
|
7
14
|
*/
|
|
@@ -9,44 +16,76 @@
|
|
|
9
16
|
const fs = require('fs');
|
|
10
17
|
const path = require('path');
|
|
11
18
|
|
|
12
|
-
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
13
|
-
|
|
14
19
|
// Gen3 puts BYAN's soul files under _byan/agent/byan/; Gen2 keeps them at the
|
|
15
20
|
// _byan/ root. Prefer Gen3 when present, fall back to Gen2 (self-contained so
|
|
16
21
|
// the hook never depends on a require that could fail).
|
|
17
|
-
function soulFile(label) {
|
|
22
|
+
function soulFile(projectDir, label) {
|
|
18
23
|
const g3 = path.join(projectDir, '_byan', 'agent', 'byan', `${label}.md`);
|
|
19
24
|
const g2 = path.join(projectDir, '_byan', `${label}.md`);
|
|
20
25
|
return fs.existsSync(g3) ? g3 : g2;
|
|
21
26
|
}
|
|
22
27
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
// Same resolution as soul-memory-triggers.js: Gen3 _byan/memoire/ first, Gen2
|
|
29
|
+
// _byan/_memory/ fallback. Kept in sync by hand (hooks avoid shared requires).
|
|
30
|
+
function nudgeMarkerPath(projectDir) {
|
|
31
|
+
const memoireDir = path.join(projectDir, '_byan', 'memoire');
|
|
32
|
+
const memoryDir = fs.existsSync(memoireDir)
|
|
33
|
+
? memoireDir
|
|
34
|
+
: path.join(projectDir, '_byan', '_memory');
|
|
35
|
+
return path.join(memoryDir, '.soul-memory-nudge-sent');
|
|
36
|
+
}
|
|
28
37
|
|
|
29
|
-
|
|
30
|
-
|
|
38
|
+
// Reset the one-shot nudge marker at session start so the mid-session
|
|
39
|
+
// soul-memory nudge can fire once per session instead of once per lifetime.
|
|
40
|
+
function resetNudgeMarker(projectDir) {
|
|
31
41
|
try {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if (content.length > 0) {
|
|
35
|
-
chunks.push(`=== BYAN ${f.label.toUpperCase()} (${path.relative(projectDir, f.path)}) ===\n${content}`);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
42
|
+
fs.rmSync(nudgeMarkerPath(projectDir), { force: true });
|
|
43
|
+
return true;
|
|
38
44
|
} catch {
|
|
39
|
-
|
|
45
|
+
return false;
|
|
40
46
|
}
|
|
41
47
|
}
|
|
42
48
|
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
function buildAdditionalContext(projectDir) {
|
|
50
|
+
const files = [
|
|
51
|
+
{ label: 'soul', path: soulFile(projectDir, 'soul') },
|
|
52
|
+
{ label: 'soul-memory', path: soulFile(projectDir, 'soul-memory') },
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
const chunks = [];
|
|
56
|
+
for (const f of files) {
|
|
57
|
+
try {
|
|
58
|
+
if (fs.existsSync(f.path)) {
|
|
59
|
+
const content = fs.readFileSync(f.path, 'utf8').trim();
|
|
60
|
+
if (content.length > 0) {
|
|
61
|
+
chunks.push(
|
|
62
|
+
`=== BYAN ${f.label.toUpperCase()} (${path.relative(projectDir, f.path)}) ===\n${content}`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
// Ignore read errors — hook must never block session start.
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return chunks.length > 0
|
|
45
72
|
? `BYAN Soul System (loaded at session start):\n\n${chunks.join('\n\n')}`
|
|
46
73
|
: '';
|
|
74
|
+
}
|
|
47
75
|
|
|
48
|
-
if (
|
|
49
|
-
process.
|
|
50
|
-
|
|
51
|
-
|
|
76
|
+
if (require.main === module) {
|
|
77
|
+
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
78
|
+
resetNudgeMarker(projectDir);
|
|
79
|
+
const additionalContext = buildAdditionalContext(projectDir);
|
|
80
|
+
if (additionalContext) {
|
|
81
|
+
process.stdout.write(
|
|
82
|
+
JSON.stringify({
|
|
83
|
+
hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext },
|
|
84
|
+
})
|
|
85
|
+
);
|
|
86
|
+
} else {
|
|
87
|
+
process.stdout.write('{}');
|
|
88
|
+
}
|
|
52
89
|
}
|
|
90
|
+
|
|
91
|
+
module.exports = { soulFile, nudgeMarkerPath, resetNudgeMarker, buildAdditionalContext };
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// PostToolUse hook — mirror the BYAN FD lifecycle onto a Leantime board with no
|
|
3
|
+
// agent action. Fires on byan_fd_advance / byan_fd_update; reads the fd-state the
|
|
4
|
+
// tool echoed; drives lib/leantime-sync.js (ensure project, create tasks, move
|
|
5
|
+
// columns) per the pure decision core lib/leantime-fd-core.js.
|
|
6
|
+
//
|
|
7
|
+
// Best-effort and bounded by design:
|
|
8
|
+
// - exits 0 in every path (a sync issue does not block the turn; this hook
|
|
9
|
+
// does not use the exit-2 blocking path);
|
|
10
|
+
// - no-ops silently when the tool is not an FD tool, no FD is active, or
|
|
11
|
+
// Leantime is not configured (syncEnabled false);
|
|
12
|
+
// - it never WRITES fd-state.json (state-coupling); it reads the state the tool
|
|
13
|
+
// echoed, with a read-only fd-state.json fallback. The Leantime id map lives
|
|
14
|
+
// in the gitignored .byan-leantime/ sidecar;
|
|
15
|
+
// - a per-call timeout plus a hook wall-clock budget keep a slow Leantime from
|
|
16
|
+
// stalling the turn; a dropped call self-heals on the next phase event
|
|
17
|
+
// (reconcile-from-state, tracked by sidecar.moveFailed).
|
|
18
|
+
//
|
|
19
|
+
// CJS shell + ESM libs reached via dynamic import() (the drain-advisory.js bridge).
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const { pathToFileURL } = require('url');
|
|
24
|
+
|
|
25
|
+
const ROOT = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
26
|
+
const SIDECAR_DIR = path.join(ROOT, '.byan-leantime');
|
|
27
|
+
const MAP_PATH = path.join(SIDECAR_DIR, 'map.json');
|
|
28
|
+
const LOG_PATH = path.join(SIDECAR_DIR, 'sync.jsonl');
|
|
29
|
+
const FD_STATE_PATH = path.join(ROOT, '_byan-output', 'fd-state.json');
|
|
30
|
+
const PER_CALL_MS = 2500; // below the lib default (5000) so the hook bounds each call
|
|
31
|
+
const HOOK_BUDGET_MS = 8000; // between-stage advisory, checked at each stage boundary (not a hard ceiling); a move issues 2 RPCs so a late stage can overrun it, though no call hangs (each aborts at PER_CALL_MS)
|
|
32
|
+
|
|
33
|
+
// Reasons that deserve a one-line breadcrumb (a real wire/host issue, not "off").
|
|
34
|
+
const LOUD = new Set(['non_json', 'timeout', 'rpc_error']);
|
|
35
|
+
const isLoud = (reason) => typeof reason === 'string' && (LOUD.has(reason) || reason.startsWith('http_'));
|
|
36
|
+
|
|
37
|
+
function readStdin() {
|
|
38
|
+
return new Promise((resolve) => {
|
|
39
|
+
if (process.stdin.isTTY) return resolve('');
|
|
40
|
+
let data = '';
|
|
41
|
+
process.stdin.on('data', (c) => (data += c));
|
|
42
|
+
process.stdin.on('end', () => resolve(data));
|
|
43
|
+
process.stdin.on('error', () => resolve(data));
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function emit(additionalContext = '') {
|
|
48
|
+
process.stdout.write(
|
|
49
|
+
JSON.stringify({ hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext } }),
|
|
50
|
+
);
|
|
51
|
+
process.exit(0);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function readMap() {
|
|
55
|
+
try {
|
|
56
|
+
return JSON.parse(fs.readFileSync(MAP_PATH, 'utf8'));
|
|
57
|
+
} catch {
|
|
58
|
+
return {};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function writeMap(map) {
|
|
63
|
+
try {
|
|
64
|
+
fs.mkdirSync(SIDECAR_DIR, { recursive: true });
|
|
65
|
+
const tmp = `${MAP_PATH}.${process.pid}.tmp`;
|
|
66
|
+
fs.writeFileSync(tmp, JSON.stringify(map, null, 2));
|
|
67
|
+
fs.renameSync(tmp, MAP_PATH); // atomic swap so a crash mid-write keeps the old map
|
|
68
|
+
} catch {
|
|
69
|
+
// the sidecar is housekeeping; a write failure must not break the hook
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function logLine(entry) {
|
|
74
|
+
try {
|
|
75
|
+
fs.mkdirSync(SIDECAR_DIR, { recursive: true });
|
|
76
|
+
fs.appendFileSync(LOG_PATH, `${JSON.stringify(entry)}\n`);
|
|
77
|
+
} catch {
|
|
78
|
+
// the log is housekeeping; swallow
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
(async () => {
|
|
83
|
+
let payload = {};
|
|
84
|
+
try {
|
|
85
|
+
const raw = await readStdin();
|
|
86
|
+
payload = raw ? JSON.parse(raw) : {};
|
|
87
|
+
} catch {
|
|
88
|
+
return emit();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const toolName = payload.tool_name || payload.toolName || '';
|
|
93
|
+
const esm = (rel) => import(pathToFileURL(path.join(ROOT, rel)).href);
|
|
94
|
+
const core = await esm('_byan/mcp/byan-mcp-server/lib/leantime-fd-core.js');
|
|
95
|
+
if (!core.fdToolKind(toolName)) return emit(); // not an FD tool
|
|
96
|
+
|
|
97
|
+
// Read state from the tool's echoed result (state-coupling: no fd-state write).
|
|
98
|
+
const resp = payload.tool_response ?? payload.toolResponse ?? payload.response ?? null;
|
|
99
|
+
let state = core.parseFdState(resp);
|
|
100
|
+
if (!state) {
|
|
101
|
+
try {
|
|
102
|
+
state = JSON.parse(fs.readFileSync(FD_STATE_PATH, 'utf8'));
|
|
103
|
+
} catch {
|
|
104
|
+
state = null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (!state || typeof state.phase !== 'string') return emit();
|
|
108
|
+
|
|
109
|
+
const lt = await esm('_byan/mcp/byan-mcp-server/lib/leantime-sync.js');
|
|
110
|
+
if (!lt.syncEnabled()) return emit(); // Leantime not configured -> silent no-op
|
|
111
|
+
|
|
112
|
+
const fdId = state.fd_id || 'unknown';
|
|
113
|
+
const map = readMap();
|
|
114
|
+
const sidecar = map[fdId] || { tasks: {} };
|
|
115
|
+
sidecar.tasks = sidecar.tasks || {};
|
|
116
|
+
const assignUserConfigured = lt.assignUserId() != null;
|
|
117
|
+
|
|
118
|
+
const { intents, column } = core.decideActions({ toolName, state, sidecar, assignUserConfigured });
|
|
119
|
+
|
|
120
|
+
if (!intents || !intents.length) {
|
|
121
|
+
if (column && sidecar.lastColumn !== column) {
|
|
122
|
+
sidecar.lastColumn = column;
|
|
123
|
+
map[fdId] = sidecar;
|
|
124
|
+
writeMap(map);
|
|
125
|
+
}
|
|
126
|
+
return emit();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const opts = { timeoutMs: PER_CALL_MS };
|
|
130
|
+
const deadline = Date.now() + HOOK_BUDGET_MS;
|
|
131
|
+
const timeLeft = () => deadline - Date.now();
|
|
132
|
+
let firstLoud = null;
|
|
133
|
+
let moveFailed = false;
|
|
134
|
+
|
|
135
|
+
const record = (event, target, r) => {
|
|
136
|
+
const ok = !!(r && r.ok);
|
|
137
|
+
const synced = !!(r && r.synced);
|
|
138
|
+
logLine({ ts: new Date().toISOString(), fd_id: fdId, phase: state.phase, event, target, ok, synced, reason: (r && r.reason) || null });
|
|
139
|
+
if (!synced && isLoud(r && r.reason) && !firstLoud) firstLoud = r.reason;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// 1. Ensure the project (sequential — every later call needs the projectId).
|
|
143
|
+
const ensureIntent = intents.find((i) => i.op === 'project_ensure');
|
|
144
|
+
if (ensureIntent && !sidecar.projectId) {
|
|
145
|
+
const r = await lt.ensureProject({ name: ensureIntent.name, slug: ensureIntent.slug, details: ensureIntent.details }, opts);
|
|
146
|
+
record('project_ensure', ensureIntent.name, r);
|
|
147
|
+
if (r.ok && r.id) {
|
|
148
|
+
sidecar.projectId = r.id;
|
|
149
|
+
map[fdId] = sidecar;
|
|
150
|
+
writeMap(map); // persist immediately so a crash cannot re-create the project
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 2. Make the project visible to the configured human (best-effort).
|
|
155
|
+
if (intents.some((i) => i.op === 'assign_user') && sidecar.projectId && timeLeft() > 0) {
|
|
156
|
+
const r = await lt.assignUserToProject({ projectId: sidecar.projectId }, opts);
|
|
157
|
+
record('assign_user', sidecar.projectId, r);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 3. Create tasks (parallel, bounded by the wall-clock budget).
|
|
161
|
+
const createIntents = intents.filter((i) => i.op === 'task_create');
|
|
162
|
+
if (createIntents.length && sidecar.projectId && timeLeft() > 0) {
|
|
163
|
+
const results = await Promise.allSettled(
|
|
164
|
+
createIntents.map((i) =>
|
|
165
|
+
lt.createTask({ projectId: sidecar.projectId, headline: i.headline }, opts).then((r) => ({ i, r })),
|
|
166
|
+
),
|
|
167
|
+
);
|
|
168
|
+
for (const s of results) {
|
|
169
|
+
if (s.status === 'fulfilled') {
|
|
170
|
+
const { i, r } = s.value;
|
|
171
|
+
record('task_create', i.backlogId, r);
|
|
172
|
+
if (r.ok && r.id) sidecar.tasks[i.backlogId] = r.id;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
map[fdId] = sidecar;
|
|
176
|
+
writeMap(map);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// 4. Move tasks to the current column (parallel, bounded).
|
|
180
|
+
const moveIntents = intents.filter((i) => i.op === 'task_move');
|
|
181
|
+
if (moveIntents.length && sidecar.projectId && timeLeft() > 0) {
|
|
182
|
+
const results = await Promise.allSettled(
|
|
183
|
+
moveIntents.map((i) => {
|
|
184
|
+
const taskId = sidecar.tasks[i.backlogId];
|
|
185
|
+
if (!taskId) return Promise.resolve({ i, r: { ok: false, synced: false, reason: 'no_task_id' } });
|
|
186
|
+
return lt.moveTask({ taskId, projectId: sidecar.projectId, column: i.column }, opts).then((r) => ({ i, r }));
|
|
187
|
+
}),
|
|
188
|
+
);
|
|
189
|
+
for (const s of results) {
|
|
190
|
+
if (s.status === 'fulfilled') {
|
|
191
|
+
const { i, r } = s.value;
|
|
192
|
+
record('task_move', i.backlogId, r);
|
|
193
|
+
if (!(r && r.synced)) moveFailed = true;
|
|
194
|
+
} else {
|
|
195
|
+
moveFailed = true;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
} else if (moveIntents.length) {
|
|
199
|
+
// could not run the moves this fire (budget/no project) -> retry next event
|
|
200
|
+
moveFailed = true;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (column) sidecar.lastColumn = column;
|
|
204
|
+
sidecar.moveFailed = moveFailed;
|
|
205
|
+
sidecar.updatedAt = new Date().toISOString();
|
|
206
|
+
map[fdId] = sidecar;
|
|
207
|
+
writeMap(map);
|
|
208
|
+
|
|
209
|
+
if (firstLoud) {
|
|
210
|
+
return emit(`Leantime sync: ${firstLoud} on ${state.phase} (board may lag; retried next phase). Check LEANTIME_API_URL / token.`);
|
|
211
|
+
}
|
|
212
|
+
return emit();
|
|
213
|
+
} catch {
|
|
214
|
+
return emit(); // any failure degrades silently — the sync is housekeeping
|
|
215
|
+
}
|
|
216
|
+
})();
|