gru-ai 0.1.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/.claude/skills/brainstorm/SKILL.md +340 -0
- package/.claude/skills/code-review-excellence/SKILL.md +198 -0
- package/.claude/skills/directive/SKILL.md +121 -0
- package/.claude/skills/directive/docs/pipeline/00-delegation-and-triage.md +181 -0
- package/.claude/skills/directive/docs/pipeline/01-checkpoint.md +34 -0
- package/.claude/skills/directive/docs/pipeline/02-read-directive.md +38 -0
- package/.claude/skills/directive/docs/pipeline/03-read-context.md +15 -0
- package/.claude/skills/directive/docs/pipeline/04-challenge.md +38 -0
- package/.claude/skills/directive/docs/pipeline/05-planning.md +64 -0
- package/.claude/skills/directive/docs/pipeline/06-technical-audit.md +88 -0
- package/.claude/skills/directive/docs/pipeline/07-plan-approval.md +145 -0
- package/.claude/skills/directive/docs/pipeline/07b-project-brainstorm.md +85 -0
- package/.claude/skills/directive/docs/pipeline/08-worktree-and-state.md +50 -0
- package/.claude/skills/directive/docs/pipeline/09-execute-projects.md +709 -0
- package/.claude/skills/directive/docs/pipeline/10-wrapup.md +242 -0
- package/.claude/skills/directive/docs/pipeline/11-completion-gate.md +75 -0
- package/.claude/skills/directive/docs/reference/rules/casting-rules.md +78 -0
- package/.claude/skills/directive/docs/reference/rules/failure-handling.md +20 -0
- package/.claude/skills/directive/docs/reference/rules/phase-definitions.md +42 -0
- package/.claude/skills/directive/docs/reference/rules/scope-and-dod.md +30 -0
- package/.claude/skills/directive/docs/reference/schemas/audit-output.md +44 -0
- package/.claude/skills/directive/docs/reference/schemas/brainstorm-output.md +52 -0
- package/.claude/skills/directive/docs/reference/schemas/challenger-output.md +13 -0
- package/.claude/skills/directive/docs/reference/schemas/checkpoint.md +18 -0
- package/.claude/skills/directive/docs/reference/schemas/current-json.md +5 -0
- package/.claude/skills/directive/docs/reference/schemas/directive-json.md +143 -0
- package/.claude/skills/directive/docs/reference/schemas/investigation-output.md +37 -0
- package/.claude/skills/directive/docs/reference/schemas/plan-schema.md +103 -0
- package/.claude/skills/directive/docs/reference/templates/architect-prompt.md +66 -0
- package/.claude/skills/directive/docs/reference/templates/auditor-prompt.md +53 -0
- package/.claude/skills/directive/docs/reference/templates/brainstorm-prompt.md +68 -0
- package/.claude/skills/directive/docs/reference/templates/challenger-prompt.md +35 -0
- package/.claude/skills/directive/docs/reference/templates/digest.md +134 -0
- package/.claude/skills/directive/docs/reference/templates/investigator-prompt.md +51 -0
- package/.claude/skills/directive/docs/reference/templates/planner-prompt.md +130 -0
- package/.claude/skills/frontend-design/SKILL.md +42 -0
- package/.claude/skills/gruai-agents/SKILL.md +161 -0
- package/.claude/skills/gruai-config/SKILL.md +61 -0
- package/.claude/skills/healthcheck/SKILL.md +216 -0
- package/.claude/skills/report/SKILL.md +380 -0
- package/.claude/skills/scout/SKILL.md +452 -0
- package/.claude/skills/seo-audit/SKILL.md +107 -0
- package/.claude/skills/walkthrough/SKILL.md +274 -0
- package/.claude/skills/webapp-testing/SKILL.md +96 -0
- package/LICENSE +21 -0
- package/README.md +206 -0
- package/cli/templates/CLAUDE.md.template +57 -0
- package/cli/templates/agent-roles/backend.md +47 -0
- package/cli/templates/agent-roles/cmo.md +52 -0
- package/cli/templates/agent-roles/content.md +48 -0
- package/cli/templates/agent-roles/coo.md +66 -0
- package/cli/templates/agent-roles/cpo.md +52 -0
- package/cli/templates/agent-roles/cto.md +63 -0
- package/cli/templates/agent-roles/data.md +46 -0
- package/cli/templates/agent-roles/design.md +46 -0
- package/cli/templates/agent-roles/frontend.md +47 -0
- package/cli/templates/agent-roles/fullstack.md +47 -0
- package/cli/templates/agent-roles/qa.md +46 -0
- package/cli/templates/backlog.json.template +3 -0
- package/cli/templates/directive.json.template +9 -0
- package/cli/templates/directive.md.template +23 -0
- package/cli/templates/goals-index.md +21 -0
- package/cli/templates/gruai.config.json.template +12 -0
- package/cli/templates/lessons.md +16 -0
- package/cli/templates/vision.md +35 -0
- package/cli/templates/welcome-directive/directive.json +9 -0
- package/cli/templates/welcome-directive/directive.md +53 -0
- package/dist/assets/GamePage-C5XQQOQH.js +49 -0
- package/dist/assets/README.md +17 -0
- package/dist/assets/characters/char_0.png +0 -0
- package/dist/assets/characters/char_1.png +0 -0
- package/dist/assets/characters/char_10.png +0 -0
- package/dist/assets/characters/char_11.png +0 -0
- package/dist/assets/characters/char_2.png +0 -0
- package/dist/assets/characters/char_3.png +0 -0
- package/dist/assets/characters/char_4.png +0 -0
- package/dist/assets/characters/char_5.png +0 -0
- package/dist/assets/characters/char_6.png +0 -0
- package/dist/assets/characters/char_7.png +0 -0
- package/dist/assets/characters/char_8.png +0 -0
- package/dist/assets/characters/char_9.png +0 -0
- package/dist/assets/index-CnTPDqpP.js +12 -0
- package/dist/assets/index-gR5q7ikB.css +1 -0
- package/dist/assets/office/furniture.png +0 -0
- package/dist/assets/office/room-builder.png +0 -0
- package/dist/index.html +16 -0
- package/dist-server/scripts/intelligence-trends.d.ts +100 -0
- package/dist-server/scripts/intelligence-trends.js +365 -0
- package/dist-server/server/actions/cleanup.d.ts +4 -0
- package/dist-server/server/actions/cleanup.js +30 -0
- package/dist-server/server/actions/send-input.d.ts +6 -0
- package/dist-server/server/actions/send-input.js +147 -0
- package/dist-server/server/actions/terminal.d.ts +4 -0
- package/dist-server/server/actions/terminal.js +427 -0
- package/dist-server/server/config.d.ts +9 -0
- package/dist-server/server/config.js +217 -0
- package/dist-server/server/db.d.ts +7 -0
- package/dist-server/server/db.js +79 -0
- package/dist-server/server/hooks/event-receiver.d.ts +11 -0
- package/dist-server/server/hooks/event-receiver.js +36 -0
- package/dist-server/server/index.d.ts +1 -0
- package/dist-server/server/index.js +552 -0
- package/dist-server/server/notifications/macos.d.ts +5 -0
- package/dist-server/server/notifications/macos.js +22 -0
- package/dist-server/server/notifications/notifier.d.ts +17 -0
- package/dist-server/server/notifications/notifier.js +110 -0
- package/dist-server/server/parsers/process-discovery.d.ts +39 -0
- package/dist-server/server/parsers/process-discovery.js +776 -0
- package/dist-server/server/parsers/session-scanner.d.ts +56 -0
- package/dist-server/server/parsers/session-scanner.js +390 -0
- package/dist-server/server/parsers/session-state.d.ts +68 -0
- package/dist-server/server/parsers/session-state.js +696 -0
- package/dist-server/server/parsers/session-state.test.d.ts +1 -0
- package/dist-server/server/parsers/session-state.test.js +950 -0
- package/dist-server/server/parsers/task-parser.d.ts +10 -0
- package/dist-server/server/parsers/task-parser.js +97 -0
- package/dist-server/server/parsers/team-parser.d.ts +3 -0
- package/dist-server/server/parsers/team-parser.js +67 -0
- package/dist-server/server/platform/__tests__/claude-code.test.d.ts +1 -0
- package/dist-server/server/platform/__tests__/claude-code.test.js +311 -0
- package/dist-server/server/platform/claude-code.d.ts +34 -0
- package/dist-server/server/platform/claude-code.js +94 -0
- package/dist-server/server/platform/index.d.ts +5 -0
- package/dist-server/server/platform/index.js +1 -0
- package/dist-server/server/platform/types.d.ts +190 -0
- package/dist-server/server/platform/types.js +9 -0
- package/dist-server/server/state/aggregator.d.ts +42 -0
- package/dist-server/server/state/aggregator.js +1080 -0
- package/dist-server/server/state/work-item-types.d.ts +555 -0
- package/dist-server/server/state/work-item-types.js +168 -0
- package/dist-server/server/types.d.ts +237 -0
- package/dist-server/server/types.js +1 -0
- package/dist-server/server/watchers/claude-watcher.d.ts +17 -0
- package/dist-server/server/watchers/claude-watcher.js +130 -0
- package/dist-server/server/watchers/context-watcher.d.ts +22 -0
- package/dist-server/server/watchers/context-watcher.js +125 -0
- package/dist-server/server/watchers/directive-watcher.d.ts +46 -0
- package/dist-server/server/watchers/directive-watcher.js +497 -0
- package/dist-server/server/watchers/session-watcher.d.ts +18 -0
- package/dist-server/server/watchers/session-watcher.js +126 -0
- package/dist-server/server/watchers/state-watcher.d.ts +36 -0
- package/dist-server/server/watchers/state-watcher.js +369 -0
- package/package.json +68 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { TeamTask } from '../types.js';
|
|
2
|
+
export declare function parseTeamTasks(claudeHome: string, teamName: string): TeamTask[];
|
|
3
|
+
export declare function parseAllTeamTasks(claudeHome: string, teamNames: string[]): Record<string, TeamTask[]>;
|
|
4
|
+
/**
|
|
5
|
+
* Parse all task directories, splitting into team-named and UUID-named (session) dirs.
|
|
6
|
+
*/
|
|
7
|
+
export declare function parseAllTasks(claudeHome: string, knownTeamNames: Set<string>): {
|
|
8
|
+
byTeam: Record<string, TeamTask[]>;
|
|
9
|
+
bySession: Record<string, TeamTask[]>;
|
|
10
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const UUID_DIR_REGEX = /^[0-9a-f]{8}-/;
|
|
4
|
+
function normalizeStatus(raw) {
|
|
5
|
+
if (!raw)
|
|
6
|
+
return 'pending';
|
|
7
|
+
const lower = raw.toLowerCase().trim();
|
|
8
|
+
if (lower === 'completed' || lower === 'done')
|
|
9
|
+
return 'completed';
|
|
10
|
+
if (lower === 'in_progress' || lower === 'in-progress')
|
|
11
|
+
return 'in_progress';
|
|
12
|
+
return 'pending';
|
|
13
|
+
}
|
|
14
|
+
function parseRawTask(raw) {
|
|
15
|
+
return {
|
|
16
|
+
id: raw.id ?? '',
|
|
17
|
+
subject: raw.subject ?? '',
|
|
18
|
+
description: raw.description ?? '',
|
|
19
|
+
activeForm: raw.activeForm ?? '',
|
|
20
|
+
status: normalizeStatus(raw.status),
|
|
21
|
+
owner: raw.owner ?? '',
|
|
22
|
+
blocks: raw.blocks ?? [],
|
|
23
|
+
blockedBy: raw.blockedBy ?? [],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export function parseTeamTasks(claudeHome, teamName) {
|
|
27
|
+
const tasksDir = path.join(claudeHome, 'tasks', teamName);
|
|
28
|
+
try {
|
|
29
|
+
if (!fs.existsSync(tasksDir))
|
|
30
|
+
return [];
|
|
31
|
+
const entries = fs.readdirSync(tasksDir, { withFileTypes: true });
|
|
32
|
+
const tasks = [];
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
// Skip non-JSON files
|
|
35
|
+
if (!entry.isFile() || !entry.name.endsWith('.json'))
|
|
36
|
+
continue;
|
|
37
|
+
const filePath = path.join(tasksDir, entry.name);
|
|
38
|
+
try {
|
|
39
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
40
|
+
const raw = JSON.parse(content);
|
|
41
|
+
tasks.push(parseRawTask(raw));
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Skip malformed files
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return tasks;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export function parseAllTeamTasks(claudeHome, teamNames) {
|
|
54
|
+
const result = {};
|
|
55
|
+
for (const teamName of teamNames) {
|
|
56
|
+
result[teamName] = parseTeamTasks(claudeHome, teamName);
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Parse all task directories, splitting into team-named and UUID-named (session) dirs.
|
|
62
|
+
*/
|
|
63
|
+
export function parseAllTasks(claudeHome, knownTeamNames) {
|
|
64
|
+
const tasksRoot = path.join(claudeHome, 'tasks');
|
|
65
|
+
const byTeam = {};
|
|
66
|
+
const bySession = {};
|
|
67
|
+
let dirs;
|
|
68
|
+
try {
|
|
69
|
+
dirs = fs.readdirSync(tasksRoot).filter((d) => {
|
|
70
|
+
try {
|
|
71
|
+
return fs.statSync(path.join(tasksRoot, d)).isDirectory();
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return { byTeam, bySession };
|
|
80
|
+
}
|
|
81
|
+
for (const dirName of dirs) {
|
|
82
|
+
const tasks = parseTeamTasks(claudeHome, dirName);
|
|
83
|
+
if (tasks.length === 0)
|
|
84
|
+
continue;
|
|
85
|
+
if (knownTeamNames.has(dirName)) {
|
|
86
|
+
byTeam[dirName] = tasks;
|
|
87
|
+
}
|
|
88
|
+
else if (UUID_DIR_REGEX.test(dirName)) {
|
|
89
|
+
bySession[dirName] = tasks;
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
// Unknown non-UUID dir — include in byTeam as a fallback
|
|
93
|
+
byTeam[dirName] = tasks;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return { byTeam, bySession };
|
|
97
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
function parseTimestamp(raw) {
|
|
4
|
+
if (typeof raw === 'number')
|
|
5
|
+
return new Date(raw).toISOString();
|
|
6
|
+
if (typeof raw === 'string')
|
|
7
|
+
return raw;
|
|
8
|
+
return new Date().toISOString();
|
|
9
|
+
}
|
|
10
|
+
function parseMember(raw) {
|
|
11
|
+
return {
|
|
12
|
+
name: raw.name ?? 'unnamed',
|
|
13
|
+
agentId: raw.agentId ?? '',
|
|
14
|
+
agentType: raw.agentType ?? 'unknown',
|
|
15
|
+
model: raw.model ?? '',
|
|
16
|
+
tmuxPaneId: raw.tmuxPaneId ?? '',
|
|
17
|
+
cwd: raw.cwd ?? '',
|
|
18
|
+
color: raw.color ?? '',
|
|
19
|
+
isActive: raw.isActive ?? false,
|
|
20
|
+
backendType: raw.backendType ?? '',
|
|
21
|
+
joinedAt: parseTimestamp(raw.joinedAt),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function parseTeamConfig(filePath) {
|
|
25
|
+
try {
|
|
26
|
+
if (!fs.existsSync(filePath))
|
|
27
|
+
return null;
|
|
28
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
29
|
+
const raw = JSON.parse(content);
|
|
30
|
+
const members = (raw.members ?? []).map(parseMember);
|
|
31
|
+
return {
|
|
32
|
+
name: raw.name ?? path.basename(path.dirname(filePath)),
|
|
33
|
+
description: raw.description ?? '',
|
|
34
|
+
members,
|
|
35
|
+
createdAt: parseTimestamp(raw.createdAt),
|
|
36
|
+
leadAgentId: raw.leadAgentId ?? '',
|
|
37
|
+
leadSessionId: raw.leadSessionId ?? '',
|
|
38
|
+
stale: false,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
console.error(`[team-parser] Error parsing ${filePath}:`, err);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function parseAllTeams(claudeHome) {
|
|
47
|
+
const teamsDir = path.join(claudeHome, 'teams');
|
|
48
|
+
const teams = [];
|
|
49
|
+
try {
|
|
50
|
+
if (!fs.existsSync(teamsDir))
|
|
51
|
+
return [];
|
|
52
|
+
const entries = fs.readdirSync(teamsDir, { withFileTypes: true });
|
|
53
|
+
for (const entry of entries) {
|
|
54
|
+
if (!entry.isDirectory())
|
|
55
|
+
continue;
|
|
56
|
+
const configPath = path.join(teamsDir, entry.name, 'config.json');
|
|
57
|
+
const team = parseTeamConfig(configPath);
|
|
58
|
+
if (team) {
|
|
59
|
+
teams.push(team);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
console.error(`[team-parser] Error scanning teams directory:`, err);
|
|
65
|
+
}
|
|
66
|
+
return teams;
|
|
67
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { describe, it, before, after } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import { ClaudeCodeAdapter } from '../claude-code.js';
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Helpers
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
let tmpDir;
|
|
11
|
+
/** Write a JSONL file from an array of entry objects. */
|
|
12
|
+
function writeJsonl(filePath, entries) {
|
|
13
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
14
|
+
const content = entries.map((e) => JSON.stringify(e)).join('\n') + '\n';
|
|
15
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
16
|
+
}
|
|
17
|
+
const ts = (offset) => `2026-02-23T10:00:${String(offset).padStart(2, '0')}Z`;
|
|
18
|
+
function userPrompt(text, extra = {}) {
|
|
19
|
+
return {
|
|
20
|
+
type: 'user',
|
|
21
|
+
message: { role: 'user', content: [{ type: 'text', text }] },
|
|
22
|
+
timestamp: ts(0),
|
|
23
|
+
...extra,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function assistantText(text, extra = {}) {
|
|
27
|
+
return {
|
|
28
|
+
type: 'assistant',
|
|
29
|
+
message: { role: 'assistant', content: [{ type: 'text', text }] },
|
|
30
|
+
timestamp: ts(1),
|
|
31
|
+
...extra,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function assistantToolUse(toolName, input = {}) {
|
|
35
|
+
return {
|
|
36
|
+
type: 'assistant',
|
|
37
|
+
message: {
|
|
38
|
+
role: 'assistant',
|
|
39
|
+
content: [
|
|
40
|
+
{ type: 'text', text: 'Let me check.' },
|
|
41
|
+
{ type: 'tool_use', name: toolName, input },
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
timestamp: ts(2),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function turnEnd() {
|
|
48
|
+
return { type: 'system', subtype: 'turn_duration', timestamp: ts(4) };
|
|
49
|
+
}
|
|
50
|
+
/** Create a stub AggregatorHandle for factory method tests. */
|
|
51
|
+
function stubAggregator() {
|
|
52
|
+
return {
|
|
53
|
+
refreshSessions() { },
|
|
54
|
+
updateSessionFromFileState() { },
|
|
55
|
+
refreshTeams() { },
|
|
56
|
+
refreshTasks() { },
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Tests
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
before(() => {
|
|
63
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-code-adapter-test-'));
|
|
64
|
+
});
|
|
65
|
+
after(() => {
|
|
66
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
67
|
+
});
|
|
68
|
+
// ===========================================================================
|
|
69
|
+
// 1. Constructor
|
|
70
|
+
// ===========================================================================
|
|
71
|
+
describe('ClaudeCodeAdapter constructor', () => {
|
|
72
|
+
it('creates adapter with empty state maps', () => {
|
|
73
|
+
const adapter = new ClaudeCodeAdapter(tmpDir);
|
|
74
|
+
assert.equal(adapter.fileStates.size, 0, 'fileStates should be empty');
|
|
75
|
+
assert.equal(adapter.parentAgentMapCache.size, 0, 'parentAgentMapCache should be empty');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
// ===========================================================================
|
|
79
|
+
// 2. getPlatformCapabilities
|
|
80
|
+
// ===========================================================================
|
|
81
|
+
describe('getPlatformCapabilities', () => {
|
|
82
|
+
it('returns correct feature flags for Claude Code', () => {
|
|
83
|
+
const adapter = new ClaudeCodeAdapter(tmpDir);
|
|
84
|
+
const caps = adapter.getPlatformCapabilities();
|
|
85
|
+
assert.equal(caps.supportsFileWatching, true);
|
|
86
|
+
assert.equal(caps.supportsIncrementalReads, true);
|
|
87
|
+
assert.equal(caps.supportsCLISpawn, true);
|
|
88
|
+
assert.equal(caps.supportsMCP, true);
|
|
89
|
+
assert.equal(caps.supportsSubagents, true);
|
|
90
|
+
assert.equal(caps.supportsTokenTracking, false);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
// ===========================================================================
|
|
94
|
+
// 3. discoverSessionFiles
|
|
95
|
+
// ===========================================================================
|
|
96
|
+
describe('discoverSessionFiles', () => {
|
|
97
|
+
it('discovers JSONL session files from a mock claudeHome', () => {
|
|
98
|
+
const claudeHome = path.join(tmpDir, 'discover-test');
|
|
99
|
+
const projectDir = path.join(claudeHome, 'projects', 'test-project');
|
|
100
|
+
fs.mkdirSync(projectDir, { recursive: true });
|
|
101
|
+
// Create a session JSONL file
|
|
102
|
+
const sessionFile = path.join(projectDir, 'abc123.jsonl');
|
|
103
|
+
writeJsonl(sessionFile, [
|
|
104
|
+
userPrompt('Hello', { sessionId: 'abc123' }),
|
|
105
|
+
]);
|
|
106
|
+
// Create a non-JSONL file that should be ignored
|
|
107
|
+
fs.writeFileSync(path.join(projectDir, 'notes.txt'), 'not a session');
|
|
108
|
+
const adapter = new ClaudeCodeAdapter(claudeHome);
|
|
109
|
+
const discovered = adapter.discoverSessionFiles();
|
|
110
|
+
assert.ok(discovered instanceof Map, 'should return a Map');
|
|
111
|
+
assert.equal(discovered.size, 1, 'should find exactly 1 session file');
|
|
112
|
+
const entry = discovered.get(sessionFile);
|
|
113
|
+
assert.ok(entry, 'should contain the JSONL file path as key');
|
|
114
|
+
assert.equal(entry.sessionId, 'abc123');
|
|
115
|
+
assert.equal(entry.isSubagent, false);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
// ===========================================================================
|
|
119
|
+
// 4. getAllFileStates
|
|
120
|
+
// ===========================================================================
|
|
121
|
+
describe('getAllFileStates', () => {
|
|
122
|
+
it('returns empty map initially (before any bootstrap)', () => {
|
|
123
|
+
const adapter = new ClaudeCodeAdapter(path.join(tmpDir, 'empty-states'));
|
|
124
|
+
const states = adapter.getAllFileStates();
|
|
125
|
+
assert.ok(states instanceof Map, 'should return a Map');
|
|
126
|
+
assert.equal(states.size, 0, 'should be empty before any initialization');
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
// ===========================================================================
|
|
130
|
+
// 5. getAgentState
|
|
131
|
+
// ===========================================================================
|
|
132
|
+
describe('getAgentState', () => {
|
|
133
|
+
it('maps machineState=working to "working"', () => {
|
|
134
|
+
const claudeHome = path.join(tmpDir, 'agent-state-test');
|
|
135
|
+
const projectDir = path.join(claudeHome, 'projects', 'test-project');
|
|
136
|
+
fs.mkdirSync(projectDir, { recursive: true });
|
|
137
|
+
const sessionFile = path.join(projectDir, 'sess-working.jsonl');
|
|
138
|
+
writeJsonl(sessionFile, [
|
|
139
|
+
userPrompt('Do something', { sessionId: 'sess-working' }),
|
|
140
|
+
assistantToolUse('Read', { file_path: '/foo.ts' }),
|
|
141
|
+
]);
|
|
142
|
+
const adapter = new ClaudeCodeAdapter(claudeHome);
|
|
143
|
+
adapter.initializeAllFileStates();
|
|
144
|
+
const state = adapter.getOrBootstrap(sessionFile);
|
|
145
|
+
assert.ok(state, 'should bootstrap the session');
|
|
146
|
+
assert.equal(state.machineState, 'working');
|
|
147
|
+
assert.equal(adapter.getAgentState(state), 'working');
|
|
148
|
+
});
|
|
149
|
+
it('maps machineState=needs_input to "needs_input"', () => {
|
|
150
|
+
const claudeHome = path.join(tmpDir, 'agent-state-test-2');
|
|
151
|
+
const projectDir = path.join(claudeHome, 'projects', 'test-project');
|
|
152
|
+
fs.mkdirSync(projectDir, { recursive: true });
|
|
153
|
+
const sessionFile = path.join(projectDir, 'sess-input.jsonl');
|
|
154
|
+
writeJsonl(sessionFile, [
|
|
155
|
+
userPrompt('Help', { sessionId: 'sess-input' }),
|
|
156
|
+
assistantText('Should I proceed?'),
|
|
157
|
+
]);
|
|
158
|
+
const adapter = new ClaudeCodeAdapter(claudeHome);
|
|
159
|
+
const state = adapter.getOrBootstrap(sessionFile);
|
|
160
|
+
assert.ok(state);
|
|
161
|
+
assert.equal(state.machineState, 'needs_input');
|
|
162
|
+
assert.equal(adapter.getAgentState(state), 'needs_input');
|
|
163
|
+
});
|
|
164
|
+
it('maps machineState=done to "done"', () => {
|
|
165
|
+
const claudeHome = path.join(tmpDir, 'agent-state-test-3');
|
|
166
|
+
const projectDir = path.join(claudeHome, 'projects', 'test-project');
|
|
167
|
+
fs.mkdirSync(projectDir, { recursive: true });
|
|
168
|
+
const sessionFile = path.join(projectDir, 'sess-done.jsonl');
|
|
169
|
+
writeJsonl(sessionFile, [
|
|
170
|
+
userPrompt('Explain', { sessionId: 'sess-done' }),
|
|
171
|
+
assistantText('Here is the answer.'),
|
|
172
|
+
turnEnd(),
|
|
173
|
+
]);
|
|
174
|
+
const adapter = new ClaudeCodeAdapter(claudeHome);
|
|
175
|
+
const state = adapter.getOrBootstrap(sessionFile);
|
|
176
|
+
assert.ok(state);
|
|
177
|
+
assert.equal(state.machineState, 'done');
|
|
178
|
+
assert.equal(adapter.getAgentState(state), 'done');
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
// ===========================================================================
|
|
182
|
+
// 6. toSessionActivity
|
|
183
|
+
// ===========================================================================
|
|
184
|
+
describe('toSessionActivity', () => {
|
|
185
|
+
it('returns null for states without sessionId', () => {
|
|
186
|
+
const claudeHome = path.join(tmpDir, 'activity-null-test');
|
|
187
|
+
const projectDir = path.join(claudeHome, 'projects', 'test-project');
|
|
188
|
+
fs.mkdirSync(projectDir, { recursive: true });
|
|
189
|
+
const sessionFile = path.join(projectDir, 'no-session-id.jsonl');
|
|
190
|
+
writeJsonl(sessionFile, [
|
|
191
|
+
// No sessionId in any entry
|
|
192
|
+
{ type: 'user', message: { role: 'user', content: [{ type: 'text', text: 'Hi' }] }, timestamp: ts(0) },
|
|
193
|
+
]);
|
|
194
|
+
const adapter = new ClaudeCodeAdapter(claudeHome);
|
|
195
|
+
const state = adapter.getOrBootstrap(sessionFile);
|
|
196
|
+
assert.ok(state);
|
|
197
|
+
assert.equal(state.sessionId, undefined);
|
|
198
|
+
const activity = adapter.toSessionActivity(state);
|
|
199
|
+
assert.equal(activity, null);
|
|
200
|
+
});
|
|
201
|
+
it('returns activity object for states with sessionId', () => {
|
|
202
|
+
const claudeHome = path.join(tmpDir, 'activity-valid-test');
|
|
203
|
+
const projectDir = path.join(claudeHome, 'projects', 'test-project');
|
|
204
|
+
fs.mkdirSync(projectDir, { recursive: true });
|
|
205
|
+
const sessionFile = path.join(projectDir, 'has-session.jsonl');
|
|
206
|
+
writeJsonl(sessionFile, [
|
|
207
|
+
userPrompt('Check file', { sessionId: 'sess-activity' }),
|
|
208
|
+
assistantToolUse('Read', { file_path: '/src/index.ts' }),
|
|
209
|
+
]);
|
|
210
|
+
const adapter = new ClaudeCodeAdapter(claudeHome);
|
|
211
|
+
const state = adapter.getOrBootstrap(sessionFile);
|
|
212
|
+
assert.ok(state);
|
|
213
|
+
// Make it look recent so active=true
|
|
214
|
+
state.mtimeMs = Date.now();
|
|
215
|
+
const activity = adapter.toSessionActivity(state);
|
|
216
|
+
assert.ok(activity);
|
|
217
|
+
assert.equal(activity.sessionId, 'sess-activity');
|
|
218
|
+
assert.equal(activity.tool, 'Read');
|
|
219
|
+
assert.equal(activity.active, true);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
// ===========================================================================
|
|
223
|
+
// 7. loadConfig
|
|
224
|
+
// ===========================================================================
|
|
225
|
+
describe('loadConfig', () => {
|
|
226
|
+
it('returns a valid ConductorConfig', () => {
|
|
227
|
+
const adapter = new ClaudeCodeAdapter(tmpDir);
|
|
228
|
+
const config = adapter.loadConfig();
|
|
229
|
+
assert.ok(config, 'config should not be null');
|
|
230
|
+
assert.ok(Array.isArray(config.projects), 'projects should be an array');
|
|
231
|
+
assert.ok(typeof config.claudeHome === 'string', 'claudeHome should be a string');
|
|
232
|
+
assert.ok(config.server, 'server config should exist');
|
|
233
|
+
assert.ok(typeof config.server.port === 'number', 'server.port should be a number');
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
// ===========================================================================
|
|
237
|
+
// 8. createSessionWatcher
|
|
238
|
+
// ===========================================================================
|
|
239
|
+
describe('createSessionWatcher', () => {
|
|
240
|
+
it('returns object with start/stop/ready', () => {
|
|
241
|
+
const claudeHome = path.join(tmpDir, 'watcher-test');
|
|
242
|
+
fs.mkdirSync(claudeHome, { recursive: true });
|
|
243
|
+
const adapter = new ClaudeCodeAdapter(claudeHome);
|
|
244
|
+
const agg = stubAggregator();
|
|
245
|
+
const watcher = adapter.createSessionWatcher(agg);
|
|
246
|
+
assert.ok(watcher, 'watcher should not be null');
|
|
247
|
+
assert.equal(typeof watcher.start, 'function', 'should have start()');
|
|
248
|
+
assert.equal(typeof watcher.stop, 'function', 'should have stop()');
|
|
249
|
+
assert.equal(typeof watcher.ready, 'boolean', 'should have ready property');
|
|
250
|
+
assert.equal(watcher.ready, false, 'should not be ready before start');
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
// ===========================================================================
|
|
254
|
+
// 9. createMetadataWatcher
|
|
255
|
+
// ===========================================================================
|
|
256
|
+
describe('createMetadataWatcher', () => {
|
|
257
|
+
it('returns a ClaudeWatcher instance (not null)', () => {
|
|
258
|
+
const claudeHome = path.join(tmpDir, 'meta-watcher-test');
|
|
259
|
+
fs.mkdirSync(claudeHome, { recursive: true });
|
|
260
|
+
const adapter = new ClaudeCodeAdapter(claudeHome);
|
|
261
|
+
const agg = stubAggregator();
|
|
262
|
+
const watcher = adapter.createMetadataWatcher(agg);
|
|
263
|
+
assert.ok(watcher !== null, 'Claude Code should always return a metadata watcher');
|
|
264
|
+
assert.equal(typeof watcher.start, 'function');
|
|
265
|
+
assert.equal(typeof watcher.stop, 'function');
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
// ===========================================================================
|
|
269
|
+
// 10. processFileUpdate + removeFileState (instance isolation)
|
|
270
|
+
// ===========================================================================
|
|
271
|
+
describe('Instance isolation', () => {
|
|
272
|
+
it('two adapters maintain separate fileStates', () => {
|
|
273
|
+
const claudeHome1 = path.join(tmpDir, 'iso-test-1');
|
|
274
|
+
const claudeHome2 = path.join(tmpDir, 'iso-test-2');
|
|
275
|
+
const projectDir1 = path.join(claudeHome1, 'projects', 'proj');
|
|
276
|
+
const projectDir2 = path.join(claudeHome2, 'projects', 'proj');
|
|
277
|
+
fs.mkdirSync(projectDir1, { recursive: true });
|
|
278
|
+
fs.mkdirSync(projectDir2, { recursive: true });
|
|
279
|
+
const file1 = path.join(projectDir1, 's1.jsonl');
|
|
280
|
+
const file2 = path.join(projectDir2, 's2.jsonl');
|
|
281
|
+
writeJsonl(file1, [userPrompt('A', { sessionId: 'iso-1' })]);
|
|
282
|
+
writeJsonl(file2, [userPrompt('B', { sessionId: 'iso-2' })]);
|
|
283
|
+
const adapter1 = new ClaudeCodeAdapter(claudeHome1);
|
|
284
|
+
const adapter2 = new ClaudeCodeAdapter(claudeHome2);
|
|
285
|
+
adapter1.initializeAllFileStates();
|
|
286
|
+
adapter2.initializeAllFileStates();
|
|
287
|
+
// Each adapter sees only its own sessions
|
|
288
|
+
assert.equal(adapter1.getAllFileStates().size, 1);
|
|
289
|
+
assert.equal(adapter2.getAllFileStates().size, 1);
|
|
290
|
+
const state1 = adapter1.getOrBootstrap(file1);
|
|
291
|
+
assert.ok(state1);
|
|
292
|
+
assert.equal(state1.sessionId, 'iso-1');
|
|
293
|
+
const state2 = adapter2.getOrBootstrap(file2);
|
|
294
|
+
assert.ok(state2);
|
|
295
|
+
assert.equal(state2.sessionId, 'iso-2');
|
|
296
|
+
// Remove from adapter1 should not affect adapter2
|
|
297
|
+
adapter1.removeFileState(file1);
|
|
298
|
+
assert.equal(adapter1.getAllFileStates().size, 0);
|
|
299
|
+
assert.equal(adapter2.getAllFileStates().size, 1);
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
// ===========================================================================
|
|
303
|
+
// 11. discoverProjects
|
|
304
|
+
// ===========================================================================
|
|
305
|
+
describe('discoverProjects', () => {
|
|
306
|
+
it('returns an array (possibly empty for temp dir)', () => {
|
|
307
|
+
const adapter = new ClaudeCodeAdapter(tmpDir);
|
|
308
|
+
const projects = adapter.discoverProjects();
|
|
309
|
+
assert.ok(Array.isArray(projects), 'should return an array');
|
|
310
|
+
});
|
|
311
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClaudeCodeAdapter -- PlatformAdapter implementation for Claude Code.
|
|
3
|
+
*
|
|
4
|
+
* Wraps existing session-state and session-scanner logic behind the
|
|
5
|
+
* PlatformAdapter interface (Strangler Fig pattern). Singleton state
|
|
6
|
+
* (fileStates Map, parentAgentMapCache) is moved to instance properties.
|
|
7
|
+
*/
|
|
8
|
+
import type { PlatformAdapter, PlatformCapabilities, AggregatorHandle, AgentState, SessionWatcher as SessionWatcherInterface, MetadataWatcher as MetadataWatcherInterface } from './types.js';
|
|
9
|
+
import type { SessionActivity, ProjectConfig, ConductorConfig } from '../types.js';
|
|
10
|
+
import type { SessionFileState, DiscoveredFile } from '../parsers/session-state.js';
|
|
11
|
+
export declare class ClaudeCodeAdapter implements PlatformAdapter {
|
|
12
|
+
/** Instance-level session file state map (replaces module-level singleton). */
|
|
13
|
+
readonly fileStates: Map<string, SessionFileState>;
|
|
14
|
+
/** Instance-level parent agent map cache (replaces module-level singleton). */
|
|
15
|
+
readonly parentAgentMapCache: Map<string, {
|
|
16
|
+
mtime: number;
|
|
17
|
+
map: Map<string, string>;
|
|
18
|
+
}>;
|
|
19
|
+
private readonly claudeHome;
|
|
20
|
+
constructor(claudeHome: string);
|
|
21
|
+
discoverSessionFiles(projectFilter?: string): Map<string, DiscoveredFile>;
|
|
22
|
+
initializeAllFileStates(projectFilter?: string): Map<string, DiscoveredFile>;
|
|
23
|
+
processFileUpdate(filePath: string): SessionFileState | null;
|
|
24
|
+
getOrBootstrap(filePath: string): SessionFileState | null;
|
|
25
|
+
removeFileState(filePath: string): void;
|
|
26
|
+
getAllFileStates(): Map<string, SessionFileState>;
|
|
27
|
+
toSessionActivity(state: SessionFileState): SessionActivity | null;
|
|
28
|
+
getAgentState(state: SessionFileState): AgentState;
|
|
29
|
+
createSessionWatcher(aggregator: AggregatorHandle, projectFilter?: string): SessionWatcherInterface;
|
|
30
|
+
createMetadataWatcher(aggregator: AggregatorHandle): MetadataWatcherInterface | null;
|
|
31
|
+
discoverProjects(): ProjectConfig[];
|
|
32
|
+
loadConfig(): ConductorConfig;
|
|
33
|
+
getPlatformCapabilities(): PlatformCapabilities;
|
|
34
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClaudeCodeAdapter -- PlatformAdapter implementation for Claude Code.
|
|
3
|
+
*
|
|
4
|
+
* Wraps existing session-state and session-scanner logic behind the
|
|
5
|
+
* PlatformAdapter interface (Strangler Fig pattern). Singleton state
|
|
6
|
+
* (fileStates Map, parentAgentMapCache) is moved to instance properties.
|
|
7
|
+
*/
|
|
8
|
+
import { initializeAllFileStates, discoverSessionFiles, getAllFileStates as getAllFileStatesRaw, getOrBootstrap as getOrBootstrapRaw, removeFileState as removeFileStateRaw, processFileUpdate as processFileUpdateRaw, toSessionActivity as toSessionActivityRaw, machineStateToLastEntryType, } from '../parsers/session-state.js';
|
|
9
|
+
import { discoverProjects, loadConfig } from '../config.js';
|
|
10
|
+
import { SessionWatcher } from '../watchers/session-watcher.js';
|
|
11
|
+
import { ClaudeWatcher } from '../watchers/claude-watcher.js';
|
|
12
|
+
export class ClaudeCodeAdapter {
|
|
13
|
+
/** Instance-level session file state map (replaces module-level singleton). */
|
|
14
|
+
fileStates = new Map();
|
|
15
|
+
/** Instance-level parent agent map cache (replaces module-level singleton). */
|
|
16
|
+
parentAgentMapCache = new Map();
|
|
17
|
+
claudeHome;
|
|
18
|
+
constructor(claudeHome) {
|
|
19
|
+
this.claudeHome = claudeHome;
|
|
20
|
+
}
|
|
21
|
+
// -------------------------------------------------------------------------
|
|
22
|
+
// Core state methods
|
|
23
|
+
// -------------------------------------------------------------------------
|
|
24
|
+
discoverSessionFiles(projectFilter) {
|
|
25
|
+
return discoverSessionFiles(this.claudeHome, projectFilter);
|
|
26
|
+
}
|
|
27
|
+
initializeAllFileStates(projectFilter) {
|
|
28
|
+
return initializeAllFileStates(this.claudeHome, projectFilter, this.fileStates);
|
|
29
|
+
}
|
|
30
|
+
processFileUpdate(filePath) {
|
|
31
|
+
return processFileUpdateRaw(filePath, this.fileStates);
|
|
32
|
+
}
|
|
33
|
+
getOrBootstrap(filePath) {
|
|
34
|
+
return getOrBootstrapRaw(filePath, this.fileStates);
|
|
35
|
+
}
|
|
36
|
+
removeFileState(filePath) {
|
|
37
|
+
removeFileStateRaw(filePath, this.fileStates);
|
|
38
|
+
}
|
|
39
|
+
getAllFileStates() {
|
|
40
|
+
return getAllFileStatesRaw(this.fileStates);
|
|
41
|
+
}
|
|
42
|
+
// -------------------------------------------------------------------------
|
|
43
|
+
// Conversion methods
|
|
44
|
+
// -------------------------------------------------------------------------
|
|
45
|
+
toSessionActivity(state) {
|
|
46
|
+
return toSessionActivityRaw(state);
|
|
47
|
+
}
|
|
48
|
+
getAgentState(state) {
|
|
49
|
+
const lastEntryType = machineStateToLastEntryType(state);
|
|
50
|
+
switch (lastEntryType) {
|
|
51
|
+
case 'assistant-tool':
|
|
52
|
+
return 'working';
|
|
53
|
+
case 'assistant-question':
|
|
54
|
+
return 'needs_input';
|
|
55
|
+
case 'assistant-text':
|
|
56
|
+
return 'done';
|
|
57
|
+
case 'user':
|
|
58
|
+
return 'working';
|
|
59
|
+
default:
|
|
60
|
+
return 'unknown';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// -------------------------------------------------------------------------
|
|
64
|
+
// Factory methods
|
|
65
|
+
// -------------------------------------------------------------------------
|
|
66
|
+
createSessionWatcher(aggregator, projectFilter) {
|
|
67
|
+
return new SessionWatcher(aggregator, this.claudeHome, projectFilter, this);
|
|
68
|
+
}
|
|
69
|
+
createMetadataWatcher(aggregator) {
|
|
70
|
+
return new ClaudeWatcher(aggregator, this.claudeHome);
|
|
71
|
+
}
|
|
72
|
+
// -------------------------------------------------------------------------
|
|
73
|
+
// Config methods
|
|
74
|
+
// -------------------------------------------------------------------------
|
|
75
|
+
discoverProjects() {
|
|
76
|
+
return discoverProjects(this.claudeHome);
|
|
77
|
+
}
|
|
78
|
+
loadConfig() {
|
|
79
|
+
return loadConfig();
|
|
80
|
+
}
|
|
81
|
+
// -------------------------------------------------------------------------
|
|
82
|
+
// Capability method
|
|
83
|
+
// -------------------------------------------------------------------------
|
|
84
|
+
getPlatformCapabilities() {
|
|
85
|
+
return {
|
|
86
|
+
supportsFileWatching: true,
|
|
87
|
+
supportsIncrementalReads: true,
|
|
88
|
+
supportsCLISpawn: true,
|
|
89
|
+
supportsMCP: true,
|
|
90
|
+
supportsSubagents: true,
|
|
91
|
+
supportsTokenTracking: false,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform adapter module -- re-exports all types from the platform boundary.
|
|
3
|
+
*/
|
|
4
|
+
export type { AggregatorHandle, AgentState, PlatformCapabilities, SessionWatcher, MetadataWatcher, PlatformAdapter, } from './types.js';
|
|
5
|
+
export { ClaudeCodeAdapter } from './claude-code.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ClaudeCodeAdapter } from './claude-code.js';
|