@doingdev/opencode-claude-manager-plugin 0.1.65 → 0.1.66

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.
Files changed (123) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/manager/team-orchestrator.js +1 -1
  3. package/dist/plugin/agents/common.d.ts +2 -2
  4. package/dist/plugin/agents/common.js +5 -0
  5. package/dist/plugin/claude-manager.plugin.js +104 -0
  6. package/dist/plugin/inbox-ops.d.ts +50 -0
  7. package/dist/plugin/inbox-ops.js +166 -0
  8. package/dist/types/contracts.d.ts +18 -0
  9. package/package.json +1 -1
  10. package/dist/claude/session-live-tailer.d.ts +0 -51
  11. package/dist/claude/session-live-tailer.js +0 -269
  12. package/dist/manager/session-controller.d.ts +0 -41
  13. package/dist/manager/session-controller.js +0 -97
  14. package/dist/metadata/claude-metadata.service.d.ts +0 -12
  15. package/dist/metadata/claude-metadata.service.js +0 -38
  16. package/dist/metadata/repo-claude-config-reader.d.ts +0 -7
  17. package/dist/metadata/repo-claude-config-reader.js +0 -154
  18. package/dist/plugin/orchestrator.plugin.d.ts +0 -2
  19. package/dist/plugin/orchestrator.plugin.js +0 -116
  20. package/dist/providers/claude-code-wrapper.d.ts +0 -13
  21. package/dist/providers/claude-code-wrapper.js +0 -13
  22. package/dist/safety/bash-safety.d.ts +0 -21
  23. package/dist/safety/bash-safety.js +0 -62
  24. package/dist/src/claude/claude-agent-sdk-adapter.d.ts +0 -28
  25. package/dist/src/claude/claude-agent-sdk-adapter.js +0 -559
  26. package/dist/src/claude/claude-session.service.d.ts +0 -9
  27. package/dist/src/claude/claude-session.service.js +0 -15
  28. package/dist/src/claude/session-live-tailer.d.ts +0 -51
  29. package/dist/src/claude/session-live-tailer.js +0 -269
  30. package/dist/src/claude/tool-approval-manager.d.ts +0 -30
  31. package/dist/src/claude/tool-approval-manager.js +0 -279
  32. package/dist/src/index.d.ts +0 -5
  33. package/dist/src/index.js +0 -3
  34. package/dist/src/manager/context-tracker.d.ts +0 -32
  35. package/dist/src/manager/context-tracker.js +0 -103
  36. package/dist/src/manager/git-operations.d.ts +0 -18
  37. package/dist/src/manager/git-operations.js +0 -86
  38. package/dist/src/manager/persistent-manager.d.ts +0 -39
  39. package/dist/src/manager/persistent-manager.js +0 -44
  40. package/dist/src/manager/session-controller.d.ts +0 -41
  41. package/dist/src/manager/session-controller.js +0 -97
  42. package/dist/src/manager/team-orchestrator.d.ts +0 -81
  43. package/dist/src/manager/team-orchestrator.js +0 -612
  44. package/dist/src/plugin/agent-hierarchy.d.ts +0 -1
  45. package/dist/src/plugin/agent-hierarchy.js +0 -2
  46. package/dist/src/plugin/agents/browser-qa.d.ts +0 -14
  47. package/dist/src/plugin/agents/browser-qa.js +0 -31
  48. package/dist/src/plugin/agents/common.d.ts +0 -36
  49. package/dist/src/plugin/agents/common.js +0 -59
  50. package/dist/src/plugin/agents/cto.d.ts +0 -9
  51. package/dist/src/plugin/agents/cto.js +0 -39
  52. package/dist/src/plugin/agents/engineers.d.ts +0 -9
  53. package/dist/src/plugin/agents/engineers.js +0 -11
  54. package/dist/src/plugin/agents/index.d.ts +0 -5
  55. package/dist/src/plugin/agents/index.js +0 -5
  56. package/dist/src/plugin/agents/team-planner.d.ts +0 -10
  57. package/dist/src/plugin/agents/team-planner.js +0 -23
  58. package/dist/src/plugin/claude-manager.plugin.d.ts +0 -10
  59. package/dist/src/plugin/claude-manager.plugin.js +0 -950
  60. package/dist/src/plugin/service-factory.d.ts +0 -38
  61. package/dist/src/plugin/service-factory.js +0 -101
  62. package/dist/src/prompts/registry.d.ts +0 -2
  63. package/dist/src/prompts/registry.js +0 -210
  64. package/dist/src/state/file-run-state-store.d.ts +0 -14
  65. package/dist/src/state/file-run-state-store.js +0 -85
  66. package/dist/src/state/team-state-store.d.ts +0 -14
  67. package/dist/src/state/team-state-store.js +0 -88
  68. package/dist/src/state/transcript-store.d.ts +0 -15
  69. package/dist/src/state/transcript-store.js +0 -44
  70. package/dist/src/team/roster.d.ts +0 -5
  71. package/dist/src/team/roster.js +0 -40
  72. package/dist/src/types/contracts.d.ts +0 -261
  73. package/dist/src/types/contracts.js +0 -2
  74. package/dist/src/util/fs-helpers.d.ts +0 -8
  75. package/dist/src/util/fs-helpers.js +0 -21
  76. package/dist/src/util/project-context.d.ts +0 -10
  77. package/dist/src/util/project-context.js +0 -105
  78. package/dist/src/util/transcript-append.d.ts +0 -7
  79. package/dist/src/util/transcript-append.js +0 -29
  80. package/dist/state/file-run-state-store.d.ts +0 -14
  81. package/dist/state/file-run-state-store.js +0 -85
  82. package/dist/test/claude-agent-sdk-adapter.test.d.ts +0 -1
  83. package/dist/test/claude-agent-sdk-adapter.test.js +0 -707
  84. package/dist/test/claude-manager.plugin.test.d.ts +0 -1
  85. package/dist/test/claude-manager.plugin.test.js +0 -316
  86. package/dist/test/context-tracker.test.d.ts +0 -1
  87. package/dist/test/context-tracker.test.js +0 -130
  88. package/dist/test/cto-active-team.test.d.ts +0 -1
  89. package/dist/test/cto-active-team.test.js +0 -199
  90. package/dist/test/file-run-state-store.test.d.ts +0 -1
  91. package/dist/test/file-run-state-store.test.js +0 -82
  92. package/dist/test/fs-helpers.test.d.ts +0 -1
  93. package/dist/test/fs-helpers.test.js +0 -56
  94. package/dist/test/git-operations.test.d.ts +0 -1
  95. package/dist/test/git-operations.test.js +0 -133
  96. package/dist/test/persistent-manager.test.d.ts +0 -1
  97. package/dist/test/persistent-manager.test.js +0 -48
  98. package/dist/test/project-context.test.d.ts +0 -1
  99. package/dist/test/project-context.test.js +0 -92
  100. package/dist/test/prompt-registry.test.d.ts +0 -1
  101. package/dist/test/prompt-registry.test.js +0 -117
  102. package/dist/test/report-claude-event.test.d.ts +0 -1
  103. package/dist/test/report-claude-event.test.js +0 -304
  104. package/dist/test/session-controller.test.d.ts +0 -1
  105. package/dist/test/session-controller.test.js +0 -149
  106. package/dist/test/session-live-tailer.test.d.ts +0 -1
  107. package/dist/test/session-live-tailer.test.js +0 -313
  108. package/dist/test/team-orchestrator.test.d.ts +0 -1
  109. package/dist/test/team-orchestrator.test.js +0 -583
  110. package/dist/test/team-state-store.test.d.ts +0 -1
  111. package/dist/test/team-state-store.test.js +0 -54
  112. package/dist/test/tool-approval-manager.test.d.ts +0 -1
  113. package/dist/test/tool-approval-manager.test.js +0 -260
  114. package/dist/test/transcript-append.test.d.ts +0 -1
  115. package/dist/test/transcript-append.test.js +0 -37
  116. package/dist/test/transcript-store.test.d.ts +0 -1
  117. package/dist/test/transcript-store.test.js +0 -50
  118. package/dist/test/undo-propagation.test.d.ts +0 -1
  119. package/dist/test/undo-propagation.test.js +0 -837
  120. package/dist/util/project-context.d.ts +0 -10
  121. package/dist/util/project-context.js +0 -105
  122. package/dist/vitest.config.d.ts +0 -2
  123. package/dist/vitest.config.js +0 -11
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Plugin } from '@opencode-ai/plugin';
2
2
  import { ClaudeManagerPlugin } from './plugin/claude-manager.plugin.js';
