@renseiai/agentfactory-linear 0.8.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 (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +91 -0
  3. package/dist/src/agent-client-project-repo.test.d.ts +2 -0
  4. package/dist/src/agent-client-project-repo.test.d.ts.map +1 -0
  5. package/dist/src/agent-client-project-repo.test.js +153 -0
  6. package/dist/src/agent-client.d.ts +261 -0
  7. package/dist/src/agent-client.d.ts.map +1 -0
  8. package/dist/src/agent-client.js +902 -0
  9. package/dist/src/agent-session.d.ts +303 -0
  10. package/dist/src/agent-session.d.ts.map +1 -0
  11. package/dist/src/agent-session.js +969 -0
  12. package/dist/src/checkbox-utils.d.ts +88 -0
  13. package/dist/src/checkbox-utils.d.ts.map +1 -0
  14. package/dist/src/checkbox-utils.js +120 -0
  15. package/dist/src/circuit-breaker.d.ts +76 -0
  16. package/dist/src/circuit-breaker.d.ts.map +1 -0
  17. package/dist/src/circuit-breaker.js +229 -0
  18. package/dist/src/circuit-breaker.test.d.ts +2 -0
  19. package/dist/src/circuit-breaker.test.d.ts.map +1 -0
  20. package/dist/src/circuit-breaker.test.js +292 -0
  21. package/dist/src/constants.d.ts +87 -0
  22. package/dist/src/constants.d.ts.map +1 -0
  23. package/dist/src/constants.js +101 -0
  24. package/dist/src/defaults/auto-trigger.d.ts +35 -0
  25. package/dist/src/defaults/auto-trigger.d.ts.map +1 -0
  26. package/dist/src/defaults/auto-trigger.js +36 -0
  27. package/dist/src/defaults/index.d.ts +12 -0
  28. package/dist/src/defaults/index.d.ts.map +1 -0
  29. package/dist/src/defaults/index.js +11 -0
  30. package/dist/src/defaults/priority.d.ts +20 -0
  31. package/dist/src/defaults/priority.d.ts.map +1 -0
  32. package/dist/src/defaults/priority.js +37 -0
  33. package/dist/src/defaults/prompts.d.ts +42 -0
  34. package/dist/src/defaults/prompts.d.ts.map +1 -0
  35. package/dist/src/defaults/prompts.js +310 -0
  36. package/dist/src/defaults/prompts.test.d.ts +2 -0
  37. package/dist/src/defaults/prompts.test.d.ts.map +1 -0
  38. package/dist/src/defaults/prompts.test.js +263 -0
  39. package/dist/src/defaults/work-type-detection.d.ts +19 -0
  40. package/dist/src/defaults/work-type-detection.d.ts.map +1 -0
  41. package/dist/src/defaults/work-type-detection.js +93 -0
  42. package/dist/src/errors.d.ts +91 -0
  43. package/dist/src/errors.d.ts.map +1 -0
  44. package/dist/src/errors.js +173 -0
  45. package/dist/src/frontend-adapter.d.ts +168 -0
  46. package/dist/src/frontend-adapter.d.ts.map +1 -0
  47. package/dist/src/frontend-adapter.js +314 -0
  48. package/dist/src/frontend-adapter.test.d.ts +2 -0
  49. package/dist/src/frontend-adapter.test.d.ts.map +1 -0
  50. package/dist/src/frontend-adapter.test.js +545 -0
  51. package/dist/src/index.d.ts +28 -0
  52. package/dist/src/index.d.ts.map +1 -0
  53. package/dist/src/index.js +30 -0
  54. package/dist/src/issue-tracker-proxy.d.ts +140 -0
  55. package/dist/src/issue-tracker-proxy.d.ts.map +1 -0
  56. package/dist/src/issue-tracker-proxy.js +10 -0
  57. package/dist/src/platform-adapter.d.ts +132 -0
  58. package/dist/src/platform-adapter.d.ts.map +1 -0
  59. package/dist/src/platform-adapter.js +260 -0
  60. package/dist/src/platform-adapter.test.d.ts +2 -0
  61. package/dist/src/platform-adapter.test.d.ts.map +1 -0
  62. package/dist/src/platform-adapter.test.js +468 -0
  63. package/dist/src/proxy-client.d.ts +103 -0
  64. package/dist/src/proxy-client.d.ts.map +1 -0
  65. package/dist/src/proxy-client.js +191 -0
  66. package/dist/src/rate-limiter.d.ts +64 -0
  67. package/dist/src/rate-limiter.d.ts.map +1 -0
  68. package/dist/src/rate-limiter.js +163 -0
  69. package/dist/src/rate-limiter.test.d.ts +2 -0
  70. package/dist/src/rate-limiter.test.d.ts.map +1 -0
  71. package/dist/src/rate-limiter.test.js +217 -0
  72. package/dist/src/retry.d.ts +59 -0
  73. package/dist/src/retry.d.ts.map +1 -0
  74. package/dist/src/retry.js +82 -0
  75. package/dist/src/types.d.ts +492 -0
  76. package/dist/src/types.d.ts.map +1 -0
  77. package/dist/src/types.js +143 -0
  78. package/dist/src/utils.d.ts +52 -0
  79. package/dist/src/utils.d.ts.map +1 -0
  80. package/dist/src/utils.js +277 -0
  81. package/dist/src/webhook-types.d.ts +308 -0
  82. package/dist/src/webhook-types.d.ts.map +1 -0
  83. package/dist/src/webhook-types.js +46 -0
  84. package/package.json +70 -0
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Mapping from Linear issue status to agent work type
3
+ */
4
+ export const STATUS_WORK_TYPE_MAP = {
5
+ 'Icebox': 'research',
6
+ 'Backlog': 'development',
7
+ 'Started': 'inflight',
8
+ 'Finished': 'qa',
9
+ 'Delivered': 'acceptance',
10
+ 'Rejected': 'refinement',
11
+ };
12
+ /**
13
+ * Terminal statuses where no agent work is needed
14
+ * Issues in these states are considered complete and should not be processed
15
+ */
16
+ export const TERMINAL_STATUSES = ['Accepted', 'Canceled', 'Duplicate'];
17
+ /**
18
+ * Status to transition to when agent session STARTS
19
+ * null means no transition on start
20
+ */
21
+ export const WORK_TYPE_START_STATUS = {
22
+ 'research': null, // No transition from Icebox on start
23
+ 'backlog-creation': null, // No transition from Icebox on start
24
+ 'development': 'Started', // Backlog -> Started when agent begins
25
+ 'inflight': null, // Already Started, no change
26
+ 'qa': null, // Already Finished
27
+ 'acceptance': null, // Already Delivered
28
+ 'refinement': null, // Already Rejected
29
+ 'refinement-coordination': null, // Already Rejected
30
+ 'coordination': 'Started', // Backlog -> Started when coordinator begins
31
+ 'qa-coordination': null, // Already Finished
32
+ 'acceptance-coordination': null, // Already Delivered
33
+ };
34
+ /**
35
+ * Status to transition to when agent session COMPLETES successfully
36
+ * null means no auto-transition on completion
37
+ */
38
+ export const WORK_TYPE_COMPLETE_STATUS = {
39
+ 'research': null, // No auto-transition, user moves to Backlog
40
+ 'backlog-creation': null, // Issues created in Backlog, source stays in Icebox
41
+ 'development': 'Finished', // Started -> Finished when work done
42
+ 'inflight': 'Finished', // Started -> Finished when work done
43
+ 'qa': 'Delivered', // Finished -> Delivered on QA pass
44
+ 'acceptance': 'Accepted', // Delivered -> Accepted on acceptance pass
45
+ 'refinement': 'Backlog', // Rejected -> Backlog after refinement
46
+ 'refinement-coordination': 'Backlog', // Rejected -> Backlog after coordinated refinement (triggers coordination which re-runs failing sub-issues)
47
+ 'coordination': 'Finished', // Started -> Finished when all sub-issues done
48
+ 'qa-coordination': 'Delivered', // Finished -> Delivered when QA coordination passes
49
+ 'acceptance-coordination': 'Accepted', // Delivered -> Accepted when acceptance coordination passes
50
+ };
51
+ /**
52
+ * Status to transition to when agent work FAILS (e.g., QA rejected)
53
+ * null means no auto-transition on failure (stays in current status)
54
+ */
55
+ export const WORK_TYPE_FAIL_STATUS = {
56
+ 'research': null,
57
+ 'backlog-creation': null,
58
+ 'development': null,
59
+ 'inflight': null,
60
+ 'qa': 'Backlog', // QA failure -> Backlog (developer/coordinator picks up with failure context)
61
+ 'acceptance': 'Rejected', // Acceptance failure -> Rejected (rejection handler diagnoses next steps)
62
+ 'refinement': null,
63
+ 'refinement-coordination': null,
64
+ 'coordination': null,
65
+ 'qa-coordination': 'Rejected', // QA coordination failure -> Rejected (refinement-coordination reads QA feedback and dispatches targeted fixes to failing sub-issues)
66
+ 'acceptance-coordination': 'Rejected', // Acceptance coordination failure -> Rejected
67
+ };
68
+ /**
69
+ * Work types that require an isolated git worktree.
70
+ * ALL work types now get worktrees to prevent agents from mutating the main
71
+ * checkout (e.g., running `git checkout` in the IDE's working tree).
72
+ */
73
+ export const WORK_TYPES_REQUIRING_WORKTREE = new Set([
74
+ 'development',
75
+ 'inflight',
76
+ 'qa',
77
+ 'acceptance',
78
+ 'coordination',
79
+ 'qa-coordination',
80
+ 'acceptance-coordination',
81
+ 'research',
82
+ 'backlog-creation',
83
+ 'refinement',
84
+ 'refinement-coordination',
85
+ ]);
86
+ /**
87
+ * Allowed statuses for each work type
88
+ * Used to validate that an agent isn't assigned to an issue in the wrong status
89
+ */
90
+ export const WORK_TYPE_ALLOWED_STATUSES = {
91
+ 'research': ['Icebox'],
92
+ 'backlog-creation': ['Icebox'],
93
+ 'development': ['Backlog'],
94
+ 'inflight': ['Started'],
95
+ 'qa': ['Finished'],
96
+ 'acceptance': ['Delivered'],
97
+ 'refinement': ['Rejected'],
98
+ 'refinement-coordination': ['Rejected'],
99
+ 'coordination': ['Backlog', 'Started'],
100
+ 'qa-coordination': ['Finished'],
101
+ 'acceptance-coordination': ['Delivered'],
102
+ };
103
+ /**
104
+ * Valid work types for each status (reverse of WORK_TYPE_ALLOWED_STATUSES)
105
+ * Used to constrain keyword detection to only valid options for the current status
106
+ *
107
+ * For example:
108
+ * - Icebox issues can use keywords to choose between 'research' and 'backlog-creation'
109
+ * - Backlog issues only have 'development' as valid, so keywords won't change work type
110
+ * but could still provide agent specialization hints
111
+ */
112
+ export const STATUS_VALID_WORK_TYPES = {
113
+ 'Icebox': ['research', 'backlog-creation'],
114
+ 'Backlog': ['development', 'coordination'],
115
+ 'Started': ['inflight'],
116
+ 'Finished': ['qa', 'qa-coordination'],
117
+ 'Delivered': ['acceptance', 'acceptance-coordination'],
118
+ 'Rejected': ['refinement', 'refinement-coordination'],
119
+ };
120
+ /**
121
+ * Get valid work types for a given status
122
+ * Returns empty array if status is unknown
123
+ */
124
+ export function getValidWorkTypesForStatus(status) {
125
+ return STATUS_VALID_WORK_TYPES[status] ?? [];
126
+ }
127
+ /**
128
+ * Validate that a work type is appropriate for an issue's current status
129
+ *
130
+ * @param workType - The work type being assigned
131
+ * @param issueStatus - The current status of the issue
132
+ * @returns Validation result with error message if invalid
133
+ */
134
+ export function validateWorkTypeForStatus(workType, issueStatus) {
135
+ const allowedStatuses = WORK_TYPE_ALLOWED_STATUSES[workType];
136
+ if (!allowedStatuses.includes(issueStatus)) {
137
+ return {
138
+ valid: false,
139
+ error: `Cannot assign ${workType} work to issue in ${issueStatus} status. Expected: ${allowedStatuses.join(', ')}`,
140
+ };
141
+ }
142
+ return { valid: true };
143
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Linear API Utilities
3
+ *
4
+ * Helper functions for working with Linear API
5
+ */
6
+ /**
7
+ * Truncate a string to a maximum length, adding truncation marker if needed
8
+ */
9
+ export declare function truncateText(text: string, maxLength?: number): string;
10
+ /**
11
+ * Build a completion comment with smart truncation.
12
+ * Prioritizes: summary > plan status > session ID
13
+ *
14
+ * If the full comment exceeds maxLength:
15
+ * 1. First, truncate plan items to show only states
16
+ * 2. If still too long, truncate the summary
17
+ */
18
+ export declare function buildCompletionComment(summary: string, planItems: Array<{
19
+ state: string;
20
+ title: string;
21
+ }>, sessionId: string | null, maxLength?: number): string;
22
+ /**
23
+ * Represents a chunk of content split for multiple comments
24
+ */
25
+ export interface CommentChunk {
26
+ body: string;
27
+ partNumber: number;
28
+ totalParts: number;
29
+ }
30
+ /**
31
+ * Split content into multiple comment chunks
32
+ *
33
+ * Splitting strategy:
34
+ * 1. Reserve space for part markers
35
+ * 2. Split at paragraph boundaries first
36
+ * 3. If paragraph too long, split at sentence boundaries
37
+ * 4. If sentence too long, split at word boundaries
38
+ * 5. Never split inside code blocks
39
+ */
40
+ export declare function splitContentIntoComments(content: string, maxLength?: number, maxComments?: number): CommentChunk[];
41
+ /**
42
+ * Build completion comments with smart splitting.
43
+ * Returns multiple comment chunks if content exceeds max length.
44
+ *
45
+ * For backward compatibility, maintains the same header/footer structure
46
+ * as buildCompletionComment, but splits long content across multiple comments.
47
+ */
48
+ export declare function buildCompletionComments(summary: string, planItems: Array<{
49
+ state: string;
50
+ title: string;
51
+ }>, sessionId: string | null, maxLength?: number): CommentChunk[];
52
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/utils.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAUH;;GAEG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,SAAS,GAAE,MAAkC,GAC5C,MAAM,CAOR;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,EAClD,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,SAAS,GAAE,MAAkC,GAC5C,MAAM,CA6DR;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;CACnB;AAkED;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,MAAM,EACf,SAAS,GAAE,MAAkC,EAC7C,WAAW,GAAE,MAAgC,GAC5C,YAAY,EAAE,CAqDhB;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,EAClD,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,SAAS,GAAE,MAAkC,GAC5C,YAAY,EAAE,CAiGhB"}
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Linear API Utilities
3
+ *
4
+ * Helper functions for working with Linear API
5
+ */
6
+ import { LINEAR_COMMENT_MAX_LENGTH, TRUNCATION_MARKER, MAX_COMPLETION_COMMENTS, COMMENT_OVERHEAD, CONTINUATION_MARKER, } from './constants.js';
7
+ /**
8
+ * Truncate a string to a maximum length, adding truncation marker if needed
9
+ */
10
+ export function truncateText(text, maxLength = LINEAR_COMMENT_MAX_LENGTH) {
11
+ if (text.length <= maxLength) {
12
+ return text;
13
+ }
14
+ const truncateAt = maxLength - TRUNCATION_MARKER.length;
15
+ return text.substring(0, truncateAt) + TRUNCATION_MARKER;
16
+ }
17
+ /**
18
+ * Build a completion comment with smart truncation.
19
+ * Prioritizes: summary > plan status > session ID
20
+ *
21
+ * If the full comment exceeds maxLength:
22
+ * 1. First, truncate plan items to show only states
23
+ * 2. If still too long, truncate the summary
24
+ */
25
+ export function buildCompletionComment(summary, planItems, sessionId, maxLength = LINEAR_COMMENT_MAX_LENGTH) {
26
+ const stateEmoji = {
27
+ pending: '\u{2B1C}',
28
+ inProgress: '\u{1F504}',
29
+ completed: '\u{2705}',
30
+ canceled: '\u{274C}',
31
+ };
32
+ // Build static parts
33
+ const header = '## Agent Work Complete\n\n';
34
+ const planHeader = '\n\n### Final Plan Status\n\n';
35
+ const footer = `\n\n---\n*Session ID: ${sessionId ?? 'unknown'}*`;
36
+ // Full plan status
37
+ const fullPlanStatus = planItems
38
+ .map((item) => `${stateEmoji[item.state] ?? '\u{2B1C}'} ${item.title}`)
39
+ .join('\n');
40
+ // Abbreviated plan status (just emoji counts)
41
+ const completedCount = planItems.filter((i) => i.state === 'completed').length;
42
+ const pendingCount = planItems.filter((i) => i.state === 'pending').length;
43
+ const canceledCount = planItems.filter((i) => i.state === 'canceled').length;
44
+ const abbreviatedPlanStatus = [
45
+ `\u{2705} ${completedCount} completed`,
46
+ pendingCount > 0 ? `\u{2B1C} ${pendingCount} pending` : null,
47
+ canceledCount > 0 ? `\u{274C} ${canceledCount} canceled` : null,
48
+ ]
49
+ .filter(Boolean)
50
+ .join(' | ');
51
+ // Try full comment first
52
+ const fullComment = header + summary + planHeader + fullPlanStatus + footer;
53
+ if (fullComment.length <= maxLength) {
54
+ return fullComment;
55
+ }
56
+ // Try with abbreviated plan
57
+ const abbreviatedComment = header + summary + planHeader + abbreviatedPlanStatus + footer;
58
+ if (abbreviatedComment.length <= maxLength) {
59
+ return abbreviatedComment;
60
+ }
61
+ // Need to truncate summary
62
+ const fixedLength = header.length +
63
+ planHeader.length +
64
+ abbreviatedPlanStatus.length +
65
+ footer.length +
66
+ TRUNCATION_MARKER.length;
67
+ const availableForSummary = maxLength - fixedLength;
68
+ if (availableForSummary > 100) {
69
+ // Only truncate if we have reasonable space
70
+ const truncatedSummary = summary.substring(0, availableForSummary) + TRUNCATION_MARKER;
71
+ return header + truncatedSummary + planHeader + abbreviatedPlanStatus + footer;
72
+ }
73
+ // Extreme case: even the fixed parts are too long, just truncate everything
74
+ return truncateText(fullComment, maxLength);
75
+ }
76
+ /**
77
+ * Check if a position is inside a code block
78
+ */
79
+ function isInsideCodeBlock(text, position) {
80
+ let insideCodeBlock = false;
81
+ let i = 0;
82
+ while (i < position && i < text.length) {
83
+ if (text.slice(i, i + 3) === '```') {
84
+ insideCodeBlock = !insideCodeBlock;
85
+ i += 3;
86
+ }
87
+ else {
88
+ i++;
89
+ }
90
+ }
91
+ return insideCodeBlock;
92
+ }
93
+ /**
94
+ * Find a safe split point in text that doesn't break code blocks
95
+ */
96
+ function findSafeSplitPoint(text, targetLength) {
97
+ if (text.length <= targetLength) {
98
+ return text.length;
99
+ }
100
+ // Try to split at paragraph boundary first
101
+ const paragraphBoundary = text.lastIndexOf('\n\n', targetLength);
102
+ if (paragraphBoundary > targetLength * 0.5 && !isInsideCodeBlock(text, paragraphBoundary)) {
103
+ return paragraphBoundary;
104
+ }
105
+ // Try to split at sentence boundary
106
+ const sentenceEnd = text.lastIndexOf('. ', targetLength);
107
+ if (sentenceEnd > targetLength * 0.5 && !isInsideCodeBlock(text, sentenceEnd)) {
108
+ return sentenceEnd + 1; // Include the period
109
+ }
110
+ // Try to split at newline
111
+ const newline = text.lastIndexOf('\n', targetLength);
112
+ if (newline > targetLength * 0.5 && !isInsideCodeBlock(text, newline)) {
113
+ return newline;
114
+ }
115
+ // Try to split at word boundary
116
+ const wordBoundary = text.lastIndexOf(' ', targetLength);
117
+ if (wordBoundary > targetLength * 0.3 && !isInsideCodeBlock(text, wordBoundary)) {
118
+ return wordBoundary;
119
+ }
120
+ // If we're inside a code block, find the end of it
121
+ if (isInsideCodeBlock(text, targetLength)) {
122
+ // Look for code block end after targetLength
123
+ const codeBlockEnd = text.indexOf('```', targetLength);
124
+ if (codeBlockEnd !== -1 && codeBlockEnd < targetLength * 1.5) {
125
+ // Include the closing fence and newline
126
+ const afterFence = text.indexOf('\n', codeBlockEnd + 3);
127
+ return afterFence !== -1 ? afterFence : codeBlockEnd + 3;
128
+ }
129
+ }
130
+ // Last resort: split at target length
131
+ return targetLength;
132
+ }
133
+ /**
134
+ * Split content into multiple comment chunks
135
+ *
136
+ * Splitting strategy:
137
+ * 1. Reserve space for part markers
138
+ * 2. Split at paragraph boundaries first
139
+ * 3. If paragraph too long, split at sentence boundaries
140
+ * 4. If sentence too long, split at word boundaries
141
+ * 5. Never split inside code blocks
142
+ */
143
+ export function splitContentIntoComments(content, maxLength = LINEAR_COMMENT_MAX_LENGTH, maxComments = MAX_COMPLETION_COMMENTS) {
144
+ // Account for overhead (part markers, continuation markers)
145
+ const effectiveMaxLength = maxLength - COMMENT_OVERHEAD;
146
+ if (content.length <= effectiveMaxLength) {
147
+ return [{ body: content, partNumber: 1, totalParts: 1 }];
148
+ }
149
+ const chunks = [];
150
+ let remaining = content;
151
+ while (remaining.length > 0 && chunks.length < maxComments) {
152
+ // Reserve space for continuation marker if not the last chunk
153
+ const reserveForContinuation = remaining.length > effectiveMaxLength
154
+ ? CONTINUATION_MARKER.length
155
+ : 0;
156
+ const chunkMaxLength = effectiveMaxLength - reserveForContinuation;
157
+ if (remaining.length <= chunkMaxLength) {
158
+ chunks.push(remaining);
159
+ remaining = '';
160
+ }
161
+ else {
162
+ const splitPoint = findSafeSplitPoint(remaining, chunkMaxLength);
163
+ const chunk = remaining.slice(0, splitPoint).trimEnd();
164
+ chunks.push(chunk);
165
+ remaining = remaining.slice(splitPoint).trimStart();
166
+ }
167
+ }
168
+ // If we hit max comments and still have content, append truncation to last chunk
169
+ if (remaining.length > 0 && chunks.length > 0) {
170
+ chunks[chunks.length - 1] += TRUNCATION_MARKER;
171
+ }
172
+ const totalParts = chunks.length;
173
+ return chunks.map((chunk, index) => {
174
+ const partNumber = index + 1;
175
+ const isLastPart = partNumber === totalParts;
176
+ // Add part marker for multi-part comments
177
+ let body = chunk;
178
+ if (totalParts > 1) {
179
+ const partMarker = `\n\n---\n*Part ${partNumber}/${totalParts}*`;
180
+ if (!isLastPart) {
181
+ body = chunk + CONTINUATION_MARKER + partMarker;
182
+ }
183
+ else {
184
+ body = chunk + partMarker;
185
+ }
186
+ }
187
+ return { body, partNumber, totalParts };
188
+ });
189
+ }
190
+ /**
191
+ * Build completion comments with smart splitting.
192
+ * Returns multiple comment chunks if content exceeds max length.
193
+ *
194
+ * For backward compatibility, maintains the same header/footer structure
195
+ * as buildCompletionComment, but splits long content across multiple comments.
196
+ */
197
+ export function buildCompletionComments(summary, planItems, sessionId, maxLength = LINEAR_COMMENT_MAX_LENGTH) {
198
+ const stateEmoji = {
199
+ pending: '\u{2B1C}',
200
+ inProgress: '\u{1F504}',
201
+ completed: '\u{2705}',
202
+ canceled: '\u{274C}',
203
+ };
204
+ // Build static parts
205
+ const header = '## Agent Work Complete\n\n';
206
+ const planHeader = '\n\n### Final Plan Status\n\n';
207
+ const footer = `\n\n---\n*Session ID: ${sessionId ?? 'unknown'}*`;
208
+ // Full plan status
209
+ const fullPlanStatus = planItems
210
+ .map((item) => `${stateEmoji[item.state] ?? '\u{2B1C}'} ${item.title}`)
211
+ .join('\n');
212
+ // Abbreviated plan status (just emoji counts)
213
+ const completedCount = planItems.filter((i) => i.state === 'completed').length;
214
+ const pendingCount = planItems.filter((i) => i.state === 'pending').length;
215
+ const canceledCount = planItems.filter((i) => i.state === 'canceled').length;
216
+ const abbreviatedPlanStatus = [
217
+ `\u{2705} ${completedCount} completed`,
218
+ pendingCount > 0 ? `\u{2B1C} ${pendingCount} pending` : null,
219
+ canceledCount > 0 ? `\u{274C} ${canceledCount} canceled` : null,
220
+ ]
221
+ .filter(Boolean)
222
+ .join(' | ');
223
+ // Try full comment first (single comment)
224
+ const fullComment = header + summary + planHeader + fullPlanStatus + footer;
225
+ if (fullComment.length <= maxLength) {
226
+ return [{ body: fullComment, partNumber: 1, totalParts: 1 }];
227
+ }
228
+ // Try with abbreviated plan (still single comment)
229
+ const abbreviatedComment = header + summary + planHeader + abbreviatedPlanStatus + footer;
230
+ if (abbreviatedComment.length <= maxLength) {
231
+ return [{ body: abbreviatedComment, partNumber: 1, totalParts: 1 }];
232
+ }
233
+ // Need to split into multiple comments
234
+ // First comment gets header + beginning of summary
235
+ // Middle comments get summary continuation
236
+ // Last comment gets end of summary + plan status + footer
237
+ const fixedSuffixLength = planHeader.length + abbreviatedPlanStatus.length + footer.length;
238
+ const headerLength = header.length;
239
+ // Split the summary into chunks
240
+ const summaryChunks = splitContentIntoComments(summary, maxLength - COMMENT_OVERHEAD - Math.max(headerLength, fixedSuffixLength), MAX_COMPLETION_COMMENTS);
241
+ // Build final comments
242
+ const result = [];
243
+ const totalParts = summaryChunks.length;
244
+ for (let i = 0; i < summaryChunks.length; i++) {
245
+ const isFirst = i === 0;
246
+ const isLast = i === summaryChunks.length - 1;
247
+ const partNumber = i + 1;
248
+ let body = '';
249
+ if (isFirst) {
250
+ body += header;
251
+ }
252
+ body += summaryChunks[i].body;
253
+ // Remove the part marker from the chunk (we'll add our own)
254
+ if (totalParts > 1) {
255
+ body = body.replace(/\n\n---\n\*Part \d+\/\d+\*$/, '');
256
+ body = body.replace(new RegExp(escapeRegExp(CONTINUATION_MARKER), 'g'), '');
257
+ }
258
+ if (isLast) {
259
+ body += planHeader + abbreviatedPlanStatus + footer;
260
+ }
261
+ // Add part marker for multi-part comments
262
+ if (totalParts > 1) {
263
+ if (!isLast) {
264
+ body += CONTINUATION_MARKER;
265
+ }
266
+ body += `\n\n---\n*Part ${partNumber}/${totalParts}*`;
267
+ }
268
+ result.push({ body, partNumber, totalParts });
269
+ }
270
+ return result;
271
+ }
272
+ /**
273
+ * Escape special regex characters in a string
274
+ */
275
+ function escapeRegExp(string) {
276
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
277
+ }