@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.
- package/LICENSE +21 -0
- package/README.md +91 -0
- package/dist/src/agent-client-project-repo.test.d.ts +2 -0
- package/dist/src/agent-client-project-repo.test.d.ts.map +1 -0
- package/dist/src/agent-client-project-repo.test.js +153 -0
- package/dist/src/agent-client.d.ts +261 -0
- package/dist/src/agent-client.d.ts.map +1 -0
- package/dist/src/agent-client.js +902 -0
- package/dist/src/agent-session.d.ts +303 -0
- package/dist/src/agent-session.d.ts.map +1 -0
- package/dist/src/agent-session.js +969 -0
- package/dist/src/checkbox-utils.d.ts +88 -0
- package/dist/src/checkbox-utils.d.ts.map +1 -0
- package/dist/src/checkbox-utils.js +120 -0
- package/dist/src/circuit-breaker.d.ts +76 -0
- package/dist/src/circuit-breaker.d.ts.map +1 -0
- package/dist/src/circuit-breaker.js +229 -0
- package/dist/src/circuit-breaker.test.d.ts +2 -0
- package/dist/src/circuit-breaker.test.d.ts.map +1 -0
- package/dist/src/circuit-breaker.test.js +292 -0
- package/dist/src/constants.d.ts +87 -0
- package/dist/src/constants.d.ts.map +1 -0
- package/dist/src/constants.js +101 -0
- package/dist/src/defaults/auto-trigger.d.ts +35 -0
- package/dist/src/defaults/auto-trigger.d.ts.map +1 -0
- package/dist/src/defaults/auto-trigger.js +36 -0
- package/dist/src/defaults/index.d.ts +12 -0
- package/dist/src/defaults/index.d.ts.map +1 -0
- package/dist/src/defaults/index.js +11 -0
- package/dist/src/defaults/priority.d.ts +20 -0
- package/dist/src/defaults/priority.d.ts.map +1 -0
- package/dist/src/defaults/priority.js +37 -0
- package/dist/src/defaults/prompts.d.ts +42 -0
- package/dist/src/defaults/prompts.d.ts.map +1 -0
- package/dist/src/defaults/prompts.js +310 -0
- package/dist/src/defaults/prompts.test.d.ts +2 -0
- package/dist/src/defaults/prompts.test.d.ts.map +1 -0
- package/dist/src/defaults/prompts.test.js +263 -0
- package/dist/src/defaults/work-type-detection.d.ts +19 -0
- package/dist/src/defaults/work-type-detection.d.ts.map +1 -0
- package/dist/src/defaults/work-type-detection.js +93 -0
- package/dist/src/errors.d.ts +91 -0
- package/dist/src/errors.d.ts.map +1 -0
- package/dist/src/errors.js +173 -0
- package/dist/src/frontend-adapter.d.ts +168 -0
- package/dist/src/frontend-adapter.d.ts.map +1 -0
- package/dist/src/frontend-adapter.js +314 -0
- package/dist/src/frontend-adapter.test.d.ts +2 -0
- package/dist/src/frontend-adapter.test.d.ts.map +1 -0
- package/dist/src/frontend-adapter.test.js +545 -0
- package/dist/src/index.d.ts +28 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +30 -0
- package/dist/src/issue-tracker-proxy.d.ts +140 -0
- package/dist/src/issue-tracker-proxy.d.ts.map +1 -0
- package/dist/src/issue-tracker-proxy.js +10 -0
- package/dist/src/platform-adapter.d.ts +132 -0
- package/dist/src/platform-adapter.d.ts.map +1 -0
- package/dist/src/platform-adapter.js +260 -0
- package/dist/src/platform-adapter.test.d.ts +2 -0
- package/dist/src/platform-adapter.test.d.ts.map +1 -0
- package/dist/src/platform-adapter.test.js +468 -0
- package/dist/src/proxy-client.d.ts +103 -0
- package/dist/src/proxy-client.d.ts.map +1 -0
- package/dist/src/proxy-client.js +191 -0
- package/dist/src/rate-limiter.d.ts +64 -0
- package/dist/src/rate-limiter.d.ts.map +1 -0
- package/dist/src/rate-limiter.js +163 -0
- package/dist/src/rate-limiter.test.d.ts +2 -0
- package/dist/src/rate-limiter.test.d.ts.map +1 -0
- package/dist/src/rate-limiter.test.js +217 -0
- package/dist/src/retry.d.ts +59 -0
- package/dist/src/retry.d.ts.map +1 -0
- package/dist/src/retry.js +82 -0
- package/dist/src/types.d.ts +492 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +143 -0
- package/dist/src/utils.d.ts +52 -0
- package/dist/src/utils.d.ts.map +1 -0
- package/dist/src/utils.js +277 -0
- package/dist/src/webhook-types.d.ts +308 -0
- package/dist/src/webhook-types.d.ts.map +1 -0
- package/dist/src/webhook-types.js +46 -0
- 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
|
+
}
|