3
- export type { ClaudeCapabilitySnapshot, ClaudeSessionRunResult, ClaudeSessionSummary, ClaudeSessionTranscriptMessage, ManagerPromptRegistry, RunClaudeSessionInput, SessionContextSnapshot, GitDiffResult, GitOperationResult, ContextWarningLevel, SessionMode, EngineerName, EngineerWorkMode, EngineerFailureKind, EngineerFailureResult, WrapperHistoryEntry, TeamEngineerRecord, TeamRecord, EngineerTaskResult, PlanDraft, SynthesizedPlanResult, ToolApprovalRule, ToolApprovalPolicy, ToolApprovalDecision, } from './types/contracts.js';
3
+ export type { ClaudeCapabilitySnapshot, ClaudeSessionRunResult, ClaudeSessionSummary, ClaudeSessionTranscriptMessage, ManagerPromptRegistry, RunClaudeSessionInput, SessionContextSnapshot, GitDiffResult, GitOperationResult, ContextWarningLevel, SessionMode, EngineerName, EngineerWorkMode, EngineerFailureKind, EngineerFailureResult, WrapperHistoryEntry, AgentMessageStatus, AgentMessage, TeamEngineerRecord, TeamRecord, EngineerTaskResult, PlanDraft, SynthesizedPlanResult, ToolApprovalRule, ToolApprovalPolicy, ToolApprovalDecision, } from './types/contracts.js';
4
4
  export { ClaudeManagerPlugin };
