claude-teammate 0.1.312 → 0.1.314
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/package.json
CHANGED
package/src/claude/prompts.js
CHANGED
|
@@ -422,6 +422,8 @@ export function buildGitHubPRPlanBreakdownSystemPrompt() {
|
|
|
422
422
|
"Each step must be self-contained: a single developer can implement it in isolation by reading only that step description.",
|
|
423
423
|
"Steps must be ordered by dependency — steps that others depend on come first.",
|
|
424
424
|
"Each step description must be specific enough to act on without reading the full PR body: name the exact files, classes, functions, or fields to change and what the change is.",
|
|
425
|
+
"Each step must describe a product code, configuration, schema, or test-code change in the target repository.",
|
|
426
|
+
"Do not create process-only steps such as opening a terminal, changing directories, locating files, reading files, running grep/ls/find, or using an editor.",
|
|
425
427
|
"Aim for 3-8 steps. Do not produce trivial steps or steps that only say 'test' or 'verify'.",
|
|
426
428
|
LANGUAGE_PRESERVATION_RULES,
|
|
427
429
|
"Return only structured output matching the provided schema.",
|
|
@@ -444,9 +446,10 @@ ${JSON.stringify(input.memory?.epic || {}, null, 2)}
|
|
|
444
446
|
Instructions:
|
|
445
447
|
- Produce an ordered list of atomic implementation steps.
|
|
446
448
|
- Each step must be specific enough to implement without reading this full description again: name the exact files, classes, methods, or enum values involved.
|
|
449
|
+
- Each step must describe a code/config/schema/test-code change, not an execution tactic or repository exploration action.
|
|
447
450
|
- Steps must be ordered by dependency (earlier steps must not depend on later ones).
|
|
448
451
|
- Aim for 3-8 steps covering the full scope of the plan.
|
|
449
|
-
- Do not include steps for committing, pushing, or testing unless the test is part of the plan.`;
|
|
452
|
+
- Do not include steps for committing, pushing, opening a terminal, changing directories, locating files, reading files, running grep/ls/find, using an editor, or testing unless the test is part of the plan.`;
|
|
450
453
|
}
|
|
451
454
|
|
|
452
455
|
export function buildGitHubPRStepBreakdownSystemPrompt() {
|
|
@@ -455,6 +458,8 @@ export function buildGitHubPRStepBreakdownSystemPrompt() {
|
|
|
455
458
|
"Each sub-step must be self-contained and implementable by reading only that sub-step description.",
|
|
456
459
|
"Sub-steps must be ordered by dependency.",
|
|
457
460
|
"Each sub-step must be specific: name the exact files, classes, functions, or fields to change.",
|
|
461
|
+
"Each sub-step must describe a code/config/schema/test-code change, not an execution tactic or repository exploration action.",
|
|
462
|
+
"Do not create process-only sub-steps such as opening a terminal, changing directories, locating files, reading files, running grep/ls/find, or using an editor.",
|
|
458
463
|
"Do not produce trivial sub-steps.",
|
|
459
464
|
LANGUAGE_PRESERVATION_RULES,
|
|
460
465
|
"Return only structured output matching the provided schema."
|
|
@@ -473,8 +478,9 @@ ${input.step}
|
|
|
473
478
|
Instructions:
|
|
474
479
|
- Produce 2-4 ordered atomic sub-steps that together complete this step.
|
|
475
480
|
- Each sub-step must be specific: name the exact files, classes, methods, or values to change.
|
|
481
|
+
- Each sub-step must describe a code/config/schema/test-code change, not an execution tactic or repository exploration action.
|
|
476
482
|
- Sub-steps must be ordered by dependency.
|
|
477
|
-
- Do not include steps for committing, pushing, or testing.`;
|
|
483
|
+
- Do not include steps for committing, pushing, opening a terminal, changing directories, locating files, reading files, running grep/ls/find, using an editor, or testing.`;
|
|
478
484
|
}
|
|
479
485
|
|
|
480
486
|
export function buildGitHubPRImplementationStepSystemPrompt() {
|
package/src/repo.js
CHANGED
|
@@ -770,6 +770,31 @@ export async function branchHasChangesAgainstMain(repoPath, baseRef) {
|
|
|
770
770
|
return Number.isFinite(count) && count > 0;
|
|
771
771
|
}
|
|
772
772
|
|
|
773
|
+
/**
|
|
774
|
+
* True when HEAD's own work has already landed on the base branch (e.g. the
|
|
775
|
+
* branch was merged via another MR / a human merged it directly). In that case
|
|
776
|
+
* HEAD is an ancestor of the base tip while still being a distinct commit, so a
|
|
777
|
+
* branch-vs-base diff is empty and re-running Claude can never produce changes.
|
|
778
|
+
* Callers use this to terminate instead of looping on "no changes to commit".
|
|
779
|
+
* A brand-new branch that merely points at the base tip (HEAD === base) returns
|
|
780
|
+
* false — that is an unstarted PR, not a merged one. Best-effort; never throws.
|
|
781
|
+
*/
|
|
782
|
+
export async function branchMergedIntoBase(repoPath, baseRef) {
|
|
783
|
+
const branch = baseRef || (await resolveRemoteDefaultBranch(repoPath));
|
|
784
|
+
try {
|
|
785
|
+
const headSha = (await execGitOutput(repoPath, ["rev-parse", "HEAD"])).trim();
|
|
786
|
+
const baseSha = (await execGitOutput(repoPath, ["rev-parse", `origin/${branch}`])).trim();
|
|
787
|
+
if (!headSha || !baseSha || headSha === baseSha) {
|
|
788
|
+
return false;
|
|
789
|
+
}
|
|
790
|
+
// exit 0 → HEAD is an ancestor of the base tip → already merged.
|
|
791
|
+
await execGit(repoPath, ["merge-base", "--is-ancestor", "HEAD", `origin/${branch}`]);
|
|
792
|
+
return true;
|
|
793
|
+
} catch {
|
|
794
|
+
return false;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
773
798
|
async function resolveRemoteDefaultBranch(repoPath) {
|
|
774
799
|
try {
|
|
775
800
|
const output = await execGitOutput(repoPath, ["symbolic-ref", "refs/remotes/origin/HEAD"]);
|
|
@@ -2,11 +2,12 @@ import { mkdir, readFile } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { atomicWriteFile } from "../fs-atomic.js";
|
|
4
4
|
|
|
5
|
-
// Durable map of Claude session ids keyed by repo + PR branch, so a PR comment
|
|
5
|
+
// Durable map of Claude session ids keyed by repo + PR branch + PR scope, so a PR comment
|
|
6
6
|
// or reopen can resume the prior conversation instead of rebuilding context
|
|
7
7
|
// from memory (which previously dropped diffs / decisions and caused the model
|
|
8
|
-
// to "forget" earlier work).
|
|
9
|
-
//
|
|
8
|
+
// to "forget" earlier work). The PR scope prevents separate PRs that reuse a
|
|
9
|
+
// generic branch name from inheriting each other's Claude conversation.
|
|
10
|
+
// Stored under the same memory dir as issue memory so it survives worker restarts.
|
|
10
11
|
const SESSIONS_FILE = "pr-sessions.json";
|
|
11
12
|
const MAX_ENTRIES = 1000;
|
|
12
13
|
|
|
@@ -14,8 +15,13 @@ function sessionStorePath(projectRoot) {
|
|
|
14
15
|
return path.join(projectRoot, "memory", SESSIONS_FILE);
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
function sessionKey(repoUrl, branchName) {
|
|
18
|
-
|
|
18
|
+
function sessionKey(repoUrl, branchName, scope = "") {
|
|
19
|
+
const parts = [String(repoUrl || "").trim(), String(branchName || "").trim()];
|
|
20
|
+
const normalizedScope = String(scope || "").trim();
|
|
21
|
+
if (normalizedScope) {
|
|
22
|
+
parts.push(normalizedScope);
|
|
23
|
+
}
|
|
24
|
+
return parts.join("#");
|
|
19
25
|
}
|
|
20
26
|
|
|
21
27
|
async function readStore(projectRoot) {
|
|
@@ -29,12 +35,12 @@ async function readStore(projectRoot) {
|
|
|
29
35
|
}
|
|
30
36
|
}
|
|
31
37
|
|
|
32
|
-
export async function loadPrSessionId(projectRoot, repoUrl, branchName) {
|
|
38
|
+
export async function loadPrSessionId(projectRoot, repoUrl, branchName, scope = "") {
|
|
33
39
|
if (!projectRoot || !branchName) {
|
|
34
40
|
return "";
|
|
35
41
|
}
|
|
36
42
|
const store = await readStore(projectRoot);
|
|
37
|
-
const entry = store[sessionKey(repoUrl, branchName)];
|
|
43
|
+
const entry = store[sessionKey(repoUrl, branchName, scope)];
|
|
38
44
|
return entry && typeof entry.session_id === "string" ? entry.session_id : "";
|
|
39
45
|
}
|
|
40
46
|
|
|
@@ -42,14 +48,17 @@ export async function loadPrSessionId(projectRoot, repoUrl, branchName) {
|
|
|
42
48
|
// never lose each other's entries.
|
|
43
49
|
let writeChain = Promise.resolve();
|
|
44
50
|
|
|
45
|
-
export async function savePrSessionId(projectRoot, repoUrl, branchName,
|
|
51
|
+
export async function savePrSessionId(projectRoot, repoUrl, branchName, scopeOrSessionId, maybeSessionId) {
|
|
52
|
+
const hasScope = maybeSessionId !== undefined;
|
|
53
|
+
const scope = hasScope ? scopeOrSessionId : "";
|
|
54
|
+
const sessionId = hasScope ? maybeSessionId : scopeOrSessionId;
|
|
46
55
|
if (!projectRoot || !branchName || !sessionId) {
|
|
47
56
|
return;
|
|
48
57
|
}
|
|
49
58
|
writeChain = writeChain
|
|
50
59
|
.then(async () => {
|
|
51
60
|
const store = await readStore(projectRoot);
|
|
52
|
-
store[sessionKey(repoUrl, branchName)] = {
|
|
61
|
+
store[sessionKey(repoUrl, branchName, scope)] = {
|
|
53
62
|
session_id: String(sessionId).trim(),
|
|
54
63
|
updated_at: new Date().toISOString()
|
|
55
64
|
};
|
|
@@ -2,11 +2,11 @@ import {
|
|
|
2
2
|
isClaudeCliError,
|
|
3
3
|
runClaudeImplementation,
|
|
4
4
|
runClaudeImplementationPlanBreakdown,
|
|
5
|
-
runClaudeImplementationStep
|
|
6
|
-
runClaudeStepBreakdown
|
|
5
|
+
runClaudeImplementationStep
|
|
7
6
|
} from "../claude.js";
|
|
8
7
|
import {
|
|
9
8
|
branchHasChangesAgainstMain,
|
|
9
|
+
branchMergedIntoBase,
|
|
10
10
|
commitAndPushRepoChanges,
|
|
11
11
|
ensureWorktreeForBranch,
|
|
12
12
|
getBranchDiffAgainstBase,
|
|
@@ -30,13 +30,23 @@ import {
|
|
|
30
30
|
formatPullRequestOutcomeComment,
|
|
31
31
|
getNextPlanStep,
|
|
32
32
|
hasImplementationPlan,
|
|
33
|
-
replaceStepWithSubSteps,
|
|
34
33
|
setImplementationPlan,
|
|
35
34
|
setPullRequestStatus,
|
|
35
|
+
stripImplementationPlan,
|
|
36
36
|
tickPlanStep
|
|
37
37
|
} from "./pull-request.js";
|
|
38
38
|
import { extractJiraKey, parseOptionalInt, summarizeForLog, toErrorMessage } from "./utils.js";
|
|
39
39
|
|
|
40
|
+
const STEP_BRANCH_DIFF_MAX_CHARS = 3000;
|
|
41
|
+
|
|
42
|
+
function truncateForPrompt(value, maxChars, label) {
|
|
43
|
+
const text = String(value || "");
|
|
44
|
+
if (text.length <= maxChars) {
|
|
45
|
+
return text;
|
|
46
|
+
}
|
|
47
|
+
return `${text.slice(0, maxChars)}\n\n[${label}: ${text.length - maxChars} chars omitted]`;
|
|
48
|
+
}
|
|
49
|
+
|
|
40
50
|
async function commitPartialPullRequestChanges(repo, detail, logger) {
|
|
41
51
|
try {
|
|
42
52
|
const commitResult = await commitAndPushRepoChanges(
|
|
@@ -81,7 +91,6 @@ export function createPullRequestImplementationServices(overrides = {}) {
|
|
|
81
91
|
runClaudeImplementation,
|
|
82
92
|
runClaudeImplementationPlanBreakdown,
|
|
83
93
|
runClaudeImplementationStep,
|
|
84
|
-
runClaudeStepBreakdown,
|
|
85
94
|
commitAndPushRepoChanges,
|
|
86
95
|
prepareRepoForBranch,
|
|
87
96
|
persistEpicImplementationKnowledge,
|
|
@@ -186,9 +195,13 @@ export async function processPullRequestImplementation({
|
|
|
186
195
|
// it from memory (which dropped diffs/decisions and caused re-work). The
|
|
187
196
|
// branchDiff + latestComment below remain as the fallback when no session
|
|
188
197
|
// id is stored or the stored id is stale.
|
|
189
|
-
const
|
|
198
|
+
const prSessionScope =
|
|
199
|
+
[githubIssueMemory?.jira_key, detail.number, detail.url].filter(Boolean).map(String).join("#") ||
|
|
200
|
+
detail.headRef ||
|
|
201
|
+
issueKey;
|
|
202
|
+
const resumeSessionId = await loadPrSessionId(projectRoot, repo.url, detail.headRef, prSessionScope);
|
|
190
203
|
const onSessionId = (sessionId) => {
|
|
191
|
-
void savePrSessionId(projectRoot, repo.url, detail.headRef, sessionId);
|
|
204
|
+
void savePrSessionId(projectRoot, repo.url, detail.headRef, prSessionScope, sessionId);
|
|
192
205
|
};
|
|
193
206
|
|
|
194
207
|
// Re-ground the run on any work earlier runs already committed to this
|
|
@@ -198,12 +211,17 @@ export async function processPullRequestImplementation({
|
|
|
198
211
|
typeof services.getBranchDiffAgainstBase === "function"
|
|
199
212
|
? await services.getBranchDiffAgainstBase(repoPath, detail.baseRef)
|
|
200
213
|
: "";
|
|
214
|
+
const stepBranchDiff = truncateForPrompt(
|
|
215
|
+
branchDiff,
|
|
216
|
+
STEP_BRANCH_DIFF_MAX_CHARS,
|
|
217
|
+
"branch diff truncated for step prompt"
|
|
218
|
+
);
|
|
201
219
|
|
|
202
220
|
// Proactively break down large PRs before attempting full implementation
|
|
203
221
|
if (!inStepMode && nextBody.length > 5000) {
|
|
204
222
|
try {
|
|
205
223
|
const proactiveBreakdown = await services.runClaudeImplementationPlanBreakdown({
|
|
206
|
-
pullRequest: { ...detail, body: nextBody.slice(0, 8000) },
|
|
224
|
+
pullRequest: { ...detail, body: stripImplementationPlan(nextBody).slice(0, 8000) },
|
|
207
225
|
memory: { epic: epicSnapshot },
|
|
208
226
|
issueKey,
|
|
209
227
|
logger
|
|
@@ -269,7 +287,7 @@ export async function processPullRequestImplementation({
|
|
|
269
287
|
step: stepText,
|
|
270
288
|
memory: { epic: epicSnapshot },
|
|
271
289
|
epicContext: epicMemory,
|
|
272
|
-
branchDiff,
|
|
290
|
+
branchDiff: stepBranchDiff,
|
|
273
291
|
repoPath,
|
|
274
292
|
repoPaths: repoAccess.repoPaths,
|
|
275
293
|
branchName: detail.headRef,
|
|
@@ -314,6 +332,16 @@ export async function processPullRequestImplementation({
|
|
|
314
332
|
pr: detail.number
|
|
315
333
|
});
|
|
316
334
|
result = { result: "implemented", summary: "Implementation already committed on previous run." };
|
|
335
|
+
} else if (!latestComment && (await branchMergedIntoBase(repoPath, detail.baseRef).catch(() => false))) {
|
|
336
|
+
// The branch's work already landed on the base (merged via another MR or
|
|
337
|
+
// by a human). There is nothing left to implement, so skip Claude
|
|
338
|
+
// entirely — running it would only report "no changes" and loop. A human
|
|
339
|
+
// comment overrides this so feedback can still be addressed.
|
|
340
|
+
await logger.info("Head branch already merged into base; skipping Claude (nothing to implement)", {
|
|
341
|
+
repo: repo.url,
|
|
342
|
+
pr: detail.number
|
|
343
|
+
});
|
|
344
|
+
result = { result: "implemented", summary: "Changes already merged into the base branch." };
|
|
317
345
|
} else {
|
|
318
346
|
result = await services.runClaudeImplementation({
|
|
319
347
|
pullRequest: { ...detail, body: nextBody },
|
|
@@ -423,7 +451,53 @@ export async function processPullRequestImplementation({
|
|
|
423
451
|
);
|
|
424
452
|
|
|
425
453
|
if (!commitResult.committed && !commitResult.branchHasImplementation) {
|
|
426
|
-
|
|
454
|
+
// No diff to commit. If the branch's work already landed on the base
|
|
455
|
+
// (merged via another MR / by a human), re-running Claude can never
|
|
456
|
+
// produce changes — treat it as terminally implemented instead of
|
|
457
|
+
// looping. Otherwise stop without preserving STUCK_AT so the bot waits
|
|
458
|
+
// for human guidance rather than machine-gun retrying an unchangeable
|
|
459
|
+
// no-op every retry window.
|
|
460
|
+
const mergedIntoBase = await branchMergedIntoBase(repoPath, detail.baseRef).catch(() => false);
|
|
461
|
+
if (mergedIntoBase) {
|
|
462
|
+
const mergedBody = setPullRequestStatus(nextBody, "IMPLEMENTED");
|
|
463
|
+
await github.updatePullRequest(repo.url, detail.number, { title: detail.title, body: mergedBody });
|
|
464
|
+
await services.clearForgePullRequestBlocked(github, { ...detail, repoUrl: repo.url });
|
|
465
|
+
if (markReadyOnSuccess) {
|
|
466
|
+
await github.markPullRequestReady(repo.url, detail.number);
|
|
467
|
+
await services.transitionLinkedJiraIssueToReview(detail.title, jira, logger, detail.number);
|
|
468
|
+
}
|
|
469
|
+
const mergedComment =
|
|
470
|
+
successCommentBody ||
|
|
471
|
+
formatPullRequestOutcomeComment(
|
|
472
|
+
result.summary,
|
|
473
|
+
"The requested changes are already merged into the base branch; nothing left to implement."
|
|
474
|
+
);
|
|
475
|
+
await postPullRequestComment(github, repo.url, detail.number, mergedComment);
|
|
476
|
+
await postLinkedJiraComment(extractJiraKey(detail.title), mergedComment, jira, logger);
|
|
477
|
+
await logger.info("Branch already merged into base; PR marked IMPLEMENTED", {
|
|
478
|
+
repo: repo.url,
|
|
479
|
+
pr: detail.number
|
|
480
|
+
});
|
|
481
|
+
return buildDraftPrState(
|
|
482
|
+
{ ...detail, body: mergedBody, draft: markReadyOnSuccess ? false : detail.draft },
|
|
483
|
+
"IMPLEMENTED",
|
|
484
|
+
"implemented"
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
const noChangeBody = clearStuckAt(setPullRequestStatus(nextBody, "STUCK"));
|
|
488
|
+
await github.updatePullRequest(repo.url, detail.number, { title: detail.title, body: noChangeBody });
|
|
489
|
+
await services.setForgePullRequestBlocked(github, { ...detail, repoUrl: repo.url });
|
|
490
|
+
const noChangeComment = formatPullRequestOutcomeComment(
|
|
491
|
+
result.summary,
|
|
492
|
+
"Claude reported success but produced no repository changes, so there was nothing to commit. Halting automatic retries until there is new guidance."
|
|
493
|
+
);
|
|
494
|
+
await postPullRequestComment(github, repo.url, detail.number, noChangeComment);
|
|
495
|
+
await postLinkedJiraComment(extractJiraKey(detail.title), noChangeComment, jira, logger);
|
|
496
|
+
await logger.error("Pull request produced no repository changes; halting auto-retry", {
|
|
497
|
+
repo: repo.url,
|
|
498
|
+
pr: detail.number
|
|
499
|
+
});
|
|
500
|
+
return buildDraftPrState({ ...detail, body: noChangeBody }, "STUCK", "stuck");
|
|
427
501
|
}
|
|
428
502
|
|
|
429
503
|
const implementedBody = setPullRequestStatus(nextBody, "IMPLEMENTED");
|
|
@@ -504,7 +578,7 @@ export async function processPullRequestImplementation({
|
|
|
504
578
|
const issueKey = githubIssueMemory?.jira_key || `PR-${detail.number}`;
|
|
505
579
|
const epicSnapshot = buildEpicMemoryPromptSnapshot(epicMemory);
|
|
506
580
|
const breakdown = await services.runClaudeImplementationPlanBreakdown({
|
|
507
|
-
pullRequest: { ...detail, body: nextBody.slice(0, 8000) },
|
|
581
|
+
pullRequest: { ...detail, body: stripImplementationPlan(nextBody).slice(0, 8000) },
|
|
508
582
|
memory: { epic: epicSnapshot },
|
|
509
583
|
issueKey,
|
|
510
584
|
logger
|
|
@@ -527,38 +601,15 @@ export async function processPullRequestImplementation({
|
|
|
527
601
|
});
|
|
528
602
|
}
|
|
529
603
|
}
|
|
530
|
-
// Try to split the failing step into sub-steps before giving up permanently.
|
|
531
604
|
if (inStepMode && stepText) {
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
issueKey,
|
|
538
|
-
logger
|
|
539
|
-
});
|
|
540
|
-
if (subBreakdown?.steps?.length > 1) {
|
|
541
|
-
const splitBody = setPullRequestStatus(
|
|
542
|
-
replaceStepWithSubSteps(nextBody, stepText, subBreakdown.steps),
|
|
543
|
-
"IMPLEMENTING"
|
|
544
|
-
);
|
|
545
|
-
await github.updatePullRequest(repo.url, detail.number, { title: detail.title, body: splitBody });
|
|
546
|
-
await logger.info("Split too-large step into sub-steps", {
|
|
547
|
-
repo: repo.url,
|
|
548
|
-
pr: detail.number,
|
|
549
|
-
subSteps: subBreakdown.steps.length
|
|
550
|
-
});
|
|
551
|
-
return buildDraftPrState({ ...detail, body: splitBody }, "IMPLEMENTING", "implementing");
|
|
552
|
-
}
|
|
553
|
-
} catch (stepBreakdownError) {
|
|
554
|
-
await logger.error("Failed to split stuck step into sub-steps", {
|
|
555
|
-
repo: repo.url,
|
|
556
|
-
pr: detail.number,
|
|
557
|
-
error: stepBreakdownError
|
|
558
|
-
});
|
|
559
|
-
}
|
|
605
|
+
await logger.error("Pull request implementation step is too large; stopping without recursive breakdown", {
|
|
606
|
+
repo: repo.url,
|
|
607
|
+
pr: detail.number,
|
|
608
|
+
step: summarizeForLog(stepText)
|
|
609
|
+
});
|
|
560
610
|
}
|
|
561
|
-
//
|
|
611
|
+
// A planned step that still exceeds the prompt budget should stop here
|
|
612
|
+
// instead of recursively expanding the PR body with more generated steps.
|
|
562
613
|
// Otherwise (no plan yet, breakdown failed) → allow auto-retry by preserving STUCK_AT.
|
|
563
614
|
const promptTooLongComment = inStepMode
|
|
564
615
|
? "Even a single implementation step is too large for Claude to process. Please split this PR into smaller, more focused changes."
|
|
@@ -132,15 +132,30 @@ const PLAN_START = "<!-- tm8:steps -->";
|
|
|
132
132
|
const PLAN_END = "<!-- /tm8:steps -->";
|
|
133
133
|
const UNCHECKED_STEP_RE = /^- \[ \] (.+)$/mu;
|
|
134
134
|
const CHECKED_STEP_RE = /^- \[x\] /mu;
|
|
135
|
+
const MAX_IMPLEMENTATION_PLAN_STEPS = 20;
|
|
136
|
+
const MAX_STEP_BREAKDOWN_STEPS = 4;
|
|
137
|
+
const MAX_IMPLEMENTATION_STEP_CHARS = 1000;
|
|
135
138
|
|
|
136
139
|
export function hasImplementationPlan(body) {
|
|
137
140
|
const s = String(body || "");
|
|
138
141
|
return s.includes(PLAN_START) && s.includes(PLAN_END);
|
|
139
142
|
}
|
|
140
143
|
|
|
144
|
+
export function stripImplementationPlan(body) {
|
|
145
|
+
const normalized = String(body || "");
|
|
146
|
+
if (!hasImplementationPlan(normalized)) {
|
|
147
|
+
return normalized;
|
|
148
|
+
}
|
|
149
|
+
return normalized
|
|
150
|
+
.replace(new RegExp(`\\n*${escapeRegExpStr(PLAN_START)}[\\s\\S]*?${escapeRegExpStr(PLAN_END)}`, "u"), "")
|
|
151
|
+
.trimEnd();
|
|
152
|
+
}
|
|
153
|
+
|
|
141
154
|
export function setImplementationPlan(body, steps) {
|
|
142
155
|
const normalized = String(body || "");
|
|
143
|
-
const planLines = steps
|
|
156
|
+
const planLines = normalizeImplementationSteps(steps, MAX_IMPLEMENTATION_PLAN_STEPS)
|
|
157
|
+
.map((step) => `- [ ] ${step}`)
|
|
158
|
+
.join("\n");
|
|
144
159
|
const planBlock = `${PLAN_START}\n${planLines}\n${PLAN_END}`;
|
|
145
160
|
if (hasImplementationPlan(normalized)) {
|
|
146
161
|
return normalized.replace(
|
|
@@ -173,7 +188,9 @@ export function replaceStepWithSubSteps(body, stepText, subSteps) {
|
|
|
173
188
|
return body;
|
|
174
189
|
}
|
|
175
190
|
const escaped = stepText.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
176
|
-
const subLines = subSteps
|
|
191
|
+
const subLines = normalizeImplementationSteps(subSteps, MAX_STEP_BREAKDOWN_STEPS)
|
|
192
|
+
.map((s) => `- [ ] ${s}`)
|
|
193
|
+
.join("\n");
|
|
177
194
|
return String(body || "").replace(new RegExp(`^- \\[ \\] ${escaped}$`, "mu"), subLines);
|
|
178
195
|
}
|
|
179
196
|
|
|
@@ -192,6 +209,25 @@ function escapeRegExpStr(value) {
|
|
|
192
209
|
return String(value).replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
193
210
|
}
|
|
194
211
|
|
|
212
|
+
function normalizeImplementationSteps(steps, maxSteps) {
|
|
213
|
+
if (!Array.isArray(steps)) {
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
return steps
|
|
217
|
+
.map((step) =>
|
|
218
|
+
String(step || "")
|
|
219
|
+
.replace(/\s+/gu, " ")
|
|
220
|
+
.trim()
|
|
221
|
+
)
|
|
222
|
+
.filter(Boolean)
|
|
223
|
+
.slice(0, maxSteps)
|
|
224
|
+
.map((step) =>
|
|
225
|
+
step.length > MAX_IMPLEMENTATION_STEP_CHARS
|
|
226
|
+
? `${step.slice(0, MAX_IMPLEMENTATION_STEP_CHARS - 3).trimEnd()}...`
|
|
227
|
+
: step
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
195
231
|
export function formatPullRequestOutcomeComment(summary, fallback) {
|
|
196
232
|
const normalized = String(summary || "").trim();
|
|
197
233
|
return normalized || fallback;
|