@supaku/agentfactory 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/LICENSE +21 -0
  2. package/dist/src/deployment/deployment-checker.d.ts +110 -0
  3. package/dist/src/deployment/deployment-checker.d.ts.map +1 -0
  4. package/dist/src/deployment/deployment-checker.js +242 -0
  5. package/dist/src/deployment/index.d.ts +3 -0
  6. package/dist/src/deployment/index.d.ts.map +1 -0
  7. package/dist/src/deployment/index.js +2 -0
  8. package/dist/src/index.d.ts +5 -0
  9. package/dist/src/index.d.ts.map +1 -0
  10. package/dist/src/index.js +4 -0
  11. package/dist/src/logger.d.ts +117 -0
  12. package/dist/src/logger.d.ts.map +1 -0
  13. package/dist/src/logger.js +430 -0
  14. package/dist/src/orchestrator/activity-emitter.d.ts +128 -0
  15. package/dist/src/orchestrator/activity-emitter.d.ts.map +1 -0
  16. package/dist/src/orchestrator/activity-emitter.js +406 -0
  17. package/dist/src/orchestrator/api-activity-emitter.d.ts +167 -0
  18. package/dist/src/orchestrator/api-activity-emitter.d.ts.map +1 -0
  19. package/dist/src/orchestrator/api-activity-emitter.js +469 -0
  20. package/dist/src/orchestrator/heartbeat-writer.d.ts +57 -0
  21. package/dist/src/orchestrator/heartbeat-writer.d.ts.map +1 -0
  22. package/dist/src/orchestrator/heartbeat-writer.js +137 -0
  23. package/dist/src/orchestrator/index.d.ts +20 -0
  24. package/dist/src/orchestrator/index.d.ts.map +1 -0
  25. package/dist/src/orchestrator/index.js +22 -0
  26. package/dist/src/orchestrator/log-analyzer.d.ts +160 -0
  27. package/dist/src/orchestrator/log-analyzer.d.ts.map +1 -0
  28. package/dist/src/orchestrator/log-analyzer.js +572 -0
  29. package/dist/src/orchestrator/log-config.d.ts +39 -0
  30. package/dist/src/orchestrator/log-config.d.ts.map +1 -0
  31. package/dist/src/orchestrator/log-config.js +45 -0
  32. package/dist/src/orchestrator/orchestrator.d.ts +246 -0
  33. package/dist/src/orchestrator/orchestrator.d.ts.map +1 -0
  34. package/dist/src/orchestrator/orchestrator.js +2525 -0
  35. package/dist/src/orchestrator/parse-work-result.d.ts +16 -0
  36. package/dist/src/orchestrator/parse-work-result.d.ts.map +1 -0
  37. package/dist/src/orchestrator/parse-work-result.js +73 -0
  38. package/dist/src/orchestrator/progress-logger.d.ts +72 -0
  39. package/dist/src/orchestrator/progress-logger.d.ts.map +1 -0
  40. package/dist/src/orchestrator/progress-logger.js +135 -0
  41. package/dist/src/orchestrator/session-logger.d.ts +159 -0
  42. package/dist/src/orchestrator/session-logger.d.ts.map +1 -0
  43. package/dist/src/orchestrator/session-logger.js +275 -0
  44. package/dist/src/orchestrator/state-recovery.d.ts +96 -0
  45. package/dist/src/orchestrator/state-recovery.d.ts.map +1 -0
  46. package/dist/src/orchestrator/state-recovery.js +301 -0
  47. package/dist/src/orchestrator/state-types.d.ts +165 -0
  48. package/dist/src/orchestrator/state-types.d.ts.map +1 -0
  49. package/dist/src/orchestrator/state-types.js +7 -0
  50. package/dist/src/orchestrator/stream-parser.d.ts +145 -0
  51. package/dist/src/orchestrator/stream-parser.d.ts.map +1 -0
  52. package/dist/src/orchestrator/stream-parser.js +131 -0
  53. package/dist/src/orchestrator/types.d.ts +205 -0
  54. package/dist/src/orchestrator/types.d.ts.map +1 -0
  55. package/dist/src/orchestrator/types.js +4 -0
  56. package/dist/src/providers/amp-provider.d.ts +20 -0
  57. package/dist/src/providers/amp-provider.d.ts.map +1 -0
  58. package/dist/src/providers/amp-provider.js +24 -0
  59. package/dist/src/providers/claude-provider.d.ts +18 -0
  60. package/dist/src/providers/claude-provider.d.ts.map +1 -0
  61. package/dist/src/providers/claude-provider.js +267 -0
  62. package/dist/src/providers/codex-provider.d.ts +21 -0
  63. package/dist/src/providers/codex-provider.d.ts.map +1 -0
  64. package/dist/src/providers/codex-provider.js +25 -0
  65. package/dist/src/providers/index.d.ts +42 -0
  66. package/dist/src/providers/index.d.ts.map +1 -0
  67. package/dist/src/providers/index.js +77 -0
  68. package/dist/src/providers/types.d.ts +147 -0
  69. package/dist/src/providers/types.d.ts.map +1 -0
  70. package/dist/src/providers/types.js +13 -0
  71. package/package.json +63 -0
