@doingdev/opencode-claude-manager-plugin 0.1.35 → 0.1.43

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 (90) hide show
  1. package/dist/claude/claude-agent-sdk-adapter.js +1 -0
  2. package/dist/manager/git-operations.d.ts +10 -1
  3. package/dist/manager/git-operations.js +18 -3
  4. package/dist/manager/persistent-manager.d.ts +19 -3
  5. package/dist/manager/persistent-manager.js +21 -9
  6. package/dist/manager/session-controller.d.ts +8 -5
  7. package/dist/manager/session-controller.js +25 -20
  8. package/dist/metadata/claude-metadata.service.d.ts +12 -0
  9. package/dist/metadata/claude-metadata.service.js +38 -0
  10. package/dist/metadata/repo-claude-config-reader.d.ts +7 -0
  11. package/dist/metadata/repo-claude-config-reader.js +154 -0
  12. package/dist/plugin/agent-hierarchy.d.ts +9 -9
  13. package/dist/plugin/agent-hierarchy.js +25 -25
  14. package/dist/plugin/claude-manager.plugin.js +83 -46
  15. package/dist/plugin/orchestrator.plugin.d.ts +2 -0
  16. package/dist/plugin/orchestrator.plugin.js +116 -0
  17. package/dist/plugin/service-factory.js +3 -8
  18. package/dist/prompts/registry.js +100 -103
  19. package/dist/providers/claude-code-wrapper.d.ts +13 -0
  20. package/dist/providers/claude-code-wrapper.js +13 -0
  21. package/dist/safety/bash-safety.d.ts +21 -0
  22. package/dist/safety/bash-safety.js +62 -0
  23. package/dist/src/claude/claude-agent-sdk-adapter.d.ts +27 -0
  24. package/dist/src/claude/claude-agent-sdk-adapter.js +517 -0
  25. package/dist/src/claude/claude-session.service.d.ts +10 -0
  26. package/dist/src/claude/claude-session.service.js +18 -0
  27. package/dist/src/claude/session-live-tailer.d.ts +51 -0
  28. package/dist/src/claude/session-live-tailer.js +269 -0
  29. package/dist/src/claude/tool-approval-manager.d.ts +27 -0
  30. package/dist/src/claude/tool-approval-manager.js +232 -0
  31. package/dist/src/index.d.ts +6 -0
  32. package/dist/src/index.js +4 -0
  33. package/dist/src/manager/context-tracker.d.ts +33 -0
  34. package/dist/src/manager/context-tracker.js +106 -0
  35. package/dist/src/manager/git-operations.d.ts +12 -0
  36. package/dist/src/manager/git-operations.js +76 -0
  37. package/dist/src/manager/persistent-manager.d.ts +77 -0
  38. package/dist/src/manager/persistent-manager.js +170 -0
  39. package/dist/src/manager/session-controller.d.ts +44 -0
  40. package/dist/src/manager/session-controller.js +147 -0
  41. package/dist/src/plugin/agent-hierarchy.d.ts +60 -0
  42. package/dist/src/plugin/agent-hierarchy.js +157 -0
  43. package/dist/src/plugin/claude-manager.plugin.d.ts +2 -0
  44. package/dist/src/plugin/claude-manager.plugin.js +563 -0
  45. package/dist/src/plugin/service-factory.d.ts +12 -0
  46. package/dist/src/plugin/service-factory.js +38 -0
  47. package/dist/src/prompts/registry.d.ts +11 -0
  48. package/dist/src/prompts/registry.js +260 -0
  49. package/dist/src/state/file-run-state-store.d.ts +14 -0
  50. package/dist/src/state/file-run-state-store.js +85 -0
  51. package/dist/src/state/transcript-store.d.ts +15 -0
  52. package/dist/src/state/transcript-store.js +44 -0
  53. package/dist/src/types/contracts.d.ts +200 -0
  54. package/dist/src/types/contracts.js +1 -0
  55. package/dist/src/util/fs-helpers.d.ts +2 -0
  56. package/dist/src/util/fs-helpers.js +10 -0
  57. package/dist/src/util/project-context.d.ts +10 -0
  58. package/dist/src/util/project-context.js +105 -0
  59. package/dist/src/util/transcript-append.d.ts +7 -0
  60. package/dist/src/util/transcript-append.js +29 -0
  61. package/dist/test/claude-agent-sdk-adapter.test.d.ts +1 -0
  62. package/dist/test/claude-agent-sdk-adapter.test.js +459 -0
  63. package/dist/test/claude-manager.plugin.test.d.ts +1 -0
  64. package/dist/test/claude-manager.plugin.test.js +331 -0
  65. package/dist/test/context-tracker.test.d.ts +1 -0
  66. package/dist/test/context-tracker.test.js +138 -0
  67. package/dist/test/file-run-state-store.test.d.ts +1 -0
  68. package/dist/test/file-run-state-store.test.js +82 -0
  69. package/dist/test/git-operations.test.d.ts +1 -0
  70. package/dist/test/git-operations.test.js +90 -0
  71. package/dist/test/persistent-manager.test.d.ts +1 -0
  72. package/dist/test/persistent-manager.test.js +208 -0
  73. package/dist/test/project-context.test.d.ts +1 -0
  74. package/dist/test/project-context.test.js +92 -0
  75. package/dist/test/prompt-registry.test.d.ts +1 -0
  76. package/dist/test/prompt-registry.test.js +256 -0
  77. package/dist/test/session-controller.test.d.ts +1 -0
  78. package/dist/test/session-controller.test.js +149 -0
  79. package/dist/test/session-live-tailer.test.d.ts +1 -0
  80. package/dist/test/session-live-tailer.test.js +313 -0
  81. package/dist/test/tool-approval-manager.test.d.ts +1 -0
  82. package/dist/test/tool-approval-manager.test.js +264 -0
  83. package/dist/test/transcript-append.test.d.ts +1 -0
  84. package/dist/test/transcript-append.test.js +37 -0
  85. package/dist/test/transcript-store.test.d.ts +1 -0
  86. package/dist/test/transcript-store.test.js +50 -0
  87. package/dist/types/contracts.d.ts +3 -4
  88. package/dist/vitest.config.d.ts +2 -0
  89. package/dist/vitest.config.js +11 -0
  90. package/package.json +2 -2
