@lumenflow/agent 1.0.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 +190 -0
- package/README.md +192 -0
- package/dist/agent-incidents.d.ts +55 -0
- package/dist/agent-incidents.js +91 -0
- package/dist/agent-session.d.ts +66 -0
- package/dist/agent-session.js +155 -0
- package/dist/agent-verification.d.ts +31 -0
- package/dist/agent-verification.js +118 -0
- package/dist/auto-session-integration.d.ts +106 -0
- package/dist/auto-session-integration.js +173 -0
- package/dist/feedback-promote-core.d.ts +137 -0
- package/dist/feedback-promote-core.js +337 -0
- package/dist/feedback-review-core.d.ts +116 -0
- package/dist/feedback-review-core.js +358 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/package.json +63 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import { readFile, writeFile, mkdir, unlink, access } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { simpleGit } from 'simple-git';
|
|
5
|
+
import { appendIncident } from './agent-incidents.js';
|
|
6
|
+
import { PATTERNS, INCIDENT_SEVERITY, BEACON_PATHS } from '@lumenflow/core/lib/wu-constants.js';
|
|
7
|
+
const SESSION_DIR = BEACON_PATHS.SESSIONS;
|
|
8
|
+
const SESSION_FILE = join(SESSION_DIR, 'current.json');
|
|
9
|
+
/**
|
|
10
|
+
* Start a new agent session
|
|
11
|
+
* @param wuId - WU ID (e.g., "WU-1234")
|
|
12
|
+
* @param tier - Context tier from bootloader
|
|
13
|
+
* @param agentType - Agent type (default: "claude-code")
|
|
14
|
+
* @returns session_id
|
|
15
|
+
* @throws {Error} if session already active or WU format invalid
|
|
16
|
+
*/
|
|
17
|
+
export async function startSession(wuId, tier, agentType = 'claude-code') {
|
|
18
|
+
// Check for existing session
|
|
19
|
+
const sessionExists = await access(SESSION_FILE)
|
|
20
|
+
.then(() => true)
|
|
21
|
+
.catch(() => false);
|
|
22
|
+
if (sessionExists) {
|
|
23
|
+
const content = await readFile(SESSION_FILE, { encoding: 'utf-8' });
|
|
24
|
+
const existing = JSON.parse(content);
|
|
25
|
+
throw new Error(`Session ${existing.session_id} already active for ${existing.wu_id}. ` +
|
|
26
|
+
`Run 'pnpm agent:session:end' first.`);
|
|
27
|
+
}
|
|
28
|
+
// Validate WU ID format
|
|
29
|
+
if (!PATTERNS.WU_ID.test(wuId)) {
|
|
30
|
+
throw new Error(`Invalid WU ID format: ${wuId}. Must match WU-XXX.`);
|
|
31
|
+
}
|
|
32
|
+
// Validate tier
|
|
33
|
+
if (![1, 2, 3].includes(tier)) {
|
|
34
|
+
throw new Error(`Invalid context tier: ${tier}. Must be 1, 2, or 3.`);
|
|
35
|
+
}
|
|
36
|
+
// Auto-detect lane from git branch if possible
|
|
37
|
+
const git = simpleGit();
|
|
38
|
+
let lane = 'Unknown';
|
|
39
|
+
try {
|
|
40
|
+
const branch = await git.revparse(['--abbrev-ref', 'HEAD']);
|
|
41
|
+
// Parse lane from branch name: lane/<lane>/wu-xxx → <lane>
|
|
42
|
+
const match = branch.match(/^lane\/([^/]+)\//);
|
|
43
|
+
if (match) {
|
|
44
|
+
lane = match[1]
|
|
45
|
+
.split('-')
|
|
46
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
47
|
+
.join(': ');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// Fallback: lane stays "Unknown"
|
|
52
|
+
}
|
|
53
|
+
const sessionId = randomUUID();
|
|
54
|
+
const session = {
|
|
55
|
+
session_id: sessionId,
|
|
56
|
+
wu_id: wuId,
|
|
57
|
+
lane,
|
|
58
|
+
started: new Date().toISOString(),
|
|
59
|
+
agent_type: agentType,
|
|
60
|
+
context_tier: tier,
|
|
61
|
+
incidents_logged: 0,
|
|
62
|
+
incidents_major: 0,
|
|
63
|
+
};
|
|
64
|
+
// Ensure directory exists
|
|
65
|
+
const dirExists = await access(SESSION_DIR)
|
|
66
|
+
.then(() => true)
|
|
67
|
+
.catch(() => false);
|
|
68
|
+
if (!dirExists) {
|
|
69
|
+
await mkdir(SESSION_DIR, { recursive: true });
|
|
70
|
+
}
|
|
71
|
+
await writeFile(SESSION_FILE, JSON.stringify(session, null, 2));
|
|
72
|
+
return sessionId;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Get the current active session
|
|
76
|
+
* @returns Session state or null if no active session
|
|
77
|
+
*/
|
|
78
|
+
export async function getCurrentSession() {
|
|
79
|
+
const sessionExists = await access(SESSION_FILE)
|
|
80
|
+
.then(() => true)
|
|
81
|
+
.catch(() => false);
|
|
82
|
+
if (!sessionExists)
|
|
83
|
+
return null;
|
|
84
|
+
const content = await readFile(SESSION_FILE, { encoding: 'utf-8' });
|
|
85
|
+
return JSON.parse(content);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Log an incident and update session counters
|
|
89
|
+
* @param incidentData - Incident data (category, severity, title, description, etc.)
|
|
90
|
+
* @throws {Error} if no active session
|
|
91
|
+
*/
|
|
92
|
+
export async function logIncident(incidentData) {
|
|
93
|
+
const session = await getCurrentSession();
|
|
94
|
+
if (!session) {
|
|
95
|
+
throw new Error('No active session. Run: pnpm agent:session start --wu WU-XXX --tier N');
|
|
96
|
+
}
|
|
97
|
+
// Get current git context
|
|
98
|
+
const git = simpleGit();
|
|
99
|
+
let gitBranch = 'unknown';
|
|
100
|
+
try {
|
|
101
|
+
gitBranch = await git.revparse(['--abbrev-ref', 'HEAD']);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// Ignore git errors
|
|
105
|
+
}
|
|
106
|
+
// Build full incident record
|
|
107
|
+
const incident = {
|
|
108
|
+
timestamp: new Date().toISOString(),
|
|
109
|
+
session_id: session.session_id,
|
|
110
|
+
wu_id: session.wu_id,
|
|
111
|
+
lane: session.lane,
|
|
112
|
+
...incidentData,
|
|
113
|
+
context: {
|
|
114
|
+
git_branch: gitBranch,
|
|
115
|
+
...(incidentData.context ?? {}),
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
// Append to NDJSON (will validate)
|
|
119
|
+
appendIncident(incident);
|
|
120
|
+
// Update session counters
|
|
121
|
+
session.incidents_logged++;
|
|
122
|
+
if (incident.severity === INCIDENT_SEVERITY.MAJOR ||
|
|
123
|
+
incident.severity === INCIDENT_SEVERITY.BLOCKER) {
|
|
124
|
+
session.incidents_major++;
|
|
125
|
+
}
|
|
126
|
+
await writeFile(SESSION_FILE, JSON.stringify(session, null, 2));
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* End the current session and return summary
|
|
130
|
+
* @returns Session summary for appending to WU YAML
|
|
131
|
+
* @throws {Error} if no active session
|
|
132
|
+
*/
|
|
133
|
+
export async function endSession() {
|
|
134
|
+
const session = await getCurrentSession();
|
|
135
|
+
if (!session) {
|
|
136
|
+
throw new Error('No active session to end.');
|
|
137
|
+
}
|
|
138
|
+
// Finalize session
|
|
139
|
+
session.completed = new Date().toISOString();
|
|
140
|
+
// Clean up session file
|
|
141
|
+
await unlink(SESSION_FILE);
|
|
142
|
+
// Return session object for WU YAML
|
|
143
|
+
return {
|
|
144
|
+
wu_id: session.wu_id,
|
|
145
|
+
lane: session.lane,
|
|
146
|
+
session_id: session.session_id,
|
|
147
|
+
started: session.started,
|
|
148
|
+
completed: session.completed,
|
|
149
|
+
agent_type: session.agent_type,
|
|
150
|
+
context_tier: session.context_tier,
|
|
151
|
+
incidents_logged: session.incidents_logged,
|
|
152
|
+
incidents_major: session.incidents_major,
|
|
153
|
+
// artifacts can be added manually later
|
|
154
|
+
};
|
|
155
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
type RunFn = (cmd: string) => string;
|
|
2
|
+
type ExistsFn = (path: string) => boolean;
|
|
3
|
+
/**
|
|
4
|
+
* Verification result type
|
|
5
|
+
*/
|
|
6
|
+
export interface VerificationResult {
|
|
7
|
+
complete: boolean;
|
|
8
|
+
failures: string[];
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Verification overrides for testing
|
|
12
|
+
*/
|
|
13
|
+
interface VerificationOverrides {
|
|
14
|
+
run?: RunFn;
|
|
15
|
+
exists?: ExistsFn;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Verify that a WU has been completed and merged to main.
|
|
19
|
+
*
|
|
20
|
+
* Checks:
|
|
21
|
+
* 1. Working tree is clean
|
|
22
|
+
* 2. Completion stamp exists
|
|
23
|
+
* 3. Main history contains a commit updating the WU YAML
|
|
24
|
+
*
|
|
25
|
+
* @param wuId - Work Unit identifier (e.g., "WU-510")
|
|
26
|
+
* @param overrides - Test overrides
|
|
27
|
+
* @returns Verification result
|
|
28
|
+
*/
|
|
29
|
+
export declare function verifyWUComplete(wuId: string, overrides?: VerificationOverrides): VerificationResult;
|
|
30
|
+
export declare function debugSummary(result: VerificationResult | null | undefined): string;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { createError, ErrorCodes } from '@lumenflow/core/lib/error-handler.js';
|
|
6
|
+
import { PATTERNS, BRANCHES, STRING_LITERALS, EXIT_CODES, } from '@lumenflow/core/lib/wu-constants.js';
|
|
7
|
+
function run(cmd) {
|
|
8
|
+
try {
|
|
9
|
+
return execSync(cmd, { stdio: 'pipe', encoding: 'utf-8' }).trim();
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return '';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function formatWUId(wuId) {
|
|
16
|
+
if (!wuId || typeof wuId !== 'string') {
|
|
17
|
+
throw createError(ErrorCodes.VALIDATION_ERROR, 'verifyWUComplete requires a WU id (e.g., WU-123)', { wuId, type: typeof wuId });
|
|
18
|
+
}
|
|
19
|
+
const normalized = wuId.trim().toUpperCase();
|
|
20
|
+
if (!PATTERNS.WU_ID.test(normalized)) {
|
|
21
|
+
throw createError(ErrorCodes.INVALID_WU_ID, `Invalid WU id "${wuId}". Expected format: WU-123`, { wuId, normalized });
|
|
22
|
+
}
|
|
23
|
+
return normalized;
|
|
24
|
+
}
|
|
25
|
+
function checkGitStatus(runFn = run) {
|
|
26
|
+
const status = runFn('git status --porcelain');
|
|
27
|
+
if (!status)
|
|
28
|
+
return null;
|
|
29
|
+
const lines = status.split(STRING_LITERALS.NEWLINE).filter(Boolean).slice(0, 10);
|
|
30
|
+
return `Working tree dirty (stage or discard changes): ${lines.join('; ')}`;
|
|
31
|
+
}
|
|
32
|
+
function stampPath(wuId) {
|
|
33
|
+
return path.join('.beacon', 'stamps', `${wuId}.done`);
|
|
34
|
+
}
|
|
35
|
+
function checkStamp(wuId, existsFn = existsSync) {
|
|
36
|
+
if (existsFn(stampPath(wuId)))
|
|
37
|
+
return null;
|
|
38
|
+
return `Missing stamp .beacon/stamps/${wuId}.done`;
|
|
39
|
+
}
|
|
40
|
+
function checkCommit(wuId, runFn = run) {
|
|
41
|
+
const history = runFn(`git log --oneline ${BRANCHES.MAIN} -- docs/04-operations/tasks/wu/${wuId}.yaml | head -n 1`);
|
|
42
|
+
if (history)
|
|
43
|
+
return null;
|
|
44
|
+
return `No commit on ${BRANCHES.MAIN} touching docs/04-operations/tasks/wu/${wuId}.yaml`;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Verify that a WU has been completed and merged to main.
|
|
48
|
+
*
|
|
49
|
+
* Checks:
|
|
50
|
+
* 1. Working tree is clean
|
|
51
|
+
* 2. Completion stamp exists
|
|
52
|
+
* 3. Main history contains a commit updating the WU YAML
|
|
53
|
+
*
|
|
54
|
+
* @param wuId - Work Unit identifier (e.g., "WU-510")
|
|
55
|
+
* @param overrides - Test overrides
|
|
56
|
+
* @returns Verification result
|
|
57
|
+
*/
|
|
58
|
+
export function verifyWUComplete(wuId, overrides = {}) {
|
|
59
|
+
const normalized = formatWUId(wuId);
|
|
60
|
+
const failures = [];
|
|
61
|
+
const runFn = typeof overrides.run === 'function' ? overrides.run : run;
|
|
62
|
+
const existsFn = typeof overrides.exists === 'function'
|
|
63
|
+
? overrides.exists
|
|
64
|
+
: (filePath) => existsSync(filePath);
|
|
65
|
+
const gitStatusFailure = checkGitStatus(runFn);
|
|
66
|
+
if (gitStatusFailure)
|
|
67
|
+
failures.push(gitStatusFailure);
|
|
68
|
+
const stampFailure = checkStamp(normalized, existsFn);
|
|
69
|
+
if (stampFailure)
|
|
70
|
+
failures.push(stampFailure);
|
|
71
|
+
const commitFailure = checkCommit(normalized, runFn);
|
|
72
|
+
if (commitFailure)
|
|
73
|
+
failures.push(commitFailure);
|
|
74
|
+
return {
|
|
75
|
+
complete: failures.length === 0,
|
|
76
|
+
failures,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export function debugSummary(result) {
|
|
80
|
+
if (!result || typeof result !== 'object') {
|
|
81
|
+
return 'No verification result';
|
|
82
|
+
}
|
|
83
|
+
if (result.complete) {
|
|
84
|
+
return 'Verification passed: WU complete.';
|
|
85
|
+
}
|
|
86
|
+
const failures = Array.isArray(result.failures) ? result.failures : [];
|
|
87
|
+
if (!failures.length) {
|
|
88
|
+
return 'Verification failed: unknown reason.';
|
|
89
|
+
}
|
|
90
|
+
return `Verification failed:${STRING_LITERALS.NEWLINE}- ${failures.join(`${STRING_LITERALS.NEWLINE}- `)}`;
|
|
91
|
+
}
|
|
92
|
+
const isDirectExecution = (() => {
|
|
93
|
+
if (typeof process === 'undefined')
|
|
94
|
+
return false;
|
|
95
|
+
if (!Array.isArray(process.argv))
|
|
96
|
+
return false;
|
|
97
|
+
if (!process.argv[1])
|
|
98
|
+
return false;
|
|
99
|
+
try {
|
|
100
|
+
return fileURLToPath(import.meta.url) === process.argv[1];
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
})();
|
|
106
|
+
if (isDirectExecution) {
|
|
107
|
+
const wuId = process.argv[2];
|
|
108
|
+
try {
|
|
109
|
+
const result = verifyWUComplete(wuId);
|
|
110
|
+
const message = debugSummary(result);
|
|
111
|
+
console.log(message);
|
|
112
|
+
process.exit(result.complete ? EXIT_CODES.SUCCESS : EXIT_CODES.ERROR);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
console.error(`Verification error: ${error.message}`);
|
|
116
|
+
process.exit(EXIT_CODES.ERROR);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session data stored in current.json
|
|
3
|
+
*/
|
|
4
|
+
interface SessionFileData {
|
|
5
|
+
session_id: string;
|
|
6
|
+
wu_id: string;
|
|
7
|
+
started: string;
|
|
8
|
+
completed?: string;
|
|
9
|
+
agent_type: string;
|
|
10
|
+
context_tier: number;
|
|
11
|
+
incidents_logged: number;
|
|
12
|
+
incidents_major: number;
|
|
13
|
+
auto_started?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Options for starting a session for a WU
|
|
17
|
+
*/
|
|
18
|
+
interface StartSessionOptions {
|
|
19
|
+
wuId: string;
|
|
20
|
+
tier?: 1 | 2 | 3;
|
|
21
|
+
agentType?: string;
|
|
22
|
+
sessionDir?: string;
|
|
23
|
+
baseDir?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Result of starting a session
|
|
27
|
+
*/
|
|
28
|
+
interface StartSessionResult {
|
|
29
|
+
sessionId: string;
|
|
30
|
+
alreadyActive?: boolean;
|
|
31
|
+
memoryNodeId?: string | null;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Start a session for a WU (called by wu:claim)
|
|
35
|
+
*
|
|
36
|
+
* Unlike startSession in agent-session.mjs, this function:
|
|
37
|
+
* - Does NOT throw if a session already exists (returns existing session)
|
|
38
|
+
* - Uses default tier 2 if not specified
|
|
39
|
+
* - Supports custom session directory for testing
|
|
40
|
+
* - Creates memory layer session node for context restoration (WU-1466)
|
|
41
|
+
*
|
|
42
|
+
* @param options - Session options
|
|
43
|
+
* @returns Session result
|
|
44
|
+
*/
|
|
45
|
+
export declare function startSessionForWU(options: StartSessionOptions): Promise<StartSessionResult>;
|
|
46
|
+
/**
|
|
47
|
+
* Options for ending a session
|
|
48
|
+
*/
|
|
49
|
+
interface EndSessionOptions {
|
|
50
|
+
sessionDir?: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Session summary
|
|
54
|
+
*/
|
|
55
|
+
interface SessionSummary {
|
|
56
|
+
wu_id: string;
|
|
57
|
+
session_id: string;
|
|
58
|
+
started: string;
|
|
59
|
+
completed: string;
|
|
60
|
+
agent_type: string;
|
|
61
|
+
context_tier: number;
|
|
62
|
+
incidents_logged: number;
|
|
63
|
+
incidents_major: number;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Result of ending a session
|
|
67
|
+
*/
|
|
68
|
+
interface EndSessionResult {
|
|
69
|
+
ended: boolean;
|
|
70
|
+
summary?: SessionSummary;
|
|
71
|
+
reason?: string;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* End the current session (called by wu:done)
|
|
75
|
+
*
|
|
76
|
+
* Unlike endSession in agent-session.mjs, this function:
|
|
77
|
+
* - Does NOT throw if no active session (returns { ended: false })
|
|
78
|
+
* - Returns structured result with summary
|
|
79
|
+
* - Supports custom session directory for testing
|
|
80
|
+
*
|
|
81
|
+
* @param options - Session options
|
|
82
|
+
* @returns Session end result
|
|
83
|
+
*/
|
|
84
|
+
export declare function endSessionForWU(options?: EndSessionOptions): EndSessionResult;
|
|
85
|
+
/**
|
|
86
|
+
* Options for getting current session
|
|
87
|
+
*/
|
|
88
|
+
interface GetSessionOptions {
|
|
89
|
+
sessionDir?: string;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Get the current active session
|
|
93
|
+
*
|
|
94
|
+
* @param options - Session options
|
|
95
|
+
* @returns Session object or null if no active session
|
|
96
|
+
*/
|
|
97
|
+
export declare function getCurrentSessionForWU(options?: GetSessionOptions): SessionFileData | null;
|
|
98
|
+
/**
|
|
99
|
+
* Check if there's an active session for a specific WU
|
|
100
|
+
*
|
|
101
|
+
* @param wuId - WU ID to check
|
|
102
|
+
* @param options - Session options
|
|
103
|
+
* @returns True if session exists and matches WU ID
|
|
104
|
+
*/
|
|
105
|
+
export declare function hasActiveSessionForWU(wuId: string, options?: GetSessionOptions): boolean;
|
|
106
|
+
export {};
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-Session Integration for wu:claim and wu:done lifecycle (WU-1438, WU-1466)
|
|
3
|
+
*
|
|
4
|
+
* Provides wrapper functions around agent-session.mjs that:
|
|
5
|
+
* 1. Auto-start sessions on wu:claim with silent no-op if already active
|
|
6
|
+
* 2. Auto-end sessions on wu:done with silent no-op if not active
|
|
7
|
+
* 3. Store session_id in WU YAML for tracking
|
|
8
|
+
* 4. Create memory layer session nodes for context restoration (WU-1466)
|
|
9
|
+
*
|
|
10
|
+
* Design principles:
|
|
11
|
+
* - Composition over modification (wraps existing agent-session.mjs)
|
|
12
|
+
* - Silent failures for idempotent operations (no throw on duplicate start/end)
|
|
13
|
+
* - Configurable session directory for testing
|
|
14
|
+
*/
|
|
15
|
+
import { randomUUID } from 'crypto';
|
|
16
|
+
import { readFileSync, writeFileSync, existsSync, unlinkSync, mkdirSync } from 'fs';
|
|
17
|
+
import { join } from 'path';
|
|
18
|
+
import { startSession as startMemorySession } from '@lumenflow/memory/start';
|
|
19
|
+
// Default session directory (same as agent-session.mjs)
|
|
20
|
+
const DEFAULT_SESSION_DIR = '.beacon/sessions';
|
|
21
|
+
const SESSION_FILENAME = 'current.json';
|
|
22
|
+
// Default context tier for auto-started sessions
|
|
23
|
+
const DEFAULT_TIER = 2;
|
|
24
|
+
// Agent type for auto-started sessions
|
|
25
|
+
const DEFAULT_AGENT_TYPE = 'claude-code';
|
|
26
|
+
/**
|
|
27
|
+
* Map numeric tier values to string names for memory layer (WU-1466)
|
|
28
|
+
*/
|
|
29
|
+
const CONTEXT_TIER_MAP = {
|
|
30
|
+
1: 'minimal',
|
|
31
|
+
2: 'core',
|
|
32
|
+
3: 'full',
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Get the session file path for a given session directory
|
|
36
|
+
* @param sessionDir - Session directory path
|
|
37
|
+
* @returns Full path to current.json
|
|
38
|
+
*/
|
|
39
|
+
function getSessionFilePath(sessionDir) {
|
|
40
|
+
return join(sessionDir, SESSION_FILENAME);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Start a session for a WU (called by wu:claim)
|
|
44
|
+
*
|
|
45
|
+
* Unlike startSession in agent-session.mjs, this function:
|
|
46
|
+
* - Does NOT throw if a session already exists (returns existing session)
|
|
47
|
+
* - Uses default tier 2 if not specified
|
|
48
|
+
* - Supports custom session directory for testing
|
|
49
|
+
* - Creates memory layer session node for context restoration (WU-1466)
|
|
50
|
+
*
|
|
51
|
+
* @param options - Session options
|
|
52
|
+
* @returns Session result
|
|
53
|
+
*/
|
|
54
|
+
export async function startSessionForWU(options) {
|
|
55
|
+
const { wuId, tier = DEFAULT_TIER, agentType = DEFAULT_AGENT_TYPE, sessionDir, baseDir = process.cwd(), } = options;
|
|
56
|
+
const sessDir = sessionDir ?? DEFAULT_SESSION_DIR;
|
|
57
|
+
const sessionFile = getSessionFilePath(sessDir);
|
|
58
|
+
// Check for existing session - return it instead of throwing
|
|
59
|
+
if (existsSync(sessionFile)) {
|
|
60
|
+
const existing = JSON.parse(readFileSync(sessionFile, { encoding: 'utf-8' }));
|
|
61
|
+
return {
|
|
62
|
+
sessionId: existing.session_id,
|
|
63
|
+
alreadyActive: true,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
// Create session directory if needed
|
|
67
|
+
if (!existsSync(sessDir)) {
|
|
68
|
+
mkdirSync(sessDir, { recursive: true });
|
|
69
|
+
}
|
|
70
|
+
// Create new session
|
|
71
|
+
const sessionId = randomUUID();
|
|
72
|
+
const session = {
|
|
73
|
+
session_id: sessionId,
|
|
74
|
+
wu_id: wuId,
|
|
75
|
+
started: new Date().toISOString(),
|
|
76
|
+
agent_type: agentType,
|
|
77
|
+
context_tier: tier,
|
|
78
|
+
incidents_logged: 0,
|
|
79
|
+
incidents_major: 0,
|
|
80
|
+
auto_started: true, // Mark as auto-started by wu:claim
|
|
81
|
+
};
|
|
82
|
+
writeFileSync(sessionFile, JSON.stringify(session, null, 2), { encoding: 'utf-8' });
|
|
83
|
+
// WU-1466: Create memory layer session node for context restoration
|
|
84
|
+
// This enables context restoration after /clear by persisting session info to memory.jsonl
|
|
85
|
+
let memoryNodeId = null;
|
|
86
|
+
try {
|
|
87
|
+
const memResult = await startMemorySession(baseDir, {
|
|
88
|
+
wuId,
|
|
89
|
+
agentType,
|
|
90
|
+
contextTier: CONTEXT_TIER_MAP[tier] ?? 'full',
|
|
91
|
+
});
|
|
92
|
+
memoryNodeId = memResult.session?.id ?? null;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// Memory layer creation is non-blocking - log but don't fail
|
|
96
|
+
// Session file was already created, so the session is functional
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
sessionId,
|
|
100
|
+
alreadyActive: false,
|
|
101
|
+
memoryNodeId,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* End the current session (called by wu:done)
|
|
106
|
+
*
|
|
107
|
+
* Unlike endSession in agent-session.mjs, this function:
|
|
108
|
+
* - Does NOT throw if no active session (returns { ended: false })
|
|
109
|
+
* - Returns structured result with summary
|
|
110
|
+
* - Supports custom session directory for testing
|
|
111
|
+
*
|
|
112
|
+
* @param options - Session options
|
|
113
|
+
* @returns Session end result
|
|
114
|
+
*/
|
|
115
|
+
export function endSessionForWU(options = {}) {
|
|
116
|
+
const { sessionDir } = options;
|
|
117
|
+
const sessDir = sessionDir ?? DEFAULT_SESSION_DIR;
|
|
118
|
+
const sessionFile = getSessionFilePath(sessDir);
|
|
119
|
+
// Check for active session - return early if none
|
|
120
|
+
if (!existsSync(sessionFile)) {
|
|
121
|
+
return {
|
|
122
|
+
ended: false,
|
|
123
|
+
reason: 'no_active_session',
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
// Read session data
|
|
127
|
+
const session = JSON.parse(readFileSync(sessionFile, { encoding: 'utf-8' }));
|
|
128
|
+
// Finalize session
|
|
129
|
+
session.completed = new Date().toISOString();
|
|
130
|
+
// Build summary for WU YAML
|
|
131
|
+
const summary = {
|
|
132
|
+
wu_id: session.wu_id,
|
|
133
|
+
session_id: session.session_id,
|
|
134
|
+
started: session.started,
|
|
135
|
+
completed: session.completed,
|
|
136
|
+
agent_type: session.agent_type,
|
|
137
|
+
context_tier: session.context_tier,
|
|
138
|
+
incidents_logged: session.incidents_logged,
|
|
139
|
+
incidents_major: session.incidents_major,
|
|
140
|
+
};
|
|
141
|
+
// Remove session file
|
|
142
|
+
unlinkSync(sessionFile);
|
|
143
|
+
return {
|
|
144
|
+
ended: true,
|
|
145
|
+
summary,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Get the current active session
|
|
150
|
+
*
|
|
151
|
+
* @param options - Session options
|
|
152
|
+
* @returns Session object or null if no active session
|
|
153
|
+
*/
|
|
154
|
+
export function getCurrentSessionForWU(options = {}) {
|
|
155
|
+
const { sessionDir } = options;
|
|
156
|
+
const sessDir = sessionDir ?? DEFAULT_SESSION_DIR;
|
|
157
|
+
const sessionFile = getSessionFilePath(sessDir);
|
|
158
|
+
if (!existsSync(sessionFile)) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
return JSON.parse(readFileSync(sessionFile, { encoding: 'utf-8' }));
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Check if there's an active session for a specific WU
|
|
165
|
+
*
|
|
166
|
+
* @param wuId - WU ID to check
|
|
167
|
+
* @param options - Session options
|
|
168
|
+
* @returns True if session exists and matches WU ID
|
|
169
|
+
*/
|
|
170
|
+
export function hasActiveSessionForWU(wuId, options = {}) {
|
|
171
|
+
const session = getCurrentSessionForWU(options);
|
|
172
|
+
return session !== null && session.wu_id === wuId;
|
|
173
|
+
}
|