@@ -0,0 +1,275 @@
1
+ /**
2
+ * Session Logger
3
+ *
4
+ * Verbose logging of agent sessions for analysis and improvement.
5
+ * Uses JSON Lines format for append efficiency and easy parsing.
6
+ *
7
+ * Logs are stored at: .agent-logs/sessions/{session-id}/
8
+ * - metadata.json: Session metadata
9
+ * - events.jsonl: Event log (JSON Lines)
10
+ */
11
+ import { existsSync, mkdirSync, writeFileSync, appendFileSync, readFileSync, } from 'fs';
12
+ import { resolve } from 'path';
13
+ /**
14
+ * SessionLogger handles verbose logging of agent sessions
15
+ */
16
+ export class SessionLogger {
17
+ config;
18
+ sessionDir;
19
+ metadataPath;
20
+ eventsPath;
21
+ metadata;
22
+ stopped = false;
23
+ constructor(config) {
24
+ this.config = {
25
+ ...config,
26
+ logsDir: config.logsDir ?? '.agent-logs',
27
+ };
28
+ // Create session directory path
29
+ this.sessionDir = resolve(this.config.logsDir, 'sessions', this.config.sessionId);
30
+ this.metadataPath = resolve(this.sessionDir, 'metadata.json');
31
+ this.eventsPath = resolve(this.sessionDir, 'events.jsonl');
32
+ // Initialize metadata
33
+ this.metadata = {
34
+ sessionId: this.config.sessionId,
35
+ issueId: this.config.issueId,
36
+ issueIdentifier: this.config.issueIdentifier,
37
+ workType: this.config.workType,
38
+ prompt: this.config.prompt,
39
+ startedAt: Date.now(),
40
+ status: 'running',
41
+ toolCallsCount: 0,
42
+ errorsCount: 0,
43
+ workerId: this.config.workerId,
44
+ };
45
+ }
46
+ /**
47
+ * Initialize the logger - create directory and write initial metadata
48
+ */
49
+ initialize() {
50
+ try {
51
+ // Create directory
52
+ if (!existsSync(this.sessionDir)) {
53
+ mkdirSync(this.sessionDir, { recursive: true });
54
+ }
55
+ // Write initial metadata
56
+ this.writeMetadata();
57
+ // Log init event
58
+ this.logEvent({
59
+ timestamp: Date.now(),
60
+ type: 'init',
61
+ content: {
62
+ issueIdentifier: this.config.issueIdentifier,
63
+ workType: this.config.workType,
64
+ promptPreview: this.config.prompt.substring(0, 200),
65
+ },
66
+ });
67
+ }
68
+ catch (error) {
69
+ // Silently fail - logging is best-effort
70
+ console.warn('SessionLogger: Failed to initialize:', error);
71
+ }
72
+ }
73
+ /**
74
+ * Log a generic event
75
+ */
76
+ logEvent(event) {
77
+ if (this.stopped)
78
+ return;
79
+ try {
80
+ const line = JSON.stringify(event) + '\n';
81
+ appendFileSync(this.eventsPath, line);
82
+ }
83
+ catch {
84
+ // Silently fail
85
+ }
86
+ }
87
+ /**
88
+ * Log a tool use event
89
+ */
90
+ logToolUse(toolName, input) {
91
+ this.metadata.toolCallsCount++;
92
+ this.logEvent({
93
+ timestamp: Date.now(),
94
+ type: 'tool_use',
95
+ tool: toolName,
96
+ content: {
97
+ tool: toolName,
98
+ input: input ? JSON.stringify(input).substring(0, 2000) : undefined,
99
+ },
100
+ });
101
+ }
102
+ /**
103
+ * Log a tool result event
104
+ */
105
+ logToolResult(toolName, result, isError = false) {
106
+ if (isError) {
107
+ this.metadata.errorsCount++;
108
+ }
109
+ this.logEvent({
110
+ timestamp: Date.now(),
111
+ type: 'tool_result',
112
+ tool: toolName,
113
+ content: {
114
+ tool: toolName,
115
+ result: typeof result === 'string' ? result.substring(0, 2000) : JSON.stringify(result).substring(0, 2000),
116
+ },
117
+ isError,
118
+ });
119
+ }
120
+ /**
121
+ * Log an assistant message/thought
122
+ */
123
+ logAssistant(content) {
124
+ this.logEvent({
125
+ timestamp: Date.now(),
126
+ type: 'assistant',
127
+ content: content.substring(0, 2000),
128
+ });
129
+ }
130
+ /**
131
+ * Log an error
132
+ */
133
+ logError(message, error, metadata) {
134
+ this.metadata.errorsCount++;
135
+ this.logEvent({
136
+ timestamp: Date.now(),
137
+ type: 'error',
138
+ content: {
139
+ message,
140
+ error: error instanceof Error ? error.message : String(error),
141
+ stack: error instanceof Error ? error.stack?.substring(0, 500) : undefined,
142
+ },
143
+ isError: true,
144
+ metadata,
145
+ });
146
+ }
147
+ /**
148
+ * Log a warning (non-fatal issue)
149
+ */
150
+ logWarning(message, details) {
151
+ this.logEvent({
152
+ timestamp: Date.now(),
153
+ type: 'warning',
154
+ content: {
155
+ message,
156
+ ...details,
157
+ },
158
+ });
159
+ }
160
+ /**
161
+ * Log a status change
162
+ */
163
+ logStatus(status, details) {
164
+ this.logEvent({
165
+ timestamp: Date.now(),
166
+ type: 'status',
167
+ content: {
168
+ status,
169
+ ...details,
170
+ },
171
+ });
172
+ }
173
+ /**
174
+ * Finalize the session with a final status
175
+ */
176
+ finalize(status, options) {
177
+ if (this.stopped)
178
+ return;
179
+ this.stopped = true;
180
+ const now = Date.now();
181
+ // Update metadata
182
+ this.metadata.endedAt = now;
183
+ this.metadata.status = status;
184
+ if (options?.errorMessage) {
185
+ this.metadata.errorMessage = options.errorMessage;
186
+ }
187
+ if (options?.pullRequestUrl) {
188
+ this.metadata.pullRequestUrl = options.pullRequestUrl;
189
+ }
190
+ // Log completion event
191
+ this.logEvent({
192
+ timestamp: now,
193
+ type: status === 'completed' ? 'complete' : 'stop',
194
+ content: {
195
+ status,
196
+ duration: now - this.metadata.startedAt,
197
+ toolCallsCount: this.metadata.toolCallsCount,
198
+ errorsCount: this.metadata.errorsCount,
199
+ ...options,
200
+ },
201
+ });
202
+ // Write final metadata
203
+ this.writeMetadata();
204
+ }
205
+ /**
206
+ * Get the session directory path
207
+ */
208
+ getSessionDir() {
209
+ return this.sessionDir;
210
+ }
211
+ /**
212
+ * Get current metadata
213
+ */
214
+ getMetadata() {
215
+ return { ...this.metadata };
216
+ }
217
+ /**
218
+ * Write metadata to file
219
+ */
220
+ writeMetadata() {
221
+ try {
222
+ writeFileSync(this.metadataPath, JSON.stringify(this.metadata, null, 2));
223
+ }
224
+ catch {
225
+ // Silently fail
226
+ }
227
+ }
228
+ }
229
+ /**
230
+ * Create and initialize a session logger
231
+ */
232
+ export function createSessionLogger(config) {
233
+ const logger = new SessionLogger(config);
234
+ logger.initialize();
235
+ return logger;
236
+ }
237
+ /**
238
+ * Read session metadata from a session directory
239
+ */
240
+ export function readSessionMetadata(sessionDir) {
241
+ const metadataPath = resolve(sessionDir, 'metadata.json');
242
+ try {
243
+ if (!existsSync(metadataPath))
244
+ return null;
245
+ const content = readFileSync(metadataPath, 'utf-8');
246
+ return JSON.parse(content);
247
+ }
248
+ catch {
249
+ return null;
250
+ }
251
+ }
252
+ /**
253
+ * Read events from a session directory
254
+ * Returns an async generator for memory efficiency
255
+ */
256
+ export function* readSessionEvents(sessionDir) {
257
+ const eventsPath = resolve(sessionDir, 'events.jsonl');
258
+ try {
259
+ if (!existsSync(eventsPath))
260
+ return;
261
+ const content = readFileSync(eventsPath, 'utf-8');
262
+ const lines = content.split('\n').filter((line) => line.trim());
263
+ for (const line of lines) {
264
+ try {
265
+ yield JSON.parse(line);
266
+ }
267
+ catch {
268
+ // Skip invalid lines
269
+ }
270
+ }
271
+ }
272
+ catch {
273
+ // Return empty if file doesn't exist or can't be read
274
+ }
275
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * State Recovery
3
+ *
4
+ * Detects and recovers agent state from the .agent/ directory.
5
+ * Enables crash recovery and duplicate agent prevention.
6
+ */
7
+ import type { WorktreeState, HeartbeatState, TodosState, RecoveryCheckResult } from './state-types';
8
+ import type { AgentWorkType } from '@supaku/agentfactory-linear';
9
+ /**
10
+ * Get the .agent directory path for a worktree
11
+ */
12
+ export declare function getAgentDir(worktreePath: string): string;
13
+ /**
14
+ * Get the path to the state.json file
15
+ */
16
+ export declare function getStatePath(worktreePath: string): string;
17
+ /**
18
+ * Get the path to the heartbeat.json file
19
+ */
20
+ export declare function getHeartbeatPath(worktreePath: string): string;
21
+ /**
22
+ * Get the path to the todos.json file
23
+ */
24
+ export declare function getTodosPath(worktreePath: string): string;
25
+ /**
26
+ * Check if a heartbeat is fresh (agent is alive)
27
+ */
28
+ export declare function isHeartbeatFresh(heartbeat: HeartbeatState | null, timeoutMs?: number): boolean;
29
+ /**
30
+ * Read the current state from a worktree
31
+ */
32
+ export declare function readWorktreeState(worktreePath: string): WorktreeState | null;
33
+ /**
34
+ * Read the current heartbeat from a worktree
35
+ */
36
+ export declare function readHeartbeat(worktreePath: string): HeartbeatState | null;
37
+ /**
38
+ * Read the current todos from a worktree
39
+ */
40
+ export declare function readTodos(worktreePath: string): TodosState | null;
41
+ /**
42
+ * Check if recovery is possible for a worktree
43
+ */
44
+ export declare function checkRecovery(worktreePath: string, options?: {
45
+ heartbeatTimeoutMs?: number;
46
+ maxRecoveryAttempts?: number;
47
+ }): RecoveryCheckResult;
48
+ /**
49
+ * Initialize the .agent directory for a worktree
50
+ */
51
+ export declare function initializeAgentDir(worktreePath: string): void;
52
+ /**
53
+ * Write the state.json file
54
+ */
55
+ export declare function writeState(worktreePath: string, state: WorktreeState): void;
56
+ /**
57
+ * Update specific fields in the state
58
+ */
59
+ export declare function updateState(worktreePath: string, updates: Partial<WorktreeState>): WorktreeState | null;
60
+ /**
61
+ * Write the todos.json file
62
+ */
63
+ export declare function writeTodos(worktreePath: string, todos: TodosState): void;
64
+ /**
65
+ * Create initial state for a new agent
66
+ */
67
+ export declare function createInitialState(options: {
68
+ issueId: string;
69
+ issueIdentifier: string;
70
+ linearSessionId: string | null;
71
+ workType: AgentWorkType;
72
+ prompt: string;
73
+ workerId?: string | null;
74
+ pid?: number | null;
75
+ }): WorktreeState;
76
+ /**
77
+ * Generate the task list ID for a worktree (matches orchestrator format)
78
+ *
79
+ * @param issueIdentifier - Issue identifier (e.g., "SUP-123")
80
+ * @param workType - Work type suffix (e.g., "development" -> "DEV")
81
+ * @returns Task list ID (e.g., "SUP-123-DEV")
82
+ */
83
+ export declare function getTaskListId(issueIdentifier: string, workType: AgentWorkType): string;
84
+ /**
85
+ * Build a recovery prompt for resuming crashed work
86
+ */
87
+ export declare function buildRecoveryPrompt(state: WorktreeState, todos?: TodosState): string;
88
+ /**
89
+ * Parse environment variable for heartbeat timeout
90
+ */
91
+ export declare function getHeartbeatTimeoutFromEnv(): number;
92
+ /**
93
+ * Parse environment variable for max recovery attempts
94
+ */
95
+ export declare function getMaxRecoveryAttemptsFromEnv(): number;
96
+ //# sourceMappingURL=state-recovery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state-recovery.d.ts","sourceRoot":"","sources":["../../../src/orchestrator/state-recovery.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EACV,aAAa,EACb,cAAc,EACd,UAAU,EACV,mBAAmB,EAEpB,MAAM,eAAe,CAAA;AACtB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAA;AAQhE;;GAEG;AACH,wBAAgB,WAAW,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAExD;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAEzD;AAeD;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,cAAc,GAAG,IAAI,EAChC,SAAS,GAAE,MAAqC,GAC/C,OAAO,CAIT;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAE5E;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,YAAY,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAEzE;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,YAAY,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAEjE;AAED;;GAEG;AACH,wBAAgB,aAAa,CAC3B,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE;IACP,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,mBAAmB,CAAC,EAAE,MAAM,CAAA;CACxB,GACL,mBAAmB,CA2ErB;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAK7D;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,GAAG,IAAI,CAI3E;AAED;;GAEG;AACH,wBAAgB,WAAW,CACzB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,OAAO,CAAC,aAAa,CAAC,GAC9B,aAAa,GAAG,IAAI,CAWtB;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,IAAI,CAIxE;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE;IAC1C,OAAO,EAAE,MAAM,CAAA;IACf,eAAe,EAAE,MAAM,CAAA;IACvB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,QAAQ,EAAE,aAAa,CAAA;IACvB,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACpB,GAAG,aAAa,CAmBhB;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,eAAe,EAAE,MAAM,EACvB,QAAQ,EAAE,aAAa,GACtB,MAAM,CAcR;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,aAAa,EACpB,KAAK,CAAC,EAAE,UAAU,GACjB,MAAM,CA6CR;AAED;;GAEG;AACH,wBAAgB,0BAA0B,IAAI,MAAM,CASnD;AAED;;GAEG;AACH,wBAAgB,6BAA6B,IAAI,MAAM,CAStD"}
@@ -0,0 +1,301 @@
1
+ /**
2
+ * State Recovery
3
+ *
4
+ * Detects and recovers agent state from the .agent/ directory.
5
+ * Enables crash recovery and duplicate agent prevention.
6
+ */
7
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
8
+ import { resolve } from 'path';
9
+ // Default heartbeat timeout: 30 seconds
10
+ const DEFAULT_HEARTBEAT_TIMEOUT_MS = 30000;
11
+ // Default max recovery attempts
12
+ const DEFAULT_MAX_RECOVERY_ATTEMPTS = 3;
13
+ /**
14
+ * Get the .agent directory path for a worktree
15
+ */
16
+ export function getAgentDir(worktreePath) {
17
+ return resolve(worktreePath, '.agent');
18
+ }
19
+ /**
20
+ * Get the path to the state.json file
21
+ */
22
+ export function getStatePath(worktreePath) {
23
+ return resolve(getAgentDir(worktreePath), 'state.json');
24
+ }
25
+ /**
26
+ * Get the path to the heartbeat.json file
27
+ */
28
+ export function getHeartbeatPath(worktreePath) {
29
+ return resolve(getAgentDir(worktreePath), 'heartbeat.json');
30
+ }
31
+ /**
32
+ * Get the path to the todos.json file
33
+ */
34
+ export function getTodosPath(worktreePath) {
35
+ return resolve(getAgentDir(worktreePath), 'todos.json');
36
+ }
37
+ /**
38
+ * Read and parse a JSON file safely
39
+ */
40
+ function readJsonSafe(path) {
41
+ try {
42
+ if (!existsSync(path))
43
+ return null;
44
+ const content = readFileSync(path, 'utf-8');
45
+ return JSON.parse(content);
46
+ }
47
+ catch {
48
+ return null;
49
+ }
50
+ }
51
+ /**
52
+ * Check if a heartbeat is fresh (agent is alive)
53
+ */
54
+ export function isHeartbeatFresh(heartbeat, timeoutMs = DEFAULT_HEARTBEAT_TIMEOUT_MS) {
55
+ if (!heartbeat)
56
+ return false;
57
+ const age = Date.now() - heartbeat.timestamp;
58
+ return age < timeoutMs;
59
+ }
60
+ /**
61
+ * Read the current state from a worktree
62
+ */
63
+ export function readWorktreeState(worktreePath) {
64
+ return readJsonSafe(getStatePath(worktreePath));
65
+ }
66
+ /**
67
+ * Read the current heartbeat from a worktree
68
+ */
69
+ export function readHeartbeat(worktreePath) {
70
+ return readJsonSafe(getHeartbeatPath(worktreePath));
71
+ }
72
+ /**
73
+ * Read the current todos from a worktree
74
+ */
75
+ export function readTodos(worktreePath) {
76
+ return readJsonSafe(getTodosPath(worktreePath));
77
+ }
78
+ /**
79
+ * Check if recovery is possible for a worktree
80
+ */
81
+ export function checkRecovery(worktreePath, options = {}) {
82
+ const heartbeatTimeoutMs = options.heartbeatTimeoutMs ?? DEFAULT_HEARTBEAT_TIMEOUT_MS;
83
+ const maxRecoveryAttempts = options.maxRecoveryAttempts ?? DEFAULT_MAX_RECOVERY_ATTEMPTS;
84
+ const agentDir = getAgentDir(worktreePath);
85
+ // Check if .agent directory exists
86
+ if (!existsSync(agentDir)) {
87
+ return {
88
+ canRecover: false,
89
+ agentAlive: false,
90
+ reason: 'no_state',
91
+ message: 'No .agent directory found in worktree',
92
+ };
93
+ }
94
+ // Read state
95
+ const state = readWorktreeState(worktreePath);
96
+ if (!state) {
97
+ return {
98
+ canRecover: false,
99
+ agentAlive: false,
100
+ reason: 'no_state',
101
+ message: 'No state.json found in .agent directory',
102
+ };
103
+ }
104
+ // Validate state
105
+ if (!state.issueId || !state.issueIdentifier) {
106
+ return {
107
+ canRecover: false,
108
+ agentAlive: false,
109
+ state,
110
+ reason: 'invalid_state',
111
+ message: 'State is missing required fields (issueId, issueIdentifier)',
112
+ };
113
+ }
114
+ // Read heartbeat
115
+ const heartbeat = readHeartbeat(worktreePath);
116
+ // Check if agent is alive
117
+ if (isHeartbeatFresh(heartbeat, heartbeatTimeoutMs)) {
118
+ return {
119
+ canRecover: false,
120
+ agentAlive: true,
121
+ state,
122
+ heartbeat: heartbeat,
123
+ reason: 'agent_alive',
124
+ message: `Agent is still running (PID: ${heartbeat.pid}, last heartbeat: ${new Date(heartbeat.timestamp).toISOString()})`,
125
+ };
126
+ }
127
+ // Check recovery attempts
128
+ if (state.recoveryAttempts >= maxRecoveryAttempts) {
129
+ return {
130
+ canRecover: false,
131
+ agentAlive: false,
132
+ state,
133
+ heartbeat: heartbeat ?? undefined,
134
+ reason: 'max_attempts',
135
+ message: `Maximum recovery attempts reached (${state.recoveryAttempts}/${maxRecoveryAttempts})`,
136
+ };
137
+ }
138
+ // Recovery is possible
139
+ const todos = readTodos(worktreePath);
140
+ return {
141
+ canRecover: true,
142
+ agentAlive: false,
143
+ state,
144
+ heartbeat: heartbeat ?? undefined,
145
+ todos: todos ?? undefined,
146
+ message: `Recovery possible (attempt ${state.recoveryAttempts + 1}/${maxRecoveryAttempts})`,
147
+ };
148
+ }
149
+ /**
150
+ * Initialize the .agent directory for a worktree
151
+ */
152
+ export function initializeAgentDir(worktreePath) {
153
+ const agentDir = getAgentDir(worktreePath);
154
+ if (!existsSync(agentDir)) {
155
+ mkdirSync(agentDir, { recursive: true });
156
+ }
157
+ }
158
+ /**
159
+ * Write the state.json file
160
+ */
161
+ export function writeState(worktreePath, state) {
162
+ const statePath = getStatePath(worktreePath);
163
+ initializeAgentDir(worktreePath);
164
+ writeFileSync(statePath, JSON.stringify(state, null, 2));
165
+ }
166
+ /**
167
+ * Update specific fields in the state
168
+ */
169
+ export function updateState(worktreePath, updates) {
170
+ const current = readWorktreeState(worktreePath);
171
+ if (!current)
172
+ return null;
173
+ const updated = {
174
+ ...current,
175
+ ...updates,
176
+ lastUpdatedAt: Date.now(),
177
+ };
178
+ writeState(worktreePath, updated);
179
+ return updated;
180
+ }
181
+ /**
182
+ * Write the todos.json file
183
+ */
184
+ export function writeTodos(worktreePath, todos) {
185
+ const todosPath = getTodosPath(worktreePath);
186
+ initializeAgentDir(worktreePath);
187
+ writeFileSync(todosPath, JSON.stringify(todos, null, 2));
188
+ }
189
+ /**
190
+ * Create initial state for a new agent
191
+ */
192
+ export function createInitialState(options) {
193
+ const now = Date.now();
194
+ const taskListId = getTaskListId(options.issueIdentifier, options.workType);
195
+ return {
196
+ issueId: options.issueId,
197
+ issueIdentifier: options.issueIdentifier,
198
+ linearSessionId: options.linearSessionId,
199
+ claudeSessionId: null,
200
+ workType: options.workType,
201
+ prompt: options.prompt,
202
+ startedAt: now,
203
+ status: 'initializing',
204
+ currentPhase: null,
205
+ lastUpdatedAt: now,
206
+ recoveryAttempts: 0,
207
+ workerId: options.workerId ?? null,
208
+ pid: options.pid ?? null,
209
+ taskListId,
210
+ };
211
+ }
212
+ /**
213
+ * Generate the task list ID for a worktree (matches orchestrator format)
214
+ *
215
+ * @param issueIdentifier - Issue identifier (e.g., "SUP-123")
216
+ * @param workType - Work type suffix (e.g., "development" -> "DEV")
217
+ * @returns Task list ID (e.g., "SUP-123-DEV")
218
+ */
219
+ export function getTaskListId(issueIdentifier, workType) {
220
+ const suffixMap = {
221
+ research: 'RES',
222
+ 'backlog-creation': 'BC',
223
+ development: 'DEV',
224
+ inflight: 'INF',
225
+ coordination: 'COORD',
226
+ qa: 'QA',
227
+ acceptance: 'AC',
228
+ refinement: 'REF',
229
+ 'qa-coordination': 'QA-COORD',
230
+ 'acceptance-coordination': 'AC-COORD',
231
+ };
232
+ return `${issueIdentifier}-${suffixMap[workType]}`;
233
+ }
234
+ /**
235
+ * Build a recovery prompt for resuming crashed work
236
+ */
237
+ export function buildRecoveryPrompt(state, todos) {
238
+ const lines = [];
239
+ lines.push(`Resume work on ${state.issueIdentifier}.`);
240
+ lines.push('');
241
+ lines.push('RECOVERY CONTEXT:');
242
+ lines.push(`- Previous work type: ${state.workType}`);
243
+ lines.push(`- Last status: ${state.status}`);
244
+ if (state.currentPhase) {
245
+ lines.push(`- Last phase: ${state.currentPhase}`);
246
+ }
247
+ lines.push(`- Recovery attempt: ${state.recoveryAttempts + 1}`);
248
+ // Include task list ID for Claude Code Tasks integration
249
+ const taskListId = getTaskListId(state.issueIdentifier, state.workType);
250
+ lines.push(`- Task list ID: ${taskListId}`);
251
+ lines.push('');
252
+ // Note about Claude Code Tasks persistence
253
+ lines.push('TASK STATE:');
254
+ lines.push(`Your task list is preserved at: ~/.claude/tasks/${taskListId}/`);
255
+ lines.push('Use TaskList to see the current state of pending/completed tasks.');
256
+ lines.push('');
257
+ if (todos && todos.items.length > 0) {
258
+ lines.push('PREVIOUS TODO LIST (legacy):');
259
+ for (const item of todos.items) {
260
+ const statusIcon = item.status === 'completed' ? '\u2713' :
261
+ item.status === 'in_progress' ? '\u2192' : '\u25CB';
262
+ lines.push(` ${statusIcon} [${item.status}] ${item.content}`);
263
+ }
264
+ lines.push('');
265
+ }
266
+ lines.push('INSTRUCTIONS:');
267
+ lines.push('1. Run TaskList to see any pending tasks from the previous session');
268
+ lines.push('2. Check git status to see what has been done');
269
+ lines.push('3. Review the codebase for any partial changes');
270
+ lines.push('4. Continue from where the previous session left off');
271
+ lines.push('5. If work appears complete, verify and create PR if needed');
272
+ lines.push('');
273
+ lines.push(`Original prompt: ${state.prompt}`);
274
+ return lines.join('\n');
275
+ }
276
+ /**
277
+ * Parse environment variable for heartbeat timeout
278
+ */
279
+ export function getHeartbeatTimeoutFromEnv() {
280
+ const envValue = process.env.AGENT_HEARTBEAT_TIMEOUT_MS;
281
+ if (envValue) {
282
+ const parsed = parseInt(envValue, 10);
283
+ if (!isNaN(parsed) && parsed > 0) {
284
+ return parsed;
285
+ }
286
+ }
287
+ return DEFAULT_HEARTBEAT_TIMEOUT_MS;
288
+ }
289
+ /**
290
+ * Parse environment variable for max recovery attempts
291
+ */
292
+ export function getMaxRecoveryAttemptsFromEnv() {
293
+ const envValue = process.env.AGENT_MAX_RECOVERY_ATTEMPTS;
294
+ if (envValue) {
295
+ const parsed = parseInt(envValue, 10);
296
+ if (!isNaN(parsed) && parsed > 0) {
297
+ return parsed;
298
+ }
299
+ }
300
+ return DEFAULT_MAX_RECOVERY_ATTEMPTS;
301
+ }