@@ -162,6 +162,7 @@ export class ClaudeAgentSdkAdapter {
162
162
  forkSession: input.forkSession,
163
163
  persistSession: input.persistSession,
164
164
  includePartialMessages: input.includePartialMessages,
165
+ enableFileCheckpointing: false,
165
166
  settingSources: input.settingSources,
166
167
  maxTurns: input.maxTurns,
167
168
  model: input.model,
@@ -2,10 +2,19 @@ import type { GitDiffResult, GitOperationResult } from '../types/contracts.js';
2
2
  export declare class GitOperations {
3
3
  private readonly cwd;
4
4
  constructor(cwd: string);
5
- diff(): Promise<GitDiffResult>;
5
+ diff(options?: {
6
+ paths?: string[];
7
+ staged?: boolean;
8
+ ref?: string;
9
+ }): Promise<GitDiffResult>;
6
10
  diffStat(): Promise<string>;
7
11
  commit(message: string): Promise<GitOperationResult>;
8
12
  resetHard(): Promise<GitOperationResult>;
13
+ status(): Promise<{
14
+ output: string;
15
+ isClean: boolean;
16
+ }>;
17
+ log(count?: number): Promise<string>;
9
18
  currentBranch(): Promise<string>;
10
19
  recentCommits(count?: number): Promise<string>;
11
20
  private git;
@@ -6,10 +6,15 @@ export class GitOperations {
6
6
  constructor(cwd) {
7
7
  this.cwd = cwd;
8
8
  }
9
- async diff() {
9
+ async diff(options = {}) {
10
+ const ref = options.staged ? '--cached' : (options.ref ?? 'HEAD');
11
+ const args = ['diff', ref];
12
+ if (options.paths && options.paths.length > 0) {
13
+ args.push('--', ...options.paths);
14
+ }
10
15
  const [diffText, statOutput] = await Promise.all([
11
- this.git(['diff', 'HEAD']),
12
- this.git(['diff', 'HEAD', '--stat']),
16
+ this.git(args),
17
+ this.git([...args, '--stat']),
13
18
  ]);
14
19
  const stats = parseStatLine(statOutput);
15
20
  return {
@@ -50,6 +55,16 @@ export class GitOperations {
50
55
  };
51
56
  }
52
57
  }
58
+ async status() {
59
+ const output = await this.git(['status', '-s']);
60
+ return {
61
+ output,
62
+ isClean: output.trim().length === 0,
63
+ };
64
+ }
65
+ async log(count = 5) {
66
+ return this.git(['log', '--oneline', `-${count}`]);
67
+ }
53
68
  async currentBranch() {
54
69
  const branch = await this.git(['rev-parse', '--abbrev-ref', 'HEAD']);
55
70
  return branch.trim();
@@ -21,6 +21,7 @@ export declare class PersistentManager {
21
21
  model?: string;
22
22
  effort?: 'low' | 'medium' | 'high' | 'max';
23
23
  mode?: 'plan' | 'free';
24
+ sessionSystemPrompt?: string;
24
25
  abortSignal?: AbortSignal;
25
26
  }, onEvent?: ClaudeSessionEventHandler): Promise<{
26
27
  sessionId: string | undefined;
@@ -35,11 +36,26 @@ export declare class PersistentManager {
35
36
  /**
36
37
  * Get the current git diff.
37
38
  */
38
- gitDiff(): Promise<GitDiffResult>;
39
+ gitDiff(options?: {
40
+ paths?: string[];
41
+ staged?: boolean;
42
+ ref?: string;
43
+ }): Promise<GitDiffResult>;
39
44
  /**
40
45
  * Commit all current changes.
41
46
  */
42
47
  gitCommit(message: string): Promise<GitOperationResult>;
48
+ /**
49
+ * Get git status summary.
50
+ */
51
+ gitStatus(): Promise<{
52
+ output: string;
53
+ isClean: boolean;
54
+ }>;
55
+ /**
56
+ * Get recent commit log.
57
+ */
58
+ gitLog(count?: number): Promise<string>;
43
59
  /**
44
60
  * Hard reset to discard all uncommitted changes.
45
61
  */
@@ -51,7 +67,7 @@ export declare class PersistentManager {
51
67
  /**
52
68
  * Clear the active session. Next send creates a fresh one.
53
69
  */
54
- clearSession(cwd: string): Promise<string | null>;
70
+ clearSession(): Promise<string | null>;
55
71
  /**
56
72
  * Compact the current session to free context.
57
73
  */
@@ -70,7 +86,7 @@ export declare class PersistentManager {
70
86
  /**
71
87
  * Try to restore session state from disk on startup.
72
88
  */
73
- tryRestore(cwd: string): Promise<boolean>;
89
+ tryRestore(): Promise<boolean>;
74
90
  listRuns(cwd: string): Promise<PersistentRunRecord[]>;
75
91
  getRun(cwd: string, runId: string): Promise<PersistentRunRecord | null>;
76
92
  }
@@ -17,7 +17,7 @@ export class PersistentManager {
17
17
  * Creates a new session if none exists.
18
18
  */
19
19
  async sendMessage(cwd, message, options, onEvent) {
20
- const result = await this.sessionController.sendMessage(cwd, message, options, onEvent);
20
+ const result = await this.sessionController.sendMessage(message, options, onEvent);
21
21
  if (result.sessionId && result.events.length > 0) {
22
22
  await this.transcriptStore.appendEvents(cwd, result.sessionId, result.events);
23
23
  }
@@ -35,8 +35,8 @@ export class PersistentManager {
35
35
  /**
36
36
  * Get the current git diff.
37
37
  */
38
- async gitDiff() {
39
- return this.gitOps.diff();
38
+ async gitDiff(options = {}) {
39
+ return this.gitOps.diff(options);
40
40
  }
41
41
  /**
42
42
  * Commit all current changes.
@@ -44,6 +44,18 @@ export class PersistentManager {
44
44
  async gitCommit(message) {
45
45
  return this.gitOps.commit(message);
46
46
  }
47
+ /**
48
+ * Get git status summary.
49
+ */
50
+ async gitStatus() {
51
+ return this.gitOps.status();
52
+ }
53
+ /**
54
+ * Get recent commit log.
55
+ */
56
+ async gitLog(count = 5) {
57
+ return this.gitOps.log(count);
58
+ }
47
59
  /**
48
60
  * Hard reset to discard all uncommitted changes.
49
61
  */
@@ -59,14 +71,14 @@ export class PersistentManager {
59
71
  /**
60
72
  * Clear the active session. Next send creates a fresh one.
61
73
  */
62
- async clearSession(cwd) {
63
- return this.sessionController.clearSession(cwd);
74
+ async clearSession() {
75
+ return this.sessionController.clearSession();
64
76
  }
65
77
  /**
66
78
  * Compact the current session to free context.
67
79
  */
68
80
  async compactSession(cwd, onEvent) {
69
- const result = await this.sessionController.compactSession(cwd, onEvent);
81
+ const result = await this.sessionController.compactSession(onEvent);
70
82
  if (result.sessionId && result.events.length > 0) {
71
83
  await this.transcriptStore.appendEvents(cwd, result.sessionId, result.events);
72
84
  }
@@ -108,7 +120,7 @@ export class PersistentManager {
108
120
  await this.stateStore.saveRun(runRecord);
109
121
  await onProgress?.(runRecord);
110
122
  try {
111
- const result = await this.sessionController.sendMessage(cwd, task, options, async (event) => {
123
+ const result = await this.sessionController.sendMessage(task, options, async (event) => {
112
124
  // Update run record with progress events
113
125
  const currentRun = await this.stateStore.getRun(cwd, runId);
114
126
  if (currentRun) {
@@ -158,8 +170,8 @@ export class PersistentManager {
158
170
  /**
159
171
  * Try to restore session state from disk on startup.
160
172
  */
161
- async tryRestore(cwd) {
162
- return this.sessionController.tryRestore(cwd);
173
+ async tryRestore() {
174
+ return this.sessionController.tryRestore();
163
175
  }
164
176
  listRuns(cwd) {
165
177
  return this.stateStore.listRuns(cwd);
@@ -5,9 +5,11 @@ export declare class SessionController {
5
5
  private readonly sdkAdapter;
6
6
  private readonly contextTracker;
7
7
  private readonly sessionPrompt;
8
+ private readonly wrapperType;
9
+ private readonly worktree;
8
10
  private readonly modePrefixes;
9
11
  private activeSessionId;
10
- constructor(sdkAdapter: ClaudeAgentSdkAdapter, contextTracker: ContextTracker, sessionPrompt: string, modePrefixes?: {
12
+ constructor(sdkAdapter: ClaudeAgentSdkAdapter, contextTracker: ContextTracker, sessionPrompt: string | undefined, wrapperType: string, worktree: string, modePrefixes?: {
11
13
  plan: string;
12
14
  free: string;
13
15
  });
@@ -17,20 +19,21 @@ export declare class SessionController {
17
19
  * Send a message to the persistent session. Creates one if none exists.
18
20
  * Returns the session result including usage data.
19
21
  */
20
- sendMessage(cwd: string, message: string, options?: {
22
+ sendMessage(message: string, options?: {
21
23
  model?: string;
22
24
  effort?: 'low' | 'medium' | 'high' | 'max';
23
25
  mode?: SessionMode;
26
+ sessionSystemPrompt?: string;
24
27
  abortSignal?: AbortSignal;
25
28
  }, onEvent?: ClaudeSessionEventHandler): Promise<ClaudeSessionRunResult>;
26
29
  /**
27
30
  * Send /compact to the current session to compress context.
28
31
  */
29
- compactSession(cwd: string, onEvent?: ClaudeSessionEventHandler): Promise<ClaudeSessionRunResult>;
32
+ compactSession(onEvent?: ClaudeSessionEventHandler): Promise<ClaudeSessionRunResult>;
30
33
  /**
31
34
  * Clear the current session. The next sendMessage will create a fresh one.
32
35
  */
33
- clearSession(cwd: string): Promise<string | null>;
36
+ clearSession(): Promise<string | null>;
34
37
  /**
35
38
  * Get current context tracking snapshot.
36
39
  */
@@ -38,7 +41,7 @@ export declare class SessionController {
38
41
  /**
39
42
  * Try to restore active session from persisted state on startup.
40
43
  */
41
- tryRestore(cwd: string): Promise<boolean>;
44
+ tryRestore(): Promise<boolean>;
42
45
  private persistActiveSession;
43
46
  private removeActiveSession;
44
47
  }
@@ -1,19 +1,25 @@
1
1
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
- import { dirname, join } from 'node:path';
3
- const ACTIVE_SESSION_FILE = '.claude-manager/active-session.json';
2
+ import { dirname } from 'node:path';
3
+ function activeSessionFile(_wrapperType) {
4
+ return `.claude-manager/active-session.json`;
5
+ }
4
6
  export class SessionController {
5
7
  sdkAdapter;
6
8
  contextTracker;
7
9
  sessionPrompt;
10
+ wrapperType;
11
+ worktree;
8
12
  modePrefixes;
9
13
  activeSessionId = null;
10
- constructor(sdkAdapter, contextTracker, sessionPrompt, modePrefixes = {
14
+ constructor(sdkAdapter, contextTracker, sessionPrompt, wrapperType, worktree, modePrefixes = {
11
15
  plan: '',
12
16
  free: '',
13
17
  }) {
14
18
  this.sdkAdapter = sdkAdapter;
15
19
  this.contextTracker = contextTracker;
16
20
  this.sessionPrompt = sessionPrompt;
21
+ this.wrapperType = wrapperType;
22
+ this.worktree = worktree;
17
23
  this.modePrefixes = modePrefixes;
18
24
  }
19
25
  get isActive() {
@@ -26,12 +32,12 @@ export class SessionController {
26
32
  * Send a message to the persistent session. Creates one if none exists.
27
33
  * Returns the session result including usage data.
28
34
  */
29
- async sendMessage(cwd, message, options, onEvent) {
35
+ async sendMessage(message, options, onEvent) {
30
36
  const mode = options?.mode ?? 'free';
31
37
  const prefix = this.modePrefixes[mode];
32
38
  const prompt = prefix ? `${prefix}\n\n${message}` : message;
33
39
  const input = {
34
- cwd,
40
+ cwd: this.worktree,
35
41
  prompt,
36
42
  persistSession: true,
37
43
  permissionMode: mode === 'plan' ? 'plan' : 'acceptEdits',
@@ -46,8 +52,8 @@ export class SessionController {
46
52
  input.resumeSessionId = this.activeSessionId;
47
53
  }
48
54
  else {
49
- // New session — use the base session prompt (project context is in the wrapper)
50
- input.systemPrompt = this.sessionPrompt;
55
+ // New session — prefer dynamically constructed prompt from wrapper, fall back to static default
56
+ input.systemPrompt = options?.sessionSystemPrompt ?? this.sessionPrompt;
51
57
  input.model ??= 'claude-opus-4-6';
52
58
  input.effort ??= 'high';
53
59
  }
@@ -66,28 +72,28 @@ export class SessionController {
66
72
  contextWindowSize: result.contextWindowSize,
67
73
  });
68
74
  // Persist active session state
69
- await this.persistActiveSession(cwd);
75
+ await this.persistActiveSession();
70
76
  return result;
71
77
  }
72
78
  /**
73
79
  * Send /compact to the current session to compress context.
74
80
  */
75
- async compactSession(cwd, onEvent) {
81
+ async compactSession(onEvent) {
76
82
  if (!this.activeSessionId) {
77
83
  throw new Error('No active session to compact');
78
84
  }
79
- const result = await this.sendMessage(cwd, '/compact', undefined, onEvent);
85
+ const result = await this.sendMessage('/compact', undefined, onEvent);
80
86
  this.contextTracker.recordCompaction();
81
87
  return result;
82
88
  }
83
89
  /**
84
90
  * Clear the current session. The next sendMessage will create a fresh one.
85
91
  */
86
- async clearSession(cwd) {
92
+ async clearSession() {
87
93
  const clearedId = this.activeSessionId;
88
94
  this.activeSessionId = null;
89
95
  this.contextTracker.reset();
90
- await this.removeActiveSession(cwd);
96
+ await this.removeActiveSession();
91
97
  return clearedId;
92
98
  }
93
99
  /**
@@ -99,12 +105,12 @@ export class SessionController {
99
105
  /**
100
106
  * Try to restore active session from persisted state on startup.
101
107
  */
102
- async tryRestore(cwd) {
103
- const filePath = join(cwd, ACTIVE_SESSION_FILE);
108
+ async tryRestore() {
109
+ const filePath = activeSessionFile(this.wrapperType);
104
110
  try {
105
111
  const raw = await readFile(filePath, 'utf-8');
106
112
  const state = JSON.parse(raw);
107
- if (state.sessionId && state.cwd === cwd) {
113
+ if (state.sessionId) {
108
114
  this.activeSessionId = state.sessionId;
109
115
  this.contextTracker.restore(state);
110
116
  return true;
@@ -115,14 +121,13 @@ export class SessionController {
115
121
  }
116
122
  return false;
117
123
  }
118
- async persistActiveSession(cwd) {
124
+ async persistActiveSession() {
119
125
  if (!this.activeSessionId) {
120
126
  return;
121
127
  }
122
128
  const snap = this.contextTracker.snapshot();
123
129
  const state = {
124
130
  sessionId: this.activeSessionId,
125
- cwd,
126
131
  startedAt: new Date().toISOString(),
127
132
  totalTurns: snap.totalTurns,
128
133
  totalCostUsd: snap.totalCostUsd,
@@ -130,12 +135,12 @@ export class SessionController {
130
135
  contextWindowSize: snap.contextWindowSize,
131
136
  latestInputTokens: snap.latestInputTokens,
132
137
  };
133
- const filePath = join(cwd, ACTIVE_SESSION_FILE);
138
+ const filePath = activeSessionFile(this.wrapperType);
134
139
  await mkdir(dirname(filePath), { recursive: true });
135
140
  await writeFile(filePath, JSON.stringify(state, null, 2));
136
141
  }
137
- async removeActiveSession(cwd) {
138
- const filePath = join(cwd, ACTIVE_SESSION_FILE);
142
+ async removeActiveSession() {
143
+ const filePath = activeSessionFile(this.wrapperType);
139
144
  try {
140
145
  const { unlink } = await import('node:fs/promises');
141
146
  await unlink(filePath);
@@ -0,0 +1,12 @@
1
+ import type { ClaudeMetadataSnapshot, ClaudeSettingSource } from '../types/contracts.js';
2
+ import type { ClaudeAgentSdkAdapter } from '../claude/claude-agent-sdk-adapter.js';
3
+ import type { RepoClaudeConfigReader } from './repo-claude-config-reader.js';
4
+ export declare class ClaudeMetadataService {
5
+ private readonly configReader;
6
+ private readonly sdkAdapter;
7
+ constructor(configReader: RepoClaudeConfigReader, sdkAdapter: ClaudeAgentSdkAdapter);
8
+ collect(cwd: string, options?: {
9
+ includeSdkProbe?: boolean;
10
+ settingSources?: ClaudeSettingSource[];
11
+ }): Promise<ClaudeMetadataSnapshot>;
12
+ }
@@ -0,0 +1,38 @@
1
+ export class ClaudeMetadataService {
2
+ configReader;
3
+ sdkAdapter;
4
+ constructor(configReader, sdkAdapter) {
5
+ this.configReader = configReader;
6
+ this.sdkAdapter = sdkAdapter;
7
+ }
8
+ async collect(cwd, options = {}) {
9
+ const baseSnapshot = await this.configReader.read(cwd);
10
+ if (!options.includeSdkProbe) {
11
+ return dedupeSnapshot(baseSnapshot);
12
+ }
13
+ const capabilities = await this.sdkAdapter.probeCapabilities(cwd, options.settingSources);
14
+ return dedupeSnapshot({
15
+ ...baseSnapshot,
16
+ commands: [...baseSnapshot.commands, ...capabilities.commands],
17
+ agents: capabilities.agents,
18
+ });
19
+ }
20
+ }
21
+ function dedupeSnapshot(snapshot) {
22
+ return {
23
+ ...snapshot,
24
+ commands: dedupeByName(snapshot.commands),
25
+ skills: dedupeByName(snapshot.skills),
26
+ hooks: dedupeByName(snapshot.hooks),
27
+ agents: dedupeByName(snapshot.agents),
28
+ };
29
+ }
30
+ function dedupeByName(items) {
31
+ const seen = new Map();
32
+ for (const item of items) {
33
+ if (!seen.has(item.name)) {
34
+ seen.set(item.name, item);
35
+ }
36
+ }
37
+ return [...seen.values()].sort((left, right) => left.name.localeCompare(right.name));
38
+ }
@@ -0,0 +1,7 @@
1
+ import type { ClaudeMetadataSnapshot } from '../types/contracts.js';
2
+ export declare class RepoClaudeConfigReader {
3
+ read(cwd: string): Promise<ClaudeMetadataSnapshot>;
4
+ private readSkills;
5
+ private readCommands;
6
+ private readSettings;
7
+ }
@@ -0,0 +1,154 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import JSON5 from 'json5';
4
+ export class RepoClaudeConfigReader {
5
+ async read(cwd) {
6
+ const claudeDirectory = path.join(cwd, '.claude');
7
+ const skillsDirectory = path.join(claudeDirectory, 'skills');
8
+ const commandsDirectory = path.join(claudeDirectory, 'commands');
9
+ const claudeMdCandidates = [
10
+ path.join(cwd, 'CLAUDE.md'),
11
+ path.join(claudeDirectory, 'CLAUDE.md'),
12
+ ];
13
+ const collectedAt = new Date().toISOString();
14
+ const [skills, commands, settingsResult, claudeMdPath] = await Promise.all([
15
+ this.readSkills(skillsDirectory),
16
+ this.readCommands(commandsDirectory),
17
+ this.readSettings(claudeDirectory),
18
+ findFirstExistingPath(claudeMdCandidates),
19
+ ]);
20
+ return {
21
+ collectedAt,
22
+ cwd,
23
+ commands: [...skillsToCommands(skills), ...commands],
24
+ skills,
25
+ hooks: settingsResult.hooks,
26
+ agents: [],
27
+ claudeMdPath: claudeMdPath ?? undefined,
28
+ settingsPaths: settingsResult.settingsPaths,
29
+ };
30
+ }
31
+ async readSkills(directory) {
32
+ if (!(await pathExists(directory))) {
33
+ return [];
34
+ }
35
+ const entries = await fs.readdir(directory, { withFileTypes: true });
36
+ const skills = await Promise.all(entries
37
+ .filter((entry) => entry.isDirectory())
38
+ .map(async (entry) => {
39
+ const skillPath = path.join(directory, entry.name, 'SKILL.md');
40
+ if (!(await pathExists(skillPath))) {
41
+ return null;
42
+ }
43
+ const content = await fs.readFile(skillPath, 'utf8');
44
+ return {
45
+ name: entry.name,
46
+ description: extractMarkdownDescription(content),
47
+ path: skillPath,
48
+ source: 'skill',
49
+ };
50
+ }));
51
+ return skills.filter((skill) => skill !== null);
52
+ }
53
+ async readCommands(directory) {
54
+ if (!(await pathExists(directory))) {
55
+ return [];
56
+ }
57
+ const commandFiles = await collectMarkdownFiles(directory);
58
+ const commands = await Promise.all(commandFiles.map(async (commandPath) => {
59
+ const content = await fs.readFile(commandPath, 'utf8');
60
+ return {
61
+ name: path.basename(commandPath, path.extname(commandPath)),
62
+ description: extractMarkdownDescription(content),
63
+ source: 'command',
64
+ path: commandPath,
65
+ };
66
+ }));
67
+ return commands.sort((left, right) => left.name.localeCompare(right.name));
68
+ }
69
+ async readSettings(claudeDirectory) {
70
+ const candidatePaths = [
71
+ path.join(claudeDirectory, 'settings.json'),
72
+ path.join(claudeDirectory, 'settings.local.json'),
73
+ ];
74
+ const settingsPaths = [];
75
+ const hooks = [];
76
+ for (const candidatePath of candidatePaths) {
77
+ if (!(await pathExists(candidatePath))) {
78
+ continue;
79
+ }
80
+ settingsPaths.push(candidatePath);
81
+ const content = await fs.readFile(candidatePath, 'utf8');
82
+ const parsed = JSON5.parse(content);
83
+ const hookEntries = Object.entries(parsed.hooks ?? {});
84
+ for (const [hookName, hookValue] of hookEntries) {
85
+ const hookMatchers = Array.isArray(hookValue) ? hookValue : [hookValue];
86
+ for (const hookMatcher of hookMatchers) {
87
+ if (!hookMatcher || typeof hookMatcher !== 'object') {
88
+ continue;
89
+ }
90
+ const matcher = typeof hookMatcher.matcher === 'string'
91
+ ? hookMatcher.matcher
92
+ : undefined;
93
+ const commandCount = Array.isArray(hookMatcher.hooks)
94
+ ? (hookMatcher.hooks?.length ?? 0)
95
+ : 0;
96
+ hooks.push({
97
+ name: hookName,
98
+ matcher,
99
+ sourcePath: candidatePath,
100
+ commandCount,
101
+ });
102
+ }
103
+ }
104
+ }
105
+ return {
106
+ settingsPaths,
107
+ hooks,
108
+ };
109
+ }
110
+ }
111
+ function extractMarkdownDescription(markdown) {
112
+ const lines = markdown
113
+ .split(/\r?\n/)
114
+ .map((line) => line.trim())
115
+ .filter(Boolean);
116
+ const descriptionLine = lines.find((line) => !line.startsWith('#') && !line.startsWith('---'));
117
+ return descriptionLine ?? 'No description provided.';
118
+ }
119
+ async function collectMarkdownFiles(directory) {
120
+ const entries = await fs.readdir(directory, { withFileTypes: true });
121
+ const files = await Promise.all(entries.map(async (entry) => {
122
+ const resolvedPath = path.join(directory, entry.name);
123
+ if (entry.isDirectory()) {
124
+ return collectMarkdownFiles(resolvedPath);
125
+ }
126
+ return entry.name.endsWith('.md') ? [resolvedPath] : [];
127
+ }));
128
+ return files.flat();
129
+ }
130
+ async function pathExists(candidatePath) {
131
+ try {
132
+ await fs.access(candidatePath);
133
+ return true;
134
+ }
135
+ catch {
136
+ return false;
137
+ }
138
+ }
139
+ async function findFirstExistingPath(candidatePaths) {
140
+ for (const candidatePath of candidatePaths) {
141
+ if (await pathExists(candidatePath)) {
142
+ return candidatePath;
143
+ }
144
+ }
145
+ return null;
146
+ }
147
+ function skillsToCommands(skills) {
148
+ return skills.map((skill) => ({
149
+ name: skill.name,
150
+ description: skill.description,
151
+ source: 'skill',
152
+ path: skill.path,
153
+ }));
154
+ }
@@ -1,17 +1,17 @@
1
1
  /**
2
2
  * Agent hierarchy configuration for the CTO + Engineer Wrapper architecture.
3
3
  *
4
- * CTO (cto) — pure orchestrator, spawns engineers, reviews diffs, commits
5
- * Engineer Plan (engineer_plan) — manages a Claude Code session for read-only investigation
6
- * Engineer Build (engineer_build) — manages a Claude Code session for implementation
7
- * Claude Code session — the underlying AI session (prompt only, no OpenCode agent)
4
+ * CTO (cto) — pure orchestrator, spawns engineers, reviews diffs, commits
5
+ * Engineer Explore (engineer_explore) — manages a Claude Code session for read-only investigation
6
+ * Engineer Implement (engineer_implement) — manages a Claude Code session for implementation
7
+ * Claude Code session — the underlying AI session (prompt only, no OpenCode agent)
8
8
  */
9
9
  import type { ManagerPromptRegistry } from '../types/contracts.js';
10
10
  export declare const AGENT_CTO = "cto";
11
- export declare const AGENT_ENGINEER_PLAN = "engineer_plan";
12
- export declare const AGENT_ENGINEER_BUILD = "engineer_build";
11
+ export declare const AGENT_ENGINEER_EXPLORE = "engineer_explore";
12
+ export declare const AGENT_ENGINEER_IMPLEMENT = "engineer_implement";
13
13
  /** All restricted tool IDs (union of all domain groups) */
14
- export declare const ALL_RESTRICTED_TOOL_IDS: readonly ["explore", "implement", "compact_context", "clear_session", "session_health", "list_transcripts", "list_history", "git_diff", "git_commit", "git_reset", "approval_policy", "approval_decisions", "approval_update"];
14
+ export declare const ALL_RESTRICTED_TOOL_IDS: readonly ["explore", "implement", "compact_context", "clear_session", "session_health", "list_transcripts", "list_history", "git_diff", "git_commit", "git_reset", "git_status", "git_log", "approval_policy", "approval_decisions", "approval_update"];
15
15
  type ToolPermission = 'allow' | 'ask' | 'deny';
16
16
  type AgentPermission = {
17
17
  '*'?: ToolPermission;
@@ -41,14 +41,14 @@ export declare function buildCtoAgentConfig(prompts: ManagerPromptRegistry): {
41
41
  permission: AgentPermission;
42
42
  prompt: string;
43
43
  };
44
- export declare function buildEngineerPlanAgentConfig(prompts: ManagerPromptRegistry): {
44
+ export declare function buildEngineerExploreAgentConfig(prompts: ManagerPromptRegistry): {
45
45
  description: string;
46
46
  mode: "subagent";
47
47
  color: string;
48
48
  permission: AgentPermission;
49
49
  prompt: string;
50
50
  };
51
- export declare function buildEngineerBuildAgentConfig(prompts: ManagerPromptRegistry): {
51
+ export declare function buildEngineerImplementAgentConfig(prompts: ManagerPromptRegistry): {
52
52
  description: string;
53
53
  mode: "subagent";
54
54
  color: string;