5
5
  export declare const plugin: Plugin;
@@ -223,7 +223,7 @@ export class TeamOrchestrator {
223
223
  }
224
224
  static classifyError(error) {
225
225
  const message = error instanceof Error ? error.message : String(error);
226
- let failureKind = 'unknown';
226
+ let failureKind;
227
227
  if (message.includes('already working on another assignment')) {
228
228
  failureKind = 'engineerBusy';
229
229
  }
@@ -11,8 +11,8 @@ export declare const ENGINEER_AGENT_IDS: {
11
11
  };
12
12
  /** General named engineers only (Tom/John/Maya/Sara/Alex). BrowserQA is a specialist registered separately. */
13
13
  export declare const ENGINEER_AGENT_NAMES: readonly ["Tom", "John", "Maya", "Sara", "Alex"];
14
- export declare const CTO_ONLY_TOOL_IDS: readonly ["team_status", "reset_engineer", "list_transcripts", "list_history", "git_diff", "git_commit", "git_reset", "git_status", "git_log", "approval_policy", "approval_decisions", "approval_update"];
15
- export declare const ALL_RESTRICTED_TOOL_IDS: readonly ["team_status", "reset_engineer", "list_transcripts", "list_history", "git_diff", "git_commit", "git_reset", "git_status", "git_log", "approval_policy", "approval_decisions", "approval_update", "claude"];
14
+ export declare const CTO_ONLY_TOOL_IDS: readonly ["team_status", "reset_engineer", "list_transcripts", "list_history", "git_diff", "git_commit", "git_reset", "git_status", "git_log", "approval_policy", "approval_decisions", "approval_update", "inspect_team_inbox"];
15
+ export declare const ALL_RESTRICTED_TOOL_IDS: readonly ["team_status", "reset_engineer", "list_transcripts", "list_history", "git_diff", "git_commit", "git_reset", "git_status", "git_log", "approval_policy", "approval_decisions", "approval_update", "inspect_team_inbox", "claude"];
16
16
  export type ToolPermission = 'allow' | 'ask' | 'deny';
17
17
  export type AgentPermission = {
18
18
  '*'?: ToolPermission;
@@ -25,6 +25,7 @@ export const CTO_ONLY_TOOL_IDS = [
25
25
  'approval_policy',
26
26
  'approval_decisions',
27
27
  'approval_update',
28
+ 'inspect_team_inbox',
28
29
  ];
29
30
  const ENGINEER_TOOL_IDS = ['claude'];
30
31
  export const ALL_RESTRICTED_TOOL_IDS = [...CTO_ONLY_TOOL_IDS, ...ENGINEER_TOOL_IDS];
@@ -50,6 +51,10 @@ export function buildEngineerPermissions() {
50
51
  '*': 'deny',
51
52
  ...denied,
52
53
  claude: 'allow',
54
+ send_message: 'allow',
55
+ list_inbox: 'allow',
56
+ get_message: 'allow',
57
+ ack_message: 'allow',
53
58
  };
54
59
  }
55
60
  export function denyRestrictedToolsGlobally(permissions) {
@@ -4,6 +4,7 @@ import { appendDebugLog } from '../util/fs-helpers.js';
4
4
  import { isEngineerName } from '../team/roster.js';
5
5
  import { TeamOrchestrator, createActionableError, getFailureGuidanceText, } from '../manager/team-orchestrator.js';
6
6
  import { AGENT_BROWSER_QA, AGENT_CTO, AGENT_TEAM_PLANNER, buildBrowserQaAgentConfig, buildCtoAgentConfig, buildEngineerAgentConfig, buildTeamPlannerAgentConfig, denyRestrictedToolsGlobally, ENGINEER_AGENT_IDS, ENGINEER_AGENT_NAMES, } from './agents/index.js';
7
+ import { ackMessage, buildInboxNotice, getMessage, getUnreadCount, listAllInboxes, listInbox, sendMessage, } from './inbox-ops.js';
7
8
  import { clearLatestRevertProcessed, clearRevertProcessed, getOrCreatePluginServices, getParentSessionId, getSessionTeam, getWrapperSessionMapping, isRevertAlreadyProcessed, markRevertProcessed, registerParentSession, registerSessionTeam, setWrapperSessionMapping, } from './service-factory.js';
8
9
  const MODEL_ENUM = ['claude-opus-4-6', 'claude-sonnet-4-6'];
9
10
  const MODE_ENUM = ['explore', 'implement', 'verify'];
@@ -314,6 +315,14 @@ export const ClaudeManagerPlugin = async ({ worktree, client }) => {
314
315
  if (wrapperContext) {
315
316
  output.system.push(wrapperContext);
316
317
  }
318
+ // Inject inbox notice if the engineer has unread peer messages.
319
+ const team = await services.teamStore.getTeam(worktree, mapping.teamId);
320
+ if (team) {
321
+ const unreadCount = getUnreadCount(team, mapping.workerName);
322
+ if (unreadCount > 0) {
323
+ output.system.push(buildInboxNotice(unreadCount));
324
+ }
325
+ }
317
326
  },
318
327
  tool: {
319
328
  claude: tool({
@@ -436,6 +445,101 @@ export const ClaudeManagerPlugin = async ({ worktree, client }) => {
436
445
  return JSON.stringify({ reset: true, engineer: engineer ?? args.engineer }, null, 2);
437
446
  },
438
447
  }),
448
+ send_message: tool({
449
+ description: 'Send a peer message to another engineer on your team. Use this for requests, questions, and recommendations — not to assign work (only CTO assigns work). Reference files by path using the filePaths arg — never paste file contents. Supply replyToId when replying to keep thread context (reply chains are capped at 5 deep; loop in the CTO if deeper coordination is needed). If you receive conflicting guidance from multiple peers, stop and ask the CTO for direction.',
450
+ args: {
451
+ to: tool.schema.enum(['Tom', 'John', 'Maya', 'Sara', 'Alex', 'BrowserQA']),
452
+ subject: tool.schema.string().min(1),
453
+ body: tool.schema.string().min(1),
454
+ filePaths: tool.schema.string().array().optional(),
455
+ replyToId: tool.schema.string().optional(),
456
+ },
457
+ async execute(args, context) {
458
+ const engineer = engineerFromAgent(context.agent);
459
+ const existing = getWrapperSessionMapping(context.worktree, context.sessionID);
460
+ const persisted = existing ??
461
+ (await services.orchestrator.findTeamByWrapperSession(context.worktree, context.sessionID));
462
+ const teamId = persisted?.teamId ?? (await resolveTeamId(context.sessionID));
463
+ annotateToolRun(context, `${engineer} → send_message to ${args.to}`, { teamId });
464
+ const result = await sendMessage(services.teamStore, context.worktree, teamId, engineer, args.to, args.subject, args.body, { filePaths: args.filePaths, replyToId: args.replyToId });
465
+ return JSON.stringify(result, null, 2);
466
+ },
467
+ }),
468
+ list_inbox: tool({
469
+ description: 'List your unread peer messages (ID, sender, subject, timestamp). Use get_message to read a full message body.',
470
+ args: {},
471
+ async execute(_args, context) {
472
+ const engineer = engineerFromAgent(context.agent);
473
+ const existing = getWrapperSessionMapping(context.worktree, context.sessionID);
474
+ const persisted = existing ??
475
+ (await services.orchestrator.findTeamByWrapperSession(context.worktree, context.sessionID));
476
+ const teamId = persisted?.teamId ?? (await resolveTeamId(context.sessionID));
477
+ annotateToolRun(context, `${engineer} → list_inbox`, { teamId });
478
+ const messages = await listInbox(services.teamStore, context.worktree, teamId, engineer);
479
+ const summaries = messages.map(({ id, fromEngineer, subject, createdAt, depth, replyToId, filePaths }) => ({
480
+ id,
481
+ from: fromEngineer,
482
+ subject,
483
+ createdAt,
484
+ depth,
485
+ ...(replyToId !== undefined && { replyToId }),
486
+ ...(filePaths !== undefined && filePaths.length > 0 && { filePaths }),
487
+ }));
488
+ return JSON.stringify({ count: summaries.length, messages: summaries }, null, 2);
489
+ },
490
+ }),
491
+ get_message: tool({
492
+ description: 'Read the full body of a peer message by its ID.',
493
+ args: {
494
+ messageId: tool.schema.string().min(1),
495
+ },
496
+ async execute(args, context) {
497
+ const engineer = engineerFromAgent(context.agent);
498
+ const existing = getWrapperSessionMapping(context.worktree, context.sessionID);
499
+ const persisted = existing ??
500
+ (await services.orchestrator.findTeamByWrapperSession(context.worktree, context.sessionID));
501
+ const teamId = persisted?.teamId ?? (await resolveTeamId(context.sessionID));
502
+ annotateToolRun(context, `${engineer} → get_message`, { teamId, messageId: args.messageId });
503
+ const message = await getMessage(services.teamStore, context.worktree, teamId, engineer, args.messageId);
504
+ if (!message) {
505
+ return JSON.stringify({ error: 'Message not found.' });
506
+ }
507
+ return JSON.stringify(message, null, 2);
508
+ },
509
+ }),
510
+ ack_message: tool({
511
+ description: 'Acknowledge a peer message to hide it from your inbox. The message is retained for CTO inspection but will not appear in future list_inbox results.',
512
+ args: {
513
+ messageId: tool.schema.string().min(1),
514
+ },
515
+ async execute(args, context) {
516
+ const engineer = engineerFromAgent(context.agent);
517
+ const existing = getWrapperSessionMapping(context.worktree, context.sessionID);
518
+ const persisted = existing ??
519
+ (await services.orchestrator.findTeamByWrapperSession(context.worktree, context.sessionID));
520
+ const teamId = persisted?.teamId ?? (await resolveTeamId(context.sessionID));
521
+ annotateToolRun(context, `${engineer} → ack_message`, { teamId, messageId: args.messageId });
522
+ const ok = await ackMessage(services.teamStore, context.worktree, teamId, engineer, args.messageId);
523
+ return JSON.stringify({ acked: ok }, null, 2);
524
+ },
525
+ }),
526
+ inspect_team_inbox: tool({
527
+ description: 'Read all peer messages across all engineers on the team, including acked messages. Optionally filter by engineer.',
528
+ args: {
529
+ teamId: tool.schema.string().optional(),
530
+ engineer: tool.schema.enum(['Tom', 'John', 'Maya', 'Sara', 'Alex', 'BrowserQA']).optional(),
531
+ },
532
+ async execute(args, context) {
533
+ const teamId = args.teamId ?? context.sessionID;
534
+ annotateToolRun(context, 'Inspecting team inbox', { teamId, engineer: args.engineer });
535
+ const team = await services.orchestrator.getOrCreateTeam(context.worktree, teamId);
536
+ const allInboxes = listAllInboxes(team);
537
+ if (args.engineer) {
538
+ return JSON.stringify({ [args.engineer]: allInboxes[args.engineer] ?? [] }, null, 2);
539
+ }
540
+ return JSON.stringify(allInboxes, null, 2);
541
+ },
542
+ }),
439
543
  git_diff: tool({
440
544
  description: 'Show diff of uncommitted changes. Use paths to filter to specific files or use ref to compare against another branch, tag, or commit.',
441
545
  args: {
@@ -0,0 +1,50 @@
1
+ import type { AgentMessage, EngineerName, TeamRecord } from '../types/contracts.js';
2
+ import type { TeamStateStore } from '../state/team-state-store.js';
3
+ export declare const MAX_INBOX_SIZE = 50;
4
+ export declare const MAX_MESSAGE_BODY_LENGTH = 4000;
5
+ export declare const MAX_MESSAGE_SUBJECT_LENGTH = 200;
6
+ /**
7
+ * Maximum reply-chain depth. A top-level message has depth 0; each reply
8
+ * increments by 1. Attempts to exceed this cap are rejected with a message
9
+ * directing the agent to loop in the CTO.
10
+ */
11
+ export declare const MAX_THREAD_DEPTH = 5;
12
+ export type SendMessageResult = {
13
+ ok: true;
14
+ message: AgentMessage;
15
+ } | {
16
+ ok: false;
17
+ error: string;
18
+ };
19
+ export interface SendMessageOptions {
20
+ /** File paths referenced in the message. Paths only — contents are never embedded. */
21
+ filePaths?: string[];
22
+ /**
23
+ * ID of the message being replied to. Must be a message in the sender's own inbox.
24
+ * Providing this increments the thread depth and enforces the MAX_THREAD_DEPTH cap.
25
+ */
26
+ replyToId?: string;
27
+ }
28
+ /**
29
+ * Deliver a peer message from one engineer to another on the same team.
30
+ * All validation and persistence happen atomically via the write queue.
31
+ */
32
+ export declare function sendMessage(store: TeamStateStore, cwd: string, teamId: string, fromEngineer: EngineerName, toEngineer: EngineerName, subject: string, body: string, options?: SendMessageOptions): Promise<SendMessageResult>;
33
+ /**
34
+ * Return all unread messages for an engineer, in FIFO order.
35
+ * Acked messages are excluded.
36
+ */
37
+ export declare function listInbox(store: TeamStateStore, cwd: string, teamId: string, engineerName: EngineerName): Promise<AgentMessage[]>;
38
+ /** Fetch a single message by ID. Returns null if not found or if it belongs to a different engineer. */
39
+ export declare function getMessage(store: TeamStateStore, cwd: string, teamId: string, engineerName: EngineerName, messageId: string): Promise<AgentMessage | null>;
40
+ /**
41
+ * Mark a message as acked (hidden from list_inbox).
42
+ * Returns true if the ack was applied, false if the message was not found or already acked.
43
+ */
44
+ export declare function ackMessage(store: TeamStateStore, cwd: string, teamId: string, engineerName: EngineerName, messageId: string): Promise<boolean>;
45
+ /** Count unread messages for one engineer without hitting the store. */
46
+ export declare function getUnreadCount(team: TeamRecord, engineerName: EngineerName): number;
47
+ /** Build a concise notice to inject into the engineer's system prompt. */
48
+ export declare function buildInboxNotice(unreadCount: number): string;
49
+ /** Return all inboxes on a team (including acked messages) for CTO inspection. */
50
+ export declare function listAllInboxes(team: TeamRecord): Record<string, AgentMessage[]>;
@@ -0,0 +1,166 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ export const MAX_INBOX_SIZE = 50;
3
+ export const MAX_MESSAGE_BODY_LENGTH = 4000;
4
+ export const MAX_MESSAGE_SUBJECT_LENGTH = 200;
5
+ /**
6
+ * Maximum reply-chain depth. A top-level message has depth 0; each reply
7
+ * increments by 1. Attempts to exceed this cap are rejected with a message
8
+ * directing the agent to loop in the CTO.
9
+ */
10
+ export const MAX_THREAD_DEPTH = 5;
11
+ /**
12
+ * Deliver a peer message from one engineer to another on the same team.
13
+ * All validation and persistence happen atomically via the write queue.
14
+ */
15
+ export async function sendMessage(store, cwd, teamId, fromEngineer, toEngineer, subject, body, options = {}) {
16
+ if (fromEngineer === toEngineer) {
17
+ return { ok: false, error: 'Cannot send a message to yourself.' };
18
+ }
19
+ if (subject.length > MAX_MESSAGE_SUBJECT_LENGTH) {
20
+ return { ok: false, error: `Subject exceeds ${MAX_MESSAGE_SUBJECT_LENGTH} characters.` };
21
+ }
22
+ if (body.length > MAX_MESSAGE_BODY_LENGTH) {
23
+ return { ok: false, error: `Body exceeds ${MAX_MESSAGE_BODY_LENGTH} characters.` };
24
+ }
25
+ const { filePaths, replyToId } = options;
26
+ if (filePaths?.some((p) => !p.trim())) {
27
+ return { ok: false, error: 'File paths must not be empty strings.' };
28
+ }
29
+ let sent = null;
30
+ let updateError = null;
31
+ try {
32
+ await store.updateTeam(cwd, teamId, (team) => {
33
+ const recipient = team.engineers.find((e) => e.name === toEngineer);
34
+ if (!recipient) {
35
+ throw new Error(`Engineer ${toEngineer} is not on team ${teamId}.`);
36
+ }
37
+ const inbox = recipient.inbox ?? [];
38
+ const unreadCount = inbox.filter((m) => m.status === 'unread').length;
39
+ if (unreadCount >= MAX_INBOX_SIZE) {
40
+ throw new Error(`${toEngineer}'s inbox is full (${MAX_INBOX_SIZE} unread messages).`);
41
+ }
42
+ // Thread-depth guard: resolve depth from the replied-to message.
43
+ let depth = 0;
44
+ if (replyToId !== undefined) {
45
+ const senderRecord = team.engineers.find((e) => e.name === fromEngineer);
46
+ const original = (senderRecord?.inbox ?? []).find((m) => m.id === replyToId);
47
+ if (!original) {
48
+ throw new Error(`Reply-to message ${replyToId} not found in ${fromEngineer}'s inbox.`);
49
+ }
50
+ if (original.toEngineer !== fromEngineer) {
51
+ throw new Error('replyToId must reference a message addressed to you.');
52
+ }
53
+ depth = original.depth + 1;
54
+ if (depth > MAX_THREAD_DEPTH) {
55
+ throw new Error(`Thread depth limit reached (max ${MAX_THREAD_DEPTH}). Loop in the CTO to coordinate further.`);
56
+ }
57
+ }
58
+ const msg = {
59
+ id: randomUUID(),
60
+ fromEngineer,
61
+ toEngineer,
62
+ teamId,
63
+ createdAt: new Date().toISOString(),
64
+ status: 'unread',
65
+ subject: subject.trim(),
66
+ body,
67
+ depth,
68
+ ...(replyToId !== undefined && { replyToId }),
69
+ ...(filePaths !== undefined && filePaths.length > 0 && { filePaths }),
70
+ };
71
+ sent = msg;
72
+ return {
73
+ ...team,
74
+ updatedAt: new Date().toISOString(),
75
+ engineers: team.engineers.map((e) => e.name === toEngineer ? { ...e, inbox: [...inbox, msg] } : e),
76
+ };
77
+ });
78
+ }
79
+ catch (error) {
80
+ updateError = error instanceof Error ? error.message : String(error);
81
+ }
82
+ if (updateError !== null) {
83
+ return { ok: false, error: updateError };
84
+ }
85
+ if (!sent) {
86
+ return { ok: false, error: 'Failed to persist message.' };
87
+ }
88
+ return { ok: true, message: sent };
89
+ }
90
+ /**
91
+ * Return all unread messages for an engineer, in FIFO order.
92
+ * Acked messages are excluded.
93
+ */
94
+ export async function listInbox(store, cwd, teamId, engineerName) {
95
+ const team = await store.getTeam(cwd, teamId);
96
+ if (!team)
97
+ return [];
98
+ const engineer = team.engineers.find((e) => e.name === engineerName);
99
+ if (!engineer)
100
+ return [];
101
+ return (engineer.inbox ?? []).filter((m) => m.status === 'unread');
102
+ }
103
+ /** Fetch a single message by ID. Returns null if not found or if it belongs to a different engineer. */
104
+ export async function getMessage(store, cwd, teamId, engineerName, messageId) {
105
+ const team = await store.getTeam(cwd, teamId);
106
+ if (!team)
107
+ return null;
108
+ const engineer = team.engineers.find((e) => e.name === engineerName);
109
+ if (!engineer)
110
+ return null;
111
+ return (engineer.inbox ?? []).find((m) => m.id === messageId) ?? null;
112
+ }
113
+ /**
114
+ * Mark a message as acked (hidden from list_inbox).
115
+ * Returns true if the ack was applied, false if the message was not found or already acked.
116
+ */
117
+ export async function ackMessage(store, cwd, teamId, engineerName, messageId) {
118
+ let acked = false;
119
+ try {
120
+ await store.updateTeam(cwd, teamId, (team) => {
121
+ const engineer = team.engineers.find((e) => e.name === engineerName);
122
+ if (!engineer)
123
+ return team;
124
+ const inbox = engineer.inbox ?? [];
125
+ const msg = inbox.find((m) => m.id === messageId);
126
+ if (!msg || msg.status === 'acked')
127
+ return team;
128
+ acked = true;
129
+ return {
130
+ ...team,
131
+ updatedAt: new Date().toISOString(),
132
+ engineers: team.engineers.map((e) => e.name === engineerName
133
+ ? {
134
+ ...e,
135
+ inbox: inbox.map((m) => m.id === messageId ? { ...m, status: 'acked' } : m),
136
+ }
137
+ : e),
138
+ };
139
+ });
140
+ }
141
+ catch {
142
+ return false;
143
+ }
144
+ return acked;
145
+ }
146
+ /** Count unread messages for one engineer without hitting the store. */
147
+ export function getUnreadCount(team, engineerName) {
148
+ const engineer = team.engineers.find((e) => e.name === engineerName);
149
+ return (engineer?.inbox ?? []).filter((m) => m.status === 'unread').length;
150
+ }
151
+ /** Build a concise notice to inject into the engineer's system prompt. */
152
+ export function buildInboxNotice(unreadCount) {
153
+ const plural = unreadCount === 1 ? '' : 's';
154
+ return `You have ${unreadCount} unread peer message${plural}. Use list_inbox to view them, get_message to read a message body, and ack_message to hide a message from your inbox.`;
155
+ }
156
+ /** Return all inboxes on a team (including acked messages) for CTO inspection. */
157
+ export function listAllInboxes(team) {
158
+ const result = {};
159
+ for (const engineer of team.engineers) {
160
+ const inbox = engineer.inbox ?? [];
161
+ if (inbox.length > 0) {
162
+ result[engineer.name] = inbox;
163
+ }
164
+ }
165
+ return result;
166
+ }
@@ -27,6 +27,23 @@ export interface WrapperHistoryEntry {
27
27
  mode?: EngineerWorkMode;
28
28
  text: string;
29
29
  }
30
+ export type AgentMessageStatus = 'unread' | 'acked';
31
+ export interface AgentMessage {
32
+ id: string;
33
+ fromEngineer: EngineerName;
34
+ toEngineer: EngineerName;
35
+ teamId: string;
36
+ createdAt: string;
37
+ status: AgentMessageStatus;
38
+ subject: string;
39
+ body: string;
40
+ /** 0-based chain depth. Top-level messages are 0; each reply increments by 1. */
41
+ depth: number;
42
+ /** ID of the message being replied to, if this is a reply. */
43
+ replyToId?: string;
44
+ /** File paths referenced in this message. Contents are not embedded — paths only. */
45
+ filePaths?: string[];
46
+ }
30
47
  interface ClaudeCommandMetadata {
31
48
  name: string;
32
49
  description: string;
@@ -145,6 +162,7 @@ export interface TeamEngineerRecord {
145
162
  lastUsedAt: string | null;
146
163
  wrapperHistory: WrapperHistoryEntry[];
147
164
  context: SessionContextSnapshot;
165
+ inbox?: AgentMessage[];
148
166
  }
149
167
  export type EngineerFailureKind = 'sdkError' | 'contextExhausted' | 'toolDenied' | 'modeNotSupported' | 'aborted' | 'engineerBusy' | 'unknown';
150
168
  export interface EngineerFailureResult {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doingdev/opencode-claude-manager-plugin",
3
- "version": "0.1.65",
3
+ "version": "0.1.66",
4
4
  "description": "OpenCode plugin that orchestrates Claude Code sessions.",
5
5
  "keywords": [
6
6
  "opencode",
@@ -1,51 +0,0 @@
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
- }