@arka-labs/nemesis 1.2.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/LICENSE +201 -0
- package/README.md +668 -0
- package/lib/core/agent-launcher.js +193 -0
- package/lib/core/audit.js +210 -0
- package/lib/core/connexions.js +80 -0
- package/lib/core/flowmap/api.js +111 -0
- package/lib/core/flowmap/cli-helpers.js +80 -0
- package/lib/core/flowmap/machine.js +281 -0
- package/lib/core/flowmap/persistence.js +83 -0
- package/lib/core/generators.js +183 -0
- package/lib/core/inbox.js +275 -0
- package/lib/core/logger.js +20 -0
- package/lib/core/mission.js +109 -0
- package/lib/core/notewriter/config.js +36 -0
- package/lib/core/notewriter/cr.js +237 -0
- package/lib/core/notewriter/log.js +112 -0
- package/lib/core/notewriter/notes.js +168 -0
- package/lib/core/notewriter/paths.js +45 -0
- package/lib/core/notewriter/reader.js +121 -0
- package/lib/core/notewriter/registry.js +80 -0
- package/lib/core/odm.js +191 -0
- package/lib/core/profile-picker.js +323 -0
- package/lib/core/project.js +287 -0
- package/lib/core/registry.js +129 -0
- package/lib/core/secrets.js +137 -0
- package/lib/core/services.js +45 -0
- package/lib/core/team.js +287 -0
- package/lib/core/templates.js +80 -0
- package/lib/kairos/agent-runner.js +261 -0
- package/lib/kairos/claude-invoker.js +90 -0
- package/lib/kairos/context-injector.js +331 -0
- package/lib/kairos/context-loader.js +108 -0
- package/lib/kairos/context-writer.js +45 -0
- package/lib/kairos/dispatcher-router.js +173 -0
- package/lib/kairos/dispatcher.js +139 -0
- package/lib/kairos/event-bus.js +287 -0
- package/lib/kairos/event-router.js +131 -0
- package/lib/kairos/flowmap-bridge.js +120 -0
- package/lib/kairos/hook-handlers.js +351 -0
- package/lib/kairos/hook-installer.js +207 -0
- package/lib/kairos/hook-prompts.js +54 -0
- package/lib/kairos/leader-rules.js +94 -0
- package/lib/kairos/pid-checker.js +108 -0
- package/lib/kairos/situation-detector.js +123 -0
- package/lib/sync/fallback-engine.js +97 -0
- package/lib/sync/hcm-client.js +170 -0
- package/lib/sync/health.js +47 -0
- package/lib/sync/llm-client.js +387 -0
- package/lib/sync/nemesis-client.js +379 -0
- package/lib/sync/service-session.js +74 -0
- package/lib/sync/sync-engine.js +178 -0
- package/lib/ui/box.js +104 -0
- package/lib/ui/brand.js +42 -0
- package/lib/ui/colors.js +57 -0
- package/lib/ui/dashboard.js +580 -0
- package/lib/ui/error-hints.js +49 -0
- package/lib/ui/format.js +61 -0
- package/lib/ui/menu.js +306 -0
- package/lib/ui/note-card.js +198 -0
- package/lib/ui/note-colors.js +26 -0
- package/lib/ui/note-detail.js +297 -0
- package/lib/ui/note-filters.js +252 -0
- package/lib/ui/note-views.js +283 -0
- package/lib/ui/prompt.js +81 -0
- package/lib/ui/spinner.js +139 -0
- package/lib/ui/streambox.js +46 -0
- package/lib/ui/table.js +42 -0
- package/lib/ui/tree.js +33 -0
- package/package.json +53 -0
- package/src/cli.js +457 -0
- package/src/commands/_helpers.js +119 -0
- package/src/commands/audit.js +187 -0
- package/src/commands/auth.js +316 -0
- package/src/commands/doctor.js +243 -0
- package/src/commands/hcm.js +147 -0
- package/src/commands/inbox.js +333 -0
- package/src/commands/init.js +160 -0
- package/src/commands/kairos.js +216 -0
- package/src/commands/kars.js +134 -0
- package/src/commands/mission.js +275 -0
- package/src/commands/notes.js +316 -0
- package/src/commands/notewriter.js +296 -0
- package/src/commands/odm.js +329 -0
- package/src/commands/orch.js +68 -0
- package/src/commands/project.js +123 -0
- package/src/commands/run.js +123 -0
- package/src/commands/services.js +705 -0
- package/src/commands/status.js +231 -0
- package/src/commands/team.js +572 -0
- package/src/config.js +84 -0
- package/src/index.js +5 -0
- package/templates/project-context.json +10 -0
- package/templates/template_CONTRIB-NAME.json +22 -0
- package/templates/template_CR-ODM-NAME-000.exemple.json +32 -0
- package/templates/template_DEC-NAME-000.json +18 -0
- package/templates/template_INTV-NAME-000.json +15 -0
- package/templates/template_MISSION_CONTRACT.json +46 -0
- package/templates/template_ODM-NAME-000.json +89 -0
- package/templates/template_REGISTRY-PROJECT.json +26 -0
- package/templates/template_TXN-NAME-000.json +24 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { pushEvent } from './event-bus.js';
|
|
2
|
+
import { verifyAgentAlive } from './pid-checker.js';
|
|
3
|
+
import { readRegistry, getLanes } from '../core/registry.js';
|
|
4
|
+
import { detectProject } from '../core/project.js';
|
|
5
|
+
import { debug } from '../core/logger.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Route an event to the target agent using PID-aware decision tree.
|
|
9
|
+
*
|
|
10
|
+
* Decision tree:
|
|
11
|
+
* 1. Agent in registry? No → failed
|
|
12
|
+
* 2. verifyAgentAlive(lane) → alive? Yes → pushEvent() (method: 'queued')
|
|
13
|
+
* 3. Agent dead + session_id → launchHeadless() with event prompt (method: 'direct')
|
|
14
|
+
* 4. No session_id → failed
|
|
15
|
+
*
|
|
16
|
+
* @param {string} projectRoot
|
|
17
|
+
* @param {object} event - { target_agent_id, event_type, payload, priority, ttl, source_agent_id }
|
|
18
|
+
* @returns {{ delivered: boolean, method: 'queued'|'direct'|'failed', detail: string }}
|
|
19
|
+
*/
|
|
20
|
+
export async function routeEvent(projectRoot, event) {
|
|
21
|
+
const project = detectProject(projectRoot);
|
|
22
|
+
if (!project) return { delivered: false, method: 'failed', detail: 'no_project' };
|
|
23
|
+
|
|
24
|
+
const reg = readRegistry(project.hcm_dir);
|
|
25
|
+
if (!reg) return { delivered: false, method: 'failed', detail: 'no_registry' };
|
|
26
|
+
|
|
27
|
+
const lanes = getLanes(reg);
|
|
28
|
+
const lane = lanes.find(l =>
|
|
29
|
+
l.id === event.target_agent_id || l.name === event.target_agent_id
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
// Step 1: Agent in registry?
|
|
33
|
+
if (!lane) {
|
|
34
|
+
return { delivered: false, method: 'failed', detail: `agent_not_found: ${event.target_agent_id}` };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Step 2: Agent alive? → queue event
|
|
38
|
+
const { alive } = verifyAgentAlive(lane);
|
|
39
|
+
if (alive) {
|
|
40
|
+
pushEvent(projectRoot, event);
|
|
41
|
+
return { delivered: true, method: 'queued', detail: `queued for PID ${lane.pid}` };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Step 3: Agent dead but has session_id → launch headless with event prompt
|
|
45
|
+
if (lane.session_id) {
|
|
46
|
+
try {
|
|
47
|
+
const { launchHeadless } = await import('./agent-runner.js');
|
|
48
|
+
const prompt = buildEventPrompt(event);
|
|
49
|
+
const txnId = `EVT-ROUTE-${Date.now()}`;
|
|
50
|
+
const { pid } = await launchHeadless(
|
|
51
|
+
{ name: lane.name || lane.id, session_id: lane.session_id },
|
|
52
|
+
prompt,
|
|
53
|
+
{ txnId, root: projectRoot },
|
|
54
|
+
);
|
|
55
|
+
return { delivered: true, method: 'direct', detail: `launched PID ${pid}` };
|
|
56
|
+
} catch (err) {
|
|
57
|
+
// Fallback: queue it anyway — agent may resume later
|
|
58
|
+
pushEvent(projectRoot, event);
|
|
59
|
+
return { delivered: true, method: 'queued', detail: `direct_failed: ${err.message}, queued as fallback` };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Step 4: No session_id
|
|
64
|
+
return { delivered: false, method: 'failed', detail: 'no_session_id' };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Deliver an event directly to an alive agent by launching a headless prompt.
|
|
69
|
+
* Only called from CLI (nemesis kairos push), NOT from hooks (timeout < 5s).
|
|
70
|
+
*
|
|
71
|
+
* @param {string} projectRoot
|
|
72
|
+
* @param {object} lane - Registry lane
|
|
73
|
+
* @param {object} event - Event object
|
|
74
|
+
* @returns {{ delivered: boolean, pid: number|null }}
|
|
75
|
+
*/
|
|
76
|
+
export async function deliverDirect(projectRoot, lane, event) {
|
|
77
|
+
if (!lane.session_id) {
|
|
78
|
+
return { delivered: false, pid: null };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const { launchHeadless } = await import('./agent-runner.js');
|
|
83
|
+
const prompt = buildEventPrompt(event);
|
|
84
|
+
const txnId = `EVT-DIRECT-${Date.now()}`;
|
|
85
|
+
const { pid } = await launchHeadless(
|
|
86
|
+
{ name: lane.name || lane.id, session_id: lane.session_id },
|
|
87
|
+
prompt,
|
|
88
|
+
{ txnId, root: projectRoot },
|
|
89
|
+
);
|
|
90
|
+
return { delivered: true, pid };
|
|
91
|
+
} catch (e) {
|
|
92
|
+
debug(`deliverDirect: ${e.message}`);
|
|
93
|
+
return { delivered: false, pid: null };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Build a prompt string from an event for headless injection.
|
|
99
|
+
* @param {object} event
|
|
100
|
+
* @returns {string}
|
|
101
|
+
*/
|
|
102
|
+
export function buildEventPrompt(event) {
|
|
103
|
+
const lines = [
|
|
104
|
+
`# Evenement Kairos : ${event.event_type}`,
|
|
105
|
+
'',
|
|
106
|
+
`Priorite: ${event.priority || 'MEDIUM'}`,
|
|
107
|
+
`De: ${event.source_agent_id || 'system'}`,
|
|
108
|
+
`Date: ${event.created_at || new Date().toISOString()}`,
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
if (event.payload) {
|
|
112
|
+
lines.push('', '## Payload');
|
|
113
|
+
if (event.payload.summary) lines.push(`Resume: ${event.payload.summary}`);
|
|
114
|
+
if (event.payload.odmId) lines.push(`OdM: ${event.payload.odmId}`);
|
|
115
|
+
if (event.payload.cr_id) lines.push(`CR: ${event.payload.cr_id}`);
|
|
116
|
+
if (event.payload.message) lines.push(`Message: ${event.payload.message}`);
|
|
117
|
+
|
|
118
|
+
// Dump remaining payload keys
|
|
119
|
+
const shown = new Set(['summary', 'odmId', 'cr_id', 'message']);
|
|
120
|
+
for (const [key, val] of Object.entries(event.payload)) {
|
|
121
|
+
if (!shown.has(key) && val !== undefined && val !== null) {
|
|
122
|
+
lines.push(`${key}: ${typeof val === 'object' ? JSON.stringify(val) : val}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
lines.push('', '## Action requise');
|
|
128
|
+
lines.push('Traite cet evenement selon son type et sa priorite.');
|
|
129
|
+
|
|
130
|
+
return lines.join('\n');
|
|
131
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flowmap Bridge — map dispatch results to XState flowmap events.
|
|
3
|
+
* Connects the dispatcher classification to the mission state machine.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { debug } from '../core/logger.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Signal patterns for automatic flowmap event detection.
|
|
10
|
+
* Each pattern has keywords and the flowmap event to fire.
|
|
11
|
+
*/
|
|
12
|
+
const SIGNAL_PATTERNS = [
|
|
13
|
+
{
|
|
14
|
+
id: 'deliver_odm',
|
|
15
|
+
keywords: ['cr depose', 'cr deposé', 'cr-odm', 'compte-rendu', 'livrable depose', 'livrable deposé'],
|
|
16
|
+
event: { type: 'DELIVER_ODM' },
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: 'complete_implementation',
|
|
20
|
+
keywords: ['implementation complete', 'implémentation complète', 'implementation terminee', 'tache terminee', 'tâche terminée', 'implementation done'],
|
|
21
|
+
event: { type: 'COMPLETE_IMPLEMENTATION' },
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 'qa_fail',
|
|
25
|
+
keywords: ['rejet', 'rejete', 'non conforme', 'qa fail', 'qa ko', 'renvoi', 'non valide'],
|
|
26
|
+
requiresLeader: true,
|
|
27
|
+
event: { type: 'QA_VERDICT', verdict: 'FAIL' },
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: 'qa_pass',
|
|
31
|
+
keywords: ['valide', 'validation ok', 'qa ok', 'qa pass', 'conforme'],
|
|
32
|
+
requiresLeader: true,
|
|
33
|
+
event: { type: 'QA_VERDICT', verdict: 'PASS' },
|
|
34
|
+
},
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Process a dispatch result and determine if a flowmap event should fire.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} projectRoot
|
|
41
|
+
* @param {object} result - dispatch result { agentName, odmId, response, ... }
|
|
42
|
+
* @param {object} classification - routing entry { target, reason, summary, priority }
|
|
43
|
+
* @returns {{ event: object, signalId: string, missionId: string|null } | null}
|
|
44
|
+
*/
|
|
45
|
+
export function processDispatchResult(projectRoot, result, classification) {
|
|
46
|
+
if (!result || !classification) return null;
|
|
47
|
+
if (classification.target === 'NOISE') return null;
|
|
48
|
+
|
|
49
|
+
const text = [
|
|
50
|
+
result.response || '',
|
|
51
|
+
classification.reason || '',
|
|
52
|
+
classification.summary || '',
|
|
53
|
+
].join(' ').toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
|
54
|
+
|
|
55
|
+
const isLeader = classification.target === 'LEADER';
|
|
56
|
+
|
|
57
|
+
for (const pattern of SIGNAL_PATTERNS) {
|
|
58
|
+
if (pattern.requiresLeader && !isLeader) continue;
|
|
59
|
+
|
|
60
|
+
const matched = pattern.keywords.some(kw => {
|
|
61
|
+
const normalizedKw = kw.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
|
62
|
+
return text.includes(normalizedKw);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (matched) {
|
|
66
|
+
return {
|
|
67
|
+
event: { ...pattern.event },
|
|
68
|
+
signalId: pattern.id,
|
|
69
|
+
missionId: result.missionId || null,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Send a detected flowmap event to the mission state machine.
|
|
79
|
+
* Graceful: returns null if no mission or flowmap error.
|
|
80
|
+
*
|
|
81
|
+
* @param {string} projectRoot
|
|
82
|
+
* @param {string} missionId
|
|
83
|
+
* @param {object} event - XState event { type, ... }
|
|
84
|
+
* @returns {Promise<{ value: string, context: object } | null>}
|
|
85
|
+
*/
|
|
86
|
+
export async function fireFlowmapEvent(projectRoot, missionId, event) {
|
|
87
|
+
if (!missionId) return null;
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const { sendFlowmapEvent } = await import('../core/flowmap/api.js');
|
|
91
|
+
return sendFlowmapEvent(missionId, event, { root: projectRoot });
|
|
92
|
+
} catch (e) {
|
|
93
|
+
debug(`fireFlowmapEvent: ${e.message}`);
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Full pipeline: analyze dispatch result, detect signal, fire flowmap event.
|
|
100
|
+
*
|
|
101
|
+
* @param {string} projectRoot
|
|
102
|
+
* @param {object} result - dispatch result
|
|
103
|
+
* @param {object} classification - routing entry
|
|
104
|
+
* @returns {Promise<{ event: object, signalId: string, newState: string|null } | null>}
|
|
105
|
+
*/
|
|
106
|
+
export async function bridgeDispatchToFlowmap(projectRoot, result, classification) {
|
|
107
|
+
const detected = processDispatchResult(projectRoot, result, classification);
|
|
108
|
+
if (!detected) return null;
|
|
109
|
+
|
|
110
|
+
const missionId = detected.missionId || result.missionId || result.odmId;
|
|
111
|
+
if (!missionId) return { ...detected, newState: null };
|
|
112
|
+
|
|
113
|
+
const state = await fireFlowmapEvent(projectRoot, missionId, detected.event);
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
event: detected.event,
|
|
117
|
+
signalId: detected.signalId,
|
|
118
|
+
newState: state?.value || null,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, appendFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { openTurn, closeTurn } from '../core/notewriter/log.js';
|
|
4
|
+
import { generateNote } from '../core/notewriter/notes.js';
|
|
5
|
+
import { shouldGenerateCR, generateCR, consumePendingCr } from '../core/notewriter/cr.js';
|
|
6
|
+
import { readNotewriterConfig } from '../core/notewriter/config.js';
|
|
7
|
+
import { findPendingDispatch, writeDispatchResult, consumePendingDispatch } from './agent-runner.js';
|
|
8
|
+
import { resolveAgentContext } from './context-loader.js';
|
|
9
|
+
import { readCurrentSituation, writeCurrentSituation } from './context-writer.js';
|
|
10
|
+
import { detectSituation } from './situation-detector.js';
|
|
11
|
+
import { routeToFile } from './dispatcher-router.js';
|
|
12
|
+
import { processDispatchResult, fireFlowmapEvent } from './flowmap-bridge.js';
|
|
13
|
+
import { buildUnifiedContext, buildSituationUpdate } from './context-injector.js';
|
|
14
|
+
|
|
15
|
+
function debugLog(hook, msg) {
|
|
16
|
+
try { appendFileSync('/tmp/kairos-debug.log', `[${new Date().toISOString()}] ${hook}: ${msg}\n`); } catch {}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function readStdin() {
|
|
20
|
+
try {
|
|
21
|
+
const data = readFileSync(0, 'utf-8'); // fd 0 = stdin
|
|
22
|
+
if (!data.trim()) return {};
|
|
23
|
+
return JSON.parse(data);
|
|
24
|
+
} catch (e) {
|
|
25
|
+
debugLog('readStdin', `parse failed: ${e.message}`);
|
|
26
|
+
return {};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Extract the last assistant response from a Claude Code transcript JSONL.
|
|
32
|
+
* Transcript entries with type "assistant" have message.content[] with text blocks.
|
|
33
|
+
*/
|
|
34
|
+
export function extractLastResponse(transcriptPath) {
|
|
35
|
+
if (!transcriptPath) return '';
|
|
36
|
+
try {
|
|
37
|
+
const raw = readFileSync(transcriptPath, 'utf-8');
|
|
38
|
+
const lines = raw.trim().split('\n');
|
|
39
|
+
let lastAssistant = null;
|
|
40
|
+
for (const line of lines) {
|
|
41
|
+
if (!line.trim()) continue;
|
|
42
|
+
try {
|
|
43
|
+
const entry = JSON.parse(line);
|
|
44
|
+
if (entry.type === 'assistant') lastAssistant = entry;
|
|
45
|
+
} catch (e) { debugLog('extractLastResponse', `skip: ${e.message}`); }
|
|
46
|
+
}
|
|
47
|
+
if (!lastAssistant?.message?.content) return '';
|
|
48
|
+
const textBlocks = lastAssistant.message.content
|
|
49
|
+
.filter(b => b.type === 'text')
|
|
50
|
+
.map(b => b.text || '');
|
|
51
|
+
return textBlocks.join('\n').trim();
|
|
52
|
+
} catch (e) {
|
|
53
|
+
debugLog('extractLastResponse', `failed: ${e.message}`);
|
|
54
|
+
return '';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function detectAgent(projectRoot, sessionId) {
|
|
59
|
+
try {
|
|
60
|
+
const { readRegistry, getLanes } = await import('../core/registry.js');
|
|
61
|
+
const { detectProject } = await import('../core/project.js');
|
|
62
|
+
const project = detectProject(projectRoot);
|
|
63
|
+
debugLog('detectAgent', `root=${projectRoot} project=${project?.id || 'null'} hcm_dir=${project?.hcm_dir || 'null'}`);
|
|
64
|
+
if (!project) return null;
|
|
65
|
+
const reg = readRegistry(project.hcm_dir);
|
|
66
|
+
debugLog('detectAgent', `reg=${reg ? 'found' : 'null'}`);
|
|
67
|
+
if (!reg) return null;
|
|
68
|
+
const lanes = getLanes(reg);
|
|
69
|
+
debugLog('detectAgent', `lanes=${lanes?.length || 0} sessionId=${sessionId}`);
|
|
70
|
+
if (!lanes || lanes.length === 0) return null;
|
|
71
|
+
// Match by session_id when available (multi-agent safe)
|
|
72
|
+
if (sessionId) {
|
|
73
|
+
const match = lanes.find(l => l.session_id === sessionId);
|
|
74
|
+
if (match) return match.name || null;
|
|
75
|
+
// No match — unknown session (service LLM, etc.), do not fallback
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
// No sessionId provided — fallback to first agent (legacy)
|
|
79
|
+
const active = lanes.find(l => l.session_id);
|
|
80
|
+
return active?.name || lanes[0]?.name || null;
|
|
81
|
+
} catch (e) { debugLog('detectAgent', `failed: ${e.message}`); return null; }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// --- SessionStart ---
|
|
85
|
+
|
|
86
|
+
export async function handlePreHook(projectRoot) {
|
|
87
|
+
try {
|
|
88
|
+
debugLog('SessionStart', 'fired');
|
|
89
|
+
const stdin = readStdin();
|
|
90
|
+
const agentName = await detectAgent(projectRoot, stdin.session_id);
|
|
91
|
+
debugLog('SessionStart', `agent=${agentName || 'none'} session=${stdin.session_id || 'none'}`);
|
|
92
|
+
if (!agentName) return;
|
|
93
|
+
|
|
94
|
+
// Update PID in registry (new process on --resume) + register for event bus
|
|
95
|
+
try {
|
|
96
|
+
const { registerPid } = await import('./pid-checker.js');
|
|
97
|
+
registerPid(projectRoot, agentName, process.ppid || process.pid, stdin.session_id);
|
|
98
|
+
} catch (e) { debugLog('SessionStart', `registerPid: ${e.message}`); }
|
|
99
|
+
|
|
100
|
+
// Poll + inject pending events + purge expired
|
|
101
|
+
try {
|
|
102
|
+
const { consumeAllEvents, formatEventsForInjection, purgeExpired } = await import('./event-bus.js');
|
|
103
|
+
purgeExpired(projectRoot);
|
|
104
|
+
const events = consumeAllEvents(projectRoot, agentName);
|
|
105
|
+
if (events.length > 0) {
|
|
106
|
+
process.stdout.write(formatEventsForInjection(events));
|
|
107
|
+
}
|
|
108
|
+
} catch (e) { debugLog('SessionStart', `event-bus: ${e.message}`); }
|
|
109
|
+
|
|
110
|
+
// Consume .pending-cr (injection CR at resume)
|
|
111
|
+
const crText = consumePendingCr(projectRoot, agentName);
|
|
112
|
+
if (crText) {
|
|
113
|
+
process.stdout.write(crText);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Resolve role + projectId for unified context
|
|
117
|
+
const ctx = resolveAgentContext(projectRoot, agentName);
|
|
118
|
+
const role = ctx?.isLeader ? 'leader' : 'expert';
|
|
119
|
+
const projectId = ctx?.projectId || '';
|
|
120
|
+
|
|
121
|
+
// Write initial situation
|
|
122
|
+
writeCurrentSituation(projectRoot, 'onboarding');
|
|
123
|
+
|
|
124
|
+
// Single unified context injection (replaces 3 separate calls)
|
|
125
|
+
const contextInjection = await buildUnifiedContext(projectRoot, agentName, role, 'onboarding', { projectId });
|
|
126
|
+
if (contextInjection) {
|
|
127
|
+
process.stdout.write(contextInjection);
|
|
128
|
+
debugLog('SessionStart', `unified context injected for ${agentName} (${role})`);
|
|
129
|
+
}
|
|
130
|
+
} catch (e) { debugLog('SessionStart', `FAILED: ${e.message}`); }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// --- UserPromptSubmit ---
|
|
134
|
+
|
|
135
|
+
export async function handleDynamicHook(projectRoot) {
|
|
136
|
+
try {
|
|
137
|
+
debugLog('UserPromptSubmit', 'fired');
|
|
138
|
+
const stdin = readStdin();
|
|
139
|
+
const sessionId = stdin.session_id;
|
|
140
|
+
const prompt = stdin.prompt;
|
|
141
|
+
debugLog('UserPromptSubmit', `session=${sessionId || 'none'} prompt=${(prompt || '').slice(0, 50)}`);
|
|
142
|
+
if (!sessionId || !prompt) return;
|
|
143
|
+
|
|
144
|
+
const agentName = await detectAgent(projectRoot, sessionId);
|
|
145
|
+
if (!agentName) return;
|
|
146
|
+
|
|
147
|
+
// Detect situation and inject update if situation changed
|
|
148
|
+
try {
|
|
149
|
+
const { situation } = detectSituation(prompt);
|
|
150
|
+
const currentSituation = readCurrentSituation(projectRoot);
|
|
151
|
+
if (situation !== currentSituation && situation !== 'general') {
|
|
152
|
+
writeCurrentSituation(projectRoot, situation);
|
|
153
|
+
const update = await buildSituationUpdate(projectRoot, situation);
|
|
154
|
+
if (update) {
|
|
155
|
+
process.stdout.write(update);
|
|
156
|
+
debugLog('UserPromptSubmit', `situation changed: ${currentSituation} → ${situation}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
} catch (e) { debugLog('UserPromptSubmit', `situation detect FAILED: ${e.message}`); }
|
|
160
|
+
|
|
161
|
+
// Poll + inject pending events (ALL agents, not just Leader)
|
|
162
|
+
try {
|
|
163
|
+
const { consumeAllEvents, formatEventsForInjection } = await import('./event-bus.js');
|
|
164
|
+
const events = consumeAllEvents(projectRoot, agentName);
|
|
165
|
+
if (events.length > 0) {
|
|
166
|
+
process.stdout.write(formatEventsForInjection(events));
|
|
167
|
+
}
|
|
168
|
+
} catch (e) { debugLog('UserPromptSubmit', `event-bus: ${e.message}`); }
|
|
169
|
+
|
|
170
|
+
const nwConfig = readNotewriterConfig(projectRoot);
|
|
171
|
+
if (!nwConfig.note_enabled) return;
|
|
172
|
+
|
|
173
|
+
// Calculate turn number from temp counter
|
|
174
|
+
const counterPath = join(projectRoot, '.nemesis', `.turn-counter-${sessionId}`);
|
|
175
|
+
let turnNumber = 1;
|
|
176
|
+
if (existsSync(counterPath)) {
|
|
177
|
+
try { turnNumber = parseInt(readFileSync(counterPath, 'utf-8').trim()) + 1; }
|
|
178
|
+
catch (e) { debugLog('UserPromptSubmit', `turnCounter: ${e.message}`); turnNumber = 1; }
|
|
179
|
+
}
|
|
180
|
+
writeFileSync(counterPath, String(turnNumber), 'utf-8');
|
|
181
|
+
|
|
182
|
+
// openTurn — stores in memory + .pending-turn file
|
|
183
|
+
openTurn(projectRoot, agentName, sessionId, turnNumber, prompt);
|
|
184
|
+
|
|
185
|
+
// Store prompt for post-hook bridge
|
|
186
|
+
const lastPromptPath = join(projectRoot, '.nemesis', 'last-prompt.tmp');
|
|
187
|
+
writeFileSync(lastPromptPath, prompt, 'utf-8');
|
|
188
|
+
|
|
189
|
+
// Consume .pending-cr if present
|
|
190
|
+
const crText = consumePendingCr(projectRoot, agentName);
|
|
191
|
+
if (crText) {
|
|
192
|
+
process.stdout.write(crText);
|
|
193
|
+
}
|
|
194
|
+
} catch (e) { debugLog('UserPromptSubmit', `FAILED: ${e.message}`); }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// --- Stop ---
|
|
198
|
+
|
|
199
|
+
export async function handlePostHook(projectRoot) {
|
|
200
|
+
try {
|
|
201
|
+
debugLog('Stop', 'fired');
|
|
202
|
+
const stdin = readStdin();
|
|
203
|
+
const sessionId = stdin.session_id;
|
|
204
|
+
debugLog('Stop', `session=${sessionId || 'none'} has_last_msg=${!!stdin.last_assistant_message}`);
|
|
205
|
+
// last_assistant_message = direct from Claude, no file parsing needed
|
|
206
|
+
// extractLastResponse = fallback via transcript JSONL
|
|
207
|
+
const response = stdin.last_assistant_message || extractLastResponse(stdin.transcript_path);
|
|
208
|
+
if (!sessionId) return;
|
|
209
|
+
|
|
210
|
+
const agentName = await detectAgent(projectRoot, sessionId);
|
|
211
|
+
if (!agentName) return;
|
|
212
|
+
|
|
213
|
+
// Dispatch signal — if this agent had a pending dispatch, write result
|
|
214
|
+
const pending = findPendingDispatch(projectRoot, agentName);
|
|
215
|
+
if (pending) {
|
|
216
|
+
const dispatchResult = {
|
|
217
|
+
agentName,
|
|
218
|
+
odmId: pending.metadata.odmId || '',
|
|
219
|
+
projectId: pending.metadata.projectId || '',
|
|
220
|
+
missionId: pending.metadata.missionId || '',
|
|
221
|
+
response: response || '',
|
|
222
|
+
finishedAt: new Date().toISOString(),
|
|
223
|
+
};
|
|
224
|
+
writeDispatchResult(projectRoot, pending.txnId, dispatchResult);
|
|
225
|
+
consumePendingDispatch(projectRoot, pending.txnId);
|
|
226
|
+
|
|
227
|
+
// Auto-deposit: update TXN status SENT → COMPLETED
|
|
228
|
+
try {
|
|
229
|
+
const txnPath = join(projectRoot, '.nemesis', 'HCM', 'transactions', `${pending.txnId}.json`);
|
|
230
|
+
if (existsSync(txnPath)) {
|
|
231
|
+
const txn = JSON.parse(readFileSync(txnPath, 'utf-8'));
|
|
232
|
+
if (txn.transaction_meta) {
|
|
233
|
+
txn.transaction_meta.status = 'COMPLETED';
|
|
234
|
+
txn.transaction_meta.completed_at = new Date().toISOString();
|
|
235
|
+
}
|
|
236
|
+
writeFileSync(txnPath, JSON.stringify(txn, null, 2) + '\n', 'utf-8');
|
|
237
|
+
debugLog('Stop', `TXN ${pending.txnId} → COMPLETED`);
|
|
238
|
+
}
|
|
239
|
+
} catch (e) { debugLog('Stop', `TXN update FAILED: ${e.message}`); }
|
|
240
|
+
|
|
241
|
+
// V2: classify response + route to file + bridge to flowmap
|
|
242
|
+
if (response) {
|
|
243
|
+
try {
|
|
244
|
+
const mod = await import('../sync/service-session.js');
|
|
245
|
+
const callLlm = mod.createServiceCaller(projectRoot, 'dispatcher');
|
|
246
|
+
const { classifyResponse } = await import('./dispatcher.js');
|
|
247
|
+
const context = { agentName, odmId: dispatchResult.odmId, projectId: dispatchResult.projectId };
|
|
248
|
+
const { routing } = await classifyResponse(response, context, callLlm);
|
|
249
|
+
|
|
250
|
+
// Route to files for inter-process consumption
|
|
251
|
+
routeToFile(routing, context, projectRoot);
|
|
252
|
+
|
|
253
|
+
// Bridge to flowmap: detect signal and fire event
|
|
254
|
+
for (const entry of routing) {
|
|
255
|
+
const detected = processDispatchResult(projectRoot, dispatchResult, entry);
|
|
256
|
+
if (detected && detected.missionId) {
|
|
257
|
+
await fireFlowmapEvent(projectRoot, detected.missionId, detected.event);
|
|
258
|
+
debugLog('Stop', `flowmap event fired: ${detected.signalId} → ${detected.missionId}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Push event to Leader's inbox for LEADER-targeted routing entries
|
|
263
|
+
try {
|
|
264
|
+
const { pushEvent, EVENT_TYPES } = await import('./event-bus.js');
|
|
265
|
+
const { readRegistry, getLanes: getRegLanes } = await import('../core/registry.js');
|
|
266
|
+
const regForLeader = readRegistry(
|
|
267
|
+
(await import('../core/project.js')).detectProject(projectRoot)?.hcm_dir
|
|
268
|
+
);
|
|
269
|
+
if (regForLeader) {
|
|
270
|
+
const allLanes = getRegLanes(regForLeader);
|
|
271
|
+
const leader = allLanes.find(l => l.leader);
|
|
272
|
+
if (leader) {
|
|
273
|
+
for (const entry of routing) {
|
|
274
|
+
if (entry.target === 'LEADER') {
|
|
275
|
+
pushEvent(projectRoot, {
|
|
276
|
+
target_agent_id: leader.id || leader.name,
|
|
277
|
+
event_type: EVENT_TYPES.CR_DEPOSITED,
|
|
278
|
+
source_agent_id: agentName,
|
|
279
|
+
priority: entry.priority || 'MEDIUM',
|
|
280
|
+
payload: { summary: entry.summary || '', odmId: dispatchResult.odmId },
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
} catch (e) { debugLog('Stop', `event-bus push to leader FAILED: ${e.message}`); }
|
|
287
|
+
} catch (e) { debugLog('Stop', `dispatch classify FAILED: ${e.message}`); }
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Note: NE PAS unregisterPid ici — Stop != session end
|
|
292
|
+
// L'agent peut etre repris via --resume
|
|
293
|
+
|
|
294
|
+
const nwConfig = readNotewriterConfig(projectRoot);
|
|
295
|
+
debugLog('Stop', `note_enabled=${nwConfig.note_enabled}`);
|
|
296
|
+
|
|
297
|
+
// 1. closeTurn (reads .pending-turn if Map empty — inter-process)
|
|
298
|
+
const turnEntry = closeTurn(projectRoot, agentName, sessionId, response || '');
|
|
299
|
+
debugLog('Stop', `turnEntry=${turnEntry ? 'found' : 'null'}`);
|
|
300
|
+
|
|
301
|
+
// 2. generateNote (if enabled)
|
|
302
|
+
if (nwConfig.note_enabled && turnEntry) {
|
|
303
|
+
let callLlm;
|
|
304
|
+
try {
|
|
305
|
+
const mod = await import('../sync/service-session.js');
|
|
306
|
+
callLlm = mod.createServiceCaller(projectRoot, 'notes');
|
|
307
|
+
debugLog('Stop', 'callLlm created');
|
|
308
|
+
} catch (e) {
|
|
309
|
+
// Service LLM not configured — skip
|
|
310
|
+
debugLog('Stop', `callLlm FAILED: ${e.message}`);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
await generateNote(projectRoot, agentName, turnEntry, callLlm);
|
|
315
|
+
debugLog('Stop', 'note generated');
|
|
316
|
+
|
|
317
|
+
// 3. Check CR timer (lazy)
|
|
318
|
+
if (nwConfig.cr_enabled) {
|
|
319
|
+
if (shouldGenerateCR(projectRoot, agentName, nwConfig.cr_timer_minutes)) {
|
|
320
|
+
await generateCR(projectRoot, agentName, 'timer_30min', callLlm);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
} catch (err) {
|
|
325
|
+
// Never crash — log warning, exit 0
|
|
326
|
+
console.error(`NoteWriter hook error: ${err.message}`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// --- PreCompact ---
|
|
331
|
+
|
|
332
|
+
export async function handlePreCompact(projectRoot) {
|
|
333
|
+
try {
|
|
334
|
+
const stdin = readStdin();
|
|
335
|
+
const agentName = await detectAgent(projectRoot, stdin.session_id);
|
|
336
|
+
if (!agentName) return;
|
|
337
|
+
|
|
338
|
+
// Resolve role + projectId
|
|
339
|
+
const ctx = resolveAgentContext(projectRoot, agentName);
|
|
340
|
+
const role = ctx?.isLeader ? 'leader' : 'expert';
|
|
341
|
+
const projectId = ctx?.projectId || '';
|
|
342
|
+
const situation = readCurrentSituation(projectRoot) || 'general';
|
|
343
|
+
|
|
344
|
+
// Single unified context re-injection (same as SessionStart)
|
|
345
|
+
const contextInjection = await buildUnifiedContext(projectRoot, agentName, role, situation, { projectId });
|
|
346
|
+
if (contextInjection) {
|
|
347
|
+
process.stdout.write(contextInjection);
|
|
348
|
+
debugLog('PreCompact', `unified context re-injected for ${agentName}`);
|
|
349
|
+
}
|
|
350
|
+
} catch (e) { debugLog('PreCompact', `FAILED: ${e.message}`); }
|
|
351
|
+
}
|