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,56 @@
|
|
|
1
|
+
export type LastEntryType = 'user' | 'assistant-tool' | 'assistant-text' | 'assistant-question' | 'unknown';
|
|
2
|
+
export declare function projectLabel(dirName: string): string;
|
|
3
|
+
/**
|
|
4
|
+
* Derive the Claude projects directory name from an absolute repo path.
|
|
5
|
+
* Inverse of the encoding Claude Code uses: `/Users/foo/bar` → `-Users-foo-bar`
|
|
6
|
+
*/
|
|
7
|
+
export declare function projectDirFromPath(repoPath: string): string;
|
|
8
|
+
export declare function cleanPromptText(raw: string): string | undefined;
|
|
9
|
+
export declare function extractInitialPrompt(filepath: string): string | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* Extract a named agent identity from the initial prompt of a subagent.
|
|
12
|
+
* Detects patterns like "You are Alex Rivera, Chief of Staff" or "You are Sarah Chen, CTO"
|
|
13
|
+
* Returns { name, role } if found, undefined otherwise.
|
|
14
|
+
*/
|
|
15
|
+
export declare function extractAgentIdentity(promptText: string): {
|
|
16
|
+
name: string;
|
|
17
|
+
role: string;
|
|
18
|
+
} | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Extract agent identity from a JSONL file's first user message.
|
|
21
|
+
* Reads the head of the file to find the initial prompt, then checks for agent identity patterns.
|
|
22
|
+
*/
|
|
23
|
+
export declare function extractAgentIdentityFromFile(filepath: string): {
|
|
24
|
+
name: string;
|
|
25
|
+
role: string;
|
|
26
|
+
} | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Scan a parent session JSONL for Agent tool_use/tool_result pairs.
|
|
29
|
+
* Returns a map of agentId → subagent_type (e.g. "a4df5875a493548ce" → "alex").
|
|
30
|
+
* Results are cached per parent file and invalidated on mtime change.
|
|
31
|
+
*/
|
|
32
|
+
export declare function extractSubagentTypesFromParent(parentFilePath: string, cache?: Map<string, {
|
|
33
|
+
mtime: number;
|
|
34
|
+
map: Map<string, string>;
|
|
35
|
+
}>): Map<string, string>;
|
|
36
|
+
/**
|
|
37
|
+
* Look up agent identity for a subagent by checking the parent session's Agent tool calls.
|
|
38
|
+
* Returns { name, role } if the subagent was spawned with a known subagent_type, undefined otherwise.
|
|
39
|
+
*/
|
|
40
|
+
export declare function resolveAgentFromParent(parentFilePath: string, childAgentId: string, cache?: Map<string, {
|
|
41
|
+
mtime: number;
|
|
42
|
+
map: Map<string, string>;
|
|
43
|
+
}>): {
|
|
44
|
+
name: string;
|
|
45
|
+
role: string;
|
|
46
|
+
} | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* Look up a known agent by its CLI agent-setting name (e.g. "riley", "sarah").
|
|
49
|
+
* Returns { name, role } if known, undefined otherwise.
|
|
50
|
+
*/
|
|
51
|
+
export declare function resolveAgentFromSetting(agentSetting: string): {
|
|
52
|
+
name: string;
|
|
53
|
+
role: string;
|
|
54
|
+
} | undefined;
|
|
55
|
+
export declare function isSystemContent(text: string): boolean;
|
|
56
|
+
export declare function extractLatestPrompt(filepath: string): string | undefined;
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
const PROMPT_TAIL_SIZE = 65536;
|
|
5
|
+
const HEAD_SIZE = 16384;
|
|
6
|
+
export function projectLabel(dirName) {
|
|
7
|
+
const decoded = dirName.replace(/^-/, '/').replace(/-/g, '/');
|
|
8
|
+
const parts = decoded.split('/').filter(Boolean);
|
|
9
|
+
return parts.slice(-3).join('/');
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Derive the Claude projects directory name from an absolute repo path.
|
|
13
|
+
* Inverse of the encoding Claude Code uses: `/Users/foo/bar` → `-Users-foo-bar`
|
|
14
|
+
*/
|
|
15
|
+
export function projectDirFromPath(repoPath) {
|
|
16
|
+
return repoPath.replace(/\//g, '-');
|
|
17
|
+
}
|
|
18
|
+
function headRead(filepath, size = HEAD_SIZE) {
|
|
19
|
+
let fd = null;
|
|
20
|
+
try {
|
|
21
|
+
fd = fs.openSync(filepath, 'r');
|
|
22
|
+
const stat = fs.fstatSync(fd);
|
|
23
|
+
const readSize = Math.min(size, stat.size);
|
|
24
|
+
if (readSize === 0) {
|
|
25
|
+
fs.closeSync(fd);
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const buffer = Buffer.allocUnsafe(readSize);
|
|
29
|
+
fs.readSync(fd, buffer, 0, readSize, 0);
|
|
30
|
+
fs.closeSync(fd);
|
|
31
|
+
fd = null;
|
|
32
|
+
return buffer.toString('utf-8');
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
if (fd !== null) {
|
|
36
|
+
try {
|
|
37
|
+
fs.closeSync(fd);
|
|
38
|
+
}
|
|
39
|
+
catch { /* ignore */ }
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export function cleanPromptText(raw) {
|
|
45
|
+
const lines = raw.trim().split('\n');
|
|
46
|
+
for (const rawLine of lines) {
|
|
47
|
+
let line = rawLine.replace(/<[^>]+>/g, '').trim();
|
|
48
|
+
if (!line)
|
|
49
|
+
continue;
|
|
50
|
+
if (line === 'Implement the following plan:')
|
|
51
|
+
continue;
|
|
52
|
+
if (line.startsWith('Caveat:'))
|
|
53
|
+
continue;
|
|
54
|
+
if (line.startsWith('# Plan:') || line.startsWith('## ')) {
|
|
55
|
+
line = line.replace(/^#+ (?:Plan:\s*)?/, '').trim();
|
|
56
|
+
if (!line)
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
return line.length > 80 ? line.slice(0, 80) + '...' : line;
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
export function extractInitialPrompt(filepath) {
|
|
64
|
+
// Try progressively larger reads to handle compacted/continued sessions
|
|
65
|
+
// where the first user entry can exceed 16KB
|
|
66
|
+
for (const size of [HEAD_SIZE, HEAD_SIZE * 16]) {
|
|
67
|
+
const content = headRead(filepath, size);
|
|
68
|
+
if (!content)
|
|
69
|
+
return undefined;
|
|
70
|
+
const lines = content.split('\n');
|
|
71
|
+
for (const line of lines) {
|
|
72
|
+
if (!line.trim())
|
|
73
|
+
continue;
|
|
74
|
+
try {
|
|
75
|
+
const entry = JSON.parse(line);
|
|
76
|
+
if (entry.type === 'user' || entry.message?.role === 'user') {
|
|
77
|
+
const msgContent = entry.message?.content;
|
|
78
|
+
if (typeof msgContent === 'string' && msgContent.length > 0) {
|
|
79
|
+
return cleanPromptText(msgContent);
|
|
80
|
+
}
|
|
81
|
+
if (Array.isArray(msgContent)) {
|
|
82
|
+
for (const block of msgContent) {
|
|
83
|
+
const text = block.content ?? block.text;
|
|
84
|
+
if (typeof text === 'string' && text.length > 0) {
|
|
85
|
+
return cleanPromptText(text);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// Partial line at buffer boundary — try larger read
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
// --- Named agent detection ---
|
|
99
|
+
/** Known named agents in the conductor system (loaded from agent-registry.json) */
|
|
100
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
101
|
+
const registryPath = path.resolve(__dirname, '../../.claude/agent-registry.json');
|
|
102
|
+
const registryData = JSON.parse(fs.readFileSync(registryPath, 'utf-8'));
|
|
103
|
+
const KNOWN_AGENTS = {};
|
|
104
|
+
for (const agent of registryData.agents) {
|
|
105
|
+
if (agent.id !== 'ceo') {
|
|
106
|
+
KNOWN_AGENTS[agent.id] = { name: agent.name.split(' ')[0], role: agent.role };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Generic roles (from subagent_type, not personality-driven -- not in registry)
|
|
110
|
+
KNOWN_AGENTS['builder'] = { name: 'Builder', role: 'Engineer' };
|
|
111
|
+
KNOWN_AGENTS['reviewer'] = { name: 'Reviewer', role: 'Code Reviewer' };
|
|
112
|
+
KNOWN_AGENTS['auditor'] = { name: 'Auditor', role: 'Technical Auditor' };
|
|
113
|
+
KNOWN_AGENTS['investigator'] = { name: 'Investigator', role: 'Codebase Scanner' };
|
|
114
|
+
/**
|
|
115
|
+
* Extract a named agent identity from the initial prompt of a subagent.
|
|
116
|
+
* Detects patterns like "You are Alex Rivera, Chief of Staff" or "You are Sarah Chen, CTO"
|
|
117
|
+
* Returns { name, role } if found, undefined otherwise.
|
|
118
|
+
*/
|
|
119
|
+
export function extractAgentIdentity(promptText) {
|
|
120
|
+
if (!promptText)
|
|
121
|
+
return undefined;
|
|
122
|
+
// Check first 2000 chars for agent identity patterns
|
|
123
|
+
const head = promptText.slice(0, 2000);
|
|
124
|
+
// Pattern 1: "You are {FirstName} {LastName}, {Role}"
|
|
125
|
+
const youAreMatch = head.match(/You are (\w+)\s+\w+,\s*([^.\n]+)/);
|
|
126
|
+
if (youAreMatch) {
|
|
127
|
+
const firstName = youAreMatch[1].toLowerCase();
|
|
128
|
+
const known = KNOWN_AGENTS[firstName];
|
|
129
|
+
if (known)
|
|
130
|
+
return known;
|
|
131
|
+
// Unknown named agent — use what we found
|
|
132
|
+
return { name: youAreMatch[1], role: youAreMatch[2].trim() };
|
|
133
|
+
}
|
|
134
|
+
// Pattern 2: "# {FirstName} {LastName} --- {Role}" (personality file header)
|
|
135
|
+
const headerMatch = head.match(/^#\s+(\w+)\s+\w+\s*(?:---|—)\s*(.+)$/m);
|
|
136
|
+
if (headerMatch) {
|
|
137
|
+
const firstName = headerMatch[1].toLowerCase();
|
|
138
|
+
const known = KNOWN_AGENTS[firstName];
|
|
139
|
+
if (known)
|
|
140
|
+
return known;
|
|
141
|
+
return { name: headerMatch[1], role: headerMatch[2].trim() };
|
|
142
|
+
}
|
|
143
|
+
// Pattern 3: Check for known agent first names in the prompt
|
|
144
|
+
for (const [key, agent] of Object.entries(KNOWN_AGENTS)) {
|
|
145
|
+
// Look for "You are {Name}" or "as {Name}" in the prompt
|
|
146
|
+
const nameRegex = new RegExp(`\\b(?:You are|as)\\s+${key}\\b`, 'i');
|
|
147
|
+
if (nameRegex.test(head))
|
|
148
|
+
return agent;
|
|
149
|
+
}
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Extract agent identity from a JSONL file's first user message.
|
|
154
|
+
* Reads the head of the file to find the initial prompt, then checks for agent identity patterns.
|
|
155
|
+
*/
|
|
156
|
+
export function extractAgentIdentityFromFile(filepath) {
|
|
157
|
+
const content = headRead(filepath, HEAD_SIZE);
|
|
158
|
+
if (!content)
|
|
159
|
+
return undefined;
|
|
160
|
+
const lines = content.split('\n');
|
|
161
|
+
for (const line of lines) {
|
|
162
|
+
if (!line.trim())
|
|
163
|
+
continue;
|
|
164
|
+
try {
|
|
165
|
+
const entry = JSON.parse(line);
|
|
166
|
+
if (entry.type === 'user' || entry.message?.role === 'user') {
|
|
167
|
+
const msgContent = entry.message?.content;
|
|
168
|
+
if (typeof msgContent === 'string' && msgContent.length > 0) {
|
|
169
|
+
return extractAgentIdentity(msgContent);
|
|
170
|
+
}
|
|
171
|
+
if (Array.isArray(msgContent)) {
|
|
172
|
+
for (const block of msgContent) {
|
|
173
|
+
const text = block.content ?? block.text;
|
|
174
|
+
if (typeof text === 'string' && text.length > 0) {
|
|
175
|
+
const identity = extractAgentIdentity(text);
|
|
176
|
+
if (identity)
|
|
177
|
+
return identity;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// skip malformed
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
// --- Parent session cross-reference for subagent type detection ---
|
|
190
|
+
/** Cache of parent file → (agentId → subagent_type) mappings */
|
|
191
|
+
const parentAgentMapCache = new Map();
|
|
192
|
+
/**
|
|
193
|
+
* Scan a parent session JSONL for Agent tool_use/tool_result pairs.
|
|
194
|
+
* Returns a map of agentId → subagent_type (e.g. "a4df5875a493548ce" → "alex").
|
|
195
|
+
* Results are cached per parent file and invalidated on mtime change.
|
|
196
|
+
*/
|
|
197
|
+
export function extractSubagentTypesFromParent(parentFilePath, cache) {
|
|
198
|
+
const cacheMap = cache ?? parentAgentMapCache;
|
|
199
|
+
// Check cache
|
|
200
|
+
let stat;
|
|
201
|
+
try {
|
|
202
|
+
stat = fs.statSync(parentFilePath);
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return new Map();
|
|
206
|
+
}
|
|
207
|
+
const cached = cacheMap.get(parentFilePath);
|
|
208
|
+
if (cached && cached.mtime === stat.mtimeMs) {
|
|
209
|
+
return cached.map;
|
|
210
|
+
}
|
|
211
|
+
const result = new Map();
|
|
212
|
+
// Scan the entire file for Agent tool calls — they can appear anywhere in a
|
|
213
|
+
// long conversation. Previously limited to 2MB which missed late-session spawns.
|
|
214
|
+
const MAX_SCAN = stat.size;
|
|
215
|
+
let fd = null;
|
|
216
|
+
try {
|
|
217
|
+
fd = fs.openSync(parentFilePath, 'r');
|
|
218
|
+
const scanSize = Math.min(MAX_SCAN, stat.size);
|
|
219
|
+
let offset = 0;
|
|
220
|
+
let partial = '';
|
|
221
|
+
// Track pending Agent tool_use entries: tool_use_id → subagent_type
|
|
222
|
+
const pendingToolCalls = new Map();
|
|
223
|
+
while (offset < scanSize) {
|
|
224
|
+
const chunkSize = Math.min(65536, scanSize - offset);
|
|
225
|
+
const buffer = Buffer.allocUnsafe(chunkSize);
|
|
226
|
+
fs.readSync(fd, buffer, 0, chunkSize, offset);
|
|
227
|
+
offset += chunkSize;
|
|
228
|
+
const content = partial + buffer.toString('utf-8');
|
|
229
|
+
const lines = content.split('\n');
|
|
230
|
+
// Last line may be partial — save for next chunk
|
|
231
|
+
partial = lines.pop() ?? '';
|
|
232
|
+
for (const line of lines) {
|
|
233
|
+
const trimmed = line.trim();
|
|
234
|
+
if (!trimmed)
|
|
235
|
+
continue;
|
|
236
|
+
// Quick pre-filter to avoid parsing every line
|
|
237
|
+
if (!trimmed.includes('"Agent"') && !trimmed.includes('agentId'))
|
|
238
|
+
continue;
|
|
239
|
+
try {
|
|
240
|
+
const entry = JSON.parse(trimmed);
|
|
241
|
+
const blocks = entry?.message?.content;
|
|
242
|
+
if (!Array.isArray(blocks))
|
|
243
|
+
continue;
|
|
244
|
+
for (const block of blocks) {
|
|
245
|
+
// Agent tool_use → capture subagent_type keyed by tool_use id
|
|
246
|
+
if (block.type === 'tool_use' && block.name === 'Agent' && block.input?.subagent_type) {
|
|
247
|
+
pendingToolCalls.set(block.id, block.input.subagent_type);
|
|
248
|
+
}
|
|
249
|
+
// tool_result → match agentId to pending tool call
|
|
250
|
+
if (block.type === 'tool_result' && typeof block.tool_use_id === 'string') {
|
|
251
|
+
const subagentType = pendingToolCalls.get(block.tool_use_id);
|
|
252
|
+
if (!subagentType)
|
|
253
|
+
continue;
|
|
254
|
+
// Extract agentId from the result text
|
|
255
|
+
const resultText = typeof block.content === 'string'
|
|
256
|
+
? block.content
|
|
257
|
+
: Array.isArray(block.content)
|
|
258
|
+
? block.content.map((b) => b.text ?? '').join('')
|
|
259
|
+
: '';
|
|
260
|
+
const match = resultText.match(/agentId:\s*(\w+)/);
|
|
261
|
+
if (match) {
|
|
262
|
+
result.set(match[1], subagentType);
|
|
263
|
+
pendingToolCalls.delete(block.tool_use_id);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
// skip malformed
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
fs.closeSync(fd);
|
|
274
|
+
fd = null;
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
if (fd !== null) {
|
|
278
|
+
try {
|
|
279
|
+
fs.closeSync(fd);
|
|
280
|
+
}
|
|
281
|
+
catch { /* ignore */ }
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
cacheMap.set(parentFilePath, { mtime: stat.mtimeMs, map: result });
|
|
285
|
+
return result;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Look up agent identity for a subagent by checking the parent session's Agent tool calls.
|
|
289
|
+
* Returns { name, role } if the subagent was spawned with a known subagent_type, undefined otherwise.
|
|
290
|
+
*/
|
|
291
|
+
export function resolveAgentFromParent(parentFilePath, childAgentId, cache) {
|
|
292
|
+
const typeMap = extractSubagentTypesFromParent(parentFilePath, cache);
|
|
293
|
+
const subagentType = typeMap.get(childAgentId);
|
|
294
|
+
if (!subagentType)
|
|
295
|
+
return undefined;
|
|
296
|
+
// Check known agents (normalize: subagent_type may include suffix like "alex" from "alex-cos")
|
|
297
|
+
const normalized = subagentType.toLowerCase().split('-')[0];
|
|
298
|
+
return KNOWN_AGENTS[normalized] ?? KNOWN_AGENTS[subagentType.toLowerCase()];
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Look up a known agent by its CLI agent-setting name (e.g. "riley", "sarah").
|
|
302
|
+
* Returns { name, role } if known, undefined otherwise.
|
|
303
|
+
*/
|
|
304
|
+
export function resolveAgentFromSetting(agentSetting) {
|
|
305
|
+
const normalized = agentSetting.toLowerCase().split('-')[0];
|
|
306
|
+
return KNOWN_AGENTS[normalized] ?? KNOWN_AGENTS[agentSetting.toLowerCase()];
|
|
307
|
+
}
|
|
308
|
+
export function isSystemContent(text) {
|
|
309
|
+
const trimmed = text.trim();
|
|
310
|
+
if (/^<system-reminder>[\s\S]*<\/system-reminder>$/.test(trimmed))
|
|
311
|
+
return true;
|
|
312
|
+
if (/^<task-notification>[\s\S]*<\/task-notification>$/.test(trimmed))
|
|
313
|
+
return true;
|
|
314
|
+
if (trimmed.startsWith('Shell cwd was reset to'))
|
|
315
|
+
return true;
|
|
316
|
+
if (trimmed.startsWith('Called the ') && trimmed.includes(' tool with'))
|
|
317
|
+
return true;
|
|
318
|
+
if (trimmed.startsWith('Result of calling the '))
|
|
319
|
+
return true;
|
|
320
|
+
if (trimmed.startsWith('This session is being continued from a previous conversation'))
|
|
321
|
+
return true;
|
|
322
|
+
if (trimmed.startsWith('[Request interrupted by user'))
|
|
323
|
+
return true;
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
export function extractLatestPrompt(filepath) {
|
|
327
|
+
let fd = null;
|
|
328
|
+
try {
|
|
329
|
+
fd = fs.openSync(filepath, 'r');
|
|
330
|
+
const stat = fs.fstatSync(fd);
|
|
331
|
+
if (stat.size === 0) {
|
|
332
|
+
fs.closeSync(fd);
|
|
333
|
+
return undefined;
|
|
334
|
+
}
|
|
335
|
+
const chunkSize = PROMPT_TAIL_SIZE;
|
|
336
|
+
const maxRead = Math.min(stat.size, chunkSize * 8);
|
|
337
|
+
let offset = stat.size;
|
|
338
|
+
while (offset > stat.size - maxRead && offset > 0) {
|
|
339
|
+
const readSize = Math.min(chunkSize, offset);
|
|
340
|
+
offset -= readSize;
|
|
341
|
+
const buffer = Buffer.allocUnsafe(readSize);
|
|
342
|
+
fs.readSync(fd, buffer, 0, readSize, offset);
|
|
343
|
+
const content = buffer.toString('utf-8');
|
|
344
|
+
const lines = content.split('\n');
|
|
345
|
+
const startIdx = offset > 0 ? 1 : 0;
|
|
346
|
+
for (let i = lines.length - 1; i >= startIdx; i--) {
|
|
347
|
+
const line = lines[i].trim();
|
|
348
|
+
if (!line)
|
|
349
|
+
continue;
|
|
350
|
+
try {
|
|
351
|
+
const entry = JSON.parse(line);
|
|
352
|
+
if (entry.type !== 'user' && entry.message?.role !== 'user')
|
|
353
|
+
continue;
|
|
354
|
+
const msgContent = entry.message?.content;
|
|
355
|
+
if (typeof msgContent === 'string' && msgContent.trim().length > 0) {
|
|
356
|
+
if (isSystemContent(msgContent))
|
|
357
|
+
continue;
|
|
358
|
+
fs.closeSync(fd);
|
|
359
|
+
return cleanPromptText(msgContent);
|
|
360
|
+
}
|
|
361
|
+
if (Array.isArray(msgContent)) {
|
|
362
|
+
for (const block of msgContent) {
|
|
363
|
+
if (block.type === 'tool_result')
|
|
364
|
+
continue;
|
|
365
|
+
const text = block.content ?? block.text;
|
|
366
|
+
if (typeof text === 'string' && text.trim().length > 0 && !isSystemContent(text)) {
|
|
367
|
+
fs.closeSync(fd);
|
|
368
|
+
return cleanPromptText(text);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
// skip malformed
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
fs.closeSync(fd);
|
|
379
|
+
fd = null;
|
|
380
|
+
}
|
|
381
|
+
catch {
|
|
382
|
+
if (fd !== null) {
|
|
383
|
+
try {
|
|
384
|
+
fs.closeSync(fd);
|
|
385
|
+
}
|
|
386
|
+
catch { /* ignore */ }
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return undefined;
|
|
390
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { SessionActivity } from '../types.js';
|
|
2
|
+
import type { LastEntryType } from './session-scanner.js';
|
|
3
|
+
type MachineState = 'working' | 'needs_input' | 'done';
|
|
4
|
+
export interface SessionFileState {
|
|
5
|
+
byteOffset: number;
|
|
6
|
+
mtimeMs: number;
|
|
7
|
+
fileSize: number;
|
|
8
|
+
machineState: MachineState;
|
|
9
|
+
toolUseCount: number;
|
|
10
|
+
toolResultCount: number;
|
|
11
|
+
pendingInputTool: boolean;
|
|
12
|
+
lastActivityAt: string;
|
|
13
|
+
messageCount: number;
|
|
14
|
+
sessionId?: string;
|
|
15
|
+
model?: string;
|
|
16
|
+
cwd?: string;
|
|
17
|
+
gitBranch?: string;
|
|
18
|
+
version?: string;
|
|
19
|
+
slug?: string;
|
|
20
|
+
tasksId?: string;
|
|
21
|
+
initialPrompt?: string;
|
|
22
|
+
latestPrompt?: string;
|
|
23
|
+
agentName?: string;
|
|
24
|
+
agentRole?: string;
|
|
25
|
+
lastToolName?: string;
|
|
26
|
+
lastToolDetail?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface DiscoveredFile {
|
|
29
|
+
filePath: string;
|
|
30
|
+
sessionId: string;
|
|
31
|
+
project: string;
|
|
32
|
+
projectDir: string;
|
|
33
|
+
isSubagent: boolean;
|
|
34
|
+
parentSessionId?: string;
|
|
35
|
+
agentId?: string;
|
|
36
|
+
}
|
|
37
|
+
export declare function getFileState(filePath: string, stateMap?: Map<string, SessionFileState>): SessionFileState | undefined;
|
|
38
|
+
export declare function getAllFileStates(stateMap?: Map<string, SessionFileState>): Map<string, SessionFileState>;
|
|
39
|
+
export declare function removeFileState(filePath: string, stateMap?: Map<string, SessionFileState>): void;
|
|
40
|
+
/**
|
|
41
|
+
* Get or bootstrap state for a file. If not in the map, does a cold-start bootstrap.
|
|
42
|
+
*/
|
|
43
|
+
export declare function getOrBootstrap(filePath: string, stateMap?: Map<string, SessionFileState>): SessionFileState | null;
|
|
44
|
+
/**
|
|
45
|
+
* Cold start: read last 64KB, feed through state machine, set byteOffset = fileSize.
|
|
46
|
+
*/
|
|
47
|
+
export declare function bootstrapFromTail(filePath: string, stateMap?: Map<string, SessionFileState>): SessionFileState | null;
|
|
48
|
+
/**
|
|
49
|
+
* Main entry: read new bytes from file, feed through state machine, return updated state.
|
|
50
|
+
* Returns null if no new data.
|
|
51
|
+
*/
|
|
52
|
+
export declare function processFileUpdate(filePath: string, stateMap?: Map<string, SessionFileState>): SessionFileState | null;
|
|
53
|
+
export declare function initializeAllFileStates(claudeHome: string, projectFilter?: string, stateMap?: Map<string, SessionFileState>): Map<string, DiscoveredFile>;
|
|
54
|
+
/**
|
|
55
|
+
* Discover .jsonl session files under ~/.claude/projects/.
|
|
56
|
+
* When projectFilter is provided, only scan that single project directory
|
|
57
|
+
* instead of iterating all directories.
|
|
58
|
+
*/
|
|
59
|
+
export declare function discoverSessionFiles(claudeHome: string, projectFilter?: string): Map<string, DiscoveredFile>;
|
|
60
|
+
/**
|
|
61
|
+
* Map machine state to SessionActivity for backward compatibility.
|
|
62
|
+
*/
|
|
63
|
+
export declare function toSessionActivity(state: SessionFileState): SessionActivity | null;
|
|
64
|
+
/**
|
|
65
|
+
* Map machine state to LastEntryType for backward compatibility with deriveSessionStatus.
|
|
66
|
+
*/
|
|
67
|
+
export declare function machineStateToLastEntryType(state: SessionFileState): LastEntryType;
|
|
68
|
+
export {};
|