@doingdev/opencode-claude-manager-plugin 0.1.15 → 0.1.17

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.
@@ -151,6 +151,7 @@ export class ClaudeAgentSdkAdapter {
151
151
  settingSources: input.settingSources,
152
152
  maxTurns: input.maxTurns,
153
153
  model: input.model,
154
+ effort: input.effort,
154
155
  permissionMode: input.permissionMode ?? 'acceptEdits',
155
156
  systemPrompt: input.systemPrompt
156
157
  ? { type: 'preset', preset: 'claude_code', append: input.systemPrompt }
@@ -244,6 +245,36 @@ function normalizeSdkMessages(message, includePartials) {
244
245
  if (message.type === 'user') {
245
246
  return normalizeUserSdkMessage(message, sessionId);
246
247
  }
248
+ if (message.type === 'tool_progress') {
249
+ const toolName = 'tool_name' in message && typeof message.tool_name === 'string'
250
+ ? message.tool_name
251
+ : 'tool';
252
+ const elapsed = 'elapsed_time_seconds' in message &&
253
+ typeof message.elapsed_time_seconds === 'number'
254
+ ? message.elapsed_time_seconds
255
+ : 0;
256
+ return [
257
+ {
258
+ type: 'tool_progress',
259
+ sessionId,
260
+ text: JSON.stringify({ name: toolName, elapsed }),
261
+ rawType: message.type,
262
+ },
263
+ ];
264
+ }
265
+ if (message.type === 'tool_use_summary') {
266
+ const summary = 'summary' in message && typeof message.summary === 'string'
267
+ ? message.summary
268
+ : '';
269
+ return [
270
+ {
271
+ type: 'tool_summary',
272
+ sessionId,
273
+ text: summary,
274
+ rawType: message.type,
275
+ },
276
+ ];
277
+ }
247
278
  return [
248
279
  {
249
280
  type: 'system',
@@ -0,0 +1,51 @@
1
+ import type { LiveTailEvent, ToolOutputPreview } from '../types/contracts.js';
2
+ /**
3
+ * Tails Claude Code session JSONL files for live tool output.
4
+ *
5
+ * The SDK streams high-level events (assistant text, tool_call summaries, results)
6
+ * but does not expose the raw tool output that Claude Code writes to the JSONL
7
+ * transcript on disk. This service fills that gap by polling the file for new
8
+ * lines, parsing each one, and forwarding parsed events to a caller-supplied
9
+ * callback.
10
+ */
11
+ export declare class SessionLiveTailer {
12
+ private activeTails;
13
+ /**
14
+ * Locate the JSONL file for a session.
15
+ *
16
+ * Claude Code stores transcripts at:
17
+ * ~/.claude/projects/<sanitized-cwd>/sessions/<session-id>.jsonl
18
+ *
19
+ * The `<sanitized-cwd>` folder name is an internal implementation detail that
20
+ * can change across Claude Code versions, so we search all project directories
21
+ * for the session file rather than attempting to replicate the sanitisation.
22
+ */
23
+ findSessionFile(sessionId: string, cwd?: string): string | null;
24
+ /**
25
+ * Check whether we can locate a JSONL file for the given session.
26
+ */
27
+ sessionFileExists(sessionId: string, cwd?: string): boolean;
28
+ /**
29
+ * Poll a session's JSONL file for new lines and emit parsed events.
30
+ *
31
+ * Returns a stop function. The caller **must** invoke it when tailing is no
32
+ * longer needed (e.g. when the session completes or the tool is interrupted).
33
+ */
34
+ startTailing(sessionId: string, cwd: string | undefined, onEvent: (event: LiveTailEvent) => void, pollIntervalMs?: number): () => void;
35
+ /**
36
+ * Stop tailing a specific session.
37
+ */
38
+ stopTailing(sessionId: string): void;
39
+ /**
40
+ * Stop all active tails (cleanup on shutdown).
41
+ */
42
+ stopAll(): void;
43
+ /**
44
+ * Read the last N lines from a session JSONL file.
45
+ */
46
+ getLastLines(sessionId: string, cwd: string | undefined, lineCount?: number): Promise<string[]>;
47
+ /**
48
+ * Extract a preview of recent tool output from the tail of a session file.
49
+ */
50
+ getToolOutputPreview(sessionId: string, cwd: string | undefined, maxEntries?: number): Promise<ToolOutputPreview[]>;
51
+ }
@@ -0,0 +1,269 @@
1
+ import { createReadStream, existsSync, readdirSync, statSync } from 'node:fs';
2
+ import { createInterface } from 'node:readline';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ /**
6
+ * Tails Claude Code session JSONL files for live tool output.
7
+ *
8
+ * The SDK streams high-level events (assistant text, tool_call summaries, results)
9
+ * but does not expose the raw tool output that Claude Code writes to the JSONL
10
+ * transcript on disk. This service fills that gap by polling the file for new
11
+ * lines, parsing each one, and forwarding parsed events to a caller-supplied
12
+ * callback.
13
+ */
14
+ export class SessionLiveTailer {
15
+ activeTails = new Map();
16
+ // ── Path discovery ──────────────────────────────────────────────────
17
+ /**
18
+ * Locate the JSONL file for a session.
19
+ *
20
+ * Claude Code stores transcripts at:
21
+ * ~/.claude/projects/<sanitized-cwd>/sessions/<session-id>.jsonl
22
+ *
23
+ * The `<sanitized-cwd>` folder name is an internal implementation detail that
24
+ * can change across Claude Code versions, so we search all project directories
25
+ * for the session file rather than attempting to replicate the sanitisation.
26
+ */
27
+ findSessionFile(sessionId, cwd) {
28
+ const projectsDir = path.join(os.homedir(), '.claude', 'projects');
29
+ if (!existsSync(projectsDir)) {
30
+ return null;
31
+ }
32
+ // If cwd is provided, try the sanitised form first (best-effort fast path).
33
+ if (cwd) {
34
+ const sanitised = cwd.replace(/\//g, '-');
35
+ const candidate = path.join(projectsDir, sanitised, 'sessions', `${sessionId}.jsonl`);
36
+ if (existsSync(candidate)) {
37
+ return candidate;
38
+ }
39
+ }
40
+ // Fall back to scanning all project directories.
41
+ try {
42
+ for (const entry of readdirSync(projectsDir, { withFileTypes: true })) {
43
+ if (!entry.isDirectory())
44
+ continue;
45
+ const candidate = path.join(projectsDir, entry.name, 'sessions', `${sessionId}.jsonl`);
46
+ if (existsSync(candidate)) {
47
+ return candidate;
48
+ }
49
+ }
50
+ }
51
+ catch {
52
+ // Permission denied or similar — nothing we can do.
53
+ }
54
+ return null;
55
+ }
56
+ /**
57
+ * Check whether we can locate a JSONL file for the given session.
58
+ */
59
+ sessionFileExists(sessionId, cwd) {
60
+ return this.findSessionFile(sessionId, cwd) !== null;
61
+ }
62
+ // ── Live tailing ────────────────────────────────────────────────────
63
+ /**
64
+ * Poll a session's JSONL file for new lines and emit parsed events.
65
+ *
66
+ * Returns a stop function. The caller **must** invoke it when tailing is no
67
+ * longer needed (e.g. when the session completes or the tool is interrupted).
68
+ */
69
+ startTailing(sessionId, cwd, onEvent, pollIntervalMs = 150) {
70
+ const filePath = this.findSessionFile(sessionId, cwd);
71
+ if (!filePath) {
72
+ onEvent({
73
+ type: 'error',
74
+ sessionId,
75
+ error: `Session JSONL not found for ${sessionId}`,
76
+ });
77
+ return () => { };
78
+ }
79
+ let offset = statSync(filePath).size;
80
+ let buffer = '';
81
+ let reading = false; // guard against overlapping reads
82
+ const interval = setInterval(() => {
83
+ if (reading)
84
+ return;
85
+ try {
86
+ const currentSize = statSync(filePath).size;
87
+ if (currentSize < offset) {
88
+ // File was truncated (rotation / compaction) — reset.
89
+ offset = 0;
90
+ buffer = '';
91
+ }
92
+ if (currentSize <= offset)
93
+ return;
94
+ reading = true;
95
+ const stream = createReadStream(filePath, {
96
+ start: offset,
97
+ end: currentSize - 1,
98
+ encoding: 'utf8',
99
+ });
100
+ let chunk = '';
101
+ stream.on('data', (data) => {
102
+ chunk += data;
103
+ });
104
+ stream.on('end', () => {
105
+ reading = false;
106
+ offset = currentSize;
107
+ buffer += chunk;
108
+ const lines = buffer.split('\n');
109
+ // Keep the last (possibly incomplete) segment in the buffer.
110
+ buffer = lines.pop() ?? '';
111
+ for (const line of lines) {
112
+ const trimmed = line.trim();
113
+ if (!trimmed)
114
+ continue;
115
+ try {
116
+ const parsed = JSON.parse(trimmed);
117
+ onEvent({
118
+ type: 'line',
119
+ sessionId,
120
+ data: parsed,
121
+ rawLine: trimmed,
122
+ });
123
+ }
124
+ catch {
125
+ onEvent({
126
+ type: 'line',
127
+ sessionId,
128
+ data: null,
129
+ rawLine: trimmed,
130
+ });
131
+ }
132
+ }
133
+ });
134
+ stream.on('error', (err) => {
135
+ reading = false;
136
+ onEvent({
137
+ type: 'error',
138
+ sessionId,
139
+ error: err.message,
140
+ });
141
+ });
142
+ }
143
+ catch (err) {
144
+ reading = false;
145
+ onEvent({
146
+ type: 'error',
147
+ sessionId,
148
+ error: err instanceof Error ? err.message : String(err),
149
+ });
150
+ }
151
+ }, pollIntervalMs);
152
+ this.activeTails.set(sessionId, interval);
153
+ return () => this.stopTailing(sessionId);
154
+ }
155
+ /**
156
+ * Stop tailing a specific session.
157
+ */
158
+ stopTailing(sessionId) {
159
+ const interval = this.activeTails.get(sessionId);
160
+ if (interval) {
161
+ clearInterval(interval);
162
+ this.activeTails.delete(sessionId);
163
+ }
164
+ }
165
+ /**
166
+ * Stop all active tails (cleanup on shutdown).
167
+ */
168
+ stopAll() {
169
+ for (const [, interval] of this.activeTails) {
170
+ clearInterval(interval);
171
+ }
172
+ this.activeTails.clear();
173
+ }
174
+ // ── Snapshot helpers ────────────────────────────────────────────────
175
+ /**
176
+ * Read the last N lines from a session JSONL file.
177
+ */
178
+ async getLastLines(sessionId, cwd, lineCount = 20) {
179
+ const filePath = this.findSessionFile(sessionId, cwd);
180
+ if (!filePath)
181
+ return [];
182
+ const lines = [];
183
+ const rl = createInterface({
184
+ input: createReadStream(filePath, { encoding: 'utf8' }),
185
+ });
186
+ for await (const line of rl) {
187
+ lines.push(line);
188
+ if (lines.length > lineCount) {
189
+ lines.shift();
190
+ }
191
+ }
192
+ return lines;
193
+ }
194
+ /**
195
+ * Extract a preview of recent tool output from the tail of a session file.
196
+ */
197
+ async getToolOutputPreview(sessionId, cwd, maxEntries = 5) {
198
+ const lastLines = await this.getLastLines(sessionId, cwd, 100);
199
+ const previews = [];
200
+ for (const line of lastLines) {
201
+ const trimmed = line.trim();
202
+ if (!trimmed)
203
+ continue;
204
+ try {
205
+ const parsed = JSON.parse(trimmed);
206
+ const preview = extractToolOutput(parsed);
207
+ if (preview) {
208
+ previews.push(preview);
209
+ }
210
+ }
211
+ catch {
212
+ // skip unparseable lines
213
+ }
214
+ }
215
+ return previews.slice(-maxEntries);
216
+ }
217
+ }
218
+ // ── Internal helpers ────────────────────────────────────────────────────
219
+ /**
220
+ * Attempt to extract tool output information from a JSONL record.
221
+ *
222
+ * Claude Code JSONL records with tool results can appear as:
223
+ * 1. A top-level `{ type: "user", message: { content: [{ type: "tool_result", ... }] } }`
224
+ * 2. Direct `tool_result` entries (less common).
225
+ */
226
+ function extractToolOutput(record) {
227
+ // Pattern 1: user message wrapping tool_result content blocks
228
+ if (record.type === 'user') {
229
+ const message = record.message;
230
+ if (!message)
231
+ return null;
232
+ const content = message.content;
233
+ if (!Array.isArray(content))
234
+ return null;
235
+ for (const block of content) {
236
+ if (block &&
237
+ typeof block === 'object' &&
238
+ block.type === 'tool_result') {
239
+ const b = block;
240
+ return {
241
+ toolUseId: typeof b.tool_use_id === 'string' ? b.tool_use_id : '',
242
+ content: stringifyContent(b.content),
243
+ isError: b.is_error === true,
244
+ };
245
+ }
246
+ }
247
+ }
248
+ // Pattern 2: direct tool_result record (varies by Claude Code version)
249
+ if (record.type === 'tool_result') {
250
+ return {
251
+ toolUseId: typeof record.tool_use_id === 'string' ? record.tool_use_id : '',
252
+ content: stringifyContent(record.content),
253
+ isError: record.is_error === true,
254
+ };
255
+ }
256
+ return null;
257
+ }
258
+ function stringifyContent(value) {
259
+ if (typeof value === 'string')
260
+ return value;
261
+ if (value === undefined || value === null)
262
+ return '';
263
+ try {
264
+ return JSON.stringify(value);
265
+ }
266
+ catch {
267
+ return String(value);
268
+ }
269
+ }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { Plugin } from '@opencode-ai/plugin';
2
2
  import { ClaudeManagerPlugin } from './plugin/claude-manager.plugin.js';
3
- export type { ClaudeCapabilitySnapshot, ClaudeMetadataSnapshot, ClaudeSessionRunResult, ClaudeSessionSummary, ClaudeSessionTranscriptMessage, ManagerPromptRegistry, RunClaudeSessionInput, SessionContextSnapshot, GitDiffResult, GitOperationResult, PersistentRunRecord, PersistentRunResult, ActiveSessionState, ContextWarningLevel, ToolApprovalRule, ToolApprovalPolicy, ToolApprovalDecision, } from './types/contracts.js';
3
+ export type { ClaudeCapabilitySnapshot, ClaudeMetadataSnapshot, ClaudeSessionRunResult, ClaudeSessionSummary, ClaudeSessionTranscriptMessage, ManagerPromptRegistry, RunClaudeSessionInput, SessionContextSnapshot, GitDiffResult, GitOperationResult, PersistentRunRecord, PersistentRunResult, ActiveSessionState, ContextWarningLevel, SessionMode, LiveTailEvent, ToolOutputPreview, ToolApprovalRule, ToolApprovalPolicy, ToolApprovalDecision, } from './types/contracts.js';
4
+ export { SessionLiveTailer } from './claude/session-live-tailer.js';
4
5
  export { ClaudeManagerPlugin };
5
6
  export declare const plugin: Plugin;
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  import { ClaudeManagerPlugin } from './plugin/claude-manager.plugin.js';
2
+ export { SessionLiveTailer } from './claude/session-live-tailer.js';
2
3
  export { ClaudeManagerPlugin };
3
4
  export const plugin = ClaudeManagerPlugin;
@@ -19,6 +19,7 @@ export declare class PersistentManager {
19
19
  */
20
20
  sendMessage(cwd: string, message: string, options?: {
21
21
  model?: string;
22
+ mode?: 'plan' | 'free';
22
23
  abortSignal?: AbortSignal;
23
24
  }, onEvent?: ClaudeSessionEventHandler): Promise<{
24
25
  sessionId: string | undefined;
@@ -1,12 +1,16 @@
1
1
  import type { ClaudeAgentSdkAdapter, ClaudeSessionEventHandler } from '../claude/claude-agent-sdk-adapter.js';
2
- import type { ClaudeSessionRunResult, SessionContextSnapshot } from '../types/contracts.js';
2
+ import type { ClaudeSessionRunResult, SessionContextSnapshot, SessionMode } from '../types/contracts.js';
3
3
  import type { ContextTracker } from './context-tracker.js';
4
4
  export declare class SessionController {
5
5
  private readonly sdkAdapter;
6
6
  private readonly contextTracker;
7
7
  private readonly sessionPrompt;
8
+ private readonly modePrefixes;
8
9
  private activeSessionId;
9
- constructor(sdkAdapter: ClaudeAgentSdkAdapter, contextTracker: ContextTracker, sessionPrompt: string);
10
+ constructor(sdkAdapter: ClaudeAgentSdkAdapter, contextTracker: ContextTracker, sessionPrompt: string, modePrefixes?: {
11
+ plan: string;
12
+ free: string;
13
+ });
10
14
  get isActive(): boolean;
11
15
  get sessionId(): string | null;
12
16
  /**
@@ -15,6 +19,8 @@ export declare class SessionController {
15
19
  */
16
20
  sendMessage(cwd: string, message: string, options?: {
17
21
  model?: string;
22
+ effort?: 'low' | 'medium' | 'high' | 'max';
23
+ mode?: SessionMode;
18
24
  settingSources?: Array<'user' | 'project' | 'local'>;
19
25
  abortSignal?: AbortSignal;
20
26
  }, onEvent?: ClaudeSessionEventHandler): Promise<ClaudeSessionRunResult>;
@@ -5,11 +5,16 @@ export class SessionController {
5
5
  sdkAdapter;
6
6
  contextTracker;
7
7
  sessionPrompt;
8
+ modePrefixes;
8
9
  activeSessionId = null;
9
- constructor(sdkAdapter, contextTracker, sessionPrompt) {
10
+ constructor(sdkAdapter, contextTracker, sessionPrompt, modePrefixes = {
11
+ plan: '',
12
+ free: '',
13
+ }) {
10
14
  this.sdkAdapter = sdkAdapter;
11
15
  this.contextTracker = contextTracker;
12
16
  this.sessionPrompt = sessionPrompt;
17
+ this.modePrefixes = modePrefixes;
13
18
  }
14
19
  get isActive() {
15
20
  return this.activeSessionId !== null;
@@ -22,13 +27,17 @@ export class SessionController {
22
27
  * Returns the session result including usage data.
23
28
  */
24
29
  async sendMessage(cwd, message, options, onEvent) {
30
+ const mode = options?.mode ?? 'free';
31
+ const prefix = this.modePrefixes[mode];
32
+ const prompt = prefix ? `${prefix}\n\n${message}` : message;
25
33
  const input = {
26
34
  cwd,
27
- prompt: message,
35
+ prompt,
28
36
  persistSession: true,
29
- permissionMode: 'acceptEdits',
37
+ permissionMode: mode === 'plan' ? 'plan' : 'acceptEdits',
30
38
  includePartialMessages: true,
31
39
  model: options?.model,
40
+ effort: options?.effort,
32
41
  settingSources: options?.settingSources ?? ['user', 'project', 'local'],
33
42
  abortSignal: options?.abortSignal,
34
43
  };
@@ -37,8 +46,10 @@ export class SessionController {
37
46
  input.resumeSessionId = this.activeSessionId;
38
47
  }
39
48
  else {
40
- // New session — apply the expert operator system prompt
49
+ // New session — apply the expert operator system prompt and defaults
41
50
  input.systemPrompt = this.sessionPrompt;
51
+ input.model ??= 'claude-opus-4-6';
52
+ input.effort ??= 'high';
42
53
  }
43
54
  const result = await this.sdkAdapter.runSession(input, onEvent);
44
55
  // Track the session ID
@@ -107,10 +107,13 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
107
107
  claude_manager_send: tool({
108
108
  description: 'Send a message to the persistent Claude Code session. ' +
109
109
  'Auto-creates a session on first call. Resumes the existing session on subsequent calls. ' +
110
- 'Returns the assistant response and current context health snapshot.',
110
+ 'Returns the assistant response and current context health snapshot. ' +
111
+ 'Use mode "plan" for read-only investigation and planning (no edits), ' +
112
+ 'or "free" (default) for normal execution with edit permissions.',
111
113
  args: {
112
114
  message: tool.schema.string().min(1),
113
115
  model: tool.schema.string().optional(),
116
+ mode: tool.schema.enum(['plan', 'free']).default('free'),
114
117
  cwd: tool.schema.string().optional(),
115
118
  },
116
119
  async execute(args, context) {
@@ -130,7 +133,7 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
130
133
  });
131
134
  let turnsSoFar = 0;
132
135
  let costSoFar = 0;
133
- const result = await services.manager.sendMessage(cwd, args.message, { model: args.model, abortSignal: context.abort }, (event) => {
136
+ const result = await services.manager.sendMessage(cwd, args.message, { model: args.model, mode: args.mode, abortSignal: context.abort }, (event) => {
134
137
  if (event.turns !== undefined) {
135
138
  turnsSoFar = event.turns;
136
139
  }
@@ -189,6 +192,66 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
189
192
  },
190
193
  });
191
194
  }
195
+ else if (event.type === 'user') {
196
+ const preview = event.text.length > 200
197
+ ? event.text.slice(0, 200) + '...'
198
+ : event.text;
199
+ context.metadata({
200
+ title: `Claude Code: Tool result (${turnsSoFar} turns, ${costLabel})`,
201
+ metadata: {
202
+ sessionId: event.sessionId,
203
+ type: event.type,
204
+ output: preview,
205
+ },
206
+ });
207
+ }
208
+ else if (event.type === 'tool_progress') {
209
+ let toolName = 'tool';
210
+ let elapsed = 0;
211
+ try {
212
+ const parsed = JSON.parse(event.text);
213
+ toolName = parsed.name ?? 'tool';
214
+ elapsed = parsed.elapsed ?? 0;
215
+ }
216
+ catch {
217
+ // ignore
218
+ }
219
+ context.metadata({
220
+ title: `Claude Code: ${toolName} running ${elapsed > 0 ? `(${elapsed.toFixed(0)}s)` : ''}... (${turnsSoFar} turns, ${costLabel})`,
221
+ metadata: {
222
+ sessionId: event.sessionId,
223
+ type: event.type,
224
+ tool: toolName,
225
+ elapsed,
226
+ },
227
+ });
228
+ }
229
+ else if (event.type === 'tool_summary') {
230
+ const summary = event.text.length > 200
231
+ ? event.text.slice(0, 200) + '...'
232
+ : event.text;
233
+ context.metadata({
234
+ title: `Claude Code: Tool done (${turnsSoFar} turns, ${costLabel})`,
235
+ metadata: {
236
+ sessionId: event.sessionId,
237
+ type: event.type,
238
+ summary,
239
+ },
240
+ });
241
+ }
242
+ else if (event.type === 'partial') {
243
+ const delta = event.text.length > 200
244
+ ? event.text.slice(0, 200) + '...'
245
+ : event.text;
246
+ context.metadata({
247
+ title: `Claude Code: Writing... (${turnsSoFar} turns, ${costLabel})`,
248
+ metadata: {
249
+ sessionId: event.sessionId,
250
+ type: event.type,
251
+ delta,
252
+ },
253
+ });
254
+ }
192
255
  else if (event.type === 'error') {
193
256
  context.metadata({
194
257
  title: `Claude Code: Error`,
@@ -214,6 +277,16 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
214
277
  metadata: { sessionId: result.sessionId },
215
278
  });
216
279
  }
280
+ // Fetch recent tool output from the JSONL file for richer feedback.
281
+ let toolOutputs = [];
282
+ if (result.sessionId) {
283
+ try {
284
+ toolOutputs = await services.liveTailer.getToolOutputPreview(result.sessionId, cwd, 3);
285
+ }
286
+ catch {
287
+ // Non-critical — the JSONL file may not exist yet.
288
+ }
289
+ }
217
290
  return JSON.stringify({
218
291
  sessionId: result.sessionId,
219
292
  finalText: result.finalText,
@@ -221,6 +294,7 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
221
294
  totalCostUsd: result.totalCostUsd,
222
295
  context: result.context,
223
296
  contextWarning,
297
+ toolOutputs: toolOutputs.length > 0 ? toolOutputs : undefined,
224
298
  }, null, 2);
225
299
  },
226
300
  }),
@@ -1,10 +1,12 @@
1
1
  import { ClaudeSessionService } from '../claude/claude-session.service.js';
2
+ import { SessionLiveTailer } from '../claude/session-live-tailer.js';
2
3
  import { ToolApprovalManager } from '../claude/tool-approval-manager.js';
3
4
  import { PersistentManager } from '../manager/persistent-manager.js';
4
5
  interface ClaudeManagerPluginServices {
5
6
  manager: PersistentManager;
6
7
  sessions: ClaudeSessionService;
7
8
  approvalManager: ToolApprovalManager;
9
+ liveTailer: SessionLiveTailer;
8
10
  }
9
11
  export declare function getOrCreatePluginServices(worktree: string): ClaudeManagerPluginServices;
10
12
  export {};
@@ -1,5 +1,6 @@
1
1
  import { ClaudeAgentSdkAdapter } from '../claude/claude-agent-sdk-adapter.js';
2
2
  import { ClaudeSessionService } from '../claude/claude-session.service.js';
3
+ import { SessionLiveTailer } from '../claude/session-live-tailer.js';
3
4
  import { ToolApprovalManager } from '../claude/tool-approval-manager.js';
4
5
  import { ClaudeMetadataService } from '../metadata/claude-metadata.service.js';
5
6
  import { RepoClaudeConfigReader } from '../metadata/repo-claude-config-reader.js';
@@ -21,17 +22,19 @@ export function getOrCreatePluginServices(worktree) {
21
22
  const metadataService = new ClaudeMetadataService(new RepoClaudeConfigReader(), sdkAdapter);
22
23
  const sessionService = new ClaudeSessionService(sdkAdapter, metadataService);
23
24
  const contextTracker = new ContextTracker();
24
- const sessionController = new SessionController(sdkAdapter, contextTracker, managerPromptRegistry.claudeCodeSessionPrompt);
25
+ const sessionController = new SessionController(sdkAdapter, contextTracker, managerPromptRegistry.claudeCodeSessionPrompt, managerPromptRegistry.modePrefixes);
25
26
  const gitOps = new GitOperations(worktree);
26
27
  const stateStore = new FileRunStateStore();
27
28
  const transcriptStore = new TranscriptStore();
28
29
  const manager = new PersistentManager(sessionController, gitOps, stateStore, contextTracker, transcriptStore);
29
30
  // Try to restore active session state (fire and forget)
30
31
  manager.tryRestore(worktree).catch(() => { });
32
+ const liveTailer = new SessionLiveTailer();
31
33
  const services = {
32
34
  manager,
33
35
  sessions: sessionService,
34
36
  approvalManager,
37
+ liveTailer,
35
38
  };
36
39
  serviceCache.set(worktree, services);
37
40
  return services;
@@ -1,42 +1,133 @@
1
1
  export const managerPromptRegistry = {
2
2
  managerSystemPrompt: [
3
- 'You orchestrate Claude Code through a persistent session. You are a user proxy —',
4
- 'your job is to make Claude Code do the work, not to do it yourself.',
3
+ 'You are a senior IC operating Claude Code through a persistent session.',
4
+ 'Your job is to make Claude Code do the work not to write code yourself.',
5
+ 'Think like a staff engineer: correctness, maintainability, tests, rollback safety,',
6
+ 'and clear communication to the user.',
5
7
  '',
6
- '## Workflow',
7
- '1. claude_manager_send send clear, specific instructions',
8
- '2. claude_manager_git_diff review what changed',
9
- '3. claude_manager_git_commit checkpoint good work',
10
- '4. claude_manager_git_reset discard bad work',
11
- '5. claude_manager_clear fresh session when needed',
8
+ '## Decision loop',
9
+ 'On every turn, choose exactly one action:',
10
+ ' investigateread files, grep, search the codebase to build context',
11
+ ' delegate — send a focused instruction to Claude Code via claude_manager_send',
12
+ ' review — run claude_manager_git_diff to inspect what changed',
13
+ ' validate — tell Claude Code to run tests, lint, or typecheck',
14
+ ' commit — checkpoint good work with claude_manager_git_commit',
15
+ ' correct — send a targeted fix instruction (never "try again")',
16
+ ' reset — discard bad work with claude_manager_git_reset',
17
+ ' ask — ask the user one narrow, high-value question',
18
+ '',
19
+ 'Default order: investigate → delegate → review → validate → commit.',
20
+ 'Skip steps only when you have strong evidence they are unnecessary.',
21
+ '',
22
+ '## Before you delegate',
23
+ '1. Read the relevant files yourself (you have read, grep, glob).',
24
+ ' For broad investigations, scope them narrowly or use subagents to avoid',
25
+ ' polluting your own context with excessive file contents.',
26
+ '2. Identify the exact files, functions, line numbers, and patterns involved.',
27
+ '3. Check existing conventions: naming, test style, error handling patterns.',
28
+ '4. Craft an instruction that a senior engineer would find unambiguous.',
29
+ ' Bad: "Fix the auth bug"',
30
+ ' Good: "In src/auth/session.ts, the `validateToken` function (line 42)',
31
+ ' throws on expired tokens instead of returning null. Change it to',
32
+ ' return null and update the caller in src/routes/login.ts:87."',
33
+ '',
34
+ '## After delegation — mandatory review',
35
+ 'Never claim success without evidence:',
36
+ '1. claude_manager_git_diff — read the actual diff, not just the summary.',
37
+ '2. Verify the diff matches what you asked for. Check for:',
38
+ ' - Unintended changes or regressions',
39
+ ' - Missing test updates',
40
+ ' - Style violations against repo conventions',
41
+ '3. If changes look correct, tell Claude Code to run tests/lint/typecheck.',
42
+ '4. Only commit after verification passes.',
43
+ '5. If the diff is wrong: send a specific correction or reset.',
44
+ '',
45
+ '## Handling ambiguity',
46
+ 'When requirements are unclear:',
47
+ '1. First, try to resolve it yourself — read code, check tests, grep for usage.',
48
+ '2. If ambiguity remains, ask the user ONE specific question.',
49
+ ' Bad: "What should I do?"',
50
+ ' Good: "The `UserService` has both `deactivate()` and `softDelete()` —',
51
+ ' should the new endpoint use deactivation (reversible) or',
52
+ ' soft-delete (audit-logged)?"',
53
+ '3. Never block on multiple questions at once.',
54
+ '',
55
+ '## Correction and recovery',
56
+ 'If Claude Code produces wrong output:',
57
+ '1. First correction: send a specific, targeted fix instruction.',
58
+ '2. Second correction on the same issue: reset, clear the session,',
59
+ ' and rewrite the prompt incorporating lessons from both failures.',
60
+ 'Never send three corrections for the same problem in one session.',
61
+ '',
62
+ '## Multi-step tasks',
63
+ '- Decompose large tasks into sequential focused instructions.',
64
+ '- Commit after each successful step (checkpoint for rollback).',
65
+ '- Tell Claude Code to use subagents for independent parallel work.',
66
+ '- For complex design decisions, tell Claude Code to "think hard".',
67
+ '- Prefer small diffs — they are easier to review and safer to ship.',
12
68
  '',
13
69
  '## Context management',
14
- 'Check the context snapshot in each send result:',
15
- '- Under 50%: proceed freely',
16
- '- 50-70%: consider if next task should reuse or start fresh',
17
- '- Over 70%: compact or clear before heavy work',
18
- '- Over 85% or 200k tokens: clear immediately',
19
- '',
20
- '## Delegation principles',
21
- '- Write specific task descriptions with file paths, function names, error messages',
22
- '- For large features, send sequential focused instructions',
23
- '- Tell Claude Code to use subagents for parallel/independent parts',
24
- '- After implementation, always review with git diff before committing',
25
- '- If work is wrong, send a correction (specific, not "try again") or reset',
70
+ 'Check the context snapshot returned by each send:',
71
+ '- Under 50%: proceed freely.',
72
+ '- 5070%: finish current step, then evaluate if a fresh session is needed.',
73
+ '- Over 70%: compact or clear before sending heavy instructions.',
74
+ '- Over 85%: clear the session immediately.',
75
+ '',
76
+ '## Tools reference',
77
+ 'claude_manager_send — send instruction (creates or resumes session)',
78
+ 'claude_manager_git_diff review all uncommitted changes',
79
+ 'claude_manager_git_commit stage all + commit',
80
+ 'claude_manager_git_reset — hard reset + clean (destructive)',
81
+ 'claude_manager_clear — drop session, next send starts fresh',
82
+ 'claude_manager_status — context health snapshot',
83
+ 'claude_manager_metadata — inspect repo Claude config',
84
+ 'claude_manager_sessions — list sessions or read transcripts',
85
+ 'claude_manager_runs — list or inspect run records',
86
+ '',
87
+ '## Autonomy blockers — surface these to the user',
88
+ 'Be candid about what you cannot do autonomously:',
89
+ '- Credentials, API keys, or secrets you do not have.',
90
+ '- Architectural decisions with trade-offs the user should weigh.',
91
+ '- Destructive actions on shared state (deploy, publish, force-push).',
92
+ '- Access to external services or environments you cannot reach.',
93
+ 'State the blocker, what you need, and a concrete suggestion to unblock.',
26
94
  ].join('\n'),
27
95
  claudeCodeSessionPrompt: [
28
- 'You are being directed by an expert automated operator. Treat each message',
29
- 'as a precise instruction from a skilled Claude Code user.',
30
- '',
31
- 'Key behaviors:',
32
- '- Execute instructions directly without asking for clarification',
33
- '- Use the Agent tool to spawn subagents for parallel/independent work',
34
- '- Be concise no preamble, no restating the task',
35
- '- Do NOT run git commit, git push, or git reset — the operator handles git',
36
- '- After completing work, end with a brief verification summary',
37
- '- When context is heavy, prefer targeted file reads over reading entire files',
38
- '- Report blockers immediately and specifically',
96
+ 'You are directed by an expert automated operator.',
97
+ 'Treat each message as a precise instruction from a senior engineer.',
98
+ '',
99
+ '## Execution rules',
100
+ '- Execute instructions directly. Do not ask for clarification.',
101
+ '- Be concise no preamble, no restating the task.',
102
+ '- Prefer targeted file reads over reading entire files.',
103
+ '- Use the Agent tool for independent parallel work.',
104
+ '',
105
+ '## Quality expectations',
106
+ '- Follow existing repo conventions (naming, style, patterns).',
107
+ '- When creating or modifying code, consider edge cases and error handling.',
108
+ '- When modifying existing code, preserve surrounding style and structure.',
109
+ '- If asked to implement a feature, include relevant tests unless told otherwise.',
110
+ '- Run tests/lint/typecheck when instructed; report exact output on failure.',
111
+ '',
112
+ '## Git boundary — do NOT run these commands:',
113
+ 'git commit, git push, git reset, git checkout, git stash.',
114
+ 'The operator manages all git operations externally.',
115
+ '',
116
+ '## Reporting',
117
+ '- End with a brief verification summary: what was done, what was verified.',
118
+ '- Report blockers immediately with specifics: file, line, error message.',
119
+ '- If a task is partially complete, state exactly what remains.',
39
120
  ].join('\n'),
121
+ modePrefixes: {
122
+ plan: [
123
+ '[PLAN MODE] You are in read-only planning mode. Do NOT create or edit any files.',
124
+ 'Use read, grep, glob, and search tools only.',
125
+ 'Analyze the codebase and produce a detailed implementation plan:',
126
+ 'files to change, functions to modify, new files to create, test strategy,',
127
+ 'and potential risks. End with a numbered step-by-step plan.',
128
+ ].join(' '),
129
+ free: '',
130
+ },
40
131
  contextWarnings: {
41
132
  moderate: 'Session context is filling up ({percent}% estimated). Consider whether a fresh session would be more efficient.',
42
133
  high: 'Session context is heavy ({percent}% estimated, {turns} turns, ${cost}). Start a new session or compact first.',
@@ -1,6 +1,10 @@
1
1
  export interface ManagerPromptRegistry {
2
2
  managerSystemPrompt: string;
3
3
  claudeCodeSessionPrompt: string;
4
+ modePrefixes: {
5
+ plan: string;
6
+ free: string;
7
+ };
4
8
  contextWarnings: {
5
9
  moderate: string;
6
10
  high: string;
@@ -8,6 +12,7 @@ export interface ManagerPromptRegistry {
8
12
  };
9
13
  }
10
14
  export type ClaudeSettingSource = 'user' | 'project' | 'local';
15
+ export type SessionMode = 'plan' | 'free';
11
16
  export interface ClaudeCommandMetadata {
12
17
  name: string;
13
18
  description: string;
@@ -44,7 +49,7 @@ export interface ClaudeMetadataSnapshot {
44
49
  settingsPaths: string[];
45
50
  }
46
51
  export interface ClaudeSessionEvent {
47
- type: 'init' | 'assistant' | 'partial' | 'user' | 'tool_call' | 'status' | 'system' | 'result' | 'error';
52
+ type: 'init' | 'assistant' | 'partial' | 'user' | 'tool_call' | 'tool_progress' | 'tool_summary' | 'status' | 'system' | 'result' | 'error';
48
53
  sessionId?: string;
49
54
  text: string;
50
55
  turns?: number;
@@ -57,6 +62,8 @@ export interface RunClaudeSessionInput {
57
62
  prompt: string;
58
63
  systemPrompt?: string;
59
64
  model?: string;
65
+ effort?: 'low' | 'medium' | 'high' | 'max';
66
+ mode?: SessionMode;
60
67
  permissionMode?: 'default' | 'acceptEdits' | 'plan' | 'dontAsk';
61
68
  allowedTools?: string[];
62
69
  disallowedTools?: string[];
@@ -167,6 +174,18 @@ export interface PersistentRunRecord {
167
174
  export interface PersistentRunResult {
168
175
  run: PersistentRunRecord;
169
176
  }
177
+ export interface LiveTailEvent {
178
+ type: 'line' | 'error' | 'end';
179
+ sessionId: string;
180
+ data?: unknown;
181
+ rawLine?: string;
182
+ error?: string;
183
+ }
184
+ export interface ToolOutputPreview {
185
+ toolUseId: string;
186
+ content: string;
187
+ isError: boolean;
188
+ }
170
189
  export interface ToolApprovalRule {
171
190
  id: string;
172
191
  description?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doingdev/opencode-claude-manager-plugin",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "OpenCode plugin that orchestrates Claude Code sessions.",
5
5
  "keywords": [
6
6
  "opencode",